Skip to content

mt7612u: a MediaTek backend, measured — is it in scope? - #412

Open
snokvist wants to merge 10 commits into
OpenIPC:masterfrom
snokvist:feat/mt7612u-mediatek-backend
Open

mt7612u: a MediaTek backend, measured — is it in scope?#412
snokvist wants to merge 10 commits into
OpenIPC:masterfrom
snokvist:feat/mt7612u-mediatek-backend

Conversation

@snokvist

@snokvist snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

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.md carries 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, and docs/mt7612u.md.

CMakeLists.txt is untouched. There is no IRtlDevice implementation, no WiFiDriver dispatch, no DeviceConfig plumbing and no ctest cell. Nothing in the shipped library changes; CI sees a docs-and-new-directory diff. The subtree builds on its own with make -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_OP plus 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_VERSION 0x76120044, 2T2R, SuperSpeed) against an RTL8812AU witness running this project's own rxdemo/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_LUT frames aired
clear 84 83 × MCS7, 1 × MCS6
set 93 93 × MCS7

LUT 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 a usbmon capture.

A-MPDU works on injected frames — the opposite of the Jaguar1 result in docs/aggregation.md:

arm frames paggr=1
no AMPDU flag (control) 352 0
AMPDU + QSEL_MGMT 326 326
frame bytes AMPDU off AMPDU on gain
200 7.01 Mbit/s 15.50 Mbit/s 2.21×
1400 34.03 Mbit/s 44.55 Mbit/s 1.31×

Hardware ACK responder — no separate responder-address register exists, so arming retargets the port identity with MT_AUTO_RSP_EN gating; clear moves the identity back, following the finding in #410 that closing the gate alone is not enough.

responder frames the stimulus radio received
off (control) 0
armed 3500+, every one len 14

USB bulk aggregationMT_TXD_INFO_NEXT_VLD chains 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, radiotap send_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 txdemo injects 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

channel switch measured, mean of 8
full, with the firmware calibration burst 526 ms
calibration skipped 48 ms
this project on Realtek, for reference 0.5–2.5 ms

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_BW is 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_ctl REQ nor a QoS No-Ack policy prevents it. A one-way link must use broadcast.

Counterparts

Stated because the numbers above are uniformly favourable.

  • One physical unit, one sample. No second MT7612U, no second board revision.
  • One witness generation. Every on-air number is an RTL8812AU running this project's rxdemo. paggr, bw and rate are that implementation's reading, not an independent instrument.
  • TX power was verified against the kernel's registers, not radiated power. No spectrum analyser, no power meter. The claim is "identical to what mt76 programs", nothing more.
  • The RX gain correction has never done anything. This EEPROM carries no gain calibration, so every term is zero and that path is unexercised.
  • The ACK identification rests on length, the 1:1 count and a zero control arm. The RA bytes inside those ACKs were not read — the dump path omits control-frame bodies.
  • No cold boot was ever tested. No hub on the test host supports per-port power switching, and reset_wlan + power_on demonstrably does not clear the firmware-running bit.
  • Nothing here runs in CI. No ctest, no sanitizer build, no lifecycle soak.
  • 80 MHz, VHT on air and NSS=2 are unexercised.

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(), the authorized toggle 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/mt7612u clean at -Wall -Wextra.
  • Every gate listed in src/mt7612u/README.md run on hardware; the numbers above are those runs.
  • The library target and CI are untouched by construction — the diff adds a directory and a doc, and modifies nothing.

Open list

If this is in scope, the work I would continue with, in order:

  1. IRtlDevice, WiFiDriver dispatch, DeviceConfig, CMakeLists.txt, ctest cells.
  2. mt76x2_phy_tssi_compensate() — without it output power drifts with die temperature.
  3. Cold-boot verification on a host with switchable USB power.
  4. A second sample and a second witness generation.
  5. 80 MHz; VHT and NSS=2 on air.
  6. Retune tuning (batch registers via CMD_RANDOM_WRITE, drop the inter-command sleep) — worth doing only if 10–20 ms is useful to someone.
  7. Whether the single MCS6 frame in the rate-LUT control arm is a witness artefact or a real fallback. Unexplained.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj

## 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 josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::CreateRtlDevice are 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.
  • WiFiDriver dispatches 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.
  • IRtlTransport is 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 ChipGeneration entry, a DEVOURER_MT7612U CMake option following the DEVOURER_HAVE_* pattern, and a regress.py cell against the MediaTek vendor driver under reference/.

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.h says it is generated by a script in PLAN.md; neither the script nor PLAN.md / INVESTIGATION.md / BRINGUP-RESULTS.md is in the PR, and 13 comments point at them. This project's rule is that tables come from tools/extract_*.py with pinned source hashes and a --check mode that reproduces the checked-in output. The hand-typed constants in phy.c and init.c should come from the same generator.
  • Windows/MSVC is first-class here. __builtin_ctz under every FIELD_PREP/GET, pthreads throughout async.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.

* full power-on, firmware load and MAC/PHY init. fw_dir may be NULL for the
* system default. Returns NULL on failure; err (optional) receives a message.
*/
struct mt7612u_dev *mt7612u_open(const char *fw_dir, const char **err);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mt7612u_open, _close, _keep_detached, _set_channel, _set_chainmask, _start, _stop, _asic_version and _mac_addr are declared here but no .c file defines them. The harness works only because it includes internal.h and calls mt_open / mt_init_hardware directly. Anything linking against this header gets undefined references, so the "standalone C library" cannot be used as one yet. (set_channel also takes unsigned here and uint8_t internally.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2142a5a. All nine are defined now — open/close/start/stop in init.c, set_channel/set_chainmask in phy.c, keep_detached/asic_version/mac_addr in usb.c. You spotted the signature disagreement too: set_channel took unsigned in the header against uint8_t internally, and the header is uint8_t now.

tests/api_link.c takes the address of all twenty public entry points while including only <mt7612u/mt7612u.h> and no internal header, so a declaration that loses its definition is a link error rather than something a caller discovers later. Mutation-tested: deleting one definition gives undefined reference to mt7612u_asic_version.

While defining them: mt7612u_start() enables RX only when an RX ring is already running, and the header says why.

Comment thread src/mt7612u/rx.c Outdated

*frame = buf + MT_DMA_HDR_LEN + MT_RXWI_LEN;
/* Fold the L2 pad out by moving the header down over it. */
if (pad && len > 24) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This folds a fixed 24 bytes over the L2 pad. L2PAD is only set when the header is not 4-aligned, which is QoS data (26 bytes) or 4-address (30 bytes). With a 26-byte header the QoS Control field at offsets 24-25 is overwritten by the two pad zeros, so every A-MPDU / QoS frame reaching the callback has TID and ack-policy zeroed. Those are exactly the frames the aggregation and ACK numbers are about. tx.c already computes the real header length from FC; reuse it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it was every QoS frame that reached the callback, not an edge case. Fixed in 2142a5a: the fold moves the real header length now, exactly as mt76x02_remove_hdr_pad() does, and TX and RX share one mt_hdrlen_from_fc() so the two sides cannot drift apart again.

tests/frame_shape covers it on a synthetic 26-byte-header QoS frame, and carries a negative control that redoes the old fixed-24 fold and asserts the QoS Control really is destroyed — without that, a passing test would say nothing about what it caught. Reverting the fix makes it report QoS Control zeroed by the pad fold: aa aa, which is the pad bytes.

Comment thread src/mt7612u/tx.c Outdated
int len = 24;

if (type == 1) /* control */
return ((stype == 0xb) || (stype == 0xa)) ? 16 : 10;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only CTS and ACK are 10-byte control frames. BlockAckReq (0x8), BlockAck (0x9) and CF-End / CF-End+CF-Ack (0xe/0xf) are 16 bytes like RTS and PS-Poll. With this table the pad is inserted at offset 10 inside a BAR/BA, so the frame airs corrupted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Rather than extend the list of 16-byte subtypes, I ported ieee80211_hdrlen(): control frames default to 16 and only CTS and ACK are 10. That is the shape that cannot be incomplete, and it is the same function mt76 reaches through ieee80211_get_hdrlen_from_skb() on both the TX and RX sides — so this now matches upstream rather than approximating it. It also picks up the HT Control cases (+4 on Order) that the old version missed.

Fixed in 2142a5a, covered in tests/frame_shape over all eight control subtypes and the five data shapes.

Worth being explicit: this one has no on-air observable in the gates here, because nothing in them injects a control frame. The unit test is the whole of the evidence for it.

Comment thread src/mt7612u/async.c
pthread_join(a->evt, NULL);

for (int i = 0; i < MT_TX_RING; i++)
if (a->tx[i]) libusb_free_transfer(a->tx[i]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mt_async_stop cancels only the RX transfers. TX transfers are never cancelled; after the bounded wait above (200 x 10 ms) running is cleared, the event thread joined, and every transfer freed regardless of tx_inflight / rx_inflight. On a wedged chip, the case docs/mt7612u.md itself describes, TX URBs never complete, so this frees transfers libusb still owns. Cancel TX too and only free once inflight counts reach zero. Related: the fail: path at line 102 joins a zeroed pthread_t if libusb_alloc_transfer fails before pthread_create, and rx_inflight / running are plain volatile int shared across threads.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of it, and the wedged chip is the case that makes it real rather than theoretical. In 2142a5a 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 whole mt_async is deliberately leaked rather than freeing memory libusb owns.

Each slot now names its own ring instead of dev->a, so a leaked ring's late completions cannot land on a replacement ring. fail: no longer joins a zeroed pthread_t — an evt_started flag guards it.

running, rx_active and both inflight counters moved under the mutex; you are right that volatile orders nothing and makes no read-modify-write atomic. The statistics moved with them, behind mt_async_stats(), because the harness was reading them live off the struct — same bug, one level down. And a caller blocked waiting for a TX slot now gives up when the ring stops instead of parking forever.

Comment thread src/mt7612u/usb.c Outdated

if (mt_vendor_req(d, req, REQ_IN, (uint16_t)(a >> 16), (uint16_t)a,
b, sizeof b) != (int)sizeof b)
return ~0u;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~0u on failure is consumed unchecked by mt_rmw, set_wlan_state, the fw handshake and every mt_set / mt_clear caller, so one exhausted EP0 read during bring-up writes 0xFFFFFFFF | val into MT_WLAN_FUN_CTRL / MT_MAC_SYS_CTRL / BBP AGC. Return a status and make the RMW helpers refuse to write after a failed read. Also: the retry loop retries every error except NO_DEVICE 10x with a 1 s timeout each, so a dead read stalls 10 s and mt_poll's microsecond timeout_us is meaningless.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2142a5a. mt_rr_chk() reports failure separately from the value, mt_rmw() refuses to write after a failed read, and mt_wait_for_mac() distinguishes "not ready" from "no transport". 0xffffffff is a real MT_MAC_CSR0 value while the core comes up, so it could never have served as a sentinel.

The second half of the problem mattered as much: mt_poll() counted sleeps, so with one access costing up to VEND_RETRIES × timeout a caller asking for 200 ms could block for seconds. It polls against a real deadline now and aborts on a read failure. The control timeout also drops from 1000 ms to mt76's own MT_VEND_REQ_TOUT_MS of 300, which bounds the worst case at 3 s.

Re-verified on hardware afterwards — bring-up is unaffected.

Comment thread src/mt7612u/tools/bringup.c Outdated
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;
if (mt_mac_start(&dev, 1)) return 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enables MAC RX and then never reads EP4: the gate sleeps 200 ms and injects 300 frames before mt_mac_stop. Same at line 978 in the ack gate. docs/mt7612u.md says the wedge was fixed by "never enabling RX for a caller that will not drain it", and mt_rx_flush drains at most 64 x 4 KiB. Either start the async RX ring here or pass RX off.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and the doc claiming the wedge was fixed a few lines away made it worse rather than better. Both gates start the RX ring before enabling the receiver now (2142a5a) — caps in particular then sat through two 200 ms sleeps and a 40 MHz channel switch with nothing reading EP 4.

Re-run on hardware: three consecutive caps cycles clean, each logging async: 16 RX transfers in flight.

The structural half is that mt7612u_start() enables RX only when a ring is already running, so the public API cannot reproduce the pattern even if a caller wants to.

Comment thread src/mt7612u/initvals.h Outdated
@@ -0,0 +1,70 @@
/* SPDX-License-Identifier: BSD-3-Clause-Clear */
/* GENERATED from openwrt/mt76 mt76x2/init.c mt76_write_mac_initvals()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generator this comment points at (../../PLAN.md Stage C) is not in the PR, so as shipped this is a hand-transcribed copy of mt76x2's MAC initvals. Project rule: tables are produced by a tools/extract_*.py generator with pinned per-source SHA-256 hashes and a --check mode that reproduces the checked-in file byte-for-byte (see tools/extract_8733b_*.py for the shape). The constants in phy.c and init.c belong in the same generator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c64975d. reference/mt76 is a pinned shallow submodule at be5ce79, and tools/extract_mt7612u_tables.py follows the extract_8733b_*.py shape: UPSTREAM string, per-source SHA-256, EXPECTED count and table hash, and --check byte-comparing the checked-in header.

The source 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. That is what lets the four DEFAULT_PROT_CFG_* macros — defined inside mt76_write_mac_initvals() itself, not in the register header — be computed rather than copied. Both the symbol table and the evaluator raise on anything they do not understand rather than guessing.

It reproduces the hand-typed table byte for byte, all sixty rows including the four computed protection-config words, so it confirms the original transcription instead of silently replacing it. --check is load-bearing: appending one newline to the header exits 1 with stale generated output.

reference/mt76 is the one entry in that directory that is not a Realtek vendor drop — the MediaTek parts have no out-of-tree vendor driver to mirror, and mt76 being BSD-3-Clause-Clear rather than GPL-2-only is what lets src/mt7612u/ carry ported sequences at all. reference/README.md says so. No workflow checks out submodules, so the twenty builds are unaffected.

The stale ../../PLAN.md pointer is gone with the regenerated header.

Comment thread src/mt7612u/regs.h Outdated
#define BIT(n) (1u << (n))
#define GENMASK(h, l) (((~0u) - (1u << (l)) + 1) & (~0u >> (31 - (h))))
/* Lowest set bit of a contiguous mask, for FIELD_PREP/GET. */
#define _SHIFT(m) (__builtin_ctz(m))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__builtin_ctz under every FIELD_PREP/GET is the first MSVC blocker; _SHIFT is also a reserved identifier. A constexpr-style ctz shim (or _BitScanForward behind #ifdef _MSC_VER) is the usual fix, and it should land with the pthreads / nanosleep / clock_gettime / <libusb-1.0/libusb.h> items during integration rather than per-platform gating.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both fixed in 2ac9b9a. _SHIFT is gone — leading underscore followed by a capital is reserved to the implementation in every scope.

I did not use _BitScanForward: it is a function taking an out-parameter, so it cannot appear in a constant expression, and these macros have to stay constant expressions because ext_cca_chan in phy.c is a static table built from FIELD_PREP. MT_CTZ isolates the low bit and binary-searches its position — a constant expression on every compiler, folded to one instruction under optimisation.

tests/field_macros checks it against __builtin_ctz over all 32 single-bit masks and all 528 contiguous GENMASK(h, l) ranges, with a FIELD_PREP/FIELD_GET round-trip on each. It also carries a static initialiser built from FIELD_PREP, so if the macro ever stops being constant-foldable the test fails to compile rather than passing at runtime.

Same commit: <libusb.h> — this project's spelling — is tried first via __has_include, with the distribution's libusb-1.0/ path as the fallback for the standalone build.

Deliberately not done, and I would rather say so than half-do it: async.c still uses pthreads and usb.c still 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 the moment the subtree joins the build. Those are the only two files involved and the README names them. Same reasoning for the Makefile staying a Makefile while nothing in CMakeLists.txt reaches the subtree — a CMake target now would be a target nothing builds.

Comment thread src/mt7612u/radiotap.c Outdated

if (flags & 0x04) r->sgi = 1;
if (coding & 0x01) r->ldpc = 1;
r->bw = bwc == 0 ? MT7612U_BW_20

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Radiotap VHT bandwidth codes 2 and 3 are the 20 MHz sub-channels of a 40 MHz frame, and 5-10 are the 20/40 sub-channels of 80. Mapping 1-3 to 40 and >=4 to 80 airs a requested 20-in-40 frame at 40 MHz. Also, DBM_TX_POWER is silently ignored a few lines up; worth a diagnostic until per-packet power is wired.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and I had this backwards on the first read. Fixed in a9cd156 with a table over the eleven codes the part can express: 2 and 3 are 20 MHz, 5 and 6 are 40, 7–10 are 20. Codes 11 and up are 160 MHz and its sub-channels, and the rate word has no 160 encoding, so those log and fall back to 20 rather than narrowing silently.

What settled it was internal to the repository rather than the spec text: the HT branch a few lines above already reads the equivalent codes correctly, and src/ieee80211_radiotap.h:112 names them IEEE80211_RADIOTAP_MCS_BW_20L and _20U. The VHT branch was disagreeing with the repository, not just with the standard.

tests/frame_shape now pins all eleven codes; restoring the old expression fails eight of them.

DBM_TX_POWER emits one diagnostic per process naming the two knobs that do work, rather than accepting the field and dropping it (2142a5a).

One thing to flag rather than fix from here: 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. I have not touched them — changing a shipping backend's on-air behaviour from inside a new-backend PR seemed like the wrong place. Happy to send it as its own PR if you want it.

Comment thread src/mt7612u/caps.c Outdated
#include "internal.h"

/*
* DW0 is the LOW word. mt76's mt76x02u_restart_pre_tbtt_timer() assembles this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The claim that mt76's mt76x02u_restart_pre_tbtt_timer() assembles TSF backwards is asserted without a file:line. The measured behaviour here stands on its own; cite the upstream line (or drop the upstream-bug claim) before it goes into the doc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cited in 2142a5a: mt76x02_usb_core.c:155-158, where

dw0 = mt76_rr(dev, MT_TSF_TIMER_DW0);
dw1 = mt76_rr(dev, MT_TSF_TIMER_DW1);
tsf = (u64)dw0 << 32 | dw1;
dev_dbg(dev->mt76.dev, "TSF: %llu us TBTT %u us\n", tsf, tbtt);

tsf is consumed only by that dev_dbg(), which is why the order has survived upstream. The comment quotes those four lines now.

The caps gate also prints both orders against a known 200 ms sleep, so the claim is re-checkable on any sample rather than taken on trust — this run reads 200347 µs for (DW1 << 32) | DW0.

snokvist and others added 5 commits September 6, 2026 17:00
## 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
@snokvist

snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

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:

2142a5a the nine confirmed defects
c64975d initvals.h generated instead of transcribed
2ac9b9a the portability fixes that stand on their own
081f952 offline tests documented, everything re-measured on hardware
a9cd156 the radiotap VHT bandwidth mapping

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: rtap tags A 401 / B 388, ampdu 41 734 of 85 580 witnessed frames with paggr=1 and the same 15.50 and 44.55 Mbit/s the PR reported, caps clean on three consecutive cycles with the RX ring now started before the receiver, 40 MHz re-confirmed 300/300 at bw=1, and 278 ambient frames on ch1 through the rewritten pad fold with rx_err=0.

make -C src/mt7612u check is new and runs three offline binaries — no hardware, no privileges. Each was mutation-tested rather than merely written: reverting any one of the frame-shape fixes, the VHT mapping or a public definition makes the suite fail. The header-length fix has no on-air observable in these gates, because nothing here injects a control frame, and the docs say that rather than implying otherwise.

CMakeLists.txt is still untouched.


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.

the code has bugs that sit under the measurements, so it is not mergeable even as an unwired subtree

I took that to mean an unwired subtree is the mergeable shape once the bugs are gone, and that

the project will need a refactor first, and that is the first milestone rather than wiring this subtree in as-is

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 SYS_CFG2 read the way Kestrel already gates PID-first, MediaTek getting its own transport behind the same factory rather than a shim over IRtlTransport (32-bit registers plus in-band MCU over EP8/EP5 do not fit it), a ChipGeneration entry, a DEVOURER_MT7612U option following DEVOURER_HAVE_*, and a regress.py cell.

The part I would push back on gently is the rename. IRtlDevice is already vendor-neutral in substance — Init / InitWrite / StartRxLoop / send_packet / SetMonitorChannel / FastRetune / GetAdapterCaps say nothing about Realtek — so a MediaTek backend does not need the rename to sit behind it, and doing it inside this work would touch every backend for a naming reason. I would rather implement against IRtlDevice / WiFiDriver::CreateRtlDevice as they stand, and send the rename separately as its own mechanical, reviewable-in-isolation PR if you still want it. Your call — it is your codebase, and if you would rather have the rename first I will do that instead.

That also decides where the remaining portability work lands. async.c still uses pthreads and usb.c nanosleep/clock_gettime; this project has no C threading or time shim because its shim is the C++ standard library. Rather than write a C shim that gets deleted at integration, I would like to do that swap as part of the integration commit, where the subtree joins the build and the answer is <thread>/<chrono> like everywhere else. Two files, and the README already names them.

snokvist and others added 2 commits September 6, 2026 17:21
`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
@snokvist

snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

One more, found while working out what the integration has to look like, and it is a limit rather than a fix.

Packet::Data in src/RxPacket.h is documented as the full 802.11 frame including the trailing FCS, and every Realtek parser here honours that. MT7612U cannot.

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 — a fixed 4-byte field plus USB 4-byte alignment, which is exactly what an FCS would look like. Handing a consumer len + 4 would have looked right in every log.

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 "123456789"0xcbf43926 vector before believing the negative. They are the FCE info trailer — mt76u_get_rx_entry_len() computes min_len = MT_DMA_HDR_LEN + MT_RX_RXWI_LEN + MT_FCE_INFO_LEN, and dma.h:48 has 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.

So a consumer that trims four bytes at its protocol boundary — which the Packet::Data comment invites, and which tools/bf_report_decode.py already does — would eat four bytes of payload off every MT7612U frame.

Documented in docs/mt7612u.md and on the RX callback in the public header (5e85f7a). No code change; the code was already right, it just did not say what it could not do. Raising it now because it is the kind of thing that should be settled before the integration commit rather than discovered inside it — and because it is a real asymmetry against the Realtek backends that a caps flag or a documented boundary will have to carry.

@snokvist

snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

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: feat/mt7612u-integration, stacked on this branch.

Shape, following your list: RtlMt7612uDevice : IRtlDevice, dispatched on VID:PID ahead of the SYS_CFG2 read (the same place Kestrel gates, for a stronger version of the reason — 0x00FC is a Realtek address issued with a Realtek vendor request, so on MediaTek silicon that read is the wrong protocol rather than merely ambiguous). It does not shim over IRtlTransport: the C library here is the transport, and mt7612u_open_handle() adopts the handle WiFiDriver already opened, reset and claimed rather than reopening it and racing the caller's lock. ChipGeneration::Mediatek, DEVOURER_MT7612U (OFF by default), three ctest cells.

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:

  • RSSI base. devourer carries RSSI as an unsigned byte biased by 110 (LinkHealth.cpp:8). Casting the HAL's signed dBm straight in reported −63 dBm as 193 — a claimed +83 dBm.
  • The receive filter. mt7612u_start() leaves MT_RX_FILTR_CFG at mt76's managed-station value, which drops control frames and anything not addressed here. A/B against the harness on the same channel in the same minute: the integrated path saw 0 OFDM frames of 244, the harness 103 of 402. With a monitor filter the same run takes 7096 frames carrying the full CCK/OFDM/HT mix. Rtl8733bDevice::Init calls configure_monitor_rx for exactly this reason and I had simply not done the equivalent.

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 docs/mt7612u.md rather than just fixed.

ctest 58/58 with the option ON, 55/55 OFF; the mapping selftest is mutation-tested; and this project's own rxdemo drives the adapter end to end through the factory.

One thing to flag about the branch split: it also carries a subtree fix that arguably belongs in this PR instead — mt_rx_parse() now drops a frame whose rate word names no valid PHY. MT_RATE_PHY is three bits, so 5–7 are representable and mean nothing; mt76 returns -EINVAL and drops them, while mine decoded one through the default arm and reported the lowest legacy rate, i.e. garbage presented as a real CCK frame. It measured 0 on the bench so it was not the cause of the filter symptom, just a hole found while chasing it. It is entangled with an API addition made for integration, which is why it landed there — say the word and I will split it back into this PR so the subtree is complete on its own.

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
@snokvist
snokvist marked this pull request as ready for review September 6, 2026 18:40
@snokvist

snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

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

mt_set_channel_ex()'s 40 MHz case derives the centre channel from the control channel and never validated the result. mt76 doesn't need to — mt76x2u_phy_set_channel() takes center_freq1 from the chandef, so an off-grid channel can't reach it. This port takes a bare channel number, so an off-grid one still produces a number and then transmits 40 MHz wide somewhere the caller didn't ask for, with every register write succeeding.

Two cases reach the MCU straight through mt7612u_set_channel(), which checks only for a zero channel:

  • the arithmetic ran in uint8_t. Control channel 254 computes 256, truncates to 0, and hands 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, so channel 1 computes −1, i.e. 255 after truncation.

mt_chan40_centre() computes in int and validates against the centres that exist, catching off-grid, out-of-band and both wraps with one check. 2.4 GHz 40 MHz now works for channels 4–11 and is refused elsewhere — a deliberate narrowing, since a bare channel number can't say which side the secondary sits on where channel 6 pairs equally well with 2 or with 10.

The caveats matter more than the fix. No measurement in this PR moves: bringup caps 149 still sends 300 frames at 40 MHz and the witness still reports 300 at bw=1. The bug was never in the path any published number exercised. The nine-case test is mutation-tested, but restoring the uint8_t arithmetic does not make it fail — no wrapped value aliases onto a legal centre, so the int only buys a refusal that names 256 instead of reporting 0 and sending the reader after the wrong bug. The comment says that rather than claiming a guard it doesn't provide.

Also here: make check now depends on bringup. Nothing else compiled it, so a change that broke a gate shipped with everything green — which is how I found this.

The second branch, and a question back

The stacked branch now carries the integration plus 80 MHz, and I've brought its build wiring to parity with src/rtl8733b and src/kestrel: an mt7612uprobe CMake target beside rtl8733bprobe, the offline tests as native ctest executables instead of a make -C shell-out (that one silently ignored CMAKE_C_COMPILER and DEVOURER_SANITIZE), a POSIX FATAL_ERROR guard mirroring DEVOURER_PCIE's, an mt7612u-only CI cell — nothing built this backend before — and -DDEVOURER_MT7612U=OFF in the no-chip-selected control, which was passing on a default rather than on naming every option.

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 (mt76x2_phy_update_channel_gain) is unported — only its width-dependent registers are.

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.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add standalone, hardware-validated MT7612U MediaTek backend

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a standalone MT7612U userspace HAL without integrating the shipped Realtek library.
• Supports firmware bring-up, channel control, radiotap injection, monitor RX, and asynchronous USB
 I/O.
• Documents hardware measurements, limitations, provenance, and offline regression coverage.
Diagram

graph TD
  API["Public API"] --> CORE["Device Core"] --> FW["Firmware MCU"] --> USB["libusb Transport"] --> HW["MT7612U"]
  CORE --> IO["TX RX Paths"] --> RINGS["Async Rings"] --> USB
  HARNESS["Bringup Tests"] --> API
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Integrate through the existing device abstraction now
  • ➕ Would expose compatibility issues with IRtlDevice, WiFiDriver, DeviceConfig, and CI immediately.
  • ➕ Would deliver a directly consumable backend rather than a separate prototype.
  • ➖ Requires substantial integration before maintainers decide whether MediaTek belongs in scope.
  • ➖ The missing RX FCS conflicts with the existing packet contract and needs an explicit design decision.
  • ➖ Adds avoidable review surface to an already hardware-intensive change.
2. Keep MediaTek support in a companion repository
  • ➕ Preserves the project’s Realtek-only scope and naming assumptions.
  • ➕ Allows independent release, portability, and hardware-validation policies.
  • ➖ Fragments shared radiotap and injection behavior.
  • ➖ Makes future adoption and cross-backend testing more difficult.
  • ➖ Duplicates project-specific interfaces if integration is later approved.
3. Drive the existing kernel mt76 backend
  • ➕ Avoids maintaining firmware loading, register initialization, and USB lifecycle code.
  • ➕ Benefits from upstream kernel testing and broader hardware coverage.
  • ➖ Kernel interfaces do not expose all descriptor-level rate, aggregation, and responder controls demonstrated here.
  • ➖ Behavior becomes dependent on kernel scheduling and monitor-mode facilities.
  • ➖ Does not satisfy consumers requiring direct userspace USB ownership.

Recommendation: For a scope decision, the current standalone prototype is the best approach: it isolates risk, provides measurable evidence, and avoids speculative framework changes. It should not merge as a production backend in its current form; if MediaTek is accepted, follow with integration through the existing device abstraction, CI coverage, an explicit FCS-contract resolution, and broader hardware validation. Otherwise, preserve the work in a companion repository or documentation branch.

Files changed (26) +6315 / -1

Enhancement (15) +3907 / -0
async.cImplement asynchronous USB TX and RX rings +301/-0

Implement asynchronous USB TX and RX rings

• Adds a pthread-driven libusb event loop with a 16-transfer RX ring and 32-slot TX pool. Teardown cancels and drains in-flight transfers, deliberately leaking unrecoverable rings rather than risking use-after-free.

src/mt7612u/async.c

caps.cExpose capabilities, TSF access, and ACK responses +118/-0

Expose capabilities, TSF access, and ACK responses

• Implements capability reporting and corrected TSF word ordering. Adds a hardware ACK responder by retargeting the programmed MAC identity and restoring it when cleared.

src/mt7612u/caps.c

eeprom.cParse EEPROM calibration and power data +276/-0

Parse EEPROM calibration and power data

• Reads and validates the 512-byte EEPROM image, extracts adapter identity, and reconstructs per-rate TX power tables. It also derives channel-specific power, LNA, RSSI, and MCU gain calibration values.

src/mt7612u/eeprom.c

fw.cLoad the MT7662 ROM patch and firmware +237/-0

Load the MT7662 ROM patch and firmware

• Implements FCE setup, chunked ROM patch upload, ILM/DLM firmware transfer, and startup polling. Firmware layouts and destination addresses follow the pinned mt76 implementation.

src/mt7612u/fw.c

mt7612u.hDefine the standalone MT7612U public API +182/-0

Define the standalone MT7612U public API

• Introduces lifecycle, channel, power, chainmask, TX/RX, radiotap, ACK responder, TSF, identity, and capability interfaces. Public types describe per-packet rates and decoded receive metadata.

src/mt7612u/include/mt7612u/mt7612u.h

init.cImplement power-on and MAC lifecycle sequencing +445/-0

Implement power-on and MAC lifecycle sequencing

• Ports power, DMA, MAC, address, key-table, and firmware initialization sequences from mt76. RX startup and endpoint flushing guard against hardware wedges caused by enabling an undrained receive path.

src/mt7612u/init.c

initvals.hAdd generated MT7612U MAC initialization values +75/-0

Add generated MT7612U MAC initialization values

• Provides the 60-register initialization table mechanically extracted from the pinned mt76 source, including provenance and a content hash.

src/mt7612u/initvals.h

internal.hDefine backend state and internal module contracts +219/-0

Define backend state and internal module contracts

• Adds device, calibration, asynchronous-ring, and power-table state shared across the implementation. It also declares transport, MCU, PHY, frame, and lifecycle helpers.

src/mt7612u/internal.h

mcu.cImplement the in-band MCU command transport +164/-0

Implement the in-band MCU command transport

• Frames MCU requests for the command endpoint and matches responses by sequence number. Provides helpers for radio state, function selection, gain setup, channel switching, and calibration.

src/mt7612u/mcu.c

phy.cImplement PHY, channel, calibration, and power control +515/-0

Implement PHY, channel, calibration, and power control

• Programs bands, bandwidth, PA settings, EEPROM-derived TX power, RX gain, and firmware calibration sequences. It validates legal 40 MHz center channels and exposes full versus calibration-skipping retune paths.

src/mt7612u/phy.c

radiotap.cAdd radiotap parsing and chained packet injection +307/-0

Add radiotap parsing and chained packet injection

• Decodes legacy, HT, and VHT injection metadata into MT7612U rate descriptors. Supports single-frame injection and multi-frame bulk transfers chained with NEXT_VLD.

src/mt7612u/radiotap.c

regs.hDefine MT7612U registers, descriptors, and field helpers +407/-0

Define MT7612U registers, descriptors, and field helpers

• Collects the trimmed register map, EEPROM fields, MCU commands, USB endpoints, and TX/RX descriptor layouts. Portable constant-expression field macros replace compiler-specific trailing-zero builtins.

src/mt7612u/regs.h

rx.cParse monitor RX descriptors and remove L2 padding +127/-0

Parse monitor RX descriptors and remove L2 padding

• Decodes RXWI rate, aggregation, sequence, error, and per-chain RSSI metadata. It removes hardware L2 padding using the actual 802.11 header length and returns frames without FCS.

src/mt7612u/rx.c

tx.cBuild MT7612U TX descriptors and submit frames +223/-0

Build MT7612U TX descriptors and submit frames

• Constructs per-packet TXWI and TXINFO metadata, including rates, ACK policy, aggregation, power adjustment, WCID selection, and L2 padding. Supports synchronous and asynchronous bulk submission.

src/mt7612u/tx.c

usb.cImplement resilient libusb transport and device ownership +311/-0

Implement resilient libusb transport and device ownership

• Adds checked register access, retrying vendor requests, bounded polling, bulk transfers, block writes, USB reset handling, interface claiming, and kernel-driver detach/reattach behavior.

src/mt7612u/usb.c

Tests (3) +390 / -0
api_link.cVerify every public API symbol resolves +47/-0

Verify every public API symbol resolves

• Links against all 20 public entry points using only the exported header, catching declarations without implementations.

src/mt7612u/tests/api_link.c

field_macros.cValidate portable register-field macros +68/-0

Validate portable register-field macros

• Checks MT_CTZ and field round-trips across all single-bit and contiguous 32-bit masks. A static initializer verifies compile-time constant behavior.

src/mt7612u/tests/field_macros.c

frame_shape.cCover frame layout and channel-width regressions +275/-0

Cover frame layout and channel-width regressions

• Tests management, control, and data header lengths, QoS L2-pad restoration, VHT bandwidth mappings, and valid 40 MHz centers. Negative controls demonstrate the prior corruption and wrapping failure modes.

src/mt7612u/tests/frame_shape.c

Documentation (3) +445 / -1
mt7612u.mdDocument MT7612U measurements, methods, and limitations +320/-0

Document MT7612U measurements, methods, and limitations

• Records hardware evidence for rates, power, aggregation, ACK responses, RX, USB chaining, TSF, and retuning. It also documents test methodology, known constraints, unverified paths, and remaining integration work.

docs/mt7612u.md

README.mdDescribe mt76 as the MediaTek reference source +13/-1

Describe mt76 as the MediaTek reference source

• Expands the reference-driver documentation to include the pinned mainline mt76 source, its licensing, and its role in regenerating MT7612U tables.

reference/README.md

README.mdExplain standalone backend usage and safety constraints +112/-0

Explain standalone backend usage and safety constraints

• Documents the subtree layout, firmware requirements, verification gates, portability status, provenance, and independent build commands. It emphasizes that RX must never be enabled without a draining endpoint.

src/mt7612u/README.md

Other (5) +1573 / -0
.gitmodulesRegister the pinned mt76 reference submodule +5/-0

Register the pinned mt76 reference submodule

• Adds openwrt/mt76 as a shallow reference submodule used to derive and verify MT7612U register sequences.

.gitmodules

mt76Pin the mt76 source revision +1/-0

Pin the mt76 source revision

• Adds the mt76 submodule at commit be5ce7910521492d4a2e4ce7ee3843680a46c047.

reference/mt76

MakefileAdd a standalone MT7612U build and test workflow +44/-0

Add a standalone MT7612U build and test workflow

• Builds the bring-up harness, library objects, and three offline test binaries with libusb and pthreads. Header dependencies prevent stale object layouts, while the check target runs hardware-independent tests.

src/mt7612u/Makefile

bringup.cAdd per-gate hardware bring-up and measurement harness +1221/-0

Add per-gate hardware bring-up and measurement harness

• Provides independently runnable gates for registers, firmware, initialization, channels, TX/RX, rate control, throughput, duplex operation, power, A-MPDU, capabilities, ACK response, radiotap, and retuning. Outputs are designed for witness-radio and kernel-register comparisons.

src/mt7612u/tools/bringup.c

extract_mt7612u_tables.pyGenerate MAC tables from pinned mt76 sources +302/-0

Generate MAC tables from pinned mt76 sources

• Parses mt76 C initializers and a constrained set of constant expressions to generate initvals.h. Source hashes, expected row counts, output hashes, and check mode make provenance reproducible.

tools/extract_mt7612u_tables.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (3) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Stuck transfers outlive USB teardown 📘 Rule violation ☼ Reliability
Description
mt_async_stop stops the event thread and returns after two seconds even when tx_inflight or
rx_inflight is still nonzero. When cancellation completion is delayed, mt7612u_close proceeds
through mt_close, releasing the interface, handle, and libusb context while libusb still owns the
leaked transfers.
Code

src/mt7612u/async.c[R213-216]

+		ERR("async stop: %d TX and %d RX transfers still in flight after 2 s "
+		    "- leaking the ring rather than freeing memory libusb owns",
+		    stuck_tx, stuck_rx);
+		return;
Evidence
Rule 14 requires all asynchronous transfers to be cancelled and reaped before the interface, handle,
or libusb context is released. The timeout path returns with outstanding transfers, while the close
path subsequently tears down each USB resource.

CLAUDE.md: Destroy Devices Before Tearing Down libusb: CLAUDE.md: Destroy Devices Before Tearing Down libusb
src/mt7612u/async.c[193-216]
src/mt7612u/init.c[420-426]
src/mt7612u/usb.c[256-269]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Device teardown can close the libusb handle and context while cancelled asynchronous transfers remain in flight.
## Issue Context
`mt_async_stop` abandons outstanding transfers after a two-second deadline, but `mt7612u_close` then continues with normal USB teardown. Keep the event machinery, handle, and context alive until libusb has returned ownership of every submitted transfer.
## Fix Focus Areas
- src/mt7612u/async.c[175-225]
- src/mt7612u/init.c[420-426]
- src/mt7612u/usb.c[256-269]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Concurrent diagnostics can interleave 📘 Rule violation ☼ Reliability
Description
LOG and ERR construct each diagnostic with separate fprintf and fputc calls, omit an
explicit flush, and use a format other than devourer [level] message. When the event thread and a
caller log concurrently, text can be inserted before another message's newline, while buffered
stderr can leave a piped consumer waiting.
Code

src/mt7612u/internal.h[R216-217]

+#define LOG(...)  do { fprintf(stderr, "[mt7612u] " __VA_ARGS__); fputc('\n', stderr); } while (0)
+#define ERR(...)  do { fprintf(stderr, "[mt7612u] ERROR " __VA_ARGS__); fputc('\n', stderr); } while (0)
Evidence
Rule 8 requires human diagnostics to use the documented format, emit each line with one write, and
flush normally. Both newly added macros split a line across two calls without flushing and can be
invoked from concurrent library paths.

CLAUDE.md: Preserve the Machine-Event and Human-Diagnostic Logging Contract: CLAUDE.md: Preserve the Machine-Event and Human-Diagnostic Logging Contract
src/mt7612u/internal.h[216-217]
src/mt7612u/async.c[145-155]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MT7612U diagnostics do not follow the repository's atomic, flushed stderr logging contract.
## Issue Context
Format each complete human diagnostic as `devourer [level] message`, emit it to stderr with one write, and preserve thread safety and required flushing.
## Fix Focus Areas
- src/mt7612u/internal.h[216-217]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Receiver guidance can drift 📘 Rule violation ⚙ Maintainability
Description
src/mt7612u/README.md repeats the public header's warning that enabling receive without draining
bulk-IN wedges the device and requires a physical replug. If the receiver startup contract or
recovery behavior changes, maintainers must update two authoritative-looking explanations and
readers can follow the stale copy.
Code

src/mt7612u/README.md[R36-39]

+Enabling MAC RX with nothing reading the bulk-IN endpoint wedges this part
+*below* the USB level: `libusb_reset_device`, the sysfs `authorized` toggle
+and rebinding the kernel driver all fail to recover it, and only a physical
+replug does. So `mt_mac_start()` takes the receiver as an explicit argument,
Evidence
Rule 2 requires secondary documentation to refer to authoritative header comments rather than
reproduce them. The README and public header both describe the same undrained-receiver wedge,
software-reset limitation, physical-replug requirement, and startup ordering.

CLAUDE.md: Do Not Duplicate Existing Header Documentation: CLAUDE.md: Do Not Duplicate Existing Header Documentation
src/mt7612u/README.md[34-41]
src/mt7612u/include/mt7612u/mt7612u.h[101-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The README duplicates the receiver startup and recovery contract already documented by the public API header.
## Issue Context
Keep the detailed contract in the authoritative header and replace the README copy with a concise reference to that declaration.
## Fix Focus Areas
- src/mt7612u/README.md[34-41]
- src/mt7612u/include/mt7612u/mt7612u.h[101-107]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (6)
4. 2.4 GHz frames use the wrong power 🐞 Bug ≡ Correctness
Description
mt_get_rate_power() shifts the 2.4 GHz VHT EEPROM word twice, so both VHT power entries are always
decoded from zero. Every 2.4 GHz VHT channel setup then programs those incorrect entries into the
transmit-power registers.
Code

src/mt7612u/eeprom.c[R117-119]

+	if (!is_5ghz)
+		v >>= 8;
+	t->vht[0] = t->vht[1] = rate_power_val(v >> 8);
Evidence
The 2.4 GHz branch first shifts v by eight and then passes v >> 8, which is necessarily zero
because v is 16 bits. The resulting VHT entries flow into the hardware power configuration writes.

src/mt7612u/eeprom.c[116-120]
src/mt7612u/phy.c[237-245]
src/mt7612u/phy.c[273-274]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Correct the 2.4 GHz VHT power-table decoding so the selected EEPROM byte is passed directly to `rate_power_val()` rather than shifted twice.
## Issue Context
The decoded entries are consumed by channel setup and written to the VHT transmit-power registers.
## Fix Focus Areas
- src/mt7612u/eeprom.c[116-120]
- src/mt7612u/phy.c[237-245]
- src/mt7612u/phy.c[273-274]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Malformed radio headers still transmit ✓ Resolved 🐞 Bug ≡ Correctness
Description
mt_radiotap_parse() returns the declared header length when a present field extends beyond that
header instead of rejecting the buffer. Both injection APIs treat that positive result as valid and
transmit the following frame using default or partially parsed rate settings.
Code

src/mt7612u/radiotap.c[R144-146]

+			off = (off + align - 1) & ~((size_t)align - 1);
+			if (off + size > rlen)
+				return (int)rlen;
Evidence
The known-field bounds check returns rlen, while callers reject only non-positive parser results.
The rate structure has already been initialized to OFDM, one stream and 20 MHz, so the malformed
field is silently replaced by those defaults.

src/mt7612u/radiotap.c[126-129]
src/mt7612u/radiotap.c[144-147]
src/mt7612u/radiotap.c[217-223]
src/mt7612u/radiotap.c[258-270]
src/mt7612u/tx.c[141-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Return a parsing failure when a declared radiotap field does not fit within the header, and ensure both injection paths reject it.
## Issue Context
The parser currently initializes a default rate before discovering the truncation, allowing malformed requests to transmit at unintended settings.
## Fix Focus Areas
- src/mt7612u/radiotap.c[126-129]
- src/mt7612u/radiotap.c[144-147]
- src/mt7612u/radiotap.c[217-223]
- src/mt7612u/radiotap.c[258-270]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Hardware setup can falsely succeed 🐞 Bug ☼ Reliability
Description
mt_wr() discards the result of mt_vendor_req() and provides no failure signal or error-state
update to its callers. When a register write exhausts its USB retries during initialization, channel
selection, or power programming, the public operation can continue and return success with only part
of the requested hardware state applied.
Code

src/mt7612u/usb.c[R100-102]

+	b[0] = val & 0xff; b[1] = (val >> 8) & 0xff;
+	b[2] = (val >> 16) & 0xff; b[3] = (val >> 24) & 0xff;
+	mt_vendor_req(d, req, REQ_OUT, (uint16_t)(a >> 16), (uint16_t)a, b, sizeof b);
Evidence
mt_vendor_req() returns a negative libusb error after exhausting retries, but mt_wr() ignores
that value. Hardware initialization and transmit-power setup issue mandatory writes through this
void helper and can reach their normal return paths without checking whether those writes reached
the device.

src/mt7612u/usb.c[33-47]
src/mt7612u/usb.c[95-107]
src/mt7612u/init.c[343-390]
src/mt7612u/phy.c[263-274]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Make register writes expose USB control-transfer failures and propagate them through operations whose correctness depends on the write succeeding.
## Issue Context
Read failures update `io_err`, but writes currently neither update it nor return status, while initialization and PHY setup perform many mandatory writes.
## Fix Focus Areas
- src/mt7612u/usb.c[95-107]
- src/mt7612u/init.c[343-390]
- src/mt7612u/phy.c[263-274]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Read failures corrupt calibration data 🐞 Bug ☼ Reliability
Description
mt_eeprom_init() reads the EEPROM through mt_rr(), which converts every transport failure into
0xffffffff, and never checks the accumulated io_err value. A failure outside the separately
validated chip and MAC cells therefore leaves all-ones power or calibration bytes in memory while
device opening continues successfully.
Code

src/mt7612u/eeprom.c[R22-24]

+	for (unsigned i = 0; i + 4 <= MT7612U_EEPROM_SIZE; i += 4) {
+		uint32_t v = mt_rr(d, EEP_ADDR(i));
+
Evidence
mt_rr() returns all ones when mt_rr_chk() fails and only increments io_err, while the EEPROM
loop stores that value without checking either result. The function validates the chip identifier
and MAC shape but not every calibration cell later consumed by PHY setup.

src/mt7612u/eeprom.c[17-43]
src/mt7612u/usb.c[64-93]
src/mt7612u/init.c[395-410]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Read EEPROM words with the checked register API and abort initialization on any transport failure rather than storing the sentinel value.
## Issue Context
Only the chip identifier and part of the MAC address are validated afterward; power and calibration cells may otherwise remain silently corrupted.
## Fix Focus Areas
- src/mt7612u/eeprom.c[17-35]
- src/mt7612u/usb.c[64-93]
- src/mt7612u/init.c[395-410]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Cleared responders keep acknowledging frames 🐞 Bug ≡ Correctness
Description
mt7612u_clear_ack_responder() restores the saved MAC address and clears ack_saved, but never
clears MT_AUTO_RSP_EN after mt7612u_set_ack_responder() enabled it. A caller that clears the
responder leaves automatic responses enabled for the restored normal device identity, and later
clear calls return without another opportunity to disable it.
Code

src/mt7612u/caps.c[117]

+	d->ack_saved = 0;
Evidence
The arm path explicitly enables the auto-response bit, while the clear path only restores MAC
registers and invalidates the saved-state guard. The public API describes this function as clearing
a hardware ACK responder.

src/mt7612u/caps.c[65-84]
src/mt7612u/caps.c[101-117]
src/mt7612u/include/mt7612u/mt7612u.h[147-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Clearing the ACK responder restores its MAC address but leaves the automatic-response enable bit set.
## Issue Context
The clear operation must undo both pieces of state established by arming: the temporary MAC identity and the auto-response gate. Clear the gate before marking the saved state inactive, so repeat calls cannot leave the device responding.
## Fix Focus Areas
- src/mt7612u/caps.c[101-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Some 40 MHz packet requests transmit at 20 MHz ✓ Resolved 🐞 Bug ≡ Correctness
Description
mt_radiotap_parse() tests known & 0x02 before applying HT bandwidth, although bit 0x01
declares that the bandwidth field is present. A radiotap header that supplies bandwidth without also
declaring an MCS index leaves r->bw at its initialized 20 MHz value, so requested 40 MHz injection
is narrowed.
Code

src/mt7612u/radiotap.c[R179-180]

+				if ((known & 0x02) && ((flags & 0x03) == 1))
+					r->bw = MT7612U_BW_40;
Evidence
The repository defines MCS_HAVE_BW as 0x01 and MCS_HAVE_MCS as 0x02; the new parser uses the
latter to gate bandwidth decoding. Its default initialized value is 20 MHz, making the missed branch
observable in transmitted rate selection.

src/mt7612u/radiotap.c[126-129]
src/mt7612u/radiotap.c[173-183]
src/ieee80211_radiotap.h[101-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HT radiotap parser uses the MCS-known flag to decide whether bandwidth is available instead of the bandwidth-known flag.
## Issue Context
Use the repository's radiotap field definitions and preserve the default bandwidth only when the bandwidth field is not known. Add a parser test for an MCS field with `MCS_HAVE_BW` but without `MCS_HAVE_MCS`.
## Fix Focus Areas
- src/mt7612u/radiotap.c[173-183]
- src/ieee80211_radiotap.h[101-113]
- src/mt7612u/tests/frame_shape.c[152-194]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

10. Table checks crash on Python 3.11 ✓ Resolved 🐞 Bug ≡ Correctness
Description
The --check path calls Path.read_text() with an unsupported newline keyword argument. Running
the documented verification command on Python 3.11 raises TypeError before the generated table can
be compared.
Code

tools/extract_mt7612u_tables.py[R289-292]

+    if args.check:
+        path = root / OUTPUT_H
+        if not path.exists() or path.read_text(encoding="utf-8", newline="") != output:
+            raise SystemExit(f"stale generated output: {OUTPUT_H}")
Evidence
The documented --check branch necessarily reaches the changed read_text() invocation, whose
Python 3.11 signature has no newline parameter. Both project documents present this path as the
generated-table verification command.

tools/extract_mt7612u_tables.py[286-295]
docs/mt7612u.md[19-19]
src/mt7612u/README.md[101-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Replace the unsupported `Path.read_text(..., newline="")` call with a portable byte or text read that preserves the intended exact comparison.
## Issue Context
The write API supports `newline`, but `Path.read_text()` on common supported Python versions does not.
## Fix Focus Areas
- tools/extract_mt7612u_tables.py[286-295]
- docs/mt7612u.md[19-19]
- src/mt7612u/README.md[101-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Radio tests pass without sending frames ✓ Resolved 🐞 Bug ≡ Correctness
Description
gate_g() and the first gate_ampdu() experiment discard every mt_tx_raw() return value before
unconditionally returning success. If the asynchronous or synchronous transmit path rejects every
frame, the harness still exits with status zero and presents the failed experiment for witness
evaluation.
Code

src/mt7612u/tools/bringup.c[470]

+			mt_tx_raw(&dev, frame, 40, &mcs7, 1, arm);
Evidence
The rate gate ignores both public and raw transmit results and returns zero, while the aggregation
gate prints its asynchronous error count but also returns zero. In contrast, the basic TX gate
counts successful submissions and fails when the count differs, demonstrating that submission is a
required local precondition.

src/mt7612u/tools/bringup.c[445-479]
src/mt7612u/tools/bringup.c[781-801]
src/mt7612u/tools/bringup.c[845-848]
src/mt7612u/tools/bringup.c[299-317]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Track every frame-submission result in the rate and aggregation gates and return failure when expected submissions are not accepted.
## Issue Context
The witness can validate on-air behavior only after local submission succeeds; a transport failure is not a valid witness-dependent outcome.
## Fix Focus Areas
- src/mt7612u/tools/bringup.c[445-479]
- src/mt7612u/tools/bringup.c[781-801]
- src/mt7612u/tools/bringup.c[845-848]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Invalid radio modes can crash the tool ✓ Resolved 🐞 Bug ☼ Reliability
Description
gate_tx() indexes a five-entry name array with phy & 7, allowing command-line values 5 through 7
to read beyond the array. The receive gate repeats the same unchecked lookup for the three-bit PHY
value decoded from device input, so either malformed user input or an unexpected descriptor can
trigger undefined behavior.
Code

src/mt7612u/tools/bringup.c[R294-295]

+	printf("injecting %d frames on ch%u, %s idx %d, no-ACK, rate word 0x%04x\n",
+	       count, chan, phy_name[phy & 7], mcs, mt_tx_rate_word(&rate));
Evidence
Both arrays contain five entries, but both indexes are merely masked to the range zero through
seven. The TX value comes directly from atoi(), while the RX value comes from the decoded
descriptor without a range check.

src/mt7612u/tools/bringup.c[266-295]
src/mt7612u/tools/bringup.c[320-363]
src/mt7612u/tools/bringup.c[1201-1205]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Validate PHY values against the name-array length before formatting them, and reject invalid command-line transmission modes.
## Issue Context
Masking to three bits does not constrain the value to the five defined enum members.
## Fix Focus Areas
- src/mt7612u/tools/bringup.c[266-295]
- src/mt7612u/tools/bringup.c[320-363]
- src/mt7612u/tools/bringup.c[1201-1205]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (2)
13. Negative durations hang the test tool ✓ Resolved 🐞 Bug ☼ Reliability
Description
The ACK gate casts its unchecked signed secs argument to unsigned before converting it to
microseconds. Passing a negative duration such as -1 therefore sleeps for roughly 49 days instead
of rejecting the invocation, while leaving the receive path active.
Code

src/mt7612u/tools/bringup.c[R1044-1045]

+	printf("listening %d s ...\n", secs);
+	mt_usleep((unsigned)secs * 1000000u);
Evidence
The command-line duration is obtained through unchecked atoi() and passed to the gate as an int.
The gate casts it to unsigned before multiplication, and mt_usleep() interprets that wrapped value
as a real duration.

src/mt7612u/tools/bringup.c[1008-1046]
src/mt7612u/tools/bringup.c[1172-1175]
src/mt7612u/usb.c[19-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Parse and range-check the ACK duration before starting device activity or converting it to an unsigned sleep interval.
## Issue Context
The current `atoi()` result is signed, but the sleep helper accepts an unsigned microsecond count.
## Fix Focus Areas
- src/mt7612u/tools/bringup.c[1008-1046]
- src/mt7612u/tools/bringup.c[1172-1175]
- src/mt7612u/usb.c[19-22]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Bringup reports unreliable receive measurements 🐞 Bug ☼ Reliability
Description
arx_cb() increments ctx.n and ctx.by_phy on the async libusb event thread while gate_arx()
and gate_duplex() read those same non-atomic fields before stopping that thread. These
unsynchronized accesses are data races, so the displayed receive rates and the duplex pass/fail
decision can use corrupted or stale counts.
Code

src/mt7612u/tools/bringup.c[R620-625]

+		printf("async RX on ch%u for %d s: %lu frames (%.0f/s), rx_err=%llu\n",
+		       chan, secs, ctx.n, ctx.n / (double)secs,
+		       (unsigned long long)st.rx_err);
+	}
+	for (int i = 0; i < 5; i++)
+		if (ctx.by_phy[i]) printf("  %-6s %lu\n", phy_name[i], ctx.by_phy[i]);
Evidence
The callback writes the counters without synchronization, and both gates read them while their RX
ring remains active. The async implementation runs callbacks from its separate event thread,
establishing the concurrent access.

src/mt7612u/tools/bringup.c[586-628]
src/mt7612u/tools/bringup.c[657-678]
src/mt7612u/async.c[35-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
RX callbacks update harness counters concurrently with main-thread reporting and result checks.
## Issue Context
Protect the callback-owned counters with a mutex or use atomics, then snapshot them consistently for reporting and pass/fail checks. Apply the same mechanism to both asynchronous RX gates.
## Fix Focus Areas
- src/mt7612u/tools/bringup.c[586-628]
- src/mt7612u/tools/bringup.c[657-678]
- src/mt7612u/async.c[35-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/mt7612u/async.c
Comment on lines +213 to +216
ERR("async stop: %d TX and %d RX transfers still in flight after 2 s "
"- leaking the ring rather than freeing memory libusb owns",
stuck_tx, stuck_rx);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Stuck transfers outlive usb teardown 📘 Rule violation ☼ Reliability

mt_async_stop stops the event thread and returns after two seconds even when tx_inflight or
rx_inflight is still nonzero. When cancellation completion is delayed, mt7612u_close proceeds
through mt_close, releasing the interface, handle, and libusb context while libusb still owns the
leaked transfers.
Agent Prompt
## Issue description
Device teardown can close the libusb handle and context while cancelled asynchronous transfers remain in flight.

## Issue Context
`mt_async_stop` abandons outstanding transfers after a two-second deadline, but `mt7612u_close` then continues with normal USB teardown. Keep the event machinery, handle, and context alive until libusb has returned ownership of every submitted transfer.

## Fix Focus Areas
- src/mt7612u/async.c[175-225]
- src/mt7612u/init.c[420-426]
- src/mt7612u/usb.c[256-269]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/mt7612u/internal.h
Comment on lines +216 to +217
#define LOG(...) do { fprintf(stderr, "[mt7612u] " __VA_ARGS__); fputc('\n', stderr); } while (0)
#define ERR(...) do { fprintf(stderr, "[mt7612u] ERROR " __VA_ARGS__); fputc('\n', stderr); } while (0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Concurrent diagnostics can interleave 📘 Rule violation ☼ Reliability

LOG and ERR construct each diagnostic with separate fprintf and fputc calls, omit an
explicit flush, and use a format other than devourer [level] message. When the event thread and a
caller log concurrently, text can be inserted before another message's newline, while buffered
stderr can leave a piped consumer waiting.
Agent Prompt
## Issue description
MT7612U diagnostics do not follow the repository's atomic, flushed stderr logging contract.

## Issue Context
Format each complete human diagnostic as `devourer [level] message`, emit it to stderr with one write, and preserve thread safety and required flushing.

## Fix Focus Areas
- src/mt7612u/internal.h[216-217]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/mt7612u/README.md
Comment on lines +36 to +39
Enabling MAC RX with nothing reading the bulk-IN endpoint wedges this part
*below* the USB level: `libusb_reset_device`, the sysfs `authorized` toggle
and rebinding the kernel driver all fail to recover it, and only a physical
replug does. So `mt_mac_start()` takes the receiver as an explicit argument,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Receiver guidance can drift 📘 Rule violation ⚙ Maintainability

src/mt7612u/README.md repeats the public header's warning that enabling receive without draining
bulk-IN wedges the device and requires a physical replug. If the receiver startup contract or
recovery behavior changes, maintainers must update two authoritative-looking explanations and
readers can follow the stale copy.
Agent Prompt
## Issue description
The README duplicates the receiver startup and recovery contract already documented by the public API header.

## Issue Context
Keep the detailed contract in the authoritative header and replace the README copy with a concise reference to that declaration.

## Fix Focus Areas
- src/mt7612u/README.md[34-41]
- src/mt7612u/include/mt7612u/mt7612u.h[101-107]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/mt7612u/eeprom.c
Comment on lines +117 to +119
if (!is_5ghz)
v >>= 8;
t->vht[0] = t->vht[1] = rate_power_val(v >> 8);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. 2.4 ghz frames use the wrong power 🐞 Bug ≡ Correctness

mt_get_rate_power() shifts the 2.4 GHz VHT EEPROM word twice, so both VHT power entries are always
decoded from zero. Every 2.4 GHz VHT channel setup then programs those incorrect entries into the
transmit-power registers.
Agent Prompt
## Issue description
Correct the 2.4 GHz VHT power-table decoding so the selected EEPROM byte is passed directly to `rate_power_val()` rather than shifted twice.

## Issue Context
The decoded entries are consumed by channel setup and written to the VHT transmit-power registers.

## Fix Focus Areas
- src/mt7612u/eeprom.c[116-120]
- src/mt7612u/phy.c[237-245]
- src/mt7612u/phy.c[273-274]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/mt7612u/radiotap.c Outdated
Comment thread src/mt7612u/tools/bringup.c
Comment thread src/mt7612u/tools/bringup.c
Comment thread src/mt7612u/caps.c
Comment thread src/mt7612u/radiotap.c Outdated
Comment on lines +620 to +625
printf("async RX on ch%u for %d s: %lu frames (%.0f/s), rx_err=%llu\n",
chan, secs, ctx.n, ctx.n / (double)secs,
(unsigned long long)st.rx_err);
}
for (int i = 0; i < 5; i++)
if (ctx.by_phy[i]) printf(" %-6s %lu\n", phy_name[i], ctx.by_phy[i]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

14. Bringup reports unreliable receive measurements 🐞 Bug ☼ Reliability

arx_cb() increments ctx.n and ctx.by_phy on the async libusb event thread while gate_arx()
and gate_duplex() read those same non-atomic fields before stopping that thread. These
unsynchronized accesses are data races, so the displayed receive rates and the duplex pass/fail
decision can use corrupted or stale counts.
Agent Prompt
## Issue description
RX callbacks update harness counters concurrently with main-thread reporting and result checks.

## Issue Context
Protect the callback-owned counters with a mutex or use atomics, then snapshot them consistently for reporting and pass/fail checks. Apply the same mechanism to both asynchronous RX gates.

## Fix Focus Areas
- src/mt7612u/tools/bringup.c[586-628]
- src/mt7612u/tools/bringup.c[657-678]
- src/mt7612u/async.c[35-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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
@snokvist

snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

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. eca657c acts on the rest.

Fixed — six, one of which is on-air correctness

The radiotap HT bandwidth was 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 exact twin of the VHT bandwidth bug you caught in review, in the same function, and I fixed that one without noticing this one. 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 simply cannot read the rest of it.

--check crashed on anything before Python 3.13. Path.read_text(newline=...) is 3.13-only, so the documented verification command and the new ctest cell would TypeError on the 3.12 the CI images ship. This was invisible here precisely because this machine runs 3.13 — a gate that only passes on the author's box.

Plus three in the harness: phy_name[phy & 7] indexed a five-entry array (by_phy[] was already 8 and fine); gate_g 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; and the ACK gate cast an unchecked signed duration to unsigned, making -1 roughly 49 days with the receiver live.

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.

Two that are not defects

The 2.4 GHz VHT power "double shift" is mt76x2_get_rate_power() byte for byte, and rate_power_val takes a u8 on both sides, so the behaviour is identical to upstream's. Those two entries do decode from zero on 2.4 GHz — that is mt76's own handling of a 5 GHz-oriented EEPROM field, for a band where VHT is an extension outside 802.11ac. Changing it would be a silent divergence from the reference this port is checked against, so it is commented instead.

clear_ack_responder() not clearing MT_AUTO_RSP_EN is correct. mt_init_hardware() writes MT_AUTO_RSP_CFG = 0x13 and MT_AUTO_RSP_EN is BIT(0), so the gate is already on before any caller arms a responder — the mt_set() in the arm path is a no-op on it. Clearing it would leave the device in a state its own init never produces; moving the identity off the responder address is what actually stops it answering. Also commented.

Five left open deliberately

Transfer teardown racing libusb ownership after the two-second cancel deadline; mt_wr() discarding failures so a partially applied hardware setup can return success; EEPROM read errors reaching calibration data as 0xff; the harness counter data race between the libusb event thread and the gate; and LOG/ERR not matching the devourer [level] message contract.

These are real. I have not touched them because each is a design change rather than a fix — error propagation through mt_wr in particular reshapes every call site in the backend — and I would rather have your view on the shape than guess and hand you a large diff to unpick. Say which you want and in what form.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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. eca657c acts on the rest.

Fixed — six, one of which is on-air correctness

The radiotap HT bandwidth was 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 exact twin of the VHT bandwidth bug you caught in review, in the same function, and I fixed that one without noticing this one. 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 simply cannot read the rest of it.

--check crashed on anything before Python 3.13. Path.read_text(newline=...) is 3.13-only, so the documented verification command and the new ctest cell would TypeError on the 3.12 the CI images ship. This was invisible here precisely because this machine runs 3.13 — a gate that only passes on the author's box.

Plus three in the harness: phy_name[phy & 7] indexed a five-entry array (by_phy[] was already 8 and fine); gate_g 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; and the ACK gate cast an unchecked signed duration to unsigned, making -1 roughly 49 days with the receiver live.

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.

Two that are not defects

The 2.4 GHz VHT power "double shift" is mt76x2_get_rate_power() byte for byte, and rate_power_val takes a u8 on both sides, so the behaviour is identical to upstream's. Those two entries do decode from zero on 2.4 GHz — that is mt76's own handling of a 5 GHz-oriented EEPROM field, for a band where VHT is an extension outside 802.11ac. Changing it would be a silent divergence from the reference this port is checked against, so it is commented instead.

clear_ack_responder() not clearing MT_AUTO_RSP_EN is correct. mt_init_hardware() writes MT_AUTO_RSP_CFG = 0x13 and MT_AUTO_RSP_EN is BIT(0), so the gate is already on before any caller arms a responder — the mt_set() in the arm path is a no-op on it. Clearing it would leave the device in a state its own init never produces; moving the identity off the responder address is what actually stops it answering. Also commented.

Five left open deliberately

Transfer teardown racing libusb ownership after the two-second cancel deadline; mt_wr() discarding failures so a partially applied hardware setup can return success; EEPROM read errors reaching calibration data as 0xff; the harness counter data race between the libusb event thread and the gate; and LOG/ERR not matching the devourer [level] message contract.

These are real. I have not touched them because each is a design change rather than a fix — error propagation through mt_wr in particular reshapes every call site in the backend — and I would rather have your view on the shape than guess and hand you a large diff to unpick. Say which you want and in what form.

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:

  1. Transfer teardown ownership — fix this first. Keep the conservative leak-on-timeout behavior, but make the lifetime state explicit and ensure no subsequent API path can reuse d->a while libusb may still invoke callbacks. A follow-up can address cancellation timeouts more comprehensively.
  2. mt_wr() error propagation — introduce a checked variant or an operation/status accumulator rather than changing every existing call site at once. Hardware setup should fail if any required write failed, while optional diagnostic writes can remain best-effort.
  3. EEPROM read errors — distinguish read failure from a valid 0xff value and abort calibration-dependent initialization when required EEPROM data is unavailable.
  4. Harness counter synchronization — protect the callback-updated counters, ideally using the existing async statistics/snapshot mechanism or atomics rather than adding ad-hoc locks to each gate.
  5. LOG/ERR formatting — lowest risk and mostly independent; align the backend logging helpers with the documented devourer [level] message contract.

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.

@snokvist

snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Filed #413 for the cross-backend half of this: AdapterCaps declares no maximum frame size for any adapter, and the limit turns out to differ per transport — PCIe already enforces a hard 32 KB bounce ceiling invisibly (PcieTransport.cpp:569), the Realtek USB backends size a heap buffer to the frame so their real limit is undocumented, and MT7612U's RX ceiling is a MAC register.

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 APImt7612u_tx() refused above 2016 bytes while mt7612u_send_packets() bounded only against its 16 KB aggregate buffer, so the same frame was refused by one entry point and aired by the other. Both now share one ceiling.

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: MT_MAX_LEN_CFG low 12 bits are the on-air maximum including FCS, so the 0xf00 this driver programs gives 3836 bytes of MPDU, measured to the byte — 3836 arrives, 3837 does not.

Both are now in mt7612u_caps (max_mpdu_tx / max_mpdu_rx, RX read from the register at runtime rather than hardcoded), bringup caps prints them, and a bringup mtu gate is the sweep that produced them.

One correction worth stating plainly, since it was mine: I added an rx_dropped counter mid-investigation and said it would make the oversize loss visible. It does not, and I mutation-tested rather than trusting it — with the TX ceiling temporarily raised, 60 frames of 6000 bytes MT7612U-to-MT7612U gave 0 received with rx_err, rx_invalid and rx_dropped all zero. The MAC discards oversize frames before USB, so that loss cannot be observed from this layer at all. The counter is real but covers short and malformed transfers only, and now says that instead of claiming the case it cannot see.

Caveats that stand: one witness generation, and 7900 is "at least" — I stopped at the buffer I had, not at a refusal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants