Skip to content

feat: device-level ErrorOccurred event — background read errors and per-frame decode failures are currently invisible #378

Description

@tylerkron

Type: Feature
Priority: High

Description

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:

  1. The consumer's ErrorOccurred (read-loop, parse, and dispatch exceptions), subscribed wherever MessageReceived is wired today.
  2. 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.
  3. Route the same failures through the existing ILogger seam (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.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions