This commit is contained in:
2026-09-13 13:30:21 +08:00
commit a6bbd520cf
2744 changed files with 1598795 additions and 0 deletions
@@ -0,0 +1,102 @@
/* BfReportDetect — event-stream detector for beamforming self-sounding
* reports, shared by rxdemo (2-adapter rig: separate capture radio) and
* txdemo (single-radio: the sounder captures its own reports via
* StartRxLoop). Header-only so the demos stay identical consumers.
*
* A VHT/HT Compressed Beamforming report is a management Action frame —
* subtype 0xD0 (Action) or 0xE0 (Action No-Ack; CSI reports are sent No-Ack)
* — whose body begins with a category + action of VHT cat 0x15 act 0x00, or
* HT cat 0x07 act 0x00. When an armed beamformee replies to our NDPA+NDP the
* report's SA is the beamformee MAC, the decode-level confirmation the
* unassociated responder produced CSI.
*
* Emission goes through `bf_events` — a module-level EventSink pointer the
* hosting demo sets right after constructing its Logger
* (`devourer::bf::bf_events = &logger->events();`). Unset = detector inert.
*
* DEVOURER_BF_DETECT_REPORT modes:
* 1 `bf.report` summary event (Nc/Nr/BW/Ng, SA) per report
* 2 `bf.any` FC/category event for EVERY frame (subtype survey)
* 3 mode 1 + capped CSI-payload hexdump (`bf.csi`, first frames)
* 4 mode 1 + `bf.report_raw` full frame hex (for the decoder,
* tools/bf_report_decode.py) */
#ifndef BF_REPORT_DETECT_H
#define BF_REPORT_DETECT_H
#include <cstdio>
#include <cstdlib>
#include "Event.h"
#include "RxPacket.h"
namespace devourer::bf {
/* Event sink for every emission in this header. The demo points it at its
* Logger's sink before the RX loop starts; nullptr disables the detector's
* output entirely. */
inline devourer::EventSink *bf_events = nullptr;
inline void detect_report(const Packet &packet) {
const char *mode_s = std::getenv("DEVOURER_BF_DETECT_REPORT");
if (!mode_s || packet.Data.size() < 29 || bf_events == nullptr)
return;
const char mode = mode_s[0];
const uint8_t *d = packet.Data.data();
const uint8_t cat = d[24], act = d[25];
const bool vht = (cat == 0x15 && act == 0x00);
const bool ht = (cat == 0x07 && act == 0x00);
if (mode == '2') {
static int any = 0;
if (++any <= 4000) {
const unsigned fc16 = (unsigned(d[0]) << 8) | d[1];
devourer::Ev(*bf_events, "bf.any")
.hexf("fc", fc16, 4)
.hexf("cat", cat, 2)
.hexf("act", act, 2)
.f("crc", packet.RxAtrib.crc_err ? 1 : 0)
.f("len", packet.Data.size());
}
}
const uint8_t sub = d[0] & 0xF0;
if ((sub == 0xD0 || sub == 0xE0) && (vht || ht)) {
static int rpt = 0;
++rpt;
const uint8_t *mc = d + 26; /* VHT MIMO control field */
unsigned nc = (mc[0] & 0x07) + 1, nr = ((mc[0] >> 3) & 0x07) + 1;
unsigned chw = (mc[0] >> 6) & 0x03, ng = mc[1] & 0x03;
char sa[18];
std::snprintf(sa, sizeof(sa), "%02x:%02x:%02x:%02x:%02x:%02x", d[10],
d[11], d[12], d[13], d[14], d[15]);
devourer::Ev(*bf_events, "bf.report")
.f("kind", vht ? "VHT" : "HT")
.f("n", rpt)
.f("sa", sa)
.f("nc", nc)
.f("nr", nr)
.f("bw", chw)
.f("ng", ng)
.f("len", packet.Data.size());
if (mode == '3' && rpt <= 6) {
size_t off = 26 + 3; /* hdr(24)+cat+act+mimoctrl(3) */
/* Only the backends that append an FCS have four bytes to drop here;
* on MediaTek those bytes are CSI, not a checksum. */
const size_t fcs = packet.RxAtrib.fcs_present ? 4u : 0u;
size_t end = packet.Data.size() >= fcs ? packet.Data.size() - fcs : off;
size_t n = end > off ? end - off : 0;
devourer::Ev(*bf_events, "bf.csi")
.f("fcs", packet.RxAtrib.fcs_present ? 1 : 0)
.f("len", n)
.hex("csi", d + off, n < 40 ? n : 40);
}
if (mode == '4' && rpt <= 200) {
/* Full-frame hex — consumed by tools/bf_report_decode.py. */
devourer::Ev(*bf_events, "bf.report_raw")
.f("fcs", packet.RxAtrib.fcs_present ? 1 : 0)
.hex("frame", d, packet.Data.size());
}
}
}
} // namespace devourer::bf
#endif /* BF_REPORT_DETECT_H */
@@ -0,0 +1,102 @@
#ifndef DEVOURER_EXAMPLES_DEVICE_SESSION_H
#define DEVOURER_EXAMPLES_DEVICE_SESSION_H
/* DeviceSession — the teardown-order invariant for the demos, in one place.
*
* The caller owns libusb (see the architecture note in CLAUDE.md), which means
* the caller also owns the order things die in. That order is not arbitrary:
*
* 1. destroy the IRadio — quiesces TX (cancels and reaps the in-flight
* bulk-OUT URBs) and drops the transport, all
* while the libusb context is still valid;
* 2. libusb_release_interface — the chip is no longer being driven;
* 3. libusb_close — no transfer references the handle any more;
* 4. libusb_exit — nothing references the context any more.
*
* Getting this backwards is not a leak, it is a crash: a device destroyed
* after libusb_exit reaps its completions against a freed context and dies
* inside libusb, and only under enough TX load to keep URBs outstanding at
* exit (which is why it hides from low-duty runs).
*
* Holding all four in one object makes the order structural instead of a
* sequence every demo has to re-type at every early return: adopt each piece
* as it is acquired, then just `return`.
*
* Header-only and demo-local by design — the library never touches libusb
* lifetime. */
#include <memory>
#include <utility>
#include <libusb.h>
#include "IRadio.h"
#include "UsbDeviceLock.h"
#include "UsbOpen.h" /* find_wifi_interface — the interface the claim used */
#include "logger.h"
namespace devourer {
class DeviceSession {
public:
explicit DeviceSession(Logger_t logger) : _logger{std::move(logger)} {}
DeviceSession(const DeviceSession &) = delete;
DeviceSession &operator=(const DeviceSession &) = delete;
~DeviceSession() { close(); }
/* Adopt each resource as it is acquired, so an early return from any point
* in the bring-up unwinds whatever exists so far, in order. */
void adopt_context(libusb_context *ctx) { _ctx = ctx; }
/* `iface` defaults to whichever interface the claim helpers pick — the
* vendor bulk one, which is 2 on a composite RTL8822BU, not 0. Pass it
* explicitly only when the demo claimed a fixed interface itself. */
void adopt_handle(libusb_device_handle *handle, int iface = -1) {
_handle = handle;
_iface = iface >= 0 ? iface : find_wifi_interface(handle);
}
void adopt_lock(std::shared_ptr<UsbDeviceLock> lock) {
_lock = std::move(lock);
}
void adopt_device(std::unique_ptr<IRadio> dev) { _dev = std::move(dev); }
libusb_context *context() const { return _ctx; }
libusb_device_handle *handle() const { return _handle; }
IRadio *device() const { return _dev.get(); }
const std::shared_ptr<UsbDeviceLock> &lock() const { return _lock; }
/* Explicit teardown for demos that have work to do after the adapter is
* released (final statistics, a summary event). Idempotent; the destructor
* calls it. */
void close() {
_dev.reset();
if (_handle != nullptr) {
/* Tolerant: an adapter that already dropped off the bus (a TX-path
* wedge, an unplug) fails the release — log it, never abort. */
const int rc = libusb_release_interface(_handle, _iface);
if (rc != 0 && _logger)
_logger->info("libusb_release_interface rc={} (device already gone?)",
rc);
libusb_close(_handle);
_handle = nullptr;
}
if (_ctx != nullptr) {
libusb_exit(_ctx);
_ctx = nullptr;
}
_lock.reset();
}
private:
Logger_t _logger;
std::unique_ptr<IRadio> _dev;
std::shared_ptr<UsbDeviceLock> _lock;
libusb_device_handle *_handle = nullptr;
libusb_context *_ctx = nullptr;
int _iface = 0;
};
} /* namespace devourer */
#endif /* DEVOURER_EXAMPLES_DEVICE_SESSION_H */
+96
View File
@@ -0,0 +1,96 @@
/* Shared `adapter.caps` machine-event emitter for the demos.
*
* One place that serializes IRadio::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 CreateRadio (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 "IRadio.h"
namespace devourer {
inline void emit_adapter_caps(EventSink &sink, IRadio *dev) {
const AdapterCaps c = dev->GetAdapterCaps();
/* Supported channel widths as an MHz int array (kBw* -> MHz). */
int bw[6];
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;
if (c.bw_mask & kBw160) bw[nbw++] = 160;
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)
.f("txpwr_rate_diffs", c.txpwr.rate_diffs ? 1 : 0)
.f("txpwr_rate_diffs_hw", c.txpwr.rate_diffs_hw_table ? 1 : 0)
.f("txpwr_rate_diffs_measured", c.txpwr.rate_diffs_measured ? 1 : 0);
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);
/* FEC RX truth table (ldpc above is the TX side, TxCaps.ldpc_ok). */
ev.f("ldpc_rx_ht", c.ldpc_rx_ht ? 1 : 0)
.f("ldpc_rx_vht", c.ldpc_rx_vht ? 1 : 0)
.f("ldpc_rx_flag", c.ldpc_rx_flag ? 1 : 0)
.f("vht_2g4", c.vht_2g4_ok ? 1 : 0);
ev.f("per_pkt_txpwr", c.per_packet_txpower ? 1 : 0)
.f("per_pkt_txpwr_steps", c.per_pkt_txpwr_steps)
.f("per_pkt_txpwr_step_qdb", c.per_pkt_txpwr_step_qdb)
.f("per_pkt_txpwr_min_qdb", c.per_pkt_txpwr_min_qdb)
.f("per_pkt_txpwr_max_qdb", c.per_pkt_txpwr_max_qdb)
.f("per_pkt_txpwr_measured", c.per_pkt_txpwr_measured ? 1 : 0)
.f("narrowband", c.narrowband_ok ? 1 : 0)
.f("fastretune", c.fastretune_ok ? 1 : 0)
.f("ack_responder", c.ack_responder_ok ? 1 : 0)
.f("tx_retry_limit", c.tx_retry_limit_ok ? 1 : 0)
.f("he_er_su", c.he_er_su_ok ? 1 : 0)
.f("per_chain_rssi", c.per_chain_rssi ? 1 : 0)
.f("hw_rx_tsf", c.hw_rx_timestamp ? 1 : 0)
.f("hw_beacon_txtsf", c.hw_beacon_txtsf ? 1 : 0)
.f("xtal_cap_max", c.xtal_cap_max)
.f("xtal_cap_default", c.xtal_cap_default);
}
} // namespace devourer
#endif /* DEVOURER_CAPS_EVENT_H */
+313
View File
@@ -0,0 +1,313 @@
#include "env_config.h"
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include "RadiotapBuilder.h"
namespace {
/* Flag semantics: set and not "0". (The library's historical readers were a
* mix of presence-checks and =='1' checks; no script sets a flag to 0, so this
* covers both.) */
bool env_flag(const char *name) {
const char *e = std::getenv(name);
return e != nullptr && std::strcmp(e, "0") != 0;
}
const char *env_str(const char *name) { return std::getenv(name); }
/* Case-insensitive ASCII string equality (strcasecmp is POSIX-only). */
bool str_ieq(const char *a, const char *b) {
for (; *a && *b; ++a, ++b)
if (std::tolower(static_cast<unsigned char>(*a)) !=
std::tolower(static_cast<unsigned char>(*b)))
return false;
return *a == *b;
}
/* strtol with base auto-detect (0x-hex or decimal), matching the parse the
* library's readers used per knob. */
bool env_long(const char *name, long *out) {
const char *e = std::getenv(name);
if (!e || !*e)
return false;
*out = std::strtol(e, nullptr, 0);
return true;
}
/* DEVOURER_RX_MODE spelling -> RxMode (UsbTransport RX-ring strategy). Accepts
* hyphen or underscore; unrecognised falls back to the default async ring. */
devourer::RxMode parse_rx_mode(const char *s) {
if (str_ieq(s, "sync"))
return devourer::RxMode::Sync;
if (str_ieq(s, "reorder-pool") || str_ieq(s, "reorder_pool") ||
str_ieq(s, "pool"))
return devourer::RxMode::ReorderPool;
if (str_ieq(s, "spsc-fat") || str_ieq(s, "spsc_fat") || str_ieq(s, "spsc"))
return devourer::RxMode::SpscFat;
if (str_ieq(s, "decoupled"))
return devourer::RxMode::Decoupled;
return devourer::RxMode::Async;
}
} // namespace
devourer::DeviceConfig devourer_config_from_env() {
devourer::DeviceConfig cfg;
long v = 0;
/* ---- rx ---- */
cfg.rx.keep_corrupted = env_flag("DEVOURER_RX_KEEP_CORRUPTED");
cfg.rx.enable_with_tx = env_str("DEVOURER_TX_WITH_RX") != nullptr;
if (const char *e = env_str("DEVOURER_RX_CSI_MASK"))
cfg.rx.csi_mask = e;
if (const char *e = env_str("DEVOURER_RX_NBI"))
cfg.rx.nbi = e;
if (const char *e = env_str("DEVOURER_RX_PATHS"))
cfg.rx.path_spec = e;
if (env_long("DEVOURER_RX_URBS", &v))
cfg.rx.urbs = static_cast<int>(v);
if (env_long("DEVOURER_RX_URB_BYTES", &v))
cfg.rx.urb_bytes = static_cast<int>(v);
if (const char *e = env_str("DEVOURER_RX_MODE"))
cfg.rx.rx_mode = parse_rx_mode(e);
if (env_long("DEVOURER_RX_POOL_SPARE", &v))
cfg.rx.pool_spare = static_cast<int>(v);
if (const char *e = env_str("DEVOURER_RX_POOL_EXHAUST")) {
if (str_ieq(e, "drop"))
cfg.rx.pool_exhaust = devourer::PoolExhaust::Drop;
else if (str_ieq(e, "backpressure") || str_ieq(e, "bp"))
cfg.rx.pool_exhaust = devourer::PoolExhaust::Backpressure;
}
if (env_long("DEVOURER_RX_RING_MS", &v))
cfg.rx.ring_ms = static_cast<int>(v);
cfg.rx.phy_status_8821c = !env_flag("DEVOURER_8821C_NO_PHYST");
cfg.rx.abs_noise_floor = env_flag("DEVOURER_RX_NOISE_FLOOR");
if (env_long("DEVOURER_IGI", &v))
cfg.rx.igi = static_cast<uint8_t>(v & 0x7f);
if (const char *e = env_str("DEVOURER_ACK_RESPONDER"))
cfg.rx.ack_responder = devourer::parse_mac(e);
/* ---- tx ---- */
if (env_long("DEVOURER_TX_EP", &v))
cfg.tx.ep = static_cast<uint8_t>(v);
if (env_long("DEVOURER_TX_TIMEOUT_MS", &v))
cfg.tx.timeout_ms = static_cast<unsigned>(v);
cfg.tx.legacy_8812_desc = env_flag("DEVOURER_TX_LEGACY_8812_DESC");
if (env_long("DEVOURER_TX_PWR", &v))
cfg.tx.power_index = static_cast<int>(v & 0x3f);
if (env_long("DEVOURER_TX_RF_BW", &v))
cfg.tx.rf_bw = static_cast<uint8_t>(v & 0x3);
cfg.tx.cw_tone = env_flag("DEVOURER_CW_TONE");
if (env_long("DEVOURER_CW_TONE_GAIN", &v))
cfg.tx.cw_tone_gain = static_cast<uint8_t>(v) & 0x1F;
if (env_long("DEVOURER_TX_USB_AGG", &v) && v > 0)
cfg.tx.usb_agg_max = static_cast<unsigned>(v);
if (env_long("DEVOURER_TX_REPORT", &v)) /* sampling divisor N, 0..255 */
cfg.tx.report = static_cast<int>(v < 0 ? 0 : v > 255 ? 255 : v);
if (const char *e = env_str("DEVOURER_TX_AMPDU_MODE")) {
devourer::AmpduMode m;
if (devourer::parse_ampdu_mode(e, m)) {
cfg.tx.ampdu = m;
} else {
/* A silently-ignored spec means a bench that thinks it measured
* aggregation and didn't — fail loudly like RETRY_FALLBACK below. */
std::fprintf(stderr,
"devourer [W] DEVOURER_TX_AMPDU_MODE='%s' unparsable — "
"A-MPDU stays off\n", e);
std::fflush(stderr);
}
}
if (env_long("DEVOURER_ACK_TIMEOUT_US", &v) && v >= 1)
/* 1..255; out-of-range low keeps the library default (a 0 collapsing
* to 1 us would write off every frame). */
cfg.tx.ack_timeout_us = static_cast<int>(v > 255 ? 255 : v);
if (env_long("DEVOURER_TX_RETRY_LIMIT", &v))
cfg.tx.retry_limit = static_cast<int>(v < 0 ? 0 : (v > 63 ? 63 : v));
if (const char *e = env_str("DEVOURER_TX_RETRY_FALLBACK")) {
if (str_ieq(e, "off")) {
cfg.tx.retry_fallback = devourer::RetryFallback::Off;
} else {
/* A floor form was measured and rejected — the fw reinterprets
* DATA_RTY_LOWEST_RATE inside the RA-group rate space (see the
* RetryFallback enum note in DeviceConfig.h). */
std::fprintf(stderr,
"devourer [W] DEVOURER_TX_RETRY_FALLBACK='%s' unsupported "
"(only \"off\") — keeping the firmware ladder\n", e);
std::fflush(stderr); /* the diagnostic plane is per-line flushed */
}
}
/* ---- bf ---- */
if (const char *snd = env_str("DEVOURER_BF_ARM_SOUNDER")) {
cfg.bf.arm_sounder = true;
/* "aa:bb:..:ff" also programs the self-MAC; a bare "1" arms only. */
cfg.bf.sounder_self_mac = devourer::parse_mac(snd);
}
if (const char *e = env_str("DEVOURER_BF_ARM_BFEE"))
cfg.bf.beamformee_of = devourer::parse_mac(e);
cfg.bf.mu = env_flag("DEVOURER_BF_ARM_BFEE_MU");
if (const char *e = env_str("DEVOURER_BF_TXBF"))
cfg.bf.txbf_peer = devourer::parse_mac(e);
if (const char *e = env_str("DEVOURER_TX_NDPA")) {
int p = std::atoi(e);
cfg.bf.ndpa_period = p > 0 ? p : 1;
}
/* ---- MediaTek MT7612U ---- */
/* Folded in here rather than read inside the backend, so neither the C
* library nor the device class consults ambient process state. */
if (const char *e = env_str("DEVOURER_MT7612U_FW_DIR"))
cfg.mt7612u.firmware_dir = std::string(e);
/* ---- tuning ---- */
/* Defaults ON, so this reads the negation: only an explicit 0 disables it. */
if (const char *e = env_str("DEVOURER_TEARDOWN_POWER_DOWN"))
cfg.tuning.teardown_power_down = (std::atoi(e) != 0);
cfg.tuning.skip_iqk = env_flag("DEVOURER_SKIP_IQK");
cfg.tuning.force_iqk = env_flag("DEVOURER_FORCE_IQK");
cfg.tuning.disable_iqk = env_flag("DEVOURER_DISABLE_IQK");
cfg.tuning.skip_txpwr = env_flag("DEVOURER_SKIP_TXPWR");
cfg.tuning.skip_txgapk = env_flag("DEVOURER_SKIP_TXGAPK");
cfg.tuning.skip_trx_reassert = env_flag("DEVOURER_SKIP_TRX_REASSERT");
cfg.tuning.skip_rfe_init = env_flag("DEVOURER_SKIP_RFEINIT");
cfg.tuning.skip_coex = env_flag("DEVOURER_SKIP_COEX");
cfg.tuning.skip_dig = env_flag("DEVOURER_SKIP_DIG");
/* Default-on knob: unset = tracking on; only "0" disables it. */
if (const char *e = env_str("DEVOURER_THERMAL_TRACK"))
cfg.tuning.thermal_track = std::strcmp(e, "0") != 0;
cfg.tuning.disable_cca = env_flag("DEVOURER_DIS_CCA");
if (env_long("DEVOURER_FASTRETUNE_FW", &v) && v >= 0)
cfg.tuning.fastretune_fw = static_cast<int>(v);
if (env_long("DEVOURER_KFR_OFLD", &v) && v >= 0)
cfg.tuning.kestrel_fastretune_ofld = static_cast<int>(v);
if (env_long("DEVOURER_FW_TABLE_OFLD", &v) && v >= 0)
cfg.tuning.fw_table_offload = static_cast<int>(v);
/* Jaguar3 per-packet power-bank step size (qdB per 0x1e70 offset-index
* step; default 4 = 1 dB) — bench slope-calibration override. */
if (env_long("DEVOURER_TXPKT_STEP_QDB", &v) && v > 0)
cfg.tuning.txpkt_step_qdb = static_cast<int>(v);
if (env_long("DEVOURER_RFE", &v))
cfg.tuning.rfe_type = static_cast<uint8_t>(v);
/* Raw codes — each backend masks to its own field width (J2/J3 3-bit,
* RTL8733B 4-bit ADC at 0x9f0[3:0], whose default codes 0xa/0xb a 0x7
* mask would silently corrupt). */
if (env_long("DEVOURER_NB_DAC", &v))
cfg.tuning.nb_dac = static_cast<uint8_t>(v & 0xf);
if (env_long("DEVOURER_NB_ADC", &v))
cfg.tuning.nb_adc = static_cast<uint8_t>(v & 0xf);
if (env_long("DEVOURER_XTAL_CAP", &v))
cfg.tuning.xtal_cap = static_cast<uint8_t>(v & 0x7f);
cfg.tuning.cfo_track = env_flag("DEVOURER_CFO_TRACK");
if (const char *e = env_str("DEVOURER_REGULATION")) {
if (str_ieq(e, "ETSI"))
cfg.tuning.regulation = devourer::Regulation::ETSI;
else if (str_ieq(e, "MKK"))
cfg.tuning.regulation = devourer::Regulation::MKK;
else if (str_ieq(e, "WW"))
cfg.tuning.regulation = devourer::Regulation::WW;
else
cfg.tuning.regulation = devourer::Regulation::FCC;
}
cfg.tuning.txpwr_by_rate = env_flag("DEVOURER_ENABLE_TXPWR_BY_RATE");
cfg.tuning.phydm_watchdog = env_flag("DEVOURER_PHYDM_WATCHDOG");
if (const char *e = env_str("DEVOURER_8814_FWDL");
e && std::strcmp(e, "rtw88") == 0)
cfg.tuning.fwdl_8814 = devourer::Fwdl8814Path::Rtw88;
if (env_long("DEVOURER_8814_FWDL_CHUNK", &v))
cfg.tuning.fwdl_8814_chunk = static_cast<uint32_t>(v);
if (const char *e = env_str("DEVOURER_DPDT_MODE")) {
if (str_ieq(e, "legacy"))
cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::Legacy;
else if (str_ieq(e, "bit24"))
cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::Bit24;
else if (str_ieq(e, "skip"))
cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::Skip;
else
cfg.tuning.dpdt_8822e = devourer::Dpdt8822eMode::EfemPinmux;
}
/* ---- debug ---- */
cfg.debug.dump_canary = env_flag("DEVOURER_DUMP_CANARY");
cfg.debug.bb_dump = env_flag("DEVOURER_BB_DUMP");
cfg.debug.efuse_dump = env_flag("DEVOURER_EFUSE_DUMP");
cfg.debug.log_writes = env_flag("DEVOURER_LOG_WRITES");
cfg.debug.log_txpwr = env_flag("DEVOURER_LOG_TXPWR");
cfg.debug.kestrel_fw_log = env_flag("DEVOURER_KESTREL_FWLOG");
cfg.debug.kestrel_cca_on = env_flag("DEVOURER_KESTREL_CCA_ON");
cfg.debug.kestrel_trigger_f2p = env_flag("DEVOURER_KESTREL_TRIGGER_F2P");
if (const char *e = env_str("DEVOURER_REPLAY_WSEQ"))
cfg.debug.replay_wseq = e;
if (env_long("DEVOURER_TX_QSEL", &v))
cfg.debug.tx_qsel = static_cast<uint8_t>(v & 0x1f);
if (env_long("DEVOURER_TX_RATEID", &v))
cfg.debug.tx_rateid = static_cast<uint8_t>(v & 0x1f);
/* "max[/density[/rty]]" — A-MPDU spike descriptor overrides. */
if (const char *e = env_str("DEVOURER_TX_AMPDU")) {
char *end = nullptr;
long maxn = std::strtol(e, &end, 0);
if (maxn > 0) {
cfg.debug.tx_ampdu_max = static_cast<uint8_t>(maxn & 0x1f);
if (end && *end == '/') {
cfg.debug.tx_ampdu_density =
static_cast<uint8_t>(std::strtol(end + 1, &end, 0) & 0x7);
if (end && *end == '/')
cfg.debug.tx_ampdu_rty =
static_cast<uint8_t>(std::strtol(end + 1, nullptr, 0) & 0x3f);
}
}
}
cfg.debug.hop_prof = env_flag("DEVOURER_HOP_PROF");
cfg.debug.gaintab_dbg = env_flag("DEVOURER_GAINTAB_DBG");
/* ---- usb ---- */
if (const char *e = env_str("TMPDIR"); e && *e)
cfg.usb.lock_dir = e;
if (env_str("DEVOURER_RX_ZEROCOPY")) /* default true; =0 forces the heap path */
cfg.usb.rx_zerocopy = env_flag("DEVOURER_RX_ZEROCOPY");
return cfg;
}
devourer::TxMode devourer_tx_mode_from_env() {
const char *raw = std::getenv("DEVOURER_TX_RATE");
return devourer::parse_tx_mode_str(raw ? raw : "");
}
void apply_logging_env(Logger &logger) {
if (const char *e = std::getenv("DEVOURER_LOG_LEVEL")) {
if (str_ieq(e, "trace"))
logger.set_level(Logger::Level::Trace);
else if (str_ieq(e, "debug"))
logger.set_level(Logger::Level::Debug);
else if (str_ieq(e, "info"))
logger.set_level(Logger::Level::Info);
else if (str_ieq(e, "warn"))
logger.set_level(Logger::Level::Warn);
else if (str_ieq(e, "error"))
logger.set_level(Logger::Level::Error);
else if (str_ieq(e, "silent"))
logger.set_level(Logger::Level::Silent);
else
std::fprintf(stderr, "devourer [W] DEVOURER_LOG_LEVEL='%s' unknown — "
"keeping default\n", e);
}
const auto flush = env_flag("DEVOURER_EVENT_FLUSH") ||
std::getenv("DEVOURER_EVENT_FLUSH") == nullptr
? devourer::EventSink::FlushPolicy::EveryLine
: devourer::EventSink::FlushPolicy::Never;
if (const char *e = std::getenv("DEVOURER_EVENTS")) {
if (str_ieq(e, "off"))
logger.events().disable();
else if (str_ieq(e, "stderr"))
logger.events().configure(stderr, flush);
else
logger.events().configure(stdout, flush);
} else {
logger.events().configure(stdout, flush);
}
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
/* DEVOURER_* environment-variable interface of the example binaries.
*
* The library is configured through devourer::DeviceConfig (see
* src/DeviceConfig.h) and runtime setters on IRadio; it reads no env vars.
* The demos — and the test scripts driving them — speak env vars, and this
* translator is where that mapping lives: every library-level DEVOURER_* var
* becomes a DeviceConfig field (devourer_config_from_env). Demo-local vars
* (DEVOURER_PID, DEVOURER_CHANNEL, ...) stay in each demo's own code. */
#include "DeviceConfig.h"
#include "TxMode.h"
#include "logger.h"
/* Every DeviceConfig-backed DEVOURER_* var -> a populated DeviceConfig.
* See env_config.cpp for the full mapping table. */
devourer::DeviceConfig devourer_config_from_env();
/* DEVOURER_TX_RATE parsed to a TxMode (unset -> the 6M-legacy default). */
devourer::TxMode devourer_tx_mode_from_env();
/* Logging env -> Logger configuration (docs/logging.md). Call once at
* main() start, before any threads. These configure the Logger object, not
* DeviceConfig — the library itself still reads no env:
* DEVOURER_LOG_LEVEL=trace|debug|info|warn|error|silent
* diagnostic verbosity on stderr (default debug).
* DEVOURER_EVENTS=stdout|stderr|off
* destination of the JSONL machine event stream (default stdout).
* DEVOURER_EVENT_FLUSH=0
* drop the per-event fflush (max-rate benches; default flush-per-line
* so piped consumers never stall on libc buffering). */
void apply_logging_env(Logger &logger);
+146
View File
@@ -0,0 +1,146 @@
// Shared stdin framing for the stdin-driven stream demos (streamtx,
// duplex) and their headless regression self-test
// (StreamStdinSelftest).
//
// Centralises the two things that have to stay correct on every Windows
// toolchain, so there is a single source of truth instead of one copy per
// demo:
//
// 1. set_stdin_binary() — put stdin in binary mode so a 0x1A (Ctrl-Z, which
// text-mode stdin treats as EOF) or a CRLF byte in the binary
// <u32_le len><PSDU> stream isn't translated away. Gated on _WIN32, NOT
// _MSC_VER: mingw/GCC defines _WIN32 but not _MSC_VER, yet still ships
// _setmode. A _MSC_VER gate silently leaves mingw stdin in TEXT mode and
// truncates the first PSDU ("short read on stdin (76/269)") before a
// single frame is transmitted.
//
// 2. read_exact() — the byte reader, returning a tri-state so each caller
// keeps its own short-read policy (the TX demo aborts on a truncated
// record; the duplex demo just stops its TX thread and lets RX run on).
//
// 3. read_record() — one whole <u32_le len><body> record, which is what
// every caller actually wanted. The little-endian assembly and the
// zero/oversize check were re-typed in four demos before this existed,
// and they had already drifted. Callers that need the length word before
// the body — the duplex demo escapes to a control TLV on its top bit —
// compose read_length() and read_body() instead.
//
// The result states are deliberately finer than "it failed": a producer that
// closed cleanly between records is an ordinary end of run, one that closed
// with a length word already written has lost a record, and a length out of
// range is a producer bug rather than a stream end. Each demo maps them to its
// own policy; none of them has to re-derive them.
//
// StreamStdinSelftest + tests/stream_stdin_test.cmake exercise this header
// headlessly (no libusb, no hardware), so a regression in the _WIN32 gate
// fails CI on the mingw job instead of only surfacing on a real radio.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <vector>
#if defined(_WIN32)
#include <io.h>
#include <fcntl.h>
#endif
namespace stream_stdin {
// Put stdin into binary mode. No-op off Windows (POSIX has no text mode).
inline void set_stdin_binary() {
#if defined(_WIN32)
_setmode(_fileno(stdin), _O_BINARY);
#endif
}
// Put stdout into binary mode. Only the self-test's --gen path needs this, but
// it lives here so all the toolchain-gated _setmode logic stays in one place.
inline void set_stdout_binary() {
#if defined(_WIN32)
_setmode(_fileno(stdout), _O_BINARY);
#endif
}
enum class ReadResult {
Ok, // got all n bytes
Eof, // clean stream close: 0 bytes read with EOF before any byte
Short, // stream ended mid-record (truncation)
};
// Read exactly n bytes from f into buf.
inline ReadResult read_exact(std::FILE *f, void *buf, std::size_t n) {
std::size_t got = 0;
auto *p = static_cast<std::uint8_t *>(buf);
while (got < n) {
std::size_t r = std::fread(p + got, 1, n - got, f);
if (r == 0) {
if (got == 0 && std::feof(f)) return ReadResult::Eof;
return ReadResult::Short;
}
got += r;
}
return ReadResult::Ok;
}
// Read the 4-byte little-endian length prefix. `len` is untouched unless Ok.
inline ReadResult read_length(std::FILE *f, std::uint32_t &len) {
std::uint8_t b[4];
const ReadResult r = read_exact(f, b, sizeof(b));
if (r == ReadResult::Ok)
len = static_cast<std::uint32_t>(b[0]) |
(static_cast<std::uint32_t>(b[1]) << 8) |
(static_cast<std::uint32_t>(b[2]) << 16) |
(static_cast<std::uint32_t>(b[3]) << 24);
return r;
}
// Read `n` body bytes into `out`, sizing it to match. A zero-length body is Ok
// and leaves `out` empty — callers that forbid one check the length first.
inline ReadResult read_body(std::FILE *f, std::vector<std::uint8_t> &out,
std::size_t n) {
out.resize(n);
return n ? read_exact(f, out.data(), n) : ReadResult::Ok;
}
enum class RecordResult {
Ok, // a complete record is in `out`
Eof, // clean close: nothing at all where a record would have started
Short, // the stream ended part-way through a record
EofMidBody, // the length word arrived, then the stream closed before a byte
// of its body — one record lost, cleanly
BadLength, // zero, or larger than the caller's bound
};
// Read one whole <u32_le len><body> record. `max` bounds the body. When
// `raw_len` is given it receives the length word verbatim even on BadLength,
// so a caller with its own convention in the high bits can inspect it.
inline RecordResult read_record(std::FILE *f, std::vector<std::uint8_t> &out,
std::size_t max,
std::uint32_t *raw_len = nullptr) {
std::uint32_t len = 0;
switch (read_length(f, len)) {
case ReadResult::Eof:
return RecordResult::Eof;
case ReadResult::Short:
return RecordResult::Short;
case ReadResult::Ok:
break;
}
if (raw_len)
*raw_len = len;
if (len == 0 || len > max)
return RecordResult::BadLength;
switch (read_body(f, out, len)) {
case ReadResult::Ok:
return RecordResult::Ok;
case ReadResult::Eof:
return RecordResult::EofMidBody;
case ReadResult::Short:
return RecordResult::Short;
}
return RecordResult::Short; // unreachable; keeps every toolchain quiet
}
} // namespace stream_stdin
@@ -0,0 +1,230 @@
// StreamStdinSelftest — headless regression test for examples/common/stream_stdin.h.
//
// The stdin-driven stream demos (streamtx, duplex) read a binary
// <u32_le len><PSDU> stream from stdin. On Windows that only works if stdin is
// put into binary mode; the gate has to be `_WIN32` (not `_MSC_VER`) so it also
// fires under mingw/GCC. A regression there is invisible to a build-only CI job
// — it compiles fine and only corrupts bytes at runtime — so this binary
// exercises the exact set_stdin_binary() + read_record() path the demos use,
// with no libusb and no radio. It also pins what each record state means,
// since the four demos map those states onto four different policies.
//
// StreamStdinSelftest --gen writes the canonical stream to stdout (binary)
// StreamStdinSelftest reads that stream from stdin and round-trips it
//
// tests/stream_stdin_test.cmake pipes the first into the second. The canonical
// records deliberately contain 0x1A (Ctrl-Z = EOF to a text-mode read), 0x0D
// and 0x0A, so any text-mode translation truncates or mangles the stream and the
// reader reports FAIL with a non-zero exit.
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
#include "stream_stdin.h"
// Canonical self-test stream. --gen and the reader share this table, so a
// byte-for-byte mismatch on read means binary mode is broken. Total = 5 records,
// 20 body bytes (5+3+4+1+7) — kept in lockstep with the expected string in
// tests/stream_stdin_test.cmake.
static const std::vector<std::vector<uint8_t>> &canonical_records() {
static const std::vector<std::vector<uint8_t>> recs = {
{0x1A, 0x0D, 0x0A, 0x00, 0xFF},
{0x41, 0x1A, 0x42},
{0x0D, 0x0A, 0x0D, 0x0A},
{0x1A},
{0x00, 0x1A, 0x0D, 0x0A, 0x1A, 0x7F, 0x80},
};
return recs;
}
// Bound for read_record. Comfortably above the canonical records, so a
// BadLength here means the stream desynced, never that the cap was too tight.
static const std::size_t kMaxRecord = 4096;
static void put_u32_le(uint8_t *p, uint32_t v) {
p[0] = static_cast<uint8_t>(v & 0xFF);
p[1] = static_cast<uint8_t>((v >> 8) & 0xFF);
p[2] = static_cast<uint8_t>((v >> 16) & 0xFF);
p[3] = static_cast<uint8_t>((v >> 24) & 0xFF);
}
static int do_gen() {
stream_stdin::set_stdout_binary();
for (const auto &rec : canonical_records()) {
uint8_t len_bytes[4];
put_u32_le(len_bytes, static_cast<uint32_t>(rec.size()));
std::fwrite(len_bytes, 1, sizeof(len_bytes), stdout);
if (!rec.empty()) std::fwrite(rec.data(), 1, rec.size(), stdout);
}
std::fflush(stdout);
return 0;
}
static int do_check() {
stream_stdin::set_stdin_binary();
const auto &expected = canonical_records();
size_t got_records = 0, got_bytes = 0;
for (const auto &exp : expected) {
// read_record is the composed path every demo now uses: the length
// assembly and the range check live in the header rather than four times
// over, so this exercises what the demos actually run.
std::vector<uint8_t> body;
uint32_t len = 0;
const auto r = stream_stdin::read_record(stdin, body, kMaxRecord, &len);
if (r == stream_stdin::RecordResult::BadLength) {
std::fprintf(stderr,
"stream_stdin_selftest: FAIL — record %zu length %u out of "
"range (stream desynced; likely CRLF/Ctrl-Z translation)\n",
got_records, len);
return 3;
}
if (r != stream_stdin::RecordResult::Ok) {
std::fprintf(stderr,
"stream_stdin_selftest: FAIL — record %zu returned %d "
"(binary stdin likely broken: a 0x1A in a prior record was "
"read as EOF)\n",
got_records, static_cast<int>(r));
return 2;
}
if (len != exp.size()) {
std::fprintf(stderr,
"stream_stdin_selftest: FAIL — record %zu length %u != "
"expected %zu (stream desynced; likely CRLF/Ctrl-Z "
"translation)\n",
got_records, len, exp.size());
return 4;
}
if (body != exp) {
std::fprintf(stderr,
"stream_stdin_selftest: FAIL — record %zu body differs from "
"source (text-mode translation corrupted the stream)\n",
got_records);
return 5;
}
++got_records;
got_bytes += len;
}
// Confirm clean EOF right after the last record — no trailing corruption.
uint8_t extra;
if (stream_stdin::read_exact(stdin, &extra, 1) != stream_stdin::ReadResult::Eof) {
std::fprintf(stderr,
"stream_stdin_selftest: FAIL — expected EOF after %zu records "
"but more bytes followed\n",
got_records);
return 6;
}
std::fprintf(stdout, "stream_stdin_selftest: records=%zu bytes=%zu OK\n",
got_records, got_bytes);
std::fflush(stdout);
return 0;
}
// Every demo maps read_record's states onto its own policy — streamtx warns on
// one and exits 2 on another, txdemo counts a lost shard, duplex stops its TX
// thread and leaves RX running. So the states have to mean exactly what they
// say. Driven through tmpfile() rather than the pipe, since these are the cases
// a well-formed stream never produces.
static int check_state(const char *what, const std::vector<uint8_t> &bytes,
std::size_t max, stream_stdin::RecordResult want,
uint32_t want_len) {
std::FILE *f = std::tmpfile();
if (!f) {
std::fprintf(stderr, "stream_stdin_selftest: tmpfile() failed\n");
return 7;
}
if (!bytes.empty()) std::fwrite(bytes.data(), 1, bytes.size(), f);
std::rewind(f);
std::vector<uint8_t> body;
uint32_t len = 0;
const auto got = stream_stdin::read_record(f, body, max, &len);
std::fclose(f);
if (got != want) {
std::fprintf(stderr,
"stream_stdin_selftest: FAIL — %s returned %d, expected %d\n",
what, static_cast<int>(got), static_cast<int>(want));
return 7;
}
if (want_len && len != want_len) {
std::fprintf(stderr,
"stream_stdin_selftest: FAIL — %s reported length %u, "
"expected %u\n", what, len, want_len);
return 7;
}
return 0;
}
static int do_states() {
using R = stream_stdin::RecordResult;
auto rec = [](uint32_t len, std::size_t body_bytes) {
std::vector<uint8_t> v(4);
put_u32_le(v.data(), len);
v.insert(v.end(), body_bytes, 0x1A); // 0x1A: EOF to a text-mode read
return v;
};
struct Case {
const char *what;
std::vector<uint8_t> bytes;
std::size_t max;
R want;
uint32_t want_len;
};
const std::vector<Case> cases = {
{"a complete record", rec(3, 3), 16, R::Ok, 3},
{"nothing at all", {}, 16, R::Eof, 0},
{"a truncated length prefix", {0x05, 0x00}, 16, R::Short, 0},
{"a length with no body at all", rec(4, 0), 16, R::EofMidBody, 4},
{"a body cut short", rec(8, 3), 16, R::Short, 8},
{"a zero length", rec(0, 0), 16, R::BadLength, 0},
{"a length past the bound", rec(99, 0), 16, R::BadLength, 99},
};
for (const auto &c : cases) {
const int rc = check_state(c.what, c.bytes, c.max, c.want, c.want_len);
if (rc) return rc;
}
/* The duplex demo escapes to a control TLV on the length's top bit, and
* bounds that body at 256 rather than at its PSDU maximum — so it reads the
* length, masks it, and only then reads the body. That composition is the
* reason read_length and read_body are public at all, and nothing else
* covers it: a record whose length has the top bit set must arrive
* verbatim, and must not be mistaken for an oversize PSDU. */
{
std::vector<uint8_t> bytes(4);
const uint32_t escaped = 0x80000000u | 3u;
put_u32_le(bytes.data(), escaped);
const std::vector<uint8_t> want = {0x01, 0x1A, 0x0D};
bytes.insert(bytes.end(), want.begin(), want.end());
std::FILE *f = std::tmpfile();
if (!f) {
std::fprintf(stderr, "stream_stdin_selftest: tmpfile() failed\n");
return 7;
}
std::fwrite(bytes.data(), 1, bytes.size(), f);
std::rewind(f);
uint32_t len = 0;
std::vector<uint8_t> body;
const bool ok =
stream_stdin::read_length(f, len) == stream_stdin::ReadResult::Ok &&
(len & 0x80000000u) != 0 &&
stream_stdin::read_body(f, body, len & 0x7fffffffu) ==
stream_stdin::ReadResult::Ok &&
body == want;
std::fclose(f);
if (!ok) {
std::fprintf(stderr, "stream_stdin_selftest: FAIL — the control-opcode "
"escape (top length bit) did not round-trip\n");
return 7;
}
}
std::fprintf(stdout, "stream_stdin_selftest: %zu record states OK\n",
cases.size());
return 0;
}
int main(int argc, char **argv) {
if (argc > 1 && std::strcmp(argv[1], "--gen") == 0) return do_gen();
if (const int rc = do_states()) return rc;
return do_check();
}
+135
View File
@@ -0,0 +1,135 @@
#include "usb_select.h"
#include <chrono>
#include <cstdlib>
#include <thread>
/* Fill the pick descriptor from an opened handle (best-effort — identity
* logging must never fail an open that succeeded). */
static void describe(libusb_device_handle *h, uint16_t vid, uint16_t pid,
UsbPick *picked) {
if (picked == nullptr || h == nullptr)
return;
picked->vid = vid;
picked->pid = pid;
libusb_device *dev = libusb_get_device(h);
if (dev == nullptr)
return;
picked->bus = libusb_get_bus_number(dev);
uint8_t ports[8];
const int pc = libusb_get_port_numbers(dev, ports, sizeof(ports));
picked->port.clear();
for (int p = 0; p < pc; ++p)
picked->port += (picked->port.empty() ? "" : ".") + std::to_string(ports[p]);
picked->speed = libusb_get_device_speed(dev);
}
libusb_device_handle *
open_selected_usb(libusb_context *ctx, const std::shared_ptr<Logger> &logger,
const uint16_t *default_pids, size_t n_default_pids,
UsbPick *picked) {
/* DEVOURER_PID env var (hex, e.g. "0x8813") restricts the open loop to a
* single PID. Useful when multiple Realtek adapters are plugged.
* DEVOURER_VID overrides the VID (default 0x0bda Realtek) — needed to reach
* OEM-rebadged Jaguar dongles like the TP-Link Archer T2U Plus (2357:0120). */
const char *pid_env = std::getenv("DEVOURER_PID");
uint16_t target_pid = 0;
if (pid_env != nullptr) {
target_pid = static_cast<uint16_t>(std::strtoul(pid_env, nullptr, 0));
logger->info("DEVOURER_PID={:04x} (limiting to this PID)", target_pid);
}
uint16_t target_vid = 0x0bda;
if (const char *vid_env = std::getenv("DEVOURER_VID")) {
target_vid = static_cast<uint16_t>(std::strtoul(vid_env, nullptr, 0));
logger->info("DEVOURER_VID={:04x} (overriding default VID)", target_vid);
}
libusb_device_handle *dev_handle = nullptr;
/* DEVOURER_USB_BUS (+ optional DEVOURER_USB_PORT) select a specific device by
* USB topology when several share one VID:PID and even the serial — e.g. two
* RTL8814AU dongles (CF-938AC vs CF-960AC) that enumerate identically, so only
* the bus/port tells them apart. DEVOURER_USB_PORT is the dotted libusb port
* path (as in sysfs `devpath` / `lsusb -t`, e.g. "2.3.2"). When bus is unset,
* the VID:PID open loop below runs as before. */
if (const char *bus_env = std::getenv("DEVOURER_USB_BUS")) {
const auto want_bus = static_cast<uint8_t>(std::strtoul(bus_env, nullptr, 0));
const char *port_env = std::getenv("DEVOURER_USB_PORT");
/* A named socket is EXPECTED to hold the device — but the previous
* session's close/kill leaves the chip re-enumerating (firmware reload
* through ROM, sometimes via the ZeroCD id) for several seconds. Poll
* bounded instead of failing on the first empty scan. */
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::seconds(15);
bool waited = false;
uint16_t matched_pid = 0;
do {
libusb_device **list = nullptr;
ssize_t n = libusb_get_device_list(ctx, &list);
for (ssize_t i = 0; i < n && dev_handle == nullptr; ++i) {
libusb_device_descriptor dd{};
if (libusb_get_device_descriptor(list[i], &dd) != 0) continue;
if (dd.idVendor != target_vid) continue;
if (target_pid != 0 && dd.idProduct != target_pid) continue;
if (libusb_get_bus_number(list[i]) != want_bus) continue;
if (port_env != nullptr) {
uint8_t ports[8];
int pc = libusb_get_port_numbers(list[i], ports, sizeof(ports));
std::string path;
for (int p = 0; p < pc; ++p)
path += (path.empty() ? "" : ".") + std::to_string(ports[p]);
if (path != port_env) continue;
}
if (libusb_open(list[i], &dev_handle) == 0) {
matched_pid = dd.idProduct;
logger->info("Opened device {:04x}:{:04x} on bus {} port {}",
dd.idVendor, dd.idProduct, want_bus,
port_env ? port_env : "(any)");
}
}
if (list != nullptr) libusb_free_device_list(list, 1);
if (dev_handle != nullptr) break;
if (!waited) {
logger->warn("DEVOURER_USB_BUS={} PORT={} matched no device — waiting "
"for it to (re-)enumerate",
want_bus, port_env ? port_env : "(any)");
waited = true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
} while (std::chrono::steady_clock::now() < deadline);
/* Topology selection is strict: falling through to the VID:PID loop here
* would silently open a DIFFERENT adapter sharing the id (the exact
* ambiguity DEVOURER_USB_BUS exists to resolve) — fail instead. */
if (dev_handle == nullptr) {
logger->error("DEVOURER_USB_BUS={} PORT={} matched no device", want_bus,
port_env ? port_env : "(any)");
return nullptr;
}
describe(dev_handle, target_vid, matched_pid, picked);
return dev_handle;
}
for (size_t i = 0; i < n_default_pids; ++i) {
const uint16_t pid = default_pids[i];
if (target_pid != 0 && pid != target_pid) continue;
dev_handle = libusb_open_device_with_vid_pid(ctx, target_vid, pid);
if (dev_handle != nullptr) {
logger->info("Opened device {:04x}:{:04x}", target_vid, pid);
describe(dev_handle, target_vid, pid, picked);
return dev_handle;
}
}
/* DEVOURER_PID can name a PID not in the default list (e.g. 0x0120 for the
* T2U Plus). Try that direct combination once before giving up. */
if (target_pid != 0) {
dev_handle = libusb_open_device_with_vid_pid(ctx, target_vid, target_pid);
if (dev_handle != nullptr) {
logger->info("Opened device {:04x}:{:04x} (via DEVOURER_PID)",
target_vid, target_pid);
describe(dev_handle, target_vid, target_pid, picked);
return dev_handle;
}
}
logger->error("Cannot find any supported device under VID {:04x}",
target_vid);
return nullptr;
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
/* Shared demo-side USB device selection — the DEVOURER_PID / DEVOURER_VID /
* DEVOURER_USB_BUS / DEVOURER_USB_PORT open loop, factored out of rxdemo so
* multi-adapter demos (chanscout as the second radio next to a primary
* receiver) bind deterministically without a third copy of the logic.
*
* Selection rules (unchanged from the historical rxdemo behaviour):
* DEVOURER_VID=0xNNNN override the default Realtek VID (OEM-rebadged
* dongles, e.g. TP-Link 2357:xxxx).
* DEVOURER_PID=0xNNNN restrict to one PID; may name a PID outside the
* caller's default list (tried directly).
* DEVOURER_USB_BUS=N select by USB topology when several adapters share
* DEVOURER_USB_PORT=a.b.c one VID:PID (and even the serial). The port path
* is the dotted libusb port chain (sysfs devpath /
* `lsusb -t`). Topology selection is STRICT: no
* silent fallback to a different same-id adapter,
* and it polls up to 15 s for a device still
* re-enumerating after the previous session.
*
* Returns an opened (not claimed) handle, or nullptr after logging why. */
#include <libusb.h>
#include <cstdint>
#include <memory>
#include <string>
#include "logger.h"
/* Physical identity of the opened device, for role/identity logging (the
* scout.id event): what was matched and where it sits on the bus. */
struct UsbPick {
uint16_t vid = 0, pid = 0;
uint8_t bus = 0;
std::string port; /* dotted port path, empty when unavailable */
int speed = 0; /* libusb_get_device_speed enum value */
};
libusb_device_handle *
open_selected_usb(libusb_context *ctx, const std::shared_ptr<Logger> &logger,
const uint16_t *default_pids, size_t n_default_pids,
UsbPick *picked = nullptr);