Repository: usetrmnl/firmware
Pull request #76: Multiple WiFi Network Support
Author: login: gdifiore
Created: 2025-03-06T14:24:51Z | Merged: 2025-03-12T01:57:32Z

Issue #71
- Remove `_lastUsed`, will shift all network credentials in the array so that last used wifi is `_savedWifis[0]`
- On failed connection to last used WiFi network, loop over all saved credentials in the array
- No longer saves duplicate networks in the array


Diff:
diff --git a/lib/wificaptive/src/WifiCaptive.cpp b/lib/wificaptive/src/WifiCaptive.cpp
index 1d2ffd34..2f0bad1c 100644
--- a/lib/wificaptive/src/WifiCaptive.cpp
+++ b/lib/wificaptive/src/WifiCaptive.cpp
@@ -73,6 +73,7 @@ void WifiCaptive::setUpWebserver(AsyncWebServer &server, const IPAddress &localI
 			return request->send(202);
 		} else {
 			// Data structure to store the highest RSSI for each SSID
+            // Warning: DO NOT USE true on this function in an async context! 
 			std::vector<Network> uniqueNetworks = getScannedUniqueNetworks(false);
             std::vector<Network> combinedNetworks = combineNetworks(uniqueNetworks, _savedWifis);
 
@@ -103,7 +104,7 @@ void WifiCaptive::setUpWebserver(AsyncWebServer &server, const IPAddress &localI
 
             WiFi.scanDelete();
 			Serial.println(json);
-            
+
 			if (WiFi.scanComplete() == -2){
 				WiFi.scanNetworks(true);
 			}
@@ -240,20 +241,18 @@ void WifiCaptive::resetSettings()
     Preferences preferences;
     preferences.begin("wificaptive", false);
     preferences.remove("api_url");
+    preferences.remove(WIFI_LAST_INDEX);
     for (int i = 0; i < WIFI_MAX_SAVED_CREDS; i++)
     {
         preferences.remove(WIFI_SSID_KEY(i));
         preferences.remove(WIFI_PSWD_KEY(i));
     }
-    preferences.remove(WIFI_LAST_USED_SSID_KEY);
-    preferences.remove(WIFI_LAST_USED_PSWD_KEY);
     preferences.end();
 
     for (int i = 0; i < WIFI_MAX_SAVED_CREDS; i++)
     {
         _savedWifis[i] = {"", ""};
     }
-    _lastUsed = {"", ""};
 
     WiFi.disconnect(true, true);
 }
@@ -321,14 +320,13 @@ void WifiCaptive::readWifiCredentials()
 {
     Preferences preferences;
     preferences.begin("wificaptive", true);
-    size_t len = preferences.getBytesLength(WIFI_SSID_KEY(0));
+
     for (int i = 0; i < WIFI_MAX_SAVED_CREDS; i++)
     {
         _savedWifis[i].ssid = preferences.getString(WIFI_SSID_KEY(i), "");
         _savedWifis[i].pswd = preferences.getString(WIFI_PSWD_KEY(i), "");
     }
-    _lastUsed.ssid = preferences.getString(WIFI_LAST_USED_SSID_KEY, "");
-    _lastUsed.pswd = preferences.getString(WIFI_LAST_USED_PSWD_KEY, "");
+
     preferences.end();
 }
 
@@ -336,16 +334,21 @@ void WifiCaptive::saveWifiCredentials(String ssid, String pass)
 {
     Log.info("Saving wifi credentials: %s\r\n", ssid.c_str());
 
-    for (u16_t i = WIFI_MAX_SAVED_CREDS - 1; i >= 1; i--)
+    // Check if the credentials already exist
+    for (u16_t i = 0; i < WIFI_MAX_SAVED_CREDS; i++)
+    {
+        if (_savedWifis[i].ssid == ssid && _savedWifis[i].pswd == pass)
+        {
+            return; // Avoid saving duplicate networks
+        }
+    }
+
+    for (u16_t i = WIFI_MAX_SAVED_CREDS - 1; i > 0; i--)
     {
-        _savedWifis[i].ssid = _savedWifis[i - 1].ssid;
-        _savedWifis[i].pswd = _savedWifis[i - 1].pswd;
+        _savedWifis[i] = _savedWifis[i - 1];
     }
 
-    _savedWifis[0].ssid = ssid;
-    _savedWifis[0].pswd = pass;
-    _lastUsed.ssid = ssid;
-    _lastUsed.pswd = pass;
+    _savedWifis[0] = {ssid, pass};
 
     Preferences preferences;
     preferences.begin("wificaptive", false);
@@ -354,21 +357,56 @@ void WifiCaptive::saveWifiCredentials(String ssid, String pass)
         preferences.putString(WIFI_SSID_KEY(i), _savedWifis[i].ssid);
         preferences.putString(WIFI_PSWD_KEY(i), _savedWifis[i].pswd);
     }
-    preferences.putString(WIFI_LAST_USED_SSID_KEY, _lastUsed.ssid);
-    preferences.putString(WIFI_LAST_USED_PSWD_KEY, _lastUsed.pswd);
+    preferences.putInt(WIFI_LAST_INDEX, 0);
     preferences.end();
 }
 
-void WifiCaptive::saveLastUsed(String ssid, String pass)
+void WifiCaptive::saveLastUsedWifiIndex(int index)
 {
-    _lastUsed.ssid = ssid;
-    _lastUsed.pswd = pass;
-
     Preferences preferences;
     preferences.begin("wificaptive", false);
-    preferences.putString(WIFI_LAST_USED_SSID_KEY, ssid);
-    preferences.putString(WIFI_LAST_USED_PSWD_KEY, pass);
+
+    // if index is out of bounds, set to 0
+    if (index < 0 || index >= WIFI_MAX_SAVED_CREDS)
+    {
+        index = 0;
+    }
+
+    // if index is greater than the total number of saved wifis, set to 0
+    if (index > 0)
+    {
+        readWifiCredentials();
+        if (_savedWifis[index].ssid == "")
+        {
+            index = 0;
+        }
+    }
+
+    preferences.putInt(WIFI_LAST_INDEX, index);
+}
+
+int WifiCaptive::readLastUsedWifiIndex()
+{
+    Preferences preferences;
+    preferences.begin("wificaptive", true);
+    int index = preferences.getInt(WIFI_LAST_INDEX, 0);
+    // if index is out of range, return 0
+    if (index < 0 || index >= WIFI_MAX_SAVED_CREDS)
+    {
+        index = 0;
+    }
+
+    // if index is greater than the total number of saved wifis, set to 0
+    if (index > 0)
+    {
+        readWifiCredentials();
+        if (_savedWifis[index].ssid == "")
+        {
+            index = 0;
+        }
+    }
     preferences.end();
+    return index;
 }
 
 void WifiCaptive::saveApiServer(String url)
@@ -388,7 +426,7 @@ std::vector<WifiCaptive::Network> WifiCaptive::getScannedUniqueNetworks(bool run
     int n = WiFi.scanComplete();
     if (runScan == true)
     {
-        WiFi.scanNetworks(true);
+        WiFi.scanNetworks(false);
         delay(100);
         int n = WiFi.scanComplete();
         while (n == WIFI_SCAN_RUNNING || n == WIFI_SCAN_FAILED)
@@ -400,14 +438,28 @@ std::vector<WifiCaptive::Network> WifiCaptive::getScannedUniqueNetworks(bool run
             }
             else if (n == WIFI_SCAN_FAILED)
             {
-                WiFi.scanNetworks(true);
-                delay(100);
+                // There is a race coniditon that can occur, particularly if you use the async flag of WiFi.scanNetworks(true),
+                // where you can race before the data is parsed. scanComplete will be -2, we'll see that and fail out, but then a few microseconds later it actually
+                // fills in. This fixes that, in case we ever move back to the async version of scanNetworks, but as long as it's sync above it'll work
+                // first shot always.
+                Log.verboseln("Supposedly failed to finish scan, let's wait 10 seconds before checking again");
+                delay(10000);
+                n = WiFi.scanComplete();
+                if (n > 0)
+                {
+                    Log.verboseln("Scan actually did complete, we have %d networks, breaking loop.", n);
+                    // it didn't actually fail, we just raced before the scan was done filling in data
+                    break;
+                }
+                WiFi.scanNetworks(false);
+                delay(500);
                 n = WiFi.scanComplete();
             }
         }
     }
 
     n = WiFi.scanComplete();
+    Log.verboseln("Scanning networks, final scan result: %d", n);
 
     // Process each found network
     for (int i = 0; i < n; ++i)
@@ -438,6 +490,12 @@ std::vector<WifiCaptive::Network> WifiCaptive::getScannedUniqueNetworks(bool run
         }
     }
 
+    Log.infoln("Unique networks found: %d", uniqueNetworks.size());
+    for (auto &network : uniqueNetworks)
+    {
+        Log.infoln("SSID: %s, RSSI: %d, Open: %d", network.ssid.c_str(), network.rssi, network.open);
+    }
+
     return uniqueNetworks;
 }
 
@@ -512,47 +570,74 @@ bool WifiCaptive::autoConnect()
     Log.info("Trying to autoconnect to wifi...\r\n");
     readWifiCredentials();
 
-    if (_lastUsed.ssid != "")
+    // if last used network is available, try to connect to it
+    int last_used_index = readLastUsedWifiIndex();
+
+    if (_savedWifis[last_used_index].ssid != "")
     {
-        Log.info("Trying to connect to last used %s...\r\n", _lastUsed.ssid.c_str());
+        Log.info("Trying to connect to last used %s...\r\n", _savedWifis[last_used_index].ssid.c_str());
         WiFi.setSleep(0);
         WiFi.setMinSecurity(WIFI_AUTH_OPEN);
         WiFi.mode(WIFI_STA);
-        connect(_lastUsed.ssid, _lastUsed.pswd);
 
-        // Check if connected
-        if (WiFi.status() == WL_CONNECTED)
+        for (int attempt = 0; attempt < WIFI_CONNECTION_ATTEMPTS; attempt++)
         {
-            Log.info("Connected to %s\r\n", _lastUsed.ssid.c_str());
-            return true;
+            Log.info("Attempt %d to connect to %s\r\n", attempt + 1, _savedWifis[last_used_index].ssid.c_str());
+            connect(_savedWifis[last_used_index].ssid, _savedWifis[last_used_index].pswd);
+
+            // Check if connected
+            if (WiFi.status() == WL_CONNECTED)
+            {
+                Log.info("Connected to %s\r\n", _savedWifis[last_used_index].ssid.c_str());
+                return true;
+            }
+            WiFi.disconnect();
+            delay(500);
         }
-        WiFi.disconnect();
     }
 
+    Log.info("Last used network unavailable, scanning for known networks...\r\n");
     std::vector<Network> scanResults = getScannedUniqueNetworks(true);
     std::vector<WifiCaptive::WifiCredentials> sortedNetworks = matchNetworks(scanResults, _savedWifis);
     // if no networks found, try to connect to saved wifis
     if (sortedNetworks.size() == 0)
     {
-        Log.info("No matched networks...\r\n");
+        Log.info("No matched networks found in scan, trying all saved networks...\r\n");
         sortedNetworks = std::vector<WifiCredentials>(_savedWifis, _savedWifis + WIFI_MAX_SAVED_CREDS);
     }
 
     WiFi.mode(WIFI_STA);
     for (auto &network : sortedNetworks)
     {
-        if (network.ssid == "")
+        if (network.ssid == "" || (network.ssid == _savedWifis[0].ssid && network.pswd == _savedWifis[0].pswd))
         {
             continue;
         }
 
-        connect(network.ssid, network.pswd);
+        Log.info("Trying to connect to saved network %s...\r\n", network.ssid.c_str());
 
-        // Check if connected
-        if (WiFi.status() == WL_CONNECTED)
+        for (int attempt = 0; attempt < WIFI_CONNECTION_ATTEMPTS; attempt++)
         {
-            saveLastUsed(network.ssid, network.pswd);
-            return true;
+            Log.info("Attempt %d to connect to %s\r\n", attempt + 1, network.ssid.c_str());
+            connect(network.ssid, network.pswd);
+
+            // Check if connected
+            if (WiFi.status() == WL_CONNECTED)
+            {
+                Log.info("Connected to %s\r\n", network.ssid.c_str());
+                // success! save the index of the last used network
+                for (int i = 0; i < WIFI_MAX_SAVED_CREDS; i++)
+                {
+                    if (_savedWifis[i].ssid == network.ssid)
+                    {
+                        saveLastUsedWifiIndex(i);
+                        break;
+                    }
+                }
+                return true;
+            }
+            WiFi.disconnect();
+            delay(2000);
         }
     }
 
diff --git a/lib/wificaptive/src/WifiCaptive.h b/lib/wificaptive/src/WifiCaptive.h
index 32f96830..29b44e91 100644
--- a/lib/wificaptive/src/WifiCaptive.h
+++ b/lib/wificaptive/src/WifiCaptive.h
@@ -31,8 +31,8 @@
 
 #define WIFI_SSID_KEY(i) ("wifi_" + String(i) + "_ssid").c_str()
 #define WIFI_PSWD_KEY(i) ("wifi_" + String(i) + "_pswd").c_str()
-#define WIFI_LAST_USED_SSID_KEY "wifi_ssid"
-#define WIFI_LAST_USED_PSWD_KEY "wifi_pswd"
+
+#define WIFI_LAST_INDEX "wifi_last_index"
 
 class WifiCaptive
 {
@@ -59,7 +59,6 @@ class WifiCaptive
     std::function<void()> _resetcallback;
 
     WifiCredentials _savedWifis[WIFI_MAX_SAVED_CREDS];
-    WifiCredentials _lastUsed;
 
     void setUpDNSServer(DNSServer &dnsServer, const IPAddress &localIP);
     void setUpWebserver(AsyncWebServer &server, const IPAddress &localIP);
@@ -68,7 +67,8 @@ class WifiCaptive
     uint8_t waitForConnectResult();
     void readWifiCredentials();
     void saveWifiCredentials(String ssid, String pass);
-    void saveLastUsed(String ssid, String pass);
+    void saveLastUsedWifiIndex(int index);
+    int readLastUsedWifiIndex();
     void saveApiServer(String url);
     std::vector<WifiCredentials> matchNetworks(std::vector<Network> &scanResults, WifiCaptive::WifiCredentials wifiCredentials[]);
     std::vector<Network> getScannedUniqueNetworks(bool runScan);
diff --git a/platformio.ini b/platformio.ini
index bf1e17d7..456f932b 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -60,4 +60,4 @@ lib_deps =
 	fabiobatsilva/ArduinoFake@^0.4.0
 build_flags =
 	-D ARDUINOJSON_ENABLE_ARDUINO_STRING=1
-	-std=gnu++11
\ No newline at end of file
+	-std=gnu++11


Reviews:
user: login: gdifiore
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T14:26:10Z
user: login: gdifiore
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T14:30:17Z
user: login: delhoume
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T14:59:38Z
user: login: gdifiore
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T22:41:31Z
user: login: Omeryl
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-11T20:54:56Z

Review comments:
diff_hunk: @@ -317,35 +314,46 @@ bool WifiCaptive::isSaved()
     return _savedWifis[0].ssid != "";
 }
 
-void WifiCaptive::readWifiCredentials()
+void WifiCaptive::readWifiCredentials(int index)
path: lib/wificaptive/src/WifiCaptive.cpp
user: login: gdifiore
body: @delhoume let me know if this is what you meant in your [comment](https://github.com/usetrmnl/firmware/issues/71#issuecomment-2692851881). Not sure what the benefit of getting an individual index in the array is
created_at: 2025-03-06T14:26:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -317,35 +314,46 @@ bool WifiCaptive::isSaved()
     return _savedWifis[0].ssid != "";
 }
 
-void WifiCaptive::readWifiCredentials()
+void WifiCaptive::readWifiCredentials(int index)
path: lib/wificaptive/src/WifiCaptive.cpp
user: login: gdifiore
body: If you wanted it to return the creds from a given index, it should probably be its own function. However, the array max size is so small, it's probably fine leaving functionality as iterating over the entire array (line 542)
created_at: 2025-03-06T14:30:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -317,35 +314,46 @@ bool WifiCaptive::isSaved()
     return _savedWifis[0].ssid != "";
 }
 
-void WifiCaptive::readWifiCredentials()
+void WifiCaptive::readWifiCredentials(int index)
path: lib/wificaptive/src/WifiCaptive.cpp
user: login: delhoume
body: yes I wanted to be able to iterate the array from outside.
but as the coder you have the liberty and prerogative to do as you like.
if the initial issue that was the ability to connect automatically to previous connected SSIDs is done then its ok for me

created_at: 2025-03-06T14:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -317,35 +314,46 @@ bool WifiCaptive::isSaved()
     return _savedWifis[0].ssid != "";
 }
 
-void WifiCaptive::readWifiCredentials()
+void WifiCaptive::readWifiCredentials(int index)
path: lib/wificaptive/src/WifiCaptive.cpp
user: login: gdifiore
body: I removed the indexing because I didn't use it in this PR
created_at: 2025-03-06T22:41:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: Omeryl
created_at: 2025-03-11T05:44:13Z
body: I'm reviewing this PR, have a couple things I've discovered (some of which was bad behavior from before this commit that I'm working around now!) More momentarily.
author_association: COLLABORATOR
user: login: Omeryl
created_at: 2025-03-11T07:39:01Z
body: OK, so I fixed two things:

1. The first commit fixes a weird race condition where the library will return -2 (SCAN_FAILED) for a scan just before populating the data for the scan. This appears to be so that they can prevent deadlocks, but because we're not using the event loop to check for the async response it just causes endless rescanning if you're in an area with a ton of SSIDs (like where I live). I changed the function to use the synchronous call, and also added code in case someone swapped it back to async.
2. I noticed that you didn't reimplement the last used WiFi reconnection feature, so I'd constantly always try the first one saved every time. This is rather inefficient, so I created some functions to bring back the previous behavior (by storing the last index of a successful ssid/pw combo). 

If you could give these a quick try again @gdifiore I'd appreciate it! If it looks good to you as well, I'd say this is good to merge.
author_association: COLLABORATOR
user: login: gdifiore
created_at: 2025-03-11T14:29:09Z
body: 1. I noticed that as well, but assumed I did something wrong on the connection v.s. a code issue. Thanks for fixing!
2. It does connect to last used WiFi, I just removed the specific variable for it. When it adds a new WiFi it shifts the array one over (removing the oldest entry) then adds the new credentials pair into the 0 index. 

    ~~I believe with your update if a user fills the credentials array and the most recently used is the last index, adding a new one would removed the previous most recently used. Just a UX issue~~

    I'll test and review later, but I'm fine leaving it more explicit as you changed it.
author_association: CONTRIBUTOR
user: login: Omeryl
created_at: 2025-03-11T16:15:56Z
body: For number 2, I specifically tested it for a good hour and it always went back to the last added WiFi network no matter the status. If that WiFi network is bad, it _should_ try the last successful one first. That's what my second change fixes :). I think the slight inefficiency in shifting the array after they added a new WiFi isn't the end of the world, but we could fix that.
author_association: COLLABORATOR
user: login: Omeryl
created_at: 2025-03-11T16:35:41Z
body: @gdifiore I fixed the issue with shifting credentials breaking the true last index :) 
author_association: COLLABORATOR
user: login: gdifiore
created_at: 2025-03-11T21:49:15Z
body: Ahh yeah last-used-order is different from last-added order. Thanks for catching that!

Tested it again, looks good to me.
author_association: CONTRIBUTOR
user: login: Omeryl
created_at: 2025-03-11T23:07:29Z
body: @ryanckulp Ready to merge!
author_association: COLLABORATOR