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
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,18 @@ target_link_libraries(TxQuiesceSelftest PRIVATE devourer PkgConfig::libusb)

add_test(NAME tx_quiesce_seam COMMAND TxQuiesceSelftest)

# Headless guard for the ACK-responder recipe (src/AckResponder.h): the arm,
# the gate-only clear staying gate-only, and the retarget seam. This test does
# not model silicon or instantiate Rtl8733bDevice; those are covered by the
# RTL8733B-only `disarmed` phase in ack_txreport_matrix.sh. Its legacy `off`
# phase remains a never-armed control.
add_executable(AckResponderSelftest
tests/ack_responder_selftest.cpp
)
target_link_libraries(AckResponderSelftest PRIVATE devourer PkgConfig::libusb)

add_test(NAME ack_responder_recipe COMMAND AckResponderSelftest)

# 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
15 changes: 10 additions & 5 deletions docs/aggregation.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,11 @@ numbers above came from.
`DEVOURER_ACK_RESPONDER=<unicast mac>`, all generations; `src/AckResponder.h`)
arms the MAC's autonomous ACK engine while monitor RX/injection continue
unchanged: port identity (MACID/BSSID 0x610/0x618 = `mac`) + net_type (0x102
[1:0] = AP). The identity+net_type pair is the whole gate — no beacon
machinery, no ADDBA session state, no CAM entry.
[1:0] = AP). No beacon machinery, no ADDBA session state, no CAM entry.
Which half of that pair is actually the gate is per-die: on the generations
the AP-mode work covered, net_type is; on the RTL8733B it is inert and the
engine matches MACID alone, so a disarm there must move the identity
(`src/AckResponder.h` retarget(), `src/AdapterCaps.h`).

With a responder armed, a peer TXing unicast QoS-Data (normal ack-policy) to
`mac` runs a full hardware ARQ loop — SIFS-timed ACKs from the responder,
Expand Down Expand Up @@ -230,6 +233,8 @@ canonical TX SA `57:42:75:05:d6:00` is a group address, so txdemo's QoS shape
takes `DEVOURER_TX_SA` to override it — a group TA yields retry-limit-pinned
reports even with the responder perfectly armed.

Arming a responder turns a passive monitor into an active transmitter, so it
is opt-in. The hardware ARQ (ACK, BlockAck, autonomous retransmission) is
complete; devourer layers no software ARQ policy above the reports.
Arming retargets hardware responses to a caller-supplied address, so it is
opt-in. That does not prove the never-armed state is passive: notably,
RTL8733B already answers for its initialization MAC. The hardware ARQ (ACK,
BlockAck, autonomous retransmission) is complete; devourer layers no software
ARQ policy above the reports.
2 changes: 1 addition & 1 deletion docs/rtl8733b.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ in these cells; the result is not a multi-sample or multi-peer qualification.
| knob | state | evidence |
| --- | --- | --- |
| `tx.ack_timeout_us` | honoured for CCK and non-CCK | 0x640 and CCK 0x639 both read back 33/200; an 11M dead-peer run submitted 1513/1129 frames in 8 s respectively (expected direction, not a timing calibration) |
| `SetAckResponder` | ported, `ack_responder_ok = true` (normal ACK and BlockAck response) | singles: on 1725/1725 ACKed, retarget 1728/1728, off 0/1723 with retries pinned at 12; A-MPDU armed/active-unarmed: 1.001/12.720 witnessed copies per payload and 14402/0 addressed BlockAck frames, with aggregation proven in both arms |
| `SetAckResponder` | ported, `ack_responder_ok = true` (normal ACK and BlockAck response) | singles: on 1725/1725 ACKed, retarget 1728/1728, and 0/1723 with retries pinned at 12 in the NEVER-ARMED cell. Live disarm was separately verified on `b733` with an RTL8822CU solicitor (armed `ack_rate=1.00`, retries 0.04; same-process disarmed `0.00`, retries 12) and on a vehicle-mounted `f72b` with an RTL8812AU solicitor (`1.00`/0.01 armed, `0.00`/12 disarmed). A-MPDU armed/active-unarmed: 1.001/12.720 witnessed copies per payload and 14402/0 addressed BlockAck frames, with aggregation proven in both arms. |
| `tx.retry_limit` | live, `tx_retry_limit_ok = true` | dead-peer dose response 0/3/12 -> 1.00/4.00/12.32–12.33 witnessed copies per frame (expected 1 + N), repeated over a 0/3/12/0/12 ladder |
| soliciting TX | normal ACK recognition measured | MCS3 responder on/off: 1.032/12.948 copies per observed frame; 11M CCK: 1.002/11.908, with 92.3–100% witness coverage |
| `tx.report` (CCX) | **not** ported — no reports observed | missing H2C/MEDIA_STATUS_RPT path remains the leading explanation; see Known gaps below |
Expand Down
17 changes: 14 additions & 3 deletions docs/scheduled-mac.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,19 @@ re-armed to a different MAC2, TX to MAC2 → proves RA and responder MAC are
arbitrary), **off** (no responder → expect 0% ok, retries pinned at the
descriptor limit set by `DEVOURER_TX_RETRY_LIMIT` — this matrix runs it at
12 — so the no-ACK outcome must be *visible*, per frame).
`report_coverage` = reports / frames sent (`tx.stats.submitted`); HalMAC adds
SW_DEFINE tag-echo gap counting.
`report_coverage` = reports / frames sent (`tx.stats.submitted`); a verdict
requires at least 0.80 by default (`MIN_REPORT_COVERAGE`), so a handful of
matching reports cannot make a mostly unobserved cell pass. HalMAC adds
SW_DEFINE tag-echo gap counting. An off verdict also requires at least 90% of
reported frames at the configured retry limit (`MIN_RETRY_PIN_RATE`); one
pinned outlier cannot stand in for the claimed retry-pinned distribution.

When the configured responder is an RTL8733B (`0bda:f72b` or `0bda:b733`),
the harness adds a fourth **disarmed** phase: arm MAC1, start a timer only
after the backend has completed bring-up and arming, clear it in the same live
process, then solicit MAC1 and expect the off verdict. That phase is
deliberately skipped for the default Jaguar1 responder; its disarm semantics
require separate backend-specific evidence.

TX sessions run `DEVOURER_TX_WITH_RX=thread`: CCX reports arrive on the C2H
RX path, so J1/J2 TX-only sessions never see them (measured: J2 TX-only = 0
Expand Down Expand Up @@ -241,7 +252,7 @@ station): **8814AU** closes the loop at retries ~0.1 (the bench responder of
choice); **8812AU** works but degraded (97% delivery at ~7 mean retries —
its SIFS ACKs only land intermittently); **8821AU works** (61–64% single-shot
across three reps, **94% at retry 8** with a healthy retry histogram,
disarm-proof-verified: 0% with the responder powered down); the 8812BU
arm-verified (the off cell is never-armed, so it establishes no disarm): 0% with the responder powered down); the 8812BU
responder was separately proven (`tests/ack_responder_check.sh`).

A cell whose responder never armed reads exactly like a broken chip — on=0%
Expand Down
48 changes: 47 additions & 1 deletion examples/rx/main.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include <atomic>
#include <cassert>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <limits>
#include <memory>
#include <mutex>
#include <random>
Expand Down Expand Up @@ -37,6 +39,9 @@
#if defined(DEVOURER_HAVE_JAGUAR3)
#include "jaguar3/RtlJaguar3Device.h"
#endif
#if defined(DEVOURER_HAVE_8733B)
#include "rtl8733b/Rtl8733bDevice.h"
#endif
#include "RtlAdapter.h"
#include "SignalStop.h"
#include "UsbOpen.h"
Expand Down Expand Up @@ -1442,6 +1447,48 @@ int main(int argc, char **argv) {
.f("stage", "demo.create_device")
.f("ms", ms_since_start());
devourer::emit_adapter_caps(*g_ev, rtlDevice);
/* RTL8733B-only measurement hook. Scheduling belongs to the concrete
* backend so the delay starts after its verified arm/bring-up rather than
* racing Init from a generic side thread. Refuse other generations: a green
* run that cleared a cold port before Init armed it is false evidence. */
if (const char *d = std::getenv("DEVOURER_ACK_DISARM_AFTER_MS")) {
const char *responder = std::getenv("DEVOURER_ACK_RESPONDER");
if (responder == nullptr) {
logger->warn("DEVOURER_ACK_DISARM_AFTER_MS ignored: no "
"DEVOURER_ACK_RESPONDER was configured");
} else if (!devourer::parse_mac(responder)) {
logger->error("DEVOURER_ACK_DISARM_AFTER_MS requires a valid "
"DEVOURER_ACK_RESPONDER MAC");
return 1;
} else {
char *end = nullptr;
errno = 0;
const unsigned long long ms = std::strtoull(d, &end, 10);
if (errno != 0 || end == d || *end != '\0' || d[0] == '-' ||
ms > (std::numeric_limits<uint32_t>::max)()) {
logger->error("DEVOURER_ACK_DISARM_AFTER_MS='{}' is not a valid "
"non-negative 32-bit millisecond delay",
d);
return 1;
}
#if defined(DEVOURER_HAVE_8733B)
auto *rtl8733b = dynamic_cast<Rtl8733bDevice *>(rtlDevice);
if (rtl8733b == nullptr) {
logger->error("DEVOURER_ACK_DISARM_AFTER_MS is RTL8733B-only: "
"refusing a race-prone measurement on {}",
devourer::generation_name(
rtlDevice->GetAdapterCaps().generation));
return 1;
}
rtl8733b->ScheduleAckResponderDisarmForTest(
static_cast<uint32_t>(ms));
#else
logger->error("DEVOURER_ACK_DISARM_AFTER_MS requires an RTL8733B-enabled "
"build");
return 1;
#endif
}
}
/* The BB-debug-port / queue-depth research helpers are Jaguar1-only, so
* they live on RtlJaguarDevice rather than the IRtlDevice interface. The
* whole block compiles out when Jaguar1 support isn't built; when it is, the
Expand Down Expand Up @@ -2159,7 +2206,6 @@ int main(int argc, char **argv) {
.ChannelWidth = width,
.Band = rx_band,
});

stop_background_emitters.Run();
if (la_thread.joinable())
la_thread.join();
Expand Down
133 changes: 101 additions & 32 deletions src/AckResponder.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

/* AckResponder — the hardware ACK engine as a first-class monitor-mode knob.
*
* The Realtek MAC auto-ACKs (SIFS-timed, zero host involvement) any unicast
* frame whose RA matches the port-0 MACID, PROVIDED the port has a network
* type. devourer's monitor bring-up leaves net_type = 0 (No Link), which is
* why a monitor radio never ACKs; the AP-mode work proved the gate — with
* MACID + net_type programmed, a real station's auth/assoc arrive at retry=0
* (docs/ap-mode.md). This header is that recipe minus the beacon machinery:
* program the port identity, flip the net type, and the chip becomes an ACK
* responder for one MAC address while everything else about monitor mode
* (promiscuous RX, injection) is unchanged.
* The Realtek MAC's immediate-response engine is SIFS-timed with zero host
* involvement. The shared recipe programs the port-0 MACID/BSSID and requests
* AP net_type. On the Jaguar generations covered by the AP-mode measurements,
* net_type participates in gating the engine. RTL8733B is different: it
* matches MACID while net_type reads NoLink, so MAC bring-up already permits
* responses to the adapter's own address and SetAckResponder retargets that
* match to the requested address. See the measured truth table in
* AdapterCaps.h and Rtl8733bDevice::ClearAckResponder.
*
* This header carries that register recipe minus the beacon machinery. Which
* half controls a live disarm is a measured per-die property: use the shared
* gate-only clear only where net_type is sufficient; otherwise the backend
* must also move the identity off the responder address with retarget().
*
* On the adapter combinations exercised by tests/ampdu_ba_check.sh, the SAME
* gate also enables the hardware BlockAck responder. RTL8733B has its own
Expand All @@ -24,8 +28,8 @@
* 0x0618..0x061d REG_BSSID — port identity companion (the proven AP
* recipe programs both)
* 0x0102[1:0] MSR/net_type (REG_CR+2) — 0 NoLink / 1 Ad-hoc / 2 Infra /
* 3 AP; any nonzero arms the responder. We
* use AP (3), the bench-proven value.
* 3 AP. We request AP (3); whether the field
* gates responses is die-specific.
*
* Turning a passive monitor into an ACTIVE transmitter is a behavioral
* change — hence opt-in only (DeviceConfig rx.ack_responder / the
Expand All @@ -34,12 +38,39 @@
* same footgun broke AP association (docs/ap-mode.md). */

#include <cstdint>
#include <cstring>

#include "RtlAdapter.h"

namespace devourer {
namespace ack {

/* The MACID pair, composed in ONE place. enable(), verify() and retarget()
* previously each carried their own copy of this packing, in a header whose
* verify() comment promises to keep the register map in one file: an
* endianness or width fix applied to one copy would leave the others reading
* the old layout while still reporting success. */
inline uint32_t macid_lo(const uint8_t mac[6]) {
return (uint32_t)mac[0] | ((uint32_t)mac[1] << 8) | ((uint32_t)mac[2] << 16) |
((uint32_t)mac[3] << 24);
}
inline uint16_t macid_hi(const uint8_t mac[6]) {
return (uint16_t)(mac[4] | (mac[5] << 8));
}

/* Is the ACK-match identity currently `mac`? The engine matches MACID, so this
* is the question "would this port answer for `mac`" — on the RTL8733B that is
* the WHOLE question (see retarget()). Throws nothing; a transport failure
* reads as "not this address". */
inline bool macid_is(RtlAdapter &dev, const uint8_t mac[6]) noexcept {
try {
return dev.rtw_read<uint32_t>(0x0610) == macid_lo(mac) &&
dev.rtw_read16(0x0614) == macid_hi(mac);
} catch (...) {
return false;
}
}

inline bool enable(RtlAdapter &dev, const uint8_t mac[6]) noexcept {
try {
const uint8_t nt = dev.rtw_read8(0x0102);
Expand All @@ -53,16 +84,10 @@ inline bool enable(RtlAdapter &dev, const uint8_t mac[6]) noexcept {
(void)dev.rtw_write8(0x0102, static_cast<uint8_t>(nt & ~0x03u));
return false;
}
if (!dev.rtw_write<uint32_t>(
0x0610, (uint32_t)mac[0] | ((uint32_t)mac[1] << 8) |
((uint32_t)mac[2] << 16) |
((uint32_t)mac[3] << 24)) ||
!dev.rtw_write16(0x0614, (uint16_t)(mac[4] | (mac[5] << 8))) ||
!dev.rtw_write<uint32_t>(
0x0618, (uint32_t)mac[0] | ((uint32_t)mac[1] << 8) |
((uint32_t)mac[2] << 16) |
((uint32_t)mac[3] << 24)) ||
!dev.rtw_write16(0x061c, (uint16_t)(mac[4] | (mac[5] << 8))))
if (!dev.rtw_write<uint32_t>(0x0610, macid_lo(mac)) ||
!dev.rtw_write16(0x0614, macid_hi(mac)) ||
!dev.rtw_write<uint32_t>(0x0618, macid_lo(mac)) ||
!dev.rtw_write16(0x061c, macid_hi(mac)))
return false;
if (dev.rtw_write8(0x0102,
static_cast<uint8_t>((nt & ~0x03u) | 0x03u)))
Expand All @@ -79,12 +104,62 @@ inline bool enable(RtlAdapter &dev, const uint8_t mac[6]) noexcept {
}
}

/* Disarm: net_type back to No Link — the gate, so the MACID may stay. */
/* Shared gate-only clear for dies where net_type controls the responder.
* RTL8733B callers must additionally retarget MACID. */
inline bool disable(RtlAdapter &dev) {
const uint8_t nt = dev.rtw_read8(0x0102);
return dev.rtw_write8(0x0102, static_cast<uint8_t>(nt & ~0x03u));
}

/* Point the ACK-match identity at another address WITHOUT touching the gate.
*
* On the RTL8733B this is the ONLY thing that changes whether the port answers.
* net_type is inert there — measured, three cells, single-shot ACK rate at
* MCS3 with a Jaguar1 soliciting:
*
* never-armed port, peer solicits the adapter's own EFUSE MAC 85.2 %, 82.5 %
* never-armed port, peer solicits an address nobody holds 0.0 % (control)
* port armed to X, peer solicits X 83.3 %
*
* A never-armed monitor answers on its own MAC at the same rate as a
* deliberately armed responder. So on this die the engine matches MACID and
* nothing else: MAC bring-up's program_mac is what makes it answer, arming
* merely repoints it, and 0x0102[1:0] does not gate it in either direction.
* (The gate IS real on the generations the AP-mode work covered; this is a
* per-die exception, not a correction to the recipe.)
*
* Consequences worth stating plainly, because they are easy to get backwards:
* every never-armed RTL8733B monitor session already auto-ACKs unicast frames
* addressed to its own EFUSE MAC — a pre-existing property of that die under
* this library, not something an arm creates. And a disarm cannot make the port
* silent; it can only take it off the responder address and put it back on the
* one it shipped with.
*
* `mac` is that restore address, normally the adapter's own. Deliberately NOT
* zero: many Realtek MAC TX paths refuse to schedule a frame when the MAC ID is
* zero — the T1 canary bug that programming REG_MACID was introduced to fix
* (src/jaguar1/HalModule.cpp, EepromManager.h) — and a radio being disarmed may
* still be injecting. Zero would not remove the match either, only move it:
* 00:00:00:00:00:00 has the I/G bit clear, so is_unicast() accepts it.
*
* Gate untouched on purpose: this is the identity half, so a caller composes it
* with disable() in whichever order its die needs, and no generation gets a
* behaviour change it was not measured for. */
inline bool retarget(RtlAdapter &dev, const uint8_t mac[6]) noexcept {
Comment thread
josephnef marked this conversation as resolved.
try {
/* Sequenced into locals rather than `&&`: both halves must be ATTEMPTED.
* Under short-circuit a failed low write would skip the high one — and by
* this file's own doctrine (see enable(): "The transfer status is not
* state readback") that low write may still have landed, leaving a
* half-updated address with the high half never even tried. */
const bool lo = dev.rtw_write<uint32_t>(0x0610, macid_lo(mac));
const bool hi = dev.rtw_write16(0x0614, macid_hi(mac));
return lo && hi;
} catch (...) {
return false;
}
}

inline bool is_disabled(RtlAdapter &dev) {
return (dev.rtw_read8(0x0102) & 0x03u) == 0;
}
Expand All @@ -106,24 +181,18 @@ inline bool disable_verified(RtlAdapter &dev) noexcept {
* the precondition is a property of the recipe, not of any one die. */
inline bool is_unicast(const uint8_t mac[6]) { return (mac[0] & 0x01u) == 0; }

/* Did the arm actually land? Reads back the gate (net_type) and the RA the ACK
/* Did the requested arm recipe land? Reads back net_type and the RA the ACK
* engine matches (MACID), composed exactly as enable() writes them — keeping
* the register map in ONE file, so a change to enable() cannot silently
* diverge from a copy of it somewhere else.
*
* NB the BSSID companion at 0x0618 is written but not checked: the ACK engine
* matches on MACID, and 0x0618 is programmed only because the proven AP recipe
* programs both. Verifying the two fields that gate the behaviour keeps this
* honest without asserting on one that does not. */
* programs both. Verifying the two behavior-relevant programmed fields keeps
* this honest without asserting that both are gates on every die. */
inline bool verify(RtlAdapter &dev, const uint8_t mac[6]) noexcept {
try {
const uint32_t want_lo =
(uint32_t)mac[0] | ((uint32_t)mac[1] << 8) |
((uint32_t)mac[2] << 16) | ((uint32_t)mac[3] << 24);
const uint16_t want_hi = (uint16_t)(mac[4] | (mac[5] << 8));
return (dev.rtw_read8(0x0102) & 0x03u) == 0x03u &&
dev.rtw_read<uint32_t>(0x0610) == want_lo &&
dev.rtw_read16(0x0614) == want_hi;
return (dev.rtw_read8(0x0102) & 0x03u) == 0x03u && macid_is(dev, mac);
} catch (...) {
return false;
}
Expand Down
Loading
Loading