refactor(device): extract channel/DIO/PWM/analog-out control into a collaborator (part of #344) - #432
Conversation
…ifiStreamingDevice (part of #344) The next extraction named in #344's status comment. `ChannelControlOperations` (internal, `Device/Internal/`) now owns channel enable/disable and the ADC/DIO enable-mask derivation, DIO direction and level, PWM enable/duty/frequency, and the analog output. `DaqifiStreamingDevice` keeps the public surface and forwards. Two members were added to the `IDeviceOperationHost` seam for this: the channels snapshot and the channels lock. That was the reason #422 stopped short of this block — mutating `IChannel.IsEnabled` and deriving the outbound mask must stay in one critical section (#409), so the collaborator has to reach the device's lock rather than take its own. It goes through the device's own members, so a subclass's `Send` override still intercepts every command. No public API change: the three PWM constants and `PwmFrequencyHz` stay on the device with their docs, and every moved method keeps its signature, validation order, and exception paramName. Also removes `SendAdcEnableMask`/`SendDioEnableState`, two private methods left uncalled since #411 replaced their call sites with the under-lock computation. DaqifiStreamingDevice: 2,189 -> 1,756 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoRefactor device channel/DIO/PWM/analog-out control into ChannelControlOperations
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTo customize comments, go to the Qodo configuration screen, or learn more in the docs. |
|
Ready for review: Qodo review came back clean (Bugs 0, rule violations 0, requirement gaps 0, no unresolved inline threads) and CI |
…344) The constructor null-checks all four injected delegates, but `Run`/`RunAsync` invoked the caller's operation without validating it, so internal misuse would have surfaced as a NullReferenceException from one of three invocation sites rather than as an ArgumentNullException naming the parameter. The guard runs before the re-entry branch, not after: that branch invokes the delegate without acquiring anything, so a guard placed later would leave exactly that path throwing NRE. Placing it first also means a null delegate can never take the gate on its way to failing. Matches the entry-point convention already established by the sibling collaborator `ChannelControlOperations` (#432). +4 tests, all four of which fail if either guard is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Problem
DaqifiStreamingDeviceis 2,189 lines and still hosts five responsibilities. #422 extracted four of them; its status comment named the channel / DIO / PWM / analog-out block as the next one, and said why it was deliberately left behind:This is that extraction, on its own, with that verification.
Fix
ChannelControlOperations(internal,Device/Internal/) now owns:EnableChannel(s),DisableChannel,DisableAllChannels,SetChannelsEnabled,ComputeAdcEnableMask,ComputeDioEnableState,EnsureChannelBelongs)PwmFrequencyHzbookkeeping and the skip-if-unchanged cache (chore: small API polish batch (PWM frequency idempotence, SetFriendlyNameAsync, SCPI error-code parsing consolidation) #345)DaqifiStreamingDevicekeeps the public surface and forwards, exactly as it does for the SD, network, LAN-info and diagnostics collaborators.The seam widening this needed.
IDeviceOperationHostgainsSnapshotChannels()andWithChannelsLock(Action). The lock is the whole reason this block was hard: mutatingIChannel.IsEnabledand computing the outbound mask from it must happen in one critical section, or a status frame resyncingIsEnabledfrom the device (#409) can interleave between them and silently drop the just-requested channel from the mask. The collaborator therefore reaches the device's lock rather than taking one of its own. Both new members forward to members the device already had, so every command still passes through the device's own virtualSend— a subclass override (and every test double) still intercepts all of it.No public API change. The three PWM constants and
PwmFrequencyHzstay onDaqifiStreamingDevicewith their docs; every moved method keeps its signature, its validation order (argument checks before the connected check), and its exceptionparamName.One deletion.
SendAdcEnableMaskandSendDioEnableStateare gone. They are private and have had no callers since #411, which replaced their call sites with the under-lock computation —git log -Sconfirms that commit removed the last use. Carrying dead code into a new file seemed worse than deleting it; it is called out here rather than buried.DaqifiStreamingDevice: 2,189 → 1,756 lines.Verification
The move is mechanical, and that is checked rather than asserted. A normalized statement multiset diff (comments/whitespace stripped,
_host.and the const qualifications canonicalized) of every line the device lost against every line the collaborator gained leaves exactly three residues, all enumerated: the two dead methods above,MaxAdcBitmaskChannelbecominginternal(the device's raw-command tracking decodes the same mask and now references it there), and_lastSentPwmFrequencyHz = nullbecoming the one-lineResetSentPwmFrequency(). Nothing else changed.Test suite unchanged and green — the acceptance criterion this issue asks for. 2,529 passed / 2 skipped on net9 and net10, plus 23 in
Daqifi.Mcp.Tests. Zero test edits: the existing coverage already pins the foreign-channel guards, the disconnected guards, the PWM range/duty/capability rules, the analog-out sequence, and — importantly — the #409 concurrent-resync regression test, which hammers a status resync against 500EnableChannelcalls and would fail or deadlock if the lock discipline had been broken by routing it through the seam. Release build: 0 warnings on both TFMs.Bench-validated on the real Nq1 (fw 3.7.2, USB), because a mask that is computed correctly and never reaches the device looks identical to a green unit test:
The frame counts confirm the recomputed mask actually took effect on the device, not just in local state. Strictly non-destructive: nothing here drives a pin (PWM frequency is the shared timer with no channel enabled; DIO direction was set to
Input, which is high-Z), writes NVM, or reboots.Still open on #344 after this
Frame decode (the hot path, warrants its own PR),
WifiModuleUpdaterat 816 lines pending #271, and the scope question aboutDaqifiDevicenow being the largest file in the repo.Closes nothing — part of #344.
Not merging — opened for your review.
🤖 Generated with Claude Code