feat(hardware): carry legacy and unknown PPDU types in MediaTek MTC1

A real MT7981 (WL-WN586X3, MT7981B + MT7976C) CSI report arrived with
rx_mode = MT_PHY_TYPE_OFDM for a 2.4 GHz client. The MTC1 PpduType had no
pre-HT value, so a bridge had to drop the frame. MediaTek's CSI driver
treats legacy OFDM as its own tone-mask group, and association traffic is
legacy-rate, so these frames are routine rather than a corner case.

Add PpduType::Legacy = 6 and PpduType::Unknown = 7. Values 1-5 and the
72-byte header are unchanged. A decoder built before this change rejects
6 and 7 as UnknownPpduType (fails safe, drops the frame), so MTC1
consumers need a rebuild.

ADR-267 gains Amendment 1 recording the change, its compatibility, and the
observed 1x1x64 dimensions of the motivating capture. The capture is not
in the tree: it holds a client transmitter address and raw CSI.

Developed in whitsentry, a RuView-based project.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
Mathew Beaneandclaude-flow committed 2026-10-02 18:17:36 -04:00
1 parent 03a897938f
commit ebb303cbb8
2 files changed
+119

No files matched your search

@@ -34,6 +34,81 @@ Define `MTC1` version 1 as a little-endian, self-delimiting envelope:
- Flags distinguish calibrated, saturated, time-synchronized, dropped-predecessor,
and synthetic frames. Synthetic provenance cannot be cleared by downstream code.
## Amendment 1 — PPDU types `Legacy` and `Unknown`
- **Date**: 2026-09-19
- **Status**: proposed. Demonstrated on one WL-WN586X3 capture. Not a maintainer acceptance.
- **Scope**: `PpduType` discriminants only. The 72-byte header layout, CRC,
payload ordering, flags, and caps are unchanged.
### What changed
`PpduType` gains two variants:
| Value | Name | Meaning |
|---|---|---|
| 6 | `Legacy` | Pre-HT: 802.11b CCK or 802.11a/g OFDM |
| 7 | `Unknown` | A PPDU format the producer could not classify |
Values 1-5 (`Ht`, `Vht`, `HeSu`, `HeMu`, `Eht`) are untouched.
### Why
The original set assumed every CSI report would be HT or later. The first real
MT7981 capture disproved that. A Wavlink WL-WN586X3 (MT7981B + MT7976C) running
an OpenWrt 24.10.8 image built with the patches in `firmware/openwrt-wn586x3/`
reported `rx_mode = 1` (`MT_PHY_TYPE_OFDM`) for a client associated on 2.4 GHz
channel 6 at 20 MHz. The bridge had no way to express it and dropped the frame.
That was a real data loss, not a corner case. MediaTek's CSI driver treats legacy
OFDM as a first-class mode: `mt7915_vendor_csi_tone_mask` skips only
`MT_PHY_TYPE_CCK`, and its `mode_map` is keyed by `MT_PHY_TYPE_OFDM`, `_HT`,
`_VHT` and `_HE_SU`, giving OFDM its own tone-mask group
(`1001-wifi-mt76-mt7915-csi-implement-csi-support.patch:1070-1088`). Association
and management exchanges are legacy-rate by design, so a sensing deployment will
see these continuously.
`Unknown` exists so the enum's gaps (5-7 and 12 in `mt76_phy_type`) and future
modes are carried explicitly rather than guessed at or dropped. It also covers
transports that omit the field: the MtkCSIdump UDP datagram carries no `rx_mode`,
and reporting `Unknown` there is honest where defaulting to a concrete format was
not.
### Compatibility
- **Forward, not backward.** A decoder built before this amendment rejects
discriminants 6 and 7 with `UnknownPpduType`, because the original parser
rejects unknown types by design. `TryFrom<u8>` returns `Err` rather than
panicking, so an old decoder fails safely — it just drops legacy frames, and
does so silently. **Rebuild MTC1 consumers after this codec change.**
- **Scope is narrower than "the workspace".** `wifi-densepose-hardware` defines
three independent `PpduType` enums — `csi_frame.rs` (ESP32),
`qualcomm_csi.rs`, and `mediatek_csi.rs` — and this amendment touches only the
MediaTek one. Neither of the others changes, so no other vendor bridge is
affected. The complete set of `mediatek_csi::PpduType` consumers is:
`wifi-densepose-hardware`'s own `mediatek-csi-sim` binary,
`wifi-densepose-mtk-bridge`, and `wifi-densepose-sensing-server`, which renders the
value with `{:?}` and so needed no code change. In particular the
`ppdu_type > 3` bound in `bootstrap_baseline.rs` is fed from the **ESP32**
enum via `CsiGridKey::from_frame(&Esp32Frame)`, not from MediaTek frames, so
`Legacy` and `Unknown` cannot reach it.
- Because the variants were added rather than renumbered, and Rust requires
exhaustive matches, the compiler locates any consumer that must be updated.
Nothing outside the files above needed one.
- Producers must emit 6 only for frames that genuinely are pre-HT, and 7 only
where the format is truly unclassified. Neither is a dumping ground for a
mapping the producer has not done.
- The mapping from `rx_mode` to `PpduType` is now infallible, so no MTC1 producer
may drop a frame on PPDU grounds.
- Unchanged: synthetic provenance still cannot be cleared by downstream code.
### Observed dimensions
The same capture assembled to 1x1x64. That is the client's behaviour, not a
protocol limit: the handset sent single-stream frames to a 2.4 GHz 20 MHz AP.
Expect 1x2 or 2x2 on 5 GHz, and size consumers for the chipset profile's
`max_chains` rather than for what one capture happened to show.
## Consequences
### Positive
@@ -59,3 +134,8 @@ Define `MTC1` version 1 as a little-endian, self-delimiting envelope:
- [ADR-266: MediaTek Filogic CSI platform](ADR-266-mediatek-filogic-csi-platform.md)
- [ADR-018: ESP32 binary CSI framing](ADR-018-esp32-csi-frame-protocol.md)
- [ADR-264: RTL8720F radar wire protocol](ADR-264-rtl8720f-radar-wire-protocol.md)
- Amendment 1 implementation: `v2/crates/wifi-densepose-hardware/src/mediatek_csi.rs`
(codec) and `v2/crates/wifi-densepose-mtk-bridge` (`ppdu_from_rx_mode`). The
motivating capture is a lab record and is not in this tree: it contains a
client transmitter address and raw CSI. The in-repo check is a fabricated
legacy-OFDM record in `wifi-densepose-mtk-bridge/tests/hardware_capture.rs`.
@@ -85,6 +85,17 @@ impl TryFrom<u8> for ElementFormat {
}
}
/// PPDU format a CSI report was measured on.
///
/// `Legacy` and `Unknown` were added after a real MT7981 capture (a 2.4 GHz
/// client on channel 6) reported `rx_mode = MT_PHY_TYPE_OFDM`. MediaTek's CSI
/// driver treats legacy OFDM as a first-class mode — its tone-mask table gives
/// `MT_PHY_TYPE_OFDM` its own group — so a vendor bridge must be able to carry
/// one. Without these, such a frame had to be dropped, which lost real data.
///
/// Wire note: a decoder older than this change rejects discriminants 6 and 7 as
/// `UnknownPpduType`. Producers should only emit them for frames that genuinely
/// are pre-HT or unrecognised.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum PpduType {
@@ -93,6 +104,11 @@ pub enum PpduType {
HeSu = 3,
HeMu = 4,
Eht = 5,
/// Pre-HT: 802.11b CCK or 802.11a/g OFDM.
Legacy = 6,
/// A PPDU format the producer could not classify. Carried rather than
/// guessed, so the frame survives and the ambiguity stays visible.
Unknown = 7,
}
impl TryFrom<u8> for PpduType {
@@ -104,6 +120,8 @@ impl TryFrom<u8> for PpduType {
3 => Ok(Self::HeSu),
4 => Ok(Self::HeMu),
5 => Ok(Self::Eht),
6 => Ok(Self::Legacy),
7 => Ok(Self::Unknown),
_ => Err(CsiParseError::UnknownPpduType(value)),
}
}
@@ -628,6 +646,27 @@ mod tests {
let w = f.to_bytes().unwrap();
assert_eq!(CsiFrame::from_bytes(&w).unwrap().0, f);
}
#[test]
fn legacy_and_unknown_ppdu_types_round_trip() {
// A real MT7981 capture reported MT_PHY_TYPE_OFDM, which has no HT/VHT/HE
// equivalent; these two discriminants exist so such a frame is carried
// rather than dropped.
let mut s = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
for want in [PpduType::Legacy, PpduType::Unknown] {
let mut f = s.next_frame();
f.ppdu_type = want;
let w = f.to_bytes().unwrap();
let (decoded, _) = CsiFrame::from_bytes(&w).unwrap();
assert_eq!(decoded.ppdu_type, want);
}
assert_eq!(PpduType::try_from(6u8).unwrap(), PpduType::Legacy);
assert_eq!(PpduType::try_from(7u8).unwrap(), PpduType::Unknown);
assert!(matches!(
PpduType::try_from(8u8),
Err(CsiParseError::UnknownPpduType(8))
));
}
#[test]
fn crc_corruption_is_rejected() {
let mut s = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();