diff --git a/Feature-Requests.md b/Feature-Requests.md index 18b9171..529e85c 100644 --- a/Feature-Requests.md +++ b/Feature-Requests.md @@ -19,12 +19,36 @@ Status: `[ ]` = offen · `[x]` = erledigt ## Offen -*(keine offenen Punkte)* - --- ## Erledigt +### Version 1.4.0 + +- [x] **FR-010** Feature: Webinterface-Redesign + MQTT-Steuerung ✅ + - **`/` – Laser Cutter Status** (öffentlich, kein Auth) + - Seitentitel `Laser Cutter Status`, bereinigte Tabelle: Laserzeit Summe, Laserzeit Aktuell (m:ss), Laserstatus (an/aus), Gratiszeit, Firmware + - Button `Summe Laserzeit zurücksetzen` (kein Auth, bewusst öffentlich) + - Button `💡 Display ausschalten/einschalten` – Toggle via `fetch()` ohne Seitenwechsel, Route `POST /display-toggle` antwortet 204 + - Button `🔒 Laser Cutter Setup & Status` → `/config` + - Auto-Refresh alle 10 s + - **`/config` – Laser Cutter Setup & Status** (Auth erforderlich) + - H2: Laser Cutter Status – Maschinenlaufzeit h:mm:ss, roter Reset-Button (`/reset-total`) + - H2: MQTT Broker, H2: Webzugang, WLAN-Abschnitt + - H2: Gerät – Reboot-Button `🔄 ESP32 neu starten` (grau, bei aktivem Laser `disabled`) + - H2: Tools – OTA Update, Log Console + - Button `← Laser Cutter Status` zurück auf `/` + - **MQTT-Steuerung**: `lasercutter/reset` ersetzt durch `lasercutter/cmd` + - `{"reset_session_nvs":true}` – Session-Summe + NVS-Maschinenlaufzeit auf 0 + - `{"reset_session_ram":true}` – nur RAM-Session-Summe auf 0, NVS bleibt + - `{"display":true/false}` – Display ein-/ausschalten + - `{"reboot":true}` – ESP32 neu starten + - **Heartbeat `lasercutter/status`**: Feld `"display_on": true/false` hinzugefügt + - **`DisplayManager`**: `setEnabled()` / `isEnabled()` via MAX7219 SHUTDOWN-Modus + - Betroffene Dateien: `src/web_server.cpp`, `src/mqtt_client.cpp`, `include/config.h`, `include/display_manager.h`, `src/display_manager.cpp` + - Commit: `TODO` + - Version: 1.4.0 + ### Version 1.2.0 - [x] **FR-009** Bug: `session_start_time` bei nachgelieferten Sessions (Queue) falsch ✅ diff --git a/README.md b/README.md index 184e8e4..14bb8f0 100644 --- a/README.md +++ b/README.md @@ -211,11 +211,10 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac ### Topics | Richtung | Topic | Format | Beschreibung | -|------------|---------------------------|------------------------------|-------------------------------------------| +|------------|---------------------------|------------------------------|-----------------------------------------| | Publish | `lasercutter/session` | JSON | Wird beim **Ende eines Laser-Bursts** oder nach **Session-Reset** gesendet | | Publish | `lasercutter/status` | JSON (retained) | Heartbeat alle 60 Sekunden + LWT (online/offline) | -| Subscribe | `lasercutter/reset` | `{"reset":true}` oder `"1"` | Setzt die **gesamte** akkumulierte Laserzeit (NVS + RAM) auf 0 | -| Subscribe | `lasercutter/reset` | `{"reset_session":true}` | Setzt nur die **Session-Summe** (RAM) auf 0, NVS bleibt erhalten | +| Subscribe | `lasercutter/cmd` | JSON | Steuerkommandos: Reset, Display, Reboot | ### JSON-Format `lasercutter/session` ```json @@ -231,7 +230,7 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac > `session_start_time` ist ein Lokalzeit-Zeitstempel (ISO 8601, CET/CEST), synchronisiert via NTP (`pool.ntp.org`) unmittelbar nach WLAN-Connect. Zeitzone wird automatisch zwischen CET (UTC+1) und CEST (UTC+2) umgeschaltet. > Wert `"unknown"` wenn die NTP-Synchronisation beim Session-Start noch nicht abgeschlossen war. -> Bei einem **Session-Reset** (Web oder MQTT `{"reset_session":true}`) wird ebenfalls ein `lasercutter/session`-Publish ausgelöst mit den bis dahin akkumulierten Werten – identisches Format wie beim normalen Session-Ende. +> Bei einem **Session-Reset** (Web oder MQTT `{"reset_session_ram":true}`) wird ebenfalls ein `lasercutter/session`-Publish ausgelöst mit den bis dahin akkumulierten Werten – identisches Format wie beim normalen Session-Ende. ### JSON-Format `lasercutter/status` ```json @@ -239,6 +238,7 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac "online": true, "session_sum": "42.50", "machine_running_time_min": "1234.75", + "display_on": true, "ip": "192.168.1.100", "uptime_s": 3600, "firmware_version": "1.2.1 (Mar 1 2026)", @@ -248,6 +248,20 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac > `reset_reason` mögliche Werte: `POWERON`, `SOFTWARE`, `PANIC`, `TASK_WDT`, `INT_WDT`, `WDT`, `BROWNOUT`, `EXT_PIN`, `DEEPSLEEP`, `SDIO`, `UNKNOWN`. Der Wert wird einmalig beim Start gespeichert und bleibt für die gesamte Laufzeit konstant. +### JSON-Format `lasercutter/cmd` + +Alle Steuerkommandos werden als JSON an `lasercutter/cmd` gesendet: + +```json +{ "reset_session_nvs": true } // Session-Summe + NVS-Maschinenlaufzeit auf 0 +{ "reset_session_ram": true } // nur RAM-Session-Summe auf 0, NVS bleibt +{ "display": true } // Display einschalten +{ "display": false } // Display ausschalten +{ "reboot": true } // ESP32 neu starten +``` + +> Mehrere Kommandos können in einem JSON kombiniert werden, z. B. `{"reset_session_ram":true, "display":false}`. + ### LWT (Last Will and Testament) Bei Verbindungsabbruch sendet der Broker automatisch auf `lasercutter/status`: ```json @@ -266,14 +280,18 @@ Bei Verbindungsabbruch sendet der Broker automatisch auf `lasercutter/status`: Das Webinterface ist über die IP-Adresse des ESP32 im Browser erreichbar. -| Seite | URL | Funktion | -|------------------|-------------|--------------------------------------------------------------| -| Status | `/` | Aktuelle Laserzeit, letzter Session-Wert, Systemstatus | -| Konfiguration | `/config` | MQTT-Broker (IP, Port, User, Passwort), Gratiszeit, Polarität, Web-Passwort | -| Reset | `/reset` | Setzt die **Session-Summe** (RAM) auf 0 zurück – NVS-Gesamtzeit bleibt erhalten | -| OTA Update | `/update` | Firmware-Update über Browser | +| Seite | URL | Auth | Funktion | +|--------------------------|-------------------|------|-----------------------------------------------------------------------| +| Laser Cutter Status | `/` | – | Laserzeit, Laserstatus, Display-Toggle, Session-Reset | +| Laser Cutter Setup | `/config` | ✔ | MQTT, Web-Auth, WLAN, Maschinenlaufzeit-Reset, Gratiszeit, Polarität, Reboot | +| Session-Reset | `/reset` | – | Setzt die **Session-Summe** (RAM) auf 0 – NVS bleibt erhalten | +| Maschinenlaufzeit-Reset | `/reset-total` | ✔ | Setzt Maschinenlaufzeit im NVS auf 0 | +| Display-Toggle | `/display-toggle` | – | Display ein-/ausschalten (POST, antwortet 204) | +| Reboot | `/reboot` | ✔ | ESP32 sofort neu starten | +| OTA Update | `/update` | – | Firmware-Update über Browser (ElegantOTA) | +| Log Console | `/log` | ✔ | Live-Log über WebSocket | -> Alle Seiten sind per **HTTP Basic Auth** geschützt, wenn in der Konfiguration ein Web-Passwort gesetzt ist. +> `/` ist **öffentlich** (kein Passwortschutz) – der Display-Toggle und Session-Reset sind bewusst ohne Auth erreichbar. Alle sicherheitsrelevanten Seiten (Setup, Reboot, Maschinenlaufzeit-Reset) erfordern HTTP Basic Auth, sofern ein Web-Passwort gesetzt ist. --- diff --git a/html-template/Laser-Cutter-Display.html b/html-template/Laser-Cutter-Display.html new file mode 100644 index 0000000..757e9f6 --- /dev/null +++ b/html-template/Laser-Cutter-Display.html @@ -0,0 +1,49 @@ + + + + + + LaserCutter Display + + + +

LaserCutter Display

+ + + + + + + + + + + +
Summe Laserzeit8 min
Aktuelle Laserzeit423 s
Letzte Laserzeit0 s
Laseraktiv
Maschinenlaufzeit (gesamt)619.50 min
IP-Adresse192.168.2.62
MQTT✓verbunden
Brokermqtt.majufilo.eu:8883
Gratiszeit10 s
Firmwarev1.3.0 (Mar 1 2026)
+
+
+ +
+ + + + + + + + + +
+ + + diff --git a/include/config.h b/include/config.h index 2a58915..5d8e3fc 100644 --- a/include/config.h +++ b/include/config.h @@ -72,7 +72,7 @@ // MQTT Topics #define MQTT_TOPIC_SESSION "lasercutter/session" // Publish beim Session-Ende #define MQTT_TOPIC_STATUS "lasercutter/status" // Publish Heartbeat -#define MQTT_TOPIC_RESET "lasercutter/reset" // Subscribe Reset-Befehl +#define MQTT_TOPIC_CMD "lasercutter/cmd" // Subscribe Steuerkommandos // ----------------------------------------------------------------------------- // WiFiManager diff --git a/include/display_manager.h b/include/display_manager.h index 8c66626..f00c18e 100644 --- a/include/display_manager.h +++ b/include/display_manager.h @@ -99,11 +99,18 @@ public: void allLedsOn(); void allLedsOff(); + // Display ein-/ausschalten (SHUTDOWN-Modus des MAX7219) + // setEnabled(false) → alle LEDs aus (MAX7219 Shutdown) + // setEnabled(true) → normaler Betrieb + void setEnabled(bool enabled); + bool isEnabled() const { return _enabled; } + // Debug-Ausgabe auf Serial void printToSerial() const; private: MD_MAX72XX _mx; + bool _enabled = true; // Schreibt genau DISP_CHARS_PER_ZONE Zeichen auf eine Zone // zone 0 → Module 0..3 (oben) diff --git a/platformio.ini b/platformio.ini index 2052356..d028053 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.3.0"' ; Semantic Versioning – hier erhöhen bei neuem Release + -DFIRMWARE_VERSION='"1.4.0"' ; Semantic Versioning – hier erhöhen bei neuem Release lib_deps = majicDesigns/MD_Parola @ ^3.7.3 majicDesigns/MD_MAX72XX @ ^3.5.1 diff --git a/src/display_manager.cpp b/src/display_manager.cpp index d549b3f..da46552 100644 --- a/src/display_manager.cpp +++ b/src/display_manager.cpp @@ -215,6 +215,13 @@ void DisplayManager::setBrightness(uint8_t level) { _mx.control(MD_MAX72XX::INTENSITY, level); } +// --------------------------------------------------------------------------- +void DisplayManager::setEnabled(bool enabled) { + _enabled = enabled; + // SHUTDOWN ON = LEDs aus; SHUTDOWN OFF = normaler Betrieb (invertierte Logik) + _mx.control(MD_MAX72XX::SHUTDOWN, enabled ? MD_MAX72XX::OFF : MD_MAX72XX::ON); +} + // --------------------------------------------------------------------------- // Einzelnes Zeichen auf ein Modul schreiben (mit CCW Rotation) // --------------------------------------------------------------------------- diff --git a/src/mqtt_client.cpp b/src/mqtt_client.cpp index 3d7ed9a..b0fe08f 100644 --- a/src/mqtt_client.cpp +++ b/src/mqtt_client.cpp @@ -101,7 +101,7 @@ void MqttClient::loop() { // -------------------------------------------------------------------------- // resetSessionCounter() - Session-ID Zähler auf 0 zurücksetzen -// Wird bei reset / reset_session MQTT-Kommando aufgerufen. +// Wird bei reset_session_nvs / reset_session_ram MQTT-Kommando aufgerufen. // -------------------------------------------------------------------------- void MqttClient::resetSessionCounter() { _sessionCounter = 0; @@ -178,17 +178,19 @@ void MqttClient::publishHeartbeat() { snprintf(ipBuf, sizeof(ipBuf), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); // Kein ArduinoJson (JsonDocument) – reine snprintf-Formatierung, kein Heap-Alloc - char buf[280]; + char buf[300]; snprintf(buf, sizeof(buf), "{\"online\":true," "\"session_sum\":%d," "\"machine_running_time_min\":\"%s\"," + "\"display_on\":%s," "\"ip\":\"%s\"," "\"uptime_s\":%lu," "\"firmware_version\":\"%s (%s)\"," "\"reset_reason\":\"%s\"}", laserTracker.getAllSessionsSumMinutes(), totalBuf, + display.isEnabled() ? "true" : "false", ipBuf, (unsigned long)(millis() / 1000UL), FIRMWARE_VERSION, __DATE__, @@ -224,8 +226,8 @@ bool MqttClient::reconnect() { if (ok) { LOG_I("MQTT", "Verbunden!"); - _client->subscribe(MQTT_TOPIC_RESET); - LOG_I("MQTT", "Abonniert: %s", MQTT_TOPIC_RESET); + _client->subscribe(MQTT_TOPIC_CMD); + LOG_I("MQTT", "Abonniert: %s", MQTT_TOPIC_CMD); publishHeartbeat(); _lastHeartbeatMs = millis(); } else { @@ -324,51 +326,46 @@ void MqttClient::onMessage(const char* topic, byte* payload, unsigned int length LOG_I("MQTT", "Nachricht: topic=%s payload=%s", topic, msg); - // Reset-Kommando - if (strcmp(topic, MQTT_TOPIC_RESET) == 0) { - bool doReset = false; - - // Plain "1" (rueckwaertskompatibel) - if (strcmp(msg, "1") == 0) { - doReset = true; - } - // JSON: {"reset":true} oder {"reset":1} - else if (msg[0] == '{') { - JsonDocument doc; - DeserializationError err = deserializeJson(doc, msg); - if (!err) { - JsonVariant v = doc["reset"]; - if (!v.isNull()) { - doReset = v.as() || v.as() == 1; - } - } else { - LOG_E("MQTT", "JSON-Parse-Fehler: %s", err.c_str()); - } + // Steuerkommandos (lasercutter/cmd) + if (strcmp(topic, MQTT_TOPIC_CMD) == 0 && msg[0] == '{') { + JsonDocument doc; + DeserializationError err = deserializeJson(doc, msg); + if (err) { + LOG_E("MQTT", "JSON-Parse-Fehler: %s", err.c_str()); + return; } - if (doReset) { - LOG_I("MQTT", "RESET-Kommando empfangen -> laserTracker.resetTotal()"); + // {"reset_session_nvs":true} – Session-Summe + NVS zurücksetzen + JsonVariant vNvs = doc["reset_session_nvs"]; + if (!vNvs.isNull() && (vNvs.as() || vNvs.as() == 1)) { + LOG_I("MQTT", "CMD reset_session_nvs -> laserTracker.resetTotal()"); laserTracker.resetTotal(); mqttClient.resetSessionCounter(); } - // reset_session: nur RAM-Session-Summe auf 0, NVS bleibt - bool doResetSession = false; - if (msg[0] == '{') { - JsonDocument doc2; - DeserializationError err = deserializeJson(doc2, msg); - if (!err) { - JsonVariant v = doc2["reset_session"]; - if (!v.isNull()) { - doResetSession = v.as() || v.as() == 1; - } - } - } - if (doResetSession) { - LOG_I("MQTT", "RESET_SESSION-Kommando empfangen -> laserTracker.resetSessionSum()"); + // {"reset_session_ram":true} – nur RAM-Session-Summe auf 0, NVS bleibt + JsonVariant vRam = doc["reset_session_ram"]; + if (!vRam.isNull() && (vRam.as() || vRam.as() == 1)) { + LOG_I("MQTT", "CMD reset_session_ram -> laserTracker.resetSessionSum()"); laserTracker.resetSessionSum(); mqttClient.resetSessionCounter(); } + + // {"display":true/false} – Display ein-/ausschalten + JsonVariant vDisplay = doc["display"]; + if (!vDisplay.isNull()) { + bool on = vDisplay.as(); + display.setEnabled(on); + LOG_I("MQTT", "CMD display -> %s", on ? "an" : "aus"); + } + + // {"reboot":true} – ESP32 neu starten + JsonVariant vReboot = doc["reboot"]; + if (!vReboot.isNull() && (vReboot.as() || vReboot.as() == 1)) { + LOG_I("MQTT", "CMD reboot -> ESP.restart()"); + delay(200); + ESP.restart(); + } } } diff --git a/src/web_server.cpp b/src/web_server.cpp index fe876cb..d651e80 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -10,6 +10,7 @@ #include "settings.h" #include "laser_tracker.h" #include "mqtt_client.h" +#include "display_manager.h" // wifi_connector.h wird hier NICHT eingebunden (Konflikt WiFiManager vs. ESPAsyncWebServer) // stattdessen freie Funktion aus wifi_connector.cpp: extern void wifiResetCredentialsAndRestart(); @@ -138,9 +139,8 @@ bool WebServerManager::requireAuth(AsyncWebServerRequest* request) { // ----------------------------------------------------------------------------- void WebServerManager::registerRoutes() { - // --- GET / – Statusseite ------------------------------------------------- + // --- GET / – Statusseite (öffentlich, kein Auth) ------------------------- _server->on("/", HTTP_GET, [this](AsyncWebServerRequest* request) { - if (!requireAuth(request)) return; request->send(200, "text/html; charset=utf-8", buildStatusPage()); }); @@ -232,6 +232,43 @@ void WebServerManager::registerRoutes() { ""); }); + // --- POST /reset-total – Maschinenlaufzeit (NVS) zurücksetzen ------------ + _server->on("/reset-total", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; + laserTracker.resetTotal(); + mqttClient.resetSessionCounter(); + LOG_I("WEB", "Maschinenlaufzeit zurueckgesetzt (NVS)"); + request->send(200, "text/html; charset=utf-8", + "" + "" + "" + "

✓ Maschinenlaufzeit zurückgesetzt

" + "

Weiterleitung …

" + ""); + }); + + // --- POST /display-toggle – Display ein-/ausschalten (öffentlich) ---------- + _server->on("/display-toggle", HTTP_POST, [this](AsyncWebServerRequest* request) { + display.setEnabled(!display.isEnabled()); + LOG_I("WEB", "Display %s", display.isEnabled() ? "an" : "aus"); + request->send(204); // No Content – Browser bleibt auf aktueller Seite + }); + + // --- POST /reboot – ESP32 neu starten (Auth erforderlich) ---------------- + _server->on("/reboot", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; + LOG_I("WEB", "Reboot angefordert (Web)"); + request->send(200, "text/html; charset=utf-8", + "" + "" + "" + "

🔄 ESP32 startet neu …

" + "

Weiterleitung in 5 Sekunden.

" + ""); + delay(200); + ESP.restart(); + }); + // --- POST /wifi-reset – WiFi-Credentials löschen und neu starten -------- _server->on("/wifi-reset", HTTP_POST, [this](AsyncWebServerRequest* request) { if (!requireAuth(request)) return; @@ -273,70 +310,68 @@ void WebServerManager::registerRoutes() { String WebServerManager::buildStatusPage() { const Settings& s = settings.get(); - String mqttStatus = mqttClient.isConnected() - ? "✓ verbunden" - : "✗ getrennt"; + // Laserzeit Aktuell: m:ss Format + int runSec = laserTracker.getRunningSessionSeconds(); + int runMin = runSec / 60; + int runSecPart = runSec % 60; + char runBuf[16]; + snprintf(runBuf, sizeof(runBuf), "%d:%02d", runMin, runSecPart); - String laserStatus = laserTracker.isActive() - ? "aktiv" - : "inaktiv"; - - String ip = WiFi.isConnected() ? WiFi.localIP().toString() : "nicht verbunden"; - - // Gesamtzeit formatieren - char totalBuf[16]; - snprintf(totalBuf, sizeof(totalBuf), "%.2f", laserTracker.getTotalMinutes()); + // Laserstatus: an / aus + const char* laserStatus = laserTracker.isActive() + ? "an" + : "aus"; String html = F( "" "" "" - "LaserCutter Display" + "Laser Cutter Status" "" "" - "

LaserCutter Display

" + "

Laser Cutter Status

" "" - "" - "" - "" - "" - "" - "" - "" - "" - "" + "" + "" + "" + "" "" "
Summe Laserzeit%ALLSESSIONS% min
Aktuelle Laserzeit%CURRENTSESSION% s
Letzte Laserzeit%LASTSESSION% s
Laser%LASER%
Maschinenlaufzeit (gesamt)%TOTAL% min
IP-Adresse%IP%
MQTT%MQTT%
Broker%BROKER%:%PORT%
Gratiszeit%GRATIS% s
Laserzeit Summe%ALLSESSIONS% Minuten
Laserzeit Aktuell%CURRENTSESSION%
Laserstatus%LASER%
Gratiszeit%GRATIS% Sekunden
Firmwarev" FIRMWARE_VERSION " (" __DATE__ ")
" - "
" - "
" - "" + "" "
" - "" - "" - "" - "
" + "" + "🔒 Laser Cutter Setup & Status" "" "" ); - html.replace("%ALLSESSIONS%", String(laserTracker.getAllSessionsSumMinutes())); - html.replace("%CURRENTSESSION%", String(laserTracker.getRunningSessionSeconds())); - html.replace("%TOTAL%", String(totalBuf)); - html.replace("%LASTSESSION%", String(laserTracker.getLastSessionSeconds())); - html.replace("%LASER%", laserStatus); - html.replace("%IP%", ip); - html.replace("%MQTT%", mqttStatus); - html.replace("%BROKER%", String(s.mqttBroker)); - html.replace("%PORT%", String(s.mqttPort)); - html.replace("%GRATIS%", String(s.gratisSeconds)); + html.replace("%ALLSESSIONS%", String(laserTracker.getAllSessionsSumMinutes())); + html.replace("%CURRENTSESSION%", String(runBuf)); + html.replace("%LASER%", laserStatus); + html.replace("%GRATIS%", String(s.gratisSeconds)); + if (display.isEnabled()) { + html.replace("%DISP_BTN_CLASS%", "btn-gray"); + html.replace("%DISP_BTN_LABEL%", "💡 Display ausschalten"); + } else { + html.replace("%DISP_BTN_CLASS%", "btn-blue"); + html.replace("%DISP_BTN_LABEL%", "💡 Display einschalten"); + } return html; } @@ -407,34 +442,52 @@ String WebServerManager::buildLogPage() { String WebServerManager::buildConfigPage() { const Settings& s = settings.get(); + // Maschinenlaufzeit als h:mm:ss + long totalSec = (long)(laserTracker.getTotalMinutes() * 60.0f); + long tH = totalSec / 3600; + long tM = (totalSec % 3600) / 60; + long tS = totalSec % 60; + char totalBuf[16]; + snprintf(totalBuf, sizeof(totalBuf), "%ld:%02ld:%02ld", tH, tM, tS); + String html = F( "" "" "" - "Konfiguration – LaserCutter Display" + "Laser Cutter Setup & Status" "" "" - "

Konfiguration

" + "

Laser Cutter Setup & Status

" + "← Laser Cutter Status" + + "

Laser Cutter Status

" + "" + "

%TOTAL%

" + "
⚠️ Maschinenlaufzeit zurücksetzen löscht den Gesamtspeicher im NVS " + "unwiderruflich – diese Aktion kann nicht rückgängig gemacht werden.
" + "
" + "" + "
" + "
" - "" - "" - "" - "" - "" - "" - "" - "" - "" + "" "" "" @@ -444,37 +497,59 @@ String WebServerManager::buildConfigPage() { "" - "
" - "

Web-Zugang

" + + "

MQTT Broker

" + "" + "" + "" + "" + "" + "" + "" + "" + + "

Webzugang

" "" "" "" "" - "