// ============================================================================= // web_server.cpp – HTTP-Webinterface für Statusanzeige und Konfiguration // Projekt: MQTT-Display LaserCutter // ============================================================================= #include "web_server.h" #include // voller Include nur in .cpp (PIMPL) – enthält AsyncWebSocket #include // ESP32 built-in, kein lib_deps-Eintrag noetig #include "config.h" #include "settings.h" #include "laser_tracker.h" #include "mqtt_client.h" // wifi_connector.h wird hier NICHT eingebunden (Konflikt WiFiManager vs. ESPAsyncWebServer) // stattdessen freie Funktion aus wifi_connector.cpp: extern void wifiResetCredentialsAndRestart(); #include #include // Globale Instanz WebServerManager webServer; // ----------------------------------------------------------------------------- // Datei-statischer WebSocket-Pointer für webLogForward() (gesetzt in begin()) // ----------------------------------------------------------------------------- static AsyncWebSocket* _wsPtr = nullptr; // Ring-Buffer für Web-Log-Console. // webLogForward() läuft auf Core 1 (Arduino loop) – kein Task, kein Queue, // kein Core-Problem. Browser pollt GET /log-data alle 2 s. static const size_t LOG_BUF_MAX = 6000; static String _logBuf = ""; void webLogForward(const char* msg) { _logBuf += msg; _logBuf += '\n'; if (_logBuf.length() > LOG_BUF_MAX) { _logBuf = _logBuf.substring(_logBuf.length() - LOG_BUF_MAX / 2); } } // ----------------------------------------------------------------------------- // Hilfsmakro: HTML-Template-Platzhalter ersetzen // ----------------------------------------------------------------------------- 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 ------------------------------------------------- _server->on("/", HTTP_GET, [this](AsyncWebServerRequest* request) { if (!requireAuth(request)) return; 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", "" "" "" "

✓ Gespeichert

" "

Einstellungen wurden in NVS geschrieben. " "MQTT-Änderungen werden nach einem Neustart aktiv.

" "

Weiterleitung in 3 s …

" ""); }); // --- 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", "" "" "" "

✓ Session beendet

" "

Weiterleitung …

" ""); }); // --- 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", "" "" "" "

✓ WLAN-Daten gelöscht

" "

Das Gerät startet neu.
" "Verbinde dich dann mit dem WLAN LaserCutter-Setup " "und öffne http://192.168.4.1 zur Konfiguration.

" ""); // 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(); String mqttStatus = mqttClient.isConnected() ? "✓ verbunden" : "✗ getrennt"; 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()); String html = F( "" "" "" "LaserCutter Display" "" "" "

LaserCutter Display

" "" "" "" "" "" "" "" "" "" "" "" "
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
Firmwarev" FIRMWARE_VERSION " (" __DATE__ ")
" "
" "
" "" "
" "" "" "" "
" "" "" ); 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)); return html; } // ----------------------------------------------------------------------------- // buildLogPage() // ----------------------------------------------------------------------------- String WebServerManager::buildLogPage() { return String(F( "" "" "" "Log Console – LaserCutter Display" "" "" "

💿 Log Console

" "
" "" "" "" "Lade…" "
" "
" "" )); } // ----------------------------------------------------------------------------- // buildConfigPage() // ----------------------------------------------------------------------------- String WebServerManager::buildConfigPage() { const Settings& s = settings.get(); String html = F( "" "" "" "Konfiguration – LaserCutter Display" "" "" "

Konfiguration

" "
" "" "" "" "" "" "" "" "" "" "" "" "" "" "
" "

Web-Zugang

" "" "" "" "" "" "
" "
" "" "" "" "
" "
" "

WLAN ändern

" "

Löscht die gespeicherten WLAN-Daten und startet das Gerät neu.
" "Danach mit LaserCutter-Setup verbinden und neues WLAN eingeben.

" "
" "" "
" "" "" ); 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)); return html; }