# Software-Architektur – MQTT-Display LaserCutter > Dieses Dokument beschreibt die aktuelle Modulstruktur, Abhängigkeiten und Datenflüsse. > Es dient als Grundlage für Architekturdiskussionen und geplante Änderungen. --- ## 1. Modul-Übersicht | Modul | Datei(en) | Verantwortung | Globale Instanz | |---|---|---|---| | **Settings** | `settings.h/.cpp` | NVS-Persistenz aller Konfigurationswerte | `settings` | | **DisplayManager** | `display_manager.h/.cpp` | MAX7219 Dot-Matrix-Ansteuerung, Zeichensatz, Rotation | `display` | | **WifiConnector** | `wifi_connector.h/.cpp` | WiFiManager-Wrapper, Verbindungsaufbau, Reconnect | `wifiConnector` | | **MqttClient** | `mqtt_client.h/.cpp` | PubSubClient-Wrapper, Publish/Subscribe, Heartbeat | `mqttClient` | | **LaserTracker** | `laser_tracker.h/.cpp` | GPIO-Debounce, Session-Zustandsmaschine, Zeitmessung | `laserTracker` | | **WebServerManager** | `web_server.h/.cpp` | ESPAsyncWebServer, HTTP-Routen, Auth | `webServer` | | **config.h** | `config.h` | Compile-Zeit-Konstanten (Pins, Timeouts, Topics) | – | | **main.cpp** | `main.cpp` | `setup()` + `loop()`, Orchestrierung aller Module | – | --- ## 2. Abhängigkeiten zwischen Modulen ```mermaid graph TD config["config.h\n(Konstanten)"] settings["Settings\n(NVS)"] display["DisplayManager\n(MAX7219)"] wifi["WifiConnector\n(WiFiManager)"] mqtt["MqttClient\n(PubSubClient)"] laser["LaserTracker\n(GPIO + Timer)"] web["WebServerManager\n(ESPAsyncWebServer)"] main["main.cpp\n(Orchestrator)"] config --> settings config --> display config --> wifi config --> mqtt config --> laser config --> web settings --> laser settings --> mqtt settings --> web main --> settings main --> display main --> wifi main --> mqtt main --> laser main --> web web --> settings web --> laser web --> mqtt mqtt --> settings mqtt --> laser ``` > **Hinweis:** `WifiConnector` und `WebServerManager` können **nicht** beide in derselben `.cpp`-Datei inkludiert werden — `WiFiManager.h` und `ESPAsyncWebServer.h` definieren dieselben `HTTP_GET/POST`-Symbole. Aktueller Workaround: freie Funktion `wifiResetCredentialsAndRestart()` in `wifi_connector.cpp`, per `extern` von `web_server.cpp` aufgerufen. --- ## 3. Initialisierungsreihenfolge (`setup()`) **Designprinzip:** LaserTracker + Display starten **sofort**. WiFi ist vollständig non-blocking. MQTT + WebServer werden **lazy** erst bei erstem WiFi-Connect gestartet. ```mermaid sequenceDiagram participant main participant Settings participant Display participant LaserTracker participant WiFi participant MQTT participant WebServer main->>Settings: begin() — NVS laden main->>Display: begin() — MAX7219 init, showIdle() main->>LaserTracker: begin() — GPIO konfigurieren (PRIORITÄT 1) main->>WiFi: begin() — kehrt SOFORT zurück (non-blocking!) Note over WiFi: Credentials vorhanden → WiFi.begin() im Hintergrund
Keine Credentials → Captive Portal async (process() in loop()) Note over MQTT,WebServer: begin() wird NICHT in setup() aufgerufen! main->>main: Watchdog starten (30 s) ``` **Lazy-Init im `loop()`:** Sobald `wifiConnector.isConnected()` erstmalig `true` zurückgibt: ``` mqttClient.begin() → webServer.begin() ``` **Fallback bei falschen Credentials:** Nach 3 fehlgeschlagenen Verbindungsversuchen (je 15 s + 30 s Pause) öffnet der WifiConnector automatisch das Captive Portal (`startConfigPortal()`). --- ## 4. Datenfluss im `loop()` ```mermaid flowchart LR GPIO["GPIO 4\n(Laser-Signal)"] -->|"HIGH/LOW\n50ms Debounce"| LT["LaserTracker\n.loop()"] LT -->|"getAllSessionsSumMinutes()"| DISP_TOP["Display\nModule 1–3\n(Laserzeit min)"] LT -->|"getCountdownRemaining()"| DISP_BOT["Display\nModule 5–7\n(Countdown/Ring/Idle)"] LT -->|"consumeSessionEnd()\ngetLastSessionSeconds()"| MQTT_PUB["MqttClient\n.publishSession()"] LT -->|"consumeSessionReset()\ngetLastSessionSeconds()"| MQTT_PUB WIFI["WifiConnector\n.loop()"] -->|"isConnected()\n→ W-Blinker"| DISP_W["Display\nModul 0\n(WiFi-Fehler)"] MQTT_CLIENT["MqttClient\n.loop()"] -->|"isConnected()"| DISP_M["Display\nModul 4\n(MQTT-Fehler)"] MQTT_SUB["MQTT Subscribe\nlasercutter/reset"] -->|"reset:true → resetTotal()\nreset_session:true → resetSessionSum()"| LT WEB["WebServer\nPOST /reset"] -->|"resetSessionSum()"| LT WEB2["WebServer\nPOST /config"] -->|"saveMqttConfig()\nsaveGratis etc."| SETTINGS["Settings\n(NVS)"] WEB3["WebServer\nPOST /wifi-reset"] -->|"wifiResetCredentials\nAndRestart()"| WIFI ``` --- ## 5. Session-Zustandsmaschine (LaserTracker) ```mermaid stateDiagram-v2 [*] --> INACTIVE INACTIVE --> GRATIS : Laser AN\n(Debounce OK) GRATIS --> NET_COUNTING : Gratiszeit abgelaufen GRATIS --> INACTIVE : Laser AUS\n(Session verworfen) NET_COUNTING --> INACTIVE : Laser AUS\n→ consumeSessionEnd()\n→ publishSession() NET_COUNTING --> GRATIS : resetSessionSum()\n(softer Reset) INACTIVE --> GRATIS : resetSessionSum()\nwenn Laser noch an ``` --- ## 6. Bekannte Architektur-Probleme / Diskussionspunkte | # | Problem | Auswirkung | Status | |---|---|---|---| | A | **Globale Instanzen** für alle Module | Enge Kopplung, schwer testbar | offen | | B | **WiFiManager ↔ ESPAsyncWebServer Konflikt** | `extern`-Workaround nötig | ✅ gelöst via `wifiResetCredentialsAndRestart()` free-function | | C | **main.cpp als God-File** | Alle Module direkt orchestriert | offen | | D | **MqttClient greift direkt auf `laserTracker`** | Abhängigkeit mqtt ← laser | offen | | E | **WiFiManager::begin() blockierte setup()** | LaserTracker startete nie | ✅ gelöst: non-blocking begin() + lazy MQTT/Web init | | F | **WiFiManager als globales Objekt** | Crash vor Arduino-Framework-Init (globale Konstruktoren) | ✅ gelöst: `WiFiManager*` Pointer, lazy `new` in `begin()` | | G | **Captive Portal nach 3 Fehlversuchen** | Falsche Credentials → kein Portal | ✅ gelöst: `_failCount` + `startConfigPortal()` nach 3 Versuchen | | H | **Port-80-Konflikt nach Captive Portal** | AsyncWebServer kann Port 80 nicht belegen | ✅ gelöst: `_wm->stopWebPortal()` in `onConnect()` | | I | **TLS-Handshake blockierte `loop()` auf Core 1** | WDT-Crash, Display/Browser eingefroren 15 s | ✅ gelöst: MqttClient-Task auf Core 0 ausgelagert | --- ## 7. CPU-Core-Verteilung Der ESP32 ist Dual-Core (Xtensa LX6, Core 0 und Core 1). | Core | Aufgaben | Hinweise | |---|---|---| | **Core 1** (Arduino-Loop) | `laserTracker.loop()`, `wifiConnector.loop()`, Display-Updates, `webServer.loop()` (ArduinoOTA), `main.cpp` orchestration | Darf **niemals** blockieren. Kein `delay()` außer 20 ms am Ende von `loop()`. | | **Core 0** | `AsyncTCP` / `ESPAsyncWebServer` (intern), `mqtt_task` (`MqttClient::_taskLoop()`) | `mqtt_task` führt den blockierenden TLS-Handshake durch, ohne Core 1 zu beeinflussen. `WiFiClientSecure`/mbedtls **muss** auf Core 0 bleiben (nicht Cross-Core-safe). | ### Datenübergabe Core 1 → Core 0 (MqttClient) `publishSession()` wird von Core 1 aufgerufen (nach `consumeSessionEnd()`). Die Übergabe erfolgt über `volatile`-Flags (kein Mutex nötig für diese einfachen Integer-Werte): ``` Core 1: publishSession(sec, gratis) → _pendingSessionSec = sec → _pendingGratisSec = gratis → _pendingSession = true ← atomares Flag (volatile bool) Core 0: _taskLoop() → if (_pendingSession) { _pendingSession = false; _doPublishSession(...); } ```