use std::time::{SystemTime, UNIX_EPOCH};
use crate::clock::{ClockAnchor, ClockSource};
use crate::error::{ClockError, Result};
pub(crate) fn system_now_ns() -> Result<i64> {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| ClockError::NotSynced(format!("system clock before epoch: {e}")))?;
i64::try_from(duration.as_nanos()).map_err(|_| ClockError::ConversionOverflow)
}
pub struct NtpClock {
anchor: ClockAnchor,
}
impl std::fmt::Debug for NtpClock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NtpClock")
.field("anchor", &self.anchor)
.finish()
}
}
impl NtpClock {
pub fn new(sample_rate: u32) -> Result<Self> {
let now_ns = system_now_ns()?;
Ok(Self {
anchor: ClockAnchor::new(sample_rate, 0, now_ns)?,
})
}
#[must_use]
pub fn with_anchor(anchor: ClockAnchor) -> Self {
Self { anchor }
}
#[must_use]
pub fn anchor(&self) -> ClockAnchor {
self.anchor
}
pub fn reanchor(&mut self, ref_sample: i64) -> Result<()> {
let now_ns = system_now_ns()?;
self.anchor = ClockAnchor::new(self.anchor.sample_rate, ref_sample, now_ns)?;
Ok(())
}
}
impl ClockSource for NtpClock {
fn ptp_now_ns(&self) -> Result<i64> {
system_now_ns()
}
fn sample_to_ptp(&self, sample_idx: i64) -> i64 {
self.anchor
.sample_to_ptp_checked(sample_idx)
.unwrap_or(i64::MIN)
}
fn ptp_to_sample(&self, ptp_ns: i64) -> i64 {
self.anchor
.ptp_to_sample_checked(ptp_ns)
.unwrap_or(i64::MIN)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_anchors_sample_zero_to_now() {
let before = system_now_ns().unwrap();
let clock = NtpClock::new(48_000).unwrap();
let after = system_now_ns().unwrap();
let t0 = clock.sample_to_ptp(0);
assert!(
t0 >= before && t0 <= after,
"t0={t0} not in [{before},{after}]"
);
}
#[test]
fn new_rejects_zero_rate() {
let err = NtpClock::new(0).unwrap_err();
assert!(matches!(err, ClockError::InvalidSampleRate(0)));
}
#[test]
fn ptp_now_ns_is_monotonic_within_window() {
let clock = NtpClock::new(48_000).unwrap();
let a = clock.ptp_now_ns().unwrap();
let b = clock.ptp_now_ns().unwrap();
assert!(b >= a, "wall clock went backwards: {a} -> {b}");
}
#[test]
fn sample_to_ptp_advances_with_rate() {
let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 0, 0).unwrap());
assert_eq!(clock.sample_to_ptp(48_000), 1_000_000_000);
}
#[test]
fn ptp_to_sample_inverse_of_anchor() {
let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 100, 2_000_000_000).unwrap());
assert_eq!(clock.ptp_to_sample(2_000_000_000), 100);
}
#[test]
fn reanchor_moves_the_reference_sample() {
let mut clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 0, 0).unwrap());
let before = system_now_ns().unwrap();
clock.reanchor(1_000).unwrap();
let t = clock.sample_to_ptp(1_000);
let after = system_now_ns().unwrap();
assert!(t >= before && t <= after, "t={t} not in [{before},{after}]");
}
#[test]
fn dyn_clock_source_object_compiles() {
let clock: Box<dyn ClockSource> = Box::new(NtpClock::new(48_000).unwrap());
assert!(clock.ptp_now_ns().is_ok());
}
}