RuView 宽频 802.11ax CSI 数据接入:基于 FeitCSI/AX210 的子载波无关管线架构(ADR-292 实践指南)
本文以 docs/adr/ADR-292-wideband-80211ax-csi-ingest.md 为骨架,结合其在
wifi-densepose-matcrate 中的 Rust 实现展开。它讲清楚三件事:为什么 RuView 需要宽频(>114 子载波)CSI 接入路径、FeitCSI 二进制记录格式如何被安全解析、以及原生子载波维度如何端到端保留并显式映射到下游管线宽度。读完你不仅能看懂这套设计的取舍,还能直接据此用文件回放或外部进程流式两种方式把 AX200/AX210 的宽频数据喂进 RuView 管线。
背景:从 802.11n 时代的 ≤114 子载波,到 802.11ax/11bf 的 160 MHz 宽频
RuView 的 CSI 接入层位于 v2/crates/wifi-densepose-mat/src/integration/hardware_adapter.rs,作为一层“防腐层”(Anti-Corruption Layer),统一把不同硬件的 CSI 转译为领域类型 CsiReadings。在该决策之前,它支持的三类硬件全部属于 802.11n 世代:
| 接入路径 | 底层 | 典型子载波数 | 带宽上限 |
|---|---|---|---|
| ESP32 串行流(ESP-CSI 固件) | ESP32 | 依固件配置(HT20 档约 56 量级) | 20/40 MHz |
| Intel 5300(Linux CSI Tool) | iwlwifi + netlink connector | 30 | 20/40 MHz |
| Atheros/Nexmon(ath9k/ath10k/ath11k 补丁驱动) | debugfs CSI 缓冲 | 56 / 114 / 234 | 40 MHz(ath11k 除外) |
而 2026 年的研究扫描表明:该领域的重心已经迁移到通过 PicoScenes(核心闭源)与 FeitCSI(开源、GPL)驱动 Intel AX200/AX210 网卡,可拿到最高 160 MHz 带宽 / 1992 个子载波、含 6 GHz 频段的 802.11ax CSI。这既是当前研究级档位的基线,也是 2026 年起 802.11bf 芯片将产出的数据形态。RuView 的 wifi-densepose-hardware crate 已经在协议层建模 802.11bf 会话类型(见 v2/crates/wifi-densepose-hardware/src/ieee80211bf/types.rs),但没有任何 ingest 路径能把宽频 CSI 真正送进管线——>114 子载波的帧在既有落地上没有可用的通道。
这带来三重后果,正是 ADR-292 要解决的:
- 无法基于“当下可得的最好信号”做开发;
- 无法把 ESP32 档位的结果与宽频理论上限做横向对比;
- 面对 802.11bf 硅片时,没有经过验证的 >114 子载波帧的接入管道。
方案权衡:为什么选 FeitCSI 而非 PicoScenes 或裸 pcap
ADR-292 在决策前对三条技术路线做了明确权衡,结论值得在接入类方案评审时复用:
- PicoScenes
.csi格式 —— 拒绝(暂缓)。该格式产自闭源核心,版本化且复杂;在没有持续维护的规范文档时自行解析,等于拿“静默损坏”做赌注。 - 裸 pcap + radiotap 解析 —— 拒绝。这会在设备端重复 FeitCSI 已完成的解包工作,还把包捕获依赖拉进管线。
- FeitCSI 文件/流 ingest —— 采纳。理由有四点:
- FeitCSI 开源,其头部布局可对照源码审计;
- 目标硬件明确为 AX200/AX210;
- 覆盖 20–160 MHz、含 6 GHz;
- 每帧输出一条紧凑的二进制记录。
需要特别强调的是许可证策略:FeitCSI 是 GPL 项目,RuView 只解析其输出格式,永不链接、永不将其纳入编译产物——它始终作为外部工具使用。这一边界由代码注释明确声明(见 feitcsi.rs 头部文档),保证 MIT workspace 不受 GPL 污染。
决策落地一:feitcsi 记录解析器
解析器整体位于 v2/crates/wifi-densepose-mat/src/integration/feitcsi.rs,并在 mod.rs 统一导出。
二进制记录布局:272 字节定长头 + CSI 负载
每条记录是 packed 的 272 字节头 + csiDataSize 字节裸 CSI。头部布局依据 FeitCSI 源码(master @ 2026-08-10)的 include/Csi.h(RawHeaderData 结构)与 Csi::save() 逐字段核对,要点是:FeitCSI 运行于小端 x86 主机并直接倾倒原生结构体内存,因此所有字段一律小端序。具体字段如下:
| 偏移 | 大小 | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | csiDataSize | u32,头部之后 CSI 负载的字节数 |
| 4 | 4 | reserved | (space4) |
| 8 | 4 | ftmClock | u32 |
| 12 | 8 | timestamp | u64,设备时间戳(微秒) |
| 20 | 26 | reserved | (space20) |
| 46 | 1 | numRx | u8,接收天线数 |
| 47 | 1 | numTx | u8,发射流数 |
| 48 | 4 | reserved | (space48) |
| 52 | 4 | numSubCarriers | u32 |
| 56 | 4 | reserved | (space54) |
| 60 | 4 | rssi1 | u32,天线 A RSSI |
| 64 | 4 | rssi2 | u32,天线 B RSSI |
| 68 | 6 | srcMac | 源 MAC 地址 |
| 74 | 18 | reserved | (space75) |
| 92 | 4 | rateNflag | u32,iwlwifi rate flags |
| 96 | 176 | reserved | (space96,44 × u32) |
负载为交错的小端 i16 I/Q 对,遍历顺序是 for rx { for tx { for subcarrier { i16 real, i16 imag } } },即每个复数样本占 4 字节,且恒等式成立:
csiDataSize == numRx * numTx * numSubCarriers * 4
rate flags:从 rateNflag 解码调制类型与带宽
rateNflag 采用 FeitCSI 内置的 iwlwifi rate/flags 编码(lib/include/rs.h),解析器用位掩码提取两个子字段:
- 调制类型(bits 8..11,
RATE_MCS_MOD_TYPE):0=CCK,1=legacy OFDM,2=HT,3=VHT,4=HE,5=EHT; - 信道宽度(bits 11..14,
RATE_MCS_CHAN_WIDTH):0=20 MHz,1=40,2=80,3=160,4=320。
对应到 Rust 侧,是 FeitCsiModType 与 FeitCsiBandwidth 两个枚举(后者的 mhz() 返回物理带宽、to_bandwidth() 映射到适配层 Bandwidth 枚举)。320 MHz(EHT)被本版本解析器显式拒绝。
“响亮地失败”:没有魔数时的结构性版本校验
该格式最大的坑是:盘上格式没有任何魔数或版本字段——它本质就是 iwlwifi 通知头的裸内存。因此 ADR-292 要求的“版本检查”必须是结构性的且严格的,体现在 validate_header 的校验顺序上:
- 维度字段为 0 → [
FeitCsiError::ZeroDimension]; - 维度超过硬上限 → [
FeitCsiError::CapExceeded]; - 声明的
csiDataSize与维度乘积不符 → [FeitCsiError::DimensionMismatch]; rateNflag落在既有编码之外(未知调制类型或 320 MHz)→ [FeitCsiError::UnsupportedFormat],拒绝猜测而不是误解析。
所有输入都被当作不可信数据:每次读取都做长度检查;任何分配发生前先做边界约束;畸形输入一律产出结构化错误,绝不 panic。错误类型在 mod.rs 中被映射到 AdapterError:UnsupportedFormat → UnsupportedAdapter、I/O 错误透传、其余归类为 DataFormat。
有界分配:对抗损坏长度字段
宽度字段若被损坏可能诱发无界分配,因此解析器设置了硬上限常量:
HEADER_LEN = 272;MAX_SUBCARRIERS = 4096(802.11ax 160 MHz HE 实际是 1992,保留 4096 头寸以容纳未来 802.11bf 截断 CIR 形态);MAX_ANTENNAS = 8(AX210 实际是 2x2,8 已很宽裕);BYTES_PER_SAMPLE = 4;MAX_RECORD_BYTES = HEADER_LEN + 8 * 8 * 4096 * 4(约 1 MiB + 272),同时作为流模式缓冲上界。
由于 8×8×4096×4 ≈ 1 MiB 的乘积上限,维度乘法在 usize 上不会溢出。校验在任何依据不可信字段定尺寸的分配之前完成。
单次遍历解码与零分配转换
decode_csi 用 chunks_exact(4) 把负载切成精确的 4 字节块,逐样本 i16::from_le_bytes 后转 f64 构造 Complex64;由于调用方已对 payload.len() 做过校验,collect 只会发生一次有界分配,转换循环本身零分配。解析入口 parse_record 返回 (FeitCsiRecord, usize)(含消费字节数),支持调用方顺序遍历多记录捕获文件;头部原地读取、负载直接从输入切片转换,CSI 字节只被遍历一次。
文件回放与流式读取两个读取器
ADR-292 要求两条读取路径都能成立:
FeitCsiFileReader:打开录制好的捕获文件做确定性顺序回放;FeitCsiStreamReader:对任意Read源(文件、FIFO、管道包装)逐条读取。
两者的核心是内部共用的 read_one_record_with_scratch:它持有调用方拥有的可复用 scratch 缓冲,长跑回放/流循环整条流只做一次有界原始字节分配(外加每条记录一份 Vec<Complex64> 输出),而不是逐记录分配。中途 EOF 被归为 Truncated 而非干净的 Ok(None)——干净的流结束只发生在两条记录之间读到 0 字节时。
决策落地二:适配器层的 DeviceType::FeitCsi
在 hardware_adapter.rs 中,DeviceType 枚举新增 FeitCsi 变体(注释明确说明其两种语义:录制捕获的文件回放,或外部 FeitCSI 进程写入的路径/管道;crate 内不做任何提权操作)。配套类型包括:
FeitCsiMode:FileReplay(确定性回放)/Stream(外部进程持续写入的路径/管道);FeitCsiSettings:path、mode、band(2.4/5/6 GHz)、channel、loop_playback、pipeline_subcarriers。
由于 FeitCSI 头部只携带带宽(经 rate flags),不携带信道与频段——这两者属于捕获配置的职权范围——所以由调用方在 settings 中给定并盖进帧元数据。
两个构造入口封装了最常见的两种用法:
use wifi_densepose_mat::integration::{
HardwareAdapter, HardwareConfig,
};
// 1) 确定性文件回放:同一输入文件 ⇒ 同一帧序列
let config = HardwareConfig::feitcsi_replay("/path/to/capture.dat");
// 2) 流式接入:读取外部 FeitCSI 进程持续写入的路径/管道
let config = HardwareConfig::feitcsi_stream("/path/to/feitcsi.pipe");
let mut adapter = HardwareAdapter::with_config(config);
adapter.initialize().await?;
let mut stream = adapter.start_csi_stream().await?;
// 逐帧消费 CsiReadings...
内部实现细节值得注意:
- 初始化不做提权操作(initialize_feitcsi):FileReplay 只校验文件存在;Stream 模式下若路径尚未创建只告警不失败,避免约束外部进程的启动顺序——NIC 配置完全由 FeitCSI 自己的工具负责(最小权限原则)。
- 文件回放推进字节偏移:状态机记录
replay_offset,每条记录通过spawn_blocking在阻塞线程池中seek + read_one_record,读毕更新偏移;配置loop_playback则在读到 EOF 后回到文件头。 - 流模式持有一个跨读取存活的文件句柄(FIFO 无法逐记录重开),首次读取时惰性打开;若流循环中途被 shutdown,孤儿任务自行结束、句柄在下一次读取时重开。
帧转译:按天线对展开,时间戳确定化
FeitCsiRecord::to_readings 完成“记录 → CsiReadings”的转译:
- 每个 (rx, tx) 天线对生成一条 [
SensorCsiReading],sensor_id形如feitcsi_rx{rx}_tx{tx},把复数样本逐点转成幅度(c.norm())与相位(c.im.atan2(c.re)); - 盘上 u32 的 RSSI 按二进制补码解释为 dBm(捕获文件把负 dBm 存在原始寄存器字段中),帧级 RSSI 取
rssi1/rssi2中较大者,noise_floor固定为 -92.0; - 源 MAC 格式化为
AA:BB:CC:DD:EE:FF写入tx_mac; - 时间戳由记录自身设备时钟(微秒)推导,绝不使用墙上时钟——这是文件回放可复现的根本保证。
决策落地三:子载波无关的管线管道
宽带元数据成为一等公民
转译出的 CsiMetadata 携带 device_type: DeviceType::FeitCsi、channel、bandwidth、num_subcarriers,并新增可选的 wideband: WidebandMeta 字段(定义见 hardware_adapter.rs):
pub struct WidebandMeta {
/// 帧捕获所在的射频频段
pub band: WifiBand, // Band2_4GHz | Band5GHz | Band6GHz
/// 信道带宽(MHz,20–160)
pub bandwidth_mhz: u16,
/// 捕获的原生子载波数(真实频谱分辨率)
pub native_subcarriers: usize,
/// 原生→管线换算记录;帧仍处于原生宽度时为 None
pub mapping: Option<SubcarrierMapping>,
}
带宽(20–160 MHz)与频段(2.4/5/6 GHz)自此成为帧元数据的一等字段,WifiBand 枚举覆盖 6 GHz 频段(802.11ax / Wi-Fi 6E 及以后)。
显式重采样:唯一被允许的原生→管线换算
Ingest 层携带原生子载波维度端到端穿过管线,再经由既有的插值/抽取级显式换算到管线宽度——换算入口是 resample_readings_to_pipeline:
pub fn resample_readings_to_pipeline(
readings: &CsiReadings,
pipeline_subcarriers: usize, // 例如 56
) -> Result<CsiReadings, AdapterError>
其底层复用 wifi-densepose-signal 的 HardwareNormalizer(ADR-027 引入的 Catmull-Rom 三次插值重采样器,默认规范宽度 56、可用 with_canonical_subcarriers 定制),对每条 reading 的幅度与相位分别 resample_to_canonical,并把换算记录进元数据:
SubcarrierMapping {
native: 1992, // 真实频谱分辨率永不丢失
pipeline: 56,
method: "catmull-rom-cubic",
}
这样下游消费者始终知道一帧的真实频谱分辨率与换算方式——即便该帧原本没有宽带元数据(例如从既有窄带路径流入),转换时也会补造 WidebandMeta 保住来源信息。而在适配器读帧路径(read_feitcsi_csi)中,FeitCsiSettings::pipeline_subcarriers 为 Some(n) 且 n != 原生宽度 时自动触发该换算,None 则保持原生宽度原样输出。
后果与边界条件
ADR-292 的 Consequences 定义了这套落地的长期契约:
- RuView 获得一条研究级宽频开发路径,以及未来 802.11bf 上报的可测试 ingest 形态——截断 CIR 是同一套管道的自然延伸(当前 4096 子载波上限即为 802.11bf 预留了头寸;对照
wifi-densepose-hardware的 802.11bf 模型,其单帧最大上报子载波数MAX_REPORT_SUBCARRIERS = 484也能被宽频管线完整承载)。 - GPL FeitCSI 只作外部工具、永不链接,仅解析其输出格式,MIT workspace 无许可证污染。
- 解析器追踪外部项目格式,版本漂移时响亮失败而非误解析。
- ESP32 仍是部署级传感器档位;宽频是开发/验证级档位。宽频捕获的精度声明必须打上捕获硬件标签——CLAUDE.md 要求的真实硬件验证在真实 AX210 上完成之前,任何捕获路径的硬件级声明都不成立。
验证:合成夹具、确定性回放与解析吞吐基准
与 ADR-292 “Validation” 一节逐条对应,仓库给出了两层证据:
1. 单元测试(cargo test -p wifi-densepose-mat)
按照仓库政策,FeitCSI 捕获夹具一律在代码中生成、绝不把二进制文件提交进仓库——合成模块 synth::record_bytes 让 CSI 样本成为样本索引的纯函数,保证往返可核对、回放可复现。测试覆盖 feitcsi.rs 测试模块:
| 测试 | 验证点 |
|---|---|
test_parse_valid_he_shapes | 20/80/160 MHz(HE 242/996/1992 tone)合法记录正确解析,维度、带宽、调制类型、样本往返一致;天线对切片宽度正确 |
test_truncated_buffer | 头部/负载截断、空缓冲均产出结构化 Truncated,不 panic |
test_dimension_mismatch | csiDataSize 与维度不符(差 4 字节)被拒 |
test_unsupported_format_fails_loudly | 320 MHz 宽度(值 4)与越界调制类型(值 7)被 UnsupportedFormat 拒绝 |
test_allocation_cap_enforcement | num_subcarriers=100_000 超上限、num_rx=9 超上限、零维度,均在分配前被拒 |
test_stream_reader_multi_record / test_stream_reader_mid_record_eof | 多记录顺序读取 + 干净 EOF;流中段 EOF 报 Truncated |
test_replay_determinism | 同一捕获两次独立读取产出逐字节一致的记录序列与转译结果(含时间戳) |
test_native_to_pipeline_mapping_recorded | 1992 → 56 显式换算后映射被记录,原生宽度在元数据中保留,原帧不被改动 |
test_to_readings_antenna_pairs | 2x2 记录展开为 4 条原生宽度 reading,MAC 格式化正确,DeviceType::FeitCsi 与带宽正确 |
2. 解析吞吐基准(cargo bench -p wifi-densepose-mat)
基准注册在 Cargo.toml 的 [[bench]] 段(feitcsi_bench,harness = false),实现见 benches/feitcsi_bench.rs:
feitcsi_parse组以 Criterion 的Throughput::Bytes计量三种宽频形态的单条解析吞吐:he20_242sc、he80_996sc、头条he160_1992sc(AX210 真实交付的 160 MHz / 1992 子载波帧,2x1 MIMO);feitcsi_stream组计量 16 条 1992 子载波记录的流读取吞吐,直接锻炼FeitCsiStreamReader的可复用 scratch 缓冲(整条流只做一次原始字节分配)。
所有夹具均为确定性合成字节,无墙上时钟或随机性进入被解析字节。
实操小结:如何把宽频数据接入 RuView
按最小可复现路径归纳成四步:
- 捕获(在 crate 之外完成):用 FeitCSI 自己的工具在 AX200/AX210 上于 20–160 MHz(含 6 GHz 频段可选)采集 CSI,产物是一连串 272 字节头 + 负载的紧凑二进制记录。
- 接入:录制文件用
HardwareConfig::feitcsi_replay(path);外部进程实时落盘/写管道用HardwareConfig::feitcsi_stream(path),配合FeitCsiSettings给出捕获时真实使用的band与channel。想直接对齐窄带管线宽度,就设pipeline_subcarriers: Some(n)。 - 消费:经
HardwareAdapter::initialize().await与start_csi_stream().await逐帧得到CsiReadings;每帧metadata.wideband里能看到真实的native_subcarriers、bandwidth_mhz、band与换算记录mapping。 - 验证与免责:开发期可用合成夹具与
cargo test/cargo bench校验解析与吞吐;任何针对真实宽频捕获的精度结论,须先完成真实 AX210 硅片验证并按 CLAUDE.md 要求标注捕获硬件。
延伸阅读
- 决策原文:docs/adr/ADR-292-wideband-80211ax-csi-ingest.md
- 解析器实现与全部测试:v2/crates/wifi-densepose-mat/src/integration/feitcsi.rs
- 适配器、
DeviceType::FeitCsi与帧元数据定义:v2/crates/wifi-densepose-mat/src/integration/hardware_adapter.rs - 接入层导出面:v2/crates/wifi-densepose-mat/src/integration/mod.rs
- 重采样底层
HardwareNormalizer:v2/crates/wifi-densepose-signal/src/hardware_norm.rs - 802.11bf 协议层模型(宽频接入的未来形态参照):v2/crates/wifi-densepose-hardware/src/ieee80211bf/types.rs
- 基准注册与依赖:v2/crates/wifi-densepose-mat/Cargo.toml
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



