feat(web+mqtt): FR-010 Webinterface-Redesign + MQTT-Steuerung
Webinterface: - /: oeffentlich, Laser Cutter Status, m:ss, an/aus, Display-Toggle (fetch/204) - /config: H2-Abschnitte (Status, MQTT, Webzugang, Geraet, Tools) Maschinenlaufzeit h:mm:ss, Reboot-Button (disabled bei aktivem Laser) - /reset-total: neue POST-Route fuer NVS-Maschinenlaufzeit-Reset (Auth) - /display-toggle: POST, 204, kein Seitenwechsel - /reboot: POST, Auth, ESP.restart() DisplayManager: - setEnabled()/isEnabled() via MAX7219 SHUTDOWN-Modus MQTT: - lasercutter/reset -> lasercutter/cmd (MQTT_TOPIC_CMD) - Kommandos: reset_session_nvs, reset_session_ram, display, reboot - Heartbeat lasercutter/status: display_on-Feld hinzugefuegt
This commit is contained in:
parent
d9322f67d8
commit
c61a67f812
|
|
@ -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 ✅
|
||||
|
|
|
|||
40
README.md
40
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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
49
html-template/Laser-Cutter-Display.html
Normal file
49
html-template/Laser-Cutter-Display.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<title>LaserCutter Display</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; max-width: 480px; margin: 2rem auto; padding: 0 1rem }
|
||||
h1 { color: #333 }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem }
|
||||
td { padding: .5rem; border-bottom: 1px solid #eee }
|
||||
td:first-child { color: #666; width: 50% }
|
||||
.btn { display: inline-block; padding: .5rem 1.2rem; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 1rem }
|
||||
.btn-primary { background: #3182ce; color: #fff }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>LaserCutter Display</h1>
|
||||
<table>
|
||||
<tr><td>Summe Laserzeit</td><td>8 min</td></tr>
|
||||
<tr><td>Aktuelle Laserzeit</td><td>423 s</td></tr>
|
||||
<tr><td>Letzte Laserzeit</td><td>0 s</td></tr>
|
||||
<tr><td>Laser</td><td><span style='color:orange'>aktiv</span></td></tr>
|
||||
<tr><td>Maschinenlaufzeit (gesamt)</td><td>619.50 min</td></tr>
|
||||
<tr><td>IP-Adresse</td><td>192.168.2.62</td></tr>
|
||||
<tr><td>MQTT</td><td><span style='color:green'>✓verbunden</span></td></tr>
|
||||
<tr><td>Broker</td><td>mqtt.majufilo.eu:8883</td></tr>
|
||||
<tr><td>Gratiszeit</td><td>10 s</td></tr>
|
||||
<tr><td>Firmware</td><td>v1.3.0 (Mar 1 2026)</td></tr>
|
||||
</table>
|
||||
<div style='display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem'>
|
||||
<form action='/reset' method='post' style='width:100%' onsubmit="return confirm('Summe Laserzeit wirklich zur\u00fccksetzen?')">
|
||||
<button class='btn btn-primary' type='submit' style='width:100%'>Summe Laserzeit zur ücksetzen</button>
|
||||
</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%'>💿Log Console</button>
|
||||
</a>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout( () => location.reload(), 10000)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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<bool>() || v.as<int>() == 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<bool>() || vNvs.as<int>() == 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<bool>() || v.as<int>() == 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<bool>() || vRam.as<int>() == 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<bool>();
|
||||
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<bool>() || vReboot.as<int>() == 1)) {
|
||||
LOG_I("MQTT", "CMD reboot -> ESP.restart()");
|
||||
delay(200);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
"</body></html>");
|
||||
});
|
||||
|
||||
// --- 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",
|
||||
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
||||
"<meta http-equiv='refresh' content='2;url=/config'></head>"
|
||||
"<body style='font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem'>"
|
||||
"<h2>✓ Maschinenlaufzeit zurückgesetzt</h2>"
|
||||
"<p>Weiterleitung …</p>"
|
||||
"</body></html>");
|
||||
});
|
||||
|
||||
// --- 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",
|
||||
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
||||
"<meta http-equiv='refresh' content='5;url=/'></head>"
|
||||
"<body style='font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem'>"
|
||||
"<h2>🔄 ESP32 startet neu …</h2>"
|
||||
"<p>Weiterleitung in 5 Sekunden.</p>"
|
||||
"</body></html>");
|
||||
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()
|
||||
? "<span style='color:green'>✓ verbunden</span>"
|
||||
: "<span style='color:red'>✗ getrennt</span>";
|
||||
// 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()
|
||||
? "<span style='color:orange'>aktiv</span>"
|
||||
: "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()
|
||||
? "<span style='color:#2f9e44;font-weight:bold'>an</span>"
|
||||
: "<span style='color:#868e96'>aus</span>";
|
||||
|
||||
String html = F(
|
||||
"<!DOCTYPE html><html>"
|
||||
"<head><meta charset='utf-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
"<title>LaserCutter Display</title>"
|
||||
"<title>Laser Cutter Status</title>"
|
||||
"<style>"
|
||||
"body{font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem}"
|
||||
"h1{color:#333}table{width:100%;border-collapse:collapse;margin-bottom:1.5rem}"
|
||||
"td{padding:.5rem;border-bottom:1px solid #eee}td:first-child{color:#666;width:50%}"
|
||||
".btn{display:inline-block;padding:.5rem 1.2rem;border:none;border-radius:4px;"
|
||||
"cursor:pointer;text-decoration:none;font-size:1rem}"
|
||||
".btn-primary{background:#3182ce;color:#fff}"
|
||||
"body{font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem;color:#222}"
|
||||
"h1{color:#333;font-size:1.4rem;margin-bottom:1.2rem}"
|
||||
"table{width:100%;border-collapse:collapse;margin-bottom:1.5rem}"
|
||||
"td{padding:.55rem .3rem;border-bottom:1px solid #eee}"
|
||||
"td:first-child{color:#555;width:55%}"
|
||||
"td:last-child{font-weight:500}"
|
||||
".btn{display:block;width:100%;padding:.55rem;border:none;border-radius:5px;"
|
||||
"cursor:pointer;font-size:1rem;box-sizing:border-box;text-align:center;"
|
||||
"text-decoration:none;margin-bottom:.5rem}"
|
||||
".btn-blue{background:#3182ce;color:#fff}"
|
||||
".btn-gray{background:#718096;color:#fff}"
|
||||
"</style></head>"
|
||||
"<body>"
|
||||
"<h1>LaserCutter Display</h1>"
|
||||
"<h1>Laser Cutter Status</h1>"
|
||||
"<table>"
|
||||
"<tr><td>Summe Laserzeit</td><td>%ALLSESSIONS% min</td></tr>"
|
||||
"<tr><td>Aktuelle Laserzeit</td><td>%CURRENTSESSION% s</td></tr>"
|
||||
"<tr><td>Letzte Laserzeit</td><td>%LASTSESSION% s</td></tr>"
|
||||
"<tr><td>Laser</td><td>%LASER%</td></tr>"
|
||||
"<tr><td>Maschinenlaufzeit (gesamt)</td><td>%TOTAL% min</td></tr>"
|
||||
"<tr><td>IP-Adresse</td><td>%IP%</td></tr>"
|
||||
"<tr><td>MQTT</td><td>%MQTT%</td></tr>"
|
||||
"<tr><td>Broker</td><td>%BROKER%:%PORT%</td></tr>"
|
||||
"<tr><td>Gratiszeit</td><td>%GRATIS% s</td></tr>"
|
||||
"<tr><td>Laserzeit Summe</td><td>%ALLSESSIONS% Minuten</td></tr>"
|
||||
"<tr><td>Laserzeit Aktuell</td><td>%CURRENTSESSION%</td></tr>"
|
||||
"<tr><td>Laserstatus</td><td>%LASER%</td></tr>"
|
||||
"<tr><td>Gratiszeit</td><td>%GRATIS% Sekunden</td></tr>"
|
||||
"<tr><td>Firmware</td><td>v" FIRMWARE_VERSION " (" __DATE__ ")</td></tr>"
|
||||
"</table>"
|
||||
"<div style='display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem'>"
|
||||
"<form action='/reset' method='post' style='width:100%' "
|
||||
"<form action='/reset' method='post' "
|
||||
"onsubmit=\"return confirm('Summe Laserzeit wirklich zur\\u00fccksetzen?')\">"
|
||||
"<button class='btn btn-primary' type='submit' style='width:100%'>Summe Laserzeit zurücksetzen</button>"
|
||||
"<button class='btn btn-blue' type='submit'>Summe Laserzeit zurücksetzen</button>"
|
||||
"</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%'>💿 Log Console</button></a>"
|
||||
"</div>"
|
||||
"<button class='btn %DISP_BTN_CLASS%' type='button' "
|
||||
"onclick=\"fetch('/display-toggle',{method:'POST'}).then(()=>location.reload())\""
|
||||
">%DISP_BTN_LABEL%</button>"
|
||||
"<a class='btn btn-gray' href='/config'>🔒 Laser Cutter Setup & Status</a>"
|
||||
"<script>setTimeout(()=>location.reload(),10000)</script>"
|
||||
"</body></html>"
|
||||
);
|
||||
|
||||
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(
|
||||
"<!DOCTYPE html><html>"
|
||||
"<head><meta charset='utf-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
"<title>Konfiguration – LaserCutter Display</title>"
|
||||
"<title>Laser Cutter Setup & Status</title>"
|
||||
"<style>"
|
||||
"body{font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem}"
|
||||
"label{display:block;margin-top:1rem;color:#555;font-size:.9rem}"
|
||||
"body{font-family:sans-serif;max-width:520px;margin:2rem auto;padding:0 1rem;color:#222}"
|
||||
"h1{font-size:1.4rem;color:#333;margin-bottom:1.5rem}"
|
||||
"h2{font-size:1.1rem;color:#444;border-bottom:2px solid #e2e8f0;padding-bottom:.3rem;margin-top:2rem}"
|
||||
"label{display:block;margin-top:.9rem;color:#555;font-size:.9rem}"
|
||||
"input[type=text],input[type=number],input[type=password]"
|
||||
"{width:100%;padding:.4rem;box-sizing:border-box;border:1px solid #ccc;border-radius:4px}"
|
||||
"{width:100%;padding:.4rem .5rem;box-sizing:border-box;border:1px solid #ccc;border-radius:4px}"
|
||||
"input[type=range]{width:100%}"
|
||||
".radio-label{display:flex;align-items:center;gap:.5rem;margin-top:.3rem}"
|
||||
".btn{padding:.5rem 1.5rem;border:none;border-radius:4px;cursor:pointer;font-size:1rem;width:100%}"
|
||||
".btn-primary{background:#3182ce;color:#fff}"
|
||||
".btn-back{background:#718096;color:#fff}"
|
||||
".radio-label{display:flex;align-items:center;gap:.5rem;margin-top:.4rem}"
|
||||
".btn{display:block;width:100%;padding:.55rem;border:none;border-radius:5px;cursor:pointer;"
|
||||
"font-size:1rem;box-sizing:border-box;margin-top:.6rem;text-align:center;text-decoration:none}"
|
||||
".btn-blue{background:#3182ce;color:#fff}"
|
||||
".btn-gray{background:#718096;color:#fff}"
|
||||
".btn-red{background:#c53030;color:#fff}"
|
||||
".info{background:#fff8e1;border:1px solid #ffe082;border-radius:4px;"
|
||||
"padding:.5rem .7rem;font-size:.85rem;color:#6d4c00;margin-top:.5rem}"
|
||||
"</style></head>"
|
||||
"<body>"
|
||||
"<h1>Konfiguration</h1>"
|
||||
"<h1>Laser Cutter Setup & Status</h1>"
|
||||
"<a class='btn btn-gray' href='/'>← Laser Cutter Status</a>"
|
||||
|
||||
"<h2>Laser Cutter Status</h2>"
|
||||
"<label>Maschinenlaufzeit (gesamt)</label>"
|
||||
"<p style='font-size:1.1rem;font-weight:600;margin:.2rem 0'>%TOTAL%</p>"
|
||||
"<div class='info'>⚠️ Maschinenlaufzeit zurücksetzen löscht den Gesamtspeicher im NVS "
|
||||
"unwiderruflich – diese Aktion kann nicht rückgängig gemacht werden.</div>"
|
||||
"<form action='/reset-total' method='post' "
|
||||
"onsubmit=\"return confirm('Maschinenlaufzeit wirklich unwiderruflich aus dem NVS l\u00f6schen?')\">"
|
||||
"<button class='btn btn-red' type='submit'>Maschinenlaufzeit zurücksetzen</button>"
|
||||
"</form>"
|
||||
|
||||
"<form action='/config' method='post'>"
|
||||
"<label>MQTT Broker</label>"
|
||||
"<input type='text' name='broker' value='%BROKER%' maxlength='63' required>"
|
||||
"<label>MQTT Port</label>"
|
||||
"<input type='number' name='port' value='%PORT%' min='1' max='65535'>"
|
||||
"<label>MQTT Benutzer</label>"
|
||||
"<input type='text' name='user' value='%USER%' maxlength='31'>"
|
||||
"<label>MQTT Passwort</label>"
|
||||
"<input type='password' name='password' value='%PASSWORD%' maxlength='31'>"
|
||||
"<label>Gratiszeit: <span id='gv'>%GRATIS%</span> s</label>"
|
||||
"<label>Gratiszeit: <span id='gv'>%GRATIS%</span> Sekunden</label>"
|
||||
"<input type='range' name='gratis' min='0' max='120' value='%GRATIS%'"
|
||||
" oninput=\"document.getElementById('gv').textContent=this.value\">"
|
||||
"<label>Signal-Polarität</label>"
|
||||
|
|
@ -444,37 +497,59 @@ String WebServerManager::buildConfigPage() {
|
|||
"<label class='radio-label'>"
|
||||
"<input type='radio' name='polarity' value='1' %POL_HIGH%>"
|
||||
"HIGH-aktiv</label>"
|
||||
"<hr style='margin-top:1.5rem'>"
|
||||
"<h2 style='font-size:1.1rem'>Web-Zugang</h2>"
|
||||
|
||||
"<h2>MQTT Broker</h2>"
|
||||
"<label>Broker</label>"
|
||||
"<input type='text' name='broker' value='%BROKER%' maxlength='63' required>"
|
||||
"<label>Port <small style='color:#888'>(8883 = TLS)</small></label>"
|
||||
"<input type='number' name='port' value='%PORT%' min='1' max='65535'>"
|
||||
"<label>Benutzer</label>"
|
||||
"<input type='text' name='user' value='%USER%' maxlength='31'>"
|
||||
"<label>Passwort</label>"
|
||||
"<input type='password' name='password' value='%PASSWORD%' maxlength='31'>"
|
||||
|
||||
"<h2>Webzugang</h2>"
|
||||
"<label>Benutzername</label>"
|
||||
"<input type='text' name='web_user' value='%WEB_USER%' maxlength='31'>"
|
||||
"<label>Passwort <small style='color:#888'>(leer lassen = unverändert)</small></label>"
|
||||
"<input type='password' name='web_password' value='' maxlength='63' "
|
||||
"placeholder='Neues Passwort eingeben oder leer lassen'>"
|
||||
"<label class='radio-label' style='margin-top:0.4rem'>"
|
||||
"<input type='checkbox' name='clear_auth' value='1'> "
|
||||
"placeholder='Neues Passwort oder leer lassen'>"
|
||||
"<label class='radio-label' style='margin-top:.4rem'>"
|
||||
"<input type='checkbox' name='clear_auth' value='1'>"
|
||||
"Passwortschutz deaktivieren (Passwort löschen)</label>"
|
||||
"<br>"
|
||||
"<div style='display:flex;flex-direction:column;gap:.6rem;margin-top:1.5rem'>"
|
||||
"<button class='btn btn-primary' type='submit'>Speichern & Zurück</button>"
|
||||
|
||||
"<button class='btn btn-blue' type='submit' style='margin-top:1.5rem'>"
|
||||
"Speichern & Zurück</button>"
|
||||
"</form>"
|
||||
"<a href='/' style='display:block'><button class='btn btn-back' type='button'>Abbrechen</button></a>"
|
||||
"</div>"
|
||||
"<hr style='margin-top:1.5rem'>"
|
||||
"<h2 style='font-size:1.1rem;color:#c53030'>WLAN ändern</h2>"
|
||||
"<p style='font-size:.9rem;color:#555'>Löscht die gespeicherten WLAN-Daten und startet das Gerät neu.<br>"
|
||||
"Danach mit <strong>LaserCutter-Setup</strong> verbinden und neues WLAN eingeben.</p>"
|
||||
|
||||
"<h2 style='color:#c53030'>WLAN</h2>"
|
||||
"<p style='font-size:.9rem;color:#555'>Löscht gespeicherte WLAN-Daten und startet neu.<br>"
|
||||
"Danach mit <strong>LaserCutter-Setup</strong> verbinden.</p>"
|
||||
"<form action='/wifi-reset' method='post' "
|
||||
"onsubmit=\"return confirm('WLAN-Daten wirklich l\u00f6schen und neu starten?')\">"
|
||||
"<button class='btn' style='background:#c53030;color:#fff;width:100%' type='submit'>"
|
||||
"WLAN neu konfigurieren</button>"
|
||||
"<button class='btn btn-red' type='submit'>WLAN neu konfigurieren</button>"
|
||||
"</form>"
|
||||
|
||||
"<h2>Gerät</h2>"
|
||||
"<p style='font-size:.9rem;color:#555'>Der ESP32 wird sofort neu gestartet. "
|
||||
"Eine laufende Laser-Session wird unterbrochen und zurückgesetzt.</p>"
|
||||
"<form action='/reboot' method='post' "
|
||||
"onsubmit=\"return confirm('ESP32 wirklich jetzt neu starten?')\">"
|
||||
"<button class='btn btn-gray' type='submit' %REBOOT_DISABLED%>"
|
||||
"🔄 ESP32 neu starten</button>"
|
||||
"</form>"
|
||||
|
||||
"<h2>Tools</h2>"
|
||||
"<a class='btn btn-gray' href='/update'>OTA Update</a>"
|
||||
"<a class='btn btn-gray' href='/log'>Log Console</a>"
|
||||
|
||||
"<footer style='margin-top:2rem;text-align:center;font-size:.8rem;color:#aaa'>"
|
||||
"v" FIRMWARE_VERSION " (" __DATE__ ")"
|
||||
"</footer>"
|
||||
"</body></html>"
|
||||
);
|
||||
|
||||
html.replace("%TOTAL%", String(totalBuf));
|
||||
html.replace("%BROKER%", String(s.mqttBroker));
|
||||
html.replace("%PORT%", String(s.mqttPort));
|
||||
html.replace("%USER%", String(s.mqttUser));
|
||||
|
|
@ -483,6 +558,8 @@ String WebServerManager::buildConfigPage() {
|
|||
html.replace("%POL_LOW%", s.signalPolarity == 0 ? "checked" : "");
|
||||
html.replace("%POL_HIGH%", s.signalPolarity == 1 ? "checked" : "");
|
||||
html.replace("%WEB_USER%", String(s.webUser));
|
||||
html.replace("%REBOOT_DISABLED%", laserTracker.isActive() ?
|
||||
"disabled title='Laser aktiv \u2013 bitte erst beenden'" : "");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user