From 974616aee2bb7e9eb3d7bcb704eee7f4d6d8fd12 Mon Sep 17 00:00:00 2001
From: MaPaLo76 <72209721+MaPaLo76@users.noreply.github.com>
Date: Thu, 26 Feb 2026 21:05:22 +0100
Subject: [PATCH] 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
---
Architektur.md | 38 +++++---
Implementation-Plan.md | 32 +++++--
include/wifi_connector.h | 41 +++++---
src/main.cpp | 174 +++++++++++++++++++++++-----------
src/wifi_connector.cpp | 196 ++++++++++++++++++++++++---------------
5 files changed, 314 insertions(+), 167 deletions(-)
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();