1. 从标题拆解这个项目的真实意图
"RuView:8 美元的 WiFi,看穿墙壁"——这个标题我第一次看到的时候,第一反应是"又是一个标题党"。但仔细琢磨了一下,它其实精准地描述了一个非常硬核的技术方向:利用廉价商用WiFi设备的信道状态信息(CSI,Channel State Information)来实现穿墙人体姿态估计与生命体征监测。
先把这个标题拆开来看。"RuView"是项目代号,从命名逻辑推测,大概率是"RF"(射频)加"View"(视图/看见)的组合词,暗示用射频信号去"看"世界。"8美元的WiFi"指的是硬件成本——一块ESP32系列模组或者类似的低成本WiFi开发板,批量采购价确实能压到个位数美元。"看穿墙壁"则是核心卖点,也是整个项目最有冲击力的部分:WiFi信号能穿透非金属墙体,通过分析信号在传播路径上因人体运动产生的微小扰动,反推出墙后的人体姿态。
这个项目解决的核心问题是:在不需要摄像头、不需要穿戴设备的前提下,实现对人的非接触式感知。传统方案要么依赖视觉(摄像头有隐私问题、有遮挡问题),要么依赖可穿戴传感器(需要佩戴、体验差)。而WiFi感知恰好卡在一个很微妙的位置——它既有穿透能力,又不像雷达那样需要昂贵的专用硬件,还能复用已经无处不在的WiFi基础设施。
适合谁来参考这个项目?我认为有三类人值得深入看:一是做智能家居和养老监护的开发者,这个技术可以直接用来做跌倒检测、呼吸监测;二是做人机交互的研究人员,WiFi手势识别是一个很有潜力的方向;三是对无线感知感兴趣的嵌入式工程师,这个项目涉及信号处理、机器学习部署、嵌入式开发等多个层面的知识。
下面我会从整体设计思路、核心技术细节、实操落地过程、常见问题排查四个维度,把这个项目彻底拆开讲清楚。
2. 整体设计思路与方案选型
2.1 为什么选WiFi而不是毫米波雷达或UWB
这是我在动手之前反复问自己的第一个问题。市面上做穿墙感知的方案不止WiFi一种,毫米波雷达(60GHz/77GHz)和UWB(超宽带)都能做,而且精度往往更高。但为什么这个项目偏偏选了WiFi?
核心原因有三个。第一是成本。一块毫米波雷达模组动辄几十到上百美元,UWB模组也要十几美元起步,而ESP32系列WiFi模组批量价确实能压到8美元以内,这个成本差距是数量级的。第二是普适性。WiFi已经无处不在,你家里、办公室里到处都是WiFi信号,这意味着这个技术可以"寄生"在现有基础设施上,不需要额外部署专用设备。第三是穿透性。2.4GHz WiFi信号对石膏板墙、木门、砖墙的穿透能力相当不错,而毫米波在穿透性上反而吃亏——频率越高,穿透损耗越大。
当然,WiFi感知也有明显的短板。它的空间分辨率远不如毫米波雷达,信号带宽只有20/40/80MHz,而毫米波雷达可以做到GHz级别的带宽。这就导致WiFi感知在精细姿态估计上天然吃亏,只能做到"粗粒度"的骨架估计,比如判断人是站着、坐着、躺着、在走动,或者识别挥手、跌倒这类大动作。想用它做手指级别的精细识别,目前还不太现实。
提示:如果你的应用场景对精度要求极高(比如手语识别),WiFi方案可能不是最优解。但如果你的需求是"有没有人""在不在动""有没有摔倒""呼吸频率多少",WiFi方案的性价比是无敌的。
2.2 CSI为什么比RSSI更适合做感知
很多人做WiFi感知的第一反应是用RSSI(接收信号强度指示),因为几乎所有WiFi芯片都能直接读到这个值。但RSSI的问题在于它太"粗"了——它只是把所有子载波的信号强度做了一个聚合平均,丢失了绝大部分信息。
CSI则完全不同。在OFDM(正交频分复用)系统中,WiFi信号被分配到多个子载波上传输,CSI记录的是每个子载波上的幅度和相位信息。以20MHz带宽为例,通常有52到56个数据子载波,每个子载波都能提供一组复数(幅度+相位)。这意味着CSI提供的是一个高维的细粒度信道响应矩阵,而不是一个标量。
打个比方:RSSI就像是你站在房间外面,只能听到里面"有声音"还是"没声音";而CSI相当于你有一组麦克风阵列,能听到声音的频率分布、相位差、回声模式,从而推断出房间里的布局和人的位置。这个信息量的差距是本质性的。
RuView项目的核心,就是围绕CSI数据的采集、清洗、特征提取和姿态推断来展开的。整个系统的技术栈大致是这样的:
| 层级 | 技术选型 | 作用 |
|---|---|---|
| 硬件层 | ESP32-S3 / Intel 5300 | 采集CSI原始数据 |
| 信号处理层 | 相位解缠、滤波、去噪 | 把原始CSI变成可用信号 |
| 特征层 | 多普勒频移、时延剖面 | 提取与人体运动相关的特征 |
| 模型层 | CNN / LSTM / Transformer | 从特征推断姿态 |
| 应用层 | 跌倒检测、呼吸监测 | 落地到具体场景 |
2.3 为什么用ESP32而不是Intel 5300
早期WiFi感知研究几乎都用Intel 5300网卡,因为它能通过修改驱动导出CSI数据,而且有成熟的CSI Tool工具链。但Intel 5300的问题是:它是PCIe接口的网卡,需要一台PC来承载,部署灵活性很差,而且这块卡早就停产了,二手市场价格被炒得很高。
ESP32系列则完全不同。它是一颗SoC,自带WiFi射频,支持CSI数据导出(乐鑫官方在ESP-IDF中提供了CSI API),体积小、功耗低、价格便宜,可以直接部署在任意位置。RuView选择ESP32作为硬件平台,本质上是在"可部署性"和"数据质量"之间做了一个权衡——ESP32的CSI数据质量确实不如Intel 5300(子载波数量少、采样率受限),但它让这个技术真正具备了大规模部署的可能性。
注意:ESP32的CSI数据需要通过串口或WiFi回传到上位机处理,ESP32本身算力有限,跑不动复杂的神经网络。所以RuView的架构一定是"ESP32采集 + 上位机推理"的分体式设计,这一点在动手之前就要想清楚。
3. 核心技术细节与实操要点
3.1 CSI数据到底长什么样
先给你一个直观的认识。ESP32通过esp_wifi_set_csi_rx_cb注册回调函数后,每次收到WiFi包都会触发回调,回调里能拿到一个wifi_csi_info_t结构体,里面包含:
buf:指向CSI数据的缓冲区,每个子载波用两个字节表示(高字节虚部,低字节实部,具体格式取决于芯片型号)len:数据长度rx_ctrl:接收控制信息,包括RSSI、信道、时间戳等
以ESP32-S3为例,在20MHz带宽、HT20模式下,通常能拿到64个子载波的CSI数据(实际有效子载波约52个)。每个子载波是一个复数,所以一次CSI采样大约是64×2=128字节的原始数据。
这里有个坑我必须提前说:ESP32的CSI数据格式在不同芯片型号之间是不一样的。ESP32、ESP32-S3、ESP32-C3的CSI数据排列方式、子载波顺序、有无导频子载波都有差异。你在网上抄的代码,换一块芯片可能就跑不通了。我建议第一步先写一个最简单的采集程序,把原始数据打印出来,对照乐鑫官方文档确认数据格式,再往下做。
3.2 相位处理是整个流程中最难的部分
CSI数据里,幅度信息相对好处理,但相位信息才是真正包含人体运动信息的关键。问题在于,原始相位存在两个致命问题:
第一是相位解缠(Phase Unwrapping)。CSI相位是通过atan2(虚部, 实部)算出来的,结果被限制在[-π, π]之间。当真实相位超过这个范围时,会发生跳变。你需要把这些跳变"接"回去,恢复出连续的相位曲线。常用的算法是Itai-Itai相位解缠,核心思路是检测相邻采样点之间的相位差,如果差值超过π,就加减2π进行修正。
第二是载波频偏(CFO)和采样频偏(SFO)。发射端和接收端的晶振不可能完全一致,这会导致相位随时间线性漂移。如果不消除这个漂移,你看到的相位变化可能全是频偏造成的,跟人体运动毫无关系。常用的消除方法是线性拟合去漂移:对一段时间窗口内的相位做线性回归,然后减去拟合出的线性分量。
import numpy as np def unwrap_phase(phase): """Itai-Itai相位解缠""" unwrapped = np.zeros_like(phase) unwrapped[0] = phase[0] for i in range(1, len(phase)): diff = phase[i] - phase[i-1] if diff > np.pi: diff -= 2 * np.pi elif diff < -np.pi: diff += 2 * np.pi unwrapped[i] = unwrapped[i-1] + diff return unwrapped def remove_linear_trend(phase): """线性拟合去漂移""" n = len(phase) x = np.arange(n) coeffs = np.polyfit(x, phase, 1) trend = np.polyval(coeffs, x) return phase - trend这两步做完之后,相位数据才真正可用。我实测下来,如果跳过相位解缠直接做后续处理,模型基本学不到任何有用的东西,准确率跟随机猜差不多。
3.3 子载波选择与降维
64个子载波不是每一个都有用。有些子载波信噪比很低,有些受多径干扰严重,还有些是导频子载波不携带数据。直接拿全部子载波喂给模型,不仅计算量大,还会引入噪声。
RuView这类项目通常的做法是:先计算每个子载波在一段时间窗口内的方差,选方差最大的前N个子载波。为什么选方差大的?因为方差大意味着这个子载波对信道变化敏感,而人体运动正是引起信道变化的主要原因。方差小的子载波要么是静态的,要么是被噪声淹没的,对姿态估计没有贡献。
一般来说,选前20到30个子载波就能覆盖大部分有效信息。这个降维过程能把数据量减少一半以上,对后续模型推理速度提升很明显。
3.4 从CSI到姿态的模型设计
这是整个项目最核心也最有挑战的部分。CSI数据本质上是时序信号,而姿态估计是一个回归/分类问题。RuView的模型设计大概率采用了CNN+LSTM的混合架构:
- CNN部分:把CSI数据看成一张"图像",横轴是时间,纵轴是子载波,每个像素是幅度或相位值。用卷积核在时间和子载波两个维度上提取局部特征。
- LSTM部分:CNN提取的特征序列送入LSTM,捕捉时间上的长距离依赖关系,比如"人从站立到跌倒"这个动作的时序模式。
- 输出层:根据任务不同,输出可以是分类(站立/坐着/躺着/跌倒)或回归(关节点坐标)。
训练数据的采集是个大工程。你需要在不同位置、不同朝向、不同人数的情况下,同步采集CSI数据和对应的真实姿态(用摄像头或动捕设备标注)。这个数据集的规模和多样性直接决定了模型的泛化能力。我见过很多demo在实验室里跑得很好,一换环境就崩,根本原因就是训练数据太单一。
实操心得:如果你刚开始做,不要一上来就追求精细姿态估计。先从"有人/没人"的二分类做起,再做到"站立/坐着/躺着"的三分类,最后再挑战跌倒检测和骨架估计。每一步都验证通过再往下走,否则你会被中间某个环节的bug卡死。
4. 完整实操流程与关键环节实现
4.1 硬件准备与接线
RuView的硬件清单其实很简洁:
| 组件 | 型号建议 | 数量 | 备注 |
|---|---|---|---|
| 主控模组 | ESP32-S3-DevKitC | 2块 | 一块发,一块收 |
| USB数据线 | Type-C | 2根 | 供电+串口通信 |
| 上位机 | 任意Linux/Windows PC | 1台 | 跑数据处理和模型 |
| 路由器 | 普通2.4GHz路由器 | 1台 | 提供WiFi信号源 |
接线方面没什么复杂的,ESP32通过USB直接连电脑就行。但有一个关键点:收发两端要配置成相同的信道和带宽。如果发送端在信道6,接收端在信道1,那收到的CSI数据基本是噪声。我建议固定使用信道6(2.437GHz),因为大多数家用路由器默认就在这个信道,干扰相对可控。
4.2 ESP32端CSI采集程序
ESP32端的核心任务是:连接WiFi、注册CSI回调、把CSI数据通过串口或UDP发出去。下面是关键代码框架:
#include "esp_wifi.h" #include "esp_log.h" static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info) { if (!info || !info->buf) return; // 打印CSI数据长度和RSSI printf("CSI_LEN:%d,RSSI:%d,", info->len, info->rx_ctrl.rssi); // 逐字节输出CSI原始数据 for (int i = 0; i < info->len; i++) { printf("%02x", info->buf[i]); } printf("\n"); } void app_main(void) { // WiFi初始化(略) esp_wifi_set_csi_rx_cb(wifi_csi_rx_cb, NULL); esp_wifi_set_csi_config(&(wifi_csi_config_t){ .lltf_en = true, .htltf_en = true, .stbc_htltf2_en = true, .ltf_merge_en = true, .channel_filter_en = false, .manu_scale = false, }); esp_wifi_set_csi(true); }这段代码里有个细节值得说:channel_filter_en我建议设为false。开启信道滤波虽然能减少噪声,但也会滤掉一些有用的多径信息。在初期调试阶段,先把原始数据拿全,后面再在上位机做滤波,灵活性更高。
4.3 上位机数据处理流水线
上位机收到串口数据后,需要经过一条完整的处理流水线才能变成模型可用的输入。我把它拆成六步:
第一步:解析原始数据。把十六进制字符串还原成复数数组。注意ESP32的CSI数据格式是每两个字节一组,具体哪个是实部哪个是虚部,要对照芯片手册确认。
第二步:相位解缠与去漂移。就是前面3.2节讲的那两步,这是整个流水线里最关键的预处理。
第三步:异常值剔除。CSI数据里经常会出现突发的异常值(比如WiFi重传、信道切换导致的跳变)。我通常用中值滤波或者3σ原则把这些点剔掉。
第四步:滑动窗口切分。姿态估计需要一段时间的信号,不能只看单次采样。通常窗口长度取0.5到1秒,步长取窗口的一半。以100Hz采样率为例,窗口就是50到100个采样点。
第五步:归一化。把幅度和相位分别归一化到均值为0、方差为1,消除不同设备、不同距离带来的尺度差异。
第六步:特征提取或直接送入模型。如果模型是端到端的,这一步可以省略;如果用的是传统机器学习方法,还需要手动提取多普勒频移、时延剖面等特征。
def process_csi_pipeline(raw_csi, window_size=100, step=50): """完整的CSI处理流水线""" # 1. 解析为复数 csi_complex = parse_csi(raw_csi) # 2. 提取幅度和相位 amplitude = np.abs(csi_complex) phase = np.angle(csi_complex) # 3. 相位处理 phase = np.array([unwrap_phase(p) for p in phase.T]).T phase = np.array([remove_linear_trend(p) for p in phase.T]).T # 4. 异常值剔除 amplitude = median_filter(amplitude, size=3) phase = median_filter(phase, size=3) # 5. 滑动窗口切分 windows = [] for start in range(0, len(amplitude) - window_size, step): amp_win = amplitude[start:start+window_size] phase_win = phase[start:start+window_size] windows.append(np.stack([amp_win, phase_win], axis=-1)) # 6. 归一化 windows = [(w - w.mean()) / (w.std() + 1e-8) for w in windows] return np.array(windows)4.4 模型训练与部署
模型训练这块,我建议先用公开数据集验证流程。WiFi感知领域有几个公开数据集可以用,比如用Intel 5300采集的那些。虽然硬件平台不同,但数据格式和处理思路是相通的,可以帮你快速验证预处理流水线是否正确。
训练的时候有几个参数需要特别注意:
- 学习率:建议从1e-3开始,用余弦退火调度。WiFi CSI数据噪声大,学习率太高容易震荡。
- Batch Size:32到64比较合适。太小了梯度不稳,太大了显存吃不消。
- 数据增强:可以对CSI数据做时间轴上的随机裁剪、幅度上的随机缩放,模拟不同距离和不同动作速度的情况。
- 正则化:Dropout和权重衰减都要加,WiFi感知模型很容易过拟合到特定环境。
部署的时候,如果上位机是PC,直接用PyTorch或TensorFlow推理就行。如果想部署到边缘设备(比如树莓派),需要把模型转成ONNX或TFLite格式,做量化压缩。我实测下来,一个轻量级的CNN+LSTM模型量化后大概2到3MB,在树莓派4上能做到实时推理。
注意:模型在不同房间之间的泛化是个大问题。在客厅训练的模型,搬到卧室可能准确率直接掉一半。解决办法要么是在多个环境采集数据训练,要么是用域适应(Domain Adaptation)技术。这个问题目前学术界也没有完美解法,实际部署时要有心理预期。
5. 常见问题与排查技巧实录
5.1 CSI数据全是噪声怎么办
这是新手最常遇到的问题。串口打印出来的CSI数据看起来毫无规律,幅度和相位都在乱跳。排查思路按以下顺序来:
先确认收发两端在同一信道。这是最容易被忽略的问题。用esp_wifi_set_channel显式设置信道,两端必须一致。
再确认CSI回调真的被触发了。在回调函数里加一个计数器,看每秒能触发多少次。如果次数是0,说明CSI功能没开启成功,检查esp_wifi_set_csi(true)是否被调用。
然后检查数据解析格式。不同芯片的CSI数据字节序可能不同,实部虚部的位置也可能相反。试着把实部虚部对调一下,看看相位是否变得连续。
最后检查环境干扰。2.4GHz频段非常拥挤,微波炉、蓝牙设备、邻居的WiFi都会造成干扰。尽量在相对干净的信道做实验,或者用5GHz频段(但5GHz穿透性差,穿墙场景还是得用2.4GHz)。
5.2 模型准确率上不去怎么调
准确率卡在某个值上不去,通常不是模型结构的问题,而是数据或预处理的问题。我整理了一个排查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 训练准确率高,测试低 | 过拟合 | 增加数据增强,加Dropout |
| 训练测试都低 | 预处理有问题 | 可视化CSI数据,检查相位是否连续 |
| 某些类别准确率特别低 | 类别不平衡 | 重采样或调整损失函数权重 |
| 换环境后准确率暴跌 | 泛化能力差 | 多环境数据训练,域适应 |
| 准确率随机波动 | 数据对齐有问题 | 检查CSI和标签的时间同步 |
5.3 穿墙后信号衰减太严重
这是物理规律决定的,2.4GHz WiFi穿过一堵砖墙大概衰减10到15dB,穿过混凝土墙衰减更多。如果穿墙后信号太弱,可以考虑这几个办法:
- 增加发射功率:ESP32的发射功率可以调到20dBm,但要注意法规限制。
- 使用定向天线:把全向天线换成定向天线,增益能提升6到8dBi。
- 缩短距离:在墙的两侧各放一个接收端,而不是隔很远。
- 降低带宽:20MHz带宽比40MHz的穿透性更好,因为功率密度更集中。
5.4 多个人同时在场怎么处理
单人场景下,CSI变化主要来自一个人的运动,相对好处理。多个人同时在场时,多个人的运动信号会叠加在一起,分离难度大幅上升。目前的解决方案主要有两个方向:一是用多天线(MIMO)做空间分离,不同天线接收到的信号来自不同路径,可以通过波束成形区分不同位置的人;二是用盲源分离算法(如ICA)把混合信号拆开。但说实话,多人场景目前还是学术界的研究热点,工程上能做到3人以内已经不错了。
实操心得:我踩过最大的坑是忽略了CSI数据的时间同步问题。ESP32的CSI回调时间戳精度有限,如果上位机同时处理多路CSI数据,时间对齐没做好,模型学到的全是错位的模式。后来我在每个CSI包前面加了一个高精度的时间戳(用
esp_timer_get_time),上位机按时间戳对齐,问题才解决。
6. 这个项目还能怎么扩展
RuView这个框架搭起来之后,能做的事情远不止姿态估计。我自己尝试过几个扩展方向,效果都还不错。
呼吸和心率监测。人在静止状态下,胸腔的起伏会引起CSI相位的周期性变化。这个变化的频率就是呼吸频率,通常在0.2到0.5Hz之间。心率稍微难一点,因为心跳引起的胸腔位移更小,但用带通滤波加FFT还是能提取出来。这个功能在养老监护场景下非常实用。
手势识别。挥手、握拳、手指滑动这些动作会在CSI数据上留下不同的模式。相比姿态估计,手势识别的动作幅度小,但模式更固定,反而更容易做到高准确率。我试过用10个手势做分类,在单人场景下能做到90%以上的准确率。
入侵检测。这个是最简单的应用——只要检测到CSI方差突然增大,就说明有人进入了监测区域。不需要复杂的模型,一个阈值判断就能搞定。适合做低功耗的长期监测。
睡眠分期。通过分析夜间CSI信号的周期性变化,可以粗略判断人处于深睡、浅睡还是清醒状态。这个方向目前还比较前沿,但已经有论文在做了。
最后分享一个我在实际部署中的小技巧:ESP32的CSI采样率不要设太高。很多人一上来就想跑100Hz甚至更高,但实际上人体运动的主要频率成分在10Hz以下,50Hz的采样率完全够用。采样率越高,数据量越大,串口传输越容易丢包,反而得不偿失。我一般用50Hz采样,配合1秒的滑动窗口,效果很稳。