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

View File

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

View File

@@ -44,9 +44,12 @@
class SerialCommand { class SerialCommand {
public: public:
typedef void (*LineHandler)(const char *);
SerialCommand(); // Constructor SerialCommand(); // Constructor
void addCommand(const char *command, void(*function)()); // Add a command to the processing dictionary. 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 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(); // Main entry point.
void readSerial(Stream &stream); // Reads commands from any Stream-compatible transport. void readSerial(Stream &stream); // Reads commands from any Stream-compatible transport.
@@ -65,6 +68,7 @@ class SerialCommand {
// Pointer to the default handler function // Pointer to the default handler function
void (*defaultHandler)(const char *); void (*defaultHandler)(const char *);
LineHandler lineHandler;
char delim[2]; // null-terminated list of character to be used as delimeters for tokenizing (default " ") 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') char term; // Character that signals end of command (default '\n')

View File

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

View File

@@ -6,7 +6,7 @@
namespace namespace
{ {
constexpr char kBleDeviceNamePrefix[] = "EggDuino_"; constexpr char kBleDeviceNamePrefix[] = "EggBot_";
constexpr char kBleServiceUuid[] = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; constexpr char kBleServiceUuid[] = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
constexpr char kBleRxCharUuid[] = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; constexpr char kBleRxCharUuid[] = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
constexpr char kBleTxCharUuid[] = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; constexpr char kBleTxCharUuid[] = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
@@ -30,18 +30,6 @@ void logBleDiag(const String &message)
Log(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 queueBleByte(uint8_t value)
{ {
bool queued = false; bool queued = false;
@@ -112,7 +100,7 @@ class EggDuinoBleRxCallbacks : public NimBLECharacteristicCallbacks
void startBleInterface() void startBleInterface()
{ {
char bleDeviceName[32] = {0}; char bleDeviceName[32] = {0};
buildBleDeviceName(bleDeviceName, sizeof(bleDeviceName)); buildDeviceName(bleDeviceName, sizeof(bleDeviceName));
if (bleDeviceName[0] == '\0') if (bleDeviceName[0] == '\0')
{ {
snprintf(bleDeviceName, sizeof(bleDeviceName), "%sUNKNOWN", kBleDeviceNamePrefix); snprintf(bleDeviceName, sizeof(bleDeviceName), "%sUNKNOWN", kBleDeviceNamePrefix);

View File

@@ -8,11 +8,15 @@ namespace
const char *kConfigPath = "/config.json"; const char *kConfigPath = "/config.json";
const size_t kConfigJsonCapacity = 4096; const size_t kConfigJsonCapacity = 4096;
const byte kDnsPort = 53; const byte kDnsPort = 53;
const unsigned long kWifiReconnectIntervalMs = 10000;
WebServer server(80); WebServer server(80);
DNSServer dnsServer; DNSServer dnsServer;
bool configStoreReady = false; bool configStoreReady = false;
bool apModeActive = false; bool apModeActive = false;
bool staReconnectEnabled = false;
bool staConnectionKnown = false;
unsigned long lastStaReconnectAttemptMs = 0;
void redirectToRoot() 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; } #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; } #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; } #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> </style>
</head> </head>
<body> <body>
@@ -91,6 +96,7 @@ button { margin-top: 18px; border: 0; background: #0b5ed7; color: white; padding
<button id="saveBtn" type="button">Speichern</button> <button id="saveBtn" type="button">Speichern</button>
<div id="status"></div> <div id="status"></div>
<div id="logTitle">Logs</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> <div id="log"></div>
</main> </main>
<script> <script>
@@ -261,6 +267,15 @@ async function pollLogs() {
server.send(200, "application/json", buildLogsJson(since)); 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() void handleNotFound()
{ {
if (apModeActive) if (apModeActive)
@@ -275,6 +290,44 @@ async function pollLogs() {
} }
handleRoot(); 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 } // namespace
ConfigParameter configParameters[] = { ConfigParameter configParameters[] = {
@@ -500,18 +553,23 @@ void startWebInterface()
initConfigStore(); initConfigStore();
bool staConnected = false; bool staConnected = false;
apModeActive = false; apModeActive = false;
staReconnectEnabled = !g_sWifiSsid.isEmpty();
staConnectionKnown = false;
lastStaReconnectAttemptMs = millis();
dnsServer.stop(); dnsServer.stop();
String hostName = g_sHostname; char deviceName[32] = {0};
hostName.trim(); buildDeviceName(deviceName, sizeof(deviceName));
String hostName = String(deviceName);
if (hostName.isEmpty()) if (hostName.isEmpty())
{ {
hostName = "EggDuino"; hostName = "EggBot_UNKNOWN";
} }
g_sHostname = hostName; g_sHostname = hostName;
if (!g_sWifiSsid.isEmpty()) if (!g_sWifiSsid.isEmpty())
{ {
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.setHostname(hostName.c_str()); WiFi.setHostname(hostName.c_str());
WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str()); WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str());
@@ -523,6 +581,7 @@ void startWebInterface()
delay(250); delay(250);
} }
staConnected = (WiFi.status() == WL_CONNECTED); staConnected = (WiFi.status() == WL_CONNECTED);
staConnectionKnown = staConnected;
if (staConnected) if (staConnected)
{ {
Serial.println(String("http://") + WiFi.localIP().toString()); 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_GET, handleGetConfig);
server.on("/api/config", HTTP_POST, handlePostConfig); server.on("/api/config", HTTP_POST, handlePostConfig);
server.on("/api/logs", HTTP_GET, handleGetLogs); server.on("/api/logs", HTTP_GET, handleGetLogs);
server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog);
server.onNotFound(handleNotFound); server.onNotFound(handleNotFound);
server.begin(); server.begin();
startWifiProtocolInterface(); startWifiProtocolInterface();
@@ -546,11 +606,11 @@ void startWebInterface()
#ifdef ESP32 #ifdef ESP32
WiFi.softAPsetHostname(hostName.c_str()); WiFi.softAPsetHostname(hostName.c_str());
#endif #endif
if (WiFi.softAP("EggDuino")) if (WiFi.softAP(hostName.c_str()))
{ {
apModeActive = true; apModeActive = true;
dnsServer.start(kDnsPort, "*", WiFi.softAPIP()); 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 else
{ {
@@ -562,6 +622,7 @@ void startWebInterface()
server.on("/api/config", HTTP_GET, handleGetConfig); server.on("/api/config", HTTP_GET, handleGetConfig);
server.on("/api/config", HTTP_POST, handlePostConfig); server.on("/api/config", HTTP_POST, handlePostConfig);
server.on("/api/logs", HTTP_GET, handleGetLogs); 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("/generate_204", HTTP_GET, redirectToRoot);
server.on("/gen_204", HTTP_GET, redirectToRoot); server.on("/gen_204", HTTP_GET, redirectToRoot);
server.on("/hotspot-detect.html", HTTP_GET, redirectToRoot); server.on("/hotspot-detect.html", HTTP_GET, redirectToRoot);
@@ -574,6 +635,8 @@ void startWebInterface()
void handleWebInterface() void handleWebInterface()
{ {
handleWifiReconnect();
if (apModeActive) if (apModeActive)
{ {
dnsServer.processNextRequest(); dnsServer.processNextRequest();

View File

@@ -1,5 +1,41 @@
#include "EggDuino.h" #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() void queryPen()
{ {
Log(__FUNCTION__); Log(__FUNCTION__);
@@ -96,6 +132,8 @@ void stepperMove()
sendAck(); sendAck();
return; return;
} }
startPrintSessionIfNeeded();
prepareMove(duration, penStepsEBB, rotStepsEBB); prepareMove(duration, penStepsEBB, rotStepsEBB);
moveToDestination(); moveToDestination();
@@ -199,10 +237,12 @@ void enableMotors()
switch (cmd) switch (cmd)
{ {
case 0: case 0:
stopPrintSession();
motorsOff(); motorsOff();
sendAck(); sendAck();
break; break;
case 1: case 1:
startPrintSessionIfNeeded();
motorsOn(); motorsOn();
sendAck(); sendAck();
break; break;
@@ -216,10 +256,12 @@ void enableMotors()
switch (value) switch (value)
{ {
case 0: case 0:
stopPrintSession();
motorsOff(); motorsOff();
sendAck(); sendAck();
break; break;
case 1: case 1:
startPrintSessionIfNeeded();
motorsOn(); motorsOn();
sendAck(); sendAck();
break; break;
@@ -318,8 +360,11 @@ void makeComInterface()
}; };
registerCommands(SCmd); registerCommands(SCmd);
SCmd.setLineHandler(captureSerialProtocolLine);
#ifdef ESP32 #ifdef ESP32
registerCommands(g_BLECmd); registerCommands(g_BLECmd);
g_BLECmd.setLineHandler(captureBleProtocolLine);
registerCommands(g_WifiCmd); registerCommands(g_WifiCmd);
g_WifiCmd.setLineHandler(captureWifiProtocolLine);
#endif #endif
} }

View File

@@ -2,6 +2,10 @@
namespace namespace
{ {
#ifdef ESP32
constexpr char kDeviceNamePrefix[] = "EggBot_";
#endif
int clampServoAngle(int angle) int clampServoAngle(int angle)
{ {
if (angle < 0) 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() void updateStepCorrectionFactors()
{ {
if (g_iRotMicrostep <= 0) if (g_iRotMicrostep <= 0)

View File

@@ -3,12 +3,19 @@
namespace { namespace {
constexpr size_t kLogCapacity = 80; constexpr size_t kLogCapacity = 80;
constexpr size_t kLogLineLength = 160; constexpr size_t kLogLineLength = 160;
constexpr size_t kIncomingLogCapacity = 128;
constexpr size_t kIncomingLogLineLength = SERIALCOMMAND_BUFFER + 24;
char g_logLines[kLogCapacity][kLogLineLength]; char g_logLines[kLogCapacity][kLogLineLength];
uint32_t g_logSeq[kLogCapacity]; uint32_t g_logSeq[kLogCapacity];
size_t g_logWritePos = 0; size_t g_logWritePos = 0;
uint32_t g_nextLogSeq = 1; 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) { void appendJsonEscaped(String &out, const char *text) {
out += "\""; out += "\"";
for (size_t i = 0; text[i] != '\0'; ++i) { for (size_t i = 0; text[i] != '\0'; ++i) {
@@ -40,6 +47,18 @@ void appendJsonEscaped(String &out, const char *text) {
} }
out += "\""; 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 } // namespace
void Log(const String &message) { void Log(const String &message) {
@@ -89,3 +108,45 @@ String buildLogsJson(uint32_t sinceSeq) {
output += "}"; output += "}";
return 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;
}