RuView 实时感知 UI 数据源透明化与信号响应式精度重构:ADR-035 工程实践解析
本文以 ADR-035-live-sensing-ui-accuracy.md 为核心骨架,深入 RuView(WiFi-DensePose)仓库源码,还原"实时感知界面(Live Sensing UI)显示失准、数据真假难辨"这一经典问题从定位到修复的完整决策过程。读完你会掌握:如何让服务端默认接入真实 ESP32 CSI 数据、如何用信号特征驱动姿态骨架动画、如何用 Goertzel 滤波器组与时序方差做生理意义特征提取,以及如何在 UI 上建立"数据源可信度"的透明呈现机制。
一、背景:Issue #86 暴露的"演示失真"三重根因
ADR-035 记载(日期 2026-03-02,状态 Accepted)了一个极具代表性的工程问题:即便 ESP32 真实 CSI 帧已在正常发送,实时演示页面仍显示一具"静态/几乎不动"的火柴人,且感知页面数据失真。Issue #86 的调查将失真相因收敛为四条:
- Docker 默认使用模拟数据源——
--source simulated是旧默认值,服务端自行生成正弦波合成数据,而非读取真实 UDP 帧; - 演示姿态是纯解析计算的——旧版
derive_pose_from_sensing()用sin(tick)数学公式生成关键点,与实际信号内容毫无关联,且默认不加载任何已训练的.rvf模型; - 感知特征提取过度简化——服务端对运动检测仅用"单帧阈值",完全没有时域分析(呼吸 FFT、滑动窗口方差、帧历史);
- 没有数据源指示器——用户无法分辨屏幕上到底是真实数据还是模拟数据。
这四条根因分别对应"数据入口、姿态合成、特征提取、前端呈现"四个层面,ADR-035 的修复也按这四个层面逐一展开,并在后续补充了暗色模式统一与渲染模式补全。
二、决策 1:Docker 数据源自动检测,让"真实数据默认可用"
2.1 CSI_SOURCE=auto:探测 UDP 5005,探测失败才回退模拟
旧默认值 simulated 意味着:接上真实 ESP32,用户得到的却是一组与硬件无关的正弦波。ADR-035 的决策是把默认值改为 auto:
auto先探测 UDP 5005 端口是否有 ESP32 在发帧;- 若探测到则进入真实接收模式,未探测到则回退到模拟数据;
- 用户可通过
CSI_SOURCE=esp32 docker-compose up显式强制覆盖。
对应配置落在 docker/docker-compose.yml 中:
environment:
- RUST_LOG=info
- CSI_SOURCE=${CSI_SOURCE:-auto}
- MODELS_DIR=${MODELS_DIR:-data/models}
Dockerfile 侧同样写入默认值:docker/Dockerfile.rust 定义 ENV CSI_SOURCE=auto,并注明四个合法取值。docker-compose 中 5005:5005/udp 端口映射专为接收 ESP32 CSI UDP 帧而开,3000(REST API)与 3001(WebSocket)默认只绑定到 127.0.0.1。
2.2 演进:从"静默回退"到"失败即显式退出"
需要特别说明:ADR 原始决策里 auto 是"探测失败回退模拟"。仓库现行实现(对应 Issue #937 的加固)已进一步收紧为失败即退出——合成数据不再是静默兜底,而必须显式选择。这在 docker/docker-entrypoint.sh 有明确注释:
auto— try ESP32 then Windows WiFi, fail-loud if no real hardware is detected (issue #937 fix: the server no longer silently falls back to synthetic data — that's now opt-in only).
ui/README.md 与 docker/docker-compose.yml 也同步记录了这一语义。四个取值的完整语义如下:
CSI_SOURCE |
含义 | 适用场景 |
|---|---|---|
auto |
依次探测 ESP32 UDP 与主机 WiFi;探测不到真实硬件则 fail-loud 退出(exit 78),不再静默生成合成数据 | 默认值,推荐大多数部署 |
esp32 |
直接从 UDP 5005 端口接收真实 ESP32 CSI 帧 | 有真实节点(1 个即可开始) |
wifi |
使用主机 Wi-Fi RSSI/扫描数据(Windows netsh 路径) | 无 ESP32、仅有 PC 网卡时 |
simulated |
显式生成合成 CSI 帧(demo 专用) | 仅演示/无硬件,必须显式声明 |
对应的运行形态有两种:环境变量方式 docker run -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest,或直接透传 CLI 标志 docker run <image> --source esp32 --tick-ms 500。docker-entrypoint.sh 会把无参数启动时的默认参数收敛为 --source ${CSI_SOURCE:-auto} --tick-ms 100 --ui-path /app/ui --http-port 3000 --ws-port 3001。
附加信息:docker-entrypoint.sh 还内置了 Issue #864 的 fail-closed 安全默认——当
RUVIEW_API_TOKEN未设置且绑定地址非回环时,会以 exit 64 拒绝启动/ws/sensing直播流,防止未鉴权暴露传感数据。详见 docker/docker-entrypoint.sh 头部注释。
三、决策 2:信号响应式姿态推导,让骨架"跟着信号动"
3.1 让 CSI 特征直接驱动 17 关键点
ADR-035 的核心决策之一是重写姿态推导逻辑:derive_pose_from_sensing() 不再用与信号无关的 sin(tick) 数学摆拍,而是逐帧读取真实感知特征来驱动骨架。现仓库中该实现位于 v2/crates/wifi-densepose-sensing-server/src/pose.rs 的 derive_single_person_pose(),特征与动画的映射关系可逐条对照源码:
| 输入特征 | 驱动的动画效果 | 源码位置 |
|---|---|---|
motion_band_power |
肢体展开幅度与走路步态判定(> 0.55 判定为行走) |
let is_walking = motion_score > 0.55; |
breathing_band_power |
躯干扩缩,相位同步到实际呼吸频率 | breath_amp = breathing_band_power * 4.0 配合 TORSO_KP 关键点扩缩 |
variance |
逐关节抖动噪声种子,使骨架独立"呼吸式"抖动 | kp_noise_* = fract(...) * variance.sqrt() * motion_score |
dominant_freq_hz |
横向躯干倾斜(lean) | lean_x = (dominant_freq_hz / 5.0 - 1.0) * 18.0 |
change_points |
四肢端点突发抖动(burst jitter) | burst = (change_points / 20.0).clamp(0.0, 0.3) |
源码层面,pose.rs 定义了 COCO-17 骨架的 17 个关键点与骨长连接(POSE_BONE_PAIRS),并将关键点划分为两组:TORSO_KP = [5, 6, 11, 12](双肩、双髋)负责呼吸扩缩;EXTREMITY_KP = [9, 10, 15, 16](双腕、双踝)负责行走摆臂摆腿与突发抖动。行走时不同肢体按相位反向摆动:
let swing_dy = if is_walking {
let stride_phase = (feat.motion_band_power * 0.7 + update.tick as f64 * 0.12 + phase_offset).sin();
match i {
7 | 9 => -stride_phase * 20.0 * motion_score, // 左上臂/左肘上摆
8 | 10 => stride_phase * 20.0 * motion_score, // 右臂反相
13 | 15 => stride_phase * 25.0 * motion_score, // 左腿
14 | 16 => -stride_phase * 25.0 * motion_score, // 右腿反相
_ => 0.0,
}
} else { 0.0 };
呼吸相位不再用固定频率,而是取自 vital_signs.breathing_rate_bpm:freq = (bpm / 60.0).clamp(0.1, 0.5),若该帧没有可靠生命体征则回退到基础相位。姿态还会经过 apply_temporal_smoothing()(EMA 平滑)与 clamp_bone_lengths_f64()(骨长钳制,防止单帧抖动导致骨架"拉伸变形"),保证视觉稳定。
3.2 帧率翻五倍:500ms → 100ms
旧实现 tick 间隔 500ms,等效 2fps,肉眼几乎看不出运动;ADR-035 将其下调到 100ms,等效 10fps,骨架动画因此具备可感知的连续性。这一默认值在 docker-entrypoint.sh 的 --tick-ms 100 与 CLI 解析模块(tick_ms: u64)中均有体现,用户可通过 --tick-ms 500 等参数覆盖。
3.3 每一帧 WebSocket 消息都携带 pose_source
为避免"看不清姿态从哪来",ADR-035 规定每条 WebSocket 帧新增 pose_source 字段,取值二选一:
signal_derived—— 由 CSI 特征信号解析推导(本 ADR 描述的默认路径);model_inference—— 由已训练的.rvf神经网络模型推理(需MODELS_DIR挂载模型,参考 ADR-023-trained-densepose-model-ruvector-pipeline.md 与 ADR-036-rvf-training-pipeline-ui.md)。
该字段从前端入口到渲染链路被完整透传(见第六节 pose_source passthrough 修复)。
四、决策 3:时域特征提取,从"单帧阈值"到"生理意义的序列分析"
如果说姿态推导解决的是"动起来",那么特征提取解决的是"动得有意义"。ADR-035 为 v2/crates/wifi-densepose-sensing-server/src/csi.rs 引入一套完整的时域信号分析管线。
4.1 100 帧环形缓冲:VecDeque 帧历史
AppStateInner(节点状态)新增容量为 100 帧的环形缓冲 frame_history: VecDeque<Vec<f64>>,保存每个 CSI 帧的幅值向量。所有时域统计(方差、呼吸、帧间运动)都基于这段滑窗,而非孤立单帧。
4.2 逐子载波时序方差(Welford 风格累积)
compute_subcarrier_variances() 对环形缓冲做一次遍历,同步累积各子载波幅值的 sum 与 sum_of_squares,再以 E[X²] − E[X]² 推出每个子载波的时序方差(并对数值下溢做 .max(0.0) 保护)。这一"单遍累积方差"正是 Welford 算法思想的简化形式——不必缓存全部中间量即可在线维护统计。
特征聚合路径在 extract_features_from_frame() 中:先求各子载波的幅值敏感度权重与加权均值 mean_amp,再据此得到帧内方差 intra_variance 与帧间时序方差 temporal_variance,最终取两者较大者作为综合 variance:
let sub_variances = compute_subcarrier_variances(frame_history, n_sub);
let temporal_variance = sub_variances.iter().sum::<f64>() / sub_variances.len() as f64;
let variance = intra_variance.max(temporal_variance);
4.3 Goertzel 滤波器组估计呼吸率(9 候选,0.1–0.5 Hz,3×SNR 门控)
呼吸(0.1–0.5 Hz,即 6–30 次/分钟)是典型的窄带周期信号。ADR-035 采用 9 候选频点的 Goertzel 滤波器组:对候选频率逐点计算 Goertzel 功率(estimate_breathing_rate_hz,约 2526 行附近实现,含"仅在滤波能量显著高于噪声时才上报呼吸率"的 3×SNR 门控),选择能量最大的频点换算为呼吸率上报。ADR 同时给出了精确的复杂度度量:Goertzel 滤波器组每帧增加约 O(9×N) 计算量(N 为帧内采样规模),对 100 帧窗口而言可忽略。
主链路(main.rs 约 1946 行附近)中 breathing_rate_bpm: breathing_raw as f64 / 100.0 显示原始值被放大 100 倍再归一,测试亦用 breathing scale 100 断言(如 (pkt.breathing_rate_bpm - 16.0).abs() < 1e-3)锁定该比例。
4.4 帧间 L2 运动评分取代单帧幅值阈值
旧实现以单帧幅值超阈值判运动,对噪声敏感且无时间语义。ADR-035 改为帧与前一帧的 L2 距离能量作为运动分数:
let temporal_motion_score = if let Some(prev_frame) = frame_history.back() {
let diff_energy = (0..n_cmp).map(|k| (frame.amplitudes[k] - prev_frame[k]).powi(2)).sum::<f64>() / n_cmp as f64;
let ref_energy = mean_amp * mean_amp + 1e-9;
(diff_energy / ref_energy).sqrt().clamp(0.0, 1.0)
} else { /* 无历史时退回帧内方差度量 */ };
从源码可见 motion_score 是时域帧差(权重 0.4)与子载波方差、频带功率的融合;presence: motion_score > 0.04、置信度 0.4 + signal_quality*0.3 + motion_score*0.3 均由它驱动。附带一提,独立工具模块 scripts/field_localize.rs 对应实现 中的 motion_score_from_power() 还做了 motion_band_power → motion_score 的量纲归一(0–100 分制并 clamp),并有对应单测锁定非线性映射行为。
4.5 信噪比驱动的信号质量指标
信号质量不再拍脑袋,而是基于 SNR(RSSI − 噪声底)再与时序稳定性混合。从特征结构看,每一帧都携带 RSSI(mean_rssi = frame.rssi),质量分用于置信度与"信号场"(signal field)的驱动——信号场改用子载波方差的空间映射(而非固定动画)来表达人体所在的空间能量分布,为后续 Zone/多节点空间感知(ADR-029 RuvSense 多基地感知)铺路。
五、决策 4:UI 数据源透明化——"所见即真伪可知"
数据源透明是 ADR-035 的 UI 侧核心诉求,共四个子改动,源码落点均已确认存在于 ui/components/SensingTab.js、ui/components/LiveDemoTab.js、ui/services/sensing.service.js 等文件中:
5.1 Sensing 标签页:数据源横幅三态色
| 状态 | 文案 | 颜色语义 |
|---|---|---|
| 真实数据 | LIVE - ESP32 |
绿色 |
| 连接中断重试中 | RECONNECTING... |
黄色 |
| 模拟回退 | SIMULATED DATA |
红色(警示) |
5.2 Live Demo 标签页:Estimation Mode 徽章
姿态估算模式以徽章形式常驻展示,颜色与来源一一对应:
| 模式 | 文案 | 颜色 |
|---|---|---|
| 信号推导 | Signal-Derived |
绿色 |
| 模型推理 | Model Inference |
蓝色 |
该徽章的数据来源正是第三节提到的 WebSocket pose_source 字段,两者构成"服务端标注—前端呈现"的完整链路。
5.3 Setup Guide 面板:管理硬件配置预期
新增的 Setup Guide 面板用直白语言说明不同 ESP32 节点数能做什么、不能做什么,从源头管理用户预期:
- 1 个 ESP32:存在性检测 + 呼吸感知;
- 3 个 ESP32:多基地定位(localization);
- 4 个及以上 + 已训练模型:完整肢体级姿态追踪。
这一说明与 ADR 的"消极后果"章节相互呼应——单节点用户可能仍会失望"手臂追踪不可用",但 UI 现在会解释原因。
5.4 模拟回退延迟:从"立即"到"5 次重连失败(约 30 秒)"
旧逻辑在连接失败时立即回退模拟数据,用户几乎感知不到重连过程。ADR-035 将模拟回退延迟到 连续 5 次重连尝试均失败后(约 30 秒),让真实连接的恢复窗口不再被"秒回退"打断。在 ui/services/sensing.service.js 中以 dataSource 状态管理该过程,并用 _simulated 内部标记区分数据来源;ui/utils/data-source-banner.js 与 ui/utils/connection-status.js 承担横幅渲染与状态判定。
六、配套改动:暗色模式、四种渲染模式与 pose_source 透传修复
6.1 决策 5:Live Demo 暗色模式一致性
Live Demo 标签页由亮色主题转换为暗色,与其余 UI 统一:所有侧边栏面板、徽章、按钮、下拉框均采用深色背景 + 弱化文字。避免"看数据时被刺眼的白底界面闪到"这一真实体验问题。
6.2 决策 6:四种渲染模式全部落地(不再回退到骨架)
姿态可视化下拉框里的四种渲染模式此前 heatmap 与 dense 是空壳(回退到骨架),本 ADR 补全为各自独立、可区分的视觉输出,实现在 ui/utils/pose-renderer.js:
| 模式 | 渲染方式 |
|---|---|
| Skeleton | 绿色连线连接关节 + 红色关键点圆点 |
| Keypoints | 大号彩色发光圆点并带标签,无连线 |
| Heatmap | 每个关键点做高斯径向光斑(按人着色),叠加 25% 透明度骨架 |
| Dense | 肢体区域分割:头(红)、躯干(蓝)、左臂(绿)、右臂(橙)、左腿(紫)、右腿(黄)彩色填充多边形 |
Dense 模式的"头/躯干/四肢分区填色"实际上就是密集姿态(DensePose)粗粒度表达,让"无视频像素却有人体分区语义"的 Wi-Fi 感知能力以最直观的方式可视化。
6.3 决策 7:pose_source 透传修复
ADR-035 明确记录了一个隐蔽 bug:WebSocket 消息中的 pose_source 字段在 ui/services/pose.service.js 的 convertZoneDataToRestFormat() 数据转换中被丢弃,导致 Estimation Mode 徽章永远显示不出来。修复即"透传",这也是 UI 侧"数据来源可标、可传、可显"的最后一块拼图。
七、影响评估:收益与代价
正面收益
- 真实硬件用户默认拿到真实数据:
auto自动检测改变了"接了 ESP32 却看正弦波"的默认体验; - 模拟数据有明确标签:真假不再混淆,为后续所有精度讨论建立可信基座;
- 骨架视觉上响应真实信号:运动、呼吸、方差的变化都会如实反映到骨架动画上;
- 特征提取产出生理意义指标:Goertzel 呼吸率、时域运动检测使"Wi-Fi 感知生命体征"不再是一句空话;
- Setup Guide 管住硬件预期:每种硬件配置能提供什么能力一目了然。
代价与边界(如实陈述,不夸大)
- 信号推导姿态仍是近似,而非神经网络推理:逐肢体级追踪必须依赖已训练的
.rvf模型 + 4 个及以上 ESP32 节点; - Goertzel 滤波器组带来每帧约 O(9×N) 计算增量:在 100 帧窗口量级下可忽略(ADR 原话 negligible);
- 单 ESP32 用户仍无法获得手臂级追踪:但 UI 现在会解释"为什么"。
八、涉及文件全景与验证入口
ADR 原"Files Changed"清单在当前仓库中的落地映射如下(供按图索骥):
| 改动 | 仓库路径 |
|---|---|
| Docker 数据源自动检测 | docker/docker-compose.yml、docker/Dockerfile.rust、docker/docker-entrypoint.sh |
| 帧历史缓冲 / Goertzel 呼吸估计 / 时域运动评分 / 信号驱动姿态 | v2/crates/wifi-densepose-sensing-server/src/main.rs、v2/crates/wifi-densepose-sensing-server/src/csi.rs、v2/crates/wifi-densepose-sensing-server/src/pose.rs |
| 数据源状态 / 延迟模拟回退 | ui/services/sensing.service.js |
| pose_source 透传修复 | ui/services/pose.service.js |
| 数据源横幅 / About This Data | ui/components/SensingTab.js、ui/utils/data-source-banner.js |
| Estimation Mode 徽章 / Setup Guide / 暗色主题 | ui/components/LiveDemoTab.js |
| 热力图与 Dense 渲染 | ui/utils/pose-renderer.js |
验证与继续阅读:derive_single_person_pose 的各类特征驱动分支可对照 pose.rs 内的单测;motion_score_from_power 的量纲行为有 motion_score_passthrough_and_clamp 测试锁定;Docker 启动参数语义由 tests/test_docker_entrypoint.sh 校验;ui 目录的 ui/README.md 说明了整体前端结构。若想追溯这条决策在更长技术演进中的位置,可顺次阅读同一脉络上的 ADR-023(.rvf 模型管线)、ADR-036(模型训练 UI)、ADR-037(多人姿态)与 ADR-082-pose-tracker-confirmed-output-filter.md(姿态追踪输出过滤)。
结语
ADR-035 的价值远超一次 Bug 修复:它把"实时 Wi-Fi 感知演示"从"看起来在动"推进到"动得有据可查、真伪一目了然",并在数据入口(auto 检测)、特征层(时域 + 呼吸谱)、呈现层(信号驱动骨架)与信任层(三态横幅 + Estimation Mode 徽章)四个维度建立了可复用的工程范式。对任何从事"传感器数据 + 前端可视化"的开发者而言,这套"先分清真伪、再让动画有依据、最后把来源写进每条消息"的治理顺序,本身就是一份难得的参考清单。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00