Skip to main content

statime_algo/
lib.rs

1//! General datastructures as defined by the ptp spec
2#![no_std]
3
4#[cfg(feature = "std")]
5extern crate std;
6
7#[cfg(test)]
8macro_rules! assert_almost_eq {
9    ($left:expr, $right:expr) => {
10        match (&$left, &$right) {
11            (left_val, right_val) => {
12                assert!(
13                    (*left_val - *right_val).abs() <= 1e-6 * right_val.abs(),
14                    "Floating point values not almost equal.\nLeft={left_val}\nRight={right_val}"
15                )
16            }
17        }
18    };
19}
20
21#[cfg(test)]
22macro_rules! assert_uv_almost_eq {
23    ($left:expr, $right:expr) => {
24        match (&$left, &$right) {
25            (left_val, right_val) => {
26                assert!((left_val.value - right_val.value).abs() <= 1e-6*right_val.value.abs(),
27                    "Floating point values not almost equal.\nLeft={left_val:?}\nRight={right_val:?}");
28                assert!((left_val.variance - right_val.variance).abs() <= 1e-6*right_val.variance.abs(),
29                    "Floating point uncertainty not almost equal.\nLeft={left_val:?}\nRight={right_val:?}");
30            }
31        }
32    };
33}
34
35mod estimator;
36mod filter;
37#[cfg(not(feature = "std"))]
38mod float_polyfill;
39mod link_noise;
40mod matrix;
41mod ringbuffer;
42mod storage;
43
44use core::marker::PhantomData;
45use statime_base::{
46    Clock, ClockError, ClockId, Controller, DirectedLinkId, Direction, Duration, LeapStatus, Link,
47    LinkId, Measurement, TAI, TimeSnapshot, Timestamp,
48};
49
50use crate::{
51    estimator::UncertainValue,
52    filter::LinkFilter,
53    matrix::MatrixError,
54    storage::{KalmanStorageInternal, StateMutex, SteeredClockStorage},
55};
56
57#[cfg(not(feature = "std"))]
58use crate::float_polyfill::FloatPolyfill;
59
60#[cfg(feature = "std")]
61pub use storage::StdKalmanStorage;
62pub use {
63    filter::ControllerConfig, filter::LinkConfig, filter::TrackedLinkConfig,
64    storage::KalmanStorage, storage::NoAllocKalmanStorage,
65};
66
67/// An error that occured in the kalman controller.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum AlgoError {
70    /// Clock was not known to the controller
71    UnknownClock(ClockId),
72    /// Clock already exists in the estimator or filter state
73    ClockAlreadyExists(ClockId),
74    /// Clock needed a jump beyond the configured limits.
75    JumpAbortLimitExceeded(ClockId),
76    /// Link not found in the estimator or filter state
77    UnknownLink(LinkId),
78    /// Link already exists in the estimator or filter state
79    LinkAlreadyExists(LinkId),
80    /// The link does not contain an external clock
81    LinkNotExternal(LinkId),
82    /// Measurements or links cannot be between two external clocks
83    BothClocksExternal(ClockId, ClockId),
84    /// Got an external clock id in a place where we didn't expect that
85    UnexpectedExternalClock(ClockId),
86    /// Measurements or links between a clock and itself are not allowed
87    ClocksEqual(ClockId),
88    /// Time moved backwards, which is not allowed
89    NonMonotonicTimeProgression {
90        /// The time as was currently known to the algorithm
91        from: Timestamp<TAI>,
92        /// The new time the algorithm was supposed to progress to
93        to: Timestamp<TAI>,
94    },
95    /// Tried to remove the system clock.
96    CannotRemoveSystemClock(ClockId),
97    /// Error from the underlying matrix library
98    MatrixError(MatrixError),
99    /// Error from the underlying clock
100    ClockError(ClockError),
101    /// There are insufficient measurements to provide estimates.
102    NotEnoughMeasurements(LinkId),
103    /// Clock is in use in the given link and cannot be removed
104    ClockInUse(ClockId, LinkId),
105    /// Something went wrong with the calculations in the filter.
106    InternalError,
107}
108
109impl From<MatrixError> for AlgoError {
110    fn from(err: MatrixError) -> Self {
111        AlgoError::MatrixError(err)
112    }
113}
114
115impl From<ClockError> for AlgoError {
116    fn from(err: ClockError) -> Self {
117        AlgoError::ClockError(err)
118    }
119}
120
121/// Threshold set for jumps during clock steering.
122#[derive(Debug, Clone, Default)]
123pub struct JumpThreshold {
124    /// Maximum backwards time jump allowed (must be a positive duration).
125    pub backwards: Option<Duration>,
126    /// Maximum forwards time jump allowed (must be a positive duration).
127    pub forwards: Option<Duration>,
128}
129
130impl JumpThreshold {
131    fn is_within(&self, duration: Duration) -> bool {
132        self.forwards.is_none_or(|v| duration < v) && self.backwards.is_none_or(|v| duration > -v)
133    }
134}
135
136/// Configuration for an internal, steered clock
137#[derive(Debug, Clone)]
138pub struct ClockConfig {
139    /// Threshold for a step at startup to trigger a warning.
140    pub startup_jump_warning_threshold: JumpThreshold,
141    /// Threshold for a step at startup to trigger an abort.
142    pub startup_jump_abort_threshold: JumpThreshold,
143    /// Threshold for a single step during normal operation to trigger a warning.
144    pub single_jump_warning_threshold: JumpThreshold,
145    /// Threshold for a single step during normal operation to trigger an abort.
146    pub single_jump_abort_threshold: JumpThreshold,
147    /// Threshold for accumulated steps to trigger a warning.
148    pub accumulated_jump_warning_threshold: Option<Duration>,
149    /// Threshold for accumulated steps to trigger an abort.
150    pub accumulated_jump_abort_threshold: Option<Duration>,
151    /// Threshold on offset uncertainty after which a clock is considered out of startup
152    pub startup_end_threshold: Duration,
153    /// How large does the offset need to be before we do a jump?
154    pub jump_threshold: Duration,
155    /// How large does a jump need to be to happen, relative to the uncertainty
156    /// of currently observed offset.
157    pub jump_certainty_threshold: f64,
158    /// Amount of time to spread slewing over. This gives a typical time scale
159    /// to reduce the offset error by about two thirds using slews.
160    pub slew_time_constant: Duration,
161    /// Offset and offset uncertainty below which we consider the clock synchronized.
162    pub synchronized_threshold: Duration,
163    /// Initial estimate of the wander of the clock, as fractions of a second per second.
164    pub initial_wander: f64,
165}
166
167impl Default for ClockConfig {
168    fn default() -> Self {
169        Self {
170            startup_jump_warning_threshold: JumpThreshold::default(),
171            startup_jump_abort_threshold: JumpThreshold::default(),
172            single_jump_warning_threshold: JumpThreshold::default(),
173            single_jump_abort_threshold: JumpThreshold::default(),
174            accumulated_jump_warning_threshold: None,
175            accumulated_jump_abort_threshold: None,
176            startup_end_threshold: Duration::from_seconds_nanos(1, 0),
177            jump_threshold: Duration::from_seconds_nanos(10, 0),
178            jump_certainty_threshold: 5.0,
179            slew_time_constant: Duration::from_seconds_nanos(8, 0),
180            synchronized_threshold: Duration::from_seconds_nanos(1, 0),
181            initial_wander: 1e-8,
182        }
183    }
184}
185
186mod hidden {
187    use statime_base::{Clock, ClockId, Duration, LeapStatus, TAI};
188
189    use crate::{
190        ClockConfig,
191        filter::{ControllerConfig, LinkFilter},
192        storage::KalmanStorageInternal,
193    };
194
195    pub struct ClockInfo<C> {
196        pub(crate) id: ClockId,
197        pub(crate) clock: C,
198        pub(crate) config: ClockConfig,
199        pub(crate) in_startup: bool,
200        pub(crate) accumulated_steps: Duration,
201    }
202
203    /// The main controller struct.
204    pub struct KalmanControllerState<Storage: KalmanStorageInternal<C>, C: Clock<TAI>> {
205        pub(crate) clocks: Storage::SteeredClockStorage,
206        pub(crate) filter: LinkFilter<Storage>,
207        pub(crate) filter_config: ControllerConfig,
208        pub(crate) root_delay: Duration,
209        pub(crate) leap_status: Option<LeapStatus>,
210    }
211}
212pub(crate) use hidden::{ClockInfo, KalmanControllerState};
213
214/// Controller for clocks using a kalman filter as its state estimation mechanism.
215pub struct KalmanController<Storage: KalmanStorage<C>, C: Clock<TAI>> {
216    state: Storage::StateMutex,
217}
218
219impl<Storage: KalmanStorage<C>, C: Clock<TAI>> Controller for KalmanController<Storage, C> {
220    type Clock = C;
221    type Link<ControllerRef: AsRef<Self>> = KalmanLink<ControllerRef, Storage, C>;
222    type Error = AlgoError;
223    type ClockConfig = ClockConfig;
224    type LinkConfig = LinkConfig;
225    type TrackedLinkConfig = TrackedLinkConfig;
226
227    /// Add an internal clock to the controller.
228    ///
229    /// # Errors
230    /// Fails if there is insufficient storage available for the new clock.
231    fn add_clock(&self, clock: C, config: ClockConfig) -> Result<ClockId, AlgoError> {
232        self.state.with_mut(|state| {
233            let (filter, id) = state.filter.clone().add_clock(
234                UncertainValue {
235                    value: 0.0,
236                    variance: 1e18,
237                },
238                UncertainValue {
239                    value: 0.0,
240                    variance: clock.max_frequency()?.powi(2),
241                },
242                config.initial_wander,
243            )?;
244            state.filter = filter;
245            state.clocks.push(ClockInfo {
246                id,
247                clock,
248                config,
249                in_startup: true,
250                accumulated_steps: Duration::ZERO,
251            });
252            log::debug!("Created new clock with id {id:?}");
253            Ok(id)
254        })
255    }
256
257    /// Remove a clock from the controller
258    ///
259    /// # Errors
260    /// Fails if the clock is not known to the controller, if the clock is the
261    /// system clock or if the clock is in use in a link.
262    fn remove_clock(&self, clock_id: ClockId) -> Result<(), AlgoError> {
263        self.state.with_mut(|state| {
264            if state.clocks[0].id == clock_id {
265                return Err(AlgoError::CannotRemoveSystemClock(clock_id));
266            }
267
268            let Some(index) = state.clocks.iter().position(|info| info.id == clock_id) else {
269                return Err(AlgoError::UnknownClock(clock_id));
270            };
271            state.filter = state.filter.clone().remove_clock(clock_id)?;
272            state.clocks.remove(index);
273            log::debug!("Removed clock {clock_id:?}");
274            Ok(())
275        })
276    }
277
278    /// Create a measurement link between clocks where the delay and noise from
279    /// the link itself are automatically determined.
280    ///
281    /// The resulting link will need measurements in both directions to succesfully
282    /// work.
283    ///
284    /// This is an associated function instead of a method to allow links to store
285    /// references to the controller in a manner most convenient for the user.
286    ///
287    /// # Errors
288    /// Fails if the clocks are not known to the controller, both external, or
289    /// when they are the same.
290    fn create_tracked_link<ControllerRef: AsRef<KalmanController<Storage, C>>>(
291        // We can't have a normal self parameter here as we want a generic
292        // reference-like thing for the link to store, and we can't use other
293        // types as self. See the comments on KalmanLink for the motivation for
294        // generic reference-like things.
295        this: ControllerRef,
296        clock_a: ClockId,
297        clock_b: Option<ClockId>,
298        config: LinkConfig,
299        tracked_config: TrackedLinkConfig,
300    ) -> Result<KalmanLink<ControllerRef, Storage, C>, AlgoError> {
301        let link_id = this.as_ref().state.with_mut(|state| {
302            let (filter, id) =
303                state
304                    .filter
305                    .clone()
306                    .add_tracked_link(clock_a, clock_b, config, tracked_config)?;
307            state.filter = filter;
308
309            Ok::<_, AlgoError>(id)
310        })?;
311
312        log::debug!("Created tracked link: {link_id:?}");
313
314        Ok(KalmanLink {
315            link_id,
316            controller: this,
317            phantomdata: PhantomData,
318        })
319    }
320
321    /// Create a measurement link between clocks without delay estimation.
322    ///
323    /// This is an associated function instead of a method to allow links to store
324    /// references to the controller in a manner most convenient for the user.
325    ///
326    /// # Errors
327    /// Fails if the clocks are not known to the controller, both external, or
328    /// when they are the same.
329    fn create_untracked_link<ControllerRef: AsRef<KalmanController<Storage, C>>>(
330        // We can't have a normal self parameter here as we want a generic
331        // reference-like thing for the link to store, and we can't use other
332        // types as self. See the comments on KalmanLink for the motivation for
333        // generic reference-like things.
334        this: ControllerRef,
335        clock_a: ClockId,
336        clock_b: Option<ClockId>,
337        config: LinkConfig,
338    ) -> Result<KalmanLink<ControllerRef, Storage, C>, AlgoError> {
339        let link_id = this.as_ref().state.with_mut(|state| {
340            let (filter, id) = state
341                .filter
342                .clone()
343                .add_untracked_link(clock_a, clock_b, config)?;
344            state.filter = filter;
345
346            Ok::<_, AlgoError>(id)
347        })?;
348
349        log::debug!("Created untracked link: {link_id:?}");
350
351        Ok(KalmanLink {
352            link_id,
353            controller: this,
354            phantomdata: PhantomData,
355        })
356    }
357
358    fn clock_snapshot(&self, clock: ClockId) -> Result<statime_base::TimeSnapshot, Self::Error> {
359        self.state.with_ref(|state| {
360            Ok(TimeSnapshot {
361                // FIXME: Augment with jump status once that is available.
362                root_delay: state.root_delay,
363                leap_indicator: state.leap_status,
364                ..state.filter.clock_snapshot(clock)?
365            })
366        })
367    }
368
369    async fn run<Fut: Future<Output = ()> + Send, F: Send + Fn(core::time::Duration) -> Fut>(
370        this: impl AsRef<Self> + Send,
371        sleep: F,
372    ) -> Result<(), Self::Error> {
373        loop {
374            this.as_ref()
375                .state
376                .with_mut(KalmanControllerState::steer_clocks)?;
377            sleep(core::time::Duration::from_secs(1)).await;
378        }
379    }
380}
381
382#[cfg(feature = "std")]
383impl<Storage: KalmanStorage<C>, C: Clock<TAI>> statime_base::StdController
384    for KalmanController<Storage, C>
385{
386    fn active_links(&self) -> std::vec::Vec<statime_base::ActiveLinkData> {
387        self.state.with_ref(|state| state.filter.active_links())
388    }
389}
390
391impl<Storage: KalmanStorage<C>, C: Clock<TAI>> KalmanController<Storage, C> {
392    /// Create a new clock controller
393    ///
394    /// # Errors
395    /// Fails if the provided system clock is not readable.
396    pub fn new(
397        system_clock: C,
398        system_clock_config: ClockConfig,
399        filter_config: ControllerConfig,
400    ) -> Result<(Self, ClockId), AlgoError> {
401        let start_time = system_clock.now()?;
402        let (filter, id) = LinkFilter::empty(start_time).add_clock(
403            UncertainValue {
404                value: 0.0,
405                variance: 1e18,
406            },
407            UncertainValue {
408                value: 0.0,
409                variance: system_clock.max_frequency()?.powi(2),
410            },
411            system_clock_config.initial_wander,
412        )?;
413        let mut clocks = Storage::SteeredClockStorage::new();
414        clocks.push(ClockInfo {
415            id,
416            clock: system_clock,
417            config: system_clock_config,
418            in_startup: true,
419            accumulated_steps: Duration::ZERO,
420        });
421        log::debug!("New controller with system clock {id:?}");
422        Ok((
423            Self {
424                state: Storage::StateMutex::new(KalmanControllerState {
425                    clocks,
426                    filter,
427                    filter_config,
428                    root_delay: Duration::ZERO,
429                    leap_status: None,
430                }),
431            },
432            id,
433        ))
434    }
435
436    /// Get the current offset of a clock.
437    ///
438    /// # Errors
439    /// Fails when the clock is not known to the filter.
440    pub fn clock_offset(&self, clock_id: ClockId) -> Result<UncertainValue, AlgoError> {
441        self.state
442            .with_ref(|state| state.filter.clock_offset(clock_id))
443    }
444
445    /// Get the current frequency offset of a clock.
446    ///
447    /// # Errors
448    /// Fails when the clock is not known to the filter.
449    pub fn clock_frequency(&self, clock_id: ClockId) -> Result<UncertainValue, AlgoError> {
450        self.state
451            .with_ref(|state| state.filter.clock_frequency(clock_id))
452    }
453}
454
455impl<C> ClockInfo<C> {
456    fn check_jump(&mut self, step: Duration) -> Result<(), AlgoError> {
457        if self.in_startup {
458            let warn = !self.config.startup_jump_warning_threshold.is_within(step);
459            let abort = !self.config.startup_jump_abort_threshold.is_within(step);
460
461            if abort {
462                log::error!(
463                    "Jump on clock {clock:?} during startup too big: {step}, aborting.",
464                    clock = self.id
465                );
466                return Err(AlgoError::JumpAbortLimitExceeded(self.id));
467            } else if warn {
468                log::warn!(
469                    "Large jump on clock {clock:?} during startup: {step}. This may confuse applications or be an indication of trouble. To disable this warning, change the startup-jump-warning-threshold for this clock to \"inf\".",
470                    clock = self.id
471                );
472            }
473        } else {
474            let warn_instant = !self.config.single_jump_warning_threshold.is_within(step);
475            let abort_instant = !self.config.single_jump_abort_threshold.is_within(step);
476
477            let old_accumulated_steps = self.accumulated_steps;
478            self.accumulated_steps += step.abs();
479
480            let warn_accumulated =
481                self.config
482                    .accumulated_jump_warning_threshold
483                    .is_some_and(|threshold| {
484                        old_accumulated_steps < threshold && self.accumulated_steps >= threshold
485                    });
486            let abort_accumulated = self
487                .config
488                .accumulated_jump_abort_threshold
489                .is_some_and(|threshold| self.accumulated_steps >= threshold);
490
491            if abort_instant {
492                log::error!(
493                    "Jump on clock {clock:?} too big: {step}, aborting.",
494                    clock = self.id
495                );
496                return Err(AlgoError::JumpAbortLimitExceeded(self.id));
497            } else if abort_accumulated {
498                log::error!(
499                    "Total amount jumped on clock {clock:?} too big: {total_steps}, aborting.",
500                    clock = self.id,
501                    total_steps = self.accumulated_steps
502                );
503                return Err(AlgoError::JumpAbortLimitExceeded(self.id));
504            } else if warn_instant {
505                log::warn!(
506                    "Large jump on clock {clock:?}: {step}. This may confuse applications or be an indication of trouble. To disable this warning, change the single-jump-warning-threshold to \"inf\".",
507                    clock = self.id
508                );
509            } else if warn_accumulated {
510                if self.config.accumulated_jump_abort_threshold.is_some() {
511                    log::warn!(
512                        "Large total amount of jumping on clock {clock:?}: {total_steps}, an abort can occur if jumping continues.",
513                        clock = self.id,
514                        total_steps = self.accumulated_steps
515                    );
516                } else {
517                    log::warn!(
518                        "Large total amount of jumping on clock {clock:?}: {total_steps}. (Note: this warning can trigger multiple times, the amount jumped is reset after each warning)",
519                        clock = self.id,
520                        total_steps = self.accumulated_steps,
521                    );
522                    self.accumulated_steps = Duration::ZERO;
523                }
524            }
525        }
526
527        // We have jumped, and so definitionally are out of startup.
528        self.in_startup = false;
529
530        Ok(())
531    }
532}
533
534impl<Storage: KalmanStorageInternal<C>, C: Clock<TAI>> KalmanControllerState<Storage, C> {
535    fn steer_clocks(&mut self) -> Result<(), AlgoError> {
536        let mut filter = self
537            .filter
538            .clone()
539            .progress_time(self.clocks[0].clock.now()?)?;
540
541        self.leap_status = self.filter.leap_vote(&self.filter_config);
542        if let Some(root_delay) = self.filter.local_root_delay(&self.filter_config) {
543            self.root_delay = Duration::from_f64_seconds(root_delay);
544        }
545
546        for (index, clock_info) in self.clocks.iter_mut().enumerate() {
547            let offset_estimate = self.filter.clock_offset(clock_info.id)?;
548            let offset = offset_estimate.value;
549            let offset_uncertainty = offset_estimate.uncertainty();
550
551            if offset > clock_info.config.jump_threshold.as_seconds()
552                && offset > clock_info.config.jump_certainty_threshold * offset_uncertainty
553            {
554                let step = Duration::from_f64_seconds(-offset);
555                clock_info.check_jump(step)?;
556                log::trace!(
557                    "Clock {:?}: {offset} +- {offset_uncertainty} (jumping {step})",
558                    clock_info.id
559                );
560                clock_info.clock.step_clock(step)?;
561                if index == 0 {
562                    filter = filter.absorb_system_clock_offset_change(
563                        clock_info.id,
564                        Duration::from_f64_seconds(-offset),
565                    )?;
566                } else {
567                    filter = filter.absorb_offset_change(clock_info.id, -offset)?;
568                }
569            } else {
570                let frequence_data = self.filter.clock_frequency(clock_info.id)?;
571                let frequency = frequence_data.value;
572
573                let cur_frequency_steer = clock_info.clock.get_frequency()?;
574                let max_frequency_steer = clock_info.clock.max_frequency()?;
575
576                let wanted_frequency_steer = cur_frequency_steer
577                    - frequency
578                    - offset / clock_info.config.slew_time_constant.as_seconds();
579
580                let actual_frequency_steer =
581                    wanted_frequency_steer.clamp(-max_frequency_steer, max_frequency_steer);
582                // FIXME: Warn here on repeated clamping.
583
584                log::trace!(
585                    "Clock {:?}: {offset} +- {offset_uncertainty} ({frequency} +- {} s/s, steer: {actual_frequency_steer}, wants {wanted_frequency_steer})",
586                    clock_info.id,
587                    frequence_data.uncertainty()
588                );
589
590                clock_info.clock.set_frequency(actual_frequency_steer)?;
591                filter = filter.absorb_frequency_steer(
592                    clock_info.id,
593                    actual_frequency_steer - cur_frequency_steer,
594                )?;
595            }
596
597            if clock_info.in_startup
598                && offset_uncertainty < clock_info.config.startup_end_threshold.as_seconds()
599            {
600                clock_info.in_startup = false;
601            }
602
603            if let Some(leap_status) = self.leap_status {
604                clock_info.clock.leap_update(leap_status)?;
605            }
606            clock_info.clock.synchronization_update(
607                offset < clock_info.config.synchronized_threshold.as_seconds()
608                    && offset_uncertainty < clock_info.config.synchronized_threshold.as_seconds(),
609            )?;
610            clock_info.clock.error_estimate_update(
611                Duration::from_f64_seconds(offset_uncertainty),
612                self.root_delay,
613            )?;
614        }
615
616        self.filter = filter;
617
618        Ok(())
619    }
620}
621
622/// A measurement link between two clocks, managed by a `KalmanController`.
623pub struct KalmanLink<
624    ControllerRef: AsRef<KalmanController<Storage, C>>,
625    Storage: KalmanStorage<C>,
626    C: Clock<TAI>,
627> {
628    link_id: LinkId,
629    // We use a generic reference to the kalman controller here to avoid
630    // forced inclusions of lifetimes in this type. This allows a link to
631    // be based on for example an Arc<KalmanController<M>> when std is
632    // available, whilst keeping &KalmanController<M> available as option
633    // for embedded platforms where things like arc don't exist.
634    controller: ControllerRef,
635    phantomdata: PhantomData<(Storage, C)>,
636}
637
638impl<ControllerRef: AsRef<KalmanController<Storage, C>>, Storage: KalmanStorage<C>, C: Clock<TAI>>
639    Drop for KalmanLink<ControllerRef, Storage, C>
640{
641    fn drop(&mut self) {
642        self.controller.as_ref().state.with_mut(|state| {
643            match state.filter.clone().remove_link(self.link_id) {
644                Ok(filter) => state.filter = filter,
645                Err(error) => {
646                    log::warn!("Internal error, please open a bug report: Could not properly dispose of link: {error:?}");
647                }
648            }
649        });
650    }
651}
652
653impl<ControllerRef: AsRef<KalmanController<Storage, C>>, Storage: KalmanStorage<C>, C: Clock<TAI>>
654    Link for KalmanLink<ControllerRef, Storage, C>
655{
656    type Error = AlgoError;
657
658    fn measurement(&self, measurement: Measurement, direction: Direction) -> Result<(), AlgoError> {
659        self.controller.as_ref().state.with_mut(|state| {
660            log::debug!(
661                "New measurement on link {:?} {direction:?}: {measurement:?}",
662                self.link_id
663            );
664
665            state.filter = state
666                .filter
667                .clone()
668                .progress_time(state.clocks[0].clock.now()?)?;
669
670            state.filter = state.filter.clone().measurement(
671                &state.filter_config,
672                DirectedLinkId::new(self.link_id, direction),
673                UncertainValue {
674                    value: (measurement.recv_timestamp - measurement.send_timestamp).as_seconds(),
675                    variance: measurement.uncertainty.as_seconds().powi(2),
676                },
677                state.clocks[0].id,
678            )?;
679            Ok(())
680        })
681    }
682
683    fn external_data_update(
684        &self,
685        root_delay: Duration,
686        leap_status: Option<LeapStatus>,
687        usable: bool,
688    ) -> Result<(), AlgoError> {
689        self.controller.as_ref().state.with_mut(|state| {
690            state.filter = state.filter.clone().external_data_update(
691                self.link_id,
692                root_delay.as_seconds(),
693                leap_status,
694                usable,
695            )?;
696            Ok(())
697        })
698    }
699
700    fn active(&self) -> Result<bool, AlgoError> {
701        self.controller
702            .as_ref()
703            .state
704            .with_ref(|state| state.filter.link_active(self.link_id))
705    }
706
707    fn desired_poll_interval(&self) -> Result<Duration, AlgoError> {
708        self.controller
709            .as_ref()
710            .state
711            .with_ref(|state| state.filter.link_desired_poll_interval(self.link_id))
712    }
713
714    fn importance(&self) -> Result<Option<f64>, Self::Error> {
715        self.controller
716            .as_ref()
717            .state
718            .with_ref(|state| state.filter.link_importance(self.link_id))
719    }
720
721    fn id(&self) -> LinkId {
722        self.link_id
723    }
724}