From d533f4d754ac029d6f23e47721c2f96e1df1ee12 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Wed, 5 Nov 2025 19:18:48 +1000 Subject: [PATCH] Refactor WiFi scanning to be fully async for ESPAsyncWebServer - Remove blocking WiFi scan operations from HTTP handlers - Make WiFi_scanNetworks() always async, remove sync path - Remove blocking wait loop that caused WDT resets - Add scan status polling in process() function - Make getScanItemOut() cache-only, never triggers scans - Add /wifi/scanstatus JSON endpoint for AJAX polling - Add client-side JavaScript for async network list updates - Remove _asyncScan toggle, always use async scanning - Add scan state management variables (_scanInProgress, _scanRequested) --- lib/WiFiManager/include/WiFiManager.h | 12 +- lib/WiFiManager/include/wm_consts_en.h | 1 + lib/WiFiManager/src/WiFiManager.cpp | 374 +++++++++++++++++++++---- 3 files changed, 330 insertions(+), 57 deletions(-) diff --git a/lib/WiFiManager/include/WiFiManager.h b/lib/WiFiManager/include/WiFiManager.h index e57fe66..a562351 100644 --- a/lib/WiFiManager/include/WiFiManager.h +++ b/lib/WiFiManager/include/WiFiManager.h @@ -595,6 +595,11 @@ class WiFiManager unsigned long _startscan = 0; // ms for timing wifi scans unsigned long _startconn = 0; // ms for timing wifi connects + // async scan state management + bool _scanInProgress = false; // flag indicating scan is currently running + bool _scanRequested = false; // flag indicating scan was requested but not yet started + unsigned long _scanRequestTime = 0; // ms when scan was requested + // async reboot/abort scheduling bool _rebootScheduled = false; // flag for scheduled reboot unsigned long _rebootTime = 0; // ms when reboot should occur @@ -697,7 +702,6 @@ class WiFiManager public: boolean _preloadwifiscan = false; // preload wifiscan if true unsigned int _scancachetime = 30000; // ms cache time for preload scans - boolean _asyncScan = false; // perform wifi network scan async protected: @@ -749,6 +753,7 @@ protected: void handleErase(AsyncWebServerRequest *request, boolean opt); void handleParam(AsyncWebServerRequest *request); void handleWiFiStatus(AsyncWebServerRequest *request); + void handleWiFiScanStatus(AsyncWebServerRequest *request); void handleRequest(AsyncWebServerRequest *request); void handleParamSave(AsyncWebServerRequest *request); void doParamSave(WiFiManagerRequestArgs requestArgs); @@ -776,8 +781,9 @@ protected: String WiFi_SSID(bool persistent = true) const; String WiFi_psk(bool persistent = true) const; bool WiFi_scanNetworks(); - bool WiFi_scanNetworks(bool force,bool async); - bool WiFi_scanNetworks(unsigned int cachetime,bool async); + bool WiFi_scanNetworks(bool force); // Always async - returns false if scan started but not complete + bool WiFi_scanNetworks(bool force,bool async); // async parameter ignored - always async + bool WiFi_scanNetworks(unsigned int cachetime,bool async); // async parameter ignored - always async bool WiFi_scanNetworks(unsigned int cachetime); void WiFi_scanComplete(int networksFound); bool WiFiSetCountry(); diff --git a/lib/WiFiManager/include/wm_consts_en.h b/lib/WiFiManager/include/wm_consts_en.h index 5ba4939..cf703fb 100644 --- a/lib/WiFiManager/include/wm_consts_en.h +++ b/lib/WiFiManager/include/wm_consts_en.h @@ -59,6 +59,7 @@ const char R_exit[] PROGMEM = "/exit"; const char R_close[] PROGMEM = "/close"; const char R_erase[] PROGMEM = "/erase"; const char R_status[] PROGMEM = "/status"; +const char R_scanstatus[] PROGMEM = "/wifi/scanstatus"; const char R_update[] PROGMEM = "/update"; const char R_updatedone[] PROGMEM = "/u"; diff --git a/lib/WiFiManager/src/WiFiManager.cpp b/lib/WiFiManager/src/WiFiManager.cpp index e1adcae..d3c4744 100644 --- a/lib/WiFiManager/src/WiFiManager.cpp +++ b/lib/WiFiManager/src/WiFiManager.cpp @@ -687,6 +687,10 @@ void WiFiManager::setupHTTPServer(){ this->handleWiFiStatus(request); }); + server->on(WM_G(R_scanstatus), HTTP_GET, [this](AsyncWebServerRequest *request) { + this->handleWiFiScanStatus(request); + }); + // OTA Update routes server->on(WM_G(R_update), HTTP_GET, [this](AsyncWebServerRequest *request) { this->handleUpdate(request); @@ -727,7 +731,7 @@ void WiFiManager::setupDNSD(){ void WiFiManager::setupConfigPortal() { setupHTTPServer(); _lastscan = 0; // reset network scan cache - if(_preloadwifiscan) WiFi_scanNetworks(true,true); // preload wifiscan , async + if(_preloadwifiscan) WiFi_scanNetworks(true); // preload wifiscan (async) } boolean WiFiManager::startConfigPortal() { @@ -892,6 +896,31 @@ boolean WiFiManager::process(){ #if defined(WM_MDNS) && defined(ESP8266) MDNS.update(); #endif + + // Poll WiFi scan status (non-blocking) + // Check if scan is in progress and poll for completion + if(_scanInProgress){ + int8_t scanStatus = WiFi.scanComplete(); + if(scanStatus >= 0){ + // Scan completed + WiFi_scanComplete(scanStatus); + } + else if(scanStatus == WIFI_SCAN_FAILED){ + // Scan failed + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed during polling")); + #endif + _scanInProgress = false; + _scanRequested = false; + } + // If WIFI_SCAN_RUNNING, continue polling next time (non-blocking) + } + + // Handle queued scan requests + if(_scanRequested && !_scanInProgress){ + WiFi_scanNetworks(true); + _scanRequested = false; + } if(webPortalActive || (configPortalActive && !_configPortalIsBlocking)){ // if timed out or abort, break @@ -1400,7 +1429,7 @@ void WiFiManager::handleRoot(AsyncWebServerRequest *request) { page += getHTTPEnd(); request->send(200, FPSTR(HTTP_HEAD_CT), page); - if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime, true); // preload wifiscan throttled, async + if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime); // preload wifiscan throttled (async) // @todo buggy, captive portals make a query on every page load, causing this to run every time in addition to the real page load // I dont understand why, when you are already in the captive portal, I guess they want to know that its still up and not done or gone // if we can detect these and ignore them that would be great, since they come from the captive portal redirect maybe there is a refferer @@ -1421,8 +1450,22 @@ void WiFiManager::handleWifi(AsyncWebServerRequest *request, boolean scan) { if (request->hasParam("refresh")) { forceRefresh = true; } - WiFi_scanNetworks(forceRefresh, false); //wifiscan, force if arg refresh + + // Always use cached data for immediate response (never block) + // Wrap scan output in container for AJAX updates + page += F("
"); page += getScanItemOut(); + page += F("
"); + + // Start async scan in background if needed (non-blocking) + if(forceRefresh || !_lastscan || (millis()-_lastscan > _scancachetime)){ + if(!_scanInProgress){ + WiFi_scanNetworks(true); // Start async scan + } else { + // Scan already in progress, will update via polling + _scanRequested = true; // Queue another scan if user wants refresh + } + } } String pitem = ""; @@ -1452,9 +1495,111 @@ void WiFiManager::handleWifi(AsyncWebServerRequest *request, boolean scan) { page += getParamOut(); } page += FPSTR(HTTP_FORM_END); - page += FPSTR(HTTP_SCAN_LINK); + // Replace refresh button with AJAX version + page += F("
"); if(_showBack) page += FPSTR(HTTP_BACKBTN); reportStatus(page); + + // Add JavaScript for AJAX polling + page += F(""); + page += getHTTPEnd(); request->send(200, FPSTR(HTTP_HEAD_CT), page); @@ -1518,6 +1663,8 @@ String WiFiManager::getMenuOut(){ void WiFiManager::WiFi_scanComplete(int networksFound){ _lastscan = millis(); _numNetworks = networksFound; + _scanInProgress = false; + _scanRequested = false; #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC completed"), "in "+(String)(_lastscan - _startscan)+" ms"); DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC found:"),_numNetworks); @@ -1525,18 +1672,22 @@ void WiFiManager::WiFi_scanComplete(int networksFound){ } bool WiFiManager::WiFi_scanNetworks(){ - return WiFi_scanNetworks(false,false); + return WiFi_scanNetworks(false); } bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime,bool async){ - return WiFi_scanNetworks(millis()-_lastscan > cachetime,async); + // async parameter ignored - always async now + return WiFi_scanNetworks(millis()-_lastscan > cachetime); } bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime){ - return WiFi_scanNetworks(millis()-_lastscan > cachetime,false); + return WiFi_scanNetworks(millis()-_lastscan > cachetime); } bool WiFiManager::WiFi_scanNetworks(bool force,bool async){ + // async parameter ignored - always async now + return WiFi_scanNetworks(force); +} +bool WiFiManager::WiFi_scanNetworks(bool force){ #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_DEV,"scanNetworks async:",async == true); // DEBUG_WM(WM_DEBUG_DEV,_numNetworks,(millis()-_lastscan )); // DEBUG_WM(WM_DEBUG_DEV,"scanNetworks force:",force == true); #endif @@ -1553,77 +1704,88 @@ bool WiFiManager::WiFi_scanNetworks(bool force,bool async){ force = true; } + // If scan already in progress, don't start another one + if(_scanInProgress){ + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan already in progress")); + #endif + return false; // Scan in progress, not complete yet + } + if(force){ - int8_t res; _startscan = millis(); - if(async && _asyncScan){ - #ifdef ESP8266 - #ifndef WM_NOASYNC // no async available < 2.4.0 - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); - #endif - using namespace std::placeholders; // for `_1` - WiFi.scanNetworksAsync(std::bind(&WiFiManager::WiFi_scanComplete,this,_1)); - #else - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan SYNC started")); - res = WiFi.scanNetworks(); - #endif - #else + _scanRequestTime = millis(); + + #ifdef ESP8266 + #ifndef WM_NOASYNC // no async available < 2.4.0 #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); - #endif - res = WiFi.scanNetworks(true); + DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); + #endif + using namespace std::placeholders; // for `_1` + WiFi.scanNetworksAsync(std::bind(&WiFiManager::WiFi_scanComplete,this,_1)); + _scanInProgress = true; + return false; // Scan started, not complete yet + #else + // ESP8266 < 2.4.0 - no async available, but we can't block here + // Return false and let process() handle it + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] Async scan not available on this ESP8266 core")); #endif return false; - } - else{ - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan SYNC started")); - res = WiFi.scanNetworks(); - } - if(res == WIFI_SCAN_FAILED){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed")); #endif - } - else if(res == WIFI_SCAN_RUNNING){ + #else // ESP32 #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan waiting")); + DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); #endif - while(WiFi.scanComplete() == WIFI_SCAN_RUNNING){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,"."); - #endif - delay(100); + int8_t res = WiFi.scanNetworks(true); + if(res == WIFI_SCAN_RUNNING){ + _scanInProgress = true; + return false; // Scan started, not complete yet } - _numNetworks = WiFi.scanComplete(); - } - else if(res >=0 ) _numNetworks = res; - _lastscan = millis(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan completed"), "in "+(String)(_lastscan - _startscan)+" ms"); + else if(res == WIFI_SCAN_FAILED){ + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed")); + #endif + _scanInProgress = false; + return false; + } + else if(res >= 0){ + // Scan completed immediately (unlikely but possible) + _numNetworks = res; + _lastscan = millis(); + _scanInProgress = false; + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan completed immediately"), "in "+(String)(_lastscan - _startscan)+" ms"); + #endif + return true; + } + return false; #endif - return true; } else { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("Scan is cached"),(String)(millis()-_lastscan )+" ms ago"); #endif + return true; // Using cached data } - return false; } String WiFiManager::WiFiManager::getScanItemOut(){ String page; - if(!_numNetworks) WiFi_scanNetworks(); // scan in case this gets called before any scans - + // Never trigger scans from here - only use cached data + // If no cached data, show message (scan should be started elsewhere if needed) int n = _numNetworks; if (n == 0) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(F("No networks found")); #endif - page += FPSTR(S_nonetworks); // @token nonetworks - page += F("

"); + if(_scanInProgress){ + page += F("Scanning for networks...

"); + } else { + page += FPSTR(S_nonetworks); // @token nonetworks + page += F("

"); + } } else { #ifdef WM_DEBUG_LEVEL @@ -1860,6 +2022,110 @@ void WiFiManager::handleWiFiStatus(AsyncWebServerRequest *request){ request->send(200, FPSTR(HTTP_HEAD_CT), page); } +/** + * HTTPD CALLBACK WiFi scan status endpoint - returns JSON with scan status and network list + */ +void WiFiManager::handleWiFiScanStatus(AsyncWebServerRequest *request){ + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP WiFi scan status")); + #endif + handleRequest(request); + + String json = "{"; + json += "\"scanning\":"; + json += _scanInProgress ? "true" : "false"; + json += ",\"count\":"; + json += String(_numNetworks); + json += ",\"lastscan\":"; + json += String(_lastscan); + + // If scan is complete and we have networks, include network list + if(!_scanInProgress && _numNetworks > 0){ + json += ",\"networks\":["; + + int n = _numNetworks; + // Sort networks by RSSI (same logic as getScanItemOut) + int indices[n]; + for (int i = 0; i < n; i++) { + indices[i] = i; + } + + // RSSI SORT + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { + std::swap(indices[i], indices[j]); + } + } + } + + // Remove duplicates (must be RSSI sorted) + if (_removeDuplicateAPs) { + String cssid; + for (int i = 0; i < n; i++) { + if (indices[i] == -1) continue; + cssid = WiFi.SSID(indices[i]); + for (int j = i + 1; j < n; j++) { + if (cssid == WiFi.SSID(indices[j])) { + indices[j] = -1; // set dup aps to index -1 + } + } + } + } + + bool first = true; + // Display networks in JSON + for (int i = 0; i < n; i++) { + if (indices[i] == -1) continue; // skip dups + + String ssid = WiFi.SSID(indices[i]); + if(ssid == "") continue; // Skip empty SSIDs + + int rssi = WiFi.RSSI(indices[i]); + int rssiperc = getRSSIasQuality(rssi); + uint8_t enc_type = WiFi.encryptionType(indices[i]); + + // Apply minimum quality filter + if (_minimumQuality != -1 && _minimumQuality >= rssiperc) { + continue; // Skip if doesn't meet minimum quality + } + + if(!first) json += ","; + first = false; + + json += "{"; + json += "\"ssid\":\""; + // Escape JSON special characters in SSID + String escapedSSID = ssid; + escapedSSID.replace("\\", "\\\\"); + escapedSSID.replace("\"", "\\\""); + escapedSSID.replace("\n", "\\n"); + escapedSSID.replace("\r", "\\r"); + escapedSSID.replace("\t", "\\t"); + json += escapedSSID; + json += "\""; + json += ",\"rssi\":"; + json += String(rssi); + json += ",\"quality\":"; + json += String(rssiperc); + json += ",\"encryption\":\""; + json += encryptionTypeStr(enc_type); + json += "\""; + json += ",\"enc_type\":"; + json += String(enc_type); + json += "}"; + } + + json += "]"; + } else { + json += ",\"networks\":[]"; + } + + json += "}"; + + request->send(200, "application/json", json); +} + /** * HTTPD CALLBACK save form and redirect to WLAN config page again */ @@ -3954,7 +4220,7 @@ String WiFiManager::WiFi_psk(bool persistent) const { WiFi.reconnect(); #endif } - else if(event == ARDUINO_EVENT_WIFI_SCAN_DONE && _asyncScan){ + else if(event == ARDUINO_EVENT_WIFI_SCAN_DONE){ uint16_t scans = WiFi.scanComplete(); WiFi_scanComplete(scans); }