first
This commit is contained in:
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user