1#![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#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum AlgoError {
70 UnknownClock(ClockId),
72 ClockAlreadyExists(ClockId),
74 JumpAbortLimitExceeded(ClockId),
76 UnknownLink(LinkId),
78 LinkAlreadyExists(LinkId),
80 LinkNotExternal(LinkId),
82 BothClocksExternal(ClockId, ClockId),
84 UnexpectedExternalClock(ClockId),
86 ClocksEqual(ClockId),
88 NonMonotonicTimeProgression {
90 from: Timestamp<TAI>,
92 to: Timestamp<TAI>,
94 },
95 CannotRemoveSystemClock(ClockId),
97 MatrixError(MatrixError),
99 ClockError(ClockError),
101 NotEnoughMeasurements(LinkId),
103 ClockInUse(ClockId, LinkId),
105 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#[derive(Debug, Clone, Default)]
123pub struct JumpThreshold {
124 pub backwards: Option<Duration>,
126 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#[derive(Debug, Clone)]
138pub struct ClockConfig {
139 pub startup_jump_warning_threshold: JumpThreshold,
141 pub startup_jump_abort_threshold: JumpThreshold,
143 pub single_jump_warning_threshold: JumpThreshold,
145 pub single_jump_abort_threshold: JumpThreshold,
147 pub accumulated_jump_warning_threshold: Option<Duration>,
149 pub accumulated_jump_abort_threshold: Option<Duration>,
151 pub startup_end_threshold: Duration,
153 pub jump_threshold: Duration,
155 pub jump_certainty_threshold: f64,
158 pub slew_time_constant: Duration,
161 pub synchronized_threshold: Duration,
163 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 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
214pub 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 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 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 fn create_tracked_link<ControllerRef: AsRef<KalmanController<Storage, C>>>(
291 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 fn create_untracked_link<ControllerRef: AsRef<KalmanController<Storage, C>>>(
330 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 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 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 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 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 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 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
622pub struct KalmanLink<
624 ControllerRef: AsRef<KalmanController<Storage, C>>,
625 Storage: KalmanStorage<C>,
626 C: Clock<TAI>,
627> {
628 link_id: LinkId,
629 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}