RX spectrum sensing: frame-free energy sensor, spectrum sweep, per-tone localizer#177
Merged
Conversation
…ne localizer
Add a receive-side spectrum-sensing / interferer-detection capability — the
inverse of DEVOURER_CW_TONE — across all three chip generations. No Realtek
88xx chip exports per-subcarrier CSI to the host, so this surfaces the
channel-wide and per-tone energy the silicon does expose.
Cross-generation plumbing:
- RxEnergy (src/RxSense.h): a neutral frame-free energy snapshot.
- IRtlDevice::GetRxEnergy(): optional virtual, overridden per generation.
Energy sensor — DEVOURER_RX_ENERGY_MS:
- Emits a <devourer-energy> line combining the phydm false-alarm + CCA
counters and DIG IGI (frame-free) with a rolling per-frame RSSI/SNR
aggregate. Jaguar1/2 read the classic-AC map; the Jaguar3 HAL gains a new
port of the FA/CCA/IGI machinery (CCA 0x2c08, CCK-FA 0x1a5c, OFDM-FA sum,
IGI 0x1d70) so all three generations are first-class.
- NHM noise histogram (src/NhmReader.h): a 12-bucket, IGI-referenced in-band
power distribution, ported from phydm CCX with one shared algorithm and a
per-generation register map (11AC for Jaguar1/2, JGR3 for Jaguar3). Emitted
as a companion <devourer-nhm> line; an interferer shifts the histogram mass
into higher power buckets.
Spectrum map — DEVOURER_RX_SWEEP:
- A live, single-process coarse spectrum map: the RX loop runs on a worker
thread while the main thread retunes between energy reads, emitting one
<devourer-energy>ch=N line per bin. Fixes RtlJaguar2Device::SetMonitorChannel,
previously a no-op that never retuned (which also broke Jaguar2 frequency
hopping via SetMonitorChannel).
Per-tone localizer:
- tests/rx_tone_localize.py locates a narrowband interferer from the
self-sounding beamforming report (per-tone SNR notch + per-tone psi
cross-frame variance), at sub-channel resolution. tools/bf_report_decode.py
gains a reusable analyze_frames()/read_frames() entry point (decoder output
unchanged). A headless CTest (rx_tone_localize_math) guards the detection +
frequency-mapping math.
Docs (docs/rx-spectrum-sensing.md, README) and validation harnesses
(rx_energy_probe.sh, rx_spectrum_sweep.sh, rx_tone_localize.sh + checkers).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95e51df to
eb297d8
Compare
josephnef
added a commit
that referenced
this pull request
Jul 5, 2026
…#186) Completes the narrowband noise/sounding sweep (#149): the fast retune the sweep dwells on, and the active two-ended sounding mode on top of the #177 spectrum map. Closes #149. ## FastRetune on every generation `IRtlDevice::FastRetune(channel)` is now the generation-agnostic lean intra-band retune (default = full `SetMonitorChannel`), with a real fast path on all three generations — the RF channel write plus channel-keyed constants on bucket change, everything bandwidth/band-keyed untouched, full-path fallback on a band change: | DUT | full path | fast | |-----|-----------|------| | RTL8812AU | ~251 ms | **~1.6 ms** | | RTL8822BU | ~62 ms | **~18 ms** | | RTL8821CU | ~29 ms | **~5.6 ms** | | RTL8822CU | ~11 ms | **~3.6 ms** | | RTL8812EU | ~11 ms | **~4.3 ms** | - **Jaguar2** (both variants): one cached full-register RF18 write collapses the full path's read-modify-write rounds (three on the 8821C); AGC index / CFO fc / RF 0xBE VCO band / ch144 RF 0xDF / spur + CCK-filter regs written on bucket change; per-hop RX kick kept. - **Jaguar3** (both variants): RF18 inside its 3-wire bracket + force-anapar + BB reset; lazily-primed 8822c RF18 cache (primed on the first fast hop — init-time IQK rewrites RF18 after the channel set, so priming earlier would cache a stale value); 5/10 MHz narrowband dividers are preserved across hops (no per-dwell re-clock + DAC-FIFO reset). `FastRetune` and `SetMonitorChannel` now serialize on the coex-thread register mutex — in-session hops no longer race the housekeeping tick. - Radiotap `CHANNEL` drives a per-packet FastRetune on **all three generations** now (was Jaguar1-only). - The subtle rule the parity oracle taught on hardware: init-time calibration rewrites BW-keyed state *after* the channel set (halrf clears the 40 MHz TX_CCK_IND bit in the 8822e's RF 0x1a; the NB recipe owns `0x808[6:4]`), and the full path re-imposes it per retune — so the fast path re-asserts that state once per cache epoch. Correctness is **end-state** parity with the full path. ## Sweep upgrades - Dwell retunes ride FastRetune (`DEVOURER_RX_SWEEP_FULL=1` forces the full path), and each `ch=N` line reports the measured `retune_us`. - Per-dwell frame stats on the bin line: `frames/rssi_mean/rssi_max/snr_mean/snr_min/evm_mean`, drained at dwell start so retune transients never leak into a bin. `DEVOURER_RX_AGG_SA=canon|<mac>` restricts the aggregate to one transmitter's SA. - `DEVOURER_RX_SWEEP` / `DEVOURER_HOP_CHANNELS` share one bin-list grammar (`src/SweepSpec.h`): channel lists, channel ranges (`36-48/4`), and MHz ranges (`5170-5250/5`). Headless `sweep_spec_math` ctest. ## Active two-ended sounding — coarse H(f) of the link The passive map is blind to fading of our own path; the active mode probes it. `tests/sounding_sweep.sh`: one end hops fixed-rate canonical-SA probe beacons across the bins via FastRetune, the other sweeps the same bins with the SA filter — synchronization is asymmetric-duty (RX dwell ≥ ~2 TX cycles), no control channel. `tests/sounding_map.py` renders the map: per-bin median of `rssi_max` (off-channel bleed decodes weaker, never stronger — bleed-robust), NOTCH/DEAD flags, optional Spearman cross-check against a B210 capture (`hop_rx_probe.py --bin-power-csv`). ## Register-parity tooling `DumpCanary` ported to Jaguar2 + Jaguar3 (same format, emitted from both the full and fast paths); `tests/hop_parity_check.sh` is family-aware (Jaguar1 keeps its TX-power/IQK exclusions plus the live RF 0x00/0x42 registers the full-vs-full control only catches probabilistically; Jaguar2/3 exclude nothing). ## Test-harness fix The kernel truncates comm to 15 chars, so every `pkill -x WiFiDriverTxDemo` cleanup in `tests/*.sh` was a silent no-op (leaked TX demos held adapters across scripts). Fixed to the real truncated comm across the harness. ## Testing - Build clean on the full config and the per-chip subsets; `ctest` green (5/5, incl. the new `sweep_spec_math`). - **Register parity** (full-vs-fast, with full-vs-full control): PASS on 8812AU (5G/40), 8822BU (2.4G + 5G), 8821CU (2.4G + 5G), 8822CU (2.4G/20, 5G/20, 5G/40, NB5), 8812EU (2.4G/20, 5G/40, NB5). The oracle caught two real fast-path bugs on the 8812EU before they shipped (the end-state rule above). - **B210 on-air hop order**: PASS on 8822CU (12 cycles), 8812EU (23), Archer T3U (6), and the radiotap-driven per-packet mode. - **Sweep A/B** (CW tone on ch6): fast and full dwells localize the same peak at 20 MHz and at 5 MHz NB on both Jaguar3 variants. NB caveat captured in the docs: a CW at an NB bin's own centre hides in the receiver's DC null and registers on the adjacent bins (identical on both paths — chip physics). - **Sounding**: 8812AU↔8822CU at 2.4 GHz (flat bench map, correct null case, B210 cross-check wired); 8822CU→8812EU at `--bins 5170-5250/5 --nb-bw 5` recovered a real H(f) — a smooth ~15 dB notch centred at 5230 MHz with monotone roll-in/out across all 17 five-MHz bins; reverse direction verified at 20 MHz (this 8812EU specimen's NB emission is marginal — same unit with the dead 2.4 GHz RX). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The receive-side counterpart to
DEVOURER_CW_TONE: use a cheap Wi-Fi adapter asa spectrum sensor / interferer detector, across all three chip generations. No
Realtek 88xx chip exports per-subcarrier CSI to the host, so this surfaces the
channel-wide and per-tone energy the silicon does expose. The elegant
validation is two-adapter, no SDR: one adapter emits
DEVOURER_CW_TONE, anotherdetects it.
Energy sensor —
DEVOURER_RX_ENERGY_MSEmits a
<devourer-energy>line combining the phydm false-alarm + CCA(channel-busy) counters and DIG IGI (all frame-free) with a rolling per-frame
RSSI/SNR aggregate. Jaguar1/2 read the classic-AC register map; the Jaguar3 HAL
gains a new port of the FA/CCA/IGI machinery (CCA
0x2c08, CCK-FA0x1a5c,OFDM-FA sum, IGI
0x1d70) so all three generations are first-class frame-freesensors.
A companion
<devourer-nhm>line carries the NHM noise histogram — a12-bucket, IGI-referenced in-band power distribution ported from phydm CCX, one
shared algorithm (
src/NhmReader.h) with a per-generation register map (11AC forJaguar1/2, JGR3 for Jaguar3). An in-band interferer shifts the histogram's mass
into higher power buckets.
Cross-generation plumbing: a neutral
RxEnergysnapshot (src/RxSense.h) and anoptional
IRtlDevice::GetRxEnergy()virtual overridden per generation.Spectrum map —
DEVOURER_RX_SWEEPA live, single-process coarse spectrum map: the RX loop runs on a worker thread
while the main thread retunes between energy reads, emitting one
<devourer-energy>ch=Nline per bin. Park a tone on one channel and the mappeaks (or, on the saturating 1T1R parts, dips) at that channel.
This also fixes
RtlJaguar2Device::SetMonitorChannel, previously a no-op thatnever retuned — which silently broke Jaguar2 frequency hopping via
SetMonitorChanneltoo.Per-tone localizer
tests/rx_tone_localize.pylocates a narrowband interferer from theself-sounding beamforming report (per-tone SNR notch + per-tone ψ cross-frame
variance) at sub-channel (~385 kHz) resolution.
tools/bf_report_decode.pygainsa reusable
analyze_frames()/read_frames()entry point shared with thelocalizer (decoder output unchanged, verified byte-identical).
Testing
rx_tone_localize_mathguards the localizer's detection +frequency-mapping math;
cteststays green (3/3).cca_ofdmspike (~13–380× on the 2T2R 8822CU) or collapse(1T1R AGC saturation), tone on/off.
co-located CW tone; the 11AC map reads correct populated histograms with the
high buckets filling under the tone.
and Jaguar3 (8822CU) sensor.
tests/rx_energy_probe.sh,tests/rx_spectrum_sweep.sh,tests/rx_tone_localize.sh(+ Python checkers/renderers).Docs:
docs/rx-spectrum-sensing.md(new) + README env-var reference.🤖 Generated with Claude Code