- Implementation-Plan.md: Phase 10 alle Tasks als erledigt markiert - README.md: Implementierungsstand-Tabelle aktualisiert (Phase 8-10 abgeschlossen)
20 KiB
Implementation Plan – Laser Cutter MQTT Display
Dieses Dokument beschreibt die Entwicklungsreihenfolge des Projekts.
Tasks werden während der Entwicklung mit[x]abgehakt.
Phase 1 – Projekt-Setup & Konfiguration
-
1.1
platformio.inimit allen benötigten Bibliotheken erweiternMD_Parola,MD_MAX72XXPubSubClientWiFiManager(tzapu/WiFiManager)ESPAsyncWebServer+AsyncTCPArduinoJsonElegantOTAPreferences(ESP32 built-in, kein Extra-Eintrag nötig)
-
1.2
include/config.herstellen – zentrale Pin-Definitionen und Konstanten- GPIO-Pins (MAX7219 SPI, Laser-Signal)
- Anzahl der Module (8), Zonen-Aufteilung
- Standard-Werte (Gratiszeit 20s, MQTT Port 1883, etc.)
- MQTT-Topic-Konstanten
-
1.3 Build prüfen (leeres Projekt kompiliert fehlerfrei)
-
1.4 Test Verdrahtung Dot-Matrix-Display
- GYMAX7219-Module gemäß Pinbelegung angeschlossen (MOSI GPIO 23, CLK GPIO 18, CS GPIO 5)
- Hardware-Typ
GENERIC_HW(verifiziert; physische 90° Verdrehung per Software CCW kompensiert) - Externes 5 V-Netzteil erforderlich (~0,5 A / ~2,5 W gemessen; USB-Port reicht nicht)
- SPI-Taktrate auf 1 MHz reduziert für stabile Ansteuerung aller 8 Module
- Modul 1 oben-links, Modul 4 oben-rechts, Modul 5 unten-links, Modul 8 unten-rechts
- Zone 0 (0-idx 0–3) = obere Reihe, Zone 1 (0-idx 4–7) = untere Reihe
-
1.5 Test Verdrahtung Potentialfreier Schalter (Phase 1: Push Button)
- Push Button an GPIO 4 und GND angeschlossen (INPUT_PULLUP, kein externer Widerstand nötig)
- Pegel verifiziert: Button offen = HIGH, Button gedrückt = LOW → Polarität: LOW_ACTIVE
- Debounce 50 ms funktioniert
Phase 2 – NVS Persistenz (Settings)
-
2.1
include/settings.h+src/settings.cpperstellen
Speichert und lädt alle Konfigurationswerte über ESP32Preferences(NVS):- MQTT Broker IP, Port, User, Passwort
- Gratiszeit (0–120 s)
- Signal-Polarität (
LOW_ACTIVE/HIGH_ACTIVE) - Akkumulierte Laserzeit (float, Minuten)
- Globale Instanz
settings,settings.begin()inmain.cpp
-
2.2 Unit-Test (Serial-Output): Werte schreiben, ESP32 neu starten, Werte lesen und verifizieren
test_sketches/test_nvs.cpperstellt,test-nvsEnvironment inplatformio.ini- Round-Trip-Test + Persistenz-Check über Neustart
Phase 3 – WiFi & WiFiManager
-
3.1 WiFiManager einbinden
include/wifi_connector.h+src/wifi_connector.cpperstellt- AP-Name:
LaserCutter-Setup(ausconfig.h) - Captive-Portal-Seite für WLAN-Credentials
- Timeout: 120 s, danach
ESP.restart()
-
3.2 Verbindungsstatus-Callback implementieren (für spätere Fehleranzeige)
WifiStatusCallback-Typ +onStatusChange()Methode- Status-Enum:
DISCONNECTED,AP_ACTIVE,CONNECTING,CONNECTED - Globale Instanz
wifiConnector,wifiConnector.begin()+.loop()inmain.cpp
-
3.3 Test: Erstverbindung mit neuem AP, gespeichertes WLAN nach Neustart automatisch verbinden
test_sketches/test_wifi.cpperstellt,test-wifiEnvironment inplatformio.ini- BOOT-Taste (GPIO 0, 3 s) löscht Credentials und erzwingt Portal
- Erstverbindung via Captive Portal ✅, Auto-Reconnect nach Neustart ✅
Phase 4 – Dot-Matrix-Display (DisplayManager)
-
4.1
include/display_manager.h+src/display_manager.cpperstellen
Implementierung mit rohemMD_MAX72XX(nicht MD_Parola) für vollständige Rotationskontrolle:- Zone 0 (Module 0–3, obere Reihe): Laserzeit in Minuten
- Zone 1 (Module 4–7, untere Reihe): Countdown / Statusmeldungen
rotateCCW()kompensiert physische 90°-CW-Verdrehung der Module- 17 Zeichen-Bitmaps definiert (0–9, Sonderzeichen, Buchstaben)
-
4.2 Methoden implementiert:
showLaserTime(float minutes)– obere Zeile (4 Formate je Wertebereich)showCountdown(int seconds)– untere Zeile, rechtsbündig (während Gratiszeit)showIdle()–--in unterer Zeile wenn kein Countdown aktivshowStatus(const char* msg)– max. 4-Zeichen-Statusmeldung (untere Zeile)setBrightness(),allLedsOn(),allLedsOff(),clear(),printToSerial()update()– inloop()aufrufen (no-op, Interface für künftige Animationen)main.cppintegriert:display.begin(),display.showIdle(),display.update()
-
4.3 Test:
test_sketches/test_display_manager.cpp, Environmenttest-display-mgr- Alle LEDs EIN/AUS ✅
showLaserTime()alle 12 Grenzwerte (0.0 – 9999.0 min) ✅showCountdown()5→0 ✅showIdle()→--✅showStatus()mit „Err ", „AP ", „WiFi", „ oF" ✅- Realistischer Loop: Laserzeit steigt, Countdown 20→0, dann Idle ✅
Phase 5 – Laser-Signaldetektion & Zeit-Tracking (LaserTracker)
-
5.1
include/laser_tracker.h+src/laser_tracker.cpperstellt- Globale Instanz
laserTracker, inmain.cppintegriert
- Globale Instanz
-
5.2 GPIO-Eingang konfiguriert:
INPUT_PULLUPauf GPIO 4- Software-Debounce 50 ms (
LASER_DEBOUNCE_MSausconfig.h) - Polarität aus
settings.get().signalPolarity(LOW_ACTIVE / HIGH_ACTIVE)
-
5.3 Session-Logik implementiert:
onSessionStart()intern →_sessionActive = true, Timer setzenonSessionEnd()intern → Netto-Sekunden berechnen,_totalMinutesBaseaddieren, NVS viasettings.saveTotalMinutes()
-
5.4 Gratiszeit-Logik:
- Countdown läuft ab Session-Start
getSessionSeconds()gibt erst nach Ablauf der Gratiszeit > 0 zurückgetCountdownRemaining()→ verbleibende Sekunden (0 wenn abgelaufen / inaktiv)getTotalMinutes()= NVS-Basis + laufende Session-Netto-Minuten (Live-Anzeige)
-
5.5 Öffentliche Getter:
getTotalMinutes()→ float (Live)getSessionSeconds()→ int (Netto ohne Gratiszeit)getCountdownRemaining()→ int (verbleibende Gratiszeit)isActive()→ boolgetLastSessionSeconds()→ int (letzte abgeschlossene Session, für MQTT)resetTotal()→ Gesamtzeit auf 0 + NVS-Speicherung
-
5.6 Test:
test_sketches/test_laser_tracker.cpp, Environmenttest-laser-tracker- Button GPIO 4 simuliert Laser-Signal
- BOOT-Taste (GPIO 0, 3 s) löscht Gesamtzeit
- Boot-Output: NVS-Werte geladen, Display zeigt Basiswert + Idle ✅
- Manuelle Verifikation: Button drücken → Countdown, Netto-Zeit, Session-Ende → NVS
-
5.7
showSessionRing(int seconds)– Kreisanzeige auf Modulen 5–7, 12-Uhr-Start, Uhrzeigersinn (Phase-5-Erweiterung)- 60 LEDs auf 3 Modulen (je 8×8, aber nur die äußere Reihe vollständig umlaufend)
- Jede Sekunde NET_COUNTING: eine weitere LED zugeschaltet
- Nach 60 Sekunden: Kreis voll, Minutenanzeige (Module 1–3) inkrementiert
- Laser-AUS: Anzeige zurück auf
-- - Implementierung in
display_manager.h/cpp - Aufruf in
main.cppwährendNET_COUNTINGstattshowIdle()
Phase 6 – MQTT Client (MqttClient)
-
6.1
include/mqtt_client.h+src/mqtt_client.cpperstellen
Wrapper umPubSubClient -
6.2 Verbindungsaufbau mit Credentials aus
Settings(TLS automatisch bei Port 8883) -
6.3 Publish-Methode
publishSession(sessionSec, summeSession, gratisSec):- Topic:
lasercutter/session - JSON-Felder (aktualisiert):
session_minutes(int, ceiling: 62 s = 2 min)session_seconds(int, Rohwert Netto-Sekunden)freetime_s(int)ip(string)
- Kein
total_min, keinsession_start_time(NTP später) - Wird von LaserTracker-Logik in main aufgerufen
- Topic:
-
6.4 Publish-Methode
publishHeartbeat(totalMin, ipStr, uptimeSec):- Topic:
lasercutter/status(retained) - Heartbeat alle 30 Sekunden
- LWT
{"online":false}bei Verbindungsverlust - JSON-Felder (aktualisiert):
online(bool)session_sum(string, Summe Session in Minuten als string mit 2 Dezimalen)machine_running_time_min(string, Maschinenlaufzeit aus NVS)ip(string)uptime_s(int)
- Topic:
-
6.5 Subscribe auf
lasercutter/reset:- Payload
"1",{"reset":true}oder{"reset":1}→LaserTracker::resetTotal()(NVS + RAM) - Payload
{"reset_session":true}→LaserTracker::resetSessionSum()(nur RAM, NVS bleibt) - QoS 1
- Payload
-
6.6 Reconnect-Logik (Non-blocking, max. alle 10 Sekunden versuchen)
-
6.7 Session-Publish bei Reset:
consumeSessionReset()inmain.cpp→ ruftpublishSession()mit akkumulierten Sekunden auf- Identisches JSON-Format wie normales Session-Ende
Phase 7 – Webinterface (WebServer)
-
7.1
include/web_server.h+src/web_server.cpperstellen
Basierend aufESPAsyncWebServer -
7.2 Route
GET /– Statusseite (HTML):- Summe Session (Minuten, RAM), Maschinenlaufzeit gesamt (NVS)
- WLAN-Status (IP), MQTT-Status (verbunden/getrennt)
- Broker-Info, Gratiszeit
- Auto-Reload alle 10 s
-
7.3 Route
GET /config– Konfigurationsformular (HTML):- MQTT Broker IP, Port, User, Passwort
- Gratiszeit (Slider 0–120 s)
- Signal-Polarität (Radio-Button)
-
7.4 Route
POST /config– Konfiguration speichern (viaSettings, NVS)
Redirect + Hinweis „Neustart für MQTT-Änderungen" -
7.5 Route
POST /reset– Session-Summe zurücksetzen (laserTracker.resetSessionSum())- Gesamtzeit (NVS) bleibt erhalten; Wartungsreset nur per MQTT
{"reset":true}oderresetTotal() - Bug fix:
resetSessionSum()setzt laufende Session-Timer korrekt zurück (vorher:getAllSessionsSumMinutes()blieb > 0)
- Gesamtzeit (NVS) bleibt erhalten; Wartungsreset nur per MQTT
-
7.6 ElegantOTA unter
/updateeinbinden
FlagELEGANTOTA_USE_ASYNC_WEBSERVER=1inplatformio.ini -
7.7 Button-Layout der Statusseite überarbeitet:
- Alle 3 Buttons (Session zurücksetzen, Konfiguration, OTA Update) übereinander, gleich breit (
width:100%) - Alle Buttons einheitlich blau (
btn-primary),btn-dangerentfernt - Flex-Layout mit
gap:.6remfür gleichmäßigen Abstand
- Alle 3 Buttons (Session zurücksetzen, Konfiguration, OTA Update) übereinander, gleich breit (
-
7.8 Button-Layout der Konfigurationsseite vereinheitlicht:
Speichern & ZurückundAbbrechennun gleich breit (width:100%über.btn-Klasse)- Identisches Flex-Spalten-Layout wie Statusseite (
display:flex;flex-direction:column;gap:.6rem;margin-top:1.5rem) - Farben beibehalten: Blau (
#3182ce) = Speichern, Grau (#718096) = Abbrechen
Phase 8 – Integration & Hauptprogramm (main.cpp)
-
8.1
main.cppaufräumen und alle Module initialisieren:- Reihenfolge:
Settings→DisplayManager→LaserTracker→WiFiConnector(non-blocking) → Watchdog - MQTT + WebServer: Lazy-Init im
loop()beim ersten WiFi-Connect - Änderung gegenüber ursprünglicher Planung: LaserTracker startet vor WiFi (Priorität 1)
- Reihenfolge:
-
8.2
loop()implementiert (non-blocking, keindelay()außer 20 ms am Ende):- Priorität 1:
laserTracker.loop()(immer zuerst) - Priorität 2:
wifiConnector.loop()(non-blocking) - Lazy-Init: MQTT + WebServer beim ersten
wifiConnector.isConnected() - MQTT:
mqttClient.loop(), Session-Publish nachconsumeSessionEnd/Reset() - Display rate-limited: W 500 ms-Blinker, Minuten-Update bei Änderung oder alle 60 s, Sekunden-Update alle 1 s, M nur bei Statuswechsel
- Priorität 1:
-
8.3 Callback von
LaserTracker→MqttClient.publishSession(...)verdrahtet -
8.4 WLAN/MQTT-Fehlerzustand auf Display gespiegelt (rate-limited)
-
8.5 Watchdog Timer aktivieren (ESP32 WDT, 30 s)
Phase 9 – OTA & Stabilisierung ✅ abgeschlossen
-
9.1 ElegantOTA via
web_server.cppeingebunden (kein separaterloop()-Aufruf nötig)ELEGANTOTA_USE_ASYNC_WEBSERVER=1→ vollständig async, keinElegantOTA.loop()inmain.cpp- Bugfix: Include-Konflikt
ESPAsyncWebServer↔WiFiManager(HTTP_GET/POST-Enum-Clash) gelöst via PIMPL-Pattern:web_server.hnur Forward-DeclarationAsyncWebServer*, vollständiger#include <ESPAsyncWebServer.h>ausschließlich inweb_server.cpp
-
9.2 Serielles Logging vereinheitlichen:
LOG_D-Makro inconfig.hergänzt- Aktiv wenn
CORE_DEBUG_LEVEL >= 3, sonst no-op
- Aktiv wenn
-
9.3 Speicherleck-Monitoring:
ESP.getFreeHeap()alle 30 s viaLOG_Dinloop() -
9.4 NTP-Zeitsynchronisation +
session_start_timeim MQTT-Session-PayloadconfigTime(0, 0, "pool.ntp.org", "time.nist.gov")inwifi_connector.cppnach WLAN-Connect (auch Reconnect)waitForNtp()blockiert max. 5 s bis Sync bestätigt (getLocalTime()), loggt Ergebnis viaLOG_ILaserTracker::onSessionStart()speicherttime(nullptr)→_sessionStartTimepublishSession()formatiert UTC-Zeitstempel als ISO-8601 (strftime,gmtime)- Fallback
"unknown"wenn NTP beim Session-Start noch nicht synchronisiert - Verifiziert:
{"session_start_time":"2026-02-23T...Z"}im MQTT-Payload bestätigt
-
9.5 Edge-Cases manuell getestet (Integrations-Hardware-Test):
- Laser aktiv, WLAN getrennt → Display läuft weiter ✅
- MQTT-Broker nicht erreichbar → Reconnect ✅
- Reset während aktiver Session ✅
- Neustart nach akkumulierter Zeit → Zeit korrekt aus NVS geladen ✅
-
9.6 Webinterface mit HTTP-Basic-Auth abgesichert
webUser+webPasswordinSettings-Struct + NVS-Keys inconfig.hsaveWebCredentials()inSettingsManagerWebServerManager::requireAuth()– prüft Auth, sendet 401 wenn nicht autorisiert- Standard:
webUser = "admin",webPassword = ""(leer = kein Schutz, rückwärtskompatibel) - Alle Routen (
/,/config,/reset) + ElegantOTA (ElegantOTA.setAuth()) geschützt - Passwort änderbar über
/config-Formular (neuer Abschnitt "Web-Zugang") - Forward-Declaration
AsyncWebServerRequestinweb_server.hergänzt (Fix Compile-Fehler) - Bug fix:
requestAuthentication()sendet standardmäßig Digest Auth; überschrieben mitfalsefür Basic Auth - Bug fix:
webPassword[32]zuwebPassword[64]vergrößert (Passwort-Abschneiden bei 31 Zeichen) - Bug fix: POST
/configüberschrieb Passwort immer mit leerem String; leeres Feld = unveraendert lassen;clear_auth-Checkbox für explizites Löschen
-
9.7
resetSession()umbenannt inresetSessionSum()(klarere Semantik)_sessionResetPendingFlag +consumeSessionReset()nachconsumeSessionEnd()-Muster- Vor Reset: akkumulierte Netto-Sekunden in
_lastSessionSecgesichert main.cpp:consumeSessionReset()→publishSession()mit gesicherten Sekunden- MQTT
{"reset_session":true}löstresetSessionSum()aus - Display zeigt nach Reset sofort 0 (alle Zeitakkumulatoren zurückgesetzt)
-
9.8 WiFi-Architektur: vollständig non-blocking (Proposal B+D)
- Problem:
wifiConnector.begin()blockierte bis zu 2 Minuten → LaserTracker startete nie WiFiManagerals Pointer (WiFiManager*), lazynewinbegin()(verhindert Crash durch globale Konstruktoren)WiFi.mode(WIFI_STA)vor Credentials-Prüfung;_wm->getWiFiSSID(true)stattWiFi.SSID()(erfordert gestarteten Stack)- Credentials vorhanden:
WiFi.begin()non-blocking, Ergebnis inloop()geprüft - Keine/falsche Credentials:
setConfigPortalBlocking(false)+setConnectTimeout(1)→ Portal sofort, Verarbeitung via_wm->process()inloop() - Nach 3 Fehlversuchen (je 15 s):
startConfigPortal()→ User kann neue Credentials eingeben onConnect():_wm->stopWebPortal()vor AsyncWebServer-Start (verhindert Port-80-Konflikt)- MQTT + WebServer: Lazy-Init erst bei erstem WiFi-Connect
- Display: rate-limited (W 500 ms-Blinker, Minuten bei Änderung/60 s, Sekunden 1 s, M bei Statuswechsel)
- Verifiziert: LaserTracker läuft sofort nach Neustart, unabhängig vom WiFi-Status ✅
- Problem:
-
9.9 ArduinoOTA Integration + platformio.ini Refaktorierung
ArduinoOTAinweb_server.cppintegriert (ESP32 built-in, kein lib_deps-Eintrag nötig)- Hostname:
lasercutter-display→ erscheint in Arduino IDE als Netzwerk-Portlasercutter-display at <IP> - Passwort:
ArduinoOTA.setPassword(cfg.webPassword)– gleiche Auth wie Webinterface, kein separates OTA-Passwort WebServerManager::loop()hinzugefügt →ArduinoOTA.handle()wird inmain.cppviawebServer.loop()aufgerufenplatformio.iniauf gemeinsamen[env]-Basisblock umgestellt (lib_deps + build_flags einmalig, alle Environments erben)- Neues Environment
az-delivery-devkit-v4-ota:upload_protocol = espota, IP konfigurierbar - Neues Environment
az-delivery-devkit-v4-ota-http: ElegantOTA HTTP-Fallback viaupload_ota.pyextra_script upload_flags = --auth=...: Kommentar + Anleitung für Passwort-Übergabe an espota (inkl. Umgebungsvariable-Option)- OTA-Passwort-Authentifizierung verifiziert: Upload via
pio run -e az-delivery-devkit-v4-ota --target uploaderfolgreich ✅ - README.md: Abschnitt "Via WiFi (OTA)" mit vollständiger Anleitung ergänzt
Phase 10 – Dokumentation & Abschluss ✅ abgeschlossen
-
10.1 README.md finalisiert
- Alle Abschnitte vollständig: Hardware, Pinbelegung, Display, Laser-Signaldetektion, Zeit-Tracking, MQTT, Webinterface, Konfiguration, WiFi-Setup, OTA, Fehlerverhalten, Bibliotheken, Build & Flash
- Screenshots eingefügt (Hauptansicht, Konfiguration, OTA-Seite)
- Build & Flash: USB und Via WiFi (OTA) dokumentiert inkl.
upload_flags --authAnleitung - Projektstruktur, Implementierungsstand-Tabelle, Commit-Konventionen dokumentiert
-
10.2
platformio.inimit finalem[env]-Basisblock konsolidiert- Alle lib_deps einmalig im gemeinsamen Block, keine Duplizierung über Environments
- Alle Environments (
USB,OTA-espota,OTA-HTTP, Test-Environments) vollständig
-
10.3 MQTT-Topics verifiziert (realer Hardware-Test)
lasercutter/sessionnach Laser-AUS ✅lasercutter/statusHeartbeat alle 60 s ✅lasercutter/resetPayload{"reset":true}und{"reset_session":true}✅- TLS (Port 8883) automatisch aktiv wenn konfiguriert ✅
-
10.4 Schaltbild in README.md als ASCII-Diagramm dokumentiert
- SPI-Ketten-Schaltbild (8 Module, 4×2-Anordnung) vollständig in README.md
Abhängigkeitsdiagramm (Reihenfolge)
Phase 1 (Setup)
└── Phase 2 (NVS/Settings)
├── Phase 3 (WiFi)
├── Phase 4 (Display)
└── Phase 5 (LaserTracker)
└── Phase 6 (MQTT)
└── Phase 7 (Webinterface)
└── Phase 8 (Integration)
└── Phase 9 (OTA/Stabilisierung)
└── Phase 10 (Doku)
Verwendete MQTT-Topics (Überblick)
| Topic | Richtung | QoS | Auslöser |
|---|---|---|---|
lasercutter/session |
Publish | 1 | Session-Ende (Laser inaktiv) oder Session-Reset |
lasercutter/status |
Publish | 0 | Heartbeat alle 60 s |
lasercutter/reset |
Subscribe | 1 | {"reset":true} = Gesamt-Reset (NVS+RAM), {"reset_session":true} = nur Session-Summe (RAM) |
MQTT JSON-Formate
lasercutter/session
{
"session_minutes": 2,
"session_seconds": 125,
"session_start_time": "2026-02-23T12:34:56Z",
"freetime_s": 20,
"ip": "192.168.1.100"
}
lasercutter/status (retained)
{
"online": true,
"session_sum": "2.08",
"machine_running_time_min": "1234.75",
"ip": "192.168.1.100",
"uptime_s": 3600
}
Erstellt: 22. Februar 2026