feat(FR-002): Web Console via HTTP-Polling (/log + /log-data) -- v1.1.0

- Ring-Buffer _logBuf in web_server.cpp: webLogForward() schreibt auf Core 1
- GET /log-data liefert Puffer als Plain-Text (kein WebSocket, kein Core-Konflikt)
- Browser pollt alle 2 s, Auto-Scroll, dunkles Terminal-Theme
- LOG_I/LOG_E/LOG_D: Timestamp (HH:MM:SS nach NTP, sonst +Xs), webLogForward()
- Alle Serial.* in laser_tracker.cpp, mqtt_client.cpp, web_server.cpp auf LOG_I/LOG_E
- main.cpp: esp_reset_reason() beim Booten loggen (POWER_ON / WATCHDOG / PANIC...)
- telnet_logger.h entfernt (war nur noch Deprecated-Stub)
- Feature-Requests.md: FR-002 abgeschlossen
This commit is contained in:
MaPaLo76 2026-02-28 17:24:56 +01:00
parent a97fa0c111
commit 4dd4ce0620
11 changed files with 388 additions and 74 deletions

View File

@ -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"`

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <IP> 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

View File

@ -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());
}
// ---------------------------------------------------------------------------

View File

@ -15,6 +15,7 @@
#include <Arduino.h>
#include <esp_task_wdt.h>
#include <esp_system.h> // 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();

View File

@ -6,6 +6,7 @@
#include "mqtt_client.h"
#include "laser_tracker.h"
#include "display_manager.h"
#include "config.h"
#include <ArduinoJson.h>
// 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<bool>() || v.as<int>() == 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", "==================");
}

View File

@ -4,7 +4,7 @@
// =============================================================================
#include "web_server.h"
#include <ESPAsyncWebServer.h> // voller Include nur in .cpp (PIMPL)
#include <ESPAsyncWebServer.h> // voller Include nur in .cpp (PIMPL) enthält AsyncWebSocket
#include <ArduinoOTA.h> // 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",
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
"<meta http-equiv='refresh' content='2;url=/'></head>"
@ -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",
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
"<meta http-equiv='refresh' content='10;url=/'></head>"
@ -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() {
"</form>"
"<a href='/config' style='display:block'><button class='btn btn-primary' type='button' style='width:100%'>Konfiguration</button></a>"
"<a href='/update' style='display:block'><button class='btn btn-primary' type='button' style='width:100%'>OTA Update</button></a>"
"<a href='/log' style='display:block'><button class='btn btn-primary' type='button' style='width:100%'>&#128191; Log Console</button></a>"
"</div>"
"<script>setTimeout(()=>location.reload(),10000)</script>"
"</body></html>"
@ -278,6 +325,66 @@ String WebServerManager::buildStatusPage() {
return html;
}
// -----------------------------------------------------------------------------
// buildLogPage()
// -----------------------------------------------------------------------------
String WebServerManager::buildLogPage() {
return String(F(
"<!DOCTYPE html><html>"
"<head><meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Log Console &ndash; LaserCutter Display</title>"
"<style>"
"body{font-family:monospace;background:#1a1a1a;color:#e0e0e0;margin:0;padding:1rem}"
"h1{font-size:1.1rem;color:#90ee90;margin:0 0 .4rem}"
"#ctrl{margin:.4rem 0}"
"button{background:#333;color:#e0e0e0;border:1px solid #555;padding:.3rem .8rem;"
"cursor:pointer;border-radius:3px;margin-right:.4rem}"
"button:hover{background:#555}"
"#status{display:inline-block;font-size:.8rem;color:#888;margin-left:.5rem}"
"#log{background:#000;border:1px solid #333;padding:.6rem;height:78vh;"
"overflow-y:auto;white-space:pre-wrap;word-break:break-all;font-size:.82rem}"
".i{color:#90ee90}.e{color:#ff6b6b}.d{color:#87ceeb}"
"</style></head>"
"<body>"
"<h1>&#128191; Log Console</h1>"
"<div id='ctrl'>"
"<button onclick='lastLen=0;logDiv.innerHTML=\"\"'>Leeren</button>"
"<button id='asBtn' onclick='toggleAS()'>Auto-Scroll: AN</button>"
"<a href='/' style='margin-left:.5rem'><button>&#8592; Status</button></a>"
"<span id='status'>Lade&hellip;</span>"
"</div>"
"<div id='log'></div>"
"<script>"
"var logDiv=document.getElementById('log'),"
"st=document.getElementById('status'),"
"asBt=document.getElementById('asBtn'),"
"autoS=true,lastLen=0;"
"function toggleAS(){autoS=!autoS;asBt.textContent='Auto-Scroll:'+(autoS?' AN':' AUS');}"
"function renderLog(text){"
"if(text.length===lastLen)return;"
"lastLen=text.length;"
"logDiv.innerHTML='';"
"text.split('\\n').forEach(function(line){"
"if(!line)return;"
"var s=document.createElement('span');"
"s.className=line.indexOf('[E]')!==-1?'e':line.indexOf('[D]')!==-1?'d':'i';"
"s.textContent=line+'\\n';"
"logDiv.appendChild(s);"
"});"
"if(autoS)logDiv.scrollTop=logDiv.scrollHeight;"
"}"
"function poll(){"
"fetch('/log-data')"
".then(function(r){return r.text();})"
".then(function(t){st.textContent='OK';st.style.color='#90ee90';renderLog(t);})"
".catch(function(){st.textContent='Fehler';st.style.color='#ff6b6b';});"
"}"
"poll();setInterval(poll,2000);"
"</script></body></html>"
));
}
// -----------------------------------------------------------------------------
// buildConfigPage()
// -----------------------------------------------------------------------------

View File

@ -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"

View File

@ -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 <IP> 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 <Arduino.h>
#include <WiFi.h>
#include <ArduinoOTA.h>
#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();
}
}
}