diff --git a/Architektur.md b/Architektur.md index 5e79ad7..d7091fb 100644 --- a/Architektur.md +++ b/Architektur.md @@ -65,25 +65,36 @@ graph TD ## 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 - participant LaserTracker main->>Settings: begin() — NVS laden main->>Display: begin() — MAX7219 init, showIdle() - main->>WiFi: onStatusChange(callback) — W-Anzeige live - main->>WiFi: begin() — blockiert max. 8 s (SSID bekannt)
oder WIFI_AP_TIMEOUT_S (Ersteinrichtung) - main->>MQTT: begin() — Verbindungsversuch - main->>WebServer: begin() — Routen registrieren, Server starten - main->>LaserTracker: begin() — GPIO konfigurieren, NVS laden + 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
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()` @@ -126,10 +137,13 @@ stateDiagram-v2 ## 6. Bekannte Architektur-Probleme / Diskussionspunkte -| # | Problem | Auswirkung | Mögliche Lösung | +| # | Problem | Auswirkung | Status | |---|---|---|---| -| A | **Globale Instanzen** für alle Module | Enge Kopplung, schwer testbar | Dependency Injection / Pointer übergeben | -| B | **WiFiManager ↔ ESPAsyncWebServer Konflikt** | `extern`-Workaround nötig | Forward-Declaration, eigene WiFi-Verbindungslogik ohne WiFiManager | -| C | **main.cpp als God-File** | Alle Module direkt orchestriert | Event-Bus oder Callback-System | -| D | **MqttClient greift direkt auf `laserTracker`** | Zirkuläre Abhängigkeit (mqtt → laser, laser → settings) | Callback/Observer-Pattern | -| E | **Kein Task-System** | `loop()` serialisiert alles, Reconnect-While blockiert trotzdem teilweise | FreeRTOS Tasks für WiFi/MQTT | +| 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()` | diff --git a/Implementation-Plan.md b/Implementation-Plan.md index c19209c..b585048 100644 --- a/Implementation-Plan.md +++ b/Implementation-Plan.md @@ -228,18 +228,20 @@ ## Phase 8 – Integration & Hauptprogramm (`main.cpp`) - [x] **8.1** `main.cpp` aufräumen und alle Module initialisieren: - - Reihenfolge: `Settings` → `DisplayManager` → `WiFiManager` → `MqttClient` → `WebServer` → `LaserTracker` + - Reihenfolge: `Settings` → `DisplayManager` → `LaserTracker` → `WiFiConnector` (non-blocking) → Watchdog + - MQTT + WebServer: Lazy-Init im `loop()` beim ersten WiFi-Connect + - **Änderung gegenüber ursprünglicher Planung:** LaserTracker startet vor WiFi (Priorität 1) -- [x] **8.2** `loop()` implementieren: - - `displayManager.update()` - - `laserTracker.update()` (GPIO-Polling + Session-Logik) - - `mqttClient.loop()` (Reconnect + Heartbeat) - - `displayManager.showLaserTime(laserTracker.getTotalMinutes())` - - `displayManager.showCountdown(...)` oder `showIdle()` oder `showError(...)` +- [x] **8.2** `loop()` implementiert (non-blocking, kein `delay()` außer 20 ms am Ende): + - Priorität 1: `laserTracker.loop()` (immer zuerst) + - Priorität 2: `wifiConnector.loop()` (non-blocking) + - Lazy-Init: MQTT + WebServer beim ersten `wifiConnector.isConnected()` + - MQTT: `mqttClient.loop()`, Session-Publish nach `consumeSessionEnd/Reset()` + - Display rate-limited: W 500 ms-Blinker, Minuten-Update bei Änderung oder alle 60 s, Sekunden-Update alle 1 s, M nur bei Statuswechsel -- [x] **8.3** Callback von `LaserTracker` → `MqttClient.publishSession(...)` verdrahten +- [x] **8.3** Callback von `LaserTracker` → `MqttClient.publishSession(...)` verdrahtet -- [x] **8.4** WLAN/MQTT-Fehlerzustand auf Display spiegeln +- [x] **8.4** WLAN/MQTT-Fehlerzustand auf Display gespiegelt (rate-limited) - [x] **8.5** Watchdog Timer aktivieren (ESP32 WDT, 30 s) @@ -291,6 +293,18 @@ - MQTT `{"reset_session":true}` löst `resetSessionSum()` aus - Display zeigt nach Reset sofort 0 (alle Zeitakkumulatoren zurückgesetzt) +- [x] **9.8** WiFi-Architektur: vollständig non-blocking (Proposal B+D) + - **Problem:** `wifiConnector.begin()` blockierte bis zu 2 Minuten → LaserTracker startete nie + - `WiFiManager` als Pointer (`WiFiManager*`), lazy `new` in `begin()` (verhindert Crash durch globale Konstruktoren) + - `WiFi.mode(WIFI_STA)` vor Credentials-Prüfung; `_wm->getWiFiSSID(true)` statt `WiFi.SSID()` (erfordert gestarteten Stack) + - Credentials vorhanden: `WiFi.begin()` non-blocking, Ergebnis in `loop()` geprüft + - Keine/falsche Credentials: `setConfigPortalBlocking(false)` + `setConnectTimeout(1)` → Portal sofort, Verarbeitung via `_wm->process()` in `loop()` + - Nach 3 Fehlversuchen (je 15 s): `startConfigPortal()` → User kann neue Credentials eingeben + - `onConnect()`: `_wm->stopWebPortal()` vor AsyncWebServer-Start (verhindert Port-80-Konflikt) + - MQTT + WebServer: Lazy-Init erst bei erstem WiFi-Connect + - Display: rate-limited (W 500 ms-Blinker, Minuten bei Änderung/60 s, Sekunden 1 s, M bei Statuswechsel) + - Verifiziert: LaserTracker läuft sofort nach Neustart, unabhängig vom WiFi-Status ✅ + --- ## Phase 10 – Dokumentation & Abschluss diff --git a/include/wifi_connector.h b/include/wifi_connector.h index ae8e383..4cebf68 100644 --- a/include/wifi_connector.h +++ b/include/wifi_connector.h @@ -4,11 +4,20 @@ // wifi_connector.h – WiFi-Verbindungsmanagement via WiFiManager // Projekt: MQTT-Display LaserCutter // +// Design-Prinzip: begin() ist vollständig NON-BLOCKING. +// LaserTracker und Display laufen sofort nach begin() – kein Warten auf WiFi. +// +// Zwei Startszenarien: +// 1) Credentials gespeichert → WiFi.begin() direkt, Ergebnis in loop() prüfen +// 2) Keine Credentials → Captive Portal sofort async öffnen, loop() ruft +// wm.process() auf bis User Daten eingibt +// // Verwendung: // wifi.onStatusChange(myCallback); // optional, vor begin() -// wifi.begin(); // blockiert bis verbunden oder Timeout +// wifi.begin(); // KEhrt sofort zurück (non-blocking) +// // in loop(): +// wifi.loop(); // Reconnect + Portal-Processing // if (wifi.isConnected()) { ... } -// wifi.loop(); // in loop() aufrufen (Reconnect-Logik) // ============================================================================= #include @@ -42,21 +51,22 @@ public: // Callback vor begin() registrieren void onStatusChange(WifiStatusCallback cb); - // WiFi starten: versucht zuerst gespeicherte Credentials zu verwenden. - // Schlägt das fehl, öffnet das Captive Portal (AP "LaserCutter-Setup"). - // Timeout: WIFI_AP_TIMEOUT_S Sekunden, dann ESP.restart(). - // Blockiert bis verbunden oder Timeout. + // WiFi starten – NICHT blockierend, kehrt sofort zurück. + // Hat das Gerät Credentials: startet WiFi.begin() im Hintergrund. + // Hat es keine Credentials: öffnet Captive Portal asynchron. void begin(); - // In loop() aufrufen: erkennt Verbindungsabbrüche und veranlasst Reconnect. + // In loop() aufrufen: Portal-Processing + Verbindungscheck + Reconnect. + // Darf niemals lange blockieren. void loop(); // Aktuellen Status abfragen - WifiStatus getStatus() const { return _status; } - bool isConnected() const { return _status == WifiStatus::CONNECTED; } - IPAddress getIP() const { return WiFi.localIP(); } - String getSSID() const { return WiFi.SSID(); } - int8_t getRSSI() const { return WiFi.RSSI(); } + WifiStatus getStatus() const { return _status; } + bool isConnected() const { return _status == WifiStatus::CONNECTED; } + bool isPortalActive() const { return _portalRunning; } + IPAddress getIP() const { return WiFi.localIP(); } + String getSSID() const { return WiFi.SSID(); } + int8_t getRSSI() const { return WiFi.RSSI(); } // Debug-Ausgabe auf Serial void printToSerial() const; @@ -65,13 +75,16 @@ public: void resetCredentials(); private: + WiFiManager* _wm; // lazy-konstruiert in begin() (nicht global!) WifiStatusCallback _cb; WifiStatus _status; char _apPassword[32]; - uint32_t _lastConnectedMs; - uint32_t _lastReconnectMs; // Cooldown zwischen Reconnect-Versuchen + uint32_t _lastReconnectMs; // Zeitstempel letzter Reconnect-Versuch + bool _portalRunning; // Captive Portal aktiv + uint8_t _failCount; // Anzahl fehlgeschlagener Verbindungsversuche void setStatus(WifiStatus s); + void onConnect(); // NTP + Log bei jedem Verbindungsaufbau // Callbacks für WiFiManager (static nötig, C-Funktionszeiger) static WifiConnector* _instance; diff --git a/src/main.cpp b/src/main.cpp index 2d80341..96a4765 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,18 @@ +// ============================================================================= +// main.cpp - Orchestrierung aller Module +// +// Prioritaeten: +// 1. LaserTracker + Display: starten SOFORT, laufen IMMER +// 2. WiFi: non-blocking, unterbricht niemals LaserTracker +// 3. MQTT + WebServer: Lazy-Init beim ersten WiFi-Connect +// +// Display-Takte (rate-limited): +// Modul 0 (W): blinkt 500ms wenn WiFi getrennt +// Module 1-3: Minutenanzeige, Update bei Wertwechsel oder max. alle 60 s +// Modul 4 (M): nur bei Statuswechsel +// Module 5-7: Countdown/Ring/Idle, Update alle 1 s +// ============================================================================= + #include #include #include "config.h" @@ -8,115 +23,164 @@ #include "mqtt_client.h" #include "web_server.h" +// Lazy-Init Flags (MQTT + WebServer erst wenn WiFi verfuegbar) +static bool mqttStarted = false; +static bool webStarted = false; + void setup() { Serial.begin(SERIAL_BAUD_RATE); - // 1. Einstellungen laden + // --- 1. Einstellungen laden (NVS) --- settings.begin(); LOG_I("MAIN", "LaserCutter Display gestartet"); LOG_I("MAIN", "Laser GPIO: %d, Display CS: %d", LASER_SIGNAL_PIN, DISPLAY_CS_PIN); settings.printToSerial(); - // 2. Display initialisieren + // --- 2. Display initialisieren (sofort betriebsbereit) --- display.begin(); display.printToSerial(); - display.showWifiError(false); + display.showWifiError(true); // W an bis WiFi-Status bekannt display.showMqttError(false); display.showLaserTime(0.0f); display.showIdle(); - // 3. WLAN verbinden – WiFi-Fehler sofort auf Display spiegeln, auch - // während des blockierenden begin() (loop() wird erst danach erreicht) - wifiConnector.onStatusChange([](WifiStatus s) { - display.showWifiError(s != WifiStatus::CONNECTED); - }); - wifiConnector.begin(); - wifiConnector.printToSerial(); - - // 4. MQTT-Client starten - mqttClient.begin(); - mqttClient.printToSerial(); - - // 5. Webserver starten (async, kein loop() nötig) - webServer.begin(); - - // 6. LaserTracker starten + // --- 3. LaserTracker starten (PRIORITAET 1 - muss sofort laufen) --- laserTracker.begin(); laserTracker.printToSerial(); - // 7. Watchdog: Timeout aus config.h, Panic bei Auslösung + // --- 4. WiFi non-blocking starten --- + // begin() kehrt SOFORT zurueck (kein Blockieren mehr!) + // MQTT + WebServer starten spaeter (lazy, in loop()) + wifiConnector.begin(); + + // --- 5. Watchdog --- esp_task_wdt_init(WDT_TIMEOUT_S, true); esp_task_wdt_add(NULL); - LOG_I("MAIN", "Watchdog aktiv (%d s)", WDT_TIMEOUT_S); + LOG_I("MAIN", "Setup abgeschlossen. Watchdog aktiv (%d s)", WDT_TIMEOUT_S); } void loop() { - // Watchdog zurücksetzen + // Watchdog zuruecksetzen esp_task_wdt_reset(); - // Kern-Module aktualisieren + // ========================================================================== + // PRIORITAET 1: LaserTracker (immer zuerst, niemals ueberspringen) + // ========================================================================== laserTracker.loop(); + + // ========================================================================== + // PRIORITAET 2: WiFi (non-blocking, kein delay/while) + // ========================================================================== wifiConnector.loop(); - mqttClient.loop(); - // Session-Publish (nur wenn Netto-Zeit vorhanden, kein GRATIS-only) - if (laserTracker.consumeSessionEnd()) { - int lastSession = laserTracker.getLastSessionSeconds(); - if (lastSession > 0) { - mqttClient.publishSession(lastSession, settings.get().gratisSeconds); + // ========================================================================== + // Lazy-Init: MQTT + WebServer beim ersten WiFi-Connect + // ========================================================================== + if (wifiConnector.isConnected()) { + if (!mqttStarted) { + mqttClient.begin(); + mqttClient.printToSerial(); + mqttStarted = true; + } + if (!webStarted) { + LOG_I("MAIN", "WebServer wird gestartet..."); + webServer.begin(); + webStarted = true; } } - // Session-Reset-Publish: wie normales Session-Ende mit den bisher - // akkumulierten Sekunden (bevor alles auf 0 gesetzt wurde) - if (laserTracker.consumeSessionReset()) { - int sec = laserTracker.getLastSessionSeconds(); - if (sec > 0) { - mqttClient.publishSession(sec, settings.get().gratisSeconds); + // ========================================================================== + // MQTT (nur wenn gestartet) + // ========================================================================== + if (mqttStarted) { + mqttClient.loop(); + + // Session-Publish (nur wenn Netto-Zeit vorhanden, kein GRATIS-only) + if (laserTracker.consumeSessionEnd()) { + int lastSession = laserTracker.getLastSessionSeconds(); + if (lastSession > 0) { + mqttClient.publishSession(lastSession, settings.get().gratisSeconds); + } + } + + // Session-Reset-Publish: akkumulierte Sekunden vor dem Reset + if (laserTracker.consumeSessionReset()) { + int sec = laserTracker.getLastSessionSeconds(); + if (sec > 0) { + mqttClient.publishSession(sec, settings.get().gratisSeconds); + } } } - // Display: Modul 0 – WiFi-Fehler (blinkt 1 Hz wenn getrennt) + // ========================================================================== + // Display: rate-limited (Modul-Updates nur bei Bedarf) + // ========================================================================== + + // --- Modul 0: WiFi-Fehler (blinkt 500ms wenn getrennt) --- { - static uint32_t wifiBlinkMs = 0; - static bool wifiBlinkOn = false; + static uint32_t wifiBlinkMs = 0; + static bool wifiBlinkOn = false; bool wifiErr = !wifiConnector.isConnected(); if (wifiErr) { if (millis() - wifiBlinkMs >= 500) { wifiBlinkMs = millis(); wifiBlinkOn = !wifiBlinkOn; + display.showWifiError(wifiBlinkOn); } - display.showWifiError(wifiBlinkOn); } else { - wifiBlinkOn = false; - display.showWifiError(false); + if (wifiBlinkOn) { + wifiBlinkOn = false; + display.showWifiError(false); + } } } - // Display: Modul 4 – MQTT-Fehler - display.showMqttError(!mqttClient.isConnected()); + // --- Module 1-3: Minutenanzeige (Update bei Wertwechsel oder alle 60 s) --- + { + static uint32_t lastMinUpdate = 0; + static int lastMinValue = -1; + int curMin = laserTracker.getAllSessionsSumMinutes(); + if (curMin != lastMinValue || (millis() - lastMinUpdate) >= 60000UL) { + display.showLaserTime((float)curMin); + lastMinValue = curMin; + lastMinUpdate = millis(); + } + } - // Display: Module 1-3 – Gesamtzeit - display.showLaserTime((float)laserTracker.getAllSessionsSumMinutes()); + // --- Modul 4: MQTT-Fehler (nur bei Statuswechsel) --- + { + static bool lastMqttErr = false; + bool mqttErr = mqttStarted ? !mqttClient.isConnected() : false; + if (mqttErr != lastMqttErr) { + display.showMqttError(mqttErr); + lastMqttErr = mqttErr; + } + } - // Display: Module 5-7 – Betriebsstatus - int countdown = laserTracker.getCountdownRemaining(); - if (countdown > 0) { - display.showCountdown(countdown); // GRATIS: Countdown - } else if (laserTracker.isActive()) { - display.showSessionRing(laserTracker.getRunningSessionSeconds() % 60); // NET_COUNTING: Ring - } else { - display.showIdle(); // INACTIVE: -- + // --- Module 5-7: Countdown/Ring/Idle (Update alle 1 s) --- + { + static uint32_t lastSecUpdate = 0; + if (millis() - lastSecUpdate >= 1000UL) { + lastSecUpdate = millis(); + int countdown = laserTracker.getCountdownRemaining(); + if (countdown > 0) { + display.showCountdown(countdown); + } else if (laserTracker.isActive()) { + display.showSessionRing(laserTracker.getRunningSessionSeconds() % 60); + } else { + display.showIdle(); + } + } } display.update(); - // Heap-Monitor: alle 30 s loggen (Speicherleck-Erkennung) + // Heap-Monitor: alle 30 s loggen static uint32_t lastHeapLog = 0; if (millis() - lastHeapLog >= 30000) { lastHeapLog = millis(); LOG_D("MAIN", "FreeHeap: %u Bytes", ESP.getFreeHeap()); } - delay(50); + delay(20); // 50Hz loop, kurz genug fuer fluessiges Blinken } diff --git a/src/wifi_connector.cpp b/src/wifi_connector.cpp index 131ca61..6c4e1ec 100644 --- a/src/wifi_connector.cpp +++ b/src/wifi_connector.cpp @@ -1,5 +1,6 @@ -// ============================================================================= -// wifi_connector.cpp – WiFi-Verbindungsmanagement via WiFiManager +// ============================================================================= +// wifi_connector.cpp - WiFi-Verbindungsmanagement via WiFiManager +// Non-blocking: begin() kehrt sofort zurueck, loop() erledigt den Rest. // ============================================================================= #include "wifi_connector.h" @@ -7,9 +8,13 @@ // Globale Instanz WifiConnector wifiConnector; +// Static-Member Initialisierung +WifiConnector* WifiConnector::_instance = nullptr; + // --------------------------------------------------------------------------- -// NTP-Sync abwarten (non-restart-blocking, max. timeoutMs) -static void waitForNtp(uint32_t timeoutMs = 5000) { +// NTP-Zeitsynchronisation (blockiert max. timeoutMs, akzeptabel nach Connect) +// --------------------------------------------------------------------------- +static void waitForNtp(uint32_t timeoutMs = 3000) { struct tm ti; uint32_t t0 = millis(); while (!getLocalTime(&ti) && (millis() - t0) < timeoutMs) { @@ -20,19 +25,18 @@ static void waitForNtp(uint32_t timeoutMs = 5000) { strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &ti); LOG_I("WIFI", "NTP synchronisiert: %s", buf); } else { - LOG_E("WIFI", "NTP-Sync Timeout (%u ms) – Zeitstempel ungueltig", timeoutMs); + LOG_E("WIFI", "NTP-Sync Timeout - Zeitstempel ungueltig"); } } -// Static-Member Initialisierung -WifiConnector* WifiConnector::_instance = nullptr; - // --------------------------------------------------------------------------- WifiConnector::WifiConnector() - : _cb(nullptr) + : _wm(nullptr) + , _cb(nullptr) , _status(WifiStatus::DISCONNECTED) - , _lastConnectedMs(0) , _lastReconnectMs(0) + , _portalRunning(false) + , _failCount(0) { _apPassword[0] = '\0'; _instance = this; @@ -55,74 +59,97 @@ void WifiConnector::setStatus(WifiStatus s) { if (_cb) _cb(s); } +// --------------------------------------------------------------------------- +// onConnect() - intern, nach jedem erfolgreichem Verbindungsaufbau +// --------------------------------------------------------------------------- +void WifiConnector::onConnect() { + _failCount = 0; // Verbindung erfolgreich - Zaehler zuruecksetzen + setStatus(WifiStatus::CONNECTED); + _portalRunning = false; + // WiFiManager internen WebServer auf Port 80 freigeben! + // Sonst kann der AsyncWebServer Port 80 nicht belegen. + _wm->stopWebPortal(); + LOG_I("WIFI", "Verbunden: SSID=%s IP=%s RSSI=%d dBm", + WiFi.SSID().c_str(), + WiFi.localIP().toString().c_str(), + WiFi.RSSI()); + // NTP konfigurieren und auf ersten Sync warten (max. 3 s, einmalig OK) + configTime(0, 0, "pool.ntp.org", "time.nist.gov"); + waitForNtp(3000); +} + +// --------------------------------------------------------------------------- +// WiFiManager AP-Callbacks (static, da C-Funktionszeiger) // --------------------------------------------------------------------------- void WifiConnector::_onApStarted() { if (_instance) { + _instance->_portalRunning = true; _instance->setStatus(WifiStatus::AP_ACTIVE); - LOG_I("WIFI", "Konfigurations-Portal gestartet – AP: %s", WIFI_AP_NAME); + LOG_I("WIFI", "Konfigurations-Portal gestartet - AP: %s", WIFI_AP_NAME); LOG_I("WIFI", "Verbinde mit AP und oeffne http://192.168.4.1"); } } void WifiConnector::_onApStopped() { if (_instance) { - LOG_I("WIFI", "Konfigurations-Portal beendet"); + LOG_I("WIFI", "Konfigurations-Portal beendet - verbinde ..."); } } +// --------------------------------------------------------------------------- +// begin() - NON-BLOCKING, kehrt sofort zurueck // --------------------------------------------------------------------------- void WifiConnector::begin() { - LOG_I("WIFI", "Starte WiFiManager (AP: %s, Timeout: %d s)", - WIFI_AP_NAME, WIFI_AP_TIMEOUT_S); - - setStatus(WifiStatus::CONNECTING); - - WiFiManager wm; - - // Callbacks registrieren - wm.setAPCallback([](WiFiManager*) { WifiConnector::_onApStarted(); }); - wm.setSaveConfigCallback([]() { WifiConnector::_onApStopped(); }); - - // Debug-Ausgabe des WiFiManagers unterdrücken (nutzen eigene Logs) - wm.setDebugOutput(false); - - // Strategie: - // - Sind Credentials gespeichert → nur 8 s verbinden, kein Portal öffnen - // (Gerät läuft weiter ohne WiFi, Portal per Web-Button + Neustart erreichbar) - // - Keine Credentials → Portal normal öffnen (WIFI_AP_TIMEOUT_S), Ersteinrichtung - bool hasCreds = (WiFi.SSID().length() > 0); - if (hasCreds) { - wm.setConnectTimeout(8); - wm.setConfigPortalTimeout(1); // Portal sofort schließen - } else { - wm.setConfigPortalTimeout(WIFI_AP_TIMEOUT_S); // Ersteinrichtung: Portal offen lassen + // WiFiManager erst hier konstruieren – NICHT als globales Objekt! + // Globale Konstruktoren laufen vor Arduino-Framework-Init → WiFi-Stack crash. + if (_wm == nullptr) { + _wm = new WiFiManager(); } + _wm->setDebugOutput(false); + _wm->setAPCallback([](WiFiManager*) { WifiConnector::_onApStarted(); }); + _wm->setSaveConfigCallback([]() { WifiConnector::_onApStopped(); }); - // Verbinden - bool connected = (_apPassword[0] != '\0') - ? wm.autoConnect(WIFI_AP_NAME, _apPassword) - : wm.autoConnect(WIFI_AP_NAME); + // WiFi.mode(WIFI_STA) initialisiert den WiFi-Stack. + // WiFi.SSID() ist danach NICHT zuverlaessig (gibt "" zurueck bis Stack bereit). + // Daher: _wm->getWiFiSSID(true) liest persistent direkt via esp_wifi_get_config. + WiFi.mode(WIFI_STA); + bool hasCreds = (_wm->getWiFiSSID(true).length() > 0); - if (connected) { - setStatus(WifiStatus::CONNECTED); - _lastConnectedMs = millis(); - LOG_I("WIFI", "Verbunden mit: %s", WiFi.SSID().c_str()); - LOG_I("WIFI", "IP-Adresse : %s", WiFi.localIP().toString().c_str()); - LOG_I("WIFI", "RSSI : %d dBm", WiFi.RSSI()); - // NTP-Zeitsynchronisation (UTC), auf Sync warten (max. 5 s) - configTime(0, 0, "pool.ntp.org", "time.nist.gov"); - waitForNtp(); - } else { - // Kein Neustart – Gerät läuft ohne WiFi weiter (LaserTracker funktioniert) - LOG_E("WIFI", "WiFi nicht erreichbar – weiter ohne Netzwerk"); - setStatus(WifiStatus::DISCONNECTED); + if (hasCreds) { + // Credentials vorhanden: direkter WiFi-Connect (SDK, kein Portal) + // WiFi.begin() kehrt sofort zurueck - Ergebnis wird in loop() geprueft. + WiFi.begin(); + setStatus(WifiStatus::CONNECTING); _lastReconnectMs = millis(); + LOG_I("WIFI", "WiFi.begin() - verbinde im Hintergrund mit: %s", _wm->getWiFiSSID(true).c_str()); + } else { + // Keine Credentials: Captive Portal sofort non-blocking oeffnen. + // setConnectTimeout(0) = WiFiManager interpretiert 0 als "kein Timeout" = BLOCKIERT! + // Daher: 1 s Verbindungsversuch, dann sofort Portal oeffnen. + _wm->setConnectTimeout(1); // max. 1 s Verbindungsversuch, dann Portal + _wm->setConfigPortalBlocking(false); // loop()-driven, nicht blockierend + _wm->setConfigPortalTimeout(0); // kein automatischer Portal-Timeout + if (_apPassword[0] != '\0') { + _wm->autoConnect(WIFI_AP_NAME, _apPassword); + } else { + _wm->autoConnect(WIFI_AP_NAME); + } + // Status wird durch _onApStarted-Callback gesetzt + LOG_I("WIFI", "Keine Credentials - Captive Portal laeuft (non-blocking)"); } } +// --------------------------------------------------------------------------- +// loop() - wird in jedem loop()-Durchlauf aufgerufen, darf NICHT blockieren // --------------------------------------------------------------------------- void WifiConnector::loop() { - // Verbindungsabbruch erkennen + + // --- Portal-Processing (solange Portal aktiv) --- + if (_portalRunning && _wm != nullptr) { + _wm->process(); // HTTP-Requests des Captive Portals bedienen + } + + // --- Verbindungsabbruch erkennen --- if (_status == WifiStatus::CONNECTED && WiFi.status() != WL_CONNECTED) { LOG_E("WIFI", "Verbindung verloren"); setStatus(WifiStatus::DISCONNECTED); @@ -130,30 +157,45 @@ void WifiConnector::loop() { return; } - // Reconnect-Versuch starten (nicht-blockierend: nur einmal auslösen) - if (_status == WifiStatus::DISCONNECTED) { - if (millis() - _lastReconnectMs < 30000UL) return; - _lastReconnectMs = millis(); - LOG_I("WIFI", "Reconnect-Versuch ..."); - WiFi.reconnect(); - setStatus(WifiStatus::CONNECTING); + // --- Verbindung hergestellt (CONNECTING oder AP_ACTIVE -> CONNECTED) --- + if ((_status == WifiStatus::CONNECTING || _status == WifiStatus::AP_ACTIVE) + && WiFi.status() == WL_CONNECTED) { + onConnect(); return; } - // Ergebnis des laufenden Reconnects prüfen (wird jeden loop()-Durchlauf gecheckt) - if (_status == WifiStatus::CONNECTING) { - if (WiFi.status() == WL_CONNECTED) { - setStatus(WifiStatus::CONNECTED); - _lastConnectedMs = millis(); - LOG_I("WIFI", "Reconnect erfolgreich – IP: %s", - WiFi.localIP().toString().c_str()); - configTime(0, 0, "pool.ntp.org", "time.nist.gov"); - waitForNtp(); - } else if (millis() - _lastReconnectMs > 15000UL) { - // Timeout – nächster Versuch in 30 s - LOG_E("WIFI", "Reconnect fehlgeschlagen – naechster Versuch in 30 s"); - setStatus(WifiStatus::DISCONNECTED); + // --- Timeout fuer laufenden Verbindungsversuch (nach 15 s aufgeben) --- + if (_status == WifiStatus::CONNECTING + && (millis() - _lastReconnectMs) > 15000UL + && WiFi.status() != WL_CONNECTED) { + _failCount++; + LOG_E("WIFI", "Verbindungsversuch fehlgeschlagen (%d/3)", _failCount); + if (_failCount >= 3) { + // Nach 3 Fehlversuchen: Captive Portal oeffnen (falsche Credentials?) + LOG_E("WIFI", "3x fehlgeschlagen - Captive Portal wird geoeffnet"); + _wm->setConnectTimeout(1); + _wm->setConfigPortalBlocking(false); + _wm->setConfigPortalTimeout(0); + if (_apPassword[0] != '\0') { + _wm->startConfigPortal(WIFI_AP_NAME, _apPassword); + } else { + _wm->startConfigPortal(WIFI_AP_NAME); + } + // _portalRunning wird durch _onApStarted-Callback gesetzt + _failCount = 0; + return; } + setStatus(WifiStatus::DISCONNECTED); + return; + } + + // --- Reconnect-Versuch (alle 30 s, nicht-blockierend) --- + if (_status == WifiStatus::DISCONNECTED) { + if ((millis() - _lastReconnectMs) < 30000UL) return; + _lastReconnectMs = millis(); + LOG_I("WIFI", "Reconnect-Versuch ..."); + WiFi.reconnect(); // nicht-blockierend, Ergebnis beim naechsten Aufruf + setStatus(WifiStatus::CONNECTING); } } @@ -163,7 +205,7 @@ void WifiConnector::printToSerial() const { switch (_status) { case WifiStatus::DISCONNECTED: LOG_I("WIFI", " Status : DISCONNECTED"); break; case WifiStatus::AP_ACTIVE: LOG_I("WIFI", " Status : AP_ACTIVE (%s)", WIFI_AP_NAME); break; - case WifiStatus::CONNECTING: LOG_I("WIFI", " Status : CONNECTING"); break; + case WifiStatus::CONNECTING: LOG_I("WIFI", " Status : CONNECTING (%s)", WiFi.SSID().c_str()); break; case WifiStatus::CONNECTED: LOG_I("WIFI", " Status : CONNECTED"); LOG_I("WIFI", " SSID : %s", WiFi.SSID().c_str()); @@ -178,10 +220,10 @@ void WifiConnector::printToSerial() const { void WifiConnector::resetCredentials() { WiFiManager wm; wm.resetSettings(); - LOG_I("WIFI", "WiFi-Credentials geloescht – AP wird beim naechsten Start geoeffnet"); + LOG_I("WIFI", "WiFi-Credentials geloescht - AP wird beim naechsten Start geoeffnet"); } -// Freie Hilfsfunktion – aufrufbar ohne WiFiManager-Header (vermeidet +// Freie Hilfsfunktion - aufrufbar ohne WiFiManager-Header (vermeidet // HTTP_GET/HTTP_POST Konflikt mit ESPAsyncWebServer in web_server.cpp) void wifiResetCredentialsAndRestart() { wifiConnector.resetCredentials();