Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2fc27ac
feat(firmware): wait for application readiness after PIC32 reconnect …
cptkoolbeenz May 12, 2026
7dc8bdf
fix: Apply Qodo /improve pass 1 on PR #200: enforce timeout against i…
cptkoolbeenz May 12, 2026
cff6e8f
fix: Apply Qodo /improve pass 2 on PR #200: preserve sub-second preci…
cptkoolbeenz May 12, 2026
eb28b8d
fix: Apply Qodo /improve pass 3 on PR #200: WaitAsync for hard timeou…
cptkoolbeenz May 12, 2026
32e6f91
fix: Apply Qodo /agentic_review pass 4 on PR #200: preserve probe roo…
cptkoolbeenz May 12, 2026
89657ba
docs(firmware): clarify PostReconnectReadinessTimeout vs JumpingToApp…
cptkoolbeenz May 12, 2026
d34ad9f
Apply Qodo /agentic_review pass 5 on PR #200
cptkoolbeenz May 12, 2026
881f65a
Apply Qodo /improve pass 6 on PR #200: unify timeout messages
cptkoolbeenz May 12, 2026
782a4f8
Apply Qodo /improve pass 7 on PR #200: handle probe-thrown OCE
cptkoolbeenz May 12, 2026
5ac58c0
Apply Qodo /agentic_review pass 8: probes-executed counter, not loop …
cptkoolbeenz May 12, 2026
ea82fd1
Apply Qodo /improve pass 10: gate readiness validation on probe presence
cptkoolbeenz May 12, 2026
b089763
Apply Qodo /agentic_review pass 11: clear stale probe exception
cptkoolbeenz May 12, 2026
26b19fe
Apply Qodo /agentic_review pass 12: surface readiness wait at Info
cptkoolbeenz May 12, 2026
af6bd19
Merge remote-tracking branch 'origin/main' into feat/post-reconnect-r…
tylerkron May 15, 2026
e59410e
fix(device): rebuild MessageProducer/Consumer after Disconnect/Connec…
tylerkron May 15, 2026
a5ce969
fix(firmware): discard race-winning serial handle after PIC32 reset
tylerkron May 15, 2026
3e4fd88
feat(firmware): auto-wake dormant device + raise stale-handle log level
tylerkron May 15, 2026
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
100 changes: 96 additions & 4 deletions src/Daqifi.Core.Tests/Device/DaqifiDeviceWithTransportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,106 @@ public void DaqifiDevice_TransportConnectionLost_ShouldUpdateStatus()
// Arrange
using var transport = new MockStreamTransport();
using var device = new DaqifiDevice("Mock Device", transport);

device.Connect();
Assert.Equal(ConnectionStatus.Connected, device.Status);
// Act - Simulate transport connection loss

// Act - Simulate transport connection loss
transport.SimulateConnectionLoss();

// Assert
Assert.Equal(ConnectionStatus.Lost, device.Status);
}

[Fact]
public void DaqifiDevice_DisconnectThenConnect_SendsToCurrentTransportStream()
{
// Regression for the latent stale-stream bug surfaced by PR #200's
// post-reconnect readiness probe: SerialStreamTransport.Stream returns
// _serialPort.BaseStream, which is a fresh instance after a transport
// Disconnect → Connect. Pre-fix, DaqifiDevice.Disconnect left
// _messageProducer / _messageConsumer alive with references to the
// PREVIOUS BaseStream, and Connect's "if (_messageProducer == null)"
// guard skipped recreation — so any subsequent Send() wrote to the
// disposed stream and silently no-op'd, leaving the text consumer
// with zero bytes on the new stream. Fix nulls them in Disconnect so
// Connect rebuilds them against the transport's current Stream.
using var transport = new SwappingMockStreamTransport();
using var device = new DaqifiDevice("Mock Device", transport);

device.Connect();
Thread.Sleep(50); // MessageProducer background thread spin-up
device.Send(ScpiMessageProducer.GetDeviceInfo);
Thread.Sleep(200); // Allow the producer to flush
var firstStreamBytes = transport.CurrentStreamSnapshot();
Assert.NotEmpty(firstStreamBytes);

device.Disconnect();
transport.RotateStream();
device.Connect();
Thread.Sleep(50);
device.Send(ScpiMessageProducer.GetDeviceInfo);
Thread.Sleep(200);

// The send AFTER reconnect must land on the new stream — not on the
// first (now-disposed) stream we captured above.
var secondStreamBytes = transport.CurrentStreamSnapshot();
Assert.NotEmpty(secondStreamBytes);
Assert.NotSame(firstStreamBytes, secondStreamBytes);
}

// Mock transport that swaps to a fresh MemoryStream on RotateStream(),
// mirroring the real SerialStreamTransport whose Stream property returns
// _serialPort.BaseStream — a new instance after each Disconnect → Connect.
private class SwappingMockStreamTransport : IStreamTransport
{
private MemoryStream _stream = new();
private bool _isConnected;
private bool _disposed;

public Stream Stream => _disposed ? throw new ObjectDisposedException(nameof(SwappingMockStreamTransport)) : _stream;
public bool IsConnected => _isConnected && !_disposed;
public string ConnectionInfo => _disposed ? "Disposed" : (_isConnected ? "Swap: Connected" : "Swap: Disconnected");

public event EventHandler<TransportStatusEventArgs>? StatusChanged;

public void RotateStream()
{
var old = _stream;
_stream = new MemoryStream();
old.Dispose();
}

public byte[] CurrentStreamSnapshot() => _stream.ToArray();

public Task ConnectAsync() => ConnectAsync(null);

public Task ConnectAsync(ConnectionRetryOptions? retryOptions)
{
if (_disposed) throw new ObjectDisposedException(nameof(SwappingMockStreamTransport));
_isConnected = true;
StatusChanged?.Invoke(this, new TransportStatusEventArgs(true, ConnectionInfo));
return Task.CompletedTask;
}

public Task DisconnectAsync()
{
_isConnected = false;
StatusChanged?.Invoke(this, new TransportStatusEventArgs(false, ConnectionInfo));
return Task.CompletedTask;
}

public void Connect() => ConnectAsync().Wait();
public void Disconnect() => DisconnectAsync().Wait();

public void Dispose()
{
if (!_disposed)
{
_isConnected = false;
_stream.Dispose();
_disposed = true;
}
}
}
}
Loading
Loading