Iter 17 — closes the per-frame mesh-time loop for ADR-018 CSI frames
that carry no per-frame local_us field (the v1 wire format reserves no
slot — see WITNESS-LOG-110 §A0.11).
Math: pair the frame's sequence number against the sync packet's
sequence high-water + an assumed CSI frame rate. Δframes × 1/fps
estimates the node-local delta from the sync, then apply_to_local
recovers the mesh epoch.
SyncPacket::mesh_aligned_us_for_sequence(frame_seq: u32, fps_hz: f64) -> u64
3 new unit tests (13 total in sync_packet::tests, all green):
* mesh_aligned_for_sequence_identity_at_sync_point — at sync.sequence
returns sync.epoch_us exactly
* mesh_aligned_for_sequence_extrapolates_forward — 20 frames @ 20 fps
extrapolates by exactly 1 s
* mesh_aligned_for_sequence_handles_seq_wraparound — u32 sequence
wrap doesn't jump backward by 2^32 (wrapping_sub guards it)
NodeState hook:
NodeState::mesh_aligned_us_for_csi_frame(frame_sequence: u32) -> Option<u64>
Wraps the SyncPacket method, defaults fps_hz=20.0 (matches the
firmware's CSI_MIN_SEND_INTERVAL_US-implied ceiling), enforces the
same 9 s staleness gate as mesh_aligned_us.
cargo check -p wifi-densepose-sensing-server --no-default-features → green.
cargo test -p wifi-densepose-hardware sync_packet → 13/13, 122 filtered.
Downstream ADR-029/030 multistatic fusion code can now do:
if frame.adr018_flags.ieee802154_sync_valid {
if let Some(mesh_us) = ns.mesh_aligned_us_for_csi_frame(frame.sequence) {
// pair this frame with frames from sibling nodes by mesh_us
}
}
Co-Authored-By: claude-flow <ruv@ruv.net>