Skip to content

sensing: a busy window must not outlive its hardware session - #442

Open
snokvist wants to merge 3 commits into
OpenIPC:masterfrom
snokvist:feat/busy-window-stop-lifecycle
Open

snokvist wants to merge 3 commits into
OpenIPC:masterfrom
snokvist:feat/busy-window-stop-lifecycle

Conversation

@snokvist

Copy link
Copy Markdown
Collaborator

Follow-up to #440. That PR gave the RTL8733B a busy_window_reset() in
Stop() and argued the other Realtek backends shared the hole. They did.
This measures it on each die, closes it, and adds the on-air arm that proves
both halves.

The bug

An armed window survives Stop(), and the next retune's note then stamps it
Retuned — a spoil reason earned by a hardware session that no longer
exists. The reading is invalid either way, so nothing reports a wrong
number; what it reports is a wrong reason, which is the one thing the
spoil field exists to get right.

The mechanism differs by backend, and the version in #440's commit message
was only right for one of them:

  • RTL8733BSetMonitorChannel re-runs bring_up_to_phy(), which sets
    the flag with_ccx gates on back to true.
  • Jaguar1/2/3 — the retune does no bring-up at all. There the window stays
    reachable because nothing ever clears _brought_up.

Measured, per die

tests/busy_window_probe.sh --mode revive (arm → Stop() → retune → read),
each backend with its own reset removed and then restored:

die without the reset with it
RTL8812AU (jaguar1) spoil=retuned spoil=none
RTL8822BU (jaguar2) spoil=retuned spoil=none
RTL8812CU (jaguar3) spoil=retuned spoil=none
RTL8733BU spoil=retuned spoil=none

The assertion is on the reason, not the reading — and the Jaguar2 is why

My first cut asserted the post-Stop() read was invalid. That is true on
three dies and false on the Jaguar2, whose Stop() only joins its runtime
threads and leaves the chip live, so its sampled path answers with a valid
2 ms window. That assertion would have failed a correct backend.

What holds everywhere is that no spoil reason survives the session that ended.
Verified through the harness on the Jaguar2 specifically: with the reset
removed the arm fails on the spoil check alone (3/3 samples not spoil=none), which is the only signal available on that die.

What the resets do not close

On the Jaguars, unlike the RTL8733B — whose Stop() holds its recursive
register lock across the whole body, which with_ccx takes first — there is
no such span, so a concurrent ArmChannelBusy can still land after the reset
and during teardown. ArmChannelBusy is single-control-thread by contract
(IRadio.h). Closing it properly means clearing _brought_up in Stop(),
which gates other paths and is a behaviour change of its own;
src/rtl8733b/CLAUDE.md records it as the half that stays open.

Locking

Every reset takes the CCX mutex alone, in a scope that acquires no other lock
and calls no device method. Jaguar3's sits outside _reg_mu rather than
inside it as the RTL8733B's does, deliberately: Stop() joins the coex
thread, which takes _reg_mu, so holding it across the join would deadlock —
the same shape as an earlier self-inflicted deadlock in this feature's
history. Jaguar1 has no family-wide register lock (its _port0_mu is narrower
and never taken under the CCX lock). No path takes the CCX mutex and then a
register lock.

The arm refuses where it cannot run

  • MediaTekStop() closes the device and nulls the handle, leaving no
    retune path. Measured: the probe wedges.
  • SENSOR_RX=1 — the Realtek Init runs on a detached thread that
    Stop() would be torn down underneath. That is the use-after-free this
    probe's own cleanup path exists to avoid, and the sequence
    RtlJaguar3Device::InitWrite refuses outright.

Both gate on the sensor VID and the RX flag rather than a capability, because
there is no cap for "Stop() is survivable" — that is a lifecycle fact, not a
feature one.

Also in this change

  • expect_all accepted a partial record set, so a probe that died after one
    rep passed every arm. It now requires REPS records, as expect_spoil does.
  • The harness header listed ten arms and claimed nine. It now lists and counts
    eleven, and says the list is by role rather than run order.
  • The MediaTek skip total is 8 (7 at the early exit plus the revive arm),
    correcting the 7 stated in sensing: port the CCX CLM busy window to the RTL8733B #440.

Verification

ctest 69/69, ASan/UBSan clean on both selftests, and on air:

  • full harness PASS — RTL8733BU sensor vs RTL8822BU flooder, 63% under load
  • full harness PASS — Jaguar2 sensor vs RTL8733BU flooder, 61%
  • the revive arm passing on all three dies present, with the documented
    validity split
  • the RX guard refusing rather than wedging

Pre-existing and unrelated, verified at fc66aa9 without these changes:
the txsess arm hangs on a Jaguar1 sensor, and an RTL8812AU txdemo exits
after ~482 frames, so that die cannot serve as the harness flooder. Worth
knowing because SENSOR_PID defaults to the 8812AU.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SbJ75HT4fqof7e5e8NKL2j

OpenIPC#440 gave the RTL8733B a busy_window_reset() in Stop() and argued the other
Realtek backends shared the hole. They did. This measures it and closes it,
and adds the on-air arm that proves both halves.

THE BUG. An armed window survives Stop(), and the next retune's note then
stamps it Retuned — a spoil reason earned by a hardware session that no
longer exists. The mechanism differs by backend, and the version in OpenIPC#440's
commit message was only right for one of them: on the RTL8733B
SetMonitorChannel re-runs bring_up_to_phy(), which sets the flag with_ccx
gates on back to true. On Jaguar1/2/3 the retune does no bring-up at all —
there the window stays reachable because nothing ever clears _brought_up.

MEASURED, tests/busy_window_probe.sh --mode revive (arm, Stop(), retune,
read), each die with its own reset removed and then restored:

  RTL8812AU  (jaguar1)   spoil=retuned -> none
  RTL8822BU  (jaguar2)   spoil=retuned -> none
  RTL8812CU  (jaguar3)   spoil=retuned -> none
  RTL8733BU              spoil=retuned -> none

THE ASSERTION IS ON THE REASON, NOT THE READING, and the Jaguar2 is why. A
first cut asserted the post-Stop read was invalid; that is true on three dies
and FALSE on the Jaguar2, whose Stop() only joins its runtime threads and
leaves the chip live, so its sampled path answers with a 2 ms window. That
assertion would have failed a correct backend. What holds everywhere is that
no spoil reason survives the session that ended. Verified through the harness
on the Jaguar2 specifically: with the reset removed the arm fails on the
spoil check alone (3/3 not spoil=none), which is the only signal available on
that die.

WHAT THE RESETS DO NOT CLOSE, on the Jaguars. Unlike the RTL8733B — whose
Stop() holds its recursive register lock across the whole body, which
with_ccx takes first — there is no such span on Jaguar1/2/3, so a concurrent
ArmChannelBusy can still land after the reset and during teardown.
ArmChannelBusy is single-control-thread by contract (IRadio.h). Closing it
properly means clearing _brought_up in Stop(), which gates other paths and is
a behaviour change of its own; src/rtl8733b/CLAUDE.md records it as the half
that stays open.

LOCKING. Every reset takes the CCX mutex alone in a scope that acquires no
other lock and calls no device method. Jaguar3's sits OUTSIDE _reg_mu rather
than inside it as the RTL8733B's does, and deliberately: Stop() joins the
coex thread, which takes _reg_mu, so holding it across the join would
deadlock — the same shape as an earlier round's self-inflicted self-deadlock.
Jaguar1 has no family-wide register lock (its _port0_mu is narrower and never
taken under the CCX lock). No path takes the CCX mutex and then a register
lock.

THE ARM REFUSES WHERE IT CANNOT RUN. MediaTek's Stop() closes the device and
nulls the handle, leaving no retune path (measured: the probe wedges). With
SENSOR_RX=1 the Realtek Init runs on a detached thread that Stop() would be
torn down underneath — the use-after-free this probe's own cleanup path
exists to avoid, and the sequence RtlJaguar3Device::InitWrite refuses
outright. Both are gated on the sensor VID and the RX flag rather than a
capability, because there is no cap for "Stop() is survivable"; that is a
lifecycle fact, not a feature one.

Also in this change: expect_all accepted a partial record set, so a probe
that died after one rep passed every arm — it now requires REPS records, as
expect_spoil does. The harness header listed ten arms and claimed nine; it
now lists and counts eleven, and says the list is by role rather than run
order. The MediaTek skip total is 8 (7 at the early exit plus the revive
arm), correcting the 7 stated in OpenIPC#440.

Verified: ctest 69/69, ASan/UBSan clean on both selftests, and on air —
full harness PASS with an RTL8733BU sensor against an RTL8822BU flooder
(63% under load) and with the Jaguar2 as sensor against the RTL8733BU
(61%); the revive arm passing on all three dies present; the RX guard
refusing rather than wedging.

Pre-existing and unrelated, verified at fc66aa9 without these changes: the
txsess arm hangs on a Jaguar1 sensor, and an RTL8812AU txdemo exits after
~482 frames so that die cannot serve as the harness flooder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbJ75HT4fqof7e5e8NKL2j
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Reset Jaguar busy windows when hardware sessions stop

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Reset armed busy windows when Jaguar hardware sessions stop.
• Add revive probing to prevent stale spoil reasons crossing hardware sessions.
• Harden sample-count checks and document the remaining Jaguar lifecycle gap.
Diagram

sequenceDiagram
    participant H as Probe Harness
    participant B as Jaguar Backend
    participant W as Busy Window
    participant R as Radio Hardware
    H->>B: Arm window
    B->>W: Store armed state
    H->>B: Stop session
    B->>W: Reset under mutex
    B->>R: End hardware session
    H->>B: Retune channel
    B->>R: Configure channel
    B->>W: Note retune
    H->>B: Read busy state
    B->>W: Query window
    W-->>B: Spoil none
    B-->>H: Backend-specific validity
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Clear Jaguar bring-up state
  • ➕ Prevents post-Stop arms from reaching torn-down hardware.
  • ➕ Closes the documented concurrent arm race more completely.
  • ➖ Changes behavior for every path gated by _brought_up.
  • ➖ Requires broader backend-specific lifecycle validation, especially for Jaguar2.
2. Track hardware session generations
  • ➕ Automatically invalidates windows created by earlier sessions.
  • ➕ Makes lifecycle ownership explicit instead of relying on resets at every teardown path.
  • ➖ Requires invasive changes to busy-window state and all session transitions.
  • ➖ Adds complexity disproportionate to the stale-reason bug.

Recommendation: Keep the scoped per-backend resets in this PR. They directly fix the measured stale spoil reason while respecting each backend's lock ordering and teardown behavior. Address _brought_up lifecycle semantics separately because clearing it would be a broader behavioral change.

Files changed (6) +187 / -9

Bug fix (3) +62 / -0
RtlJaguarDevice.cppReset Jaguar1 busy windows at session stop +20/-0

Reset Jaguar1 busy windows at session stop

• 'Stop()' now clears the armed busy-window state while holding the CCX mutex. This prevents a later retune from assigning a stale 'retuned' spoil reason to a window from the previous hardware session.

src/jaguar1/RtlJaguarDevice.cpp

RtlJaguar2Device.cppReset Jaguar2 busy windows before stopping runtime threads +22/-0

Reset Jaguar2 busy windows before stopping runtime threads

• 'Stop()' now forgets the active busy window under the CCX mutex before joining runtime threads. The reset preserves Jaguar2's live sampled-read behavior while ensuring no prior-session spoil reason survives.

src/jaguar2/RtlJaguar2Device.cpp

RtlJaguar3Device.cppReset Jaguar3 busy windows with safe lock ordering +20/-0

Reset Jaguar3 busy windows with safe lock ordering

• 'Stop()' clears busy-window state before stopping the coexistence thread. The CCX lock scope remains outside '_reg_mu' to avoid deadlocking with the joined thread.

src/jaguar3/RtlJaguar3Device.cpp

Tests (2) +116 / -3
busy_window_probe.cppAdd revive mode for cross-session busy-window testing +49/-1

Add revive mode for cross-session busy-window testing

• Adds a 'revive' probe sequence that arms a window, stops the device, retunes, and reads the result. It rejects RX-loop usage to avoid unsafe detached-thread teardown and deliberately leaves result validity backend-dependent.

tests/busy_window_probe.cpp

busy_window_probe.shAssert revive spoil reasons and complete sample sets +67/-2

Assert revive spoil reasons and complete sample sets

• Adds the Realtek-only revive arm and verifies every returned spoil reason is 'none' without asserting validity. The harness now rejects partial record sets and corrects its documented arm count and skip accounting.

tests/busy_window_probe.sh

Documentation (1) +9 / -6
CLAUDE.mdDocument Jaguar lifecycle fix and remaining gap +9/-6

Document Jaguar lifecycle fix and remaining gap

• The lifecycle notes now record that Jaguar1/2/3 reset pre-Stop windows. They also document that post-Stop arms remain possible because Jaguar backends do not clear '_brought_up'.

src/rtl8733b/CLAUDE.md

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

qodo-free-for-open-source-projects Bot commented Sep 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Busy contract is copied into three guides 📘 Rule violation ⚙ Maintainability
Description
src/jaguar1/CLAUDE.md, src/jaguar2/CLAUDE.md, and src/jaguar3/CLAUDE.md restate the Stop()
and ArmChannelBusy() behavior already documented on IRadio::ArmChannelBusy instead of only
directing readers to that declaration. Because all three copies describe the same reset and
post-stop gating contract, future API changes can leave generation guidance inconsistent with the
authoritative header.
Code

src/jaguar1/CLAUDE.md[R42-45]

+**`Stop()` forgets any armed busy window, and the hazard it leaves is not
+fully closed.** `with_ccx` gates on `_brought_up`, which `Stop()` does not
+clear, so an `ArmChannelBusy()` issued *after* a `Stop()` still succeeds
+against a chip that has been torn down. The reset in `Stop()` only handles a
Evidence
Compliance rule 2 requires CLAUDE.md guidance to point to authoritative header declarations rather
than copying their documented contracts. The three added guide sections repeat the lifecycle
behavior documented at IRadio::ArmChannelBusy, including that Stop() forgets a window and how
post-stop arming is governed, while also acknowledging that the header contains the contract.

CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md
src/IRadio.h[622-630]
src/jaguar1/CLAUDE.md[42-48]
src/jaguar2/CLAUDE.md[75-81]
src/jaguar3/CLAUDE.md[109-115]

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

## Issue description
Three per-generation `CLAUDE.md` guides copy the common busy-window lifecycle contract that is authoritative on the `IRadio::ArmChannelBusy` declaration, creating multiple copies that can drift.
## Fix Focus Areas
- src/jaguar1/CLAUDE.md[42-48]
- src/jaguar2/CLAUDE.md[75-81]
- src/jaguar3/CLAUDE.md[109-115]
## Recommended Fix
Replace the repeated common contract text with concise references to `IRadio::ArmChannelBusy`. Retain only generation-specific teardown, locking, measured-hardware, and remaining-hazard details in each guide.

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


2. Jaguar guidance sits in the wrong guide ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
src/rtl8733b/CLAUDE.md adds Jaguar1/2/3 lifecycle, gating, and teardown guidance even though
dedicated src/jaguar1, src/jaguar2, and src/jaguar3 guides exist. Jaguar maintainers loading
their generation's nearest guide will miss the remaining post-stop arm hazard, while readers of the
RTL8733B guide must distinguish unrelated backend behavior.
Code

src/rtl8733b/CLAUDE.md[R483-486]

+Jaguar1/2/3 had the same hole and are fixed in the same change — measured on
+each die with the reset removed, an arm/Stop/retune/read reports
+`spoil=retuned`. One half of the asymmetry remains on them and is NOT fixed
+here: their `with_ccx` gates on `_brought_up`, which no `Stop()` clears, so an
Evidence
Compliance rule 1 requires new subsystem- and generation-specific guidance to live in the nearest
applicable nested CLAUDE.md. The cited addition places detailed Jaguar lifecycle behavior in the
RTL8733B-specific guide despite dedicated Jaguar guides being present.

CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md: CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md: CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md: CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md: CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md: CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md: CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md: CLAUDE.md: Place Documentation in the Narrowest Applicable CLAUDE.md
src/rtl8733b/CLAUDE.md[483-491]

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 RTL8733B guide now contains lifecycle and teardown guidance specific to the three Jaguar generations, despite each generation having its own nested guide.
## Fix Focus Areas
- src/rtl8733b/CLAUDE.md[483-491]
- src/jaguar1/CLAUDE.md[88-111]
- src/jaguar2/CLAUDE.md[73-94]
- src/jaguar3/CLAUDE.md[107-138]
## Recommended Fix
Move each generation's post-stop busy-window behavior and remaining arm hazard into its corresponding Jaguar guide. Retain only a concise cross-reference in the RTL8733B guide if the comparison is necessary to explain RTL8733B behavior.

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



Remediation recommended

3. Other supported sensors fail this probe ✓ Resolved 🐞 Bug ≡ Correctness
Description
realtek_sensor is cleared only for vendor 0x0e8d, yet the new revive branch treats every other
vendor as one of the four busy-window backends whose stop lifecycle it tests. Kestrel adapters
refuse ArmChannelBusy() and emit no samples, while supported MediaTek OEM identities also pass
this vendor gate when RX is off, so the harness reports a failed revive arm for hardware outside the
test's scope.
Code

tests/busy_window_probe.sh[R349-350]

+if [ "$realtek_sensor" = "1" ] && [ "$SENSOR_RX" != "1" ]; then
+  run_arm revive "arm, Stop(), retune, read (no load needed)"
Evidence
The new branch runs for every sensor whose VID is not 0x0e8d, but the factory also returns Kestrel
devices for numerous such VIDs and MediaTek devices for many OEM VIDs. Kestrel inherits the default
zero-returning ArmChannelBusy(), causing the probe to emit only busy.skip; expect_spoil then
converts the absence of window records into a test failure.

tests/busy_window_probe.sh[279-282]
tests/busy_window_probe.sh[349-359]
src/kestrel/KestrelUsbIds.h[25-51]
src/mt7612u/Mt7612uUsbIds.h[48-67]
src/WiFiDriver.cpp[156-186]
src/IRadio.h[636-639]
tests/busy_window_probe.cpp[302-305]
tests/busy_window_probe.sh[165-180]

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 revive arm uses a vendor-only classification that includes Kestrel and MediaTek OEM devices even though the test applies only to Jaguar1/2/3 and RTL8733B busy-window implementations.
## Fix Focus Areas
- tests/busy_window_probe.sh[279-282]
- tests/busy_window_probe.sh[349-359]
- tests/busy_window_probe.cpp[228-235]
## Recommended Fix
Read the backend generation from the emitted capability record and run revive only for Jaguar1, Jaguar2, Jaguar3, and RTL8733B with RX disabled. Explicitly skip every other generation rather than inferring lifecycle behavior from the USB vendor ID.

ⓘ 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 add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/rtl8733b/CLAUDE.md Outdated
Review round on OpenIPC#442. The rule this PR adds was written into one backend's
chip guide and three copies of a code comment; none of that is where a reader
looks for it.

THE CONTRACT NOW HAS A DECLARATION. IRadio::ArmChannelBusy lists the three
spoilers and said nothing about the session ending underneath a window;
IRtlRadio::busy_window_reset described itself as a bring-up concern. Both now
carry the lifetime rule, which is the only place a fifth backend would find
it. The review that prompted this asked only for the Jaguar text to move — it
would have moved out of the RTL8733B guide into three others and still had no
home above them.

PER-GENERATION FACTS MOVED TO THE PER-GENERATION GUIDES, worded per die
rather than copied: Jaguar1's reset sits above the teardown_power_down=0
early return and takes the CCX lock alone (no family-wide register lock
exists on that generation); Jaguar2's Stop() only joins its runtime threads,
so the chip stays live and its sampled path still answers; Jaguar3's reset is
outside _reg_mu because Stop() joins a thread that takes it. The measured
four-die matrix moves to docs/rx-spectrum-sensing.md, which the root guide
designates for measured numbers and the generation matrix.

The three Jaguar Stop() comments each asserted RTL8733B internals to explain
their own behaviour; each is now self-contained.

TWO ERRORS OF MINE THAT THIS ROUND CAUGHT, both worth naming because the
first was at the declaration site:

  - The new IRadio.h text said a surviving window "stays reachable across the
    next bring-up". That is the RTL8733B's mechanism and only its own — on
    Jaguar1/2/3 the retune runs no bring-up at all and the window survives
    because nothing clears _brought_up. This PR's own commit message says so
    two paragraphs down. Corrected in both the header and the doc.
  - The new doc section was inserted between the spoiler table and the two
    paragraphs that reference its rows by position ("The third row...", "The
    first row..."), silently re-pointing both at the wrong table. Moved below
    them.

Also: the RTL8733B guide read as contradicting itself, saying in consecutive
paragraphs that clearing _phy_ready does not protect a window and that it
does — true of a window armed before a Stop and an arm issued after one
respectively, now hinged. It also said the Jaguars lack "both" halves of the
protection; they lack either. And the RTL8733BU row of the new matrix
implied its "no reading" came from teardown depth, when that die has no
sampled path at all.

Repaired three mangled comment lines (a blank ' *' welded onto the end of the
preceding line) introduced by 2f95498 in the Jaguar Stop() blocks.

Comments and docs only; no behaviour change. Verified: ctest 69/69,
ASan/UBSan clean on both selftests, and the on-air harness PASS on an
RTL8733BU sensor against an RTL8822BU flooder, 63% under load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbJ75HT4fqof7e5e8NKL2j
@snokvist

Copy link
Copy Markdown
Collaborator Author

Addressed in 200a880, and the finding was the right thread to pull.

The placement finding is correct — Jaguar1/2/3 lifecycle facts were in src/rtl8733b/CLAUDE.md while all three generations have their own guides. Moved, and worded per die rather than copied: Jaguar1's reset sits above the teardown_power_down=0 early return and takes the CCX lock alone (no family-wide register lock exists there); Jaguar2's Stop() only joins its runtime threads so the chip stays live; Jaguar3's reset is outside _reg_mu because Stop() joins a thread that takes it. The measured four-die matrix went to docs/rx-spectrum-sensing.md, which the root guide designates for measured numbers.

But moving it alone would have left the rule homeless. IRadio::ArmChannelBusy lists the three spoilers and said nothing about the session ending under a window, and IRtlRadio::busy_window_reset described itself as a bring-up concern. So the contract existed only in one chip guide and three copies of a code comment. Both declarations now carry it — that is where a fifth backend would actually look.

Also fixed while in there: the three Jaguar Stop() comments each asserted RTL8733B internals to explain their own behaviour; each is now self-contained.

Two errors of mine that this round caught, the first at the declaration site:

  • My new IRadio.h text said a surviving window "stays reachable across the next bring-up". That is the RTL8733B's mechanism and only its own — on Jaguar1/2/3 the retune runs no bring-up at all, and this PR's own commit message says so. Corrected in the header and the doc.
  • My new doc section landed between the spoiler table and the two paragraphs that reference its rows by position ("The third row...", "The first row..."), silently re-pointing both at the wrong table. Moved below them.

And three smaller ones: the RTL8733B guide read as contradicting itself about _phy_ready (true of a window armed before a Stop vs an arm issued after one — now hinged); it said the Jaguars lack "both" halves of the protection when they lack either; and the RTL8733BU row implied its "no reading" came from teardown depth when that die has no sampled path at all.

Comments and docs only, no behaviour change. ctest 69/69, ASan/UBSan clean, and the on-air harness still PASSes on an RTL8733BU sensor against an RTL8822BU flooder at 63% under load.

@snokvist

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread src/jaguar1/CLAUDE.md Outdated
Comment on lines +42 to +45
**`Stop()` forgets any armed busy window, and the hazard it leaves is not
fully closed.** `with_ccx` gates on `_brought_up`, which `Stop()` does not
clear, so an `ArmChannelBusy()` issued *after* a `Stop()` still succeeds
against a chip that has been torn down. The reset in `Stop()` only handles a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Busy contract is copied into three guides 📘 Rule violation ⚙ Maintainability

src/jaguar1/CLAUDE.md, src/jaguar2/CLAUDE.md, and src/jaguar3/CLAUDE.md restate the Stop()
and ArmChannelBusy() behavior already documented on IRadio::ArmChannelBusy instead of only
directing readers to that declaration. Because all three copies describe the same reset and
post-stop gating contract, future API changes can leave generation guidance inconsistent with the
authoritative header.
Agent Prompt
## Issue description
Three per-generation `CLAUDE.md` guides copy the common busy-window lifecycle contract that is authoritative on the `IRadio::ArmChannelBusy` declaration, creating multiple copies that can drift.

## Fix Focus Areas
- src/jaguar1/CLAUDE.md[42-48]
- src/jaguar2/CLAUDE.md[75-81]
- src/jaguar3/CLAUDE.md[109-115]

## Recommended Fix
Replace the repeated common contract text with concise references to `IRadio::ArmChannelBusy`. Retain only generation-specific teardown, locking, measured-hardware, and remaining-hazard details in each guide.

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

Comment thread tests/busy_window_probe.sh
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 200a880

Review round on OpenIPC#442. The harness gated its new arms on "the sensor VID is
not 0x0e8d", which is wrong in both directions and is the discriminator this
whole series argues against.

WRONG IN BOTH DIRECTIONS, verified against the ID tables:

  - Kestrel ships under 0x0bda, 0x0586 and 0x0b05. All pass a "not MediaTek"
    test, and Kestrel overrides neither ArmChannelBusy nor with_ccx, so it
    inherits the default returning 0: the probe emits only busy.skip and the
    harness reports failures for hardware that was never in scope.
  - MediaTek ships under nine OEM VIDs beyond 0x0e8d. Those pass the same
    test, so an OEM-VID MediaTek did not merely fail the new arm — it ran the
    seven LOADED arms the early exit exists to prevent, whose whole premise
    is that this backend cannot bring up on a saturated channel.

0x0b05, 0x2c4e and 0x7392 appear in BOTH the Kestrel and MediaTek tables, so
no VID test can separate those two vendors at all.

Every per-backend decision now reads the probe's own caps record. The first
arm runs on every backend and needs no flooder, so generation and
busy_airtime_ok are available before any decision. A backend with no
busy-airtime engine — Kestrel is the only one — exits 77 as a clean SKIP
rather than emitting a failure per arm. busy_airtime_ok maps exactly to "an
arm is implemented" across all six backends; it does not promise the arm
succeeds right now, and the block says so.

The stale arm's guard turned out to be a tautology: the MediaTek early exit
above it is unconditional, so every path reaching it can arm. Removed, rather
than left as accounting that can never fire.

DOCS. The session-lifetime paragraph was byte-identical in all three Jaguar
guides, which re-created one level up the duplication this PR had just
reduced in the Stop() comments. Replaced with a pointer to the declaration,
keeping each guide's per-die measured facts, which do differ.

That pointer promised "the rule and what it does not cover", and
IRadio::ArmChannelBusy carried only the rule. Rather than weaken the pointer,
the residual is now stated where it belongs: a reset forgets a window armed
BEFORE the teardown, and where nothing clears the flag the engine's accessor
gates on, an arm issued AFTER Stop() still succeeds against a torn-down chip
— true of Jaguar1/2/3, not of the RTL8733B. The three Stop() comments said
"this generation's guide records where it stands", which the same change made
false; they point at the declaration now.

Three claims of mine this round corrected: the revive gate's comment still
described the VID gate it no longer was; "a dozen" OEM VIDs is nine; and
5/5 spoil=none reads as five confirmations when the arm sits inside the rep
loop, so reps 2..N arm a chip the previous rep already tore down and only
rep 1 exercises the intended sequence. The negative control is what
discriminates, and that is now in the probe comment.

Comments, docs and harness gating; no behaviour change to the library.
Verified: ctest 69/69, ASan/UBSan clean on both selftests, and on air —
Jaguar3 sensor against an MT7612U flooder PASS with no skips, MediaTek sensor
against a Jaguar3 flooder PASS with 8 arms skipped (revive by its own branch,
then seven at the early exit; three ran; 11 total).

Pre-existing and unrelated, unchanged by this round: the txsess arm fails on
a Jaguar1 sensor, verified at fc66aa9 without these changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbJ75HT4fqof7e5e8NKL2j
@snokvist

Copy link
Copy Markdown
Collaborator Author

Both findings addressed in 667cb8c, and the first one was worse than reported.

Other supported sensors fail this probe — real, and the blast radius was bigger. The gate was VID != 0x0e8d, which is wrong in both directions: Kestrel ships under 0x0bda/0x0586/0x0b05 and overrides neither ArmChannelBusy nor with_ccx, so it inherits the default returning 0; MediaTek ships under nine OEM VIDs beyond 0x0e8d. And 0x0b05, 0x2c4e and 0x7392 are in both tables, so no VID test can separate those two vendors at all.

The part the review did not reach: that same gate also guards the MediaTek loaded-arms early exit, so an OEM-VID MediaTek did not merely fail the new arm — it ran the seven loaded arms that branch exists to prevent, whose premise is that this backend cannot bring up on a saturated channel. That was the larger hole.

Every per-backend decision now reads the probe's own caps record. The first arm runs on every backend and needs no flooder, so generation and busy_airtime_ok are available before any decision. Kestrel — the only backend with no busy-airtime engine — now exits 77 as a clean SKIP instead of emitting a failure per arm for hardware never in scope. I also found the stale arm's guard was a tautology (the MediaTek exit above it is unconditional), so it is removed rather than left as accounting that can never fire.

Busy contract copied into three guides — correct. The paragraph was byte-identical in all three; replaced with a pointer to the declaration, keeping each guide's per-die measured facts, which do differ.

That pointer promised "the rule and what it does not cover", and IRadio::ArmChannelBusy carried only the rule. Rather than weaken the pointer I put the residual where it belongs: a reset forgets a window armed before the teardown, and where nothing clears the flag the engine's accessor gates on, an arm issued after Stop() still succeeds against a torn-down chip — true of Jaguar1/2/3, not of the RTL8733B. The three Stop() comments claimed "this generation's guide records where it stands", which the same change made false; they point at the declaration now.

Three of my own claims corrected this round: the revive gate's comment still described the VID gate it no longer was; "a dozen" OEM VIDs is nine; and 5/5 spoil=none reads as five confirmations when the arm sits inside the rep loop — reps 2..N arm a chip the previous rep already tore down, so only rep 1 exercises the intended sequence and the negative control is what discriminates. That is now in the probe comment.

ctest 69/69, ASan/UBSan clean, and on air: Jaguar3 sensor vs MT7612U flooder PASS with no skips; MediaTek sensor vs Jaguar3 flooder PASS with 8 arms skipped (1 + 7, three ran, 11 total).

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