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