Pseudo preamble puncturing: keep a wide channel usable when one 20 MHz slice is dirty#159
Merged
Merged
Conversation
Wi-Fi 7 preamble puncturing (use a wide channel, skip a dirty 20 MHz slice) has no true rendition on Jaguar silicon — VHT preambles must be contiguous and no generation exposes TX tone nulling. This ports the closest thing the chips do have and measures what it is worth: - src/ToneMask.h: phydm CSI-mask (per-tone receive-equalizer de-weight) + NBI notch, one shared header for all three generations (11ac bit-arrays 0x874/0x880-0x89C; Jaguar3 indexed table behind 0x1D94 with per-tone weight, enable 0xC0C[3]). New env knobs DEVOURER_RX_CSI_MASK=<f_lo>[-<f_hi>][/wgt] (masks an arbitrary tone range — the vendor wrapper only notches one interferer frequency) and DEVOURER_RX_NBI=<f_mhz>, applied in each generation's StartRxLoop. - Register landing validated on 8821AU / 8814AU / 8822BU / 8812CU / 8822EU (tests/tone_mask_regcheck.sh); the mask provably gates demodulation (tests/tone_mask_rx_sanity.sh: 29->6 hits masking the tuned channel, 29->30 off-frequency). Tone math is ctest-guarded (tone_mask_math). - Jammed-slice experiment (tests/pseudo_puncture_onair.sh, B210 AWGN on one slice of a VHT80 link): the CSI mask is inert against a jammed slice — with DEVOURER_RX_KEEP_CORRUPTED=1 the jammed cell delivers zero FCS-failed frames, so the loss is pre-FCS (sync/AGC), upstream of the equalizer. Dropping TX to the clean 40 MHz half recovers 78% of clean delivery; a victim-link probe scan locates the jammed slice (tests/pseudo_puncture_detect.sh). Study written up in docs/pseudo-preamble-puncturing.md. - tests/sdr_interferer.py: pre-generated fixed-seed noise ring — the per-buffer numpy draw could not feed 20 MS/s, so the jam radiated only in underflow bursts and the gain knob did nothing. - demo: DEVOURER_BW now accepts 80 (VHT80 monitor RX). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bench harnesses behind the adaptive-link bandwidth lever (docs/pseudo-preamble-puncturing.md "puncture by primary selection"): - tests/rx80_narrow_tx_probe.sh: an 80 MHz-tuned RX decodes 40 MHz VHT and legacy-20 frames sent on its primary sub-channels WITHOUT retuning — TX-side bandwidth adaptation is unilateral (per-packet radiotap, no RX coordination), as long as the TX keeps the RX's primary. The unilateral choice set is the nested ladder 20(primary) < 40(primary half) < 80. - tests/wide_rx_penalty_sweep.sh: the sensitivity cost of that wide-tuned reception, measured differentially (identical 40 MHz TX, only the RX tune differs) as the 50%-delivery crossing shift over a swept B210 noise floor: 0.0 dB — both tunes collapse at the same 83.9 dB crossing (bounded by the ~1 dB capture-cliff width, so "<= 1 dB"). Measurement notes baked into the harness: gate the TX window on the RX's listening marker (per-bandwidth init variance otherwise aliases into hit counts — this artifact produced the apparent 25% penalty in the first probe), sweep noise gain rather than TXAGC (bench margin swallows the whole 24 dB TXAGC range), and retry a cell's TX once on the T3U's warm-DLFW abort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opens the channel-bandwidth lever the adaptive-link design listed but never actuated, built on the two measured facts from the wide-RX probes: the 20 c 40 c 80 primary-nested ladder is unilateral (TX picks a rung per-packet via radiotap while the RX parks at the widest) and costs the receiver nothing (wide-RX penalty 0 dB / <= 1 dB). - energy_model: VHT 1SS rate tables (20/40/80 — the 80 MHz rung HT lacks) and bw_noise_db() (+3 dB of integrated noise per bandwidth doubling vs the 20 MHz reference). - op_table.build_link_rows: opt-in bw_set/mode row dimensions; a wider row's snr_req carries its noise-bandwidth cost, and resolve() consistently hands the demodulator recv minus that same term. - ControllerConfig.bw_set/mode (default None/ht — single-bw behaviour unchanged): the existing e_bit ranking then answers "drop bandwidth or drop MCS?" per tick. Swept: the controller rides VHT/80 from 45 dB down to 14 dB (spending TXAGC to hold the wide rung — power is the weak energy lever, airtime the strong one) and sheds to 40 MHz only at 9 dB. - adaptive_link.op_to_ladder: VHT rows map to VHT1SS_MCSn/bw ladder specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion The sensing half of the bandwidth dimension — the "victim link is the sensor" verdict from the pseudo-puncturing study, operationalized with zero new wire fields: - rc_proto: a shared bandwidth-probe schedule (slots 0/8/16 of each 32-seq cycle fly the sorted rungs, ~3% duty per rung) that both ends derive from the video seq alone, and a v2 PROFILE byte encoding (mode/MCS/bw; legacy 0..7 bytes still decode as HT/20, and an HT/20 encoding IS the legacy byte). - score.RungWindow: per-rung delivery at the VRX — received seqs are attributed to their scheduled rung on arrival, missed seqs from the wrap-safe gaps between arrivals. - Controller.report_rung_delivery: blocks (for a hold) a rung whose probed delivery contrasts badly with the best rung's — detection by contrast, so it fires on the interference signature (one rung's extra spectrum is dirty) and not on plain path loss (all rungs sag together). The narrowest rung is never blocked: when it, too, misses the target the controller raises `primary_dirty` — the condition no unilateral bandwidth choice can fix, which the embedding app escalates into a coordinated channel move (the one moment the 4-slice probe scan earns its cost). - AdaptiveVrx feeds the sensor into the controller each feedback tick and surfaces `primary_dirty`; AdaptiveVtx exposes probe_bw_for_seq() for injectors and builds its ladder from the decoded v2 profile via the now-shared ladder_spec(). 230/230 precoder tests, incl. closed VRX-loop pins: kill exactly the 80-rung probe seqs -> the controller vacates 80 and 20/40 stay clean; kill all rungs while video still flows -> primary_dirty fires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mingw job builds an explicit target list (the POSIX-only demos are not mingw targets) and then runs the full ctest suite — so every registered test's binary must be in that list. The new tone_mask_math test failed there as "Not Run": ToneMaskSelftest was never built. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The avoidance lever's two strongest measured properties landed after the study was written: it is unilateral (RX@80 decodes primary-nested narrower frames without retuning) and free at the receiver (wide-RX penalty 0 dB / <= 1 dB), which is what makes it consumable by the adaptive controller. State them where the lever is judged. 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.
Problem
A long-range link wants the widest channel it can get, but one dirty 20 MHz slice (an interferer, a neighbouring network) conventionally forces the whole 80 MHz block to be abandoned. Wi-Fi 7 solves this with preamble puncturing — transmit around the dirty slice — but that is 802.11be PHY signaling (EHT-SIG puncturing bitmap + TX IFFT tone nulls). The question this PR answers: how much of that capability can 802.11ac-era Jaguar silicon recover, and which mechanism actually carries the weight? Answered with ported vendor knobs + on-air measurements; the study lives in
docs/pseudo-preamble-puncturing.md.What lands
1. The RX tone-mask knobs — the only tone-level hardware the silicon has (
src/ToneMask.h, all three generations):DEVOURER_RX_CSI_MASK=<f_lo>[-<f_hi>][/wgt]— per-tone receive-equalizer de-weight over an arbitrary frequency range (the vendor phydm wrapper only notches one interferer; masking a 20 MHz slice needs the range form). 11ac:0x874[0]+ bit arrays0x880-0x89C; Jaguar3: indexed table behind0x1D94, per-tone 3-bit weight, enable0xC0C[3].DEVOURER_RX_NBI=<f_mhz>— the vendor-parity narrowband notch.tests/tone_mask_regcheck.sh), demod-active not just readback (tests/tone_mask_rx_sanity.sh: masking the tuned channel collapses a live link 29→6 hits, off-frequency mask 29→30), tone math ctest-guarded (tone_mask_math).2. The jammed-slice measurements — which lever works (
tests/pseudo_puncture_onair.sh, B210 jamming one slice of a VHT80 link):The CSI mask is inert against a jammed slice — and the PR shows why: with
DEVOURER_RX_KEEP_CORRUPTED=1the jammed cell delivers zero FCS-failed frames, i.e. the loss is pre-FCS (sync/AGC), upstream of the equalizer and of sub-block salvage. The mask stays valuable for its designed purpose (in-band spurs on decodable frames), now exposed. TX-side avoidance is the working lever (78% of clean delivery), and a victim-link probe scan locates the dirty slice where per-tone SNR self-sounding cannot (it senses the peer's channel, not the victim's —tests/pseudo_puncture_detect.sh).3. The avoidance lever measured precisely (
tests/rx80_narrow_tx_probe.sh,tests/wide_rx_penalty_sweep.sh): an 80 MHz-tuned RX decodes narrower frames on its primary sub-channels without retuning and at 0 dB (≤1 dB) sensitivity cost — so TX bandwidth adaptation along the primary-nested ladder20 ⊂ 40 ⊂ 80is unilateral and per-packet, no coordination protocol needed.4. The adaptive-link integration (
tools/precoder/, 230 tests): bandwidth as an energy-ranked op-table dimension (ControllerConfig.bw_set, VHT 1SS rates incl. the 80 MHz rung, +3 dB/doubling noise cost per row); per-rung delivery sensing with zero new wire fields (a shared seq-derived probe schedule, ~3% duty per rung); rung blocking by delivery contrast (fires on interference, not on path loss); and aprimary_dirtyescalation flag for the one case no unilateral bandwidth choice can fix. Wire-compatible: legacy PROFILE bytes decode unchanged.Also fixed en route:
tests/sdr_interferer.pycould not feed 20 MS/s from per-buffer numpy draws — the jam radiated only in underflow bursts and its gain knob silently did nothing (pre-generated fixed-seed noise ring now);WiFiDriverDemoacceptsDEVOURER_BW=80.Verdict
The honest Jaguar rendition of
[clean][clean][skip][clean]is puncturing by primary-channel selection: probe-scan the slices with the victim link, transmit around the dirty one at the widest clean primary-nested bandwidth, escalate to a coordinated channel move only when the primary itself is dirty. The cost vs Wi-Fi 7 is the bandwidth quantization (60 MHz usable → 40).Validation
ctest2/2; precoderpytest230/230.tests/are re-runnable on the 2-DUT + B210 bench.🤖 Generated with Claude Code