From 8551804bb44a2bdb5c110e013210515748fe027b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Fri, 29 May 2026 17:24:07 +0100 Subject: [PATCH 1/2] Fix WiFi helper retry/reset behavior and test API usage (#351) (cherry picked from commit f3af6adda10c3cbfd19afd486e032910606faab5) --- README.md | 53 ++++ .../NetworkHelper/WifiNetworkHelper.cs | 232 +++++++++++------- .../ConnectToWifiFixIPAddressTests.cs | 4 +- .../ConnectToWifiWithCredentialsScanTests.cs | 9 +- .../ConnectToWifiWithCredentialsTests.cs | 63 ++++- .../ConnectToWifiWithoutCredentialsTests.cs | 2 +- 6 files changed, 264 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 78623a1..9c9a9bf 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,59 @@ var success = WaitForValidIPAndDate(true, NetworkInterfaceType.Ethernet, cs.Toke // if success is true then you are connected ``` +### Retry and reconfiguration + +#### Retry after timeout + +Token-based methods (`ConnectDhcp`, `ScanAndConnectDhcp`, `ConnectFixAddress`) are retryable. If a connection attempt times out, call the method again: + +```csharp +bool connected = false; +while (!connected) +{ + CancellationTokenSource cs = new(30000); + connected = WifiNetworkHelper.ConnectDhcp(Ssid, Password, requiresDateTime: true, token: cs.Token); + if (!connected) + { + Debug.WriteLine($"Not ready, status: {WifiNetworkHelper.Status}"); + Thread.Sleep(5000); + } +} +``` + +#### Switching networks or restarting + +To switch to a different SSID or restart the event-based helper, call `Reset()` first: + +```csharp +// Disconnect from current network if needed +WifiNetworkHelper.Disconnect(); + +// Reset the helper so it can be reconfigured +WifiNetworkHelper.Reset(); + +// Connect to a different SSID +CancellationTokenSource cs = new(30000); +WifiNetworkHelper.ConnectDhcp("NewSSID", "NewPassword", token: cs.Token); +``` + +The same applies to the event-based `SetupNetworkHelper`: + +```csharp +WifiNetworkHelper.Reset(); +WifiNetworkHelper.SetupNetworkHelper("NewSSID", "NewPassword"); +``` + +#### `Reconnect()` vs. `ConnectDhcp()` + +`Reconnect()` does **not** actively send join credentials. It only waits for the platform's automatic reconnection (configured via `WifiReconnectionKind.Automatic`) to produce a valid IP address. Use it only when credentials are already stored on the device and the platform is expected to reconnect automatically. + +If you need an explicit reconnect with credentials, use `ConnectDhcp()` instead. + +#### `NetworkReady` behaviour + +When using `SetupNetworkHelper()`, `NetworkReady` is reset when the connection is lost and re-signaled when it is restored, accurately reflecting live network state. Code that previously assumed `NetworkReady` would remain set permanently after first connect should be updated to handle transient disconnects. + ## Feedback and documentation For documentation, providing feedback, issues and finding out how to contribute please refer to the [Home repo](https://github.com/nanoframework/Home). diff --git a/System.Device.Wifi/NetworkHelper/WifiNetworkHelper.cs b/System.Device.Wifi/NetworkHelper/WifiNetworkHelper.cs index ae0b274..85af7d3 100644 --- a/System.Device.Wifi/NetworkHelper/WifiNetworkHelper.cs +++ b/System.Device.Wifi/NetworkHelper/WifiNetworkHelper.cs @@ -33,7 +33,7 @@ public static class WifiNetworkHelper private static NetworkInterfaceType _workingNetworkInterface = NetworkInterfaceType.Wireless80211; /// - /// This flag will make sure there is only one and only call to any of the helper methods. + /// This flag will make sure there is only one and only one call to the event-based helper methods. /// private static bool _helperInstanciated = false; @@ -42,7 +42,12 @@ public static class WifiNetworkHelper /// /// /// The conditions for this are setup in the call to . - /// It will be a composition of network connected, IpAddress available and valid system . + /// It will be a composition of network connected, IpAddress available and valid system . + /// + /// When using , this event is reset when the connection is lost + /// and re-signaled when it is restored, accurately reflecting live network state. + /// + /// public static ManualResetEvent NetworkReady => _networkReady; /// @@ -60,7 +65,7 @@ public static class WifiNetworkHelper /// That will be the network connection to be up, having a valid IpAddress and optionally for a valid date and time to become available. /// /// Set to if valid date and time are required. - /// If any of the methods is called more than once. + /// If called more than once without an intervening call to . /// There is no network interface configured. Open the 'Edit Network Configuration' in Device Explorer and configure one. public static void SetupNetworkHelper(bool requiresDateTime = false) { @@ -78,6 +83,7 @@ public static void SetupNetworkHelper(bool requiresDateTime = false) /// /// The static IP configuration you want to apply. /// Set to if valid date and time are required. + /// If called more than once without an intervening call to . /// There is no network interface configured. Open the 'Edit Network Configuration' in Device Explorer and configure one. public static void SetupNetworkHelper( IPConfiguration ipConfiguration, @@ -100,7 +106,7 @@ public static void SetupNetworkHelper( /// The SSID of the network you are trying to connect to. /// The password associated to the SSID of the network you are trying to connect to. /// The to setup for the connection. - /// If any of the methods is called more than once. + /// If called more than once without an intervening call to . /// There is no network interface configured. Open the 'Edit Network Configuration' in Device Explorer and configure one. public static void SetupNetworkHelper( string ssid, @@ -137,6 +143,7 @@ public static void Disconnect() /// /// This method will connect the network with DHCP enabled, for your SSID and try to connect to it with the credentials you've passed. This will save as well /// the configuration of your network. + /// This method is retryable and can be called multiple times after a previous call times out or fails. /// /// The SSID of the network you are trying to connect to. /// The password associated to the SSID of the network you are trying to connect to. @@ -165,6 +172,7 @@ public static bool ConnectDhcp( /// /// This method will connect the network with the static IP address you are providing, for your SSID and try to connect to it with the credentials you've passed. This will save as well /// the configuration of your network. + /// This method is retryable and can be called multiple times after a previous call times out or fails. /// /// The SSID you are trying to connect to. /// The password associated to the SSID you are trying to connect to. @@ -195,6 +203,7 @@ public static bool ConnectFixAddress( /// /// This method will scan and connect the network with DHCP enabled, for your SSID and try to connect to it with the credentials you've passed. This will save as well /// the configuration of your network. + /// This method is retryable and can be called multiple times after a previous call times out or fails. /// /// The SSID you are trying to connect to. /// The password associated to the SSID you are trying to connect to. @@ -221,15 +230,21 @@ public static bool ScanAndConnectDhcp( token); /// - /// This method will connect the network, the information used to connect is the one already stored on the device. + /// Waits for the platform's automatic WiFi reconnection to produce a valid IP address. + /// This method does not actively send join credentials to the access point. + /// It relies on the behaviour previously configured + /// when calling or . /// + /// + /// Use this method only when the device credentials are already stored and the platform is expected + /// to reconnect automatically. If you need an explicit reconnect with credentials, call + /// or instead. + /// This method is retryable and can be called multiple times. + /// /// Set to if valid date and time are required. /// The index of the Wifi adapter to be used. Relevant only if there are multiple Wifi adapters. /// A used for timing out the operation. /// on success. On failure returns and details with the cause of the failure are made available in the property - /// - /// This function can be called only if an existing network has been setup previously and if the credentials are valid. - /// public static bool Reconnect( bool requiresDateTime = false, int wifiAdapterId = 0, @@ -255,6 +270,33 @@ public static bool Reconnect( } } + /// + /// Resets the WifiNetworkHelper to its initial state, allowing to be called again + /// or the network configuration to be changed. + /// + /// + /// Call this before switching to a different WiFi network or restarting the event-based helper. + /// This method does not disconnect the WiFi adapter; call first if needed. + /// + public static void Reset() + { + // deregister event handler to prevent a handler leak + NetworkChange.NetworkAddressChanged -= AddressChangedCallback; + + _helperInstanciated = false; + _ipAddressAvailable = null; + _networkReady = new(false); + _requiresDateTime = false; + _networkHelperStatus = NetworkHelperStatus.None; + _helperException = null; + _ipConfiguration = null; + _ssid = null; + _password = null; + + // reset the underlying NetworkHelper as well + NetworkHelper.Reset(); + } + private static bool ScanAndConnect( string ssid, string password, @@ -492,97 +534,80 @@ private static void WorkingThread() } /// - /// Perform setup of the various fields and events, along with any of the required event handlers. + /// Ensures the target Wi-Fi profile is configured and returns whether an explicit connect should be performed. /// - /// Set true to setup the events. Required for the thread approach. Not required for the CancelationToken implementation. - private static void SetupHelper(bool setupEvents) + /// Network interfaces currently available. + /// when a connect should be issued after IP setup; otherwise . + private static bool TryPrepareWifiConnection(NetworkInterface[] nis) { - if (_helperInstanciated) + if (string.IsNullOrEmpty(_ssid) || string.IsNullOrEmpty(_password)) { - throw new InvalidOperationException(); + return false; } - else + + foreach (NetworkInterface ni in nis) + { + if (ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) + { + Wireless80211Configuration wc = Wireless80211Configuration.GetAllWireless80211Configurations()[ni.SpecificConfigId]; + + if (wc.Ssid == _ssid) + { + return false; + } + } + } + + _wifi.Disconnect(); + + foreach (NetworkInterface ni in nis) { + if (ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) + { + StoreWifiConfiguration(ni); + return true; + } + } + + return false; + } + + /// + /// Perform setup of the various fields and events, along with any of the required event handlers. + /// + /// Set to setup the events and background thread. Required for the event-based approach. Not required for the CancellationToken approach. + private static void SetupHelper(bool setupEvents) + { + if (setupEvents) + { + if (_helperInstanciated) + { + throw new InvalidOperationException(); + } + // set flag _helperInstanciated = true; - // flag to connect to Wifi after IP setup - bool connectToWifi = false; - // setup event _ipAddressAvailable = new(false); - // currently we only support one Wifi adapter, so this is it _wifi = WifiAdapter.FindAllAdapters()[0]; NetworkInterface[] nis = NetworkInterface.GetAllNetworkInterfaces(); - if (setupEvents) + // check if there are any network interface setup + if (nis.Length == 0) { - // check if there are any network interface setup - if (nis.Length == 0) - { - _networkHelperStatus = NetworkHelperStatus.FailedNoNetworkInterface; - - throw new NotSupportedException(); - } + _networkHelperStatus = NetworkHelperStatus.FailedNoNetworkInterface; - // setup handler - NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(AddressChangedCallback); + throw new NotSupportedException(); } - if (!string.IsNullOrEmpty(_ssid) && - !string.IsNullOrEmpty(_password)) - { - bool isAlreadyConnected = false; + // setup handler + NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(AddressChangedCallback); - // this is to connect to a specific Wifi network - // check if device it's already connected to the correct network - foreach (NetworkInterface ni in nis) - { - if (ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) - { - Wireless80211Configuration wc = Wireless80211Configuration.GetAllWireless80211Configurations()[ni.SpecificConfigId]; - - // Let's make sure this configuration is saved - if (wc.Ssid == _ssid) - { - isAlreadyConnected = true; - break; - } - } - } - - if (!isAlreadyConnected) - { - _wifi.Disconnect(); - isAlreadyConnected = false; - } - - if (!isAlreadyConnected) - { - nis = NetworkInterface.GetAllNetworkInterfaces(); - - foreach (NetworkInterface ni in nis) - { - if (ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) - { - _wifi.Disconnect(); - - // Make sure we store the configuration - StoreWifiConfiguration(ni); - - // set flag to connect to Wifi after IP config - connectToWifi = true; - - // done here - break; - } - } - } - - } + bool connectToWifi = TryPrepareWifiConnection(nis); NetworkHelperInternal.InternalSetupHelper( nis, @@ -598,6 +623,18 @@ private static void SetupHelper(bool setupEvents) _password); } + // update status + _networkHelperStatus = NetworkHelperStatus.Started; + } + else + { + NetworkInterface[] nis = NetworkInterface.GetAllNetworkInterfaces(); + + NetworkHelperInternal.InternalSetupHelper( + nis, + _workingNetworkInterface, + _ipConfiguration); + // update status _networkHelperStatus = NetworkHelperStatus.Started; } @@ -609,26 +646,39 @@ private static void AddressChangedCallback(object sender, EventArgs e) _workingNetworkInterface, _ipConfiguration)) { - _ipAddressAvailable.Set(); + if (_ipAddressAvailable != null) + { + _ipAddressAvailable.Set(); + } + + // re-signal ready; check DateTime condition in case it was required + if (!_requiresDateTime || DateTime.UtcNow.Year >= 2021) + { + _networkReady.Set(); + _networkHelperStatus = NetworkHelperStatus.NetworkIsReady; + } + } + else + { + // IP was lost — reset signals so callers block until the connection is restored + _networkReady.Reset(); + + if (_ipAddressAvailable != null) + { + _ipAddressAvailable.Reset(); + } + + _networkHelperStatus = NetworkHelperStatus.Reconnecting; } } /// - /// Method to reset internal fields to it's defaults + /// Method to reset internal fields to their defaults. /// ONLY TO BE USED BY UNIT TESTS /// internal static void ResetInstance() { - _ipAddressAvailable = null; - _networkReady = new(false); - _requiresDateTime = false; - _networkHelperStatus = NetworkHelperStatus.None; - _helperException = null; - _ipConfiguration = null; - _helperInstanciated = false; - _ssid = null; - _password = null; - NetworkHelper.ResetInstance(); + Reset(); } } -} +} \ No newline at end of file diff --git a/Tests/NFUnitTestWifiConnection/ConnectToWifiFixIPAddressTests.cs b/Tests/NFUnitTestWifiConnection/ConnectToWifiFixIPAddressTests.cs index 45d26bb..5f89876 100644 --- a/Tests/NFUnitTestWifiConnection/ConnectToWifiFixIPAddressTests.cs +++ b/Tests/NFUnitTestWifiConnection/ConnectToWifiFixIPAddressTests.cs @@ -46,7 +46,7 @@ public void TestFixIPAddress_01() Assert.IsNull(WifiNetworkHelper.HelperException); // need to reset this internal flag to allow calling the NetworkHelper again - WifiNetworkHelper.ResetInstance(); + WifiNetworkHelper.Reset(); } [TestMethod] @@ -62,7 +62,7 @@ public void TestFixedIPAddress_02() Assert.IsTrue(WifiNetworkHelper.NetworkReady.WaitOne(10000, true)); // need to reset this internal flag to allow calling the NetworkHelper again - WifiNetworkHelper.ResetInstance(); + WifiNetworkHelper.Reset(); } } } diff --git a/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsScanTests.cs b/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsScanTests.cs index 07d27ad..5516fb9 100644 --- a/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsScanTests.cs +++ b/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsScanTests.cs @@ -42,7 +42,7 @@ public void TestNormalConnectionScanAndConnect() Assert.IsNull(WifiNetworkHelper.HelperException); // need to reset this internal flag to allow calling the NetworkHelper again - WifiNetworkHelper.ResetInstance(); + WifiNetworkHelper.Reset(); } [TestMethod] @@ -58,7 +58,7 @@ public void TestDhcp_01() Assert.IsNull(WifiNetworkHelper.HelperException); // need to reset this internal flag to allow calling the NetworkHelper again - WifiNetworkHelper.ResetInstance(); + WifiNetworkHelper.Reset(); } [TestMethod] @@ -70,7 +70,7 @@ public void TestDhcp_02() Assert.IsTrue(WifiNetworkHelper.NetworkReady.WaitOne(10000, true)); // need to reset this internal flag to allow calling the NetworkHelper again - WifiNetworkHelper.ResetInstance(); + WifiNetworkHelper.Reset(); } [TestMethod] @@ -84,6 +84,9 @@ public void TestSingleUsage() // call twice, it's a NO NO and should throw an exception WifiNetworkHelper.SetupNetworkHelper(); }); + + // clear static state so this test doesn't affect later tests + WifiNetworkHelper.Reset(); } } } diff --git a/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsTests.cs b/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsTests.cs index ade89f3..98eac38 100644 --- a/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsTests.cs +++ b/Tests/NFUnitTestWifiConnection/ConnectToWifiWithCredentialsTests.cs @@ -57,8 +57,67 @@ public void TestNormalConnection() Assert.IsTrue(success); Assert.IsNull(WifiNetworkHelper.HelperException); - // need to reset this internal flag to allow calling the NetworkHelper again - WifiNetworkHelper.ResetInstance(); + WifiNetworkHelper.Reset(); + } + + [TestMethod] + public void TestRetryAfterTimeout() + { + // First attempt: very short timeout so it expires + CancellationTokenSource cs1 = new(1000); + var firstResult = WifiNetworkHelper.ConnectDhcp( + Ssid, + Password, + token: cs1.Token); + + Assert.IsFalse(firstResult, "First call should have timed out"); + + // Second attempt with a longer timeout — must not throw InvalidOperationException + CancellationTokenSource cs2 = new(15000); + var secondResult = WifiNetworkHelper.ConnectDhcp( + Ssid, + Password, + requiresDateTime: true, + token: cs2.Token); + + DisplayLastError(secondResult); + Assert.IsTrue(secondResult, "Second attempt should succeed after retry"); + + WifiNetworkHelper.Reset(); + } + + [TestMethod] + public void TestResetAllowsEventBasedRestart() + { + // Use event-based helper once + WifiNetworkHelper.SetupNetworkHelper(Ssid, Password); + + bool connected = WifiNetworkHelper.NetworkReady.WaitOne(15000, true); + Assert.IsTrue(connected, "Expected to connect on first event-based attempt"); + + // Reset and restart with same credentials + WifiNetworkHelper.Reset(); + WifiNetworkHelper.SetupNetworkHelper(Ssid, Password); + + connected = WifiNetworkHelper.NetworkReady.WaitOne(15000, true); + Assert.IsTrue(connected, "Expected to connect after Reset + SetupNetworkHelper restart"); + + WifiNetworkHelper.Reset(); + } + + [TestMethod] + public void TestSingleUsageEventBased() + { + Assert.ThrowsException(typeof(System.InvalidOperationException), () => + { + // First call is OK + WifiNetworkHelper.SetupNetworkHelper(Ssid, Password); + + // Second call without Reset must throw + WifiNetworkHelper.SetupNetworkHelper(Ssid, Password); + }); + + WifiNetworkHelper.Reset(); } } } diff --git a/Tests/NFUnitTestWifiConnection/ConnectToWifiWithoutCredentialsTests.cs b/Tests/NFUnitTestWifiConnection/ConnectToWifiWithoutCredentialsTests.cs index b508b91..af30441 100644 --- a/Tests/NFUnitTestWifiConnection/ConnectToWifiWithoutCredentialsTests.cs +++ b/Tests/NFUnitTestWifiConnection/ConnectToWifiWithoutCredentialsTests.cs @@ -34,7 +34,7 @@ public void TestReconnection() Assert.IsNull(WifiNetworkHelper.HelperException); // need to reset this internal flag to allow calling the NetworkHelper again - WifiNetworkHelper.ResetInstance(); + WifiNetworkHelper.Reset(); } } } From 7fe49b94f16a5ecbeec7fbff5f68aacda9a59fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Fri, 29 May 2026 17:42:05 +0100 Subject: [PATCH 2/2] Bump native assembly version --- System.Device.Wifi/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/System.Device.Wifi/Properties/AssemblyInfo.cs b/System.Device.Wifi/Properties/AssemblyInfo.cs index 2542723..fc60cdc 100644 --- a/System.Device.Wifi/Properties/AssemblyInfo.cs +++ b/System.Device.Wifi/Properties/AssemblyInfo.cs @@ -12,7 +12,7 @@ //////////////////////////////////////////////////////////////// // update this whenever the native assembly signature changes // -[assembly: AssemblyNativeVersion("100.2.0.0")] +[assembly: AssemblyNativeVersion("100.2.0.1")] //////////////////////////////////////////////////////////////// // Setting ComVisible to false makes the types in this assembly not visible