statime/
ptp_instance.rs

1use core::{
2    cell::RefCell,
3    marker::PhantomData,
4    sync::atomic::{AtomicI8, Ordering},
5};
6
7use rand::Rng;
8
9#[allow(unused_imports)]
10use crate::float_polyfill::FloatPolyfill;
11use crate::{
12    bmc::{acceptable_master::AcceptableMasterList, bmca::Bmca},
13    clock::Clock,
14    config::{ClockQuality, InstanceConfig, PortConfig},
15    datastructures::{
16        common::PortIdentity,
17        datasets::{
18            InternalCurrentDS, InternalDefaultDS, InternalParentDS, PathTraceDS, TimePropertiesDS,
19        },
20    },
21    filters::{Filter, FilterEstimate},
22    observability::{current::CurrentDS, default::DefaultDS, parent::ParentDS},
23    port::{InBmca, Port},
24    time::Duration,
25};
26
27/// A PTP node.
28///
29/// This object handles the complete running of the PTP protocol once created.
30/// It provides all the logic for both ordinary and boundary clock mode.
31///
32/// # Example
33///
34/// ```no_run
35/// # struct MockClock;
36/// # impl statime::Clock for MockClock {
37/// #     type Error = ();
38/// #     fn now(&self) -> statime::time::Time {
39/// #         unimplemented!()
40/// #     }
41/// #     fn step_clock(&mut self, _: statime::time::Duration) -> Result<statime::time::Time, Self::Error> {
42/// #         unimplemented!()
43/// #     }
44/// #     fn set_frequency(&mut self, _: f64) -> Result<statime::time::Time, Self::Error> {
45/// #         unimplemented!()
46/// #     }
47/// #     fn set_properties(&mut self, _: &TimePropertiesDS) -> Result<(), Self::Error> {
48/// #         unimplemented!()
49/// #     }
50/// # }
51/// # mod system {
52/// #     pub fn get_mac() -> [u8; 6] { unimplemented!() }
53/// #     pub fn sleep(time: core::time::Duration) { unimplemented!() }
54/// # }
55/// # let port_config: statime::config::PortConfig<AcceptAnyMaster> = unimplemented!();
56/// # let filter_config = unimplemented!();
57/// # let clock: MockClock = unimplemented!();
58/// # let rng: rand::rngs::mock::StepRng = unimplemented!();
59/// #
60/// use statime::PtpInstance;
61/// use statime::config::{AcceptAnyMaster, ClockIdentity, ClockQuality, InstanceConfig, TimePropertiesDS, TimeSource};
62/// use statime::filters::BasicFilter;
63///
64/// let instance_config = InstanceConfig {
65///     clock_identity: ClockIdentity::from_mac_address(system::get_mac()),
66///     priority_1: 128,
67///     priority_2: 128,
68///     domain_number: 0,
69///     slave_only: false,
70///     sdo_id: Default::default(),
71///     path_trace: false,
72///     clock_quality: ClockQuality::default(),
73/// };
74/// let time_properties_ds = TimePropertiesDS::new_arbitrary_time(false, false, TimeSource::InternalOscillator);
75///
76/// let mut instance = PtpInstance::<BasicFilter>::new(
77///     instance_config,
78///     time_properties_ds,
79/// );
80///
81/// let mut port = instance.add_port(port_config, filter_config, clock, rng);
82///
83/// // Send of port to its own thread/task to do its work
84///
85/// loop {
86///     instance.bmca(&mut [&mut port]);
87///     system::sleep(instance.bmca_interval());
88/// }
89/// ```
90pub struct PtpInstance<F, S = RefCell<PtpInstanceState>> {
91    state: S,
92    log_bmca_interval: AtomicI8,
93    _filter: PhantomData<F>,
94}
95
96/// The inner state of a [`PtpInstance`]
97#[derive(Debug)]
98pub struct PtpInstanceState {
99    pub(crate) default_ds: InternalDefaultDS,
100    pub(crate) current_ds: InternalCurrentDS,
101    pub(crate) parent_ds: InternalParentDS,
102    pub(crate) path_trace_ds: PathTraceDS,
103    pub(crate) time_properties_ds: TimePropertiesDS,
104}
105
106impl PtpInstanceState {
107    fn bmca<A: AcceptableMasterList, C: Clock, F: Filter, R: Rng, S: PtpInstanceStateMutex>(
108        &mut self,
109        ports: &mut [&mut Port<'_, InBmca, A, R, C, F, S>],
110        bmca_interval: Duration,
111    ) {
112        debug_assert_eq!(self.default_ds.number_ports as usize, ports.len());
113
114        for port in ports.iter_mut() {
115            port.calculate_best_local_announce_message()
116        }
117
118        let ebest = Bmca::<()>::find_best_announce_message(
119            ports
120                .iter()
121                .filter_map(|port| port.best_local_announce_message_for_bmca()),
122        );
123
124        for port in ports.iter_mut() {
125            let recommended_state = Bmca::<()>::calculate_recommended_state(
126                &self.default_ds,
127                ebest,
128                port.best_local_announce_message_for_state(), // erbest
129                port.state(),
130            );
131
132            log::debug!(
133                "Recommended state port {}: {recommended_state:?}",
134                port.number(),
135            );
136
137            if let Some(recommended_state) = recommended_state {
138                port.set_recommended_state(
139                    recommended_state,
140                    &mut self.path_trace_ds,
141                    &mut self.time_properties_ds,
142                    &mut self.current_ds,
143                    &mut self.parent_ds,
144                    &self.default_ds,
145                );
146            }
147        }
148
149        // And update announce message ages
150        for port in ports.iter_mut() {
151            port.step_announce_age(bmca_interval);
152        }
153    }
154}
155
156impl<F, S: PtpInstanceStateMutex> PtpInstance<F, S> {
157    /// Construct a new [`PtpInstance`] with the given config and time
158    /// properties
159    pub fn new(config: InstanceConfig, time_properties_ds: TimePropertiesDS) -> Self {
160        let default_ds = InternalDefaultDS::new(config);
161
162        Self {
163            state: S::new(PtpInstanceState {
164                default_ds,
165                current_ds: Default::default(),
166                parent_ds: InternalParentDS::new(default_ds),
167                path_trace_ds: PathTraceDS::new(config.path_trace),
168                time_properties_ds,
169            }),
170            log_bmca_interval: AtomicI8::new(i8::MAX),
171            _filter: PhantomData,
172        }
173    }
174
175    /// Return IEEE-1588 defaultDS for introspection
176    pub fn default_ds(&self) -> DefaultDS {
177        self.state.with_ref(|s| (&s.default_ds).into())
178    }
179
180    /// Return IEEE-1588 currentDS for introspection
181    pub fn current_ds(&self, port_contribution: Option<FilterEstimate>) -> CurrentDS {
182        self.state
183            .with_ref(|s| CurrentDS::from_state(&s.current_ds, port_contribution))
184    }
185
186    /// Return IEEE-1588 parentDS for introspection
187    pub fn parent_ds(&self) -> ParentDS {
188        self.state.with_ref(|s| (&s.parent_ds).into())
189    }
190
191    /// Return IEEE-1588 timePropertiesDS for introspection
192    pub fn time_properties_ds(&self) -> TimePropertiesDS {
193        self.state.with_ref(|s| s.time_properties_ds)
194    }
195
196    /// Return IEEE-1588 pathTraceDS for introspection
197    pub fn path_trace_ds(&self) -> PathTraceDS {
198        self.state.with_ref(|s| s.path_trace_ds.clone())
199    }
200}
201
202impl<F: Filter, S: PtpInstanceStateMutex> PtpInstance<F, S> {
203    /// Add and initialize this port
204    ///
205    /// We start in the BMCA state because that is convenient
206    ///
207    /// When providing the port with a different clock than the instance clock,
208    /// the caller is responsible for propagating any property changes to this
209    /// clock, and for synchronizing this clock with the instance clock as
210    /// appropriate based on the ports state.
211    pub fn add_port<A, C, R: Rng>(
212        &self,
213        config: PortConfig<A>,
214        filter_config: F::Config,
215        clock: C,
216        rng: R,
217    ) -> Port<'_, InBmca, A, R, C, F, S> {
218        self.log_bmca_interval
219            .fetch_min(config.announce_interval.as_log_2(), Ordering::Relaxed);
220        let port_identity = self.state.with_mut(|state| {
221            state.default_ds.number_ports += 1;
222            PortIdentity {
223                clock_identity: state.default_ds.clock_identity,
224                port_number: state.default_ds.number_ports,
225            }
226        });
227
228        Port::new(
229            &self.state,
230            config,
231            filter_config,
232            clock,
233            port_identity,
234            rng,
235        )
236    }
237
238    /// Run the best master clock algorithm (BMCA)
239    ///
240    /// The caller must pass all the ports that were created on this instance in
241    /// the slice!
242    pub fn bmca<A: AcceptableMasterList, C: Clock, R: Rng>(
243        &self,
244        ports: &mut [&mut Port<'_, InBmca, A, R, C, F, S>],
245    ) {
246        self.state.with_mut(|state| {
247            state.bmca(
248                ports,
249                Duration::from_seconds(
250                    2f64.powi(self.log_bmca_interval.load(Ordering::Relaxed) as i32),
251                ),
252            );
253        });
254    }
255
256    /// Time to wait between calls to [`PtpInstance::bmca`]
257    pub fn bmca_interval(&self) -> core::time::Duration {
258        core::time::Duration::from_secs_f64(
259            2f64.powi(self.log_bmca_interval.load(Ordering::Relaxed) as i32),
260        )
261    }
262
263    /// Set the clock quality of the instance
264    ///
265    /// This allows changing the clock quality after the instance has been created.
266    /// The change will be reflected in all subsequent PTP messages sent by this instance
267    /// and the BMCA algorithm will use the new clock quality.
268    pub fn set_clock_quality(&self, clock_quality: ClockQuality) {
269        self.state.with_mut(|state| {
270            state.default_ds.clock_quality = clock_quality;
271        });
272    }
273}
274
275/// A mutex over a [`PtpInstanceState`]
276///
277/// This provides an abstraction for locking state in various environments.
278/// Implementations are provided for [`core::cell::RefCell`] and
279/// [`std::sync::RwLock`].
280pub trait PtpInstanceStateMutex {
281    /// Creates a new instance of the mutex
282    fn new(state: PtpInstanceState) -> Self;
283
284    /// Takes a shared reference to the contained state and calls `f` with it
285    fn with_ref<R, F: FnOnce(&PtpInstanceState) -> R>(&self, f: F) -> R;
286
287    /// Takes a mutable reference to the contained state and calls `f` with it
288    fn with_mut<R, F: FnOnce(&mut PtpInstanceState) -> R>(&self, f: F) -> R;
289}
290
291impl PtpInstanceStateMutex for RefCell<PtpInstanceState> {
292    fn new(state: PtpInstanceState) -> Self {
293        RefCell::new(state)
294    }
295
296    fn with_ref<R, F: FnOnce(&PtpInstanceState) -> R>(&self, f: F) -> R {
297        f(&RefCell::borrow(self))
298    }
299
300    fn with_mut<R, F: FnOnce(&mut PtpInstanceState) -> R>(&self, f: F) -> R {
301        f(&mut RefCell::borrow_mut(self))
302    }
303}
304
305#[cfg(feature = "std")]
306impl PtpInstanceStateMutex for std::sync::RwLock<PtpInstanceState> {
307    fn new(state: PtpInstanceState) -> Self {
308        std::sync::RwLock::new(state)
309    }
310
311    fn with_ref<R, F: FnOnce(&PtpInstanceState) -> R>(&self, f: F) -> R {
312        f(&self.read().unwrap())
313    }
314
315    fn with_mut<R, F: FnOnce(&mut PtpInstanceState) -> R>(&self, f: F) -> R {
316        f(&mut self.write().unwrap())
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::{
324        config::{
325            ClockAccuracy, ClockIdentity, ClockQuality, InstanceConfig, TimePropertiesDS,
326            TimeSource,
327        },
328        filters::BasicFilter,
329    };
330
331    fn create_test_instance() -> PtpInstance<BasicFilter> {
332        let config = InstanceConfig {
333            clock_identity: ClockIdentity::from_mac_address([1, 2, 3, 4, 5, 6]),
334            priority_1: 128,
335            priority_2: 128,
336            domain_number: 0,
337            slave_only: false,
338            sdo_id: Default::default(),
339            path_trace: false,
340            clock_quality: ClockQuality::default(),
341        };
342        let time_properties_ds =
343            TimePropertiesDS::new_arbitrary_time(false, false, TimeSource::InternalOscillator);
344        PtpInstance::new(config, time_properties_ds)
345    }
346
347    #[test]
348    fn test_set_clock_quality_basic() {
349        let instance = create_test_instance();
350
351        let default_ds = instance.default_ds();
352        assert_eq!(default_ds.clock_quality.clock_class, 248);
353        assert_eq!(
354            default_ds.clock_quality.clock_accuracy,
355            ClockAccuracy::Unknown
356        );
357
358        // Set custom clock quality and verify that it is applied
359        let custom_quality = ClockQuality {
360            clock_class: 6,
361            clock_accuracy: ClockAccuracy::NS1,
362            offset_scaled_log_variance: 0x8000 - (20 * 256),
363        };
364
365        instance.set_clock_quality(custom_quality);
366
367        let updated_ds = instance.default_ds();
368        assert_eq!(updated_ds.clock_quality.clock_class, 6);
369        assert_eq!(updated_ds.clock_quality.clock_accuracy, ClockAccuracy::NS1);
370        assert_eq!(
371            updated_ds.clock_quality.offset_scaled_log_variance,
372            0x8000 - (20 * 256)
373        );
374    }
375}