diff --git a/README.md b/README.md index e7aeb7b..efaef48 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The DAQiFi Core Library is a .NET library designed to simplify interaction with - **Firmware Update Orchestration**: Programmatic PIC32 bootloader updates with state/progress reporting, timeout/retry handling, and cancellation support - **Transport Layer**: TCP, UDP, and Serial communication with async/await patterns - **Protocol Buffers**: Efficient binary message serialization for device communication -- **Network Configuration**: Programmatic WiFi SSID/password/mode updates via `INetworkConfigurable` +- **Network Configuration**: Programmatic WiFi SSID/password/mode and LAN static IP/subnet/gateway updates via `INetworkConfigurable` - **Cross-Platform**: Compatible with .NET 9.0 and .NET 10.0 ## Getting Started @@ -173,6 +173,24 @@ if (device is INetworkConfigurable networkDevice) } ``` +To assign a static LAN address, set `StaticIP` / `SubnetMask` / `Gateway` (IPv4 only). Leaving any of them `null` means "leave unchanged" so DHCP-only callers see no behavior change: + +```csharp +using System.Net; +using Daqifi.Core.Device.Network; + +var config = new NetworkConfiguration +{ + Ssid = "MyNetwork", + Password = "secret", + Mode = WifiMode.ExistingNetwork, + StaticIP = IPAddress.Parse("192.168.1.42"), + SubnetMask = IPAddress.Parse("255.255.255.0"), + Gateway = IPAddress.Parse("192.168.1.1"), +}; +await networkDevice.UpdateNetworkConfigurationAsync(config); +``` + ### Firmware Update Orchestration The core library exposes `IFirmwareUpdateService` for update orchestration: diff --git a/src/Daqifi.Core.Tests/Communication/Producers/ScpiMessageProducerTests.cs b/src/Daqifi.Core.Tests/Communication/Producers/ScpiMessageProducerTests.cs index cad79ab..c386d6e 100644 --- a/src/Daqifi.Core.Tests/Communication/Producers/ScpiMessageProducerTests.cs +++ b/src/Daqifi.Core.Tests/Communication/Producers/ScpiMessageProducerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Text; using Daqifi.Core.Communication; using Daqifi.Core.Communication.Messages; @@ -236,6 +237,114 @@ public void SetNetworkWifiPassword_ReturnsCorrectCommand() AssertMessageFormat(message); } + [Fact] + public void SetLanAddress_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.SetLanAddress(IPAddress.Parse("192.168.1.42")); + Assert.Equal("SYSTem:COMMunicate:LAN:ADDRess \"192.168.1.42\"", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void SetLanAddress_NullAddress_Throws() + { + Assert.Throws(() => ScpiMessageProducer.SetLanAddress(null!)); + } + + [Fact] + public void SetLanAddress_IPv6_Throws() + { + Assert.Throws(() => ScpiMessageProducer.SetLanAddress(IPAddress.IPv6Loopback)); + } + + [Fact] + public void SetLanMask_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.SetLanMask(IPAddress.Parse("255.255.255.0")); + Assert.Equal("SYSTem:COMMunicate:LAN:MASK \"255.255.255.0\"", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void SetLanMask_NullMask_Throws() + { + Assert.Throws(() => ScpiMessageProducer.SetLanMask(null!)); + } + + [Fact] + public void SetLanMask_IPv6_Throws() + { + Assert.Throws(() => ScpiMessageProducer.SetLanMask(IPAddress.IPv6Loopback)); + } + + [Fact] + public void SetLanGateway_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.SetLanGateway(IPAddress.Parse("192.168.1.1")); + Assert.Equal("SYSTem:COMMunicate:LAN:GATEway \"192.168.1.1\"", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void SetLanGateway_NullGateway_Throws() + { + Assert.Throws(() => ScpiMessageProducer.SetLanGateway(null!)); + } + + [Fact] + public void SetLanGateway_IPv6_Throws() + { + Assert.Throws(() => ScpiMessageProducer.SetLanGateway(IPAddress.IPv6Loopback)); + } + + [Fact] + public void GetLanAddress_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.GetLanAddress; + Assert.Equal("SYSTem:COMMunicate:LAN:ADDRess?", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void GetLanMask_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.GetLanMask; + Assert.Equal("SYSTem:COMMunicate:LAN:MASK?", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void GetLanGateway_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.GetLanGateway; + Assert.Equal("SYSTem:COMMunicate:LAN:GATEway?", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void GetLanConfiguredAddress_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.GetLanConfiguredAddress; + Assert.Equal("SYSTem:COMMunicate:LAN:CONFigure:ADDRess?", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void GetLanConfiguredMask_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.GetLanConfiguredMask; + Assert.Equal("SYSTem:COMMunicate:LAN:CONFigure:MASK?", message.Data); + AssertMessageFormat(message); + } + + [Fact] + public void GetLanConfiguredGateway_ReturnsCorrectCommand() + { + var message = ScpiMessageProducer.GetLanConfiguredGateway; + Assert.Equal("SYSTem:COMMunicate:LAN:CONFigure:GATEway?", message.Data); + AssertMessageFormat(message); + } + [Fact] public void DisableNetworkLan_ReturnsCorrectCommand() { diff --git a/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurableTests.cs b/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurableTests.cs index 95a0808..3110b91 100644 --- a/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurableTests.cs +++ b/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurableTests.cs @@ -152,6 +152,152 @@ public async Task UpdateNetworkConfigurationAsync_UpdatesLocalConfiguration() Assert.Equal("UpdatedPassword", device.NetworkConfiguration.Password); } + [Fact] + public async Task UpdateNetworkConfigurationAsync_WithStaticIP_SendsAddressMaskGatewayBeforeApply() + { + // Arrange + var device = new TestableDaqifiStreamingDevice("TestDevice"); + device.Connect(); + var config = new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "Net", + "Pass", + IPAddress.Parse("10.0.0.5"), + IPAddress.Parse("255.255.255.0"), + IPAddress.Parse("10.0.0.1")); + + // Act + await device.UpdateNetworkConfigurationAsync(config); + + // Assert + var sentCommands = device.SentMessages.Select(m => m.Data).ToList(); + + Assert.Contains("SYSTem:COMMunicate:LAN:ADDRess \"10.0.0.5\"", sentCommands); + Assert.Contains("SYSTem:COMMunicate:LAN:MASK \"255.255.255.0\"", sentCommands); + Assert.Contains("SYSTem:COMMunicate:LAN:GATEway \"10.0.0.1\"", sentCommands); + + // Apply must follow all three (firmware reads runtime config at APPLY time). + var applyIndex = sentCommands.IndexOf("SYSTem:COMMunicate:LAN:APPLY"); + Assert.True(applyIndex > sentCommands.IndexOf("SYSTem:COMMunicate:LAN:ADDRess \"10.0.0.5\"")); + Assert.True(applyIndex > sentCommands.IndexOf("SYSTem:COMMunicate:LAN:MASK \"255.255.255.0\"")); + Assert.True(applyIndex > sentCommands.IndexOf("SYSTem:COMMunicate:LAN:GATEway \"10.0.0.1\"")); + + // SAVE persists what APPLY pushed, so it must come after every static-IP setter too. + var saveIndex = sentCommands.IndexOf("SYSTem:COMMunicate:LAN:SAVE"); + Assert.True(saveIndex > sentCommands.IndexOf("SYSTem:COMMunicate:LAN:ADDRess \"10.0.0.5\"")); + Assert.True(saveIndex > sentCommands.IndexOf("SYSTem:COMMunicate:LAN:MASK \"255.255.255.0\"")); + Assert.True(saveIndex > sentCommands.IndexOf("SYSTem:COMMunicate:LAN:GATEway \"10.0.0.1\"")); + } + + [Fact] + public async Task UpdateNetworkConfigurationAsync_WithoutStaticIP_DoesNotSendAddressMaskGateway() + { + // Arrange + var device = new TestableDaqifiStreamingDevice("TestDevice"); + device.Connect(); + var config = new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "Net", + "Pass"); + + // Act + await device.UpdateNetworkConfigurationAsync(config); + + // Assert + var sentCommands = device.SentMessages.Select(m => m.Data).ToList(); + + Assert.DoesNotContain(sentCommands, c => c.StartsWith("SYSTem:COMMunicate:LAN:ADDRess ")); + Assert.DoesNotContain(sentCommands, c => c.StartsWith("SYSTem:COMMunicate:LAN:MASK ")); + Assert.DoesNotContain(sentCommands, c => c.StartsWith("SYSTem:COMMunicate:LAN:GATEway ")); + } + + [Fact] + public async Task UpdateNetworkConfigurationAsync_WithPartialStaticIP_OnlySendsNonNullFields() + { + // Arrange + var device = new TestableDaqifiStreamingDevice("TestDevice"); + device.Connect(); + var config = new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "Net", + "Pass", + IPAddress.Parse("10.0.0.5"), + subnetMask: null, + gateway: null); + + // Act + await device.UpdateNetworkConfigurationAsync(config); + + // Assert + var sentCommands = device.SentMessages.Select(m => m.Data).ToList(); + + Assert.Contains("SYSTem:COMMunicate:LAN:ADDRess \"10.0.0.5\"", sentCommands); + Assert.DoesNotContain(sentCommands, c => c.StartsWith("SYSTem:COMMunicate:LAN:MASK ")); + Assert.DoesNotContain(sentCommands, c => c.StartsWith("SYSTem:COMMunicate:LAN:GATEway ")); + } + + [Fact] + public async Task UpdateNetworkConfigurationAsync_WithNullStaticFields_PreservesPreviouslyCachedValues() + { + // Arrange — first call seeds the cache with a known static IP. + var device = new TestableDaqifiStreamingDevice("TestDevice"); + device.Connect(); + var originalStaticIP = IPAddress.Parse("10.0.0.5"); + var originalSubnet = IPAddress.Parse("255.255.255.0"); + var originalGateway = IPAddress.Parse("10.0.0.1"); + await device.UpdateNetworkConfigurationAsync(new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "Net", + "Pass", + originalStaticIP, + originalSubnet, + originalGateway)); + + // Act — second call only changes WiFi settings; static IP fields are + // null which means "leave unchanged". The cache must not be cleared. + await device.UpdateNetworkConfigurationAsync(new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "OtherNet", + "OtherPass")); + + // Assert + Assert.Equal(originalStaticIP, device.NetworkConfiguration.StaticIP); + Assert.Equal(originalSubnet, device.NetworkConfiguration.SubnetMask); + Assert.Equal(originalGateway, device.NetworkConfiguration.Gateway); + } + + [Fact] + public async Task UpdateNetworkConfigurationAsync_WithStaticIP_UpdatesLocalConfiguration() + { + // Arrange + var device = new TestableDaqifiStreamingDevice("TestDevice"); + device.Connect(); + var staticIP = IPAddress.Parse("10.0.0.5"); + var subnet = IPAddress.Parse("255.255.255.0"); + var gateway = IPAddress.Parse("10.0.0.1"); + var config = new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "Net", + "Pass", + staticIP, + subnet, + gateway); + + // Act + await device.UpdateNetworkConfigurationAsync(config); + + // Assert + Assert.Equal(staticIP, device.NetworkConfiguration.StaticIP); + Assert.Equal(subnet, device.NetworkConfiguration.SubnetMask); + Assert.Equal(gateway, device.NetworkConfiguration.Gateway); + } + [Fact] public async Task UpdateNetworkConfigurationAsync_PreparesLanInterface() { diff --git a/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurationTests.cs b/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurationTests.cs index eea24bf..5648c1c 100644 --- a/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurationTests.cs +++ b/src/Daqifi.Core.Tests/Device/Network/NetworkConfigurationTests.cs @@ -1,3 +1,4 @@ +using System.Net; using Daqifi.Core.Device.Network; using Xunit; @@ -16,6 +17,9 @@ public void DefaultConstructor_InitializesWithDefaultValues() Assert.Equal(WifiSecurityType.WpaPskPhrase, config.SecurityType); Assert.Equal(string.Empty, config.Ssid); Assert.Equal(string.Empty, config.Password); + Assert.Null(config.StaticIP); + Assert.Null(config.SubnetMask); + Assert.Null(config.Gateway); } [Fact] @@ -80,6 +84,102 @@ public void Properties_CanBeModified() Assert.Equal("NewSSID", config.Ssid); Assert.Equal("NewPassword", config.Password); } + + [Fact] + public void StaticIPConstructor_SetsAllProperties() + { + // Arrange + var staticIP = IPAddress.Parse("10.0.0.5"); + var subnet = IPAddress.Parse("255.255.255.0"); + var gateway = IPAddress.Parse("10.0.0.1"); + + // Act + var config = new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "Net", + "Pass", + staticIP, + subnet, + gateway); + + // Assert + Assert.Equal(WifiMode.ExistingNetwork, config.Mode); + Assert.Equal(WifiSecurityType.WpaPskPhrase, config.SecurityType); + Assert.Equal("Net", config.Ssid); + Assert.Equal("Pass", config.Password); + Assert.Equal(staticIP, config.StaticIP); + Assert.Equal(subnet, config.SubnetMask); + Assert.Equal(gateway, config.Gateway); + } + + [Fact] + public void StaticIPProperties_CanBeAssignedAndCleared() + { + // Arrange + var config = new NetworkConfiguration + { + StaticIP = IPAddress.Parse("192.168.1.10"), + SubnetMask = IPAddress.Parse("255.255.255.0"), + Gateway = IPAddress.Parse("192.168.1.1") + }; + + // Act + config.StaticIP = null; + config.SubnetMask = null; + config.Gateway = null; + + // Assert + Assert.Null(config.StaticIP); + Assert.Null(config.SubnetMask); + Assert.Null(config.Gateway); + } + + [Fact] + public void Clone_PreservesStaticIPFields() + { + // Arrange + var original = new NetworkConfiguration( + WifiMode.ExistingNetwork, + WifiSecurityType.WpaPskPhrase, + "Net", + "Pass", + IPAddress.Parse("10.0.0.5"), + IPAddress.Parse("255.255.255.0"), + IPAddress.Parse("10.0.0.1")); + + // Act + var clone = original.Clone(); + + // Assert + Assert.Equal(original.StaticIP, clone.StaticIP); + Assert.Equal(original.SubnetMask, clone.SubnetMask); + Assert.Equal(original.Gateway, clone.Gateway); + + // Verify independence — replacing the clone's references should not + // affect the original. + clone.StaticIP = IPAddress.Parse("172.16.0.5"); + Assert.NotEqual(original.StaticIP, clone.StaticIP); + } + + [Fact] + public void Clone_PreservesNullStaticIPFields() + { + // Arrange + var original = new NetworkConfiguration( + WifiMode.SelfHosted, + WifiSecurityType.None, + "Net", + string.Empty); + + // Act + var clone = original.Clone(); + + // Assert + Assert.Null(clone.StaticIP); + Assert.Null(clone.SubnetMask); + Assert.Null(clone.Gateway); + } } public class WifiModeTests diff --git a/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs b/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs index 3b61435..1b436a7 100644 --- a/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs +++ b/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs @@ -1,4 +1,6 @@ using System; +using System.Net; +using System.Net.Sockets; using Daqifi.Core.Communication.Messages; namespace Daqifi.Core.Communication.Producers; @@ -459,6 +461,117 @@ public static IOutboundMessage SetNetworkWifiPassword(string password) return new ScpiMessage($"SYSTem:COMMunicate:LAN:PASs \"{password}\""); } + /// + /// Creates a command message to set the static IP address for the LAN. + /// + /// The static IP address to assign to the device. + /// + /// The new value is staged into the device's runtime WiFi settings and takes + /// effect on the next . Persist across reboots + /// with . + /// Command: SYSTem:COMMunicate:LAN:ADDRess "x.x.x.x" + /// + public static IOutboundMessage SetLanAddress(IPAddress address) + { + RequireIPv4(address, nameof(address)); + return new ScpiMessage($"SYSTem:COMMunicate:LAN:ADDRess \"{address}\""); + } + + /// + /// Creates a command message to set the subnet mask for the LAN. + /// + /// The subnet mask to assign to the device. + /// + /// Command: SYSTem:COMMunicate:LAN:MASK "x.x.x.x" + /// + public static IOutboundMessage SetLanMask(IPAddress mask) + { + RequireIPv4(mask, nameof(mask)); + return new ScpiMessage($"SYSTem:COMMunicate:LAN:MASK \"{mask}\""); + } + + /// + /// Creates a command message to set the default gateway for the LAN. + /// + /// The default gateway to assign to the device. + /// + /// Command: SYSTem:COMMunicate:LAN:GATEway "x.x.x.x" + /// + public static IOutboundMessage SetLanGateway(IPAddress gateway) + { + RequireIPv4(gateway, nameof(gateway)); + return new ScpiMessage($"SYSTem:COMMunicate:LAN:GATEway \"{gateway}\""); + } + + // Firmware's LAN setters parse the payload via inet_addr (IPv4 dotted quad + // only — see SCPILAN.c's SCPI_LANAddrSetImpl). IPv6 inputs would stringify + // with colons and silently mis-set on the device, so reject them here. + private static void RequireIPv4(IPAddress address, string paramName) + { + if (address == null) + { + throw new ArgumentNullException(paramName); + } + + if (address.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentException( + "Address must be IPv4 (AddressFamily.InterNetwork).", + paramName); + } + } + + /// + /// Creates a query message to read the staged (pre-apply) LAN IP address. + /// + /// + /// Returns the address currently in the device's runtime WiFi settings — the value + /// that would push next, which may differ from the + /// active address reported by . + /// Command: SYSTem:COMMunicate:LAN:CONFigure:ADDRess? + /// + public static IOutboundMessage GetLanConfiguredAddress => new ScpiMessage("SYSTem:COMMunicate:LAN:CONFigure:ADDRess?"); + + /// + /// Creates a query message to read the staged (pre-apply) LAN subnet mask. + /// + /// + /// Command: SYSTem:COMMunicate:LAN:CONFigure:MASK? + /// + public static IOutboundMessage GetLanConfiguredMask => new ScpiMessage("SYSTem:COMMunicate:LAN:CONFigure:MASK?"); + + /// + /// Creates a query message to read the staged (pre-apply) LAN default gateway. + /// + /// + /// Command: SYSTem:COMMunicate:LAN:CONFigure:GATEway? + /// + public static IOutboundMessage GetLanConfiguredGateway => new ScpiMessage("SYSTem:COMMunicate:LAN:CONFigure:GATEway?"); + + /// + /// Creates a query message to read the active LAN IP address. + /// + /// + /// Command: SYSTem:COMMunicate:LAN:ADDRess? + /// + public static IOutboundMessage GetLanAddress => new ScpiMessage("SYSTem:COMMunicate:LAN:ADDRess?"); + + /// + /// Creates a query message to read the active LAN subnet mask. + /// + /// + /// Command: SYSTem:COMMunicate:LAN:MASK? + /// + public static IOutboundMessage GetLanMask => new ScpiMessage("SYSTem:COMMunicate:LAN:MASK?"); + + /// + /// Creates a query message to read the active LAN default gateway. + /// + /// + /// Command: SYSTem:COMMunicate:LAN:GATEway? + /// + public static IOutboundMessage GetLanGateway => new ScpiMessage("SYSTem:COMMunicate:LAN:GATEway?"); + /// /// Creates a command message to disable LAN communication. /// diff --git a/src/Daqifi.Core/Device/DaqifiStreamingDevice.cs b/src/Daqifi.Core/Device/DaqifiStreamingDevice.cs index 5c7a362..bb61173 100644 --- a/src/Daqifi.Core/Device/DaqifiStreamingDevice.cs +++ b/src/Daqifi.Core/Device/DaqifiStreamingDevice.cs @@ -235,6 +235,22 @@ public async Task UpdateNetworkConfigurationAsync(NetworkConfiguration configura throw new ArgumentOutOfRangeException(nameof(configuration), configuration.SecurityType, "Unsupported WiFi security type."); } + // Stage static IP fields (firmware writes these into the runtime + // WiFi settings that ApplyNetworkLan consumes). Skip any field the + // caller left null so DHCP-only callers see no behavior change. + if (configuration.StaticIP != null) + { + Send(ScpiMessageProducer.SetLanAddress(configuration.StaticIP)); + } + if (configuration.SubnetMask != null) + { + Send(ScpiMessageProducer.SetLanMask(configuration.SubnetMask)); + } + if (configuration.Gateway != null) + { + Send(ScpiMessageProducer.SetLanGateway(configuration.Gateway)); + } + // Apply configuration Send(ScpiMessageProducer.ApplyNetworkLan); @@ -247,11 +263,25 @@ public async Task UpdateNetworkConfigurationAsync(NetworkConfiguration configura // Save configuration to persist across restarts Send(ScpiMessageProducer.SaveNetworkLan); - // Update local configuration + // Update local configuration. Static IP fields use null = "leave + // unchanged" semantics, so only overwrite when the caller provided + // a value — otherwise we'd clobber the previously known static IP. _networkConfiguration.Mode = configuration.Mode; _networkConfiguration.SecurityType = configuration.SecurityType; _networkConfiguration.Ssid = configuration.Ssid; _networkConfiguration.Password = configuration.Password; + if (configuration.StaticIP != null) + { + _networkConfiguration.StaticIP = configuration.StaticIP; + } + if (configuration.SubnetMask != null) + { + _networkConfiguration.SubnetMask = configuration.SubnetMask; + } + if (configuration.Gateway != null) + { + _networkConfiguration.Gateway = configuration.Gateway; + } } /// diff --git a/src/Daqifi.Core/Device/Network/INetworkConfigurable.cs b/src/Daqifi.Core/Device/Network/INetworkConfigurable.cs index 88ddf3e..af55f3c 100644 --- a/src/Daqifi.Core/Device/Network/INetworkConfigurable.cs +++ b/src/Daqifi.Core/Device/Network/INetworkConfigurable.cs @@ -29,6 +29,7 @@ public interface INetworkConfigurable /// Sets the SSID /// Sets the security type /// Sets the password (if security is enabled) + /// Sets the static IP, subnet mask, and gateway (only those provided as non-null) /// Applies the LAN configuration /// Waits for the WiFi module to restart /// Re-enables the LAN interface diff --git a/src/Daqifi.Core/Device/Network/NetworkConfiguration.cs b/src/Daqifi.Core/Device/Network/NetworkConfiguration.cs index 8e35a43..b55b0c3 100644 --- a/src/Daqifi.Core/Device/Network/NetworkConfiguration.cs +++ b/src/Daqifi.Core/Device/Network/NetworkConfiguration.cs @@ -1,3 +1,5 @@ +using System.Net; + namespace Daqifi.Core.Device.Network { /// @@ -29,6 +31,33 @@ public class NetworkConfiguration /// public string Password { get; set; } = string.Empty; + /// + /// Gets or sets the static IP address to assign to the device. + /// + /// + /// A null value means "leave unchanged" — the device retains its current + /// IP configuration (typically DHCP). Provide all three of + /// , , and + /// to fully define a static IP configuration. + /// + public IPAddress? StaticIP { get; set; } + + /// + /// Gets or sets the subnet mask to use with . + /// + /// + /// A null value means "leave unchanged" on the device. + /// + public IPAddress? SubnetMask { get; set; } + + /// + /// Gets or sets the default gateway to use with . + /// + /// + /// A null value means "leave unchanged" on the device. + /// + public IPAddress? Gateway { get; set; } + /// /// Initializes a new instance of the class /// with default values. @@ -39,7 +68,7 @@ public NetworkConfiguration() /// /// Initializes a new instance of the class - /// with the specified settings. + /// with the specified WiFi settings. /// /// The WiFi operating mode. /// The WiFi security type. @@ -53,13 +82,49 @@ public NetworkConfiguration(WifiMode mode, WifiSecurityType securityType, string Password = password; } + /// + /// Initializes a new instance of the class + /// with the specified WiFi and static IP settings. + /// + /// The WiFi operating mode. + /// The WiFi security type. + /// The network SSID. + /// The network password. + /// The static IP address, or null to leave unchanged. + /// The subnet mask, or null to leave unchanged. + /// The default gateway, or null to leave unchanged. + public NetworkConfiguration( + WifiMode mode, + WifiSecurityType securityType, + string ssid, + string password, + IPAddress? staticIP, + IPAddress? subnetMask, + IPAddress? gateway) + { + Mode = mode; + SecurityType = securityType; + Ssid = ssid; + Password = password; + StaticIP = staticIP; + SubnetMask = subnetMask; + Gateway = gateway; + } + /// /// Creates a copy of this network configuration. /// /// A new instance with the same values. public NetworkConfiguration Clone() { - return new NetworkConfiguration(Mode, SecurityType, Ssid, Password); + return new NetworkConfiguration( + Mode, + SecurityType, + Ssid, + Password, + StaticIP, + SubnetMask, + Gateway); } } }