Skip to main content

audio_clock_bsd/
ptp.rs

1//! [`PtpClock`] — a `PTPv2` (IEEE 1588-2008) [`ClockSource`] backend.
2//!
3//! [`PtpClock`] obtains PTP timestamps from an injectable [`PtpTimeProvider`].
4//! FreeBSD hardware PTP timestamping support is NIC-dependent (igb/ixl/mlx5en)
5//! and not guaranteed, so the default construction ([`PtpClock::unavailable`])
6//! reports [`ClockError::Unavailable`] until a provider is supplied — typically
7//! by a daemon-polling thread that reads `/dev/ptp*` or queries `ptpd2`.
8//!
9//! # Threading
10//!
11//! Like all [`ClockSource`] implementors, [`PtpClock::ptp_now_ns`] may block
12//! (the provider might read a device or poll a daemon) and must run off the RT
13//! audio thread. The anchor conversions remain allocation-free and RT-safe.
14
15use crate::clock::{ClockAnchor, ClockSource};
16use crate::error::{ClockError, Result};
17
18/// A source of live PTP timestamps, injected into [`PtpClock`].
19///
20/// Implementations typically wrap a daemon-polling thread or a `/dev/ptp*`
21/// device read. For tests, a deterministic stub returning a fixed value is
22/// sufficient.
23pub trait PtpTimeProvider: Send + Sync {
24    /// Returns the current PTP timestamp in nanoseconds since the PTP epoch.
25    ///
26    /// # Errors
27    ///
28    /// - [`ClockError::Unavailable`] when no PTP source is reachable.
29    /// - [`ClockError::NotSynced`] when the source exists but has not locked.
30    fn ptp_now_ns(&self) -> Result<i64>;
31}
32
33/// A [`ClockSource`] backed by an injectable [`PtpTimeProvider`].
34///
35/// Construct with [`PtpClock::with_provider`] when a live PTP source is
36/// available, or [`PtpClock::unavailable`] for the stub (returns
37/// [`ClockError::Unavailable`] from [`ClockSource::ptp_now_ns`]).
38pub struct PtpClock {
39    anchor: ClockAnchor,
40    provider: Box<dyn PtpTimeProvider>,
41}
42
43impl std::fmt::Debug for PtpClock {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.debug_struct("PtpClock")
46            .field("anchor", &self.anchor)
47            .field("provider", &"<dyn PtpTimeProvider>")
48            .finish()
49    }
50}
51
52impl PtpClock {
53    /// Creates a PTP clock with a live time provider.
54    #[must_use]
55    pub fn with_provider(anchor: ClockAnchor, provider: Box<dyn PtpTimeProvider>) -> Self {
56        Self { anchor, provider }
57    }
58
59    /// Creates a PTP clock whose [`ClockSource::ptp_now_ns`] always reports
60    /// [`ClockError::Unavailable`].
61    ///
62    /// The anchor conversions still work, so the clock is usable for offline
63    /// timestamp mapping even without a live source.
64    #[must_use]
65    pub fn unavailable(anchor: ClockAnchor) -> Self {
66        Self {
67            anchor,
68            provider: Box::new(NoPtp),
69        }
70    }
71
72    /// Returns the current anchor.
73    #[must_use]
74    pub fn anchor(&self) -> ClockAnchor {
75        self.anchor
76    }
77}
78
79impl ClockSource for PtpClock {
80    fn ptp_now_ns(&self) -> Result<i64> {
81        self.provider.ptp_now_ns()
82    }
83
84    fn sample_to_ptp(&self, sample_idx: i64) -> i64 {
85        self.anchor
86            .sample_to_ptp_checked(sample_idx)
87            .unwrap_or(i64::MIN)
88    }
89
90    fn ptp_to_sample(&self, ptp_ns: i64) -> i64 {
91        self.anchor
92            .ptp_to_sample_checked(ptp_ns)
93            .unwrap_or(i64::MIN)
94    }
95}
96
97/// Default provider when no PTP hardware/daemon is present.
98struct NoPtp;
99
100impl PtpTimeProvider for NoPtp {
101    fn ptp_now_ns(&self) -> Result<i64> {
102        Err(ClockError::Unavailable(
103            "no PTP hardware timestamping available".into(),
104        ))
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::sync::atomic::{AtomicI64, Ordering};
112
113    /// A deterministic PTP provider returning a controllable timestamp.
114    struct StubProvider {
115        t: AtomicI64,
116    }
117
118    impl PtpTimeProvider for StubProvider {
119        fn ptp_now_ns(&self) -> Result<i64> {
120            Ok(self.t.load(Ordering::Relaxed))
121        }
122    }
123
124    #[test]
125    fn unavailable_reports_unavailable() {
126        let clock = PtpClock::unavailable(ClockAnchor::new(48_000, 0, 0).unwrap());
127        let err = clock.ptp_now_ns().unwrap_err();
128        assert!(matches!(err, ClockError::Unavailable(_)));
129    }
130
131    #[test]
132    fn unavailable_conversions_still_work() {
133        let clock = PtpClock::unavailable(ClockAnchor::new(48_000, 0, 0).unwrap());
134        assert_eq!(clock.sample_to_ptp(48_000), 1_000_000_000);
135        assert_eq!(clock.ptp_to_sample(1_000_000_000), 48_000);
136    }
137
138    #[test]
139    fn with_provider_reads_live_timestamp() {
140        let provider = StubProvider {
141            t: AtomicI64::new(7_000_000_000),
142        };
143        let clock =
144            PtpClock::with_provider(ClockAnchor::new(48_000, 0, 0).unwrap(), Box::new(provider));
145        assert_eq!(clock.ptp_now_ns().unwrap(), 7_000_000_000);
146    }
147
148    #[test]
149    fn provider_not_synced_propagates() {
150        struct Unsynced;
151        impl PtpTimeProvider for Unsynced {
152            fn ptp_now_ns(&self) -> Result<i64> {
153                Err(ClockError::NotSynced("ptp master not locked".into()))
154            }
155        }
156        let clock =
157            PtpClock::with_provider(ClockAnchor::new(48_000, 0, 0).unwrap(), Box::new(Unsynced));
158        let err = clock.ptp_now_ns().unwrap_err();
159        assert!(matches!(err, ClockError::NotSynced(_)));
160    }
161
162    #[test]
163    fn dyn_clock_source_object_with_provider() {
164        let clock: Box<dyn ClockSource> = Box::new(PtpClock::unavailable(
165            ClockAnchor::new(48_000, 0, 0).unwrap(),
166        ));
167        // ptp_now_ns errors, but conversions work.
168        assert!(clock.ptp_now_ns().is_err());
169        assert_eq!(clock.sample_to_ptp(0), 0);
170    }
171}