MQTT-Display-LaserCutter/src/web_server.cpp

253 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =============================================================================
// 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>&#10003; Gespeichert</h2>"
"<p>Einstellungen wurden in NVS geschrieben. "
"MQTT-&Auml;nderungen werden nach einem <strong>Neustart</strong> aktiv.</p>"
"<p>Weiterleitung in 3 s &hellip;</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>&#10003; Session gel&ouml;scht</h2>"
"<p>Weiterleitung &hellip;</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'>&#10003; verbunden</span>"
: "<span style='color:red'>&#10007; 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&uuml;cksetzen</button>"
"</form>"
"&nbsp;&nbsp;"
"<a href='/config'><button class='btn btn-primary' type='button'>Konfiguration</button></a>"
"&nbsp;&nbsp;"
"<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 &ndash; 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&auml;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 &amp; Zur&uuml;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;
}