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:
parent
7fb14307ea
commit
974616aee2
|
|
@ -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()` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
174
src/main.cpp
174
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 <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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user