
RuView 多节点传感架构解读基于 ADR-068 的 Per-Node State Pipeline 设计【免费下载链接】RuViewπ RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video.项目地址: https://gitcode.com/GitHub_Trending/wi/RuView导读本文围绕 RuView 仓库中传感服务器wifi-densepose-sensing-server的设计决策文档 ADR-068完整讲解该项目的核心数据通路重构——从单一全局状态管线升级为每个 ESP32 节点独立一条感知管线再在服务器侧按节点聚合。多节点并行采集 WiFi CSI信道状态信息时人数统计、动作分类与心率/呼吸等生命体征如何做到互不串扰、可扩展至 256 个节点是本文要回答的技术问题。读完本文你将掌握AppStateInner的状态组织方式、NodeState的核心字段语义、smooth_and_classify_node的平滑与去抖实现细节以及该项目用于验证多节点正确性的 QEMU 仿真与真实固件通路。一、问题背景为什么多节点会共用一个大脑RuView 的传感服务器最初为单节点工作方式设计对应 crate 源码位于 v2/crates/wifi-densepose-sensing-server。当多个 ESP32 节点同时向服务器推送 CSI 帧时所有数据会被混入同一条共享管线。ADR-068 中列出了当时共享状态的具体表现共享字段造成的影响frame_history时序分析混入所有节点的 CSI 数据smoothed_person_score人数统计把所有节点聚合在一起smoothed_motion动作分类无法区分节点smoothed_hr/br心率/呼吸变成全局值而非按节点baseline_motion自适应基线建立在混合数据上debounce_counter所有节点共享同一个去抖状态这种单一管线导致的直接后果是无论部署了几个节点、人在房间哪个位置、房间里有几个人检测窗口都输出一模一样的聚合结果。ADR-068 明确记录这是该项目当时反馈最集中的问题issue #24924 条评论相关症状还包括 #2370/1/2 人显示相同12 条评论、#276只能检测到一个人8 条评论、#282检测失败5 条评论。1.1 根因已被源码级证实ADR-068 说明该根因在AppStateInner文档写作时位于main.rs279–367 行区间中得到了直接确认frame_history、smoothed_person_score、smoothed_motion、smoothed_hr/br、baseline_motion、debounce_counter这些字段全部是进程级单一实例。今天的源码仍然保留这些全局字段用于兼容单节点/仿真路径见下文向后兼容一节但已经额外引入按节点状态二者共同构成了完整的演进脉络。在引入 per-node 状态之前PR #295 的迟滞平滑hysteresis smoothing只能算部分缓解无法真正隔离节点间数据。二、架构决策HashMapu8, NodeState的多节点状态追踪ADR-068 的核心决策非常直接在AppStateInner中引入HashMapu8, NodeState其中 key 是每个 ESP32 节点帧头里的node_id字节value 是该节点独立的完整感知管线状态。以下架构图即 ADR-068 原文所描述的数据流┌─────────────────────────────────────────┐ UDP frames │ AppStateInner │ ───────────► │ │ node_id1 ──► │ node_states: HashMapu8, NodeState │ node_id2 ──► │ ├── 1: NodeState { frame_history, │ node_id3 ──► │ │ smoothed_motion, vitals, ... }│ │ ├── 2: NodeState { ... } │ │ └── 3: NodeState { ... } │ │ │ │ ┌── Per-Node Pipeline ──┐ │ │ │ extract_features() │ │ │ │ smooth_and_classify() │ │ │ │ smooth_vitals() │ │ │ │ score_to_person_count()│ │ │ └────────────────────────┘ │ │ │ │ ┌── Multi-Node Fusion ──┐ │ │ │ Aggregate person count │ │ │ │ Per-node classification│ │ │ │ All-nodes WebSocket msg│ │ │ └────────────────────────┘ │ │ │ │ ──► WebSocket broadcast (sensing_update) │ └─────────────────────────────────────────┘2.1 在真实源码中定位该设计这一决策在今天的主干代码里是可见且持续演进的AppStateInner中新增了字段node_states: HashMapu8, NodeState注释写明Keyed bynode_idfrom the ESP32 frame headermain.rs并在初始化时置为HashMap::new()。UDP 帧处理路径通过s.node_states.entry(node_id).or_insert_with(NodeState::new)惰性创建节点状态——也就是说节点第一次出现时状态被自动分配见 main.rs。同一模式也用于边缘 vitals 通路issue #249与 sync 包处理通路。全局版本smooth_and_classify作用于AppStateInner与按节点版本smooth_and_classify_node作用于mut NodeState并存。ADR-068 把后者列为新增函数并从源码注释Per-node variant ofsmooth_and_classifythat operates on aNodeStateinstead ofAppStateInner(issue #249)可以确认它的职责边界见 main.rs。2.2 NodeState 核心结构ADR-068 给出了设计时的NodeState结构struct NodeState { frame_history: VecDequeVecf64, smoothed_person_score: f64, prev_person_count: usize, smoothed_motion: f64, current_motion_level: String, debounce_counter: u32, debounce_candidate: String, baseline_motion: f64, baseline_frames: u64, smoothed_hr: f64, smoothed_br: f64, smoothed_hr_conf: f64, smoothed_br_conf: f64, hr_buffer: VecDequef64, br_buffer: VecDequef64, rssi_history: VecDequef64, vital_detector: VitalSignDetector, latest_vitals: VitalSigns, last_frame_time: Optionstd::time::Instant, edge_vitals: OptionEsp32VitalsPacket, }ADR-068 强调这个结构把原来散落在全局的时序历史、平滑缓冲、基线、去抖与生命体征状态全部收拢到节点维度。值得说明的是源码实现随后又在结构体上追加了大量字段注释标明它们均以 per-node 为前提main.rs时间同步与速率感知latest_sync/latest_sync_atADR-110 网格同步包、latest_csi_sequence、csi_fps_ema/csi_fps_samples按节点 EMA 跟踪真实 CSI 帧率特征与融合latest_features供跨节点融合使用、prev_keypoints、motion_energy_history与coherence_scoreRuVector 相位二时序平滑网格一致性active_gridADR-110 / issue #1005防止把 HE-SU 256-bin 与 HT 64-bin 两套符号网格混入同一个frame_history否则会破坏方差/基线统计NodeState::accept_grid负责校验可选择性增强feature_history/last_novelty_scoreADR-084 Pass 3 的按节点新奇度传感器置None可在节点维度关闭。这些演进说明 ADR-068 确立的节点独立状态已成为后续功能同步、速率感知、融合、新奇度门控的公共基座。2.3 smooth_and_classify_node节点级分类的实际执行逻辑ADR-068 指出smooth_and_classify_node从全局版本复制了部分逻辑列为负面项。结合源码可看到它的几个关键特征main.rs帧率自适应而非假设 10 FPS函数从ns.csi_fps_ema取该节点当前真实 CSI 帧率dt 1.0 / fps将三个时间常数换算为逐帧系数motion EMA 时间常数NODE_MOTION_TIME_CONSTANT_SECS 0.6154baseline EMA 时间常数NODE_BASELINE_TIME_CONSTANT_SECS 33.28去抖时长NODE_DEBOUNCE_DURATION_SECS 0.4等价于原来假设 10 FPS下的DEBOUNCE_FRAMES 4基线预热NODE_BASELINE_WARMUP_SECS 5.0等价于原 50 帧。自适应基线预热阶段baseline_frames warmup_frames_needed用较粗的 0.9/0.1 加权累积基线之后仅在房间安静raw_motion smoothed_motion 0.05时用baseline_alpha缓慢更新抑制环境慢漂移。去抖状态机原始分类结果只有连续debounce_frames_needed帧都同意某个候选级别时才切换current_motion_level从而避免边界抖动。输出映射沿用全局版本语义raw.presence sm 0.03raw.confidence (0.4 sm * 0.6).clamp(0.0, 1.0)。值得注意ADR-068 之外的实现还补充了突发帧过滤MIN_PLAUSIBLE_CSI_DT_SEC 0.005拒绝低于 200 fps 物理上限对应间隔的子毫秒突发到达样本防止 UDP 突发把csi_fps_ema抬高 1~3 个数量级issue #1180见 main.rs。三、多节点聚合规则人数、分类、生命体征与信号场ADR-068 对多节点如何汇总给 UI给出了五条明确规则它们决定了 WebSocket 消息的最终形态人数统计对存活节点10 秒内出现过帧的prev_person_count求和分类每个节点的分类结果进入SensingUpdate.nodesUI 可逐节点渲染生命体征按节点给出心率/呼吸UI 可选择按节点展示或再聚合信号场signal field取最近更新节点的特征来生成过期节点超过 10 秒无帧的节点被排除出聚合并标记为离线与 PR #300 保持一致。3.1 存活判定与离线标记在代码中的时间尺度ADR-068 写作时用10 秒作为聚合/离线的阈值。在后续代码演进中离线判定收敛到更短的ESP32_OFFLINE_TIMEOUT5 秒并作用于数据源回退到 offlinemain.rs以及build_node_features的 per-nodestale标记main.rs。因此阅读最新主干时以 5 秒离线阈值为准这与source esp32路径的esp32:offline视图回退保持一致ADR-068 中的 10 秒表述是设计当时的约定。3.2 聚合函数与它背后的计数演进aggregate_person_countmain.rs正是 ADR-068 聚合规则在代码中的落点。有意思的是它的实现并非简单求和而是fn aggregate_person_count( activity_count: usize, node_states: std::collections::HashMapu8, NodeState, ) - usize { let node_max node_states .values() .map(|n| n.prev_person_count) .max() .unwrap_or(0); activity_count.max(node_max) }即取活动度计数activity score与节点自报人数最大值两者的较大值。背后的原因是 issue #803全局活动度分数会饱和于 1无法区分房间是否多于一人而 ESP32 固件自带的n_persons或 DynamicMinCut 的corr_persons已经被写入NodeState::prev_person_count聚合时必须取大不取小。该函数配套的单元测试aggregate_person_count_tests覆盖了空节点回退、节点报告提升饱和计数、活动度更高时不被节点值拉低、多节点取最大值四类情形main.rs。3.3 sensing_update 消息格式兼容ADR-068 明确要求WebSocket 消息格式sensing_update保持不变但nodes数组现在包含所有活跃节点estimated_persons反映跨节点聚合结果。主干代码中SensingUpdate结构体确实保留了nodes: VecNodeInfo、estimated_persons: Optionusize且源码注释写明Per-node feature breakdown for multi-node deploymentsmain.rs。这一约定让现有 UI桌面端Sensing.tsx、移动端LiveScreen的sensing.ts类型、MCP 的presence-now工具无需大幅改动即可消费多节点数据。四、扩展性与内存特性按 u8 上限 256 节点线性扩展ADR-068 用一张表直接估算了多节点内存开销核心假设是每节点约 50 KB其中大头是frame_history100 帧 × 每帧约 500 字节。当前主干中FRAME_HISTORY_CAPACITY 100main.rs与 ADR 估算口径吻合——全局frame_history与每个NodeState.frame_history共用同一个环形缓冲上限常量超限即pop_front淘汰最老帧。节点数每节点内存总开销说明1~50 KB~50 KB与单节点现状一致3~50 KB~150 KB典型家庭部署10~50 KB~500 KB小型办公室50~50 KB~2.5 MB整层楼100~50 KB~5 MB大型部署256~50 KB~12.8 MB上限u8 node_id 编码范围结论是内存随节点数线性增长即使到 256 个节点u8的编码上限也只占约 12.8 MB服务器端完全可承受。4.1 HashMap 增长风险与清理策略ADR-068 把节点 ID 碰撞和HashMap 无清理增长列为两大风险并分别给出缓解手段ID 碰撞自 v0.5.0 起 ESP32 节点通过 NVS非易失性存储持久化node_id重启后保持不变无界增长通过过期节点驱逐来缓解——build_node_features依据last_frame_time距当前超过离线阈值即标记stale配合ESP32_OFFLINE_TIMEOUT实现超过 N 秒无帧即视为离线。从代码结构看stale_nodes_are_excluded_from_rank_assignment这类测试还进一步验证了过期节点不参与位置/等级分配main.rs说明驱逐逻辑被多个下游环节共享。五、验证路径QEMU Swarm 多节点仿真与真实固件通路ADR-068 的验证计划直接建立在既有 QEMU 仿真基建上其前置设施 ADR-062 QEMU Swarm Configurator对应脚本为 scripts/qemu_swarm.py 与 scripts/swarm_health.py。其支持四种可配置拓扑拓扑含义star中心协调器 传感器节点mesh全连接对等网络line顺序链ring环形拓扑每个 QEMU 实例通过 NVS 预置唯一的node_idswarm_health.py则按节点检查其 UART 输出。ADR-068 建议的验证步骤为用 mesh 拓扑起 3–5 节点的 QEMU swarm验证服务器能产出互不相同的逐节点分类结果验证聚合人数能反映多节点的贡献验证超时后过期节点被正确驱逐。这套QEMU 多实例 逐节点健康校验的方法论在仓库中持续延用参见 scripts/qemu-mesh-test.sh、scripts/qemu-snapshot-test.sh 等衍生脚本。除仿真外真实数据通路同样被覆盖UDP 帧处理分支会按frame.node_id写入对应NodeStatemain.rs而 ESP32 固件侧的n_persons/同步包则分别经边缘 vitals 与 sync 分支更新同一节点状态构成固件 → 服务器 per-node → 聚合的完整链路。六、落地影响与已知代价6.1 正向收益每个节点的 CSI 数据独立处理不存在跨节点交叉污染人数统计随部署节点数扩展不再永远显示 1 人生命体征按节点给出为房间级健康监护打下基础为空间定位逐节点位置 三角定位提供状态基座256 节点规模下内存开销 13 MB扩展性可控。6.2 代价与风险每节点约 50 KB 额外内存smooth_and_classify_node与全局版本存在逻辑重复代码中也保留了全局版本供仿真/单源路径使用每节点一个VitalSignDetectorCPU 开销随节点数近似线性增长节点 ID 碰撞风险以 NVS 持久化缓解、HashMap 无清理增长风险以过期驱逐缓解。6.3 向后兼容设计ADR-068 特别列出三点兼容承诺它们在架构层面保证了平滑升级仿真数据通路simulated_data_task继续使用全局状态单节点部署行为不变HashMap 中只有一项sensing_update消息格式保持不变仅nodes数组与estimated_persons语义扩展边缘 vitals 通路issue #323 修复同样改用 per-node 状态。对应地当前AppStateInner仍完整保留frame_history、smoothed_person_score、smoothed_motion、smoothed_hr/br、baseline_motion、debounce_counter等全局字段main.rs仿真/单源分支如simulated_data_task生成的SensingUpdate继续写入这些字段——这与 ADR-068 的兼容承诺一致。七、后续演进ADR-069 与 per-node 基座的延伸ADR-068 在Related ADRs一节指向了 ADR-069ESP32 CSI → Cognitum Seed RVF Ingest Pipeline后者直接复用本 ADR 确立的 per-node 状态架构把逐节点特征向量经 bridge 送入 Cognitum Seed 的 RVF 存储并带 witness chain 见证。文档记录的 2026-04-02 实机验证确认了这条链路该验证描述以 ADR-069 文档为准。从代码结构看NodeState.latest_featuresLatest extracted features for cross-node fusion正是这类跨层消费的接口点说明节点状态自治已经成为 RuView 多节点传感数据面的通用设计语言。八、关联资料速查ADR-068 末尾记录的驱动问题与相关改动方便按需回溯Issue #249检测窗口无论节点数都相同24 条评论触发本 ADR 的核心问题Issue #2370/1/2 人显示相同12 条评论Issue #276只能检测到一个人8 条评论Issue #282检测失败5 条评论PR #295迟滞平滑仅部分缓解PR #300ESP32 超过 5 秒离线检测ADR-062QEMU Swarm ConfiguratorADR-069ESP32 CSI → Cognitum Seed RVF Ingest Pipeline技术正文以外的补充指引想直接运行/调试该服务器可阅读 crate 自身的 README想观察 per-node 状态在 UI 侧的消费方式可查看桌面端 Sensing.tsx 与移动端 sensing.ts想复现多节点推流仓库还提供了 MQTT 示例examples/mqtt_publisher.rs与 Python 侧 WebSocket 客户端python/wifi_densepose/client/ws.py。【免费下载链接】RuViewπ RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video.项目地址: https://gitcode.com/GitHub_Trending/wi/RuView创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考