diff --git a/docs/trust-and-engine-errors.md b/docs/trust-and-engine-errors.md index 29843dde0..e49f304f0 100644 --- a/docs/trust-and-engine-errors.md +++ b/docs/trust-and-engine-errors.md @@ -116,6 +116,7 @@ handler (`health_ready`, `main.rs:8128,8133`) and return a `trust` block "waiting_for_frames": false, "last_frame_age_ms": 42, "trust": { + "node_positions": { "1": [0.5, 0.0, 1.0], "2": [4.0, 3.0, 1.0] }, "last_witness": "…64 hex chars or null…", "effective_class": "Anonymous | Restricted | …", "demoted": false, @@ -126,6 +127,11 @@ handler (`health_ready`, `main.rs:8128,8133`) and return a `trust` block } ``` +`node_positions` is the `--node-positions` map the governed fuser holds, +keyed by node id. It is `{}` when no positions are configured, and then every +node fuses at the origin. A node the map does not name also fuses at the +origin. + **This is a real, currently-shipped diagnostic surface — but it is honestly limited.** It tells you *that* errors are occurring and *that* the current class is demoted, and the total count, but not *why* for your specific run: diff --git a/v2/crates/wifi-densepose-engine/src/lib.rs b/v2/crates/wifi-densepose-engine/src/lib.rs index 6d86c80d4..f22351043 100644 --- a/v2/crates/wifi-densepose-engine/src/lib.rs +++ b/v2/crates/wifi-densepose-engine/src/lib.rs @@ -264,6 +264,14 @@ impl StreamingEngine { self.fuser.set_node_positions_by_id(positions); } + /// Node positions the governed fuser currently holds, keyed by node id. + /// Empty until [`Self::set_node_positions`] is called, in which case every + /// node fuses at the origin. + #[must_use] + pub fn node_positions(&self) -> &std::collections::HashMap { + self.fuser.node_positions_by_id() + } + /// Activate a per-room calibration adapter (ADR-150 §3.4). From the next /// cycle on, the adapter id is part of provenance `model_version` — and /// therefore of the witness — so the exact weights shaping inference are diff --git a/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs b/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs index f046a28d9..dc733624c 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs @@ -135,6 +135,21 @@ impl EngineBridge { self.engine.set_semantic_retention(max_states); } + /// Give the governed fuser the `--node-positions` map, keyed by node id. + /// The fuser resolves each frame's position by its `node_id`, so nodes + /// discovered later pick up their entry without another call; nodes with + /// no entry fuse at the origin. + #[must_use] + pub fn with_node_positions(mut self, positions: HashMap) -> Self { + self.engine.set_node_positions(positions); + self + } + + /// Node positions the governed fuser is using, keyed by node id. + pub fn node_positions(&self) -> &HashMap { + self.engine.node_positions() + } + /// Switch the active privacy mode (operator/control-plane action). pub fn set_privacy_mode(&mut self, mode: PrivacyMode) { self.engine.set_privacy_mode(mode); @@ -309,6 +324,33 @@ mod tests { m } + fn configured_positions() -> HashMap { + [(0u8, [0.5, 0.0, 1.0]), (1u8, [4.0, 3.0, 1.0])] + .into_iter() + .collect() + } + + #[test] + fn with_node_positions_reaches_the_governed_fuser() { + let bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None) + .with_node_positions(configured_positions()); + assert_eq!(bridge.node_positions(), &configured_positions()); + } + + #[test] + fn node_positions_survive_a_guard_config() { + let cfg = MultistaticConfig { + guard_interval_us: 200_000, + min_nodes: 1, + ..MultistaticConfig::default() + }; + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", Some(cfg)) + .with_node_positions(configured_positions()); + assert_eq!(bridge.node_positions(), &configured_positions()); + assert!(bridge.observe_cycle(&two_node_states(), 1_000).is_some()); + assert_eq!(bridge.node_positions(), &configured_positions()); + } + #[test] fn empty_states_produce_no_belief() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room", None); diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 667b3524b..d93edb7e3 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -7142,6 +7142,31 @@ async fn health_live(State(state): State) -> Json) -> serde_json::Value { + let sorted: BTreeMap = positions + .iter() + .map(|(id, p)| (id.to_string(), *p)) + .collect(); + serde_json::json!(sorted) +} + +#[cfg(test)] +mod engine_node_positions_json_tests { + use super::*; + + #[test] + fn engine_node_positions_json_is_keyed_by_id() { + let configured: HashMap = [(12, [4.0, 3.0, 1.0]), (11, [0.5, 0.0, 1.0])] + .into_iter() + .collect(); + assert_eq!( + engine_node_positions_json(&configured), + serde_json::json!({"11": [0.5, 0.0, 1.0], "12": [4.0, 3.0, 1.0]}) + ); + } +} + /// Lowercase hex of a 32-byte witness for JSON exposure. fn witness_hex(w: [u8; 32]) -> String { use std::fmt::Write; @@ -7168,6 +7193,8 @@ async fn health_ready(State(state): State) -> Json = None; let mut node_positions_config: HashMap = HashMap::new(); + // The same map for the governed fuser behind `engine_bridge`, which + // otherwise places every node at the origin. + let mut engine_bridge_node_positions: HashMap = HashMap::new(); let state: SharedState = Arc::new(RwLock::new(AppStateInner { latest_update: None, rssi_history: VecDeque::new(), @@ -13146,6 +13176,7 @@ async fn main() { // no `node_id:` prefix takes its list index as its id), so // an existing `--node-positions` string keeps its meaning. fuser.set_node_positions_by_id(node_positions_config.clone()); + engine_bridge_node_positions = node_positions_config.clone(); } } engine_bridge_multistatic_cfg = Some(MultistaticConfig { @@ -13162,7 +13193,8 @@ async fn main() { "default", "Default Room", engine_bridge_multistatic_cfg, - ), + ) + .with_node_positions(engine_bridge_node_positions), field_model: bootstrap_field_model, installation_id: args.installation_id.clone(), bootstrap_baseline: bootstrap_metadata.clone(),