refactor(device): extract the stream frame decode path into a collaborator (part of #344) - #435
Conversation
…rator (part of #344) Moves the streaming hot path — the two frame guards, timestamp reconstruction, gap detection, and the analog/digital unpacking — out of DaqifiStreamingDevice into a new internal StreamFrameDecoder, the item #344 names as the next (and riskiest) extraction. The device keeps the public events and delegates. DaqifiStreamingDevice: 1,756 -> 1,368 lines. No public API or behavior change; zero edits to existing tests. The three events stay on the device and are reached through IDeviceOperationHost, because their sender has to remain the device a subscriber attached to, and the raw re-raise has to run through the device's base OnStreamMessageReceived so a subclass override still sees the frame. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR Summary by QodoExtract streaming frame decode pipeline into StreamFrameDecoder
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
The remarks claimed the unsynchronized session fields "are only ever touched from" the message-consumer thread. That is not true of BeginSession, which runs on whichever thread called StartStreaming() or sent a raw start-streaming command through Send() — so the contract as written invited a future change to assume a thread confinement the API surface does not provide. Replaced with what actually holds: the decode path is consumer-thread only, BeginSession is the exception, and what makes it sound is the session boundary rather than synchronization — StartStreaming resets before sending the command, and the raw-Send path leaves IsStreaming false until its reset completes, so a frame landing in that window is re-raised as a stray and never decoded. Documentation only; no behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 0e11946 |
|
Ready for review: Qodo came back clean on the latest commit (0 bugs, 0 rule violations, 0 skill insights; the earlier Note on the one red CI run here ( |
Problem
DaqifiStreamingDeviceis still 1,756 lines against #344's ~800 target. Its own status comment names the frame-decode block as the next extraction and flags it as the riskiest one: it is the hot path, it owns three public events, and it is where a mistake is silent — samples stamped with times that never happened, or a raw frame handed to consumers twice.#432 (merged) took the channel/DIO/PWM block and widened the
IDeviceOperationHostseam withSnapshotChannels(), which is what made this one possible without duplicating that work.What changed
The whole path from "a frame arrives" to "per-channel samples" moves into a new internal
StreamFrameDecoder(Device/Internal/): the cross-session leftover gate, the #351 warmup-frame guard, timestamp reconstruction, gap detection, and the analog/digital unpacking — plus the session-scoped state thatBeginSessionresets together and the two per-session counters.DaqifiStreamingDevice: 1,756 → 1,368 lines. Public API unchanged;DiscardedStreamFrameCountandDecodeFailureCountnow delegate.What deliberately did not move
StreamFrameDiscarded,GapDetected, and the raw-frame re-raise stay on the device, reached through four newIDeviceOperationHostmembers:senderhas to remain the device a subscriber attached to. A collaborator raising them in its own name would be a silent, compile-clean behavior change — the same reasoning already recorded onRaiseLowSdSpaceWarning, and the same class of bug refactor(firmware): split FirmwareUpdateService into focused collaborators (part of #344) #419 had to add a guard test for.base.OnStreamMessageReceived, so the base implementation and any subclass between it and the streaming device still see the frame. Calling the override would recurse; there's a comment at the call site saying so.SafeTracestays too — it's still used by the session-command tracking, so the isolatingtry/catcharound each subscriber stays with it.The decoder owns the discard counter and increments it before asking the device to raise the event, preserving the documented guarantee that a handler reading
DiscardedStreamFrameCountalready sees the frame it's being told about.Verifying it's a pure move
Same method as #419/#422/#432/#433: a normalized statement multiset diff of what the device lost against what the decoder plus the delegating call sites gained (comments stripped, mechanical renames canonicalized). Every surviving residue is structural — new-file scaffolding, the four seam forwarders, the two one-line delegations, and the split of the old
RaiseStreamFrameDiscarded(reason, frame, counts)into "decoder counts and builds the args" / "device raises". No decode statement is lost or altered.Also fixed a pre-existing doc/member mismatch carried along by the move: the
<summary>describingDecodeAnalogwas attached toCountEnabledAnalogChannels. Each now documents itself.Tests
Zero edits to existing tests —
DaqifiStreamingDeviceDecodeTests(38 cases) still drives the same pipeline through the device, and that is the evidence the extraction changed nothing.+15 new cases against the collaborator directly, covering what only a direct test can see: the order and multiplicity of the calls back into the host. Through the device those callbacks are invisible — a frame re-raised twice, or a discard counted after the event, looks identical from the outside until a consumer trips over it. The fake host records the call sequence, and its non-decode members throw, so a future change that makes the decoder reach for device I/O fails loudly.
Mutation-verified rather than eyeballed:
DiscardIsCountedBeforeTheEventIsRaisedfailselse)BeginSessionskips the gap-detector resetBeginSession_ResetsTheGapDetectorfailsFULL suite green net9 + net10 (2,544 passed, 2 skipped, was 2,529) plus
Daqifi.Mcp.Tests(23). Release solution build 0 warnings on both TFMs.Bench (real Nq1, fw 3.7.2, USB, non-destructive)
A unit test cannot show that the device's own stream still flows through the moved code, so this ran against the board:
Session 1 — channels 0,1,2 @ 200 Hz, 3 s
rawFrames=475,decodedCh0=475— every delivered frame reached both consumer paths exactly once. This is the raw re-raise multiplicity contract, on hardware.discarded=1,reason=PartialAnalogFrame[an=1/en=3]— the firmware's malformed leading warmup frame (Streaming: malformed first sample (partial analog channels) — Core lacks desktop's first-frame/leftover-frame protection (#573) #351) was caught by the moved guard, reported with the counts the decision was made on, and withheld from raw consumers.decodeFailures=0,gaps=0. Every event'ssenderwas asserted to be the device — it was.Session 2 — channel 0 only @ 100 Hz, 3 s (same device instance)
discarded=0,decodeFailures=0—BeginSessionreset both counters, and no leftover frame from session 1 tripped the gate.rawFrames=238vsdecodedCh0=237: one frame arrived after the stop command landed and was re-raised but not decoded — exactly theif (!IsStreaming)branch, working on hardware.ch1=0, ch2=0decoded — the disable reached the device and the enabled-channel snapshot the decode maps against is still correct; a broken snapshot would have mis-mapped values into the disabled channels.Only channel enable/disable and stream start/stop. No NVM write, no reboot, no SD, nothing driven.
Scope
Part of #344 — does not close it. Remaining:
DaqifiStreamingDeviceis still above the ~800 target (the SD-card block at 1,357 lines is now the dominant remaining piece, extracted but not yet under target),DaqifiDevice, andWifiModuleUpdateronce #271 settles.Not merging — for review.