Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0dc2327
feat(device): surface background failures via ErrorOccurred and close…
tylerkron Jul 31, 2026
a8c9b1d
feat(device): opt-in auto-reconnect and streaming session resume afte…
tylerkron Jul 31, 2026
3908260
fix(consumers): no reader-loop failure may spin, escape, or outlive i…
tylerkron Jul 31, 2026
60e76eb
fix(device): close two reconnect holes found in review
tylerkron Jul 31, 2026
4b19d59
fix(consumers): suppress fault reporting during intentional teardown
tylerkron Jul 31, 2026
aebb59d
fix(consumers): isolate every subscriber and health-sink callback in …
tylerkron Jul 31, 2026
b130df6
fix(device): unwind a reconnect that raced a caller teardown, and hon…
tylerkron Jul 31, 2026
048d29e
test: honor Stream.Read's partial-read contract in the test doubles
tylerkron Jul 31, 2026
5e157b4
fix(device): serialize connect against disconnect, and bound the test…
tylerkron Jul 31, 2026
c2517f3
fix(device): make the lifecycle lock an actual guarantee, not a logge…
tylerkron Jul 31, 2026
5463748
Merge remote-tracking branch 'origin/fix/connection-loss-detection-an…
tylerkron Jul 31, 2026
9882740
fix(device): bound the teardown wait — a wedged connect must not hang…
tylerkron Jul 31, 2026
2daa77c
fix(device): honour an abandoned teardown on the caller's connect pat…
tylerkron Jul 31, 2026
f8a473e
fix(device): track a streaming session driven by raw SCPI, so reconne…
tylerkron Jul 31, 2026
5bc5cc1
fix(device): only track a start-streaming command that carries a usab…
tylerkron Jul 31, 2026
c6ddd0b
fix(device): give a raw-started stream the same session reset as Star…
tylerkron Jul 31, 2026
f5531b7
Merge remote-tracking branch 'origin/main' into feature/auto-reconnec…
tylerkron Aug 1, 2026
f65a2ce
fix(device): a cancelled DisconnectAsync must still tear the connecti…
tylerkron Aug 1, 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
109 changes: 108 additions & 1 deletion docs/DEVICE_INTERFACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,12 +523,21 @@ device.StatusChanged += (sender, args) =>
Console.WriteLine("Device connected successfully");
break;
case ConnectionStatus.Lost:
Console.WriteLine("Connection lost - may attempt reconnection");
Console.WriteLine("Connection lost");
break;
case ConnectionStatus.Retrying:
Console.WriteLine("Reconnecting..."); // only with reconnect enabled
break;
case ConnectionStatus.Failed:
Console.WriteLine("Reconnection gave up");
break;
}
};
```

See [Reconnecting automatically after a drop](#reconnecting-automatically-after-a-drop) for the
`Retrying` and `Failed` states, which a device only ever reports once reconnection is turned on.

### Detecting a dropped connection

`ConnectionStatus.Lost` means the connection ended without anyone asking it to — the USB cable was
Expand Down Expand Up @@ -582,6 +591,102 @@ The failures that feed this escalation are also reported individually, as they h
`IDevice.ErrorOccurred` — see [Error Surface](#error-surface). That event is diagnostics only;
`ConnectionStatus.Lost` remains the single signal that means "the connection is over".

### Reconnecting automatically after a drop

By default a drop is where the story ends: `Lost` is reported and nothing else happens. Set
`ReconnectOptions` and the device will rebuild the session by itself — reconnect the transport,
re-initialize, put the channel configuration back, and restart a stream that was interrupted — with
no code from you in the loop.

```csharp
device.ReconnectOptions = ReconnectOptions.Default; // 5 attempts, 1 s backing off to 30 s

device.Reconnected += (_, e) =>
Console.WriteLine($"back after {e.Outage.TotalSeconds:0.#}s (attempt {e.AttemptNumber})");

device.ReconnectFailed += (_, e) =>
Console.WriteLine($"gave up after {e.AttemptsMade} attempts: {e.LastError?.Message}");
```

`ReconnectOptions.Fast` and `ReconnectOptions.Resilient` are ready-made policies for links that
blip briefly and for unattended long runs respectively; build your own for anything else.
`ReconnectOptions.Disabled` (the default) says so explicitly.

**What you can watch.** `ReconnectAttempt` fires before each attempt with its number and the wait
that precedes it; `Reconnected` fires once the session is fully back; `ReconnectFailed` fires when
it stops without one. The status follows along, so a UI can show progress without subscribing to
anything new:

| Status | Meaning |
|---|---|
| `Lost` | The drop was detected. Also where a cancelled reconnect leaves the device. |
| `Retrying` | Waiting out the backoff before the next attempt. |
| `Connected` | The session is back, configuration and stream included. |
| `Failed` | Every attempt failed. Terminal — nothing more will be tried. |

Running out of attempts is deliberately hard to miss: as well as `ReconnectFailed` and the `Failed`
status, it is logged as an error and raised on `ErrorOccurred` with source
`DeviceErrorSource.Reconnect`, carrying a `DeviceReconnectFailedException` whose inner exception is
whatever ended the final attempt.

**What gets restored.** Only what the library itself owns:

- the set of enabled channels (analog and digital),
- the streaming frequency, and
- an active stream, unless `ResumeStreaming` is turned off.

It does not matter whether you established that state through the typed API
(`EnableChannels`, `StartStreaming`) or by sending the SCPI yourself with
`Send(ScpiMessageProducer.StartStreaming(...))` — the device recognizes its own streaming and
ADC-enable commands whichever way they were sent, so a session driven entirely by raw commands is
restored just the same. The one exception is the global DIO enable: it is a single switch for the
whole port rather than a per-channel mask, so sending it directly tells the device nothing about
*which* digital channels you wanted. Use `EnableChannels` for those.

**What does not.** Everything else is the device's own state, and Core does not presume to know
what it should be after an outage of unknown length:

- DIO directions and output levels, PWM enable/duty/frequency, analog outputs, and calibration
written only to device RAM;
- an SD card logging session — the device keeps logging or does not, entirely on its own;
- **any operation that was in flight.** An SD card download interrupted by a drop fails, and is
neither resumed nor retried; run it again once `Reconnected` says the device is back.

A resumed stream is a genuinely new session: timestamp reconstruction re-anchors and the gap
detector resets, because the device's tick counter may well have restarted while it was away.
`Reconnected.Outage` is the measure of the interruption, not a `GapDetected` event.

**Same endpoint only.** Reconnection re-opens the endpoint the device was already using. It cannot
follow a device that moved: a serial device that comes back on a different port path, or one whose
IP address changed after a reboot, is a new endpoint and needs a fresh `DaqifiDeviceFactory`
connect. Failing over from one transport to another (USB to WiFi, say) is out of scope.

**Stopping it.** `CancelReconnect()` stops the loop at its next checkpoint and leaves the device on
`Lost`; `Disconnect()` and `Dispose()` do the same and then tear down. A caller-issued `Connect()`
or `Disconnect()` always wins — the loop unwinds without touching the session the caller
established, and a `Disconnect()` issued from inside a `Lost` handler stops the reconnect before it
even starts.

Which is the other half of the rule: with reconnect enabled, **stop tearing down on `Lost`
yourself**. The teardown shown under [Detecting a dropped
connection](#detecting-a-dropped-connection) is for devices without a reconnect policy. Here the
device does it for you, between attempts, and doing it as well just cancels the recovery you asked
for.

```csharp
device.ReconnectOptions = new ReconnectOptions
{
Enabled = true,
MaxAttempts = 10,
InitialDelay = TimeSpan.FromSeconds(2),
MaxDelay = TimeSpan.FromMinutes(1),
ResumeStreaming = true
};
```

All three events are raised on a background thread, and a handler that throws is caught and
ignored — it cannot stop a reconnect in progress.

### Working with Device Metadata

After initialization, device metadata is populated:
Expand Down Expand Up @@ -942,6 +1047,8 @@ streaming.
- **Event-Driven**: Status changes and messages handled via events
- **Observable Failures**: Background read and decode errors surface on `ErrorOccurred` instead of
failing silently
- **Opt-in Auto-Reconnect**: A dropped connection can rebuild itself — transport, initialization,
channel configuration and stream — with no consumer code
- **Type Safety**: Generic message types provide compile-time safety
- **Retry Support**: Built-in connection retry with exponential backoff
- **Thread-Safe Sending**: Background message queue for thread-safe command sending
Expand Down
Loading