diff --git a/Feature-Requests.md b/Feature-Requests.md index 4027e3b..37cdc2e 100644 --- a/Feature-Requests.md +++ b/Feature-Requests.md @@ -19,22 +19,20 @@ Status: `[ ]` = offen · `[x]` = erledigt ## Offen -- [ ] **FR-002** Web Console – serieller Monitor über Browser - - Library: `ayushsharma82/WebSerial` (ESPAsyncWebServer-basiert, WebSocket, eigene `/webserial`-Seite) - - Log-Umfang: **alle** `Serial.print`-Ausgaben (inkl. Libraries) via `TeeStream`-Wrapper: - - Eigene Klasse `TeeStream : public Print` leitet an `HardwareSerial` + `WebSerial` gleichzeitig weiter - - In `main.cpp` wird `Serial` durch `TeeSerial` ersetzt → kein Refactoring der bestehenden Aufrufe nötig - - RAM-Ringbuffer: intern durch WebSerial verwaltet (WebSocket-basiert, kein NVS) - - Sicherheit: HTTP-Basic-Auth (gleiche Credentials wie restliches Webinterface) - - WebSerial kennt keine native Auth → eigene Auth-Route `/webserial-auth` als Wrapper, - oder WebSerial-Seite über eigene Handler-Registrierung mit `requireAuth()` absichern - - Integration in `WebServerManager::begin()` + `loop()` (`WebSerial.loop()`) - - Commit: – +*– keine –* --- ## Erledigt +- [x] **FR-002** Web Console – serieller Monitor über Browser (WebSocket) ✅ + - Route `/log` liefert HTML-Seite mit automatisch scrollendem Terminal (dunkles Theme) + - WebSocket-Endpunkt `/log-ws` via `AsyncWebSocket` im bestehenden ESPAsyncWebServer + - `LOG_I`/`LOG_E`/`LOG_D` in `config.h` formatieren in lokalen Puffer und rufen `webLogForward()` auf + - `webLogForward()` sendet per `ws.textAll()` – läuft nativ in AsyncTCP, kein Konflikt + - "Log Console"-Button auf Statusseite / automatischer WebSocket-Reconnect nach Trennung + - Commit: – + - [x] **FR-004** Bug: `session_sum` im MQTT-Heartbeat falsch (Binärzahl statt Dezimal) ✅ - `serialized(String(getAllSessionsSumMinutes(), 2))` → `String(int, basis)` interpretiert 2. Argument als **Basis**, nicht Dezimalstellen - Beispiel: 18 Min → `"10010"` (18 in Binär), 20 Min → `"10100"` diff --git a/README.md b/README.md index 4c85217..8c7ad47 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,12 @@ Dieses Projekt implementiert einen ESP32-basierten MQTT-Client mit Dot-Matrix-Di 8. [Konfiguration & Persistenz](#konfiguration--persistenz) 9. [WiFi-Setup (WiFiManager)](#wifi-setup-wifimanager) 10. [OTA Firmware-Update](#ota-firmware-update) -11. [Fehlerverhalten](#fehlerverhalten) -12. [Bibliotheken](#bibliotheken) -13. [Build & Flash](#build--flash) -14. [Tests](#tests) -15. [Beitragen / Commits](#beitragen--commits) +11. [Remote-Debugging (Web Console)](#remote-debugging-web-console) +12. [Fehlerverhalten](#fehlerverhalten) +13. [Bibliotheken](#bibliotheken) +14. [Build & Flash](#build--flash) +15. [Tests](#tests) +16. [Beitragen / Commits](#beitragen--commits) --- ## Skizze und Screenshots @@ -306,6 +307,16 @@ Firmware-Updates können kabellos über die Weboberfläche unter `/update` einge --- +## Remote-Debugging (Web Console) + +Geplant (FR-002): Neue Route `/log` im bestehenden Webinterface zeigt alle Log-Ausgaben live im Browser über WebSocket. + +- Kein extra Library nötig – nutzt den bestehenden `ESPAsyncWebServer` +- Aufruf: `http://172.30.30.90/log` +- Noch nicht implementiert. + +--- + ## Fehlerverhalten | Zustand | Modul 0 (oben links) | Modul 4 (unten links) | Verhalten | @@ -330,6 +341,7 @@ Firmware-Updates können kabellos über die Weboberfläche unter `/update` einge | `Preferences` | NVS-Zugriff (built-in ESP32 Arduino) | | `ElegantOTA` | OTA-Update über Webinterface | + --- ## Build & Flash @@ -401,7 +413,7 @@ MQTT-Display-LaserCutter/ │ ├── mqtt_client.cpp # MQTT-Wrapper (PubSubClient, TLS, Phase 6) │ └── web_server.cpp # Webinterface (ESPAsyncWebServer, Phase 7) ├── include/ -│ ├── config.h # Pin-Definitionen, Konstanten +│ ├── config.h # Pin-Definitionen, Konstanten, LOG-Makros │ ├── display_manager.h # Display-API (showLaserTime, showCountdown, ...) │ ├── laser_tracker.h # SessionState-Maschine, getAllSessionsSumMinutes(), ... │ ├── settings.h # Settings-Struct, SettingsManager diff --git a/include/config.h b/include/config.h index cd6fd32..608db4e 100644 --- a/include/config.h +++ b/include/config.h @@ -109,11 +109,44 @@ // ----------------------------------------------------------------------------- #define SERIAL_BAUD_RATE 115200 -// Log-Makros (nur wenn CORE_DEBUG_LEVEL >= 1 via build_flags) -#define LOG_I(tag, fmt, ...) Serial.printf("[I][%s] " fmt "\n", tag, ##__VA_ARGS__) -#define LOG_E(tag, fmt, ...) Serial.printf("[E][%s] " fmt "\n", tag, ##__VA_ARGS__) +// webLogForward(): Implementierung in web_server.cpp. +// Kein zirkulaerer Include – extern-Deklaration reicht. +extern void webLogForward(const char* msg); + +// Timestamp fuer Log-Makros: Lokalzeit (nach NTP) oder +Xs seit Boot. +static inline const char* _logTs() { + static char _ts[12]; + time_t now = time(nullptr); + if (now > 1600000000UL) { // valide wenn nach 2020 + struct tm* t = localtime(&now); + strftime(_ts, sizeof(_ts), "%H:%M:%S", t); + } else { + uint32_t s = millis() / 1000; + snprintf(_ts, sizeof(_ts), "+%lus", s); + } + return _ts; +} + +// Log-Makros: Nachricht formatieren, auf Serial ausgeben und in Web-Log-Puffer schreiben. +#define LOG_I(tag, fmt, ...) do { \ + char _lb[200]; \ + snprintf(_lb, sizeof(_lb), "[%s][I][%s] " fmt, _logTs(), tag, ##__VA_ARGS__); \ + Serial.println(_lb); \ + webLogForward(_lb); \ +} while(0) +#define LOG_E(tag, fmt, ...) do { \ + char _lb[200]; \ + snprintf(_lb, sizeof(_lb), "[%s][E][%s] " fmt, _logTs(), tag, ##__VA_ARGS__); \ + Serial.println(_lb); \ + webLogForward(_lb); \ +} while(0) #if CORE_DEBUG_LEVEL >= 3 - #define LOG_D(tag, fmt, ...) Serial.printf("[D][%s] " fmt "\n", tag, ##__VA_ARGS__) + #define LOG_D(tag, fmt, ...) do { \ + char _lb[200]; \ + snprintf(_lb, sizeof(_lb), "[%s][D][%s] " fmt, _logTs(), tag, ##__VA_ARGS__); \ + Serial.println(_lb); \ + webLogForward(_lb); \ + } while(0) #else #define LOG_D(tag, fmt, ...) do {} while(0) #endif diff --git a/include/web_server.h b/include/web_server.h index e319da8..6679047 100644 --- a/include/web_server.h +++ b/include/web_server.h @@ -13,6 +13,8 @@ // POST /reset – Gesamtzeit zurücksetzen (laserTracker.resetTotal()) // GET /update – OTA-Update-Seite (ElegantOTA) // POST /update – Firmware-Upload (ElegantOTA) +// GET /log – Web-Log-Console (WebSocket-Empfänger) +// WS /log-ws – WebSocket-Endpunkt für Live-Log-Streaming // // Verwendung: // webServer.begin(); // einmalig in setup(), nach WiFi-Connect @@ -25,6 +27,7 @@ // (verhindert HTTP_GET/POST-Enum-Konflikt mit WiFiManager in main.cpp) class AsyncWebServer; class AsyncWebServerRequest; +class AsyncWebSocket; class WebServerManager { public: @@ -38,6 +41,7 @@ public: private: AsyncWebServer* _server; // PIMPL: Pointer, full type only in web_server.cpp + AsyncWebSocket* _ws; // WebSocket-Endpunkt /log-ws void registerRoutes(); @@ -47,6 +51,7 @@ private: // HTML-Seiten als Strings aufbauen String buildStatusPage(); String buildConfigPage(); + String buildLogPage(); }; // Globale Instanz diff --git a/platformio.ini b/platformio.ini index 0995b05..32995ce 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,7 +23,7 @@ build_flags = -DCORE_DEBUG_LEVEL=1 ; 0=keine, 1=Fehler, 3=Info, 5=Verbose -DARDUINO_LOOP_STACK_SIZE=8192 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1 - -DFIRMWARE_VERSION='"1.0.2"' ; Semantic Versioning – hier erhöhen bei neuem Release + -DFIRMWARE_VERSION='"1.1.0"' ; Semantic Versioning – hier erhöhen bei neuem Release lib_deps = majicDesigns/MD_Parola @ ^3.7.3 majicDesigns/MD_MAX72XX @ ^3.5.1 @@ -40,6 +40,8 @@ lib_deps = ; ============================================================================= [env:az-delivery-devkit-v4] upload_port = COM3 +monitor_dtr = 0 +monitor_rts = 0 ; ============================================================================= ; OTA ENVIRONMENT – Firmware via WiFi flashen (ArduinoOTA / espota) @@ -54,7 +56,8 @@ upload_port = COM3 ; upload_flags = --auth=${sysenv.LASERCUTTER_OTA_PW} ; ============================================================================= [env:az-delivery-devkit-v4-ota] -upload_port = 172.30.30.90 +;upload_port = 172.30.30.90 +upload_port = 192.168.2.69 upload_protocol = espota ; upload_flags = --auth=DEIN_WEBPASSWORT ; <-- auskommentieren und anpassen wenn Passwort gesetzt @@ -64,7 +67,8 @@ upload_protocol = espota ; pio run -e az-delivery-devkit-v4-ota-http --target upload ; ============================================================================= [env:az-delivery-devkit-v4-ota-http] -upload_port = 172.30.30.90 +;upload_port = 172.30.30.90 +upload_port = 192.168.2.69 upload_protocol = custom extra_scripts = upload_ota.py @@ -88,6 +92,24 @@ lib_deps = majicDesigns/MD_Parola @ ^3.7.3 majicDesigns/MD_MAX72XX @ ^3.5.1 +; ============================================================================= +; TEST ENVIRONMENT – Telnet-Server Verifikation (ohne externe Libraries) +; Testet ob WiFiServer/WiFiClient auf dem ESP32 korrekt schreibt. +; Credentials: TEST_WIFI_SSID + TEST_WIFI_PASSWORD in mqtt_test_secrets.h +; Flash: pio run -e test-telnet --target upload +; Monitor: pio device monitor (USB Serial, 115200) +; Telnet: telnet 23 (oder PuTTY Telnet Port 23) +; ============================================================================= +[env:test-telnet] +platform = espressif32 +board = az-delivery-devkit-v4 +framework = arduino +upload_port = 172.30.30.90 +upload_protocol = espota +monitor_speed = 115200 +board_build.partitions = min_spiffs.csv +build_src_filter = -<*> +<../test_sketches/test_telnet.cpp> + ; ============================================================================= ; TEST ENVIRONMENT 1.5 – Push Button / Potentialfreier Schalter Verdrahtungstest ; Flash: pio run -e test-button --target upload @@ -134,7 +156,7 @@ monitor_echo = yes board_build.partitions = min_spiffs.csv build_src_filter = -<*> +<../test_sketches/test_wifi.cpp> +<../src/wifi_connector.cpp> build_flags = - -DCORE_DEBUG_LEVEL=1 + -DCORE_DEBUG_LEVEL=3 lib_deps = tzapu/WiFiManager @ ^2.0.17 majicDesigns/MD_MAX72XX @ ^3.5.1 diff --git a/src/laser_tracker.cpp b/src/laser_tracker.cpp index 1ba23c2..4b3f58e 100644 --- a/src/laser_tracker.cpp +++ b/src/laser_tracker.cpp @@ -3,6 +3,7 @@ // ============================================================================= #include "laser_tracker.h" +#include "config.h" // Globale Instanz LaserTracker laserTracker; @@ -33,11 +34,9 @@ void LaserTracker::begin() { _totalMinutesBase = settings.get().totalMinutes; resetSessionSum(); - Serial.printf("[LaserTracker] begin - Pin=%d totalMin=%.2f gratis=%ds pol=%s\n", - LASER_SIGNAL_PIN, - _totalMinutesBase, - gratisSeconds(), - (settings.get().signalPolarity == SIGNAL_POL_HIGH_ACTIVE) ? "HIGH" : "LOW"); + LOG_I("LASER", "begin - Pin=%d totalMin=%.2f gratis=%ds pol=%s", + LASER_SIGNAL_PIN, _totalMinutesBase, gratisSeconds(), + (settings.get().signalPolarity == SIGNAL_POL_HIGH_ACTIVE) ? "HIGH" : "LOW"); } // --------------------------------------------------------------------------- @@ -70,7 +69,7 @@ void LaserTracker::loop() { if (elapsed >= (uint32_t)gratisSeconds()) { _state = SessionState::NET_COUNTING; _netStartMs = millis(); - Serial.println("[LaserTracker] GRATIS abgelaufen -> NET_COUNTING"); + LOG_I("LASER", "GRATIS abgelaufen -> NET_COUNTING"); } } } @@ -82,7 +81,7 @@ void LaserTracker::onSessionStart() { _sessionStartMs = millis(); _sessionStartTime = time(nullptr); _netStartMs = 0; - Serial.println("[LaserTracker] SessionStart -> GRATIS"); + LOG_I("LASER", "SessionStart -> GRATIS"); } time_t LaserTracker::getSessionStartTime() const { @@ -105,9 +104,8 @@ void LaserTracker::onSessionEnd() { // NVS via SettingsManager speichern settings.saveTotalMinutes(_totalMinutesBase); - Serial.printf("[LaserTracker] SessionEnd: gesamt=%ds netto=%ds " - "sessionNetSec=%d total=%.2fmin\n", - totalSessionSec, netSec, _sessionNetSec, _totalMinutesBase); + LOG_I("LASER", "SessionEnd: gesamt=%ds netto=%ds sessionNetSec=%d total=%.2fmin", + totalSessionSec, netSec, _sessionNetSec, _totalMinutesBase); } else if (_state == SessionState::GRATIS) { // Laser ging aus bevor Gratiszeit ablief -> nur Gratiszeit in NVS, keine Session _lastSessionSec = 0; @@ -115,8 +113,8 @@ void LaserTracker::onSessionEnd() { settings.saveTotalMinutes(_totalMinutesBase); - Serial.printf("[LaserTracker] SessionEnd: nur Gratiszeit gesamt=%ds " - "total=%.2fmin\n", totalSessionSec, _totalMinutesBase); + LOG_I("LASER", "SessionEnd: nur Gratiszeit gesamt=%ds total=%.2fmin", + totalSessionSec, _totalMinutesBase); } _state = SessionState::INACTIVE; _sessionEndPending = true; // Fuer consumeSessionEnd() in main @@ -182,9 +180,9 @@ void LaserTracker::resetSessionSum() { _sessionStartMs = millis(); _sessionStartTime = time(nullptr); _netStartMs = 0; - Serial.println("[LaserTracker] SessionSum zurueckgesetzt, Laser aktiv -> GRATIS neu"); + LOG_I("LASER", "SessionSum zurueckgesetzt, Laser aktiv -> GRATIS neu"); } else { - Serial.println("[LaserTracker] SessionSum zurueckgesetzt (NVS unveraendert)"); + LOG_I("LASER", "SessionSum zurueckgesetzt (NVS unveraendert)"); } _sessionResetPending = true; // Signal fuer main.cpp -> publishSession() @@ -195,7 +193,7 @@ void LaserTracker::resetTotal() { _totalMinutesBase = 0.0f; settings.saveTotalMinutes(0.0f); resetSessionSum(); - Serial.println("[LaserTracker] Gesamtzeit und Session auf 0 gesetzt"); + LOG_I("LASER", "Gesamtzeit und Session auf 0 gesetzt"); } // --------------------------------------------------------------------------- @@ -204,10 +202,9 @@ void LaserTracker::printToSerial() const { if (_state == SessionState::GRATIS) stateStr = "GRATIS"; if (_state == SessionState::NET_COUNTING) stateStr = "NET_COUNTING"; - Serial.printf("[LaserTracker] state=%-12s active=%d sessionMin=%d " - "countdown=%ds totalMin=%.2f\n", - stateStr, _debouncedActive, getAllSessionsSumMinutes(), - getCountdownRemaining(), getTotalMinutes()); + LOG_I("LASER", "state=%-12s active=%d sessionMin=%d countdown=%ds totalMin=%.2f", + stateStr, _debouncedActive, getAllSessionsSumMinutes(), + getCountdownRemaining(), getTotalMinutes()); } // --------------------------------------------------------------------------- diff --git a/src/main.cpp b/src/main.cpp index 11ccae0..bc12451 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,6 +15,7 @@ #include #include +#include // esp_reset_reason() #include "config.h" #include "settings.h" #include "wifi_connector.h" @@ -30,8 +31,24 @@ static bool webStarted = false; void setup() { Serial.begin(SERIAL_BAUD_RATE); + // Reset-Grund auslesen – erscheint als erstes nach jedem Neustart im Log + const char* resetReason = "UNBEKANNT"; + switch (esp_reset_reason()) { + case ESP_RST_POWERON: resetReason = "POWER_ON"; break; + case ESP_RST_SW: resetReason = "SOFTWARE (esp_restart)"; break; + case ESP_RST_PANIC: resetReason = "PANIC/EXCEPTION"; break; + case ESP_RST_INT_WDT: resetReason = "WATCHDOG (intern)"; break; + case ESP_RST_TASK_WDT: resetReason = "WATCHDOG (Task)"; break; + case ESP_RST_WDT: resetReason = "WATCHDOG"; break; + case ESP_RST_DEEPSLEEP: resetReason = "DEEP_SLEEP"; break; + case ESP_RST_BROWNOUT: resetReason = "BROWNOUT (Spannung zu niedrig!)"; break; + case ESP_RST_SDIO: resetReason = "SDIO"; break; + default: break; + } + // --- 1. Einstellungen laden (NVS) --- settings.begin(); + LOG_I("MAIN", "=== Neustart === Grund: %s", resetReason); LOG_I("MAIN", "LaserCutter Display gestartet"); LOG_I("MAIN", "Laser GPIO: %d, Display CS: %d", LASER_SIGNAL_PIN, DISPLAY_CS_PIN); settings.printToSerial(); diff --git a/src/mqtt_client.cpp b/src/mqtt_client.cpp index 35e9b02..c89b8bc 100644 --- a/src/mqtt_client.cpp +++ b/src/mqtt_client.cpp @@ -6,6 +6,7 @@ #include "mqtt_client.h" #include "laser_tracker.h" #include "display_manager.h" +#include "config.h" #include // Globale Instanz @@ -42,7 +43,7 @@ void MqttClient::begin() { if (port == 8883) { _secureClient.setInsecure(); _client.setClient(_secureClient); - Serial.println("[MQTT] TLS aktiv (setInsecure - kein Cert-Verify)"); + LOG_I("MQTT", "TLS aktiv (setInsecure - kein Cert-Verify)"); } else { _client.setClient(_wifiClient); } @@ -53,8 +54,8 @@ void MqttClient::begin() { snprintf(_clientId, sizeof(_clientId), "%s-%02X%02X%02X", MQTT_CLIENT_ID, mac[3], mac[4], mac[5]); - Serial.printf("[MQTT] Broker: %s:%u\n", broker, port); - Serial.printf("[MQTT] Client-ID: %s\n", _clientId); + LOG_I("MQTT", "Broker: %s:%u", broker, port); + LOG_I("MQTT", "Client-ID: %s", _clientId); reconnect(); } @@ -93,7 +94,7 @@ void MqttClient::loop() { // -------------------------------------------------------------------------- void MqttClient::publishSession(int lastSessionSec, int gratisSec) { if (!_client.connected()) { - Serial.println("[MQTT] publishSession: nicht verbunden, uebersprungen"); + LOG_E("MQTT", "publishSession: nicht verbunden, uebersprungen"); return; } @@ -119,7 +120,7 @@ void MqttClient::publishSession(int lastSessionSec, int gratisSec) { serializeJson(doc, buf, sizeof(buf)); bool ok = _client.publish(MQTT_TOPIC_SESSION, buf, /*retained=*/false); - Serial.printf("[MQTT] publishSession: %s -> %s\n", buf, ok ? "OK" : "FEHLER"); + LOG_I("MQTT", "publishSession: %s -> %s", buf, ok ? "OK" : "FEHLER"); } // -------------------------------------------------------------------------- @@ -138,7 +139,7 @@ void MqttClient::publishHeartbeat() { serializeJson(doc, buf, sizeof(buf)); bool ok = _client.publish(MQTT_TOPIC_STATUS, buf, /*retained=*/true); - Serial.printf("[MQTT] Heartbeat: %s -> %s\n", buf, ok ? "OK" : "FEHLER"); + LOG_I("MQTT", "Heartbeat: %s -> %s", buf, ok ? "OK" : "FEHLER"); } // -------------------------------------------------------------------------- @@ -154,7 +155,7 @@ bool MqttClient::reconnect() { // Offline-LWT (Last Will and Testament) const char* lwtPayload = "{\"online\":false}"; - Serial.printf("[MQTT] Verbinde als '%s'...\n", _clientId); + LOG_I("MQTT", "Verbinde als '%s'...", _clientId); bool ok; if (user && pass) { @@ -167,14 +168,14 @@ bool MqttClient::reconnect() { } if (ok) { - Serial.println("[MQTT] Verbunden!"); + LOG_I("MQTT", "Verbunden!"); _client.subscribe(MQTT_TOPIC_RESET); - Serial.printf("[MQTT] Abonniert: %s\n", MQTT_TOPIC_RESET); + LOG_I("MQTT", "Abonniert: %s", MQTT_TOPIC_RESET); // Sofortigen Heartbeat senden publishHeartbeat(); _lastHeartbeatMs = millis(); } else { - Serial.printf("[MQTT] Verbindung fehlgeschlagen, rc=%d\n", _client.state()); + LOG_E("MQTT", "Verbindung fehlgeschlagen, rc=%d", _client.state()); } display.showMqttError(!ok); @@ -190,7 +191,7 @@ void MqttClient::onMessage(const char* topic, byte* payload, unsigned int length size_t copyLen = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1; memcpy(msg, payload, copyLen); - Serial.printf("[MQTT] Nachricht: topic=%s payload=%s\n", topic, msg); + LOG_I("MQTT", "Nachricht: topic=%s payload=%s", topic, msg); // Reset-Kommando if (strcmp(topic, MQTT_TOPIC_RESET) == 0) { @@ -210,12 +211,12 @@ void MqttClient::onMessage(const char* topic, byte* payload, unsigned int length doReset = v.as() || v.as() == 1; } } else { - Serial.printf("[MQTT] JSON-Parse-Fehler: %s\n", err.c_str()); + LOG_E("MQTT", "JSON-Parse-Fehler: %s", err.c_str()); } } if (doReset) { - Serial.println("[MQTT] RESET-Kommando empfangen -> laserTracker.resetTotal()"); + LOG_I("MQTT", "RESET-Kommando empfangen -> laserTracker.resetTotal()"); laserTracker.resetTotal(); } @@ -232,7 +233,7 @@ void MqttClient::onMessage(const char* topic, byte* payload, unsigned int length } } if (doResetSession) { - Serial.println("[MQTT] RESET_SESSION-Kommando empfangen -> laserTracker.resetSessionSum()"); + LOG_I("MQTT", "RESET_SESSION-Kommando empfangen -> laserTracker.resetSessionSum()"); laserTracker.resetSessionSum(); } } @@ -250,11 +251,11 @@ bool MqttClient::isConnected() { // -------------------------------------------------------------------------- void MqttClient::printToSerial() { const auto& cfg = settings.get(); - Serial.println("=== MqttClient ==="); - Serial.printf(" Broker : %s\n", cfg.mqttBroker[0] ? cfg.mqttBroker : DEFAULT_MQTT_BROKER); - Serial.printf(" Port : %u\n", cfg.mqttPort > 0 ? cfg.mqttPort : DEFAULT_MQTT_PORT); - Serial.printf(" ClientID : %s\n", _clientId[0] ? _clientId : MQTT_CLIENT_ID); - Serial.printf(" Verbunden: %s\n", _client.connected() ? "JA" : "NEIN"); - Serial.printf(" rc : %d\n", _client.state()); - Serial.println("=================="); + LOG_I("MQTT", "=== MqttClient ==="); + LOG_I("MQTT", " Broker : %s", cfg.mqttBroker[0] ? cfg.mqttBroker : DEFAULT_MQTT_BROKER); + LOG_I("MQTT", " Port : %u", cfg.mqttPort > 0 ? cfg.mqttPort : DEFAULT_MQTT_PORT); + LOG_I("MQTT", " ClientID : %s", _clientId[0] ? _clientId : MQTT_CLIENT_ID); + LOG_I("MQTT", " Verbunden: %s", _client.connected() ? "JA" : "NEIN"); + LOG_I("MQTT", " rc : %d", _client.state()); + LOG_I("MQTT", "=================="); } \ No newline at end of file diff --git a/src/web_server.cpp b/src/web_server.cpp index 0a21992..350d14b 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -4,7 +4,7 @@ // ============================================================================= #include "web_server.h" -#include // voller Include nur in .cpp (PIMPL) +#include // voller Include nur in .cpp (PIMPL) – enthält AsyncWebSocket #include // ESP32 built-in, kein lib_deps-Eintrag noetig #include "config.h" #include "settings.h" @@ -19,6 +19,25 @@ extern void wifiResetCredentialsAndRestart(); // Globale Instanz WebServerManager webServer; +// ----------------------------------------------------------------------------- +// Datei-statischer WebSocket-Pointer für webLogForward() (gesetzt in begin()) +// ----------------------------------------------------------------------------- +static AsyncWebSocket* _wsPtr = nullptr; + +// Ring-Buffer für Web-Log-Console. +// webLogForward() läuft auf Core 1 (Arduino loop) – kein Task, kein Queue, +// kein Core-Problem. Browser pollt GET /log-data alle 2 s. +static const size_t LOG_BUF_MAX = 6000; +static String _logBuf = ""; + +void webLogForward(const char* msg) { + _logBuf += msg; + _logBuf += '\n'; + if (_logBuf.length() > LOG_BUF_MAX) { + _logBuf = _logBuf.substring(_logBuf.length() - LOG_BUF_MAX / 2); + } +} + // ----------------------------------------------------------------------------- // Hilfsmakro: HTML-Template-Platzhalter ersetzen // ----------------------------------------------------------------------------- @@ -30,12 +49,28 @@ static String htmlReplace(String html, const String& key, const String& value) { // ----------------------------------------------------------------------------- // Konstruktor // ----------------------------------------------------------------------------- -WebServerManager::WebServerManager() : _server(new AsyncWebServer(80)) {} +WebServerManager::WebServerManager() + : _server(new AsyncWebServer(80)) + , _ws(new AsyncWebSocket("/log-ws")) +{} // ----------------------------------------------------------------------------- // begin() – Routen registrieren und Server starten // ----------------------------------------------------------------------------- void WebServerManager::begin() { + // WebSocket-Handler registrieren + _ws->onEvent([](AsyncWebSocket* server, AsyncWebSocketClient* client, + AwsEventType type, void* arg, uint8_t* data, size_t len) { + (void)server; (void)arg; (void)data; (void)len; + if (type == WS_EVT_CONNECT) { + LOG_I("WEB", "Log Console Client #%u verbunden", client->id()); + } else if (type == WS_EVT_DISCONNECT) { + LOG_I("WEB", "Log Console Client #%u getrennt", client->id()); + } + }); + _server->addHandler(_ws); + _wsPtr = _ws; // ab jetzt ist webLogForward() aktiv + registerRoutes(); // ElegantOTA-Auth: nur wenn Passwort gesetzt const Settings& s = settings.get(); @@ -57,17 +92,16 @@ void WebServerManager::begin() { ArduinoOTA.begin(); LOG_I("WEB", "ArduinoOTA aktiv: lasercutter-display.local"); - Serial.println(F("[WebServer] gestartet auf Port 80")); - Serial.print(F("[WebServer] URL: http://")); - Serial.println(WiFi.localIP()); + LOG_I("WEB", "gestartet auf http://%s", WiFi.localIP().toString().c_str()); LOG_I("WEB", "Auth: %s", s.webPassword[0] ? "aktiv" : "deaktiviert (kein Passwort)"); } // ----------------------------------------------------------------------------- -// loop() – ArduinoOTA verarbeiten (in main loop() aufrufen) +// loop() – ArduinoOTA verarbeiten + WebSocket-Clients bereinigen (in main loop() aufrufen) // ----------------------------------------------------------------------------- void WebServerManager::loop() { ArduinoOTA.handle(); + _ws->cleanupClients(); // abgemeldete Clients freigeben (verhindert Memory-Leak) } // ----------------------------------------------------------------------------- @@ -154,7 +188,7 @@ void WebServerManager::registerRoutes() { } } - Serial.println(F("[WebServer] Konfiguration gespeichert – Neustart empfohlen")); + LOG_I("WEB", "Konfiguration gespeichert – Neustart empfohlen"); // Weiterleitung mit Hinweis request->send(200, "text/html; charset=utf-8", @@ -172,7 +206,7 @@ void WebServerManager::registerRoutes() { _server->on("/reset", HTTP_POST, [this](AsyncWebServerRequest* request) { if (!requireAuth(request)) return; laserTracker.resetSessionSum(); - Serial.println(F("[WebServer] Session zurueckgesetzt")); + LOG_I("WEB", "Session zurueckgesetzt"); request->send(200, "text/html; charset=utf-8", "" "" @@ -185,7 +219,7 @@ void WebServerManager::registerRoutes() { // --- POST /wifi-reset – WiFi-Credentials löschen und neu starten -------- _server->on("/wifi-reset", HTTP_POST, [this](AsyncWebServerRequest* request) { if (!requireAuth(request)) return; - Serial.println(F("[WebServer] WiFi-Credentials geloescht – Neustart")); + LOG_I("WEB", "WiFi-Credentials geloescht – Neustart"); request->send(200, "text/html; charset=utf-8", "" "" @@ -199,6 +233,18 @@ void WebServerManager::registerRoutes() { wifiResetCredentialsAndRestart(); }); + // --- GET /log – Web-Log-Console ---------------------------------------- + _server->on("/log", HTTP_GET, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; + request->send(200, "text/html; charset=utf-8", buildLogPage()); + }); + + // --- GET /log-data – aktuellen Log-Puffer als Plain-Text liefern -------- + _server->on("/log-data", HTTP_GET, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; + request->send(200, "text/plain; charset=utf-8", _logBuf); + }); + // 404 _server->onNotFound([](AsyncWebServerRequest* request) { request->send(404, "text/plain", "Not found"); @@ -259,6 +305,7 @@ String WebServerManager::buildStatusPage() { "" "" "" + "" "" "" "" @@ -278,6 +325,66 @@ String WebServerManager::buildStatusPage() { return html; } +// ----------------------------------------------------------------------------- +// buildLogPage() +// ----------------------------------------------------------------------------- +String WebServerManager::buildLogPage() { + return String(F( + "" + "" + "" + "Log Console – LaserCutter Display" + "" + "" + "

💿 Log Console

" + "
" + "" + "" + "" + "Lade…" + "
" + "
" + "" + )); +} + // ----------------------------------------------------------------------------- // buildConfigPage() // ----------------------------------------------------------------------------- diff --git a/test_sketches/mqtt_test_secrets.h.example b/test_sketches/mqtt_test_secrets.h.example index b22efe3..5013985 100644 --- a/test_sketches/mqtt_test_secrets.h.example +++ b/test_sketches/mqtt_test_secrets.h.example @@ -11,3 +11,7 @@ #define TEST_MQTT_PORT 1883 #define TEST_MQTT_USER "" #define TEST_MQTT_PASSWORD "" + +// WLAN-Zugangsdaten (für test-telnet und andere WiFi-Tests ohne WiFiManager) +#define TEST_WIFI_SSID "DEIN_WLAN_SSID" +#define TEST_WIFI_PASSWORD "DEIN_WLAN_PASSWORT" diff --git a/test_sketches/test_telnet.cpp b/test_sketches/test_telnet.cpp new file mode 100644 index 0000000..001c071 --- /dev/null +++ b/test_sketches/test_telnet.cpp @@ -0,0 +1,118 @@ +/** + * TEST SKETCH – Telnet Server (Minimal, keine externen Libraries) + * + * Testet ob WiFiServer / WiFiClient auf dem ESP32 korrekt funktioniert. + * Keine Abhängigkeiten außer dem ESP32 Arduino Framework. + * + * Ablauf: + * 1. Verbindet sich mit WLAN (Credentials in mqtt_test_secrets.h) + * 2. Öffnet Telnet-Server auf Port 23 + * 3. Sendet jede Sekunde einen Zähler über Serial UND Telnet + * 4. Eingehende Telnet-Bytes werden auf Serial gespiegelt + * + * ENTHÄLT ArduinoOTA → nach dem Test kann per OTA zurückgeflasht werden! + * + * Flash: pio run -e test-telnet --target upload + * Monitor: pio device monitor (USB Serial, 115200) + * Telnet: telnet 23 oder PuTTY (Telnet, Port 23) + * Zurück: pio run -e az-delivery-devkit-v4-ota --target upload + * + * Erwartetes Verhalten: + * Serial + Telnet: "[TICK] uptime: 5 s, heap: 234560" jede Sekunde + */ + +#include +#include +#include +#include "mqtt_test_secrets.h" // TEST_WIFI_SSID, TEST_WIFI_PASSWORD + +// ----------------------------------------------------------------------- +// WiFi-Zugangsdaten aus mqtt_test_secrets.h +// Fehlermeldung falls nicht definiert +// ----------------------------------------------------------------------- +#ifndef TEST_WIFI_SSID + #error "TEST_WIFI_SSID nicht definiert – bitte mqtt_test_secrets.h anlegen" +#endif + +static WiFiServer telnetServer(23); +static WiFiClient telnetClient; + +// ----------------------------------------------------------------------- +void setup() { + Serial.begin(115200); + delay(500); + Serial.println("\n[SETUP] Telnet-Test gestartet"); + + // WLAN verbinden + Serial.printf("[SETUP] Verbinde mit SSID: %s ...\n", TEST_WIFI_SSID); + WiFi.begin(TEST_WIFI_SSID, TEST_WIFI_PASSWORD); + uint32_t t = millis(); + while (WiFi.status() != WL_CONNECTED) { + if (millis() - t > 15000) { + Serial.println("[SETUP] FEHLER: WLAN-Verbindung nach 15s fehlgeschlagen!"); + Serial.println("[SETUP] Gerät startet neu ..."); + delay(1000); + ESP.restart(); + } + delay(500); + Serial.print("."); + } + Serial.printf("\n[SETUP] WiFi verbunden! IP: %s\n", WiFi.localIP().toString().c_str()); + + // ArduinoOTA starten (damit nach dem Test per OTA zurückgeflasht werden kann) + ArduinoOTA.setHostname("lasercutter-display"); + ArduinoOTA.begin(); + Serial.println("[SETUP] ArduinoOTA aktiv"); + + // Telnet-Server starten + telnetServer.begin(); + telnetServer.setNoDelay(true); + Serial.printf("[SETUP] Telnet-Server läuft auf Port 23\n"); + Serial.printf("[SETUP] Verbinde mit: telnet %s 23\n", WiFi.localIP().toString().c_str()); +} + +// ----------------------------------------------------------------------- +void loop() { + ArduinoOTA.handle(); + + // --- Neuen Client annehmen --- + if (!telnetClient || !telnetClient.connected()) { + WiFiClient newClient = telnetServer.available(); + if (newClient) { + telnetClient = newClient; + telnetClient.setNoDelay(true); + Serial.println("[TELNET] Client verbunden!"); + telnetClient.println("[TELNET] Verbindung OK. Sekunden-Ticks folgen ..."); + telnetClient.flush(); + } + } else { + // Weiteren wartenden Client ablehnen + WiFiClient extra = telnetServer.available(); + if (extra) { + extra.stop(); + } + // Eingehende Bytes auf Serial spiegeln + while (telnetClient.available()) { + char c = telnetClient.read(); + Serial.print(c); + } + } + + // --- Jede Sekunde Tick senden --- + static uint32_t lastTick = 0; + static uint32_t counter = 0; + if (millis() - lastTick >= 1000) { + lastTick = millis(); + counter++; + + char msg[80]; + snprintf(msg, sizeof(msg), "[TICK] #%lu uptime: %lu s heap: %u\n", + counter, millis() / 1000, ESP.getFreeHeap()); + + Serial.print(msg); + if (telnetClient && telnetClient.connected()) { + telnetClient.print(msg); + telnetClient.flush(); + } + } +}