From 3ebaab71002416a7d95822a510ce546e1dccbfc4 Mon Sep 17 00:00:00 2001 From: bring42 Date: Mon, 27 Jul 2026 15:46:29 +0200 Subject: [PATCH] fix(wifi): stop STA scan churn from breaking SoftAP DHCP during setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A saved-but-unreachable station network made the SoftAP unusable for provisioning: every WIFI_RETRY_INTERVAL_MS the device ran WiFi.begin(), whose full-band scan channel-hops the single C3 radio off the SoftAP's channel. When that scan lands during a client's DHCP handshake the lease never completes — the client falls back to a 169.254 link-local address, can't reach 192.168.4.1, and appears to "jump between addresses" as it re-associates and re-tries. The IDF's own auto-reconnect scanned behind our back too, so the previous "back off to 120s while a client is on the AP" mitigation wasn't enough. - Take ownership of STA reconnection: setAutoReconnect(false) + persistent(false) so the IDF stops scanning/auto-connecting on its own. - handleWifiMaintenance() now SKIPS the reconnect entirely while a client is parked on the SoftAP (station count > 0), instead of just lengthening the interval. Retries resume once the client leaves, so an idle client can't wedge the device offline and the link still self-heals when a reachable network appears. - Drop the now-unused WIFI_RETRY_INTERVAL_AP_BUSY_MS constant. Verified on a XIAO ESP32-C3 (saved SSID unreachable): phone now gets a 192.168.4.x lease, http://192.168.4.1 loads, and 0 reconnection attempts fire while a client is connected. Co-Authored-By: Claude Opus 4.8 --- src/constants.h | 7 +++---- src/network/wifi.cpp | 30 ++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/constants.h b/src/constants.h index eb1311d..f1a9498 100644 --- a/src/constants.h +++ b/src/constants.h @@ -110,11 +110,10 @@ constexpr uint8_t LED_CORRECTION_B = 240; // TypicalLEDStrip blue #define MDNS_HOSTNAME "lume" // Timeouts (milliseconds) +// STA reconnect cadence. Reconnect is SKIPPED entirely while a client is parked on the +// SoftAP (see handleWifiMaintenance): the single-radio scan a reconnect triggers would +// channel-hop the AP and break the client's DHCP mid-provisioning. constexpr uint32_t WIFI_RETRY_INTERVAL_MS = 30000; -// Slower reconnect cadence while a client is parked on the SoftAP: keeps a scan's -// brief AP disruption rare during setup, while still letting the device recover on -// its own if a saved network returns while an idle client is holding the AP. -constexpr uint32_t WIFI_RETRY_INTERVAL_AP_BUSY_MS = 120000; // ═══════════════════════════════════════════════════════════════════════════ // SYSTEM LIMITS & BUFFERS diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp index a12b17d..b6dd1fe 100644 --- a/src/network/wifi.cpp +++ b/src/network/wifi.cpp @@ -66,6 +66,15 @@ void setupWiFi() { // the SoftAP + web server sluggish/unreliable (slow loads, dropped connections). WiFi.setSleep(false); + // Own STA (re)connection entirely from handleWifiMaintenance(). The IDF's built-in + // auto-reconnect scans the single radio behind our back, and every scan channel-hops + // the SoftAP off its channel — which kills a provisioning client's DHCP handshake + // (the "169.254 link-local / jumping addresses" symptom when the saved network is + // out of range). persistent(false) also stops the IDF from auto-connecting a stale + // SSID out of its own NVS before we decide to; our creds live in Preferences. + WiFi.persistent(false); + WiFi.setAutoReconnect(false); + // Start Access Point WiFi.softAP(AP_SSID, AP_PASSWORD); LOG_INFO(LogTag::WIFI, "AP started: %s", AP_SSID); @@ -89,16 +98,17 @@ void setupWiFi() { // Helper function for WiFi reconnection and status monitoring void handleWifiMaintenance() { - // WiFi reconnection logic. While a client is connected to the SoftAP, back OFF - // but do NOT stop: WiFi.begin() channel-hops the single radio to scan, which - // briefly drops AP clients (annoying mid-setup). Using a much longer interval - // instead of skipping entirely means an idle phone parked on the AP can't wedge - // the device offline forever if it drops its saved network (e.g. router reboot). - if (!wifiConnected && config.wifiSSID.length() > 0) { - uint32_t interval = (WiFi.softAPgetStationNum() > 0) - ? WIFI_RETRY_INTERVAL_AP_BUSY_MS - : WIFI_RETRY_INTERVAL_MS; - if (millis() - lastWifiAttempt > interval) { + // WiFi reconnection logic. While a client is connected to the SoftAP, SKIP the + // reconnect entirely: WiFi.begin() channel-hops the single radio to scan, which + // drops the AP client mid-DHCP — the exact provisioning failure this addresses (an + // unreachable saved network otherwise scans every retry and makes the setup AP + // unusable). Once the client leaves (station count back to 0) the retry resumes, so + // an idle phone parked on the AP can't wedge the device offline forever and the + // link still self-heals when the saved network (or a freshly-provisioned one) + // returns. + if (!wifiConnected && config.wifiSSID.length() > 0 && + WiFi.softAPgetStationNum() == 0) { + if (millis() - lastWifiAttempt > WIFI_RETRY_INTERVAL_MS) { lastWifiAttempt = millis(); LOG_INFO(LogTag::WIFI, "Attempting WiFi reconnection..."); WiFi.begin(config.wifiSSID.c_str(), config.wifiPassword.c_str());