jaguar1/2/3: the Realtek station arm for IRadio::SetStationIdentity - #463
Conversation
|
@josephnef PR5 of the station series: the first library item of #461, the Realtek station arm behind the It is one commit on 87dde96. The arm is ported to Jaguar1/2/3.
The new unarmed-uplink arm (H) answers a question #461 left open: the AP acknowledges the station's frames whether or not it is armed, so on Jaguar2/3 the arm is what the downlink half needs. Every other die stays false. Before opening, two independent review passes ran, each finding checked against the tree. The limits are in the description: one unit per die, one rig, near field, and the AP is an MT7612U running hostapd. Jaguar1 has no measurement on this bench. |
PR Summary by QodoAdd Realtek station identity arms for Jaguar1, Jaguar2, and Jaguar3
AI Description
Diagram
High-Level Assessment
Files changed (19)
|
Code Review by Qodo
1.
|
josephnef
left a comment
There was a problem hiding this comment.
Reviewed every hunk against the tree at 6737727 and ran the on-air cell on this rig. The arm, its refusals, the Clear rollback and the Jaguar3 try_lock change read correctly; the StationArm state machine matches the selftest and the documented contract. Two defects below, both small to fix. The hardware record reproduces yours.
Changes requested
1. _brought_up is set before the last port-0 writes of Init/InitWrite, so the "refused before bring-up" guard does not give the ordering StationArm.h claims.
src/jaguar2/RtlJaguar2Device.cpp:255 (also src/jaguar3/RtlJaguar3Device.cpp:140, src/jaguar1/RtlJaguarDevice.cpp:2079). After the flag goes true and outside _reg_mu, bring-up still writes the beamformee identity into 0x0610 (J2:546, J3:164) and arms the configured ACK responder (J2:566, J3:184, J1:134/1697). A SetStationIdentity from another thread in that window passes the guard and arms; then either kBfeeMac overwrites MACID while _station.armed() stays true (Clear later "verifies" against a snapshot that no longer describes the port), or the config SetAckResponder is refused because the station holds port 0 and Init throws "configured ACK responder could not be armed". rxdemo spawns its arm thread before dev->Init (examples/rx/main.cpp:1901 vs 2079/2251) and arms at the 10 s silent-channel cap regardless of bring-up state, so a slow bring-up hits this without any library caller. Scope, honestly: the window only has content when bf.beamformee_of or rx.ack_responder is configured; the default tail after the flag is apply_replay_wseq and the bb_dump. Fix shape: set _brought_up after the last port-0 write (J3's InitWrite already has the BroughtUpGuard pattern to commit late), or hold _reg_mu across the tail. The ORDERING clause in StationArm.h:46-48 and docs/realtek-station-arm.md should then be true as written.
2. station_args_ok accepts an all-zero own. src/AckResponder.h:418. is_unicast tests only the I/G bit, so 00:00:00:00:00:00 passes and arm_station programs MACID = 0. The same header documents why that is unsafe (has_safe_restore_mac, line 255: "stops TX scheduling on the Jaguar1 8812 path"), and DEVOURER_STA_IDENTITY lets a typo produce exactly that address, reported as a successful arm. Apply the zero check to own as well.
Hardware record (this rig, this head, one run each)
Rig as in the PR: ch6, near field, MCS3, retry limit 12, MT7612U hostapd AP, rtw88 blacklisted. 8812CU at 3-2.4, 8812BU at 4-2.3.3. Every adapter was handed back after each cell.
| arm | 8812CU station | 8812BU station |
|---|---|---|
| A armed, to own | 698/740, 100.0%, 0.13, rx 698 | 1664/1714, 100.0%, 0.30, rx 1664 |
| B armed, to nobody | 353/732, 0.0%, 12.00, rx 365 | 164/1218, 0.0%, 12.00, rx 166 |
| C DUT absent | 369/753, 0.0%, 12.00 | 163/1217, 0.0%, 12.00 |
| D unarmed | 364/742, 0.0%, 12.00, rx 577 | 163/1219, 0.0%, 12.00, rx 165 |
| E armed then cleared | 367/746, 0.0%, 12.00, rx 575 | 160/1216, 0.0%, 12.00, rx 163 |
| F uplink to AP | 3035/3085, 100.0%, 0.02 | 2080/2122, 100.0%, 0.02 |
| G uplink to nobody | 290/1572, 0.0%, 12.00 | 1078/2010, 0.0%, 12.00 |
| H uplink unarmed (not scored) | 3137/3187, 100.0%, 0.01 | 2180/2222, 100.0%, 0.02 |
Both directions: 5 passed, 0 failed, 0 inconclusive. Same shape as your table, including H: the uplink half holds unarmed on both dies.
8822E: INCONCLUSIVE on this rig, not a verdict on the arm. The 0bda:a81a dongle here (adapter.caps says RTL8822E, chip-id 0x17) ran the same cell with EXPECT_UNARMED_SILENT=0, 8812CU as peer: 0 passed, 3 failed. The arm itself succeeded on every arm (sta.arm ok=1, sta.clear ok=1 on E). But the UP half failed because this unit's TX is dead on ch6 regardless of the arm (F, G and H all bulk_send EP 8 FAIL rc=-7, 300+ each - the pre-existing 8812EU TX fault already on record for this unit), and in arm A the DUT delivered 0 distinct frames with a rtw_read(2c08) failure right after the arm, while B/D/E on the same unit delivered 160-1267. A plausible reading is that the first own-addressed frame made the MAC try to ACK through a dead TX path and stalled the chip, but one unit with a known TX fault cannot separate the arm from the unit. The flag stays false there, as the PR says; a second 8822E unit is the follow-up. ctest 80/80 on the PR head, station_arm included.
Not blocking
- qodo-gate is red on its harness finding (a transmitter that stalls after
MIN_REPORTSstill scores). Fair as a nit;submittedis already in eachres_*line, so a floor on submitted per arm would close it cheaply. Once 1 and 2 are in and the two dies re-run, theskip-qodo-gatelabel is the way through rather than more rounds. - The 10 s silent-channel arm in rxdemo (
examples/rx/main.cpp:1901) is the same shape as Qodo's note on arming after a failedInit; closing item 1 in the library leaves that as a demo-only nicety.
SetStationIdentity / ClearStationIdentity on Jaguar1, Jaguar2 and Jaguar3 (OpenIPC#461, first library item), over a station recipe in AckResponder.h and the per-device state in src/StationArm.h: gate closed, MACID = own, BSSID = the AP, net_type = Infra, all read back; Clear restores the exact pre-arm MACID/BSSID/net_type and reads it back. How it differs from the MT7612U arm, stated at StationArm.h and each backend's declaration: - it configures rather than checks: Jaguar2/3 bring-up never programs MACID, and net_type comes up NoLink; - later port-0 claimants are refused rather than dropping the arm. SetAckResponder and StartBeacon return false while a station is armed, and on Jaguar2/3 ClearAckResponder leaves the port alone; - Stop() and the destructor clear the arm, because Jaguar2's teardown does not power the chip down, and a re-Init of an armed object clears it before the bring-up, keeping the record for a retry if that clear does not verify. The station is also refused while a failed beacon start's enables are still set, and the beacon disable leaves net_type alone while a station holds it. Clear's gate close is a precondition: if it fails, the identity is not touched and the arm stays recorded for a retry. The arm waits for each backend's _station_ready, which is cleared at the top of Init/InitWrite and committed only after that bring-up's last port-0 write (the BF beamformee identity, the configured ACK responder). It is not _brought_up, which goes true mid-bring-up for the tail's own setters. An all-zero own or BSSID is refused: is_unicast alone passes it, and it would program MACID = 0. The arm also prints the MT7612U's arm-time retry-limit WARN, and the 8814A variant says that die airs every unicast once. Jaguar3's CFO tracker and BF apply took _reg_mu blocking on the RX thread, the libusb event thread in the Async ring. A _reg_mu holder doing sync USB I/O (the coex tick, now the arm) would deadlock against them. Both now try_lock. The CFO tracker takes the lock before it steps, so a skipped tick cannot feed its polarity detection. Found by reading the code; not reproduced on hardware. rxdemo / txdemo (with DEVOURER_TX_WITH_RX=thread) gain DEVOURER_STA_IDENTITY=<own|self>,<bssid> and DEVOURER_STA_CLEAR_AFTER_MS. They emit sta.arm / sta.clear events. An arm is refused while the demo's RX worker has failed or ended; txdemo checks the knob before any of its threads start, waits for its RX loop's first frame, and sends nothing after a refused arm. tests/realtek_station_onair.sh measures both halves of station_mode_ok's bar from the transmitter's own CCX reports: - DOWN: a Realtek peer injects to the armed DUT, with controls for a destination nobody holds, the DUT absent, the DUT unarmed and the DUT armed then cleared, plus the DUT's rx.seq reception; - UP: the armed DUT sends to a hostapd AP, against a destination nobody holds, plus an unarmed uplink arm (H) that is reported and not scored. An arm is scored only when its transmitter kept airing through the window (MAX_GAP_MS between CCX reports; the final tx.stats now carries t), it submitted MIN_SUBMITTED frames, and its receiver was still running at the end. Reception is judged against the peer's reported frames. station_mode_ok is TRUE on the 8822C and 8822B dies only, scoped by variant. On one RTL8812CU and one RTL8812BU (each the other's peer, an MT7612U/hostapd AP, ch6, near field, two records on this rig, the current one on this head matching the first), the claim arms read as follows: - A (armed, to own): 100.0% ACKed, 0.03 / 0.33 mean retries; - F (uplink to the AP): 100.0%, 0.09 / 0.20. Every control (nobody, DUT absent, unarmed, cleared, uplink to nobody) read 0.0% at the 12-retry limit. Arm H (uplink, NOT armed) was ACKed 100% (0.08 / 0.20): the AP acknowledges by address, so the uplink half holds without the arm. The arm is what the DOWN half needs (D and E at 0%). The flag rests on both halves met while armed. Limits, in docs/realtek-station-arm.md: - one unit per die, two runs on one rig, near field, one AP type; - the station receives in the controls too, because it is promiscuous; the arm changes the ACK; - submitted exceeds reports on every arm. The 8822E, the 8821C and every Jaguar1 die stay false, unmeasured. Kestrel and the RTL8733B keep the not-ported default. ctest station_arm, gated on the Jaguar options. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
6737727 to
fc7eb72
Compare
|
@josephnef thanks, and for running the cell on your rig. Both changes are in fc7eb72 (still one commit on 87dde96), plus the submitted floor. 1. The arm racing the Init/InitWrite tail. The arm now waits for a new flag, I didn't move One thing I left as it was: 2. All-zero Not blocking: the submitted floor. Each arm now also needs Re-run on fc7eb72 here (ch6, near field, MCS3, retry limit 12, MT7612U hostapd AP; 8812CU at 5-1 and 8812BU at 8-1). Both ways: 5 passed, 0 failed, 0 inconclusive,
ctest is 82/82 with Agreed on the 8822E: its flag stays false until a second unit runs the cell. |
josephnef
left a comment
There was a problem hiding this comment.
Re-reviewed fc7eb72. Both items are closed the way I'd have closed them: _station_ready cleared under the station lock at the top of each Init/InitWrite next to retire(), committed after the configured ACK responder / AMPDU / replay tail, left false by a throwing bring-up, and station_args_ok refuses zero on either side. Keeping the snapshot when a retire-time clear does not verify is the right call for the reason the comment gives. ctest 80/80 here. Approving.
Hardware record on fc7eb72 (this rig, one run each way)
ch6, near field, MCS3, retry limit 12, MT7612U hostapd AP, SECS=20 (why below). Every adapter handed back.
| arm | 8812CU station | 8812BU station |
|---|---|---|
| A armed, to own | 2567/2609, 99.9%, 0.20, rx 2565 | 3526/3576, 100.0%, 0.02, rx 3526 |
| B armed, to nobody | 1362/2294, 0.0%, 12.00, rx 1375 | 350/1696, 0.0%, 12.00, rx 354 |
| C DUT absent | 1364/2296, 0.0%, 12.00 | 352/1696, 0.0%, 12.00 |
| D unarmed | 1358/2291, 0.0%, 12.00, rx 1760 | 348/1719, 0.0%, 12.00, rx 350 |
| E armed then cleared | 1361/2294, 0.0%, 12.00, rx 2114 | 349/1708, 0.0%, 12.00, rx 350 |
| F uplink to AP | 4984/5034, 100.0%, 0.03 | 4192/4234, 100.0%, 0.11 |
| G uplink to nobody | 488/2083, 0.0%, 12.00 | 2169/3101, 0.0%, 12.00 |
| H uplink unarmed (not scored) | 4986/5036, 100.0%, 0.02 | 4204/4246, 100.0%, 0.10 |
Both ways 5 passed, 0 failed, 0 inconclusive, live=1 on every arm. Same shape as the 6737727 run and as your table.
One harness note for the follow-ups issue, not blocking
The MIN_SUBMITTED floor is scaled from SECS, but the window includes the peer's bring-up. At the default SECS=10 the 8812CU-station cell came back INCONCLUSIVE here on every DOWN arm: the 8812BU peer's txdemo.init_write took 6.2 s and 8.7 s in two runs on this host (the 8812CU's takes 0.85 s), so it aired for 1.3-3.7 s and submitted 283-327 against the 500 floor, with live=1 and max_gap_ms under 35 the whole time. A healthy transmitter, scored as a stall. Suggest scaling the floor from the TX-active span (first tx.frame or txdemo.first_tx_submit to the final tx.stats t) rather than from SECS, or starting the window at first submit. Related: the AP sysfs preflight at the top of the UP half runs after the whole DOWN half; my first re-run spent 60 s on DOWN and then refused on an AP that had left the bus. A cheap check beside the AP_SYSFS non-empty test at the top would fail fast.
On the arm-after-Stop() question: I'd close it in the follow-ups rather than here, since it matches the pre-existing _brought_up behaviour and the destructor still clears.
## What changed - **`tests/sta_client.cpp`** — a station client over `IRadio` + `src/sta/`: scan, authenticate, associate, the WPA2-PSK four-way and CCMP, and a TAP device for the host. It is the in-tree caller of `IRadio::SetStationIdentity`: - armed only when `AdapterCaps::station_mode_ok` is true; otherwise refused at start-up with exit 2 (`DEVOURER_STA_ARM=0` runs unarmed); - armed for the BSSID actually joined, after `StartRxLoop` and outside the mutex the RX callback takes; - `ClearStationIdentity` on the way out whenever an arm was attempted, with the result printed (`restored (verified)` / `NOT VERIFIED`; trivially true on MT7612U, whose arm writes nothing). - Teardown: TAP reader stopped → RX loop stopped and joined → TAP fd closed under the station mutex → final send (the leave's deauth) → clear → ledger. Bring-up, main-loop and receive-path exceptions are caught so the run still leaves, clears and prints its ledger. - The duplicate cache is reset per association, not per key: a Retry copy of a rekey's message 3 is a duplicate, not a MIC failure. - Unicast requests an ACK; `tx.retry_limit` defaults to 7 unless `DEVOURER_TX_RETRY_LIMIT` is set, so `DEVOURER_TX_RETRY_LIMIT=0` is how a run asks for the single-shot uplink the library warns about. - The bring-up asks for RX with TX (`rx.enable_with_tx`). Jaguar3's `InitWrite` keeps the RX path only when that is set, and enabling RX after a TX-only bring-up is not reliable there. Without it, an 8822C station could join nothing. The MT7612U and Jaguar2 backends don't read the flag. - Data plane: one `DupDetector` (one transmitter, the joined AP), a non-Key EAPOL packet returned unconsumed by `on_decrypted_msdu` is delivered to the host, a `ccmp_encrypted_len` overflow is refused, and the FCS is trimmed by `RxAtrib.fcs_present`. - **`tests/sta_client_selftest.inc`** + ctest **`sta_client_headless`** (`StaClientSelftest`, Linux + OpenSSL, collected by the `selftests` aggregate) — headless cells against a fixture that plays the authenticator: scan selection and sweep, re-join policy, key selection by key id, replay and duplicate windows, PTK/GTK rekeys, refusal of plaintext/fragments/A-MSDU, the FCS trim, the ledger's identities. - **`tests/mt7612u_sta_onair.sh`** — the MT7612U joins hostapd running in a network namespace (so the ping crosses the air; the route is asserted first). Cells `open`, `wpa2` (group + pairwise rekeys), `noarm` (control) and `retry0` (the `tx.retry_limit == 0` warning). Exit 0 pass / 1 fail / 2 inconclusive / 3 interrupted. Uses `tests/mt7612u_sta_lib.sh` for the private OUT, the run lock, PID-recorded kills and the DUT hand-back. - **`docs/station-client.md`**; `docs/station-core.md` and `src/sta/CLAUDE.md` now name the client as the core's caller. ## Why Nothing in-tree called `SetStationIdentity`. This harness exercises the arm-then-measure path end to end — arm, associate, key, carry traffic, clear — and is where the `tx.retry_limit == 0` warning gets bench-checked. It also gives `DupDetector` and the MSDU<->Ethernet helpers their first caller. ## What is measured On-air (`tests/mt7612u_sta_onair.sh` on this head: MT7612U station, hostapd on an RTL8812BU under the in-kernel rtw88 driver, ch6, near field): **16 passed, 0 failed, 0 inconclusive**. Every adapter was handed back, with no hostapd, monitor interface or namespace left. | Cell | Result | |---|---| | `open` | associated; ping over the air 0% loss; ledger 22 plaintext, 0 encrypted received (19-23 across runs); armed for the joined BSSID on attempt 1; clear ran | | `wpa2` | four-way completed; the AP completed a group rekey and a pairwise rekey; ping 0% loss before and after the rekeys; ledger 1 association, 3 rekeys answered, 2 PTK installs, MIC failures 0; armed; clear ran; no `tx.retry_limit=0` warning (retry limit 7, the station default) | | `noarm` | no arm and no clear ran (scored); the link is reported, not scored: four-way completed, ping 0% loss | | `retry0` | the library's arm-time `tx.retry_limit=0` warning appeared (scored); clear ran; the link is reported: four-way completed, ping 0% loss with single-shot unicast | The first cut of this commit ran the same 16/16 on the same rig before the review fixes, and again after them, after the rebase onto #463, after the qodo fixes and after the maintainer review (this head); the `wpa2` cell also passed alone. Interrupt check on this head: SIGINT to the harness one second after `sta_client` started, i.e. inside its bring-up. The harness was gone 7 s later, every adapter was back on its kernel driver, and no `sta_client`, hostapd or netns was left. The maintainer's independent run on b33421b (MT7612U and a T3U 8812BU under rtw88) was also 16/16. Against it: - One run per cell, one rig, one channel, near field, and one AP driver (rtw88). The out-of-tree vendor `rtl88x2cu` driver cannot be the AP, because its phy cannot change network namespace (`iw phy set netns` returns -95). The cell refuses such an AP before taking the DUT (exit 2), and that was checked on this rig. - `noarm` passing with a working link is what the MT7612U predicts, since its arm writes no register. It does not show what the arm buys on this chip. - `retry0`'s link held at near field. A single-shot uplink is fragile by construction, and the cell scores the warning, not the link. - Sending the leave's deauth after the RX loop has stopped is checked on the MT7612U only. By reading, it holds on Jaguar2/3 too: `StopRxLoop` only sets a stop flag, the loop's exit stops the DIG/phydm workers and nothing on the TX side, and `send_packet` there is a synchronous bounded bulk-OUT. ## What it can't do - The on-air cell is MT7612U-only (`sta_dut_take`) until a generic DUT take/hand-back exists. The Realtek arm landed in #463 (8822C / 8822B report `station_mode_ok` true), so the client arms those dies when `DEVOURER_VID` / `DEVOURER_PID` select one, but no on-air cell here covers it. A die that reports `station_mode_ok` false is refused cleanly (exit 2); `DEVOURER_STA_ARM=0` runs it unarmed. - The `noarm` control cannot separate "the arm made it work" on MT7612U: there the arm writes no register (it checks `MT_MAC_ADDR` and `MT_AUTO_RSP_EN`), so it scores only that no arm/clear ran and reports the link. - Software CCMP; WPA2-PSK/CCMP or open; no PMF, no fragment reassembly, no A-MSDU, no roaming or background scan while associated. A pairwise rekey can cost one received frame (802.11-2016 12.7.6.5). ## Follow-ups - Realtek DUTs in the on-air cell (needs a generic DUT take/hand-back). - A reconnect cell (AP stopped and restarted) — the policy is pinned headlessly only. - Software-CCMP throughput under load, and a long soak with the default retry settings. ## Review record Two Flash reviews and an Opus check on the first cut; all fixed in this commit: - shutdown order (TAP fd closed outside the mutex while the RX thread could still write through it; the ledger identities not exact); - no exception handling (an InitWrite throw aborted; a loop or RX-path throw skipped leave, clear and ledger); - the duplicate cache was reset at a PTK rekey (cell `test_the_dup_cache_spans_a_rekey_not_an_association`, which fails both with the reset at the rekey and with no reset); - on-air cell: a station that never printed `sta_client up:` is INCONCLUSIVE (rig/bring-up), not FAIL; a hung station is escalated to SIGKILL and the DUT is not re-enumerated under it; the clear is scored as having run (its MT7612U result is trivially true); the AP phy must list `set_wiphy_netns`; traps installed before the DUT is taken; the AP interface's UP state restored; - the queue-identity cell no longer fakes `g_sent`; the ledger is described as printed once `sta_client up:` has printed; `sta_client_headless` named in the OpenSSL-absent configure messages. After the rebase onto #463, a check against the merged Realtek arm and one more Flash review of the delta: - the bring-up now asks for RX with TX (`rx.enable_with_tx`), which a Jaguar3 station needs; - the TAP reader thread and the teardown `StopRxLoop()` are guarded like the rest, so a throw there can no longer skip leave, clear and ledger; - every guard also catches non-`std::exception` throws. - qodo's pass on #464: non-blocking TAP, guarded thread starts, atomic stop flag. Maintainer review (josephnef, CHANGES_REQUESTED at b33421b), all fixed: - a caught exception exited 0 and read as "raise SECS": `g_fault`, exit 3, `fault=1` in the ledger; the on-air cell scores exit 3 as FAIL with the cause named; - SIGINT/SIGTERM handlers now installed at the top of `main` (undoing an inherited SIG_IGN); the cell's cleanup escalates INT -> KILL, and `sta_pid_kill` (shared lib) polls before it reaps instead of a bare blocking `wait`; - the station retry-limit default is decided from the library's strict parse of `DEVOURER_TX_RETRY_LIMIT`, not from the variable being set; - a fatal TAP poll/read is a counted fault, not a silent end of the reader; - no backend reports a frame's RX channel, so a beacon without a DS Parameter Set is not folded in during a retune or for 50 ms after it; - `test_forged_mic_is_refused` now feeds the real AP's frame at the forged PN and checks delivery and the replay window; - the script header says `FW_DIR` must hold decompressed blobs. - a pattern hunt for more of the same classes: RNG failure and a not-verified clear are faults; teardown leave guarded; strict parse of the duration and channel; a check that could not fail removed. ## Verification - `cmake -DDEVOURER_MT7612U=ON -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON`: build clean, `ctest` 83/83 (with #463's `station_arm`). - Same under `-DDEVOURER_SANITIZE=address+undefined`: `ctest` 83/83. - CI subsets: MT7612U-only (`ctest` 72/72), Jaguar3-only (`ctest` 72/72, `sta_client_headless` and `station_arm` pass). - `bash -n` and `shellcheck -x` clean on `tests/mt7612u_sta_onair.sh` and `tests/mt7612u_sta_lib.sh`. - On air: see the table above. Command: `sudo DUT_SYSFS=<mt7612u> AP_SYSFS=<rtw88 adapter> CH=6 tests/mt7612u_sta_onair.sh` (one cell: append `open`, `wpa2`, `noarm` or `retry0`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… of #465 (#466) Follow-ups and fixes collected from merged PRs: the maintainer's non-blocking review notes, a probable cause for #461's one-entry loss, part of the #465 harness defect sweep, and two txdemo teardown fixes we found ourselves. Fifteen commits, one per item or per harness, so any can be dropped or reordered on review, plus three follow-ups from the qodo review of this PR. Refs #461, Refs #465 (partial). ## What changed ### Maintainer review notes | Commit | Change | |---|---| | `0fee106` | #452: `txs_parse_long` carried two comment blocks back to back. The first one documents `txs_retry_limit_env`, and it now sits there. | | `4d862ff` | #453: TxBeaconGuard retries StopBeacon when an armed beacon's disable is refused. After a successful StartBeacon, the Jaguar2/3 StopBeacon returns false only for a refused disable, and `_bcn_hw_touched` makes a retry re-run it. A clean false after a refused StartBeacon still ends the loop. | | `294a671` | #453: the dated, attributed bench paragraph in `docs/jaguar3-tx-ring.md` becomes plain measurement. `IRtlRadio::GetTxDmaStatus` now says bit 13 (`BIT_PAYLOAD_OVF_8822C`) **can** latch at max duty on USB2, not that it does. | | `ddd8467` | The older bring-up gates in `src/mt7612u/tools/bringup.cpp` now call `mt_mac_stop()` when `mt_mac_start()` fails. `mt_mac_start` sets `ENABLE_TX` before its WPDMA poll, so a failed start could leave TX enabled. Each gate keeps its own teardown order (`rx_teardown()` first where a ring was draining EP 4). | | `2cc5330` | #463 review: Jaguar1/2/3 `Stop()` clears `_station_ready` in the block that clears the arm, under the station lock. A `SetStationIdentity` after `Stop()` is refused until the next bring-up. | | `907a5b5` | #463 review: `tests/realtek_station_onair.sh`'s submission floor is scaled to the span the arm actually aired, not to SECS, which also holds the peer's bring-up. The span runs from the first submit to the final `tx.stats` (`fcdec63`, below). | | `da8cd6d` | #463 review: the same script runs its whole AP guard in the preflight, before the DOWN half spends its minute, and again at the start of UP. | ### #461: the txs gate's lost status entry | Commit | Change | |---|---| | `2e7a3de` | `gate_txs` claims an arm's first status entry back when a stale `MT_TX_STAT_FIFO_EXT` word mislabelled it with the previous arm's pktid. Details under Why. | ### #465 (partial): the harness defect sweep | Commit | Harness | Change | |---|---|---| | `53477e0` | `mt7612u_ap_onair.sh` | An interrupt stops the run instead of carrying on into the next cell. Before re-enumerating `AP_SYSFS`, cleanup waits for its children to exit (10 s bound, zombie-aware via the lib's `sta_pid_alive`), and skips the re-enumeration if it had to KILL one. The stop cell FAILs when PHASE 2 never appeared, rather than scoring "beacon gone". The ping-loss figure is quoted whole ("66.6667%", not "6667%"). | | `05bdaf7` | `mt7612u_sta_autoack.sh` | DUT hand-back only after its process is confirmed gone, with a CLEANED guard so the EXIT pass after an INT cannot undo that refusal. | | `8a2ab14` | `mt7612u_sta_uplink.sh` | The same hand-back rule. The DUT's `mt7612uprobe txs` runs under `timeout -s INT -k 10`, bounded by the gate's own worst case. | | `bf9c0dd` | `mt7612u_sta_identity.sh` | `AP_REENUM` is set before hostapd starts, and hostapd is killed unconditionally. The BSSID gate is bounded. The monitor vif is probed in the preflight. Exit codes are 0/1/2/3, with rig refusals as 2 and the INT/TERM trap as 3. | | `d473027` | `mt7612u_sta_onair.sh` | The hostapd restart race. Before each launch, `ap_up` waits (10 s bound, inside the netns) for the AP netdev, forces it back to `type managed`, and brings it up. | Not in this PR, from #465: the generic Realtek DUT take, the reconnect cell and the CCMP soak. Those come in a separate PR. ### Our own finding: txdemo thread joins | Commit | Change | |---|---| | `64ebce3` | `examples/tx/main.cpp` joins the optional IN drainers (`DEVOURER_DRAIN_BULK_IN`, `DEVOURER_POLL_INTR_IN`) on every exit. | | `6b31d5f` | It also joins the RX and USB event threads on every exit. | In both cases an early return used to destroy a still-joinable `std::thread`, which calls `std::terminate`. Small scope guards now join the threads in the normal teardown's order, before `session.close()`. The legacy fork child is covered by `18f0a20`, below. ### qodo review of this PR | Commit | Change | |---|---| | `fcdec63` | `realtek_station_onair.sh`: the aired span started at the first `tx.report`, so a transmitter that started, stalled and then burst 50 reports in its last second passed both floors. txdemo's `tx.frame` now carries `t` (the `tx.report` timebase; `docs/logging.md`). The span runs from the first submit, and liveness also refuses a first report more than `MAX_GAP_MS` after it (`lead_ms`). | | `92dfa1d` | `mt7612u_ap_onair.sh`: if a between-cell cleanup could not reset the AP (a process outlived TERM), the remaining cells are recorded as NOT RUN and the run exits 2, instead of being scored on an unreset adapter that may still be beaconing. | | `18f0a20` | txdemo: the `DEVOURER_TX_WITH_RX` fork child leaves through `std::_Exit` after flushing stdio, with `Init` in a try. No destructor runs in it, so its copies of the IN-drainer threads no longer terminate it. | ## Why - **#461 (the txs entry loss).** The FIFO read is two USB control transfers: EXT first, then the main word, which pops the entry. - If an entry is filed between the two reads, the pop is paired with the previous entry's EXT word. - Inside an arm that is harmless, because every entry carries the same pktid. - On an arm's first entry it carries the previous arm's pktid. That entry is counted late, the arm stays one short, and every per-frame wait then times out. These are the ~6 fps N-1/N rows. - `docs/mt7612u-tx-retry.md`'s own table rules out the alternative, "status posted only on the next TX". At limit 0, receiver ON, arm f lags with its late entry in its own row, after arm e settled and owed nothing. Arm g after it shows no late entry. - **#465.** Each item was a way for a harness to score a dead or wedged device as a result, hang without bound, or re-enumerate an adapter under a process still in its de-init. - **The txdemo threads.** A joinable `std::thread` destructor terminates the process, so an otherwise clean early exit aborted. ## Measured - **#461, on the author's unit:** `gate_txs` with the stale-EXT claim settled 16/16 arms at retry limits 5 and 0 (recorded on #461). - The claim's current form is keyed to the last arm that actually sent a frame, so an arm whose every submit failed is skipped. That refinement has not been run on hardware. - **The uplink harness** (`tests/mt7612u_sta_uplink.sh`) is INCONCLUSIVE both on this branch (48/53 status entries) and on master (57/60). It predates the branch. - That loss is several entries per arm. It is **not** the race above, which accounts for at most one entry per arm, and it is still being investigated. - **Submission floor (`907a5b5`), checked on synthetic JSONL through the script's own `summarize`/`usable`:** | Fixture | Old floor | New floor | |---|---|---| | Slow bring-up, 327 frames in 2.0 s | refused at 500 | usable, floor 101 | | Start, stall, 60 reports in the last second | refused at 500 | refused: lead_ms 8800 (live=0), and 60 < floor 490 | | Rate stall, 110 frames over 10 s | — | refused, floor 500 | | Hard stall | — | refused, live=0 | | No `tx.frame` with `t` (an older txdemo) | — | refused, live=0 | ## What it can't do - The stale-EXT claim recovers at most one entry per arm, and only when the previous sending arm settled. A deficit larger than one entry is not this race. - **The INT/TERM exit code is not uniform across the harnesses:** - realtek_station_onair, mt7612u_sta_onair and now mt7612u_sta_identity exit 3, as their headers document; - autoack, uplink and ap_onair still exit 130. - Unifying them is left for later. ## Follow-ups - Find the uplink harness's multi-entry status loss. - Hardware-run the "last arm that sent" form of the txs claim. - Unify the interrupt exit code across the station harnesses. - From #465: the generic Realtek DUT take, the reconnect cell and the CCMP soak (separate PR). ## Verification - `ctest`: 83/83, both plain and under `-DDEVOURER_SANITIZE=address+undefined`, built with `-DDEVOURER_MT7612U=ON -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON`. The two reference-submodule tests are skipped without `reference/`. - `make -C src/mt7612u check` passes. - `bash -n` (and `sh -n` for the `/bin/sh` scripts) is clean on every touched script. `shellcheck -x` is clean on all of them except `tests/mt7612u_ap_onair.sh`, whose remaining SC2046/SC2015/SC2012 notes predate this branch. - **Hardware, on this head (6b31d5f):** MT7612U at 1-1, RTL8812CU at 5-1, RTL8812BU at 8-1 (rtw88), ch6, near field. Every adapter was handed back after each harness, and no hostapd, netns or monitor interface was left. | Harness | Result | |---|---| | realtek_station_onair | 5 passed, 0 failed, 0 inconclusive, both ways (8812CU and 8812BU as the station) | | mt7612u_sta_onair | 16/16 | | mt7612u_sta_identity | rc 0 (STAID 12/12; STAACK reports its own A/B as non-discriminating on this rig, and only arm C is scored, as before) | | mt7612u_ap_onair | 14/14 | | mt7612u_sta_autoack | 3/3 | - **The three qodo follow-ups (`fcdec63`, `92dfa1d`, `18f0a20`), re-run on 18f0a20:** `realtek_station_onair` 5 passed, 0 failed, 0 inconclusive, both ways, under the floor that now starts at the first submit. `mt7612u_ap_onair` 14/14. Every adapter was handed back. - The same harnesses also ran on the pre-squash head, with the same results. `mt7612u_ap_onair` showed one flaky open-cell ping run there, which passed on three repeats. `mt7612u_sta_uplink` was INCONCLUSIVE there and on master alike. That is the multi-entry loss under #461, which this PR doesn't address. - **Review record:** qodo-gate flagged three threads on 6b31d5f. All three held, and are fixed in `fcdec63`, `92dfa1d` and `18f0a20`. ## Blast radius - **Library:** the three Jaguar `Stop()`s only. Each clears `_station_ready`, and only the `SetStationIdentity` gate reads it. Every bring-up already clears it on entry and sets it again at the end. - **Everything else** is `src/mt7612u/tools/` (the bring-up tool), `examples/tx/` (txdemo), `tests/` and `docs/`. - **One event-schema addition:** txdemo's `tx.frame` gains a trailing `t`. Every in-tree consumer either matches on `"n"`/`"rc"` or parses the JSON, so none of them changes. - **`src/IRtlRadio.h` and `src/StationArm.h`:** comment-only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3 --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jaguar1/2/3: the Realtek station arm for IRadio::SetStationIdentity
First library item of #461:
SetStationIdentity/ClearStationIdentityon Jaguar1, Jaguar2 and Jaguar3, plus the on-air cell that measures both halves ofstation_mode_ok's bar on a Realtek die. Both halves passed with their controls on one RTL8812CU and one RTL8812BU, sostation_mode_okis true on the 8822C and 8822B dies only and stays false on every other Realtek die.What changed
src/StationArm.h(new) - the per-device state behind the seam on the three generations that share the port-0 register map, over a station recipe added tosrc/AckResponder.h(arm_station/station_is/restore_station): gate closed, MACID = own, BSSID = the AP, net_type = Infra, all read back; Clear restores the exact pre-arm MACID, BSSID and net_type bits and reads them back. The receive filter is left alone (monitor RCR; asrc/stastation filters in software, as the MT7612U one does).SetStationIdentity/ClearStationIdentityoverrides, each under the backend's existing port-0 lock. Where the MT7612U arm differs, the header says why:own; the arm has to write it (StationArm.h, "CONFIGURE, NOT CHECK").is_unicastalone passes 00:00:00:00:00:00, which would program MACID = 0 (an existing arm stays in place, as on the MT7612U); until Init/InitWrite has made its last port-0 write; while a beacon or an ACK responder owns port 0 (the beacon's own record on Jaguar2/3, not net_type, including a beacon whose failed start left its enables set), and any readback mismatch - rolled back and verified, or left recorded so Clear can retry it.SetAckResponderandStartBeaconreturn false, and on Jaguar2/3ClearAckResponderleaves the port alone (its gate-only clear would close the station's net_type). IRadio leaves the rule backend-specific; the MT7612U drops its arm instead.Stop()and the destructor clear it, because Jaguar2's teardown does not power the chip down and Jaguar1's power-down is optional - otherwise MACID = own / Infra goes on acknowledging after the process has gone. A re-Init/InitWriteof an armed object clears it before the bring-up; a clear that does not verify keeps the record, soClearStationIdentitycan retry it (Jaguar2/3 bring-up does not reprogram MACID or net_type). The beacon disable leaves net_type alone while a station holds it.tx.retry_limit(tx_retry_limit_okfalse), it says the die sends every unicast once._station_ready: cleared at the top of Init/InitWrite, committed after that bring-up's last port-0 write, and left false by a bring-up that throws. It is not_brought_up, which goes true mid-bring-up because the tail's own setters (CCA, crystal cap) apply live only once it is set. Before or after StartRxLoop is the same._reg_mublocking on the RX thread, which in the Async ring is the libusb event thread. A_reg_muholder doing synchronous USB I/O (the coex tick already, and now the station arm) then waits for that thread forever: IRadio'sStartRxLooplock rule, broken inside the library. Both nowtry_lock. The CFO tracker takes the lock before it steps, so a skipped tick does not feed its polarity detection a step that was never written. The BF apply logs a debug line when it skips, and when its register access fails; the next CBR retries. Fixed from reading the code; the same shape was caught in gdb in a caller on the station branch, but this in-library instance has not been reproduced on hardware.DEVOURER_STA_IDENTITY=<own|self>,<bssid>(+DEVOURER_STA_CLEAR_AFTER_MS) on rxdemo, and on txdemo withDEVOURER_TX_WITH_RX=thread: calls the seam once the RX loop is up and emitssta.arm/sta.clear(examples/common/station_arm_env.h,docs/logging.md). It means the same on every backend: it calls the seam and reports the return value. An arm is refused (sta.armwhy:"rx_not_running") when the demo's RX worker has failed or ended, and txdemo waits for its RX loop's first frame (3 s cap) rather than a fixed delay. txdemo sends nothing after a refused arm, and rxdemo's PCIe path refuses the knob rather than running unarmed.tests/realtek_station_onair.sh- the on-air cell (below). An arm is scored only when its transmitter kept airing through the window (no gap overMAX_GAP_MSbetween its CCX reports or after the last one; the finaltx.statsnow carriestfor that), submitted at leastMIN_SUBMITTEDframes (default: a quarter of the nominalSECS/GAP_USrate, 500), and the receiver was still running at the end of it. Reception in arm A is judged against the frames the peer reported, which aired, with the submitted-but-unreported count printed beside it.tests/station_arm_selftest.cpp- cteststation_arm, gated on any Jaguar1/2/3 option being built.AdapterCaps::station_mode_okis TRUE on the 8822C (Jaguar3) and 8822B (Jaguar2), scoped by variant. It is FALSE on the 8822E, the 8821C, every Jaguar1 die, Kestrel and the RTL8733B. The declaration carries the numbers and their limits;docs/realtek-station-arm.mdholds the full bench record.Why
#461: "
SetStationIdentityreturns the not-portedfalseon Jaguar1/2/3, Kestrel and the RTL8733B ... the seam needs its own measured arm per family, with the same two halves of the bar." On Realtek the ACK engine matches address 1 against MACID and net_type gates it, so the arm is a configuration, not the MT7612U's check, and the port is shared with two existing claimants.What is measured
tests/realtek_station_onair.shasks the transmitter in both halves: a devourer Realtek peer's per-frame CCX reports for frames sent TO the armed station, and the station's own CCX reports for frames it sends. Unlike the MT7612U cells, it arms through the seam itself. Rig: ch6, near field, retry limit 12, MCS3. One RTL8812CU (8822C) and one RTL8812BU (8822B) served as each other's peer, with an MT7612U on mt76x2u running hostapd as the AP. rtw88 was not blacklisted: the demos detached it, and every adapter was handed back. There are three records on this rig: the first on the arm's first version (A to G), a second that added arm H, and the current one on this head (fc7eb72, after both review rounds), scored by the stricter harness. Every run exited 0 with 5 passed, 0 failed, 0 inconclusive, every arm readlive=1, and the records match arm for arm within a few frames and a few hundredths of a retry. The full first record is indocs/realtek-station-arm.md.Current record (fc7eb72). Each cell is reports / submitted, ok%, mean retries, then rx_distinct where the arm counts reception:
The largest gap between reports was 11-74 ms on every arm, against the 2000 ms liveness limit. The maintainer's run on his own rig (8812CU and 8812BU, at 6737727) has the same shape, H included.
The arm is what the DOWN half needs, not the UP half. Arm H, the uplink with the station NOT armed, was ACKed 100% on both dies, at the same retries as armed F. So on Jaguar2/3 the AP acknowledges by address whether or not the station is armed. Unarmed (D) or cleared (E), the DUT ACKs nothing addressed to it. The flag rests on both halves being met while armed, which is how a station runs. H shows the uplink half holds without the arm too.
The adversarial side:
Small sample. One unit per die, two runs per arm on one rig (one for H), near field, one channel, one AP type (an MT7612U running hostapd), and unassociated throughout.
"Received" is not the discriminator. In arm A it is the DUT's
rx.seqcount of distinct frames from the peer. The controls received too: B, D and E show about 870–1110 frames on the CU and about 86–93 on the BU. The station runs promiscuous, so it receives frames not addressed to it, and while unarmed. It just does not ACK them. What the arm changes is the ACK.Submitted exceeds reports on every arm.
That fits frames still queued in the chip when the window closes, since each unacknowledged frame airs 13 times before its report. It is not proven. In arm A, rx_distinct equals the report count on the BU and is one short of it on the CU.
The 8822E is not measured by this cell. Its flag stays false.
The station branch's earlier numbers are not used. It ran these dies through an associated client that is not in this PR, and recorded duplicate ratios: armed 2/24111 against unarmed 42122/14126 on the 8812CU, and 13/24115 against 35117/11744 on the 8812BU. That evidence was indirect, and its uplink half had no control. The flag rests on the cell above.
Jaguar1 has no cell here. On the station branch, the 8812AU's unarmed control did not discriminate (38 duplicates against 27), because bring-up programs the EFUSE MAC into MACID.
What it can't do
IRadio.Follow-ups
EXPECT_UNARMED_SILENT=0on the 8812 (its control is predicted not to discriminate).src/stacaller (Follow-ups left open by the station identity seam (#460) #461, third library item) - the associated end-to-end run the station branch's table came from.Verification
-DDEVOURER_MT7612U=ON -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON: 82/82,station_armincluded; the same under-DDEVOURER_SANITIZE=address+undefined: 82/82, no reports.station_armis registered only where a Jaguar backend is built.bash -nandshellcheckclean ontests/realtek_station_onair.shandtests/mt7612u_sta_lib.sh(comment-only change).sudo DUT_PID=0xc812 DUT_SYSFS=5-1 PEER_PID=0xb812 PEER_SYSFS=8-1 AP_SYSFS=1-1 tests/realtek_station_onair.shReview record
qodo's automatic pass (7 threads), each verified against the code and fixed:
usable()now requireslive=1- no silence overMAX_GAP_MSbetween reports or after the last one.Initfailed: the worker publishes its end and the arm is refused.kill -0passes on an exited, unreaped child: liveness reads the process state from/proc/PID/stat(Z is dead).retire()dropped the snapshot when its clear failed: it now keeps the record so a later Clear retries.tests/station_arm_selftest.cppcovers both outcomes.Maintainer review:
_brought_upgoes true before the BF beamformee identity write into 0x0610 and the configuredSetAckResponder. ASetStationIdentityfrom another thread in that window armed. The beamformee write then overwrote MACID while the station still read as armed, or the config arm was refused and Init threw. The arm now waits for_station_ready, which each Init/InitWrite clears at its start and commits only after its last port-0 write, on all three generations (src/jaguar{1,2,3}/RtlJaguar*Device.{cpp,h}). It is a separate flag rather than a later_brought_up, because the tail's ownSetCcaMode,SetXtalCapandSetAmpduModeapply live only once_brought_upis true; committing that later would change what bring-up does. Not changed, and as before:Stop()clears an arm but leaves the flag set, so an arm afterStop()is accepted, as it was under_brought_up. The ORDERING clause insrc/StationArm.his reworded to match. No selftest reaches it: the flag lives in the backends, and only a device exercises them.ownwas accepted.station_args_ok(src/AckResponder.h) now refuses a zero address on either side. Selftest: zero own, zero BSSID, and a zero own against a live arm, which writes nothing and keeps the arm.MIN_REPORTScould still score. Each arm now also needsMIN_SUBMITTEDsubmissions (tests/realtek_station_onair.sh).🤖 Generated with Claude Code
https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3