Skip to content

GetRxQuality() — runtime RX link-quality feed + passive noise floor#201

Merged
josephnef merged 1 commit into
masterfrom
rx-quality-feed
Jul 6, 2026
Merged

GetRxQuality() — runtime RX link-quality feed + passive noise floor#201
josephnef merged 1 commit into
masterfrom
rx-quality-feed

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Why

Seeded by Fluke (BlackFox's experimental alink replacement): it "measures its environment and builds its own optimal txprofile" from richer driver telemetry than alink's RSSI/SNR — SNR/EVM, a noise-floor number, and PA/LNA-oversaturation. Its sharpest insight is self-jamming (Paddy's 10-ft-indoors): benching at high TX power raises the receiver's noise floor → SNR collapses → the link jams itself; the manual fix is "decrease txpower until NF returns to normal", which needs the driver to report the floor.

devourer had the pieces but not the feed: per-frame RSSI/SNR/EVM only on the Packet callback, the windowing + LinkHealth verdict only in demo/main.cpp, and no noise-floor metric at all. A controller linked against the library (fluke_gs) had to scrape stdout.

What

  • src/RxQuality.hRxQuality snapshot + a thread-safe RxQualityAccumulator (the demo's RxAgg promoted, plus a passive noise-floor term) + build_rx_quality() fusing the frame aggregate, GetRxEnergy, and classify_link_health once.
    • Passive noise floor: nf_dbm = rssi_dbm − snr_db per OFDM/HT frame — the effective floor the receiver sees. TX self-jam drops SNR while RSSI holds, so it rises. Works on all three families incl. Jaguar3 (no background DIG, so IGI can't track the floor).
  • IRtlDevice::GetRxQuality() — the runtime feed; subsumes GetRxEnergy (calls it internally, consumes the FA/CCA delta). Each device owns an accumulator, feeds it per decoded frame in the RX loop, returns build_rx_quality(...).
  • demo: DEVOURER_RXQUALITY=1 emits <devourer-rxquality> (dogfoods the API; existing <devourer-energy>/<devourer-linkhealth> untouched).

Validation

  • Unit (RxQualitySelftest, ctest + mingw list): aggregate math, the NF formula against a synthesized saturation tuple, EVM/NF present-gates, delta-reset.
  • On-air (tests/rx_noise_floor_onair.sh): the feed is valid across a TX-power sweep and the noise floor is self-consistent on real frames — worst |nf−(rssi−snr)| = 1.0 dB — with SATURATED firing at high power.
txidx | frames | nf_dbm | rssi_dbm | snr_db | verdict
   8  |  9362  | -76.2  |  -55.0   |  20.2  | HEALTHY
  20  |  7576  | -70.2  |  -51.0   |  19.5  | HEALTHY
  34  |  7432  | -64.8  |  -46.0   |  19.0  | SATURATED
  48  |  2258  | -91.1  |  -69.5   |  21.5  | SATURATED
  63  |  2575  | -91.1  |  -70.0   |  21.5  | SATURATED

Bench caveat (measured, honest): the bench can't produce a large controllable NF excursion — the wanted beacon is ~−50 dBm inches away, and neither TX self-jam nor a co-located B210 AWGN at 78 dB moves the 8822EU's SNR (its AGC absorbs it) — the same near-field/front-end limit the SDR-power + dis_cca work hit. The unit test covers the NF math directly; a large controllable swing needs a real far-field link.

Full all-chips build + J1-only subset + ctest (10/10) green. M2 (the frame-free active absolute noise floor, measure-first) builds on this struct next.

🤖 Generated with Claude Code

A Fluke-style adaptive-link controller measures its environment from the driver:
SNR/EVM, a noise-floor number (the self-jamming signal), and PA/LNA saturation.
devourer had the per-frame RSSI/SNR/EVM (only on the Packet callback) and the
LinkHealth classifier + windowing living demo-side — so a controller *linked
against the library* (fluke_gs) couldn't get them without scraping stdout, and
there was no noise-floor metric at all.

- src/RxQuality.h — RxQuality snapshot + a thread-safe RxQualityAccumulator
  (the demo's RxAgg promoted, with a passive noise-floor term) + build_rx_quality
  fusing the frame aggregate, GetRxEnergy, and classify_link_health once.
  Noise floor is passive: nf_dbm = rssi_dbm - snr_db per OFDM/HT frame — the
  effective floor the receiver sees. TX self-jam drops SNR while RSSI holds, so
  it rises ("decrease txpower until NF returns to normal"). Works on all three
  families incl. Jaguar3 (which has no background DIG, so its IGI can't track
  the floor).
- IRtlDevice::GetRxQuality() — the runtime feed; subsumes GetRxEnergy (calls it
  internally, consumes the FA/CCA delta). Each device owns an accumulator, feeds
  it per decoded frame in the RX loop, and returns build_rx_quality(...).
- demo: DEVOURER_RXQUALITY=1 emits <devourer-rxquality> (dogfoods the API;
  existing <devourer-energy>/<devourer-linkhealth> untouched).

Validation: RxQualitySelftest (ctest + mingw list) covers the aggregate math,
the NF formula against a synthesized saturation tuple, EVM/NF present-gates, and
delta-reset. On-air (tests/rx_noise_floor_onair.sh): the feed is valid across a
TX-power sweep and the noise floor is SELF-CONSISTENT on real frames
(worst |nf-(rssi-snr)| = 1.0 dB), with SATURATED firing at high power. NB the
bench can't produce a large controllable NF excursion — the wanted beacon is
~-50 dBm inches away and neither TX self-jam nor a co-located B210 AWGN at 78 dB
moves the 8822EU's SNR (its AGC absorbs it); the same near-field/front-end limit
the SDR-power + dis_cca work hit. The unit test covers the NF math directly.

Full all-chips build + J1-only subset + ctest (10/10) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef merged commit e0e5307 into master Jul 6, 2026
15 checks passed
@josephnef josephnef deleted the rx-quality-feed branch July 6, 2026 10:13
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