Add print log functionality and device name handling

- Introduced functions to capture and manage incoming print logs.
- Added a download link for the incoming print log in the web interface.
- Updated device name prefix for BLE and added device name building logic.
- Enhanced SerialCommand class with line handler support.
- Implemented WiFi reconnect logic and status tracking.
This commit is contained in:
2026-02-28 22:36:12 +01:00
parent 3580e32142
commit 83a55fcc47
9 changed files with 216 additions and 20 deletions

View File

@@ -146,6 +146,7 @@ void handleWebInterface();
void startBleInterface();
void handleBleInterface();
bool bleProtocolWrite(const char *message);
void buildDeviceName(char *nameBuffer, size_t bufferSize);
void startWifiProtocolInterface();
void handleWifiProtocolInterface();
bool wifiProtocolWrite(const char *message);
@@ -157,6 +158,11 @@ inline bool bleProtocolWrite(const char *message)
(void)message;
return false;
}
inline void buildDeviceName(char *nameBuffer, size_t bufferSize)
{
(void)nameBuffer;
(void)bufferSize;
}
inline void startWifiProtocolInterface() {}
inline void handleWifiProtocolInterface() {}
inline bool wifiProtocolWrite(const char *message)
@@ -168,5 +174,8 @@ inline bool wifiProtocolWrite(const char *message)
void Log(const String &message);
void Log(const char *message);
String buildLogsJson(uint32_t sinceSeq);
void captureIncomingPrintLine(const char *line, ProtocolTransport transport);
String buildIncomingPrintLogText();
void clearIncomingPrintLog();
#endif

View File

@@ -30,6 +30,7 @@ SerialCommand::SerialCommand()
: commandList(NULL),
commandCount(0),
defaultHandler(NULL),
lineHandler(NULL),
term('\r'), // default terminator for commands, newline character
last(NULL)
{
@@ -64,6 +65,10 @@ void SerialCommand::setDefaultHandler(void (*function)(const char *)) {
defaultHandler = function;
}
void SerialCommand::setLineHandler(LineHandler function) {
lineHandler = function;
}
/**
* This checks the Serial stream for characters, and assembles them into a buffer.
@@ -92,6 +97,10 @@ void SerialCommand::readChar(char inChar) {
Serial.println(buffer);
#endif
if ((lineHandler != NULL) && (bufPos > 0)) {
(*lineHandler)(buffer);
}
char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
if (command != NULL) {
boolean matched = false;

View File

@@ -44,9 +44,12 @@
class SerialCommand {
public:
typedef void (*LineHandler)(const char *);
SerialCommand(); // Constructor
void addCommand(const char *command, void(*function)()); // Add a command to the processing dictionary.
void setDefaultHandler(void (*function)(const char *)); // A handler to call when no valid command received.
void setLineHandler(LineHandler function); // Optional handler called with each completed input line.
void readSerial(); // Main entry point.
void readSerial(Stream &stream); // Reads commands from any Stream-compatible transport.
@@ -65,6 +68,7 @@ class SerialCommand {
// Pointer to the default handler function
void (*defaultHandler)(const char *);
LineHandler lineHandler;
char delim[2]; // null-terminated list of character to be used as delimeters for tokenizing (default " ")
char term; // Character that signals end of command (default '\n')

View File

@@ -29,7 +29,6 @@ board = esp32dev
framework = arduino
monitor_speed = 115200
monitor_port = /dev/cu.usb*
upload_speed = 115200
upload_port = /dev/cu.usb*
lib_deps =
arminjo/ServoEasing

View File

@@ -6,7 +6,7 @@
namespace
{
constexpr char kBleDeviceNamePrefix[] = "EggDuino_";
constexpr char kBleDeviceNamePrefix[] = "EggBot_";
constexpr char kBleServiceUuid[] = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
constexpr char kBleRxCharUuid[] = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
constexpr char kBleTxCharUuid[] = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
@@ -30,18 +30,6 @@ void logBleDiag(const String &message)
Log(message);
}
void buildBleDeviceName(char *nameBuffer, size_t bufferSize)
{
if ((nameBuffer == NULL) || (bufferSize == 0))
{
return;
}
// Tasmota-style chip ID uses the lower 24 bits of the ESP32 efuse MAC.
const uint32_t chipId = static_cast<uint32_t>(ESP.getEfuseMac() & 0xFFFFFFULL);
snprintf(nameBuffer, bufferSize, "%s%06X", kBleDeviceNamePrefix, chipId);
}
bool queueBleByte(uint8_t value)
{
bool queued = false;
@@ -112,7 +100,7 @@ class EggDuinoBleRxCallbacks : public NimBLECharacteristicCallbacks
void startBleInterface()
{
char bleDeviceName[32] = {0};
buildBleDeviceName(bleDeviceName, sizeof(bleDeviceName));
buildDeviceName(bleDeviceName, sizeof(bleDeviceName));
if (bleDeviceName[0] == '\0')
{
snprintf(bleDeviceName, sizeof(bleDeviceName), "%sUNKNOWN", kBleDeviceNamePrefix);

View File

@@ -8,11 +8,15 @@ namespace
const char *kConfigPath = "/config.json";
const size_t kConfigJsonCapacity = 4096;
const byte kDnsPort = 53;
const unsigned long kWifiReconnectIntervalMs = 10000;
WebServer server(80);
DNSServer dnsServer;
bool configStoreReady = false;
bool apModeActive = false;
bool staReconnectEnabled = false;
bool staConnectionKnown = false;
unsigned long lastStaReconnectAttemptMs = 0;
void redirectToRoot()
{
@@ -82,6 +86,7 @@ button { margin-top: 18px; border: 0; background: #0b5ed7; color: white; padding
#status { margin-top: 12px; min-height: 1.2em; }
#log { margin-top: 20px; border: 1px solid #d6dfef; border-radius: 8px; background: #0f172a; color: #d2e3ff; padding: 10px; height: 220px; overflow-y: auto; white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.9rem; }
#logTitle { margin-top: 24px; margin-bottom: 8px; font-weight: 700; }
#downloadPrintLog { display: inline-block; margin-top: 8px; color: #0b5ed7; text-decoration: none; font-weight: 600; }
</style>
</head>
<body>
@@ -91,6 +96,7 @@ button { margin-top: 18px; border: 0; background: #0b5ed7; color: white; padding
<button id="saveBtn" type="button">Speichern</button>
<div id="status"></div>
<div id="logTitle">Logs</div>
<a id="downloadPrintLog" href="/api/print-log.txt" download="incoming-print-log.txt">Incoming print log herunterladen</a>
<div id="log"></div>
</main>
<script>
@@ -261,6 +267,15 @@ async function pollLogs() {
server.send(200, "application/json", buildLogsJson(since));
}
void handleDownloadPrintLog()
{
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.sendHeader("Content-Disposition", "attachment; filename=\"incoming-print-log.txt\"");
server.send(200, "text/plain; charset=utf-8", buildIncomingPrintLogText());
}
void handleNotFound()
{
if (apModeActive)
@@ -275,6 +290,44 @@ async function pollLogs() {
}
handleRoot();
}
void handleWifiReconnect()
{
if (!staReconnectEnabled || apModeActive)
{
return;
}
const wl_status_t wifiStatus = WiFi.status();
if (wifiStatus == WL_CONNECTED)
{
if (!staConnectionKnown)
{
staConnectionKnown = true;
Log(String("WLAN verbunden: ") + WiFi.localIP().toString());
}
return;
}
if (staConnectionKnown)
{
staConnectionKnown = false;
Log("WLAN Verbindung verloren");
}
const unsigned long now = millis();
if (now - lastStaReconnectAttemptMs < kWifiReconnectIntervalMs)
{
return;
}
lastStaReconnectAttemptMs = now;
if (!WiFi.reconnect())
{
WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str());
}
Log("WLAN Reconnect versucht");
}
} // namespace
ConfigParameter configParameters[] = {
@@ -500,18 +553,23 @@ void startWebInterface()
initConfigStore();
bool staConnected = false;
apModeActive = false;
staReconnectEnabled = !g_sWifiSsid.isEmpty();
staConnectionKnown = false;
lastStaReconnectAttemptMs = millis();
dnsServer.stop();
String hostName = g_sHostname;
hostName.trim();
char deviceName[32] = {0};
buildDeviceName(deviceName, sizeof(deviceName));
String hostName = String(deviceName);
if (hostName.isEmpty())
{
hostName = "EggDuino";
hostName = "EggBot_UNKNOWN";
}
g_sHostname = hostName;
if (!g_sWifiSsid.isEmpty())
{
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.setHostname(hostName.c_str());
WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str());
@@ -523,6 +581,7 @@ void startWebInterface()
delay(250);
}
staConnected = (WiFi.status() == WL_CONNECTED);
staConnectionKnown = staConnected;
if (staConnected)
{
Serial.println(String("http://") + WiFi.localIP().toString());
@@ -536,6 +595,7 @@ void startWebInterface()
server.on("/api/config", HTTP_GET, handleGetConfig);
server.on("/api/config", HTTP_POST, handlePostConfig);
server.on("/api/logs", HTTP_GET, handleGetLogs);
server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog);
server.onNotFound(handleNotFound);
server.begin();
startWifiProtocolInterface();
@@ -546,11 +606,11 @@ void startWebInterface()
#ifdef ESP32
WiFi.softAPsetHostname(hostName.c_str());
#endif
if (WiFi.softAP("EggDuino"))
if (WiFi.softAP(hostName.c_str()))
{
apModeActive = true;
dnsServer.start(kDnsPort, "*", WiFi.softAPIP());
Serial.println(String("AP aktiv: EggDuino / http://") + WiFi.softAPIP().toString() + " (Name: " + hostName + ")");
Serial.println(String("AP aktiv: ") + hostName + " / http://" + WiFi.softAPIP().toString() + " (Name: " + hostName + ")");
}
else
{
@@ -562,6 +622,7 @@ void startWebInterface()
server.on("/api/config", HTTP_GET, handleGetConfig);
server.on("/api/config", HTTP_POST, handlePostConfig);
server.on("/api/logs", HTTP_GET, handleGetLogs);
server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog);
server.on("/generate_204", HTTP_GET, redirectToRoot);
server.on("/gen_204", HTTP_GET, redirectToRoot);
server.on("/hotspot-detect.html", HTTP_GET, redirectToRoot);
@@ -574,6 +635,8 @@ void startWebInterface()
void handleWebInterface()
{
handleWifiReconnect();
if (apModeActive)
{
dnsServer.processNextRequest();

View File

@@ -1,5 +1,41 @@
#include "EggDuino.h"
namespace
{
bool g_bPrintSessionActive = false;
void captureSerialProtocolLine(const char *line)
{
captureIncomingPrintLine(line, PROTOCOL_TRANSPORT_SERIAL);
}
#ifdef ESP32
void captureBleProtocolLine(const char *line)
{
captureIncomingPrintLine(line, PROTOCOL_TRANSPORT_BLE);
}
void captureWifiProtocolLine(const char *line)
{
captureIncomingPrintLine(line, PROTOCOL_TRANSPORT_WIFI);
}
#endif
void startPrintSessionIfNeeded()
{
if (!g_bPrintSessionActive)
{
clearIncomingPrintLog();
g_bPrintSessionActive = true;
}
}
void stopPrintSession()
{
g_bPrintSessionActive = false;
}
}
void queryPen()
{
Log(__FUNCTION__);
@@ -97,6 +133,8 @@ void stepperMove()
return;
}
startPrintSessionIfNeeded();
prepareMove(duration, penStepsEBB, rotStepsEBB);
moveToDestination();
sendAck();
@@ -199,10 +237,12 @@ void enableMotors()
switch (cmd)
{
case 0:
stopPrintSession();
motorsOff();
sendAck();
break;
case 1:
startPrintSessionIfNeeded();
motorsOn();
sendAck();
break;
@@ -216,10 +256,12 @@ void enableMotors()
switch (value)
{
case 0:
stopPrintSession();
motorsOff();
sendAck();
break;
case 1:
startPrintSessionIfNeeded();
motorsOn();
sendAck();
break;
@@ -318,8 +360,11 @@ void makeComInterface()
};
registerCommands(SCmd);
SCmd.setLineHandler(captureSerialProtocolLine);
#ifdef ESP32
registerCommands(g_BLECmd);
g_BLECmd.setLineHandler(captureBleProtocolLine);
registerCommands(g_WifiCmd);
g_WifiCmd.setLineHandler(captureWifiProtocolLine);
#endif
}

View File

@@ -2,6 +2,10 @@
namespace
{
#ifdef ESP32
constexpr char kDeviceNamePrefix[] = "EggBot_";
#endif
int clampServoAngle(int angle)
{
if (angle < 0)
@@ -36,6 +40,20 @@ uint_fast16_t servoSpeedFromRate(int rate)
}
}
#ifdef ESP32
void buildDeviceName(char *nameBuffer, size_t bufferSize)
{
if ((nameBuffer == NULL) || (bufferSize == 0))
{
return;
}
// Tasmota-style chip ID uses the lower 24 bits of the ESP32 efuse MAC.
const uint32_t chipId = static_cast<uint32_t>(ESP.getEfuseMac() & 0xFFFFFFULL);
snprintf(nameBuffer, bufferSize, "%s%06X", kDeviceNamePrefix, chipId);
}
#endif
void updateStepCorrectionFactors()
{
if (g_iRotMicrostep <= 0)

View File

@@ -3,12 +3,19 @@
namespace {
constexpr size_t kLogCapacity = 80;
constexpr size_t kLogLineLength = 160;
constexpr size_t kIncomingLogCapacity = 128;
constexpr size_t kIncomingLogLineLength = SERIALCOMMAND_BUFFER + 24;
char g_logLines[kLogCapacity][kLogLineLength];
uint32_t g_logSeq[kLogCapacity];
size_t g_logWritePos = 0;
uint32_t g_nextLogSeq = 1;
char g_incomingLogLines[kIncomingLogCapacity][kIncomingLogLineLength];
uint32_t g_incomingLogSeq[kIncomingLogCapacity];
size_t g_incomingLogWritePos = 0;
uint32_t g_nextIncomingLogSeq = 1;
void appendJsonEscaped(String &out, const char *text) {
out += "\"";
for (size_t i = 0; text[i] != '\0'; ++i) {
@@ -40,6 +47,18 @@ void appendJsonEscaped(String &out, const char *text) {
}
out += "\"";
}
const char *transportLabel(ProtocolTransport transport) {
switch (transport) {
case PROTOCOL_TRANSPORT_BLE:
return "BLE";
case PROTOCOL_TRANSPORT_WIFI:
return "WIFI";
case PROTOCOL_TRANSPORT_SERIAL:
default:
return "SERIAL";
}
}
} // namespace
void Log(const String &message) {
@@ -89,3 +108,45 @@ String buildLogsJson(uint32_t sinceSeq) {
output += "}";
return output;
}
void captureIncomingPrintLine(const char *line, ProtocolTransport transport) {
if ((line == NULL) || (line[0] == '\0')) {
return;
}
snprintf(
g_incomingLogLines[g_incomingLogWritePos],
kIncomingLogLineLength,
"[%010lu] %s %s",
static_cast<unsigned long>(millis()),
transportLabel(transport),
line
);
g_incomingLogSeq[g_incomingLogWritePos] = g_nextIncomingLogSeq++;
g_incomingLogWritePos = (g_incomingLogWritePos + 1) % kIncomingLogCapacity;
}
String buildIncomingPrintLogText() {
String output;
output.reserve(4096);
for (size_t i = 0; i < kIncomingLogCapacity; ++i) {
const size_t idx = (g_incomingLogWritePos + i) % kIncomingLogCapacity;
if (g_incomingLogSeq[idx] == 0) {
continue;
}
output += g_incomingLogLines[idx];
output += "\n";
}
return output;
}
void clearIncomingPrintLog() {
for (size_t i = 0; i < kIncomingLogCapacity; ++i) {
g_incomingLogSeq[i] = 0;
g_incomingLogLines[i][0] = '\0';
}
g_incomingLogWritePos = 0;
g_nextIncomingLogSeq = 1;
}