- 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
7.5 KiB
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
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:
WifiConnectorundWebServerManagerkönnen nicht beide in derselben.cpp-Datei inkludiert werden —WiFiManager.hundESPAsyncWebServer.hdefinieren dieselbenHTTP_GET/POST-Symbole. Aktueller Workaround: freie FunktionwifiResetCredentialsAndRestart()inwifi_connector.cpp, perexternvonweb_server.cppaufgerufen.
3. Initialisierungsreihenfolge (setup())
Designprinzip: LaserTracker + Display starten sofort. WiFi ist vollständig non-blocking. MQTT + WebServer werden lazy erst bei erstem WiFi-Connect gestartet.
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()
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)
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(...);
}