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 Laserzeit | 8 min |
+ | Aktuelle Laserzeit | 423 s |
+ | Letzte Laserzeit | 0 s |
+ | Laser | aktiv |
+ | Maschinenlaufzeit (gesamt) | 619.50 min |
+ | IP-Adresse | 192.168.2.62 |
+ | MQTT | ✓verbunden |
+ | Broker | mqtt.majufilo.eu:8883 |
+ | Gratiszeit | 10 s |
+ | Firmware | v1.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() {
"