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
This commit is contained in:
MaPaLo76 2026-02-26 21:05:22 +01:00
parent 7fb14307ea
commit 974616aee2
5 changed files with 314 additions and 167 deletions

View File

@ -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)<br>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<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()`
@ -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()` |

View File

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

View File

@ -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 <Arduino.h>
@ -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;

View File

@ -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 <Arduino.h>
#include <esp_task_wdt.h>
#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
}

View File

@ -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();