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

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