Conversation
Reimplements github.com/tipoman9/Spectrum's 8812eu.py channel-utilization
scanner in C/C++ (no Python/pip) for GS-side rtl88x2eu-family adapters:
- src/spectrum_scanner.{hpp,cpp}: background scanner. Enumerates
/proc/net/rtl*/<iface>/chan_info, drives `iw <iface> scan passive`
via fork/execlp (no shell), maps channel->frequency via `iw dev`/
`iw phy info`, and exposes a thread-safe snapshot to the UI. Each
discovered adapter gets its own scan thread so one adapter's several-
second scan round doesn't stall another's display, and each thread
loops continuously (no inter-round pause). chan_info is re-read every
~150ms while a scan is in flight so channels update as the radio
visits them instead of only once the whole scan finishes.
- src/gsmenu/spectrum_screen.{h,c}: a dedicated full-screen LVGL view
(System > Receiver > Spectrum Scanner) — a single-screen, no-scroll
bar graph (5GHz only) since the GS's 4-way joystick can't scroll a
list. One color-coded bar per channel (green/yellow/red by
utilization), a quality dot, and a recommended-channel line (lowest
utilization, tie-broken by quality).
- lvgl (submodule): pulls in the local fix/sdl-keypad-indev-null-driver-data
commit — a NULL-driver-data guard in lv_sdl_keyboard_handler() that fixed
a simulator segfault-on-any-keypress unrelated to this feature but hit
while building it (input.cpp's custom KEYPAD indev has no driver data,
which lv_sdl_window's SDL event pump assumed every KEYPAD indev has).
PR Summary by QodoAdd Native rtl88x2eu Spectrum Scanner Screen
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
Code Review by Qodo
1.
|
1. parse_chan_info's header-detection loop broke on the iteration AFTER finding the "Index" line, so that next getline() (the first data row) was consumed and discarded before the row-parsing loop ever saw it. Every scan silently lost its first channel (and a single-channel report looked empty). Now breaks in the same iteration the header line is found. 2. run_iface exited its polling loop on shutdown/unplug without terminating or reaping the in-flight `iw scan` child, leaking a running scan process and risking an overlapping scan on the same radio if the screen reopened. Now SIGTERMs and reaps it before returning. 3. A fork failure or a scan that exited immediately (missing `iw`, a rejected scan, no radio) had no back-off, so run_iface would fork/exec as fast as the system allowed. Now backs off 1s (interruptible) before retrying after any non-clean exit; a successful scan still restarts immediately with no pause.
WIN_20260915_21_47_36_Pro.mp4Reference video, while running the scan starting a speedtest on my phone. |
… as a library primitive (#432) ## Why Analysing [OpenIPC/PixelPilot_rk#148](OpenIPC/PixelPilot_rk#148) — a GS spectrum screen built on the vendor driver's `chan_info` procfs — sent us looking for the equivalent gaps here. Three turned out to be structural and connected: 1. **Frame-free sensing is Realtek-locked.** `GetRxEnergy` is on `IRtlRadio`, so `src/chanmig/` and `src/hopset/` — the whole adaptive-link story — are vendor-neutral in their *logic* but can gather no evidence at all on the MediaTek backend shipped in #422. Commit `90e1cad` (#415) named this exact follow-up: *"hopset TX-side sensing and the chanmig energy probe are Realtek-only until a neutral frame-free energy type exists"*. 2. **`RxQuality` never got #431's new sensors.** `clm`/`nhm_env` went onto `RxEnergy` (Realtek-only); the neutral struct that already carries `fa_ofdm`/`cca_ofdm`/`igi` did not. 3. **No library-level survey API.** The dwell loop lived only in `examples/chanscout/main.cpp` — and was untested: the scheduler has a selftest, the consumer has one, the executor between them had none. ## The neutral concept is busy airtime, not phydm counters `RxEnergy` cannot be the portable type — it is phydm-shaped, and `IRadio.h` and `IRtlRadio.h` both assign those counters to the Realtek level on purpose. But one field in it is already neutral and says so: CLM is *"AIRTIME, directly comparable across channels and adapters, and unlike NHM it needs no gain reference."* Both silicon families count it in hardware: | family | mechanism | |---|---| | Realtek J1/J2/J3 | CCX CLM, 4 µs busy ticks | | MediaTek MT7612U | `MT_CH_BUSY` / `MT_CH_IDLE`, already implemented and armed in this port | ## Commit 1 — `ChannelBusy` on `IRadio` `ChannelBusy` + `BusySource` in `src/RxSense.h`, with two **pure** conversions so every arithmetic decision is testable with no device. `BusySource` is load-bearing, not metadata: the MediaTek timers count TX+RX+NAV+EIFS, so a transmitting radio includes its own airtime, while Realtek's CLM is receive-side deferral only. Ranking across a mixed adapter pair compares two rulers. Implemented **once** on `IRtlRadio` in terms of `GetRxEnergy(true)` — all five Realtek backends inherit it with no per-backend edits, and the two without CLM (RTL8733B, Kestrel) report *no reading* rather than a fabricated zero, by construction. `AdapterCaps::busy_airtime_ok` / `_measured` / `rx_energy_ok` retire a discriminator that was never correct: the RTL8733B passes `dynamic_cast<IRtlRadio*>` and implements no energy reader at all. The MediaTek side goes through a **narrow** new `mt7612u_ch_time()` touching only the two channel-timer registers. Not `mt7612u_link_stats()`: that also reads `MT_RX_STAT_1`, whose false-CCA field is read-and-clear and owned by `mt7612u_phy_tick()`'s AGC loop, so polling it at caller cadence would both misreport the figure and starve the gain tracking. For the same reason `energy_pct` is left invalid there — the only candidate counter has an owner. Also: the Realtek DIG rails hardcoded inside neutral `build_rx_quality` move into `LinkHealthThresholds` with the same defaults. Behaviour-identical; a neutral header stops asserting Realtek register constants. ## Commit 2 — `src/sensing/`, the dwell as a library primitive A new subtree, the one that calls device methods. It owns no thread, performs no sleep and takes no clock of record — which is what lets `chanmig/` and `hopset/` keep the purity they both assert. (`src/cell/` was not a precedent: it takes raw scalars and includes no `IRadio.h`.) Two layers, so a later `examples/tx` conversion is mechanical: `SenseWindow.h` is the shared settle → barrier → observe → read discipline with no retune and no frame term — `hopset_sense_window` still carries its own copy and its comment already says *"the discipline is chanscout's"* — and `DwellExecutor.h` is the survey-shaped layer. `chanscout` converts onto it: **40 insertions, 186 deletions**, with all the demo policy (health, thermal, p95, bin-age, advise) deliberately left behind. ### A latent bug the new test caught A `SetMonitorChannel` throw part-way through a full-width dwell left the width-restoration latch clear, so the next bin dwell took the lean same-width `FastRetune` at a possibly-80 MHz width — and every later bin would have observed at the candidate's width. A wrong reading that still looks entirely plausible. Now any full-gate tune that throws sets the latch, because the chip may have been part-way reconfigured. ## Validation **Headless:** 66/66 ctest including two new gates; ASan+UBSan clean; TSan clean — newly meaningful, because the frame aggregator is exercised by a test for the first time; jaguar1-only, jaguar3-only and mt7612u-only subsets build and pass. The mt7612u subset is what caught a caps block landing in `GetTxCaps` instead of `GetAdapterCaps`, since MT7612U is off in a default build. **On air (8812CU):** a baseline `chanscout` built from master, run back to back against the refactored one on the same plan — 314 dwells each side, seq gapless on both, identical flag histogram, no field lost, `observe_ms` median 112 → 112 (0% drift), `retune_us` 1304 → 1314 (1%). An earlier run showed 107 → 102, which is what exposed `finish()` sampling its timestamp before the observation read and silently shortening the window the plausibility ceiling derives from. `tests/chanscout_stress.sh` completed its full run: **2728 consecutive dwell retunes, seq gapless, zero flags of any kind set across the whole stream** (no truncation, no retune failure, no read failure, no counter-suspect, no missing NHM), scout health `ok` throughout, no wedge. **Not validated, and shipped saying so:** the MediaTek path — no MT7612U on the bench. `busy_airtime_measured = false`, register basis documented in `src/mt7612u/CLAUDE.md`. Two things to measure when an adapter is available: that busy/idle track real occupancy beyond repetition noise, and — the real risk — that polling at dwell cadence does not disturb `phy_tick`'s gain tracking. ## Deliberately not here - **Rewiring `ChannelScore`** off its `fa_rate/(fa_rate+200)` magic constant onto real CLM airtime. That changes the migration law and needs its own re-validation of the 14-row failure matrix and an on-air soak. - **Converting `examples/tx`.** It is the TX hot path, called inside the slot-timed hop loop, and its correctness criterion is on-air FHSS lockstep that headless tests cannot see. - Nothing calls `GetChannelBusy` yet, so commit 1 is provably no-behaviour-change on every Realtek path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Au9D2ntoFn4ABLJqp9vYDk --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Reimplements tipoman9/Spectrum's 8812eu.py channel-utilization scanner in C/C++ (no Python/pip dependency) as a native LVGL screen, for GS-side rtl88x2eu-family USB WiFi adapters used for the video link.
What's new
src/spectrum_scanner.{hpp,cpp}— background scanner:/proc/net/rtl*/<iface>/chan_info(any rtl88x2eu-family adapter exposing it).iw <iface> scan passiveviafork/execlp(no shell, no Python).iw dev <iface> info+iw phy <phy> info.chan_infoevery ~150ms while a scan is in flight, so channels update progressively as the radio visits them (matching how a full passive scan is inherently sequential per-channel) instead of only refreshing once the whole scan finishes.spectrum_scanner_snapshot()) — the caller always gets a copy, never a pointer into data the worker thread could mutate concurrently.src/gsmenu/spectrum_screen.{h,c}— a dedicated full-screen LVGL view, reached from System → Receiver → Spectrum Scanner:Testing
gcc/g++) and fullcmake --build(both-DUSE_SIMULATOR=ONand the real target'sLIB_SOURCE_FILESlist).SDL_VIDEODRIVER=x11+ syntheticxdotoolkeypresses) navigating System → Receiver → Spectrum Scanner — stable across the empty-adapter state and general navigation.wlxdc840328d590) on an actual ground station with feedback from screenshots.