Skip to main content

cu29_clock/
sync.rs

1//! Transport-independent reference discipline with fixed storage.
2//!
3//! One controller owns a clock's curve. Reference I/O belongs to the caller;
4//! readers only evaluate a published integer affine mapping.
5
6use crate::{ClockOrigin, CuDuration, CuInstant, CuTime};
7use bincode::{Decode, Encode};
8use core::fmt;
9use serde::{Deserialize, Serialize};
10
11const BILLION: i128 = 1_000_000_000;
12
13/// Reference identity and session, independent of timestamp epoch.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
15pub struct ClockDomain {
16    /// PTP domain number or application domain identifier.
17    pub id: u8,
18    /// Root clock identity, unchanged across followers of the same parent.
19    pub identity: [u8; 8],
20    /// Reference session generation, changed when the reference resets.
21    pub session: u64,
22}
23
24/// Paired counter/reference measurement at the same event.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
26pub struct ClockObservation {
27    /// Undisciplined local counter in nanoseconds.
28    pub raw_local: CuInstant,
29    /// Provider reference time at the capture event, without epoch conversion.
30    pub parent_ns: u64,
31    /// Source and epoch of the supplied reference time.
32    pub origin: ClockOrigin,
33    /// Upstream and capture error combined.
34    pub uncertainty: CuDuration,
35    /// Epoch and session identity.
36    pub domain: ClockDomain,
37}
38
39/// Error and correction bounds. All rates are parts per billion.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
41pub struct SyncConfig {
42    /// Maximum usable timestamp uncertainty.
43    pub max_error: CuDuration,
44    /// Maximum age of an accepted reference measurement.
45    pub max_age: CuDuration,
46    /// Relative oscillator error during holdover.
47    pub drift_bound_ppb: u32,
48    /// Additional phase correction rate; must leave the total rate positive.
49    pub max_slew_ppb: u32,
50}
51
52impl SyncConfig {
53    /// Conservative starting bounds: five seconds, 100 ppm drift, 500 ppm slew.
54    pub fn new(max_error: CuDuration) -> Self {
55        Self {
56            max_error,
57            max_age: CuDuration::from_secs(5),
58            drift_bound_ppb: 100_000,
59            max_slew_ppb: 500_000,
60        }
61    }
62
63    /// Validates positive error/age bounds and a strictly positive total rate.
64    pub fn validate(self) -> Result<(), SyncError> {
65        if self.max_error.0 == 0
66            || self.max_age.0 == 0
67            || u64::from(self.drift_bound_ppb) + u64::from(self.max_slew_ppb) >= 1_000_000_000
68        {
69            return Err(SyncError::InvalidConfig);
70        }
71        Ok(())
72    }
73}
74
75/// Reference acquisition and health.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
77pub enum SyncState {
78    /// Collecting samples or slewing into the requested error budget.
79    Acquiring,
80    /// Tracking a usable reference within the error budget.
81    Locked,
82    /// Reference unavailable; extrapolating the last curve within its bounds.
83    Holdover,
84    /// Budget, age, or parent continuity violated; reacquisition is required.
85    Expired,
86}
87
88/// Current quality of the execution timeline.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
90pub struct SyncStatus {
91    /// Current acquisition/health state.
92    pub state: SyncState,
93    /// Reference epoch identity.
94    pub domain: ClockDomain,
95    /// Raw elapsed time since the last accepted sample.
96    pub sample_age: CuDuration,
97    /// Capture, upstream, extrapolation and remaining phase error combined.
98    pub estimated_error: CuDuration,
99    /// Maximum combined timestamp uncertainty accepted by this clock.
100    pub max_error: CuDuration,
101    /// Signed parent minus execution time in nanoseconds.
102    pub phase_offset_ns: i64,
103    /// Estimated reference/counter rate difference in ppb.
104    pub drift_ppb: i64,
105    /// Oscillator error bound used to age uncertainty during holdover.
106    pub drift_bound_ppb: u32,
107}
108
109/// Bounded errors that do not allocate, including on embedded targets.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum SyncError {
112    /// Invalid error/rate bounds or counter frequency.
113    InvalidConfig,
114    /// A controller already owns this clock.
115    AlreadyControlled,
116    /// Observation belongs to another epoch/session.
117    WrongDomain,
118    /// Observation is future-dated, stale, duplicate or reordered.
119    InvalidSample,
120    /// Reference jumped or measurements violate the oscillator bound.
121    Discontinuity,
122    /// Timestamp or arithmetic cannot be represented.
123    OutOfRange,
124    /// Synchronization expired and requires explicit resync.
125    Expired,
126    /// A publication was deferred because readers were active.
127    ReadersActive,
128}
129
130impl fmt::Display for SyncError {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "clock synchronization: {self:?}")
133    }
134}
135impl core::error::Error for SyncError {}
136
137/// Recorded affine mapping and quality, restored without reference I/O.
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
139pub struct ClockSnapshot {
140    raw_anchor: u64,
141    time_anchor: u64,
142    rate_ppb: i64,
143    quality: Option<Quality>,
144    origin: ClockOrigin,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
148struct Quality {
149    domain: ClockDomain,
150    config: SyncConfig,
151    state: SyncState,
152    sample: Option<ClockObservation>,
153    drift_ppb: i64,
154}
155
156fn scaled(delta: i128, rate_ppb: i64) -> i128 {
157    delta * (BILLION + i128::from(rate_ppb)) / BILLION
158}
159fn checked_time(value: i128) -> Result<u64, SyncError> {
160    if !(0..=i128::from(CuTime::MAX.0)).contains(&value) {
161        return Err(SyncError::OutOfRange);
162    }
163    Ok(value as u64)
164}
165fn ceil_drift(age: u64, ppb: u32) -> u64 {
166    (u128::from(age) * u128::from(ppb))
167        .div_ceil(BILLION as u128)
168        .min(u128::from(u64::MAX)) as u64
169}
170
171impl ClockSnapshot {
172    /// Recorded provenance and epoch of the execution mapping.
173    pub fn origin(&self) -> ClockOrigin {
174        self.origin
175    }
176
177    /// Evaluates the recorded execution curve at an undisciplined counter time.
178    pub fn at(&self, raw: CuInstant) -> Result<CuTime, SyncError> {
179        checked_time(
180            i128::from(self.time_anchor)
181                + scaled(
182                    i128::from(raw.as_nanos()) - i128::from(self.raw_anchor),
183                    self.rate_ppb,
184                ),
185        )
186        .map(CuTime)
187    }
188
189    /// Ages the recorded quality against an undisciplined counter time.
190    pub fn status(&self, raw: CuInstant) -> Option<SyncStatus> {
191        let q = self.quality?;
192        let mut result = SyncStatus {
193            state: q.state,
194            domain: q.domain,
195            sample_age: CuDuration(0),
196            estimated_error: CuDuration::MAX,
197            max_error: q.config.max_error,
198            phase_offset_ns: 0,
199            drift_ppb: q.drift_ppb,
200            drift_bound_ppb: q.config.drift_bound_ppb,
201        };
202        if let Some(sample) = q.sample {
203            let age = raw.as_nanos().saturating_sub(sample.raw_local.as_nanos());
204            let parent = i128::from(sample.parent_ns) + scaled(i128::from(age), q.drift_ppb);
205            let phase = self.at(raw).map(|now| parent - i128::from(now.as_nanos()));
206            result.sample_age = CuDuration(age);
207            if let Ok(phase) = phase
208                && let Ok(offset) = i64::try_from(phase)
209            {
210                result.phase_offset_ns = offset;
211                result.estimated_error = CuDuration(
212                    sample
213                        .uncertainty
214                        .0
215                        .saturating_add(ceil_drift(age, q.config.drift_bound_ppb))
216                        .saturating_add(offset.unsigned_abs()),
217                );
218            }
219            if age > q.config.max_age.0 || result.estimated_error > q.config.max_error {
220                // Acquisition is allowed to slew a large initial phase error.
221                if q.state != SyncState::Acquiring || age > q.config.max_age.0 {
222                    result.state = SyncState::Expired;
223                }
224            }
225        }
226        Some(result)
227    }
228}
229
230#[cfg(feature = "clock-sync")]
231mod live;
232
233#[cfg(feature = "clock-sync")]
234pub use live::ClockSync;
235#[cfg(feature = "clock-sync")]
236pub(crate) use live::SharedClock;