diff --git a/docs/adr/ADR-266-mediatek-filogic-csi-platform.md b/docs/adr/ADR-266-mediatek-filogic-csi-platform.md index 7ad6e4d04..98e6ae2f9 100644 --- a/docs/adr/ADR-266-mediatek-filogic-csi-platform.md +++ b/docs/adr/ADR-266-mediatek-filogic-csi-platform.md @@ -66,10 +66,64 @@ hardware claims. timestamps, sequence behavior, calibration, maximum size, and report rate. - Validate OpenWrt One first, then BPI-R3 4x4, before considering MT7996/MLO. +## Amendment 1 — vendor CSI on MT7981, still unvalidated + +- **Date**: 2026-09-19, recorded here 2026-10-01 +- **Status**: proposed. One board model (two units). The hardware gates below are still open. + +The 2026-07-18 decision said the public `mt76` tree has no supported CSI export, +and that a firmware change might be required before physical CSI exists. Both +statements need a narrower reading. + +Mainline `openwrt/mt76` still has no CSI export. MediaTek's public OpenWrt feed +does: vendor id `0x0ce7`, MCU event `0xc2`, patch +`1001-wifi-mt76-mt7915-csi-implement-csi-support`. On a Wavlink WL-WN586X3 +Rev A (MT7981BA + MT7976CN + MT7531AE) that patch, rebased onto the `mt76` +revision pinned by OpenWrt 24.10.8, produced complex samples without a firmware +blob swap. The `0xc2` hook is already in the MT7981 firmware that the OpenWrt +24.10.8 image carries. The +rebased patch and the two userspace fixes are in `firmware/openwrt-wn586x3/`. + +What the first capture was: one record, 2.4 GHz, 20 MHz, legacy OFDM, 64 +subcarriers, a single chain. CSI arrived only while a client sent frames. +The rate is client traffic, not a sensor sample clock. The capture is not in +this repository. + +What it was not: + +- Not a calibration, a sequence check, a timestamp check, or a repeatability run. + The label on the wire is `mediatek:physical-unvalidated`. Plain `mediatek` stays + reserved for a frame that has passed those gates. +- Not a 4x4 array. Each radio is two chains. The four antennas are a 2.4 GHz pair + and a 5 GHz pair. Two routers are two receivers. Cross-box phase is not a + shared local oscillator, so it is not a beam. +- Not the OpenWrt One or the BPI-R3. Those remain the upstream-friendly targets + in the original decision. The WN586X3 is the board this path was actually run on. + +`wifi-densepose-mtk-bridge` translates `mt76-vendor` JSON, or an MtkCSIdump +datagram, into ADR-267 MTC1 and sends it to the sensing server. The server keeps +one snapshot and a bounded ring of per-subcarrier amplitude and phase per +`device_id`. It runs `mediatek-amplitude-heuristic-v0` on mean and peak amplitude, +and `activity-index-v0` on per-subcarrier fluctuation. Both are `CLAIMED`. Neither +is the ESP32 vital-sign path. + +One measured property supports further work, and nothing more. Offline, on six +local dump captures from two units (2.4 GHz, HT20, chains rx0 and rx1 of transmit +stream 0), the cross-chain phase `arg(H0·conj(H1))`, summed over subcarriers, +moved 0.014-0.039 rad (p50) between adjacent frames for well-sampled +transmitters. Raw per-chain phase moved about 1.56 rad (p50), which is what +uniformly random phase gives (MEASURED; reproducer +`scripts/mediatek-csi-phase-stability.py`). That shows a stable observable inside +one radio. It does not show that the observable tracks people. + +The operator guide is [`docs/mediatek-router-csi.md`](../mediatek-router-csi.md). + ## Links - [ADR-123: BFLD capture path](ADR-123-bfld-capture-path-nexmon-and-esp32.md) - [ADR-264: RTL8720F radar wire protocol](ADR-264-rtl8720f-radar-wire-protocol.md) +- [ADR-267: MediaTek MIMO CSI wire protocol](ADR-267-mediatek-mimo-csi-wire-protocol.md) +- [MediaTek router CSI guide](../mediatek-router-csi.md) - [upstream mt76](https://github.com/openwrt/mt76) - [OpenWrt One](https://openwrt.org/toh/openwrt/one) - [MediaTek OpenWrt feed](https://git01.mediatek.com/openwrt/feeds/mtk-openwrt-feeds/) diff --git a/docs/mediatek-router-csi.md b/docs/mediatek-router-csi.md new file mode 100644 index 000000000..c1bd7fbf3 --- /dev/null +++ b/docs/mediatek-router-csi.md @@ -0,0 +1,361 @@ +# MediaTek Router CSI (MT7981, experimental) + +RuView can take channel state information (CSI) from a MediaTek Filogic +router, such as an MT7981B with an MT7976C radio running OpenWrt with +MediaTek's vendor CSI patch. The router reports CSI. A host-side bridge +converts it to RuView's MTC1 wire format (ADR-267). The sensing server ingests +it like any other UDP source. + +**Status: experimental.** This page documents a working data path, measured +on one board model. It makes no sensing-quality claim. Frames from this path +carry the provenance label `mediatek:physical-unvalidated`. The ADR-266 +hardware gates are still open: calibration, sequence, timestamp, and +repeatability. + +Developed in whitsentry, a RuView-based project. + +Evidence tags used below: **MEASURED** (with a reproducer), **CODE-DERIVED** +(read from source or build scripts, not run here), **CLAIMED** (stated, not +independently verified), **SYNTHETIC** (simulated or fabricated data). + +## Contents + +- [Supported hardware](#supported-hardware) +- [How the path works](#how-the-path-works) +- [Build the OpenWrt image](#build-the-openwrt-image) +- [Arm CSI on the router](#arm-csi-on-the-router) +- [Run the bridge](#run-the-bridge) +- [Start the sensing server](#start-the-sensing-server) +- [What the data is, and is not](#what-the-data-is-and-is-not) +- [Evidence](#evidence) +- [Troubleshooting](#troubleshooting) +- [Reference](#reference) + +## Supported hardware + +| Board | SoC + radio | Status | +|---|---|---| +| Wavlink WL-WN586X3 **Rev A** | MT7981BA + MT7976CN (switch MT7531AE) | Run on two units. CSI captured from both is analysed offline below (MEASURED). Live delivery to the sensing server is CLAIMED from local run logs that are not in the tree. | +| OpenWrt One | MT7981B + MT7976C | CLAIMED: same silicon per the OpenWrt hardware table and ADR-266. Not run with this patch set. | +| Xiaomi AX3000T | MT7981B + MT7976C | CLAIMED: same silicon per the OpenWrt hardware table. Not run with this patch set. | +| Other MT7981/MT7986 boards | — | Untested. The CSI patch targets the `mt7915` driver family, so other Filogic radios may work, but nothing here shows that. | + +The board patch in `firmware/openwrt-wn586x3/` is specific to the WN586X3 +**Rev A**. The CSI driver patch and the two userspace fixes do not depend on +the board. + +Each MT7976C radio has **two receive chains**. The WN586X3's four external +antennas are a 2.4 GHz pair and a 5 GHz pair. It is not a 4x4 array, and two +routers are two independent receivers, not one phased array. + +## How the path works + +``` + router (OpenWrt 24.10.8) host + ┌────────────────────────────────┐ ┌──────────────────────────────┐ + │ mt76 + MediaTek CSI patch │ │ wifi-densepose-mtk-bridge │ + │ firmware MCU event 0xc2 │ JSON dump │ groups chains into frames, │ + │ mt76-vendor dump csi ─────────┼──────────────►│ applies provenance gate, │ + │ CSIdump (UDP, optional) ───────┼──── UDP ─────►│ emits ADR-267 MTC1 │ + └────────────────────────────────┘ └──────────────┬───────────────┘ + │ UDP :5005 + ┌──────────────▼───────────────┐ + │ sensing-server │ + │ --source mediatek │ + │ /api/v1/csi/mediatek/* │ + └──────────────────────────────┘ +``` + +Two transports leave the router: + +- **`mt76-vendor dump csi `** writes a JSON array of + per-chain records: timestamp, transmitter address, RSSI, SNR, bandwidth, + PPDU mode, tx/rx chain index, chain marker, and I/Q. This is the richer + path, and the one that carried sustained use. You copy the file to the host + (for example, a loop over SSH) and replay it through the bridge. +- **`CSIdump`** (MtkCSIdump) streams one antenna per UDP datagram and drops + RSSI, PPDU mode, and transmitter identity. The bridge emits one 1x1 frame per + datagram. This path was verified end to end once, as a recording. It has not + run as a standing service. + +## Build the OpenWrt image + +The patch set lives in [`firmware/openwrt-wn586x3/`](../firmware/openwrt-wn586x3/README.md). +It is not a firmware image. Build and hash your own. + +| Patch | Applies to | +|---|---| +| `1001-mtk-mt76-mt7915-csi-implement-csi-support-REBASED.patch` | `mt76` package (`package/kernel/mt76/patches/`) | +| `mt76-vendor-csi-dump-bounds.patch` | `mt76-vendor` source (`feed/app/mt76-vendor/src/csi.c` in `mediatek/mtk-openwrt-feeds`) | +| `csidump-nl80211-attr-enum.patch` | MtkCSIdump `src/wifi_drv_api/mt76_api.cpp` (only if you use CSIdump) | +| `0001-wn586x3-reva-single-image-sysupgrade-and-generic-spi-nor.patch` | `target/linux/mediatek` (WN586X3 Rev A only) | + +The route that produced the tested image (CODE-DERIVED from its build scripts) +used the stock OpenWrt **24.10.8** `mediatek/filogic` SDK and ImageBuilder in +an x86_64 Linux container: + +1. **SDK.** Unpack `openwrt-sdk-24.10.8-mediatek-filogic`, then run + `./scripts/feeds update -a && ./scripts/feeds install -a`. +2. **CSI driver.** Copy the rebased `1001-...` patch into + `feeds/base/package/kernel/mt76/patches/`. Raise `PKG_RELEASE` in that + package's `Makefile` (the tested build used `99`) so the ImageBuilder + prefers your build over the CDN package of the same version. +3. **mt76-vendor.** Add MediaTek's `mt76-vendor` package from + `mtk-openwrt-feeds` as `package/mt76-vendor`, with the bounds patch applied + to `src/csi.c`. +4. **Configure and compile.** Select `CONFIG_TARGET_mediatek_filogic_DEVICE_wavlink_wl-wn586x3`, + `kmod-mt7915e`, `mt76-vendor`, and `libnl-tiny`. Then run + `make package/feeds/base/mt76/compile package/mt76-vendor/compile`. +5. **Image.** Copy the built `.ipk` files into the ImageBuilder's `packages/` + and run: + ```bash + make image PROFILE=wavlink_wl-wn586x3 \ + PACKAGES="kmod-mt7915e kmod-mt7981-firmware mt7981-wo-firmware mt76-vendor libnl-tiny" + ``` +6. **Check the manifest.** The image's `.manifest` must list `kmod-mt7915e` + at your raised release (for example `-r99`) and `mt76-vendor`. If it lists + the CDN release, the image has no CSI. + +**Rev A board patch.** An ImageBuilder cannot compile a device tree, and its +`make image` reuses the prebuilt DTB and FIT kernel. The tested Rev A build +compiled the patched DTB itself, rebuilt the FIT kernel around it, added the +patched `lib/upgrade/platform.sh` as a rootfs overlay, then checked that the +final sysupgrade image embedded that DTB. A full OpenWrt buildroot applies +`target/` patches directly and avoids those steps. That route was not tested +here. + +Flash only a Rev A unit, keep the stock firmware backup, and follow the +OpenWrt wiki and forum thread for this board. Neither this guide nor the patch +set names a LAN, an account, or a password. + +**Firmware blobs.** The CSI report is MCU event `0xc2`. The MT7981 firmware in +the tested image (`kmod-mt7981-firmware`, `mt7981-wo-firmware` from OpenWrt +24.10.8) already emits it, so no firmware blob swap was needed on the tested +board. Other firmware versions are untested. + +## Arm CSI on the router + +CSI follows traffic from **associated clients**. With nothing associated and +transmitting, there is no CSI. Run these on the router, as root, against the AP +interface (`phy0-ap0` was the 2.4 GHz AP on the tested board). The sequence +below is CODE-DERIVED from the capture scripts used on the tested board: + +```sh +mt76-vendor phy0-ap0 set csi ctrl=0,0,0,0 # disarm and reset +mt76-vendor phy0-ap0 set csi interval=1000 +mt76-vendor phy0-ap0 set csi ctrl=2,3,0,34 # frame-type filter, as used on +mt76-vendor phy0-ap0 set csi ctrl=2,9,1,0 # the tested board (data frames) +mt76-vendor phy0-ap0 set csi ctrl=1,0,0,0 # arm +``` + +Check that the driver is collecting: + +```sh +cat /sys/kernel/debug/ieee80211/phy0/mt76/csi_stats # data_cnt should climb +iw dev phy0-ap0 station dump | grep -E "Station|signal" +``` + +Pull a batch: + +```sh +rm -f /tmp/csi.json # the tool APPENDS; remove first +mt76-vendor phy0-ap0 dump csi 64 /tmp/csi.json +``` + +`dump csi` needs the filename argument. Without it, the tool returns nothing. + +## Run the bridge + +```bash +cd v2 +cargo build --release -p wifi-densepose-mtk-bridge +B=./target/release/wifi-densepose-mtk-bridge +``` + +**Dump path (recommended).** Copy `/tmp/csi.json` to the host, then replay it. +A file cannot prove it came off a radio, so `--replay` refuses to start until +you either attest the hardware or declare the file synthetic: + +```bash +$B --replay csi.json --captured-on / \ + --node --center-freq-khz --sink 127.0.0.1:5005 +``` + +- `--node` is hashed into the 64-bit `device_id`. Give each router its own name. +- `--center-freq-khz` is the AP channel's centre frequency, for example + `2437000` for 2.4 GHz channel 6. +- `--captured-on` takes `/`, for example + `WN586X3-RevA/openwrt-24.10.8-csi`. It is logged at startup. + +For a continuous feed, repeat dump, copy, and replay in a loop, one loop per +router. Keep a copy of each dump only if you need it, and never commit one +(see below). + +**Streaming path (CSIdump).** Start `CSIdump phy0-ap0 8888` on the +router. Then, on the host: + +```bash +$B --listen 0.0.0.0:8888 --csidump :8888 --listen-allow \ + --node --center-freq-khz --sink 127.0.0.1:5005 \ + --record captures/.mtkcap +``` + +`--listen-allow` is mandatory. Datagrams from any other source are dropped and +counted. CSIdump disarms CSI when it receives SIGTERM. The dump path and +CSIdump cannot share the driver's ring, so run one or the other. + +**No router.** The fabricated fixture exercises the whole host path. It must +be replayed with `--synthetic`, and the server labels it `mediatek:simulated`: + +```bash +$B --replay crates/wifi-densepose-mtk-bridge/fixtures/FABRICATED-mt76-vendor-dump-mt7981-bw80.synthetic.json \ + --synthetic --node demo --sink 127.0.0.1:5005 +``` + +The ADR-266 simulator is another synthetic source: + +```bash +cargo run -p wifi-densepose-hardware --bin mediatek-csi-sim -- \ + --profile mt7981 --udp 127.0.0.1:5005 --frames 1000 --realtime +``` + +Full flag reference: [`v2/crates/wifi-densepose-mtk-bridge/README.md`](../v2/crates/wifi-densepose-mtk-bridge/README.md). + +## Start the sensing server + +```bash +cd v2 +cargo run --release -p wifi-densepose-sensing-server -- --source mediatek \ + --activity-state /activity-floor.json +``` + +- `--source mediatek` binds the UDP receiver (loopback by default, ADR-296) and + does **not** start the ESP32-shaped simulator. Before this source existed, + `--source mediatek` bound nothing. +- If the bridge runs on another host, bind a routable address with + `--udp-bind` and name the bridge host in `--udp-allow`. +- `--activity-state` is optional. It persists each device's learned + quiet-room activity floor so that a restart does not relearn it. +- `--csi-ring ` sets frames retained per device for inspection (default + 256, about 0.5 MiB per 2x2 device at 64 subcarriers). + +Endpoints: + +| Route | Returns | +|---|---| +| `GET /api/v1/csi/mediatek/latest` | Last MTC1 snapshot, any device | +| `GET /api/v1/csi/mediatek/devices` | One snapshot per `device_id`, with `age_ms`. Devices silent for over 30 s are omitted. | +| `GET /api/v1/csi/mediatek/devices/:device_id/frames?n=&fields=amp,phase` | Newest `n` frames of per-chain, per-subcarrier amplitude and phase | +| `GET /api/v1/csi/mediatek/devices/:device_id/summary` | Per-chain amplitude mean/std per subcarrier, RSSI per Rx chain, frame rate, heuristic window stats | +| `GET /api/v1/csi/mediatek/activity?seconds=300` | About 1 Hz history of the relative activity index | +| `GET /api/v1/sensing/latest` | Room update. On this source, `classifier` is `mediatek-amplitude-heuristic-v0`. | + +Check the label on every snapshot: + +| `source` | Meaning | +|---|---| +| `mediatek:simulated` | `SYNTHETIC` flag set. Nothing downstream can clear it. | +| `mediatek:physical-unvalidated` | Real silicon, not calibrated. Everything this path produces today. | +| `mediatek` | Real and calibrated. Reserved. Nothing emits it yet. | + +## What the data is, and is not + +**It is** complex channel estimates per receive chain from a real MT7976C +radio, carried end to end with provenance intact. + +**It is not:** + +- **A sensor with its own clock.** The report rate is the clients' traffic + rate. An idle phone gives a few frames per second, and a busy one gives + hundreds. With no associated, transmitting client, there is no data. A + stationary device that keeps transmitting to the AP (an "illuminator", for + example an ESP32 sending a steady packet stream) gives a regular rate + (CLAIMED: the 09-20 and 09-24 captures below include an illuminator's + frames; transmitter identity was not re-checked in this analysis). +- **Calibrated.** `scale` is 1.0 in raw firmware units. Nothing has been + checked against a reference. The ADR-266 gates are open. +- **Validated for presence, motion, or vital signs.** The room output on this + source comes from `mediatek-amplitude-heuristic-v0` (presence and motion from + mean amplitude) and `activity-index-v0` (fluctuation against a learned + floor). Both are **CLAIMED**. Neither has been scored against labelled ground + truth. `activity` blocks carry `is_occupancy_estimate: false`. These outputs + do not feed the validated ESP32 vital-sign pipeline. +- **Coherent across routers.** Each radio has its own oscillator. Cross-chain + phase is meaningful within one radio, not between two boxes. +- **Free of personal data.** A raw dump holds client transmitter addresses + (MACs) and channel samples that encode movement. RuView policy (ADR-299) + forbids committing captures. Keep them out of the repository, and run + `scripts/csi-data-policy-check.sh --staged` before committing near capture + directories. + +## Evidence + +| Claim | Tag | Basis | +|---|---|---| +| A real WL-WN586X3 frame decodes and reaches the server as `mediatek:physical-unvalidated` | MEASURED (path) | First record, 2026-09-19: legacy OFDM, 20 MHz, 64 subcarriers, one chain. Its shape is pinned by `wifi-densepose-mtk-bridge/tests/hardware_capture.rs`, which uses a fabricated record. The capture itself is not in the tree. | +| Cross-chain phase is steady frame to frame on one MT7976C radio, and raw per-chain phase is not | MEASURED (offline) | See below | +| `mediatek-amplitude-heuristic-v0`, `activity-index-v0` | CLAIMED | No labelled evaluation | +| ADR-266 simulator and bridge fixtures | SYNTHETIC | Labelled on the wire | + +**Cross-chain phase stability.** Six local dump captures from two WN586X3 units, +2.4 GHz, 20 MHz HT frames carrying chains (tx0, rx0) and (tx0, rx1), adjacent +frames under 100 ms apart, 56 subcarriers (DC and guard bins excluded). The +two 2026-09-19 captures were analysed whole; the four 09-20 and 09-24 captures +by their first 20 MB each. + +| Quantity, per adjacent-frame step | Well-sampled transmitters (6, at 825 to 14,104 frames each) | +|---|---| +| Raw per-chain phase `arg(H0[k])`, per bin, p50 | 1.557 to 1.601 rad, with 49.6 to 50.9 % of steps over π/2 | +| Cross-chain phase `arg(H0[k]·conj(H1[k]))`, per bin, p50 | 0.023 to 0.056 rad | +| Cross-chain phase summed coherently over bins, p50 / p90 / p99 | 0.014–0.039 / 0.205–0.358 / 0.437–0.820 rad | + +Uniformly random phase gives a step p50 of π/2 = 1.571 rad, and half of all +steps exceed π/2. Raw per-chain phase is indistinguishable from that, as +expected with per-packet CFO and phase offset. In the cross-chain term those +offsets cancel, and the step drops to hundredths of a radian. Steps are +wrapped, so p99 can never exceed π, and is not evidence by itself. + +Transmitters with few, irregular frames (229 to 595 frames) were much worse: +coherent-sum p50 0.177 to 0.456 rad, p99 1.857 to 2.849 rad. So the result +holds only with a steady frame rate. + +This shows a **stable phase observable exists** within one radio. It does +**not** show that the observable tracks people, breathing, or anything else. + +Reproduce it on your own capture (one `dump csi` array per line): + +```bash +python3 scripts/mediatek-csi-phase-stability.py [max_MB] +``` + +The script prints transmitters as `TX-A`, `TX-B`, and so on, never addresses. + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---|---|---| +| `csi_stats` `data_cnt` stays at 0 | No associated client transmitting, or CSI not armed | Associate a client and generate traffic, then re-run the arm sequence | +| `data_cnt` stopped after a client roamed or the filter changed | Changing the CSI filter stops capture | Re-run the arm sequence ending in `ctrl=1,0,0,0`. Re-arming periodically is a reasonable guard. | +| `mt76-vendor dump csi` prints nothing | No filename argument | Use `dump csi ` | +| `mt76-vendor dump csi` segfaults | Unpatched tool: the driver can return n+1 records | Rebuild with `mt76-vendor-csi-dump-bounds.patch` | +| Bridge warns about repeated `(ta, ts, tx_idx, rx_idx)` chains | The dump file was not removed between dumps, and the tool appends | `rm -f` the file before every dump | +| `--replay` refuses to start | Provenance not declared | Add `--captured-on /`, or `--synthetic` for fabricated input | +| `--captured-on` is rejected | The input declares itself synthetic (sidecar, `.synthetic.` name, or capture header) | Expected. Synthetic is sticky. Use `--synthetic`. | +| `--listen` refuses to start | No `--listen-allow` | Name the router's address or subnet | +| `rejected_source` counts climb on `--listen` | The router's address is not in `--listen-allow` | Fix the allowlist | +| CSIdump runs but nothing arrives | Unpatched CSIdump uses an off-by-one netlink attribute enum against this driver | Rebuild with `csidump-nl80211-attr-enum.patch` | +| CSI stopped after CSIdump exited | CSIdump disarms CSI on SIGTERM | Re-run the arm sequence | +| `/api/v1/csi/mediatek/devices` is empty | Server not on `--source mediatek` (or `auto` without UDP), wrong `--sink` port, or a non-loopback bridge not in `--udp-allow` | Check `--source`, port 5005, `--udp-bind`, and `--udp-allow` | +| Two routers show as one device | Same `--node` on both bridges | Give each router its own `--node` (or `--device-id`) | +| A frame shows `ppdu_type: legacy` | Normal. Association and management traffic is pre-HT. | None. ADR-267 Amendment 1 carries these frames rather than dropping them. | +| Room status reads `unknown` for a device | The heuristic abstains below its confidence threshold | Expected with sparse frames. It is not an "absent" call. | +| Room reads `absent` with `confidence: 0.0` | Every device abstained, so there is no evidence either way | Treat confidence 0 as "no information", not as an empty room | +| Image flashes, but there is no `mt76-vendor` or CSI | The ImageBuilder used the CDN `kmod-mt7915e` | Check the manifest for your raised `PKG_RELEASE` | + +## Reference + +- [ADR-266: MediaTek Filogic CSI platform](adr/ADR-266-mediatek-filogic-csi-platform.md) (Amendment 1: vendor CSI on MT7981) +- [ADR-267: MediaTek MIMO CSI wire protocol](adr/ADR-267-mediatek-mimo-csi-wire-protocol.md) (Amendment 1: `Legacy` and `Unknown` PPDU types) +- [`firmware/openwrt-wn586x3/`](../firmware/openwrt-wn586x3/README.md): patches, licences, origins +- [`v2/crates/wifi-densepose-mtk-bridge/`](../v2/crates/wifi-densepose-mtk-bridge/README.md): bridge transports, provenance rules, frame mapping +- [MediaTek OpenWrt feeds](https://git01.mediatek.com/openwrt/feeds/mtk-openwrt-feeds/), [MtkCSIdump](https://github.com/MtkWifiRev/MtkCSIdump), [OpenWrt One](https://openwrt.org/toh/openwrt/one) diff --git a/docs/user-guide.md b/docs/user-guide.md index 4f6f5be00..69324034e 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -21,6 +21,7 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation - [Windows WiFi (RSSI Only)](#windows-wifi-rssi-only) - [ESP32-S3 (Full CSI)](#esp32-s3-full-csi) - [ESP32 Multistatic Mesh (Advanced)](#esp32-multistatic-mesh-advanced) + - [MediaTek Router CSI (Experimental)](#mediatek-router-csi-experimental) - [Connect Mesh Data to the Dashboard and Observatory](#connect-mesh-data-to-the-dashboard-and-observatory) - [Cognitum Spaces activation](#cognitum-spaces-activation) - [Cognitum Seed Integration (ADR-069)](#cognitum-seed-integration-adr-069) @@ -422,6 +423,16 @@ The mesh uses a **Time-Division Multiplexing (TDM)** protocol so nodes take turn See [ADR-029](adr/ADR-029-ruvsense-multistatic-sensing-mode.md) and [ADR-032](adr/ADR-032-multistatic-mesh-security-hardening.md) for the full design. +### MediaTek Router CSI (Experimental) + +An OpenWrt router with an MT7981B + MT7976C radio (tested on a Wavlink WL-WN586X3 Rev A) can report CSI through MediaTek's vendor CSI patch. A host-side bridge converts it to MTC1 (ADR-267) for the sensing server: + +```bash +./target/release/sensing-server --source mediatek --udp-port 5005 --http-port 3000 --ws-port 3001 +``` + +Frames are labelled `mediatek:physical-unvalidated`. The presence and activity outputs on this source are uncalibrated heuristics with no sensing-quality validation. CSI exists only while associated clients transmit. See the [MediaTek Router CSI guide](mediatek-router-csi.md) for the OpenWrt build, arming CSI, the bridge, and limitations. + ### Connect Mesh Data to the Dashboard and Observatory If a standalone `aggregator` command prints live packets, the ESP32 fleet is already reaching that host. To visualize the same data, stop the standalone aggregator and run `sensing-server` on that same host and UDP port. The sensing server is the aggregator used by the REST API, WebSocket stream, dashboard, and Observatory. diff --git a/firmware/openwrt-wn586x3/README.md b/firmware/openwrt-wn586x3/README.md new file mode 100644 index 000000000..cccda51b5 --- /dev/null +++ b/firmware/openwrt-wn586x3/README.md @@ -0,0 +1,78 @@ +# WL-WN586X3 CSI patches + +OpenWrt patches that made a Wavlink WL-WN586X3 Rev A emit CSI into +`wifi-densepose-mtk-bridge`. Chipset on the board that was flashed: MT7981BA, +MT7976CN, MT7531AE. Image base: OpenWrt 24.10.8. This directory is the patch +set. It is not a firmware image, a sysupgrade binary, or a capture. + +Developed in whitsentry, a RuView-based project. The user guide, covering +the build, arming CSI, the bridge, the server, and limitations, is +[`docs/mediatek-router-csi.md`](../../docs/mediatek-router-csi.md). + +Provenance of frames from this path is `mediatek:physical-unvalidated` until +the ADR-266 gates (calibration, sequence, timestamp, repeatability) pass on +this model. Two of these radios are two receivers. They are not one phased array. + +## Files + +| Patch | What it is | License of the base | +|---|---|---| +| `patches/1001-mtk-mt76-mt7915-csi-implement-csi-support-REBASED.patch` | MediaTek's 2022 mt7915 CSI patch, rebased onto `openwrt/mt76` `eb567bc7` (the revision OpenWrt 24.10.8 pins). Two dead-code warnings were removed so `-Werror` builds. | BSD-3-Clause-Clear, MediaTek | +| `patches/0001-wn586x3-reva-single-image-sysupgrade-and-generic-spi-nor.patch` | Board support ported from [dadogroove/openwrt `wl-wn586x3`](https://github.com/dadogroove/openwrt/tree/wl-wn586x3) @ `b080a655` onto the 24.10.8 tag. Single-image sysupgrade, generic SPI-NOR (the Boya flash path is a deletion of quad-SPI width, not a new chip driver), factory MAC offsets. | OpenWrt target patches, GPL-2.0 | +| `patches/mt76-vendor-csi-dump-bounds.patch` | `mt76-vendor dump csi` wrote past its buffer once a dump held more records than requested. The driver seeds its counter and then decrements, so a request for N can return N+1. Also bounds `snprintf` and checks attribute presence. Base: `mediatek/mtk-openwrt-feeds` @ `a15454c8`, `feed/app/mt76-vendor/src/csi.c`. | GPL-2.0, MediaTek | +| `patches/csidump-nl80211-attr-enum.patch` | [MtkCSIdump](https://github.com/MtkWifiRev/MtkCSIdump) @ `276b4b08` was one attribute off from this driver (`STA_INTERVAL`, `DATA_NUM`). The dump callback returned nothing and the ring sat full. | Apache-2.0 (MtkCSIdump `LICENSE.txt`) | + +Do not copy `mt76` itself, the imagebuilder download cache, `out/`, stock +backups, or anything under a `captures/` directory into this repo. + +### Licences + +These files are patches against third-party code. Each one keeps the licence +of the code it modifies, as listed in the table above. They are not covered +by the repository's MIT/Apache-2.0 licence. In particular, the `0001-` board +patch and `mt76-vendor-csi-dump-bounds.patch` modify GPL-2.0 sources, and the +`1001-` CSI patch is MediaTek's BSD-3-Clause-Clear work with its original +author line kept. Nothing here is a firmware blob, a private header, or SDK +code (ADR-266 decision 5). + +## What the radio actually did + +CSI follows client traffic. An idle associated station is a few frames per +second. The vendor dump tool opens its output with `fopen(path, "a+")`, so a +loop that does not remove the file replays every earlier batch. Changing the +CSI filter stops capture until `ctrl=1,0,0,0` is sent again. `CSIdump` disarms +CSI on SIGTERM; an explicit disarm is the right stop, and `kill -9` is how the +lab kept the radio armed, which is a bug in the tool rather than a procedure +to keep. + +`mt76-vendor` JSON is the path that keeps RSSI, SNR, transmit address, bandwidth, +PPDU mode, and per-chain indices. The CSIdump UDP datagram has none of those +and sends antennas one after another, so the bridge emits one 1x1 frame per +datagram. Prefer the dump path when the chain layout matters. Neither stock +tool prints `pkt_sn`, which the firmware event already carries, so the bridge +cannot see loss on an unpatched dumper. + +Each radio is two chains. The four external antennas are a 2.4 GHz pair and a +5 GHz pair. Live CSI in the lab was 2x2 on 2.4 GHz. The 5 GHz radio was idle +for sensing. + +## Build outline + +Apply the rebased CSI patch to the `mt76` tree OpenWrt 24.10.8 builds, and the +board patch to `target/linux/mediatek`. Point the image at the patched +`mt76-vendor` and, if you use it, the patched CSIdump. Flash only a Rev A unit, +and only an image you built and hashed yourself. This README does not name a +LAN, an account, or a password. + +Host side, after the radio is emitting: + +```bash +cd v2 +cargo build -p wifi-densepose-mtk-bridge +./target/debug/wifi-densepose-mtk-bridge --help +``` + +Replay stays on loopback unless you choose otherwise. `--listen` refuses to +start without `--listen-allow`. `--replay` of a file refuses to start until +you pass `--synthetic` or `--captured-on /`. A filename +containing `.synthetic.` cannot be attested as hardware. diff --git a/firmware/openwrt-wn586x3/patches/0001-wn586x3-reva-single-image-sysupgrade-and-generic-spi-nor.patch b/firmware/openwrt-wn586x3/patches/0001-wn586x3-reva-single-image-sysupgrade-and-generic-spi-nor.patch new file mode 100644 index 000000000..6235778ea --- /dev/null +++ b/firmware/openwrt-wn586x3/patches/0001-wn586x3-reva-single-image-sysupgrade-and-generic-spi-nor.patch @@ -0,0 +1,187 @@ +Subject: [PATCH] mediatek: wl-wn586x3 Rev A - single-image sysupgrade, generic SPI-NOR + +Origin: https://github.com/dadogroove/openwrt/tree/wl-wn586x3 + branch wl-wn586x3 @ b080a6550ec6928400735dab4837601efaaf6243 (2026-09-19), + which tracks openwrt/main. Ported here onto the v24.10.8 tag. + Forum: https://forum.openwrt.org/t/wavlink-wl-wn586x3-ver-a-installation-advice/243893 + +Four separable changes, all in target/, none touching package/kernel/mt76, so +this is orthogonal to the CSI driver patch and composes with it. + +1. Single-image sysupgrade (filogic.mk, platform.sh). Replaces the stock + wn586x3 recipe with the append-kernel/append-rootfs/append-metadata form + that wl-wn573hx3 already uses in 24.10.8, adds a `fit lzma` KERNEL, drops + IMAGE_SIZE 15424k to 14336k, marks the firmware partition `denx,fit`, and + adds the board to the default_do_upgrade case list. One sysupgrade.bin then + serves both the initial mtd write and later sysupgrades. This is the + soft-brick fix dadogroove landed 2026-04-25. + +2. Generic SPI-NOR operation (the Boya fix). There is NO Boya BY25Q128AS + driver patch anywhere - the fix is a DELETION. Dropping + spi-tx-bus-width/spi-rx-bus-width = <4> stops the controller driving the + chip in quad mode, so the generic jedec,spi-nor binding is sufficient and + the chip no longer needs to be recognised by name. spi-max-frequency also + goes 25 -> 50 MHz. + +3. MAC addressing. Sources both ethernet MACs from absolute factory offsets + 0x3fff4/0x3fffa instead of the mac-base cell at hw+0x44e, and drops the + wifi mac-address cell so wifi takes its MAC from the EEPROM. Entangled with + (2) only by file, but NOT separable within this patch: the eth nodes and the + cell definitions must move together. This CHANGES the MACs the board + presents versus an official 24.10.8 image. Adopted because dadogroove has + the hardware and his build is the one known to boot on Boya units. + +4. Cosmetic: gpio-keys node renamed button-reset -> button-0; mdio_pins + pinctrl moved onto ð. + +Adaptations made while porting to 24.10.8: + - platform.sh: the case list is ordered differently; wl-wn586x3 was inserted + next to the wl-wn573hx3 entry that is already there. + - &wifi: 24.10.8 takes the EEPROM via `mediatek,mtd-eeprom = <&factory 0x0>`, + not snapshot's eeprom_factory_0 nvmem cell, so only the mac-address cell + was removed. + - filogic.mk hunk landed at a -1824 line offset; content identical. +Verified: no dangling DTS labels. NOT yet verified: DTB compilation, and +nothing has been flashed. +--- +diff --git a/target/linux/mediatek/dts/mt7981b-wavlink-wl-wn586x3.dts b/target/linux/mediatek/dts/mt7981b-wavlink-wl-wn586x3.dts +index 62be0faa4af3..2260df89e9b5 100644 +--- a/target/linux/mediatek/dts/mt7981b-wavlink-wl-wn586x3.dts ++++ b/target/linux/mediatek/dts/mt7981b-wavlink-wl-wn586x3.dts +@@ -24,7 +24,7 @@ + gpio-keys { + compatible = "gpio-keys"; + +- button-reset { ++ button-0 { + label = "reset"; + linux,code = ; + gpios = <&pio 11 GPIO_ACTIVE_LOW>; +@@ -79,13 +79,17 @@ + }; + + ð { ++ ++ pinctrl-names = "default"; ++ pinctrl-0 = <&mdio_pins>; ++ + status = "okay"; + + gmac0: mac@0 { + compatible = "mediatek,eth-mac"; + reg = <0>; + phy-mode = "2500base-x"; +- nvmem-cells = <&macaddr_hw_44e 0>; ++ nvmem-cells = <&macaddr_factory_3fff4>; + nvmem-cell-names = "mac-address"; + + fixed-link { +@@ -100,7 +104,7 @@ + reg = <1>; + phy-mode = "gmii"; + phy-handle = <&int_gbe_phy>; +- nvmem-cells = <&macaddr_hw_44e 1>; ++ nvmem-cells = <&macaddr_factory_3fffa>; + nvmem-cell-names = "mac-address"; + }; + }; +@@ -118,8 +122,6 @@ + }; + + &spi0 { +- pinctrl-names = "default"; +- pinctrl-0 = <&spi0_flash_pins>; + status = "disabled"; + }; + +@@ -135,9 +137,7 @@ + compatible = "jedec,spi-nor"; + reg = <0>; + +- spi-max-frequency = <25000000>; +- spi-tx-bus-width = <4>; +- spi-rx-bus-width = <4>; ++ spi-max-frequency = <50000000>; + + partitions { + compatible = "fixed-partitions"; +@@ -166,10 +166,12 @@ + #address-cells = <1>; + #size-cells = <1>; + +- macaddr_factory_4: macaddr@4 { +- compatible = "mac-base"; +- reg = <0x4 0x6>; +- #nvmem-cell-cells = <1>; ++ macaddr_factory_3fff4: macaddr@3fff4 { ++ reg = <0x3fff4 0x6>; ++ }; ++ ++ macaddr_factory_3fffa: macaddr@3fffa { ++ reg = <0x3fffa 0x6>; + }; + }; + }; +@@ -181,6 +183,7 @@ + }; + + partition@f0000 { ++ compatible = "denx,fit"; + label = "firmware"; + reg = <0x180000 0xe00000>; + }; +@@ -189,17 +192,6 @@ + label = "hw"; + reg = <0xf80000 0x80000>; + +- nvmem-layout { +- compatible = "fixed-layout"; +- #address-cells = <1>; +- #size-cells = <1>; +- +- macaddr_hw_44e: macaddr@44e { +- compatible = "mac-base"; +- reg = <0x44e 0x11>; +- #nvmem-cell-cells = <1>; +- }; +- }; + }; + }; + }; +@@ -265,6 +257,4 @@ + &wifi { + status = "okay"; + mediatek,mtd-eeprom = <&factory 0x0>; +- nvmem-cells = <&macaddr_factory_4 0>; +- nvmem-cell-names = "mac-address"; + }; +diff --git a/target/linux/mediatek/filogic/base-files/lib/upgrade/platform.sh b/target/linux/mediatek/filogic/base-files/lib/upgrade/platform.sh +index 8af61b028926..4a71285bea01 100644 +--- a/target/linux/mediatek/filogic/base-files/lib/upgrade/platform.sh ++++ b/target/linux/mediatek/filogic/base-files/lib/upgrade/platform.sh +@@ -137,6 +137,7 @@ platform_do_upgrade() { + cudy,wr3000-v1|\ + yuncore,ax835|\ + wavlink,wl-wn573hx3|\ ++ wavlink,wl-wn586x3|\ + totolink,x6000r) + default_do_upgrade "$1" + ;; +diff --git a/target/linux/mediatek/image/filogic.mk b/target/linux/mediatek/image/filogic.mk +index b7f8ffd47c51..756dc86b8541 100644 +--- a/target/linux/mediatek/image/filogic.mk ++++ b/target/linux/mediatek/image/filogic.mk +@@ -1910,8 +1910,12 @@ define Device/wavlink_wl-wn586x3 + DEVICE_DTS := mt7981b-wavlink-wl-wn586x3 + DEVICE_DTS_DIR := ../dts + DEVICE_DTS_LOADADDR := 0x47000000 +- IMAGE_SIZE := 15424k + DEVICE_PACKAGES := kmod-mt7915e kmod-mt7981-firmware mt7981-wo-firmware ++ IMAGE_SIZE := 14336k ++ IMAGES := sysupgrade.bin ++ KERNEL := kernel-bin | lzma | \ ++ fit lzma $$(KDIR)/image-$$(firstword $$(DEVICE_DTS)).dtb ++ IMAGE/sysupgrade.bin := append-kernel | pad-to 128k | append-rootfs | pad-rootfs | check-size | append-metadata + endef + TARGET_DEVICES += wavlink_wl-wn586x3 + diff --git a/firmware/openwrt-wn586x3/patches/1001-mtk-mt76-mt7915-csi-implement-csi-support-REBASED.patch b/firmware/openwrt-wn586x3/patches/1001-mtk-mt76-mt7915-csi-implement-csi-support-REBASED.patch new file mode 100644 index 000000000..6d248ec9a --- /dev/null +++ b/firmware/openwrt-wn586x3/patches/1001-mtk-mt76-mt7915-csi-implement-csi-support-REBASED.patch @@ -0,0 +1,1332 @@ +From: Bo Jiao +Date: Mon, 6 Jun 2022 20:13:02 +0800 +Subject: [PATCH] wifi: mt76: mt7915: csi: implement csi support + +Origin: mediatek/mtk-openwrt-feeds @ a15454c888f4f4144c50e33b5feef4f247c5f78b + autobuild/autobuild_5.4_mac80211_release/package/kernel/mt76/patches/ + 1001-wifi-mt76-mt7915-csi-implement-csi-support.patch +Rebased onto openwrt/mt76 eb567bc7f9b692bbf1ddfe31dd740861c58ec85b (2025-11-06), +the tree pinned by OpenWrt 24.10.8. License: BSD-3-Clause-Clear. + +Two dead-code items from the original were dropped so the module builds under +OpenWrt's -Werror: an unused 'reg' local and an unreferenced 'out:' label, both +in mt7915_get_csi_stats. No behaviour change. +--- +diff --git a/mt76_connac_mcu.h b/mt76_connac_mcu.h +index 8d59cf43f0e2..2da666cefb2c 100644 +--- a/mt76_connac_mcu.h ++++ b/mt76_connac_mcu.h +@@ -1046,6 +1046,7 @@ enum { + MCU_EXT_EVENT_BCC_NOTIFY = 0x75, + MCU_EXT_EVENT_WF_RF_PIN_CTRL = 0x9a, + MCU_EXT_EVENT_MURU_CTRL = 0x9f, ++ MCU_EXT_EVENT_CSI_REPORT = 0xc2, + }; + + /* unified event table */ +@@ -1261,6 +1262,7 @@ enum { + MCU_EXT_CMD_GROUP_PRE_CAL_INFO = 0xab, + MCU_EXT_CMD_DPD_PRE_CAL_INFO = 0xac, + MCU_EXT_CMD_PHY_STAT_INFO = 0xad, ++ MCU_EXT_CMD_CSI_CTRL = 0xc2, + MCU_EXT_CMD_WF_RF_PIN_CTRL = 0xbd, + }; + +diff --git a/mt7915/Makefile b/mt7915/Makefile +index 6b0058caafe5..c04e8e7a34fc 100644 +--- a/mt7915/Makefile ++++ b/mt7915/Makefile +@@ -1,10 +1,10 @@ + # SPDX-License-Identifier: BSD-3-Clause-Clear + +-EXTRA_CFLAGS += -DCONFIG_MT76_LEDS ++EXTRA_CFLAGS += -DCONFIG_MT76_LEDS -DCONFIG_MTK_VENDOR + obj-$(CONFIG_MT7915E) += mt7915e.o + + mt7915e-y := pci.o init.o dma.o eeprom.o main.o mcu.o mac.o \ +- debugfs.o mmio.o ++ debugfs.o mmio.o vendor.o + + mt7915e-$(CONFIG_NL80211_TESTMODE) += testmode.o + mt7915e-$(CONFIG_MT798X_WMAC) += soc.o +diff --git a/mt7915/debugfs.c b/mt7915/debugfs.c +index e80d15483d6d..2b427b3ee622 100644 +--- a/mt7915/debugfs.c ++++ b/mt7915/debugfs.c +@@ -1289,6 +1289,50 @@ mt7915_rf_regval_set(void *data, u64 val) + DEFINE_DEBUGFS_ATTRIBUTE(fops_rf_regval, mt7915_rf_regval_get, + mt7915_rf_regval_set, "0x%08llx\n"); + ++#ifdef CONFIG_MTK_VENDOR ++static ssize_t ++mt7915_get_csi_stats(struct file *file, char __user *user_buf, ++ size_t count, loff_t *ppos) ++ ++{ ++ struct mt7915_phy *phy = file->private_data; ++ struct csi_mac_filter *current_mac, *tmp_mac; ++ static const size_t sz = 4096; ++ char *buf; ++ u32 len = 0; ++ int ret; ++ ++ buf = kzalloc(sz, GFP_KERNEL); ++ if (!buf) ++ return -ENOMEM; ++ ++ len += scnprintf(buf + len, sz - len, "CSI enable: %d\n", phy->csi.enable); ++ ++ if (phy->csi.enable) { ++ len += scnprintf(buf + len, sz - len, "CSI data_cnt: %d\n", phy->csi.count); ++ ++ mutex_lock(&phy->csi.mac_filter_lock); ++ ++ list_for_each_entry_safe(current_mac, tmp_mac, &phy->csi.mac_filter_list, node) { ++ len += scnprintf(buf + len, sz - len, "mac: %pM, interval: %d\n", current_mac->mac, current_mac->interval); ++ } ++ ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ } ++ ++ ret = simple_read_from_buffer(user_buf, count, ppos, buf, len); ++ ++ kfree(buf); ++ return ret; ++} ++ ++static const struct file_operations mt7915_csi_ops = { ++ .read = mt7915_get_csi_stats, ++ .open = simple_open, ++ .llseek = default_llseek, ++}; ++#endif ++ + int mt7915_init_debugfs(struct mt7915_phy *phy) + { + struct mt7915_dev *dev = phy->dev; +@@ -1333,6 +1377,9 @@ int mt7915_init_debugfs(struct mt7915_phy *phy) + debugfs_create_devm_seqfile(dev->mt76.dev, "rdd_monitor", dir, + mt7915_rdd_monitor); + } ++#ifdef CONFIG_MTK_VENDOR ++ debugfs_create_file("csi_stats", 0400, dir, phy, &mt7915_csi_ops); ++#endif + + if (!ext_phy) + dev->debugfs_dir = dir; +diff --git a/mt7915/init.c b/mt7915/init.c +index 2da6ea91bfc6..bcf0f47f9078 100644 +--- a/mt7915/init.c ++++ b/mt7915/init.c +@@ -719,6 +719,14 @@ mt7915_register_ext_phy(struct mt7915_dev *dev, struct mt7915_phy *phy) + /* init wiphy according to mphy and phy */ + mt7915_init_wiphy(phy); + ++#ifdef CONFIG_MTK_VENDOR ++ INIT_LIST_HEAD(&phy->csi.data_list); ++ spin_lock_init(&phy->csi.data_lock); ++ INIT_LIST_HEAD(&phy->csi.mac_filter_list); ++ mutex_init(&phy->csi.mac_filter_lock); ++ mt7915_vendor_register(phy); ++#endif ++ + ret = mt76_register_phy(mphy, true, mt76_rates, + ARRAY_SIZE(mt76_rates)); + if (ret) +@@ -1196,6 +1204,28 @@ void mt7915_set_stream_he_caps(struct mt7915_phy *phy) + } + } + ++#ifdef CONFIG_MTK_VENDOR ++static int mt7915_unregister_features(struct mt7915_phy *phy) ++{ ++ struct csi_data *c, *tmp_c; ++ ++ spin_lock_bh(&phy->csi.data_lock); ++ phy->csi.enable = 0; ++ ++ list_for_each_entry_safe(c, tmp_c, &phy->csi.data_list, node) { ++ list_del(&c->node); ++ kfree(c); ++ } ++ spin_unlock_bh(&phy->csi.data_lock); ++ ++ mutex_lock(&phy->csi.mac_filter_lock); ++ mt7915_csi_mac_filter_clear(phy); ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ ++ return 0; ++} ++#endif ++ + static void mt7915_unregister_ext_phy(struct mt7915_dev *dev) + { + struct mt7915_phy *phy = mt7915_ext_phy(dev); +@@ -1204,6 +1234,10 @@ static void mt7915_unregister_ext_phy(struct mt7915_dev *dev) + if (!phy) + return; + ++#ifdef CONFIG_MTK_VENDOR ++ mt7915_unregister_features(phy); ++#endif ++ + mt7915_unregister_thermal(phy); + mt76_unregister_phy(mphy); + ieee80211_free_hw(mphy->hw); +@@ -1216,6 +1250,10 @@ static void mt7915_stop_hardware(struct mt7915_dev *dev) + mt7915_dma_cleanup(dev); + tasklet_disable(&dev->mt76.irq_tasklet); + ++#ifdef CONFIG_MTK_VENDOR ++ mt7915_unregister_features(&dev->phy); ++#endif ++ + if (is_mt798x(&dev->mt76)) + mt7986_wmac_disable(dev); + } +@@ -1254,6 +1292,14 @@ int mt7915_register_device(struct mt7915_dev *dev) + dev->mt76.test_ops = &mt7915_testmode_ops; + #endif + ++#ifdef CONFIG_MTK_VENDOR ++ INIT_LIST_HEAD(&dev->phy.csi.data_list); ++ spin_lock_init(&dev->phy.csi.data_lock); ++ INIT_LIST_HEAD(&dev->phy.csi.mac_filter_list); ++ mutex_init(&dev->phy.csi.mac_filter_lock); ++ mt7915_vendor_register(&dev->phy); ++#endif ++ + ret = mt76_register_device(&dev->mt76, true, mt76_rates, + ARRAY_SIZE(mt76_rates)); + if (ret) +diff --git a/mt7915/main.c b/mt7915/main.c +index aa1f7ac38595..5d9cb8131776 100644 +--- a/mt7915/main.c ++++ b/mt7915/main.c +@@ -874,6 +874,20 @@ void mt7915_mac_sta_remove(struct mt76_dev *mdev, struct ieee80211_vif *vif, + { + struct mt7915_dev *dev = container_of(mdev, struct mt7915_dev, mt76); + struct mt7915_sta *msta = (struct mt7915_sta *)sta->drv_priv; ++#ifdef CONFIG_MTK_VENDOR ++ struct mt7915_phy *phy = msta->vif->phy; ++ struct csi_mac_filter *ent; ++ ++ mutex_lock(&phy->csi.mac_filter_lock); ++ ent = mt7915_csi_mac_filter_find(phy, sta->addr); ++ if (ent && !mt7915_mcu_set_csi(phy, 2, 8, 1, 0, sta->addr, 0)) { ++ list_del(&ent->node); ++ kfree(ent); ++ phy->csi.mac_filter_cnt--; ++ } ++ ++ mutex_unlock(&phy->csi.mac_filter_lock); ++#endif + + mt7915_mac_wtbl_update(dev, msta->wcid.idx, + MT_WTBL_UPDATE_ADM_COUNT_CLEAR); +diff --git a/mt7915/mcu.c b/mt7915/mcu.c +index 40364eeaa255..479801469ec9 100644 +--- a/mt7915/mcu.c ++++ b/mt7915/mcu.c +@@ -39,6 +39,10 @@ static bool sr_scene_detect = true; + module_param(sr_scene_detect, bool, 0644); + MODULE_PARM_DESC(sr_scene_detect, "Enable firmware scene detection algorithm"); + ++#ifdef CONFIG_MTK_VENDOR ++static int mt7915_mcu_report_csi(struct mt7915_dev *dev, struct sk_buff *skb); ++#endif ++ + static u8 + mt7915_mcu_get_sta_nss(u16 mcs_map) + { +@@ -421,6 +425,11 @@ mt7915_mcu_rx_ext_event(struct mt7915_dev *dev, struct sk_buff *skb) + case MCU_EXT_EVENT_FW_LOG_2_HOST: + mt7915_mcu_rx_log_message(dev, skb); + break; ++#ifdef CONFIG_MTK_VENDOR ++ case MCU_EXT_EVENT_CSI_REPORT: ++ mt7915_mcu_report_csi(dev, skb); ++ break; ++#endif + case MCU_EXT_EVENT_BCC_NOTIFY: + mt7915_mcu_rx_bcc_notify(dev, skb); + break; +@@ -4139,3 +4148,197 @@ int mt7915_mcu_rf_regval(struct mt7915_dev *dev, u32 regidx, u32 *val, bool set) + + return 0; + } ++ ++#ifdef CONFIG_MTK_VENDOR ++int mt7915_mcu_set_csi(struct mt7915_phy *phy, u8 mode, ++ u8 cfg, u8 v1, u32 v2, u8 *mac_addr, u32 sta_interval) ++{ ++ struct mt7915_dev *dev = phy->dev; ++ struct mt7915_mcu_csi req = { ++ .band = phy != &dev->phy, ++ .mode = mode, ++ .cfg = cfg, ++ .v1 = v1, ++ .v2 = cpu_to_le32(v2), ++ }; ++ ++ if (is_valid_ether_addr(mac_addr)) { ++ ether_addr_copy(req.mac_addr, mac_addr); ++ ++ if ((req.v2 == ADD_CSI_MAC) && sta_interval) ++ req.sta_interval = sta_interval; ++ } ++ ++ return mt76_mcu_send_msg(&dev->mt76, MCU_EXT_CMD(CSI_CTRL), &req, ++ sizeof(req), true); ++} ++ ++static int csi_integret_segment_data(struct mt7915_phy *phy, struct csi_data *csi) ++{ ++ struct csi_data *csi_temp = NULL; ++ ++ if (csi->segment_num == 0 && ++ csi->remain_last == 0) ++ return CSI_CHAIN_COMPLETE; ++ else if (csi->segment_num == 0 && ++ csi->remain_last == 1) { ++ memcpy(&phy->csi.buffered_csi, ++ csi, sizeof(struct csi_data)); ++ ++ return CSI_CHAIN_SEGMENT_FIRST; ++ } else if (csi->segment_num != 0) { ++ csi_temp = &phy->csi.buffered_csi; ++ if (csi->chain_info != ++ csi_temp->chain_info || ++ csi->segment_num != ++ (csi_temp->segment_num + 1)) ++ return CSI_CHAIN_SEGMENT_ERR; ++ ++ memcpy(&csi_temp->data_i[csi_temp->data_num], ++ csi->data_i, csi->data_num * sizeof(s16)); ++ ++ memcpy(&csi_temp->data_q[csi_temp->data_num], ++ csi->data_q, csi->data_num * sizeof(s16)); ++ ++ csi_temp->data_num += csi->data_num; ++ csi_temp->segment_num = csi->segment_num; ++ csi_temp->remain_last = csi->remain_last; ++ ++ if (csi->remain_last == 0) ++ return CSI_CHAIN_SEGMENT_LAST; ++ else if (csi->remain_last == 1) ++ return CSI_CHAIN_SEGMENT_MIDDLE; ++ } ++ ++ return CSI_CHAIN_ERR; ++} ++ ++static int ++mt7915_mcu_report_csi(struct mt7915_dev *dev, struct sk_buff *skb) ++{ ++ struct mt76_connac2_mcu_rxd *rxd = (struct mt76_connac2_mcu_rxd *)skb->data; ++ struct mt7915_phy *phy = &dev->phy; ++ int len, i; ++ struct mt7915_mcu_csi_report *cr; ++ int ret; ++ struct csi_data *current_csi = NULL; ++ struct csi_data *target_csi = NULL; ++ ++ skb_pull(skb, sizeof(struct mt76_connac2_mcu_rxd)); ++ ++ len = le16_to_cpu(rxd->len) - sizeof(struct mt76_connac2_mcu_rxd) + 24; ++ ++ cr = (struct mt7915_mcu_csi_report *)skb->data; ++ ++ if (phy->csi.interval && ++ le32_to_cpu(cr->ts) < phy->csi.last_record + phy->csi.interval) ++ return 0; ++ ++ current_csi = kzalloc(sizeof(*current_csi), GFP_KERNEL); ++ ++ if (!current_csi) ++ return -ENOMEM; ++ ++ memset(current_csi, 0, sizeof(struct csi_data)); ++ ++#define SET_CSI_DATA(_field) (current_csi->_field = le32_to_cpu((cr->_field))) ++ SET_CSI_DATA(ch_bw); ++ SET_CSI_DATA(rssi); ++ SET_CSI_DATA(snr); ++ SET_CSI_DATA(data_num); ++ SET_CSI_DATA(data_bw); ++ SET_CSI_DATA(pri_ch_idx); ++ SET_CSI_DATA(ext_info); ++ SET_CSI_DATA(rx_mode); ++ SET_CSI_DATA(chain_info); ++ SET_CSI_DATA(ts); ++ ++ if (is_mt798x(&dev->mt76) || is_mt7916(&dev->mt76)) { ++ SET_CSI_DATA(segment_num); ++ SET_CSI_DATA(remain_last); ++ SET_CSI_DATA(pkt_sn); ++ SET_CSI_DATA(tr_stream); ++ } ++ ++ SET_CSI_DATA(band); ++ if (current_csi->band && !phy->mt76->band_idx) ++ phy = mt7915_ext_phy(dev); ++#undef SET_CSI_DATA ++ ++ switch (current_csi->ch_bw) { ++ case CSI_BW20: ++ if (is_mt798x(&dev->mt76) || is_mt7916(&dev->mt76)) ++ current_csi->data_num = CSI_BW20_DATA_COUNT; ++ break; ++ case CSI_BW40: ++ if (is_mt798x(&dev->mt76) || is_mt7916(&dev->mt76)) ++ current_csi->data_num = CSI_BW40_DATA_COUNT; ++ break; ++ default: ++ break; ++ } ++ ++ for (i = 0; i < current_csi->data_num; i++) { ++ current_csi->data_i[i] = le16_to_cpu(cr->data_i[i]); ++ current_csi->data_q[i] = le16_to_cpu(cr->data_q[i]); ++ } ++ ++ memcpy(current_csi->ta, cr->ta, ETH_ALEN); ++ current_csi->tx_idx = le32_get_bits(cr->trx_idx, GENMASK(31, 16)); ++ current_csi->rx_idx = le32_get_bits(cr->trx_idx, GENMASK(15, 0)); ++ ++ /* integret the bw80 segment */ ++ if ((is_mt798x(&dev->mt76) || is_mt7916(&dev->mt76)) && current_csi->ch_bw >= CSI_BW80) { ++ ret = csi_integret_segment_data(phy, current_csi); ++ ++ /* event data error or event drop */ ++ if (ret == CSI_CHAIN_ERR || ret == CSI_CHAIN_SEGMENT_ERR) { ++ kfree(current_csi); ++ return -EINVAL; ++ } ++ ++ if (ret == CSI_CHAIN_SEGMENT_FIRST || ret == CSI_CHAIN_SEGMENT_MIDDLE) { ++ kfree(current_csi); ++ return 0; ++ } else if (ret == CSI_CHAIN_COMPLETE) { ++ target_csi = current_csi; ++ } else if (ret == CSI_CHAIN_SEGMENT_LAST) { ++ target_csi = current_csi; ++ memcpy(target_csi, &phy->csi.buffered_csi, sizeof(struct csi_data)); ++ memset(&phy->csi.buffered_csi, 0, sizeof(struct csi_data)); ++ } ++ } else { ++ target_csi = current_csi; ++ } ++ ++ /* put the csi data into list */ ++ INIT_LIST_HEAD(&target_csi->node); ++ spin_lock_bh(&phy->csi.data_lock); ++ ++ if (!phy->csi.enable) { ++ kfree(target_csi); ++ spin_unlock_bh(&phy->csi.data_lock); ++ return 0; ++ } ++ ++ list_add_tail(&target_csi->node, &phy->csi.data_list); ++ phy->csi.count++; ++ ++ if (phy->csi.count > CSI_MAX_BUF_NUM) { ++ struct csi_data *old; ++ ++ old = list_first_entry(&phy->csi.data_list, ++ struct csi_data, node); ++ ++ list_del(&old->node); ++ kfree(old); ++ phy->csi.count--; ++ } ++ ++ if (target_csi->chain_info & BIT(15)) /* last chain */ ++ phy->csi.last_record = target_csi->ts; ++ spin_unlock_bh(&phy->csi.data_lock); ++ ++ return 0; ++} ++#endif +diff --git a/mt7915/mcu.h b/mt7915/mcu.h +index 3be105bc28de..c37530b5c4ae 100644 +--- a/mt7915/mcu.h ++++ b/mt7915/mcu.h +@@ -522,4 +522,78 @@ enum { + sizeof(struct bss_info_bmc_rate) +\ + sizeof(struct bss_info_ext_bss)) + ++ ++#ifdef CONFIG_MTK_VENDOR ++struct mt7915_mcu_csi { ++ u8 band; ++ u8 mode; ++ u8 cfg; ++ u8 v1; ++ __le32 v2; ++ u8 mac_addr[ETH_ALEN]; ++ u8 _rsv1[2]; ++ u32 sta_interval; ++ u8 _rsv2[28]; ++} __packed; ++ ++struct csi_tlv { ++ __le32 tag; ++ __le32 len; ++} __packed; ++ ++#define CSI_MAX_BUF_NUM 3000 ++ ++struct mt7915_mcu_csi_report { ++ struct csi_tlv _t0; ++ __le32 ver; ++ struct csi_tlv _t1; ++ __le32 ch_bw; ++ struct csi_tlv _t2; ++ __le32 rssi; ++ struct csi_tlv _t3; ++ __le32 snr; ++ struct csi_tlv _t4; ++ __le32 band; ++ struct csi_tlv _t5; ++ __le32 data_num; ++ struct csi_tlv _t6; ++ __le16 data_i[CSI_BW80_DATA_COUNT]; ++ struct csi_tlv _t7; ++ __le16 data_q[CSI_BW80_DATA_COUNT]; ++ struct csi_tlv _t8; ++ __le32 data_bw; ++ struct csi_tlv _t9; ++ __le32 pri_ch_idx; ++ struct csi_tlv _t10; ++ u8 ta[8]; ++ struct csi_tlv _t11; ++ __le32 ext_info; ++ struct csi_tlv _t12; ++ __le32 rx_mode; ++ struct csi_tlv _t17; ++ __le32 chain_info; ++ struct csi_tlv _t18; ++ __le32 trx_idx; ++ struct csi_tlv _t19; ++ __le32 ts; ++ struct csi_tlv _t20; ++ __le32 pkt_sn; ++ struct csi_tlv _t21; ++ __le32 segment_num; ++ struct csi_tlv _t22; ++ __le32 remain_last; ++ struct csi_tlv _t23; ++ __le32 tr_stream; ++} __packed; ++ ++enum CSI_CHAIN_TYPE { ++ CSI_CHAIN_ERR, ++ CSI_CHAIN_COMPLETE, ++ CSI_CHAIN_SEGMENT_FIRST, ++ CSI_CHAIN_SEGMENT_MIDDLE, ++ CSI_CHAIN_SEGMENT_LAST, ++ CSI_CHAIN_SEGMENT_ERR, ++}; ++#endif ++ + #endif +diff --git a/mt7915/mt7915.h b/mt7915/mt7915.h +index 7d98a42d3a88..b2e3811e4992 100644 +--- a/mt7915/mt7915.h ++++ b/mt7915/mt7915.h +@@ -195,6 +195,57 @@ struct mt7915_hif { + u32 index; + }; + ++#ifdef CONFIG_MTK_VENDOR ++enum csi_bw { ++ CSI_BW20, ++ CSI_BW40, ++ CSI_BW80, ++ CSI_BW160 ++}; ++ ++#define CSI_BW20_DATA_COUNT 64 ++#define CSI_BW40_DATA_COUNT 128 ++#define CSI_BW80_DATA_COUNT 256 ++#define CSI_BW160_DATA_COUNT 512 ++ ++struct csi_data { ++ u8 ch_bw; ++ u16 data_num; ++ s16 data_i[CSI_BW160_DATA_COUNT]; ++ s16 data_q[CSI_BW160_DATA_COUNT]; ++ u8 band; ++ s8 rssi; ++ u8 snr; ++ u32 ts; ++ u8 data_bw; ++ u8 pri_ch_idx; ++ u8 ta[ETH_ALEN]; ++ u32 ext_info; ++ u8 rx_mode; ++ u32 chain_info; ++ u16 tx_idx; ++ u16 rx_idx; ++ u32 segment_num; ++ u8 remain_last; ++ u16 pkt_sn; ++ u8 tr_stream; ++ ++ struct list_head node; ++}; ++struct csi_mac_filter { ++ struct list_head node; ++ ++ u8 mac[ETH_ALEN]; ++ u32 interval; ++}; ++ ++#define DEL_CSI_MAC 0 ++#define ADD_CSI_MAC 1 ++#define SHOW_CSI_MAC 2 ++ ++#define MAX_CSI_MAC_NUM 10 ++#endif ++ + struct mt7915_phy { + struct mt76_phy *mt76; + struct mt7915_dev *dev; +@@ -240,6 +291,25 @@ struct mt7915_phy { + u8 spe_idx; + } test; + #endif ++ ++#ifdef CONFIG_MTK_VENDOR ++ struct { ++ struct list_head data_list; ++ spinlock_t data_lock; ++ u32 count; ++ bool mask; ++ bool reorder; ++ bool enable; ++ ++ struct mutex mac_filter_lock; ++ struct list_head mac_filter_list; ++ u8 mac_filter_cnt; ++ ++ struct csi_data buffered_csi; ++ u32 interval; ++ u32 last_record; ++ } csi; ++#endif + }; + + struct mt7915_dev { +@@ -628,4 +698,12 @@ void mt7915_sta_add_debugfs(struct ieee80211_hw *hw, struct ieee80211_vif *vif, + int mt7915_mmio_wed_init(struct mt7915_dev *dev, void *pdev_ptr, + bool pci, int *irq); + ++#ifdef CONFIG_MTK_VENDOR ++void mt7915_vendor_register(struct mt7915_phy *phy); ++int mt7915_mcu_set_csi(struct mt7915_phy *phy, u8 mode, ++ u8 cfg, u8 v1, u32 v2, u8 *mac_addr, u32 sta_interval); ++struct csi_mac_filter *mt7915_csi_mac_filter_find(struct mt7915_phy *phy, u8 *addr); ++void mt7915_csi_mac_filter_clear(struct mt7915_phy *phy); ++#endif ++ + #endif +diff --git a/mt7915/vendor.c b/mt7915/vendor.c +new file mode 100644 +index 000000000000..74abb9e95f2a +--- /dev/null ++++ b/mt7915/vendor.c +@@ -0,0 +1,606 @@ ++// SPDX-License-Identifier: BSD-3-Clause-Clear ++/* ++ * Copyright (C) 2020, MediaTek Inc. All rights reserved. ++ */ ++ ++#include ++ ++#include "mt7915.h" ++#include "mcu.h" ++#include "vendor.h" ++ ++static const struct nla_policy ++csi_ctrl_policy[NUM_MTK_VENDOR_ATTRS_CSI_CTRL] = { ++ [MTK_VENDOR_ATTR_CSI_CTRL_CFG] = {.type = NLA_NESTED }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_CFG_MODE] = { .type = NLA_U8 }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_CFG_TYPE] = { .type = NLA_U8 }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_CFG_VAL1] = { .type = NLA_U8 }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_CFG_VAL2] = { .type = NLA_U8 }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_MAC_ADDR] = { .type = NLA_NESTED }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_INTERVAL] = { .type = NLA_U32 }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_STA_INTERVAL] = { .type = NLA_U32 }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_DUMP_NUM] = { .type = NLA_U16 }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_DATA] = { .type = NLA_NESTED }, ++ [MTK_VENDOR_ATTR_CSI_CTRL_DUMP_MAC_FILTER] = { .type = NLA_NESTED }, ++}; ++ ++struct csi_null_tone { ++ u8 start; ++ u8 end; ++}; ++ ++struct csi_reorder { ++ u8 dest; ++ u8 start; ++ u8 end; ++}; ++ ++struct csi_mask { ++ struct csi_null_tone null[10]; ++ u8 pilot[8]; ++ struct csi_reorder ro[3]; ++}; ++ ++static const struct csi_mask csi_mask_groups[] = { ++ /* OFDM */ ++ { .null = { { 0 }, { 27, 37 } }, ++ .ro = { {0, 0, 63} }, ++ }, ++ { .null = { { 0, 69 }, { 96 }, { 123, 127 } }, ++ .ro = { { 0, 96 }, { 38, 70, 95 }, { 1, 97, 122 } }, ++ }, ++ { .null = { { 0, 5 }, { 32 }, { 59, 127 } }, ++ .ro = { { 0, 32 }, { 38, 6, 31 }, { 1, 33, 58 } }, ++ }, ++ { .null = { { 0, 5 }, { 32 }, { 59, 69 }, { 96 }, { 123, 127 } }, ++ .ro = { { 0, 0, 127 } }, ++ }, ++ { .null = { { 0, 133 }, { 160 }, { 187, 255 } }, ++ .ro = { { 0, 160 }, { 1, 161, 186 }, { 38, 134, 159 } }, ++ }, ++ { .null = { { 0, 197 }, { 224 }, { 251, 255 } }, ++ .ro = { { 0, 224 }, { 1, 225, 250 }, { 38, 198, 223 } }, ++ }, ++ { .null = { { 0, 5 }, { 32 }, { 59, 255 } }, ++ .ro = { { 0, 32 }, { 1, 33, 58 }, { 38, 6, 31 } }, ++ }, ++ { .null = { { 0, 69 }, { 96 }, { 123, 255 } }, ++ .ro = { { 0, 96 }, { 1, 97, 122 }, { 38, 70, 95 } }, ++ }, ++ { .null = { { 0, 133 }, { 160 }, { 187, 197 }, { 224 }, { 251, 255 } }, ++ .ro = { { 0, 192 }, { 2, 198, 250 }, { 74, 134, 186 } }, ++ }, ++ { .null = { { 0, 5 }, { 32 }, { 59, 69 }, { 96 }, { 123, 255 } }, ++ .ro = { { 0, 64 }, { 2, 70, 122 }, { 74, 6, 58 } }, ++ }, ++ { .null = { { 0, 5 }, { 32 }, { 59, 69 }, { 96 }, { 123, 133 }, ++ { 160 }, { 187, 197 }, { 224 }, { 251, 255 } }, ++ .ro = { { 0, 0, 255 } }, ++ }, ++ ++ /* HT/VHT */ ++ { .null = { { 0 }, { 29, 35 } }, ++ .pilot = { 7, 21, 43, 57 }, ++ .ro = { { 0, 0, 63 } }, ++ }, ++ { .null = { { 0, 67 }, { 96 }, { 125, 127 } }, ++ .pilot = { 75, 89, 103, 117 }, ++ .ro = { { 0, 96 }, { 36, 68, 95 }, { 1, 97, 124 } }, ++ }, ++ { .null = { { 0, 3 }, { 32 }, { 61, 127 } }, ++ .pilot = { 11, 25, 39, 53 }, ++ .ro = { { 0, 32 }, { 36, 4, 31 }, { 1, 33, 60 } }, ++ }, ++ { .null = { { 0, 1 }, { 59, 69 }, { 127 } }, ++ .pilot = { 11, 25, 53, 75, 103, 117 }, ++ .ro = { { 0, 0, 127 } }, ++ }, ++ { .null = { { 0, 131 }, { 160 }, { 189, 255 } }, ++ .pilot = { 139, 153, 167, 181 }, ++ .ro = { { 0, 160 }, { 1, 161, 188 }, { 36, 132, 159 } }, ++ }, ++ { .null = { { 0, 195 }, { 224 }, { 253 }, { 255 } }, ++ .pilot = { 203, 217, 231, 245 }, ++ .ro = { { 0, 224 }, { 1, 225, 252 }, { 36, 196, 223 } }, ++ }, ++ { .null = { { 0, 3 }, { 32 }, { 61, 255 } }, ++ .pilot = { 11, 25, 39, 53 }, ++ .ro = { { 0, 32 }, { 1, 33, 60 }, { 36, 4, 31 } }, ++ }, ++ { .null = { { 0, 67 }, { 96 }, { 125, 255 } }, ++ .pilot = { 75, 89, 103, 117 }, ++ .ro = { { 0, 96 }, { 1, 97, 124 }, { 36, 68, 95 } }, ++ }, ++ { .null = { { 0, 133 }, { 191, 193 }, { 251, 255 } }, ++ .pilot = { 139, 167, 181, 203, 217, 245 }, ++ .ro = { { 0, 192 }, { 2, 194, 250 }, { 70, 134, 190 } }, ++ }, ++ { .null = { { 0, 5 }, { 63, 65 }, { 123, 127 } }, ++ .pilot = { 11, 39, 53, 75, 89, 117 }, ++ .ro = { { 0, 64 }, { 2, 66, 122 }, { 70, 6, 62 } }, ++ }, ++ { .null = { { 0, 1 }, { 123, 133 }, { 255 } }, ++ .pilot = { 11, 39, 75, 103, 153, 181, 217, 245 }, ++ .ro = { { 0, 0, 255 } }, ++ }, ++ ++ /* HE */ ++ { .null = { { 0 }, { 31, 33 } }, ++ .pilot = { 12, 29, 35, 52 }, ++ .ro = { { 0, 0, 63 } }, ++ }, ++ { .null = { { 30, 34 }, { 96 } }, ++ .pilot = { 4, 21, 43, 60, 70, 87, 105, 122 }, ++ .ro = { { 0, 96 }, { 34, 66, 95 }, { 1, 97, 126 } }, ++ }, ++ { .null = { { 32 }, { 94, 98 } }, ++ .pilot = { 6, 23, 41, 58, 68, 85, 107, 124 }, ++ .ro = { { 0, 32 }, { 34, 2, 31 }, { 1, 31, 62 } }, ++ }, ++ { .null = { { 0 }, { 62, 66 } }, ++ .pilot = { 9, 26, 36, 53, 75, 92, 102, 119 }, ++ .ro = { { 0, 0, 127 } }, ++ }, ++ { .null = { { 30, 34 }, { 160 } }, ++ .pilot = { 4, 21, 43, 60, 137, 154, 166, 183 }, ++ .ro = { { 0, 160 }, { 1, 161, 190 }, { 34, 130, 159 } }, ++ }, ++ { .null = { { 94, 98 }, { 224 } }, ++ .pilot = { 68, 85, 107, 124, 201, 218, 230, 247 }, ++ .ro = { { 0, 224 }, { 1, 225, 254 }, { 34, 194, 223 } }, ++ }, ++ { .null = { { 32 }, { 158, 162 } }, ++ .pilot = { 9, 26, 38, 55, 132, 149, 171, 188 }, ++ .ro = { { 0, 32 }, { 1, 33, 62 }, { 34, 2, 31 } }, ++ }, ++ { .null = { { 96 }, { 222, 226 } }, ++ .pilot = { 73, 90, 102, 119, 196, 213, 235, 252 }, ++ .ro = { { 0, 96 }, { 1, 97, 126 }, { 34, 66, 95 } }, ++ }, ++ { .null = { { 62, 66 }, { 192 } }, ++ .pilot = { 36, 53, 75, 92, 169, 186, 198, 215 }, ++ .ro = { { 0, 192 }, { 1, 193, 253 }, { 67, 131, 191 } }, ++ }, ++ { .null = { { 64 }, { 190, 194 } }, ++ .pilot = { 41, 58, 70, 87, 164, 181, 203, 220 }, ++ .ro = { { 0, 64 }, { 1, 65, 125 }, { 67, 3, 63 } }, ++ }, ++ { .null = { { 0 }, { 126, 130 } }, ++ .pilot = { 6, 23, 100, 117, 139, 156, 233, 250 }, ++ .ro = { { 0, 0, 255 } }, ++ }, ++}; ++ ++static inline u8 csi_group_idx(u8 mode, u8 ch_bw, u8 data_bw, u8 pri_ch_idx) ++{ ++ if (ch_bw < 2 || data_bw < 1) ++ return mode * 11 + ch_bw * ch_bw + pri_ch_idx; ++ else ++ return mode * 11 + ch_bw * ch_bw + (data_bw + 1) * 2 + pri_ch_idx; ++} ++ ++struct csi_mac_filter *mt7915_csi_mac_filter_find(struct mt7915_phy *phy, u8 *addr) ++{ ++ struct csi_mac_filter *ent, *tmp_ent; ++ ++ list_for_each_entry_safe(ent, tmp_ent, &phy->csi.mac_filter_list, node) { ++ if (ether_addr_equal(ent->mac, addr)) ++ return ent; ++ } ++ ++ return NULL; ++} ++ ++void mt7915_csi_mac_filter_clear(struct mt7915_phy *phy) ++{ ++ struct csi_mac_filter *ent, *tmp_ent; ++ list_for_each_entry_safe(ent, tmp_ent, &phy->csi.mac_filter_list, node) { ++ list_del(&ent->node); ++ kfree(ent); ++ } ++ ++ phy->csi.mac_filter_cnt = 0; ++} ++ ++static int mt7915_vendor_reply_csi_mac_filter(struct wiphy *wiphy, struct mt7915_phy *phy) ++{ ++ struct sk_buff *skb; ++ struct csi_mac_filter *ent, *tmp_ent; ++ int idx = 0; ++ struct nlattr *a, *b; ++ ++ mutex_lock(&phy->csi.mac_filter_lock); ++ ++#define ENTRY_SIZE nla_total_size(nla_total_size(ETH_ALEN) + \ ++ nla_total_size(sizeof(ent->interval))) ++ ++ skb = cfg80211_vendor_cmd_alloc_reply_skb(wiphy, ENTRY_SIZE * ++ phy->csi.mac_filter_cnt); ++ if (!skb) { ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return -ENOMEM; ++ } ++ ++ a = nla_nest_start(skb, MTK_VENDOR_ATTR_CSI_CTRL_DUMP_MAC_FILTER); ++ ++ list_for_each_entry_safe(ent, tmp_ent, &phy->csi.mac_filter_list, node) { ++ ++ b = nla_nest_start(skb, idx++); ++ nla_put(skb, MTK_VENDOR_ATTR_CSI_MAC_FILTER_MAC, ++ ETH_ALEN, ent->mac); ++ nla_put_u32(skb, MTK_VENDOR_ATTR_CSI_MAC_FILTER_INTERVAL, ent->interval); ++ nla_nest_end(skb, b); ++ } ++ ++ nla_nest_end(skb, a); ++#undef ENTRY_SIZE ++ ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ ++ return cfg80211_vendor_cmd_reply(skb); ++} ++ ++static int mt7915_vendor_csi_ctrl(struct wiphy *wiphy, ++ struct wireless_dev *wdev, ++ const void *data, ++ int data_len) ++{ ++ struct ieee80211_hw *hw = wiphy_to_ieee80211_hw(wiphy); ++ struct mt7915_phy *phy = mt7915_hw_phy(hw); ++ struct nlattr *tb[NUM_MTK_VENDOR_ATTRS_CSI_CTRL]; ++ int err; ++ ++ err = nla_parse(tb, MTK_VENDOR_ATTR_CSI_CTRL_MAX, data, data_len, ++ csi_ctrl_policy, NULL); ++ if (err) ++ return err; ++ ++ if (is_mt7915(phy->mt76->dev) && phy->mt76->chandef.width > NL80211_CHAN_WIDTH_80) { ++ err = -EINVAL; ++ return err; ++ } ++ ++ if (tb[MTK_VENDOR_ATTR_CSI_CTRL_CFG]) { ++ u8 mode = 0, type = 0, v1 = 0, v2 = 0; ++ u8 mac_addr[ETH_ALEN] = {}; ++ struct nlattr *cur; ++ int rem; ++ u32 sta_interval = 0; ++ ++ nla_for_each_nested(cur, tb[MTK_VENDOR_ATTR_CSI_CTRL_CFG], rem) { ++ switch (nla_type(cur)) { ++ case MTK_VENDOR_ATTR_CSI_CTRL_CFG_MODE: ++ mode = nla_get_u8(cur); ++ break; ++ case MTK_VENDOR_ATTR_CSI_CTRL_CFG_TYPE: ++ type = nla_get_u8(cur); ++ break; ++ case MTK_VENDOR_ATTR_CSI_CTRL_CFG_VAL1: ++ v1 = nla_get_u8(cur); ++ break; ++ case MTK_VENDOR_ATTR_CSI_CTRL_CFG_VAL2: ++ v2 = nla_get_u8(cur); ++ break; ++ default: ++ return -EINVAL; ++ }; ++ } ++ ++ if (tb[MTK_VENDOR_ATTR_CSI_CTRL_MAC_ADDR]) { ++ int idx = 0; ++ ++ nla_for_each_nested(cur, tb[MTK_VENDOR_ATTR_CSI_CTRL_MAC_ADDR], rem) { ++ mac_addr[idx++] = nla_get_u8(cur); ++ } ++ ++ /* when configure mac filter, add interval for report interval per sta */ ++ if (tb[MTK_VENDOR_ATTR_CSI_CTRL_STA_INTERVAL]) ++ sta_interval = ++ nla_get_u32(tb[MTK_VENDOR_ATTR_CSI_CTRL_STA_INTERVAL]); ++ } ++ ++ if (mode == 0) { ++ mutex_lock(&phy->csi.mac_filter_lock); ++ ++ err = mt7915_mcu_set_csi(phy, mode, type, v1, v2, ++ mac_addr, sta_interval); ++ if (err) { ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return err; ++ } ++ ++ mt7915_csi_mac_filter_clear(phy); ++ ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ } else if (mode == 2 && type == 8 && v1 == 1 && (v2 == ADD_CSI_MAC || v2 == DEL_CSI_MAC)) { ++ struct csi_mac_filter *ent; ++ ++ mutex_lock(&phy->csi.mac_filter_lock); ++ ++ if (v2 == DEL_CSI_MAC) { ++ ent = mt7915_csi_mac_filter_find(phy, mac_addr); ++ if (!ent) { ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return -ENOENT; ++ } ++ } else { ++ if (is_mt7915(phy->mt76->dev)) { ++ if (phy->csi.mac_filter_cnt >= MAX_CSI_MAC_NUM) { ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return -ENOSPC; ++ } ++ } else { ++ if (phy->csi.mac_filter_cnt >= MAX_CSI_MAC_NUM/2) { ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return -ENOSPC; ++ } ++ } ++ ++ ent = mt7915_csi_mac_filter_find(phy, mac_addr); ++ if (ent) { ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return -EEXIST; ++ } ++ ++ ent = kzalloc(sizeof(*ent), GFP_KERNEL); ++ if (!ent) { ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return -ENOMEM; ++ } ++ ++ ether_addr_copy(ent->mac, mac_addr); ++ ent->interval = sta_interval; ++ } ++ ++ err = mt7915_mcu_set_csi(phy, mode, type, v1, v2, ++ mac_addr, sta_interval); ++ if (err) { ++ if (v2 == ADD_CSI_MAC) ++ kfree(ent); ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ return err; ++ } ++ ++ if (v2 == DEL_CSI_MAC) { ++ list_del(&ent->node); ++ kfree(ent); ++ phy->csi.mac_filter_cnt--; ++ } else { ++ list_add_tail(&ent->node, &phy->csi.mac_filter_list); ++ phy->csi.mac_filter_cnt++; ++ } ++ ++ mutex_unlock(&phy->csi.mac_filter_lock); ++ } else if (mode == 2 && type == 8 && v1 == 1 && v2 == SHOW_CSI_MAC) { ++ return mt7915_vendor_reply_csi_mac_filter(wiphy, phy); ++ } else { ++ err = mt7915_mcu_set_csi(phy, mode, type, v1, v2, ++ mac_addr, sta_interval); ++ if (err) ++ return err; ++ } ++ ++ phy->csi.enable = !!mode; ++ ++ if (mode == 2 && type == 5) { ++ if (v1 >= 1) ++ phy->csi.mask = 1; ++ if (v1 == 2) ++ phy->csi.reorder = 1; ++ } ++ ++ /* clean up old csi stats */ ++ if (mode == 0 && !list_empty(&phy->csi.data_list)) { ++ struct csi_data *c, *tmp_c; ++ ++ spin_lock_bh(&phy->csi.data_lock); ++ list_for_each_entry_safe(c, tmp_c, &phy->csi.data_list, ++ node) { ++ list_del(&c->node); ++ kfree(c); ++ phy->csi.count--; ++ } ++ spin_unlock_bh(&phy->csi.data_lock); ++ } else if (mode == 1) { ++ phy->csi.last_record = 0; ++ } ++ } ++ ++ if (tb[MTK_VENDOR_ATTR_CSI_CTRL_INTERVAL]) ++ phy->csi.interval = nla_get_u32(tb[MTK_VENDOR_ATTR_CSI_CTRL_INTERVAL]); ++ ++ return 0; ++} ++ ++static void ++mt7915_vendor_csi_tone_mask(struct mt7915_phy *phy, struct csi_data *csi) ++{ ++ static const u8 mode_map[] = { ++ [MT_PHY_TYPE_OFDM] = 0, ++ [MT_PHY_TYPE_HT] = 1, ++ [MT_PHY_TYPE_VHT] = 1, ++ [MT_PHY_TYPE_HE_SU] = 2, ++ }; ++ const struct csi_mask *cmask; ++ int i; ++ ++ if (csi->rx_mode == MT_PHY_TYPE_CCK || !phy->csi.mask) ++ return; ++ ++ if (csi->data_bw == IEEE80211_STA_RX_BW_40) ++ csi->pri_ch_idx /= 2; ++ ++ cmask = &csi_mask_groups[csi_group_idx(mode_map[csi->rx_mode], ++ csi->ch_bw, ++ csi->data_bw, ++ csi->pri_ch_idx)]; ++ ++ for (i = 0; i < 10; i++) { ++ const struct csi_null_tone *ntone = &cmask->null[i]; ++ u8 start = ntone->start; ++ u8 end = ntone->end; ++ int j; ++ ++ if (!start && !end && i > 0) ++ break; ++ ++ if (!end) ++ end = start; ++ ++ for (j = start; j <= end; j++) { ++ csi->data_i[j] = 0; ++ csi->data_q[j] = 0; ++ } ++ } ++ ++ for (i = 0; i < 8; i++) { ++ u8 pilot = cmask->pilot[i]; ++ ++ if (!pilot) ++ break; ++ ++ csi->data_i[pilot] = 0; ++ csi->data_q[pilot] = 0; ++ } ++ ++ if (!phy->csi.reorder) ++ return; ++ ++ for (i = 0; i < 3; i++) { ++ const struct csi_reorder *ro = &cmask->ro[i]; ++ u8 dest = ro->dest; ++ u8 start = ro->start; ++ u8 end = ro->end; ++ ++ if (!dest && !start && !end) ++ break; ++ ++ if (dest == start) ++ continue; ++ ++ if (end) { ++ memmove(&csi->data_i[dest], &csi->data_i[start], ++ end - start + 1); ++ memmove(&csi->data_q[dest], &csi->data_q[start], ++ end - start + 1); ++ } else { ++ csi->data_i[dest] = csi->data_i[start]; ++ csi->data_q[dest] = csi->data_q[start]; ++ } ++ } ++} ++ ++static int ++mt7915_vendor_csi_ctrl_dump(struct wiphy *wiphy, struct wireless_dev *wdev, ++ struct sk_buff *skb, const void *data, int data_len, ++ unsigned long *storage) ++{ ++#define RESERVED_SET BIT(31) ++ struct ieee80211_hw *hw = wiphy_to_ieee80211_hw(wiphy); ++ struct mt7915_phy *phy = mt7915_hw_phy(hw); ++ struct nlattr *tb[NUM_MTK_VENDOR_ATTRS_CSI_CTRL]; ++ int err = 0; ++ ++ if (*storage & RESERVED_SET) { ++ if ((*storage & GENMASK(15, 0)) == 0) ++ return -ENOENT; ++ (*storage)--; ++ } ++ ++ if (data) { ++ err = nla_parse(tb, MTK_VENDOR_ATTR_CSI_CTRL_MAX, data, data_len, ++ csi_ctrl_policy, NULL); ++ if (err) ++ return err; ++ } ++ ++ if (!(*storage & RESERVED_SET) && tb[MTK_VENDOR_ATTR_CSI_CTRL_DUMP_NUM]) { ++ *storage = nla_get_u16(tb[MTK_VENDOR_ATTR_CSI_CTRL_DUMP_NUM]); ++ *storage |= RESERVED_SET; ++ } ++ ++ spin_lock_bh(&phy->csi.data_lock); ++ ++ if (!list_empty(&phy->csi.data_list)) { ++ struct csi_data *csi; ++ void *a, *b; ++ int i; ++ ++ csi = list_first_entry(&phy->csi.data_list, struct csi_data, node); ++ ++ mt7915_vendor_csi_tone_mask(phy, csi); ++ ++ a = nla_nest_start(skb, MTK_VENDOR_ATTR_CSI_CTRL_DATA); ++ ++ if (nla_put_u8(skb, MTK_VENDOR_ATTR_CSI_DATA_VER, 1) || ++ nla_put_u8(skb, MTK_VENDOR_ATTR_CSI_DATA_RSSI, csi->rssi) || ++ nla_put_u8(skb, MTK_VENDOR_ATTR_CSI_DATA_SNR, csi->snr) || ++ nla_put_u8(skb, MTK_VENDOR_ATTR_CSI_DATA_BW, csi->data_bw) || ++ nla_put_u8(skb, MTK_VENDOR_ATTR_CSI_DATA_CH_IDX, csi->pri_ch_idx) || ++ nla_put_u8(skb, MTK_VENDOR_ATTR_CSI_DATA_MODE, csi->rx_mode)) ++ goto out; ++ ++ if (nla_put_u16(skb, MTK_VENDOR_ATTR_CSI_DATA_TX_ANT, csi->tx_idx) || ++ nla_put_u16(skb, MTK_VENDOR_ATTR_CSI_DATA_RX_ANT, csi->rx_idx)) ++ goto out; ++ ++ if (nla_put_u32(skb, MTK_VENDOR_ATTR_CSI_DATA_INFO, csi->ext_info) || ++ nla_put_u32(skb, MTK_VENDOR_ATTR_CSI_DATA_CHAIN_INFO, csi->chain_info) || ++ nla_put_u32(skb, MTK_VENDOR_ATTR_CSI_DATA_TS, csi->ts)) ++ goto out; ++ ++ b = nla_nest_start(skb, MTK_VENDOR_ATTR_CSI_DATA_TA); ++ for (i = 0; i < ARRAY_SIZE(csi->ta); i++) ++ if (nla_put_u8(skb, i, csi->ta[i])) ++ goto out; ++ nla_nest_end(skb, b); ++ ++ if (nla_put_u32(skb, MTK_VENDOR_ATTR_CSI_DATA_NUM, csi->data_num)) ++ goto out; ++ ++ b = nla_nest_start(skb, MTK_VENDOR_ATTR_CSI_DATA_I); ++ for (i = 0; i < csi->data_num; i++) ++ if (nla_put_u16(skb, i, csi->data_i[i])) ++ goto out; ++ nla_nest_end(skb, b); ++ ++ b = nla_nest_start(skb, MTK_VENDOR_ATTR_CSI_DATA_Q); ++ for (i = 0; i < csi->data_num; i++) ++ if (nla_put_u16(skb, i, csi->data_q[i])) ++ goto out; ++ nla_nest_end(skb, b); ++ ++ nla_nest_end(skb, a); ++ ++ list_del(&csi->node); ++ kfree(csi); ++ phy->csi.count--; ++ ++ err = phy->csi.count; ++ } ++out: ++ spin_unlock_bh(&phy->csi.data_lock); ++ ++ return err; ++} ++ ++static const struct wiphy_vendor_command mt7915_vendor_commands[] = { ++ { ++ .info = { ++ .vendor_id = MTK_NL80211_VENDOR_ID, ++ .subcmd = MTK_NL80211_VENDOR_SUBCMD_CSI_CTRL, ++ }, ++ .flags = WIPHY_VENDOR_CMD_NEED_NETDEV | ++ WIPHY_VENDOR_CMD_NEED_RUNNING, ++ .doit = mt7915_vendor_csi_ctrl, ++ .dumpit = mt7915_vendor_csi_ctrl_dump, ++ .policy = csi_ctrl_policy, ++ .maxattr = MTK_VENDOR_ATTR_CSI_CTRL_MAX, ++ } ++}; ++ ++void mt7915_vendor_register(struct mt7915_phy *phy) ++{ ++ phy->mt76->hw->wiphy->vendor_commands = mt7915_vendor_commands; ++ phy->mt76->hw->wiphy->n_vendor_commands = ARRAY_SIZE(mt7915_vendor_commands); ++} +diff --git a/mt7915/vendor.h b/mt7915/vendor.h +new file mode 100644 +index 000000000000..ff85eb0f6834 +--- /dev/null ++++ b/mt7915/vendor.h +@@ -0,0 +1,79 @@ ++/* SPDX-License-Identifier: BSD-3-Clause-Clear */ ++/* ++ * Copyright (c) 2022 MediaTek Inc. All rights reserved. ++ */ ++ ++#ifndef __MT7915_VENDOR_H ++#define __MT7915_VENDOR_H ++ ++#define MTK_NL80211_VENDOR_ID 0x0ce7 ++ ++enum mtk_nl80211_vendor_subcmds { ++ MTK_NL80211_VENDOR_SUBCMD_CSI_CTRL = 0xc2, ++}; ++ ++enum mtk_vendor_attr_csi_ctrl { ++ MTK_VENDOR_ATTR_CSI_CTRL_UNSPEC, ++ ++ MTK_VENDOR_ATTR_CSI_CTRL_CFG, ++ MTK_VENDOR_ATTR_CSI_CTRL_CFG_MODE, ++ MTK_VENDOR_ATTR_CSI_CTRL_CFG_TYPE, ++ MTK_VENDOR_ATTR_CSI_CTRL_CFG_VAL1, ++ MTK_VENDOR_ATTR_CSI_CTRL_CFG_VAL2, ++ MTK_VENDOR_ATTR_CSI_CTRL_MAC_ADDR, ++ MTK_VENDOR_ATTR_CSI_CTRL_INTERVAL, ++ MTK_VENDOR_ATTR_CSI_CTRL_STA_INTERVAL, ++ ++ MTK_VENDOR_ATTR_CSI_CTRL_DUMP_NUM, ++ ++ MTK_VENDOR_ATTR_CSI_CTRL_DATA, ++ MTK_VENDOR_ATTR_CSI_CTRL_DUMP_MAC_FILTER, ++ ++ /* keep last */ ++ NUM_MTK_VENDOR_ATTRS_CSI_CTRL, ++ MTK_VENDOR_ATTR_CSI_CTRL_MAX = ++ NUM_MTK_VENDOR_ATTRS_CSI_CTRL - 1 ++}; ++ ++enum mtk_vendor_attr_csi_data { ++ MTK_VENDOR_ATTR_CSI_DATA_UNSPEC, ++ MTK_VENDOR_ATTR_CSI_DATA_PAD, ++ ++ MTK_VENDOR_ATTR_CSI_DATA_VER, ++ MTK_VENDOR_ATTR_CSI_DATA_TS, ++ MTK_VENDOR_ATTR_CSI_DATA_RSSI, ++ MTK_VENDOR_ATTR_CSI_DATA_SNR, ++ MTK_VENDOR_ATTR_CSI_DATA_BW, ++ MTK_VENDOR_ATTR_CSI_DATA_CH_IDX, ++ MTK_VENDOR_ATTR_CSI_DATA_TA, ++ MTK_VENDOR_ATTR_CSI_DATA_NUM, ++ MTK_VENDOR_ATTR_CSI_DATA_I, ++ MTK_VENDOR_ATTR_CSI_DATA_Q, ++ MTK_VENDOR_ATTR_CSI_DATA_INFO, ++ MTK_VENDOR_ATTR_CSI_DATA_RSVD1, ++ MTK_VENDOR_ATTR_CSI_DATA_RSVD2, ++ MTK_VENDOR_ATTR_CSI_DATA_RSVD3, ++ MTK_VENDOR_ATTR_CSI_DATA_RSVD4, ++ MTK_VENDOR_ATTR_CSI_DATA_TX_ANT, ++ MTK_VENDOR_ATTR_CSI_DATA_RX_ANT, ++ MTK_VENDOR_ATTR_CSI_DATA_MODE, ++ MTK_VENDOR_ATTR_CSI_DATA_CHAIN_INFO, ++ ++ /* keep last */ ++ NUM_MTK_VENDOR_ATTRS_CSI_DATA, ++ MTK_VENDOR_ATTR_CSI_DATA_MAX = ++ NUM_MTK_VENDOR_ATTRS_CSI_DATA - 1 ++}; ++ ++enum mtk_vendor_attr_csi_mac_filter { ++ MTK_VENDOR_ATTR_CSI_MAC_FILTER_UNSPEC, ++ ++ MTK_VENDOR_ATTR_CSI_MAC_FILTER_MAC, ++ MTK_VENDOR_ATTR_CSI_MAC_FILTER_INTERVAL, ++ ++ /* keep last */ ++ NUM_MTK_VENDOR_ATTRS_CSI_MAC_FILTER, ++ MTK_VENDOR_ATTR_CSI_MAC_FILTER_MAX = ++ NUM_MTK_VENDOR_ATTRS_CSI_MAC_FILTER - 1 ++}; ++#endif diff --git a/firmware/openwrt-wn586x3/patches/csidump-nl80211-attr-enum.patch b/firmware/openwrt-wn586x3/patches/csidump-nl80211-attr-enum.patch new file mode 100644 index 000000000..5e3f5de21 --- /dev/null +++ b/firmware/openwrt-wn586x3/patches/csidump-nl80211-attr-enum.patch @@ -0,0 +1,128 @@ +Fix: CSIdump forwarded nothing against the mt76 CSI driver built here. +Base: github.com/MtkWifiRev/MtkCSIdump @ 276b4b0814a283dcbc6544a76033e0e37154f092 + +CSIdump was written against a DIFFERENT mt76 vendor ABI than the 2022 +MediaTek CSI patch we build. Two enumerators are missing from its copies of +the attribute enums, so every attribute number past them was off by one: + CTRL: our driver has STA_INTERVAL between INTERVAL and DUMP_NUM + DATA: our driver has DATA_NUM between TA and I, and ends CHAIN_INFO not H_IDX + +Failure chain. CSIdump put DUMP_NUM at attr 8, which is the driver's +STA_INTERVAL (policy NLA_U32); a 2-byte u16 payload failed the driver's +nla_parse minimum-length check, so the dumpit returned -EINVAL and emitted +NOTHING. The ring therefore never drained (a unit sat pinned at 3000) and the +dump callback never ran. Even had the request landed, the driver replies with +DATA at attr 10 while CSIdump looked at attr 9, so the callback would have +returned NL_SKIP at its first check with no message printed. And the error +print used a stale ret that was always 0, so any failure read as 'Success'. +--- +--- /tmp/mt76_api.cpp.orig 2026-09-19 23:14:12 ++++ src/wifi_drv_api/mt76_api.cpp 2026-09-19 23:14:12 +@@ -47,10 +47,12 @@ + MTK_VENDOR_ATTR_CSI_CTRL_CFG_VAL2, + MTK_VENDOR_ATTR_CSI_CTRL_MAC_ADDR, + MTK_VENDOR_ATTR_CSI_CTRL_INTERVAL, ++ MTK_VENDOR_ATTR_CSI_CTRL_STA_INTERVAL, + + MTK_VENDOR_ATTR_CSI_CTRL_DUMP_NUM, + + MTK_VENDOR_ATTR_CSI_CTRL_DATA, ++ MTK_VENDOR_ATTR_CSI_CTRL_DUMP_MAC_FILTER, + + /* keep last */ + NUM_MTK_VENDOR_ATTRS_CSI_CTRL, +@@ -70,6 +72,7 @@ + MTK_VENDOR_ATTR_CSI_DATA_BW, + MTK_VENDOR_ATTR_CSI_DATA_CH_IDX, + MTK_VENDOR_ATTR_CSI_DATA_TA, ++ MTK_VENDOR_ATTR_CSI_DATA_NUM, + MTK_VENDOR_ATTR_CSI_DATA_I, + MTK_VENDOR_ATTR_CSI_DATA_Q, + MTK_VENDOR_ATTR_CSI_DATA_INFO, +@@ -80,7 +83,7 @@ + MTK_VENDOR_ATTR_CSI_DATA_TX_ANT, + MTK_VENDOR_ATTR_CSI_DATA_RX_ANT, + MTK_VENDOR_ATTR_CSI_DATA_MODE, +- MTK_VENDOR_ATTR_CSI_DATA_H_IDX, ++ MTK_VENDOR_ATTR_CSI_DATA_CHAIN_INFO, + + /* keep last */ + NUM_MTK_VENDOR_ATTRS_CSI_DATA, +@@ -123,7 +126,8 @@ + csi_data_policy[MTK_VENDOR_ATTR_CSI_DATA_TX_ANT].type = NLA_U8; + csi_data_policy[MTK_VENDOR_ATTR_CSI_DATA_RX_ANT].type = NLA_U8; + csi_data_policy[MTK_VENDOR_ATTR_CSI_DATA_MODE].type = NLA_U8; +- csi_data_policy[MTK_VENDOR_ATTR_CSI_DATA_H_IDX].type = NLA_U32; ++ csi_data_policy[MTK_VENDOR_ATTR_CSI_DATA_NUM].type = NLA_U32; ++ csi_data_policy[MTK_VENDOR_ATTR_CSI_DATA_CHAIN_INFO].type = NLA_U32; + } + ~MT76APIPrivate() + { +@@ -172,7 +176,7 @@ + tb_data[MTK_VENDOR_ATTR_CSI_DATA_Q] && + tb_data[MTK_VENDOR_ATTR_CSI_DATA_INFO] && + tb_data[MTK_VENDOR_ATTR_CSI_DATA_MODE] && +- tb_data[MTK_VENDOR_ATTR_CSI_DATA_H_IDX])) ++ tb_data[MTK_VENDOR_ATTR_CSI_DATA_CHAIN_INFO])) + { + fprintf(stderr, "Attributes error for CSI data\n"); + return NL_SKIP; +@@ -192,7 +196,7 @@ + c->rx_idx = nla_get_u16(tb_data[MTK_VENDOR_ATTR_CSI_DATA_RX_ANT]); + + c->ext_info = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_INFO]); +- c->h_idx = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_H_IDX]); ++ c->h_idx = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_CHAIN_INFO]); + + c->ts = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_TS]); + +@@ -271,8 +275,9 @@ + + nla_nest_end(msg, data); + +- if (unl_genl_request(&unl, msg, md_csi_dump_cb, NULL)) +- fprintf(stderr, "nl80211 call failed: %s\n", strerror(-ret)); ++ ret = unl_genl_request(&unl, msg, md_csi_dump_cb, NULL); ++ if (ret) ++ fprintf(stderr, "csi dump: nl80211 call failed: %s\n", strerror(-ret)); + + unl_free(&unl); + } +--- /tmp/motion_detector.cpp.orig 2026-09-19 23:14:58 ++++ src/motion_detector.cpp 2026-09-19 23:14:58 +@@ -1,3 +1,4 @@ ++#include + #include "motion_detector.h" + #include "wifi_drv_api/mt76_api.h" + #include "parsers/parser_mt76.h" +@@ -35,6 +36,12 @@ + std::vector *list = wifi.motion_detection_dump(ifname.c_str(), pkt_num); + oldTime = currTime; + ++ /* Visibility: without this a failing dump is completely silent, which is ++ * exactly how the enum/ABI mismatch went unnoticed. */ ++ fprintf(stderr, "csi: dump returned %zu records (%d ms since last)\n", ++ list ? list->size() : (size_t)0, diff); ++ ++ unsigned sentThisInterval = 0; + dataMutex.lock(); + unsigned antIdx = antMonIdx; + dataMutex.unlock(); +@@ -49,6 +56,7 @@ + if (!packet.empty()) { + std::vector> singlePacket = { packet }; + sendCsiDataUdp(singlePacket, i); ++ sentThisInterval++; + } + } + } +@@ -56,6 +64,9 @@ + } + } + ++ if (sentThisInterval) ++ fprintf(stderr, "csi: sent %u udp datagrams\n", sentThisInterval); ++ + std::this_thread::sleep_for(std::chrono::milliseconds(interval)); + } + } diff --git a/firmware/openwrt-wn586x3/patches/mt76-vendor-csi-dump-bounds.patch b/firmware/openwrt-wn586x3/patches/mt76-vendor-csi-dump-bounds.patch new file mode 100644 index 000000000..14e75c13a --- /dev/null +++ b/firmware/openwrt-wn586x3/patches/mt76-vendor-csi-dump-bounds.patch @@ -0,0 +1,192 @@ +Fix for mt76-vendor 'dump csi' SIGSEGV. +Base: mediatek/mtk-openwrt-feeds @ a15454c888f4f4144c50e33b5feef4f247c5f78b + feed/app/mt76-vendor/src/csi.c + +Root cause: the driver seeds its per-dump counter on the FIRST dumpit +callback and only then decrements it, so a request for CSI_DUMP_PER_NUM=3 +yields FOUR records. Userspace sized csi[] for pkt_num but looped pkt_num/3 +times, storing up to 4*(pkt_num/3) records with no bounds check, so any dump +with >= pkt_num records buffered wrote past the allocation. A dump that found +only one record buffered never overran, which is why the single association +record succeeded and every data-frame dump crashed. + +Also fixed: the JSON writer passed the full MAX_BUF_SIZE to every snprintf +instead of the remaining space, and three attributes were dereferenced without +being in the presence check. +--- +--- /tmp/csi.c.orig 2026-09-19 19:46:49 ++++ src/csi.c 2026-09-19 19:47:16 +@@ -6,6 +6,7 @@ + + struct csi_data *csi; + int csi_idx; ++int csi_max; /* entries allocated in csi[] */ + + static struct nla_policy csi_ctrl_policy[NUM_MTK_VENDOR_ATTRS_CSI_CTRL] = { + [MTK_VENDOR_ATTR_CSI_CTRL_CFG] = { .type = NLA_NESTED }, +@@ -51,8 +52,13 @@ + struct nlattr *cur; + size_t idx; + int rem; +- struct csi_data *c = &csi[csi_idx]; ++ struct csi_data *c; + ++ if (!csi || csi_idx >= csi_max) ++ return NL_SKIP; ++ ++ c = &csi[csi_idx]; ++ + attr = unl_find_attr(&unl, msg, NL80211_ATTR_VENDOR_DATA); + if (!attr) { + fprintf(stderr, "Testdata attribute not found\n"); +@@ -90,15 +96,20 @@ + c->pri_ch_idx = nla_get_u8(tb_data[MTK_VENDOR_ATTR_CSI_DATA_CH_IDX]); + c->rx_mode = nla_get_u8(tb_data[MTK_VENDOR_ATTR_CSI_DATA_MODE]); + +- c->tx_idx = nla_get_u16(tb_data[MTK_VENDOR_ATTR_CSI_DATA_TX_ANT]); +- c->rx_idx = nla_get_u16(tb_data[MTK_VENDOR_ATTR_CSI_DATA_RX_ANT]); ++ c->tx_idx = tb_data[MTK_VENDOR_ATTR_CSI_DATA_TX_ANT] ? ++ nla_get_u16(tb_data[MTK_VENDOR_ATTR_CSI_DATA_TX_ANT]) : 0; ++ c->rx_idx = tb_data[MTK_VENDOR_ATTR_CSI_DATA_RX_ANT] ? ++ nla_get_u16(tb_data[MTK_VENDOR_ATTR_CSI_DATA_RX_ANT]) : 0; + + c->ext_info = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_INFO]); + c->chain_info = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_CHAIN_INFO]); + + c->ts = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_TS]); + +- c->data_num = nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_NUM]); ++ c->data_num = tb_data[MTK_VENDOR_ATTR_CSI_DATA_NUM] ? ++ nla_get_u32(tb_data[MTK_VENDOR_ATTR_CSI_DATA_NUM]) : 0; ++ if (c->data_num > CSI_BW160_DATA_COUNT) ++ c->data_num = CSI_BW160_DATA_COUNT; + + idx = 0; + nla_for_each_nested(cur, tb_data[MTK_VENDOR_ATTR_CSI_DATA_TA], rem) { +@@ -125,7 +136,7 @@ + + static int mt76_csi_to_json(const char *name) + { +-#define MAX_BUF_SIZE 6000 ++#define MAX_BUF_SIZE 16384 /* 2 x 512 samples of "-32768," plus header */ + FILE *f; + int i, ret = -ENOMEM; + +@@ -142,7 +153,7 @@ + + for (i = 0; i < csi_idx; i++) { + struct csi_data *c = &csi[i]; +- char *pos, *buf; ++ char *pos, *buf, *end; + int j; + + buf = malloc(MAX_BUF_SIZE); +@@ -150,40 +161,51 @@ + goto out; + + pos = buf; +- pos += snprintf(pos, MAX_BUF_SIZE, "%c", '['); ++ end = buf + MAX_BUF_SIZE; ++#define EMIT(...) do { \ ++ int _n = snprintf(pos, (size_t)(end - pos), __VA_ARGS__); \ ++ if (_n < 0 || _n >= (int)(end - pos)) { \ ++ fprintf(stderr, "csi json: record %d truncated\n", i); \ ++ free(buf); \ ++ goto out; \ ++ } \ ++ pos += _n; \ ++ } while (0) + +- pos += snprintf(pos, MAX_BUF_SIZE, "%d,", c->ts); +- pos += snprintf(pos, MAX_BUF_SIZE, "\"%02x%02x%02x%02x%02x%02x\",", c->ta[0], c->ta[1], c->ta[2], c->ta[3], c->ta[4], c->ta[5]); ++ EMIT("%c", '['); + +- pos += snprintf(pos, MAX_BUF_SIZE, "%d,", c->rssi); +- pos += snprintf(pos, MAX_BUF_SIZE, "%u,", c->snr); +- pos += snprintf(pos, MAX_BUF_SIZE, "%u,", c->data_bw); +- pos += snprintf(pos, MAX_BUF_SIZE, "%u,", c->pri_ch_idx); +- pos += snprintf(pos, MAX_BUF_SIZE, "%u,", c->rx_mode); +- pos += snprintf(pos, MAX_BUF_SIZE, "%d,", c->tx_idx); +- pos += snprintf(pos, MAX_BUF_SIZE, "%d,", c->rx_idx); +- pos += snprintf(pos, MAX_BUF_SIZE, "%d,", c->chain_info); +- pos += snprintf(pos, MAX_BUF_SIZE, "%d,", c->ext_info); ++ EMIT("%d,", c->ts); ++ EMIT("\"%02x%02x%02x%02x%02x%02x\",", c->ta[0], c->ta[1], c->ta[2], c->ta[3], c->ta[4], c->ta[5]); + +- pos += snprintf(pos, MAX_BUF_SIZE, "%c", '['); +- for (j = 0; j < c->data_num; j++) { +- pos += snprintf(pos, MAX_BUF_SIZE, "%d", c->data_i[j]); +- if (j != (c->data_num - 1)) +- pos += snprintf(pos, MAX_BUF_SIZE, ","); +- } +- pos += snprintf(pos, MAX_BUF_SIZE, "%c,", ']'); ++ EMIT("%d,", c->rssi); ++ EMIT("%u,", c->snr); ++ EMIT("%u,", c->data_bw); ++ EMIT("%u,", c->pri_ch_idx); ++ EMIT("%u,", c->rx_mode); ++ EMIT("%d,", c->tx_idx); ++ EMIT("%d,", c->rx_idx); ++ EMIT("%d,", c->chain_info); ++ EMIT("%d,", c->ext_info); + +- pos += snprintf(pos, MAX_BUF_SIZE, "%c", '['); ++ EMIT("%c", '['); + for (j = 0; j < c->data_num; j++) { +- pos += snprintf(pos, MAX_BUF_SIZE, "%d", c->data_q[j]); ++ EMIT("%d", c->data_i[j]); + if (j != (c->data_num - 1)) +- pos += snprintf(pos, MAX_BUF_SIZE, ","); ++ EMIT(","); + } +- pos += snprintf(pos, MAX_BUF_SIZE, "%c", ']'); ++ EMIT("%c,", ']'); + +- pos += snprintf(pos, MAX_BUF_SIZE, "%c", ']'); ++ EMIT("%c", '['); ++ for (j = 0; j < c->data_num; j++) { ++ EMIT("%d", c->data_q[j]); ++ if (j != (c->data_num - 1)) ++ EMIT(","); ++ } ++ EMIT("%c", ']'); ++ ++ EMIT("%c", ']'); + if (i != csi_idx - 1) +- pos += snprintf(pos, MAX_BUF_SIZE, ","); ++ EMIT(","); + + if (fwrite(buf, 1, pos - buf, f) != (pos - buf)) { + perror("fwrite"); +@@ -192,6 +214,7 @@ + } + + free(buf); ++#undef EMIT + } + + if (fwrite("]", 1, 1, f) != 1) { +@@ -221,10 +244,21 @@ + return -EINVAL; + + #define CSI_DUMP_PER_NUM 3 ++/* The driver seeds its per-dump counter on the first callback and only then ++ * decrements it, so a request for N yields N+1 records. Size for that. */ ++#define CSI_DUMP_PER_REQ (CSI_DUMP_PER_NUM + 1) + csi_idx = 0; +- csi = (struct csi_data *)calloc(pkt_num, sizeof(*csi)); ++ csi_max = pkt_num; ++ csi = (struct csi_data *)calloc(csi_max, sizeof(*csi)); ++ if (!csi) { ++ fprintf(stderr, "csi: cannot allocate %d records\n", csi_max); ++ return -ENOMEM; ++ } + +- for (i = 0; i < pkt_num / CSI_DUMP_PER_NUM; i++) { ++ for (i = 0; i < DIV_ROUND_UP(pkt_num, CSI_DUMP_PER_REQ); i++) { ++ if (csi_idx >= csi_max) ++ break; ++ + if (unl_genl_init(&unl, "nl80211") < 0) { + fprintf(stderr, "Failed to connect to nl80211\n"); + return 2; diff --git a/scripts/mediatek-csi-phase-stability.py b/scripts/mediatek-csi-phase-stability.py new file mode 100755 index 000000000..ee3c0e14c --- /dev/null +++ b/scripts/mediatek-csi-phase-stability.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Cross-chain phase stability of MediaTek mt76 vendor CSI (offline, read-only). + +Reproducer for the MEASURED figures in docs/mediatek-router-csi.md. Input is a +capture made on the router by repeated `mt76-vendor dump csi `, +one dump (a JSON array of 13/14-element records) per line. Captures hold client +transmitter addresses and raw channel samples: keep them out of git. This script +never prints an address; transmitters are reported as TX-A, TX-B, ... + +For every HT (rx_mode 2) frame that carries both receive chains of transmit +stream 0, it compares: + raw per-chain phase arg(H0[k]) between adjacent frames, and + cross cross-chain phase arg(H0[k] * conj(H1[k])), per bin and summed + coherently over bins. +CFO, SFO and the per-packet phase offset are common to both chains of one +radio, so they cancel in the cross term. Adjacent means the frames are under +100 ms apart. Steps are wrapped to [-pi, pi], so p99 can never exceed pi; read +p50 against pi/2 (1.571 rad), which is what uniformly random phase gives. + +usage: python3 mediatek-csi-phase-stability.py [max_MB] +""" +import json +import sys + +import numpy as np + +U32 = 1 << 32 +MAX_DT = 0.1 # s; consecutive frames closer than this count as adjacent +GUARD = set(range(29, 36)) # BW20 bins 29..35: guard tones +VALID = np.array([k for k in range(1, 64) if k not in GUARD]) # DC excluded +KPHYS = np.where(VALID < 32, VALID, VALID - 64).astype(float) + + +def iter_lines(path, max_bytes=None): + read = 0 + with open(path, "rb") as fh: + for raw in fh: + read += len(raw) + raw = raw.strip() + if raw: + try: + yield json.loads(raw) + except json.JSONDecodeError: + pass # truncated tail line + if max_bytes and read >= max_bytes: + return + + +def frames_from_line(recs, txidx): + """Group a dump's records into PPDUs: per transmitter, consecutive records + sharing ts, closed on chain_info BIT15 or a repeated (tx, rx) slot.""" + frames, open_ = [], {} + for r in recs: + ts = r[0] % U32 + if r[1] not in txidx: + n = len(txidx) + txidx[r[1]] = "TX-" + chr(ord("A") + n) if n < 26 else f"TX-{n}" + tx = txidx[r[1]] + key = (r[7], r[8]) + h = np.asarray(r[11], np.float64) + 1j * np.asarray(r[12], np.float64) + cur = open_.get(tx) + if cur is not None and (cur["ts"] != ts or key in cur["H"]): + frames.append(cur) + cur = None + if cur is None: + cur = {"tx": tx, "ts": ts, "rx_mode": r[6], "H": {}} + open_[tx] = cur + cur["H"][key] = h + if r[9] & (1 << 15): + frames.append(cur) + del open_[tx] + frames.extend(open_.values()) + return frames + + +def load(path, max_bytes=None): + """Per transmitter: unwrapped time (s) and chains (0,0), (0,1) of HT frames. + Within a dump the signed ts delta is used; between dumps the unsigned delta + mod 2^32, so a gap over 71.6 min between dumps would alias.""" + txidx, per = {}, {} + t_unwrapped, prev_ts = None, None + for recs in iter_lines(path, max_bytes): + first = True + for f in frames_from_line(recs, txidx): + ts = f["ts"] + if prev_ts is None: + t_unwrapped = 0 + elif first: + t_unwrapped += (ts - prev_ts) % U32 + else: + t_unwrapped += ((ts - prev_ts + (U32 >> 1)) % U32) - (U32 >> 1) + prev_ts, first = ts, False + H = f["H"] + if (0, 0) not in H or (0, 1) not in H or f["rx_mode"] != 2: + continue + d = per.setdefault(f["tx"], {"t": [], "h0": [], "h1": []}) + d["t"].append(t_unwrapped / 1e6) + d["h0"].append(H[(0, 0)][VALID].astype(np.complex64)) + d["h1"].append(H[(0, 1)][VALID].astype(np.complex64)) + out = {} + for tx, d in per.items(): + t = np.asarray(d["t"]) + srt = np.argsort(t, kind="stable") + out[tx] = {"t": t[srt], "h0": np.vstack(d["h0"])[srt], "h1": np.vstack(d["h1"])[srt]} + return out + + +def wrap(x): + return (x + np.pi) % (2 * np.pi) - np.pi + + +def sanitize(phase_rows): + """Remove per-frame linear phase (offset + slope over subcarrier index).""" + order = np.argsort(KPHYS) + k = KPHYS[order] + u = np.unwrap(phase_rows[:, order], axis=1) + A = np.vstack([k, np.ones_like(k)]).T + coef, *_ = np.linalg.lstsq(A, u.T, rcond=None) + res = u - (A @ coef).T + out = np.empty_like(res) + out[:, order] = res + return out + + +def step_stats(phase, t): + dt = np.diff(t) + ok = (dt > 0) & (dt < MAX_DT) + st = np.abs(wrap(np.diff(phase, axis=0)))[ok].ravel() + if st.size == 0: + return None + p = np.percentile(st, [50, 90, 99]) + return {"n": int(st.size), "p50": round(float(p[0]), 3), "p90": round(float(p[1]), 3), + "p99": round(float(p[2]), 3), "frac_gt_pi_2": round(float((st > np.pi / 2).mean()), 4)} + + +def analyse(d): + t, h0, h1 = d["t"], d["h0"].astype(complex), d["h1"].astype(complex) + raw0 = np.angle(h0) + x = h0 * np.conj(h1) + dt = np.diff(t) + adj = dt[(dt > 0) & (dt < MAX_DT)] + return { + "frames": int(len(t)), + "span_s": round(float(t[-1] - t[0]), 1), + "adjacent_pairs": int(adj.size), + "adjacent_dt_ms_p50": round(float(np.median(adj) * 1e3), 2) if adj.size else None, + "steps": { + "raw_chain0_per_bin": step_stats(raw0, t), + "sanitized_chain0_per_bin": step_stats(sanitize(raw0), t), + "cross_per_bin": step_stats(np.angle(x), t), + "cross_coherent_sum": step_stats(np.angle(x.sum(1)), t), + }, + } + + +if __name__ == "__main__": + if len(sys.argv) < 2: + sys.exit(__doc__) + path = sys.argv[1] + mb = float(sys.argv[2]) if len(sys.argv) > 2 else None + data = load(path, int(mb * 1e6) if mb else None) + out = {"sampled_MB": mb or "all", "tx": {}} + for tx, d in sorted(data.items()): + if len(d["t"]) >= 50: + out["tx"][tx] = analyse(d) + print(json.dumps(out, indent=1)) diff --git a/v2/crates/wifi-densepose-mtk-bridge/README.md b/v2/crates/wifi-densepose-mtk-bridge/README.md index 24daf753d..4f0c28e06 100644 --- a/v2/crates/wifi-densepose-mtk-bridge/README.md +++ b/v2/crates/wifi-densepose-mtk-bridge/README.md @@ -3,7 +3,8 @@ Bridges MediaTek MT7981 (`mt76`) vendor CSI into RuView's ADR-267 MTC1 frames and sends them to the sensing server's UDP ingest (default `127.0.0.1:5005`). -Developed in whitsentry, a RuView-based project. +Developed in whitsentry, a RuView-based project. User guide: +[`docs/mediatek-router-csi.md`](../../../docs/mediatek-router-csi.md). MT7981B + MT7976C is the silicon in the Wavlink WL-WN586X3, and per the OpenWrt hardware tables also in the OpenWrt One and the Xiaomi AX3000T (CLAIMED: only the