Skip to content

sensing: port the CCX CLM busy window to the RTL8733B - #440

Merged
josephnef merged 2 commits into
OpenIPC:masterfrom
snokvist:feat/8733b-clm
Sep 20, 2026
Merged

josephnef merged 2 commits into
OpenIPC:masterfrom
snokvist:feat/8733b-clm

Conversation

@snokvist

@snokvist snokvist commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

The RTL8733B has a working CCX CLM engine. #437's caps note recorded that and
left the port as a follow-up; this is it.

Where the register block comes from

In this tree: hal/hal8733b_tables.c:792 onward — the shipped 8733B phy_reg
init table — programs 0x1e40, 0x1e44, 0x1e48, 0x1e5c and 0x1e60 at
bring-up. That is the CCX block at the JGR3 addresses, on this die, checkable
without leaving the repo. The vendor agrees: at the pinned
reference/rtl8733bu-20230626, hal/phydm/phydm_pre_define.h:513 lists
ODM_RTL8733B in PHYDM_IC_SUPPORT_IFS_CLM, and :523-525 define
PHYDM_IC_JGR3_SERIES_SUPPORT when RTL8733B_SUPPORT is set.

That init table is also the only other writer of those registers —
nothing in src/rtl8733b/ touches them at runtime — so the masked
read-modify-writes cannot corrupt another subsystem's state, and
arm_clm_only never touches 0x1e5c, where the table leaves a non-zero
value.

with_ccx() lends the engine nhm_regs_jgr3() under _reg_mu then the CCX
mutex, the way Jaguar2/3 do. No new machinery.

Busy airtime without a phydm counter block

arm_clm_only/read_clm_only take no IGI argument — busy airtime is a
hardware tick count, not a histogram referenced to the receiver's own noise
floor the way NHM's thresholds are. So CLM ports cleanly to a backend that
implements no GetRxEnergy.

This is the first backend where the two caps split: busy_airtime_ok
true, rx_energy_ok false. The sampled path still reports no reading here;
an armed window is the only way a number comes out. AdapterCaps.h's
busy_airtime_ok definition is amended accordingly — it promised that
GetChannelBusy answers, which on this die is true only after
ArmChannelBusy.

Measured

tests/busy_window_probe.sh, RTL8733BU sensor against an MT7612U flooder on
ch165:

arm result
quiet 0.0%
steady load 69%, spread 0 over 6 windows
bursty 50/450 ms mean 9-10%, true duty ~9%
retune mid-window refused, spoil=retuned
read before elapsed refused, spoil=not-elapsed
re-arm then read never returns the previous latched value
sensor transmitting valid, own_tx flagged

A Jaguar3 8812CU on the same flooder read 69% too, so the two device paths
onto the JGR3 map agree on one load. busy_airtime_measured is set on that
evidence, per the house rule.

The Interrupted spoiler is unreachable on this die by construction: with no
NHM read, nothing can re-arm the shared engine mid-window. The CCA gate cannot
bias the reading either — SetCcaMode throws on this backend, so a
CCA-disabled session is unreachable rather than merely unlikely.

Known gap, pre-existing, with one affected consumer

src/sensing/SenseWindow.h picks its source by whether the IRtlRadio* is
non-null rather than by rx_energy_ok, so on this backend it takes the phydm
branch, gets the all-invalid base GetRxEnergy, and never calls
GetChannelBusy(). That is the same wrong discriminator AdapterCaps.h opens
by warning about.

examples/chanscout hits it. main.cpp:323 builds an IRtlRadio* by
dynamic_cast, which succeeds on this die, and hands it to a DwellExecutor
at :342 — so a scout on an RTL8733B reports neither CLM nor NHM.

(An earlier revision of this description claimed nothing constructs
SenseWindow outside tests. That was false, and it was the premise this
section rested on. Corrected.)

Gating that branch on the capability is necessary but not sufficient: an
unarmed GetChannelBusy() falls back to
busy_from_rx_energy(GetRxEnergy(true)) and yields nothing here, and nothing
in src/sensing/ ever calls ArmChannelBusy — the only callers in the tree
are tests/busy_window_probe.cpp. A complete fix is the gate plus an arm,
and arming needs an observation window that no layer between
ScanPlanConfig::dwell_ms and SenseWindow carries. It would also change
behaviour on four other backends, where an arm interacts with the NHM read
that spoils the same engine — which is what tests/dwell_exec_selftest.cpp
exists to pin.

So this is a real gap with a real consumer, and I do not think it belongs in a
backend port: the fix is a src/sensing/ design change needing its own tests
and its own review. The defect is pre-existing — on master the same die
already produces the same nothing — and leaving busy_airtime_ok false to
mask it would be a lie about the silicon in a header whose whole point is that
the flag describes hardware. Documented in docs/rx-spectrum-sensing.md and
src/rtl8733b/CLAUDE.md. Say the word if you would rather have it here.

One thing for out-of-tree consumers: busy_airtime_ok's definition
changes meaning slightly — it now says the backend has an engine and
explicitly does not promise an unarmed GetChannelBusy() answers. Anything
that sampled on the strength of that flag must handle "no reading" from a
backend whose flag is true.

Locking

Both retune notes are scoped. FastRetune calls SetMonitorChannel on its
declined path while already holding _reg_mu — recursive, so that part is
fine — and the CCX mutex is not recursive, so holding it across the tune
would self-deadlock there. One note per site suffices because _reg_mu spans
the whole tune and with_ccx takes _reg_mu first, so a concurrent arm
cannot interleave: the Jaguar2/3 situation, not Jaguar1's.

The Init/InitWrite window resets sit inside _reg_mu for the same
reason — ahead of it, an arm landing between the released CCX lock and
_reg_mu would survive the bring-up the reset exists to forget. with_ccx
reads _phy_ready under _reg_mu rather than before it, which the Jaguar2/3
overrides do not: it is a plain bool written under that lock, so the earlier
ordering was a data race and a TOCTOU against a concurrent Stop().

Harness

busy_window_probe.sh gated its NHM and sampled arms on the sensor's USB VID,
which calls this chip Realtek and then runs three arms it cannot pass. It now
gates on rx_energy_ok from the probe's own caps record — the capability, not
the identity, which is the discriminator AdapterCaps.h argues for.

A caps value it cannot parse is a loud failure, not a silent skip:
defaulting to "no GetRxEnergy" would switch off the sampled arm, all three NHM
arms and the bursty comparison while still printing PASS. The verdict line now
carries the skip count for the same reason. Also fixed: expect_all printed
"ok" unconditionally, so a failing arm reported its FAILs and then said ok
underneath them.

Verification

ctest 69/69 (67 run, 2 MediaTek generator tests skipped), ASan/UBSan clean on
both selftests, and five on-air runs:

run result
RTL8733BU steady PASS — 69%, spread 0, 4 arms skipped
RTL8733BU bursty 50/450 PASS — mean 10% vs ~9% true duty, 4 arms + 1 check skipped
Jaguar3 8812CU PASS — every arm ran, zero skips, 69%
MT7612U sensor vs 8812CU flooder PASS — quiet arms, 7 arms skipped
caps-field rename mutation fails loudly where it previously printed PASS

The Stop() window reset is compile- and reasoning-verified rather than
re-measured: its triggering sequence — a Stop followed by a retune with no
re-Init — is out of contract, and there is no mock seam for the device
class. Lock order and re-entrancy for it were checked against every
busy_window_mutex() site in the tree.

Note: #439 edits the adjacent busy_airtime_measured paragraph in the same
AdapterCaps.h comment block. Whichever lands second needs a trivial rebase.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SbJ75HT4fqof7e5e8NKL2j

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

Copy link
Copy Markdown

PR Summary by Qodo

Port RTL8733B CCX CLM busy-window sensing

✨ Enhancement 🧪 Tests 📝 Documentation 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Enables armed CCX CLM busy-airtime measurements on RTL8733B through the shared JGR3 path.
• Preserves window correctness across bring-up and retunes with ordered locking and spoil tracking.
• Gates hardware probe arms by capabilities and documents CLM-only sensing limitations.
Diagram

sequenceDiagram
  actor Caller
  participant Radio as IRtlRadio API
  participant Device as RTL8733B Device
  participant Window as CLM Window
  participant CCX as JGR3 CCX
  Caller->>Radio: Arm busy window
  Radio->>Device: with_ccx callback
  Device->>Device: Lock registers then CCX
  Device->>Window: Arm period
  Window->>CCX: Program CLM
  Caller->>Radio: Read busy window
  Radio->>Device: with_ccx callback
  Device->>Window: Read state
  Window->>CCX: Read result
  CCX-->>Window: Ready and ticks
  Window-->>Caller: Busy or spoil
Loading
High-Level Assessment

The chosen approach is appropriate: reuse the existing ClmWindow state machine and proven JGR3 register map rather than duplicating CLM logic inside the RTL8733B backend. Implementing unsupported PHYDM/NHM counters or bundling the separate SenseWindow capability bug would increase risk and scope without improving this hardware port.

Files changed (6) +326 / -40

Enhancement (2) +119 / -7
Rtl8733bDevice.cppIntegrate busy-window lifecycle and RTL8733B capability flags +67/-7

Integrate busy-window lifecycle and RTL8733B capability flags

• Resets armed windows during initialization and marks them spoiled before real channel retunes while preserving lock ordering. Advertises RTL8733B busy airtime as implemented and measured while leaving RX energy unsupported.

src/rtl8733b/Rtl8733bDevice.cpp

Rtl8733bDevice.hExpose synchronized JGR3 CCX access to the shared CLM engine +52/-0

Expose synchronized JGR3 CCX access to the shared CLM engine

• Implements with_ccx using the RTL8733B register mutex, CCX mutex, JGR3 register map, and device read/write accessors. It verifies PHY readiness under the register lock to avoid a Stop race and pre-initialization hardware access.

src/rtl8733b/Rtl8733bDevice.h

Documentation (3) +126 / -21
rx-spectrum-sensing.mdDocument RTL8733B CLM-only sensing support +35/-15

Document RTL8733B CLM-only sensing support

• Adds RTL8733B to armed busy-airtime coverage and records its JGR3 CLM behavior, validation results, and spoiler semantics. Documents that SenseWindow still selects the unavailable RX-energy path and that callers must currently use the armed IRadio API directly.

docs/rx-spectrum-sensing.md

AdapterCaps.hClarify independent busy-airtime and RX-energy capabilities +18/-4

Clarify independent busy-airtime and RX-energy capabilities

• Updates the capability contract to explain that RTL8733B supports armed hardware busy-airtime measurements without GetRxEnergy. Clarifies that busy_airtime_ok does not guarantee an unarmed sampled reading.

src/AdapterCaps.h

CLAUDE.mdRecord RTL8733B CLM architecture and measured behavior +73/-2

Record RTL8733B CLM architecture and measured behavior

• Distinguishes unported CCX TX reports from supported CCX CLM sensing. Documents locking, register provenance, measured results, unsupported sampled sensing, and the pre-existing SenseWindow capability-discrimination gap.

src/rtl8733b/CLAUDE.md

Other (1) +81 / -12
busy_window_probe.shGate busy-window probe arms by RX-energy capability +81/-12

Gate busy-window probe arms by RX-energy capability

• Reads rx_energy_ok from the probe capability record and skips sampled or NHM-dependent arms only when unsupported, failing loudly if capabilities cannot be parsed. Also suppresses false success messages and reports skipped arms in the final verdict.

tests/busy_window_probe.sh

@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 (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Chip guide repeats capability rules ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
src/rtl8733b/CLAUDE.md restates the capability values and armed-versus-sampled behavior already
defined authoritatively in AdapterCaps.h. When those semantics change, maintainers must update
both copies or the chip guide will give callers stale guidance.
Code

src/rtl8733b/CLAUDE.md[R446-449]

+- The **sampled** path (`GetChannelBusy()` with nothing armed) reports NO
+  READING on this backend, because it is fed by `GetRxEnergy`. An armed window
+  is the only way a number comes out of here.
+- `busy_airtime_ok` is true and `rx_energy_ok` is false — this is the first
Evidence
Compliance rule 2 requires CLAUDE.md documentation to reference authoritative headers rather than
reproduce their detailed contracts. The new chip-guide text repeats the same flag relationship and
armed-only behavior documented in AdapterCaps.h.

CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md: CLAUDE.md: Do Not Duplicate Header Documentation in CLAUDE.md
src/rtl8733b/CLAUDE.md[440-450]
src/AdapterCaps.h[278-295]

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 duplicates the detailed `busy_airtime_ok` and `rx_energy_ok` contract from the authoritative `AdapterCaps.h`, creating two sources that can drift.
## Fix Focus Areas
- src/rtl8733b/CLAUDE.md[440-450]
## Recommended Fix
Replace the repeated capability values and armed-versus-sampled API contract with a concise reference to the relevant documentation in `AdapterCaps.h`. Retain only RTL8733B-specific implementation or measurement details that are not part of the header contract.

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



Remediation recommended

2. Sensing surveys miss busy airtime 🐞 Bug ≡ Correctness
Description
SenseWindow::read selects GetRxEnergy whenever rtl_ is non-null, while RTL8733B's newly
enabled CLM reading is available only through an armed GetChannelBusy window. Every
DwellExecutor survey using this backend therefore records valid_clm=false, because its barrier
neither selects nor arms the only working busy-airtime source.
Code

src/rtl8733b/Rtl8733bDevice.cpp[1104]

+  caps.busy_airtime_ok = true;
Evidence
RTL8733B now advertises working busy airtime but no receive-energy reader, while SenseWindow
unconditionally chooses the energy path for every IRtlRadio and DwellExecutor copies that
invalid result into its CLM fields. The base radio implementation also proves that the RTL8733B
result requires an earlier arm, so changing only the branch discriminator would not recover it.

src/rtl8733b/Rtl8733bDevice.cpp[1088-1111]
src/rtl8733b/Rtl8733bDevice.h[120-146]
src/IRtlRadio.h[88-128]
src/sensing/SenseWindow.h[127-164]
src/sensing/DwellExecutor.h[116-183]

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

## Issue description
Enabling RTL8733B busy airtime does not make it reachable through `SenseWindow`: the Realtek branch calls the unsupported energy reader, while merely switching to an unarmed `GetChannelBusy()` would still return no reading on this backend.
## Fix Focus Areas
- src/sensing/SenseWindow.h[127-164]
- src/sensing/DwellExecutor.h[116-163]
- src/rtl8733b/Rtl8733bDevice.cpp[1088-1111]
## Recommended Fix
Pass the relevant capabilities and intended observation duration into the sensing path. For a backend with busy airtime but no receive-energy support, arm `ArmChannelBusy()` at the discard barrier and consume `GetChannelBusy()` at finish; retain the existing single energy read for backends whose busy result is derived from `GetRxEnergy`, and add coverage for the split-capability case.

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


3. Restart reports a stale spoiler ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new with_ccx path makes _busy_window persistent for RTL8733B, but Stop() clears hardware
readiness without resetting that software state. If a window is armed before shutdown and the device
is revived through SetMonitorChannel or FastRetune, the next unarmed read consumes the old
window as Retuned rather than reflecting the restarted session.
Code

src/rtl8733b/Rtl8733bDevice.h[R120-121]

+  bool with_ccx(const CcxFn &fn) override {
+    std::lock_guard<std::recursive_mutex> reg(_reg_mu);
Evidence
Arming stores state in the base-class ClmWindow, and RTL8733B's newly added override exposes that
state whenever _phy_ready is true. Stop() never resets it, while retune revival merely marks the
still-armed object as retuned; GetChannelBusy() then detects the stale arm and returns its spoiled
result before considering the unarmed fallback.

src/IRtlRadio.h[88-128]
src/IRtlRadio.h[241-256]
src/BusyWindow.h[91-133]
src/BusyWindow.h[149-153]
src/rtl8733b/Rtl8733bDevice.cpp[488-551]
src/rtl8733b/Rtl8733bDevice.cpp[1039-1068]

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

## Issue description
RTL8733B shutdown leaves an armed CLM window in software after the corresponding hardware session has ended, allowing that stale state to survive a later cold bring-up through a retune API.
## Fix Focus Areas
- src/rtl8733b/Rtl8733bDevice.cpp[1039-1068]
- src/rtl8733b/Rtl8733bDevice.cpp[488-551]
- src/rtl8733b/Rtl8733bDevice.h[120-146]
## Recommended Fix
While `Stop()` holds `_reg_mu`, acquire `busy_window_mutex()` in the established lock order and call `busy_window_reset()` before completing teardown. Add a lifecycle test that arms a window, stops the device, revives it through each supported retune path, and verifies that no old arm or spoil reason is returned.

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


4. Test summaries understate skipped arms ✓ Resolved 🐞 Bug ◔ Observability
Description
The no-GetRxEnergy branch skips three NHM modes (interrupt, quality, and race) but
increments skips only once. On RTL8733B, the separately skipped sampled mode makes the final
verdict report two skipped arms although four arms did not run, misleading anyone using the verdict
to assess test coverage.
Code

tests/busy_window_probe.sh[R340-342]

+  echo "== NHM arms skipped: no GetRxEnergy on this backend, so there is no"
+  echo "   NHM read that could re-arm the shared engine mid-window."
+  skips=$((skips + 1))
Evidence
The sampled branch increments the count once when it skips one mode, but the following branch
bypasses three distinct run_arm calls and also increments only once. The newly added verdict
presents that accumulated value as a count of skipped arms.

tests/busy_window_probe.sh[315-324]
tests/busy_window_probe.sh[328-342]
tests/busy_window_probe.sh[431-440]

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

The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
Issue description
The harness reports its skip count as a number of skipped "arm(s)", but the no-`GetRxEnergy` branch skips three separate NHM arms while incrementing the counter only once. Together with the sampled-arm skip, an RTL8733B run reports two skipped arms even though four arms were skipped.
Fix Focus Areas
- tests/busy_window_probe.sh[315-324]
- tests/busy_window_probe.sh[328-342]
- tests/busy_window_probe.sh[431-440]
Recommended Fix
Increment the skip count by the actual number of skipped arm modes in each branch: one for `sampled` and three for `interrupt`, `quality`, and `race`. Keep any non-arm skipped comparison separately accounted for or adjust the verdict wording so its unit remains accurate.

ⓘ 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 turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/rtl8733b/CLAUDE.md Outdated
Comment thread src/rtl8733b/Rtl8733bDevice.cpp
Comment thread src/rtl8733b/Rtl8733bDevice.h
Comment thread tests/busy_window_probe.sh Outdated
@snokvist

Copy link
Copy Markdown
Collaborator Author

Addressed all four findings; pushed c444f17 and corrected the description.

2 — sensing surveys miss busy airtime. Real, and the sharp part of the finding is right: changing only the discriminator would not recover it, because an unarmed GetChannelBusy() falls back to busy_from_rx_energy(GetRxEnergy(true)) and nothing in src/sensing/ ever calls ArmChannelBusy.

It was also worse than I wrote. The description claimed "nothing constructs SenseWindow outside tests today"that was false, and it was the premise the whole section rested on. examples/chanscout/main.cpp:323 builds an IRtlRadio* by dynamic_cast and hands it to a DwellExecutor at :342, so a scout on this die reports neither CLM nor NHM. Corrected in the description, the commit message and both docs.

I have still left the fix out of this PR, with the reasoning now stated honestly rather than resting on a false premise: the complete fix is the capability gate plus an arm plus an observation window no layer carries, and it changes behaviour on four other backends where an arm interacts with the NHM read that spoils the same engine. Happy to do it here if you would rather.

3 — restart reports a stale spoiler. Real, and my earlier reasoning was wrong: I had concluded Stop() was safe because it clears _phy_ready, but SetMonitorChannel/FastRetune call bring_up_to_phy(), which sets it true again. Added a scoped busy_window_reset() under the _reg_mu Stop() already holds. Worth noting Jaguar1/2/3 share this hole and have it worse — none resets the window, and none clears the flag its with_ccx gates on, so on those a GetChannelBusy() straight after Stop() needs no retune at all. Not fixed here; recorded in the chip guide.

4 — skip counts. Real, and my own comment said "skips four arms" while the code counted 2. Each branch now states how many arms it bypasses: NHM three, sampled one, MediaTek exit seven (my first attempt said six — it omitted stale). The bursty comparison is counted as a check, not an arm.

1 — chip guide duplication. Partly. The rule is real (root CLAUDE.md:12-15) though the name given for it is not, and it is scoped to the root file rather than nested guides. Three passages did restate the AdapterCaps.h contract near-verbatim; replaced with a pointer, keeping the chip-specific facts (the no-IGI mechanism, the unreachable Interrupted spoiler, measurements, locking).

Re-verified on air after the changes: RTL8733BU steady and bursty PASS, 8812CU PASS with every arm running and zero skips, MT7612U quiet arms PASS, and the caps-gate mutation still fails loudly. The Stop() reset is reasoning-verified rather than re-measured — its trigger is out of contract and there is no mock seam for the device class.

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-validated on the bench (no MT7612U plugged, so the flooder is an RTL8812CU txdemo), all through tests/busy_window_probe.sh at 686a0b4:

run result
RTL8733BU sensor, steady, ch165 quiet 0%, loaded 62-63% (spread 1), retune → retuned, early → not-elapsed, stale re-arm never leaks, own-TX flagged. PASS, 4 arms skipped
RTL8733BU sensor, bursty 50/450 ms per-window 0 / 13 / 0 / 19 / 0, PASS
Jaguar3 8812CU sensor vs 8822BU flooder (harness control) every arm ran, zero skips, armed spread 1 vs sampled spread 27. PASS

ctest 69/69 in a clean worktree. Lock order (_reg_mu → CCX), the readiness flag read under the lock, and the Init/InitWrite/Stop resets all read correctly; I also confirmed the claim that none of the Jaguar Stop() paths resets the window.

Four small things, which I'll push onto this branch after rebasing over #439:

  1. Conflicts with #439 in docs/rx-spectrum-sensing.md (the "today" row), not only in AdapterCaps.h.
  2. tests/channel_busy_selftest.cpp:33 still says the RTL8733B has "no CCX engine".
  3. The bursty figure ("10% vs true duty ~9%") reads as agreement it does not have: a 240 ms window inside a 500 ms burst period misses whole bursts, and my five reads spread 19 points around a 6% mean. The spread belongs beside the number.
  4. The src/sensing/ discriminator gap is a sensing-layer bug; the narrowest home is src/sensing/CLAUDE.md plus a follow-up issue, so the ~40-line copy in src/rtl8733b/CLAUDE.md shrinks to a pointer.

snokvist and others added 2 commits September 20, 2026 18:13
reports as absent. It does. Ported and measured.

The register block is in this tree: hal/hal8733b_tables.c:792 onward, the
shipped 8733B phy_reg init table, programs 0x1e40, 0x1e44, 0x1e48, 0x1e5c and
0x1e60 at bring-up — the CCX block at the JGR3 addresses. The vendor agrees:
at the pinned reference/rtl8733bu-20230626, hal/phydm/phydm_pre_define.h:513
lists ODM_RTL8733B in PHYDM_IC_SUPPORT_IFS_CLM and :523-525 define
PHYDM_IC_JGR3_SERIES_SUPPORT when RTL8733B_SUPPORT is set. That init table is
also the only other writer of those registers — nothing in src/rtl8733b/
touches them at runtime — so the masked read-modify-writes cannot corrupt
another subsystem, and arm_clm_only never touches 0x1e5c where the table
leaves a non-zero value.

with_ccx() lends the engine nhm_regs_jgr3() under _reg_mu then the CCX mutex,
the way Jaguar2/3 do. No new machinery.

BUSY AIRTIME WITHOUT A PHYDM COUNTER BLOCK. arm_clm_only/read_clm_only take
no IGI argument — busy airtime is a hardware tick count, not a histogram
referenced to the receiver's own floor the way NHM's thresholds are — so CLM
ports cleanly to a backend that implements no GetRxEnergy. This is the first
backend where the two caps split: busy_airtime_ok true, rx_energy_ok false.
The sampled path still reports NO READING here; the armed window is the only
way a number comes out. AdapterCaps.h's busy_airtime_ok definition is
amended to say so — it promised that GetChannelBusy answers, which on this
die is true only after ArmChannelBusy. An out-of-tree consumer that sampled
on the strength of that flag must now handle "no reading" from a backend
whose flag is true.

KNOWN GAP, pre-existing, with one affected consumer. src/sensing/SenseWindow.h
picks its source by whether the IRtlRadio* is non-null rather than by
rx_energy_ok, so on this backend it takes the phydm branch, gets the
all-invalid base GetRxEnergy and never calls GetChannelBusy(). That is the
wrong discriminator AdapterCaps.h opens by warning about. examples/chanscout
hits it: main.cpp:323 builds an IRtlRadio* by dynamic_cast, which succeeds on
this die, and hands it to a DwellExecutor at :342 — so a scout here reports
neither CLM nor NHM.

(An earlier draft of this message and of the PR body claimed nothing
constructs SenseWindow outside tests. That was false; chanscout does.)

Gating that branch on the capability is necessary but NOT sufficient: an
unarmed GetChannelBusy() falls back to busy_from_rx_energy(GetRxEnergy(true))
and yields nothing here, and nothing in src/sensing/ ever calls
ArmChannelBusy — the only callers in the tree are tests/busy_window_probe.cpp.
A complete fix is the gate plus an arm, and arming needs an observation window
that no layer between ScanPlanConfig::dwell_ms and SenseWindow carries. It
would also change behaviour on four other backends, where an arm interacts
with the NHM read that spoils the same engine — which is what
tests/dwell_exec_selftest.cpp exists to pin. That belongs in src/sensing/ with
its own tests, not in a backend port. Documented in
docs/rx-spectrum-sensing.md and src/rtl8733b/CLAUDE.md.

MEASURED, tests/busy_window_probe.sh, RTL8733BU sensor against an MT7612U
flooder on ch165:

  quiet                 0.0%
  steady load           69%, spread 0 over 6 windows
  bursty 50/450 ms      mean 10%, true duty ~9%
  retune mid-window     refused, spoil=retuned
  read before elapsed   refused, spoil=not-elapsed
  re-arm then read      never returns the previous latched value
  sensor transmitting   valid, own_tx flagged

A Jaguar3 8812CU on that same flooder read 69% too, so the two device paths
onto the JGR3 map agree on one load. busy_airtime_measured is set on that
evidence, per the house rule.

The Interrupted spoiler is unreachable on this die by construction: with no
NHM read there is nothing that can re-arm the shared engine mid-window. The
other two fire normally. The CCA gate cannot bias the reading either —
SetCcaMode throws on this backend, so a CCA-disabled session is unreachable
rather than merely unlikely.

LOCKING AND LIFECYCLE. Both retune notes are scoped. FastRetune calls
SetMonitorChannel on its declined path while already holding _reg_mu —
recursive, so that part is fine — and the CCX mutex is NOT recursive, so
holding it across the tune would self-deadlock there. One note per site
suffices because _reg_mu spans the whole tune and with_ccx takes _reg_mu
first, so a concurrent arm cannot interleave: the Jaguar2/3 situation, not
Jaguar1's. The Init/InitWrite resets sit INSIDE _reg_mu for the same reason.
with_ccx reads _phy_ready under _reg_mu rather than before it, which the
Jaguar2/3 overrides do not: it is a plain bool written under that lock, so
the earlier read was a data race and a TOCTOU against a concurrent Stop().

Stop() forgets the window too, which is not obvious: a stop is not a retune,
but clearing _phy_ready does not protect an armed window either, because
SetMonitorChannel and FastRetune both call bring_up_to_phy(), which sets that
flag true again. Without the reset, a window armed before a stop revives on
the brought-up chip and the retune note hands back a Retuned spoil earned by
a session that no longer exists. Jaguar1/2/3 share this hole and have it
worse — none of their Stop() implementations resets the window, and none
clears the flag their with_ccx gates on — noted in the chip guide so the
asymmetry is not read as an 8733B quirk.

HARNESS. busy_window_probe.sh gated its NHM and sampled arms on the sensor's
USB VID, which calls this chip Realtek and then runs arms it cannot pass. It
now gates on rx_energy_ok from the probe's own caps record — the capability,
not the identity, which is the discriminator AdapterCaps.h argues for. A caps
value it cannot parse is a loud failure, not a silent skip: defaulting to "no
GetRxEnergy" would switch off the sampled arm, all three NHM arms and the
bursty comparison while still printing PASS. Each skip branch now states how
many arms it bypasses (the NHM branch skips three, not one; the MediaTek exit
seven, not one) and the bursty comparison is counted as a check rather than
an arm, so the verdict's number means "arms that did not run". Also fixed:
expect_all printed "ok" unconditionally, so a failing arm reported its FAILs
and then said ok underneath them.

Verified: ctest 69/69 (67 run, 2 MediaTek generator tests skipped),
ASan/UBSan clean on both selftests, and five on-air runs — RTL8733BU steady
(PASS, 4 arms skipped) and bursty (PASS, 4 arms + 1 check), Jaguar3 8812CU
PASS with every arm running and zero skips (so the gate discriminates rather
than disabling), MT7612U sensor against an 8812CU flooder PASS on the quiet
arms (7 arms skipped), and a mutation renaming the caps field fails loudly
where it would previously have printed PASS. The Stop() reset is compile- and
reasoning-verified rather than re-measured: its triggering sequence, a Stop
followed by a retune with no re-Init, is out of contract and there is no mock
seam for the device class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbJ75HT4fqof7e5e8NKL2j
The 50/450 ms burst figure now carries its 19-point spread: a 240 ms
window inside a 500 ms burst period misses whole bursts, so only the
mean is a measurement. The SenseWindow discriminator gap is a
sensing-layer bug and now lives in src/sensing/CLAUDE.md, with the
8733B subtree and the sensing article pointing at it. The
channel_busy selftest comment no longer calls the RTL8733B a die
without a CCX engine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@josephnef
josephnef merged commit fc66aa9 into OpenIPC:master Sep 20, 2026
27 checks passed
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.

2 participants