fix(device): stop broadcasting the malformed first stream frame (closes #425) - #428
Conversation
#425) Firmware up to and including 3.7.2 emits a leading frame at stream start whose analog payload holds a single value regardless of the enabled channel mask. Core's per-channel decode has guarded against it since #351, but the raw MessageReceived event was still handed the frame verbatim — and that is the path most callers use, including the example CLI, whose offline export inferred a channel count of one from it and truncated every sample that followed (daqifi-core-example-app#34). Both consumer paths are now gated together, and every drop is reported through the new StreamFrameDiscarded event and DiscardedStreamFrameCount so a suppressed frame is never invisible. Also adds the cross-session leftover-frame guard #351 asked for (daqifi-nyquist-firmware #533) as a StreamFrameGate collaborator: the frame the device latches across a stop is recognised by its device-tick counter using wrap-safe modular arithmetic, measured against a reference fixed at session start so a quick restart cannot cascade, and capped so a stream can never be withheld indefinitely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoFix: discard malformed first stream frame and report discards
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1.
|
…iscard counts from the decision Addresses both Qodo findings on #428. Trace dispatches to consumer-installed listeners, so logging from inside the catch that contains a bad subscriber is itself consumer code — a throwing listener escapes the containment and takes down the frame pipeline the catch was protecting. Routed through a new SafeTrace helper, mirroring the guarantee DaqifiDevice.SafeLog already gives RaiseClassifiedEvent. Applied to the two other Trace sites in the file as well: RaiseGapDetected had the same unguarded pattern I copied, and TrackStreamingStart documents that tracking a command must never fail the send that carried it. StreamFrameDiscarded now reports the analog and enabled-channel counts the suppression decision was actually made on, instead of re-reading channel state that another thread may have changed in between. Self-inconsistent telemetry would undermine the observability this PR exists to add. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit e256014 |
…bal Trace state Trace.Listeners is process-global, so a test that installs a throwing listener can be reached by anything else running in the same process. This was the only Trace.Listeners usage in the repo, and it was added to a suite that is concurrently being destabilised by exactly that class of problem (#430). The test now proves the property that actually ships — a throwing StreamFrameDiscarded subscriber does not break the frame pipeline, and the next frame still decodes — using only a throwing subscriber, with a call counter so it cannot pass vacuously. SafeTrace stays in production, where it is the real fix; the codebase already treats its twin, DaqifiDevice.SafeLog, as covered through an injected logger rather than global state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 4165dff |
Why
On the firmware that ships today (3.7.2), the very first frame of every stream carries one analog value no matter how many channels you enabled. Core already hid that from its decoded per-channel path, but handed the bad frame straight to raw-frame consumers — which is the path most callers actually use. The example CLI's offline export read a channel count of one off it and silently truncated every sample after it (example-app#34). Exit codes stayed 0 the whole time.
What
That first frame no longer reaches consumers on either path. A well-formed first frame is completely unaffected, so this is safe to leave in permanently once the firmware fix ships. Because silently dropping data is its own kind of bug, every drop is now reported: a new
StreamFrameDiscardedevent says which frame went and why, andDiscardedStreamFrameCountgives a running total for the session.How
The partial-frame check that already existed for the decoded path moved up one level, so it now gates the raw
MessageReceivedevent too — a frame with fewer analog values than enabled channels is withheld from both. Its digital payload is still decoded and its timestamp still anchors the session clock, exactly as before.This also adds the cross-session leftover guard #351 asked for (firmware #533), as a small
StreamFrameGatecollaborator rather than more state on the device class. The device latches the last frame of a stopped session and replays it at the next start; the guard spots it by its device-tick counter, using modularuintarithmetic so it stays correct across the counter's 86-second wrap. Two deliberate departures from the desktop reference it was ported from: the window is measured in sample periods rather than a fixed 2.5 s, and comparisons are made against a counter fixed at session start rather than the last frame seen — together those stop a quick stop/start from cascading into a run of discarded real frames, which the reference implementation would do. Discards are capped as a backstop.Behaviour change worth knowing about:
MessageReceived/StreamMessageReceivedno longer fire for a frame Core rejects. That is the fix, but a consumer that counts raw frames will see one fewer at stream start —StreamFrameDiscardedis how you tell that apart from a dropout.Bench test
Real Nq1, FW 3.7.2, example CLI built against this branch. The raw serial bytes confirm the defect is on the wire, not in decoding — frame 1 is
12 01 02(analog field, 1 byte), frame 2 is12 04 04 10 00 00(4 bytes).USB/serial — the issue's exact repro,
--rate 20 --limit 4 --channels 15:Steady state, USB, 25 s — no drops or stall introduced:
WiFi/TCP
192.168.1.30,--channels 3(one consolidated connect):Full Core suite green on net9.0 and net10.0 (2487 passed).
Closes #425