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