Skip to main content

rtime_core/
timestamp.rs

1use std::fmt;
2use std::ops::{Add, Sub};
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5/// Seconds between NTP epoch (1900-01-01) and Unix epoch (1970-01-01).
6const NTP_UNIX_EPOCH_DIFF: u64 = 2_208_988_800;
7
8/// NTP uses a 64-bit timestamp: 32-bit seconds since 1900-01-01 + 32-bit fraction.
9/// Stored as a single u64 for arithmetic efficiency.
10/// Upper 32 bits = seconds, lower 32 bits = fractional seconds.
11#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
12pub struct NtpTimestamp(pub u64);
13
14impl NtpTimestamp {
15    pub const ZERO: Self = Self(0);
16
17    /// Create from separate seconds (since NTP epoch) and fraction parts.
18    pub fn new(seconds: u32, fraction: u32) -> Self {
19        Self(((seconds as u64) << 32) | (fraction as u64))
20    }
21
22    /// Create from the current system time.
23    pub fn now() -> Self {
24        Self::from_system_time(SystemTime::now())
25    }
26
27    /// Convert from a SystemTime.
28    pub fn from_system_time(time: SystemTime) -> Self {
29        let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
30        let ntp_seconds = duration.as_secs() + NTP_UNIX_EPOCH_DIFF;
31        let fraction = ((duration.subsec_nanos() as u64) << 32) / 1_000_000_000;
32        Self::new(ntp_seconds as u32, fraction as u32)
33    }
34
35    /// Convert to a SystemTime.
36    pub fn to_system_time(self) -> SystemTime {
37        let secs = self.seconds() as u64;
38        let unix_secs = secs.saturating_sub(NTP_UNIX_EPOCH_DIFF);
39        let nanos = ((self.fraction() as u64) * 1_000_000_000) >> 32;
40        UNIX_EPOCH + Duration::new(unix_secs, nanos as u32)
41    }
42
43    /// Seconds since NTP epoch (upper 32 bits).
44    pub fn seconds(self) -> u32 {
45        (self.0 >> 32) as u32
46    }
47
48    /// Fractional seconds (lower 32 bits).
49    pub fn fraction(self) -> u32 {
50        self.0 as u32
51    }
52
53    /// Raw 64-bit value.
54    pub fn raw(self) -> u64 {
55        self.0
56    }
57
58    /// Create from raw 64-bit value.
59    pub fn from_raw(raw: u64) -> Self {
60        Self(raw)
61    }
62
63    /// Serialize to 8-byte big-endian wire format.
64    pub fn to_bytes(self) -> [u8; 8] {
65        self.0.to_be_bytes()
66    }
67
68    /// Deserialize from 8-byte big-endian wire format.
69    pub fn from_bytes(bytes: [u8; 8]) -> Self {
70        Self(u64::from_be_bytes(bytes))
71    }
72}
73
74impl fmt::Debug for NtpTimestamp {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(
77            f,
78            "NtpTimestamp({}.{:010})",
79            self.seconds(),
80            self.fraction()
81        )
82    }
83}
84
85impl fmt::Display for NtpTimestamp {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        let nanos = ((self.fraction() as u64) * 1_000_000_000) >> 32;
88        write!(f, "{}.{:09}", self.seconds(), nanos)
89    }
90}
91
92/// PTP uses an 80-bit timestamp: 48-bit seconds + 32-bit nanoseconds.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
94pub struct PtpTimestamp {
95    /// Seconds since PTP epoch (TAI, 1970-01-01). Only lower 48 bits used on wire.
96    pub seconds: u64,
97    /// Nanoseconds [0, 999_999_999].
98    pub nanoseconds: u32,
99}
100
101impl PtpTimestamp {
102    pub const ZERO: Self = Self {
103        seconds: 0,
104        nanoseconds: 0,
105    };
106
107    pub fn new(seconds: u64, nanoseconds: u32) -> Self {
108        Self {
109            seconds,
110            nanoseconds,
111        }
112    }
113
114    /// Convert to NtpTimestamp (approximate, ignores TAI-UTC offset).
115    pub fn to_ntp_timestamp(self) -> NtpTimestamp {
116        let ntp_secs = self.seconds + NTP_UNIX_EPOCH_DIFF;
117        let fraction = ((self.nanoseconds as u64) << 32) / 1_000_000_000;
118        NtpTimestamp::new(ntp_secs as u32, fraction as u32)
119    }
120
121    /// Serialize the 10-byte wire format (6 bytes seconds + 4 bytes nanos).
122    pub fn to_bytes(self) -> [u8; 10] {
123        let mut buf = [0u8; 10];
124        let sec_bytes = self.seconds.to_be_bytes();
125        buf[0..6].copy_from_slice(&sec_bytes[2..8]);
126        buf[6..10].copy_from_slice(&self.nanoseconds.to_be_bytes());
127        buf
128    }
129
130    /// Deserialize from 10-byte wire format.
131    pub fn from_bytes(bytes: [u8; 10]) -> Self {
132        let mut sec_buf = [0u8; 8];
133        sec_buf[2..8].copy_from_slice(&bytes[0..6]);
134        let seconds = u64::from_be_bytes(sec_buf);
135        let nanoseconds = u32::from_be_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]);
136        Self {
137            seconds,
138            nanoseconds,
139        }
140    }
141}
142
143impl fmt::Display for PtpTimestamp {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        write!(f, "{}.{:09}", self.seconds, self.nanoseconds)
146    }
147}
148
149/// High-resolution signed duration for offset/delay calculations.
150/// Stored as signed nanoseconds in i128 for sub-nanosecond precision
151/// with the lower 32 bits representing fractional nanoseconds.
152#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
153pub struct NtpDuration(i128);
154
155impl NtpDuration {
156    pub const ZERO: Self = Self(0);
157
158    /// Number of fractional bits in the fixed-point representation.
159    const FRAC_BITS: u32 = 32;
160
161    /// Create from whole nanoseconds.
162    pub fn from_nanos(nanos: i64) -> Self {
163        Self((nanos as i128) << Self::FRAC_BITS)
164    }
165
166    /// Create from seconds as f64.
167    pub fn from_seconds_f64(secs: f64) -> Self {
168        let nanos = secs * 1_000_000_000.0;
169        Self((nanos as i128) << Self::FRAC_BITS)
170    }
171
172    /// Create from milliseconds.
173    pub fn from_millis(ms: i64) -> Self {
174        Self::from_nanos(ms * 1_000_000)
175    }
176
177    /// Convert to nanoseconds (truncating fractional part).
178    pub fn to_nanos(self) -> i64 {
179        (self.0 >> Self::FRAC_BITS) as i64
180    }
181
182    /// Convert to seconds as f64.
183    pub fn to_seconds_f64(self) -> f64 {
184        (self.0 as f64) / ((1i128 << Self::FRAC_BITS) as f64) / 1_000_000_000.0
185    }
186
187    /// Convert to milliseconds as f64.
188    pub fn to_millis_f64(self) -> f64 {
189        self.to_seconds_f64() * 1_000.0
190    }
191
192    /// Absolute value.
193    pub fn abs(self) -> Self {
194        Self(self.0.abs())
195    }
196
197    /// Create from NTP short format (16.16 fixed-point, in seconds).
198    pub fn from_ntp_short(raw: u32) -> Self {
199        let seconds = (raw >> 16) as i64;
200        let fraction = (raw & 0xFFFF) as i64;
201        let nanos = seconds * 1_000_000_000 + (fraction * 1_000_000_000) / 65536;
202        Self::from_nanos(nanos)
203    }
204
205    /// Convert to NTP short format (16.16 fixed-point, in seconds).
206    /// Only works for non-negative durations.
207    pub fn to_ntp_short(self) -> u32 {
208        let nanos = self.to_nanos().max(0) as u64;
209        let seconds = nanos / 1_000_000_000;
210        let frac_nanos = nanos % 1_000_000_000;
211        let frac = (frac_nanos * 65536) / 1_000_000_000;
212        ((seconds as u32) << 16) | (frac as u32)
213    }
214
215    /// Compute the difference between two NTP timestamps as a duration.
216    pub fn between(a: NtpTimestamp, b: NtpTimestamp) -> Self {
217        let diff = (b.0 as i128) - (a.0 as i128);
218        // Convert from NTP fixed-point (32.32 seconds) to our format (nanos with 32 frac bits)
219        // NTP: upper 32 = seconds, lower 32 = fractional seconds
220        // We need nanoseconds << 32
221        // diff is in 32.32 seconds format
222        // nanos = diff * 1_000_000_000 / (1 << 32)
223        // But we want nanos << 32, so: diff * 1_000_000_000
224        Self(diff * 1_000_000_000 / (1i128 << 32) * (1i128 << Self::FRAC_BITS))
225    }
226
227    /// Raw internal value for serialization.
228    pub fn raw(self) -> i128 {
229        self.0
230    }
231
232    /// Create from raw internal value (inverse of `raw()`).
233    pub fn from_raw(raw: i128) -> Self {
234        Self(raw)
235    }
236}
237
238impl Add for NtpDuration {
239    type Output = Self;
240    fn add(self, rhs: Self) -> Self {
241        Self(self.0 + rhs.0)
242    }
243}
244
245impl Sub for NtpDuration {
246    type Output = Self;
247    fn sub(self, rhs: Self) -> Self {
248        Self(self.0 - rhs.0)
249    }
250}
251
252impl std::ops::Div<i64> for NtpDuration {
253    type Output = Self;
254    fn div(self, rhs: i64) -> Self {
255        Self(self.0 / rhs as i128)
256    }
257}
258
259impl std::ops::Neg for NtpDuration {
260    type Output = Self;
261    fn neg(self) -> Self {
262        Self(-self.0)
263    }
264}
265
266impl fmt::Debug for NtpDuration {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        write!(f, "NtpDuration({:.9}s)", self.to_seconds_f64())
269    }
270}
271
272impl fmt::Display for NtpDuration {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        let secs = self.to_seconds_f64();
275        if secs.abs() < 0.001 {
276            write!(f, "{:.3}us", secs * 1_000_000.0)
277        } else if secs.abs() < 1.0 {
278            write!(f, "{:.3}ms", secs * 1_000.0)
279        } else {
280            write!(f, "{:.6}s", secs)
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn ntp_timestamp_roundtrip_bytes() {
291        let ts = NtpTimestamp::new(3_900_000_000, 2_147_483_648);
292        let bytes = ts.to_bytes();
293        let ts2 = NtpTimestamp::from_bytes(bytes);
294        assert_eq!(ts, ts2);
295    }
296
297    #[test]
298    fn ntp_timestamp_system_time_roundtrip() {
299        let now = SystemTime::now();
300        let ts = NtpTimestamp::from_system_time(now);
301        let back = ts.to_system_time();
302        let diff = now
303            .duration_since(back)
304            .or_else(|e| Ok::<_, std::convert::Infallible>(e.duration()))
305            .unwrap();
306        // Should be within 1 microsecond (NTP fraction precision)
307        assert!(diff < Duration::from_micros(1));
308    }
309
310    #[test]
311    fn ntp_timestamp_zero() {
312        let ts = NtpTimestamp::ZERO;
313        assert_eq!(ts.seconds(), 0);
314        assert_eq!(ts.fraction(), 0);
315    }
316
317    #[test]
318    fn ptp_timestamp_roundtrip_bytes() {
319        let ts = PtpTimestamp::new(1_000_000, 500_000_000);
320        let bytes = ts.to_bytes();
321        let ts2 = PtpTimestamp::from_bytes(bytes);
322        assert_eq!(ts, ts2);
323    }
324
325    #[test]
326    fn ptp_timestamp_48bit_seconds() {
327        // Verify only lower 48 bits are used in wire format
328        let ts = PtpTimestamp::new(0x0000_FFFF_FFFF_FFFF, 0);
329        let bytes = ts.to_bytes();
330        let ts2 = PtpTimestamp::from_bytes(bytes);
331        assert_eq!(ts2.seconds, 0x0000_FFFF_FFFF_FFFF);
332    }
333
334    #[test]
335    fn ntp_duration_from_nanos() {
336        let d = NtpDuration::from_nanos(1_000_000_000); // 1 second
337        assert!((d.to_seconds_f64() - 1.0).abs() < 1e-9);
338    }
339
340    #[test]
341    fn ntp_duration_from_seconds_f64() {
342        let d = NtpDuration::from_seconds_f64(0.5);
343        assert!((d.to_seconds_f64() - 0.5).abs() < 1e-9);
344    }
345
346    #[test]
347    fn ntp_duration_arithmetic() {
348        let a = NtpDuration::from_nanos(100);
349        let b = NtpDuration::from_nanos(50);
350        assert_eq!((a + b).to_nanos(), 150);
351        assert_eq!((a - b).to_nanos(), 50);
352        assert_eq!((a / 2).to_nanos(), 50);
353        assert_eq!((-a).to_nanos(), -100);
354    }
355
356    #[test]
357    fn ntp_duration_abs() {
358        let d = NtpDuration::from_nanos(-500);
359        assert_eq!(d.abs().to_nanos(), 500);
360    }
361
362    #[test]
363    fn ntp_duration_between_timestamps() {
364        let t1 = NtpTimestamp::new(1000, 0);
365        let t2 = NtpTimestamp::new(1001, 0);
366        let diff = NtpDuration::between(t1, t2);
367        assert!((diff.to_seconds_f64() - 1.0).abs() < 1e-6);
368    }
369
370    #[test]
371    fn ntp_duration_ntp_short_roundtrip() {
372        let d = NtpDuration::from_millis(500);
373        let short = d.to_ntp_short();
374        let d2 = NtpDuration::from_ntp_short(short);
375        assert!((d.to_millis_f64() - d2.to_millis_f64()).abs() < 0.1);
376    }
377
378    #[test]
379    fn ntp_duration_display() {
380        let d = NtpDuration::from_nanos(500);
381        assert!(format!("{}", d).contains("us"));
382
383        let d = NtpDuration::from_millis(50);
384        assert!(format!("{}", d).contains("ms"));
385
386        let d = NtpDuration::from_seconds_f64(2.5);
387        assert!(format!("{}", d).contains("s"));
388    }
389}