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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ those are the ones listed below.
degradation predictor — `docs/warm-tx-degradation.md` has delivery scattered
63–83% with no relation to the meter, and inside one uninterrupted session
the meter stays pinned while delivery drifts.
- `DEVOURER_STA_IDENTITY=<own|self>,<bssid>` (rxdemo; txdemo with
`DEVOURER_TX_WITH_RX=thread`) — call `IRadio::SetStationIdentity` once the
RX loop is up, `DEVOURER_STA_CLEAR_AFTER_MS=N` to clear it later; `sta.arm`
/ `sta.clear` events (`examples/common/station_arm_env.h`); txdemo sends
nothing after a refused arm. The Realtek cell is `tests/realtek_station_onair.sh`.
- `DEVOURER_RX_BUSY_MS=N` (rxdemo) — the vendor-neutral busy-airtime window
at a fixed cadence: arm, wait N ms, read, one `rx.busy` event per window
(`IRadio::ArmChannelBusy`/`GetChannelBusy`, so it runs on the MT7612U where
Expand Down
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,19 @@ target_link_libraries(AckResponderSelftest PRIVATE devourer PkgConfig::libusb)

add_test(NAME ack_responder_recipe COMMAND AckResponderSelftest)

# Headless guard for the Realtek station arm (src/StationArm.h over the
# AckResponder.h station recipe) - the Jaguar1/2/3 half of
# IRadio::SetStationIdentity: the exact registers armed and restored, every
# refusal writing nothing, and readback catching a write that did not land.
# Built only when a backend that uses it is. Silicon behaviour is on-air only:
# tests/realtek_station_onair.sh.
if(DEVOURER_JAGUAR1 OR DEVOURER_JAGUAR2_8822B OR DEVOURER_JAGUAR2_8821C OR
DEVOURER_JAGUAR3_8822C OR DEVOURER_JAGUAR3_8822E)
add_executable(StationArmSelftest tests/station_arm_selftest.cpp)
target_link_libraries(StationArmSelftest PRIVATE devourer PkgConfig::libusb)
add_test(NAME station_arm COMMAND StationArmSelftest)
endif()

# Headless guard for the windowed RX-receipt primitives (src/cell/RxReceipt.h):
# TLV round-trip, ring eviction, late accounting, ledger merge idempotence,
# strict-parse rejections. The on-air halves are tests/receipt_verify.py over
Expand Down
4 changes: 3 additions & 1 deletion docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets];
|---|---|---|
| `init.timing` | L (`src/InitTimer.h`) + demos | stage ("scope.stage", e.g. "demo.first_rx_frame", "txdemo.first_tx_submit"), ms, [xfers] (register transfers the stage spent on that adapter's transport, USB only — present on the Jaguar3 `j3hal.*` / `j3init.*` stages) |
| `adapter.caps` | RX, TX, doctor, txpower (`examples/common/caps_event.h`) | supported, chip, names, chip_id "0x..", gen, variant, transport, tx_chains, rx_chains, n_ss, stbc, ldpc, sgi, bw_max, bw[] (MHz), txpwr_max, txpwr_step_qdb, txpwr_step_measured, txpwr_min_qdb, txpwr_max_qdb, txpwr_rate_diffs, txpwr_rate_diffs_hw, txpwr_rate_diffs_measured, tune_2g4[]\|null, tune_5g[]\|null, char_2g4[]\|null, char_5g[]\|null, ldpc_rx_ht, ldpc_rx_vht, ldpc_rx_flag, vht_2g4, per_pkt_txpwr, per_pkt_txpwr_steps, per_pkt_txpwr_step_qdb, per_pkt_txpwr_min_qdb, per_pkt_txpwr_max_qdb, per_pkt_txpwr_measured, narrowband, fastretune, ack_responder, station_mode, tx_retry_limit, tx_no_agg, he_er_su, per_chain_rssi, hw_rx_tsf, hw_beacon_txtsf, tsf_write, xtal_cap_max, xtal_cap_default |
| `sta.arm` | RX, TX (`DEVOURER_STA_IDENTITY`, `examples/common/station_arm_env.h`) | ok (0/1, `IRadio::SetStationIdentity`'s return), own "aa:..", bssid, attempts (a refused arm is retried; this is the last outcome), [why] on ok 0: "refused", "no_mac", or "rx_not_running" (the RX worker failed or ended; an arm that landed is cleared again) |
| `sta.clear` | RX, TX (`DEVOURER_STA_CLEAR_AFTER_MS`) | ok (0/1, `IRadio::ClearStationIdentity`'s return: rollback restored and verified) |
| `debug.wreg` | L (`DEVOURER_LOG_WRITES`) | addr "0x0nnn", width, val "0x…" |
| `hop.prof` | L (`DEVOURER_HOP_PROF`) | gen, ch, `<stage>_us`…, total_us |
| `tx.fail` | L (send failure; regress.py keys on it) | {status, actual_len, timeout} or {rc, timeout} |
Expand Down Expand Up @@ -109,7 +111,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets];
| ev | emitter | fields |
|---|---|---|
| `tx.frame` | TX | n, rc — precoder demo variant: n, ok |
| `tx.stats` | TX | submitted, failed, was_timeout, last_rc; periodic events (not the `final:1` one) also carry `txdma_status` (the raw `REG_TXDMA_STATUS` latch; which bits mean a stopped transmitter: `IRtlRadio::GetTxDmaStatus`) where `IRtlRadio::HasTxDmaStatus()` (which backends: its declaration), or `txdma_read_failed:1` when that sample's register read failed |
| `tx.stats` | TX | submitted, failed, was_timeout, last_rc; the `final:1` event also carries t (the `tx.report` timebase, so a harness can tell how long before the end a transmitter last reported); periodic events (not the `final:1` one) also carry `txdma_status` (the raw `REG_TXDMA_STATUS` latch; which bits mean a stopped transmitter: `IRtlRadio::GetTxDmaStatus`) where `IRtlRadio::HasTxDmaStatus()` (which backends: its declaration), or `txdma_read_failed:1` when that sample's register read failed |
| `tx.agg` | L (`DEVOURER_TX_USB_AGG`, send_packets) | frames, bytes, shim, ok — one per multi-frame bulk-OUT URB. The sync-TX generations (Jaguar2/Jaguar3/RTL8733B) also emit `sent` — bytes actually transferred, OR the negative libusb rc on a transport error (deliberately raw: this event is the only machine-readable carrier of the aggregated-path error code) — and set `ok` only on a FULL write, so `ok=false` splits as `sent < 0` transport error vs `0 <= sent < bytes` short write. Jaguar1 TX is async: its `ok` means URB accepted by the transport and there is no `sent` field (bytes resolve at completion reaping) |
| `tx.report` | L (`DEVOURER_TX_REPORT`, CCX decode) | t, state (0=delivered, 1=retry-drop), ok, retries, final_rate, queue_time_raw, bmc, macid, fmt ("8812"\|"halmac"); halmac adds tag (SW_DEFINE echo), rts_retries, missed (fw-stuffed constant on Jaguar3 — tag gaps are the drop signal; `tests/txrpt_coverage_attrib.py`) — t is the achieved-report-rate timebase (the CCX emission ceiling is reports/s) |
| `tx.status` | RX, duplex (C2H TX_RPT decode) | hoff, queue, retry, airtime_us, rate |
Expand Down
96 changes: 96 additions & 0 deletions docs/realtek-station-arm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# The Realtek station arm: bench record

`IRadio::SetStationIdentity` on Jaguar1/2/3. The contract is on the
declaration in `src/IRadio.h`. How the Realtek arm differs from the MT7612U's
is in `src/StationArm.h`. The flag is `AdapterCaps::station_mode_ok`. This
page holds the run behind that flag and its limits.

## The cell

`tests/realtek_station_onair.sh`. Its header defines the arms (A–H), their
controls and the verdicts. Both halves are read off the transmitter's own CCX
reports (`tx.report`), at retry limit 12, MCS3, with 200-byte frames, a 5 ms
gap and 10 s per arm.

## Runs

The rig:
- ch6, near field, one run per arm per record;
- an RTL8812CU (8822C) and an RTL8812BU (8822B), each the other's peer;
- the AP is an MT7612U on mt76x2u running hostapd;
- rtw88 was not blacklisted: the demos detached it, and the harness handed
every adapter back.

There are two records on this rig: the first on the arm's first version, and
the current one on the reviewed code, which adds arm H. Every run exited 0
with 5 verdicts passed. The second record matches the first arm for arm,
within a few frames and a few hundredths of a retry. Both were scored before
the harness gained its transmitter-liveness gate and per-arm submission
floor, and before it judged reception against reported rather than
submitted frames; a run on the current harness is pending.

Each cell is reports / submitted, ok, mean retries, then rx_distinct where
the arm counts reception.

| arm | 8812CU station | 8812BU station |
|---|---|---|
| A | 1616 / 1658, 100.0%, 0.03, 1616 | 860 / 910, 100.0%, 0.33, 860 |
| B | 861 / 1694, 0.0%, 12.00, 870 | 85 / 902, 0.0%, 12.00, 86 |
| C | 858 / 1684, 0.0%, 12.00 | 88 / 911, 0.0%, 12.00 |
| D | 861 / 1698, 0.0%, 12.00, 1101 | 87 / 918, 0.0%, 12.00, 90 |
| E | 859 / 1694, 0.0%, 12.00, 1098 | 88 / 918, 0.0%, 12.00, 91 |
| F | 2291 / 2341, 100.0%, 0.09 | 3095 / 3137, 100.0%, 0.20 |
| G | 222 / 1369, 0.0%, 12.00 | 1615 / 2547, 0.0%, 12.00 |
| H | 2399 / 2449, 100.0%, 0.08 | 3202 / 3244, 100.0%, 0.20 |

The first record has no arm H; the other arms read:

| arm | 8812CU station | 8812BU station |
|---|---|---|
| A | 1616 / 1658, 100.0%, 0.02, 1616 | 847 / 897, 100.0%, 0.28, 847 |
| B | 862 / 1689, 0.0%, 12.00, 870 | 88 / 917, 0.0%, 12.00, 91 |
| C | 862 / 1688, 0.0%, 12.00 | 87 / 917, 0.0%, 12.00 |
| D | 858 / 1690, 0.0%, 12.00, 1100 | 89 / 926, 0.0%, 12.00, 93 |
| E | 866 / 1691, 0.0%, 12.00, 1096 | 87 / 918, 0.0%, 12.00, 90 |
| F | 2317 / 2367, 100.0%, 0.11 | 3095 / 3137, 100.0%, 0.20 |
| G | 229 / 1377, 0.0%, 12.00 | 1605 / 2538, 0.0%, 12.00 |

## What the arm is for

Arm H is F's uplink with the DUT NOT armed. It was ACKed 100% on both dies,
at the same retries as armed F. So on Jaguar2/3 the UP half of the bar does
not depend on the arm: the AP acknowledges by address, and the transmitter
counts that ACK whether or not MACID holds the station's address. What
needs the arm is the DOWN half. With the arm absent (D) or cleared (E), the
DUT ACKs nothing addressed to it.

`station_mode_ok` rests on both halves being met while armed, which is how
a station runs. H adds that the uplink half holds without the arm too. It
is reported and never scored.

## Limits

- **The run's scope.** One unit per die, two runs per arm on one rig (one
for H), near field, one channel, one AP type, and unassociated
throughout. Power save, TIM, cross-BSS duplicate detection, hardware key
lookup and the managed receive filter are all untested.
- **What "received" means.** `rx_distinct` is the DUT's count of distinct
frames from the peer, taken from rxdemo's `rx.seq` stream. The station runs
the promiscuous monitor filter, so it also received in the controls where
the frames were not addressed to it, or where it was unarmed: B, D and E
received about 870–1100 frames on the CU and about 86–93 on the BU. In
arm A, "received" means only that the frames arrived. The arm changes the ACK,
which is read off the peer's reports, not reception.
- **Submitted exceeds reports on every arm.** By arm group:
- acknowledged arms (A, F, H): 40–50 frames;
- unacknowledged downlink arms (B to E): about half the submissions on the
CU (about 860 of 1690) and about nine in ten on the BU (about 87 of 910);
- the G arms: 222–229 of about 1370 reported on the CU (83–84% missing),
and 1605–1615 of about 2540 on the BU (36–37% missing).

That fits frames still queued in the chip when the window closes, because
an unacknowledged frame airs 13 times before its report. It is not proven.
In arm A, `rx_distinct` equals the report count on both units.
- **Dies not measured by this cell:** the 8822E, the 8821C, and every
Jaguar1 die. On the 8812, arm D is predicted to answer, because bring-up
programs the EFUSE MAC into MACID; run it with `EXPECT_UNARMED_SILENT=0`.
183 changes: 183 additions & 0 deletions examples/common/station_arm_env.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/* DEVOURER_STA_IDENTITY / DEVOURER_STA_CLEAR_AFTER_MS - drive
* IRadio::SetStationIdentity / ClearStationIdentity from rxdemo and txdemo.
*
* Demo-local (no DeviceConfig field): the seam is a runtime call that IRadio
* orders after the RX loop is running, which a construction-time config
* cannot express. The knob means the same on every backend - it calls the
* seam and reports the return value; a backend that has not ported it returns
* false, and the event says so.
*
* DEVOURER_STA_IDENTITY=<own|self>,<bssid>
* own the station's address, or `self` for the adapter's permanent
* (EFUSE) MAC via IRadio::GetPermanentMacAddress.
* bssid the AP's address.
* DEVOURER_STA_CLEAR_AFTER_MS=N
* N ms after a successful arm, call ClearStationIdentity - the on-air
* form of "Clear returns to the pre-arm state".
*
* Events (docs/logging.md): `sta.arm` {ok, own, bssid, attempts, [why]} once
* the arm has been tried; `sta.clear` {ok} after a scheduled clear. A refused arm is
* retried (a backend refuses before bring-up), ten times 500 ms apart; the
* event reports the last outcome. */
#ifndef DEVOURER_STATION_ARM_ENV_H
#define DEVOURER_STATION_ARM_ENV_H

#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <optional>
#include <string>
#include <thread>

#include "DeviceConfig.h"
#include "Event.h"
#include "IRadio.h"
#include "logger.h"

namespace devourer {

struct StationArmRequest {
bool own_self = false;
MacAddr own{};
MacAddr bssid{};
uint32_t clear_after_ms = 0; /* 0 = never */
};

/* nullopt when DEVOURER_STA_IDENTITY is unset. `bad` is set when it is set
* but malformed - the demo refuses to run rather than measure an unarmed
* station as an armed one. */
inline std::optional<StationArmRequest>
station_arm_request_from_env(const Logger_t &log, bool &bad) {
bad = false;
const char *e = std::getenv("DEVOURER_STA_IDENTITY");
if (e == nullptr || *e == '\0')
return std::nullopt;
const std::string v(e);
const size_t comma = v.find(',');
StationArmRequest r;
std::optional<MacAddr> bssid;
if (comma != std::string::npos) {
const std::string own = v.substr(0, comma);
bssid = parse_mac(v.substr(comma + 1));
if (own == "self") {
r.own_self = true;
} else if (auto m = parse_mac(own)) {
r.own = *m;
} else {
bssid.reset();
}
}
if (!bssid) {
log->error("DEVOURER_STA_IDENTITY='{}' is not <own|self>,<bssid>", e);
bad = true;
return std::nullopt;
}
r.bssid = *bssid;
if (const char *c = std::getenv("DEVOURER_STA_CLEAR_AFTER_MS")) {
char *end = nullptr;
const unsigned long ms = std::strtoul(c, &end, 10);
if (end == c || *end != '\0' || c[0] == '-' || ms > 3600000ul) {
log->error("DEVOURER_STA_CLEAR_AFTER_MS='{}' is not 0..3600000", c);
bad = true;
return std::nullopt;
}
r.clear_after_ms = static_cast<uint32_t>(ms);
}
return r;
}

inline std::string station_mac_str(const MacAddr &m) {
char b[18];
std::snprintf(b, sizeof(b), "%02x:%02x:%02x:%02x:%02x:%02x", m.bytes[0],
m.bytes[1], m.bytes[2], m.bytes[3], m.bytes[4], m.bytes[5]);
return b;
}

/* Arm (with the bounded retry), emit `sta.arm`, and return the outcome. Call
* after the RX loop has started (IRadio's ORDERING clause). `stop` aborts the
* retry wait. `rx_ended`, when given, is set by the caller once its RX worker
* has returned or thrown: an arm is refused while it is set (a station with no
* receive loop is not one), and an arm that lands just as the worker ends is
* cleared again and reported refused. Never holds a lock the RX callback
* takes. */
inline bool station_arm_run(IRadio *dev, const StationArmRequest &req,
EventSink &ev, const Logger_t &log,
const std::atomic<bool> &stop,
const std::atomic<bool> *rx_ended = nullptr) {
auto rx_gone = [&] { return rx_ended != nullptr && rx_ended->load(); };
MacAddr own = req.own;
if (req.own_self) {
uint8_t m[6];
if (!dev->GetPermanentMacAddress(m)) {
log->error("DEVOURER_STA_IDENTITY: own=self, but this backend reports "
"no permanent MAC");
Ev(ev, "sta.arm").f("ok", 0).f("own", nullptr).f("bssid",
station_mac_str(req.bssid)).f("attempts", 0).f("why", "no_mac");
return false;
}
for (int i = 0; i < 6; ++i)
own.bytes[i] = m[i];
}
bool ok = false;
int attempts = 0;
while (attempts < 10 && !stop.load() && !rx_gone()) {
++attempts;
ok = dev->SetStationIdentity(own, req.bssid);
if (ok)
break;
for (int s = 0; s < 500 && !stop.load(); s += 50)
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
const char *why = ok ? nullptr : "refused";
if (rx_gone()) {
if (ok)
(void)dev->ClearStationIdentity();
ok = false;
why = "rx_not_running";
}
Ev e(ev, "sta.arm");
e.f("ok", ok ? 1 : 0)
.f("own", station_mac_str(own))
.f("bssid", station_mac_str(req.bssid))
.f("attempts", attempts);
if (why)
e.f("why", why);
if (ok)
log->info("DEVOURER_STA_IDENTITY: station identity armed (own {}, "
"BSSID {}, attempt {})",
station_mac_str(own), station_mac_str(req.bssid), attempts);
else if (rx_gone())
log->error("DEVOURER_STA_IDENTITY: not armed - the RX loop is not "
"running (it failed or ended)");
else
log->error("DEVOURER_STA_IDENTITY: SetStationIdentity refused after {} "
"attempt(s)",
attempts);
return ok;
}

/* The scheduled clear, after a successful arm. Emits `sta.clear`. */
inline void station_clear_after(IRadio *dev, const StationArmRequest &req,
EventSink &ev, const Logger_t &log,
const std::atomic<bool> &stop) {
if (req.clear_after_ms == 0)
return;
for (uint32_t s = 0; s < req.clear_after_ms && !stop.load(); s += 50)
std::this_thread::sleep_for(std::chrono::milliseconds(50));
if (stop.load())
return;
const bool ok = dev->ClearStationIdentity();
Ev(ev, "sta.clear").f("ok", ok ? 1 : 0);
if (ok)
log->info("DEVOURER_STA_CLEAR_AFTER_MS: station identity cleared and "
"verified");
else
log->error("DEVOURER_STA_CLEAR_AFTER_MS: ClearStationIdentity could not "
"verify its rollback");
}

} /* namespace devourer */

#endif /* DEVOURER_STATION_ARM_ENV_H */
Loading
Loading