mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
feat(adr-110 P10): apply_to_local + NodeState::mesh_aligned_us + full ADR rewrite
Iter 16 closes the math loop and updates ADR-110 to reflect the full
P1-P10 sprint outcome (per user request).
Code (the math layer that converts the iter 15 stored sync into a
per-frame mesh-aligned timestamp):
wifi-densepose-hardware:
SyncPacket::apply_to_local(local_at_frame_us: u64) -> u64
Pure integer math: offset = epoch - local; mesh = local_at_frame + offset.
3 new unit tests (10 total, all green):
- apply_to_local_recovers_packet_epoch (identity at the packet's local_us)
- apply_to_local_preserves_inter_frame_delta (Δlocal == Δmesh)
- apply_to_local_on_leader_is_near_identity (leader offset ≈ 0)
wifi-densepose-sensing-server:
NodeState::mesh_aligned_us(local_at_frame_us: u64) -> Option<u64>
Returns the recovered mesh timestamp using the most-recent sync
packet, or None if no sync seen or last one older than 9 s
(3× firmware VALID_WINDOW_MS = 9 s staleness gate).
cargo check -p wifi-densepose-sensing-server --no-default-features
→ green
ADR-110 substantial rewrite (per user "update adr 110 with details"):
- Status line: P1-P10 complete, firmware-side substrate closed at v0.7.0.
- Front matter now lists all 4 firmware releases + witness link.
- Phase table grows a P10 row capturing the v0.6.8 / v0.6.9 / v0.7.0
arc (EMA smoother + sync packet + bit-4 wire-fix + host crates).
- New §4.1 — /loop 5m SOTA sprint summary table (iters 1-16, 4 releases,
17 commits, 13 unit tests, what shipped each iter).
- New §4.2 — measured numbers table with 99.56% RX, 104.1 µs smoothed
stdev, 3.95x suppression, 1.4 ppm crystal skew, etc — every cell
backed by a witness §A0.x entry and a preserved bench log.
- New §4.3 — host-side production surface listing (sync_packet.rs +
sensing-server NodeState + Python parser, with file paths).
- §5 open question on 802.15.4 channel resolved (Kconfig, default ch26
not ch15, with the witness §D1 rationale).
- New §6 — explicit scope of what's outside this ADR (multistatic fusion
math in ADR-029/030, hardware-gated measurements needing INA / 11ax AP,
IDF upstream fixes pending).
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -121,6 +121,34 @@ impl SyncPacket {
|
||||
(self.local_us as i64) - (self.epoch_us as i64)
|
||||
}
|
||||
|
||||
/// Given a CSI frame's node-local `esp_timer_get_time()` snapshot,
|
||||
/// recover the mesh-aligned timestamp using this sync packet as the
|
||||
/// reference point.
|
||||
///
|
||||
/// Math (all in node-local µs, see ADR-110 §A0.12):
|
||||
///
|
||||
/// ```text
|
||||
/// offset = epoch_us - local_us (signed; this packet)
|
||||
/// mesh_epoch(frame) = local_at_frame_us + offset
|
||||
/// = local_at_frame_us + (epoch_us - local_us)
|
||||
/// ```
|
||||
///
|
||||
/// On the leader this gives `≈ local_at_frame_us`. On a follower this
|
||||
/// gives the mesh-aligned time aligned to the leader's clock within
|
||||
/// the §A0.10 measured 104 µs stdev (the same EMA-smoothed offset
|
||||
/// the firmware applied when it built this sync packet's `epoch_us`).
|
||||
///
|
||||
/// Use this on the host side whenever a CSI frame arrives with
|
||||
/// ADR-018 byte 19 bit 4 set: look up the matching node's most-recent
|
||||
/// `SyncPacket`, call `apply_to_local(frame.local_us)`, stamp the
|
||||
/// result on the frame for downstream multistatic fusion.
|
||||
pub fn apply_to_local(&self, local_at_frame_us: u64) -> u64 {
|
||||
// Compute the offset as a signed delta in the µs domain. Adding it
|
||||
// back to the frame's local snapshot recovers the mesh epoch.
|
||||
let offset = (self.epoch_us as i64).wrapping_sub(self.local_us as i64);
|
||||
(local_at_frame_us as i64).wrapping_add(offset) as u64
|
||||
}
|
||||
|
||||
/// Serialize back to wire bytes (32 bytes, little-endian).
|
||||
pub fn to_bytes(&self) -> [u8; SYNC_PACKET_SIZE] {
|
||||
let mut out = [0u8; SYNC_PACKET_SIZE];
|
||||
@@ -234,6 +262,53 @@ mod tests {
|
||||
assert_ne!(SYNC_PACKET_MAGIC, crate::esp32_parser::ESP32_CSI_MAGIC);
|
||||
}
|
||||
|
||||
/// Applying a sync packet to its own local_us must recover its own
|
||||
/// epoch_us. Foundational identity for the math.
|
||||
#[test]
|
||||
fn apply_to_local_recovers_packet_epoch() {
|
||||
let pkt = SyncPacket {
|
||||
node_id: 9, proto_ver: 1,
|
||||
flags: SyncPacketFlags { is_leader: false, is_valid: true, smoothed_used: true },
|
||||
local_us: 28_798_450, epoch_us: 27_634_885, sequence: 20,
|
||||
};
|
||||
assert_eq!(pkt.apply_to_local(pkt.local_us), pkt.epoch_us);
|
||||
}
|
||||
|
||||
/// A CSI frame's local timestamp arriving after the sync packet
|
||||
/// gets the same offset applied — the µs delta between sync and frame
|
||||
/// is preserved on both clocks.
|
||||
#[test]
|
||||
fn apply_to_local_preserves_inter_frame_delta() {
|
||||
let pkt = SyncPacket {
|
||||
node_id: 9, proto_ver: 1,
|
||||
flags: SyncPacketFlags { is_leader: false, is_valid: true, smoothed_used: true },
|
||||
local_us: 28_798_450, epoch_us: 27_634_885, sequence: 20,
|
||||
};
|
||||
// Frame arrives 100 ms after the sync packet on the follower's local clock.
|
||||
let local_at_frame = pkt.local_us + 100_000;
|
||||
let mesh_epoch = pkt.apply_to_local(local_at_frame);
|
||||
// Mesh epoch should also be 100 ms after the sync packet's epoch.
|
||||
assert_eq!(mesh_epoch, pkt.epoch_us + 100_000);
|
||||
// Offset must equal local - epoch on both clocks.
|
||||
assert_eq!(local_at_frame - mesh_epoch, pkt.local_us - pkt.epoch_us);
|
||||
}
|
||||
|
||||
/// Leader sync packet has near-zero offset, so apply_to_local is
|
||||
/// approximately identity (modulo the few µs call-stack delta).
|
||||
#[test]
|
||||
fn apply_to_local_on_leader_is_near_identity() {
|
||||
let pkt = SyncPacket {
|
||||
node_id: 12, proto_ver: 1,
|
||||
flags: SyncPacketFlags { is_leader: true, is_valid: true, smoothed_used: false },
|
||||
local_us: 28_864_932, epoch_us: 28_864_939, sequence: 20,
|
||||
};
|
||||
let frame_local = 30_000_000u64;
|
||||
let mesh = pkt.apply_to_local(frame_local);
|
||||
assert!((mesh as i64 - frame_local as i64).abs() <= 100,
|
||||
"leader apply should be within 100 µs of identity, got {} delta",
|
||||
mesh as i64 - frame_local as i64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_size_constant_is_correct() {
|
||||
let pkt = SyncPacket {
|
||||
|
||||
@@ -419,6 +419,23 @@ const NOVELTY_HISTORY_CAPACITY: usize = 64;
|
||||
const NOVELTY_SKETCH_VERSION: u16 = 1;
|
||||
|
||||
impl NodeState {
|
||||
/// ADR-110 §A0.12 timestamp recovery: given a CSI frame's node-local
|
||||
/// `esp_timer_get_time()` snapshot, return the mesh-aligned epoch
|
||||
/// computed from this node's most recent sync packet — or `None`
|
||||
/// if no sync has been received yet, or the last one is too stale
|
||||
/// (older than 3 × VALID_WINDOW_MS = 9 s, matching the firmware's own
|
||||
/// staleness gate).
|
||||
pub(crate) fn mesh_aligned_us(&self, local_at_frame_us: u64) -> Option<u64> {
|
||||
let sync = self.latest_sync.as_ref()?;
|
||||
let seen_at = self.latest_sync_at?;
|
||||
// Drop stale syncs — firmware emits at ~0.5 Hz default, anything
|
||||
// older than 9 s likely means the mesh transport dropped.
|
||||
if seen_at.elapsed() > std::time::Duration::from_secs(9) {
|
||||
return None;
|
||||
}
|
||||
Some(sync.apply_to_local(local_at_frame_us))
|
||||
}
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
frame_history: VecDeque::new(),
|
||||
|
||||
Reference in New Issue
Block a user