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
566 lines
26 KiB
C++
566 lines
26 KiB
C++
// =============================================================================
|
||
// web_server.cpp – HTTP-Webinterface für Statusanzeige und Konfiguration
|
||
// Projekt: MQTT-Display LaserCutter
|
||
// =============================================================================
|
||
|
||
#include "web_server.h"
|
||
#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"
|
||
#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();
|
||
#include <ElegantOTA.h>
|
||
#include <WiFi.h>
|
||
|
||
// 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;
|
||
// Fester BSS-Puffer – keinerlei Heap-Verwendung (kein Arduino-String!)
|
||
static char _logBuf[LOG_BUF_MAX + 1] = {0};
|
||
static size_t _logBufLen = 0;
|
||
|
||
void webLogForward(const char* msg) {
|
||
size_t msgLen = strlen(msg);
|
||
size_t addLen = msgLen + 1; // +1 für '\n'
|
||
// Wenn Nachricht nicht reinpasst: hintere Hälfte per memmove behalten
|
||
if (_logBufLen + addLen >= LOG_BUF_MAX) {
|
||
size_t keep = LOG_BUF_MAX / 2;
|
||
if (keep < _logBufLen) {
|
||
memmove(_logBuf, _logBuf + (_logBufLen - keep), keep);
|
||
_logBufLen = keep;
|
||
} else {
|
||
_logBufLen = 0;
|
||
}
|
||
_logBuf[_logBufLen] = '\0';
|
||
}
|
||
if (_logBufLen + addLen < LOG_BUF_MAX) {
|
||
memcpy(_logBuf + _logBufLen, msg, msgLen);
|
||
_logBufLen += msgLen;
|
||
_logBuf[_logBufLen++] = '\n';
|
||
_logBuf[_logBufLen] = '\0';
|
||
}
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// Hilfsmakro: HTML-Template-Platzhalter ersetzen
|
||
// -----------------------------------------------------------------------------
|
||
static String htmlReplace(String html, const String& key, const String& value) {
|
||
html.replace(key, value);
|
||
return html;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// Konstruktor
|
||
// -----------------------------------------------------------------------------
|
||
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();
|
||
if (s.webPassword[0] != '\0') {
|
||
ElegantOTA.setAuth(s.webUser, s.webPassword);
|
||
}
|
||
ElegantOTA.begin(_server);
|
||
_server->begin();
|
||
|
||
// ArduinoOTA – erscheint als Netzwerk-Port in Arduino IDE und PlatformIO
|
||
const Settings& cfg = settings.get();
|
||
ArduinoOTA.setHostname("lasercutter-display");
|
||
if (cfg.webPassword[0] != '\0') {
|
||
ArduinoOTA.setPassword(cfg.webPassword); // gleiche Auth wie Webinterface
|
||
}
|
||
ArduinoOTA.onStart([]() { LOG_I("OTA", "Start OTA-Update..."); });
|
||
ArduinoOTA.onEnd([]() { LOG_I("OTA", "OTA fertig – Neustart"); });
|
||
ArduinoOTA.onError([](ota_error_t e) { LOG_E("OTA", "Fehler [%u]", e); });
|
||
ArduinoOTA.begin();
|
||
LOG_I("WEB", "ArduinoOTA aktiv: lasercutter-display.local");
|
||
|
||
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 + WebSocket-Clients bereinigen (in main loop() aufrufen)
|
||
// -----------------------------------------------------------------------------
|
||
void WebServerManager::loop() {
|
||
ArduinoOTA.handle();
|
||
_ws->cleanupClients(); // abgemeldete Clients freigeben (verhindert Memory-Leak)
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// requireAuth() – HTTP Basic Auth pruefen (nur wenn Passwort konfiguriert)
|
||
// -----------------------------------------------------------------------------
|
||
bool WebServerManager::requireAuth(AsyncWebServerRequest* request) {
|
||
const Settings& s = settings.get();
|
||
if (s.webPassword[0] == '\0') return true; // kein Passwort = kein Schutz
|
||
if (!request->authenticate(s.webUser, s.webPassword)) {
|
||
request->requestAuthentication("LaserCutter Display", false); // false = Basic Auth
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// registerRoutes()
|
||
// -----------------------------------------------------------------------------
|
||
void WebServerManager::registerRoutes() {
|
||
|
||
// --- GET / – Statusseite (öffentlich, kein Auth) -------------------------
|
||
_server->on("/", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
||
request->send(200, "text/html; charset=utf-8", buildStatusPage());
|
||
});
|
||
|
||
// --- GET /config – Konfigurationsformular --------------------------------
|
||
_server->on("/config", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
||
if (!requireAuth(request)) return;
|
||
request->send(200, "text/html; charset=utf-8", buildConfigPage());
|
||
});
|
||
|
||
// --- POST /config – Konfiguration speichern ------------------------------
|
||
_server->on("/config", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||
if (!requireAuth(request)) return;
|
||
const Settings& s = settings.get();
|
||
|
||
// MQTT
|
||
const char* broker = s.mqttBroker;
|
||
uint16_t port = s.mqttPort;
|
||
const char* user = s.mqttUser;
|
||
const char* pass = s.mqttPassword;
|
||
|
||
if (request->hasParam("broker", true))
|
||
broker = request->getParam("broker", true)->value().c_str();
|
||
if (request->hasParam("port", true))
|
||
port = (uint16_t)request->getParam("port", true)->value().toInt();
|
||
if (request->hasParam("user", true))
|
||
user = request->getParam("user", true)->value().c_str();
|
||
if (request->hasParam("password", true))
|
||
pass = request->getParam("password", true)->value().c_str();
|
||
|
||
settings.saveMqttConfig(broker, port, user, pass);
|
||
|
||
// Gratiszeit
|
||
if (request->hasParam("gratis", true)) {
|
||
uint8_t g = (uint8_t)request->getParam("gratis", true)->value().toInt();
|
||
settings.saveGratisSeconds(g);
|
||
}
|
||
|
||
// Signal-Polarität
|
||
if (request->hasParam("polarity", true)) {
|
||
uint8_t pol = (uint8_t)request->getParam("polarity", true)->value().toInt();
|
||
settings.saveSignalPolarity(pol);
|
||
}
|
||
|
||
// Webinterface-Zugangsdaten
|
||
if (request->hasParam("web_user", true)) {
|
||
const char* wUser = request->getParam("web_user", true)->value().c_str();
|
||
bool clearAuth = request->hasParam("clear_auth", true) &&
|
||
request->getParam("clear_auth", true)->value() == "1";
|
||
if (clearAuth) {
|
||
// Passwortschutz explizit deaktivieren
|
||
settings.saveWebCredentials(wUser, "");
|
||
} else {
|
||
// Passwortfeld leer => bestehendes Passwort unveraendert lassen
|
||
String wPassStr = request->hasParam("web_password", true)
|
||
? request->getParam("web_password", true)->value()
|
||
: String("");
|
||
const char* effectivePass = (wPassStr.length() > 0)
|
||
? wPassStr.c_str()
|
||
: settings.get().webPassword;
|
||
settings.saveWebCredentials(wUser, effectivePass);
|
||
}
|
||
}
|
||
|
||
LOG_I("WEB", "Konfiguration gespeichert – Neustart empfohlen");
|
||
|
||
// Weiterleitung mit Hinweis
|
||
request->send(200, "text/html; charset=utf-8",
|
||
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
||
"<meta http-equiv='refresh' content='3;url=/'></head>"
|
||
"<body style='font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem'>"
|
||
"<h2>✓ Gespeichert</h2>"
|
||
"<p>Einstellungen wurden in NVS geschrieben. "
|
||
"MQTT-Änderungen werden nach einem <strong>Neustart</strong> aktiv.</p>"
|
||
"<p>Weiterleitung in 3 s …</p>"
|
||
"</body></html>");
|
||
});
|
||
|
||
// --- POST /reset – Session zurücksetzen ---------------------------------
|
||
_server->on("/reset", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||
if (!requireAuth(request)) return;
|
||
laserTracker.resetSessionSum();
|
||
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>"
|
||
"<body style='font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem'>"
|
||
"<h2>✓ Session beendet</h2>"
|
||
"<p>Weiterleitung …</p>"
|
||
"</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;
|
||
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>"
|
||
"<body style='font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem'>"
|
||
"<h2>✓ WLAN-Daten gelöscht</h2>"
|
||
"<p>Das Gerät startet neu.<br>"
|
||
"Verbinde dich dann mit dem WLAN <strong>LaserCutter-Setup</strong> "
|
||
"und öffne <strong>http://192.168.4.1</strong> zur Konfiguration.</p>"
|
||
"</body></html>");
|
||
// Neustart nach kurzer Pause (Response muss zuerst gesendet werden)
|
||
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");
|
||
});
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// buildStatusPage()
|
||
// -----------------------------------------------------------------------------
|
||
String WebServerManager::buildStatusPage() {
|
||
const Settings& s = settings.get();
|
||
|
||
// 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);
|
||
|
||
// 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>Laser Cutter Status</title>"
|
||
"<style>"
|
||
"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>Laser Cutter Status</h1>"
|
||
"<table>"
|
||
"<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>"
|
||
"<form action='/reset' method='post' "
|
||
"onsubmit=\"return confirm('Summe Laserzeit wirklich zur\\u00fccksetzen?')\">"
|
||
"<button class='btn btn-blue' type='submit'>Summe Laserzeit zurücksetzen</button>"
|
||
"</form>"
|
||
"<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(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;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// 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 – 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>💿 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>← Status</button></a>"
|
||
"<span id='status'>Lade…</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()
|
||
// -----------------------------------------------------------------------------
|
||
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>Laser Cutter Setup & Status</title>"
|
||
"<style>"
|
||
"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 .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:.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>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>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>"
|
||
"<label class='radio-label'>"
|
||
"<input type='radio' name='polarity' value='0' %POL_LOW%>"
|
||
"LOW-aktiv (Schalter nach GND, Standard)</label>"
|
||
"<label class='radio-label'>"
|
||
"<input type='radio' name='polarity' value='1' %POL_HIGH%>"
|
||
"HIGH-aktiv</label>"
|
||
|
||
"<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 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>"
|
||
|
||
"<button class='btn btn-blue' type='submit' style='margin-top:1.5rem'>"
|
||
"Speichern & Zurück</button>"
|
||
"</form>"
|
||
|
||
"<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 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));
|
||
html.replace("%PASSWORD%", String(s.mqttPassword));
|
||
html.replace("%GRATIS%", String(s.gratisSeconds));
|
||
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;
|
||
}
|