feat(device): eliminate the DaqifiStreamingDevice downcast - #476
Conversation
DaqifiDeviceFactory's Connect*/DiscoverAndConnectAsync methods were typed Task<DaqifiDevice> / DaqifiDevice, but the constructed instance was always a DaqifiStreamingDevice — every caller had to cast or pattern-match to reach streaming, SD-card, network, or diagnostics operations. Narrow the return types to DaqifiStreamingDevice directly (source-compatible: existing DaqifiDevice-typed call sites still compile via implicit upcast; note the binary break in release notes). Promote Channels, GetChannelsSnapshot(), Metadata, and the ChannelsPopulated event onto IStreamingDevice, so a consumer holding only the interface can obtain a channel to pass into the interface's own enable/disable/DIO/PWM methods — previously impossible, since neither IDevice nor IStreamingDevice exposed them. DaqifiStreamingDevice already satisfies these via its DaqifiDevice base class, so no implementation changes were needed there. DaqifiDeviceRegistry's internal connector delegate stays typed over the base DaqifiDevice (its public Register(DaqifiDevice, ...) API deliberately accepts any manually-constructed DaqifiDevice, not only ones the factory built), so its connector lambda needed an async adjustment to bridge Task<DaqifiStreamingDevice> to Task<DaqifiDevice>. For the same reason, MCP's RequireStreaming keeps its runtime IStreamingDevice check — documented why in a comment, since every device it actually connects is in practice a DaqifiStreamingDevice but the registry's contract doesn't guarantee it. Updated README.md and docs/DEVICE_INTERFACES.md: removed the now-unnecessary cast/pattern-match advice from every example that connects through the factory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR Summary by QodoReturn DaqifiStreamingDevice from factory and expose channels/metadata on IStreamingDevice
AI Description
Diagram
High-Level Assessment
Files changed (10)
|
Code Review by Qodo
1.
|
device.Channels is a live view that can be repopulated concurrently on the consumer thread; the example enumerated it directly with First()/OfType(), which is exactly the racy pattern the SDK's own docs warn against elsewhere. Use GetChannelsSnapshot() once and query that instead, matching every other example in these docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 189cd26 |
Summary
DaqifiDeviceFactory's eight publicConnect*/DiscoverAndConnectAsyncmethods were typedTask<DaqifiDevice>/DaqifiDevice, but the constructed instance was always aDaqifiStreamingDevice— every caller had to cast or pattern-match (if (device is IStreamingDevice streamingDevice)) to reach streaming, SD-card, network, or diagnostics operations. The README explained the downcast twice,docs/DEVICE_INTERFACES.mdpattern-matched it in four sections, and the in-repo MCP consumer wrapped it inRequireStreaming/RequireSdCardhelpers.Worse,
IStreamingDevice's own docs said a channel argument "must belong to this device'sChannelscollection," but neitherIDevicenorIStreamingDeviceexposedChannels,GetChannelsSnapshot(), orMetadata— a consumer holding only the interface couldn't obtain a channel to pass into the interface's own methods.Changes
Following the issue's proposed "simplest path":
DaqifiDeviceFactory— narrowed everyConnect*/DiscoverAndConnectAsyncreturn type fromDaqifiDevicetoDaqifiStreamingDevice. Source-compatible: existingDaqifiDevice-typed call sites still compile via implicit upcast. Binary break — worth calling out in release notes.IStreamingDevice— promotedChannels,GetChannelsSnapshot(),Metadata, and theChannelsPopulatedevent onto the interface.DaqifiStreamingDevicealready satisfies these via itsDaqifiDevicebase class, so no implementation changes were needed — this is purely a contract widening.DaqifiDeviceRegistry— its internalDeviceConnectordelegate stays typed over the baseDaqifiDevice(its publicRegister(DaqifiDevice, ...)deliberately accepts any manually-constructedDaqifiDevice, not only ones the factory built — see the "Manual Device Connection (Advanced)" doc section), so the connector lambda needed a small async adjustment to bridgeTask<DaqifiStreamingDevice>→Task<DaqifiDevice>.DaqifiAgent.RequireStreaming) — for the same reason as SCPI Commands #3, this keeps its runtimeIStreamingDevicecheck; I documented why in a comment rather than removing it, since the registry's contract doesn't statically guarantee every registered device is aDaqifiStreamingDeviceeven though in practice it always is. Fully eliminating it would mean retypingDaqifiDeviceRegistryitself, which is a real design change beyond this issue's proposed scope (and would break the registry's legitimate "register any manually-connectedDaqifiDevice" use case) — flagging this as a candidate follow-up rather than doing it here.README.mdanddocs/DEVICE_INTERFACES.md: removed the cast/pattern-match advice from every example that connects through the factory (digital output, PWM, network configuration, channel management, device diagnostics). The "Manual Device Connection (Advanced)" section, which constructs a plainDaqifiDevicedirectly rather than through the factory, is unchanged since it doesn't get the narrowed type.Testing
IStreamingDevicemembers (DaqifiDeviceFactoryTests.cs)IStreamingDeviceimplementers across 3 files to satisfy the widened interfaceDaqifi.Core.Testssuite: 2867 passedDaqifi.Mcp.Testssuite: 36 passedFixes #333
🤖 Generated with Claude Code