use serde::{Deserialize, Serialize};
use crate::error::ParseError;
pub const SYNC_PACKET_MAGIC: u32 = 0xC511_A110;
pub const SYNC_PACKET_SIZE: usize = 32;
pub const SYNC_PACKET_PROTO_VER: u8 = 0x01;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncPacket {
pub node_id: u8,
pub proto_ver: u8,
pub flags: SyncPacketFlags,
pub local_us: u64,
pub epoch_us: u64,
pub sequence: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SyncPacketFlags {
pub is_leader: bool,
pub is_valid: bool,
pub smoothed_used: bool,
}
impl SyncPacketFlags {
pub fn from_byte(b: u8) -> Self {
Self {
is_leader: (b & 0x01) != 0,
is_valid: (b & 0x02) != 0,
smoothed_used: (b & 0x04) != 0,
}
}
pub fn to_byte(self) -> u8 {
let mut b = 0u8;
if self.is_leader { b |= 0x01; }
if self.is_valid { b |= 0x02; }
if self.smoothed_used { b |= 0x04; }
b
}
}
impl SyncPacket {
pub fn from_bytes(buf: &[u8]) -> Result<Self, ParseError> {
if buf.len() < SYNC_PACKET_SIZE {
return Err(ParseError::InsufficientData {
needed: SYNC_PACKET_SIZE,
got: buf.len(),
});
}
let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
if magic != SYNC_PACKET_MAGIC {
return Err(ParseError::InvalidMagic { expected: SYNC_PACKET_MAGIC, got: magic });
}
let node_id = buf[4];
let proto_ver = buf[5];
let flags = SyncPacketFlags::from_byte(buf[6]);
let local_us = u64::from_le_bytes(buf[8..16].try_into().unwrap());
let epoch_us = u64::from_le_bytes(buf[16..24].try_into().unwrap());
let sequence = u32::from_le_bytes(buf[24..28].try_into().unwrap());
Ok(Self {
node_id,
proto_ver,
flags,
local_us,
epoch_us,
sequence,
})
}
pub fn local_minus_epoch_us(&self) -> i64 {
(self.local_us as i64) - (self.epoch_us as i64)
}
pub fn apply_to_local(&self, local_at_frame_us: u64) -> u64 {
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
}
pub fn mesh_aligned_us_for_sequence(&self, frame_seq: u32, fps_hz: f64) -> u64 {
debug_assert!(fps_hz > 0.0, "fps_hz must be positive");
let dframes = (frame_seq.wrapping_sub(self.sequence)) as i64;
let dus = (dframes as f64 * 1_000_000.0 / fps_hz) as i64;
let local_at = (self.local_us as i64).wrapping_add(dus) as u64;
self.apply_to_local(local_at)
}
pub fn to_bytes(&self) -> [u8; SYNC_PACKET_SIZE] {
let mut out = [0u8; SYNC_PACKET_SIZE];
out[0..4].copy_from_slice(&SYNC_PACKET_MAGIC.to_le_bytes());
out[4] = self.node_id;
out[5] = self.proto_ver;
out[6] = self.flags.to_byte();
out[8..16].copy_from_slice(&self.local_us.to_le_bytes());
out[16..24].copy_from_slice(&self.epoch_us.to_le_bytes());
out[24..28].copy_from_slice(&self.sequence.to_le_bytes());
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn follower_typical_packet_roundtrips() {
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,
};
let wire = pkt.to_bytes();
let decoded = SyncPacket::from_bytes(&wire).unwrap();
assert_eq!(decoded, pkt);
assert_eq!(decoded.local_minus_epoch_us(), 1_163_565);
assert_eq!(decoded.flags.to_byte(), 0x06);
}
#[test]
fn leader_packet_has_local_close_to_epoch() {
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 wire = pkt.to_bytes();
let decoded = SyncPacket::from_bytes(&wire).unwrap();
assert_eq!(decoded.flags.to_byte(), 0x03);
assert_eq!(decoded.local_minus_epoch_us(), -7); assert!(decoded.flags.is_leader);
assert!(decoded.flags.is_valid);
assert!(!decoded.flags.smoothed_used);
}
#[test]
fn magic_mismatch_is_typed_error() {
let mut wire = SyncPacket {
node_id: 1, proto_ver: 1, flags: SyncPacketFlags::default(),
local_us: 0, epoch_us: 0, sequence: 0,
}.to_bytes();
wire[0] = 0x01; let err = SyncPacket::from_bytes(&wire).unwrap_err();
match err {
ParseError::InvalidMagic { got, .. } => assert_ne!(got, SYNC_PACKET_MAGIC),
other => panic!("expected InvalidMagic, got {other:?}"),
}
}
#[test]
fn short_packet_is_typed_error() {
let wire = [0u8; 16]; let err = SyncPacket::from_bytes(&wire).unwrap_err();
match err {
ParseError::InsufficientData { needed, got } => {
assert_eq!(needed, SYNC_PACKET_SIZE);
assert_eq!(got, 16);
}
other => panic!("expected InsufficientData, got {other:?}"),
}
}
#[test]
fn all_flag_combinations_roundtrip() {
for &is_leader in &[false, true] {
for &is_valid in &[false, true] {
for &smoothed_used in &[false, true] {
let flags = SyncPacketFlags { is_leader, is_valid, smoothed_used };
let pkt = SyncPacket {
node_id: 1, proto_ver: 1, flags,
local_us: 1234, epoch_us: 5678, sequence: 99,
};
let wire = pkt.to_bytes();
let decoded = SyncPacket::from_bytes(&wire).unwrap();
assert_eq!(decoded.flags, flags);
assert_eq!(decoded.flags.to_byte(), flags.to_byte());
}
}
}
}
#[test]
fn sync_and_csi_magics_differ() {
assert_ne!(SYNC_PACKET_MAGIC, crate::esp32_parser::ESP32_CSI_MAGIC);
}
#[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);
}
#[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,
};
let local_at_frame = pkt.local_us + 100_000;
let mesh_epoch = pkt.apply_to_local(local_at_frame);
assert_eq!(mesh_epoch, pkt.epoch_us + 100_000);
assert_eq!(local_at_frame - mesh_epoch, pkt.local_us - pkt.epoch_us);
}
#[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 mesh_aligned_for_sequence_identity_at_sync_point() {
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.mesh_aligned_us_for_sequence(20, 20.0), pkt.epoch_us);
}
#[test]
fn mesh_aligned_for_sequence_extrapolates_forward() {
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,
};
let mesh = pkt.mesh_aligned_us_for_sequence(40, 20.0);
assert_eq!(mesh, pkt.epoch_us + 1_000_000);
}
#[test]
fn mesh_aligned_for_sequence_handles_seq_wraparound() {
let pkt = SyncPacket {
node_id: 9, proto_ver: 1,
flags: SyncPacketFlags { is_leader: false, is_valid: true, smoothed_used: true },
local_us: 10_000, epoch_us: 10_000, sequence: u32::MAX,
};
let mesh = pkt.mesh_aligned_us_for_sequence(0, 20.0);
assert_eq!(mesh, pkt.epoch_us + 50_000); }
#[test]
fn end_to_end_sync_decode_then_frame_mesh_recovery() {
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,
};
let wire = pkt.to_bytes();
assert_eq!(wire.len(), SYNC_PACKET_SIZE);
let decoded = SyncPacket::from_bytes(&wire).unwrap();
assert_eq!(decoded, pkt);
let frame_seq = pkt.sequence + 100;
let mesh_us = decoded.mesh_aligned_us_for_sequence(frame_seq, 20.0);
assert_eq!(mesh_us, pkt.epoch_us + 5_000_000);
let local_at_frame = pkt.local_us + 5_000_000;
assert_eq!(decoded.apply_to_local(local_at_frame), mesh_us);
}
#[test]
fn wire_size_constant_is_correct() {
let pkt = SyncPacket {
node_id: 0, proto_ver: 1, flags: SyncPacketFlags::default(),
local_us: 0, epoch_us: 0, sequence: 0,
};
assert_eq!(pkt.to_bytes().len(), SYNC_PACKET_SIZE);
assert_eq!(SYNC_PACKET_SIZE, 32);
}
#[test]
fn canonical_wire_bytes_match_python_decoder() {
let canonical: [u8; 32] = [
0x10, 0xa1, 0x11, 0xc5, 0x09, 0x01, 0x06, 0x00, 0xf2, 0x6d, 0xb7, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc5, 0xac, 0xa5, 0x01, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let decoded = SyncPacket::from_bytes(&canonical).unwrap();
assert_eq!(decoded.node_id, 9);
assert_eq!(decoded.proto_ver, 1);
assert_eq!(decoded.flags.to_byte(), 0x06);
assert!(!decoded.flags.is_leader);
assert!(decoded.flags.is_valid);
assert!(decoded.flags.smoothed_used);
assert_eq!(decoded.local_us, 28_798_450);
assert_eq!(decoded.epoch_us, 27_634_885);
assert_eq!(decoded.sequence, 20);
assert_eq!(decoded.local_minus_epoch_us(), 1_163_565);
let re_encoded = decoded.to_bytes();
assert_eq!(re_encoded, canonical,
"Rust to_bytes drifted from the canonical pin — Python decoder will break");
}
}