首页
/ RuView 实时感知 UI 数据源透明化与信号响应式精度重构:ADR-035 工程实践解析

RuView 实时感知 UI 数据源透明化与信号响应式精度重构:ADR-035 工程实践解析

2026-09-07 16:06:24作者:虞亚竹Luna

本文以 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 的调查将失真相因收敛为四条:

  1. Docker 默认使用模拟数据源——--source simulated 是旧默认值,服务端自行生成正弦波合成数据,而非读取真实 UDP 帧;
  2. 演示姿态是纯解析计算的——旧版 derive_pose_from_sensing()sin(tick) 数学公式生成关键点,与实际信号内容毫无关联,且默认不加载任何已训练的 .rvf 模型;
  3. 感知特征提取过度简化——服务端对运动检测仅用"单帧阈值",完全没有时域分析(呼吸 FFT、滑动窗口方差、帧历史);
  4. 没有数据源指示器——用户无法分辨屏幕上到底是真实数据还是模拟数据。

这四条根因分别对应"数据入口、姿态合成、特征提取、前端呈现"四个层面,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-compose5005: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.mddocker/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.rsderive_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_bpmfreq = (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 字段,取值二选一:

该字段从前端入口到渲染链路被完整透传(见第六节 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() 对环形缓冲做一次遍历,同步累积各子载波幅值的 sumsum_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.jsui/components/LiveDemoTab.jsui/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.jsui/utils/connection-status.js 承担横幅渲染与状态判定。

六、配套改动:暗色模式、四种渲染模式与 pose_source 透传修复

6.1 决策 5:Live Demo 暗色模式一致性

Live Demo 标签页由亮色主题转换为暗色,与其余 UI 统一:所有侧边栏面板、徽章、按钮、下拉框均采用深色背景 + 弱化文字。避免"看数据时被刺眼的白底界面闪到"这一真实体验问题。

6.2 决策 6:四种渲染模式全部落地(不再回退到骨架)

姿态可视化下拉框里的四种渲染模式此前 heatmapdense 是空壳(回退到骨架),本 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.jsconvertZoneDataToRestFormat() 数据转换中被丢弃,导致 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.ymldocker/Dockerfile.rustdocker/docker-entrypoint.sh
帧历史缓冲 / Goertzel 呼吸估计 / 时域运动评分 / 信号驱动姿态 v2/crates/wifi-densepose-sensing-server/src/main.rsv2/crates/wifi-densepose-sensing-server/src/csi.rsv2/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.jsui/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 徽章)四个维度建立了可复用的工程范式。对任何从事"传感器数据 + 前端可视化"的开发者而言,这套"先分清真伪、再让动画有依据、最后把来源写进每条消息"的治理顺序,本身就是一份难得的参考清单。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391