fix(sensing-server): wire --node-positions into live NodeInfo output

Node position parsing/storage for multistatic fusion worked correctly,
but the per-node JSON builder (both the vitals-only and governed/
privacy-gated ESP32 live-data paths) never read the parsed positions -
it always emitted the hardcoded fallback [2.0, 0.0, 1.5] regardless of
--node-positions. Store the parsed positions in AppStateInner, keyed
by 0-based index (= node_id), and read from it at both call sites.

Matches upstream issues #228, #249, and the "Known Issues" #301 symptom
class ("same output regardless of position").

Co-Authored-By: claude-flow <ruv@ruv.net>
(cherry picked from commit d34d76c54e7ba4a466cf9dc27060684f49756b6b)
This commit is contained in:
Joe committed 2026-09-03 08:00:30 -04:00
1 parent db5a898372
commit 25d7be3c25
1 file changed
+19 -3
@@ -1417,6 +1417,9 @@ struct AppStateInner {
last_tracker_instant: Option<std::time::Instant>,
/// Attention-weighted multi-node CSI fusion engine.
multistatic_fuser: MultistaticFuser,
/// Node positions parsed from --node-positions, keyed by 0-based index
/// into the parsed list (which corresponds to node_id).
node_positions_config: HashMap<u8, [f32; 3]>,
/// Governed trust-path bridge (ADR-135..146): runs the same live frames
/// through the privacy/provenance/witness control plane. Does not alter
/// person-count behavior; its trust state (witness, effective class,
@@ -1678,6 +1681,7 @@ impl AppStateInner {
pose_tracker: PoseTracker::new(),
last_tracker_instant: None,
multistatic_fuser: MultistaticFuser::new(),
node_positions_config: HashMap::new(),
engine_bridge: engine_bridge::EngineBridge::new(
wifi_densepose_bfld::PrivacyMode::PrivateHome,
1,
@@ -6526,7 +6530,11 @@ async fn udp_receiver_task(
.map(|(&id, n)| NodeInfo {
node_id: id,
rssi_dbm: n.rssi_history.back().copied().unwrap_or(0.0),
position: [2.0, 0.0, 1.5],
position: s
.node_positions_config
.get(&id)
.map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
.unwrap_or([2.0, 0.0, 1.5]),
amplitude: vec![],
subcarrier_count: 0,
// Vitals-only path; still expose the sync snapshot
@@ -7010,7 +7018,11 @@ async fn udp_receiver_task(
.map(|(&id, n)| NodeInfo {
node_id: id,
rssi_dbm: n.rssi_history.back().copied().unwrap_or(0.0),
position: [2.0, 0.0, 1.5],
position: s
.node_positions_config
.get(&id)
.map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
.unwrap_or([2.0, 0.0, 1.5]),
amplitude: if suppress_raw {
vec![]
} else {
@@ -8581,7 +8593,7 @@ async fn main() {
// threaded into `engine_bridge` so both fusion paths honor the same
// WDP_TDM_SLOTS/WDP_GUARD_INTERVAL_US-derived guard (#1049/#1057).
let mut engine_bridge_multistatic_cfg: Option<MultistaticConfig> = None;
let mut node_positions_config: HashMap<u8, [f32; 3]> = HashMap::new();
let state: SharedState = Arc::new(RwLock::new(AppStateInner {
latest_update: None,
rssi_history: VecDeque::new(),
@@ -8672,6 +8684,9 @@ async fn main() {
"Configured {} node positions for multistatic fusion",
positions.len()
);
for (idx, p) in positions.iter().enumerate() {
node_positions_config.insert(idx as u8, *p);
}
fuser.set_node_positions(positions);
}
}
@@ -8681,6 +8696,7 @@ async fn main() {
});
fuser
},
node_positions_config,
engine_bridge: engine_bridge::EngineBridge::new(
wifi_densepose_bfld::PrivacyMode::PrivateHome,
1,