feat(hardware): add MediaTek Filogic CSI simulator (#1358)
This commit is contained in:
parent
8a5af5dad4
commit
232b1c79f6
|
|
@ -83,7 +83,7 @@ This ADR covers Phase 1 (TV box as aggregator) and Phase 2 (custom WiFi firmware
|
||||||
|---------|--------|-------------|--------------|--------|
|
|---------|--------|-------------|--------------|--------|
|
||||||
| Broadcom BCM43455 | brcmfmac | **Proven** (Nexmon CSI) | Yes | Low — patches exist |
|
| Broadcom BCM43455 | brcmfmac | **Proven** (Nexmon CSI) | Yes | Low — patches exist |
|
||||||
| Realtek RTL8822CS | rtw88 | **Moderate** — driver is open-source, CSI hooks need adding | Yes (patched) | Medium |
|
| Realtek RTL8822CS | rtw88 | **Moderate** — driver is open-source, CSI hooks need adding | Yes (patched) | Medium |
|
||||||
| MediaTek MT7661 | mt76 | **Unknown** — MediaTek has released CSI tools for some chips | Yes | Medium-High |
|
| MediaTek MT7661 | mt76 | **Unverified** — no supported public CSI capture API was found in upstream `mt76` or public MediaTek SDK material | Yes | Research only |
|
||||||
|
|
||||||
2. **CSI extraction architecture** (Linux kernel driver modification):
|
2. **CSI extraction architecture** (Linux kernel driver modification):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
# ADR-266: MediaTek Filogic CSI Platform
|
||||||
|
|
||||||
|
- **Status**: accepted
|
||||||
|
- **Date**: 2026-07-18
|
||||||
|
- **Deciders**: RuView maintainers
|
||||||
|
- **Tags**: mediatek, filogic, mt76, csi, openwrt, rust
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
RuView needs a high-antenna-count, router-class Wi-Fi sensing path beyond ESP32.
|
||||||
|
MediaTek Filogic platforms are attractive because the upstream BSD-3-Clause
|
||||||
|
`mt76` driver supports MT7915/MT792x/MT7996 families and OpenWrt supports
|
||||||
|
MT7981/MT7986/MT7988 systems. The OpenWrt One (MT7981B + MT7976C) additionally
|
||||||
|
publishes schematics, platform datasheets, register documentation, serial, and
|
||||||
|
JTAG access. The BPI-R3 (MT7986 + MT7975N/P) offers dual-band 4x4 radios.
|
||||||
|
|
||||||
|
The current upstream `mt76` tree has testmode, debugfs, RX descriptors, and MCU
|
||||||
|
event plumbing, but no supported public interface for exporting per-packet
|
||||||
|
complex channel estimates. Public MediaTek SDK material likewise does not expose
|
||||||
|
an equivalent to Espressif's CSI callback. PHY computation of channel estimates
|
||||||
|
does not imply that firmware transfers those estimates to host memory.
|
||||||
|
|
||||||
|
Existing RuView documents that describe MT7661 CSI-over-UDP or released
|
||||||
|
MediaTek CSI tools are unverified architectural hypotheses, not supported
|
||||||
|
hardware claims.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. Use the OpenWrt One as the primary future hardware/upstreaming target and the
|
||||||
|
BPI-R3 as the secondary 4x4 validation target.
|
||||||
|
2. Build a Rust-first simulator and host transport before hardware arrives.
|
||||||
|
3. Keep the transport independent of private firmware structures. A future
|
||||||
|
`mt76` adapter must translate a documented kernel/firmware report into it.
|
||||||
|
4. Prefer Generic Netlink for capability/control messages and relayfs or a
|
||||||
|
bounded character-device stream if sustained CSI volume exceeds Netlink's
|
||||||
|
practical throughput.
|
||||||
|
5. Do not redistribute vendor firmware, private headers, or SDK components.
|
||||||
|
6. Label simulator frames end-to-end and never present them as physical capture.
|
||||||
|
7. Do not claim MediaTek hardware CSI support until complex CSI from a physical
|
||||||
|
device passes calibration, sequence, timestamp, and repeatability tests.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Development and integration testing can start without fabricating a vendor ABI.
|
||||||
|
- OpenWrt One provides a repairable, upstream-friendly hardware target.
|
||||||
|
- The same RuView ingestion path can accept simulator, replay, and future driver data.
|
||||||
|
- Rust bounds checking isolates untrusted kernel/network input from inference code.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- The simulator cannot prove firmware export availability or sensing accuracy.
|
||||||
|
- A firmware change or MediaTek cooperation may be required before physical CSI exists.
|
||||||
|
- Router-class builds and driver iteration are slower than MCU firmware development.
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
|
||||||
|
- NeuroPilot may later accelerate inference but is unrelated to CSI capture.
|
||||||
|
- Wi-Fi 7/MLO support remains a later phase after a single-link contract is stable.
|
||||||
|
|
||||||
|
## Hardware gates
|
||||||
|
|
||||||
|
- Identify a firmware/host report containing complex channel estimates.
|
||||||
|
- Document dimensions, quantization, chain ordering, subcarrier indexing, lifetime,
|
||||||
|
timestamps, sequence behavior, calibration, maximum size, and report rate.
|
||||||
|
- Validate OpenWrt One first, then BPI-R3 4x4, before considering MT7996/MLO.
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
- [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/)
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# ADR-267: MediaTek MIMO CSI Wire Protocol
|
||||||
|
|
||||||
|
- **Status**: accepted
|
||||||
|
- **Date**: 2026-07-18
|
||||||
|
- **Deciders**: RuView maintainers
|
||||||
|
- **Tags**: mediatek, csi, protocol, rust, udp, replay
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The MediaTek simulator, captured regression fixtures, and a future `mt76` agent
|
||||||
|
need one safe host-side representation. Copying an undocumented firmware layout
|
||||||
|
would couple RuView to a private ABI and make malformed kernel/network data risky.
|
||||||
|
MIMO CSI also requires explicit Tx/Rx/subcarrier dimensions and per-Rx-chain RSSI.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Define `MTC1` version 1 as a little-endian, self-delimiting envelope:
|
||||||
|
|
||||||
|
- 72-byte fixed header with magic, version, report kind, total length, sequence,
|
||||||
|
monotonic timestamp, device ID, chipset profile, frequency, bandwidth, flags,
|
||||||
|
Tx/Rx dimensions, numeric format, PPDU type, subcarrier count, noise floor,
|
||||||
|
scale, subcarrier spacing, calibration ID, and payload length.
|
||||||
|
- CSI payload begins with one signed RSSI byte per Rx chain, followed by
|
||||||
|
`tx_count * rx_count * subcarrier_count` complex values in Tx-major,
|
||||||
|
Rx-major, subcarrier-major order.
|
||||||
|
- Supported numeric formats are complex signed i16 and complex finite f32.
|
||||||
|
- Capability reports use bounded opaque TLVs until a public driver contract exists.
|
||||||
|
- CRC-32/IEEE covers header and payload; the final four bytes carry the checksum.
|
||||||
|
- One envelope maps to one UDP datagram, capped at the IPv4 UDP payload maximum
|
||||||
|
of 65,507 bytes. Replay files prefix each envelope with a little-endian `u32`.
|
||||||
|
- Parsers reject unknown versions/types/formats, invalid dimensions/bandwidth,
|
||||||
|
multiplication overflow, inconsistent payload lengths, non-finite floats,
|
||||||
|
bad CRC, trailing datagram bytes, and frames above the cap.
|
||||||
|
- Flags distinguish calibrated, saturated, time-synchronized, dropped-predecessor,
|
||||||
|
and synthetic frames. Synthetic provenance cannot be cleared by downstream code.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Deterministic simulator and future hardware use identical parsing and APIs.
|
||||||
|
- Explicit dimensions prevent ambiguous antenna or subcarrier interpretation.
|
||||||
|
- CRC, finite-value checks, and hard caps make network/replay ingestion robust.
|
||||||
|
- The format supports MT7981, MT7986, and MT7996 profiles without claiming their
|
||||||
|
undocumented firmware layouts.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- A translation/copy step is required from a future kernel report.
|
||||||
|
- Maximum-size Wi-Fi 7 matrices may need segmentation in a later protocol version.
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
|
||||||
|
- Version 1 models one link per report; MLO correlation is a future extension.
|
||||||
|
- Capability TLVs are intentionally conservative until hardware metadata is known.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [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)
|
||||||
|
|
@ -425,7 +425,7 @@ pub enum WifiChipset {
|
||||||
BroadcomBcm43455,
|
BroadcomBcm43455,
|
||||||
/// Realtek RTL8822CS via modified rtw88 driver.
|
/// Realtek RTL8822CS via modified rtw88 driver.
|
||||||
RealtekRtl8822cs,
|
RealtekRtl8822cs,
|
||||||
/// MediaTek MT7661 via mt76 driver modification.
|
/// Proposed MediaTek MT7661 research target; no public CSI export is verified.
|
||||||
MediatekMt7661,
|
MediatekMt7661,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,7 +455,7 @@ pub struct Esp32CompatFrame {
|
||||||
```
|
```
|
||||||
|
|
||||||
**Domain Services:**
|
**Domain Services:**
|
||||||
- `CsiExtractionService` — Reads raw CSI from patched driver via Netlink socket (BCM43455), procfs (RTL8822CS), or UDP (MT7661)
|
- `CsiExtractionService` — Reads raw CSI from a validated chipset adapter. Nexmon/BCM43455 is the established Linux example; RTL8822CS and MT7661 remain unverified research targets and must not be advertised as working capture paths.
|
||||||
- `SubcarrierResamplerService` — Resamples chipset-specific subcarrier counts to match ESP32 format (e.g., 256 → 128 via decimation or interpolation)
|
- `SubcarrierResamplerService` — Resamples chipset-specific subcarrier counts to match ESP32 format (e.g., 256 → 128 via decimation or interpolation)
|
||||||
- `ProtocolTranslatorService` — Converts `ChipsetCsiFrame` to `Esp32CompatFrame` with ADR-018 binary encoding
|
- `ProtocolTranslatorService` — Converts `ChipsetCsiFrame` to `Esp32CompatFrame` with ADR-018 binary encoding
|
||||||
- `CalibrationService` — Compensates for chipset-specific phase offsets, antenna spacing, and gain differences relative to ESP32 CSI
|
- `CalibrationService` — Compensates for chipset-specific phase offsets, antenna spacing, and gain differences relative to ESP32 CSI
|
||||||
|
|
@ -625,7 +625,7 @@ pub struct EspNodeConnection {
|
||||||
|
|
||||||
### ESP32 Protocol ACL (CSI Bridge)
|
### ESP32 Protocol ACL (CSI Bridge)
|
||||||
|
|
||||||
The WiFi CSI Bridge translates chipset-specific CSI formats (Nexmon, rtw88, mt76) into the ESP32 binary protocol (ADR-018). The sensing server never knows whether frames came from a real ESP32 or a TV box WiFi chipset. Virtual node IDs (200-254) prevent collision with physical ESP32 IDs but are otherwise treated identically by the ingestion context.
|
The WiFi CSI Bridge translates validated chipset-specific CSI formats into a versioned RuView envelope. Nexmon is the established Linux example; rtw88 and mt76 require a verified complex-CSI export before implementation. Virtual node IDs (200-254) prevent collision with physical ESP32 IDs but are otherwise treated identically by the ingestion context.
|
||||||
|
|
||||||
### Armbian Platform ACL
|
### Armbian Platform ACL
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# RuView v0.9.1-mediatek-beta.1
|
||||||
|
|
||||||
|
This simulator-first beta adds a Rust MediaTek Filogic MIMO CSI transport and
|
||||||
|
RuView ingestion path while preserving the boundary between demonstrated host
|
||||||
|
integration and unavailable physical CSI export.
|
||||||
|
|
||||||
|
## Included
|
||||||
|
|
||||||
|
- ADR-266 selects OpenWrt One (MT7981/MT7976) as the primary future hardware
|
||||||
|
target and BPI-R3 (MT7986/MT7975) as the secondary 4x4 target.
|
||||||
|
- ADR-267 defines the bounded, versioned, CRC-protected `MTC1` wire protocol.
|
||||||
|
- `mediatek-csi-sim` provides deterministic MT7981, MT7986, and MT7996 profiles,
|
||||||
|
complex MIMO CSI, per-chain RSSI, UDP streaming, and replay output.
|
||||||
|
- RuView validates MediaTek datagrams, publishes bounded WebSocket summaries,
|
||||||
|
and exposes `/api/v1/csi/mediatek/latest`.
|
||||||
|
- `mediatek:simulated` provenance is retained end to end.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Codec round trips, deterministic output, corruption/truncation rejection,
|
||||||
|
dimension limits, finite-value enforcement, and prefix parsing are tested.
|
||||||
|
- All hardware and sensing-server regression tests pass.
|
||||||
|
- All three profiles were streamed over loopback UDP and verified through the
|
||||||
|
RuView REST API.
|
||||||
|
|
||||||
|
## Hardware boundary
|
||||||
|
|
||||||
|
Upstream `mt76` and public MediaTek SDK material do not currently expose a
|
||||||
|
supported raw complex CSI API. This release does not redistribute private SDK
|
||||||
|
material, invent a firmware ABI, or claim physical MediaTek capture. Hardware
|
||||||
|
support requires a documented firmware/driver channel-estimate export followed
|
||||||
|
by calibration and repeatability validation.
|
||||||
|
|
@ -28,6 +28,21 @@ cargo run -p wifi-densepose-hardware --bin rtl8720f-sim -- `
|
||||||
Add `--udp 127.0.0.1:5005 --realtime` to stream one ADR-264 frame per UDP datagram. Replay files
|
Add `--udp 127.0.0.1:5005 --realtime` to stream one ADR-264 frame per UDP datagram. Replay files
|
||||||
contain a little-endian `u32` frame length followed by the encoded frame.
|
contain a little-endian `u32` frame length followed by the encoded frame.
|
||||||
|
|
||||||
|
## MediaTek Filogic CSI simulator (ADR-266/267)
|
||||||
|
|
||||||
|
The Rust-only simulator models bounded MIMO CSI for MT7981/MT7976,
|
||||||
|
MT7986/MT7975, and MT7988/MT7996 profiles without claiming an undocumented
|
||||||
|
MediaTek firmware ABI. Every frame is marked `SYNTHETIC`.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo run -p wifi-densepose-hardware --bin mediatek-csi-sim -- `
|
||||||
|
--profile mt7981 --frames 100 --output mediatek-synthetic.mtc
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--udp 127.0.0.1:5005 --realtime` to stream one CRC-protected ADR-267
|
||||||
|
frame per UDP datagram. Physical support remains gated on a documented `mt76`
|
||||||
|
or MediaTek firmware channel-estimate export.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **ESP32 binary parser** -- Parses ADR-018 binary CSI frames streamed over UDP from ESP32 and
|
- **ESP32 binary parser** -- Parses ADR-018 binary CSI frames streamed over UDP from ESP32 and
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
//! Deterministic MediaTek Filogic MIMO CSI simulator (ADR-266/267).
|
||||||
|
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{self, Write},
|
||||||
|
net::{SocketAddr, UdpSocket},
|
||||||
|
path::PathBuf,
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use wifi_densepose_hardware::mediatek_csi::{
|
||||||
|
simulator::{MediatekCsiSimulator, SimulatorConfig},
|
||||||
|
ChipsetProfile, CsiFrame,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||||
|
enum Profile {
|
||||||
|
Mt7981,
|
||||||
|
Mt7986,
|
||||||
|
Mt7996,
|
||||||
|
}
|
||||||
|
impl Profile {
|
||||||
|
fn chipset(self) -> ChipsetProfile {
|
||||||
|
match self {
|
||||||
|
Self::Mt7981 => ChipsetProfile::Mt7981Mt7976,
|
||||||
|
Self::Mt7986 => ChipsetProfile::Mt7986Mt7975,
|
||||||
|
Self::Mt7996 => ChipsetProfile::Mt7988Mt7996,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn default_chains(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Mt7981 => 3,
|
||||||
|
Self::Mt7986 | Self::Mt7996 => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "mediatek-csi-sim",
|
||||||
|
about = "Emit synthetic ADR-267 MediaTek Filogic MIMO CSI frames"
|
||||||
|
)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(long, value_enum, default_value_t=Profile::Mt7981)]
|
||||||
|
profile: Profile,
|
||||||
|
#[arg(long, default_value_t = 100)]
|
||||||
|
frames: u32,
|
||||||
|
#[arg(long, default_value="0x4d544b4353490001", value_parser=parse_u64)]
|
||||||
|
seed: u64,
|
||||||
|
#[arg(long, default_value_t = 80)]
|
||||||
|
bandwidth: u16,
|
||||||
|
#[arg(long, default_value_t = 2)]
|
||||||
|
tx: u8,
|
||||||
|
#[arg(long)]
|
||||||
|
rx: Option<u8>,
|
||||||
|
#[arg(long, default_value_t = 256)]
|
||||||
|
subcarriers: u16,
|
||||||
|
#[arg(long, default_value_t = 20)]
|
||||||
|
interval_ms: u64,
|
||||||
|
#[arg(long)]
|
||||||
|
udp: Option<SocketAddr>,
|
||||||
|
/// Replay: little-endian u32 length followed by one ADR-267 envelope.
|
||||||
|
#[arg(long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
#[arg(long)]
|
||||||
|
realtime: bool,
|
||||||
|
}
|
||||||
|
fn parse_u64(v: &str) -> Result<u64, String> {
|
||||||
|
if let Some(h) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
|
||||||
|
u64::from_str_radix(h, 16).map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
v.parse()
|
||||||
|
.map_err(|e: std::num::ParseIntError| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn emit(
|
||||||
|
frame: CsiFrame,
|
||||||
|
socket: Option<&UdpSocket>,
|
||||||
|
destination: Option<SocketAddr>,
|
||||||
|
output: &mut Option<File>,
|
||||||
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
|
let wire = frame.to_bytes()?;
|
||||||
|
if let (Some(s), Some(d)) = (socket, destination) {
|
||||||
|
if s.send_to(&wire, d)? != wire.len() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::WriteZero, "partial UDP datagram").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(f) = output {
|
||||||
|
f.write_all(&(wire.len() as u32).to_le_bytes())?;
|
||||||
|
f.write_all(&wire)?;
|
||||||
|
}
|
||||||
|
Ok(wire.len())
|
||||||
|
}
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let a = Args::parse();
|
||||||
|
if a.udp.is_none() && a.output.is_none() {
|
||||||
|
return Err("select at least one sink with --udp or --output".into());
|
||||||
|
}
|
||||||
|
let cfg = SimulatorConfig {
|
||||||
|
seed: a.seed,
|
||||||
|
chipset: a.profile.chipset(),
|
||||||
|
bandwidth_mhz: a.bandwidth,
|
||||||
|
tx_count: a.tx,
|
||||||
|
rx_count: a.rx.unwrap_or_else(|| a.profile.default_chains()),
|
||||||
|
subcarriers: a.subcarriers,
|
||||||
|
frame_period_us: a.interval_ms * 1000,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut sim = MediatekCsiSimulator::new(cfg)?;
|
||||||
|
let socket = a.udp.map(|_| UdpSocket::bind("0.0.0.0:0")).transpose()?;
|
||||||
|
let mut output = a.output.as_ref().map(File::create).transpose()?;
|
||||||
|
let mut bytes = emit(
|
||||||
|
sim.capabilities_frame(),
|
||||||
|
socket.as_ref(),
|
||||||
|
a.udp,
|
||||||
|
&mut output,
|
||||||
|
)?;
|
||||||
|
for _ in 0..a.frames {
|
||||||
|
bytes += emit(sim.next_frame(), socket.as_ref(), a.udp, &mut output)?;
|
||||||
|
if a.realtime {
|
||||||
|
thread::sleep(Duration::from_millis(a.interval_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"emitted {} synthetic MediaTek CSI frames ({} bytes, profile={}, seed={:#x})",
|
||||||
|
a.frames + 1,
|
||||||
|
bytes,
|
||||||
|
a.profile.chipset().name(),
|
||||||
|
a.seed
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,8 @@ pub mod sync_packet;
|
||||||
// coordinator-node Rust code drive the controller stack without
|
// coordinator-node Rust code drive the controller stack without
|
||||||
// touching any downstream signal/ruvector/train/mat crate.
|
// touching any downstream signal/ruvector/train/mat crate.
|
||||||
pub mod radio_ops;
|
pub mod radio_ops;
|
||||||
|
/// ADR-267 vendor-neutral MediaTek Filogic MIMO CSI framing and simulator.
|
||||||
|
pub mod mediatek_csi;
|
||||||
/// ADR-264 host-side framing for Realtek RTL8720F CFR and FMCW radar reports.
|
/// ADR-264 host-side framing for Realtek RTL8720F CFR and FMCW radar reports.
|
||||||
/// This module has no dependency on the vendor SDK.
|
/// This module has no dependency on the vendor SDK.
|
||||||
pub mod rtl8720f;
|
pub mod rtl8720f;
|
||||||
|
|
@ -73,6 +75,13 @@ pub use radio_ops::{
|
||||||
RadioError, RadioHealth, RadioMode, RadioOps, MESH_HEADER_SIZE, MESH_MAGIC, MESH_MAX_PAYLOAD,
|
RadioError, RadioHealth, RadioMode, RadioOps, MESH_HEADER_SIZE, MESH_MAGIC, MESH_MAX_PAYLOAD,
|
||||||
MESH_VERSION,
|
MESH_VERSION,
|
||||||
};
|
};
|
||||||
|
pub use mediatek_csi::{
|
||||||
|
ChipsetProfile as MediatekChipsetProfile, CsiFlags as MediatekCsiFlags,
|
||||||
|
CsiFrame as MediatekCsiFrame, CsiParseError as MediatekCsiParseError,
|
||||||
|
CsiPayload as MediatekCsiPayload, ElementFormat as MediatekElementFormat,
|
||||||
|
PpduType as MediatekPpduType, ReportKind as MediatekReportKind,
|
||||||
|
MEDIATEK_CSI_HEADER_LEN, MEDIATEK_CSI_MAGIC, MEDIATEK_CSI_VERSION,
|
||||||
|
};
|
||||||
pub use rtl8720f::{
|
pub use rtl8720f::{
|
||||||
ElementFormat as Rtl8720fElementFormat, RadarFlags as Rtl8720fRadarFlags,
|
ElementFormat as Rtl8720fElementFormat, RadarFlags as Rtl8720fRadarFlags,
|
||||||
RadarFrame as Rtl8720fRadarFrame, RadarParseError as Rtl8720fRadarParseError,
|
RadarFrame as Rtl8720fRadarFrame, RadarParseError as Rtl8720fRadarParseError,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,680 @@
|
||||||
|
//! Vendor-neutral MediaTek Filogic MIMO CSI transport and deterministic simulator.
|
||||||
|
//! This is not a MediaTek firmware ABI; see ADR-266/267.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub const MEDIATEK_CSI_MAGIC: u32 = 0x3143_544d; // "MTC1" little endian
|
||||||
|
pub const MEDIATEK_CSI_VERSION: u8 = 1;
|
||||||
|
pub const MEDIATEK_CSI_HEADER_LEN: usize = 72;
|
||||||
|
pub const MEDIATEK_CSI_CRC_LEN: usize = 4;
|
||||||
|
pub const MEDIATEK_CSI_MAX_FRAME_LEN: usize = 65_507;
|
||||||
|
pub const MEDIATEK_CSI_MAX_ELEMENTS: usize = 16_384;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum ReportKind {
|
||||||
|
Csi = 1,
|
||||||
|
Capabilities = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for ReportKind {
|
||||||
|
type Error = CsiParseError;
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Self::Csi),
|
||||||
|
2 => Ok(Self::Capabilities),
|
||||||
|
_ => Err(CsiParseError::UnknownReportKind(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum ChipsetProfile {
|
||||||
|
Mt7981Mt7976 = 1,
|
||||||
|
Mt7986Mt7975 = 2,
|
||||||
|
Mt7988Mt7996 = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u16> for ChipsetProfile {
|
||||||
|
type Error = CsiParseError;
|
||||||
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Self::Mt7981Mt7976),
|
||||||
|
2 => Ok(Self::Mt7986Mt7975),
|
||||||
|
3 => Ok(Self::Mt7988Mt7996),
|
||||||
|
_ => Err(CsiParseError::UnknownChipset(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChipsetProfile {
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mt7981Mt7976 => "mt7981-mt7976",
|
||||||
|
Self::Mt7986Mt7975 => "mt7986-mt7975",
|
||||||
|
Self::Mt7988Mt7996 => "mt7988-mt7996",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn max_chains(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Mt7981Mt7976 => 3,
|
||||||
|
Self::Mt7986Mt7975 | Self::Mt7988Mt7996 => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum ElementFormat {
|
||||||
|
ComplexI16 = 1,
|
||||||
|
ComplexF32 = 2,
|
||||||
|
Bytes = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for ElementFormat {
|
||||||
|
type Error = CsiParseError;
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Self::ComplexI16),
|
||||||
|
2 => Ok(Self::ComplexF32),
|
||||||
|
3 => Ok(Self::Bytes),
|
||||||
|
_ => Err(CsiParseError::UnknownElementFormat(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum PpduType {
|
||||||
|
Ht = 1,
|
||||||
|
Vht = 2,
|
||||||
|
HeSu = 3,
|
||||||
|
HeMu = 4,
|
||||||
|
Eht = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for PpduType {
|
||||||
|
type Error = CsiParseError;
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Self::Ht),
|
||||||
|
2 => Ok(Self::Vht),
|
||||||
|
3 => Ok(Self::HeSu),
|
||||||
|
4 => Ok(Self::HeMu),
|
||||||
|
5 => Ok(Self::Eht),
|
||||||
|
_ => Err(CsiParseError::UnknownPpduType(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CsiFlags(pub u16);
|
||||||
|
|
||||||
|
impl CsiFlags {
|
||||||
|
pub const CALIBRATED: u16 = 1 << 0;
|
||||||
|
pub const SATURATED: u16 = 1 << 1;
|
||||||
|
pub const TIME_SYNCHRONIZED: u16 = 1 << 2;
|
||||||
|
pub const DROPPED_PREDECESSOR: u16 = 1 << 3;
|
||||||
|
pub const SYNTHETIC: u16 = 1 << 15;
|
||||||
|
pub fn contains(self, flag: u16) -> bool {
|
||||||
|
self.0 & flag != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum CsiPayload {
|
||||||
|
ComplexI16 {
|
||||||
|
rssi_dbm: Vec<i8>,
|
||||||
|
values: Vec<[i16; 2]>,
|
||||||
|
},
|
||||||
|
ComplexF32 {
|
||||||
|
rssi_dbm: Vec<i8>,
|
||||||
|
values: Vec<[f32; 2]>,
|
||||||
|
},
|
||||||
|
Bytes(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsiPayload {
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::ComplexI16 { values, .. } => values.len(),
|
||||||
|
Self::ComplexF32 { values, .. } => values.len(),
|
||||||
|
Self::Bytes(values) => values.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
pub fn rssi_dbm(&self) -> &[i8] {
|
||||||
|
match self {
|
||||||
|
Self::ComplexI16 { rssi_dbm, .. } | Self::ComplexF32 { rssi_dbm, .. } => rssi_dbm,
|
||||||
|
Self::Bytes(_) => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn format(&self) -> ElementFormat {
|
||||||
|
match self {
|
||||||
|
Self::ComplexI16 { .. } => ElementFormat::ComplexI16,
|
||||||
|
Self::ComplexF32 { .. } => ElementFormat::ComplexF32,
|
||||||
|
Self::Bytes(_) => ElementFormat::Bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn encoded_len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::ComplexI16 { rssi_dbm, values } => rssi_dbm.len() + values.len() * 4,
|
||||||
|
Self::ComplexF32 { rssi_dbm, values } => rssi_dbm.len() + values.len() * 8,
|
||||||
|
Self::Bytes(values) => values.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct CsiFrame {
|
||||||
|
pub report_kind: ReportKind,
|
||||||
|
pub sequence: u32,
|
||||||
|
pub timestamp_us: u64,
|
||||||
|
pub device_id: u64,
|
||||||
|
pub chipset: ChipsetProfile,
|
||||||
|
pub bandwidth_mhz: u16,
|
||||||
|
pub center_freq_khz: u32,
|
||||||
|
pub flags: CsiFlags,
|
||||||
|
pub tx_count: u8,
|
||||||
|
pub rx_count: u8,
|
||||||
|
pub ppdu_type: PpduType,
|
||||||
|
pub subcarrier_count: u16,
|
||||||
|
pub noise_floor_dbm: i8,
|
||||||
|
pub scale: f32,
|
||||||
|
pub subcarrier_spacing_hz: f32,
|
||||||
|
pub calibration_id: u32,
|
||||||
|
pub payload: CsiPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsiFrame {
|
||||||
|
pub fn to_bytes(&self) -> Result<Vec<u8>, CsiParseError> {
|
||||||
|
self.validate()?;
|
||||||
|
let payload_len = self.payload.encoded_len();
|
||||||
|
let frame_len = MEDIATEK_CSI_HEADER_LEN
|
||||||
|
.checked_add(payload_len)
|
||||||
|
.and_then(|n| n.checked_add(MEDIATEK_CSI_CRC_LEN))
|
||||||
|
.ok_or(CsiParseError::LengthOverflow)?;
|
||||||
|
if frame_len > MEDIATEK_CSI_MAX_FRAME_LEN {
|
||||||
|
return Err(CsiParseError::FrameTooLarge(frame_len));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(frame_len);
|
||||||
|
out.extend_from_slice(&MEDIATEK_CSI_MAGIC.to_le_bytes());
|
||||||
|
out.push(MEDIATEK_CSI_VERSION);
|
||||||
|
out.push(self.report_kind as u8);
|
||||||
|
out.extend_from_slice(&(MEDIATEK_CSI_HEADER_LEN as u16).to_le_bytes());
|
||||||
|
out.extend_from_slice(&(frame_len as u32).to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.sequence.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.timestamp_us.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.device_id.to_le_bytes());
|
||||||
|
out.extend_from_slice(&(self.chipset as u16).to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.bandwidth_mhz.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.center_freq_khz.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.flags.0.to_le_bytes());
|
||||||
|
out.push(self.tx_count);
|
||||||
|
out.push(self.rx_count);
|
||||||
|
out.push(self.payload.format() as u8);
|
||||||
|
out.push(self.ppdu_type as u8);
|
||||||
|
out.extend_from_slice(&self.subcarrier_count.to_le_bytes());
|
||||||
|
out.push(self.payload.rssi_dbm().len() as u8);
|
||||||
|
out.push(self.noise_floor_dbm as u8);
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.scale.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.subcarrier_spacing_hz.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.calibration_id.to_le_bytes());
|
||||||
|
out.extend_from_slice(&(payload_len as u32).to_le_bytes());
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
debug_assert_eq!(out.len(), MEDIATEK_CSI_HEADER_LEN);
|
||||||
|
match &self.payload {
|
||||||
|
CsiPayload::ComplexI16 { rssi_dbm, values } => {
|
||||||
|
out.extend(rssi_dbm.iter().map(|v| *v as u8));
|
||||||
|
for [i, q] in values {
|
||||||
|
out.extend_from_slice(&i.to_le_bytes());
|
||||||
|
out.extend_from_slice(&q.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CsiPayload::ComplexF32 { rssi_dbm, values } => {
|
||||||
|
out.extend(rssi_dbm.iter().map(|v| *v as u8));
|
||||||
|
for [i, q] in values {
|
||||||
|
out.extend_from_slice(&i.to_le_bytes());
|
||||||
|
out.extend_from_slice(&q.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CsiPayload::Bytes(values) => out.extend_from_slice(values),
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&crc32_ieee(&out).to_le_bytes());
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(input: &[u8]) -> Result<(Self, usize), CsiParseError> {
|
||||||
|
if input.len() < MEDIATEK_CSI_HEADER_LEN {
|
||||||
|
return Err(CsiParseError::InsufficientData {
|
||||||
|
needed: MEDIATEK_CSI_HEADER_LEN,
|
||||||
|
got: input.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let magic = u32_at(input, 0);
|
||||||
|
if magic != MEDIATEK_CSI_MAGIC {
|
||||||
|
return Err(CsiParseError::InvalidMagic(magic));
|
||||||
|
}
|
||||||
|
if input[4] != MEDIATEK_CSI_VERSION {
|
||||||
|
return Err(CsiParseError::UnsupportedVersion(input[4]));
|
||||||
|
}
|
||||||
|
let report_kind = ReportKind::try_from(input[5])?;
|
||||||
|
let header_len = u16_at(input, 6) as usize;
|
||||||
|
if header_len != MEDIATEK_CSI_HEADER_LEN {
|
||||||
|
return Err(CsiParseError::InvalidHeaderLength(header_len));
|
||||||
|
}
|
||||||
|
let frame_len = u32_at(input, 8) as usize;
|
||||||
|
if frame_len > MEDIATEK_CSI_MAX_FRAME_LEN {
|
||||||
|
return Err(CsiParseError::FrameTooLarge(frame_len));
|
||||||
|
}
|
||||||
|
if frame_len < header_len + MEDIATEK_CSI_CRC_LEN {
|
||||||
|
return Err(CsiParseError::InvalidFrameLength(frame_len));
|
||||||
|
}
|
||||||
|
if input.len() < frame_len {
|
||||||
|
return Err(CsiParseError::InsufficientData {
|
||||||
|
needed: frame_len,
|
||||||
|
got: input.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let expected_crc = u32_at(input, frame_len - 4);
|
||||||
|
let actual_crc = crc32_ieee(&input[..frame_len - 4]);
|
||||||
|
if expected_crc != actual_crc {
|
||||||
|
return Err(CsiParseError::CrcMismatch {
|
||||||
|
expected: expected_crc,
|
||||||
|
actual: actual_crc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let chipset = ChipsetProfile::try_from(u16_at(input, 32))?;
|
||||||
|
let format = ElementFormat::try_from(input[44])?;
|
||||||
|
let ppdu_type = PpduType::try_from(input[45])?;
|
||||||
|
let tx_count = input[42];
|
||||||
|
let rx_count = input[43];
|
||||||
|
let subcarrier_count = u16_at(input, 46);
|
||||||
|
let rssi_count = input[48] as usize;
|
||||||
|
let payload_len = u32_at(input, 64) as usize;
|
||||||
|
if header_len + payload_len + 4 != frame_len {
|
||||||
|
return Err(CsiParseError::PayloadLengthMismatch);
|
||||||
|
}
|
||||||
|
let payload_bytes = &input[header_len..header_len + payload_len];
|
||||||
|
let elements = (tx_count as usize)
|
||||||
|
.checked_mul(rx_count as usize)
|
||||||
|
.and_then(|n| n.checked_mul(subcarrier_count as usize))
|
||||||
|
.ok_or(CsiParseError::LengthOverflow)?;
|
||||||
|
let payload = match format {
|
||||||
|
ElementFormat::Bytes => CsiPayload::Bytes(payload_bytes.to_vec()),
|
||||||
|
ElementFormat::ComplexI16 => {
|
||||||
|
if rssi_count > payload_bytes.len()
|
||||||
|
|| payload_bytes.len() - rssi_count != elements * 4
|
||||||
|
{
|
||||||
|
return Err(CsiParseError::PayloadLengthMismatch);
|
||||||
|
}
|
||||||
|
let rssi_dbm = payload_bytes[..rssi_count]
|
||||||
|
.iter()
|
||||||
|
.map(|v| *v as i8)
|
||||||
|
.collect();
|
||||||
|
let values = payload_bytes[rssi_count..]
|
||||||
|
.chunks_exact(4)
|
||||||
|
.map(|b| {
|
||||||
|
[
|
||||||
|
i16::from_le_bytes([b[0], b[1]]),
|
||||||
|
i16::from_le_bytes([b[2], b[3]]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
CsiPayload::ComplexI16 { rssi_dbm, values }
|
||||||
|
}
|
||||||
|
ElementFormat::ComplexF32 => {
|
||||||
|
if rssi_count > payload_bytes.len()
|
||||||
|
|| payload_bytes.len() - rssi_count != elements * 8
|
||||||
|
{
|
||||||
|
return Err(CsiParseError::PayloadLengthMismatch);
|
||||||
|
}
|
||||||
|
let rssi_dbm = payload_bytes[..rssi_count]
|
||||||
|
.iter()
|
||||||
|
.map(|v| *v as i8)
|
||||||
|
.collect();
|
||||||
|
let mut values = Vec::with_capacity(elements);
|
||||||
|
for b in payload_bytes[rssi_count..].chunks_exact(8) {
|
||||||
|
let i = f32::from_le_bytes(b[0..4].try_into().unwrap());
|
||||||
|
let q = f32::from_le_bytes(b[4..8].try_into().unwrap());
|
||||||
|
if !i.is_finite() || !q.is_finite() {
|
||||||
|
return Err(CsiParseError::NonFiniteValue);
|
||||||
|
}
|
||||||
|
values.push([i, q]);
|
||||||
|
}
|
||||||
|
CsiPayload::ComplexF32 { rssi_dbm, values }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let frame = Self {
|
||||||
|
report_kind,
|
||||||
|
sequence: u32_at(input, 12),
|
||||||
|
timestamp_us: u64_at(input, 16),
|
||||||
|
device_id: u64_at(input, 24),
|
||||||
|
chipset,
|
||||||
|
bandwidth_mhz: u16_at(input, 34),
|
||||||
|
center_freq_khz: u32_at(input, 36),
|
||||||
|
flags: CsiFlags(u16_at(input, 40)),
|
||||||
|
tx_count,
|
||||||
|
rx_count,
|
||||||
|
ppdu_type,
|
||||||
|
subcarrier_count,
|
||||||
|
noise_floor_dbm: input[49] as i8,
|
||||||
|
scale: f32_at(input, 52),
|
||||||
|
subcarrier_spacing_hz: f32_at(input, 56),
|
||||||
|
calibration_id: u32_at(input, 60),
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
frame.validate()?;
|
||||||
|
Ok((frame, frame_len))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> Result<(), CsiParseError> {
|
||||||
|
if !matches!(self.bandwidth_mhz, 20 | 40 | 80 | 160) {
|
||||||
|
return Err(CsiParseError::InvalidBandwidth(self.bandwidth_mhz));
|
||||||
|
}
|
||||||
|
if self.tx_count == 0
|
||||||
|
|| self.rx_count == 0
|
||||||
|
|| self.tx_count > self.chipset.max_chains()
|
||||||
|
|| self.rx_count > self.chipset.max_chains()
|
||||||
|
{
|
||||||
|
return Err(CsiParseError::InvalidDimensions);
|
||||||
|
}
|
||||||
|
if !self.scale.is_finite()
|
||||||
|
|| self.scale <= 0.0
|
||||||
|
|| !self.subcarrier_spacing_hz.is_finite()
|
||||||
|
|| self.subcarrier_spacing_hz <= 0.0
|
||||||
|
{
|
||||||
|
return Err(CsiParseError::NonFiniteValue);
|
||||||
|
}
|
||||||
|
match (&self.report_kind, &self.payload) {
|
||||||
|
(ReportKind::Csi, CsiPayload::ComplexI16 { rssi_dbm, values }) => {
|
||||||
|
self.validate_csi(rssi_dbm, values.len())
|
||||||
|
}
|
||||||
|
(ReportKind::Csi, CsiPayload::ComplexF32 { rssi_dbm, values }) => {
|
||||||
|
if !values.iter().flatten().all(|v| v.is_finite()) {
|
||||||
|
return Err(CsiParseError::NonFiniteValue);
|
||||||
|
}
|
||||||
|
self.validate_csi(rssi_dbm, values.len())
|
||||||
|
}
|
||||||
|
(ReportKind::Capabilities, CsiPayload::Bytes(v)) if !v.is_empty() => Ok(()),
|
||||||
|
_ => Err(CsiParseError::PayloadTypeMismatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn validate_csi(&self, rssi: &[i8], values: usize) -> Result<(), CsiParseError> {
|
||||||
|
let expected =
|
||||||
|
self.tx_count as usize * self.rx_count as usize * self.subcarrier_count as usize;
|
||||||
|
if expected == 0 || expected > MEDIATEK_CSI_MAX_ELEMENTS {
|
||||||
|
return Err(CsiParseError::InvalidDimensions);
|
||||||
|
}
|
||||||
|
if values != expected || rssi.len() != self.rx_count as usize {
|
||||||
|
return Err(CsiParseError::PayloadLengthMismatch);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, PartialEq)]
|
||||||
|
pub enum CsiParseError {
|
||||||
|
#[error("insufficient data: needed {needed}, got {got}")]
|
||||||
|
InsufficientData { needed: usize, got: usize },
|
||||||
|
#[error("invalid magic {0:#010x}")]
|
||||||
|
InvalidMagic(u32),
|
||||||
|
#[error("unsupported version {0}")]
|
||||||
|
UnsupportedVersion(u8),
|
||||||
|
#[error("unknown report kind {0}")]
|
||||||
|
UnknownReportKind(u8),
|
||||||
|
#[error("unknown chipset profile {0}")]
|
||||||
|
UnknownChipset(u16),
|
||||||
|
#[error("unknown element format {0}")]
|
||||||
|
UnknownElementFormat(u8),
|
||||||
|
#[error("unknown PPDU type {0}")]
|
||||||
|
UnknownPpduType(u8),
|
||||||
|
#[error("invalid header length {0}")]
|
||||||
|
InvalidHeaderLength(usize),
|
||||||
|
#[error("invalid frame length {0}")]
|
||||||
|
InvalidFrameLength(usize),
|
||||||
|
#[error("frame too large: {0}")]
|
||||||
|
FrameTooLarge(usize),
|
||||||
|
#[error("length arithmetic overflow")]
|
||||||
|
LengthOverflow,
|
||||||
|
#[error("payload length mismatch")]
|
||||||
|
PayloadLengthMismatch,
|
||||||
|
#[error("payload type does not match report kind")]
|
||||||
|
PayloadTypeMismatch,
|
||||||
|
#[error("invalid MIMO dimensions")]
|
||||||
|
InvalidDimensions,
|
||||||
|
#[error("invalid bandwidth {0} MHz")]
|
||||||
|
InvalidBandwidth(u16),
|
||||||
|
#[error("non-finite or non-positive numeric metadata/value")]
|
||||||
|
NonFiniteValue,
|
||||||
|
#[error("CRC mismatch: expected {expected:#010x}, actual {actual:#010x}")]
|
||||||
|
CrcMismatch { expected: u32, actual: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod simulator {
|
||||||
|
use super::*;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SimulatorConfig {
|
||||||
|
pub seed: u64,
|
||||||
|
pub device_id: u64,
|
||||||
|
pub chipset: ChipsetProfile,
|
||||||
|
pub bandwidth_mhz: u16,
|
||||||
|
pub center_freq_khz: u32,
|
||||||
|
pub tx_count: u8,
|
||||||
|
pub rx_count: u8,
|
||||||
|
pub subcarriers: u16,
|
||||||
|
pub frame_period_us: u64,
|
||||||
|
}
|
||||||
|
impl Default for SimulatorConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
seed: 0x4d54_4b43_5349_0001,
|
||||||
|
device_id: 0x4f57_5254_4d54_4b31,
|
||||||
|
chipset: ChipsetProfile::Mt7981Mt7976,
|
||||||
|
bandwidth_mhz: 80,
|
||||||
|
center_freq_khz: 5_210_000,
|
||||||
|
tx_count: 2,
|
||||||
|
rx_count: 3,
|
||||||
|
subcarriers: 256,
|
||||||
|
frame_period_us: 20_000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct MediatekCsiSimulator {
|
||||||
|
config: SimulatorConfig,
|
||||||
|
rng: u64,
|
||||||
|
sequence: u32,
|
||||||
|
timestamp_us: u64,
|
||||||
|
motion_phase: f32,
|
||||||
|
}
|
||||||
|
impl MediatekCsiSimulator {
|
||||||
|
pub fn new(config: SimulatorConfig) -> Result<Self, CsiParseError> {
|
||||||
|
let s = Self {
|
||||||
|
rng: config.seed,
|
||||||
|
config,
|
||||||
|
sequence: 0,
|
||||||
|
timestamp_us: 0,
|
||||||
|
motion_phase: 0.0,
|
||||||
|
};
|
||||||
|
s.csi_frame()?.validate()?;
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
pub fn capabilities_frame(&self) -> CsiFrame {
|
||||||
|
self.base(
|
||||||
|
ReportKind::Capabilities,
|
||||||
|
CsiPayload::Bytes(vec![
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
self.config.chipset.max_chains(),
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
0b0000_1111,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
(self.config.subcarriers & 255) as u8,
|
||||||
|
(self.config.subcarriers >> 8) as u8,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn next_frame(&mut self) -> CsiFrame {
|
||||||
|
let frame = self.csi_frame().expect("validated simulator config");
|
||||||
|
self.sequence = self.sequence.wrapping_add(1);
|
||||||
|
self.timestamp_us = self.timestamp_us.wrapping_add(self.config.frame_period_us);
|
||||||
|
self.motion_phase += 0.037;
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
fn csi_frame(&self) -> Result<CsiFrame, CsiParseError> {
|
||||||
|
let mut rng = self.rng ^ self.sequence as u64;
|
||||||
|
let count = self.config.tx_count as usize
|
||||||
|
* self.config.rx_count as usize
|
||||||
|
* self.config.subcarriers as usize;
|
||||||
|
let values = (0..count)
|
||||||
|
.map(|idx| {
|
||||||
|
rng ^= rng << 13;
|
||||||
|
rng ^= rng >> 7;
|
||||||
|
rng ^= rng << 17;
|
||||||
|
let noise = ((rng >> 48) as i16 % 24) as f32;
|
||||||
|
let sc = (idx % self.config.subcarriers as usize) as f32;
|
||||||
|
let chain = (idx / self.config.subcarriers as usize) as f32;
|
||||||
|
let phase = sc * 0.031 + chain * 0.23 + self.motion_phase;
|
||||||
|
[
|
||||||
|
((phase.cos() * 1800.0) + noise) as i16,
|
||||||
|
((phase.sin() * 1800.0) - noise) as i16,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(self.base(
|
||||||
|
ReportKind::Csi,
|
||||||
|
CsiPayload::ComplexI16 {
|
||||||
|
rssi_dbm: (0..self.config.rx_count)
|
||||||
|
.map(|i| -42 - i as i8 * 2)
|
||||||
|
.collect(),
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn base(&self, kind: ReportKind, payload: CsiPayload) -> CsiFrame {
|
||||||
|
CsiFrame {
|
||||||
|
report_kind: kind,
|
||||||
|
sequence: self.sequence,
|
||||||
|
timestamp_us: self.timestamp_us,
|
||||||
|
device_id: self.config.device_id,
|
||||||
|
chipset: self.config.chipset,
|
||||||
|
bandwidth_mhz: self.config.bandwidth_mhz,
|
||||||
|
center_freq_khz: self.config.center_freq_khz,
|
||||||
|
flags: CsiFlags(CsiFlags::CALIBRATED | CsiFlags::SYNTHETIC),
|
||||||
|
tx_count: self.config.tx_count,
|
||||||
|
rx_count: self.config.rx_count,
|
||||||
|
ppdu_type: PpduType::HeSu,
|
||||||
|
subcarrier_count: self.config.subcarriers,
|
||||||
|
noise_floor_dbm: -95,
|
||||||
|
scale: 1.0 / 2048.0,
|
||||||
|
subcarrier_spacing_hz: 312_500.0,
|
||||||
|
calibration_id: 1,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u16_at(b: &[u8], o: usize) -> u16 {
|
||||||
|
u16::from_le_bytes([b[o], b[o + 1]])
|
||||||
|
}
|
||||||
|
fn u32_at(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn u64_at(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn f32_at(b: &[u8], o: usize) -> f32 {
|
||||||
|
f32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn crc32_ieee(data: &[u8]) -> u32 {
|
||||||
|
let mut crc = 0xffff_ffffu32;
|
||||||
|
for &byte in data {
|
||||||
|
crc ^= byte as u32;
|
||||||
|
for _ in 0..8 {
|
||||||
|
crc = (crc >> 1) ^ ((0u32.wrapping_sub(crc & 1)) & 0xedb8_8320);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!crc
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use simulator::*;
|
||||||
|
#[test]
|
||||||
|
fn simulator_round_trip_is_deterministic() {
|
||||||
|
let cfg = SimulatorConfig::default();
|
||||||
|
let mut a = MediatekCsiSimulator::new(cfg.clone()).unwrap();
|
||||||
|
let mut b = MediatekCsiSimulator::new(cfg).unwrap();
|
||||||
|
let wa = a.next_frame().to_bytes().unwrap();
|
||||||
|
assert_eq!(wa, b.next_frame().to_bytes().unwrap());
|
||||||
|
let (decoded, n) = CsiFrame::from_bytes(&wa).unwrap();
|
||||||
|
assert_eq!(n, wa.len());
|
||||||
|
assert!(decoded.flags.contains(CsiFlags::SYNTHETIC));
|
||||||
|
assert_eq!(decoded.payload.len(), 2 * 3 * 256);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn capabilities_round_trip() {
|
||||||
|
let s = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||||
|
let f = s.capabilities_frame();
|
||||||
|
let w = f.to_bytes().unwrap();
|
||||||
|
assert_eq!(CsiFrame::from_bytes(&w).unwrap().0, f);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn crc_corruption_is_rejected() {
|
||||||
|
let mut s = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||||
|
let mut w = s.next_frame().to_bytes().unwrap();
|
||||||
|
w[80] ^= 1;
|
||||||
|
assert!(matches!(
|
||||||
|
CsiFrame::from_bytes(&w),
|
||||||
|
Err(CsiParseError::CrcMismatch { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn truncation_is_rejected() {
|
||||||
|
let mut s = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||||
|
let w = s.next_frame().to_bytes().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
CsiFrame::from_bytes(&w[..w.len() - 1]),
|
||||||
|
Err(CsiParseError::InsufficientData { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn invalid_dimensions_are_rejected() {
|
||||||
|
let cfg = SimulatorConfig {
|
||||||
|
rx_count: 4,
|
||||||
|
chipset: ChipsetProfile::Mt7981Mt7976,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
MediatekCsiSimulator::new(cfg),
|
||||||
|
Err(CsiParseError::InvalidDimensions)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn non_finite_float_is_rejected() {
|
||||||
|
let mut s = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||||
|
let mut f = s.next_frame();
|
||||||
|
f.payload = CsiPayload::ComplexF32 {
|
||||||
|
rssi_dbm: vec![-40, -42, -44],
|
||||||
|
values: vec![[f32::NAN, 0.0]; 2 * 3 * 256],
|
||||||
|
};
|
||||||
|
assert_eq!(f.to_bytes().unwrap_err(), CsiParseError::NonFiniteValue);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn parser_never_panics_on_prefixes() {
|
||||||
|
let mut s = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||||
|
let w = s.next_frame().to_bytes().unwrap();
|
||||||
|
for end in 0..w.len() {
|
||||||
|
let _ = CsiFrame::from_bytes(&w[..end]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ mod field_bridge;
|
||||||
mod field_localize;
|
mod field_localize;
|
||||||
mod model_format;
|
mod model_format;
|
||||||
mod multistatic_bridge;
|
mod multistatic_bridge;
|
||||||
|
mod mediatek_csi;
|
||||||
mod realtek_radar;
|
mod realtek_radar;
|
||||||
pub mod pose;
|
pub mod pose;
|
||||||
mod rvf_container;
|
mod rvf_container;
|
||||||
|
|
@ -1033,6 +1034,10 @@ struct AppStateInner {
|
||||||
latest_realtek_radar: Option<realtek_radar::RealtekRadarSnapshot>,
|
latest_realtek_radar: Option<realtek_radar::RealtekRadarSnapshot>,
|
||||||
/// Instant of the last validated RTL8720F UDP frame.
|
/// Instant of the last validated RTL8720F UDP frame.
|
||||||
last_realtek_frame: Option<std::time::Instant>,
|
last_realtek_frame: Option<std::time::Instant>,
|
||||||
|
/// Latest validated MediaTek CSI summary; raw matrices are not retained here.
|
||||||
|
latest_mediatek_csi: Option<mediatek_csi::MediatekCsiSnapshot>,
|
||||||
|
/// Instant of the last validated MediaTek CSI UDP frame.
|
||||||
|
last_mediatek_frame: Option<std::time::Instant>,
|
||||||
tx: broadcast::Sender<String>,
|
tx: broadcast::Sender<String>,
|
||||||
// ADR-099 D2/D3/D4: real-time CSI introspection tap. Per-frame state +
|
// ADR-099 D2/D3/D4: real-time CSI introspection tap. Per-frame state +
|
||||||
// a parallel broadcast topic (`/ws/introspection`) running alongside
|
// a parallel broadcast topic (`/ws/introspection`) running alongside
|
||||||
|
|
@ -1211,6 +1216,13 @@ impl AppStateInner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.source.starts_with("mediatek") {
|
||||||
|
if let Some(last) = self.last_mediatek_frame {
|
||||||
|
if last.elapsed() > ESP32_OFFLINE_TIMEOUT {
|
||||||
|
return format!("{}:offline", self.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.source.clone()
|
self.source.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3371,6 +3383,14 @@ async fn latest_realtek_radar(State(state): State<SharedState>) -> Json<serde_js
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn latest_mediatek_csi(State(state): State<SharedState>) -> Json<serde_json::Value> {
|
||||||
|
let s = state.read().await;
|
||||||
|
match &s.latest_mediatek_csi {
|
||||||
|
Some(snapshot) => Json(serde_json::to_value(snapshot).unwrap_or_default()),
|
||||||
|
None => Json(serde_json::json!({"status": "no MediaTek CSI data yet"})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate WiFi-derived pose keypoints from sensing data.
|
/// Generate WiFi-derived pose keypoints from sensing data.
|
||||||
///
|
///
|
||||||
/// Keypoint positions are modulated by real signal features rather than a pure
|
/// Keypoint positions are modulated by real signal features rather than a pure
|
||||||
|
|
@ -5465,7 +5485,7 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||||
let addr = format!("0.0.0.0:{udp_port}");
|
let addr = format!("0.0.0.0:{udp_port}");
|
||||||
let socket = match UdpSocket::bind(&addr).await {
|
let socket = match UdpSocket::bind(&addr).await {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
info!("UDP listening on {addr} for ESP32 CSI and RTL8720F radar frames");
|
info!("UDP listening on {addr} for ESP32 CSI, MediaTek CSI, and RTL8720F radar frames");
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -5478,6 +5498,26 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||||
loop {
|
loop {
|
||||||
match socket.recv_from(&mut buf).await {
|
match socket.recv_from(&mut buf).await {
|
||||||
Ok((len, src)) => {
|
Ok((len, src)) => {
|
||||||
|
if len >= 4
|
||||||
|
&& u32::from_le_bytes(buf[..4].try_into().expect("four-byte slice"))
|
||||||
|
== wifi_densepose_hardware::mediatek_csi::MEDIATEK_CSI_MAGIC
|
||||||
|
{
|
||||||
|
match wifi_densepose_hardware::mediatek_csi::CsiFrame::from_bytes(&buf[..len]) {
|
||||||
|
Ok((frame, consumed)) if consumed == len => {
|
||||||
|
let snapshot = mediatek_csi::MediatekCsiSnapshot::from_frame(&frame);
|
||||||
|
debug!("MediaTek CSI from {src}: profile={} seq={} dimensions={}x{}x{}", snapshot.chipset, snapshot.sequence, snapshot.tx_count, snapshot.rx_count, snapshot.subcarrier_count);
|
||||||
|
let json = serde_json::to_string(&snapshot).ok();
|
||||||
|
let mut s = state.write().await;
|
||||||
|
s.source = snapshot.source.to_string();
|
||||||
|
s.last_mediatek_frame = Some(std::time::Instant::now());
|
||||||
|
s.latest_mediatek_csi = Some(snapshot);
|
||||||
|
if let Some(json) = json { let _ = s.tx.send(json); }
|
||||||
|
}
|
||||||
|
Ok((_, consumed)) => warn!("MediaTek CSI datagram from {src} has trailing bytes: consumed={consumed} received={len}"),
|
||||||
|
Err(error) => warn!("Rejected MediaTek CSI datagram from {src}: {error}"),
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if len >= 4
|
if len >= 4
|
||||||
&& u32::from_le_bytes(buf[..4].try_into().expect("four-byte slice"))
|
&& u32::from_le_bytes(buf[..4].try_into().expect("four-byte slice"))
|
||||||
== wifi_densepose_hardware::rtl8720f::RTL8720F_RADAR_MAGIC
|
== wifi_densepose_hardware::rtl8720f::RTL8720F_RADAR_MAGIC
|
||||||
|
|
@ -7596,6 +7636,8 @@ async fn main() {
|
||||||
last_esp32_frame: None,
|
last_esp32_frame: None,
|
||||||
latest_realtek_radar: None,
|
latest_realtek_radar: None,
|
||||||
last_realtek_frame: None,
|
last_realtek_frame: None,
|
||||||
|
latest_mediatek_csi: None,
|
||||||
|
last_mediatek_frame: None,
|
||||||
tx,
|
tx,
|
||||||
intro: wifi_densepose_sensing_server::introspection::IntrospectionState::new(),
|
intro: wifi_densepose_sensing_server::introspection::IntrospectionState::new(),
|
||||||
intro_tx,
|
intro_tx,
|
||||||
|
|
@ -7813,6 +7855,7 @@ async fn main() {
|
||||||
// Sensing endpoints
|
// Sensing endpoints
|
||||||
.route("/api/v1/sensing/latest", get(latest))
|
.route("/api/v1/sensing/latest", get(latest))
|
||||||
.route("/api/v1/radar/latest", get(latest_realtek_radar))
|
.route("/api/v1/radar/latest", get(latest_realtek_radar))
|
||||||
|
.route("/api/v1/csi/mediatek/latest", get(latest_mediatek_csi))
|
||||||
// Per-node health endpoint
|
// Per-node health endpoint
|
||||||
.route("/api/v1/nodes", get(nodes_endpoint))
|
.route("/api/v1/nodes", get(nodes_endpoint))
|
||||||
// ADR-110 iter 29 — per-node mesh sync state for HTTP clients.
|
// ADR-110 iter 29 — per-node mesh sync state for HTTP clients.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
//! Bounded summaries for ADR-267 MediaTek MIMO CSI frames.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use wifi_densepose_hardware::mediatek_csi::{CsiFlags, CsiFrame, CsiPayload, ReportKind};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub(crate) struct MediatekCsiSnapshot {
|
||||||
|
pub event_type: &'static str,
|
||||||
|
pub source: &'static str,
|
||||||
|
pub report_kind: &'static str,
|
||||||
|
pub sequence: u32,
|
||||||
|
pub timestamp_us: u64,
|
||||||
|
pub device_id: String,
|
||||||
|
pub chipset: &'static str,
|
||||||
|
pub center_freq_khz: u32,
|
||||||
|
pub bandwidth_mhz: u16,
|
||||||
|
pub tx_count: u8,
|
||||||
|
pub rx_count: u8,
|
||||||
|
pub subcarrier_count: u16,
|
||||||
|
pub element_count: usize,
|
||||||
|
pub ppdu_type: String,
|
||||||
|
pub rssi_dbm: Vec<i8>,
|
||||||
|
pub noise_floor_dbm: i8,
|
||||||
|
pub calibrated: bool,
|
||||||
|
pub synthetic: bool,
|
||||||
|
pub saturated: bool,
|
||||||
|
pub time_synchronized: bool,
|
||||||
|
pub dropped_predecessor: bool,
|
||||||
|
pub calibration_id: u32,
|
||||||
|
pub subcarrier_spacing_hz: f32,
|
||||||
|
pub mean_amplitude: Option<f32>,
|
||||||
|
pub peak_amplitude: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediatekCsiSnapshot {
|
||||||
|
pub(crate) fn from_frame(frame: &CsiFrame) -> Self {
|
||||||
|
let synthetic = frame.flags.contains(CsiFlags::SYNTHETIC);
|
||||||
|
let (mean_amplitude, peak_amplitude) = amplitude_summary(frame);
|
||||||
|
Self {
|
||||||
|
event_type: "mediatek_csi",
|
||||||
|
source: if synthetic {
|
||||||
|
"mediatek:simulated"
|
||||||
|
} else {
|
||||||
|
"mediatek"
|
||||||
|
},
|
||||||
|
report_kind: match frame.report_kind {
|
||||||
|
ReportKind::Csi => "csi",
|
||||||
|
ReportKind::Capabilities => "capabilities",
|
||||||
|
},
|
||||||
|
sequence: frame.sequence,
|
||||||
|
timestamp_us: frame.timestamp_us,
|
||||||
|
device_id: format!("{:016x}", frame.device_id),
|
||||||
|
chipset: frame.chipset.name(),
|
||||||
|
center_freq_khz: frame.center_freq_khz,
|
||||||
|
bandwidth_mhz: frame.bandwidth_mhz,
|
||||||
|
tx_count: frame.tx_count,
|
||||||
|
rx_count: frame.rx_count,
|
||||||
|
subcarrier_count: frame.subcarrier_count,
|
||||||
|
element_count: frame.payload.len(),
|
||||||
|
ppdu_type: format!("{:?}", frame.ppdu_type).to_ascii_lowercase(),
|
||||||
|
rssi_dbm: frame.payload.rssi_dbm().to_vec(),
|
||||||
|
noise_floor_dbm: frame.noise_floor_dbm,
|
||||||
|
calibrated: frame.flags.contains(CsiFlags::CALIBRATED),
|
||||||
|
synthetic,
|
||||||
|
saturated: frame.flags.contains(CsiFlags::SATURATED),
|
||||||
|
time_synchronized: frame.flags.contains(CsiFlags::TIME_SYNCHRONIZED),
|
||||||
|
dropped_predecessor: frame.flags.contains(CsiFlags::DROPPED_PREDECESSOR),
|
||||||
|
calibration_id: frame.calibration_id,
|
||||||
|
subcarrier_spacing_hz: frame.subcarrier_spacing_hz,
|
||||||
|
mean_amplitude,
|
||||||
|
peak_amplitude,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn amplitude_summary(frame: &CsiFrame) -> (Option<f32>, Option<f32>) {
|
||||||
|
let amplitudes: Vec<f32> = match &frame.payload {
|
||||||
|
CsiPayload::ComplexI16 { values, .. } => values
|
||||||
|
.iter()
|
||||||
|
.map(|[i, q]| (*i as f32).hypot(*q as f32) * frame.scale)
|
||||||
|
.collect(),
|
||||||
|
CsiPayload::ComplexF32 { values, .. } => values
|
||||||
|
.iter()
|
||||||
|
.map(|[i, q]| i.hypot(*q) * frame.scale)
|
||||||
|
.collect(),
|
||||||
|
CsiPayload::Bytes(_) => return (None, None),
|
||||||
|
};
|
||||||
|
if amplitudes.is_empty() {
|
||||||
|
return (None, None);
|
||||||
|
}
|
||||||
|
let mean = amplitudes.iter().sum::<f32>() / amplitudes.len() as f32;
|
||||||
|
let peak = amplitudes.into_iter().max_by(f32::total_cmp);
|
||||||
|
(Some(mean), peak)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use wifi_densepose_hardware::mediatek_csi::simulator::{MediatekCsiSimulator, SimulatorConfig};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simulator_summary_preserves_dimensions_and_provenance() {
|
||||||
|
let mut sim = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||||
|
let snapshot = MediatekCsiSnapshot::from_frame(&sim.next_frame());
|
||||||
|
assert_eq!(snapshot.source, "mediatek:simulated");
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
snapshot.tx_count,
|
||||||
|
snapshot.rx_count,
|
||||||
|
snapshot.subcarrier_count
|
||||||
|
),
|
||||||
|
(2, 3, 256)
|
||||||
|
);
|
||||||
|
assert_eq!(snapshot.element_count, 1536);
|
||||||
|
assert!(snapshot.mean_amplitude.unwrap() > 0.0);
|
||||||
|
assert!(snapshot.peak_amplitude.unwrap() >= snapshot.mean_amplitude.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capability_summary_does_not_invent_signal_statistics() {
|
||||||
|
let sim = MediatekCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||||
|
let snapshot = MediatekCsiSnapshot::from_frame(&sim.capabilities_frame());
|
||||||
|
assert_eq!(snapshot.report_kind, "capabilities");
|
||||||
|
assert_eq!(snapshot.mean_amplitude, None);
|
||||||
|
assert!(snapshot.rssi_dbm.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue