diff --git a/docs/rx-spectrum-sensing.md b/docs/rx-spectrum-sensing.md index 4bc4bed9..d019f887 100644 --- a/docs/rx-spectrum-sensing.md +++ b/docs/rx-spectrum-sensing.md @@ -127,7 +127,7 @@ hardware it runs on: |---|---|---| | returns | `ChannelBusy` — busy airtime + energy-above-floor | `RxEnergy` — the phydm counter set | | available on | any backend with a hardware busy-airtime counter | Realtek only | -| today | Jaguar1/2/3 (CCX CLM), MT7612U (MAC channel timers, **unvalidated**) | Jaguar1/2/3, Kestrel (floor only) | +| today | Jaguar1/2/3 (CCX CLM), MT7612U (MAC channel timers) | Jaguar1/2/3, Kestrel (floor only) | | not available | Kestrel, RTL8733B — both report *no reading*, never zero | RTL8733B, MT7612U | Advertised statically by `AdapterCaps::busy_airtime_ok` / diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index a2c6adb8..772dd20d 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -280,10 +280,14 @@ struct AdapterCaps { * channel timers. FALSE on Kestrel (its NHM rides the halbb glue, not * NhmReader, so it has no CLM) and on the RTL8733B. * - * busy_airtime_measured: that reading has been separated arm-vs-quiet ON AIR - * for this family (tests/ccx_clm_probe.sh), not merely implemented. False on - * Jaguar1 (shares the validated Jaguar2 11AC map, unmeasured) and on the - * MT7612U (no adapter available). False-as-unmeasured, per the house rule. + * busy_airtime_measured: that reading has been separated from a quiet + * channel ON AIR for this family, not merely implemented. The harness is + * tests/busy_window_probe.sh, which pits an armed window against the quiet + * floor under a known load, and against each way a window can be spoiled. + * True today on all four backends that set busy_airtime_ok. The flag stays + * because the two facts are independent: a port can land the engine before + * anyone has run it on air, and false-as-unmeasured is the house rule for + * that state. * * rx_energy_ok: IRtlRadio::GetRxEnergy returns real phydm FA/CCA/IGI * counters. Always false on a non-Realtek radio; false on the RTL8733B and diff --git a/tests/busy_window_selftest.cpp b/tests/busy_window_selftest.cpp index 25c3d2f4..59eb0b18 100644 --- a/tests/busy_window_selftest.cpp +++ b/tests/busy_window_selftest.cpp @@ -244,6 +244,40 @@ int main() { check("early: consumed by the completed read", w.armed() ? 1 : 0, 0); } + /* --- the other half of that rule: a SPOILED window IS consumed. + * + * "Not elapsed" keeps the window because nothing disturbed it — the caller + * was merely early. Interrupted and Retuned are the opposite: the hardware + * window was re-armed underneath it, or it spans two channels, so there is + * nothing left to wait for. + * + * Consuming it is what hands the caller BACK to the sampled path. + * IRtlRadio::GetChannelBusy takes the armed branch on armed(), so a spoiled + * window left armed strands every later call there: `spoil_` is sticky + * (only arm() clears it), so each one re-enters the spoil branch and + * returns invalid-Retuned, and the sampled fallback is unreachable for + * every unrelated consumer until something re-arms or resets the window. + * Not a wrong number — a sensor that stops answering. + * + * Without this block the asymmetry is invisible to the suite: a build that + * keeps the window armed on EVERY verdict passes every other test in this + * file. It is exactly the difference a later editor removes while tidying, + * so it is asserted rather than only described in the header. --- */ + { + MockBb bb; + ClmWindow w; + w.arm(regs, 240000, 0, bb.wr()); + w.note_retune(); + const ChannelBusy spoiled = w.read(regs, 0, bb.rd()); + check("spoiled: refused", spoiled.valid, 0); + /* On the READING, not just in last_spoil(): the block above checks the + * accessor, so a branch that reported every spoil as Interrupted would + * pass it. What a consumer acts on is the field. */ + check("spoiled: with its reason", static_cast(spoiled.spoil), + static_cast(BusySpoil::Retuned)); + check("spoiled: window consumed", w.armed() ? 1 : 0, 0); + } + /* --- reading without arming is not a quiet channel --- */ { MockBb bb; diff --git a/tests/channel_busy_selftest.cpp b/tests/channel_busy_selftest.cpp index c412ac95..bf42cfee 100644 --- a/tests/channel_busy_selftest.cpp +++ b/tests/channel_busy_selftest.cpp @@ -219,6 +219,21 @@ int main() { static_cast(b.spoil), static_cast(BusySpoil::Interrupted)); } + { /* The negative control for the block above. The SAME dead counters + * with no window armed are the sampled path finding a backend that is + * not counting — "no sensor here", which is what a bare invalid + * reading means, and it must stay bare. A build that reported a spoil + * reason here would tell every sampled caller that its window was + * lost, when it never armed one. + * + * This is what pins the `w.armed &&` half of the guard: without it the + * reason leaks onto the sampled path and no other test notices. */ + ChTimeWindow w; /* unarmed */ + const ChannelBusy b = busy_from_ch_time_window(w, 0, 0, 250000, false, 0); + check("mt unarmed over dead counters: no reading", b.valid, 0); + check("mt unarmed over dead counters: and no window to have lost", + static_cast(b.spoil), static_cast(BusySpoil::None)); + } } if (g_fail) {