MQTT-Display-LaserCutter/Architektur.md
MaPaLo76 974616aee2 refactor(wifi): non-blocking WiFi-Architektur (Proposal B+D)
- WiFiManager als Pointer, lazy new in begin() verhindert Crash durch
  globale Konstruktoren vor Arduino-Framework-Init
- WiFi.mode(WIFI_STA) vor Credentials-Pruefung; getWiFiSSID(true) statt
  WiFi.SSID() (erfordert gestarteten Stack)
- Credentials vorhanden: WiFi.begin() non-blocking, Ergebnis in loop()
- Keine Credentials: setConfigPortalBlocking(false) + process() in loop()
- Nach 3 Fehlversuchen: startConfigPortal() automatisch
- onConnect(): stopWebPortal() vor AsyncWebServer-Start (Port-80-Konflikt)
- main.cpp: LaserTracker startet vor WiFi (Prioritaet 1)
- MQTT + WebServer: Lazy-Init beim ersten WiFi-Connect
- Display rate-limited: W 500ms-Blinker, Minuten 60s, Sekunden 1s, M bei Aenderung
- Manuell getestet: LaserTracker laeuft sofort, WiFi non-blocking verifiziert
2026-02-26 21:05:22 +01:00

150 lines
6.2 KiB
Markdown
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.

# 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<br>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 13\n(Laserzeit min)"]
LT -->|"getCountdownRemaining()"| DISP_BOT["Display\nModule 57\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()` |