Skip to content

stream: per-frame air-side timing in addr3 + a hardware-clocked marker - #479

Merged
josephnef merged 2 commits into
masterfrom
stream-timing
Oct 10, 2026
Merged

josephnef merged 2 commits into
masterfrom
stream-timing

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

What

Per-frame TX-side timing for the stream link, the way kestrel-air's slice header does it, on devourer's own clock (the MAC TSF):

  • addr3 of every stream frame (streamtx, svctx, duplex) carries six bytes no receiver otherwise reads: backlog depth, capture→send_packet, and the transmitter's predicted TSF at the send (src/StreamTelemetry.h). FEC bodies untouched, MTU unchanged; the old addr3 contents decode as "no telemetry".
  • A periodic marker (DEVOURER_STREAM_TIMING=N), a probe response with the canonical SA, carries the host↔TSF fit state and the window's p50/max of stdin-read→send, send_packet wall time, capture→send and depth.
  • The producer's capture stamp arrives through the stdin control escape streamtx and duplex now share (stream_stdin::read_item, opcode 4 CAPTURE_TS); the Python producers emit it with --capture-ts.
  • rxdemo fits the transmitter's clock from hardware egress pairs only and adds lat_us / c2a_us to rx.frame, plus rx.timing per marker. tests/stream_timing_analyze.py summarizes a capture.
  • AdapterCaps::hw_injected_mgmt_txtsf, TxStats::inflight, a shared tsf_linfit.h (timesync re-exports it).

Measured, not assumed

tests/probe_resp_egress_tsf_check.sh (a constant in the timestamp field, an independent witness reading it back): an injected probe response / beacon is MAC-stamped at egress on Jaguar2 (8812BU), Jaguar3 (8812CU, 8812EU) and Kestrel (8832CU), 34–41 µs spread; not on Jaguar1 — the 8821AU writes a free-running counter that is neither TSF port (both sampled live). Its hardware TBTT beacon is stamped (3.2 µs), so that family arms the beacon as its clock carrier on a fixed channel and reports durations only while hopping. The RxPacket.h comment that claimed otherwise is corrected.

tests/stream_timing_onair.sh, CF-924AC (8822BU) witness, 20 s runs, floor first:

transmitter submit→air p50 run-to-run sd depth
8812CU ch6 (5 runs) 98–146 µs 3–16 µs 0
8832CU ch6 90 µs 0 0
8821AU ch6, beacon clock 358–434 µs 34 µs 2
8812EU ch36, producer paced 15 ms 109–122 µs 6 µs 0
8812CU, svctx / duplex 155–166 / 124–150 µs — 0

A 20 ms producer delay on every 10th record is recovered as 20.08–20.09 ms on 9.8–10.2% of frames on every part; slot hopping (1/6/11 @ 50 ms) keeps the clock on Jaguar3; CRC-failed frames never feed the fit. Headless: ctest 85/85, the new selftests green under ASan+UBSan.

The adversarial readings sit in docs/stream-timing.md beside the numbers: Jaguar1's async transport shows as depth 2 and ~400 µs; the 8812EU's 20 ms bulk-OUT stalls on 5 GHz pin capture→send at its clip when the producer outruns it (a true backlog reading, hence PACE_US); a fit sampled across the Jaguar1 beacon arm reads a decaying 62 ms (hence the beacon-first order and the discontinuity reset); a duplex fed during bring-up times out every send (hence the 12 s feeder lead the ARQ harness already uses, and the fit arming on the first record).

Follow-ups filed

#474 addr1 as five more bytes · #475 svctx live stdin mode · #476 CCX queue time join (and A-MPDU semantics) · #477 hw_injected_mgmt_txtsf on 8812AU/8814AU/MT7612U · #478 duplex gating its TX thread on bring-up.

🤖 Generated with Claude Code

Six bytes in every stream frame's addr3 (no receiver read it: every consumer
keys on addr2 and slices the body at +24) carry the transmitter's backlog
depth, capture->send_packet and its predicted TSF at the send; a periodic
probe-response marker (DEVOURER_STREAM_TIMING=N) carries the host<->TSF fit
state and the window's stage statistics. rxdemo fits the transmitter's clock
from hardware egress pairs only and reports a one-way submit->arrival and
capture->arrival latency per frame (rx.frame lat_us / c2a_us, rx.timing per
marker). The producer's capture time arrives through the stdin control escape
streamtx and duplex now share (opcode 4, CAPTURE_TS); the Python producers
emit it with --capture-ts.

Measured first (tests/probe_resp_egress_tsf_check.sh): an injected probe
response is MAC-stamped with the egress TSF on Jaguar2, Jaguar3 and Kestrel
but not on Jaguar1, where the 8821AU writes a counter that is neither TSF
port -- hence AdapterCaps::hw_injected_mgmt_txtsf, and the hardware beacon as
Jaguar1's clock carrier on a fixed channel. Both fits restart on a >50 ms
discontinuity (the beacon arm pulses the TSF; a re-init zeroes it). The TX
fit never reads a register on the send path: one ReadTsf per 100 ms.

On air (tests/stream_timing_onair.sh, CF-924AC witness): submit->air p50
~100-150 us on 8812CU / 8832CU / paced 8812EU, ~400 us with depth 2 on the
async 8821AU; a 20 ms producer delay on every 10th record is recovered as
20.08 ms on 10% of frames; the clock survives slot hopping; svctx and duplex
stamp identically. A producer that outruns the chip (8812EU on 5 GHz, 20 ms
bulk-OUT stalls) pins capture->send at its clip -- a true backlog reading,
documented with the harness's pacing knob.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add hardware-clocked per-frame stream timing telemetry

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Carry backlog, capture-to-send time, and predicted TSF in every stream frame without changing its
 payload.
• Add hardware-clocked markers and receiver fits for one-way latency, with a beacon fallback where
 needed.
• Validate clock behavior and latency across chip families, stream demos, hopping, and corrupted
 frames.
Diagram

sequenceDiagram
    actor P as Producer
    participant S as Stdin control
    participant T as Stream TX
    participant F as Host TSF fit
    participant R as Radio MAC
    participant X as rxdemo
    participant A as Analyzer
    P->>S: Capture stamp and record
    S->>T: Next-record timing
    T->>F: Request predicted TSF
    F-->>T: Fitted timestamp
    T->>R: Frame with addr3 timing
    R-->>X: Frame and RX timestamp
    T->>R: Periodic timing marker
    R-->>X: Hardware egress pair
    Note over R,X: Jaguar1 uses a hardware beacon on fixed channels
    X-->>A: Frame latency and marker events
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reuse timesync beacons as the sole clock carrier
  • ➕ Would reuse an established hardware-clocked synchronization path.
  • ➖ Would require beacon operation for transmitters that can stamp injected markers.
  • ➖ A fixed-channel beacon cannot follow the supported hopping case.

Recommendation: Prefer the PR's integrated marker and per-frame header approach: it preserves existing stream payloads, avoids register reads on the send path, and supports hopping on capable chips. Retain the hardware-beacon path specifically for chips whose injected management timestamps are not usable TSFs.

Files changed (35) +2091 / -87

Enhancement (18) +1043 / -29
host_tsf_fit.hPredict TSF from off-path host-clock samples +135/-0

Predict TSF from off-path host-clock samples

• Adds a polling host-to-TSF linear fit for send-time prediction. It handles failed reads, unsupported clocks, and large TSF discontinuities.

examples/common/host_tsf_fit.h

stream_stdin.hShare stdin control-item parsing +75/-0

Share stdin control-item parsing

• Adds a bounded reader that distinguishes PSDUs from control TLVs and parses next-record CAPTURE_TS stamps.

examples/common/stream_stdin.h

stream_timing_tx.hCentralize transmitter timing and marker emission +215/-0

Centralize transmitter timing and marker emission

• Adds shared per-frame stamping, capture association, stage statistics, and configurable periodic markers. It starts clock sampling on the first record and arms a fixed-channel hardware beacon when injected frames lack trustworthy egress stamps.

examples/common/stream_timing_tx.h

main.cppAdd timed frames and capture controls to duplex +38/-21

Add timed frames and capture controls to duplex

• Uses the shared stdin item reader and transmitter timing helper to associate capture stamps, fill addr3, and send markers under the transmit lock.

examples/duplex/main.cpp

main.cppCalculate receiver-side stream latency +90/-0

Calculate receiver-side stream latency

• Fits the transmitter clock using uncorrupted hardware-egress pairs, decodes timing markers, and emits per-frame latency where the fit is ready. The fit resets on large clock discontinuities.

examples/rx/main.cpp

main.cppStamp streamtx frames and send timing markers +37/-3

Stamp streamtx frames and send timing markers

• Accepts capture control items, stamps addr3 before each send, and emits periodic markers, including on hopped channels. Reports unused capture stamps at completion.

examples/streamtx/main.cpp

main.cppStamp SVC stream frames +14/-0

Stamp SVC stream frames

• Adds the shared timing field and markers to replayed NAL fragments, using the current loop iteration as the capture fallback.

examples/svctx/main.cpp

AdapterCaps.hDescribe injected-management timestamp capability +17/-1

Describe injected-management timestamp capability

• Adds a conservative capability flag distinguishing hardware-stamped host-injected management frames from hardware beacons.

src/AdapterCaps.h

StreamTelemetry.hDefine versioned frame and marker wire formats +319/-0

Define versioned frame and marker wire formats

• Adds six-byte addr3 timing and vendor-IE marker codecs, latency and TSF-unwrapping helpers, management-frame builders, and windowed statistics.

src/StreamTelemetry.h

TxStats.hExpose transport in-flight depth +7/-0

Expose transport in-flight depth

• Adds an in-flight frame count to transmit statistics for the per-frame backlog field.

src/TxStats.h

UsbTransport.cppPopulate in-flight transmit statistics +2/-0

Populate in-flight transmit statistics

• Reports the nonnegative outstanding USB transmit count through TxStats.

src/UsbTransport.cpp

RtlJaguar2Device.cppEnable injected-frame TSF stamps on Jaguar2 +1/-0

Enable injected-frame TSF stamps on Jaguar2

• Advertises the measured egress-stamping capability for injected probe responses and beacons.

src/jaguar2/RtlJaguar2Device.cpp

RtlJaguar3Device.cppEnable injected-frame TSF stamps on Jaguar3 +1/-0

Enable injected-frame TSF stamps on Jaguar3

• Advertises the measured egress-stamping capability used by timing markers, including while hopping.

src/jaguar3/RtlJaguar3Device.cpp

RtlKestrelDevice.cppEnable injected-frame TSF stamps on Kestrel +1/-0

Enable injected-frame TSF stamps on Kestrel

• Advertises the measured egress-stamping capability for injected management frames.

src/kestrel/RtlKestrelDevice.cpp

fused_fec_tx.pyOptionally stamp fused-FEC output +5/-1

Optionally stamp fused-FEC output

• Adds capture-time CLI options and prefixes emitted records with capture controls when enabled.

tools/precoder/fused_fec_tx.py

stream.pyShare producer capture-stamp support +66/-0

Share producer capture-stamp support

• Defines stdin control framing, CAPTURE_TS encoding, capture CLI options, and an optional validation delay between stamp and record.

tools/precoder/stream.py

stream_tx.pyAdd capture stamps and pacing to stream producer +11/-1

Add capture stamps and pacing to stream producer

• Optionally prefixes each record with a capture timestamp and flushes paced records to keep producer backlog measurable.

tools/precoder/stream_tx.py

tun_p2p.pyOptionally timestamp TUN stream records +9/-2

Optionally timestamp TUN stream records

• Adds capture CLI options and prefixes records emitted by both normal and FEC-flush transmit paths.

tools/precoder/tun_p2p.py

Bug fix (1) +4 / -0
RtlJaguarDevice.cppReject injected-frame TSF stamps on Jaguar1 +4/-0

Reject injected-frame TSF stamps on Jaguar1

• Marks host-injected management timestamps as unsuitable clock pairs, leaving the hardware beacon as the fallback.

src/jaguar1/RtlJaguarDevice.cpp

Refactor (3) +61 / -54
tsf_linfit.hExtract reusable TSF reconstruction and fitting +55/-0

Extract reusable TSF reconstruction and fitting

• Moves timestamp reconstruction and incremental linear fitting into a device-independent header shared with timesync.

examples/common/tsf_linfit.h

timesync.hReuse shared TSF fitting primitives +5/-43

Reuse shared TSF fitting primitives

• Re-exports the extracted reconstruction and linear-fit types so existing timesync call sites remain unchanged.

examples/timesync/timesync.h

adaptive_link.pyReuse shared stream control encoders +1/-11

Reuse shared stream control encoders

• Imports existing control and PSDU framing functions from the stream module instead of maintaining local duplicates.

tools/precoder/adaptive_link.py

Documentation (6) +193 / -4
CLAUDE.mdDocument stream timing conventions +13/-0

Document stream timing conventions

• Explains the addr3 field, periodic marker, capture control opcode, and hardware-clock limitations for contributors.

CLAUDE.md

README.mdLink stream timing documentation +3/-0

Link stream timing documentation

• Adds the new per-frame stream timing guide to the documentation index.

README.md

logging.mdSpecify stream timing event fields +5/-3

Specify stream timing event fields

• Documents addr3 and latency fields on rx.frame, the rx.timing marker event, and transmitter timing and capture-drop events.

docs/logging.md

stream-timing.mdDescribe the timing protocol and measurements +150/-0

Describe the timing protocol and measurements

• Documents the wire carriers, clock model, hardware-stamp qualification, measured latency matrix, and operational limitations.

docs/stream-timing.md

RxPacket.hClarify when management timestamps are hardware TSFs +5/-1

Clarify when management timestamps are hardware TSFs

• Corrects the TxEgressTsf documentation: injected frames are not trustworthy on every generation, even though hardware beacons are stamped.

src/RxPacket.h

README.mdDocument on-air timing validation +17/-0

Document on-air timing validation

• Explains the witness setup, validation phases, producer pacing, and separate injected-stamp qualification test.

tests/README.md

Other (7) +790 / -0
CMakeLists.txtRegister telemetry codec and selftest +7/-0

Register telemetry codec and selftest

• Adds the telemetry header to the library sources and registers its headless known-answer test with CTest.

CMakeLists.txt

stream_stdin_selftest.cppTest capture control followed by a record +28/-0

Test capture control followed by a record

• Verifies that the shared stdin reader delivers a CAPTURE_TS item, its following data record, and EOF in order.

examples/common/stream_stdin_selftest.cpp

probe_resp_egress_tsf_check.shAutomate injected-frame timestamp qualification +108/-0

Automate injected-frame timestamp qualification

• Runs a transmitter and independent receiver to classify management-frame timestamps and optionally checks the hardware-beacon fallback.

tests/probe_resp_egress_tsf_check.sh

probe_resp_egress_tx.cppInject constant-timestamp witness frames +125/-0

Inject constant-timestamp witness frames

• Alternates probe responses and beacons with a known timestamp while logging live TSF reads for independent clock verification.

tests/probe_resp_egress_tx.cpp

stream_telemetry_selftest.cppCover telemetry wire and latency invariants +137/-0

Cover telemetry wire and latency invariants

• Checks known bytes, backward rejection, clipping, TSF wrap handling, marker validation, latency arithmetic, and window statistics.

tests/stream_telemetry_selftest.cpp

stream_timing_analyze.pySummarize and check receiver timing captures +131/-0

Summarize and check receiver timing captures

• Computes latency and stage summaries after warm-up and validates absolute latency and injected producer-delay recovery.

tests/stream_timing_analyze.py

stream_timing_onair.shExercise timing across on-air scenarios +254/-0

Exercise timing across on-air scenarios

• Orchestrates baseline, delay, hopping, corruption, svctx, and duplex runs against a witness receiver, with pacing and preflight checks.

tests/stream_timing_onair.sh

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Early duplex records can wedge transmission ✓ Resolved
Description
duplex calls timing.maybe_marker() from its TX thread without waiting for the main thread's
Init() to complete. An immediately writing feeder can therefore trigger the new marker send during
bring-up, before the first data-frame send, and no stream.* event reports a gated or ready state.
Code

examples/duplex/main.cpp[386]

+        timing.maybe_marker(g_radiotap);
Evidence
The TX thread is spawned before Init(), and the added marker call sends through that thread without
a readiness check. The PR's on-air documentation records persistent send timeouts when a duplex
feeder writes during bring-up.

Gate duplex transmission until chip bring-up completes
examples/duplex/main.cpp[383-390]
examples/duplex/main.cpp[523-528]
examples/duplex/main.cpp[581-588]
examples/common/stream_timing_tx.h[134-156]
docs/stream-timing.md[130-135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The duplex TX thread can send the new timing marker while the main thread is still bringing up the radio.
## Fix Focus Areas
- examples/duplex/main.cpp[383-390]
- examples/duplex/main.cpp[523-528]
- examples/common/stream_timing_tx.h[134-156]
## Recommended Fix
Buffer early stdin records until Init() signals readiness, then send markers and data in order. Emit a stream.* event when transmission becomes gated or ready.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Two adapters advertise unverified stamps ✓ Resolved
Description
GetAdapterCaps() sets hw_injected_mgmt_txtsf for every Jaguar2 and Kestrel variant, although the
cited injected-frame measurements cover only the 8812BU and 8832CU respectively. The unmeasured
8821C and 8852B variants consequently skip the beacon fallback and mark their probe-response
timestamps as trustworthy to the receiver.
Code

src/jaguar2/RtlJaguar2Device.cpp[1359]

+  c.hw_injected_mgmt_txtsf = true; /* bench 8812BU: injected 0x50/0x80 stamped, 34 µs spread */
Evidence
Both new unconditional assignments cover more variants than their measurement comments identify.
StreamTimingTx uses the flag to decide whether to arm the hardware-beacon fallback and whether to
label a marker's timestamp as hardware-stamped.

CLAUDE.md: Preserve Chip-Specific Feature Support and Honest Fallbacks: CLAUDE.md: Preserve Chip-Specific Feature Support and Honest Fallbacks: CLAUDE.md: Preserve Chip-Specific Feature Support and Honest Fallbacks: CLAUDE.md: Preserve Chip-Specific Feature Support and Honest Fallbacks
src/jaguar2/RtlJaguar2Device.cpp[1347-1359]
src/kestrel/RtlKestrelDevice.cpp[979-982]
src/jaguar2/ChipVariant.h[10-20]
src/kestrel/ChipVariant.h[8-19]
examples/common/stream_timing_tx.h[64-79]
examples/common/stream_timing_tx.h[139-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The injected-management timestamp capability is enabled on variants for which the PR records no positive measurement.
## Fix Focus Areas
- src/jaguar2/RtlJaguar2Device.cpp[1357-1359]
- src/kestrel/RtlKestrelDevice.cpp[979-982]
## Recommended Fix
Set the capability only for the measured Jaguar2 and Kestrel variants. Leave other variants false unless an on-air injected-frame measurement establishes that their timestamps are live egress TSF values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Some receivers report invented latency ✓ Resolved
Description
packetProcessor builds its clock fit from RxAtrib.tsfl without checking whether the receiver
supplies a hardware RX timestamp. On an MT7612U receiver, whose capability is false and whose
implementation does not populate tsfl, enough marker pairs make the fit ready despite having no
usable local clock, allowing lat_us to be emitted from that fit.
Code

examples/rx/main.cpp[R1144-1145]

+    if (sa_canon && !corrupted && packet.Data.size() >= 24) {
+      rt_local = g_rt_recon(packet.RxAtrib.tsfl);
Evidence
The MT7612U explicitly reports no hardware RX timestamp, but the new fit accepts its tsfl values;
readiness depends on sample count rather than distinct local timestamps.

src/mt7612u/Mt7612uRadio.cpp[1295-1295]
examples/rx/main.cpp[1144-1145]
examples/rx/main.cpp[1174-1187]
examples/common/tsf_linfit.h[38-45]
examples/rx/main.cpp[1364-1369]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Receivers without hardware RX timestamps can fit and publish meaningless one-way latency.
## Fix Focus Areas
- examples/rx/main.cpp[1144-1145]
- examples/rx/main.cpp[1365-1369]
## Recommended Fix
Check the receiver's `hw_rx_timestamp` capability before adding egress pairs or emitting absolute latency; continue emitting stage durations and marker statistics without a fit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (1)
4. Short frames cause out-of-bounds reads in rxdemo ✓ Resolved
Description
packetProcessor calls ev.hex("a3", packet.Data.data() + 16, 6) and FrameTiming::decode for
rx.frame after checking only that packet.Data.size() >= 16, although both operations need bytes
16–21. A 16–21-byte frame that passes the stream SA and corruption gates—such as an RTS or PS-Poll
frame with DEVOURER_RX_AGG_SA=any, or a retained truncated canonical-SA frame—reaches both reads,
and the resulting garbage bytes can enter the rx.frame event.
Code

examples/rx/main.cpp[R1356-1359]

+      ev.hex("a3", packet.Data.data() + 16, 6);
+      {
+        devourer::stream_timing::FrameTiming ft;
+        if (devourer::stream_timing::FrameTiming::decode(packet.Data.data() + 16, ft)) {
Evidence
The enclosing check at line 1120 admits frames as short as 16 bytes, and the stream output gate at
line 1300 adds SA and corruption conditions but no length check. Ev::hex reads the six bytes
supplied at offset 16, while FrameTiming::decode dereferences in[0] through in[5]; both
therefore require at least 22 bytes. The timing-pair block at line 1144 checks for at least 24
bytes, but the rx.frame addr3 operations do not.

examples/rx/main.cpp[1120-1131]
examples/rx/main.cpp[1300-1301]
src/StreamTelemetry.h[103-113]
examples/rx/main.cpp[1119-1144]
examples/rx/main.cpp[1301-1301]
examples/rx/main.cpp[1356-1359]
src/Event.h[159-171]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`packetProcessor` emits and decodes addr3 from bytes 16–21 of `rx.frame` without ensuring those bytes are present, allowing short frames that pass the stream gates to trigger out-of-bounds reads.
## Fix Focus Areas
- examples/rx/main.cpp[1356-1372]
## Recommended Fix
Guard both the `ev.hex("a3", ...)` call and the `FrameTiming::decode` block with `packet.Data.size() >= 24`, matching the header size assumed elsewhere in the stream path. Omit both addr3 emission and its telemetry decode for shorter frames.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Root guide repeats timing wire details ✓ Resolved
Description
The new CLAUDE.md paragraph restates the addr3 fields, marker behavior, clock-carrier fallback and
control opcode already described in the timing documentation and headers. A later change to the
field or fallback must now update this root-level copy as well as the narrowly scoped descriptions,
or leave conflicting guidance.
Code

CLAUDE.md[R454-457]

+Per-frame TX-side timing rides every stream frame's addr3 (six bytes no
+receiver otherwise reads: backlog depth, capture→send, the transmitter's
+predicted TSF) plus a periodic marker (`DEVOURER_STREAM_TIMING=N`), so
+`rxdemo` reports a one-way submit→arrival and capture→arrival latency per
Evidence
The added root paragraph repeats stream-specific details present in the new focused document and
wire-codec header, rather than only directing readers to them.

CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence-Balanced: CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence-Balanced: CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence-Balanced: CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence-Balanced
CLAUDE.md[454-465]
docs/stream-timing.md[10-43]
src/StreamTelemetry.h[4-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The root guide duplicates stream-specific wire and fallback details maintained in narrower documentation and headers.
## Fix Focus Areas
- CLAUDE.md[454-465]
## Recommended Fix
Replace the detailed paragraph with a short cross-cutting overview and links to docs/stream-timing.md and the relevant headers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A restart can corrupt early latency readings ✓ Resolved
Description
packetProcessor checks an egress pair for a TSF discontinuity only after g_rt_fit.ready()
becomes true. If the transmitter restarts during the first 15 pairs, the next pair joins the
previous clock epoch and can make the mixed fit ready, so subsequent stream frames receive incorrect
lat_us and c2a_us until a later pair resets it.
Code

examples/rx/main.cpp[1176]

+          if (g_rt_fit.ready()) {
Evidence
Readiness requires 16 samples, the reset is guarded by readiness, and every pair is otherwise
retained and used once the fit becomes ready.

examples/common/tsf_linfit.h[33-38]
examples/rx/main.cpp[1176-1187]
examples/rx/main.cpp[1364-1369]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A transmitter restart during receiver fit warm-up mixes samples from two TSF epochs.
## Fix Focus Areas
- examples/rx/main.cpp[1174-1185]
## Recommended Fix
Detect clock discontinuities while collecting the initial pairs, before the 16-sample readiness threshold, and discard the prior epoch before adding the new pair.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Channel changes mislabel timing markers ✓ Resolved
Description
duplex sets the timing object's channel only from its startup channel, while its SET_CHAN
control retunes the radio without updating that object. After a live channel change, subsequent
timing markers still advertise the original channel in their DS parameter set.
Code

examples/duplex/main.cpp[318]

+  timing.set_channel(static_cast<uint8_t>(args.channel));
Evidence
The new timing state receives only the initial channel; the existing control path changes the device
channel, and marker construction reads the unchanged timing state.

examples/duplex/main.cpp[316-319]
examples/duplex/main.cpp[351-354]
examples/common/stream_timing_tx.h[94-95]
examples/common/stream_timing_tx.h[153-156]
src/StreamTelemetry.h[251-259]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Duplex timing markers retain the startup channel after a live channel change.
## Fix Focus Areas
- examples/duplex/main.cpp[318-318]
- examples/duplex/main.cpp[351-354]
## Recommended Fix
Update `StreamTimingTx` when `SET_CHAN` successfully changes the radio channel, and ensure any active beacon's advertised channel is refreshed as appropriate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (3)
8. Long timing windows report the wrong median ✓ Resolved
Description
TimingWindow::push stops retaining samples after 8,192 frames, but drain_into computes p50 from
those retained samples while reporting counts and maxima for the full window. When
DEVOURER_STREAM_TIMING exceeds 8,192 data frames, later frames cannot influence the reported
queue, write, or capture-to-send medians.
Code

src/StreamTelemetry.h[R300-303]

+  static void push(std::vector<uint64_t> &v, uint64_t x, uint64_t &mx) {
+    if (x > mx) mx = x;
+    if (v.size() < kMaxSamples) v.push_back(x);
+  }
Evidence
The marker interval is configurable without an 8,192-frame limit; the accumulator updates
full-window counts and maxima but discards later samples before computing p50.

examples/common/stream_timing_tx.h[55-59]
src/StreamTelemetry.h[273-283]
src/StreamTelemetry.h[286-308]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
For marker intervals above 8,192 frames, timing p50 values describe only the start of each window.
## Fix Focus Areas
- src/StreamTelemetry.h[273-273]
- src/StreamTelemetry.h[286-308]
## Recommended Fix
Use a bounded quantile estimator or representative sampling across the entire window, or enforce and document a marker interval that keeps every frame represented.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Adaptive link now needs numpy to start ✓ Resolved
Description
adaptive_link.py now imports ctl_frame/psdu_frame/SET_* from stream, and stream.py
imports numpy and encode_subcarriers (which also imports numpy) at module load. Before this
change adaptive_link and its dependencies (rc_proto, rendezvous, controller, op_table, energy_model,
fec_subblock) were deliberately numpy-free. The on-air scripts launch it with plain python3, so in
an interpreter without numpy (for example the GNU Radio/orchestrator env) it now fails at import,
before it ever drives duplex.
Code

tools/precoder/adaptive_link.py[356]

+from stream import ctl_frame, psdu_frame, SET_PWR, SET_RATE, SET_CHAN  # noqa: E402
Evidence
stream.py line 41 is import numpy as np, followed by an import from encode_subcarriers, which also
imports numpy. adaptive_link's other imports are numpy-free by design: energy_model.py and
fec_subblock.py state they are numpy-free so they can import into the GNU Radio / orchestrator env.
The on-air harnesses run python3 $PREC/adaptive_link.py directly rather than through the uv
project env.

tools/precoder/stream.py[34-55]
tools/precoder/energy_model.py[27-27]
tools/precoder/fec_subblock.py[62-62]
tests/adaptive_onair.sh[59-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
adaptive_link.py now imports from stream.py, which pulls in numpy and encode_subcarriers at import time and breaks adaptive_link's numpy-free dependency set.
## Fix Focus Areas
- tools/precoder/adaptive_link.py[356-356]
- tools/precoder/stream.py[584-645]
## Recommended Fix
Move CTL_FLAG, SET_*, CAPTURE_TS, ctl_frame, psdu_frame, capture_ts_frame, CaptureStamper and the arg helpers into a new dependency-light module (e.g. tools/precoder/stdin_ctl.py) that imports only struct/time. Import that module from adaptive_link.py, and re-export the names from stream.py for the other producers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Timing markers are counted as video frames by the adaptive link ✓ Resolved
Description
Receivers gate only on the canonical SA, so the new 0x50 marker frames (and, on Jaguar1, the 0x80
hardware beacon) are emitted as rx.frame alongside the data frames. adaptive_link.py takes
body[:2] as a sequence number and feeds it to vrx.on_video() without validating the stream
envelope. When DEVOURER_STREAM_TIMING is set on a duplex VTX, every marker or beacon body (whose
first bytes are the timestamp field) becomes a bogus sequence observation, which skews loss and
rung/MCS tracking.
Code

examples/duplex/main.cpp[R384-387]

+      {
+        std::lock_guard<std::mutex> lr(g_rt_mu);
+        timing.maybe_marker(g_radiotap);
+      }
Evidence
The duplex RX path checks only size >= 16 and the canonical addr2 before emitting
rx.frame/stream.rx. The marker is built with the same SA as addr2. adaptive_link derives a seq from
the raw body and calls on_video with no magic/CRC check. stream_rx.py and tun_p2p.py reject such
bodies through stream.decode_body, but adaptive_link does not.

examples/duplex/main.cpp[186-221]
tools/precoder/adaptive_link.py[387-403]
tools/precoder/adaptive_link.py[102-115]
src/StreamTelemetry.h[237-256]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Stream-timing markers (FC 0x50) and Jaguar1 beacons (FC 0x80) share the canonical SA and reach adaptive_link's video accounting as if they were data frames.
## Fix Focus Areas
- tools/precoder/adaptive_link.py[387-403]
- examples/duplex/main.cpp[186-221]
## Recommended Fix
Pick one: (a) have the duplex/rx demos emit an `fc0` field in rx.frame and have adaptive_link ignore frames whose FC is not the probe-request data subtype (0x40); or (b) validate the stream envelope magic before calling on_video.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

11. Fit status log always reads 'running' ✓ Resolved
Description
ensure_started() logs _fit->unsupported() ? "unsupported" : "running" right after
_fit->start() spawns the poller thread. That thread only sets _unsupported after five zero
reads, about 500 ms later, so this line can never report "unsupported". On a part without a TSF read
(the RTL8733B), operators see "fit running" even though no frame will ever carry a TSF.
Code

examples/common/stream_timing_tx.h[R81-84]

+    _fit = std::make_unique<HostTsfFit>(_dev);
+    _fit->start();
+    _log.info("stream timing: marker every {} frames, fit {}", _marker_every,
+              _fit->unsupported() ? "unsupported" : "running");
Evidence
_unsupported is set only inside run() after kGiveUpZeros consecutive zero reads with 100 ms sleeps
in between. The log is evaluated synchronously right after the thread is created.

examples/common/host_tsf_fit.h[89-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The 'fit running/unsupported' log is evaluated before the poller can possibly decide, so it always says 'running'.
## Fix Focus Areas
- examples/common/stream_timing_tx.h[81-84]
- examples/common/host_tsf_fit.h[100-104]
## Recommended Fix
Drop the unsupported/running ternary from the start log. Instead, emit a warning (or a stream.timing field) the first time `unsupported()` becomes true, e.g. checked in maybe_marker.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread examples/duplex/main.cpp
Comment thread src/jaguar2/RtlJaguar2Device.cpp Outdated
Comment thread CLAUDE.md Outdated
Comment thread examples/rx/main.cpp Outdated
Comment thread examples/rx/main.cpp
Comment thread src/StreamTelemetry.h Outdated
Comment thread examples/rx/main.cpp Outdated
Comment thread tools/precoder/adaptive_link.py Outdated
Comment thread examples/duplex/main.cpp
Comment thread examples/common/stream_timing_tx.h Outdated
…ean stops

- hw_injected_mgmt_txtsf only on the dies measured (8822B, 8852C); the 8821C
  and 8852B stay false until a cell exists.
- rxdemo: no clock fit or latency on a receiver without a hardware RX stamp
  (hw_rx_timestamp false); addr3 read and decoded only on frames >= 24 bytes;
  a TSF discontinuity is caught from the second pair on, not only once the
  fit is ready (the TX fit likewise); beacon pairs are taken only when the
  live marker says a beacon carries the clock -- a Jaguar1 hardware beacon
  outlived its killed transmitter and poisoned the next run's fit.
- streamtx/svctx/duplex: SIGINT/SIGTERM end the loop through the ordinary
  exit path (device stopped, timing beacon disarmed; verified zero stray
  beacons after a timeout-ended Jaguar1 run); no marker before the first
  data frame went out; the fit log no longer claims a state it cannot know,
  and stream.timing carries fit_unsupported.
- duplex: a live SET_CHAN updates the marker's DS channel.
- TimingWindow: a uniform reservoir, so a window longer than 8192 frames
  reports the whole window's median (selftest: 40000-frame ramp).
- rx.frame gains fc0 (rxdemo + duplex); adaptive_link counts only 0x40 data
  frames as video, so markers and beacons never enter its sequence ledger.
- The stdin control TLVs move to tools/precoder/stdin_ctl.py (stdlib only);
  stream.py re-exports them, adaptive_link stays numpy-free.
- CLAUDE.md keeps a pointer, not a copy, of the wire details.

Re-validated on air: 8812CU floor/delay/hop/corrupt/duplex, 8821AU floor
(beacon clock, fit_resets 0), 8832CU floor, all PASS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@josephnef
josephnef enabled auto-merge (squash) October 10, 2026 06:38
@josephnef
josephnef merged commit 91b4cdc into master Oct 10, 2026
50 of 51 checks passed
@josephnef
josephnef deleted the stream-timing branch October 10, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant