1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
15pub struct ClockDomain {
16 pub id: u8,
18 pub identity: [u8; 8],
20 pub session: u64,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
26pub struct ClockObservation {
27 pub raw_local: CuInstant,
29 pub parent_ns: u64,
31 pub origin: ClockOrigin,
33 pub uncertainty: CuDuration,
35 pub domain: ClockDomain,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
41pub struct SyncConfig {
42 pub max_error: CuDuration,
44 pub max_age: CuDuration,
46 pub drift_bound_ppb: u32,
48 pub max_slew_ppb: u32,
50}
51
52impl SyncConfig {
53 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
77pub enum SyncState {
78 Acquiring,
80 Locked,
82 Holdover,
84 Expired,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
90pub struct SyncStatus {
91 pub state: SyncState,
93 pub domain: ClockDomain,
95 pub sample_age: CuDuration,
97 pub estimated_error: CuDuration,
99 pub max_error: CuDuration,
101 pub phase_offset_ns: i64,
103 pub drift_ppb: i64,
105 pub drift_bound_ppb: u32,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum SyncError {
112 InvalidConfig,
114 AlreadyControlled,
116 WrongDomain,
118 InvalidSample,
120 Discontinuity,
122 OutOfRange,
124 Expired,
126 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#[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 pub fn origin(&self) -> ClockOrigin {
174 self.origin
175 }
176
177 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 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 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;