This commit is contained in:
2026-09-13 13:30:21 +08:00
commit a6bbd520cf
2744 changed files with 1598795 additions and 0 deletions
+387
View File
@@ -0,0 +1,387 @@
/* txpower — reference consumer of the runtime TX-power API.
*
* Opens one adapter, brings it up for TX, prints the family's TxPowerCaps,
* then walks the requested knob sequence — a quarter-dB offset ramp
* (SetTxPowerOffsetQdb), a flat index (SetTxPowerIndexOverride), per-rate
* diffs (SetTxPowerRateDiffs), or both — echoing the applied qdB and a
* TxPowerState snapshot after every step. This is the shape of an adaptive-link
* controller's power leg: set, read back, observe saturation, react.
*
* Pure CLI configuration (no environment variables — the API is the point):
*
* --vid 0xNNNN --pid 0xNNNN adapter select (default: first Realtek PID)
* --channel N monitor channel for bring-up (default 36)
* --bw 20|40|80 bandwidth (default 20)
* --flat N force flat TXAGC index before the ramp (-1 clears)
* --offset-start Q offset ramp start, qdB (default: no ramp)
* --offset-stop Q offset ramp stop, qdB (default = start)
* --step-qdb Q ramp increment, qdB (default 4 = 1 dB)
* --step-ms N dwell per step, ms (default 500)
* --rate-diffs I,I,...,I 10 comma-separated qdB per rate
* (cck,legacy,m0..m7) or 'clear' to nullopt.
* Honoured where txpwr.caps reports
* rate_diffs=1; rate_diffs_hw=0 marks the
* software send-time fold (Kestrel)
* --flat-pulse N after --rate-diffs: force flat index N, dump
* state, then clear the override (-1) and dump
* state again — proves a flat override
* temporarily flattens the chip's per-rate
* table and the configured diffs come back once
* the override clears, all in one process
* --switch-channel N after the ramp: SetMonitorChannel(N) and re-dump
* state — proves the offset is sticky across a
* full channel set (and re-folds against the new
* channel's per-rate table)
* --retune N after the ramp: FastRetune(N) and re-dump state
* — proves the hop path leaves TXAGC alone
* --thermal print a thermal snapshot with each state line
*
* Machine-readable output (one JSONL event per step, consumed by
* tests/txpwr_offset_regcheck.sh):
*
* {"ev":"txpwr.caps","supported":1,"max":63,"step_qdb":2,...}
* {"ev":"txpwr.state","flat":-1,"offset_qdb":-24,"steps":-12,"satlo":0,
* "sathi":0,"cck":28,"ofdm":34,"mcs7":30,"rb":1,"rate_diffs":0}
*/
#ifdef _WIN32
#define NOMINMAX
#endif
#if defined(__ANDROID__) || defined(_MSC_VER) || defined(__APPLE__)
#include <libusb.h>
#else
#include <libusb-1.0/libusb.h>
#endif
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <optional>
#include <string>
#include <thread>
#include "DeviceSession.h"
#include "SignalStop.h"
#include "ThermalStatus.h"
#include "TxPower.h"
#include "UsbOpen.h"
#include "WiFiDriver.h"
#include "caps_event.h"
#include "env_config.h"
#include "logger.h"
namespace {
/* Event sink for the demo's JSONL emissions (print_state is a free function)
* — points at the main() Logger's sink, set right after Logger construction. */
devourer::EventSink *g_ev = nullptr;
/* The Realtek PIDs the demos' open loop iterates; --pid narrows to one. */
const uint16_t kRealtekPids[] = {0x8812, 0x8813, 0x881a, 0x0811, 0xa811,
0x0820, 0x0821, 0x8822, 0x0120, 0x012d,
0xb82c, 0xc811, 0xc812, 0xa81a};
struct Args {
uint16_t vid = 0x0bda;
int pid = -1; /* -1 = iterate kRealtekPids */
int channel = 36;
int bw = 20;
int flat = -2; /* -2 = untouched, -1 = clear, >=0 = force */
int offset_start = 0;
int offset_stop = 0;
bool have_ramp = false;
int step_qdb = 4;
int step_ms = 500;
int switch_channel = -1;
int retune = -1;
bool thermal = false;
bool have_rate_diffs = false;
bool rate_diffs_clear = false;
devourer::TxRateDiffsQdb rate_diffs;
int flat_pulse = 0;
bool have_flat_pulse = false;
};
bool parse_int(const char *s, int &out) {
char *end = nullptr;
long v = std::strtol(s, &end, 0);
if (end == s || *end != '\0')
return false;
out = static_cast<int>(v);
return true;
}
bool parse_args(int argc, char **argv, Args &a) {
for (int i = 1; i < argc; ++i) {
const std::string k = argv[i];
auto next = [&](int &out) {
return i + 1 < argc && parse_int(argv[++i], out);
};
int v = 0;
if (k == "--vid" && next(v))
a.vid = static_cast<uint16_t>(v);
else if (k == "--pid" && next(v))
a.pid = v;
else if (k == "--channel" && next(a.channel))
;
else if (k == "--bw" && next(a.bw))
;
else if (k == "--flat" && next(a.flat))
;
else if (k == "--offset-start" && next(a.offset_start))
a.have_ramp = true;
else if (k == "--offset-stop" && next(a.offset_stop))
a.have_ramp = true;
else if (k == "--step-qdb" && next(a.step_qdb))
;
else if (k == "--step-ms" && next(a.step_ms))
;
else if (k == "--switch-channel" && next(a.switch_channel))
;
else if (k == "--retune" && next(a.retune))
;
else if (k == "--thermal")
a.thermal = true;
else if (k == "--rate-diffs") {
if (i + 1 >= argc)
return false;
const std::string val = argv[++i];
a.have_rate_diffs = true;
if (val == "clear") {
a.rate_diffs_clear = true;
} else {
/* Parse and range-check the 10 ints here so a typo fails before the
* chip is ever brought up (every other flag validates in parse_args).
* Range is the library's [-64, 63] — a bare strtol->int8_t cast would
* silently truncate (200 -> -56), so reject out-of-range with a real
* error rather than clamp-and-surprise. */
int v[10] = {0};
int n = 0;
const char *p = val.c_str();
char *end = nullptr;
while (n < 10) {
v[n++] = static_cast<int>(std::strtol(p, &end, 10));
if (*end != ',')
break;
p = end + 1;
}
if (n != 10 || *end != '\0') {
std::fprintf(stderr, "devourer [E] --rate-diffs wants 10 comma ints "
"(cck,legacy,m0..m7) or 'clear'\n");
return false;
}
for (int j = 0; j < 10; ++j) {
if (v[j] < -64 || v[j] > 63) {
std::fprintf(stderr, "devourer [E] --rate-diffs value %d out of "
"range [-64, 63]\n",
v[j]);
return false;
}
}
a.rate_diffs.cck = static_cast<int8_t>(v[0]);
a.rate_diffs.legacy = static_cast<int8_t>(v[1]);
for (int j = 0; j < 8; ++j)
a.rate_diffs.mcs[j] = static_cast<int8_t>(v[2 + j]);
}
} else if (k == "--flat-pulse" && next(a.flat_pulse))
a.have_flat_pulse = true;
else {
std::fprintf(stderr, "devourer [W] unknown/incomplete arg: %s\n",
k.c_str());
return false;
}
}
if (a.have_ramp && a.offset_stop == 0 && a.offset_start != 0)
a.offset_stop = a.offset_start;
return true;
}
ChannelWidth_t bw_enum(int bw) {
switch (bw) {
case 40:
return CHANNEL_WIDTH_40;
case 80:
return CHANNEL_WIDTH_80;
default:
return CHANNEL_WIDTH_20;
}
}
void print_state(IRadio *dev, bool with_thermal) {
const devourer::TxPowerState s = dev->GetTxPowerState();
devourer::Ev(*g_ev, "txpwr.state")
.f("flat", s.flat_index)
.f("offset_qdb", s.offset_qdb)
.f("steps", s.offset_steps)
.f("satlo", s.saturated_low ? 1 : 0)
.f("sathi", s.saturated_high ? 1 : 0)
.f("cck", s.cck_index)
.f("ofdm", s.ofdm_index)
.f("mcs7", s.mcs7_index)
.f("rb", s.hw_readback ? 1 : 0)
.f("rate_diffs", s.rate_diffs_custom ? 1 : 0);
if (with_thermal) {
const devourer::ThermalStatus t = dev->GetThermalStatus();
devourer::Ev ev(*g_ev, "thermal");
ev.t().f("raw", t.raw);
if (t.valid)
ev.f("baseline", t.baseline).f("delta", t.delta);
else
ev.f("baseline", nullptr);
ev.f("status", devourer::ThermalBucket(t));
}
}
} // namespace
int main(int argc, char **argv) {
Args a;
if (!parse_args(argc, argv, a))
return 2;
auto logger = std::make_shared<Logger>();
apply_logging_env(*logger); /* DEVOURER_LOG_LEVEL / DEVOURER_EVENTS / ... */
g_ev = &logger->events();
install_devourer_signal_handlers();
/* Owns the teardown order (device -> interface -> handle -> context; see
* DeviceSession.h). Each early return from here on unwinds whatever has been
* adopted so far. */
devourer::DeviceSession session{logger};
libusb_context *ctx = nullptr;
if (libusb_init(&ctx) < 0) {
logger->error("libusb_init failed");
return 1;
}
session.adopt_context(ctx);
libusb_device_handle *handle = nullptr;
if (a.pid >= 0) {
handle = libusb_open_device_with_vid_pid(ctx, a.vid,
static_cast<uint16_t>(a.pid));
} else {
for (uint16_t pid : kRealtekPids) {
handle = libusb_open_device_with_vid_pid(ctx, a.vid, pid);
if (handle)
break;
}
}
if (!handle) {
logger->error("no adapter found ({:04x}:{})", a.vid,
a.pid >= 0 ? "requested pid" : "any Realtek pid");
return 1;
}
std::shared_ptr<devourer::UsbDeviceLock> lock;
if (devourer::claim_interface_then_reset(handle, devourer::find_wifi_interface(handle), logger, true, lock) !=
0) {
/* The claim failed, so nothing owns the handle yet — hand it to the
* session purely so the unwind closes it. */
session.adopt_handle(handle);
return 1;
}
session.adopt_handle(handle);
session.adopt_lock(lock);
WiFiDriver driver(logger);
std::unique_ptr<IRadio> owned_device =
driver.CreateRadio(handle, ctx, lock, devourer_config_from_env());
if (!owned_device) {
logger->error("CreateRadio failed (chip support not built?)");
return 1;
}
/* The session owns the device from here: it is what guarantees the device
* (and its in-flight TX) dies before libusb does. */
session.adopt_device(std::move(owned_device));
IRadio *const dev = session.device();
devourer::emit_adapter_caps(*g_ev, dev);
const devourer::TxPowerCaps caps = dev->GetTxPowerCaps();
devourer::Ev(*g_ev, "txpwr.caps")
.f("supported", caps.supported ? 1 : 0)
.f("max", caps.index_max)
.f("step_qdb", caps.step_qdb)
.f("step_measured", caps.step_measured ? 1 : 0)
.f("min_qdb", caps.offset_min_qdb)
.f("max_qdb", caps.offset_max_qdb)
.f("rate_diffs", caps.rate_diffs ? 1 : 0)
.f("rate_diffs_hw", caps.rate_diffs_hw_table ? 1 : 0)
.f("rate_diffs_measured", caps.rate_diffs_measured ? 1 : 0);
if (!caps.supported) {
logger->error("TX-power API not wired for this family yet");
dev->Stop();
return 3;
}
dev->InitWrite(SelectedChannel{.Channel = static_cast<uint8_t>(a.channel),
.ChannelOffset = 0,
.ChannelWidth = bw_enum(a.bw)});
logger->info("brought up on ch{} bw{}", a.channel, a.bw);
/* Baseline state before any knob moves (the offset=0 parity reference). */
print_state(dev, a.thermal);
if (a.flat >= -1) {
dev->SetTxPowerIndexOverride(a.flat);
logger->info("flat index override -> {}", a.flat);
print_state(dev, a.thermal);
}
if (a.have_rate_diffs) {
if (a.rate_diffs_clear) {
const bool ok = dev->SetTxPowerRateDiffs(std::nullopt);
logger->info("rate-diffs clear -> {}", ok ? "ok" : "unsupported");
} else {
const bool ok = dev->SetTxPowerRateDiffs(a.rate_diffs);
logger->info("rate-diffs -> {}", ok ? "applied" : "unsupported");
}
print_state(dev, a.thermal);
}
if (a.have_flat_pulse) {
dev->SetTxPowerIndexOverride(a.flat_pulse);
logger->info("flat pulse -> {}", a.flat_pulse);
print_state(dev, a.thermal);
dev->SetTxPowerIndexOverride(-1);
logger->info("flat pulse cleared");
print_state(dev, a.thermal);
}
if (a.have_ramp) {
const int dir = (a.offset_stop >= a.offset_start) ? 1 : -1;
const int inc = (a.step_qdb > 0 ? a.step_qdb : 4) * dir;
for (int q = a.offset_start;
(dir > 0 ? q <= a.offset_stop : q >= a.offset_stop) &&
!g_devourer_should_stop;
q += inc) {
const int applied = dev->SetTxPowerOffsetQdb(q);
devourer::Ev(*g_ev, "txpwr.offset").f("requested", q).f("applied", applied);
print_state(dev, a.thermal);
std::this_thread::sleep_for(std::chrono::milliseconds(a.step_ms));
}
}
if (a.switch_channel > 0 && !g_devourer_should_stop) {
dev->SetMonitorChannel(
SelectedChannel{.Channel = static_cast<uint8_t>(a.switch_channel),
.ChannelOffset = 0,
.ChannelWidth = bw_enum(a.bw)});
logger->info("SetMonitorChannel -> ch{} (offset must re-fold)",
a.switch_channel);
print_state(dev, a.thermal);
}
if (a.retune > 0 && !g_devourer_should_stop) {
dev->FastRetune(static_cast<uint8_t>(a.retune));
logger->info("FastRetune -> ch{} (TXAGC registers must be untouched)",
a.retune);
print_state(dev, a.thermal);
}
dev->Stop();
session.close();
return 0;
}