mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
feat(adr-110): surface NodeSyncSnapshot in WebSocket sensing_update JSON
Iter 23 — converts the iter 1-21 firmware-side mesh substrate from
"works internally" to "visible to UI clients". WebSocket sensing_update
broadcasts now carry a per-node optional `sync` object exposing the
mesh state the iter 15-22 wire and storage capture:
{
"type": "sensing_update",
...
"nodes": [
{
"node_id": 9,
...
"sync": {
"offset_us": 1163565, // §A0.10's measured 1.16 s
"is_leader": false,
"is_valid": true,
"smoothed": true, // EMA seeded
"sequence": 20, // §A0.12 pairing key
"csi_fps_ema": 10.0, // iter 18 measured rate
"csi_fps_samples": 47 // ≥5 means trust csi_fps_ema
}
}
],
...
}
`sync` is `Option<NodeSyncSnapshot>` with `#[serde(skip_serializing_if =
"Option::is_none")]` so non-mesh paths (multi-BSSID scan / synthetic RSSI
/ simulation) emit no `sync` key — preserves backwards compatibility
with existing UI clients.
Plumbed into all four NodeInfo construction sites:
1. multi-BSSID scan path → sync: None
2. synthetic-RSSI fallback → sync: None
3. simulated frame path → sync: None
4. real ESP32 CSI path (line 4528) → sync: snapshot from NodeState
5. ADR-039 vitals-only path (line 4207) → sync: snapshot from NodeState
cargo check -p wifi-densepose-sensing-server --no-default-features → green.
UI clients (viz.html, future Tauri desktop, downstream automation) can
now render leader/follower badges, jitter histograms, and the §A0.10
clock-skew trajectory without any further firmware or aggregator work.
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -293,6 +293,38 @@ struct NodeInfo {
|
||||
position: [f64; 3],
|
||||
amplitude: Vec<f64>,
|
||||
subcarrier_count: usize,
|
||||
/// ADR-110 iter 23 — cross-board sync snapshot for this node.
|
||||
/// `None` when no fresh sync packet has been observed (no mesh peer
|
||||
/// reachable, or this node is a singleton). Populated from
|
||||
/// `NodeState::latest_sync` and the iter 18 fps EMA.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sync: Option<NodeSyncSnapshot>,
|
||||
}
|
||||
|
||||
/// ADR-110 iter 23 — per-node mesh-sync snapshot embedded in NodeInfo.
|
||||
/// Surfaces what was previously only visible in the debug log so UI clients
|
||||
/// can render leader / follower / offset / measured-fps live.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct NodeSyncSnapshot {
|
||||
/// Smoothed local-vs-mesh offset in µs (negative when this node's clock
|
||||
/// is behind the leader's — see §A0.10's measured -1.16 s on the bench).
|
||||
offset_us: i64,
|
||||
/// True when this node is the elected mesh leader.
|
||||
is_leader: bool,
|
||||
/// True when this node has heard a fresh leader beacon within the
|
||||
/// firmware's VALID_WINDOW_MS gate (3 s).
|
||||
is_valid: bool,
|
||||
/// True once the EMA-smoothed offset has seeded (one full beacon round-trip).
|
||||
smoothed: bool,
|
||||
/// Sync packet's sequence high-water — used by the host to pair CSI
|
||||
/// frames against this snapshot for §A0.12 mesh-time recovery.
|
||||
sequence: u32,
|
||||
/// Per-node measured CSI frame rate (iter 18 EMA). 20.0 until the
|
||||
/// EMA has at least 5 samples; the actually-observed rate after that.
|
||||
csi_fps_ema: f64,
|
||||
/// How many CSI frames have contributed to `csi_fps_ema`. Clients can
|
||||
/// treat <5 as "not yet trustworthy" and fall back to 20 Hz.
|
||||
csi_fps_samples: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -2034,6 +2066,7 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) {
|
||||
position: [0.0, 0.0, 0.0],
|
||||
amplitude: multi_ap_frame.amplitudes,
|
||||
subcarrier_count: obs_count,
|
||||
sync: None, // multi-BSSID scan path — no mesh peer
|
||||
}],
|
||||
features,
|
||||
classification,
|
||||
@@ -2178,6 +2211,7 @@ async fn windows_wifi_fallback_tick(state: &SharedState, seq: u32) {
|
||||
position: [0.0, 0.0, 0.0],
|
||||
amplitude: vec![signal_pct],
|
||||
subcarrier_count: 1,
|
||||
sync: None, // synthetic-RSSI fallback path — no mesh peer
|
||||
}],
|
||||
features,
|
||||
classification,
|
||||
@@ -4178,6 +4212,17 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
position: [2.0, 0.0, 1.5],
|
||||
amplitude: vec![],
|
||||
subcarrier_count: 0,
|
||||
// Vitals-only path; still expose the sync snapshot
|
||||
// if the node also speaks ESP-NOW.
|
||||
sync: n.latest_sync.as_ref().map(|s| NodeSyncSnapshot {
|
||||
offset_us: s.local_minus_epoch_us(),
|
||||
is_leader: s.flags.is_leader,
|
||||
is_valid: s.flags.is_valid,
|
||||
smoothed: s.flags.smoothed_used,
|
||||
sequence: s.sequence,
|
||||
csi_fps_ema: n.csi_fps_ema,
|
||||
csi_fps_samples: n.csi_fps_samples,
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -4501,6 +4546,16 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
.map(|a| a.iter().take(56).cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
subcarrier_count: n.frame_history.back().map_or(0, |a| a.len()),
|
||||
// ADR-110 iter 23: snapshot the latest mesh sync.
|
||||
sync: n.latest_sync.as_ref().map(|s| NodeSyncSnapshot {
|
||||
offset_us: s.local_minus_epoch_us(),
|
||||
is_leader: s.flags.is_leader,
|
||||
is_valid: s.flags.is_valid,
|
||||
smoothed: s.flags.smoothed_used,
|
||||
sequence: s.sequence,
|
||||
csi_fps_ema: n.csi_fps_ema,
|
||||
csi_fps_samples: n.csi_fps_samples,
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -4646,6 +4701,7 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
|
||||
position: [2.0, 0.0, 1.5],
|
||||
amplitude: frame_amplitudes,
|
||||
subcarrier_count: frame_n_sub as usize,
|
||||
sync: None, // simulated frame path — no mesh peer
|
||||
}],
|
||||
features: features.clone(),
|
||||
classification,
|
||||
|
||||
Reference in New Issue
Block a user