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

7.5 KiB
Raw Blame History

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: 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.

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 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)

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(...);
             }