@@ -30,8 +30,14 @@ WiFiManager::WiFiManager() :
3030 ntpRetries(0 ),
3131 debugPrintFunc(nullptr ),
3232 debugPrintfFunc(nullptr ),
33- firstImageLoaded(false )
33+ firstImageLoaded(false ),
34+ roamScanPending(false ),
35+ roamInProgress(false ),
36+ lastRoamScanTime(0 ),
37+ roamStartTime(0 ),
38+ roamTargetChannel(0 )
3439{
40+ memset (roamTargetBSSID, 0 , sizeof (roamTargetBSSID));
3541}
3642
3743bool WiFiManager::begin () {
@@ -166,11 +172,135 @@ void WiFiManager::setDebugFunctions(void (*debugPrint)(const char*, uint16_t),
166172 }
167173}
168174
175+ void WiFiManager::checkForBetterAP () {
176+ unsigned long now = millis ();
177+
178+ // --- State: Roam in progress (polling reconnection to target AP) ---
179+ if (roamInProgress) {
180+ if (WiFi.status () == WL_CONNECTED ) {
181+ // Roam succeeded
182+ roamInProgress = false ;
183+ wifiConnected = true ;
184+ LOG_INFO_F (" [WiFi] Roam successful - connected to %s (RSSI: %d dBm, ch: %d) in %lu ms\n " ,
185+ WiFi.BSSIDstr ().c_str (), WiFi.RSSI (), WiFi.channel (),
186+ now - roamStartTime);
187+ DeviceHealthAnalyzer::recordRoam ();
188+ syncNTPTime ();
189+ } else if (now - roamStartTime > WIFI_MAX_WAIT_TIME ) {
190+ // Roam timed out — fall back to normal reconnection
191+ LOG_WARNING_F (" [WiFi] Roam failed (timeout after %lu ms) - falling back to auto-connect\n " ,
192+ now - roamStartTime);
193+ roamInProgress = false ;
194+ wifiConnected = false ;
195+ WiFi.disconnect ();
196+ // Next update() cycle will call connectToWiFi() without BSSID lock
197+ } else if (WiFi.status () == WL_CONNECT_FAILED || WiFi.status () == WL_NO_SSID_AVAIL ) {
198+ LOG_WARNING_F (" [WiFi] Roam failed (status: %d) - falling back to auto-connect\n " ,
199+ WiFi.status ());
200+ roamInProgress = false ;
201+ wifiConnected = false ;
202+ WiFi.disconnect ();
203+ }
204+ return ;
205+ }
206+
207+ // Don't start roam logic if not connected or if NTP sync is in progress
208+ if (!isConnected () || ntpSyncInProgress) {
209+ return ;
210+ }
211+
212+ // --- State: Scan pending (poll for async scan completion) ---
213+ if (roamScanPending) {
214+ int scanResult = WiFi.scanComplete ();
215+
216+ if (scanResult == WIFI_SCAN_RUNNING ) {
217+ return ; // Still scanning
218+ }
219+
220+ roamScanPending = false ;
221+
222+ if (scanResult == WIFI_SCAN_FAILED || scanResult < 0 ) {
223+ LOG_WARNING (" [WiFi] Roam scan failed" );
224+ WiFi.scanDelete ();
225+ return ;
226+ }
227+
228+ // Get current connection info for comparison
229+ int currentRSSI = WiFi.RSSI ();
230+ String currentBSSID = WiFi.BSSIDstr ();
231+ String currentSSID = String (WIFI_SSID );
232+
233+ // Find the strongest AP with the same SSID but different BSSID
234+ int bestIndex = -1 ;
235+ int bestRSSI = currentRSSI;
236+
237+ for (int i = 0 ; i < scanResult; i++) {
238+ String scanSSID = WiFi.SSID (i);
239+ String scanBSSID = WiFi.BSSIDstr (i);
240+ int scanRSSI = WiFi.RSSI (i);
241+
242+ if (scanSSID == currentSSID && scanBSSID != currentBSSID) {
243+ if (scanRSSI > bestRSSI + WIFI_ROAM_RSSI_THRESHOLD ) {
244+ bestRSSI = scanRSSI;
245+ bestIndex = i;
246+ }
247+ }
248+ }
249+
250+ if (bestIndex >= 0 ) {
251+ // Found a significantly stronger AP — initiate roam
252+ LOG_INFO_F (" [WiFi] Roaming from %s (%d dBm) to %s (%d dBm, ch: %d)\n " ,
253+ currentBSSID.c_str (), currentRSSI,
254+ WiFi.BSSIDstr (bestIndex).c_str (), bestRSSI, WiFi.channel (bestIndex));
255+
256+ // Save target BSSID and channel before scanDelete clears them
257+ memcpy (roamTargetBSSID, WiFi.BSSID (bestIndex), 6 );
258+ roamTargetChannel = WiFi.channel (bestIndex);
259+
260+ WiFi.scanDelete ();
261+
262+ // Disconnect and reconnect to the stronger AP
263+ WiFi.disconnect ();
264+ roamInProgress = true ;
265+ roamStartTime = now;
266+ WiFi.begin (WIFI_SSID , WIFI_PASSWORD , roamTargetChannel, roamTargetBSSID);
267+ } else {
268+ LOG_DEBUG_F (" [WiFi] Roam scan: no better AP found (current: %s, %d dBm, %d candidates)\n " ,
269+ currentBSSID.c_str (), currentRSSI, scanResult);
270+ WiFi.scanDelete ();
271+ }
272+
273+ return ;
274+ }
275+
276+ // --- State: Idle (check if it's time to start a new roam scan) ---
277+ if (now - lastRoamScanTime >= WIFI_ROAM_CHECK_INTERVAL ) {
278+ // Don't start a scan if one is already running (e.g., web API scan)
279+ if (WiFi.scanComplete () == WIFI_SCAN_RUNNING ) {
280+ return ;
281+ }
282+
283+ lastRoamScanTime = now;
284+ roamScanPending = true ;
285+ DeviceHealthAnalyzer::recordRoamScan ();
286+ WiFi.scanNetworks (true , false ); // async=true, show_hidden=false
287+ LOG_DEBUG (" [WiFi] Roam scan started (async)" );
288+ }
289+ }
290+
169291void WiFiManager::update () {
292+ // During a roam, skip checkConnection/connectToWiFi to prevent interference
293+ if (roamInProgress) {
294+ checkForBetterAP (); // Poll roam connection status
295+ return ;
296+ }
297+
170298 checkConnection ();
171299
172- // Attempt reconnection if disconnected (non-blocking)
173- if (!isConnected ()) {
300+ if (isConnected ()) {
301+ checkForBetterAP (); // Scan for better APs / evaluate results
302+ } else {
303+ // Attempt reconnection if disconnected (non-blocking)
174304 connectToWiFi ();
175305 }
176306
0 commit comments