From b5ce60081bd7818b641f26e5ebebfb709f15ecc4 Mon Sep 17 00:00:00 2001 From: erichkusuki Date: Sat, 11 Jul 2026 13:32:01 +0200 Subject: [PATCH] fix(sensing-server): wire WDP_GUARD_INTERVAL_US/WDP_TDM_* into EngineBridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EngineBridge::new built its own StreamingEngine, whose internal MultistaticFuser was hardcoded to MultistaticConfig::default() (60 ms guard) — the #1031/#1049 env overrides only reached the sibling multistatic_fuser field on AppState, so governed trust cycles failed against the default guard no matter what the deployment configured. - wifi-densepose-engine: add StreamingEngine::set_multistatic_config() - engine_bridge: EngineBridge::new takes Option - main.rs: thread the same env-derived config into both fusion paths Verified on a 2-node ESP32-S3 deployment: the failure message now reflects the configured guard, and with healthy nodes governed cycles ran 90 s with zero fusion errors. Fixes #1309 Co-Authored-By: claude-flow --- v2/crates/wifi-densepose-engine/src/lib.rs | 11 ++++++ .../src/engine_bridge.rs | 39 +++++++++++++------ .../wifi-densepose-sensing-server/src/main.rs | 12 +++++- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/v2/crates/wifi-densepose-engine/src/lib.rs b/v2/crates/wifi-densepose-engine/src/lib.rs index d80b0031..f9b02c6b 100644 --- a/v2/crates/wifi-densepose-engine/src/lib.rs +++ b/v2/crates/wifi-densepose-engine/src/lib.rs @@ -230,6 +230,17 @@ impl StreamingEngine { /// cycle on, the adapter id is part of provenance `model_version` — and /// therefore of the witness — so the exact weights shaping inference are /// pinned in the trust chain. Pass the result of hashing the adapter file. + /// Override the multistatic fuser's timestamp guard interval (#1049/#1057). + /// Without this, `StreamingEngine::new` always builds + /// `MultistaticFuser::with_config(MultistaticConfig::default())` — a + /// hardcoded 60 ms hard guard that ignores whatever schedule/override the + /// caller derived from `WDP_TDM_SLOTS`/`WDP_GUARD_INTERVAL_US`, so + /// WiFi/ESP-NOW-synced multi-node deployments spuriously fail governed + /// trust cycles even after widening the guard elsewhere. + pub fn set_multistatic_config(&mut self, cfg: MultistaticConfig) { + self.fuser = MultistaticFuser::with_config(cfg); + } + pub fn set_room_adapter(&mut self, info: AdapterInfo) { self.adapter = Some(info); } 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 c03ec0e9..5941a655 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs @@ -38,6 +38,7 @@ use wifi_densepose_bfld::{PrivacyClass, PrivacyMode}; use wifi_densepose_engine::{AdapterInfo, EngineError, StreamingEngine, TrustedOutput}; use wifi_densepose_geo::types::GeoRegistration; use wifi_densepose_signal::ruvsense::fusion_quality::CalibrationId; +use wifi_densepose_signal::ruvsense::multistatic::MultistaticConfig; use wifi_densepose_worldgraph::WorldId; use super::multistatic_bridge::node_frames_from_states; @@ -79,8 +80,24 @@ pub struct EngineBridge { impl EngineBridge { /// Build a bridge for one installation. `room_area_id`/`room_name` name the /// observation scope; `mode` is the starting privacy mode. - pub fn new(mode: PrivacyMode, model_version: u16, room_area_id: &str, room_name: &str) -> Self { + /// + /// `multistatic_cfg` is `None` on the pure default (60 ms/20 ms guard); + /// callers that derive a config from `WDP_TDM_SLOTS`/`WDP_GUARD_INTERVAL_US` + /// (see `main.rs::multistatic_guard_config_from_env`) should pass it here + /// too — otherwise the governed trust cycle silently keeps using the + /// hardcoded default even though the sibling `multistatic_fuser` field on + /// `AppState` picked up the override (#1049/#1057). + pub fn new( + mode: PrivacyMode, + model_version: u16, + room_area_id: &str, + room_name: &str, + multistatic_cfg: Option, + ) -> Self { let mut engine = StreamingEngine::new(mode, model_version, GeoRegistration::default()); + if let Some(cfg) = multistatic_cfg { + engine.set_multistatic_config(cfg); + } let room = engine.add_room(room_area_id, room_name); Self { engine, @@ -267,7 +284,7 @@ mod tests { #[test] fn empty_states_produce_no_belief() { - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room", None); let out = bridge.process_cycle_from_states(&HashMap::new(), 1_000); assert!(out.is_none()); // No belief published, no sensor wired. @@ -276,7 +293,7 @@ mod tests { #[test] fn live_cycle_produces_witnessed_belief_with_provenance() { - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room", None); let states = two_node_states(); let out = bridge .process_cycle_from_states(&states, 10_000) @@ -299,7 +316,7 @@ mod tests { fn live_path_is_deterministic() { let states = two_node_states_fixed(); let run = || { - let mut b = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut b = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); b.process_cycle_from_states(&states, 5_000).unwrap().unwrap() }; let a = run(); @@ -325,7 +342,7 @@ mod tests { #[test] fn nodes_registered_once_across_cycles() { - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); let states = two_node_states(); bridge.process_cycle_from_states(&states, 1_000); bridge.process_cycle_from_states(&states, 2_000); @@ -336,7 +353,7 @@ mod tests { #[test] fn retention_bounds_world_graph_growth() { - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); bridge.set_semantic_retention(5); let states = two_node_states(); for i in 0..20i64 { @@ -349,7 +366,7 @@ mod tests { #[test] fn adapter_identity_flows_into_live_witness() { let states = two_node_states_fixed(); - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); let base = bridge .process_cycle_from_states(&states, 1_000) .unwrap() @@ -381,7 +398,7 @@ mod tests { /// the status endpoint, with a zero error count on the happy path. #[test] fn observe_cycle_records_trust_state() { - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); assert!(bridge.last_trust_witness().is_none()); assert_eq!(bridge.effective_class(), None); @@ -430,7 +447,7 @@ mod tests { m } - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); let mismatched = mismatched_states(); assert!(bridge.observe_cycle(&mismatched, 1_000).is_none()); @@ -461,7 +478,7 @@ mod tests { /// mapping bfld's privacy gate applies at `Restricted`. #[test] fn restricted_class_suppresses_raw_outputs() { - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); bridge.set_privacy_mode(PrivacyMode::StrictNoIdentity); // base = Restricted bridge .observe_cycle(&two_node_states(), 1_000) @@ -472,7 +489,7 @@ mod tests { #[test] fn identity_strict_mode_is_carried_into_provenance() { - let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None); bridge.set_privacy_mode(PrivacyMode::StrictNoIdentity); let out = bridge .process_cycle_from_states(&two_node_states(), 7_000) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 38a543d3..120eaf5d 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -7514,6 +7514,11 @@ async fn main() { let field_surface: rufield_surface::FieldState = Arc::new(RwLock::new(rufield_surface::FieldSurface::from_env())); + // Populated inside the `multistatic_fuser` field initializer below, then + // 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 = None; + let state: SharedState = Arc::new(RwLock::new(AppStateInner { latest_update: None, rssi_history: VecDeque::new(), @@ -7588,7 +7593,7 @@ async fn main() { ); let mut fuser = MultistaticFuser::with_config(MultistaticConfig { min_nodes: 1, // single-node passthrough - ..cfg + ..cfg.clone() }); if let Some(ref pos_str) = args.node_positions { let positions = field_bridge::parse_node_positions(pos_str); @@ -7600,6 +7605,10 @@ async fn main() { fuser.set_node_positions(positions); } } + engine_bridge_multistatic_cfg = Some(MultistaticConfig { + min_nodes: 1, + ..cfg + }); fuser }, engine_bridge: engine_bridge::EngineBridge::new( @@ -7607,6 +7616,7 @@ async fn main() { 1, "default", "Default Room", + engine_bridge_multistatic_cfg, ), field_model: if args.calibrate { info!("Field model calibration enabled — room should be empty during startup");