253 lines
11 KiB
C++
253 lines
11 KiB
C++
// =============================================================================
|
||
// web_server.cpp – HTTP-Webinterface für Statusanzeige und Konfiguration
|
||
// Projekt: MQTT-Display LaserCutter
|
||
// =============================================================================
|
||
|
||
#include "web_server.h"
|
||
#include "settings.h"
|
||
#include "laser_tracker.h"
|
||
#include "mqtt_client.h"
|
||
#include <ElegantOTA.h>
|
||
#include <WiFi.h>
|
||
|
||
// Globale Instanz
|
||
WebServerManager webServer;
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// 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(80) {}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// begin() – Routen registrieren und Server starten
|
||
// -----------------------------------------------------------------------------
|
||
void WebServerManager::begin() {
|
||
registerRoutes();
|
||
ElegantOTA.begin(&_server);
|
||
_server.begin();
|
||
Serial.println(F("[WebServer] gestartet auf Port 80"));
|
||
Serial.print(F("[WebServer] URL: http://"));
|
||
Serial.println(WiFi.localIP());
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// registerRoutes()
|
||
// -----------------------------------------------------------------------------
|
||
void WebServerManager::registerRoutes() {
|
||
|
||
// --- GET / – Statusseite -------------------------------------------------
|
||
_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) {
|
||
request->send(200, "text/html; charset=utf-8", buildConfigPage());
|
||
});
|
||
|
||
// --- POST /config – Konfiguration speichern ------------------------------
|
||
_server.on("/config", HTTP_POST, [](AsyncWebServerRequest* request) {
|
||
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);
|
||
}
|
||
|
||
Serial.println(F("[WebServer] 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, [](AsyncWebServerRequest* request) {
|
||
laserTracker.resetSession();
|
||
Serial.println(F("[WebServer] 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 gelöscht</h2>"
|
||
"<p>Weiterleitung …</p>"
|
||
"</body></html>");
|
||
});
|
||
|
||
// 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()
|
||
? "<span style='color:green'>✓ verbunden</span>"
|
||
: "<span style='color:red'>✗ getrennt</span>";
|
||
|
||
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());
|
||
|
||
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>"
|
||
"<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-danger{background:#e53e3e;color:#fff}"
|
||
".btn-primary{background:#3182ce;color:#fff}"
|
||
"</style></head>"
|
||
"<body>"
|
||
"<h1>LaserCutter Display</h1>"
|
||
"<table>"
|
||
"<tr><td>Summe Session</td><td>%ALLSESSIONS% min</td></tr>"
|
||
"<tr><td>Maschinenlaufzeit (gesamt)</td><td>%TOTAL% min</td></tr>"
|
||
"<tr><td>Letzte Laserzeit</td><td>%LASTSESSION% s</td></tr>"
|
||
"<tr><td>Laser</td><td>%LASER%</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>"
|
||
"</table>"
|
||
"<form action='/reset' method='post' "
|
||
"onsubmit=\"return confirm('Session wirklich zur\\u00fccksetzen?')\">"
|
||
"<button class='btn btn-danger' type='submit'>Session zurücksetzen</button>"
|
||
"</form>"
|
||
" "
|
||
"<a href='/config'><button class='btn btn-primary' type='button'>Konfiguration</button></a>"
|
||
" "
|
||
"<a href='/update'><button class='btn btn-primary' type='button'>OTA Update</button></a>"
|
||
"<script>setTimeout(()=>location.reload(),10000)</script>"
|
||
"</body></html>"
|
||
);
|
||
|
||
html.replace("%ALLSESSIONS%", String(laserTracker.getAllSessionsSumMinutes()));
|
||
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;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// buildConfigPage()
|
||
// -----------------------------------------------------------------------------
|
||
String WebServerManager::buildConfigPage() {
|
||
const Settings& s = settings.get();
|
||
|
||
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>"
|
||
"<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}"
|
||
"input[type=text],input[type=number],input[type=password]"
|
||
"{width:100%;padding:.4rem;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}"
|
||
".btn-primary{background:#3182ce;color:#fff;margin-top:1.5rem}"
|
||
".btn-back{background:#718096;color:#fff}"
|
||
"</style></head>"
|
||
"<body>"
|
||
"<h1>Konfiguration</h1>"
|
||
"<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>"
|
||
"<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>"
|
||
"<br>"
|
||
"<button class='btn btn-primary' type='submit'>Speichern & Zurück</button>"
|
||
"</form><br>"
|
||
"<a href='/'><button class='btn btn-back' type='button'>Abbrechen</button></a>"
|
||
"</body></html>"
|
||
);
|
||
|
||
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" : "");
|
||
|
||
return html;
|
||
}
|