diff --git a/CMakeLists.txt b/CMakeLists.txt index 216830ac..ec19b65f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -387,12 +387,14 @@ if(DEVOURER_MT7612U) src/mt7612u/phy.cpp src/mt7612u/radiotap.cpp src/mt7612u/rx.cpp + src/mt7612u/station.cpp src/mt7612u/tx.cpp src/mt7612u/usb.cpp src/mt7612u/Mt7612uRadio.cpp src/mt7612u/Mt7612uRadio.h src/mt7612u/Mt7612uMapping.h src/mt7612u/Mt7612uRxQueue.h src/mt7612u/Mt7612uTsfRead.h + src/mt7612u/StationIdentity.h src/mt7612u/internal.h src/mt7612u/regs.h src/mt7612u/initvals.h @@ -1098,6 +1100,22 @@ target_include_directories(BssTableSelftest PRIVATE target_compile_features(BssTableSelftest PRIVATE cxx_std_20) add_test(NAME bss_table COMMAND BssTableSelftest) +# The MT7612U station-identity decision and the ownership hand-off +# (src/mt7612u/StationIdentity.h), in the style of +# tests/ack_responder_selftest.cpp. The policy is split from the register I/O +# so it can run here: its failure modes (a failed read that fails open, a +# failed read-back taken for a moved identity) are decisions, not writes, and +# are not visible in a register trace. Hardware coverage is +# `mt7612uprobe staid`. Header-only and pure, so it needs no link against the +# library. +if(DEVOURER_MT7612U) + add_executable(Mt7612uStationSelftest tests/mt7612u_station_selftest.cpp) + target_include_directories(Mt7612uStationSelftest PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/mt7612u) + target_compile_features(Mt7612uStationSelftest PRIVATE cxx_std_20) + add_test(NAME mt7612u_station_identity COMMAND Mt7612uStationSelftest) +endif() + # Headless guard for the TX quiesce seam (ITransport::quiesce_tx via # RtlAdapter): the explicit "stop TX and wait it out" call every device makes # before anything is released. UsbTransport's cancel/drain is validated on diff --git a/docs/logging.md b/docs/logging.md index 01fbeecc..90accf87 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -77,7 +77,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets]; | ev | emitter | fields | |---|---|---| | `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, tx_retry_limit, he_er_su, per_chain_rssi, hw_rx_tsf, hw_beacon_txtsf, tsf_write, xtal_cap_max, xtal_cap_default | +| `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, he_er_su, per_chain_rssi, hw_rx_tsf, hw_beacon_txtsf, tsf_write, xtal_cap_max, xtal_cap_default | | `debug.wreg` | L (`DEVOURER_LOG_WRITES`) | addr "0x0nnn", width, val "0x…" | | `hop.prof` | L (`DEVOURER_HOP_PROF`) | gen, ch, `_us`…, total_us | | `tx.fail` | L (send failure; regress.py keys on it) | {status, actual_len, timeout} or {rc, timeout} | diff --git a/docs/mt7612u-station-identity.md b/docs/mt7612u-station-identity.md new file mode 100644 index 00000000..75d0f56e --- /dev/null +++ b/docs/mt7612u-station-identity.md @@ -0,0 +1,302 @@ +# What an MT7612U station needs programmed + +The measurement record behind the MT7612U half of `IRadio::SetStationIdentity` +and behind `AdapterCaps::station_mode_ok`. The contract lives at those +declarations (`src/IRadio.h`, `src/AdapterCaps.h`) and at +`mt7612u_set_station_identity()` (`src/mt7612u/include/mt7612u/mt7612u.h`); +the decision logic is `src/mt7612u/StationIdentity.h`, covered headlessly by +ctest `mt7612u_station_identity` (`tests/mt7612u_station_selftest.cpp`) and on +hardware by `mt7612uprobe staid`. This page holds the numbers. + +Two questions had to be measured rather than read off registers: + +- **BSSID** — does programming the joined BSSID (`MT_MAC_BSSID`, the + `MT_MAC_APC_BSSID` slot table) change what a *managed station* receives, and + is a wrong value silent, harmless or fatal? +- **Auto-ACK** — does this MAC acknowledge unicast addressed to its own + address with nothing armed, and what does moving `MT_MAC_ADDR` (the port + identity, which the ACK responder and the beacon path also write) do to it? + +Rig for every cell: DUT = MT7612U, own MAC `40:a5:ef:5a:32:f8`, driven by +`mt7612uprobe` (not through `IRadio`); channel 6, near field, one DUT. + +## Withdrawn numbers — read this first + +The first run of the BSSID gate called `mt7612u_set_monitor_rx()` in every +arm. That function writes `MT_RX_FILTR_CFG = PHY_ERR|CRC_ERR` and nothing else +— every address and BSS drop bit off — so all six arms ran promiscuous and +were identical by construction. Its null result is withdrawn. The reasoning +that let it through was also wrong: in the managed filter `0x00015f97`, bit 3 +(`OTHER_BSS`) is clear but bit **2** (`PROMISC`) is set, and mt76 maps bit 2 +to `FIF_OTHER_BSS`. The gate now leaves the managed value `mt_mac_start()` +programs, prints it per arm, and flags an arm that is not running it. + +Also withdrawn: a "0.8% retried vs 98% control" auto-ACK figure from the +probe-response method (below), whose control ran with the monitor filter and +retargeted `MT_MAC_ADDR` at the same time. Everything below is re-measured +under the managed filter. + +## Which APC slot a station's BSSID lives in + +mt76 keys the slot on the station's **own** address, not on the BSSID: +`mt76x02_add_interface()` gives an interface index 0 (or `1 + (((base[0] ^ +addr[0]) >> 2) & 7)` for a locally administered own address) and adds 8 for a +station; `mt76x02_bss_info_changed()` then writes the AP's BSSID into APC slot +`idx & 7`. The base is `MT_MAC_BSSID`, which mt76's station configuration +leaves equal to the station's own address. For this DUT (factory address +`40:a5:ef:5a:32:f8`, not locally administered) that is **slot 0**. The gates +compute it with `sta_station_slot()` (`src/mt7612u/tools/bringup.cpp`). + +The first two runs below used the AP-side rule instead — the one +`beacon.cpp` applies to an AP's own address, which for an AP is the BSSID — +applied to the station's BSSID, giving slot 1. Slot 1 is not where a station's +BSSID lives, so every row that wrote the "derived slot" tested a slot the +station does not use. Those rows are marked below and are **not evidence**. +The third run (auto-ACK arm E) uses the station slot. + +## BSSID: `MT_MAC_BSSID` and slot 0 do not gate a managed station's receive + +`sudo AP_SYSFS=6-1 DUT_SYSFS=7-1 CH=6 tests/mt7612u_sta_identity.sh` +(`mt7612uprobe sta`). AP = RTL8812AU on the in-tree rtw88 driver, hostapd +2.10, BSSID `02:42:75:05:d6:aa`. Unicast at the DUT comes from a monitor vif on +the AP's phy (`tests/sta_unicast_inject.py`); hostapd sends an unassociated +station none. 20 s per arm; no arm touches `MT_MAC_ADDR`. + +| arm | configuration | unicast to us (first run) | (re-run, AP = RTL8812BU) | +|---|---|---|---| +| A | init only, nothing programmed | 6250 | 6478 | +| B | `MT_MAC_BSSID` = AP | 5877 | 5875 | +| C | APC slot 0 = AP | 5878 | 5857 | +| D | APC slot 1 = AP *(not the station slot)* | 5891 | 5858 | +| E | `MT_MAC_BSSID` + slot 1 = AP *(slot: not evidence)* | 5888 | 5870 | +| **F** | **`MT_MAC_BSSID` WRONG** + slot 1 wrong *(slot: not evidence)* | **5877** | **5909** | + +`filtr=00015f97` in every arm of both runs. Arms D-F of these two runs wrote +slot 1, which is not the station's slot, so those rows say nothing about the +slot table. + +Against these two runs: + +- The second run inherited state. Its D/E/F rows show slot 1's BIT(16) SET, + left behind by the auto-ACK harness's `bssen` arm in an earlier process + (the chip keeps registers across bring-up tool runs, and the gate reset only + the address halves). The gate now clears the whole slot, enable bit + included, and checks every other slot reads empty. +- The first run's `MT_MAC_BSSID` was not read back against the value written. + The gate now reads the base and every slot back after `mt_mac_start()`, and + an arm that does not read back as written makes it INCONCLUSIVE. So does an + all-zero `to_us` column. + +### On the station-slot gate + +The gate as it now stands: station slot 0, every write read back, the +stimulus started only after the gate reports its bring-up done. Rig: DUT +MT7612U at 480 Mbit/s, AP RTL8812BU on rtw_8822bu on a USB3 port, hostapd, +ch6, near field; the injector achieved 35964 frames in 124 s (about 290/s of +300 asked). No MCU timeouts. + +| arm | configuration | rx_total | from_bss | beacons | unicast to us | vs A | +|---|---|---|---|---|---|---| +| A | init only, nothing programmed | 6591 | 6470 | 193 | 6277 | - | +| B | `MT_MAC_BSSID` = AP | 6196 | 6050 | 193 | 5857 | -6.7% | +| C | APC slot 0 = AP | 6194 | 6055 | 194 | 5861 | -6.6% | +| D | station slot (mt76 rule, = slot 0) = AP | 6151 | 6040 | 194 | 5846 | -6.9% | +| E | `MT_MAC_BSSID` + slot 0 = AP | 6145 | 6046 | 194 | 5852 | -6.8% | +| **F** | **both WRONG** (base and slot 0) | 6261 | 6193 | 190 | **6003** | -4.4% | + +Every write verified, every other slot empty, BIT(16) clear in C-F, +`filtr=00015f97` in every arm, and the gate reported "measured". A +deliberately wrong BSSID in both the base and the station's slot (F) received +as much as the correct ones (B-E) - slightly more. + +What "no gating" can and cannot mean at this spread: + +- It can mean: neither register decides whether a managed station accepts + unicast addressed to it. A gate would show as a collapse of F (or of A, + where both are empty), not a few percent. +- It cannot mean: the registers are without effect. B-F sit 4-7% below A, + and most of that drop is B-E against A. That is the same first-arm excess + every run of this gate has shown (6%, 10%, and now 6.7%), unexplained; with + one run per arm it cannot be separated from ambient drift, and a real + effect of a few percent would hide inside it. +- The receiver measured is the managed filter `0x00015f97`, not the monitor + filter the library's own RX loop installs. One unit, one AP, one run per + arm; BIT(16) was clear in every row here, and the enabled-slot case rests + on the auto-ACK harness's arm E (acknowledgement, not reception). + +### Runs that measured nothing: the rtw88 AP stalls (inferred) + +Twice, on the station-slot gate before the stimulus ordering was added, the +gate's bring-up logged eight `mcu command timed out waiting for response` +(its channel calibrations), every arm then read 0-8 frames and no beacons, +and the gate returned INCONCLUSIVE, as it should. The AP (RTL8812BU on +rtw88) sat on a hub port that enumerated at FULL SPEED (12 Mbit/s); its +transmit path stalled at about 30 frames/s of the 300 asked (about 230/s in +the good run) and its beacons stopped, while the DUT's calibrations timed out +during the flood - the late-reply failure `mcu.cpp` documents under a strong +nearby transmitter. With both adapters on high-speed ports the same harness +then measured cleanly (above). + +**The full-speed hub is not the whole story.** On the second unit's rig (below) +the AP - a TP-Link T3U, also RTL8812BU on rtw88, at a SuperSpeed root port - +stalled mid-table: `rtw88_8822bu: failed to get tx report from firmware` in +the kernel log, the injector at 12289 frames in 123 s (about 100/s), beacons +down to about 120 per arm, and `to_us` 0 from arm D on. So the rtw88 8822bu +transmit stall happens on a fast port too; a full-speed hub makes it likely, +it does not explain it. The good run on the earlier harness also had the AP at +the full-speed position. The cause is inferred, not proven, and it is on the +AP side: the DUT-side code before the timeouts is identical between the runs, +and no kernel driver touched the DUT during them. + +Two guards stay in the harness: the stimulus starts only after the gate's +bring-up, and a table the injector fed at under half its rate is refused as +an AP-side stall - which is what refused the second unit's table. The harness +header states the rig requirement as necessary, not sufficient: both adapters +at high speed or better and no full-speed hub, and a stalled run is re-run, +not read. + +This is the opposite of the AP-side finding in `docs/mt7612u-ap-mode.md` +(a wrong APC slot "beacons perfectly, acknowledges nobody"), which is about +acknowledgement, not reception; the two do not conflict. + +## Auto-ACK: acknowledged with nothing armed, and `MT_AUTO_RSP_EN` is the gate + +`sudo tests/mt7612u_sta_autoack.sh`. The instrument asks the *transmitter*: a +Realtek peer (RTL8812CU; Jaguar3 drains C2H off its coex runtime) injects +unicast QoS-Data at the DUT through `txdemo` (`DEVOURER_TX_QOS_DATA=1`, +`DEVOURER_TX_RA=`, `DEVOURER_TX_REPORT=1`, `DEVOURER_TX_RETRY_LIMIT=12`) +and reads its own per-frame CCX `tx.report`. The DUT receives under +`mt7612uprobe norsp` (arms A, D) or `bssen` (arm E); A and D share one code +path and filter and differ by one bit. + +| arm | first run: reports / ok / retries | second run: ok / retries | third run (station slot): ok / retries | +|---|---|---|---| +| **A** — DUT receiving, **nothing armed** | 1279 / **100.0%** / **0.45** | 845/845 / 0.12 | 858/858 / 0.04 | +| B — destination nobody holds | 400 / 0.0% / 12.00 | 0% / 12.00 | 0% / 12.00 | +| C — DUT not running | 400 / 0.0% / 12.00 | 0% / 12.00 | 0% / 12.00 | +| **D** — DUT receiving, `MT_AUTO_RSP_EN` **cleared** | 400 / 0.0% / 12.00 | 0% / 12.00 | 0% / 12.00 | +| **E** — wrong BSSID in the enabled **station** slot | *(slot 1: not evidence)* | *(slot 1: not evidence)* | **867/867** | + +The report counts differ because an acknowledged frame retires at once while +an unacknowledged one holds the descriptor for 12 retries; the comparison is on +`ok`, a ratio. A vs B and C: the DUT acknowledges unicast to its own address, +and only its own. A vs D: `MT_AUTO_RSP_EN` gates it — which is why the seam +refuses when that bit is clear. + +**Arm E: a wrong BSSID in the enabled station slot does not gate the +station.** The first two runs wrote slot 1 (above) and are not evidence. The +third run's DUT log reads `WRONG BSSID 02:00:00:de:ad:02 in station APC slot +0, BIT(16) SET (high reg 000102ad), other slots empty, MT_MAC_BSSID = own +address (verified)` - the slot mt76 would program, enabled, holding a BSSID +nobody has, with `MT_MAC_BSSID` left where mt76's station configuration leaves +it - and the peer's frames were acknowledged 867/867. So the BSSID plane does +not gate acknowledgement for a station on this part even with the enable +set. Against it: one run, one peer, one DUT, and acknowledgement only - no +reception count was taken in that arm. + +Against it: one peer, one DUT, one run per arm per session. A monitor-filter +run of the same arms (arm A with `mt7612uprobe arx`, so A and D then differed +in filter and init path as well as the bit) read A 100% / 0.10 (887 reports), +B/C/D 0% — the same shape, but not a single-variable A/D pair. + +**Two methods that cannot answer this**, kept so they are not retried: +capturing the DUT's ACKs on a monitor vif on the peer's own phy read zero with +the DUT present *and* absent (a radio cannot hear an ACK to its own +transmission; mac80211 injects no-ack); and counting retried probe responses +(`mt7612uprobe staack`) cannot fail, because its single-variable control +(clear `MT_AUTO_RSP_EN`) does not move — hostapd does not retransmit an +unacknowledged probe response. + +`staack`'s arm C is still evidence of something else: with `MT_MAC_ADDR` +retargeted under the managed filter, the DUT's reception of the AP's unicast +went from 103 frames to **zero**. The port identity gates what a managed +station receives, not only whether it acknowledges — the reason the seam must +not write it, and must refuse when something else holds it. + +## Uplink: what the station transmits is acknowledged + +`sudo tests/mt7612u_sta_uplink.sh`. The DUT's own `MT_TX_STAT_FIFO` +(`mt7612uprobe txs`, `docs/mt7612u-tx-retry.md`) gives the per-MPDU retry +count; the peer is a Realtek adapter running `rxdemo` with +`DEVOURER_ACK_RESPONDER`. The row is the gate's arm `d` — unicast from the +DUT's own address, ACK requested — from its "MAC receiver ON" table (with the +receiver off the MAC cannot hear an ACK at all). + +| arm | first run | second run (limit programmed) | third run (limit programmed) | +|---|---|---|---| +| **A** — peer answers for the address we transmit to | **200/200**, 0.0 retries (max 1) | 200/200, 0.0 retries | 200/200, 0.0 retries (max 1) | +| B — peer answers for a *different* address (control) | 0/200, 16.0 retries | not completed (outer timeout) | 0/157, 16.0 retries | + +The first run used the chip's **initvals retry limit** (short limit 15). The +harness now passes `DEVOURER_TX_RETRY_LIMIT=15` explicitly and requires the +gate's read-back line; the re-run confirmed it (`retry limit set to 15 +(MT_TX_RETRY_CFG 47f00f0f)`). The second run's control arm B was cut off by +an outer timeout: each harness arm runs the whole eight-arm gate twice at +about 6 frames/s, about 9 minutes for arm A at FRAMES=200 and longer for B, +where nothing is acknowledged; the script header carries the runtime budget. +The third run completed both arms; its control settled 157 of 200 status +entries (the UNSETTLED floor below). It says nothing about a default library session, which airs NOACK +stream radiotap and `tx.retry_limit` 0 and so sends each unicast once — a +station session must request ACKs and set a nonzero limit +(`IRadio::SetStationIdentity`; `Mt7612uRadio` warns at arm time when the limit +is 0). + +Against it: arm B trips the gate's UNSETTLED marker by construction (every +frame runs the full ladder, and the 16-slot status ring cannot keep up). It is +accepted only as a floor: misattributed entries could come only from the +neighbouring 200/200 arms, so contamination can only make B look *better*. The +harness refuses an UNSETTLED arm that claims success. Peer is an ACK +responder, not an AP; one run per arm per session. + +## Second unit, second rig + +The maintainer's run: DUT MT7612U Comfast CF-922AC (`40:a5:ef:5f:65:51`, USB3 +hub port 4-2.3.2), peer RTL8812CU (0bda:c812, high-speed port 3-2.4), AP +TP-Link T3U (RTL8812BU) on rtw88 at a SuperSpeed root port, hostapd 2.11, ch6, +near field, one run per arm. + +| cell | second unit | first unit (b94e119) | +|---|---|---| +| `mt7612uprobe staid` | 12 passed, 0 failed | 12/12 | +| autoack A (nothing armed) | 1706/1706, 0.01 retries | 860/860, 0.06 | +| autoack B, C, D | 0% at 12.00 | 0% at 12.00 | +| autoack E (wrong BSSID, enabled station slot 0) | 1695/1695, 0.02 | 873/873, 0.11 | +| uplink A (FRAMES=60, limit 15 read back) | 60/60, **1.9** mean retries (max 3) | 200/200, **0.0** (max 1) | +| uplink B (control) | 0/60 at 16.0 (UNSETTLED floor) | 0/157 at 16.0 | +| staack arm C (`MT_MAC_ADDR` moved) | received nothing | received nothing | +| `sta` (BSSID) table | **not reproduced**: A 6141, B 5747 (−6.4%), then the AP stalled from arm C; the harness refused the table | A 6268 … F 6018, measured | + +What the second unit reproduces: the auto-ACK half entire (claim, all three +controls, and arm E), the uplink claim and its control, and the arm-C +deafness. What it does not: the BSSID receive table, lost to the rtw88 AP +stall above (no non-rtw88 AP was available on that rig). + +Against it, and against the comparison: + +- **The uplink's mean retries differ between units: 1.9 against 0.0.** Both + acknowledged every frame, so the claim holds on both, but 1.9 retries per + frame is not "answered at once". One run per unit, different peers' + placement and different RF; it is not explained here. +- B is 6.4% below A on the second unit's partial table - the same unexplained + first-arm excess every run shows. +- Two units, one peer model, one channel, near field, one run per arm. + +## What is not established + +- **The library's own RX path does not run the managed filter.** + `Mt7612uRadio::StartRxLoop` installs the monitor filter unconditionally, so + a station driven through `IRadio` runs promiscuous. Acknowledgement holds + there (the monitor-filter auto-ACK run above), but "moving `MT_MAC_ADDR` + makes a station deaf" is a managed-filter property: under the monitor filter + it would keep receiving and stop acknowledging. A role-selected managed + filter is not implemented. +- **No cell drove `SetStationIdentity` through `IRadio`.** The seam writes no + register - `mt7612uprobe staid` reads `MT_MAC_ADDR`, `MT_MAC_BSSID` and + all eight APC slots before and after arming and clearing and checks them + unchanged - so the measured state is what a successful arm leaves behind, + but "arm the seam, then measure" is unexercised here. +- **Every cell is an unassociated station** receiving traffic it did not + negotiate: power save, TIM parsing, cross-BSS duplicate detection and + hardware key lookup are untested. +- Two DUTs for the auto-ACK, uplink and arm-C cells, one for the BSSID + table; one peer model, one channel, near field, no soak. diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 7197e311..a796a43b 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -380,11 +380,11 @@ and nothing was ever queued on the healthy path. ## Offline tests -`ctest` runs seven MediaTek cells. The first four are C++ and need neither +`ctest` runs eight MediaTek cells. The first four are C++ and need neither hardware nor the `DEVOURER_MT7612U` option — the code they cover is header-only. The fifth is C++ and needs the option, because it calls the -subtree's own symbols. The last two are Python and need the `reference/mt76` -submodule: +subtree's own symbols. The sixth is header-only C++ gated on the option. The +last two are Python and need the `reference/mt76` submodule: | cell | what it holds | |---|---| @@ -393,6 +393,7 @@ submodule: | `mt7612u_rx_queue` | the RX hand-off queue's two load-bearing properties: a full ring drops the **newest** frame and counts it, and a popped slot outlives the queue lock. Broken, the first reorders frames or wedges the part and the second is a use-after-free — and both look like a healthy link until a packet processor falls behind | | `mt7612u_tsf_read` | that the TSF read stays coherent when the low word wraps between any two of its register accesses: a scripted counter swept across the wrap one microsecond at a time, every failure position, and an all-ones low word as a value. The pre-fix DW0,DW1 order runs against the same sweep and must tear, or the cell cannot see the bug. Mutation-tested by hand three ways — no retry, a retry that keeps the first high word, a retry that skips the low-word re-read — each fails | | `mt7612u_tsf_api` | that a failed TSF read stays distinguishable from a value at the C entry points: `mt7612u_read_tsf_chk` refuses a NULL device or output with -1 and does not write through the pointer, and `mt7612u_read_tsf` answers 0. `0xffffffff` is a legitimate register word here, so only the return code can carry a failure. Not free: deleting the guard in `mt7612u_read_tsf_chk` segfaults the cell (hand-run). Reaches the NULL refusals only; a failed transfer on a live device, the `Mt7612uRadio::ReadTsf` throw and the `tsf_write` caps bit need the part (`bringup caps` prints it, and `Mt7612uRadio::GetAdapterCaps` takes `tsf_write_ok` from it rather than restating it) | +| `mt7612u_station_identity` | the station-identity decision and ownership hand-off (`src/mt7612u/StationIdentity.h`): both register reads fail closed, a failed port read is reported as a failed read rather than a mismatch, argument checks run before any I/O, the port comparison is tri-state (a failed read is UNKNOWN, never DIFFERENT), a verified move drops the arm, an unreadable one keeps it, and a failed start that unwinds restores it. Header-only, gated on `DEVOURER_MT7612U`, which the MediaTek CI jobs set. The hardware counterpart is `mt7612uprobe staid` (`docs/mt7612u-station-identity.md`) | | `mt7612u_usb_ids_vs_mt76` | that the id table above really is the complete `mt76x2u_device_table` from the pinned reference, byte-checked. An id we *forgot* is invisible to `mt7612u_usb_ids` — the adapter just falls through to the Realtek path — and the first draft of that header had 11 of the 16, taken from the host's kernel tree | | `mt7612u_initvals_generated` | that the checked-in `initvals.h` still matches what `tools/extract_mt7612u_tables.py` produces from the pinned reference | @@ -420,7 +421,7 @@ nothing. Each fails the cell, and each names the property it broke. | test | what it holds | |---|---| -| `api_link` | takes the address of all 36 public entry points while including only the public header, so a declaration that loses its definition is a link error. Still compiled as C, which is what keeps the `extern "C"` guard honest now the library itself is C++ | +| `api_link` | takes the address of all 39 public entry points while including only the public header, so a declaration that loses its definition is a link error. Still compiled as C, which is what keeps the `extern "C"` guard honest now the library itself is C++ | | `frame_shape` | `mt_hdrlen_from_fc()` over management, all eight control subtypes and the five data shapes; the RX L2-pad fold on a synthetic QoS frame, with a negative control that redoes the old fixed-24 fold and asserts the QoS Control really is destroyed; the radiotap VHT bandwidth mapping over all eleven codes the part can express; the txwi pktid for a normal send (0), `MT_TXOPT_TXS` alone and `MT_TXOPT_PKTID(id)` | | `field_macros` | `MT_CTZ` against `__builtin_ctz` over all 32 single-bit and all 528 contiguous masks, plus a `FIELD_PREP`/`FIELD_GET` round-trip, plus a static initialiser that fails to compile if the macro stops being constant-foldable, plus the `MT_TX_RETRY_CFG` read-modify-write (both limits set, bits 16+ kept) | | `log_sink` | that `mt7612u_set_log_sink()` **diverts** rather than copies — stderr must stay silent while a sink is installed — that the sink gets the bare message with no prefix to double up, that every level letter arrives, and that NULL restores the built-in sink instead of silencing the library | @@ -580,13 +581,14 @@ Stated because the numbers above are uniformly favourable. firmware-running bit. - **The 48 ms fast retune is our implementation, not the floor.** The floor is unmeasured. -- **Five ctest cells run in CI, two more only on a bench, and the subtree's own +- **Six ctest cells run in CI, two more only on a bench, and the subtree's own four still run nowhere automatically.** With `DEVOURER_MT7612U=ON` the whole platform matrix (gcc, clang, MSVC, mingw, macOS) builds the subtree and the sanitizer job links it, so a portability or lifetime regression is caught. `mt7612u_usb_ids`, `mt7612u_mapping`, `mt7612u_rx_queue` and - `mt7612u_tsf_read` run on every configuration, and `mt7612u_tsf_api` - wherever the option is on, which the MediaTek CI jobs set. The TSF pair and + `mt7612u_tsf_read` run on every configuration, and `mt7612u_tsf_api` and + `mt7612u_station_identity` wherever the option is on, which the MediaTek CI + jobs set. The TSF pair and the queue cover backend *behaviour* rather than a lookup, and nothing else does. Bring-up, the teardown ordering, the 1 Hz tick and TX still have no automated coverage, diff --git a/examples/common/caps_event.h b/examples/common/caps_event.h index d8d5f370..59625ec5 100644 --- a/examples/common/caps_event.h +++ b/examples/common/caps_event.h @@ -82,6 +82,7 @@ inline void emit_adapter_caps(EventSink &sink, IRadio *dev) { .f("narrowband", c.narrowband_ok ? 1 : 0) .f("fastretune", c.fastretune_ok ? 1 : 0) .f("ack_responder", c.ack_responder_ok ? 1 : 0) + .f("station_mode", c.station_mode_ok ? 1 : 0) .f("tx_retry_limit", c.tx_retry_limit_ok ? 1 : 0) .f("he_er_su", c.he_er_su_ok ? 1 : 0) .f("per_chain_rssi", c.per_chain_rssi ? 1 : 0) diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index 21bed0cb..45cfd174 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -236,6 +236,55 @@ struct AdapterCaps { bool ack_responder_ok = false; bool tx_retry_limit_ok = false; + /* station_mode_ok: IRadio::SetStationIdentity can program this MAC for the + * STATION half of an infrastructure BSS, and the behaviour a station needs + * from the silicon has been measured on air. Gate station-mode callers on + * this rather than on SetStationIdentity's return value alone, so a caller + * can refuse before it starts a handshake it cannot finish. + * + * False means "not ported / not measured", never "the silicon cannot". Do + * not set it from a code-reading: the bar is an on-air cell showing this + * adapter receiving unicast addressed to it and being ACKed for what it + * sends - the same shape of evidence ack_responder_ok carries, measured per + * die. (The MT7612U acknowledgement cells use a raw injector and an armed + * ACK responder as the peer, not an AP.) + * + * TRUE on MT7612U, and read docs/mt7612u-station-identity.md - its + * retraction section first - before quoting a number from it. Both halves + * of the bar are measured there, with controls: a Realtek peer's own CCX + * reports show this MAC acknowledging 100% of unicast addressed to it with + * nothing armed (0.45 mean retries, 1279 reports) against three controls + * pinned at the peer's 12-retry limit (a destination nobody holds, the DUT + * absent, and MT_AUTO_RSP_EN cleared); and the MAC's own TX status FIFO + * shows its uplink acknowledged 200/200 at 0.0 mean retries against a + * 0/200 control run to the full ladder. The uplink cell sent from the + * bring-up tool with an ACK-requesting TXWI and a retry limit of 15 - not + * a library session, whose defaults (NOACK stream radiotap, tx.retry_limit + * 0) send each unicast once; see IRadio::SetStationIdentity. Note the + * limits the measurements do NOT clear, which a caller should know: + * + * - every cell ran an UNASSOCIATED station receiving traffic it had not + * negotiated, so power save, TIM parsing, cross-BSS duplicate detection + * and hardware key lookup are untested; + * - those cells did not drive SetStationIdentity itself. On this part the + * seam writes no register, so the measured hardware state is the state + * a successful arm leaves behind, but the literal "arm through IRadio, + * then measure" path is not what the cells ran; + * - the cells ran the MANAGED receive filter, and the library's own RX + * path does not: Mt7612uRadio::StartRxLoop calls + * mt7612u_set_monitor_rx() unconditionally, so a station driven through + * IRadio runs PROMISCUOUS. Acknowledgement does not depend on it (a + * monitor-filter run of the same auto-ACK cell also read 100%), but the + * "moving the port identity makes a station deaf" half of the rationale + * is specific to the managed filter; + * - two units, one peer model, one channel, near field, no soak; the + * second unit reproduced the acknowledgement and uplink cells (its + * uplink at 1.9 mean retries against the first unit's 0.0), not the + * BSSID receive table. + * + * FALSE on every other backend: not ported. */ + bool station_mode_ok = false; + /* --- feature flags --- */ /* Per-packet TX power: a per-frame power trim driven by radiotap * DBM_TX_POWER (dB delta vs the calibrated table / session base) or a diff --git a/src/IRadio.h b/src/IRadio.h index fad4c766..801a9be0 100644 --- a/src/IRadio.h +++ b/src/IRadio.h @@ -60,7 +60,30 @@ class IRadio { * StopRxLoop() is called or the global stop flag is set; it is restartable * after it returns. This is the piece that lets one process bring up once * (InitWrite) and then run TX and RX concurrently on the same claimed handle - * — Init is the RX-only convenience wrapper (bring-up + StartRxLoop). */ + * — Init is the RX-only convenience wrapper (bring-up + StartRxLoop). + * + * WHICH THREAD THE CALLBACK RUNS ON IS BACKEND- AND MODE-SPECIFIC. In the + * Realtek USB backends' default RxMode::Async ring it runs on the thread + * that drives libusb's event handling; under RxMode::SpscFat it runs on the + * ring's consumer thread; on the MT7612U it runs on the thread that called + * StartRxLoop while a C-layer thread pumps libusb. Keep to the lock rule + * below on every backend regardless: where the callback is not on the + * event thread the deadlock does not form, but a callback parked on the + * caller's lock still stalls the receive path behind it, and the mode is a + * configuration detail a caller should not have to track. In the Async + * case a synchronous USB transfer made from any other thread (every register + * read or write behind a control call - SetStationIdentity, the TX-power + * setters, SetMonitorChannel, ...) waits for that thread to come back out + * of it. So never make a device call while holding a lock the callback + * takes: the callback blocks on the lock, the call blocks on the callback, + * and neither returns. The mechanism is libusb's: a synchronous transfer + * completes only when some thread handles libusb events, and in the Async + * case the thread that handles them IS the RX thread - which is inside the + * callback, waiting for the caller's lock. How many transfers a call makes + * only changes how often it loses the race: a call that makes almost none + * hides the deadlock in testing rather than avoiding it. The same holds + * inside a backend: a callback path that takes a lock some other thread + * holds across USB I/O must try_lock rather than block. */ virtual void StartRxLoop(Action_ParsedRadioPacket packetProcessor) = 0; /* Ask a running StartRxLoop to exit (sets a flag; the caller then joins @@ -219,6 +242,86 @@ class IRadio { } virtual void ClearAckResponder() {} + /* --- 802.11 infrastructure station (client) identity --------------------- + * + * Program the MAC for the STATION half of an infrastructure BSS: this + * adapter is `own`, the AP it has joined is `bssid`. Arms whatever the + * silicon needs to receive that BSS's traffic addressed to `own` and to + * auto-ACK it. `ClearStationIdentity` returns to the pre-arm state. + * + * WHY THIS IS NOT SetAckResponder(bssid). The two look interchangeable and + * are not, at least on MT7612U, where the auto-response engine matches + * address 1 against the port identity register. Arming an ACK responder + * there *retargets* that register, so `SetAckResponder(bssid)` on a station + * would move the port identity to the AP's address and break ACK for the + * station's own traffic - the exact opposite of what a station needs. A + * station's port identity is its OWN address, which is where MAC bring-up + * already leaves it, so a correct implementation on that part must write + * the BSSID somewhere else and leave the port identity alone. Backends + * where one register genuinely serves both may implement this in terms of + * the other; they must not assume it. + * + * ORDERING. Call after the RX loop is running, not before. This is not a + * style preference: a backend may program the receive filter when the RX + * loop starts and overwrite anything an earlier call wrote (MT7612U does + * exactly this - see Mt7612uRadio::StartRxLoop). An implementation that + * cannot detect being called too early must say so at its declaration; + * one that can should refuse and log rather than arm something that will + * be silently undone. + * + * `own` and `bssid` must both be unicast (I/G clear) and must differ. + * Returns false when unsupported, when the arguments are refused, or when + * the arm cannot be read back. As with SetAckResponder, false is not proof + * of passive state: an implementation that cannot verify its own rollback + * logs that rather than claiming it. Clear is a non-throwing best effort + * and does not promise the MAC stops responding - a die that matches on an + * address alone will answer for whatever address is left programmed. + * + * A LATER PORT-0 CLAIMANT IS BACKEND-SPECIFIC, and a caller must not assume + * either rule. SetAckResponder, ClearAckResponder and StartBeacon all want + * the same port identity a station holds. A backend may REFUSE them while a + * station is armed (return false, station stays armed), or let them proceed + * and drop the station arm. The MT7612U does the latter: a verified move of + * the port identity drops the station arm with a WARN (a write that leaves + * it where it was drops nothing; a StartBeacon that fails and unwinds it + * back restores the arm), SetAckResponder returns true, + * and the station stops being acknowledged until it is re-armed - and under + * the managed receive filter stops receiving too (measured there: reception + * goes to zero). So after any of those calls on a live station, re-check + * the station arm (re-arm, or ClearStationIdentity) rather than inferring + * it from the call's return value. + * + * TRANSMISSION IS NOT PART OF THE ARM. This covers what the MAC receives + * and acknowledges. A station's own unicast (management, EAPOL, data) must + * request an ACK in its radiotap - build_stream_radiotap(mode, false), + * the no_ack argument; the default stream radiotap is NOACK, which never + * retries - and the hardware retransmits an unacknowledged frame only + * DeviceConfig::tx.retry_limit times, whose default is 0. A station + * session sets a nonzero limit explicitly; left at the default it sends + * each unicast frame once. + * + * Gate this on AdapterCaps::station_mode_ok rather than on a nullptr check; + * the default here returns false for every backend that has not ported it, + * which is all of them until a backend says otherwise. */ + virtual bool SetStationIdentity(const devourer::MacAddr &own, + const devourer::MacAddr &bssid) { + (void)own; + (void)bssid; + return false; + } + /* Returns whether the pre-arm state was restored AND verified. On a + * backend whose arm wrote nothing there is nothing to undo and this is + * trivially true; on one that moved a filter or a MACID, a false return is + * the only way a caller learns the rollback could not be confirmed - the + * same contract SetAckResponder's clear half carries. It is not `void` for + * exactly that reason: a void clear would make an unverifiable rollback + * unreportable on the backends where rollback is real. + * + * The not-ported default returns TRUE: its SetStationIdentity arms nothing, + * so there is nothing to undo - the "trivially true" case above. A false + * here would report an unverified rollback for a port nothing touched. */ + virtual bool ClearStationIdentity() { return true; } + /* 802.11 A-MPDU TX mode (src/AmpduMode.h): the first-class bundle of the * recipe the spike + pacing sweep proved on-air. When enabled, every data * frame is marked aggregatable (data QSEL + AGG_EN + MAX_AGG_NUM + diff --git a/src/mt7612u/CLAUDE.md b/src/mt7612u/CLAUDE.md index 92d45175..e008b964 100644 --- a/src/mt7612u/CLAUDE.md +++ b/src/mt7612u/CLAUDE.md @@ -64,3 +64,10 @@ Where each piece lives: NOACK vs. ACK-requesting radiotap - `mt7612u_set_retry_limit` (`caps.cpp`); measurements - `docs/mt7612u-tx-retry.md`; reproducer - `mt7612uprobe txs` (`gate_txs` in `tools/bringup.cpp`). + +## Station identity + +Contract: `mt7612u_set_station_identity` (`include/mt7612u/mt7612u.h`) and +`IRadio::SetStationIdentity` (`src/IRadio.h`). Measurements, the receive-filter +caveat and the retractions: `docs/mt7612u-station-identity.md`. Headless cell +`mt7612u_station_identity`; hardware gate `mt7612uprobe staid`. diff --git a/src/mt7612u/Mt7612uRadio.cpp b/src/mt7612u/Mt7612uRadio.cpp index 1b3aa41c..3231c994 100644 --- a/src/mt7612u/Mt7612uRadio.cpp +++ b/src/mt7612u/Mt7612uRadio.cpp @@ -1034,6 +1034,47 @@ void Mt7612uRadio::ClearAckResponder() { mt7612u_clear_ack_responder(_dev); } +/* Thin, like the rest of the control plane: on this part a station identity + * is a check, not a configuration (station.cpp, + * docs/mt7612u-station-identity.md). The ordering note IRadio requires is at + * the declaration (Mt7612uRadio.h). */ +bool Mt7612uRadio::SetStationIdentity(const devourer::MacAddr &own, + const devourer::MacAddr &bssid) { + std::lock_guard lock(_mu); + if (!_dev) + return false; + if (mt7612u_set_station_identity(_dev, own.data(), bssid.data()) != 0) + return false; + /* The arm covers RECEIVE and auto-ACK only. What a station transmits is + * the caller's: its unicast (management, EAPOL, data) must request an ACK - + * build_stream_radiotap(mode, false), since the default stream radiotap is + * NOACK and never retries - and the MAC retransmits an unacknowledged frame + * only tx.retry_limit times. That limit is programmed at bring-up into a + * GLOBAL register and defaults to 0, so a station session left at the + * default sends every unicast frame exactly once: one lost frame is a lost + * association step. Not refused - an RX-only or test session may want + * exactly that - but said, because nothing else would say it. */ + if (std::clamp(_cfg.tx.retry_limit, 0, 63) == 0) + _logger->warn("MT7612U: station identity armed with tx.retry_limit=0 - " + "the MAC will not retransmit this station's unacknowledged " + "unicast. Set DEVOURER_TX_RETRY_LIMIT / tx.retry_limit " + "(nonzero) and send unicast with an ACK-requesting " + "radiotap"); + return true; +} + +bool Mt7612uRadio::ClearStationIdentity() { + std::lock_guard lock(_mu); + /* No device: nothing can have been armed, so the pre-arm state trivially + * holds (IRadio's contract for a clear with nothing to undo). */ + if (!_dev) + return true; + mt7612u_clear_station_identity(_dev); + /* True without qualification because the arm writes no hardware state on + * this part: there is nothing to restore, so nothing to verify. */ + return true; +} + /* The beacon plane. Thin on purpose: the sequence these wrap is the one the * bring-up harness's Stage A and Stage B gates run, device-verified on * 2026-09-08 - beacon on air on both bands, hardware TSF and sequence, and a @@ -1257,6 +1298,11 @@ devourer::AdapterCaps Mt7612uRadio::GetAdapterCaps() { c.tsf_write_ok = hw.tsf_write; /* Measured on air: 0 frames at the stimulus radio unarmed, 3500+ armed. */ c.ack_responder_ok = true; + /* station_mode_ok: TRUE. The evidence, its controls and its limits - + * the promiscuous station RX path included - are kept in ONE place, at the + * AdapterCaps::station_mode_ok declaration, with the full record in + * docs/mt7612u-station-identity.md (read its retraction section first). */ + c.station_mode_ok = true; /* tx.retry_limit reaches MT_TX_RETRY_CFG (global, not per frame) at every * bring-up and is read back. On air, by the chip's own TX status: * docs/mt7612u-tx-retry.md. */ diff --git a/src/mt7612u/Mt7612uRadio.h b/src/mt7612u/Mt7612uRadio.h index ceb67021..77d7ef0d 100644 --- a/src/mt7612u/Mt7612uRadio.h +++ b/src/mt7612u/Mt7612uRadio.h @@ -102,6 +102,14 @@ class Mt7612uRadio : public IRadio { devourer::ChannelBusy GetChannelBusy() override; uint32_t ArmChannelBusy(uint32_t window_us) override; bool SetAckResponder(const devourer::MacAddr &mac) override; + /* IRadio's ORDERING clause, answered here as it requires: this CANNOT + * detect being called before the RX loop. It writes no filter and no + * identity (a check, not a configuration), so it is order-independent as + * implemented - but StartRxLoop reprograms the receive filter after + * mt7612u_start(), so call it after StartRxLoop as the interface says. */ + bool SetStationIdentity(const devourer::MacAddr &own, + const devourer::MacAddr &bssid) override; + bool ClearStationIdentity() override; bool StartBeacon(const uint8_t *beacon, size_t len, int interval_tu) override; bool UpdateBeaconPayload(const uint8_t *beacon, size_t len) override; bool StopBeacon() override; diff --git a/src/mt7612u/StationIdentity.h b/src/mt7612u/StationIdentity.h new file mode 100644 index 00000000..c43a2433 --- /dev/null +++ b/src/mt7612u/StationIdentity.h @@ -0,0 +1,186 @@ +/* + * StationIdentity.h - the decision half of SetStationIdentity, with no I/O. + * + * station.cpp reads three things off the chip (the port identity, whether that + * read worked, and MT_AUTO_RSP_CFG) and then decides whether to arm. The + * reading needs a device; the deciding does not, and the deciding is where + * the easy mistakes are - a failed read that fails OPEN (arming a station + * whose ability to acknowledge is unknown), or a failed read laundered into a + * mismatch against 00:00:00:00:00:00 (refusing for the wrong reason). Neither + * is visible in a register trace. Split out here, every branch runs in ctest: + * tests/mt7612u_station_selftest.cpp. + * + * Pure: no device type, no register access, no logging. Safe to include from + * a test with nothing but -I src/mt7612u. + */ +#ifndef MT7612U_STATION_IDENTITY_H +#define MT7612U_STATION_IDENTITY_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum mt7612u_sta_verdict { + MT7612U_STA_OK = 0, + /* A caller mistake: null pointers, a multicast address, or `own` and + * `bssid` the same - a station whose own address is its BSSID is not a + * station. */ + MT7612U_STA_BAD_ARGS, + MT7612U_STA_MULTICAST, + MT7612U_STA_SAME_ADDR, + /* Could not read the chip. Refused, not assumed: if we cannot find out + * whether this MAC will acknowledge anything, we do not get to claim it + * will. */ + MT7612U_STA_READ_FAILED, + /* `own` is not the address the MAC is holding. Something else owns the + * port identity - a beacon, or an ACK responder - and moving it here + * would make this station deaf: measured, reception goes to zero. + * docs/mt7612u-station-identity.md. */ + MT7612U_STA_PORT_MISMATCH, + /* The auto-response engine is switched off. Whatever did that did it + * deliberately, so this refuses rather than silently re-enabling it. */ + MT7612U_STA_AUTO_RSP_OFF +}; + +/* + * `port_ok` / `rsp_ok` are whether the corresponding register read SUCCEEDED, + * not whether its value is acceptable. Passing 0 for either must refuse - + * that asymmetry is the bug this file exists to make testable. + * + * `auto_rsp_en_mask` is MT_AUTO_RSP_EN, passed in so this header needs no + * register definitions. + */ +/* The argument half of the decision, callable before any register I/O so a + * caller mistake costs no USB transfer and is reported as what it is. */ +static inline enum mt7612u_sta_verdict +mt7612u_sta_check_args(const uint8_t *own, const uint8_t *bssid) +{ + if (!own || !bssid) + return MT7612U_STA_BAD_ARGS; + if ((own[0] & 0x01) || (bssid[0] & 0x01)) + return MT7612U_STA_MULTICAST; + if (memcmp(own, bssid, 6) == 0) + return MT7612U_STA_SAME_ADDR; + return MT7612U_STA_OK; +} + +static inline enum mt7612u_sta_verdict +mt7612u_sta_decide(const uint8_t *own, const uint8_t *bssid, + const uint8_t *port, int port_ok, + uint32_t auto_rsp_cfg, int rsp_ok, + uint32_t auto_rsp_en_mask) +{ + enum mt7612u_sta_verdict a = mt7612u_sta_check_args(own, bssid); + + if (a != MT7612U_STA_OK) + return a; + if (!port) + return MT7612U_STA_BAD_ARGS; + if (!port_ok) + return MT7612U_STA_READ_FAILED; + if (memcmp(port, own, 6) != 0) + return MT7612U_STA_PORT_MISMATCH; + if (!rsp_ok) + return MT7612U_STA_READ_FAILED; + if (!(auto_rsp_cfg & auto_rsp_en_mask)) + return MT7612U_STA_AUTO_RSP_OFF; + return MT7612U_STA_OK; +} + +/* + * The ownership hand-off, as a pure state machine. + * + * SetStationIdentity's check is one-shot: it verifies the port identity when + * it arms and has no further say. A beacon or an ACK responder armed LATER + * can move MT_MAC_ADDR out from under a live station - the ordering a real + * caller is likelier to hit than the one the arm-time check covers. + * + * This does not veto those paths; a station arm does not refuse them. After + * any write to MT_MAC_ADDR the caller reads the register back, compares it + * with the station's own address (mt7612u_port_compare) and hands the verdict + * here: + * + * SAME - the identity did not move: the arm stands. If this operation + * had dropped it and then put the identity back (a start that + * failed and unwound), `allow_restore` re-arms it. + * DIFFERENT - the identity verifiably moved: the arm is dropped, and its + * own/BSSID kept aside so an unwind can restore it. + * UNKNOWN - the read failed: nothing is known, so the arm stands and the + * caller says so. + */ +enum mt7612u_port_cmp { + MT7612U_PORT_SAME = 0, + MT7612U_PORT_DIFFERENT, + MT7612U_PORT_UNKNOWN +}; + +enum mt7612u_sta_event { + MT7612U_STA_EV_NONE = 0, /* nothing changed */ + MT7612U_STA_EV_DROPPED, /* armed -> dropped: the identity moved */ + MT7612U_STA_EV_RESTORED, /* dropped -> armed: it came back */ + MT7612U_STA_EV_UNVERIFIED /* armed, but the read failed */ +}; + +struct mt7612u_sta_state { + uint8_t own[6]; + uint8_t bssid[6]; + int armed; + /* Dropped by a port-identity move, own/bssid kept for a restore. */ + int lost; +}; + +/* `port` as read from MT_MAC_ADDR (DW0 + the low half of DW1) against `own`. + * A failed read is UNKNOWN, never DIFFERENT. */ +static inline enum mt7612u_port_cmp +mt7612u_port_compare(const uint8_t *port, int read_ok, const uint8_t *own) +{ + if (!read_ok || !port || !own) + return MT7612U_PORT_UNKNOWN; + return memcmp(port, own, 6) == 0 ? MT7612U_PORT_SAME + : MT7612U_PORT_DIFFERENT; +} + +static inline void mt7612u_sta_arm(struct mt7612u_sta_state *s, + const uint8_t *own, const uint8_t *bssid) +{ + memcpy(s->own, own, 6); + memcpy(s->bssid, bssid, 6); + s->armed = 1; + s->lost = 0; +} + +static inline void mt7612u_sta_clear(struct mt7612u_sta_state *s) +{ + memset(s, 0, sizeof *s); +} + +/* Idempotent: observing the same move twice is one loss, not two. */ +static inline enum mt7612u_sta_event +mt7612u_sta_port_observed(struct mt7612u_sta_state *s, + enum mt7612u_port_cmp c, int allow_restore) +{ + if (s->armed) { + if (c == MT7612U_PORT_DIFFERENT) { + s->armed = 0; + s->lost = 1; + return MT7612U_STA_EV_DROPPED; + } + return c == MT7612U_PORT_UNKNOWN ? MT7612U_STA_EV_UNVERIFIED + : MT7612U_STA_EV_NONE; + } + if (s->lost && allow_restore && c == MT7612U_PORT_SAME) { + s->armed = 1; + s->lost = 0; + return MT7612U_STA_EV_RESTORED; + } + return MT7612U_STA_EV_NONE; +} + +#ifdef __cplusplus +} +#endif + +#endif /* MT7612U_STATION_IDENTITY_H */ diff --git a/src/mt7612u/beacon.cpp b/src/mt7612u/beacon.cpp index 2f3fa1a0..434de47b 100644 --- a/src/mt7612u/beacon.cpp +++ b/src/mt7612u/beacon.cpp @@ -341,8 +341,12 @@ int mt7612u_beacon_start(struct mt7612u_dev *dev, const void *buf, size_t len, uint8_t idx; unsigned before; int took = 0; + /* Whether a station arm was already lost before this call: a failed + * start restores only an arm it dropped itself. */ + int sta_lost_before; if (!dev) return -1; + sta_lost_before = dev->sta.lost; if (beacon_split(buf, len, &mpdu, &mpdu_len, &rate)) return -1; ta = mpdu + 10; /* addr2 - the transmitter, i.e. the port identity */ @@ -445,7 +449,7 @@ int mt7612u_beacon_start(struct mt7612u_dev *dev, const void *buf, size_t len, * caller can act on. Silence is a worse outcome than a deaf AP only if * you are not told about it. */ - if (mt7612u_set_ack_responder(dev, ta)) + if (mt7612u_set_ack_responder_as(dev, ta, "a beacon")) goto fail_post; if (mt_mac_set_bss_base(dev, ta)) goto fail_post; @@ -557,6 +561,10 @@ int mt7612u_beacon_start(struct mt7612u_dev *dev, const void *buf, size_t len, mt_ap_set_bssid(dev, 0, zero6); mt_ap_set_bssid(dev, 1, zero6); unwind_identity(dev, took); + /* The identity is back where this call found it (when the restore + * landed), so a station arm this call dropped is valid again. */ + mt7612u_station_identity_check(dev, "a failed beacon start", + !sta_lost_before); return -2; } diff --git a/src/mt7612u/caps.cpp b/src/mt7612u/caps.cpp index c9d4885c..51c13f0c 100644 --- a/src/mt7612u/caps.cpp +++ b/src/mt7612u/caps.cpp @@ -131,14 +131,36 @@ int mt7612u_set_retry_limit(struct mt7612u_dev *d, int limit) * closing the gate alone does not stop a die that matches on identity, the * clear path moves the identity back rather than only clearing the gate. */ +static int ack_responder_write(struct mt7612u_dev *d, const uint8_t mac[6]); + int mt7612u_set_ack_responder(struct mt7612u_dev *d, const uint8_t mac[6]) { - uint32_t dw0, rb; + return mt7612u_set_ack_responder_as(d, mac, "an ACK responder"); +} + +/* Arming a responder retargets MT_MAC_ADDR, which is the register a station + * identity depends on. The station arm is re-checked AFTER the write, against + * what the register then holds: re-arming the address already there leaves + * the station exactly as it was, a write that failed without moving anything + * drops nothing, and an unreadable register keeps the arm with a warning + * (mt7612u_station_identity_check). */ +int mt7612u_set_ack_responder_as(struct mt7612u_dev *d, const uint8_t mac[6], + const char *who) +{ + int rc; if (!mac || (mac[0] & 0x01)) { ERR("ack responder address must be unicast"); return -1; } + rc = ack_responder_write(d, mac); + mt7612u_station_identity_check(d, who, 0); + return rc; +} + +static int ack_responder_write(struct mt7612u_dev *d, const uint8_t mac[6]) +{ + uint32_t dw0, rb; if (!d->ack_saved) { memcpy(d->ack_saved_mac, d->macaddr, 6); @@ -229,6 +251,10 @@ void mt7612u_clear_ack_responder(struct mt7612u_dev *d) * through unwind_identity(), against an MT_MAC_ADDR still sitting on the * responder address. The flag means "a restore is still owed" and nothing * reads it as "a responder is armed", so leaving it set is safe. */ + /* Moving the identity back can move it off a station armed on the + * responder's address; re-check, never restore (IRadio: the caller + * re-arms after a port-0 claimant). */ + mt7612u_station_identity_check(d, "clearing the ACK responder", 0); if (mt_io_errors(d) != before) return; d->ack_saved = 0; diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index 5420290e..68922476 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -294,6 +294,38 @@ void mt7612u_clear_ack_responder(struct mt7612u_dev *dev); */ int mt7612u_set_retry_limit(struct mt7612u_dev *dev, int limit); +/* + * Infrastructure-station identity: this adapter is `own`, the AP it has + * joined is `bssid`. Backs IRadio::SetStationIdentity. + * + * On this part the job is almost entirely refusal, and that is a measured + * result rather than a shortcut - docs/mt7612u-station-identity.md: + * + * - It does NOT write MT_MAC_ADDR; it requires `own` to already BE the port + * identity and fails if it is not. The auto-response engine matches + * address 1 against that register, so moving it stops the station being + * acknowledged; under the MANAGED receive filter it also stops the + * station receiving at all. This is why a station must not be armed with + * mt7612u_set_ack_responder(bssid): that call retargets the very register + * a station needs left alone. + * - It does NOT write MT_MAC_BSSID or the APC slot table. MT_MAC_BSSID + * programmed wrong, and the AP's BSSID in the station's slot, receive + * what nothing programmed receives. + * The BSSID is recorded for the host (it is addr3 on every frame a station + * sends) and retrievable with mt7612u_station_bssid(). + * - It verifies MT_AUTO_RSP_EN, since the measured auto-ACK depends on it. + * + * Returns 0 when armed, -1 when refused - including when something else (a + * beacon, an ACK responder) owns the port identity. A beacon or ACK responder + * armed LATER that moves the port identity drops the station arm with a + * warning; re-arm once it has been given back. + */ +int mt7612u_set_station_identity(struct mt7612u_dev *dev, + const uint8_t own[6], const uint8_t bssid[6]); +void mt7612u_clear_station_identity(struct mt7612u_dev *dev); +/* The BSSID last armed; -1 if no station identity is armed. */ +int mt7612u_station_bssid(struct mt7612u_dev *dev, uint8_t out[6]); + /* * Hardware beacon, from the MAC's reserved page. * diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 650feb64..4be1fc75 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -28,6 +28,7 @@ #include #include #include +#include "StationIdentity.h" #include "regs.h" #include "Mt7612uRxCorr.h" #include "include/mt7612u/mt7612u.h" @@ -253,6 +254,13 @@ struct mt7612u_dev { * caller had already armed an ACK responder, because then the identity is * theirs and restoring would silently disarm it. */ int beacon_took_identity; + + /* Station identity (src/mt7612u/station.cpp). The BSSID is RECORDED, + * not programmed: in the arms measured the hardware BSSID registers made + * no difference to what a managed station receives, and MT_MAC_BSSID + * already has two owners. The host still needs the value - it is addr3 + * on every frame a station transmits. docs/mt7612u-station-identity.md */ + struct mt7612u_sta_state sta; /* The addr2 AND addr3 mt7612u_beacon_start() programmed, so an in-place * update can refuse a beacon that would change either. Both, because they * land in different registers: addr2 in MT_MAC_ADDR and the MBSS base, @@ -302,6 +310,16 @@ void mt_wr(struct mt7612u_dev *d, uint32_t addr, uint32_t val); /* Returns -1 without writing when the read half fails. */ int mt_rmw(struct mt7612u_dev *d, uint32_t addr, uint32_t mask, uint32_t val); int mt_wr_chk(struct mt7612u_dev *d, uint32_t addr, uint32_t val); +/* Re-check a station arm against what MT_MAC_ADDR holds after a write to it: + * drop it on a verified move, keep it (and say so) when the register cannot + * be read, and with `allow_restore` re-arm one this operation dropped once + * the identity is back. src/mt7612u/station.cpp. */ +void mt7612u_station_identity_check(struct mt7612u_dev *d, const char *who, + int allow_restore); +/* mt7612u_set_ack_responder() naming its caller in that announcement - the + * beacon path takes MT_MAC_ADDR through here too. src/mt7612u/caps.cpp. */ +int mt7612u_set_ack_responder_as(struct mt7612u_dev *d, + const uint8_t mac[6], const char *who); /* Register-I/O failure accumulator; see the comment above mt_io_clear(). */ void mt_io_clear(struct mt7612u_dev *d); /* Restore a previously sampled accumulator; see the note in usb.c. */ diff --git a/src/mt7612u/station.cpp b/src/mt7612u/station.cpp new file mode 100644 index 00000000..c3648a9e --- /dev/null +++ b/src/mt7612u/station.cpp @@ -0,0 +1,223 @@ +/* + * station.cpp — the MT7612U half of IRadio::SetStationIdentity. + * + * This file is small, and it is small BECAUSE of a measurement rather than in + * spite of one. docs/mt7612u-station-identity.md has the numbers; the short + * version is that on this part a station needs almost nothing programmed, and + * the one thing it must not do is the thing that looks most like the job. + * + * WHAT WAS MEASURED (mt7612uprobe's `sta` and `staack` gates, against + * hostapd on independent silicon): + * + * - MT_MAC_BSSID does not gate a managed station's receive: programmed + * deliberately WRONG, the DUT received 5877 unicast frames addressed to + * it against 6250 with nothing programmed; the AP's BSSID in the + * station's APC slot (slot 0) changed nothing either. So this function + * does NOT write them. A wrong BSSID in the station slot with its enable + * bit set was acknowledged 867/867 by a peer (one run); its effect on + * reception is not measured (docs/mt7612u-station-identity.md). + * + * - Moving MT_MAC_ADDR makes a station DEAF. With the managed receive + * filter in force, retargeting the port identity took reception of the + * AP's unicast from 103 frames to ZERO. So this function does NOT write + * MT_MAC_ADDR either - it CHECKS it, and refuses if it has moved. + * + * That second number is the whole reason this seam exists separately from + * mt7612u_set_ack_responder(). Arming an ACK responder on this part retargets + * the port identity, so SetAckResponder(bssid) on a station would silence AND + * deafen it. A station must leave MT_MAC_ADDR exactly where MAC bring-up put + * it. + * + * AUTO-ACK NEEDS NO CALL. tests/mt7612u_sta_autoack.sh asks the TRANSMITTER, + * the only party that knows whether its frame was acknowledged: a Realtek + * peer injects unicast at this MAC and reads its own CCX reports. With nothing + * armed, 100% acknowledged at 0.45 mean retries, against controls pinned at + * the peer's retry limit - including MT_AUTO_RSP_EN cleared, which is why + * this function refuses when that bit is clear. + * + * WHAT THIS DOES NOT DO: anything about transmission. A station's unicast + * needs ACK-requesting radiotap and a nonzero tx.retry_limit - see + * Mt7612uRadio::SetStationIdentity. + * + * So the useful work here is refusal and verification, not configuration. + */ +#include + +#include "StationIdentity.h" +#include "internal.h" +#include "regs.h" + +/* + * Read the port identity back out of the hardware. This is what the + * auto-response engine matches an incoming frame's address 1 against, and on + * this part it is the entire mechanism behind a station's auto-ACK. + */ +static int sta_read_port_identity(struct mt7612u_dev *d, uint8_t out[6]) +{ + uint32_t dw0 = 0, dw1 = 0; + + /* Checked, not assumed. mt_rr_chk() leaves *val untouched when the + * transfer fails, so the zero-initialised locals would otherwise turn a + * failed read into the address 00:00:00:00:00:00 and refuse for the + * wrong reason. */ + if (mt_rr_chk(d, MT_MAC_ADDR_DW0, &dw0) != 0 || + mt_rr_chk(d, MT_MAC_ADDR_DW1, &dw1) != 0) + return -1; + out[0] = (uint8_t)(dw0 & 0xff); + out[1] = (uint8_t)((dw0 >> 8) & 0xff); + out[2] = (uint8_t)((dw0 >> 16) & 0xff); + out[3] = (uint8_t)((dw0 >> 24) & 0xff); + out[4] = (uint8_t)(dw1 & 0xff); + out[5] = (uint8_t)((dw1 >> 8) & 0xff); + return 0; +} + +int mt7612u_set_station_identity(struct mt7612u_dev *dev, + const uint8_t own[6], const uint8_t bssid[6]) +{ + uint8_t port[6] = { 0 }; + uint32_t rsp = 0; + int port_ok, rsp_ok; + enum mt7612u_sta_verdict v; + + if (!dev) + return -1; + + /* Arguments first: a caller mistake costs no USB transfer. */ + v = mt7612u_sta_check_args(own, bssid); + if (v != MT7612U_STA_OK) + goto refused; + + /* Read, then decide. The deciding is in StationIdentity.h so that every + * branch below - both failed-read paths included - is exercised + * headlessly by tests/mt7612u_station_selftest.cpp rather than only on a + * device. */ + port_ok = (sta_read_port_identity(dev, port) == 0); + rsp_ok = (mt_rr_chk(dev, MT_AUTO_RSP_CFG, &rsp) == 0); + + v = mt7612u_sta_decide(own, bssid, port, port_ok, rsp, + rsp_ok, MT_AUTO_RSP_EN); +refused: + switch (v) { + case MT7612U_STA_OK: + break; + case MT7612U_STA_BAD_ARGS: + WARN("station identity refused: null address"); + return -1; + case MT7612U_STA_MULTICAST: + WARN("station identity refused: own and bssid must both be unicast"); + return -1; + case MT7612U_STA_SAME_ADDR: + WARN("station identity refused: own == bssid"); + return -1; + case MT7612U_STA_READ_FAILED: + WARN("station identity refused: could not read the MAC back, so " + "there is nothing to verify against - refusing rather than " + "arming a station whose ability to receive and acknowledge is " + "unknown"); + return -1; + case MT7612U_STA_PORT_MISMATCH: + WARN("station identity refused: the MAC's port identity is " + "%02x:%02x:%02x:%02x:%02x:%02x, not the requested " + "%02x:%02x:%02x:%02x:%02x:%02x. Something else owns it (a " + "beacon or an ACK responder). A station on any other address " + "is not acknowledged, and under the managed receive filter " + "does not receive either.", + port[0], port[1], port[2], port[3], port[4], port[5], + own[0], own[1], own[2], own[3], own[4], own[5]); + return -1; + case MT7612U_STA_AUTO_RSP_OFF: + WARN("station identity refused: MT_AUTO_RSP_EN is CLEAR (cfg %08x) " + "- the auto-response engine is switched off", rsp); + return -1; + } + + /* + * NOT written, deliberately: MT_MAC_ADDR, MT_MAC_BSSID and the + * MT_MAC_APC_BSSID slot table. The first must not move - that is the + * measured "reception goes to zero" failure. The other two made no + * measurable difference to what a managed station receives in the arms + * measured, and MT_MAC_BSSID already has two owners; a third writer on a + * register nothing needs would recreate the hazard this seam exists to + * avoid. docs/mt7612u-station-identity.md. + * + * The BSSID is recorded for the host: it is addr3 on every frame a + * station transmits. Power save, TIM parsing and per-BSS key lookup, + * which could give it a hardware use, are untested on this part. + */ + mt7612u_sta_arm(&dev->sta, own, bssid); + return 0; +} + +void mt7612u_clear_station_identity(struct mt7612u_dev *dev) +{ + if (!dev) + return; + /* Nothing to undo in hardware - this seam never wrote any. That is a + * property of this part and not a promise of the interface. */ + mt7612u_sta_clear(&dev->sta); +} + +/* + * Called after every write to MT_MAC_ADDR (mt7612u_set_ack_responder_as(), + * which the beacon path goes through, and the beacon start's unwind), with + * the register read back. It decides from what the register HOLDS, not from + * what the caller meant to do, so a write that failed and left the identity + * where it was does not drop anything. + * + * It does not refuse: a station arm does not veto the beacon and responder + * paths. It makes the consequence audible and keeps `sta.armed` true to the + * hardware. `allow_restore` is for an operation that failed and unwound the + * identity back: the arm it dropped comes back with it. + */ +void mt7612u_station_identity_check(struct mt7612u_dev *dev, const char *who, + int allow_restore) +{ + uint8_t port[6] = { 0 }; + unsigned io; + int port_ok; + + if (!dev || (!dev->sta.armed && !dev->sta.lost)) + return; + /* Kept out of the I/O-error accumulator: this read must not fail an + * operation (a beacon start counts io errors) that did its own job. */ + io = mt_io_errors(dev); + port_ok = sta_read_port_identity(dev, port) == 0; + mt_io_restore(dev, io); + + switch (mt7612u_sta_port_observed(&dev->sta, + mt7612u_port_compare(port, port_ok, dev->sta.own), + allow_restore)) { + case MT7612U_STA_EV_NONE: + break; + case MT7612U_STA_EV_DROPPED: + WARN("station identity DROPPED: %s moved MT_MAC_ADDR to " + "%02x:%02x:%02x:%02x:%02x:%02x, away from this station's own " + "address. The MAC no longer acknowledges the AP's unicast to " + "the station; under the MANAGED receive filter it no longer " + "receives it either (measured: reception goes to zero). Under " + "the monitor filter Mt7612uRadio's RX loop installs, frames " + "still arrive but go unacknowledged. Re-arm the station " + "identity after %s releases it.", + who, port[0], port[1], port[2], port[3], port[4], port[5], who); + break; + case MT7612U_STA_EV_RESTORED: + WARN("station identity RESTORED: %s put MT_MAC_ADDR back to this " + "station's own address", who); + break; + case MT7612U_STA_EV_UNVERIFIED: + WARN("station identity UNVERIFIED after %s: MT_MAC_ADDR could not " + "be read back, so whether it still holds this station's own " + "address is unknown. The arm is kept; re-arm to re-check it.", + who); + break; + } +} + +int mt7612u_station_bssid(struct mt7612u_dev *dev, uint8_t out[6]) +{ + if (!dev || !out || !dev->sta.armed) + return -1; + memcpy(out, dev->sta.bssid, 6); + return 0; +} diff --git a/src/mt7612u/tests/api_link.c b/src/mt7612u/tests/api_link.c index b66ab333..8cc609d0 100644 --- a/src/mt7612u/tests/api_link.c +++ b/src/mt7612u/tests/api_link.c @@ -35,6 +35,9 @@ static void *const api[] = { (void *)mt7612u_set_ack_responder, (void *)mt7612u_clear_ack_responder, (void *)mt7612u_set_retry_limit, + (void *)mt7612u_set_station_identity, + (void *)mt7612u_clear_station_identity, + (void *)mt7612u_station_bssid, (void *)mt7612u_beacon_start, (void *)mt7612u_beacon_update, (void *)mt7612u_beacon_stop, diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 4af4ad0e..346dadf9 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -4178,6 +4178,1192 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) return 0; } +/* --------------------------------------------------------------------------- + * Station-identity gates: `sta`, `staack`, `staid`, `norsp`, `bssen`. + * docs/mt7612u-station-identity.md is the record they produced; the harnesses + * are tests/mt7612u_sta_identity.sh and tests/mt7612u_sta_autoack.sh (the + * uplink half is gate_txs, driven by tests/mt7612u_sta_uplink.sh). + */ + +/* ---------------------------------------------------------------- gate_sta + * + * Does programming the joined BSSID anywhere - MT_MAC_BSSID or the + * MT_MAC_APC_BSSID slot table - change what a MANAGED STATION receives, and + * is a wrong value silent, harmless, or fatal? Six arms; every write is read + * back after mt_mac_start(), every other slot is checked empty, and an arm + * whose state does not read back as written is marked UNVERIFIED and makes + * the gate INCONCLUSIVE (rc 2) - as does an all-zero to_us column, which + * would mean the table measured broadcast reception only. + * + * The station's slot is sta_station_slot() - slot 0 on a factory address, so + * arms C and D write the same slot there. Arms B, E and F move MT_MAC_BSSID, + * which mt76's station configuration never does; they ask whether a wrong + * base matters, not what mt76 would program. + * + * The receive filter is what makes this a question at all. The managed value + * mt_mac_start() programs, 0x00015f97, has bit 2 (PROMISC) SET, and in mt76 + * bit 2 is the one mapped to FIF_OTHER_BSS (init.cpp describes that value as + * dropping other-BSS frames). Bit 3 (OTHER_BSS) is clear. Do not reason about + * this register from one bit: the gate prints the full value per arm for the + * reader, and flags an arm whose PROMISC drop bit is clear (the monitor + * filter). + * + * The AP-side finding (docs/mt7612u-ap-mode.md: a wrong APC slot "beacons + * perfectly, acknowledges nobody") is about acknowledgement, not reception, + * and does not transfer to a station's receive path. + * + * NO ARM TOUCHES MT_MAC_ADDR. The auto-response engine matches address 1 + * against it, and moving it is what breaks a station + * (docs/mt7612u-station-identity.md). + * + * WHAT THIS GATE CANNOT SEE. It counts RX only. Whether the MAC auto-ACKed is + * a property of what the transmitter observed, and this process cannot ask - + * tests/mt7612u_sta_autoack.sh asks the transmitter. A healthy RX arm is not + * evidence about ACKing. + * + * bringup sta + */ +struct sta_rx_count { + std::atomic total{0}; /* every frame off the ring */ + std::atomic from_bss{0}; /* addr2 == the AP */ + std::atomic to_us{0}; /* addr1 == our own MAC */ + std::atomic to_us_data{0}; + std::atomic beacons{0}; + uint8_t bssid[6]; + uint8_t own[6]; +}; + +static void sta_rx_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + struct sta_rx_count *c = (struct sta_rx_count *)user; + const uint8_t *f = (const uint8_t *)frame; + + (void)info; + c->total.fetch_add(1, std::memory_order_relaxed); + if (len < 24) return; + + /* addr1 at 4, addr2 at 10, addr3 at 16 - true for every non-4-address + * frame, which is all an infrastructure station ever sees. */ + if (memcmp(f + 10, c->bssid, 6) == 0) + c->from_bss.fetch_add(1, std::memory_order_relaxed); + if (memcmp(f + 4, c->own, 6) == 0) { + c->to_us.fetch_add(1, std::memory_order_relaxed); + if ((f[0] & 0x0c) == 0x08) + c->to_us_data.fetch_add(1, std::memory_order_relaxed); + } + if (f[0] == 0x80 && memcmp(f + 16, c->bssid, 6) == 0) + c->beacons.fetch_add(1, std::memory_order_relaxed); +} + +/* + * The APC slot a STATION's BSSID lives in, by mt76's rule - which keys the + * slot on the station's OWN address, not on the BSSID: + * + * mt76x02_add_interface(): idx = 0, or 1 + (((macaddr[0] ^ vif->addr[0]) + * >> 2) & 7) when vif->addr is locally administered; a STATION then gets + * idx += 8 ("bssidx 8-15 for client mode"); + * mt76x02_bss_info_changed() -> mt76x02_mac_set_bssid(mvif->idx, bssid), + * which writes APC slot (idx & 7). + * + * `macaddr` there is the MBSS base, which mt76x02_mac_setaddr() keeps equal to + * the station's own address - mt76's station configuration never moves it. + * So the slot is computed against the base init leaves (the station's own + * address), and arms that reprogram MT_MAC_BSSID are, by construction, not an + * mt76 station configuration. A station on a factory (globally administered) + * address - this tree's case - is slot 0 whatever the base holds. The AP-side + * rule in beacon.cpp keys on the AP's own address, which for an AP is the + * BSSID; applying that rule to a station's BSSID picks the wrong slot. + */ +static int sta_station_slot(const uint8_t *base, const uint8_t *own) +{ + int idx = 0; + + if (own[0] & 0x02) + idx = 1 + (((base[0] ^ own[0]) >> 2) & 7); + return (idx + 8) & 7; +} + +/* Local copies: beacon.cpp's equivalents are static to that file. */ +static int sta_set_bss_base(struct mt7612u_dev *d, const uint8_t *a) +{ + const uint32_t dw0 = (uint32_t)a[0] | ((uint32_t)a[1] << 8) | + ((uint32_t)a[2] << 16) | ((uint32_t)a[3] << 24); + const uint32_t dw1 = (uint32_t)a[4] | ((uint32_t)a[5] << 8); + + if (mt_wr_chk(d, MT_MAC_BSSID_DW0, dw0)) + return -1; + return mt_rmw(d, MT_MAC_BSSID_DW1, MT_MAC_BSSID_DW1_ADDR, dw1); +} + +/* Read a slot back into `out`. Without the read-back an arm could be writing + * a slot the hardware never consults, and the table would look identical + * either way. */ +static int sta_read_apc(struct mt7612u_dev *d, int idx, uint8_t *out) +{ + uint32_t lo = 0, hi = 0; + + if (mt_rr_chk(d, MT_MAC_APC_BSSID_L(idx), &lo) || + mt_rr_chk(d, MT_MAC_APC_BSSID_H(idx), &hi)) + return -1; + out[0] = (uint8_t)(lo & 0xff); + out[1] = (uint8_t)((lo >> 8) & 0xff); + out[2] = (uint8_t)((lo >> 16) & 0xff); + out[3] = (uint8_t)((lo >> 24) & 0xff); + out[4] = (uint8_t)(hi & 0xff); + out[5] = (uint8_t)((hi >> 8) & 0xff); + return 0; +} + +/* The slot high register's BIT(16) is MT_MAC_APC_BSSID0_H_EN upstream in mt76 + * (defined, never written there); this tree does not define it and gate_sta + * does not set it. If a per-slot enable is real on this part, a "slot + * programmed" arm may have written a slot the engine was not consulting, + * which would make a null result here much weaker than it appears. This gate + * reports the bit; gate_bssen sets it and measures. Returns -1 on a failed + * read. */ +static int sta_apc_high_raw(struct mt7612u_dev *d, int idx, uint32_t *hi) +{ + return mt_rr_chk(d, MT_MAC_APC_BSSID_H(idx), hi) ? -1 : 0; +} + +static int sta_write_apc(struct mt7612u_dev *d, int idx, const uint8_t *a) +{ + const uint32_t lo = (uint32_t)a[0] | ((uint32_t)a[1] << 8) | + ((uint32_t)a[2] << 16) | ((uint32_t)a[3] << 24); + const uint32_t hi = (uint32_t)a[4] | ((uint32_t)a[5] << 8); + + if (mt_wr_chk(d, MT_MAC_APC_BSSID_L(idx), lo)) + return -1; + return mt_rmw(d, MT_MAC_APC_BSSID_H(idx), MT_MAC_APC_BSSID_H_ADDR, hi); +} + +/* The MBSS base (MT_MAC_BSSID's address halves) back into `out`. */ +static int sta_read_bss_base(struct mt7612u_dev *d, uint8_t *out) +{ + uint32_t dw0 = 0, dw1 = 0; + + if (mt_rr_chk(d, MT_MAC_BSSID_DW0, &dw0) || + mt_rr_chk(d, MT_MAC_BSSID_DW1, &dw1)) + return -1; + out[0] = (uint8_t)(dw0 & 0xff); + out[1] = (uint8_t)((dw0 >> 8) & 0xff); + out[2] = (uint8_t)((dw0 >> 16) & 0xff); + out[3] = (uint8_t)((dw0 >> 24) & 0xff); + out[4] = (uint8_t)(dw1 & 0xff); + out[5] = (uint8_t)((dw1 >> 8) & 0xff); + return 0; +} + +/* + * Put BOTH register families back to their init state, so an arm cannot + * inherit anything - from its predecessor, or from an earlier process: the + * chip keeps register state across bring-up tool runs, and mac_setaddr() + * rewrites only the address halves of the APC slots, so a BIT(16) that + * gate_bssen set survives into the next gate_sta unless it is cleared here. + * The whole high word is written, enable bit included. Returns -1 if any + * write fails, in which case the arm has not started from a known state. + */ +static int sta_reset_bss(struct mt7612u_dev *d, const uint8_t *own) +{ + int rc = sta_set_bss_base(d, own); + + for (int z = 0; z < 8; z++) { + if (mt_wr_chk(d, MT_MAC_APC_BSSID_L(z), 0) || + mt_wr_chk(d, MT_MAC_APC_BSSID_H(z), 0)) + rc = -1; + } + return rc; +} + +/* sta_reset_bss(), then read it all back: the base holds `own` and both words + * of every slot read zero. The chip keeps these registers across processes, so + * a reset that did not land is reported, not assumed. 0 when verified. */ +static int sta_reset_bss_verified(struct mt7612u_dev *d, const uint8_t *own, + const char *gate) +{ + uint8_t base[6] = { 0 }; + int ok = sta_reset_bss(d, own) == 0 && + sta_read_bss_base(d, base) == 0 && memcmp(base, own, 6) == 0; + + for (int z = 0; ok && z < 8; z++) { + uint32_t lo = 1, hi = 1; + + if (mt_rr_chk(d, MT_MAC_APC_BSSID_L(z), &lo) || + mt_rr_chk(d, MT_MAC_APC_BSSID_H(z), &hi) || lo || hi) + ok = 0; + } + if (!ok) + printf("GATE %s: FAIL - MT_MAC_BSSID / the APC slots could not be " + "restored to their init state; the next process inherits " + "them\n", gate); + return ok ? 0 : -1; +} + +/* The receive filter the gate is about to measure under, read back and printed. + * 0 when the PROMISC drop bit is set (the managed filter mt_mac_start() + * programs); -1 on a failed read or a clear bit (the monitor filter), which + * voids an arm that claims to run managed. */ +static int sta_check_managed_filter(const char *gate) +{ + uint32_t filtr = 0; + + if (mt_rr_chk(&dev, MT_RX_FILTR_CFG, &filtr)) { + printf("GATE %s: FAIL - MT_RX_FILTR_CFG unreadable\n", gate); + return -1; + } + if (!(filtr & MT_RX_FILTR_CFG_PROMISC)) { + printf("GATE %s: FAIL - filtr=%08x: PROMISC drop bit clear, the " + "monitor filter - this arm would not run managed\n", + gate, filtr); + return -1; + } + printf("filtr=%08x (managed: PROMISC drop bit set)\n", filtr); + return 0; +} + +/* Set MT_AUTO_RSP_EN again and read it back. 0 once it verifiably holds - + * the state init leaves and everything else on the part assumes. `gate` names + * the caller in the failure line. */ +static int sta_restore_auto_rsp(const char *gate) +{ + uint32_t v = 0; + + if (mt_rmw(&dev, MT_AUTO_RSP_CFG, MT_AUTO_RSP_EN, MT_AUTO_RSP_EN) || + mt_rr_chk(&dev, MT_AUTO_RSP_CFG, &v) || !(v & MT_AUTO_RSP_EN)) { + printf("GATE %s: FAIL - MT_AUTO_RSP_EN could not be restored " + "(read %08x); the chip is left with auto-response OFF\n", + gate, v); + return -1; + } + return 0; +} + +/* 0 when MT_MAC_ADDR reads back as this adapter's own address (DW0 and the + * low half of DW1; the U2ME byte above it is write-only). -1 on a failed read + * or any other address - the port identity did not come back. */ +static int sta_port_is_own(void) +{ + uint32_t dw0 = 0, dw1 = 0; + const uint8_t *m = dev.macaddr; + + if (mt_rr_chk(&dev, MT_MAC_ADDR_DW0, &dw0) || + mt_rr_chk(&dev, MT_MAC_ADDR_DW1, &dw1)) + return -1; + if (dw0 != ((uint32_t)m[0] | ((uint32_t)m[1] << 8) | + ((uint32_t)m[2] << 16) | ((uint32_t)m[3] << 24)) || + (dw1 & 0xffff) != ((uint32_t)m[4] | ((uint32_t)m[5] << 8))) + return -1; + return 0; +} + +static int gate_sta(uint8_t chan, int secs, const char *bssid_str) +{ + static const uint8_t wrong[6] = { 0x02, 0x00, 0x00, 0xde, 0xad, 0x01 }; + static const uint8_t zero[6] = { 0 }; + static struct sta_rx_count ctr; + uint8_t bssid[6]; + unsigned long base_bss = 0, base_bcn = 0, any_to_us = 0; + int any_beacon = 0, unverified = 0, slot; + + /* mbss: write `want` into MT_MAC_BSSID. apc0: into APC slot 0. + * apc_sta: into the station's slot by mt76's rule (sta_station_slot). */ + static const struct { + char tag; int mbss; int apc_sta; int apc0; int bad; + const char *what; + } arms[] = { + { 'A', 0, 0, 0, 0, "init only - nothing programmed" }, + { 'B', 1, 0, 0, 0, "MT_MAC_BSSID = AP" }, + { 'C', 0, 0, 1, 0, "APC slot 0 = AP" }, + { 'D', 0, 1, 0, 0, "APC station slot (mt76 rule) = AP" }, + { 'E', 1, 1, 0, 0, "MT_MAC_BSSID + station slot = AP" }, + { 'F', 1, 1, 0, 1, "both programmed WRONG (is it silent?)" }, + }; + + if (parse_mac6(bssid_str, bssid)) { + printf("GATE STA: FAIL - need the AP's BSSID, e.g.\n" + " bringup sta 6 20 02:42:75:05:d6:00\n"); + return 2; + } + if (bssid[0] & 0x01) { + printf("GATE STA: FAIL - %02x:%02x:%02x:%02x:%02x:%02x is multicast\n", + bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + return 2; + } + + if (mt_eeprom_init(&dev)) return 1; + if (mt_init_hardware(&dev, NULL)) return 1; + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; + + /* Against the base init leaves, which is the station's own address. */ + slot = sta_station_slot(dev.macaddr, dev.macaddr); + + printf("=== GATE STA: what the BSSID registers do for a managed station ===\n"); + printf("chan %u, %d s per arm, AP %02x:%02x:%02x:%02x:%02x:%02x, " + "own %02x:%02x:%02x:%02x:%02x:%02x, station APC slot %d\n", + chan, secs, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], + dev.macaddr[0], dev.macaddr[1], dev.macaddr[2], + dev.macaddr[3], dev.macaddr[4], dev.macaddr[5], slot); + printf("RX ONLY - whether the MAC auto-ACKed is not visible from here.\n"); + printf("No arm touches MT_MAC_ADDR.\n"); + /* + * The bring-up - its calibrations above all - is done. Say so, flushed, + * and give a harness time to start its unicast stimulus before arm A. + * The MT7612U's calibration replies come late under a strong nearby + * transmitter (mcu.cpp, mcu_wait_resp), and the stimulus here is a + * monitor-vif flood from 20 cm; starting it only after this line keeps + * the two from overlapping (tests/mt7612u_sta_identity.sh waits for it). + */ + printf("GATE STA: bring-up done - start the stimulus\n\n"); + fflush(stdout); + if (!wait_ms(3000)) return 2; + printf(" arm %-38s %5s %8s %8s %7s %8s\n", + "configuration", "slot", "rx_total", "from_bss", "beacons", "to_us"); + + for (unsigned a = 0; a < sizeof arms / sizeof arms[0]; a++) { + const uint8_t *want = arms[a].bad ? wrong : bssid; + const uint8_t *want_base = arms[a].mbss ? want : dev.macaddr; + uint32_t filtr = 0, apc_hi = 0; + uint8_t apc_rb[6] = { 0 }, base_rb[6] = { 0 }; + int wrote_slot = -1, ok = 1, base_ok, apc_ok = 1, hi_ok = 0; + + memcpy(ctr.bssid, bssid, 6); + memcpy(ctr.own, dev.macaddr, 6); + ctr.total = 0; ctr.from_bss = 0; ctr.to_us = 0; + ctr.to_us_data = 0; ctr.beacons = 0; + + if (sta_reset_bss(&dev, dev.macaddr)) ok = 0; + if (arms[a].mbss && sta_set_bss_base(&dev, want)) ok = 0; + if (arms[a].apc0) wrote_slot = 0; + if (arms[a].apc_sta) wrote_slot = slot; + if (wrote_slot >= 0 && sta_write_apc(&dev, wrote_slot, want)) + ok = 0; + + /* Every early exit leaves the registers as init does: the chip + * keeps them across runs. */ + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) { + mt_mac_stop(&dev); sta_reset_bss(&dev, dev.macaddr); return 1; + } + if (mt_async_start(&dev, sta_rx_cb, &ctr)) { + mt_mac_stop(&dev); sta_reset_bss(&dev, dev.macaddr); return 1; + } + /* The RECEIVER must be on: with ENABLE_RX clear the MAC filters + * nothing and the gate could not test its own claim. */ + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { + rx_teardown(); mt_mac_stop(&dev); + sta_reset_bss(&dev, dev.macaddr); return 1; + } + /* + * DO NOT call mt7612u_set_monitor_rx() here. It writes + * MT_RX_FILTR_CFG = PHY_ERR|CRC_ERR and nothing else - every + * address and BSS drop bit OFF - so every arm would run + * PROMISCUOUS, the hardware would never consult MT_MAC_BSSID or + * the APC table, and six identical arms would be guaranteed + * before the dwell began (docs/mt7612u-station-identity.md, the + * retraction). The filter under test is what mt_mac_start() + * already left: 0x00015f97, mt76's managed-station value. + */ + if (mt_rr_chk(&dev, MT_RX_FILTR_CFG, &filtr)) ok = 0; + + /* Read everything back AFTER mt_mac_start(): anything written + * before it could have been overwritten since. The base must + * hold `want_base`; every slot but the one written must be + * empty, and that one must hold `want` with BIT(16) reported. */ + base_ok = sta_read_bss_base(&dev, base_rb) == 0 && + memcmp(base_rb, want_base, 6) == 0; + if (!base_ok) ok = 0; + for (int z = 0; z < 8; z++) { + uint8_t rb[6] = { 0 }; + + if (sta_read_apc(&dev, z, rb) || + memcmp(rb, z == wrote_slot ? want : zero, 6) != 0) { + apc_ok = 0; + ok = 0; + } + if (z == wrote_slot) + memcpy(apc_rb, rb, 6); + } + if (wrote_slot >= 0) + hi_ok = sta_apc_high_raw(&dev, wrote_slot, &apc_hi) == 0; + + /* The receiving dwell ticks the PHY about once a second, as the + * public header requires of every receiving consumer. */ + wait_ticking(secs * 1000.0); + + rx_teardown(); + mt_mac_stop(&dev); + + printf(" %c %-38s %5d %8lu %8lu %7lu %8lu%s\n", + arms[a].tag, arms[a].what, wrote_slot, + ctr.total.load(), ctr.from_bss.load(), + ctr.beacons.load(), ctr.to_us.load(), + ok ? "" : " UNVERIFIED"); + printf(" base %02x:%02x:%02x:%02x:%02x:%02x%s filtr=%08x%s " + "to_us_data=%lu\n", + base_rb[0], base_rb[1], base_rb[2], base_rb[3], base_rb[4], + base_rb[5], base_ok ? " (as written)" : " *** NOT AS WRITTEN ***", + filtr, + /* The label reads the way the BIT does, not the way the + * word sounds: these are DROP bits, so PROMISC SET means + * "drop frames not addressed here" - the managed state we + * want. Clear means promiscuous: the monitor filter, under + * which this gate measures nothing. */ + (filtr & MT_RX_FILTR_CFG_PROMISC) + ? "" : " *** MONITOR FILTER - THIS ARM IS PROMISCUOUS ***", + ctr.to_us_data.load()); + if (!apc_ok) + printf(" APC table NOT AS WRITTEN - a slot other than %d " + "is non-empty, or slot %d reads " + "%02x:%02x:%02x:%02x:%02x:%02x\n", wrote_slot, + wrote_slot, apc_rb[0], apc_rb[1], apc_rb[2], + apc_rb[3], apc_rb[4], apc_rb[5]); + else if (wrote_slot >= 0) + printf(" APC slot %d verified, others empty, high reg " + "%08x (bit16 %s - mt76's per-slot enable)\n", + wrote_slot, apc_hi, + !hi_ok ? "UNREAD" : + (apc_hi & (1u << 16)) ? "SET" : "clear"); + if (!(filtr & MT_RX_FILTR_CFG_PROMISC)) ok = 0; + if (!ok) unverified++; + + if (ctr.beacons.load()) any_beacon = 1; + any_to_us += ctr.to_us.load(); + if (a == 0) { base_bss = ctr.from_bss.load(); base_bcn = ctr.beacons.load(); } + if (g_stop) break; + } + /* Leave the registers as init does, verified, so the next process starts + * clean. */ + if (sta_reset_bss_verified(&dev, dev.macaddr, "STA")) + return 1; + /* 0 measured, 1 FAIL, 2 inconclusive or bad invocation, 3 no verdict: + * interrupted, as gate_txs and gate_tsfwrap. A table cut short mid-arm + * is not a measurement, whatever the arms before it saw. */ + if (g_stop) { + printf("\nGATE STA: INTERRUPTED - no verdict\n"); + return 3; + } + + printf("\nHow to read this:\n"); + if (!any_beacon) { + printf(" NO BEACONS IN ANY ARM. The AP was not on channel %u, or its\n" + " BSSID is not the one given. Nothing here is comparable and\n" + " the run says NOTHING about the BSSID registers - fix the rig\n" + " and re-run.\n", chan); + printf("GATE STA: INCONCLUSIVE\n"); + return 2; + } + if (!any_to_us) { + printf(" NO UNICAST TO US IN ANY ARM. Beacons arrived, but nothing was\n" + " addressed to this station, so the table measures broadcast\n" + " reception only - not the question. Drive unicast at the DUT\n" + " (tests/sta_unicast_inject.py) and re-run.\n"); + printf("GATE STA: INCONCLUSIVE\n"); + return 2; + } + if (unverified) { + printf(" %d arm(s) UNVERIFIED: a write did not read back, or the\n" + " managed filter was not in force. Those rows test nothing.\n", + unverified); + printf("GATE STA: INCONCLUSIVE\n"); + return 2; + } + printf(" arm A baseline: from_bss=%lu beacons=%lu\n", base_bss, base_bcn); + printf(" - if B..E match A, the BSSID registers do not gate a station's\n"); + printf(" RX on this MAC, and the answer for receive is 'nothing'.\n"); + printf(" - if arm F (deliberately WRONG) also matches, a wrong BSSID\n"); + printf(" is HARMLESS for RX here - the opposite of the AP-side finding.\n"); + printf(" - the ACK half is measured from the transmitter\n"); + printf(" (tests/mt7612u_sta_autoack.sh).\n"); + printf("GATE STA: measured (verdict is the operator's - see above)\n"); + return 0; +} + +/* ------------------------------------------------------------- gate_staack + * + * Probe-response retry counting, from the DUT alone - kept for the register + * state it prints and for its arm C, NOT for its auto-ACK verdict. + * + * Method: a directed probe request from our own address makes hostapd answer + * with a unicast probe response addressed to us. If we ACK it the AP is done + * (one copy, FC Retry clear); if not, the AP retransmits and we see the same + * response again with FC Retry SET. + * + * WHY ITS VERDICT IS NOT EVIDENCE. The single-variable control (arm B: clear + * MT_AUTO_RSP_EN, hold reception constant) does not move against hostapd, + * because that AP does not retransmit an unacknowledged probe response - so + * the method cannot fail and therefore cannot measure. The auto-ACK answer + * comes from the transmitter instead: tests/mt7612u_sta_autoack.sh reads a + * Realtek peer's per-frame CCX reports. docs/mt7612u-station-identity.md + * records both failed methods. + * + * WHAT ARM C DOES SHOW. Arm C retargets MT_MAC_ADDR with + * mt7612u_set_ack_responder() - what SetAckResponder(bssid) would do to a + * station. Under the managed filter the station then receives none of the + * AP's responses: it goes deaf, not merely silent. + * + * bringup staack + */ +struct staack_count { + std::atomic resp{0}; /* probe responses to us */ + std::atomic resp_retry{0};/* ... with FC Retry set */ + std::atomic other_to_us{0}; + std::atomic other_retry{0}; + uint8_t own[6]; + uint8_t bssid[6]; +}; + +static void staack_rx_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + struct staack_count *c = (struct staack_count *)user; + const uint8_t *f = (const uint8_t *)frame; + int retry; + + (void)info; + if (len < 24) return; + if (memcmp(f + 4, c->own, 6) != 0) return; /* addr1 must be us */ + if (memcmp(f + 10, c->bssid, 6) != 0) return; /* from the AP */ + + retry = (f[1] & 0x08) ? 1 : 0; /* FC Retry */ + if (f[0] == 0x50) { /* probe response */ + c->resp.fetch_add(1, std::memory_order_relaxed); + if (retry) c->resp_retry.fetch_add(1, std::memory_order_relaxed); + } else { + c->other_to_us.fetch_add(1, std::memory_order_relaxed); + if (retry) c->other_retry.fetch_add(1, std::memory_order_relaxed); + } +} + +static int gate_staack(uint8_t chan, int secs, const char *bssid_str) +{ + static struct staack_count ctr; + struct mt7612u_tx_rate rate = { }; + uint8_t bssid[6]; + static uint8_t probe[128]; + size_t plen; + double t0, last_tick, a_frac = -1.0, b_frac = -1.0, c_frac = -1.0; + unsigned long sent = 0, resp = 0, retried = 0; + unsigned long a_resp = 0, b_resp = 0, c_resp = 0; + + if (parse_mac6(bssid_str, bssid)) { + printf("GATE STAACK: FAIL - need the AP's BSSID\n"); + return 2; + } + + if (mt_eeprom_init(&dev)) return 1; + if (mt_init_hardware(&dev, NULL)) return 1; + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; + + memcpy(ctr.own, dev.macaddr, 6); + memcpy(ctr.bssid, bssid, 6); + ctr.resp = 0; ctr.resp_retry = 0; ctr.other_to_us = 0; ctr.other_retry = 0; + + /* Directed probe request: addr1 = addr3 = the AP, addr2 = US. Addressed + * to the AP rather than broadcast so the response comes back unicast to + * our address, which is the frame whose acknowledgement we are testing. */ + memset(probe, 0, sizeof probe); + probe[0] = 0x40; /* probe request */ + memcpy(probe + 4, bssid, 6); + memcpy(probe + 10, dev.macaddr, 6); + memcpy(probe + 16, bssid, 6); + plen = 24; + probe[plen++] = 0x00; /* SSID element, wildcard */ + probe[plen++] = 0x00; + probe[plen++] = 0x01; /* supported rates */ + probe[plen++] = 0x04; + probe[plen++] = 0x82; probe[plen++] = 0x84; + probe[plen++] = 0x8b; probe[plen++] = 0x96; + + rate.phy = MT7612U_PHY_OFDM; + rate.mcs = 0; /* 6 Mbit/s - robust */ + rate.nss = 1; + rate.bw = MT7612U_BW_20; + rate.no_ack = 0; + + printf("=== GATE STAACK: does this MAC auto-ACK unicast to its own address? ===\n"); + printf("chan %u, %d s per arm, AP %02x:%02x:%02x:%02x:%02x:%02x, own %02x:%02x:%02x:%02x:%02x:%02x\n", + chan, secs, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], + dev.macaddr[0], dev.macaddr[1], dev.macaddr[2], + dev.macaddr[3], dev.macaddr[4], dev.macaddr[5]); + printf("\n"); + + /* + * THREE ARMS. Arm A is the claim; arm B is the control that lets it + * mean anything; arm C is a diagnostic. + * + * Retargeting MT_MAC_ADDR is not a control: under the MANAGED receive + * filter it also makes the filter drop the AP's responses, so it cannot + * tell "we did not acknowledge" from "we did not receive". + * + * The clean control changes ONE thing: clear MT_AUTO_RSP_EN and leave + * MT_MAC_ADDR alone. Reception is then identical to arm A - same port + * identity, same filter, the AP's responses still addressed to us and + * still accepted - and the only difference is that the MAC stops + * answering them. Retried copies must rise. If they do not, the + * retried-copy signal does not track acknowledgement on this rig and + * arm A proves nothing. + * + * Arm C demonstrates the MT_MAC_ADDR hazard rather than asserting it: + * what SetAckResponder(bssid) would do to a station. Under the managed + * filter the station goes deaf as well as silent. + */ + for (int armi = 0; armi < 3; armi++) { + static const uint8_t foreign[6] = + { 0x02, 0x00, 0x00, 0xac, 0x1d, 0x01 }; + double frac; + + ctr.resp = 0; ctr.resp_retry = 0; + ctr.other_to_us = 0; ctr.other_retry = 0; + sent = 0; + + /* mt_mac_start() sets ENABLE_TX before its WPDMA poll, so a failed + * start can leave TX on: stop the MAC on that path too. */ + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) { mt_mac_stop(&dev); return 1; } + if (mt_async_start(&dev, staack_rx_cb, &ctr)) { mt_mac_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { + rx_teardown(); mt_mac_stop(&dev); return 1; + } + /* As in gate_sta: mt7612u_set_monitor_rx() installs the MONITOR + * filter, not the managed one. Leave what + * mt_mac_start() programmed. The auto-ACK conclusion does not rest + * on the filter - a probe response addressed to us is accepted + * either way - but the arm should still run in the configuration a + * station uses. */ + + if (armi == 1) { + /* The single-variable control: stop answering, keep + * receiving. */ + if (mt_rmw(&dev, MT_AUTO_RSP_CFG, MT_AUTO_RSP_EN, 0)) { + printf(" B could not clear MT_AUTO_RSP_EN - no control\n"); + rx_teardown(); mt_mac_stop(&dev); + return 2; + } + } else if (armi == 2 && mt7612u_set_ack_responder(&dev, foreign)) { + printf(" C could not retarget MT_MAC_ADDR\n"); + rx_teardown(); mt_mac_stop(&dev); + return 2; + } + + printf(" %c %s\n", (char)('A' + armi), + armi == 0 ? "nothing armed - MT_MAC_ADDR as init left it" : + armi == 1 ? "MT_AUTO_RSP_EN CLEARED (control: same RX, no ACK)" + : "MT_MAC_ADDR RETARGETED away (the station hazard)"); + + t0 = now_ms(); + last_tick = t0; + while (now_ms() - t0 < secs * 1000.0 && !g_stop) { + /* Receiving: tick the PHY about once a second. */ + txs_tick(1, &last_tick); + probe[22] = (uint8_t)((sent & 0xf) << 4); + probe[23] = (uint8_t)(sent >> 4); + if (mt_tx_raw(&dev, probe, plen, &rate, 0xff, 0) == 0) + sent++; + usleep(200000); /* 5/s - inside any AP's rate */ + } + + resp = ctr.resp.load(); + retried = ctr.resp_retry.load(); + frac = resp ? 100.0 * (double)retried / (double)resp : -1.0; + + /* Put the arm's change back, verified: the next arm (or the next + * process - the chip keeps registers) must not start with + * auto-response off or the port identity elsewhere. */ + if (armi == 1 && sta_restore_auto_rsp("STAACK")) { + rx_teardown(); mt_mac_stop(&dev); + return 1; + } + if (armi == 2) { + mt7612u_clear_ack_responder(&dev); + if (sta_port_is_own()) { + printf("GATE STAACK: FAIL - MT_MAC_ADDR did not come back " + "to this adapter's own address after arm C\n"); + rx_teardown(); mt_mac_stop(&dev); + return 1; + } + } + rx_teardown(); + mt_mac_stop(&dev); + + printf(" sent %lu, responses to us %lu, retried %lu", + sent, resp, retried); + if (frac >= 0.0) printf(" -> %.1f%% retried\n", frac); + else printf(" -> no responses\n"); + printf(" other unicast to us %lu (retried %lu)\n", + ctr.other_to_us.load(), ctr.other_retry.load()); + + if (armi == 0) { a_resp = resp; a_frac = frac; } + else if (armi == 1) { b_resp = resp; b_frac = frac; } + else { c_resp = resp; c_frac = frac; } + if (g_stop) break; + } + + /* Interrupted: no verdict (rc 3), as gate_sta. */ + if (g_stop) { + printf("\nGATE STAACK: INTERRUPTED - no verdict\n"); + return 3; + } + printf("\n"); + if (a_resp == 0) { + printf("Arm A got no probe response at all. Either the AP is not on this\n" + "channel/BSSID or our probe requests are not reaching it. This says\n" + "NOTHING about acknowledgement - do not read it as a failure to ACK.\n" + "GATE STAACK: INCONCLUSIVE\n"); + return 2; + } + if (b_resp == 0) { + printf("Arm B got no probe response, so the control could not run and\n" + "arm A's %.1f%% is UNCONTROLLED - do not quote it. Clearing\n" + "MT_AUTO_RSP_EN should not have changed what we RECEIVE, so if\n" + "this happens the assumption behind the control is wrong too.\n" + "GATE STAACK: INCONCLUSIVE\n", a_frac); + return 2; + } + printf("A (nothing armed) : %5.1f%% retried over %lu responses\n", a_frac, a_resp); + printf("B (AUTO_RSP_EN cleared) : %5.1f%% retried over %lu responses\n", b_frac, b_resp); + if (c_resp) + printf("C (MT_MAC_ADDR moved) : %5.1f%% retried over %lu responses\n", + c_frac, c_resp); + else + printf("C (MT_MAC_ADDR moved) : received NOTHING - under the managed\n" + " filter the station goes DEAF as well\n" + " as silent. A larger failure than the\n" + " one this seam was designed around.\n"); + + if (b_frac > a_frac + 10.0) { + printf("\nB rose with reception held constant, so the retried-copy signal\n" + "does track acknowledgement here and A is meaningful: this MAC\n" + "DOES auto-ACK unicast addressed to its own address with nothing\n" + "armed at all.\n"); + printf("GATE STAACK: PASS\n"); + return 0; + } + printf("\nB did NOT rise above A even though only the answering engine was\n" + "disabled. Either this MAC acknowledges by some path MT_AUTO_RSP_EN\n" + "does not gate, or retried copies do not track acknowledgement on\n" + "this rig. Either way the method did not demonstrate it can fail, so\n" + "A's number proves nothing.\n"); + printf("GATE STAACK: INCONCLUSIVE\n"); + return 2; +} + +/* -------------------------------------------------------------- gate_staid + * + * The SetStationIdentity contract, checked against real hardware. No AP and + * no peer: every property here is about what this MAC holds and what the + * function refuses, which is the whole of the job on this part. + * + * The case that matters is 5. mt7612u_set_ack_responder() retargets + * MT_MAC_ADDR, which is the register the auto-response engine matches address + * 1 against - and under the managed receive filter gate_staack's arm C shows + * reception itself going to zero when it moves. So a station identity armed + * while an ACK responder holds the port identity would be a station that + * cannot acknowledge anything, silently. It must be REFUSED, and this checks + * that it is, on the hardware, rather than trusting the branch to be right. + * + * bringup staid + */ +/* + * Every register a station identity could plausibly write: the port identity + * (MT_MAC_ADDR), the MBSS base (MT_MAC_BSSID) and both words of all eight APC + * slots. The seam's defining property is that it writes none of them; + * gate_staid compares a snapshot before and after. Returns -1 on any failed + * read - an unreadable register cannot be shown unchanged. + */ +struct staid_regs { uint32_t w[20]; }; + +static int staid_snapshot(struct staid_regs *r) +{ + int n = 0; + + if (mt_rr_chk(&dev, MT_MAC_ADDR_DW0, &r->w[n++]) || + mt_rr_chk(&dev, MT_MAC_ADDR_DW1, &r->w[n++]) || + mt_rr_chk(&dev, MT_MAC_BSSID_DW0, &r->w[n++]) || + mt_rr_chk(&dev, MT_MAC_BSSID_DW1, &r->w[n++])) + return -1; + for (int z = 0; z < 8; z++) { + if (mt_rr_chk(&dev, MT_MAC_APC_BSSID_L(z), &r->w[n++]) || + mt_rr_chk(&dev, MT_MAC_APC_BSSID_H(z), &r->w[n++])) + return -1; + } + return 0; +} + +/* 1 when both snapshots were taken and are identical. */ +static int staid_unchanged(const struct staid_regs *a, int a_ok, + const struct staid_regs *b, int b_ok) +{ + return a_ok && b_ok && memcmp(a->w, b->w, sizeof a->w) == 0; +} + +static int gate_staid(void) +{ + static const uint8_t bssid[6] = { 0x02, 0x42, 0x75, 0x05, 0xd6, 0xaa }; + static const uint8_t foreign[6] = { 0x02, 0x00, 0x00, 0xac, 0x1d, 0x01 }; + static const uint8_t mcast[6] = { 0x01, 0x00, 0x5e, 0x00, 0x00, 0x01 }; + uint8_t own[6], got[6]; + int pass = 0, fail = 0, snap0_ok, snap1_ok; + struct staid_regs snap0, snap1; + + if (mt_eeprom_init(&dev)) return 1; + if (mt_init_hardware(&dev, NULL)) return 1; + + memcpy(own, dev.macaddr, 6); + printf("=== GATE STAID: the SetStationIdentity contract on hardware ===\n"); + printf("own %02x:%02x:%02x:%02x:%02x:%02x bssid %02x:%02x:%02x:%02x:%02x:%02x\n\n", + own[0], own[1], own[2], own[3], own[4], own[5], + bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + +#define CHK(cond, what) do { \ + if (cond) { pass++; printf(" ok %s\n", what); } \ + else { fail++; printf(" FAIL %s\n", what); } \ + } while (0) + + /* 1. the ordinary case - and the seam's defining property: arming + * writes nothing (MT_MAC_ADDR, MT_MAC_BSSID and all eight APC slots + * read the same before and after). */ + snap0_ok = staid_snapshot(&snap0) == 0; + CHK(mt7612u_set_station_identity(&dev, own, bssid) == 0, + "arms with the factory address as own"); + snap1_ok = staid_snapshot(&snap1) == 0; + CHK(mt7612u_station_bssid(&dev, got) == 0 && memcmp(got, bssid, 6) == 0, + "records the BSSID it was given"); + CHK(staid_unchanged(&snap0, snap0_ok, &snap1, snap1_ok), + "arming writes no register (MT_MAC_ADDR, MT_MAC_BSSID, APC slots " + "read back unchanged)"); + + /* 2. an address this MAC is not holding */ + CHK(mt7612u_set_station_identity(&dev, foreign, bssid) != 0, + "refuses an `own` that is not the port identity"); + + /* 3. malformed arguments */ + /* Not an isolating test: `mcast` is also not the port identity, so the + * later branch would refuse it even if the multicast branch were + * deleted. Kept because the refusal is still the required behaviour, + * and labelled so nobody reads it as coverage of that branch. */ + CHK(mt7612u_set_station_identity(&dev, mcast, bssid) != 0, + "refuses a multicast own (not an isolating test - see comment)"); + CHK(mt7612u_set_station_identity(&dev, own, mcast) != 0, + "refuses a multicast bssid"); + CHK(mt7612u_set_station_identity(&dev, own, own) != 0, + "refuses own == bssid"); + + /* 4. clear - which also writes nothing */ + snap0_ok = staid_snapshot(&snap0) == 0; + mt7612u_clear_station_identity(&dev); + snap1_ok = staid_snapshot(&snap1) == 0; + CHK(mt7612u_station_bssid(&dev, got) != 0, + "reports no BSSID once cleared"); + CHK(staid_unchanged(&snap0, snap0_ok, &snap1, snap1_ok), + "clearing writes no register (same registers read back unchanged)"); + + /* + * 5. THE ONE THAT MATTERS. Arm an ACK responder on a foreign address - + * which moves MT_MAC_ADDR - and the station arm must refuse, because a + * station whose port identity points elsewhere acknowledges nothing. + */ + if (mt7612u_set_ack_responder(&dev, foreign) == 0) { + CHK(mt7612u_set_station_identity(&dev, own, bssid) != 0, + "REFUSES while an ACK responder holds the port identity"); + mt7612u_clear_ack_responder(&dev); + CHK(mt7612u_set_station_identity(&dev, own, bssid) == 0, + "arms again once the responder has given it back"); + } else { + printf(" SKIP could not arm an ACK responder - case 5 not run\n"); + fail++; /* the most important case did not run; do not pass. */ + } + mt7612u_clear_station_identity(&dev); + + /* + * 6. THE OTHER ORDERING, the one a real caller is likelier to hit. Case + * 5 covers "responder first, station second" - refused. This covers + * "station first, responder second", where the arm-time check cannot + * help: the responder moves MT_MAC_ADDR out from under a live station. + * + * It is not refused - a station arm does not veto the beacon and + * responder paths - but it must not be silent, and the armed state must + * not go on claiming a station is configured once its identity has been + * taken. + */ + if (mt7612u_set_station_identity(&dev, own, bssid) == 0 && + mt7612u_set_ack_responder(&dev, foreign) == 0) { + CHK(mt7612u_station_bssid(&dev, got) != 0, + "drops the armed station when a responder takes the identity"); + mt7612u_clear_ack_responder(&dev); + } else { + printf(" SKIP could not set up case 6\n"); + fail++; + } + mt7612u_clear_station_identity(&dev); + +#undef CHK + printf("\nGATE STAID: %d passed, %d failed\n", pass, fail); + return fail ? 1 : 0; +} + +/* -------------------------------------------------------------- gate_norsp + * + * Receive with MT_AUTO_RSP_EN CLEARED, for the single-variable arm of + * tests/mt7612u_sta_autoack.sh. + * + * That harness asks a peer whether this MAC acknowledges unicast addressed to + * it. SetStationIdentity refuses to arm when MT_AUTO_RSP_EN is clear; this + * gate is what tests that the bit matters: same receiver, same port identity, + * same filter, one bit different. If the peer's ok rate collapses with the + * bit clear, the refusal is justified; if it does not, the refusal rests on a + * bit that does not gate acknowledgement here. + * + * The third argument selects which side of the comparison this is: + * 1 (default) - clear MT_AUTO_RSP_EN: the CONTROL + * 0 - leave it set: the CLAIM + * Both run the SAME code path with the SAME managed filter, so the two arms + * differ by exactly one bit. (Pairing this with `bringup arx`, which installs + * the MONITOR filter at the top of gate_arx, would vary the filter and the + * init path as well.) + * + * bringup norsp [clear_rsp] + */ +static void norsp_rx_cb(void *user, const void *frame, size_t len, + const struct mt7612u_rx_info *info) +{ + (void)user; (void)frame; (void)len; (void)info; +} + +static int gate_norsp(uint8_t chan, int secs, int clear_rsp) +{ + uint32_t before = 0, after = 0; + int rc = 0; + + if (mt_eeprom_init(&dev)) return 1; + if (mt_init_hardware(&dev, NULL)) return 1; + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; + /* mt_mac_start() sets ENABLE_TX before its WPDMA poll, so a failed start + * can leave TX on: stop the MAC on that path too. */ + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) { mt_mac_stop(&dev); return 1; } + /* A NON-NULL callback, because mt_async_start(NULL) starts the TX slots + * and NOT the RX ring - and mac_start(MT_RX_DRAIN_RING) then refuses, + * correctly, with "no ring draining EP4". */ + if (mt_async_start(&dev, norsp_rx_cb, NULL)) { mt_mac_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { + rx_teardown(); mt_mac_stop(&dev); return 1; + } + /* Managed filter left exactly as mt_mac_start() programmed it - do NOT + * call mt7612u_set_monitor_rx(), which installs the monitor value (see + * gate_sta). Read back, not assumed. */ + if (sta_check_managed_filter("NORSP")) { + rx_teardown(); mt_mac_stop(&dev); return 2; + } + + if (mt_rr_chk(&dev, MT_AUTO_RSP_CFG, &before)) { + printf("GATE NORSP: FAIL - cannot read MT_AUTO_RSP_CFG\n"); + rx_teardown(); mt_mac_stop(&dev); return 1; + } + if (clear_rsp) { + if (mt_rmw(&dev, MT_AUTO_RSP_CFG, MT_AUTO_RSP_EN, 0)) { + printf("GATE NORSP: FAIL - cannot clear MT_AUTO_RSP_EN\n"); + rx_teardown(); mt_mac_stop(&dev); return 1; + } + /* Checked: mt_rr_chk() leaves `after` untouched on a failed read, + * and 0 would read as "EN cleared". */ + if (mt_rr_chk(&dev, MT_AUTO_RSP_CFG, &after)) { + printf("GATE NORSP: FAIL - cannot read MT_AUTO_RSP_CFG back " + "after clearing EN\n"); + sta_restore_auto_rsp("NORSP"); + rx_teardown(); mt_mac_stop(&dev); return 1; + } + if (after & MT_AUTO_RSP_EN) { + printf("GATE NORSP: FAIL - MT_AUTO_RSP_EN did not stay clear " + "(%08x -> %08x); the arm would measure nothing\n", + before, after); + sta_restore_auto_rsp("NORSP"); + rx_teardown(); mt_mac_stop(&dev); return 2; + } + } else { + after = before; + if (!(after & MT_AUTO_RSP_EN)) { + printf("GATE NORSP: FAIL - asked to LEAVE MT_AUTO_RSP_EN set but " + "it is already clear (%08x); this arm would be the control, " + "not the claim\n", after); + rx_teardown(); mt_mac_stop(&dev); return 2; + } + } + + printf("MT_AUTO_RSP_CFG %08x -> %08x (EN %s), managed filter, " + "receiving %d s on ch%u\n", before, after, + clear_rsp ? "CLEARED" : "left SET", secs, chan); + + /* The receiving dwell ticks the PHY about once a second, as the public + * header requires of every receiving consumer. 0 means interrupted. */ + const int completed = wait_ticking(secs * 1000.0); + + /* Put it back, verified - interrupted or not. */ + if (clear_rsp && sta_restore_auto_rsp("NORSP")) + rc = 1; + rx_teardown(); + mt_mac_stop(&dev); + if (rc) + return rc; + if (!completed) { + printf("GATE NORSP: INTERRUPTED - no verdict (restored)\n"); + return 3; + } + printf("GATE NORSP: done (restored)\n"); + return 0; +} + +/* -------------------------------------------------------------- gate_bssen + * + * A WRONG BSSID in the APC slot a station's BSSID lives in, with that slot's + * BIT(16) SET - the DUT arm for tests/mt7612u_sta_autoack.sh arm E. + * + * gate_sta leaves BIT(16) clear - upstream mt76 calls it + * MT_MAC_APC_BSSID0_H_EN and never writes it; this tree does not define it. + * So a "slot programmed" arm there may write a slot the engine is not + * consulting, and "a wrong BSSID changes nothing" would be uninteresting if + * nothing was reading the BSSID. + * + * Which slot: the STATION slot by mt76's rule (sta_station_slot), keyed on + * the station's own address against the MBSS base - slot 0 for a factory + * address. MT_MAC_BSSID is left at the base init programs (the station's own + * address), exactly as mt76's station configuration leaves it, so the slot + * the hardware derives is the slot written. Every other slot is emptied, + * enable bit included. Base, slot, bit and emptiness are all read back; if + * any of them does not hold, the arm refuses: an unsettable or misplaced + * write is not evidence about anything. + * + * The peer (tests/mt7612u_sta_autoack.sh) transmits unicast at this station's + * own address throughout. If acknowledgement and reception survive, the BSSID + * plane does not gate a station on this part even when its enable is set. + * + * bringup bssen + */ +static int gate_bssen(uint8_t chan, int secs) +{ + static const uint8_t wrong[6] = { 0x02, 0x00, 0x00, 0xde, 0xad, 0x02 }; + static const uint8_t zero[6] = { 0 }; + uint8_t rb[6] = { 0 }, base_rb[6] = { 0 }; + uint32_t hi = 0; + /* bad: 1 a register write / read-back failed (a defect, rc 1); + * 2 BIT(16) would not stay set (inconclusive, rc 2). */ + int idx, bad = 0; + + if (mt_eeprom_init(&dev)) return 1; + if (mt_init_hardware(&dev, NULL)) return 1; + if (mt_set_channel(&dev, chan, MT7612U_BW_20)) return 1; + /* As in gate_norsp: a failed start can leave TX on. */ + if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) { mt_mac_stop(&dev); return 1; } + if (mt_async_start(&dev, norsp_rx_cb, NULL)) { mt_mac_stop(&dev); return 1; } + if (mt_mac_start(&dev, MT_RX_DRAIN_RING)) { + rx_teardown(); mt_mac_stop(&dev); return 1; + } + /* Managed filter as mt_mac_start() left it - read back, not assumed. */ + if (sta_check_managed_filter("BSSEN")) { + rx_teardown(); mt_mac_stop(&dev); return 2; + } + + idx = sta_station_slot(dev.macaddr, dev.macaddr); + if (sta_reset_bss(&dev, dev.macaddr) || + sta_write_apc(&dev, idx, wrong) || + mt_rmw(&dev, MT_MAC_APC_BSSID_H(idx), 1u << 16, 1u << 16)) { + printf("GATE BSSEN: FAIL - a register write failed\n"); + bad = 1; + } + if (!bad && (sta_read_bss_base(&dev, base_rb) || + memcmp(base_rb, dev.macaddr, 6) != 0)) { + printf("GATE BSSEN: FAIL - MT_MAC_BSSID does not hold the station's " + "own address, so the derived slot is not certain\n"); + bad = 1; + } + for (int z = 0; !bad && z < 8; z++) { + if (sta_read_apc(&dev, z, rb) || + memcmp(rb, z == idx ? wrong : zero, 6) != 0) { + printf("GATE BSSEN: FAIL - APC slot %d did not read back as " + "written\n", z); + bad = 1; + } + } + if (!bad && sta_apc_high_raw(&dev, idx, &hi)) { + printf("GATE BSSEN: FAIL - APC slot %d high register unreadable\n", idx); + bad = 1; + } + if (!bad && !(hi & (1u << 16))) { + printf("GATE BSSEN: INCONCLUSIVE - BIT(16) of the APC high register " + "would not stay set (%08x). Either it is not a per-slot " + "enable on this part, or it is not writable here; either way " + "this arm proves nothing about an enabled slot.\n", hi); + bad = 2; + } + if (bad) { + const int restore_failed = + sta_reset_bss_verified(&dev, dev.macaddr, "BSSEN"); + + rx_teardown(); mt_mac_stop(&dev); + /* A failed restore is a defect whatever the arm's own verdict. */ + return restore_failed ? 1 : bad; + } + + printf("WRONG BSSID %02x:%02x:%02x:%02x:%02x:%02x in station APC slot %d, " + "BIT(16) SET (high reg %08x), other slots empty, MT_MAC_BSSID = own " + "address (verified)\n", + wrong[0], wrong[1], wrong[2], wrong[3], wrong[4], wrong[5], idx, hi); + printf("receiving %d s on ch%u as %02x:%02x:%02x:%02x:%02x:%02x\n", + secs, chan, dev.macaddr[0], dev.macaddr[1], dev.macaddr[2], + dev.macaddr[3], dev.macaddr[4], dev.macaddr[5]); + + /* The receiving dwell ticks the PHY about once a second, as the public + * header requires of every receiving consumer. 0 means interrupted. */ + const int completed = wait_ticking(secs * 1000.0); + + /* Leave the registers as init does, verified - interrupted or not: the + * chip keeps them across runs. */ + { + const int restore_failed = + sta_reset_bss_verified(&dev, dev.macaddr, "BSSEN"); + + rx_teardown(); + mt_mac_stop(&dev); + if (restore_failed) + return 1; + } + if (!completed) { + printf("GATE BSSEN: INTERRUPTED - no verdict (restored)\n"); + return 3; + } + printf("GATE BSSEN: done\n"); + return 0; +} + +/* + * The station gates' numeric arguments, parsed before any device I/O with + * txs_parse_long()'s strictness: [chan] 1..255, [secs] 1..3600, and norsp's + * [clear_rsp] 0 or 1. A malformed or out-of-range value is a usage error - + * atoi() would turn it into 0, and a zero dwell skips the measurement while + * the gate still reports "done". Absent arguments keep the defaults passed + * in. Returns 0, or 2 after printing the usage line. + */ +static int sta_parse_args(int argc, char **argv, const char *usage, + long *chan, long *secs, long *flag) +{ + if ((argc > 2 && (txs_parse_long(argv[2], chan) || + *chan < 1 || *chan > 255))) { + fprintf(stderr, "bad channel '%s': a number 1..255\n", argv[2]); + fprintf(stderr, "usage: %s\n", usage); + return 2; + } + if (argc > 3 && (txs_parse_long(argv[3], secs) || + *secs < 1 || *secs > 3600)) { + fprintf(stderr, "bad duration '%s': seconds, 1..3600\n", argv[3]); + fprintf(stderr, "usage: %s\n", usage); + return 2; + } + if (flag && argc > 4 && (txs_parse_long(argv[4], flag) || + (*flag != 0 && *flag != 1))) { + fprintf(stderr, "bad clear_rsp '%s': 0 or 1\n", argv[4]); + fprintf(stderr, "usage: %s\n", usage); + return 2; + } + return 0; +} + int main(int argc, char **argv) { const char *err = NULL, *cmd = argc > 1 ? argv[1] : "regs"; @@ -4213,6 +5399,8 @@ int main(int argc, char **argv) * finer "does this chip tune it" check to reuse), frames a positive int, * peer a whole MAC. */ long txs_chan = 149, txs_frames = 40; + /* The station gates' arguments (sta_parse_args): defaults per gate. */ + long sta_chan = 6, sta_secs = 15, sta_flag = 1; if (!strcmp(cmd, "txs")) { uint8_t mac[6]; @@ -4241,6 +5429,21 @@ int main(int argc, char **argv) } } + if (!strcmp(cmd, "sta") || !strcmp(cmd, "staack") || + !strcmp(cmd, "norsp") || !strcmp(cmd, "bssen")) { + const int norsp = !strcmp(cmd, "norsp"); + + sta_secs = !strcmp(cmd, "sta") ? 15 : + !strcmp(cmd, "staack") ? 20 : 25; + if (sta_parse_args(argc, argv, + norsp ? "bringup norsp [chan] [secs] [clear 1|0]" : + !strcmp(cmd, "bssen") ? "bringup bssen [chan] [secs]" : + !strcmp(cmd, "sta") ? "bringup sta [chan] [secs] " : + "bringup staack [chan] [secs] ", + &sta_chan, &sta_secs, norsp ? &sta_flag : NULL)) + return 2; + } + signal(SIGINT, on_signal); signal(SIGTERM, on_signal); /* The knob lives here, not in the library: mt_recover_usb() reads the @@ -4345,6 +5548,18 @@ int main(int argc, char **argv) } else if (!strcmp(cmd, "ap")) { rc = gate_ap(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, argc > 3 ? atoi(argv[3]) : 30); + } else if (!strcmp(cmd, "sta")) { + rc = gate_sta((uint8_t)sta_chan, (int)sta_secs, + argc > 4 ? argv[4] : NULL); + } else if (!strcmp(cmd, "staack")) { + rc = gate_staack((uint8_t)sta_chan, (int)sta_secs, + argc > 4 ? argv[4] : NULL); + } else if (!strcmp(cmd, "staid")) { + rc = gate_staid(); + } else if (!strcmp(cmd, "norsp")) { + rc = gate_norsp((uint8_t)sta_chan, (int)sta_secs, (int)sta_flag); + } else if (!strcmp(cmd, "bssen")) { + rc = gate_bssen((uint8_t)sta_chan, (int)sta_secs); } else if (!strcmp(cmd, "chan")) { rc = gate_chan(argc > 2 ? (uint8_t)atoi(argv[2]) : 149, argc > 3 ? argv[3] : NULL); @@ -4363,6 +5578,11 @@ int main(int argc, char **argv) fprintf(stderr, " bringup beacon [chan] [secs] (Stage A: static AP beacon on air)\n"); fprintf(stderr, " bringup ap [chan] [secs] (Stage B: beacon + RX, probe/auth/assoc)\n"); fprintf(stderr, " bringup txs [chan] [frames] [peer MAC] (per-frame retry count off MT_TX_STAT_FIFO; honours DEVOURER_TX_RETRY_LIMIT)\n"); + fprintf(stderr, " bringup staid (the SetStationIdentity contract on hardware, no AP)\n"); + fprintf(stderr, " bringup sta [chan] [secs] (what the BSSID registers do for a managed station)\n"); + fprintf(stderr, " bringup staack [chan] [secs] (probe-response retry count; register state, not a verdict)\n"); + fprintf(stderr, " bringup norsp [chan] [secs] [clear 1|0] (receive with MT_AUTO_RSP_EN cleared or left set)\n"); + fprintf(stderr, " bringup bssen [chan] [secs] (receive with a WRONG BSSID in an ENABLED APC slot)\n"); fprintf(stderr, " bringup [sweep|coding|vht] [chan] [count] [bw 0=20 1=40 2=80]\n"); fprintf(stderr, " the witness must listen at the same width (DEVOURER_BW=40|80)\n"); rc = 2; diff --git a/tests/mt7612u_sta_autoack.sh b/tests/mt7612u_sta_autoack.sh new file mode 100755 index 00000000..f8091aa1 --- /dev/null +++ b/tests/mt7612u_sta_autoack.sh @@ -0,0 +1,300 @@ +#!/bin/sh +# mt7612u_sta_autoack.sh - does an MT7612U station acknowledge unicast sent to +# its own address, with NOTHING armed? +# +# Results and limits (notably: a ONE-PEER result): docs/mt7612u-station- +# identity.md. +# +# The transmitter is the only party that knows whether its frame was +# acknowledged, and on Realtek that knowledge is a per-frame CCX report. Two +# methods that ask anyone else cannot answer, and are recorded in that doc so +# they are not retried: capturing the DUT's ACKs on a monitor vif on the +# peer's own phy (a radio cannot hear an ACK to its own transmission), and +# counting retried probe responses from hostapd (it does not retransmit an +# unacknowledged probe response, so the control cannot move). +# +# So: a Realtek adapter under devourer injects unicast QoS-Data at the DUT and +# reads its own tx.report events. retries~0 means the DUT answered; retries +# pinned at the descriptor limit means it did not. That is the same instrument +# tests/ack_txreport_matrix.sh uses, pointed the other way round - there the +# Realtek part is the responder, here it is the witness. +# +# Jaguar3 (8812CU/8822CU) is the preferred peer because it drains C2H off its +# coex runtime, so the reports arrive without further arrangement. Any +# generation with ack_responder_ok works if it runs DEVOURER_TX_WITH_RX=thread. +# +# sudo tests/mt7612u_sta_autoack.sh +# sudo PEER_PID=0xc812 DUT_SYSFS=7-1 CH=6 tests/mt7612u_sta_autoack.sh +# +# Env: PEER_VID, PEER_PID, PEER_SYSFS, DUT_SYSFS, CH, SECS, RETRY_LIMIT, OUT. + +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${BUILD:-$ROOT/build}" +# The bring-up tool resolves its firmware directory RELATIVE TO THE WORKING +# DIRECTORY ("firmware/mt7662_rom_patch.bin"), and the symlink below is created +# at $ROOT. Running this script from anywhere else therefore fails the DUT's +# firmware load, which surfaces as "could not read the DUT's MAC" - a message +# that names neither the cause nor the cure. Pin the directory instead. +cd "$ROOT" || exit 1 +PEER_VID="${PEER_VID:-0x0bda}" +PEER_PID="${PEER_PID:-0xc812}" +PEER_SYSFS="${PEER_SYSFS:-5-1}" +DUT_SYSFS="${DUT_SYSFS:-7-1}" +CH="${CH:-6}" +SECS="${SECS:-10}" +RETRY_LIMIT="${RETRY_LIMIT:-12}" +# Unset: a fresh private directory (sta_out_prepare in the lib). +OUT="${OUT:-}" +FW_DIR="${FW_DIR:-/lib/firmware/mediatek}" +# An address nobody holds. The control arm targets this: same transmitter, +# same rate, same channel, only the destination changes. +NOBODY="${NOBODY:-02:00:00:de:ad:07}" +TX_SA="${TX_SA:-02:aa:bb:cc:dd:07}" + +[ "$(id -u)" = 0 ] || { echo "must run as root"; exit 2; } +# shellcheck source=tests/mt7612u_sta_lib.sh +. "$ROOT/tests/mt7612u_sta_lib.sh" +sta_out_prepare || exit 2 +sta_lock_take || exit 2 +sta_pid_init dut peer +sta_peer_record || { sta_lock_release; exit 2; } +# Only a link THIS run created is removed afterwards - anything already at +# $ROOT/firmware, a dangling symlink included, is the operator's. +sta_fw_link + +pass=0; fail=0 +ok() { pass=$((pass+1)); printf ' PASS %s\n' "$*"; } +bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$*"; } + +DUT_PID="" +# shellcheck disable=SC2317 # reached through the traps below +cleanup() { + # arm() runs in a command substitution, so the PIDs it starts are recorded + # in $OUT (tests/mt7612u_sta_lib.sh) for this trap to find. The peer first: + # an orphan txdemo keeps its USB lock and fails the NEXT run's peer open + # with "adapter already in use", which yields zero reports - and zero is a + # control's passing value. INT, as timeout(1) forwards it to txdemo. + sta_pid_kill peer INT; peer_gone=$? + sta_pid_kill dut + DUT_PID="" + sta_dut_handback + # Only once the peer process has really exited: re-enumerating an adapter + # still inside its de-init is what the hand-back must not do. + if [ "$peer_gone" = 0 ]; then sta_peer_handback + else echo "peer still running - not re-enumerating PEER_SYSFS=$PEER_SYSFS"; fi + sta_fw_unlink + sta_lock_release +} +trap cleanup EXIT +# AND IT MUST STOP: with INT/TERM on the EXIT trap the shell runs cleanup +# and then CARRIES ON into the next arm. cleanup is idempotent, so the EXIT +# pass after it is harmless. +trap 'cleanup; exit 130' INT TERM + +sta_dut_take || exit 2 + +DUT_MAC=$("$BUILD/mt7612uprobe" staid 2>&1 | sed -n 's/^own \([0-9a-f:]\{17\}\).*/\1/p' | head -1) +[ -n "$DUT_MAC" ] || { echo "could not read the DUT's MAC"; exit 1; } +echo "DUT MT7612U at $DUT_SYSFS, own $DUT_MAC" +echo "peer $PEER_VID:$PEER_PID at $PEER_SYSFS, ch$CH, retry limit $RETRY_LIMIT" +echo + +# $1 = tag, $2 = destination, $3 = 1 if the DUT should be receiving +arm() { + tag="$1"; ra="$2"; dut_up="$3" + DUT_PID="" + if [ "$dut_up" != 0 ]; then + # 1 = receiver on, MANAGED filter, nothing armed - the claim. + # 2 = the same code path with MT_AUTO_RSP_EN cleared - the control. + # 3 = managed, a WRONG BSSID in the station's APC slot (mt76's station + # rule), ENABLED - closes the BSSID question + # (docs/mt7612u-station-identity.md). + # + # Arms 1 and 2 both run `norsp`, which takes the bit as an argument, so + # they share one code path and one (managed) receive filter and differ by + # exactly that bit. `bringup arx` would NOT do for arm 1: it installs the + # MONITOR filter at the top of gate_arx. + if [ "$dut_up" = 3 ]; then + "$BUILD/mt7612uprobe" bssen "$CH" $((SECS + 14)) \ + >"$OUT/dut_$tag.log" 2>&1 & + elif [ "$dut_up" = 2 ]; then + "$BUILD/mt7612uprobe" norsp "$CH" $((SECS + 14)) 1 \ + >"$OUT/dut_$tag.log" 2>&1 & + else + "$BUILD/mt7612uprobe" norsp "$CH" $((SECS + 14)) 0 \ + >"$OUT/dut_$tag.log" 2>&1 & + fi + DUT_PID=$! + # arm() runs inside a command substitution, so this assignment is invisible + # to the parent's EXIT trap. Record it where cleanup can find it, or a + # Ctrl-C mid-arm leaves a bringup holding the adapter. + sta_pid_record dut "$DUT_PID" + sleep 8 + # `arm` runs inside a command substitution, so `exit` here would only kill + # the subshell and the caller would carry on with an EMPTY result - which + # parses as 0% and reads as a passing control. Emit a marker instead and + # let the verdicts refuse it. + kill -0 "$DUT_PID" 2>/dev/null || { + printf '%s ABORTED the DUT exited before this arm: %s' \ + "$tag" "$(tail -1 "$OUT/dut_$tag.log" 2>/dev/null)" + rm -f "$OUT/.pid_dut"; return 1; } + fi + + tx_start=$(date +%s) + sta_peer_opened + env DEVOURER_VID="$PEER_VID" DEVOURER_PID="$PEER_PID" \ + DEVOURER_USB_BUS="${PEER_SYSFS%%-*}" DEVOURER_USB_PORT="${PEER_SYSFS#*-}" \ + DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_QOS_DATA=1 DEVOURER_TX_RA="$ra" DEVOURER_TX_SA="$TX_SA" \ + DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PAYLOAD_BYTES=200 \ + DEVOURER_TX_GAP_US=5000 DEVOURER_TX_REPORT=1 \ + DEVOURER_TX_RETRY_LIMIT="$RETRY_LIMIT" \ + DEVOURER_TX_WITH_RX=thread DEVOURER_LOG_LEVEL=warn \ + timeout -s INT -k 3 "$SECS" "$BUILD/txdemo" \ + >"$OUT/tx_$tag.jsonl" 2>"$OUT/tx_$tag.err" & + peer=$! + sta_pid_record peer "$peer" + wait "$peer" + rm -f "$OUT/.pid_peer" + tx_end=$(date +%s) + + # -k 3 IS LOAD-BEARING. Without it a peer that does not act on SIGINT blocks + # this arm indefinitely, still holding its USB lock - and that orphan then + # fails the NEXT run's peer open with "adapter already in use", which yields + # zero reports, and zero is a control's passing value. + + # Did the PEER stay inside its own window? Ask this BEFORE asking anything + # about the DUT, because a peer that overran is the one explanation under + # which the DUT's apparent death is not a death at all. + # + # The DUT gets SECS+14 s and the peer SECS, so the DUT outlives any peer that + # behaves. If the peer blocks, the DUT reaches the end of its OWN window and + # exits NORMALLY - and the liveness check below then reports "the DUT died + # during the measurement window" while quoting the DUT's own success line as + # the evidence ("ABORTED the DUT died DURING ... GATE NORSP: done + # (restored)"). A clean completion is not a death; the fault is the peer's, + # and this names it as the peer's. + # SECS+5, not SECS+8: with -k 3 a well-behaved peer is done by SECS+3, and + # the DUT's own window expires SECS+16 s after ITS launch, i.e. SECS+8 s + # after the peer started. A threshold of SECS+8 would sit exactly on that + # boundary and let the misfire back in on a tie. + if [ $((tx_end - tx_start)) -gt $((SECS + 5)) ]; then + printf '%s ABORTED the PEER overran its %ss window (took %ss) - nothing about the DUT can be read from this arm' \ + "$tag" "$SECS" "$((tx_end - tx_start))" + [ -n "$DUT_PID" ] && { kill "$DUT_PID" 2>/dev/null; wait "$DUT_PID" 2>/dev/null; } + DUT_PID=""; rm -f "$OUT/.pid_dut"; return 1 + fi + + # LIVENESS AFTER THE WINDOW, not only before it. + # + # The 8 s probe above only proves the arm STARTED. If the DUT wedges at + # second 9 - a documented failure mode on this part - the peer keeps + # injecting at an address nobody answers, ok_pct reads ~0, and for a CONTROL + # arm that is the PASSING value. Arm D would then print "MT_AUTO_RSP_EN is + # the gate" on the strength of a dead device. Arms A and E fail closed, so + # this matters for the controls specifically, which is the worse direction. + if [ -n "$DUT_PID" ] && ! kill -0 "$DUT_PID" 2>/dev/null; then + printf '%s ABORTED the DUT died DURING the measurement window: %s' \ + "$tag" "$(tail -1 "$OUT/dut_$tag.log" 2>/dev/null)" + DUT_PID=""; rm -f "$OUT/.pid_dut"; return 1 + fi + [ -n "$DUT_PID" ] && { kill "$DUT_PID" 2>/dev/null; wait "$DUT_PID" 2>/dev/null; } + DUT_PID=""; rm -f "$OUT/.pid_dut" + + python3 - "$OUT/tx_$tag.jsonl" "$tag" <<'PYEOF' +import json, sys +path, tag = sys.argv[1], sys.argv[2] +n = okc = 0 +retries = 0 +for line in open(path, errors='replace'): + if '"ev":"tx.report"' not in line: + continue + try: + e = json.loads(line) + except ValueError: + continue + n += 1 + if e.get('ok'): + okc += 1 + retries += int(e.get('retries', 0) or 0) +if not n: + print(f"{tag} reports=0") +else: + print(f"{tag} reports={n} ok={okc} ok_pct={100.0*okc/n:.1f} " + f"retries_mean={retries/n:.2f}") +PYEOF +} + +echo "== A: destination is the DUT, DUT receiving, nothing armed ==" +a=$(arm A "$DUT_MAC" 1); echo " $a" +echo "== B: destination is an address NOBODY holds (control) ==" +b=$(arm B "$NOBODY" 1); echo " $b" +echo "== C: destination is the DUT, DUT NOT running (control) ==" +c=$(arm C "$DUT_MAC" 0); echo " $c" +echo "== D: destination is the DUT, DUT receiving, MT_AUTO_RSP_EN CLEARED ==" +echo " (single-variable: which mechanism answers?)" +d=$(arm D "$DUT_MAC" 2); echo " $d" +echo "== E: destination is the DUT, DUT receiving, WRONG BSSID in the station APC slot, ENABLED ==" +echo " (the BSSID question's last caveat: does an enabled slot gate a station?)" +e=$(arm E "$DUT_MAC" 3); echo " $e" +echo + +sta_fw_unlink + +# Verdicts. A must differ from BOTH controls, or the instrument is not +# measuring the DUT. +a_ok=$(printf '%s' "$a" | sed -n 's/.*ok_pct=\([0-9.]*\).*/\1/p') +b_ok=$(printf '%s' "$b" | sed -n 's/.*ok_pct=\([0-9.]*\).*/\1/p') +c_ok=$(printf '%s' "$c" | sed -n 's/.*ok_pct=\([0-9.]*\).*/\1/p') +d_ok=$(printf '%s' "$d" | sed -n 's/.*ok_pct=\([0-9.]*\).*/\1/p') +e_ok=$(printf '%s' "$e" | sed -n 's/.*ok_pct=\([0-9.]*\).*/\1/p') + +# EVERY arm must have produced reports. An arm that aborted, or one the peer +# never reported on, yields an empty ok_pct - and an empty value compared +# numerically reads as 0, which is the PASSING value for a control - an +# aborted arm D would read as "MT_AUTO_RSP_EN is the gate". +for pair in "A:$a" "B:$b" "C:$c" "D:$d" "E:$e"; do + t=${pair%%:*}; v=${pair#*:} + case "$v" in + *ABORTED*) echo "ARM $t ABORTED: ${v#* ABORTED }" + echo "GATE AUTOACK: INCONCLUSIVE"; exit 2 ;; + esac + n=$(printf '%s' "$v" | sed -n 's/.*reports=\([0-9]*\).*/\1/p') + if [ -z "${n:-}" ] || [ "${n:-0}" -eq 0 ]; then + echo "ARM $t produced NO tx.report events, so it is not a measurement and" + echo "must not be compared. Check the peer runs DEVOURER_TX_WITH_RX=thread" + echo "and that its generation has a CCX path." + echo "GATE AUTOACK: INCONCLUSIVE"; exit 2 + fi +done + +if awk -v a="${a_ok:-0}" -v b="${b_ok:-0}" -v c="${c_ok:-0}" 'BEGIN{ + printf "A (DUT present) ok=%.1f%%\nB (nobody) ok=%.1f%%\nC (DUT absent) ok=%.1f%%\n", a, b, c + exit !(a > b + 40 && a > c + 40) +}'; then ok "the DUT acknowledges unicast to its own address with nothing armed" +else bad "A is not clearly above both controls - the DUT is not shown to acknowledge" +fi + +# D decides whether SetStationIdentity's MT_AUTO_RSP_EN refusal is justified. +# It is a separate verdict: the claim above stands either way. +if awk -v a="${a_ok:-0}" -v d="${d_ok:-0}" 'BEGIN{ + printf "D (AUTO_RSP_EN off) ok=%.1f%%\n", d + exit !(d < a - 40) +}'; then ok "MT_AUTO_RSP_EN is the gate - SetStationIdentity is right to refuse when it is clear" +else bad "MT_AUTO_RSP_EN is NOT the gate here - the seam refuses on a bit that does not control this" +fi + +# E closes the BSSID question's last caveat: `mt7612uprobe sta` leaves mt76's +# per-slot enable clear, so "a wrong BSSID changes nothing" could mean +# "nothing was reading the BSSID". Here the slot is wrong AND enabled. +if awk -v a="${a_ok:-0}" -v e="${e_ok:-0}" 'BEGIN{ + printf "E (wrong BSSID, slot ENABLED) ok=%.1f%%\n", e + exit !(e > a - 20) +}'; then ok "a wrong BSSID in the ENABLED station APC slot does not gate the station" +else bad "an enabled APC slot DOES gate the station - the BSSID gate's null result was an artefact of the enable bit being clear" +fi + +echo +echo "=== $pass passed, $fail failed (logs: $OUT) ===" +exit $(( fail > 0 )) diff --git a/tests/mt7612u_sta_identity.sh b/tests/mt7612u_sta_identity.sh new file mode 100755 index 00000000..9ea8b3ed --- /dev/null +++ b/tests/mt7612u_sta_identity.sh @@ -0,0 +1,324 @@ +#!/usr/bin/env bash +# mt7612u_sta_identity.sh - the two measurements behind the MT7612U half of +# SetStationIdentity, end to end: +# +# BSSID what does programming the BSSID do for a MANAGED STATION? +# auto-ACK does this MAC auto-ACK unicast to its own address with nothing +# armed, and what happens to that if MT_MAC_ADDR is moved? +# +# The auto-ACK verdict itself comes from tests/mt7612u_sta_autoack.sh (it asks +# the transmitter); the `staack` gate run here is kept for the register state +# it prints and for its MT_MAC_ADDR-moved arm. +# +# Results and how to read them: docs/mt7612u-station-identity.md. +# +# Rig: a second adapter with AP-mode support runs hostapd and is the AP; the +# MT7612U is the device under test and is driven by build/mt7612uprobe. The +# BSSID does not choose the APC slot: mt76 keys a station's slot on the +# station's OWN address (sta_station_slot in the bring-up tool), which is slot +# 0 for a factory MAC - so on such a DUT the "slot 0" and "station slot" arms +# (C and D) write the same slot. The BSSID here is locally administered only +# so it cannot collide with a real device's address. +# +# sudo tests/mt7612u_sta_identity.sh +# sudo AP_SYSFS=1-1 DUT_SYSFS=7-1 CH=6 tests/mt7612u_sta_identity.sh +# +# RIG REQUIREMENT: both adapters at high speed (480 Mbit/s) or better, and no +# hub between either of them and the host that negotiates FULL speed (check +# with `lsusb -t`). That is necessary, not sufficient: an rtw88 AP (RTL8812BU, +# `rtw88_8822bu: failed to get tx report from firmware`) has stalled its +# transmit path under the 300 frames/s stimulus on a full-speed hub AND on a +# SuperSpeed root port, its beacons dropping with it +# (docs/mt7612u-station-identity.md). The harness catches that - the injector +# rate check refuses a table fed at under half the asked rate - but it cannot +# prevent it; a stalled run is re-run, not read. +# +# PORTABILITY: +# - The AP is judged up by `iw dev info` reporting `type AP`, not by +# hostapd's log: some hostapd builds (Arch 2.11) accept -f but never write +# the file. The -f log is still kept for diagnostics where it is written. +# - An AP adapter can re-enumerate to a DIFFERENT sysfs path when its driver +# first binds (seen: bus 9 -> 10, 3-2.3.3 -> 4-2.3.3). Read AP_SYSFS from +# `lsusb -t` after the driver has loaded; a stale one is refused. +# +# Env: AP_SYSFS, DUT_SYSFS, CH, BSSID, SECS, OUT, FW_DIR. + +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${BUILD:-$ROOT/build}" +# The bring-up tool resolves its firmware directory RELATIVE TO THE WORKING +# DIRECTORY ("firmware/mt7662_rom_patch.bin"), and the symlink below is created +# at $ROOT. Running this script from anywhere else therefore fails the DUT's +# firmware load, which surfaces as "could not read the DUT's MAC" - a message +# that names neither the cause nor the cure. Pin the directory instead. +cd "$ROOT" || exit 1 +AP_SYSFS="${AP_SYSFS:-1-1}" +DUT_SYSFS="${DUT_SYSFS:-7-1}" +CH="${CH:-6}" +BSSID="${BSSID:-02:42:75:05:d6:aa}" +SECS="${SECS:-20}" +# Unset: a fresh private directory (sta_out_prepare in the lib). +OUT="${OUT:-}" +FW_DIR="${FW_DIR:-/lib/firmware/mediatek}" + +[ "$(id -u)" = 0 ] || { echo "must run as root"; exit 2; } +command -v hostapd >/dev/null || { echo "hostapd is required"; exit 2; } +# shellcheck source=tests/mt7612u_sta_lib.sh +. "$ROOT/tests/mt7612u_sta_lib.sh" +sta_out_prepare || exit 2 +sta_lock_take || exit 2 +sta_pid_init hostapd inject gate + +# mt7612uprobe takes no firmware-directory argument and looks for ./firmware, +# so give it one rather than requiring the caller to cd somewhere specific. +# Only a link THIS run created is removed afterwards - anything already at +# $ROOT/firmware, a dangling symlink included, is the operator's. +sta_fw_link + +AP_IF="" +# The accepted AP's idVendor:idProduct:serial, recorded once the guard has +# passed; cleanup re-enumerates AP_SYSFS only while it still names this device. +AP_ID="" +# Set only once hostapd is up on a verified AP-capable interface: before +# that, the trap has no business re-enumerating anything (a wrong or default +# AP_SYSFS naming a hub would power-cycle every device under it). +AP_REENUM=no +CLEANED=no +# shellcheck disable=SC2317 # reached through the traps below +cleanup() { + [ "$CLEANED" = yes ] && return 0 + CLEANED=yes + sta_fw_unlink + sta_pid_kill inject + sta_pid_kill gate + sta_dut_handback + [ "$AP_REENUM" = yes ] || { sta_lock_release; return 0; } + # hostapd -B daemonizes; its PID is the one it wrote to -P for this run. + sta_pid_kill hostapd + sleep 1 + iw dev staid_mon del 2>/dev/null + # RE-ENUMERATE the AP adapter, do not just bounce the link. + # + # hostapd's `bssid=` leaves the interface carrying that address after it + # exits, and the adapter does not recover from `ip link down/up` - it comes + # back still holding the BSSID, DOWN, and scanning nothing. Left that way it + # silently breaks the next harness that expects this adapter to be a + # station (tests/mt7612u_ap_onair.sh, for one). + if [ -n "$AP_ID" ] && [ "$(sta_usb_id "$AP_SYSFS")" = "$AP_ID" ]; then + echo 0 > "/sys/bus/usb/devices/$AP_SYSFS/authorized" 2>/dev/null + sleep 3 + echo 1 > "/sys/bus/usb/devices/$AP_SYSFS/authorized" 2>/dev/null + sleep 8 + else + echo "AP_SYSFS=$AP_SYSFS no longer names the accepted AP ($AP_ID) -" \ + "not re-enumerating it" + fi + AP_IF=$(sta_first_netdev "$AP_SYSFS") + [ -n "$AP_IF" ] && { + rfkill unblock wlan 2>/dev/null + ip link set "$AP_IF" up 2>/dev/null + nmcli device set "$AP_IF" managed yes >/dev/null 2>&1 + } + sta_lock_release +} +trap cleanup EXIT +# AND IT MUST STOP: with INT/TERM on the EXIT trap the shell runs cleanup +# and then CARRIES ON into the next arm. cleanup is idempotent, so the EXIT +# pass after it is harmless. +trap 'cleanup; exit 130' INT TERM + +# --- the AP ---------------------------------------------------------------- +# THE AP GUARD. Cleanup re-enumerates AP_SYSFS as root, so it is accepted only +# when ALL of these hold, checked before anything is written to it: +# 1. it is a USB device (idVendor readable) and not a hub (class 09); +# 2. it is not the DUT's path; +# 3. its interface 0 carries a WIRELESS netdev (a phy80211 link); +# 4. that netdev carries no default route, IPv4 or IPv6 - an adapter the +# host is using for its uplink is never an AP here; +# 5. its phy advertises AP mode. +# Anything else is refused with the reason. +ap_refuse() { + echo "refusing AP_SYSFS=$AP_SYSFS: $* - cleanup would re-enumerate it." + exit 2 +} +ap_cls=$(cat "/sys/bus/usb/devices/$AP_SYSFS/bDeviceClass" 2>/dev/null) +ap_vid=$(cat "/sys/bus/usb/devices/$AP_SYSFS/idVendor" 2>/dev/null) +[ -n "$ap_vid" ] || ap_refuse "not a USB device - if its driver just loaded, it may have moved; re-read lsusb -t" +[ "$ap_cls" != "09" ] || ap_refuse "a hub" +[ "$AP_SYSFS" != "$DUT_SYSFS" ] || ap_refuse "the DUT's own path" +AP_IF=$(sta_first_netdev "$AP_SYSFS") +if [ -z "$AP_IF" ]; then + echo "$AP_SYSFS:1.0" > /sys/bus/usb/drivers_probe 2>/dev/null + sleep 3 + AP_IF=$(sta_first_netdev "$AP_SYSFS") +fi +[ -n "$AP_IF" ] || ap_refuse "no network interface on it" +[ -e "/sys/class/net/$AP_IF/phy80211" ] || ap_refuse "$AP_IF is not wireless" +for fam in -4 -6; do + ip "$fam" route show default 2>/dev/null | + grep -qw "dev $AP_IF" && ap_refuse "$AP_IF carries a default route" +done +PHY=$(basename "$(readlink -f "/sys/class/net/$AP_IF/phy80211")") +iw phy "$PHY" info 2>/dev/null | grep -q '\* AP$' || + ap_refuse "$AP_IF ($PHY) does not support AP mode" +AP_ID=$(sta_usb_id "$AP_SYSFS") + +echo "AP $AP_IF ($PHY) bssid $BSSID ch$CH" +echo "DUT $DUT_SYSFS (MT7612U)" + +# hostapd and NetworkManager fight over the interface; and a previous run can +# leave the vif in AP type, which makes hostapd fail with "Match already +# configured" rather than anything that names the real problem. +nmcli device set "$AP_IF" managed no >/dev/null 2>&1 +sleep 1 +ip link set "$AP_IF" down 2>/dev/null +iw dev "$AP_IF" set type managed 2>/dev/null +ip link set "$AP_IF" up 2>/dev/null + +cat > "$OUT/hostapd.conf" </dev/null 2>&1 +# Up means the interface is in AP mode, read from the kernel (host- +# independent); the log is diagnostics only. +ap_up=no +for _ in 1 2 3 4 5 6 7 8 9 10; do + if iw dev "$AP_IF" info 2>/dev/null | grep -q 'type AP'; then + ap_up=yes; break + fi + sleep 1 +done +AP_REENUM=yes # hostapd may have run and left its bssid behind +if [ "$ap_up" != yes ]; then + echo "hostapd did not bring $AP_IF up in AP mode:" + tail -12 "$OUT/hostapd.log" 2>/dev/null || echo "(no hostapd log written)" + exit 1 +fi + +# --- free the DUT ---------------------------------------------------------- +sta_dut_take || exit 2 + +# The contract gate needs no AP, prints the DUT's own address, and is the +# cheapest thing that fails loudly if the DUT is not usable - so it runs +# first and doubles as this harness's source for the MAC. (`regs` does not +# work for this: it never calls mt_eeprom_init, so it prints no MAC.) +echo +echo "########## the SetStationIdentity contract (no AP needed) ##########" +# PIPESTATUS[0], captured straight after each pipeline: `$?` is tee's +# status, so a failing probe would score as a pass. PIPESTATUS is a bash-ism, +# which is why this script is #!/usr/bin/env bash and not #!/bin/sh. +"$BUILD/mt7612uprobe" staid 2>&1 | tee "$OUT/staid.txt" +staid=${PIPESTATUS[0]} +DUT_MAC=$(sed -n 's/^own \([0-9a-f:]\{17\}\).*/\1/p' "$OUT/staid.txt" | head -1) +[ -n "$DUT_MAC" ] || { echo "could not read the DUT's MAC from the staid gate"; exit 1; } +echo "DUT MAC $DUT_MAC" + +# --- probe-response gate: no monitor vif needed ---------------------------- +# Its auto-ACK VERDICT is not evidence and reads INCONCLUSIVE (rc 2) against +# hostapd by construction - the control cannot move (mt7612uprobe's gate_staack +# comment; the auto-ACK answer is tests/mt7612u_sta_autoack.sh). What this +# harness takes from it is arm C: MT_MAC_ADDR moved under the managed filter +# receives nothing. rc 1 is an error; rc 0 or 2 is a run, judged on arm C. +echo +echo "########## probe-response gate (arm C: MT_MAC_ADDR moved) ##########" +"$BUILD/mt7612uprobe" staack "$CH" "$SECS" "$BSSID" 2>&1 | tee "$OUT/staack.txt" +r_ack=${PIPESTATUS[0]} +if [ "$r_ack" = 0 ] || [ "$r_ack" = 2 ]; then + if grep -q '^C (MT_MAC_ADDR moved) : received NOTHING' "$OUT/staack.txt"; then + r_ack=0 + echo "arm C: received nothing with MT_MAC_ADDR moved - as recorded" + elif grep -q '^C (MT_MAC_ADDR moved) : *[0-9]' "$OUT/staack.txt"; then + echo "arm C RECEIVED with MT_MAC_ADDR moved - contradicts the record" + r_ack=1 + else + # Arm A or B got no probe response and the gate stopped before arm C. + echo "the gate stopped before reporting arm C - no measurement" + r_ack=2 + fi +fi + +# --- BSSID: needs unicast aimed at the DUT for the whole run ---------------- +echo +echo "########## BSSID: what does programming the BSSID change? ##########" +iw dev staid_mon del 2>/dev/null +if ! { iw phy "$PHY" interface add staid_mon type monitor 2>/dev/null && + ip link set staid_mon up 2>/dev/null; }; then + echo "no monitor vif on $PHY: the BSSID gate would measure broadcast" + echo "reception only, which is not the question - refusing to run it." + exit 1 +fi +# ORDER MATTERS. The gate's bring-up runs the MT7612U's calibrations, whose +# MCU replies arrive late under a strong transmitter nearby (mcu.cpp); the +# unicast stimulus is a 300 pps flood from 20 cm. So the gate starts first, +# the injector only once the gate prints "bring-up done", and the gate then +# pauses 3 s before arm A so the stimulus covers every arm. +: > "$OUT/bssid.txt" +"$BUILD/mt7612uprobe" sta "$CH" "$SECS" "$BSSID" > "$OUT/bssid.txt" 2>&1 & +sta_gate=$! +sta_pid_record gate "$sta_gate" +waited=0 +until grep -q '^GATE STA: bring-up done' "$OUT/bssid.txt" 2>/dev/null; do + if ! kill -0 "$sta_gate" 2>/dev/null || [ "$waited" -ge 120 ]; then + break + fi + sleep 1; waited=$((waited + 1)) +done +inj_t0=$(date +%s) +if grep -q '^GATE STA: bring-up done' "$OUT/bssid.txt" 2>/dev/null; then + # Without this every arm reads to_us=0: hostapd sends an unassociated + # station no unicast, so the gate would measure broadcast reception only + # and could not answer the question it exists for. + python3 "$ROOT/tests/sta_unicast_inject.py" staid_mon "$DUT_MAC" "$BSSID" \ + $(( SECS * 6 + 40 )) 300 > "$OUT/inject.log" 2>&1 & + sta_pid_record inject $! + echo "(unicast injector running on staid_mon)" +fi +wait "$sta_gate" +r_bss=$? +rm -f "$OUT/.pid_gate" +inj_secs=$(( $(date +%s) - inj_t0 )) +cat "$OUT/bssid.txt" +# Stop the injector (it prints its count on SIGTERM) and require that it +# actually injected - and at a plausible rate: an AP whose transmit path has +# stalled (rtw88 logs "failed to get tx report from firmware") sends a +# fraction of what was asked while its beacons stop too, and the gate then +# sees an empty channel that is the AP's fault, not the DUT's. +sta_pid_kill inject +injected=$(sed -n 's/^injected \([0-9][0-9]*\) unicast frames.*/\1/p' "$OUT/inject.log" | tail -1) +if [ "${injected:-0}" -gt 0 ] 2>/dev/null; then + echo "injector: $injected unicast frames at $DUT_MAC in ${inj_secs}s (asked 300/s)" + if [ "$inj_secs" -gt 0 ] && [ $((injected / inj_secs)) -lt 150 ]; then + echo "the injector achieved under half its rate: the AP's transmit path" + echo "stalled (check the kernel log for its driver), so this BSSID table" + echo "is not a measurement of the DUT." + [ "$r_bss" = 0 ] && r_bss=2 + fi +else + echo "the unicast injector injected NOTHING (see $OUT/inject.log) - the BSSID" + echo "table measured broadcast reception only." + [ "$r_bss" = 3 ] || r_bss=1 # an interrupted gate stays "no verdict" +fi + +echo +echo "=== logs: $OUT ===" +# A gate's rc 3 is INTERRUPTED - no verdict: never a pass. +[ "${staid:-0}" = 0 ] || echo "the contract gate FAILED - see $OUT/staid.txt" +case "$r_ack" in + 0) ;; + 3) echo "the probe-response gate was INTERRUPTED - no verdict" ;; + *) echo "the probe-response gate's arm C did not hold - see $OUT/staack.txt" ;; +esac +case "$r_bss" in + 0) ;; + 3) echo "the BSSID gate was INTERRUPTED - no verdict" ;; + *) echo "the BSSID gate did not pass - see $OUT/bssid.txt" ;; +esac +exit $(( r_bss != 0 || r_ack != 0 || ${staid:-0} != 0 )) diff --git a/tests/mt7612u_sta_lib.sh b/tests/mt7612u_sta_lib.sh new file mode 100644 index 00000000..071671f6 --- /dev/null +++ b/tests/mt7612u_sta_lib.sh @@ -0,0 +1,263 @@ +# shellcheck shell=sh +# mt7612u_sta_lib.sh - shared plumbing for the MT7612U station harnesses +# (tests/mt7612u_sta_identity.sh, _autoack.sh, _uplink.sh). Sourced, not run. +# +# Four rules these scripts run as root under: +# +# - A private OUT. sta_out_prepare() gives an unset OUT a fresh +# `mktemp -d` directory (mode 0700). A given OUT must not be a symlink, +# must be a directory owned by root or by the invoking user (SUDO_UID +# when run through sudo), and is created 0700 when missing - so a local +# user cannot point this root run's writes somewhere else. +# - One run per OUT. sta_lock_take() claims $OUT/.lock (mkdir is atomic) and +# refuses while the run that holds it is alive, so two concurrent runs +# cannot share - and kill each other through - one set of PID files. A +# lock whose holder is gone is reclaimed. (The adapters are exclusive +# anyway: mt7612uprobe and the Realtek demos take a per-adapter lock.) +# - Kill only what this run started, by recorded PID. No pattern kills, and +# no PID read from a file an earlier run left behind: sta_pid_init() +# removes stale PID files before anything is started. +# - Hand every adapter back: the Realtek peer through sta_peer_handback() +# (below), the AP in tests/mt7612u_sta_identity.sh's cleanup, and the DUT +# here. The harnesses unbind the MT7612U from mt76x2u so +# mt7612uprobe can claim it; sta_dut_handback() re-enumerates it with an +# `authorized` 0/1 toggle so the kernel driver binds again, exactly as +# tests/mt7612u_ap_onair.sh does - and only after confirming the path +# still names an MT7612U (idVendor 0e8d, idProduct 7612), because a stale +# DUT_SYSFS would otherwise re-enumerate whatever else is plugged there. +# The toggle is not a power cycle: chip state survives it. +# +# Needs: DUT_SYSFS, OUT (and PEER_SYSFS for the sta_peer_* helpers). + +sta_out_prepare() { + if [ -z "${OUT:-}" ]; then + OUT=$(mktemp -d "${TMPDIR:-/tmp}/mt7612u-sta.XXXXXX") || { + echo "could not create a private OUT directory"; return 1; } + return 0 + fi + if [ -L "$OUT" ]; then + echo "refusing OUT=$OUT - it is a symlink"; return 1 + fi + if [ ! -e "$OUT" ]; then + # One level only, so the mode applies to the directory this run owns. + mkdir -m 0700 "$OUT" || { + echo "could not create OUT=$OUT (its parent must exist)"; return 1; } + fi + if [ ! -d "$OUT" ]; then + echo "refusing OUT=$OUT - not a directory"; return 1 + fi + _sta_owner=$(stat -c %u "$OUT" 2>/dev/null) + if [ "$_sta_owner" != "$(id -u)" ] && [ "$_sta_owner" != "${SUDO_UID:-x}" ]; then + echo "refusing OUT=$OUT - owned by uid $_sta_owner, not by root or the" \ + "invoking user" + return 1 + fi + return 0 +} + +# A process's start time in clock ticks since boot (/proc/PID/stat field 22), +# or nothing. Field 2 is the command name in parentheses and may hold spaces, +# so the fields are counted from after its closing parenthesis. +sta_proc_start() { + sed 's/^.*) //' "/proc/$1/stat" 2>/dev/null | cut -d' ' -f20 +} + +STA_LOCKED=no +# The lock records "PID starttime". A holder counts as live only when that +# PID exists AND started at the recorded time - a recycled PID of an +# unrelated process is a stale lock, not a live run. +sta_lock_take() { + if ! mkdir "$OUT/.lock" 2>/dev/null; then + read -r _sta_holder _sta_hstart < "$OUT/.lock/pid" 2>/dev/null + case "${_sta_holder:-}" in + ''|*[!0-9]*) ;; + *) if [ -n "${_sta_hstart:-}" ] && + [ "$(sta_proc_start "$_sta_holder")" = "$_sta_hstart" ]; then + echo "OUT=$OUT is in use by run $_sta_holder - refusing; give this" \ + "run its own OUT" + return 1 + fi ;; + esac + rm -rf "$OUT/.lock" + mkdir "$OUT/.lock" 2>/dev/null || { echo "could not lock OUT=$OUT"; return 1; } + fi + echo "$$ $(sta_proc_start "$$")" > "$OUT/.lock/pid" + STA_LOCKED=yes + return 0 +} + +sta_lock_release() { + [ "$STA_LOCKED" = yes ] || return 0 + STA_LOCKED=no + rm -rf "$OUT/.lock" +} + +sta_is_mt7612u() { + [ "$(cat "/sys/bus/usb/devices/$1/idVendor" 2>/dev/null)" = "0e8d" ] && + [ "$(cat "/sys/bus/usb/devices/$1/idProduct" 2>/dev/null)" = "7612" ] +} + +STA_DUT_TAKEN=no +STA_DUT_ID="" +# Refuse a DUT_SYSFS that is not an MT7612U, then unbind it from mt76x2u and +# require that interface 0 really has no driver afterwards - a failed unbind +# leaves the kernel driver owning the chip under the probe. Only a DUT that was +# taken is handed back, and only while DUT_SYSFS still reports the +# idVendor:idProduct:serial recorded here. +sta_dut_take() { + if ! sta_is_mt7612u "$DUT_SYSFS"; then + echo "refusing DUT_SYSFS=$DUT_SYSFS - not an MT7612U (0e8d:7612)" + return 1 + fi + if [ -e "/sys/bus/usb/devices/$DUT_SYSFS:1.0/driver" ]; then + echo "$DUT_SYSFS:1.0" > /sys/bus/usb/drivers/mt76x2u/unbind 2>/dev/null + sleep 2 + fi + if [ -e "/sys/bus/usb/devices/$DUT_SYSFS:1.0/driver" ]; then + echo "could not free DUT_SYSFS=$DUT_SYSFS: interface 0 is still bound to" \ + "$(basename "$(readlink -f "/sys/bus/usb/devices/$DUT_SYSFS:1.0/driver")")" + return 1 + fi + STA_DUT_ID=$(sta_usb_id "$DUT_SYSFS") + STA_DUT_TAKEN=yes + return 0 +} + +sta_dut_handback() { + [ "$STA_DUT_TAKEN" = yes ] || return 0 + STA_DUT_TAKEN=no + if ! sta_is_mt7612u "$DUT_SYSFS" || + [ "$(sta_usb_id "$DUT_SYSFS")" != "$STA_DUT_ID" ]; then + echo "DUT_SYSFS=$DUT_SYSFS no longer names the DUT taken ($STA_DUT_ID)" \ + "- not re-enumerating it" + return 0 + fi + echo 0 > "/sys/bus/usb/devices/$DUT_SYSFS/authorized" 2>/dev/null + sleep 2 + echo 1 > "/sys/bus/usb/devices/$DUT_SYSFS/authorized" 2>/dev/null +} + +# PID files live in $OUT as .pid_. Every name a script uses is listed +# once in sta_pid_init so a previous run's file is gone before this run +# starts anything. +sta_pid_init() { + for _sta_n in "$@"; do rm -f "$OUT/.pid_$_sta_n"; done +} + +sta_pid_record() { echo "$2" > "$OUT/.pid_$1"; } + +# Signal ($2, default TERM) the process recorded under $1, reap it if it is +# our child, and forget it. A process started inside a command substitution +# is not this shell's child, so `wait` returns at once for it: poll `kill -0` +# for up to 10 s so the caller knows it has really exited (a demo's chip +# de-init runs after the signal). Returns 1, and says so, if it is still +# alive then; 0 otherwise, and silently when nothing is recorded. +sta_pid_kill() { + [ -f "$OUT/.pid_$1" ] || return 0 + _sta_pid=$(cat "$OUT/.pid_$1" 2>/dev/null) + rm -f "$OUT/.pid_$1" + case "$_sta_pid" in ''|*[!0-9]*) return 0 ;; esac + kill "-${2:-TERM}" "$_sta_pid" 2>/dev/null + wait "$_sta_pid" 2>/dev/null + _sta_t=0 + while kill -0 "$_sta_pid" 2>/dev/null; do + if [ "$_sta_t" -ge 100 ]; then + echo "$1 (pid $_sta_pid) is still running 10 s after SIG${2:-TERM}" + return 1 + fi + sleep 0.1; _sta_t=$((_sta_t + 1)) + done + return 0 +} + +# A USB device's identity as idVendor:idProduct:serial (serial empty when the +# device has none), or nothing when the path names no device. Recorded when a +# device is accepted and compared before anything destructive is done to the +# same path later: a different device can enumerate there in between. +sta_usb_id() { + [ -e "/sys/bus/usb/devices/$1/idVendor" ] || return 0 + printf '%s:%s:%s\n' \ + "$(cat "/sys/bus/usb/devices/$1/idVendor" 2>/dev/null)" \ + "$(cat "/sys/bus/usb/devices/$1/idProduct" 2>/dev/null)" \ + "$(cat "/sys/bus/usb/devices/$1/serial" 2>/dev/null)" +} + +# The Realtek peer (PEER_SYSFS) is opened by txdemo / rxdemo, whose libusb +# open detaches its kernel driver and never re-attaches it. sta_peer_record() +# checks and notes the peer's identity before the run; sta_peer_opened() marks +# it touched (a file in OUT, because the peer is started inside a command +# substitution whose variables the parent never sees) just before a peer +# process starts; sta_peer_handback() re-enumerates it with an `authorized` +# 0/1 toggle so its driver binds again - only when this run did open it, and +# only while PEER_SYSFS still reports the recorded identity, so a device that +# replaced it at the same path is left alone. +STA_PEER_ID="" +# The peer must be the adapter the run was told about: PEER_VID:PEER_PID at +# PEER_SYSFS, not a hub, not the DUT's path. Checked before anything runs. +sta_peer_record() { + _sta_pd="/sys/bus/usb/devices/$PEER_SYSFS" + if [ "$PEER_SYSFS" = "$DUT_SYSFS" ]; then + echo "refusing PEER_SYSFS=$PEER_SYSFS - it is the DUT's path"; return 1 + fi + if [ "$(cat "$_sta_pd/bDeviceClass" 2>/dev/null)" = "09" ]; then + echo "refusing PEER_SYSFS=$PEER_SYSFS - a hub"; return 1 + fi + _sta_want=$(printf '%04x:%04x' "$((PEER_VID))" "$((PEER_PID))" 2>/dev/null) + _sta_have="$(cat "$_sta_pd/idVendor" 2>/dev/null):$(cat "$_sta_pd/idProduct" 2>/dev/null)" + if [ "$_sta_have" != "$_sta_want" ]; then + echo "refusing PEER_SYSFS=$PEER_SYSFS - it reports $_sta_have, not" \ + "PEER_VID:PEER_PID $_sta_want" + return 1 + fi + STA_PEER_ID=$(sta_usb_id "$PEER_SYSFS") + rm -f "$OUT/.peer_opened" + return 0 +} + +sta_peer_opened() { : > "$OUT/.peer_opened"; } + +sta_peer_handback() { + [ -n "$STA_PEER_ID" ] || return 0 + _sta_peer_id=$STA_PEER_ID + STA_PEER_ID="" + if [ ! -e "$OUT/.peer_opened" ]; then + return 0 + fi + rm -f "$OUT/.peer_opened" + if [ "$(sta_usb_id "$PEER_SYSFS")" != "$_sta_peer_id" ]; then + echo "PEER_SYSFS=$PEER_SYSFS no longer names the recorded peer" \ + "($_sta_peer_id) - not re-enumerating it" + return 0 + fi + echo 0 > "/sys/bus/usb/devices/$PEER_SYSFS/authorized" 2>/dev/null + sleep 2 + echo 1 > "/sys/bus/usb/devices/$PEER_SYSFS/authorized" 2>/dev/null +} + +# mt7612uprobe loads its firmware from ./firmware. sta_fw_link() creates +# $ROOT/firmware -> FW_DIR only when nothing is there - not even a dangling +# symlink, which `-e` alone would miss - and sta_fw_unlink() removes it only +# if this run created it and it still points where this run pointed it. +STA_FW_LINK_OURS=no +sta_fw_link() { + if [ ! -e "$ROOT/firmware" ] && [ ! -L "$ROOT/firmware" ] && + ln -sn "$FW_DIR" "$ROOT/firmware" 2>/dev/null; then + STA_FW_LINK_OURS=yes + fi +} + +sta_fw_unlink() { + [ "$STA_FW_LINK_OURS" = yes ] || return 0 + STA_FW_LINK_OURS=no + [ -L "$ROOT/firmware" ] && + [ "$(readlink "$ROOT/firmware")" = "$FW_DIR" ] && rm -f "$ROOT/firmware" + return 0 +} + +# The first netdev on USB interface :1.0, or nothing. +sta_first_netdev() { + for _sta_d in "/sys/bus/usb/devices/$1:1.0/net/"*; do + [ -e "$_sta_d" ] && { basename "$_sta_d"; return 0; } + done + return 0 +} diff --git a/tests/mt7612u_sta_uplink.sh b/tests/mt7612u_sta_uplink.sh new file mode 100755 index 00000000..0eb064ea --- /dev/null +++ b/tests/mt7612u_sta_uplink.sh @@ -0,0 +1,264 @@ +#!/bin/sh +# mt7612u_sta_uplink.sh - is an MT7612U station's OWN traffic acknowledged? +# +# The other half of AdapterCaps::station_mode_ok's bar. +# tests/mt7612u_sta_autoack.sh measures frames sent TO the DUT; this measures +# frames sent BY it. A station whose uplink is never acknowledged retransmits +# everything to the retry limit and gives up, which looks like a link problem +# and is not. +# +# Instrument: the DUT's own MT_TX_STAT_FIFO, via `mt7612uprobe txs`, which +# reports the MAC's per-MPDU retry count. The peer is a Realtek adapter running +# rxdemo with DEVOURER_ACK_RESPONDER armed on a chosen address - the same +# responder tests/ack_txreport_matrix.sh uses. Results: +# docs/mt7612u-station-identity.md, "The uplink". +# +# Two arms, and the second is what makes the first mean anything: +# +# A peer armed on the address we transmit to -> expect retries ~0 +# B peer armed on a DIFFERENT address -> expect retries pinned +# +# B is the control. It holds the peer present, on channel, and transmitting +# nothing different - only the address it answers for changes. Without it, +# "few retries" might be what this channel always gives. +# +# The DUT's retry limit is set EXPLICITLY (RETRY_LIMIT, default 15, passed to +# the gate as DEVOURER_TX_RETRY_LIMIT) rather than left to whatever the chip +# holds: the uplink question is only answerable when an unacknowledged frame +# is retried, and the gate's frames request an ACK. 15 is the initvals' short +# limit, so the default reproduces docs/mt7612u-station-identity.md's table. +# It is NOT what a library session airs by default - there tx.retry_limit +# defaults to 0 and the stream radiotap to NOACK (IRadio::SetStationIdentity). +# RETRY_LIMIT=0 is refused here: it would make arm B indistinguishable from a +# one-shot send. +# +# RUNTIME. Each harness arm runs the whole `txs` gate - eight gate arms, each +# twice (MAC receiver off, then on), FRAMES frames apiece - and most gate arms +# settle one frame at a time at about 6 frames/s. Measured at FRAMES=200: about +# 9 minutes for arm A; arm B, where nothing is acknowledged, is slower. Budget +# 25 minutes at FRAMES=200 and about a third of that at the default 60 - an +# outer timeout shorter than that cuts arm B off and reads as INCONCLUSIVE. +# +# sudo tests/mt7612u_sta_uplink.sh +# +# Env: PEER_VID, PEER_PID, PEER_SYSFS, DUT_SYSFS, CH, FRAMES, RETRY_LIMIT, OUT. + +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${BUILD:-$ROOT/build}" +# The bring-up tool resolves its firmware directory RELATIVE TO THE WORKING +# DIRECTORY ("firmware/mt7662_rom_patch.bin"), and the symlink below is created +# at $ROOT. Running this script from anywhere else therefore fails the DUT's +# firmware load, which surfaces as "could not read the DUT's MAC" - a message +# that names neither the cause nor the cure. Pin the directory instead. +cd "$ROOT" || exit 1 +PEER_VID="${PEER_VID:-0x0bda}" +PEER_PID="${PEER_PID:-0xc812}" +PEER_SYSFS="${PEER_SYSFS:-5-1}" +DUT_SYSFS="${DUT_SYSFS:-7-1}" +CH="${CH:-6}" +FRAMES="${FRAMES:-60}" +RETRY_LIMIT="${RETRY_LIMIT:-15}" +# Unset: a fresh private directory (sta_out_prepare in the lib). +OUT="${OUT:-}" +FW_DIR="${FW_DIR:-/lib/firmware/mediatek}" +# The address the DUT transmits to. The peer answers for this in arm A and for +# OTHER in arm B. +TARGET="${TARGET:-02:aa:bb:cc:dd:11}" +OTHER="${OTHER:-02:aa:bb:cc:dd:12}" + +[ "$(id -u)" = 0 ] || { echo "must run as root"; exit 2; } +case "$RETRY_LIMIT" in + ''|*[!0-9]*|0) echo "RETRY_LIMIT must be a positive integer (got '$RETRY_LIMIT')"; exit 2 ;; +esac +# shellcheck source=tests/mt7612u_sta_lib.sh +. "$ROOT/tests/mt7612u_sta_lib.sh" +sta_out_prepare || exit 2 +sta_lock_take || exit 2 +sta_pid_init resp dut +sta_peer_record || { sta_lock_release; exit 2; } +# Only a link THIS run created is removed afterwards - anything already at +# $ROOT/firmware, a dangling symlink included, is the operator's. +sta_fw_link + +pass=0; fail=0 +ok() { pass=$((pass+1)); printf ' PASS %s\n' "$*"; } +bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$*"; } + +RESP="" +# shellcheck disable=SC2317 # reached through the traps below +cleanup() { + # arm() runs in a command substitution, so its PIDs are recorded in $OUT + # (tests/mt7612u_sta_lib.sh) for this trap to find. + sta_pid_kill dut + sta_pid_kill resp; peer_gone=$? + RESP="" + sta_dut_handback + # Only once the peer process has really exited: re-enumerating an adapter + # still inside its de-init is what the hand-back must not do. + if [ "$peer_gone" = 0 ]; then sta_peer_handback + else echo "peer still running - not re-enumerating PEER_SYSFS=$PEER_SYSFS"; fi + sta_fw_unlink + sta_lock_release +} +trap cleanup EXIT +# AND IT MUST STOP: with INT/TERM on the EXIT trap the shell runs cleanup +# and then CARRIES ON into the next arm. cleanup is idempotent, so the EXIT +# pass after it is harmless. +trap 'cleanup; exit 130' INT TERM + +sta_dut_take || exit 2 +echo "DUT MT7612U at $DUT_SYSFS transmitting to $TARGET" +echo "peer $PEER_VID:$PEER_PID at $PEER_SYSFS, ch$CH" +echo + +# $1 = tag, $2 = the address the peer answers for +arm() { + tag="$1"; resp="$2" + sta_peer_opened + env DEVOURER_VID="$PEER_VID" DEVOURER_PID="$PEER_PID" \ + DEVOURER_USB_BUS="${PEER_SYSFS%%-*}" DEVOURER_USB_PORT="${PEER_SYSFS#*-}" \ + DEVOURER_CHANNEL="$CH" DEVOURER_ACK_RESPONDER="$resp" \ + DEVOURER_LOG_LEVEL=info \ + "$BUILD/rxdemo" >"$OUT/resp_$tag.jsonl" 2>"$OUT/resp_$tag.err" & + RESP=$! + # Same subshell trap as the autoack harness: record the pid where the + # parent's cleanup can reach it. + sta_pid_record resp "$RESP" + sleep 10 + if ! kill -0 "$RESP" 2>/dev/null; then + printf '%s ABORTED the peer exited: %s' "$tag" "$(tail -1 "$OUT/resp_$tag.err")" + RESP=""; rm -f "$OUT/.pid_resp"; return 1 + fi + # The arm must be VERIFIED, not assumed: an unarmed responder and a + # responder armed on the wrong address look identical from here, and that is + # exactly what arm B is supposed to be. + # The SUCCESS line only: "ack responder" alone also matches the + # backends' refusal and write-failure lines, which would pass as armed. + if ! grep -q "ACK responder armed for" "$OUT/resp_$tag.err"; then + printf '%s ABORTED the peer never reported arming a responder' "$tag" + sta_pid_kill resp; RESP=""; return 1 + fi + + DEVOURER_TX_RETRY_LIMIT="$RETRY_LIMIT" \ + "$BUILD/mt7612uprobe" txs "$CH" "$FRAMES" "$TARGET" \ + >"$OUT/dut_$tag.txt" 2>&1 & + dut=$! + sta_pid_record dut "$dut" + wait "$dut" + rm -f "$OUT/.pid_dut" + # The limit must have LANDED, not merely been asked for: the gate prints + # this line only after mt7612u_set_retry_limit() read it back. + if ! grep -q "^retry limit set to $RETRY_LIMIT " "$OUT/dut_$tag.txt"; then + printf '%s ABORTED the DUT did not confirm retry limit %s: %s' \ + "$tag" "$RETRY_LIMIT" "$(grep -m1 -i 'retry limit' "$OUT/dut_$tag.txt")" + sta_pid_kill resp; RESP=""; return 1 + fi + + # LIVENESS AFTER THE WINDOW. The 10 s probe proves the peer started; if it + # dies mid-dwell the DUT's frames go unanswered, arm B reads 0/200 at the + # retry limit, and that is the CONTROL's passing value - so a dead peer + # would be scored as a working control. + if ! kill -0 "$RESP" 2>/dev/null; then + printf '%s ABORTED the peer died DURING the measurement window: %s' \ + "$tag" "$(tail -1 "$OUT/resp_$tag.err" 2>/dev/null)" + RESP=""; rm -f "$OUT/.pid_resp"; return 1 + fi + sta_pid_kill resp; RESP="" + + # `mt7612uprobe txs` prints the arm table TWICE - once with the MAC receiver + # OFF and once with it ON. With the receiver off the MAC cannot hear an ACK, + # so every unicast arm runs its retry ladder to exhaustion regardless of what + # the peer does (docs/mt7612u-tx-retry.md). + # + # A station runs with its receiver on, so the "MAC receiver ON" table is the + # only one that answers this question. The first arm-d row in the output is + # the receiver-OFF one, which reads UNSETTLED in both arms whatever the peer + # does - so the parser keys on the section header, not on the first match. + # + # The row is arm `d`, "ucast peer ownSA Normal": transmitted from our OWN + # address to the peer with normal ack policy, which is what a station's + # uplink is. Columns: fps, entries/sent, success, mean retries, max retries, + # plus an UNSETTLED marker when too few frames were sent for the 16-slot + # status ring to attribute cleanly. + python3 - "$OUT/dut_$tag.txt" "$tag" <<'PYEOF' +import re, sys +path, tag = sys.argv[1], sys.argv[2] + +section = None +row = None +for line in open(path, errors='replace'): + if 'MAC receiver' in line: + section = 'ON' if 'ON' in line else 'OFF' + continue + if section == 'ON' and re.match(r'\s*d\s+ucast peer ownSA Normal', line): + row = line + break + +if row is None: + print(f"{tag} NOPARSE no receiver-ON arm-d row in {path}") + sys.exit() +m = re.search(r'(\d+)\s*/\s*(\d+)\s+(\d+)\s+([\d.]+)\s+(\d+)', row) +if not m: + print(f"{tag} NOPARSE arm-d row unreadable: {row.strip()}") + sys.exit() +entries, sent, success, mean_rtry, max_rtry = m.groups() +if 'UNSETTLED' in row and int(success) > 0: + # UNSETTLED means fewer status entries landed than frames were sent, so + # the gate cannot guarantee each entry belongs to the arm it is printed + # under. That matters when an arm CLAIMS SUCCESS - refuse it. + # + # It does not matter for an arm that succeeded at nothing, and refusing + # those would make this measurement impossible: the failing control is + # UNSETTLED BY CONSTRUCTION. When no peer acknowledges, every frame runs + # the full RETRY_LIMIT ladder, the MAC is roughly two orders of magnitude + # slower per frame, and the 16-slot status ring can never keep up with + # submission. A control that always reads UNSETTLED is not a control. + # + # The direction of the risk also points the safe way. Misattributed + # entries would come from the NEIGHBOURING arms, which in this table run + # at 200/200 and zero retries - so contamination can only make a failing + # arm look BETTER. An arm reading 0 success at max retries is therefore a + # floor, and a floor is all a control needs to be. + print(f"{tag} UNSETTLED entries={entries}/{sent} success={success} " + f"retries={mean_rtry} - success claimed on unreliable attribution") + sys.exit() +sent_i = int(sent) or 1 +note = " [entries b + 40) +}'; then ok "the station's own uplink is acknowledged, and the control shows the gate can fail" +else bad "A is not clearly above the control - the uplink is not shown to be acknowledged" +fi + +echo +echo "=== $pass passed, $fail failed (logs: $OUT) ===" +exit $(( fail > 0 )) diff --git a/tests/mt7612u_station_selftest.cpp b/tests/mt7612u_station_selftest.cpp new file mode 100644 index 00000000..58f2ce1f --- /dev/null +++ b/tests/mt7612u_station_selftest.cpp @@ -0,0 +1,232 @@ +/* Headless guard for the MT7612U station-identity decision and the ownership + * hand-off (src/mt7612u/StationIdentity.h). + * + * In the style of tests/ack_responder_selftest.cpp. The hardware counterpart, + * `mt7612uprobe staid`, needs a device and root; the logic that can be wrong + * without the silicon being involved is a policy decision rather than a + * register write, so all of it is reachable from here. The two cells that + * matter most: + * + * - BOTH register reads fail CLOSED: a failed MT_AUTO_RSP_CFG read must not + * arm a station whose ability to acknowledge is unknown. + * - a failed port-identity read is reported as a failed read, not laundered + * into a mismatch against 00:00:00:00:00:00. + * + * What this does NOT cover: anything about the silicon. Whether the MAC + * actually receives or acknowledges with a given identity is measured on + * hardware - docs/mt7612u-station-identity.md, whose retraction section is + * worth reading before quoting any number from it. */ +#include +#include +#include + +#include "StationIdentity.h" + +static int failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + failures++; \ + } \ + } while (0) + +namespace { + +constexpr uint32_t kAutoRspEn = 1u << 0; /* MT_AUTO_RSP_EN */ + +const uint8_t kOwn[6] = {0x40, 0xa5, 0xef, 0x5a, 0x32, 0xf8}; +const uint8_t kBssid[6] = {0x02, 0x42, 0x75, 0x05, 0xd6, 0xaa}; +const uint8_t kOther[6] = {0x02, 0x00, 0x00, 0xac, 0x1d, 0x01}; +const uint8_t kMcast[6] = {0x01, 0x00, 0x5e, 0x00, 0x00, 0x01}; +const uint8_t kZero[6] = {0, 0, 0, 0, 0, 0}; + +mt7612u_sta_verdict decide(const uint8_t *own, const uint8_t *bssid, + const uint8_t *port, int port_ok, + uint32_t cfg, int rsp_ok) { + return mt7612u_sta_decide(own, bssid, port, port_ok, cfg, rsp_ok, kAutoRspEn); +} + +void test_the_ordinary_case() { + CHECK(decide(kOwn, kBssid, kOwn, 1, kAutoRspEn, 1) == MT7612U_STA_OK); +} + +void test_malformed_arguments() { + CHECK(decide(nullptr, kBssid, kOwn, 1, kAutoRspEn, 1) == MT7612U_STA_BAD_ARGS); + CHECK(decide(kOwn, nullptr, kOwn, 1, kAutoRspEn, 1) == MT7612U_STA_BAD_ARGS); + CHECK(decide(kMcast, kBssid, kMcast, 1, kAutoRspEn, 1) == MT7612U_STA_MULTICAST); + CHECK(decide(kOwn, kMcast, kOwn, 1, kAutoRspEn, 1) == MT7612U_STA_MULTICAST); + CHECK(decide(kOwn, kOwn, kOwn, 1, kAutoRspEn, 1) == MT7612U_STA_SAME_ADDR); + + /* Argument validation must come BEFORE anything that depends on a register + * read, so a caller mistake is reported as a caller mistake even when the + * device is unreachable. */ + CHECK(decide(kMcast, kBssid, kOwn, 0, 0, 0) == MT7612U_STA_MULTICAST); + CHECK(decide(kOwn, kOwn, kOwn, 0, 0, 0) == MT7612U_STA_SAME_ADDR); +} + +/* BOTH reads must fail CLOSED. If they disagreed, a failed read on one side + * would arm. */ +void test_failed_reads_refuse() { + CHECK(decide(kOwn, kBssid, kOwn, 0, kAutoRspEn, 1) == MT7612U_STA_READ_FAILED); + CHECK(decide(kOwn, kBssid, kOwn, 1, kAutoRspEn, 0) == MT7612U_STA_READ_FAILED); + CHECK(decide(kOwn, kBssid, kOwn, 0, 0, 0) == MT7612U_STA_READ_FAILED); + + /* And specifically: a failed AUTO_RSP read must NOT be waved through just + * because the value that came back happens to look armed - a check of the + * form `read_ok && !(rsp & EN)` skips exactly this case. */ + CHECK(decide(kOwn, kBssid, kOwn, 1, kAutoRspEn, 0) != MT7612U_STA_OK); + + /* A failed port read must not be laundered into a mismatch against the + * all-zero address that zero-initialised locals would hold. The verdict + * has to say the read failed. */ + CHECK(decide(kZero, kBssid, kZero, 0, kAutoRspEn, 1) == MT7612U_STA_READ_FAILED); +} + +void test_port_identity_must_be_ours() { + CHECK(decide(kOwn, kBssid, kOther, 1, kAutoRspEn, 1) == MT7612U_STA_PORT_MISMATCH); + /* The case the hardware gate covers as case 5: a responder holds the port + * identity, so arming a station on our own address must be refused. */ + CHECK(decide(kOwn, kBssid, kOther, 1, kAutoRspEn, 1) != MT7612U_STA_OK); + /* And the reverse - asking to be the address the responder moved it to - + * is refused too, because that address is not this adapter. */ + CHECK(decide(kOther, kBssid, kOwn, 1, kAutoRspEn, 1) == MT7612U_STA_PORT_MISMATCH); +} + +void test_auto_response_engine_must_be_on() { + CHECK(decide(kOwn, kBssid, kOwn, 1, 0, 1) == MT7612U_STA_AUTO_RSP_OFF); + /* Other bits set but not the enable is still off. */ + CHECK(decide(kOwn, kBssid, kOwn, 1, 0xfffffffeu, 1) == MT7612U_STA_AUTO_RSP_OFF); + CHECK(decide(kOwn, kBssid, kOwn, 1, 0xffffffffu, 1) == MT7612U_STA_OK); +} + +/* --- the ownership hand-off ---------------------------------------------- */ +mt7612u_sta_event observe(mt7612u_sta_state *s, const uint8_t *port, + int read_ok, int allow_restore) { + return mt7612u_sta_port_observed( + s, mt7612u_port_compare(port, read_ok, s->own), allow_restore); +} + +void test_port_compare_is_tri_state() { + CHECK(mt7612u_port_compare(kOwn, 1, kOwn) == MT7612U_PORT_SAME); + CHECK(mt7612u_port_compare(kOther, 1, kOwn) == MT7612U_PORT_DIFFERENT); + /* A failed read is UNKNOWN - never DIFFERENT, whatever the buffer holds. */ + CHECK(mt7612u_port_compare(kOther, 0, kOwn) == MT7612U_PORT_UNKNOWN); + CHECK(mt7612u_port_compare(kZero, 0, kOwn) == MT7612U_PORT_UNKNOWN); + CHECK(mt7612u_port_compare(nullptr, 1, kOwn) == MT7612U_PORT_UNKNOWN); +} + +void test_ownership_handoff() { + mt7612u_sta_state s{}; + CHECK(s.armed == 0); + + /* Nothing armed: a responder taking the identity is not a loss, and must + * not produce a diagnostic. */ + CHECK(observe(&s, kOther, 1, 0) == MT7612U_STA_EV_NONE); + + mt7612u_sta_arm(&s, kOwn, kBssid); + CHECK(s.armed == 1); + CHECK(std::memcmp(s.bssid, kBssid, 6) == 0); + CHECK(std::memcmp(s.own, kOwn, 6) == 0); + + /* A write that left the identity where it was - re-armed on the same + * address, or failed before it moved anything - drops nothing. */ + CHECK(observe(&s, kOwn, 1, 0) == MT7612U_STA_EV_NONE); + CHECK(s.armed == 1); + + /* A failed read-back is not a move: the arm stands, and the caller is told + * it is unverified. */ + CHECK(observe(&s, kOther, 0, 0) == MT7612U_STA_EV_UNVERIFIED); + CHECK(s.armed == 1); + + /* A verified move AFTERWARDS invalidates the station - the ordering the + * arm-time check cannot help with, and the one a real caller is likelier + * to hit. */ + CHECK(observe(&s, kOther, 1, 0) == MT7612U_STA_EV_DROPPED); + CHECK(s.armed == 0); + CHECK(s.lost == 1); + + /* Idempotent: observing the move twice is one loss, not two. */ + CHECK(observe(&s, kOther, 1, 0) == MT7612U_STA_EV_NONE); + + /* The identity coming back does NOT re-arm by itself (a stopped responder + * or beacon: the caller re-arms). */ + CHECK(observe(&s, kOwn, 1, 0) == MT7612U_STA_EV_NONE); + CHECK(s.armed == 0); + + /* Re-arming after the responder gives it back works, and the recorded + * BSSID is the new one rather than a survivor of the previous arm. */ + mt7612u_sta_arm(&s, kOwn, kOther); + CHECK(s.armed == 1); + CHECK(s.lost == 0); + CHECK(std::memcmp(s.bssid, kOther, 6) == 0); + mt7612u_sta_clear(&s); + CHECK(s.armed == 0); + CHECK(observe(&s, kOther, 1, 0) == MT7612U_STA_EV_NONE); +} + +/* A beacon start that moves the identity, then fails and unwinds it back: + * the arm it dropped is restored - but only when the identity really is + * back, and a cleared station is never resurrected. */ +void test_failed_start_restores_the_arm() { + mt7612u_sta_state s{}; + mt7612u_sta_arm(&s, kOwn, kBssid); + CHECK(observe(&s, kOther, 1, 0) == MT7612U_STA_EV_DROPPED); + + /* The unwind did not land (still elsewhere) or cannot be read: no. */ + CHECK(observe(&s, kOther, 1, 1) == MT7612U_STA_EV_NONE); + CHECK(observe(&s, kOwn, 0, 1) == MT7612U_STA_EV_NONE); + CHECK(s.armed == 0); + + /* The identity is back: restored, with its own address and BSSID. */ + CHECK(observe(&s, kOwn, 1, 1) == MT7612U_STA_EV_RESTORED); + CHECK(s.armed == 1); + CHECK(s.lost == 0); + CHECK(std::memcmp(s.bssid, kBssid, 6) == 0); + + /* Cleared, then an unwind: nothing to restore. */ + mt7612u_sta_clear(&s); + CHECK(observe(&s, kOwn, 1, 1) == MT7612U_STA_EV_NONE); + CHECK(s.armed == 0); +} + +/* A clear must not leave the previous BSSID readable: the C API returns it to + * callers, and a stale value would name a BSS this station is not on. */ +void test_clear_wipes_the_bssid() { + mt7612u_sta_state s{}; + mt7612u_sta_arm(&s, kOwn, kBssid); + mt7612u_sta_clear(&s); + CHECK(std::memcmp(s.bssid, kZero, 6) == 0); + CHECK(std::memcmp(s.own, kZero, 6) == 0); +} + +/* Argument validation runs before any register I/O. */ +void test_check_args_needs_no_device() { + CHECK(mt7612u_sta_check_args(kOwn, kBssid) == MT7612U_STA_OK); + CHECK(mt7612u_sta_check_args(nullptr, kBssid) == MT7612U_STA_BAD_ARGS); + CHECK(mt7612u_sta_check_args(kMcast, kBssid) == MT7612U_STA_MULTICAST); + CHECK(mt7612u_sta_check_args(kOwn, kOwn) == MT7612U_STA_SAME_ADDR); +} + +} // namespace + +int main() { + test_the_ordinary_case(); + test_malformed_arguments(); + test_failed_reads_refuse(); + test_port_identity_must_be_ours(); + test_auto_response_engine_must_be_on(); + test_port_compare_is_tri_state(); + test_ownership_handoff(); + test_failed_start_restores_the_arm(); + test_clear_wipes_the_bssid(); + test_check_args_needs_no_device(); + + if (failures) { + std::fprintf(stderr, "mt7612u_station_selftest: %d failure(s)\n", failures); + return 1; + } + std::printf("mt7612u_station_selftest: all checks passed\n"); + return 0; +} diff --git a/tests/sta_unicast_inject.py b/tests/sta_unicast_inject.py new file mode 100755 index 00000000..6322055a --- /dev/null +++ b/tests/sta_unicast_inject.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Inject unicast 802.11 data frames at a MAC from a monitor interface. + +Companion to tests/mt7612u_sta_identity.sh and `mt7612uprobe sta`. + +That gate counts what a managed station RECEIVES. Beacons alone only exercise +broadcast reception, and the question it exists to answer is whether the BSSID +registers gate UNICAST delivery to the station's own address. hostapd sends an +unassociated station no unicast at all, so without this the gate reads +to_us=0 in every arm and cannot answer it. This makes the traffic. + +The monitor vif lives on the AP's own phy, so the injection rides the AP's +radio and lands on the AP's channel without needing a third adapter. + + sta_unicast_inject.py [seconds] [pps] + +Note what this does NOT do: mac80211 marks injected frames no-ack by default, +so these do not solicit an acknowledgement and cannot be used to measure one. +Acknowledgement is measured by tests/mt7612u_sta_autoack.sh, which asks the +transmitter (a Realtek peer's per-frame CCX reports). +""" +import signal +import socket +import struct +import sys +import time + +# version 0, pad 0, length 8, present bitmap 0 - no fields, just the header. +RADIOTAP = struct.pack(' 0: %r' % (name, s)) + if cap is not None and v > cap: + raise Usage('%s must be <= %g: %r' % (name, cap, s)) + return v + + +class Stop(Exception): + pass + + +def on_term(signum, frame): + raise Stop() + + +def main(argv): + if len(argv) < 4 or len(argv) > 6: + print(__doc__, file=sys.stderr) + return 2 + try: + mon = argv[1] + dst = mac(argv[2]) + bssid = mac(argv[3]) + secs = positive('seconds', argv[4]) if len(argv) > 4 else 120.0 + pps = positive('pps', argv[5], MAX_PPS) if len(argv) > 5 else 300.0 + except Usage as e: + print('sta_unicast_inject: %s' % e, file=sys.stderr) + print(__doc__, file=sys.stderr) + return 2 + + sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) + sock.bind((mon, 0)) + # The harness stops this with SIGTERM once its gate is done; the count is + # printed either way, because the harness requires a nonzero one. + signal.signal(signal.SIGTERM, on_term) + + # Data frame, FromDS: addr1 = the station, addr2 = addr3 = the BSSID. + # That is the shape an AP's downlink traffic has, which is what a station + # would be filtering for. + body = b'STA-UNICAST-PROBE' * 4 + gap = 1.0 / pps + end = time.time() + secs + sent = 0 + seq = 0 + try: + while time.time() < end: + hdr = (struct.pack('