Iter 18 (after recovery from a cross-branch slip — see commit-history
context below). Replaces the hardcoded 20 Hz CSI_FPS_HZ constant in
mesh_aligned_us_for_csi_frame with a per-node EMA of observed
inter-frame intervals, falling back to 20 Hz until ≥5 samples land.
Real bench data (§A0.12 captures) showed the actual CSI rate at ~10 fps
because the firmware's CSI_MIN_SEND_INTERVAL_US gate combined with
ruv.net's traffic level paces it to that. Using 20 Hz against actual
10 fps inflates Δus 2× and shifts the recovered mesh timestamp by up
to the inter-sync interval / 2 = ~1 s. Measured fps fixes that.
State on NodeState:
csi_fps_ema: f64 — EMA (seeded at 20.0)
csi_fps_samples: u32 — counts inter-frame deltas observed
API:
NodeState::observe_csi_frame_arrival(now) — call once per CSI frame
from udp_receiver_task
update_csi_fps_ema(prev_fps, dt_sec) -> Option<f64> — free fn,
testable
mesh_aligned_us_for_csi_frame now uses the measured fps when samples ≥ 5,
falls back to 20 Hz otherwise.
4 unit tests (fps_ema_tests module, all passing on the binary):
* steady_10hz_converges_toward_10 — 40 samples at 100 ms converge to ±0.1 Hz
* steady_20hz_stays_near_20 — 20 samples at 50 ms stay within 0.05 Hz
* nonpositive_dt_rejected — dt ≤ 0 returns None
* long_gap_rejected_as_implausible — dt > 1 s rejected (likely a dropout)
Branch-coordination note: this iter's working tree was briefly applied
to feat/adr-115-ha-mqtt-matter by a `git checkout` between iter 17 and
iter 18. Stashed the ADR-115 agent's MQTT/Matter Cargo.toml work
(`stash@adr115-pending-work`) before switching back here. No code lost.
Co-Authored-By: claude-flow <ruv@ruv.net>