Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Net;
using System.Text;
using Daqifi.Core.Communication;
using Daqifi.Core.Communication.Messages;
Expand Down Expand Up @@ -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<ArgumentNullException>(() => ScpiMessageProducer.SetLanAddress(null!));
}

[Fact]
public void SetLanAddress_IPv6_Throws()
{
Assert.Throws<ArgumentException>(() => 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<ArgumentNullException>(() => ScpiMessageProducer.SetLanMask(null!));
}

[Fact]
public void SetLanMask_IPv6_Throws()
{
Assert.Throws<ArgumentException>(() => 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<ArgumentNullException>(() => ScpiMessageProducer.SetLanGateway(null!));
}

[Fact]
public void SetLanGateway_IPv6_Throws()
{
Assert.Throws<ArgumentException>(() => 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()
{
Expand Down
146 changes: 146 additions & 0 deletions src/Daqifi.Core.Tests/Device/Network/NetworkConfigurableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading
Loading