You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Core has no error surface for background failures. The two places failures currently land are both dead ends:
Consumer read/dispatch errors fire into the void.IMessageConsumer.ErrorOccurred (src/Daqifi.Core/Communication/Consumers/IMessageConsumer.cs:29) is raised from the consumer thread at StreamMessageConsumer.cs:273 (read exception), :300 (loop exception), and :336 (subscriber dispatch exception) — but nothing in Daqifi.Core subscribes to it. DaqifiDevice wires only MessageReceived (src/Daqifi.Core/Device/DaqifiDevice.cs:509, 766, 997); the only ErrorOccurred += in the repo are in tests.
Per-frame decode failures are silently swallowed.DaqifiStreamingDevice.OnStreamMessageReceived wraps DecodeStreamFrame in an intentionally empty catch (src/Daqifi.Core/Device/DaqifiStreamingDevice.cs:453-461). The per-frame isolation is by design and should stay (a single malformed frame must never tear down the stream — see also the fix(device): suppress firmware's malformed warmup stream frame (closes #351) #362 warmup-frame handling and the settled silent-catch convention from fix: bind UDP discovery per-NIC and skip virtual adapters (#179) #180). The problem is a systematic failure that throws on every frame: it yields zero samples with zero diagnostics, indistinguishable from "device sending nothing".
Proposal: add a device-level error event — e.g. event EventHandler<DeviceErrorEventArgs>? ErrorOccurred on IDevice — fed by:
The consumer's ErrorOccurred (read-loop, parse, and dispatch exceptions), subscribed wherever MessageReceived is wired today.
Decode failures: keep the catch, but make it observable — raise the event and/or increment a decode-failure counter visible on stream stats. At kHz frame rates a raise-per-frame would storm; use a bounded policy (e.g. always first failure, then every Nth or once per interval) and document it.
Semantics must stay observational: raising the event never changes stream behavior (no tear-down, no retry-policy change). Escalating fatal transport errors to ConnectionStatus.Lost is #377's job — this issue is only the visibility layer, though #377 will likely consume the same consumer subscription, so coordinate if both are in flight.
Acceptance Criteria
IDevice exposes an error event; consumer read-loop/parse/dispatch errors reach a subscriber
Per-frame decode failures remain isolated (stream survives) but become observable via the event and/or a counter, plus ILogger output
Event volume is bounded under systematic failure (throttle/dedup policy documented in XML docs)
Existing per-frame isolation behavior and tests unchanged and green
New tests: a read-loop exception reaches an ErrorOccurred subscriber; a systematically-throwing decode path is observable while the stream keeps running
docs/DEVICE_INTERFACES.md documents the error surface and its non-escalating semantics
Value
Silent failure is the worst diagnostic outcome for a DAQ library. This gives consumers (desktop wrapper, MCP server) and the bench flows one place to answer "why am I getting no samples", and gives #377 a clean signal source to escalate from.
Type: Feature
Priority: High
Description
Core has no error surface for background failures. The two places failures currently land are both dead ends:
IMessageConsumer.ErrorOccurred(src/Daqifi.Core/Communication/Consumers/IMessageConsumer.cs:29) is raised from the consumer thread atStreamMessageConsumer.cs:273(read exception),:300(loop exception), and:336(subscriber dispatch exception) — but nothing inDaqifi.Coresubscribes to it.DaqifiDevicewires onlyMessageReceived(src/Daqifi.Core/Device/DaqifiDevice.cs:509, 766, 997); the onlyErrorOccurred +=in the repo are in tests.DaqifiStreamingDevice.OnStreamMessageReceivedwrapsDecodeStreamFramein an intentionally empty catch (src/Daqifi.Core/Device/DaqifiStreamingDevice.cs:453-461). The per-frame isolation is by design and should stay (a single malformed frame must never tear down the stream — see also the fix(device): suppress firmware's malformed warmup stream frame (closes #351) #362 warmup-frame handling and the settled silent-catch convention from fix: bind UDP discovery per-NIC and skip virtual adapters (#179) #180). The problem is a systematic failure that throws on every frame: it yields zero samples with zero diagnostics, indistinguishable from "device sending nothing".Proposal: add a device-level error event — e.g.
event EventHandler<DeviceErrorEventArgs>? ErrorOccurredonIDevice— fed by:ErrorOccurred(read-loop, parse, and dispatch exceptions), subscribed whereverMessageReceivedis wired today.ILoggerseam (feat: consistent ILogger seam — device diagnostics currently go to Trace.WriteLine #340) as a secondary sink, so they are visible in logs even with no event subscriber.Semantics must stay observational: raising the event never changes stream behavior (no tear-down, no retry-policy change). Escalating fatal transport errors to
ConnectionStatus.Lostis #377's job — this issue is only the visibility layer, though #377 will likely consume the same consumer subscription, so coordinate if both are in flight.Acceptance Criteria
IDeviceexposes an error event; consumer read-loop/parse/dispatch errors reach a subscriberILoggeroutputErrorOccurredsubscriber; a systematically-throwing decode path is observable while the stream keeps runningdocs/DEVICE_INTERFACES.mddocuments the error surface and its non-escalating semanticsValue
Silent failure is the worst diagnostic outcome for a DAQ library. This gives consumers (desktop wrapper, MCP server) and the bench flows one place to answer "why am I getting no samples", and gives #377 a clean signal source to escalate from.