Skip to main content

rusty_time_core/
refclock.rs

1//! Reference-clock samples: what a GPS, a PPS edge or a PTP hardware clock
2//! hands us, and what must be true before any of it is believed.
3//!
4//! Portable on purpose. The transports differ wildly per platform — SysV shared
5//! memory, a Unix datagram, an ioctl on a character device — but what arrives
6//! is always the same shape: "at *my* time T, the reference said R". Deciding
7//! whether that pairing is usable is arithmetic, and it belongs here where it
8//! can be tested without a GPS on the desk.
9
10use core::fmt;
11
12/// One reading from a reference clock.
13///
14/// **The offset is the stored quantity, not the reference time.** Storing a
15/// reference timestamp and recovering the offset by subtraction loses about
16/// 100 ns: an f64 holding a Unix-epoch value (~1.76e9) has only ~1e-7 s of
17/// resolution left below the decimal point, so `local + offset - local` does
18/// not return `offset`. That silently caps every refclock at roughly 100 ns
19/// however precise the hardware is — fatal for a PPS source, and it was found
20/// by a round-trip test asserting exactness rather than "close enough".
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct RefclockSample {
23    /// Our own clock when the reading was taken, Unix seconds.
24    pub local_s: f64,
25    /// Seconds to ADD to our clock to match the reference. Authoritative.
26    pub offset_s: f64,
27    /// The reference's own claim about its precision, log2 seconds.
28    pub precision_log2: i8,
29    pub leap: LeapWarning,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum LeapWarning {
34    None,
35    AddSecond,
36    DeleteSecond,
37    /// The reference is not currently trustworthy.
38    NotSynchronized,
39}
40
41impl LeapWarning {
42    /// Decode the leap field used by both the SHM and SOCK protocols.
43    pub fn from_wire(value: i32) -> LeapWarning {
44        match value {
45            0 => LeapWarning::None,
46            1 => LeapWarning::AddSecond,
47            2 => LeapWarning::DeleteSecond,
48            _ => LeapWarning::NotSynchronized,
49        }
50    }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum RefclockError {
55    /// The reference says it is not synchronized.
56    NotSynchronized,
57    /// The offset is larger than any sane reference could imply.
58    OffsetTooLarge,
59    /// A timestamp was zero, negative or otherwise not a time.
60    Implausible,
61    /// This reading is not newer than the one before it.
62    Stale,
63}
64
65impl fmt::Display for RefclockError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        let s = match self {
68            RefclockError::NotSynchronized => "reference clock reports it is unsynchronized",
69            RefclockError::OffsetTooLarge => "reference offset is implausibly large",
70            RefclockError::Implausible => "reference timestamps are not plausible times",
71            RefclockError::Stale => "reference sample is not newer than the last one",
72        };
73        f.write_str(s)
74    }
75}
76
77/// The widest offset a reference clock may imply before we refuse it.
78///
79/// A GPS receiver that has lost lock, or a shared-memory segment left by a dead
80/// process, can present a wildly stale reading that is otherwise well formed.
81/// Sixteen seconds is far beyond any real refclock's error and far short of the
82/// jumps a stale sample produces.
83pub const MAX_REFCLOCK_OFFSET_S: f64 = 16.0;
84
85/// Earliest time we will accept as a real reading (2020-01-01). A zeroed or
86/// partially-written segment reads as an epoch-ish timestamp, and this is what
87/// catches it.
88const MIN_PLAUSIBLE_UNIX_S: f64 = 1_577_836_800.0;
89
90impl RefclockSample {
91    /// Seconds to ADD to our clock to match the reference.
92    pub fn offset_s(&self) -> f64 {
93        self.offset_s
94    }
95
96    /// What the reference says our `local_s` instant actually was.
97    ///
98    /// Derived, and therefore subject to the same epoch-magnitude rounding the
99    /// struct exists to avoid — use it for display, never to recompute the
100    /// offset.
101    pub fn reference_s(&self) -> f64 {
102        self.local_s + self.offset_s
103    }
104
105    /// Is this reading usable? `last_local_s` is the previous accepted
106    /// reading's local timestamp, if any.
107    pub fn validate(&self, last_local_s: Option<f64>) -> Result<(), RefclockError> {
108        if self.leap == LeapWarning::NotSynchronized {
109            return Err(RefclockError::NotSynchronized);
110        }
111        if !self.local_s.is_finite()
112            || !self.offset_s.is_finite()
113            || self.local_s < MIN_PLAUSIBLE_UNIX_S
114            || self.reference_s() < MIN_PLAUSIBLE_UNIX_S
115        {
116            return Err(RefclockError::Implausible);
117        }
118        if let Some(previous) = last_local_s
119            && self.local_s <= previous
120        {
121            // Re-reading the same segment must not be mistaken for a new
122            // sample: a dead producer would otherwise keep "confirming" a
123            // frozen time forever.
124            return Err(RefclockError::Stale);
125        }
126        if self.offset_s().abs() > MAX_REFCLOCK_OFFSET_S {
127            return Err(RefclockError::OffsetTooLarge);
128        }
129        Ok(())
130    }
131
132    /// The dispersion this sample implies, from the reference's own precision
133    /// claim.
134    pub fn dispersion_s(&self) -> f64 {
135        2f64.powi(self.precision_log2 as i32)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn sample(local: f64, reference: f64) -> RefclockSample {
144        RefclockSample {
145            local_s: local,
146            offset_s: reference - local,
147            precision_log2: -20,
148            leap: LeapWarning::None,
149        }
150    }
151
152    /// Build directly from an offset, which is how the transports do it.
153    fn with_offset(local: f64, offset: f64) -> RefclockSample {
154        RefclockSample {
155            local_s: local,
156            offset_s: offset,
157            precision_log2: -20,
158            leap: LeapWarning::None,
159        }
160    }
161
162    #[test]
163    fn offset_is_reference_minus_local() {
164        let s = sample(1_756_224_000.0, 1_756_224_000.5);
165        assert!((s.offset_s() - 0.5).abs() < 1e-12);
166        assert!(s.validate(None).is_ok());
167    }
168
169    #[test]
170    fn an_unsynchronized_reference_is_refused() {
171        let mut s = sample(1_756_224_000.0, 1_756_224_000.0);
172        s.leap = LeapWarning::NotSynchronized;
173        assert_eq!(s.validate(None), Err(RefclockError::NotSynchronized));
174    }
175
176    #[test]
177    fn a_stale_segment_cannot_keep_confirming_a_frozen_time() {
178        // The failure this prevents: a producer dies, its shared memory keeps
179        // the last value, and every read looks like a fresh confirmation.
180        let s = sample(1_756_224_000.0, 1_756_224_000.0);
181        assert!(s.validate(Some(1_756_223_999.0)).is_ok());
182        assert_eq!(
183            s.validate(Some(1_756_224_000.0)),
184            Err(RefclockError::Stale),
185            "the same timestamp twice is not two samples"
186        );
187        assert_eq!(s.validate(Some(1_756_224_001.0)), Err(RefclockError::Stale));
188    }
189
190    #[test]
191    fn a_zeroed_segment_reads_as_implausible_not_as_1970() {
192        // An uninitialised or half-written segment is all zeroes, which is a
193        // valid-looking struct describing the epoch.
194        let s = sample(0.0, 0.0);
195        assert_eq!(s.validate(None), Err(RefclockError::Implausible));
196        // Half-written: local is current but the reference is not.
197        let half = sample(1_756_224_000.0, 0.0);
198        assert_eq!(half.validate(None), Err(RefclockError::Implausible));
199    }
200
201    #[test]
202    fn a_wildly_stale_reading_is_refused_however_well_formed() {
203        // A GPS that lost lock an hour ago still writes a tidy struct.
204        let s = sample(1_756_224_000.0, 1_756_224_000.0 - 3600.0);
205        assert_eq!(s.validate(None), Err(RefclockError::OffsetTooLarge));
206        // Just inside the bound is fine.
207        let ok = sample(1_756_224_000.0, 1_756_224_000.0 + 15.0);
208        assert!(ok.validate(None).is_ok());
209    }
210
211    #[test]
212    fn an_offset_survives_a_round_trip_exactly() {
213        // The defect this guards: storing a reference timestamp and
214        // subtracting to recover the offset loses ~100 ns at epoch magnitude,
215        // which would cap a nanosecond-class reference at 100 ns.
216        for offset in [1e-9, -1e-9, 1.5e-3, -1.5e-3, 123e-6] {
217            let s = with_offset(1_756_224_000.0, offset);
218            assert_eq!(
219                s.offset_s(),
220                offset,
221                "offset {offset} did not survive storage exactly"
222            );
223        }
224    }
225
226    #[test]
227    fn nan_and_infinity_are_not_times() {
228        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
229            assert_eq!(
230                sample(bad, 1_756_224_000.0).validate(None),
231                Err(RefclockError::Implausible)
232            );
233            assert_eq!(
234                with_offset(1_756_224_000.0, bad).validate(None),
235                Err(RefclockError::Implausible)
236            );
237        }
238    }
239
240    #[test]
241    fn leap_decoding_matches_the_wire_values() {
242        assert_eq!(LeapWarning::from_wire(0), LeapWarning::None);
243        assert_eq!(LeapWarning::from_wire(1), LeapWarning::AddSecond);
244        assert_eq!(LeapWarning::from_wire(2), LeapWarning::DeleteSecond);
245        // Anything else, including the protocol's own "3", means do not trust.
246        for unknown in [3, 4, -1, 99] {
247            assert_eq!(
248                LeapWarning::from_wire(unknown),
249                LeapWarning::NotSynchronized
250            );
251        }
252    }
253
254    #[test]
255    fn dispersion_follows_the_precision_claim() {
256        let mut s = sample(1_756_224_000.0, 1_756_224_000.0);
257        s.precision_log2 = -20; // ~1 us
258        assert!((s.dispersion_s() - 9.5367e-7).abs() < 1e-9);
259        s.precision_log2 = 0; // 1 s: a very coarse reference
260        assert!((s.dispersion_s() - 1.0).abs() < 1e-12);
261    }
262}