mt7612u: a MediaTek backend, measured — is it in scope? - #412
Conversation
## Problem Every backend here is Realtek. A consumer scoping a fixed-channel, fixed-rate video link had an MT7612U on the bench and no way to drive it from this library. Whether MediaTek silicon belongs in this project at all is a scope question for the maintainers, and one worth answering against measurements rather than a proposal. ## Change Adds `src/mt7612u/`, a standalone C library plus a per-gate bringup harness, and `docs/mt7612u.md` carrying the measurements, the methods and the limits. `CMakeLists.txt` is untouched. There is no `IRtlDevice` implementation and no `WiFiDriver` dispatch, so nothing in the shipped library changes and CI sees a docs-and-new-directory diff. The subtree builds on its own with `make -C src/mt7612u`. What is measured on hardware, against an RTL8812AU witness running this project's own rxdemo/txdemo: - TX rate is authoritative: three rates match exactly, per-frame alternation is 229/229, and the hardware rate LUT cannot override the descriptor even with MT_TXWI_FLAGS_TX_RATE_LUT set. - TX power: all nine registers identical to what the kernel driver programs for the same channel. - A-MPDU works on injected frames (paggr 0/352 control vs 326/326 armed), lifting 34.03 to 44.55 Mbit/s at 1400 bytes and 7.01 to 15.50 at 200. This is the opposite of the Jaguar1 result in docs/aggregation.md. - Hardware ACK responder: 0 frames at the stimulus radio unarmed, 3500+ ACKs armed. - 40 MHz, TSF, monitor RX with per-chain RSSI, radiotap send_packet and send_packets with USB chaining via MT_TXD_INFO_NEXT_VLD. What it cannot do, measured: channel switch is 526 ms full / 48 ms with calibration skipped against 0.5-2.5 ms on the Realtek parts, because the RF plane lives behind the MCU. FHSS is out of reach. Narrowband 5/10 MHz has no encoding in the rate word. docs/mt7612u.md carries a Counterparts section: one unit, one witness generation, TX power verified against registers rather than radiated power, an RX gain path that has never done anything because this EEPROM has no calibration, no cold-boot test, and nothing in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
josephnef
left a comment
There was a problem hiding this comment.
Scope: yes. A MediaTek backend belongs here. The project will need a refactor first, and that is the first milestone rather than wiring this subtree in as-is.
The seams the refactor has to open, from reading the current tree:
IRtlDevice/RtlAdapter/WiFiDriver::CreateRtlDeviceare Realtek-branded names on an interface that is already vendor-neutral (Init / InitWrite / StartRxLoop / send_packet / SetMonitorChannel / FastRetune / GetAdapterCaps). That is a rename, not a redesign.WiFiDriverdispatches from a SYS_CFG2 read issued as a Realtek vendor request. It needs a vendor-neutral VID:PID gate in front of that read, the way Kestrel already gates PID-first.IRtlTransportis shaped around 16-bit registers and Realtek bulk endpoints. MT7612U is 32-bit registers plus in-band MCU commands over bulk EP8/EP5, so the MediaTek backend gets its own transport behind the same factory rather than a shim over the Realtek one.- A
ChipGenerationentry, aDEVOURER_MT7612UCMake option following theDEVOURER_HAVE_*pattern, and aregress.pycell against the MediaTek vendor driver underreference/.
Licensing is fine. I checked the mt76 file headers upstream: they really are BSD-3-Clause-Clear, so the attribution here is accurate and the code is GPL-2 compatible. A subtree LICENSE notice would be good practice in this tree.
The code has bugs that sit under the measurements, so it is not mergeable even as an unwired subtree. Inline below, most severe first: the public API is declared but never defined; RX corrupts every QoS-data header; TX header length is wrong for BAR/BA/CF-End; async teardown frees in-flight TX transfers; a failed register read feeds all-ones into every RMW; and the harness's caps/ack gates enable MAC RX without draining it, which is the wedge pattern the doc says is fixed.
Two cross-cutting items:
initvals.hsays it is generated by a script inPLAN.md; neither the script norPLAN.md/INVESTIGATION.md/BRINGUP-RESULTS.mdis in the PR, and 13 comments point at them. This project's rule is that tables come fromtools/extract_*.pywith pinned source hashes and a--checkmode that reproduces the checked-in output. The hand-typed constants inphy.candinit.cshould come from the same generator.- Windows/MSVC is first-class here.
__builtin_ctzunder every FIELD_PREP/GET, pthreads throughoutasync.c,nanosleep,clock_gettime, the<libusb-1.0/libusb.h>include path and the gcc-only Makefile all need to land in portability shims during the integration, not per-platform gating.
## Problem Review of OpenIPC#412 found bugs that the hardware measurements sat on top of. Two of them change what the driver puts on air or hands to a caller, one is a use-after-free during teardown, and one made the "standalone library" claim false. ## Change Ten confirmed findings, each with the fix and its evidence. **The public header declared nine functions that had no definition anywhere** (`mt7612u_open/_close/_keep_detached/_set_channel/_set_chainmask/_start/ _stop/_asic_version/_mac_addr`). The bringup tool builds because it calls internals directly, so nothing caught it. They are now defined, and `tests/api_link.c` takes the address of all twenty public entry points while including only the public header, so the link fails if a declaration ever loses its definition again. `set_channel` also disagreed with the internal signature on `unsigned` vs `uint8_t`; it is `uint8_t` now. **The RX L2-pad fold moved a fixed 24 bytes.** L2PAD is only ever set when the header is not 4-aligned, i.e. 26 bytes (QoS) or 30 (4-address), so the last two header bytes were left behind and overwritten by the pad. On a QoS frame those two bytes are the QoS Control field, which means every TID and ack-policy reached the callback as zero. It now moves the real header length, as mt76x02_remove_hdr_pad() does. **Header length treated only RTS and PS-Poll as 16-byte control frames.** BlockAckReq, BlockAck and both CF-End subtypes are also 16, so on TX the L2 pad was inserted ten bytes in, inside the frame. Replaced with a port of ieee80211_hdrlen(): control frames are 16 by default and only CTS and ACK are 10. That is the same function mt76 reaches via ieee80211_get_hdrlen_from_skb() on both sides, so TX and RX now share one implementation instead of two that could drift. Both are covered by `tests/frame_shape.c`, which also carries a negative control: it redoes the old fixed-24 fold and asserts the QoS Control really is destroyed, so a passing test says something. Reverting either fix makes it fail - the RX one reports `QoS Control zeroed by the pad fold: aa aa`. **mt_async_stop() cancelled only RX, then freed every transfer.** On a wedged chip - the case this driver's own notes describe - TX URBs never complete, and libusb owns a submitted transfer until its callback runs. Now both rings are cancelled, both are waited on with the event thread still running, and if either still has transfers outstanding at the deadline the ring is deliberately leaked rather than freeing memory the kernel may still write into. Each slot names its own ring instead of dev->a, so a leaked ring's late completions cannot touch a replacement one. The `fail:` path no longer joins a zeroed pthread_t. `running`, `rx_active` and both inflight counters move under the mutex - volatile orders nothing and makes no read-modify-write atomic - and the statistics move with them behind mt_async_stats(), because the harness was reading them live off the struct. A caller blocked waiting for a TX slot now gives up when the ring stops. **mt_rr() returned ~0u on failure and mt_rmw() consumed it unchecked**, so one exhausted EP0 read wrote 0xFFFFFFFF | val into MT_WLAN_FUN_CTRL, MT_MAC_SYS_CTRL or the BBP AGC block. 0xffffffff is a real value on this part (MT_MAC_CSR0 reads it while the core comes up), so it cannot double as a sentinel: mt_rr_chk() reports failure separately, mt_rmw() refuses to write after a failed read, and mt_wait_for_mac() distinguishes "not ready" from "no transport". mt_poll() polls against a real deadline and aborts on a read failure - one register access can cost VEND_RETRIES * timeout, so counting sleeps let a caller asking for 200 ms block for seconds. The control timeout drops to mt76's own 300 ms. **mt_wr_copy() rounded len up and then memcpy'd from the caller's buffer**, over-reading whenever len % 4. Latent at the current call sites (8 and 32 bytes); it now rounds the transfer up and zero-fills instead. **Two bringup gates enabled MAC RX and never drained EP 4** - the exact wedge pattern documented three paragraphs above them, and one of them then sat through two 200 ms sleeps and a channel switch. Both start the ring before the receiver. mt7612u_start() enables RX only when an RX ring is already running, and the header says why. **Radiotap DBM_TX_POWER was parsed and silently dropped.** It now says so once per process, pointing at the two knobs that do work. **The mt76 TSF bug claim now carries its citation** - mt76x02_usb_core.c:155-158, where tsf = (u64)dw0 << 32 | dw1 feeds only the dev_dbg() on the next line, which is why the order has survived upstream. Also fixed while in send_packets(): the radiotap parse moved into the selection pass. A frame the build pass could still reject would break the chain it was building, since NEXT_VLD and the single trailing zero word are assigned by position - dropping whichever frame happened to be last left the transfer unterminated. ## Verification `make -C src/mt7612u check` - twenty public entry points resolved, frame_shape PASS. Both new tests were mutation-tested: removing one definition, and reverting each frame-shape fix, each makes them fail. No hardware and no privileges needed. Clean build, no warnings. `CMakeLists.txt` is still untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## Problem `src/mt7612u/initvals.h` was hand-copied from mt76 and pointed at a regeneration script in a PLAN.md that is not in this repository. The project rule is that tables come from `tools/extract_*.py` against a pinned `reference/` tree with a `--check` mode, so the checked-in artifact stays re-verifiable from a fresh checkout. ## Change Adds `reference/mt76` as a pinned shallow submodule (`openwrt/mt76` at `be5ce79`) and `tools/extract_mt7612u_tables.py`, following the `extract_8733b_*.py` shape: `UPSTREAM` string, per-source SHA-256, an `EXPECTED` count and table hash, and `--check` that byte-compares the checked-in header. The source here is C initialiser syntax rather than a vendor parameter blob, so the script carries a symbol table over `mt76x02_regs.h` and a small constant-expression evaluator for `BIT` / `GENMASK` / `FIELD_PREP`. Both are deliberately narrow - they understand only what these definitions use and raise on anything else rather than guessing. That is what lets the four `DEFAULT_PROT_CFG_*` macros, defined inside `mt76_write_mac_initvals()` itself, be computed rather than copied. `reference/mt76` is the one entry there that is not a Realtek vendor drop: the MediaTek parts have no out-of-tree vendor driver to mirror, mt76 is the mainline reference, and it is BSD-3-Clause-Clear rather than GPL-2-only, which is why `src/mt7612u/` can carry ported sequences at all. `reference/README.md` says so. ## Verification The generator reproduces the previously hand-typed table byte for byte - all sixty rows, register addresses and values, including the four computed protection-config words. So this also independently confirms the original transcription was correct rather than silently replacing it. `--check` is load-bearing: appending one newline to the header makes it exit 1 with `stale generated output`. No CI workflow checks out submodules, so the twenty builds are unaffected. `make -C src/mt7612u check` still passes. `CMakeLists.txt` still untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## Problem Review of OpenIPC#412 asked for MSVC/Windows portability, which is first-class in this project. Three of the items are correctness issues independent of any compiler; the rest depend on how the subtree eventually joins the build. ## Change **`_SHIFT` was a reserved identifier.** Leading underscore followed by a capital is reserved to the implementation in every scope. Renamed. **`FIELD_PREP`/`FIELD_GET` no longer call `__builtin_ctz`.** MSVC has no such builtin, and the obvious substitute, `_BitScanForward`, is a function with an out-parameter and therefore cannot appear in a constant expression. These macros must stay constant expressions: `ext_cca_chan` in phy.c is a static table built from them. `MT_CTZ` isolates the low bit and binary-searches its position - a constant expression on every compiler, folded to one instruction under optimisation. **libusb include path.** `<libusb.h>` is what the rest of this project includes, letting the build system supply the directory; the standalone Makefile has no pkg-config step and most distributions ship the header under `libusb-1.0/`. `__has_include` tries the project spelling first and falls back. ## What is deliberately not here `async.c` uses pthreads and `usb.c` uses `nanosleep`/`clock_gettime`. This project has no C threading or time shim - its shim is the C++ standard library, which every other backend uses directly. A C shim written now would be deleted when the subtree joins the build, so those two files keep POSIX until then. They are the only two files involved, and the README says so. Likewise the Makefile stays a Makefile: the subtree is deliberately not reachable from `CMakeLists.txt` yet, so a CMake target now would be a target nothing builds. ## Verification `tests/field_macros` is new and checks the replacement against `__builtin_ctz` over every mask the driver can form - all 32 single-bit masks and all 528 contiguous `GENMASK(h, l)` ranges - plus a `FIELD_PREP`/ `FIELD_GET` round-trip on each. It carries a static initialiser built from `FIELD_PREP`, so if `MT_CTZ` ever stops being constant-foldable the test fails to compile rather than silently passing at runtime. Both libusb spellings build and pass: default, and with the header on the include path directly. `make -C src/mt7612u check`: 20 entry points, frame_shape PASS, field_macros 528 masks PASS. No warnings. `CMakeLists.txt` untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
The three fix commits are re-measured on the same bench and the same RTL8812AU witness. Nothing regressed, and the A-MPDU figures reproduce the original PR numbers exactly. - `init` clean, so the 300 ms control timeout and the deadline-based `mt_poll` did not break bring-up. - `rtap`: tag A 401 frames, tag B 388 - `send_packet` and `send_packets` both still air, with `send_packets` still chaining 16 frames per USB transfer. - `ampdu`: 41 734 of 85 580 witnessed frames carry `paggr=1`. 6.26 -> 15.50 Mbit/s at 200 bytes, 34.05 -> 44.55 at 1400 - the same two figures the PR reported. - `caps`: PASS on three consecutive cycles with the RX ring now started before the receiver rather than after it. 40 MHz re-confirmed at 300/300 frames reporting `bw=1`, against a witness also at 40 MHz. TSF 200347 us over a 200000 us sleep. - `arx` on ch1: 278 ambient frames, `rx_err=0`, CCK/OFDM/HT all decoded through the rewritten L2-pad fold. The header-length fix has no on-air observable in these gates: it only changes control frames, and nothing here injects one. The unit test with its negative control is the evidence for that one. Counterparts updated: the offline tests and the generator's `--check` pass locally, but no workflow runs them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## Problem `radiotap.c` mapped VHT bandwidth codes 1-3 to 40 MHz and >=4 to 80. The code names a channel width and the sub-channel used within it, so 2 is "40 (20L)" - a 20 MHz frame in the lower half of a 40 MHz channel, not a 40 MHz frame. A caller asking for 20-in-40 got 40 MHz on air, and 20-in-80 got 80. This contradicted the file's own HT branch, which reads the equivalent HT codes correctly - and this project's `src/ieee80211_radiotap.h:112` names them `IEEE80211_RADIOTAP_MCS_BW_20L` and `_20U`, which is the same semantics spelled out in the repository already. ## Change A table over the eleven codes this radio can express, mapping each to the width the frame is actually sent at. Codes 11 and above are 160 MHz and its sub-channels; the rate word has no 160 MHz encoding, so those log and fall back to 20 rather than silently narrowing. ## Verification `tests/frame_shape` gains the whole table, all eleven codes. Restoring the old expression fails eight of them, so the test is load-bearing rather than decorative. ## Elsewhere in this repository `src/jaguar1/RtlJaguarDevice.cpp:1097-1100` and `src/jaguar3/RtlJaguar3Device.cpp:1940-1943` carry the same `bw >= 1 && bw <= 3 -> CHANNEL_WIDTH_40`, `bw >= 4 && bw <= 10 -> CHANNEL_WIDTH_80` mapping, and are not touched here - a shipping backend's on-air behaviour is not something to change from inside a new-backend PR. Reported so it can be triaged separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Thanks — that review was worth more than the measurements were. Every one of the eleven findings was real, including the VHT one I initially thought I could defend. All are fixed and answered in-thread; I have left the threads for you to resolve. Five commits on top of the original:
Two of the fixes change what the driver puts on air or hands to a caller, so nothing here is claimed from a rebuild alone. The gates were re-run on the same bench against the same RTL8812AU witness:
On sequencing. Your review reads two ways and it changes what I build next, so let me state how I read it and you can correct me.
I took that to mean an unwired subtree is the mergeable shape once the bugs are gone, and that
puts the refactor before wiring, not before landing. So my plan is: this PR lands unwired, and the refactor plus integration arrives as its own PR. Bundling a rename that touches all three shipping backends together with a brand-new backend makes one diff nobody can review. Say the word if you meant the other order and I will hold this one. On the refactor itself, one question, because I would rather ask than guess at the shape of your codebase. Everything substantive in your list I agree with: a vendor-neutral VID:PID gate ahead of the Realtek The part I would push back on gently is the rename. That also decides where the remaining portability work lands. |
`tests/field_macros` was staged in 2ac9b9a before `.gitignore` learned about it, so a 300 KB build artifact went into the tree. The ignore entry landed one commit later and could not retroactively untrack it. The other two test binaries were never tracked; `git ls-files src/mt7612u/tests/` now lists sources only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
## Problem `Packet::Data` in `src/RxPacket.h` is documented as the full 802.11 frame including the trailing FCS, and every Realtek parser here honours it. Whether MT7612U can is an integration question, and guessing at it is how a four-byte truncation ships. Every RX buffer does carry 4-7 bytes past `MPDU_LEN` - over 4263 ambient frames the tail was exactly 4 on 3375 of them and 5-7 on the rest, which reads precisely like a fixed 4-byte field plus USB 4-byte alignment. So the obvious move is to hand a consumer `len + 4` and call it the FCS. ## What it actually is Not the FCS. CRC-32 over the MPDU matched those four bytes on **0 of 4263** frames. The probe's own CRC-32 was checked against the standard `"123456789"` -> `0xcbf43926` vector before the negative was believed. They are the FCE info trailer. mt76's `mt76u_get_rx_entry_len()` computes `min_len = MT_DMA_HDR_LEN + MT_RX_RXWI_LEN + MT_FCE_INFO_LEN`, and `dma.h:48` defines `MT_FCE_INFO_LEN 4`. mt76 never sets `RX_FLAG_INCLUDE_FCS` for this family either. The MAC strips the checksum and does not hand it up. ## Change Documentation only - the code was already correct, it just did not say what it could not do. `docs/mt7612u.md` gains the measurement under RX and a Counterparts entry; the public header's RX callback says the frame carries no FCS and why the trailing bytes are not one. This matters at the boundary: a consumer that trims four bytes because the `Packet::Data` contract invites it - as `tools/bf_report_decode.py` already does - would eat four bytes of payload off every frame. Whatever shape integration takes, the divergence gets declared rather than smoothed over. Also here: `tests/field_macros` was untracked in the previous commit after slipping past `.gitignore`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
One more, found while working out what the integration has to look like, and it is a limit rather than a fix.
Every RX buffer does carry 4–7 bytes past It is not the FCS. CRC-32 over the MPDU matched those four bytes on 0 of 4263 frames. I checked the probe's own CRC-32 against the standard So a consumer that trims four bytes at its protocol boundary — which the Documented in |
|
The integration exists and works, on a branch rather than a second PR — that ordering is yours to call, so I have not opened one: Shape, following your list: The one place I diverged from your list is the rename, and I have argued the case in the comment above rather than quietly skipping it. Everything else is as you described it. Two bugs surfaced that only integration could find — both invisible to the bring-up harness, because it never crosses this boundary:
The second is the one worth the paragraph: 244 beacons looks like a working receiver. Only the A/B against a second implementation of the same silicon made it visible, and that is now written down in
One thing to flag about the branch split: it also carries a subtree fix that arguably belongs in this PR instead — |
mt76 never validates a control channel because it never derives one:
mt76x2u_phy_set_channel() takes the segment centre from cfg80211's chandef, so
an off-grid channel cannot reach it. This port takes a bare channel number and
derives the centre from the standard pairing, which means an off-grid channel
still produces *a* number -- and then transmits 40 MHz wide somewhere the
caller did not ask for, with every register write succeeding and nothing
saying so.
Two cases were reachable straight through mt7612u_set_channel(), which checks
only that the channel is non-zero:
- the arithmetic ran in uint8_t. Control channel 254 computes 256, which
truncates to 0, handing the MCU channel index 0 with the 5 GHz register
set loaded; 255 gives 1.
- in 2.4 GHz the pairing only ever reaches centres 6-9. Channel 1 computes
-1, i.e. 255 after truncation. Channels 12 and 13 would need a secondary
above channel 13.
mt_chan40_centre() computes the centre in int and validates it against the
centres that exist, which catches the off-grid case, both wraps and the
out-of-band case together. The 5 GHz ceiling is 159 rather than 175 because
centres 167 and 175 span past the 5825 MHz that mt7612u_caps declares.
Pinned in tests/frame_shape.c, and mutation-tested: removing the validation
makes nine cases fail. Restoring the uint8_t arithmetic does *not* make the
suite fail, because no wrapped value aliases onto a legal centre -- the int is
there so the refusal names 256 rather than reporting 0 and sending whoever
reads it after the wrong bug. The comment says that rather than claiming a
guard it does not provide.
On air, unchanged: `bringup caps 149` still sends 300 frames at 40 MHz and the
RTL8812AU witness still reports 300 at bw=1.
Also: `make check` now depends on `bringup`. Nothing else compiled it, so a
change that broke a gate shipped green -- which is exactly what happened while
preparing this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Marked ready for review. It should have been from the start — asking you to weigh in on merge order while leaving it a draft was my mistake, not a signal. A defect in this branch, found since your review
Two cases reach the MCU straight through
The caveats matter more than the fix. No measurement in this PR moves: Also here: The second branch, and a question backThe stacked branch now carries the integration plus 80 MHz, and I've brought its build wiring to parity with 80 MHz airs and is witnessed as 80 MHz, with the negative control: a 20 MHz witness decodes 0 of it, and a witness on a sibling control channel of the same group decodes it, which is what shows both ends resolved the same centre. Still one witness generation; VHT 2SS MCS5–9 thin out at 80 MHz in a way I attribute to link budget without having measured the cause; and the periodic RX gain worker ( On sequencing: you asked for the refactor before wiring this in, and I read that as before wiring, not before landing. That is still a guess, and it's the one thing I don't want to decide for you. Either shape is one push from here — land this unwired and keep the integration as a separate PR, or I push the integration commits onto this branch so you review a single full-parity backend against the others. Say which and it's done. |
PR Summary by QodoAdd standalone, hardware-validated MT7612U MediaTek backend
AI Description
Diagram
High-Level Assessment
Files changed (26)
|
Code Review by Qodo
1. Callers can open an unintended adapter
|
Fourteen findings; each was checked against the code and against mt76 rather
than taken on trust.
Fixed:
- **Radiotap HT bandwidth gated on the wrong bit.** The MCS `known` byte
declares bandwidth with HAVE_BW (0x01); HAVE_MCS is 0x02. We tested 0x02,
so a header declaring bandwidth without an MCS index -- legal radiotap --
silently narrowed a requested 40 MHz frame to 20. This is the HT twin of
the VHT bandwidth bug caught in review; the same slip, missed twice.
Mutation-tested: restoring 0x02 fails two cases in both directions.
- **A malformed radiotap header transmitted anyway.** A declared field
running past the declared header length returned the header length, which
both injection entry points read as success and then aired at whatever
defaults had accumulated. Now refused. An unknown *trailing* present bit
still stops cleanly -- that header is well formed, we just cannot read the
rest of it.
- **The table generator's --check crashed on anything before Python 3.13.**
`Path.read_text(newline=...)` is 3.13-only; the documented command and the
ctest cell would TypeError on the 3.12 that CI images ship. This one was
invisible here precisely because this box runs 3.13.
- **phy_name[phy & 7] indexed a five-entry array.** MT_RATE_PHY is three
bits, so 5-7 are representable and read past the end -- reachable from
command-line input and from a descriptor. (by_phy[] was already 8 and is
fine.)
- **gate_g reported frames it never sent.** It discarded every transmit
return, printed the requested count and returned 0, so a run where every
submit failed still handed the witness an experiment that never aired --
and "no frames decoded" would have read as a negative finding rather than
as a silent transmitter. Now counts what actually went out and fails when
an arm sent nothing.
- The ACK gate cast an unchecked signed duration to unsigned and slept
uninterruptibly; -1 meant roughly 49 days with the receiver running. Now
the SIGINT-aware wait every other gate uses.
Not defects, and now commented so the next reader does not re-derive them:
- The 2.4 GHz VHT power double shift is byte-for-byte mt76's own
`mt76x2_get_rate_power()`, and `rate_power_val` takes a u8 on both sides.
Changing it would be a silent divergence from the reference this port is
checked against.
- `mt7612u_clear_ack_responder()` not clearing MT_AUTO_RSP_EN is correct:
init writes MT_AUTO_RSP_CFG = 0x13 and the EN bit is BIT(0), so the gate is
already on before any caller arms a responder. Clearing it would leave the
device in a state its own init never produces.
On air, unchanged: `bringup rtap 149 400` still gives tag A 400/400 and tag B
377/400 at rate 19, against the 401/388 recorded before. 60/60 ctest.
Still open from the review and NOT addressed here: transfer teardown racing
libusb ownership, mt_wr() discarding failures, EEPROM read errors reaching
calibration data as 0xff, the harness counter data race, and the LOG/ERR
format. They are real, but each is a design change rather than a fix, and they
want the maintainer's view first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Qodo posted fourteen findings when this left draft. I checked each against the code and against the pinned mt76 tree rather than taking them on trust, which turned out to matter: two were wrong, and "fixing" either would have made things worse. Fixed — six, one of which is on-air correctnessThe radiotap HT bandwidth was gated on the wrong bit. The MCS A malformed radiotap header transmitted anyway. A declared field running past the declared header length returned the header length, which both injection entry points read as success and then aired at whatever defaults had accumulated. Now refused. An unknown trailing present bit still stops cleanly — that header is well formed, we simply cannot read the rest of it.
Plus three in the harness: On air, unchanged: Two that are not defectsThe 2.4 GHz VHT power "double shift" is
Five left open deliberatelyTransfer teardown racing libusb ownership after the two-second cancel deadline; These are real. I have not touched them because each is a design change rather than a fix — error propagation through |
Thanks for validating the proposed fixes against the pinned tree. I agree with leaving the two upstream-matching behaviors unchanged and would prioritize the remaining issues in this order:
For the next change, I’d take teardown ownership plus the checked-write/error-status design together, then handle EEPROM validation and the harness race separately. I would not alter either of the two behaviors you identified as matching mt76. |
|
Filed #413 for the cross-backend half of this: Keeping it out of this PR deliberately: the field is shared, the "what should the default be" question is yours rather than a backend's, and the Realtek numbers need a register hunt plus a bench sweep that has nothing to do with MediaTek. What did land here, because it was a defect rather than a design question: this backend had two disagreeing ceilings behind one public API — The numbers, measured rather than assumed: TX is not the constraint on this part — every size up to 7900 bytes aired 60/60 against an RTL8812AU witness with zero CRC errors, well past 802.11's 2304 non-A-MSDU ceiling, so the old 2048 was a buffer constant with nothing behind it. RX is the constraint and it is the MAC's: Both are now in One correction worth stating plainly, since it was mine: I added an Caveats that stand: one witness generation, and 7900 is "at least" — I stopped at the buffer I had, not at a refusal. |
|
Re-verified at eca657c against the code, not the replies: all eleven findings from my review are fixed. Build, Answers to the three open questions:
The pthread / |
Per review: land this unwired but complete, and address the five items rather than waiting on a shape. Nothing here touches CMakeLists, WiFiDriver or the workflows -- the subtree is still unreachable from the shipped library. **The subtree is now complete on its own.** The invalid-PHY rate-word drop is back, as asked, and so is the rest of the subtree-level work that had drifted onto the integration branch, because the same argument applies to all of it: the per-frame noise floor and derived SNR, 80 MHz, the frame-size limits, and the adapter lock shared with UsbDeviceLock. What stayed behind is only the wiring -- the IRtlDevice implementation, the VID:PID table and the mapping header. **Write failures now propagate.** mt_wr() discarded mt_vendor_req()'s result, so a write that exhausted its retries left the hardware partly configured while the call returned success. Threading a status through every writer would touch several hundred call sites in a sequence that is deliberately a verbatim port of mt76's, and the churn would bury what it protects. So this follows the shape the reads already had -- mt_rr_chk() has always bumped d->io_err: writes stay best-effort at the call site, and a SEQUENCE checks the accumulator at its boundary. mt_eeprom_init(), mt_init_hardware() and mt_set_channel_ex() are bracketed and now fail as a whole; optional writes stay best-effort by not being bracketed. mt_wr_chk() exists for a caller that wants to fail at the write instead. **EEPROM read errors no longer reach calibration as data.** The slurp must not treat 0xffffffff as an error by value -- unprogrammed cells legitimately read all-ones -- but the accumulator distinguishes a blank cell from a failed transfer. A failure outside the two cells that were already validated used to leave all-ones power and calibration bytes in memory while the device opened successfully and transmitted from them. **Teardown ownership is explicit.** Leaking the ring on the two-second deadline was the safe half; the other half was that mt_close() then released the interface, closed the handle and exited the context while libusb still owned those transfers and the event thread had just been joined, so nothing could ever complete them. The device is marked stranded: mt_close() leaks the USB objects too rather than freeing what libusb holds, and mt_async_start() refuses to submit a second ring onto the same endpoints. **The harness counter race is closed.** arx_cb() and ack_cb() run on the libusb event thread while gate_arx()/gate_duplex() read the same fields before stopping it, so a displayed rate or a duplex verdict could be built from torn counts. Relaxed atomics rather than per-gate locks: they are counters, nothing orders off them, and this is the RX hot path in a throughput gate. **LOG/ERR follow docs/logging.md.** They used a private `[mt7612u]` prefix instead of `devourer [X] `, emitted text and newline as two stdio calls so a line from the event thread could split one from a caller, and never flushed. mt_diag() formats the line first and emits it with one fwrite + fflush, as src/Event.h does for the machine plane. Warning-level diagnostics moved to [W]. Verified on hardware after the rework, 19 gates, 0 failures: regs, fw, init, chan, caps, hop, pwr, linkstat, ampdu and soak self-check; rtap 781 frames, sweep 1617 at 20 MHz / 1639 at bw=1 / 828 at bw=2, coding 1919, gateg 450, mtu 400 and vht 748 all witnessed by an RTL8812AU; and MT7612U-to-MT7612U 2999 frames with rx_err, rx_invalid and rx_dropped all zero. Offline: make check green and the extractor --check still reproduces initvals.h against the pinned reference/mt76. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Done at 1. Merge shape — unwired, and now completeNothing in this branch touches The invalid-PHY rate-word drop is back, as asked. I also brought over the rest of the subtree-level work that had drifted onto the integration branch, because your reason applies to all of it equally: the per-frame noise floor and derived SNR, 80 MHz, the frame-size limits, and the adapter lock shared with 2. The rename — agreed, and I was wrong to push backYour reason is better than mine. I was arguing the interface is neutral in substance, which is true and beside the point: after this lands the names are read by people, and 3. The five items — all closed hereWrite-failure propagation. EEPROM read errors. The slurp must not treat Teardown ownership. Leaking the ring on the two-second deadline was the safe half. The other half was that Harness counter race.
Hardware verification after the reworkRe-run end to end so this is one record rather than a claim — 19 gates, 0 failures:
Offline: The standing caveats are unchanged and worth repeating rather than burying: one witness generation (a single RTL8812AU) behind every on-air number, two samples of one board revision, no cold-boot test on this host, and the periodic RX gain worker ( |
josephnef
left a comment
There was a problem hiding this comment.
Verified at 1b032b5: the five deferred items are closed as described (accumulator-bracketed sequences, EEPROM blank-vs-failed separation, stranded-device teardown, atomic harness counters, single-fwrite diagnostics), the subtree-level work is back in this branch, and nothing touches the shipped build. Local build, make check and the extractor --check against the pinned reference/mt76 all reproduce. Landing unwired; the rename PR and the integration PR follow in that order.
|
/review |
|
Code review by qodo was updated up to the latest commit 1b032b5 |
|
Qodo's re-review of 1b032b5 raised seven. I checked each against the code and the pinned mt76; two are closed in-thread (the 5 GHz target-power one is
Approval stands; I will merge once these land and the gate is green. |
I can’t apply or land code changes to this PR from this chat because implementation actions aren’t available here. The five fixes are clear:
I can review a patch for these changes, help draft the tests, or verify the resulting diff. |
**The ring rejected what the builders produce.** MT_TX_BUFSZ was 2048 while
mt_tx_raw() built up to MT_TX_BUF_MAX (4096) and mt7612u_send_packets() up to
MT_USB_AGG_BUF (16 KB), and mt_tx_raw() takes the ring whenever one is running
-- the normal integrated shape. So a frame over ~2 KB was refused after being
built and every multi-frame batch reported zero accepted. The bench never saw
it because the sync path has no ring, which is exactly the blind spot: the mtu
sweep only ever exercised mt_bulk.
Fixed by making the ring slot the single ceiling (MT_TX_BUFSZ = MT_USB_AGG_BUF)
with a compile-time check that neither builder can outgrow it -- mutation-tested,
putting the ring back to 2048 now fails the build rather than the link. Ring
depth drops 32 -> 16 so slots carrying a full aggregate cost 256 KB rather than
512 KB; depth is not what buys throughput here, and soak/ampdu were re-measured
after the change.
The `mtu` gate now sweeps **both** TX paths and fails if they disagree, so this
cannot hide again. On air, sync and async are now identical at every size:
40/40 each up to 4064, both refusing 4065, and the witness saw 80 per size --
proof both paths aired.
Also from the re-review:
- gate_coding / gate_sweep / gate_vht printed a shortfall and returned 0, the
same structurally-guaranteed pass fixed in gate_g. They now count short
arms and fail; non-positive frame counts are rejected outright.
- gate_init and gate_chan passed NULL to mt_init_hardware(), so the
documented `init <fw_dir>` form silently used the default directory.
- open_selected() kept the adapter lock when libusb_open() failed. Only
mt_close() releases it and a failed mt_open() never reaches there, so the
process collided with its own stale lock on the next retry.
- g_stats_last_us was file-static, so two adapters in one process overwrote
each other's mark -- and every MIB counter is read-and-clear, so both got
wrong rates. Now per device.
Two more found while verifying, neither from the review:
- bringup.c named a private downstream consumer in a comment. Described by
role now.
- gate_duplex's verdict was (n && ctx.n), which its own setup cannot satisfy:
the radio is half duplex and the gate saturates TX, so RX during the flood
is ~0. It has therefore never passed. Confirmed pre-existing by building
the previous commit and reproducing it. Rewritten to assert what it can
establish -- TX sustained, no errors, and the receiver still delivering
after the flood stops -- and to say plainly that it needs a peer.
Verified: 18 of 19 gates. The one gap is the MT7612U-to-MT7612U arm, blocked on
the second adapter sitting in the RX-undrained wedge that only a physical
replug clears (EP0 reads answer, bulk-OUT times out). The new ring is still
covered on air by the mtu sweep's async pass against the RTL8812AU witness;
what is NOT re-verified since the ring change is MT-to-MT, and the reworked
duplex assertion has never had a working peer to run against. Both are stated
rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
All five are in at 1. Async ring ceiling — the one that matteredYou were right about why it hid: the
Rather than only add a static test, I made the The witness saw 80 frames per size — 40 from each path — so both genuinely aired. 2–5
Two more, found while verifying
Verification, and what is not covered18 of 19 gates pass. Ten self-check; The gap, stated rather than buried: the MT7612U-to-MT7612U arm did not run. The second adapter is sitting in the RX-undrained wedge this port documents — EP0 reads answer, bulk-OUT times out — and only a physical replug clears it, which I cannot do remotely. So:
I chased that one badly before concluding it: three separate "RX is 0" readings turned out to be a dead peer, then a |
The previous commit said gate_duplex's (n && ctx.n) verdict was unsatisfiable because RX is ~0 under TX saturation, and that it had therefore never passed. That was wrong, and wrong for the reason this port keeps relearning: the peer adapter had silently failed its firmware load, so nothing was on air at all. Every "RX is 0" reading behind that conclusion was a dead observation channel. Measured with a live peer: 2894 fps out while 20-44 fps still arrives, against 166 fps when the same peer is received with no local TX. So a saturated transmit throttles receive to roughly a quarter and does not stop it, and the receiver is fully healthy afterwards. The added post-flood check stands on its own merits and is kept: the in-flood count alone cannot distinguish a throttled receiver from a wedged one or from a quiet channel, and the failure message now names the peer requirement because a missing stimulus and a wedged receiver look identical from inside the gate. What changes here is the justification, which asserted something false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Adapter replugged, so the two arms I owed you are done — and one of them corrects something I told you. Correction firstI said Measured with a live peer: 2894 fps out while 20–44 fps still arrives, against 166 fps when the same peer is received with no local TX. So a saturated transmit throttles receive to roughly a quarter and does not stop it. The added post-flood check stays on its own merits — the in-flood count alone cannot tell a throttled receiver from a wedged one, or from a quiet channel — but the justification I committed asserted something false and is fixed in The two blocked armsMT7612U → MT7612U on the new ring: 3954 frames across 20/40/80 MHz sweeps,
Full coverage, every gateSelf-checking: Explicit arms for your findings: One methodology note, since it bit me twice more today: both adapters transmit with the same source MAC, so a leaked peer inflates every witness count. One sweep produced Stepping back — is it fit for purpose as a WLAN adapter?Three gaps that gate-by-gate testing hid, none blocking, all worth naming:
Incidentally, the adapter lock proved itself in anger today: two sweeps collided on one adapter and the second refused to claim it rather than wedging it, which is the failure this port previously recovered from only with a physical replug. |
…oints Blocking - The wedge recovery is factored into mt_recover_usb() and runs after identify on BOTH open paths, so a libusb-owning consumer arriving through mt7612u_open_handle()/mt_adopt() gets it too. It lived in mt_open() alone, which left that consumer paying both mt_fw_init() attempts and failing with the exact error the recovery removes. - Teardown no longer runs the receiver undrained. New mt_mac_rx_disable() clears ENABLE_RX before the EP 4 ring is cancelled - in mt7612u_close() and at every ring-cancelling site in bringup, via one rx_teardown() helper rather than the pair open-coded twenty times. Inline - phy.c: the AGC 8/9 snapshot moved above the fast return, so a fast retune no longer programs the previous channel's gain base. - rx.c/phy.c: the ambient-RSSI EMA is gone. mt76_get_min_avg_rssi() averages ASSOCIATED stations and returns 0 for a monitor consumer, so mt76 sits at the -75 fallback; feeding it from every frame let a -40 dBm neighbour step the gain down against a wanted peer at -80 dBm. phy_update_channel_gain() uses -75 directly and the RX hot path does no cross-thread write at all. - mcu.c: the EP 5 drain is armed by mcu_stale_pending instead of running before every command, where it cost a guaranteed 5 ms timeout each. - usb.c/phy.c: mt_rr_chk() on the U3DMA and MT_RX_STAT_1 reads - ~0u has TX_BUSY set, and reads as 65535 false CCAs. - fw.c: the retry keys on a distinct upload/device failure, so a missing blob fails immediately naming the file; mt_io_errors is restored (not zeroed) around the retry. - bringup: gate_swreset polls MT_MAC_STATUS_TX, and gate_rx says in its own output that it is not a valid tick witness under load. Process - MT7612U_NO_AUTORECOVER is now d->no_autorecover, set by bringup. MT7612U_DEV stays deferred as agreed in OpenIPC#412: there is no public way to pass a selector, so removing it would leave a multi-adapter consumer unable to choose at all. - One figure, one home: the mt7612u_phy_tick() comment. phy.c, bringup.c and docs point at it. The 4362/s figure is deleted rather than re-homed - nothing says which build produced it. - docs/mt7612u-usb-wedge.md loses its provenance header. Also fixed, from an adversarial pass over the merged result - both were integration defects no single-file change could see: - Hoisting the AGC reads above the fast return also moved them outside the only mt_io_errors() bracket guarding them. A failed read is ~0u, and FIELD_GET(MT_BBP_AGC_GAIN, ~0u) is 0x7f, so one EP0 hiccup during a fast retune would have programmed a gain base of 127 and gone deaf until the next full tune, silently. Now checked, keeping the previous snapshot. - mt_io_clear() in the fw retry would have wiped the caller's whole bracket: mt_init_hardware() opens it before mt_power_cycle(), and there is only one accumulator, so a partly powered WLAN core could have reported success. - Also: the bulk-OUT failure path now arms the drain (a failed OUT can still have been delivered), the drain only disarms on a clean timeout, gate_rx got the teardown invariant it was missing, and gate_adopt takes the interface claim as a guard before resetting and releases/reattaches on every path. Device-verified on MT7612U 2-1 (peer: a second MT7612U flooding ch149): RX 4902-4913 fps over 5 runs, rx_err/invalid/dropped all 0 adopt `bringup adopt` brings the device up; with NO_AUTORECOVER=1 the recovery announces itself ON THAT PATH, which is the code-path fact the review asked for drain 9 MCU timeouts under RF load produced ONE drain of 9 replies hop 530 ms full / 50.6 ms fast retune Strict build (-Wall -Wextra -Wshadow -Wmissing-prototypes) and make check clean. Not reproduced this session: the soft USB wedge (5 kill cycles, U3DMA_CFG stayed healthy), so "adopt recovers a WEDGED adapter" remains a code-path fact - the same shared function on both paths - not a measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
… wedge (#414) ## Summary Follow-up to #412 (merged as `daabab7`). Library-internal — no `IRtlDevice` surface. Two faults found by exercising the subtree the way a ground station would, both device-verified, plus a hardware review of the whole delta. ## Public header (for the wrapper) One addition, no changed signatures: **`int mt7612u_phy_tick(struct mt7612u_dev *dev)`** in `include/mt7612u/mt7612u.h`. It is **required for receive** — a consumer that receives must call it about once a second or the front end goes deaf against a strong transmitter (§1). The `IRtlDevice` wrapper should call it on a 1 Hz timer once it drives RX; everything else in the header is unchanged, so the rename and integration PRs that only consume this header are unaffected apart from adding that one call. `docs/mt7612u.md` is updated in this PR for the lock and the RX-undrained recovery story, as requested. ## 1. A receiver went deaf against a strong transmitter Against a peer ~20 cm away airing 1400-byte HT-MCS7 at 3037 fps, `arx` took **3 frames in 10 s**. Not the adapter (symmetric both directions; both hear 400–1000 fps of ambient on ch1/36/40/44/48), not the ring (its own counter agreed at 3), not the air (an RTL8812AU witness counted 103805 frames from the same transmitter). mt76 runs `cal_work` every second (`MT_CALIBRATE_INTERVAL == HZ`, `mt76x2/usb_phy.c:42`); this port ran none of it. Bisected with one verified peer per arm: | arm | frames / 10 s | |---|---| | nothing | 3 (reproduced 8×) | | `MT_RX_STAT_*` reads only | 3 | | a periodic MCU calibration only | 43902 | | the ported gain tracking only | 4 | | the full tick | 49002 | New public `mt7612u_phy_tick()`, caller-driven (MCU commands share one 4-bit sequence number and one response endpoint), called once a second from every receiving path. Final reliability, both adapters as receiver, single verified peer: **eight consecutive runs at 5415–5470 fps.** The MCU answers slowly under RF load — nine calibration timeouts per bring-up on this bench — so `mcu_wait_resp()` waits 10×300 ms and `mt_mcu_send()` drains any late reply off EP 5 before the next command (without which the reply channel desynced and cascaded `mcu resp mismatch … (want 1)`). The calibration takes regardless of those timeouts; the channel is marked calibrated once, as mt76 does. ## 2. A run killed mid-transfer left the adapter unable to load firmware Register reads *and* writes still round-trip, MAC and RF are fine, and the kernel `mt76x2u` driver cannot bind it either — every bulk OUT NAKs. `libusb_reset_device` (already on every open) does not reach it. Isolated one step at a time against a freshly wedged adapter: `clear_halt` on all four endpoints, MAC + USB DMA stop, WLAN_EN/WLAN_CLK_EN down, and the PBF block reset each left it wedged; pulsing `MT_USB_DMA_CFG_TX_CLR` — a bit mt76 declares and never writes — clears it alone. That plus an RX-pipe drain is `mt_recover_usb()`, which runs after identify on **both** open paths — `mt_open()` and `mt_adopt()` — so a libusb-owning consumer arriving through `mt7612u_open_handle()` self-heals too. (Review fix: it lived in `mt_open()` alone, which was precisely the consumer this subtree exists for getting none of it. `bringup adopt` now exercises that path.) A separately-observed **transient**: a post-kill firmware upload fails for 3–8 s then succeeds by itself (12/12 kills, no repair applied) — a settling window, not a wedge. `mt_fw_init()` absorbs it with one bounded retry after a 3 s settle. On a healthy idle adapter the recovery is a no-op: 0/40 healing-open failures. **Known issue, called out honestly:** a harder tier — `TX_BUSY` **stuck** at `0x80c00020` — was seen once and is **not** cleared by any of the above; it needed a physical replug, and it has not reproduced since (36+ kills). `docs/mt7612u-usb-wedge.md` traces the vendor tree's separate UDMA and IFDMA/FCE resets that none of the attempts reached, implemented as `bringup swreset` and still awaiting a reproduction to test against. `mt_open()` warns clearly when it hits this state rather than failing with an opaque timeout. ## Verification - Receiver: 8/8 runs 5415–5470 fps, both adapters. - Wedge recovery: soft tier 3/3; healthy-adapter soak 0/40; short stress on the shipping binary 0/12 across the scenarios that actually wedge. - Post-kill transient: 12/12 clear by themselves, absorbed by the loader retry. - `mt_mac_start()` now refuses to enable the receiver with no ring draining (`MT_RX_DRAIN_*`; the public `mt7612u_start()` always derived this, three gates bypassed it). - `make check` green; strict `-Wall -Wextra -Wshadow -Wmissing-prototypes` build clean. ## Retracted in the course of this Two claims made and then falsified: that the inverted ring/receiver order in `arx` explained the 3 frames (it didn't), and that a synchronous MCU command "hangs forever" beside the RX ring's event thread — it never reproduced in 8 stack-capture attempts, and libusb's own source shows that shape is supported. The comments say so where they used to say otherwise. ## Not in scope The vendor-neutral rename, `IRtlDevice` wiring, and a proven recovery for the stuck-`TX_BUSY` tier (needs a reproduction first). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --- ## Review round (head `4a6a14c`) Both blocking items and all eight inline points are addressed; see the review reply for the point-by-point. Headlines: the recovery is shared by both open paths, teardown clears `ENABLE_RX` before the EP 4 ring is cancelled, the EP 5 drain is armed rather than unconditional, the ambient-RSSI EMA is gone in favour of mt76's monitor behaviour, and the library no longer reads `MT7612U_NO_AUTORECOVER` (`MT7612U_DEV` stays deferred per #412). **Re-verified on this head** — a different stimulus from §1's, so quoted separately rather than folded into that figure. Peer: a second MT7612U flooding ch149; DUT: MT7612U at `2-1`. | check | result | |---|---| | RX rate | **4902–4913 fps**, 5 runs, `rx_err`/`rx_invalid`/`rx_dropped` all 0, peer verified airing before and after every run | | adopt path | `bringup adopt` brings the device up; with `MT7612U_NO_AUTORECOVER=1` the recovery announces itself **on that path** | | MCU drain | 9 calibration timeouts under RF load produced **one** drain of 9 replies, where the old code paid a 5 ms timeout before every command | | retune | 530 ms full / 50.6 ms fast | | build | strict `-Wall -Wextra -Wshadow -Wmissing-prototypes` clean; `make check` green | **Not** reproduced this session: the soft USB wedge (5 kill cycles with TX+RX up, `U3DMA_CFG` stayed healthy each time). So "the adopt path recovers a *wedged* adapter" is a code-path fact — one shared function, two call sites — not a measurement from this round. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The rename PR agreed above is open as #415: |
…; Realtek-only members on IRtlRadio (#415) Precondition for a non-Realtek backend (the MT7612U subtree from #412 / #414). Pure rename plus one interface split; no behaviour change on any Realtek path, no new CMake option, no factory dispatch on `0e8d:7612`. ## Renames (commit 1) | before | after | |---|---| | `IRtlDevice` (`src/IRtlDevice.h`) | `IRadio` (`src/IRadio.h`) | | `WiFiDriver::CreateRtlDevice` | `WiFiDriver::CreateRadio` | | `WiFiDriver::CreateRtlDevicePcie` | `WiFiDriver::CreateRadioPcie` | | `devourer::IRtlTransport` (`src/RtlTransport.h`) | `devourer::ITransport` (`src/Transport.h`) | `RtlAdapter`, `UsbTransport`, `PcieTransport`, `WiFiDriver` and the five backend classes keep their names: they are Realtek by construction or vendor-neutral already. **Downstream-breaking, intentionally.** No `using IRtlDevice = IRadio` alias and no `CreateRtlDevice` forwarder. Embedders (OpenIPC PixelPilot and any other consumer) update in the same release. The `git grep | xargs sed` in commit 1's message applies unchanged to a downstream tree, plus `#include "IRtlDevice.h"` → `"IRadio.h"`. ## The split (commit 3) `IRadio` keeps only what any 802.11 monitor/injection radio can implement. The five members whose meaning is fixed by Realtek silicon move to `IRtlRadio : public IRadio` (`src/IRtlRadio.h`), which every Realtek backend derives from: - `SetXtalCap` / `GetXtalCap` — AFE crystal-cap trim code - `GetRxEnergy` — phydm FA/CCA/IGI/NHM snapshot (`RxEnergy`) - `ProbeEfuseStability` — EFUSE logical-map re-read, `0x8129` id - `DumpChipState` — rtw canary register dump `GetFwBootStatus` stays on `IRadio`: its struct names no Realtek constant. Callers reach the five through `dynamic_cast<IRtlRadio*>` and treat `nullptr` as "not on this radio": `doctor` skips the EFUSE leg, `chipstate` refuses, `rxdemo`'s energy telemetry and sweep, `txdemo`'s hopset sensing and xtal stepping, `chanmig`'s probe and `chanscout`'s dwells each say so once and carry on with frame statistics only. Consequence worth stating: hopset TX-side sensing and the chanmig energy probe are Realtek-only until a neutral frame-free energy type exists (integration-PR work). `tests/radio_iface_selftest.cpp` implements `IRadio`'s pure-virtual core with no Realtek type and pins the not-ported defaults; ctest cell `radio_iface`. A Realtek-typed pure virtual added to `IRadio` stops it compiling. ## Verification - gcc all-chips Release: build clean, ctest 56/56 (incl. `radio_iface`). - clang all-chips Release: build clean. - `build-configs` subsets jaguar1-only (ctest 47/47) and kestrel-only (50/50); `DEVOURER_PCIE=ON` (56/56, compiles `CreateRadioPcie` and `PcieTransport : ITransport`). - `git grep -E 'IRtlDevice|CreateRtlDevice|IRtlTransport|RtlTransport\.h|IRTL_DEVICE_H'` outside `reference/` and `hal/`: empty. - On air, channel 36: `regress.py` devourer→devourer cell, TP-Link T3U (8822BU) TX to 8812CU RX: **6100 hits / 6500 TX / 15 s**. The kernel cells of that run fail because neither adapter's vendor kernel driver binds on this host kernel; unrelated to this PR. - `doctor --pid 0xb812`: EFUSE probe reached through the `IRtlRadio` cast, verdict HEALTHY (3 reads, 0 mismatched, id 0x8129). - `rxdemo` on the 8812CU with `DEVOURER_RX_ENERGY_MS=1000`: seven `rx.energy` events with live FA/CCA/IGI counters. - `tests/beacon_interval_shift.sh` (inline-compiled C++ calling `CreateRadio`): compiles and runs; actuator verdict unchanged. - MSVC and mingw: CI. ## Not in this PR The MT7612U integration (`DEVOURER_MT7612U`, a VID:PID gate ahead of the `SYS_CFG2` read, a MediaTek transport, an `IRadio` implementation with the 1 Hz PHY tick on its own thread, a `regress.py` cell), a neutral RX type with an explicit `has_fcs` flag, a neutral frame-free energy type, and removal of the older `Rtl8812aDevice` / `RtlUsbAdapter` aliases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015VtdB2ZryKELUwREjPBH3v --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…onment qodo's first finding on OpenIPC#421, and it is right for a reason that is specific to this PR: OpenIPC#412 deferred the getenv while the subtree was unreachable, but DEVOURER_MT7612U now links that open path into libdevourer, so "the library picks its adapter from ambient process state" stopped being a bench-tool quirk and became a library property. A consumer with two adapters could claim the wrong one because of a variable it never set. open_selected() takes the selector as a parameter, mt7612u_open_selected() is the public way to pass it, and mt7612u_open() is that with NULL. bringup fills d->dev_selector from MT7612U_DEV before mt_open(), so the operator-facing spelling is unchanged. The library now reads no environment at all — the claim two comments made prematurely in the previous commit is finally true. The messages had to move with it. They named MT7612U_DEV, which the library no longer reads, so a consumer that is not bringup would have been told to set something it does not use. They now name "the selector" and point bringup users at the variable. tests/api_link.c covers the new entry point (26 now, still compiled as C), which is the test's whole purpose: a public declaration with no definition fails there rather than at the first caller. Verified on hardware, two adapters attached: MT7612U_DEV=2-1 and =5-1 each open their own unit (distinct EEPROM MACs 40:a5:ef:50:27:a1 and 40:a5:ef:5a:32:f8), and with no selector it still takes the first and warns. 59/59 ctest in all three configs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
…onment qodo's first finding on OpenIPC#421, and it is right for a reason that is specific to this PR: OpenIPC#412 deferred the getenv while the subtree was unreachable, but DEVOURER_MT7612U now links that open path into libdevourer, so "the library picks its adapter from ambient process state" stopped being a bench-tool quirk and became a library property. A consumer with two adapters could claim the wrong one because of a variable it never set. open_selected() takes the selector as a parameter, mt7612u_open_selected() is the public way to pass it, and mt7612u_open() is that with NULL. bringup fills d->dev_selector from MT7612U_DEV before mt_open(), so the operator-facing spelling is unchanged. The library now reads no environment at all — the claim two comments made prematurely in the previous commit is finally true. The messages had to move with it. They named MT7612U_DEV, which the library no longer reads, so a consumer that is not bringup would have been told to set something it does not use. They now name "the selector" and point bringup users at the variable. tests/api_link.c covers the new entry point (26 now, still compiled as C), which is the test's whole purpose: a public declaration with no definition fails there rather than at the first caller. Verified on hardware, two adapters attached: MT7612U_DEV=2-1 and =5-1 each open their own unit (distinct EEPROM MACs 40:a5:ef:50:27:a1 and 40:a5:ef:5a:32:f8), and with no selector it still takes the first and warns. 59/59 ctest in all three configs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
… that stops the Jaguar1 misroute (#421) > **Stacked on #420.** This branch is cut from that PR's head, so the diff shows > its four commits too (`8c5dd0d`, `eb10389`, `8dd504c`, `31cb738` — the FCS > contract). The five that belong to this one are `dde4766`, `cdbb72d`, > `20e0b7e`, `9238f39`, `b839df3`. Merge #420 first and this rebases to nothing; > happy to retarget it if you would rather they went in together. Second block of #419. Takes the two scope items that do not need a radio: the `DEVOURER_MT7612U` option with the portability work behind it, and the factory gate. There is still no `IRadio` backend — `CreateRadio` recognises the MediaTek ids only to **refuse** them. ## The bug the gate fixes, measured `CreateRadio` ends in an unconditional Jaguar1 construction, and `read_chip_id()` discarded its libusb return code. So a device that does not answer the Realtek `SYS_CFG2` vendor read arrived there as chip-id `0x00` and was brought up as an RTL8812AU. Both arms of the same control transfer, on this bench: ``` bmRequestType 0xC0, bRequest 5, wValue 0x00FC, 1 byte RTL8812AU 0bda:8812 rc=1 chip_id=0x04 MT7612U 0e8d:7612 rc=-7 LIBUSB_ERROR_TIMEOUT destination byte UNTOUCHED MT7612U 0e8d:7612 rc=-7 LIBUSB_ERROR_TIMEOUT (second adapter, same) ``` The MediaTek arm ran with a `0xAA` poison byte in the destination and it survived, so the `0` that reached the dispatch was the caller's own initialiser, never a reading. `read_chip_id`'s own comment already described the consequence — *"Returns 0 on a failed read, which falls through to the Jaguar1 path"* — this just makes it not happen. Two guards, both ahead of the Realtek read: - **`Mt7612uUsbIds.h`** — the complete 16-entry `mt76x2u_device_table`, taken from the pinned `reference/mt76 @ be5ce79` so it is re-verifiable from a fresh checkout, gated the way Kestrel gates. - **`read_chip_id` returns `std::optional`** — `nullopt` when the *transfer* failed. A successful read of `0x00` still returns a value and keeps its existing fall-through, so no cold-boot transient changes behaviour. ## Why the gate is vid:pid, and not what the issue asked for literally The scope item says *"an unrecognised non-Realtek VID must refuse"*. I did not implement that, because it would refuse working adapters. Six vendor ids ship both silicon families, and they do not merely coexist — they **interleave inside one vendor's product-id range**: | vendor | MediaTek ids | Realtek ids | |---|---|---| | `0x0846` Netgear | `9014`, `9053` | `9051`, `9052`, `9054` | | `0x056e` ELECOM | `400a` | `4007`, `400b`, `400d`, `400e`, … | | `0x0b05` ASUS | `17eb`, `180b`, `1833` | `17d2`, `1817`, `1852`, `1853`, `1a62` | | `0x2357` TP-Link | `0137` | `0101`, `0103`, `0106`, `010d`, … | | `0x7392` Edimax | `b711` | `6822`, `a811`, `a812`, `a813`, … | | `0x2c4e` Mercury | `0103` | `0127` | The **pairs**, by contrast, are disjoint: none of the 16 MediaTek ids matches any of the **91** Realtek ids devourer can serve. That is the same property the Kestrel gate already relies on. The transfer-failure guard then covers the intent behind the scope item without the cost — it refuses *every* non-Realtek part, named or not. Say the word if you want the literal vendor-id rule as well; I think this pair is strictly better, and the table above is why. ## Portability The subtree is now C++ (`.cpp`), so there is one compilation mode rather than a second path that can rot unnoticed. Cost measured by compiling each file with `g++ -std=c++20 -Wall -Wextra` *before* changing anything: **12** sites in the library (11 implicit `void*` conversions, 1 `uint8_t`→enum) and 40 in `bringup`, 32 of which cascaded from three `_Atomic` declarations. No C++ keyword collisions, no VLAs, no compound literals, no tentative definitions; both designated initialisers were already in declaration order. ``` pthread_mutex_t -> std::mutex io_lock (recursive) -> std::recursive_mutex pthread_cond_t -> std::condition_variable_any pthread_t -> std::thread nanosleep -> std::this_thread::sleep_for clock_gettime(MONOTONIC) -> std::chrono::steady_clock ``` `condition_variable_any` rather than `condition_variable` so it waits on the bare mutex and every lock site keeps its original shape — a much smaller diff through code whose teardown ordering is documented as a use-after-free hazard. Those waits are teardown and TX back-pressure, never a hot path. **The exported surface is still C.** `include/mt7612u/mt7612u.h` already had an `extern "C"` guard and `tests/api_link.c` is deliberately still compiled by `$(CC)`, so the ABI is tested rather than asserted: it resolves 25 entry points against the C++ objects, and `nm` shows the public symbols unmangled. Two things fell out that are worth naming: - A constructed `std::recursive_mutex` member **retires the `io_lock_ready` flag**. A zeroed `pthread_mutex_t` is a valid NON-recursive lock, so an open path that skipped the explicit init self-deadlocked the PHY tick — the adopt path did exactly that once. That state is now unrepresentable. - The teardown's `pthread_cond_timedwait` deadline arithmetic became one `wait_for`, dropping a `CLOCK_REALTIME` dependence where a wall-clock step could stretch or skip the 2 s budget. The two structs holding those members moved from `calloc`/`free` to `new` / `delete` — `calloc` never runs a constructor. `{}` still zeroes every scalar. `-Wclass-memaccess` then caught a real defect that introduced: a `memset` over a constructed device in `frame_shape`. Its loop case now declares the device inside the loop, preserving the per-iteration reset. **Not ported:** the `flock` adapter lock is `_WIN32`-guarded rather than mirrored. It works by contending for the *same* lock file `UsbDeviceLock` uses, but on Windows that class is a named mutex — so a file lock there would exclude nobody, and mirroring the mutex would duplicate what the devourer path already owns. Unprotected on Windows is a bare `mt7612u_open()` with no devourer around it, which is the bench tool's case, and the bench is Linux. ## Capability fallout `bw_mask_for_generation` ends in a catch-all returning 20/40/80 **plus** `kBw5|kBw10`, so the new enumerator silently inherited narrowband: ``` old chain, ChipGeneration::Mt7612u -> 0x1f (kBw5|kBw10 set) new chain, ChipGeneration::Mt7612u -> 0x1c (20/40/80 only) every other generation -> unchanged ``` `MT_RATE_BW` encodes nothing narrower than 20 MHz, and this issue lists that among the part's known limits. Nothing calls it with `Mt7612u` today, which is why it was worth fixing now — the first caller is the backend, and a capability claimed there is claimed silently. `adapter_caps_selftest` pins the value and the narrowband bits. ## What review changed Two reviewers went at this, and both found things I had wrong. Recording them because they are the substance of the branch, not footnotes. **The selftest could not see Jaguar.** It cross-checked the MediaTek set against `KestrelUsbIds.h` and `Rtl8733bUsbIds.h` — 27 ids. Jaguar1/2/3 dispatch by chip-id and have no table in devourer, so 64 more were invisible, and *every* interleaved vendor id above lives in that 64. Adding `{0x0b05, 0x17d2}` — an ASUS USB-AC56, a real RTL8812AU — passed the old test while refusing a genuine Jaguar1 adapter. The test now carries a 64-entry witness list from `reference/rtl8812au`, checks all 91, and asserts the coverage count so a future trim cannot silently restore the blindness. Both attacks now fail it: ``` mt7612u_usb_ids: FAIL 0b05:17d2 is Jaguar1/2/3 but the MediaTek gate claims it mt7612u_usb_ids: FAIL 0846:9054 is Jaguar1/2/3 but the MediaTek gate claims it ``` **My table was 11 of 16, from the wrong source** — this machine's kernel tree rather than the pinned submodule. Recomputing against the complete table also turned "three shared vendor ids" into six, which is the table above. **The library read the environment and wrote to stderr.** qodo raised both, and both are things this PR's own `DEVOURER_MT7612U` created by linking the subtree into `libdevourer`: a bench tool that reads `MT7612U_DEV` is a quirk, a *library* that picks its adapter from ambient process state is a defect, and the same for one that writes diagnostics to a stream its host does not control. `mt7612u_open_selected()` takes the selector and `mt7612u_set_log_sink()` diverts the diagnostics; the library now reads no environment and names stderr in exactly one place. `mt_diag()` turned out to be reimplementing `Logger` — same format, same fwrite+fflush atomicity, none of `set_level` / `set_diag_stream` / `DEVOURER_LOG_MAX_LEVEL` — and on Android `Logger::emit` has an `__android_log_write` branch these writes did not, so the subtree's diagnostics landed nowhere visible on the platform devourer ships to. **Two exceptions could cross the `extern "C"` boundary**, both proved by probe rather than argued: `catch (const std::system_error &)` is narrower than what `std::thread`'s constructor throws (libstdc++ allocates the thread state with a *throwing* `new`, so OOM arrives as `bad_alloc`), and `new (std::nothrow) T{}` is not nothrow for these types, because `std::condition_variable_any` holds a `shared_ptr<mutex>` that allocates in *its* constructor. Both are now `try`/`catch (...)` returning the existing `-1` / `NULL`. Also folded: three comments still said `mt_dev_state_init/destroy` manage `io_lock` after this branch made them empty; one comment quoted a `read_chip_id` note the same commit deleted; and `mt_usleep` is no longer interruptible (measured: a 200 ms request under 100 Hz `SIGALRM` returned after 10 ms with `nanosleep`, 201 ms with `sleep_for`) and now says so. ## Verification Local: - Both CMake configs build warning-free, **59/59 ctest** (58 before; the new cell is `mt7612u_usb_ids`). - The stripped `mt7612u+jaguar1` config builds, 49/49. - `make -C src/mt7612u check` passes (four binaries now, `log_sink` added); `api_link` resolves 27 C entry points against the C++ objects. - Sanitized (`address+undefined`, RelWithDebInfo) with the option ON: 59/59. On hardware, the gate end to end — the same adapter, the same command, across the parent commit and this branch: ``` parent 31cb738: Creating RtlJaguarDevice (PID 0x7612, chip-id 0x00) this branch: MediaTek MT7612U (0e8d:7612) detected; devourer has no MediaTek radio backend yet — refusing rather than misdetecting it as Realtek No driver for this chip in this build — exiting (exit 1) ``` The parent really does construct an `RtlJaguarDevice` for a MediaTek adapter, so the bug is demonstrated rather than inferred. Control, unchanged on both: `Creating RtlJaguarDevice (PID 0x8812, chip-id 0x04)` for the RTL8812AU. And the library itself, on an MT7612U at `0e8d:7612`: - `bringup regs` — `MT_ASIC_VERSION 0x76120044`, CFG- and MAC-space write / read-back / restore, EEPROM MAC. **GATE A: PASS**, and again after the RX run, so nothing wedged. - `bringup arx 1 10` — ROM patch + ILM/DLM load, ring up, **7918 frames in 10 s (790/s), rx_err=0 rx_invalid=0 rx_dropped=0**. The callback count and the driver's ring count agree exactly, which is what exercises the new `std::atomic` and mutex paths against each other. - The same run under ASan+UBSan with `detect_leaks=1`: 2623 frames (sanitizers cost most of the rate), **no ASan report, no UBSan report, no leak** — the part that actually tests the ring's `new`/`delete` teardown. CI is what verifies the Windows claim, since none of it builds locally. `DEVOURER_MT7612U=ON` is now set in the multi-platform matrix (gcc, clang, MSVC `cl`, macOS), the mingw job, and `build-sanitizers`, plus one stripped `build-configs` cell so the subtree cannot quietly acquire a dependency on a Realtek chip's sources. ## Blast radius For anyone who never sets the option, the compiled delta is confined to `WiFiDriver.cpp` and is exactly the two refusals above, plus an unused `ChipGeneration` enumerator. No Realtek dispatch path changes: a chip that answers `SYS_CFG2` reaches precisely the branch it reached before. ## Still open, deliberately - No `IRadio` backend, no transport adoption, no firmware embedding — next block. - `MT7612U_DEV` is still a `getenv` in `open_selected()`. #412 deferred it because there is no public way to pass a selector (`mt7612u_open()` allocates the device and the struct is opaque). I corrected two comments that claimed the library reads no environment, since it does. - MT7612U deliberately does **not** join the "No chip support selected" `FATAL_ERROR` list: with no backend, an MT7612U-only build would produce a library that can open nothing. It joins when the backend lands. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening this to ask a scope question, not to merge: does a MediaTek backend belong in this project at all? Every generation here is Realtek, and MT7612U is not. I would rather ask against measurements than against a proposal, so the work is on the branch and
docs/mt7612u.mdcarries the numbers, the methods and the limits.If the answer is no, that is a fine answer and the branch can be closed — the measurements are still recorded for anyone who asks later.
What changed
src/mt7612u/— a standalone C library (~4600 lines including the harness) plus a per-gate bringup tool, anddocs/mt7612u.md.CMakeLists.txtis untouched. There is noIRtlDeviceimplementation, noWiFiDriverdispatch, noDeviceConfigplumbing and noctestcell. Nothing in the shipped library changes; CI sees a docs-and-new-directory diff. The subtree builds on its own withmake -C src/mt7612u. That integration work is deliberately not done, because doing it before the scope question is answered would be wasted either way.Why a MediaTek port is small
On the Realtek generations the host programs the synthesizer and carries the PHY tables. Here the RF plane lives behind the MCU — a channel change is one 8-byte
CMD_SWITCH_CHANNEL_OPplus a firmware calibration burst, and there are no RF register tables to reimplement. That is also exactly why it can never hop fast.What is measured
All on one MT7612U (
0e8d:7612,MT_ASIC_VERSION0x76120044, 2T2R, SuperSpeed) against an RTL8812AU witness running this project's ownrxdemo/txdemo.TX rate is authoritative, unconditionally. Three rates, three exact matches; per-frame alternation 300 sent → 229 received, 229 aired the rate their own index called for, 0 mismatches. The hardware rate LUT cannot override the descriptor:
MT_TXWI_FLAGS_TX_RATE_LUTLUT loaded with OFDM 6 Mbps and read back; setting the flag mt76 defines and never sets changed nothing.
TX power — all nine registers identical to what the kernel driver programs for ch149 (
MT_TX_PWR_CFG_0..4/7/8/9,MT_TX_ALC_CFG_0=2f2f171a), checked against ausbmoncapture.A-MPDU works on injected frames — the opposite of the Jaguar1 result in
docs/aggregation.md:paggr=1QSEL_MGMTHardware ACK responder — no separate responder-address register exists, so arming retargets the port identity with
MT_AUTO_RSP_ENgating; clear moves the identity back, following the finding in #410 that closing the gate alone is not enough.len14USB bulk aggregation —
MT_TXD_INFO_NEXT_VLDchains blocks inside one bulk-OUT transfer. mt76 never sets it, so this is not a port and needed on-air proof: 400 frames in 25 transfers, 352 aired.Also measured: 40 MHz (242/242 frames at
bw=1), TSF (200231 µs over a 200000 µs sleep), monitor RX decoding CCK/OFDM/HT with per-chain RSSI, radiotapsend_packet.A register-stream diff against the kernel driver's own probe: 522 kernel EP0 writes vs 521 ours, 376 common addresses, one final-value mismatch (beacon config, which we skip), six kernel-only addresses (all beacon config), and zero addresses we write that the kernel does not.
Two results stated against interest
The async rings bought no throughput. At saturation sync and async both sit at 3040 fps / 34 Mbit/s with an identical 0.329 ms mean submit, because that is the airtime of one frame. What they bought is the callback RX path, concurrent TX+RX on one handle, and 3× lower submit latency below saturation (21 µs vs 65 µs). A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s.
The first ACK-responder attempt returned INCONCLUSIVE, not a pass. It used this project's retry-collapse method, but
txdemoinjects without retries, so there was nothing to collapse. The gate says so rather than reporting success; the passing result above uses a different observable.What it cannot do
Our implementation has headroom, but the floor is four MCU round trips over bulk endpoints plus firmware time — 10–20 ms at best. FHSS and per-packet hopping are out of reach for this part; seconds-scale migration is not. Narrowband 5/10 MHz has no encoding:
MT_RATE_BWis two bits with three defined values.Also worth knowing: unicast injection is a 40× cliff (3037 → 75 fps). The MAC arms an ACK timeout for a peer that never answers, and neither clearing
txwi.ack_ctlREQ nor a QoS No-Ack policy prevents it. A one-way link must use broadcast.Counterparts
Stated because the numbers above are uniformly favourable.
rxdemo.paggr,bwandrateare that implementation's reading, not an independent instrument.reset_wlan+power_ondemonstrably does not clear the firmware-running bit.ctest, no sanitizer build, no lifecycle soak.One more, on the code rather than the measurements: enabling MAC RX without draining the bulk-IN endpoint wedges the chip below USB level, unrecoverable by
libusb_reset_device(), theauthorizedtoggle or the kernel driver — only a physical replug. Fixed by never enabling RX for a caller that will not drain it, plus an endpoint flush (20 consecutive cycles clean afterwards against a death after ~5 before). Two things changed at once, so that run does not attribute the wedge to one of them.Verification
make -C src/mt7612uclean at-Wall -Wextra.src/mt7612u/README.mdrun on hardware; the numbers above are those runs.Open list
If this is in scope, the work I would continue with, in order:
IRtlDevice,WiFiDriverdispatch,DeviceConfig,CMakeLists.txt,ctestcells.mt76x2_phy_tssi_compensate()— without it output power drifts with die temperature.CMD_RANDOM_WRITE, drop the inter-command sleep) — worth doing only if 10–20 ms is useful to someone.🤖 Generated with Claude Code
https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj