# 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 - [x] **1.1** `platformio.ini` mit allen benötigten Bibliotheken erweitern - `MD_Parola`, `MD_MAX72XX` - `PubSubClient` - `WiFiManager` (tzapu/WiFiManager) - `ESPAsyncWebServer` + `AsyncTCP` - `ArduinoJson` - `ElegantOTA` - `Preferences` (ESP32 built-in, kein Extra-Eintrag nötig) - [x] **1.2** `include/config.h` erstellen – 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 - [x] **1.3** Build prüfen (leeres Projekt kompiliert fehlerfrei) - [x] **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 - [x] **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`) - [x] **2.1** `include/settings.h` + `src/settings.cpp` erstellen Speichert und lädt alle Konfigurationswerte über ESP32 `Preferences` (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()` in `main.cpp` - [x] **2.2** Unit-Test (Serial-Output): Werte schreiben, ESP32 neu starten, Werte lesen und verifizieren - `test_sketches/test_nvs.cpp` erstellt, `test-nvs` Environment in `platformio.ini` - Round-Trip-Test + Persistenz-Check über Neustart --- ## Phase 3 – WiFi & WiFiManager - [x] **3.1** WiFiManager einbinden - `include/wifi_connector.h` + `src/wifi_connector.cpp` erstellt - AP-Name: `LaserCutter-Setup` (aus `config.h`) - Captive-Portal-Seite für WLAN-Credentials - Timeout: 120 s, danach `ESP.restart()` - [x] **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()` in `main.cpp` - [x] **3.3** Test: Erstverbindung mit neuem AP, gespeichertes WLAN nach Neustart automatisch verbinden - `test_sketches/test_wifi.cpp` erstellt, `test-wifi` Environment in `platformio.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`) - [x] **4.1** `include/display_manager.h` + `src/display_manager.cpp` erstellen Implementierung mit rohem `MD_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) - [x] **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 aktiv - `showStatus(const char* msg)` – max. 4-Zeichen-Statusmeldung (untere Zeile) - `setBrightness()`, `allLedsOn()`, `allLedsOff()`, `clear()`, `printToSerial()` - `update()` – in `loop()` aufrufen (no-op, Interface für künftige Animationen) - `main.cpp` integriert: `display.begin()`, `display.showIdle()`, `display.update()` - [x] **4.3** Test: `test_sketches/test_display_manager.cpp`, Environment `test-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`) - [x] **5.1** `include/laser_tracker.h` + `src/laser_tracker.cpp` erstellt - Globale Instanz `laserTracker`, in `main.cpp` integriert - [x] **5.2** GPIO-Eingang konfiguriert: - `INPUT_PULLUP` auf GPIO 4 - Software-Debounce 50 ms (`LASER_DEBOUNCE_MS` aus `config.h`) - Polarität aus `settings.get().signalPolarity` (LOW_ACTIVE / HIGH_ACTIVE) - [x] **5.3** Session-Logik implementiert: - `onSessionStart()` intern → `_sessionActive = true`, Timer setzen - `onSessionEnd()` intern → Netto-Sekunden berechnen, `_totalMinutesBase` addieren, NVS via `settings.saveTotalMinutes()` - [x] **5.4** Gratiszeit-Logik: - Countdown läuft ab Session-Start - `getSessionSeconds()` gibt erst nach Ablauf der Gratiszeit > 0 zurück - `getCountdownRemaining()` → verbleibende Sekunden (0 wenn abgelaufen / inaktiv) - `getTotalMinutes()` = NVS-Basis + laufende Session-Netto-Minuten (Live-Anzeige) - [x] **5.5** Öffentliche Getter: - `getTotalMinutes()` → float (Live) - `getSessionSeconds()` → int (Netto ohne Gratiszeit) - `getCountdownRemaining()` → int (verbleibende Gratiszeit) - `isActive()` → bool - `getLastSessionSeconds()` → int (letzte abgeschlossene Session, für MQTT) - `resetTotal()` → Gesamtzeit auf 0 + NVS-Speicherung - [x] **5.6** Test: `test_sketches/test_laser_tracker.cpp`, Environment `test-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 - [x] **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.cpp` während `NET_COUNTING` statt `showIdle()` --- ## Phase 6 – MQTT Client (`MqttClient`) - [x] **6.1** `include/mqtt_client.h` + `src/mqtt_client.cpp` erstellen Wrapper um `PubSubClient` - [x] **6.2** Verbindungsaufbau mit Credentials aus `Settings` (TLS automatisch bei Port 8883) - [x] **6.3** Publish-Methode `publishSession(sessionSec, summeSession, gratisSec)`: - Topic: `lasercutter/session` - JSON-Felder (aktualisiert): - `session_minuten` (int, ceiling: 62 s = 2 min) - `session_seconds` (int, Rohwert Netto-Sekunden) - `gratiszeit_s` (int) - `ip` (string) - Kein `total_min`, kein `session_start_time` (NTP später) - Wird von LaserTracker-Logik in main aufgerufen - [x] **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) - [x] **6.5** Subscribe auf `lasercutter/reset`: - Payload `"1"`, `{"reset":true}` oder `{"reset":1}` → `LaserTracker::resetTotal()` - QoS 1 - [ ] **6.6** Reconnect-Logik (Non-blocking, max. alle 10 Sekunden versuchen) - [ ] **6.7** Test: Session-Daten mit MQTT Explorer empfangen, Reset auslösen und verifizieren ⚠️ Offen: MQTT-Error Modul 5 bei Verbindungsverlust, Reconnect nach WiFi-Ausfall --- ## Phase 7 – Webinterface (`WebServer`) - [x] **7.1** `include/web_server.h` + `src/web_server.cpp` erstellen Basierend auf `ESPAsyncWebServer` - [x] **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 - [x] **7.3** Route `GET /config` – Konfigurationsformular (HTML): - MQTT Broker IP, Port, User, Passwort - Gratiszeit (Slider 0–120 s) - Signal-Polarität (Radio-Button) - [x] **7.4** Route `POST /config` – Konfiguration speichern (via `Settings`, NVS) Redirect + Hinweis „Neustart für MQTT-Änderungen" - [x] **7.5** Route `POST /reset` – Session zurücksetzen (`laserTracker.resetSession()`) Gesamtzeit (NVS) bleibt erhalten; Wartungsreset nur per BOOT-Taste oder MQTT - [x] **7.6** ElegantOTA unter `/update` einbinden Flag `ELEGANTOTA_USE_ASYNC_WEBSERVER=1` in `platformio.ini` - [ ] **7.7** Test: Alle Seiten im Browser aufrufen, Konfiguration ändern und nach Neustart prüfen --- ## Phase 8 – Integration & Hauptprogramm (`main.cpp`) - [x] **8.1** `main.cpp` aufräumen und alle Module initialisieren: - Reihenfolge: `Settings` → `DisplayManager` → `WiFiManager` → `MqttClient` → `WebServer` → `LaserTracker` - [x] **8.2** `loop()` implementieren: - `displayManager.update()` - `laserTracker.update()` (GPIO-Polling + Session-Logik) - `mqttClient.loop()` (Reconnect + Heartbeat) - `displayManager.showLaserTime(laserTracker.getTotalMinutes())` - `displayManager.showCountdown(...)` oder `showIdle()` oder `showError(...)` - [x] **8.3** Callback von `LaserTracker` → `MqttClient.publishSession(...)` verdrahten - [x] **8.4** WLAN/MQTT-Fehlerzustand auf Display spiegeln - [x] **8.5** Watchdog Timer aktivieren (ESP32 WDT, 30 s) --- ## Phase 9 – OTA & Stabilisierung - [x] **9.1** ElegantOTA via `web_server.cpp` eingebunden (kein separater `loop()`-Aufruf nötig) - `ELEGANTOTA_USE_ASYNC_WEBSERVER=1` → vollständig async, kein `ElegantOTA.loop()` in `main.cpp` - **Bugfix:** Include-Konflikt `ESPAsyncWebServer` ↔ `WiFiManager` (HTTP_GET/POST-Enum-Clash) gelöst via PIMPL-Pattern: `web_server.h` nur Forward-Declaration `AsyncWebServer*`, vollständiger `#include ` ausschließlich in `web_server.cpp` - [x] **9.2** Serielles Logging vereinheitlichen: `LOG_D`-Makro in `config.h` ergänzt - Aktiv wenn `CORE_DEBUG_LEVEL >= 3`, sonst no-op - [x] **9.3** Speicherleck-Monitoring: `ESP.getFreeHeap()` alle 30 s via `LOG_D` in `loop()` - [x] **9.4** NTP-Zeitsynchronisation + `session_start_time` im MQTT-Session-Payload - `configTime(0, 0, "pool.ntp.org", "time.nist.gov")` in `wifi_connector.cpp` nach WLAN-Connect (auch Reconnect) - `waitForNtp()` blockiert max. 5 s bis Sync bestätigt (`getLocalTime()`), loggt Ergebnis via `LOG_I` - `LaserTracker::onSessionStart()` speichert `time(nullptr)` → `_sessionStartTime` - `publishSession()` 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 - [x] **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 ✅ --- ## Phase 10 – Dokumentation & Abschluss - [ ] **10.1** README.md finalisieren (Screenshots, Schaltplan-Skizze ggf. ergänzen) - [ ] **10.2** `platformio.ini` mit finalen Library-Versionen dokumentieren - [ ] **10.3** MQTT-Topics in README.md validieren (mit realem Broker testen) - [ ] **10.4** Optionaler Schaltplan (Fritzing / ASCII) für Laser-Signal-Anschluss (Optokoppler) --- ## 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) | | `lasercutter/status` | Publish | 0 | Heartbeat alle 30 s | | `lasercutter/reset` | Subscribe | 1 | Externer Reset-Befehl | ### MQTT JSON-Formate **`lasercutter/session`** ```json { "session_minuten": 2, "session_seconds": 125, "gratiszeit_s": 20, "ip": "192.168.1.100" } ``` **`lasercutter/status`** (retained) ```json { "online": true, "session_sum": "2.08", "machine_running_time_min": "1234.75", "ip": "192.168.1.100", "uptime_s": 3600 } ``` --- *Erstellt: 22. Februar 2026*