use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use super::core::ClockOffset;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BerkeleyConfig {
pub timeout_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum ClockSyncProtocol {
#[default]
NTP,
PTP,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CristianConfig {
pub server_address: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CustomProtocolConfig {
pub protocol_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NtpConfig {
pub server: String,
}
#[derive(Debug, Clone, Default)]
pub struct NtpSynchronizer {
pub config: NtpConfig,
}
impl NtpSynchronizer {
pub fn estimate_offset<P: NtpPeer>(&self, peer: &P) -> Result<ClockOffset, ProtocolError> {
let originate = SystemTime::now();
let (peer_receive, peer_transmit) = peer.respond();
let destination = SystemTime::now();
NtpTimestamps {
originate,
peer_receive,
peer_transmit,
destination,
}
.offset()
}
}
pub trait NtpPeer {
fn respond(&self) -> (SystemTime, SystemTime);
}
#[derive(Debug, Clone, Copy)]
pub struct NtpTimestamps {
pub originate: SystemTime,
pub peer_receive: SystemTime,
pub peer_transmit: SystemTime,
pub destination: SystemTime,
}
impl NtpTimestamps {
fn signed_nanos_between(later: SystemTime, earlier: SystemTime) -> i128 {
match later.duration_since(earlier) {
Ok(d) => d.as_nanos() as i128,
Err(e) => -(e.duration().as_nanos() as i128),
}
}
pub fn round_trip_delay(&self) -> Result<Duration, ProtocolError> {
let total = Self::signed_nanos_between(self.destination, self.originate);
let peer_turnaround = Self::signed_nanos_between(self.peer_transmit, self.peer_receive);
let delay_ns = total - peer_turnaround;
if delay_ns < 0 {
return Err(ProtocolError::new(format!(
"inconsistent NTP exchange: negative round-trip delay ({delay_ns}ns); \
timestamps cannot be trusted for offset estimation"
)));
}
Ok(Duration::from_nanos(delay_ns as u64))
}
pub fn offset(&self) -> Result<ClockOffset, ProtocolError> {
self.round_trip_delay()?;
let a = Self::signed_nanos_between(self.peer_receive, self.originate);
let b = Self::signed_nanos_between(self.peer_transmit, self.destination);
let offset_ns = (a + b) / 2;
Ok(ClockOffset {
offset_ns: offset_ns.clamp(i64::MIN as i128, i64::MAX as i128) as i64,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ProtocolError {
pub reason: String,
}
impl ProtocolError {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
impl std::fmt::Display for ProtocolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.reason.is_empty() {
write!(f, "Protocol error")
} else {
write!(f, "Protocol error: {}", self.reason)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FixedOffsetPeer {
shift_ns: i64,
}
impl FixedOffsetPeer {
fn shifted_now(&self) -> SystemTime {
let now = SystemTime::now();
if self.shift_ns >= 0 {
now + Duration::from_nanos(self.shift_ns as u64)
} else {
now - Duration::from_nanos((-self.shift_ns) as u64)
}
}
}
impl NtpPeer for FixedOffsetPeer {
fn respond(&self) -> (SystemTime, SystemTime) {
let receive = self.shifted_now();
std::thread::sleep(Duration::from_millis(5));
let transmit = self.shifted_now();
(receive, transmit)
}
}
#[test]
fn estimate_offset_recovers_a_known_positive_peer_shift() {
let synchronizer = NtpSynchronizer::default();
let peer = FixedOffsetPeer {
shift_ns: 200_000_000, };
let offset = synchronizer
.estimate_offset(&peer)
.expect("a physically consistent exchange must not error");
assert!(
offset.offset_ns > 0,
"a peer-ahead shift must yield a positive offset, got {}",
offset.offset_ns
);
let error_ns = (offset.offset_ns - 200_000_000).abs();
assert!(
error_ns < 50_000_000,
"expected offset near +200ms, got {}ns (error {}ns)",
offset.offset_ns,
error_ns
);
}
#[test]
fn estimate_offset_recovers_a_known_negative_peer_shift() {
let synchronizer = NtpSynchronizer::default();
let peer = FixedOffsetPeer {
shift_ns: -150_000_000, };
let offset = synchronizer
.estimate_offset(&peer)
.expect("a physically consistent exchange must not error");
assert!(
offset.offset_ns < 0,
"a peer-behind shift must yield a negative offset, got {}",
offset.offset_ns
);
let error_ns = (offset.offset_ns - (-150_000_000)).abs();
assert!(
error_ns < 50_000_000,
"expected offset near -150ms, got {}ns (error {}ns)",
offset.offset_ns,
error_ns
);
}
#[test]
fn zero_shift_zero_latency_offset_is_exactly_zero() {
let t = SystemTime::now();
let timestamps = NtpTimestamps {
originate: t,
peer_receive: t,
peer_transmit: t,
destination: t,
};
let offset = timestamps
.offset()
.expect("a degenerate exchange with all four timestamps equal is trivially consistent");
assert_eq!(offset.offset_ns, 0);
assert_eq!(
timestamps
.round_trip_delay()
.expect("degenerate exchange is consistent"),
Duration::ZERO
);
}
#[test]
fn round_trip_delay_rejects_physically_inconsistent_timestamps() {
let now = SystemTime::now();
let timestamps = NtpTimestamps {
originate: now,
destination: now + Duration::from_millis(20),
peer_receive: now + Duration::from_millis(5),
peer_transmit: now + Duration::from_millis(500),
};
assert!(timestamps.round_trip_delay().is_err());
assert!(timestamps.offset().is_err());
}
}
#[derive(Debug, Clone, Default)]
pub struct ProtocolManager {
pub protocol: ClockSyncProtocol,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PtpConfig {
pub domain: u8,
}
#[derive(Debug, Clone, Default)]
pub struct PtpSynchronizer {
pub config: PtpConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SntpConfig {
pub server: String,
}
#[derive(Debug, Clone, Default)]
pub struct SntpSynchronizer {
pub config: SntpConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum PtpVersion {
V1,
#[default]
V2,
V2_1,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum PtpTransport {
#[default]
UDP,
Ethernet,
Serial,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum PtpProfile {
#[default]
Default,
Telecom,
Power,
Industrial,
}