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
47 changes: 47 additions & 0 deletions docs/DEVICE_INTERFACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,53 @@ catch (OperationCanceledException)
}
```

#### Telling "the device went away" apart from a bug

Every device operation opens with a connectivity guard. When it fails, it throws
`DeviceNotConnectedException` — a typed exception, so a disconnect can be classified without
matching on the exception message:

```csharp
using Daqifi.Core.Device;
using Daqifi.Core.Device.SdCard;

var sdCard = (ISdCardOperations)device;

try
{
IReadOnlyList<SdCardFileInfo> files = await sdCard.GetSdCardFilesAsync();
}
catch (DeviceNotConnectedException ex)
{
// Ordinary and expected: the user pressed Disconnect mid-refresh, or WiFi dropped.
// Log at warning with reconnect guidance — do NOT raise an error-tracker issue.
logger.LogWarning(ex, ex.IsShuttingDown
? "Device is disconnecting; skipping refresh."
: "Device is not connected. Reconnect and try again.");
}
catch (InvalidOperationException ex)
{
// Everything else on this path really is a defect — e.g. calling a text command
// re-entrantly, or using a device constructed without a transport.
logger.LogError(ex, "Bug: invalid SD card operation.");
}
```

Notes:

- `DeviceNotConnectedException` derives from `InvalidOperationException`, which is what these
guards threw before, so existing `catch (InvalidOperationException)` blocks keep working. Order
the `catch` clauses most-specific-first, as above.
- `IsShuttingDown` is `true` when the guard fired because a `Disconnect()` or `Dispose()` is in
flight (or already finished) rather than because the device was never connected. Both mean "the
device is unavailable"; the flag is there for callers that want to suppress a retry prompt when
the user initiated the disconnect themselves.
- `TransportNotConnectedException` is its sibling, not its base: it reports that the underlying
stream is gone (a serial unplug, a dropped TCP socket) while the device still believed it was
connected. Catch `DeviceNotConnectedException` for the device-state case and
`TransportNotConnectedException` for the transport case — or both, since they classify the same
way for reporting purposes.

### Connection Status Monitoring

```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public async Task ReadCapabilityDocumentAsync_WhenDisconnected_Throws()
{
var device = new TestableCapabilityDevice("BenchNq1");

await Assert.ThrowsAsync<InvalidOperationException>(
// Typed since #395; still an InvalidOperationException by inheritance, so a consumer's
// existing catch keeps working. ThrowsAsync matches exactly, hence the derived type here.
await Assert.ThrowsAsync<DeviceNotConnectedException>(
() => device.ReadCapabilityDocumentAsync());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public async Task InitializeAsync_WhenDisconnected_Throws()
// Not connected

// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
var ex = await Assert.ThrowsAsync<DeviceNotConnectedException>(
() => device.InitializeAsync());
Assert.Equal("Device must be connected before initialization.", ex.Message);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Daqifi.Core.Tests/Device/DaqifiDeviceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ public void Disconnect_ChangesStatusAndRaisesEvent()
}

[Fact]
public void Send_WhenDisconnected_ThrowsInvalidOperationException()
public void Send_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
// Arrange
var device = new DaqifiDevice("TestDevice");

// Act & Assert
Assert.Throws<System.InvalidOperationException>(() => device.Send(new Daqifi.Core.Communication.Messages.ScpiMessage("")));
Assert.Throws<DeviceNotConnectedException>(() => device.Send(new Daqifi.Core.Communication.Messages.ScpiMessage("")));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ public async Task ExecuteTextCommandAsync_WhenAlreadyInsideAsyncFlow_ThrowsInval
}

[Fact]
public async Task ExecuteTextCommandAsync_WhenDisposing_ThrowsInvalidOperation()
public async Task ExecuteTextCommandAsync_WhenDisposing_ThrowsDeviceNotConnected()
{
var device = new TextCommandTestableDevice("TestDevice");
SetIsDisconnecting(device, true);

var ex = await Assert.ThrowsAsync<InvalidOperationException>(
var ex = await Assert.ThrowsAsync<DeviceNotConnectedException>(
() => device.CallExecuteTextCommandAsync(() => { }));
Assert.Contains("disposing or disconnecting", ex.Message);
}

[Fact]
public async Task ExecuteTextCommandAsync_WhenDisposed_ThrowsInvalidOperation()
public async Task ExecuteTextCommandAsync_WhenDisposed_ThrowsDeviceNotConnected()
{
var device = new TextCommandTestableDevice("TestDevice");
SetDisposed(device, true);

var ex = await Assert.ThrowsAsync<InvalidOperationException>(
var ex = await Assert.ThrowsAsync<DeviceNotConnectedException>(
() => device.CallExecuteTextCommandAsync(() => { }));
Assert.Contains("disposing or disconnecting", ex.Message);
}
Expand All @@ -70,12 +70,12 @@ public async Task ExecuteTextCommandAsync_ReleasesLockAfterValidationFailure()
// not block on WaitAsync.
var device = new TextCommandTestableDevice("TestDevice");

await Assert.ThrowsAsync<InvalidOperationException>(
await Assert.ThrowsAsync<DeviceNotConnectedException>(
() => device.CallExecuteTextCommandAsync(() => { }));
// Second call: also throws, but ONLY if the lock was released.
// If the lock leaked, this would deadlock and xunit's per-test
// budget would time it out instead.
await Assert.ThrowsAsync<InvalidOperationException>(
await Assert.ThrowsAsync<DeviceNotConnectedException>(
() => device.CallExecuteTextCommandAsync(() => { }));
}

Expand All @@ -87,7 +87,7 @@ public async Task ExecuteTextCommandAsync_AsyncLocalClearedAfterReturn()
// the same flow doesn't false-positive the re-entrancy check.
var device = new TextCommandTestableDevice("TestDevice");

await Assert.ThrowsAsync<InvalidOperationException>(
await Assert.ThrowsAsync<DeviceNotConnectedException>(
() => device.CallExecuteTextCommandAsync(() => { }));

Assert.False(GetIsInsideTextExchange(device).Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void DaqifiDevice_SendMessage_WhenDisconnected_ShouldThrowException()
using var device = new DaqifiDevice("Test Device", stream);

// Act & Assert
Assert.Throws<InvalidOperationException>(() => device.Send(ScpiMessageProducer.GetDeviceInfo));
Assert.Throws<DeviceNotConnectedException>(() => device.Send(ScpiMessageProducer.GetDeviceInfo));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void DaqifiDevice_SendMessage_WithoutConnection_ShouldThrowException()
using var device = new DaqifiDevice("Test Device", transport);

// Act & Assert
Assert.Throws<InvalidOperationException>(() => device.Send(ScpiMessageProducer.GetDeviceInfo));
Assert.Throws<DeviceNotConnectedException>(() => device.Send(ScpiMessageProducer.GetDeviceInfo));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,13 +571,13 @@ public void SetPwmFrequency_OutOfRange_ThrowsArgumentOutOfRangeException(int fre
}

[Fact]
public void SetPwmEnabled_WhenDisconnected_ThrowsInvalidOperationException()
public void SetPwmEnabled_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
var device = CreateConnectedDevice(digitalChannels: 8);
var channel = DigitalChannelAt(device, 4);
device.Disconnect();

Assert.Throws<InvalidOperationException>(() => device.SetPwmEnabled(channel, true));
Assert.Throws<DeviceNotConnectedException>(() => device.SetPwmEnabled(channel, true));
}

[Fact]
Expand Down Expand Up @@ -688,7 +688,7 @@ public void DisableChannel_WithNullChannel_ThrowsArgumentNullException()
}

[Fact]
public void ChannelManagement_WhenDisconnected_ThrowsInvalidOperationException()
public void ChannelManagement_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
// Populate channels but do not connect.
var device = new TestableDaqifiStreamingDevice("TestDevice");
Expand All @@ -701,14 +701,14 @@ public void ChannelManagement_WhenDisconnected_ThrowsInvalidOperationException()
var analog = AnalogChannelAt(device, 0);
var digital = DigitalChannelAt(device, 0);

Assert.Throws<InvalidOperationException>(() => device.EnableChannel(analog));
Assert.Throws<InvalidOperationException>(() => device.EnableChannels(new[] { analog }));
Assert.Throws<InvalidOperationException>(() => device.DisableChannel(analog));
Assert.Throws<InvalidOperationException>(() => device.DisableAllChannels());
Assert.Throws<InvalidOperationException>(() => device.SetDioDirection(digital, ChannelDirection.Output));
Assert.Throws<InvalidOperationException>(() => device.SetDioValue(digital, true));
Assert.Throws<InvalidOperationException>(() => device.SetAnalogOutput(0, 1.0));
Assert.Throws<InvalidOperationException>(() => device.Reboot());
Assert.Throws<DeviceNotConnectedException>(() => device.EnableChannel(analog));
Assert.Throws<DeviceNotConnectedException>(() => device.EnableChannels(new[] { analog }));
Assert.Throws<DeviceNotConnectedException>(() => device.DisableChannel(analog));
Assert.Throws<DeviceNotConnectedException>(() => device.DisableAllChannels());
Assert.Throws<DeviceNotConnectedException>(() => device.SetDioDirection(digital, ChannelDirection.Output));
Assert.Throws<DeviceNotConnectedException>(() => device.SetDioValue(digital, true));
Assert.Throws<DeviceNotConnectedException>(() => device.SetAnalogOutput(0, 1.0));
Assert.Throws<DeviceNotConnectedException>(() => device.Reboot());
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public async Task SetFriendlyNameAsync_InvalidName_Throws(string name)
public async Task SetFriendlyNameAsync_NotConnected_Throws()
{
var device = new CapturingStreamingDevice(); // not connected
await Assert.ThrowsAsync<InvalidOperationException>(() => device.SetFriendlyNameAsync("Lab Nq1"));
await Assert.ThrowsAsync<DeviceNotConnectedException>(() => device.SetFriendlyNameAsync("Lab Nq1"));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void SetPwmFrequency_AfterReconnect_SendsAgainEvenIfUnchanged()
public void SetPwmFrequency_NotConnected_ThrowsAndSendsNothing()
{
var device = new CapturingStreamingDevice(); // not connected
Assert.Throws<System.InvalidOperationException>(() => device.SetPwmFrequency(2000));
Assert.Throws<DeviceNotConnectedException>(() => device.SetPwmFrequency(2000));
Assert.Empty(device.PwmFrequencySends);
}

Expand Down
24 changes: 12 additions & 12 deletions src/Daqifi.Core.Tests/Device/DaqifiStreamingDeviceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,24 +133,24 @@ public void StopStreaming_WhenConnected_SendsCorrectCommandAndSetsIsStreaming()
}

[Fact]
public void StartStreaming_WhenDisconnected_ThrowsInvalidOperationException()
public void StartStreaming_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
// Arrange
var device = new DaqifiStreamingDevice("TestDevice");

// Act & Assert
var exception = Assert.Throws<System.InvalidOperationException>(() => device.StartStreaming());
var exception = Assert.Throws<DeviceNotConnectedException>(() => device.StartStreaming());
Assert.Equal("Device is not connected.", exception.Message);
}

[Fact]
public void StopStreaming_WhenDisconnected_ThrowsInvalidOperationException()
public void StopStreaming_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
// Arrange
var device = new DaqifiStreamingDevice("TestDevice");

// Act & Assert
var exception = Assert.Throws<System.InvalidOperationException>(() => device.StopStreaming());
var exception = Assert.Throws<DeviceNotConnectedException>(() => device.StopStreaming());
Assert.Equal("Device is not connected.", exception.Message);
}

Expand Down Expand Up @@ -201,14 +201,14 @@ public void NvmPersistence_WhenConnected_SendsCorrectCommand(string methodName,

[Theory]
[MemberData(nameof(NvmPersistenceCommands))]
public void NvmPersistence_WhenDisconnected_ThrowsInvalidOperationException(string methodName, string expectedCommand)
public void NvmPersistence_WhenDisconnected_ThrowsDeviceNotConnectedException(string methodName, string expectedCommand)
{
// Arrange
_ = expectedCommand;
var device = new DaqifiStreamingDevice("TestDevice");

// Act & Assert
var exception = Assert.Throws<System.InvalidOperationException>(() => InvokeNvmMethod(device, methodName));
var exception = Assert.Throws<DeviceNotConnectedException>(() => InvokeNvmMethod(device, methodName));
Assert.Equal("Device is not connected.", exception.Message);
}

Expand Down Expand Up @@ -264,26 +264,26 @@ public void UseAdcCalibration_WhenConnected_SendsCorrectCommand(int bank)
}

[Fact]
public void SetAdcCalibrationSlope_WhenDisconnected_ThrowsInvalidOperationException()
public void SetAdcCalibrationSlope_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
var device = new DaqifiStreamingDevice("TestDevice");
var exception = Assert.Throws<System.InvalidOperationException>(() => device.SetAdcCalibrationSlope(0, 1.0));
var exception = Assert.Throws<DeviceNotConnectedException>(() => device.SetAdcCalibrationSlope(0, 1.0));
Assert.Equal("Device is not connected.", exception.Message);
}

[Fact]
public void SetAdcCalibrationOffset_WhenDisconnected_ThrowsInvalidOperationException()
public void SetAdcCalibrationOffset_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
var device = new DaqifiStreamingDevice("TestDevice");
var exception = Assert.Throws<System.InvalidOperationException>(() => device.SetAdcCalibrationOffset(0, 1.0));
var exception = Assert.Throws<DeviceNotConnectedException>(() => device.SetAdcCalibrationOffset(0, 1.0));
Assert.Equal("Device is not connected.", exception.Message);
}

[Fact]
public void UseAdcCalibration_WhenDisconnected_ThrowsInvalidOperationException()
public void UseAdcCalibration_WhenDisconnected_ThrowsDeviceNotConnectedException()
{
var device = new DaqifiStreamingDevice("TestDevice");
var exception = Assert.Throws<System.InvalidOperationException>(() => device.UseAdcCalibration(1));
var exception = Assert.Throws<DeviceNotConnectedException>(() => device.UseAdcCalibration(1));
Assert.Equal("Device is not connected.", exception.Message);
}

Expand Down
Loading