Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cmake-multi-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ jobs:
ToneMaskSelftest BfReportDecodeSelftest SweepSpecSelftest
TxPowerQuantSelftest LinkHealthSelftest TxCapsSelftest TxPktPwrSelftest
RxQualitySelftest AdapterHealthSelftest LogEventSelftest
AdapterCapsSelftest

- name: Test
working-directory: build
Expand Down
15 changes: 15 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 2 additions & 0 deletions docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, `<stage>_us`…, total_us |
| `tx.fail` | L (send failure; regress.py keys on it) | {status, actual_len, timeout} or {rc, timeout} |
Expand All @@ -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 |

Expand Down
74 changes: 74 additions & 0 deletions examples/common/caps_event.h
Original file line number Diff line number Diff line change
@@ -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<size_t>(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 */
3 changes: 3 additions & 0 deletions examples/doctor/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
#include <thread>

#include "AdapterHealth.h"
#include "caps_event.h"
#include "RxPacket.h"
#include "SignalStop.h"
#include "UsbOpen.h"
Expand Down Expand Up @@ -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 */
Expand Down
17 changes: 17 additions & 0 deletions examples/rx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <libusb.h>

#include "BfReportDetect.h"
#include "caps_event.h"
#include "LinkHealth.h"
#include "RxPacket.h"
#include "SweepSpec.h"
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions examples/tx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#endif

#include "BfReportDetect.h"
#include "caps_event.h"
#include "ChannelFreq.h"
#include "RxPacket.h"
#include "SweepSpec.h"
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions examples/txpower/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
#include "TxPower.h"
#include "UsbOpen.h"
#include "WiFiDriver.h"
#include "caps_event.h"
#include "env_config.h"
#include "logger.h"

Expand Down Expand Up @@ -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)
Expand Down
127 changes: 127 additions & 0 deletions src/AdapterCaps.h
Original file line number Diff line number Diff line change
@@ -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 <cstdint>

#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 */
14 changes: 11 additions & 3 deletions src/ChannelFreq.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading
Loading