1use std::time::{SystemTime, UNIX_EPOCH};
18
19use crate::clock::{ClockAnchor, ClockSource};
20use crate::error::{ClockError, Result};
21
22pub(crate) fn system_now_ns() -> Result<i64> {
28 let duration = SystemTime::now()
29 .duration_since(UNIX_EPOCH)
30 .map_err(|e| ClockError::NotSynced(format!("system clock before epoch: {e}")))?;
31 i64::try_from(duration.as_nanos()).map_err(|_| ClockError::ConversionOverflow)
32}
33
34pub struct NtpClock {
41 anchor: ClockAnchor,
42}
43
44impl std::fmt::Debug for NtpClock {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("NtpClock")
47 .field("anchor", &self.anchor)
48 .finish()
49 }
50}
51
52impl NtpClock {
53 pub fn new(sample_rate: u32) -> Result<Self> {
60 let now_ns = system_now_ns()?;
61 Ok(Self {
62 anchor: ClockAnchor::new(sample_rate, 0, now_ns)?,
63 })
64 }
65
66 #[must_use]
68 pub fn with_anchor(anchor: ClockAnchor) -> Self {
69 Self { anchor }
70 }
71
72 #[must_use]
74 pub fn anchor(&self) -> ClockAnchor {
75 self.anchor
76 }
77
78 pub fn reanchor(&mut self, ref_sample: i64) -> Result<()> {
86 let now_ns = system_now_ns()?;
87 self.anchor = ClockAnchor::new(self.anchor.sample_rate, ref_sample, now_ns)?;
88 Ok(())
89 }
90}
91
92impl ClockSource for NtpClock {
93 fn ptp_now_ns(&self) -> Result<i64> {
94 system_now_ns()
95 }
96
97 fn sample_to_ptp(&self, sample_idx: i64) -> i64 {
98 self.anchor
99 .sample_to_ptp_checked(sample_idx)
100 .unwrap_or(i64::MIN)
101 }
102
103 fn ptp_to_sample(&self, ptp_ns: i64) -> i64 {
104 self.anchor
105 .ptp_to_sample_checked(ptp_ns)
106 .unwrap_or(i64::MIN)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn new_anchors_sample_zero_to_now() {
116 let before = system_now_ns().unwrap();
117 let clock = NtpClock::new(48_000).unwrap();
118 let after = system_now_ns().unwrap();
119 let t0 = clock.sample_to_ptp(0);
121 assert!(
122 t0 >= before && t0 <= after,
123 "t0={t0} not in [{before},{after}]"
124 );
125 }
126
127 #[test]
128 fn new_rejects_zero_rate() {
129 let err = NtpClock::new(0).unwrap_err();
130 assert!(matches!(err, ClockError::InvalidSampleRate(0)));
131 }
132
133 #[test]
134 fn ptp_now_ns_is_monotonic_within_window() {
135 let clock = NtpClock::new(48_000).unwrap();
136 let a = clock.ptp_now_ns().unwrap();
137 let b = clock.ptp_now_ns().unwrap();
138 assert!(b >= a, "wall clock went backwards: {a} -> {b}");
139 }
140
141 #[test]
142 fn sample_to_ptp_advances_with_rate() {
143 let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 0, 0).unwrap());
144 assert_eq!(clock.sample_to_ptp(48_000), 1_000_000_000);
146 }
147
148 #[test]
149 fn ptp_to_sample_inverse_of_anchor() {
150 let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 100, 2_000_000_000).unwrap());
151 assert_eq!(clock.ptp_to_sample(2_000_000_000), 100);
152 }
153
154 #[test]
155 fn reanchor_moves_the_reference_sample() {
156 let mut clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 0, 0).unwrap());
157 let before = system_now_ns().unwrap();
158 clock.reanchor(1_000).unwrap();
159 let t = clock.sample_to_ptp(1_000);
161 let after = system_now_ns().unwrap();
162 assert!(t >= before && t <= after, "t={t} not in [{before},{after}]");
163 }
164
165 #[test]
166 fn dyn_clock_source_object_compiles() {
167 let clock: Box<dyn ClockSource> = Box::new(NtpClock::new(48_000).unwrap());
168 assert!(clock.ptp_now_ns().is_ok());
169 }
170}