From 7cea756d6dfa86dee7b99e88e149c67496a146f4 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:51:15 +0300 Subject: [PATCH 1/2] Adapter capabilities API + extended out-of-band 5 GHz tuning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coupled additions for dependent apps (OpenIPC-FPV tooling, link managers, test harnesses): AdapterCaps API. New src/AdapterCaps.h aggregates what the opened radio IS and can do — chip identity (name/generation/variant/transport/id), TX/RX chain counts, the composed TxCaps + TxPowerCaps, the supported channel-width set, per-band tunable + characterized frequency spans, and per-family feature flags (per-packet TX power, narrowband, fast retune, per-chain RSSI). Surfaced via IRtlDevice::GetAdapterCaps() (static, thread-safe, callable pre-Init), overridden per generation. GetActiveRxPaths() is the live companion — a best-effort per-chain-RSSI estimate of which antennas actually carry signal (RxPathActivityAccumulator + classify_active_paths in RxQuality.h, fed from all three RX loops). The demos emit the static caps as the adapter.caps JSONL event via the shared examples/common/caps_event.h; rxdemo emits adapter.rxpaths for the live hint. Extended tuning. The 5 GHz synthesizer tunes past the UNII channels (~5080-6165 MHz, chan up to 253, freq = 5000 + 5*chan — the vendor rtl88x2bu monitor_chan_override range). ChannelFreq.h lifts the 5895 MHz cap; the per-generation channel-programming buckets (fc/AGC/RF18/SCO/VCO band, IQK/TXGAPK calibration bands) are opened at both edges so ch 15-35 and >177 tune instead of falling through. TX power for uncharacterized channels falls back to the nearest characterized channel's index (never the near-zero index 0), with a one-shot diagnostic. No regulatory enforcement — the caller owns compliance, as in monitor mode generally. Validated on-air across all three generations: adapter.caps correct on 8812AU/8814AU/8822C/8821C/8822E/8822B; TX+RX confirmed at ch180 = 5900 MHz (75 MHz past the ch165 edge) on cross-generation pairs; ctest 13/13 with the new adapter_caps_derive selftest. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 15 +++ CMakeLists.txt | 12 +++ docs/logging.md | 2 + examples/common/caps_event.h | 74 ++++++++++++++ examples/doctor/main.cpp | 3 + examples/rx/main.cpp | 17 ++++ examples/tx/main.cpp | 2 + examples/txpower/main.cpp | 3 + src/AdapterCaps.h | 127 +++++++++++++++++++++++++ src/ChannelFreq.h | 14 ++- src/IRtlDevice.h | 21 ++++ src/RxQuality.h | 108 +++++++++++++++++++++ src/jaguar1/EepromManager.cpp | 25 +++-- src/jaguar1/RadioManagementModule.cpp | 29 ++++-- src/jaguar1/RadioManagementModule.h | 1 + src/jaguar1/RtlJaguarDevice.cpp | 50 +++++++++- src/jaguar1/RtlJaguarDevice.h | 8 ++ src/jaguar2/HalJaguar2.cpp | 48 +++++++--- src/jaguar2/HalJaguar2.h | 1 + src/jaguar2/RtlJaguar2Device.cpp | 36 ++++++- src/jaguar2/RtlJaguar2Device.h | 8 ++ src/jaguar3/Halrf8822c.cpp | 2 +- src/jaguar3/Halrf8822e.cpp | 11 ++- src/jaguar3/RadioManagementJaguar3.cpp | 33 +++++-- src/jaguar3/RadioManagementJaguar3.h | 1 + src/jaguar3/RtlJaguar3Device.cpp | 33 ++++++- src/jaguar3/RtlJaguar3Device.h | 8 ++ tests/adapter_caps_probe.sh | 52 ++++++++++ tests/adapter_caps_selftest.cpp | 104 ++++++++++++++++++++ tests/extended_freq_smoke.sh | 67 +++++++++++++ 30 files changed, 871 insertions(+), 44 deletions(-) create mode 100644 examples/common/caps_event.h create mode 100644 src/AdapterCaps.h create mode 100755 tests/adapter_caps_probe.sh create mode 100644 tests/adapter_caps_selftest.cpp create mode 100755 tests/extended_freq_smoke.sh diff --git a/CLAUDE.md b/CLAUDE.md index 2348f46..adf60d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,6 +158,21 @@ grammar), passed as `CreateRtlDevice`'s defaulted fourth argument. Mid-session knobs are runtime setters on `IRtlDevice` (`SetTxMode`, `SetTxPowerOffsetQdb`, `SetTxPowerIndexOverride`, `SetRxPathMask`, `SetCcaMode`, `FastRetune`, ...). +**Adapter capabilities**: `IRtlDevice::GetAdapterCaps()` (`src/AdapterCaps.h`) +returns a static aggregate of chip identity (name / generation / variant / +transport / chip-id), TX/RX chain counts, the composed `GetTxCaps` + +`GetTxPowerCaps`, the supported channel-width set, per-band tunable + +characterized frequency spans, and feature flags (per-packet TX power, +narrowband, fast retune, per-chain RSSI) — resolved at construction, thread-safe, +callable pre-`Init`. The demos emit it as the `adapter.caps` JSONL event. +`GetActiveRxPaths()` is the live companion: a best-effort per-chain-RSSI estimate +of which antennas actually carry signal (needs an RX loop + traffic). The 5 GHz +synthesizer tunes past the UNII channels (extended range ~5080–6165 MHz, chan up +to 253, `freq = 5000 + 5*chan`); out-of-band channels tune but their TX power / +per-channel constants are extrapolated from the nearest characterized channel +(one-shot `W` diagnostic). No regulatory enforcement — the caller owns +compliance. + **Env vars are the demos' interface**: `examples/common/env_config.{h,cpp}` maps every library-level `DEVOURER_*` var onto `DeviceConfig`, so the test scripts drive everything through env. For the per-var reference, read the diff --git a/CMakeLists.txt b/CMakeLists.txt index 0272b82..fcc36b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -390,6 +390,8 @@ target_include_directories(sense PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/co add_executable(doctor examples/doctor/main.cpp ) +# caps_event.h (shared adapter.caps emitter) lives in examples/common/. +target_include_directories(doctor PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common) target_link_libraries(doctor PUBLIC devourer PRIVATE PkgConfig::libusb) # Headless regression guard for the binary-stdin framing shared by the two @@ -493,6 +495,16 @@ target_link_libraries(TxCapsSelftest PRIVATE devourer) add_test(NAME tx_caps_derive COMMAND TxCapsSelftest) +# Headless guard for the adapter-capabilities API pure logic (src/AdapterCaps.h +# bw mask / gen names, the ActiveRxPaths classifier + accumulator in +# src/RxQuality.h, and the extended freq<->chan mapping in src/ChannelFreq.h). +add_executable(AdapterCapsSelftest + tests/adapter_caps_selftest.cpp +) +target_link_libraries(AdapterCapsSelftest PRIVATE devourer) + +add_test(NAME adapter_caps_derive COMMAND AdapterCapsSelftest) + # Headless guard for the Jaguar2 per-packet TX-power quantizer # (jaguar2::txpkt_pwr_step_for_db) — dB delta -> the descriptor TXPWR_OFSET LUT # step. Gated on Jaguar2 being built (the header is behind DEVOURER_HAVE_JAGUAR2). diff --git a/docs/logging.md b/docs/logging.md index ecf56ca..22137e5 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -77,6 +77,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets]; | ev | emitter | fields | |---|---|---| | `init.timing` | L (`src/InitTimer.h`) + demos | stage ("scope.stage", e.g. "demo.first_rx_frame", "txdemo.first_tx_submit"), ms | +| `adapter.caps` | RX, TX, doctor, txpower (`examples/common/caps_event.h`) | supported, chip, names, chip_id "0x..", gen, variant, transport, tx_chains, rx_chains, n_ss, stbc, ldpc, sgi, bw_max, bw[] (MHz), txpwr_max, txpwr_step_qdb, txpwr_step_measured, txpwr_min_qdb, txpwr_max_qdb, tune_2g4[]\|null, tune_5g[]\|null, char_2g4[]\|null, char_5g[]\|null, per_pkt_txpwr, narrowband, fastretune, per_chain_rssi | | `debug.wreg` | L (`DEVOURER_LOG_WRITES`) | addr "0x0nnn", width, val "0x…" | | `hop.prof` | L (`DEVOURER_HOP_PROF`) | gen, ch, `_us`…, total_us | | `tx.fail` | L (send failure; regress.py keys on it) | {status, actual_len, timeout} or {rc, timeout} | @@ -96,6 +97,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets]; | `rx.energy` | RX (`DEVOURER_RX_ENERGY_MS` / sweep) | t, [ch], cca_ofdm\|null, cca_cck\|null, fa_ofdm\|null, fa_cck\|null, igi\|null, [retune_us], frames, rssi_mean, rssi_max, snr_mean, snr_min, evm_mean | | `rx.nhm` | RX | [ch], peak, busy, dur, hist[12] | | `rx.quality` | RX (`DEVOURER_RXQUALITY`) | verdict, frames, rssi_mean_dbm, rssi_max_dbm, snr_mean_db, snr_min_db, evm_db\|null, noise_floor_dbm\|null, igi | +| `adapter.rxpaths` | RX (`DEVOURER_RXQUALITY`) | active_mask "0xNN", n_active, n_chains, frames, rssi_dbm[] — GetActiveRxPaths live per-chain activity (the caps rx_chains companion) | | `link.health` | RX (`DEVOURER_LINKHEALTH`) | verdict, rssi_dbm, snr_db, evm_db\|null, frames, fa_ofdm\|null, igi\|null, [igi_floor], [igi_ceil], cause, fix | | `fw.c2h` | RX, duplex (`DEVOURER_TX_STATUS`) | len, bytes hex | diff --git a/examples/common/caps_event.h b/examples/common/caps_event.h new file mode 100644 index 0000000..19275b8 --- /dev/null +++ b/examples/common/caps_event.h @@ -0,0 +1,74 @@ +/* Shared `adapter.caps` machine-event emitter for the demos. + * + * One place that serializes IRtlDevice::GetAdapterCaps() (src/AdapterCaps.h) to + * the JSONL event plane so rxdemo / txdemo / doctor / txpower all emit the same + * schema — a dependent app or test script consumes one event instead of calling + * the C++ API. Emit it right after CreateRtlDevice (the caps are static and + * resolved at construction — no bring-up needed). Mirrors the txpwr.caps + * emission in examples/txpower/main.cpp: booleans as 0/1, chip_id as a hex + * string, bandwidths + frequency spans as arrays (absent band -> null). */ +#ifndef DEVOURER_CAPS_EVENT_H +#define DEVOURER_CAPS_EVENT_H + +#include "AdapterCaps.h" +#include "Event.h" +#include "IRtlDevice.h" + +namespace devourer { + +inline void emit_adapter_caps(EventSink &sink, IRtlDevice *dev) { + const AdapterCaps c = dev->GetAdapterCaps(); + + /* Supported channel widths as an MHz int array (kBw* -> MHz). */ + int bw[5]; + int nbw = 0; + if (c.bw_mask & kBw5) bw[nbw++] = 5; + if (c.bw_mask & kBw10) bw[nbw++] = 10; + if (c.bw_mask & kBw20) bw[nbw++] = 20; + if (c.bw_mask & kBw40) bw[nbw++] = 40; + if (c.bw_mask & kBw80) bw[nbw++] = 80; + + Ev ev(sink, "adapter.caps"); + ev.f("supported", c.supported ? 1 : 0) + .f("chip", c.chip_name) + .f("names", c.marketing_names) + .hexf("chip_id", c.chip_id, 2) + .f("gen", generation_name(c.generation)) + .f("variant", c.variant) + .f("transport", c.transport) + .f("tx_chains", c.tx_chains) + .f("rx_chains", c.rx_chains) + .f("n_ss", c.tx.n_ss) + .f("stbc", c.tx.stbc_ok ? 1 : 0) + .f("ldpc", c.tx.ldpc_ok ? 1 : 0) + .f("sgi", c.tx.sgi_ok ? 1 : 0) + .f("bw_max", c.tx.bw_max_mhz) + .arr("bw", bw, static_cast(nbw)) + .f("txpwr_max", c.txpwr.index_max) + .f("txpwr_step_qdb", c.txpwr.step_qdb) + .f("txpwr_step_measured", c.txpwr.step_measured ? 1 : 0) + .f("txpwr_min_qdb", c.txpwr.offset_min_qdb) + .f("txpwr_max_qdb", c.txpwr.offset_max_qdb); + + auto band = [&ev](const char *k, const BandRange &b) { + if (b.valid) { + const int v[2] = {b.min_mhz, b.max_mhz}; + ev.arr(k, v, 2); + } else { + ev.f(k, nullptr); + } + }; + band("tune_2g4", c.tune_2g4); + band("tune_5g", c.tune_5g); + band("char_2g4", c.characterized_2g4); + band("char_5g", c.characterized_5g); + + ev.f("per_pkt_txpwr", c.per_packet_txpower ? 1 : 0) + .f("narrowband", c.narrowband_ok ? 1 : 0) + .f("fastretune", c.fastretune_ok ? 1 : 0) + .f("per_chain_rssi", c.per_chain_rssi ? 1 : 0); +} + +} // namespace devourer + +#endif /* DEVOURER_CAPS_EVENT_H */ diff --git a/examples/doctor/main.cpp b/examples/doctor/main.cpp index 4fde07d..4e82ca1 100644 --- a/examples/doctor/main.cpp +++ b/examples/doctor/main.cpp @@ -64,6 +64,7 @@ #include #include "AdapterHealth.h" +#include "caps_event.h" #include "RxPacket.h" #include "SignalStop.h" #include "UsbOpen.h" @@ -226,6 +227,8 @@ int main(int argc, char **argv) { return 3; } + devourer::emit_adapter_caps(logger->events(), dev.get()); + devourer::AdapterHealthInput in; /* 1. bring-up */ diff --git a/examples/rx/main.cpp b/examples/rx/main.cpp index 3f90351..d322e15 100644 --- a/examples/rx/main.cpp +++ b/examples/rx/main.cpp @@ -12,6 +12,7 @@ #include #include "BfReportDetect.h" +#include "caps_event.h" #include "LinkHealth.h" #include "RxPacket.h" #include "SweepSpec.h" @@ -564,6 +565,7 @@ int main() { devourer::Ev(*g_ev, "init.timing") .f("stage", "demo.create_device") .f("ms", ms_since_start()); + devourer::emit_adapter_caps(*g_ev, dev.get()); int pch = 36; if (const char *ch_env = std::getenv("DEVOURER_CHANNEL")) pch = std::atoi(ch_env); @@ -722,6 +724,7 @@ int main() { devourer::Ev(*g_ev, "init.timing") .f("stage", "demo.create_device") .f("ms", ms_since_start()); + devourer::emit_adapter_caps(*g_ev, rtlDevice.get()); /* The BB-debug-port / queue-depth / thermal research helpers are Jaguar1-only, * so they live on RtlJaguarDevice rather than the IRtlDevice interface. The * whole block compiles out when Jaguar1 support isn't built; when it is, the @@ -915,6 +918,20 @@ int main() { else ev.f("noise_floor_dbm", nullptr); ev.f("igi", q.igi_valid ? q.igi : -1); + + /* Live per-chain RX-path activity (GetActiveRxPaths) — which + * antennas actually carry signal, vs the static rx_chains the caps + * report. Rides the same dogfood knob + cadence; only meaningful on + * a >=2-chain part with ambient traffic. */ + devourer::ActiveRxPaths ap = dev->GetActiveRxPaths(); + if (ap.valid) { + devourer::Ev pev(*g_ev, "adapter.rxpaths"); + pev.hexf("active_mask", ap.active_mask, 2) + .f("n_active", ap.n_active) + .f("n_chains", ap.n_chains) + .f("frames", ap.frames) + .arr("rssi_dbm", ap.rssi_mean_dbm, ap.n_chains); + } } nap(g_rx_energy_ms); } diff --git a/examples/tx/main.cpp b/examples/tx/main.cpp index 2d2e373..15a6206 100644 --- a/examples/tx/main.cpp +++ b/examples/tx/main.cpp @@ -31,6 +31,7 @@ #endif #include "BfReportDetect.h" +#include "caps_event.h" #include "ChannelFreq.h" #include "RxPacket.h" #include "SweepSpec.h" @@ -461,6 +462,7 @@ int main(int argc, char **argv) { devourer::Ev(*g_ev, "init.timing") .f("stage", "txdemo.create_device") .f("ms", ms_since_start()); + devourer::emit_adapter_caps(*g_ev, rtlDevice.get()); /* Jaguar1-only research features (TX-mode default, fast-retune hopping, * thermal telemetry, TXAGC override, BB-reg probe) are not part of the diff --git a/examples/txpower/main.cpp b/examples/txpower/main.cpp index 3c2b08a..2248ea2 100644 --- a/examples/txpower/main.cpp +++ b/examples/txpower/main.cpp @@ -55,6 +55,7 @@ #include "TxPower.h" #include "UsbOpen.h" #include "WiFiDriver.h" +#include "caps_event.h" #include "env_config.h" #include "logger.h" @@ -224,6 +225,8 @@ int main(int argc, char **argv) { return 1; } + devourer::emit_adapter_caps(*g_ev, dev.get()); + const devourer::TxPowerCaps caps = dev->GetTxPowerCaps(); devourer::Ev(*g_ev, "txpwr.caps") .f("supported", caps.supported ? 1 : 0) diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h new file mode 100644 index 0000000..070baa1 --- /dev/null +++ b/src/AdapterCaps.h @@ -0,0 +1,127 @@ +/* Aggregate adapter-capability report — what the opened radio actually IS and + * what it can do, resolved from the chip identity devourer already determines at + * construction (SYS_CFG2 chip-id + USB PID + EFUSE RF-type). A dependent app + * (OpenIPC-FPV tooling, a link manager, a test harness) can enumerate the RF + * frequency coverage, channel widths, spatial-stream / chain count, and the + * per-family feature levers (per-packet TX power, narrowband, fast retune) + * without hardcoding a chip table of its own or scraping bring-up logs. + * + * This is the identity+radio superset of the narrower GetTxCaps (modulation + * features) and GetTxPowerCaps (power-knob range), which it composes by value so + * there is one source of truth per fact. Like those, it is STATIC — resolved at + * construction, safe from any thread, callable before Init/InitWrite. The live + * "which antennas look connected" question is deliberately NOT here (it needs + * traffic); see IRtlDevice::GetActiveRxPaths / ActiveRxPaths in RxQuality.h. + * + * FREQUENCY COVERAGE. The 5 GHz synthesizer on these parts tunes well past the + * regulatory UNII channels (the vendor rtl88x2bu "monitor_chan_override" hack: + * chan 16..253, freq = 5000 + 5*chan, ~5080..6165 MHz). devourer drives that + * whole span in monitor mode; `tune_5g` reports the tunable range while + * `characterized_5g` reports the sub-range backed by the generated txpwr_lmt / + * EFUSE PG tables (outside it, per-channel constants and TX power are + * extrapolated from the nearest characterized channel — the radio still tunes, + * but power is uncalibrated). Regulatory compliance is the CALLER's problem: the + * library enforces none. + */ +#ifndef DEVOURER_ADAPTER_CAPS_H +#define DEVOURER_ADAPTER_CAPS_H + +#include + +#include "TxCaps.h" +#include "TxPower.h" + +namespace devourer { + +enum class ChipGeneration : uint8_t { Unknown = 0, Jaguar1, Jaguar2, Jaguar3 }; + +inline const char *generation_name(ChipGeneration g) { + switch (g) { + case ChipGeneration::Jaguar1: + return "jaguar1"; + case ChipGeneration::Jaguar2: + return "jaguar2"; + case ChipGeneration::Jaguar3: + return "jaguar3"; + default: + return "unknown"; + } +} + +/* Supported channel widths, one bit per width (MHz). A mask, not a max, because + * the set is not contiguous per family: Jaguar3 adds 5/10 MHz narrowband BELOW + * the 20/40/80 the AC families do. */ +constexpr uint8_t kBw5 = 1u << 0; +constexpr uint8_t kBw10 = 1u << 1; +constexpr uint8_t kBw20 = 1u << 2; +constexpr uint8_t kBw40 = 1u << 3; +constexpr uint8_t kBw80 = 1u << 4; + +/* J1/J2 do 20/40/80; J3 adds the 5/10 MHz narrowband re-clock. Pure; + * unit-tested in tests/adapter_caps_selftest.cpp. */ +inline uint8_t bw_mask_for_generation(ChipGeneration g) { + const uint8_t ac = kBw20 | kBw40 | kBw80; + return g == ChipGeneration::Jaguar3 ? (ac | kBw5 | kBw10) + : g == ChipGeneration::Unknown ? 0 + : ac; +} + +/* A tunable / characterized frequency span (MHz). valid=false = band absent. */ +struct BandRange { + bool valid = false; + uint16_t min_mhz = 0; + uint16_t max_mhz = 0; +}; + +struct AdapterCaps; /* fwd for the frequency-range helper below */ + +/* Fill the 2.4 + 5 GHz tunable / characterized spans shared by all three Jaguar + * families. tune_* = what the synthesizer reaches in monitor mode; the 5 GHz + * span runs past the UNII channels (the extended synth ~5080..6165 MHz, chan + * 16..253 — per-chip lock varies, validated on the bench). characterized_* = + * the sub-range the generated txpwr_lmt / EFUSE PG tables cover; outside it TX + * power is extrapolated from the nearest characterized channel. Defined + * out-of-line below (needs the full AdapterCaps). */ +inline void set_standard_freq_ranges(AdapterCaps &c); + +struct AdapterCaps { + bool supported = false; /* false on a generation that hasn't wired this */ + + /* --- identity --- */ + const char *chip_name = ""; /* silicon die, no bus suffix: "RTL8822C" */ + const char *marketing_names = "";/* alias list, e.g. "RTL8812CU/RTL8822CU" */ + uint8_t chip_id = 0; /* SYS_CFG2 (0x00FC) dispatch byte */ + ChipGeneration generation = ChipGeneration::Unknown; + const char *variant = ""; /* per-family variant tag ("C8822B", ICType) */ + const char *transport = ""; /* "usb" | "pcie" */ + + /* --- chains (EFUSE RF-type on Jaguar1; per-variant on Jaguar2/3) --- */ + uint8_t tx_chains = 0; + uint8_t rx_chains = 0; + + /* --- composed sub-caps (single source of truth) --- */ + TxCaps tx; /* = GetTxCaps() — modulation features */ + TxPowerCaps txpwr; /* = GetTxPowerCaps() — power-knob range/step */ + + /* --- bandwidth + frequency --- */ + uint8_t bw_mask = 0; /* kBw* bits */ + BandRange tune_2g4, tune_5g; /* synthesizer-tunable spans */ + BandRange characterized_2g4, characterized_5g; /* txpwr-table-backed spans */ + + /* --- feature flags --- */ + bool per_packet_txpower = false; /* Jaguar2 descriptor TXPWR_OFSET LUT only */ + bool narrowband_ok = false; /* 5/10 MHz re-clock (Jaguar3) */ + bool fastretune_ok = false; /* lean FastRetune override exists */ + bool per_chain_rssi = false; /* frame parser fills per-chain rssi (>=2ch) */ +}; + +inline void set_standard_freq_ranges(AdapterCaps &c) { + c.tune_2g4 = BandRange{true, 2412, 2484}; + c.characterized_2g4 = BandRange{true, 2412, 2484}; + c.tune_5g = BandRange{true, 5080, 6165}; + c.characterized_5g = BandRange{true, 5180, 5825}; +} + +} // namespace devourer + +#endif /* DEVOURER_ADAPTER_CAPS_H */ diff --git a/src/ChannelFreq.h b/src/ChannelFreq.h index 7431cf5..fb5ce15 100644 --- a/src/ChannelFreq.h +++ b/src/ChannelFreq.h @@ -10,14 +10,22 @@ namespace devourer { /* Map a radiotap CHANNEL frequency (MHz) to a Wi-Fi channel number. Returns 0 - * for a frequency outside the bands devourer drives (caller ignores it). */ + * for a frequency outside the bands devourer drives (caller ignores it). + * + * The 5 GHz clause deliberately runs past the UNII channels: the RF + * synthesizer on these chips covers roughly 5080..6165 MHz (vendor + * rtl88x2bu "monitor_chan_override": chan 16..253, freq = 5000 + 5*chan), + * so any 5 MHz-gridded frequency up to chan 253 maps to a tunable channel. + * Channel stays uint8_t everywhere; 253 is the ceiling. Not every chip + * locks every frequency — out-of-band use is validated per chip, and TX + * power there is extrapolated from the nearest characterized channel. */ static inline int freq_to_chan(uint16_t freq_mhz) { if (freq_mhz == 2484) return 14; if (freq_mhz >= 2412 && freq_mhz <= 2472) return (freq_mhz - 2407) / 5; /* 2.4 GHz ch1..13 */ - if (freq_mhz >= 5000 && freq_mhz <= 5895) - return (freq_mhz - 5000) / 5; /* 5 GHz UNII ch */ + if (freq_mhz >= 5000 && freq_mhz <= 6265) + return (freq_mhz - 5000) / 5; /* 5 GHz ch, incl. extended synth range */ return 0; } diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index d65b324..d5b5fe5 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -5,6 +5,7 @@ #include #include +#include "AdapterCaps.h" #include "AdapterHealth.h" #include "RxQuality.h" #include "RxSense.h" @@ -134,6 +135,26 @@ class IRtlDevice { * frame that never decodes). Default returns supported=false. */ virtual devourer::TxCaps GetTxCaps() { return {}; } + /* Aggregate STATIC adapter-capability report (see src/AdapterCaps.h): + * identity (chip name, generation, variant, transport, chip-id), TX/RX chain + * counts, the composed TxCaps + TxPowerCaps, the supported channel-width set, + * per-band tunable + characterized frequency spans, and the per-family + * feature flags (per-packet TX power, narrowband, fast retune, per-chain + * RSSI). Resolved at construction — safe from any thread and callable BEFORE + * Init/InitWrite (the demos emit it as the `adapter.caps` event right after + * CreateRtlDevice). Default returns supported=false. */ + virtual devourer::AdapterCaps GetAdapterCaps() { return {}; } + + /* Best-effort live estimate of which RX chains are actually carrying signal + * (see ActiveRxPaths in src/RxQuality.h) — derived from the per-frame + * per-chain RSSI the frame parser fills, so it needs an RX loop running and + * ambient traffic to mean anything. A chain whose mean RSSI sits far below + * the strongest is reported inactive (a disconnected/blocked antenna, or a + * software-masked path). Delta semantics like GetRxQuality: the window drains + * on read. Default is an all-invalid snapshot; a generation with >=2 RX + * chains overrides. NOT part of AdapterCaps, which is static. */ + virtual devourer::ActiveRxPaths GetActiveRxPaths() { return {}; } + virtual bool send_packet(const uint8_t *packet, size_t length) = 0; virtual SelectedChannel GetSelectedChannel() = 0; diff --git a/src/RxQuality.h b/src/RxQuality.h index 925f72a..c4c89db 100644 --- a/src/RxQuality.h +++ b/src/RxQuality.h @@ -203,6 +203,114 @@ inline RxQuality build_rx_quality(const RxQualitySnapshot &s, const RxEnergy &e, return q; } +/* --- Live per-chain RX-path activity (the companion to the static AdapterCaps + * chain count) --- + * + * AdapterCaps.rx_chains says how many chains the silicon HAS; this says how many + * currently look CONNECTED, inferred from the per-frame per-chain RSSI the frame + * parser fills (RxAtrib.rssi[0..3]). It's the one antenna question that can't be + * answered statically: a chain with a disconnected/blocked antenna (or one a + * caller software-masked via SetRxPathMask) still exists, but its RSSI collapses + * to the noise floor. Best-effort: only meaningful with an RX loop running and + * ambient traffic; a strong near-field frame can briefly light a dead chain via + * coupling, so treat single-window results as a hint, not a verdict. */ +struct ActiveRxPaths { + bool valid = false; /* false when no frames were sampled in the window */ + uint32_t frames = 0; + uint8_t n_chains = 0; /* chains considered (the adapter's rx_chains) */ + uint8_t n_active = 0; /* chains classified active */ + uint8_t active_mask = 0; /* bit i set = chain i active */ + int rssi_mean_dbm[4] = {0, 0, 0, 0}; /* per-chain window mean (raw - 110) */ + bool chain_sampled[4] = {false, false, false, false}; /* had any sample */ +}; + +/* Classify which of `n_chains` are active: a chain is active if it was sampled + * and its mean RSSI is within `margin_db` of the strongest sampled chain. Pure; + * unit-tested in tests/adapter_caps_selftest.cpp. `rssi_mean_dbm` and + * `sampled` are the per-chain window aggregates; fills `active_mask` and returns + * the active count. A single strong chain → mask 0x1; two balanced → 0x3; a + * chain pinned at the noise floor is excluded. */ +inline uint8_t classify_active_paths(const int *rssi_mean_dbm, + const bool *sampled, uint8_t n_chains, + int margin_db, uint8_t *active_mask) { + int strongest = -1000; + for (uint8_t i = 0; i < n_chains && i < 4; i++) + if (sampled[i] && rssi_mean_dbm[i] > strongest) + strongest = rssi_mean_dbm[i]; + uint8_t mask = 0, count = 0; + if (strongest > -1000) { + for (uint8_t i = 0; i < n_chains && i < 4; i++) { + if (sampled[i] && (strongest - rssi_mean_dbm[i]) <= margin_db) { + mask |= static_cast(1u << i); + ++count; + } + } + } + if (active_mask) + *active_mask = mask; + return count; +} + +/* Thread-safe per-chain RSSI accumulator — the ActiveRxPaths analogue of + * RxQualityAccumulator. The device feeds add() the full rssi[0..3] tuple for + * every decoded frame; snapshot() drains the window (delta semantics) and + * classifies. `margin_db` default 20 dB: a chain more than 20 dB below the + * strongest is a disconnected/masked antenna, not thermal spread across + * connected chains. */ +class RxPathActivityAccumulator { +public: + /* Feed the per-chain RSSI (raw devourer units) of one decoded frame; only the + * first `n_chains` are considered. A chain reading <= 0 (no phy-status power) + * is not a sample for that chain. */ + void add(const uint8_t *rssi_raw, uint8_t n_chains) { + std::lock_guard lk(mu_); + if (n_chains > 4) + n_chains = 4; + n_chains_ = n_chains; + bool any = false; + for (uint8_t i = 0; i < n_chains; i++) { + if (rssi_raw[i] == 0) + continue; + sum_[i] += rssi_raw[i]; + ++cnt_[i]; + any = true; + } + if (any) + ++frames_; + } + + ActiveRxPaths snapshot(int margin_db = 20) { + ActiveRxPaths s; + std::lock_guard lk(mu_); + s.frames = frames_; + s.n_chains = n_chains_; + s.valid = frames_ > 0; + for (uint8_t i = 0; i < n_chains_ && i < 4; i++) { + if (cnt_[i]) { + s.rssi_mean_dbm[i] = + static_cast(sum_[i] / cnt_[i]) - 110; + s.chain_sampled[i] = true; + } + } + s.n_active = classify_active_paths(s.rssi_mean_dbm, s.chain_sampled, + s.n_chains, margin_db, &s.active_mask); + /* reset (delta semantics) */ + frames_ = 0; + for (uint8_t i = 0; i < 4; i++) { + sum_[i] = 0; + cnt_[i] = 0; + } + return s; + } + +private: + std::mutex mu_; + uint32_t frames_ = 0; + uint8_t n_chains_ = 0; + uint64_t sum_[4] = {0, 0, 0, 0}; + uint32_t cnt_[4] = {0, 0, 0, 0}; +}; + } // namespace devourer #endif /* DEVOURER_RX_QUALITY_H */ diff --git a/src/jaguar1/EepromManager.cpp b/src/jaguar1/EepromManager.cpp index 376bb7f..d11d059 100644 --- a/src/jaguar1/EepromManager.cpp +++ b/src/jaguar1/EepromManager.cpp @@ -398,7 +398,7 @@ static uint8_t classify_channel(uint8_t ch, uint8_t *group, uint8_t *cck_group) * (core/rtw_rf.c). NB the gp3/gp4 split is at 98/100 (not 80/82) and the * top group runs to 253: channels 82..98 belong to gp3, not gp4. */ int gp = -1; - if (16 <= ch && ch <= 42) gp = 0; + if (15 <= ch && ch <= 42) gp = 0; /* 15: below-band edge (kCenterCh5gAll[0]) */ else if (44 <= ch && ch <= 48) gp = 1; else if (50 <= ch && ch <= 58) gp = 2; else if (60 <= ch && ch <= 98) gp = 3; @@ -838,16 +838,25 @@ uint8_t EepromManager::GetTxPowerIndexBase(uint8_t path, uint8_t rate, return 0; ch_idx = static_cast(channel - 1); } else { - bool found = false; + /* Nearest-entry fallback for channels the table doesn't list (odd-grid or + * extended-synth channels >177): returning 0 here would silently program + * a near-zero TX power index. The table is sorted, so track the closest + * entry — an exact hit still wins. Power on a non-listed channel is + * extrapolated, not characterized. */ + uint8_t best = 0; + int best_d = 0x7fffffff; for (uint8_t i = 0; i < kCenterCh5gAllNum; i++) { - if (kCenterCh5gAll[i] == channel) { - ch_idx = i; - found = true; - break; + const int d = (kCenterCh5gAll[i] > channel) + ? kCenterCh5gAll[i] - channel + : channel - kCenterCh5gAll[i]; + if (d < best_d) { + best_d = d; + best = i; } + if (d == 0) + break; } - if (!found) - return 0; + ch_idx = best; } int txPower = 0; diff --git a/src/jaguar1/RadioManagementModule.cpp b/src/jaguar1/RadioManagementModule.cpp index 4e7ab91..c7c771a 100644 --- a/src/jaguar1/RadioManagementModule.cpp +++ b/src/jaguar1/RadioManagementModule.cpp @@ -1459,6 +1459,20 @@ void RadioManagementModule::Set_HW_VAR_ENABLE_RX_BAR(bool val) { } void RadioManagementModule::phy_SwChnl8812() { + /* Extended-synth channels (below-band 15..35 / above 177, freq = + * 5000 + 5*ch up to ch 253): the RF tunes them, but the power tables and + * per-channel constants are clamped from the nearest characterized + * channel. Warn once so an unexpected retune there is visible. */ + if (!_warned_uncharacterized && + ((_currentChannel >= 15 && _currentChannel <= 35) || + _currentChannel > 177)) { + _warned_uncharacterized = true; + _logger->warn("channel {} is outside the characterized range — TX power " + "and per-channel constants extrapolated from the nearest " + "table entry", + _currentChannel); + } + /* 8814 has its own channel-set: different fc_area boundaries, RF_MOD_AG * channel ranges, a 5G AGC-table sub-select at 0x958[4:0], 2.4G CCK * TX DFIR writes, and the combined channel+mod RF write pattern. The @@ -1609,9 +1623,11 @@ void RadioManagementModule::phy_SwChnl8814A() { _logger->error("error Chnl {} !", channelToSW); } - /* fc_area — 8814A boundaries. */ + /* fc_area — 8814A boundaries. Below-band ch 15..35 rides the low 5G bucket + * (extended-synth clamp, matching the 8812 path); >177 is already covered by + * the open-ended 118<= case. */ uint32_t fc_area; - if (36 <= channelToSW && channelToSW <= 48) { + if (15 <= channelToSW && channelToSW <= 48) { fc_area = 0x494; } else if (50 <= channelToSW && channelToSW <= 64) { fc_area = 0x453; @@ -1626,9 +1642,9 @@ void RadioManagementModule::phy_SwChnl8814A() { for (uint8_t eRFPath = 0; eRFPath < _eepromManager->numTotalRfPath; ++eRFPath) { - /* RF_MOD_AG — 8814A boundaries. */ + /* RF_MOD_AG — 8814A boundaries. ch 15..35 -> low band (extended-synth). */ uint32_t rf_val; - if (36 <= channelToSW && channelToSW <= 64) { + if (15 <= channelToSW && channelToSW <= 64) { rf_val = 0x101; } else if (100 <= channelToSW && channelToSW <= 140) { rf_val = 0x301; @@ -1648,8 +1664,9 @@ void RadioManagementModule::phy_SwChnl8814A() { combined); } - /* 5G AGC table sub-select (rAGC_table_Jaguar2 = 0x958, 8814-only). */ - if (36 <= channelToSW && channelToSW <= 64) { + /* 5G AGC table sub-select (rAGC_table_Jaguar2 = 0x958, 8814-only). + * ch 15..35 -> low-band table (extended-synth clamp). */ + if (15 <= channelToSW && channelToSW <= 64) { _device.phy_set_bb_reg(0x958, 0x1F, 1); } else if (100 <= channelToSW && channelToSW <= 144) { _device.phy_set_bb_reg(0x958, 0x1F, 2); diff --git a/src/jaguar1/RadioManagementModule.h b/src/jaguar1/RadioManagementModule.h index 033e760..0ea6cff 100644 --- a/src/jaguar1/RadioManagementModule.h +++ b/src/jaguar1/RadioManagementModule.h @@ -70,6 +70,7 @@ class RadioManagementModule { Logger_t _logger; HwPort _hwPort = HwPort::HW_PORT0; bool _needIQK = false; + bool _warned_uncharacterized = false; /* one-shot extended-channel warning */ ChannelWidth_t _currentChannelBw; uint8_t _currentChannel; BandType current_band_type; diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 2131243..99019b4 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -728,8 +728,10 @@ void RtlJaguarDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { std::span{const_cast(data), (size_t)n})) { if (should_stop || g_devourer_should_stop) break; - if (!p.RxAtrib.crc_err) + if (!p.RxAtrib.crc_err) { _rxq.add(p.RxAtrib.rssi[0], p.RxAtrib.snr[0], p.RxAtrib.evm[0]); + _rxpaths.add(p.RxAtrib.rssi, _eepromManager->numTotalRfPath); + } _packetProcessor(p); } }; @@ -854,6 +856,52 @@ devourer::TxCaps RtlJaguarDevice::GetTxCaps() { return devourer::tx_caps_for_chains(_eepromManager->numTotalRfPath); } +devourer::AdapterCaps RtlJaguarDevice::GetAdapterCaps() { + devourer::AdapterCaps c; + c.supported = true; + c.generation = devourer::ChipGeneration::Jaguar1; + c.transport = _device.is_usb() ? "usb" : "pcie"; + c.tx = GetTxCaps(); + c.txpwr = GetTxPowerCaps(); + const uint8_t chains = _eepromManager->numTotalRfPath; + c.tx_chains = chains; + c.rx_chains = chains; + c.per_chain_rssi = chains >= 2; + c.bw_mask = devourer::bw_mask_for_generation(c.generation); + c.fastretune_ok = true; /* phy_SwChnl8812_fast (8812/8821) + full-path fallback */ + devourer::set_standard_freq_ranges(c); + + /* Identity from the EFUSE version-id. The die name is refined by the RF-type: + * the 8812 die shipped as both the 2T2R 8812AU and the 1T1R 8811AU cut. */ + switch (_eepromManager->version_id.ICType) { + case CHIP_8814A: + c.chip_name = "RTL8814A"; + c.marketing_names = "RTL8814AU"; + c.chip_id = 0x08; + c.variant = "8814A"; + break; + case CHIP_8821: + c.chip_name = "RTL8821A"; + c.marketing_names = "RTL8821AU"; + c.chip_id = 0x05; + c.variant = "8821A"; + break; + default: /* CHIP_8812 */ + if (_eepromManager->version_id.RFType == RF_TYPE_1T1R) { + c.chip_name = "RTL8811A"; + c.marketing_names = "RTL8811AU/RTL8811AR"; + c.variant = "8811A"; + } else { + c.chip_name = "RTL8812A"; + c.marketing_names = "RTL8812AU/RTL8812AR"; + c.variant = "8812A"; + } + c.chip_id = 0x04; + break; + } + return c; +} + devourer::TxPowerState RtlJaguarDevice::GetTxPowerState() { devourer::TxPowerState s; s.valid = true; diff --git a/src/jaguar1/RtlJaguarDevice.h b/src/jaguar1/RtlJaguarDevice.h index aab7bee..3809f64 100644 --- a/src/jaguar1/RtlJaguarDevice.h +++ b/src/jaguar1/RtlJaguarDevice.h @@ -73,6 +73,7 @@ class RtlJaguarDevice : public IRtlDevice { * loop for every decoded frame; drained by GetRxQuality on the caller's * cadence. */ devourer::RxQualityAccumulator _rxq; + devourer::RxPathActivityAccumulator _rxpaths; public: RtlJaguarDevice(RtlAdapter device, Logger_t logger, @@ -127,6 +128,13 @@ class RtlJaguarDevice : public IRtlDevice { * RF-type. STBC needs >=2 chains, so 1T1R cuts (8811AU/8821AU) report * stbc_ok=false and send_packet drops an STBC request. */ devourer::TxCaps GetTxCaps() override; + /* Aggregate identity + radio + feature caps (IRtlDevice). Composes GetTxCaps + * / GetTxPowerCaps; identity from the EFUSE version-id + RF-type. */ + devourer::AdapterCaps GetAdapterCaps() override; + /* Live per-chain RX-path activity (fed via _rxpaths in the RX loop). */ + devourer::ActiveRxPaths GetActiveRxPaths() override { + return _rxpaths.snapshot(); + } /* Read a baseband register (debug/diagnostic). Thin passthrough to the * radio manager's BB read — handy for confirming a TXAGC write landed. */ uint32_t ReadBBReg(uint16_t addr, uint32_t mask); diff --git a/src/jaguar2/HalJaguar2.cpp b/src/jaguar2/HalJaguar2.cpp index 6de94ee..edac53d 100644 --- a/src/jaguar2/HalJaguar2.cpp +++ b/src/jaguar2/HalJaguar2.cpp @@ -544,6 +544,19 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, _last_df18 = -1; _last_cck_key = -1; + /* Extended-synth channels (below-band 15..35 / above 177, freq = + * 5000 + 5*ch up to ch 253): the RF tunes them, but the power tables and + * per-channel constants are clamped from the nearest characterized + * channel. Warn once so an unexpected retune there is visible. */ + if (!_warned_uncharacterized && + ((channel >= 15 && channel <= 35) || channel > 177)) { + _warned_uncharacterized = true; + _logger->warn("channel {} is outside the characterized range — TX power " + "and per-channel constants extrapolated from the nearest " + "table entry", + channel); + } + if (_variant == ChipVariant::C8821C) { set_channel_bw_8821c(channel, bw, rfe_type, primary_ch_idx); _last_tuned_ch = channel; @@ -574,20 +587,23 @@ void HalJaguar2::set_channel_bw(uint8_t channel, uint8_t bw, uint8_t rfe_type, _device.phy_set_bb_reg(0x0a28, 0x0000ffff, 0x1525); } } else { - /* 5G AGC table + fc (config_phydm_switch_channel_8822b 5G branch) */ - if (cch >= 36 && cch <= 64) + /* 5G AGC table + fc (config_phydm_switch_channel_8822b 5G branch). + * Extended-range clamps: below-band ch 15..35 rides the low bucket + * (mirrors Jaguar1's explicit 15..35 case) and ch >177 keeps the top + * bucket — the vendor tables end at 177 but the synth tunes past it. */ + if (cch <= 64) _device.phy_set_bb_reg(0x0958, 0x1f, 0x1); else if (cch >= 100 && cch <= 144) _device.phy_set_bb_reg(0x0958, 0x1f, 0x2); else if (cch >= 149) _device.phy_set_bb_reg(0x0958, 0x1f, 0x3); - if (cch >= 36 && cch <= 48) + if (cch <= 48) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x494); else if (cch >= 52 && cch <= 64) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x453); else if (cch >= 100 && cch <= 116) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x452); - else if (cch >= 118 && cch <= 177) + else if (cch >= 118) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x412); } @@ -739,12 +755,20 @@ uint8_t HalJaguar2::rf_be_for_8822b(uint8_t cch) { 0x0, 0x7, 0x7, 0x6, 0x5, 0x5, 0x0}; if (cch <= 14) return 0x0; - if (cch >= 36 && cch <= 64) + /* Extended-range clamps: the vendor tables cover 36..64 / 100..144 / + * 149..177; the synth tunes below 36 and above 177 (chan*5+5000 grid up to + * 253). Clamp to the nearest table edge so the VCO band is still set — + * extrapolated, not characterized. */ + if (cch < 36) + return low_band[0]; + if (cch <= 64) return low_band[(cch - 36) >> 1]; if (cch >= 100 && cch <= 144) return middle_band[(cch - 100) >> 1]; if (cch >= 149 && cch <= 177) return high_band[(cch - 149) >> 1]; + if (cch > 177) + return high_band[(177 - 149) >> 1]; return 0xff; } @@ -791,7 +815,7 @@ bool HalJaguar2::fast_retune(uint8_t channel, uint8_t bw, int agc_bucket = -1; if (g2) agc_bucket = 0; - else if (cch >= 36 && cch <= 64) + else if (cch <= 64) /* incl. below-band 15..35 (extended-range clamp) */ agc_bucket = 1; else if (cch >= 100 && cch <= 144) agc_bucket = 2; @@ -812,13 +836,13 @@ bool HalJaguar2::fast_retune(uint8_t channel, uint8_t bw, uint32_t fc = 0xffffffff; if (g2) fc = 0x96a; - else if (cch >= 36 && cch <= 48) + else if (cch <= 48) /* incl. below-band 15..35 (extended-range clamp) */ fc = 0x494; else if (cch >= 52 && cch <= 64) fc = 0x453; else if (cch >= 100 && cch <= 116) fc = 0x452; - else if (cch >= 118 && cch <= 177) + else if (cch >= 118) /* open-ended: extended channels >177 keep 0x412 */ fc = 0x412; if (fc != 0xffffffff && fc != _last_fc) { _cw_fc = (_cw_fc & ~0x1ffe0000u) | (fc << 17); @@ -1131,19 +1155,21 @@ void HalJaguar2::set_channel_bw_8821c(uint8_t channel, uint8_t bw, _device.phy_set_bb_reg(0x0aac, 0xffffffff, _cck_aac_8821c); } } else { - if (cch >= 36 && cch <= 64) + /* Extended-range clamps (see the 8822B branch): ch 15..35 rides the low + * bucket, ch >177 keeps the top bucket. */ + if (cch <= 64) _device.phy_set_bb_reg(0x0c1c, 0x00000F00, 0x1); else if (cch >= 100 && cch <= 144) _device.phy_set_bb_reg(0x0c1c, 0x00000F00, 0x2); else if (cch >= 149) _device.phy_set_bb_reg(0x0c1c, 0x00000F00, 0x3); - if (cch >= 36 && cch <= 48) + if (cch <= 48) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x494); else if (cch >= 52 && cch <= 64) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x453); else if (cch >= 100 && cch <= 116) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x452); - else if (cch >= 118 && cch <= 177) + else if (cch >= 118) _device.phy_set_bb_reg(0x0860, 0x1ffe0000, 0x412); if (cch >= 100 && cch <= 140) rf18 |= (1u << 17); diff --git a/src/jaguar2/HalJaguar2.h b/src/jaguar2/HalJaguar2.h index f554963..bd727ef 100644 --- a/src/jaguar2/HalJaguar2.h +++ b/src/jaguar2/HalJaguar2.h @@ -257,6 +257,7 @@ class HalJaguar2 { int _last_rf_be = -1; int _last_df18 = -1; /* 8822B ch144 RF 0xdf[18] state */ int _last_cck_key = -1; /* 2G spur (8822B) / CCK-filter (8821C) key: ch14? */ + bool _warned_uncharacterized = false; /* one-shot extended-channel warning */ /* --- 8821C-specific (C8821C variant) channel/RF-set/LCK, transcribed from * reference/8821cu phydm_hal_api8821c.c + halrf_8821c.c. The 8822B channel-set diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 9e00871..ea75014 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -295,8 +295,11 @@ void RtlJaguar2Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { f.drvinfo_size, f.rx_rate <= 3, p.RxAtrib); p.Data = std::span(const_cast(f.frame), f.frame_len); - if (!p.RxAtrib.crc_err) + if (!p.RxAtrib.crc_err) { _rxq.add(p.RxAtrib.rssi[0], p.RxAtrib.snr[0], p.RxAtrib.evm[0]); + _rxpaths.add(p.RxAtrib.rssi, + _variant == jaguar2::ChipVariant::C8821C ? 1 : 2); + } _packetProcessor(p); } ++frames; @@ -658,6 +661,37 @@ devourer::TxCaps RtlJaguar2Device::GetTxCaps() { return devourer::tx_caps_for_chains(chains); } +devourer::AdapterCaps RtlJaguar2Device::GetAdapterCaps() { + devourer::AdapterCaps c; + c.supported = true; + c.generation = devourer::ChipGeneration::Jaguar2; + c.transport = _device.is_usb() ? "usb" : "pcie"; + c.tx = GetTxCaps(); + c.txpwr = GetTxPowerCaps(); + const uint8_t chains = _variant == jaguar2::ChipVariant::C8821C ? 1 : 2; + c.tx_chains = chains; + c.rx_chains = chains; + c.per_chain_rssi = chains >= 2; + c.bw_mask = devourer::bw_mask_for_generation(c.generation); + c.fastretune_ok = true; + c.per_packet_txpower = true; /* TX descriptor TXPWR_OFSET LUT — Jaguar2 only */ + devourer::set_standard_freq_ranges(c); + + if (_variant == jaguar2::ChipVariant::C8821C) { + c.chip_name = "RTL8821C"; + c.marketing_names = c.transport[0] == 'p' ? "RTL8821CE" + : "RTL8811CU/RTL8821CU"; + c.chip_id = 0x09; + c.variant = "C8821C"; + } else { + c.chip_name = "RTL8822B"; + c.marketing_names = "RTL8822BU/RTL8812BU"; + c.chip_id = 0x0a; + c.variant = "C8822B"; + } + return c; +} + devourer::EfuseStability RtlJaguar2Device::ProbeEfuseStability(int reads) { if (!_brought_up) return {}; /* supported=false — the efuse read needs a powered chip */ diff --git a/src/jaguar2/RtlJaguar2Device.h b/src/jaguar2/RtlJaguar2Device.h index 2f342a3..0269ebf 100644 --- a/src/jaguar2/RtlJaguar2Device.h +++ b/src/jaguar2/RtlJaguar2Device.h @@ -84,6 +84,13 @@ class RtlJaguar2Device : public IRtlDevice { /* Per-chip TX caps (IRtlDevice): the 8821C is 1T1R (no STBC), the 8822B * 2T2R. send_packet drops an STBC request the variant can't honour. */ devourer::TxCaps GetTxCaps() override; + /* Aggregate identity + radio + feature caps (IRtlDevice). Composes GetTxCaps + * / GetTxPowerCaps; identity from ChipVariant, transport from the adapter. */ + devourer::AdapterCaps GetAdapterCaps() override; + /* Live per-chain RX-path activity (fed via _rxpaths in the RX loop). */ + devourer::ActiveRxPaths GetActiveRxPaths() override { + return _rxpaths.snapshot(); + } /* Per-packet TX-power offset — the zero-cost per-frame power trim the * adaptive link wants (distinct from the per-rate TXAGC that @@ -145,6 +152,7 @@ class RtlJaguar2Device : public IRtlDevice { const devourer::DeviceConfig _cfg; /* Rolling per-frame RX link-quality aggregate (GetRxQuality). */ devourer::RxQualityAccumulator _rxq; + devourer::RxPathActivityAccumulator _rxpaths; Logger_t _logger; jaguar2::ChipVariant _variant; jaguar2::HalJaguar2 _hal; diff --git a/src/jaguar3/Halrf8822c.cpp b/src/jaguar3/Halrf8822c.cpp index 1cff0cc..aedd27c 100644 --- a/src/jaguar3/Halrf8822c.cpp +++ b/src/jaguar3/Halrf8822c.cpp @@ -904,7 +904,7 @@ void Halrf8822c::fill_report(uint8_t ch) { void Halrf8822c::phy_iq_calibrate(ChannelWidth_t bw, uint8_t channel) { _iqk.is_nb_iqk = (bw == CHANNEL_WIDTH_5 || bw == CHANNEL_WIDTH_10); _iqk.bw_val = _iqk.is_nb_iqk ? 0 : static_cast(bw); /* 0/1/2=20/40/80 */ - _iqk.is_5g = (channel >= 36); + _iqk.is_5g = (channel > 14); /* incl. below-band 15..35 extended-synth */ uint32_t mac[MAC_REG_NUM_8822C], bbk[BB_REG_NUM_8822C], rf[RF_REG_NUM_8822C][2]; diff --git a/src/jaguar3/Halrf8822e.cpp b/src/jaguar3/Halrf8822e.cpp index 6f75391..a71e1a9 100644 --- a/src/jaguar3/Halrf8822e.cpp +++ b/src/jaguar3/Halrf8822e.cpp @@ -1389,11 +1389,13 @@ void Halrf8822e::txgapk_calculate_offset(uint8_t path, uint8_t channel) { rf_write(path, 0xdf, 0x20000, 0x1); rf_write(path, 0x89, 0x00003, 0x3); rf_write(path, 0x00, 0x003e0, 0x0f); - if (channel >= 36 && channel <= 64) + /* Extended-range clamps: below-band 15..35 rides the low bucket, >177 + * keeps the top bucket (the synth tunes past the vendor tables). */ + if (channel <= 64) bb_set(0x1b98, 0x00007000, 0x2); else if (channel >= 100 && channel <= 144) bb_set(0x1b98, 0x00007000, 0x3); - else if (channel >= 149 && channel <= 177) + else if (channel >= 149) bb_set(0x1b98, 0x00007000, 0x4); } @@ -1559,10 +1561,11 @@ void Halrf8822e::txgapk_save_all() { void Halrf8822e::txgapk_write_tx_gain(uint8_t channel) { uint32_t base = 0x20; int band = 1; + /* Extended-range clamps: 15..35 → low 5G band, >177 → top band. */ if (channel >= 1 && channel <= 14) { base = 0x20; band = 1; } - else if (channel >= 36 && channel <= 64) { base = 0x200; band = 2; } + else if (channel >= 15 && channel <= 64) { base = 0x200; band = 2; } else if (channel >= 100 && channel <= 144) { base = 0x280; band = 3; } - else if (channel >= 149 && channel <= 177) { base = 0x300; band = 4; } + else if (channel >= 149) { base = 0x300; band = 4; } for (uint8_t path = 0; path < SS_8822E; ++path) { /* Guard: if the saved RF gain table for this band read back all-zero, the diff --git a/src/jaguar3/RadioManagementJaguar3.cpp b/src/jaguar3/RadioManagementJaguar3.cpp index 860abf3..b7e0ce1 100644 --- a/src/jaguar3/RadioManagementJaguar3.cpp +++ b/src/jaguar3/RadioManagementJaguar3.cpp @@ -48,6 +48,20 @@ void RadioManagementJaguar3::set_channel_bwmode(uint8_t channel, * fast-path writes (the J1 RadioManagementModule pattern). */ invalidate_fast_caches(); + /* Extended-synth channels (below-band 15..35 / above 177, freq = + * 5000 + 5*ch up to ch 253): the RF tunes them, but the power tables, + * calibration bands and per-channel constants are clamped from the nearest + * characterized channel. Warn once so an unexpected retune there is + * visible. */ + if (!_warned_uncharacterized && + ((channel >= 15 && channel <= 35) || channel > 177)) { + _warned_uncharacterized = true; + _logger->warn("channel {} is outside the characterized range — TX power " + "and per-channel constants extrapolated from the nearest " + "table entry", + channel); + } + _last_channel = channel; /* --- bandwidth block (config_phydm_switch_bandwidth_8822c CH_20 / CH_40). @@ -306,15 +320,18 @@ uint32_t RadioManagementJaguar3::rf18_for(uint8_t central, /* SCO tracking f_c value (phydm_sco_trk_fc_setting_8822c), keyed on central. */ uint32_t RadioManagementJaguar3::sco_for(uint8_t central) { - if (central >= 36 && central <= 51) return 0x494; - if (central >= 52 && central <= 55) return 0x493; - if (central >= 56 && central <= 111) return 0x453; - if (central >= 112 && central <= 119) return 0x452; - if (central >= 120 && central <= 172) return 0x412; - if (central >= 173) return 0x411; - if (central >= 1 && central <= 10) return 0x9aa; + /* 2.4 GHz first so the 5 GHz test can carry an open lower bound. */ + if (central <= 10) return 0x9aa; if (central == 11 || central == 12) return 0x96a; - return 0x969; /* 13/14 */ + if (central <= 14) return 0x969; /* 13/14 */ + /* 5 GHz. Below-band ch 15..35 rides the low bucket (extended-synth clamp); + * the upper bound is already open. */ + if (central <= 51) return 0x494; + if (central <= 55) return 0x493; + if (central <= 111) return 0x453; + if (central <= 119) return 0x452; + if (central <= 172) return 0x412; + return 0x411; } /* Per-band RX AGC-table selection. 8822C: phydm_cck/ofdm_agc_tab_sel_8822c — diff --git a/src/jaguar3/RadioManagementJaguar3.h b/src/jaguar3/RadioManagementJaguar3.h index 90e1303..6164d76 100644 --- a/src/jaguar3/RadioManagementJaguar3.h +++ b/src/jaguar3/RadioManagementJaguar3.h @@ -160,6 +160,7 @@ class RadioManagementJaguar3 { Logger_t _logger; ChipVariant _variant; uint8_t _last_channel = 0; /* set by set_channel_bwmode; 0 = not yet tuned */ + bool _warned_uncharacterized = false; /* one-shot extended-channel warning */ /* fast_retune write-on-change caches. Invalidated whenever the full * set_channel_bwmode runs (it rewrites all of them); refreshed by the fast diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 7cdafee..eb921bb 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -185,8 +185,10 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { jaguar3::parse_phy_sts_jgr3(data + off + jaguar3::RXDESC_SIZE_8822C, f.drvinfo_size, p.RxAtrib); p.Data = std::span(const_cast(f.frame), f.frame_len); - if (!p.RxAtrib.crc_err) + if (!p.RxAtrib.crc_err) { _rxq.add(p.RxAtrib.rssi[0], p.RxAtrib.snr[0], p.RxAtrib.evm[0]); + _rxpaths.add(p.RxAtrib.rssi, 2); /* 8822C/8822E are 2T2R */ + } /* TX-BF apply gate (DEVOURER_BF_TXBF): a VHT Compressed Beamforming * Report (category 0x15, action 0x00) from the target peer (addr2) means * the WMAC just ingested a fresh V matrix — enable the apply toggle so @@ -887,6 +889,35 @@ devourer::TxCaps RtlJaguar3Device::GetTxCaps() { return devourer::tx_caps_for_chains(2); /* 8822C/8822E are 2T2R */ } +devourer::AdapterCaps RtlJaguar3Device::GetAdapterCaps() { + devourer::AdapterCaps c; + c.supported = true; + c.generation = devourer::ChipGeneration::Jaguar3; + c.transport = _device.is_usb() ? "usb" : "pcie"; + c.tx = GetTxCaps(); + c.txpwr = GetTxPowerCaps(); + c.tx_chains = 2; /* 8822C/8822E are 2T2R */ + c.rx_chains = 2; + c.per_chain_rssi = true; + c.bw_mask = devourer::bw_mask_for_generation(c.generation); + c.fastretune_ok = true; + c.narrowband_ok = true; /* 5/10 MHz baseband re-clock — Jaguar3 only */ + devourer::set_standard_freq_ranges(c); + + if (_variant == jaguar3::ChipVariant::C8822E) { + c.chip_name = "RTL8822E"; + c.marketing_names = "RTL8812EU/RTL8822EU"; + c.chip_id = 0x17; + c.variant = "C8822E"; + } else { + c.chip_name = "RTL8822C"; + c.marketing_names = "RTL8812CU/RTL8822CU"; + c.chip_id = 0x13; + c.variant = "C8822C"; + } + return c; +} + devourer::EfuseStability RtlJaguar3Device::ProbeEfuseStability(int reads) { if (!_brought_up) return {}; /* supported=false — the efuse read needs a powered chip */ diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index f111721..b523eb2 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -87,6 +87,13 @@ class RtlJaguar3Device : public IRtlDevice { devourer::ThermalStatus GetThermalStatus() override; /* Per-chip TX caps (IRtlDevice): 8822C/8822E are 2T2R (STBC ok). */ devourer::TxCaps GetTxCaps() override; + /* Aggregate identity + radio + feature caps (IRtlDevice). Composes GetTxCaps + * / GetTxPowerCaps; identity from ChipVariant, transport from the adapter. */ + devourer::AdapterCaps GetAdapterCaps() override; + /* Live per-chain RX-path activity (fed via _rxpaths in the RX loop). */ + devourer::ActiveRxPaths GetActiveRxPaths() override { + return _rxpaths.snapshot(); + } /* Runtime TX-mode default — applied in send_packet when the radiotap carries * no rate. Without this the Jaguar3 TX path fell back to MGN_1M for rate-less * frames (so DEVOURER_TX_RATE/an MCS flood went on-air at 1 Mbps): the feature @@ -175,6 +182,7 @@ class RtlJaguar3Device : public IRtlDevice { bool _brought_up = false; /* Rolling per-frame RX link-quality aggregate (GetRxQuality). */ devourer::RxQualityAccumulator _rxq; + devourer::RxPathActivityAccumulator _rxpaths; /* Frame counter for periodic NDPA sounding (DEVOURER_TX_NDPA=N). */ uint64_t _ndpa_ctr = 0; /* TX beamforming apply state (DEVOURER_BF_TXBF). The entry is configured at diff --git a/tests/adapter_caps_probe.sh b/tests/adapter_caps_probe.sh new file mode 100755 index 0000000..1ccf04c --- /dev/null +++ b/tests/adapter_caps_probe.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Adapter-capabilities smoke: open each plugged devourer-supported adapter in +# turn, run rxdemo just long enough to emit the `adapter.caps` JSONL event +# (examples/common/caps_event.h), and print it. Validates the Part-A API on real +# silicon across generations — chip name, chain counts, bandwidth set, tunable / +# characterized frequency spans, feature flags. +# +# sudo tests/adapter_caps_probe.sh # probe a default PID list +# PIDS="8812 8813 012d" sudo tests/adapter_caps_probe.sh # explicit set +# +# Each PID is a USB product id (DEVOURER_PID); VID defaults to Realtek 0x0bda, +# override per-PID as VID:PID (e.g. 2357:012d for the TP-Link 8812BU). +set -u +cd "$(dirname "$0")/.." + +# Default set covers what's typically on the bench; missing ones are skipped. +PIDS=${PIDS:-"8812 8813 c812 c811 a81a 2357:012d"} + +cleanup() { sudo pkill -x rxdemo 2>/dev/null; } +trap cleanup EXIT + +have_jq=0; command -v jq >/dev/null 2>&1 && have_jq=1 + +fail=0 +for spec in $PIDS; do + if [[ "$spec" == *:* ]]; then vid=${spec%%:*}; pid=${spec##*:}; else vid=0bda; pid=$spec; fi + # Skip PIDs that aren't actually plugged in. + if ! lsusb -d "${vid}:${pid}" >/dev/null 2>&1; then + echo "-- ${vid}:${pid}: not present, skipping" + continue + fi + echo "== probing ${vid}:${pid} ==" + log=$(mktemp) + # rxdemo emits adapter.caps right after CreateRtlDevice, well before the RX + # loop; 6 s covers bring-up on the slowest chip. 2>/dev/null = pure events. + sudo timeout 6 env DEVOURER_VID="0x${vid}" DEVOURER_PID="0x${pid}" \ + DEVOURER_LOG_LEVEL=silent build/rxdemo >"$log" 2>/dev/null || true + cleanup + caps=$(grep -F '"ev":"adapter.caps"' "$log" | head -1) + if [ -z "$caps" ]; then + echo " FAIL: no adapter.caps event emitted" + fail=$((fail + 1)) + elif [ "$have_jq" = 1 ]; then + echo "$caps" | jq -c '{chip,names,gen,variant,transport,tx_chains,rx_chains,bw,tune_5g,char_5g,per_pkt_txpwr,narrowband,per_chain_rssi}' + else + echo " $caps" + fi + rm -f "$log" +done + +echo "=== adapter_caps_probe: $fail adapter(s) failed to report caps ===" +[ "$fail" -eq 0 ] diff --git a/tests/adapter_caps_selftest.cpp b/tests/adapter_caps_selftest.cpp new file mode 100644 index 0000000..91ff9fe --- /dev/null +++ b/tests/adapter_caps_selftest.cpp @@ -0,0 +1,104 @@ +/* Headless guard for the pure-logic parts of the adapter-capabilities API + * (src/AdapterCaps.h + the ActiveRxPaths classifier and extended freq<->chan + * mapping in src/RxQuality.h / src/ChannelFreq.h) — so a regression fails + * `ctest` instead of only surfacing on a radio. Prints the failing check and + * exits nonzero. */ +#include +#include + +#include "AdapterCaps.h" +#include "ChannelFreq.h" +#include "RxQuality.h" + +static int g_fail = 0; + +static void expect(const char *what, bool ok) { + if (ok) + return; + ++g_fail; + std::printf("FAIL: %s\n", what); +} + +int main() { + using namespace devourer; + + /* --- bandwidth mask per generation --- */ + const uint8_t ac = kBw20 | kBw40 | kBw80; + expect("J1 bw = 20/40/80", + bw_mask_for_generation(ChipGeneration::Jaguar1) == ac); + expect("J2 bw = 20/40/80", + bw_mask_for_generation(ChipGeneration::Jaguar2) == ac); + expect("J3 bw adds 5/10", + bw_mask_for_generation(ChipGeneration::Jaguar3) == + (ac | kBw5 | kBw10)); + expect("Unknown bw = 0", + bw_mask_for_generation(ChipGeneration::Unknown) == 0); + + /* --- generation names --- */ + expect("gen name jaguar3", + std::string_view(generation_name(ChipGeneration::Jaguar3)) == + "jaguar3"); + expect("gen name unknown", + std::string_view(generation_name(ChipGeneration::Unknown)) == + "unknown"); + + /* --- extended freq<->chan round-trip (Part B lifted the 5895 MHz cap) --- */ + expect("5080 MHz -> ch16", freq_to_chan(5080) == 16); + expect("ch16 -> 5080 MHz", chan_to_freq(16) == 5080); + expect("5900 MHz -> ch180", freq_to_chan(5900) == 180); + expect("ch180 -> 5900 MHz", chan_to_freq(180) == 5900); + expect("6165 MHz -> ch233", freq_to_chan(6165) == 233); + expect("6265 MHz -> ch253 (ceiling)", freq_to_chan(6265) == 253); + expect("6270 MHz dropped (past ceiling)", freq_to_chan(6270) == 0); + expect("legacy ch36 still 5180", chan_to_freq(36) == 5180); + expect("2.4G ch6 unaffected", freq_to_chan(2437) == 6); + + /* --- active-RX-path classification --- */ + { + /* Single strong chain, second at the noise floor -> only chain 0. */ + const int rssi[4] = {-40, -95, 0, 0}; + const bool samp[4] = {true, true, false, false}; + uint8_t mask = 0; + const uint8_t n = classify_active_paths(rssi, samp, 2, 20, &mask); + expect("one strong + one deaf -> mask 0x1", mask == 0x1 && n == 1); + } + { + /* Two balanced chains -> both active. */ + const int rssi[4] = {-50, -58, 0, 0}; + const bool samp[4] = {true, true, false, false}; + uint8_t mask = 0; + const uint8_t n = classify_active_paths(rssi, samp, 2, 20, &mask); + expect("two balanced -> mask 0x3", mask == 0x3 && n == 2); + } + { + /* No samples -> nothing active. */ + const int rssi[4] = {0, 0, 0, 0}; + const bool samp[4] = {false, false, false, false}; + uint8_t mask = 0xff; + const uint8_t n = classify_active_paths(rssi, samp, 2, 20, &mask); + expect("no samples -> mask 0", mask == 0 && n == 0); + } + + /* --- accumulator drain semantics --- */ + { + RxPathActivityAccumulator acc; + const uint8_t f1[4] = {70, 30, 0, 0}; /* raw; chain0 strong, chain1 weak */ + acc.add(f1, 2); + acc.add(f1, 2); + ActiveRxPaths s = acc.snapshot(20); + expect("acc valid after frames", s.valid && s.frames == 2); + expect("acc chain0 active", (s.active_mask & 0x1) != 0); + expect("acc chain1 inactive (40 raw below)", + (s.active_mask & 0x2) == 0); + /* Second snapshot drains to empty (delta semantics). */ + ActiveRxPaths s2 = acc.snapshot(20); + expect("acc drained on read", !s2.valid && s2.frames == 0); + } + + if (g_fail) { + std::printf("adapter_caps_selftest: %d failure(s)\n", g_fail); + return 1; + } + std::printf("adapter_caps_selftest: all checks passed\n"); + return 0; +} diff --git a/tests/extended_freq_smoke.sh b/tests/extended_freq_smoke.sh new file mode 100755 index 0000000..495ed51 --- /dev/null +++ b/tests/extended_freq_smoke.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Extended-frequency on-air smoke: does devourer actually TX+RX at an +# out-of-band 5 GHz channel the vendor tables don't cover? (Part B lifted the +# ChannelFreq.h 5895 MHz cap and opened the per-generation channel buckets.) +# +# Two devourer adapters: TX injects the canonical beacon (SA 57:42:75:05:d6:00) +# at CHAN; RX counts rx.txhit at the same channel. Kernel drivers can't tune the +# extended band, so devourer-vs-devourer is the authoritative cell. A control +# run at a standard channel (149) confirms the rig itself is sound. +# +# TX_PID=8812 RX_PID=c812 sudo -E tests/extended_freq_smoke.sh +# CHAN=180 CTRL=149 SECS=8 TX_PID=8812 RX_PID=c812 sudo -E tests/extended_freq_smoke.sh +# +# CHAN 180 = 5900 MHz (5000 + 5*180), ~75 MHz above the ch165 regulatory edge. +set -u +cd "$(dirname "$0")/.." + +TX_PID=${TX_PID:?set TX_PID=} +RX_PID=${RX_PID:?set RX_PID=} +TX_VID=${TX_VID:-0x0bda} +RX_VID=${RX_VID:-0x0bda} +CHAN=${CHAN:-180} # extended: 5900 MHz +CTRL=${CTRL:-149} # control: in-band 5745 MHz +SECS=${SECS:-8} + +cleanup() { sudo pkill -x txdemo 2>/dev/null; sudo pkill -x rxdemo 2>/dev/null; } +trap cleanup EXIT + +run_cell() { + local ch=$1 label=$2 + local rxlog txlog + rxlog=$(mktemp); txlog=$(mktemp) + echo "-- $label: ch$ch ($((5000 + 5 * ch)) MHz), ${SECS}s --" + sudo env DEVOURER_VID="$RX_VID" DEVOURER_PID="0x${RX_PID}" \ + DEVOURER_CHANNEL="$ch" DEVOURER_LOG_LEVEL=silent \ + build/rxdemo >"$rxlog" 2>/dev/null & + sleep 3 # RX bring-up before TX starts + sudo env DEVOURER_VID="$TX_VID" DEVOURER_PID="0x${TX_PID}" \ + DEVOURER_CHANNEL="$ch" DEVOURER_TX_GAP_US=2000 DEVOURER_LOG_LEVEL=silent \ + build/txdemo >"$txlog" 2>/dev/null & + sleep "$SECS" + cleanup + sleep 1 + # rx.txhit carries a cumulative "hits" count; take the last one seen. + local hits + hits=$(grep -F '"ev":"rx.txhit"' "$rxlog" | tail -1 | grep -oE '"hits":[0-9]+' | grep -oE '[0-9]+') + hits=${hits:-0} + echo " rx.txhit hits=$hits" + rm -f "$rxlog" "$txlog" + echo "$hits" +} + +echo "=== extended-freq smoke: TX ${TX_VID}:${TX_PID} -> RX ${RX_VID}:${RX_PID} ===" +ctrl_hits=$(run_cell "$CTRL" "control" | tail -1) +ext_hits=$(run_cell "$CHAN" "extended" | tail -1) + +echo "--- control ch$CTRL: $ctrl_hits hits | extended ch$CHAN: $ext_hits hits ---" +if [ "$ctrl_hits" -lt 1 ]; then + echo "INCONCLUSIVE: control cell saw no frames — rig/adapters suspect, not the extended band" + exit 2 +fi +if [ "$ext_hits" -ge 1 ]; then + echo "PASS: on-air TX+RX confirmed at extended ch$CHAN ($((5000 + 5 * CHAN)) MHz)" + exit 0 +fi +echo "FAIL: control worked but no frames at extended ch$CHAN — this chip may not lock there" +exit 1 From 27b77029c69fe9916cc23cde7428fd8379d1b07e Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:56:36 +0300 Subject: [PATCH 2/2] CI: build AdapterCapsSelftest in the mingw explicit target list The build-mingw job builds a hand-listed set of targets (its demo binaries aren't mingw targets), and every ctest-registered selftest binary must be in it or the Test step fails it as Not Run. Add the new AdapterCapsSelftest. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/cmake-multi-platform.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 8c54f36..556c625 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -149,6 +149,7 @@ jobs: ToneMaskSelftest BfReportDecodeSelftest SweepSpecSelftest TxPowerQuantSelftest LinkHealthSelftest TxCapsSelftest TxPktPwrSelftest RxQualitySelftest AdapterHealthSelftest LogEventSelftest + AdapterCapsSelftest - name: Test working-directory: build