refactor(device): extract status-frame channel mapping into a collaborator (part of #344) - #433
Conversation
…rator (part of #344) DaqifiDevice is the largest file in the repo at 4,398 lines — larger than either class #344 originally named — and the status-frame-to-channel mapping is the most self-contained block in it: protobuf fields in, channel instances out, with no lock, no event, and no device state of its own. Moves it to StatusChannelPopulator (internal, Device/Internal/). The device keeps everything that is not mapping: the channels lock, the list swap, the timestamp-frequency update, and the ChannelsPopulated event. The mapping still runs inside the lock, exactly where it did before. No public API change: PopulateChannelsFromStatus keeps its signature, its virtual-ness, and its documented behaviour. The seam is two constructor arguments — the logger and a Func<string> for the device name. A delegate rather than a captured string because the name can change during the device's lifetime, and a warning naming the wrong device is worse than one naming none. The field is readonly, so nullable analysis makes a future constructor that forgets to build it a compile error rather than an NRE on the first status frame. Verified as a move rather than by inspection: a normalized statement multiset diff of the old block against the device's remainder plus the new file leaves only structural residues — the three log calls (Name -> _deviceName()), the existingByKey loop (renamed local), the two Populate* call sites (renamed parameter), PopulateDigitalChannels becoming static, and the new file's scaffolding. DaqifiDevice: 4,398 -> 4,193 lines. Tests: zero edits to existing tests, which is the evidence that behaviour is unchanged — ChannelPopulationTests' 44 cases still exercise the same mapping through the device. Adds 23 cases against the collaborator directly, which is what the extraction newly makes possible (no device, no connection, no lock): constructor guards, the analog-then-digital ordering, in-place instance reuse, the enabled-mask resync and the empty-mask ambiguity, the PWM-capable channel set, the assumed-resolution and non-finite-scaling fallbacks, short calibration arrays, that the device name is read at populate time, and that a throwing consumer logger still leaves the population complete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoRefactor device status-frame channel mapping into StatusChannelPopulator
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 (0 bugs, 0 rule violations, 0 requirement gaps, no unresolved threads) and CI |
Problem
DaqifiDeviceis now the largest file in the repo at 4,398 lines — larger than either class #344 originally named. The status comment on #344 flagged this as item 4 and left the scope question open; this PR answers it by taking the smallest, safest bite rather than by re-scoping the whole issue.The status-frame-to-channel mapping is the most self-contained block in the file: protobuf fields in, channel instances out. It owns no lock, raises no event, and holds no device state — it is translation sitting inside a connection-lifecycle class.
Fix
Moved to
StatusChannelPopulator(internal,Device/Internal/). The device keeps everything that is not mapping — the channels lock, the list swap, the timestamp-frequency update, and theChannelsPopulatedevent. The mapping still runs inside the lock, exactly where it did before.No public API change.
PopulateChannelsFromStatuskeeps its signature, itsvirtual-ness, and its documented behaviour.The seam is two constructor arguments: the logger, and a
Func<string>for the device name. A delegate rather than a captured string because the name can change during the device's lifetime, and a warning naming the wrong device is worse than one naming none. The field isreadonly, so nullable analysis turns "a future constructor forgot to build it" into a compile error rather than an NRE on the first status frame.DaqifiDevice: 4,398 → 4,193 lines.Verifying it is a move, not a rewrite
Same method as #419/#422/#432: a normalized statement multiset diff (comments and doc-comments stripped, whitespace canonicalized) of the original block against the device's remainder plus the new file. Every surviving residue is structural and enumerated:
SafeLog(... LogWarning ...)Name→_deviceName()foreach (var existing in _channels)+ index writeexistingparameter; local renamed tochannelanalogCount/digitalCount = Populate*(…, updatedChannels)destinationprivate int PopulateDigitalChannelsprivate static— it touches no instance stateSafeLogtwin /Populateentry pointSafeLogis a local twin rather than a shared helper, matching the existing convention —MessageProducerandDaqifiStreamingDevice(asSafeTrace) already each carry their own, andDaqifiDevice's stays private to that class.Tests
Zero edits to existing tests — that is the evidence behaviour is unchanged.
ChannelPopulationTests' 44 cases still drive the same mapping through the device and still pass untouched.Adds 23 cases against the collaborator directly, which is what the extraction newly makes possible (no device, no connection, no channels lock): constructor guards, analog-then-digital ordering, in-place instance reuse, the enabled-mask resync and the empty-mask ambiguity (#409), the PWM-capable channel set, assumed-resolution and non-finite-scaling fallbacks, short calibration arrays, that the device name is read at populate time rather than at construction, and that a throwing consumer logger still leaves the population complete.
Daqifi.Mcp.Tests23/23.Bench (real Nq1, fw 3.7.2, USB, non-destructive)
Channel population is exercised on every connect, so this is directly bench-observable:
analogIn=16 digital=16— the real 16+16 Nyquist channel description, not a fixture.--channels 3(ch 0,1) → 2 analog values per frame;--channels 5(ch 0,2) → 2;--channels 11(ch 0,1,3) → 3. The decode maps values onto channels byIsEnabledin channel order, so a broken enabled-mask resync would mis-map or drop values here — it doesn't.Not merging — for review.
Part of #344 (item 4 in its status comment). Deliberately does not touch
DaqifiStreamingDevice, so it does not conflict with #432.