From 23fd8ac3715af42a7f20ceb7693b69e4560ee4c4 Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 23 May 2026 13:11:35 -0400 Subject: [PATCH] =?UTF-8?q?feat(sensing-server):=20consume=20ADR-110=20?= =?UTF-8?q?=C2=A7A0.12=20sync=20packets,=20store=20per-node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iter 15 — converts the iter 14 SyncPacket decoder from "shipped" to "consumed" by wiring it into the sensing-server UDP receive loop. Wiring: - Cargo.toml gains wifi-densepose-hardware = path = "../wifi-densepose-hardware" to pull in the SyncPacket decoder + SYNC_PACKET_MAGIC dispatch constant. - NodeState gains two new fields: latest_sync: Option — the parsed packet latest_sync_at: Option — staleness clock - udp_receiver_task now magic-dispatches every incoming datagram against SYNC_PACKET_MAGIC (0xC511A110) before falling through to the existing ADR-039 vitals / ADR-040 WASM / ADR-018 CSI parsers. Same Option-returning pattern as the other parsers, so future packet types slot in cleanly. When a sync packet arrives: * write-lock state, lookup-or-create NodeState by node_id * stash the SyncPacket + Instant::now() on the node * debug-log node, leader/valid/smoothed flags, sequence, offset_us * continue (don't fall through — we know it's not a CSI frame) Downstream multistatic CSI fusion now has a documented landing pad: any CSI frame with byte 19 bit 4 set looks up the matching NodeState, applies ns.latest_sync.epoch_us + (now_local - ns.latest_sync.local_us) to get a mesh-aligned timestamp. Implementation of that fusion math is the next ADR-029/030 layer (wifi-densepose-signal). Verification: - cargo check -p wifi-densepose-sensing-server --no-default-features → green - cargo test -p wifi-densepose-hardware sync_packet → 7/7 pass, 122 filtered - Zero behavioral change for nodes that don't emit sync packets — the dispatch only fires on magic match. Co-Authored-By: claude-flow --- .../wifi-densepose-sensing-server/Cargo.toml | 3 ++ .../wifi-densepose-sensing-server/src/main.rs | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/v2/crates/wifi-densepose-sensing-server/Cargo.toml b/v2/crates/wifi-densepose-sensing-server/Cargo.toml index 0f21baf9..46483456 100644 --- a/v2/crates/wifi-densepose-sensing-server/Cargo.toml +++ b/v2/crates/wifi-densepose-sensing-server/Cargo.toml @@ -50,6 +50,9 @@ wifi-densepose-wifiscan = { version = "0.3.0", path = "../wifi-densepose-wifisca # build without vcpkg/openblas (issue #366, #415). wifi-densepose-signal = { version = "0.3.0", path = "../wifi-densepose-signal", default-features = false } +# Hardware crate — SyncPacket decoder for ADR-110 §A0.12 mesh-aligned timestamps. +wifi-densepose-hardware = { version = "0.3.0", path = "../wifi-densepose-hardware" } + # midstream — real-time introspection / low-latency tap (ADR-099 D1). # Two crates only, on purpose: scheduler / neural-solver / strange-loop are # explicitly out of scope of ADR-099 (D5). diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 681ef6fb..7c03e212 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -371,6 +371,13 @@ struct NodeState { latest_vitals: VitalSigns, pub(crate) last_frame_time: Option, edge_vitals: Option, + /// ADR-110 §A0.12: Latest sync packet received from this node. When a + /// CSI frame arrives with byte 19 bit 4 set (`adr018_flags.ieee802154_sync_valid`), + /// the host can recover a mesh-aligned timestamp via + /// `latest_sync.epoch_us + (now_local - latest_sync.local_us)`. + latest_sync: Option, + /// Last time a sync packet from this node was received (for staleness). + latest_sync_at: Option, /// Latest extracted features for cross-node fusion. latest_features: Option, // ── RuVector Phase 2: Temporal smoothing & coherence gating ── @@ -434,6 +441,8 @@ impl NodeState { latest_vitals: VitalSigns::default(), last_frame_time: None, edge_vitals: None, + latest_sync: None, + latest_sync_at: None, latest_features: None, prev_keypoints: None, motion_energy_history: VecDeque::with_capacity(COHERENCE_WINDOW), @@ -4146,6 +4155,37 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) { continue; } + // ADR-110 §A0.12: Try sync packet (magic 0xC511_A110). + // A 32-byte UDP datagram carrying mesh-aligned epoch + sequence + // high-water from the node's c6_sync_espnow EMA-smoothed offset. + // Stored per-node so subsequent CSI frames with byte 19 bit 4 + // set can have an aligned timestamp recovered downstream. + if len >= wifi_densepose_hardware::SYNC_PACKET_SIZE { + let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); + if magic == wifi_densepose_hardware::SYNC_PACKET_MAGIC { + match wifi_densepose_hardware::SyncPacket::from_bytes(&buf[..len]) { + Ok(sync) => { + debug!("ESP32 sync from {src}: node={} leader={} valid={} smoothed={} \ + seq={} offset_us={}", + sync.node_id, sync.flags.is_leader, sync.flags.is_valid, + sync.flags.smoothed_used, sync.sequence, + sync.local_minus_epoch_us()); + let mut s = state.write().await; + let ns = s.node_states.entry(sync.node_id) + .or_insert_with(NodeState::new); + ns.latest_sync = Some(sync); + ns.latest_sync_at = Some(std::time::Instant::now()); + continue; + } + Err(e) => { + debug!("Sync packet decode error from {src}: {e}"); + // Fall through — magic matched but decode failed; not a CSI frame. + continue; + } + } + } + } + // ADR-040: Try WASM output packet (magic 0xC511_0004). if let Some(wasm_output) = parse_wasm_output(&buf[..len]) { debug!("WASM output from {src}: node={} module={} events={}",