use crate::{ClockOrigin, CuDuration, CuInstant, CuTime};
use bincode::{Decode, Encode};
use core::fmt;
use serde::{Deserialize, Serialize};
const BILLION: i128 = 1_000_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
pub struct ClockDomain {
pub id: u8,
pub identity: [u8; 8],
pub session: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
pub struct ClockObservation {
pub raw_local: CuInstant,
pub parent_ns: u64,
pub origin: ClockOrigin,
pub uncertainty: CuDuration,
pub domain: ClockDomain,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
pub struct SyncConfig {
pub max_error: CuDuration,
pub max_age: CuDuration,
pub drift_bound_ppb: u32,
pub max_slew_ppb: u32,
}
impl SyncConfig {
pub fn new(max_error: CuDuration) -> Self {
Self {
max_error,
max_age: CuDuration::from_secs(5),
drift_bound_ppb: 100_000,
max_slew_ppb: 500_000,
}
}
pub fn validate(self) -> Result<(), SyncError> {
if self.max_error.0 == 0
|| self.max_age.0 == 0
|| u64::from(self.drift_bound_ppb) + u64::from(self.max_slew_ppb) >= 1_000_000_000
{
return Err(SyncError::InvalidConfig);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
pub enum SyncState {
Acquiring,
Locked,
Holdover,
Expired,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
pub struct SyncStatus {
pub state: SyncState,
pub domain: ClockDomain,
pub sample_age: CuDuration,
pub estimated_error: CuDuration,
pub max_error: CuDuration,
pub phase_offset_ns: i64,
pub drift_ppb: i64,
pub drift_bound_ppb: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SyncError {
InvalidConfig,
AlreadyControlled,
WrongDomain,
InvalidSample,
Discontinuity,
OutOfRange,
Expired,
ReadersActive,
}
impl fmt::Display for SyncError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "clock synchronization: {self:?}")
}
}
impl core::error::Error for SyncError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
pub struct ClockSnapshot {
raw_anchor: u64,
time_anchor: u64,
rate_ppb: i64,
quality: Option<Quality>,
origin: ClockOrigin,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
struct Quality {
domain: ClockDomain,
config: SyncConfig,
state: SyncState,
sample: Option<ClockObservation>,
drift_ppb: i64,
}
fn scaled(delta: i128, rate_ppb: i64) -> i128 {
delta * (BILLION + i128::from(rate_ppb)) / BILLION
}
fn checked_time(value: i128) -> Result<u64, SyncError> {
if !(0..=i128::from(CuTime::MAX.0)).contains(&value) {
return Err(SyncError::OutOfRange);
}
Ok(value as u64)
}
fn ceil_drift(age: u64, ppb: u32) -> u64 {
(u128::from(age) * u128::from(ppb))
.div_ceil(BILLION as u128)
.min(u128::from(u64::MAX)) as u64
}
impl ClockSnapshot {
pub fn origin(&self) -> ClockOrigin {
self.origin
}
pub fn at(&self, raw: CuInstant) -> Result<CuTime, SyncError> {
checked_time(
i128::from(self.time_anchor)
+ scaled(
i128::from(raw.as_nanos()) - i128::from(self.raw_anchor),
self.rate_ppb,
),
)
.map(CuTime)
}
pub fn status(&self, raw: CuInstant) -> Option<SyncStatus> {
let q = self.quality?;
let mut result = SyncStatus {
state: q.state,
domain: q.domain,
sample_age: CuDuration(0),
estimated_error: CuDuration::MAX,
max_error: q.config.max_error,
phase_offset_ns: 0,
drift_ppb: q.drift_ppb,
drift_bound_ppb: q.config.drift_bound_ppb,
};
if let Some(sample) = q.sample {
let age = raw.as_nanos().saturating_sub(sample.raw_local.as_nanos());
let parent = i128::from(sample.parent_ns) + scaled(i128::from(age), q.drift_ppb);
let phase = self.at(raw).map(|now| parent - i128::from(now.as_nanos()));
result.sample_age = CuDuration(age);
if let Ok(phase) = phase
&& let Ok(offset) = i64::try_from(phase)
{
result.phase_offset_ns = offset;
result.estimated_error = CuDuration(
sample
.uncertainty
.0
.saturating_add(ceil_drift(age, q.config.drift_bound_ppb))
.saturating_add(offset.unsigned_abs()),
);
}
if age > q.config.max_age.0 || result.estimated_error > q.config.max_error {
if q.state != SyncState::Acquiring || age > q.config.max_age.0 {
result.state = SyncState::Expired;
}
}
}
Some(result)
}
}
#[cfg(feature = "clock-sync")]
mod live;
#[cfg(feature = "clock-sync")]
pub use live::ClockSync;
#[cfg(feature = "clock-sync")]
pub(crate) use live::SharedClock;