Skip to content

feat: eliminate the DaqifiStreamingDevice downcast — fix factory return types and promote Channels/Metadata onto the interfaces #333

Closed

Description

@tylerkron

Type: Feature
Priority: High

Description

The first thing every consumer hits is a cast. All eight public DaqifiDeviceFactory.Connect* methods are typed Task<DaqifiDevice> / DaqifiDevice, but the constructed instance is always a DaqifiStreamingDevice (src/Daqifi.Core/Device/DaqifiDeviceFactory.cs). The README has to explain the downcast twice (lines 38-40 and 170-171), docs/DEVICE_INTERFACES.md pattern-matches if (device is IStreamingDevice ...) in four separate sections, and the in-repo MCP consumer wraps it in RequireStreaming/RequireSdCard helpers (src/Daqifi.Mcp/DaqifiAgent.cs:484-502).

Worse, the interfaces can't stand alone: every enable/disable/DIO/PWM method on IStreamingDevice documents that the channel "must belong to this device's Channels collection" (src/Daqifi.Core/Device/IStreamingDevice.cs), yet neither IDevice nor IStreamingDevice exposes Channels, GetChannelsSnapshot(), Metadata, or Capabilities — those live only on the concrete DaqifiDevice. A consumer holding the interface literally cannot obtain a channel to pass into the interface's own methods (DEVICE_INTERFACES.md admits this at line ~397).

Proposed fix (simplest path):

  1. Change the factory return types to DaqifiStreamingDevice (source-compatible narrowing for existing callers; note the binary break in release notes).
  2. Promote GetChannelsSnapshot(), Metadata, and ChannelsPopulated onto IStreamingDevice (or IDevice where they belong).

Acceptance Criteria

  • DaqifiDeviceFactory.Connect* returns the streaming type — no cast needed for streaming, SD, network, or diagnostics APIs
  • IStreamingDevice (and/or IDevice) exposes channel access and metadata so the interface is usable in isolation
  • README and DEVICE_INTERFACES.md updated — the downcast caveats deleted
  • MCP server's RequireStreaming/RequireSdCard helpers simplified or removed

Value

Removes the single biggest piece of friction in the public API, makes the interfaces mockable/usable on their own, and directly supports the daqifi-desktop thin-wrapper migration.

Activity

  1. tylerkron commented on Jul 22, 2026

    @tylerkron
    ContributorAuthor

    Adding a data point for whatever type surface this settles on: #381's DaqifiDeviceRegistry is typed on DaqifiDevice, so it inherits this decision.

    That typing is fine for Core's own consumers (the in-repo MCP server holds DaqifiDevice directly and ported cleanly), but it blocks the reference consumer this feature was built from. daqifi-desktop's AbstractStreamingDevice doesn't extend a Core device — it wraps one (AbstractStreamingDevice.cs:139, protected CoreStreamingDevice? CoreDevice) — and its ConnectionManager.ConnectedDevices is a List<IStreamingDevice> of desktop's interface. So desktop cannot put its devices in the registry; registering the inner Core device instead would leave it maintaining a side-map back to the wrapper, which is the bookkeeping the registry exists to remove. Tracked as daqifi-desktop#752 stage 2.

    Two ways out, and this ticket is where the choice belongs:

    1. Desktop's device types collapse onto Core's — the thin-wrapper end state. Then the registry works as-is and no Core change is needed.
    2. Type the registry on an interface (IDevice, or whatever this ticket promotes Channels/Metadata onto) so a consumer wrapping Core devices can register its own type.

    Option 1 is the better end state and makes option 2 unnecessary, so this is a note rather than a request — worth knowing that the registry is now a second consumer of the same decision, not an argument for generics on its own.

  2. tylerkron commented on Aug 6, 2026

    @tylerkron
    ContributorAuthor

    Verification pass against current main (2026-08-06) — still fully live, and the gap is wider than the body describes:

    • The factory now has 11 public construction/connect entry points returning the concrete DaqifiDevice/DaqifiStreamingDevice types (it has grown since filing).
    • The promotion list is much longer than GetChannelsSnapshot()/Metadata/ChannelsPopulated. On DaqifiDevice but on no interface: Metadata, Channels/GetChannelsSnapshot(), IsFirmwareVersionSupported, Supports/EnsureSupported(DeviceFeature), TimestampFrequency, State, PreserveActiveStream, the StatusMessageReceived/StreamMessageReceived/ChannelsPopulated/SendFailed events, RunExclusiveAsync, DrainErrorQueueAsync, ReadCapabilityDocumentAsync, the entire reconnect surface (ReconnectAttempt/Reconnected/ReconnectFailed/IsReconnecting/CancelReconnect), and InitializeAsync.
    • On DaqifiStreamingDevice but not on IStreamingDevice: StreamSamplesAsync — the documented recommended consumption API is interface-invisible — plus the StreamFrameDiscarded/GapDetected events, DiscardedStreamFrameCount/DecodeFailureCount, IsUsbConnection, the live-sample buffer knobs, SetFriendlyNameAsync, and the PWM frequency constants (which IStreamingDevice's own xml-doc <see cref>s on the concrete class).
    • The MCP consumer's workaround has hardened into a (DaqifiDevice device, IStreamingDevice streaming) tuple (src/Daqifi.Mcp/DaqifiAgent.cs:468) because neither type alone is sufficient. test(mcp): Daqifi.Mcp is 1,185 lines with one 165-line test file — contract tests for the agent tool layer #465 (MCP contract tests) gets simpler once this lands.

    Interface-shape problems found in the same pass — the all-sync IStreamingDevice, the ConnectAsync/DisconnectAsync DIM shims, and IDevice lacking IDisposable/IAsyncDisposable — are split out to #460 so this ticket stays scoped to member promotion + factory return types. Suggest treating the inventory above as the working checklist for the second acceptance criterion, and sequencing this before further #344 extraction so collaborators get carved along interface seams.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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