use rtime_core::timestamp::{NtpDuration, PtpTimestamp};
fn ptp_diff(a: PtpTimestamp, b: PtpTimestamp) -> NtpDuration {
let a_nanos = a.seconds as i128 * 1_000_000_000 + a.nanoseconds as i128;
let b_nanos = b.seconds as i128 * 1_000_000_000 + b.nanoseconds as i128;
let diff_nanos = a_nanos - b_nanos;
NtpDuration::from_nanos(diff_nanos.clamp(i64::MIN as i128, i64::MAX as i128) as i64)
}
pub fn compute_e2e(
t1: PtpTimestamp,
t2: PtpTimestamp,
t3: PtpTimestamp,
t4: PtpTimestamp,
) -> (NtpDuration, NtpDuration) {
let forward = ptp_diff(t2, t1); let reverse = ptp_diff(t4, t3);
let offset = (forward - reverse) / 2;
let delay = (forward + reverse) / 2;
(offset, delay)
}
#[derive(Debug, Default)]
pub struct E2eDelayState {
pub t1: Option<PtpTimestamp>,
pub t2: Option<PtpTimestamp>,
pub t3: Option<PtpTimestamp>,
pub t4: Option<PtpTimestamp>,
}
impl E2eDelayState {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.t1 = None;
self.t2 = None;
self.t3 = None;
self.t4 = None;
}
pub fn set_sync_departure(&mut self, t1: PtpTimestamp) {
self.t1 = Some(t1);
}
pub fn set_sync_arrival(&mut self, t2: PtpTimestamp) {
self.t2 = Some(t2);
}
pub fn set_delay_req_departure(&mut self, t3: PtpTimestamp) {
self.t3 = Some(t3);
}
pub fn set_delay_resp_arrival(&mut self, t4: PtpTimestamp) {
self.t4 = Some(t4);
}
pub fn compute(&self) -> Option<(NtpDuration, NtpDuration)> {
match (self.t1, self.t2, self.t3, self.t4) {
(Some(t1), Some(t2), Some(t3), Some(t4)) => Some(compute_e2e(t1, t2, t3, t4)),
_ => None,
}
}
pub fn is_complete(&self) -> bool {
self.t1.is_some() && self.t2.is_some() && self.t3.is_some() && self.t4.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symmetric_delay_zero_offset() {
let t1 = PtpTimestamp::new(100, 0);
let t2 = PtpTimestamp::new(100, 1_000_000); let t3 = PtpTimestamp::new(100, 2_000_000); let t4 = PtpTimestamp::new(100, 3_000_000);
let (offset, delay) = compute_e2e(t1, t2, t3, t4);
assert!(
offset.to_nanos().abs() < 10,
"expected ~0 offset, got {} ns",
offset.to_nanos()
);
assert!(
(delay.to_nanos() - 1_000_000).abs() < 10,
"expected ~1ms delay, got {} ns",
delay.to_nanos()
);
}
#[test]
fn symmetric_delay_with_offset() {
let t1 = PtpTimestamp::new(100, 0);
let t2 = PtpTimestamp::new(100, 1_500_000); let t3 = PtpTimestamp::new(100, 2_500_000); let t4_fixed = PtpTimestamp::new(100, 3_000_000);
let (offset, delay) = compute_e2e(t1, t2, t3, t4_fixed);
assert!(
(offset.to_nanos() - 500_000).abs() < 10,
"expected ~500us offset, got {} ns",
offset.to_nanos()
);
assert!(
(delay.to_nanos() - 1_000_000).abs() < 10,
"expected ~1ms delay, got {} ns",
delay.to_nanos()
);
}
#[test]
fn negative_offset() {
let t1 = PtpTimestamp::new(100, 0);
let t2 = PtpTimestamp::new(100, 300_000);
let t3 = PtpTimestamp::new(100, 1_300_000);
let t4 = PtpTimestamp::new(100, 2_000_000);
let (offset, delay) = compute_e2e(t1, t2, t3, t4);
assert!(
(offset.to_nanos() - (-200_000)).abs() < 10,
"expected ~-200us offset, got {} ns",
offset.to_nanos()
);
assert!(
(delay.to_nanos() - 500_000).abs() < 10,
"expected ~500us delay, got {} ns",
delay.to_nanos()
);
}
#[test]
fn e2e_state_tracker() {
let mut state = E2eDelayState::new();
assert!(!state.is_complete());
assert!(state.compute().is_none());
state.set_sync_departure(PtpTimestamp::new(100, 0));
state.set_sync_arrival(PtpTimestamp::new(100, 1_000_000));
state.set_delay_req_departure(PtpTimestamp::new(100, 2_000_000));
assert!(!state.is_complete());
assert!(state.compute().is_none());
state.set_delay_resp_arrival(PtpTimestamp::new(100, 3_000_000));
assert!(state.is_complete());
let (offset, delay) = state.compute().unwrap();
assert!(offset.to_nanos().abs() < 10);
assert!((delay.to_nanos() - 1_000_000).abs() < 10);
}
#[test]
fn e2e_state_reset() {
let mut state = E2eDelayState::new();
state.set_sync_departure(PtpTimestamp::new(100, 0));
state.set_sync_arrival(PtpTimestamp::new(100, 1_000_000));
state.reset();
assert!(!state.is_complete());
assert!(state.t1.is_none());
assert!(state.t2.is_none());
}
}