MQTT-Display-LaserCutter/Architektur.md
MaPaLo76 a7c6edb458 fix(mqtt): MQTT-Task auf Core 0 auslagern - TLS-Blocking behebt WDT-Crash und Display-Freeze
- WiFiClientSecure/mbedtls ausschliesslich auf Core 0 initialisiert und verwendet
  (Cross-Core-Heap-Korruption durch mbedtls vermieden)
- xTaskCreatePinnedToCore('mqtt_task', Core 0, 16 KB Stack)
- begin() startet nur Task, kein Netzwerk-Zugriff auf Core 1
- mqttClient.loop() in main.cpp ist No-Op
- publishSession() von Core 1 via volatile-Flags an Core-0-Task uebergeben
- Version: 1.1.0 -> 1.1.1
2026-02-28 18:07:27 +01:00

180 lines
7.5 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()` |
| 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(...);
}
```