1use alloc::boxed::Box;
11use alloc::collections::BTreeMap;
12use alloc::string::String;
13use alloc::vec::Vec;
14use core::mem::size_of;
15
16use crate::router::{Router, encode_slice_le};
17use crate::{
18 DataEndpoint, DataType, TelemetryError, TelemetryResult, config::runtime_device_identifier,
19 message_meta, packet::Packet,
20};
21
22pub const TIMESYNC_ANNOUNCE_WORDS: usize = 2;
24pub const TIMESYNC_REQUEST_WORDS: usize = 2;
26pub const TIMESYNC_RESPONSE_WORDS: usize = 4;
28pub const INTERNAL_TIMESYNC_SOURCE_ID: &str = "__timesync_remote__";
30pub const LOCAL_TIMESYNC_FULL_SOURCE_ID: &str = "__timesync_local_full__";
32pub const LOCAL_TIMESYNC_DATE_SOURCE_ID: &str = "__timesync_local_date__";
34pub const LOCAL_TIMESYNC_TOD_SOURCE_ID: &str = "__timesync_local_tod__";
36pub const LOCAL_TIMESYNC_SUBSEC_SOURCE_ID: &str = "__timesync_local_subsec__";
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum TimeSyncRole {
42 Consumer,
44 Source,
46 Auto,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct TimeSyncConfig {
53 pub role: TimeSyncRole,
55 pub priority: u64,
57 pub source_timeout_ms: u64,
59 pub announce_interval_ms: u64,
61 pub request_interval_ms: u64,
63 pub consumer_promotion_enabled: bool,
65 pub max_slew_ppm: u32,
67}
68
69impl Default for TimeSyncConfig {
70 fn default() -> Self {
71 Self {
72 role: TimeSyncRole::Consumer,
73 priority: 100,
74 source_timeout_ms: 5_000,
75 announce_interval_ms: 1_000,
76 request_interval_ms: 1_000,
77 consumer_promotion_enabled: true,
78 max_slew_ppm: 50_000,
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum TimeSyncLeader {
86 Local { priority: u64 },
88 Remote(TimeSyncSource),
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct TimeSyncSource {
95 pub sender: String,
97 pub priority: u64,
99 pub last_announce_ms: u64,
101 pub last_time_ms: u64,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum TimeSyncUpdate {
108 NoChange,
110 SourceChanged,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct TimeSyncRequestFields {
117 pub seq: u64,
119 pub t1_ms: u64,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct TimeSyncResponseFields {
126 pub seq: u64,
128 pub t1_ms: u64,
130 pub t2_ms: u64,
132 pub t3_ms: u64,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct TimeSyncAnnounceFields {
139 pub priority: u64,
141 pub time_ms: u64,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct TimeSyncSample {
148 pub offset_ms: i64,
150 pub delay_ms: u64,
152}
153
154#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
156pub struct PartialNetworkTime {
157 pub year: Option<i32>,
159 pub month: Option<u8>,
161 pub day: Option<u8>,
163 pub hour: Option<u8>,
165 pub minute: Option<u8>,
167 pub second: Option<u8>,
169 pub nanosecond: Option<u32>,
171}
172
173impl PartialNetworkTime {
174 pub fn from_unix_ms(unix_ms: u64) -> Self {
176 let unix_ns = (unix_ms as i128) * 1_000_000;
177 if let Some(full) = NetworkTime::from_unix_ns(unix_ns) {
178 Self::from(full)
179 } else {
180 Self::default()
181 }
182 }
183
184 pub fn is_complete_date(&self) -> bool {
186 self.year.is_some() && self.month.is_some() && self.day.is_some()
187 }
188
189 pub fn is_complete_time(&self) -> bool {
191 self.hour.is_some() && self.minute.is_some() && self.second.is_some()
192 }
193
194 pub fn to_network_time(&self) -> Option<NetworkTime> {
196 Some(NetworkTime {
197 year: self.year?,
198 month: self.month?,
199 day: self.day?,
200 hour: self.hour?,
201 minute: self.minute?,
202 second: self.second?,
203 nanosecond: self.nanosecond.unwrap_or(0),
204 })
205 }
206}
207
208impl From<NetworkTime> for PartialNetworkTime {
209 fn from(value: NetworkTime) -> Self {
210 Self {
211 year: Some(value.year),
212 month: Some(value.month),
213 day: Some(value.day),
214 hour: Some(value.hour),
215 minute: Some(value.minute),
216 second: Some(value.second),
217 nanosecond: Some(value.nanosecond),
218 }
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub struct NetworkTime {
225 pub year: i32,
227 pub month: u8,
229 pub day: u8,
231 pub hour: u8,
233 pub minute: u8,
235 pub second: u8,
237 pub nanosecond: u32,
239}
240
241impl NetworkTime {
242 pub fn from_unix_ns(unix_ns: i128) -> Option<Self> {
244 if unix_ns < 0 {
245 return None;
246 }
247
248 let secs = u64::try_from(unix_ns / 1_000_000_000).ok()?;
249 let nanos = unix_ns.rem_euclid(1_000_000_000) as u32;
250 let days = secs / 86_400;
251 let sod = (secs % 86_400) as u32;
252 let (year, month, day) = civil_from_days(days)?;
253
254 Some(Self {
255 year,
256 month: month as u8,
257 day: day as u8,
258 hour: (sod / 3_600) as u8,
259 minute: ((sod % 3_600) / 60) as u8,
260 second: (sod % 60) as u8,
261 nanosecond: nanos,
262 })
263 }
264
265 pub fn as_unix_ns(&self) -> Option<i128> {
267 if !(1..=12).contains(&self.month)
268 || !(1..=31).contains(&self.day)
269 || self.hour > 23
270 || self.minute > 59
271 || self.second > 59
272 || self.nanosecond >= 1_000_000_000
273 {
274 return None;
275 }
276
277 let days = days_from_civil(self.year, self.month as u32, self.day as u32);
278 let secs = (days as i128) * 86_400
279 + (self.hour as i128) * 3_600
280 + (self.minute as i128) * 60
281 + (self.second as i128);
282 Some(secs * 1_000_000_000 + self.nanosecond as i128)
283 }
284
285 pub fn as_unix_ms(&self) -> Option<u64> {
287 let unix_ns = self.as_unix_ns()?;
288 if unix_ns < 0 {
289 return None;
290 }
291 u64::try_from(unix_ns / 1_000_000).ok()
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct NetworkTimeReading {
298 pub time: PartialNetworkTime,
300 pub unix_time_ms: Option<u64>,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub struct SlewedNetworkClock {
307 anchor_mono_ns: u64,
308 anchor_unix_ns: i128,
309 pending_adjust_ns: i128,
310 max_slew_ppm: u32,
311 initialized: bool,
312}
313
314impl Default for SlewedNetworkClock {
315 fn default() -> Self {
316 Self {
317 anchor_mono_ns: 0,
318 anchor_unix_ns: 0,
319 pending_adjust_ns: 0,
320 max_slew_ppm: 50_000,
321 initialized: false,
322 }
323 }
324}
325
326impl SlewedNetworkClock {
327 pub fn new(max_slew_ppm: u32) -> Self {
329 Self {
330 max_slew_ppm: max_slew_ppm.min(999_999),
331 ..Default::default()
332 }
333 }
334
335 pub fn is_initialized(&self) -> bool {
337 self.initialized
338 }
339
340 pub fn read_unix_ns(&self, now_mono_ns: u64) -> Option<i128> {
342 if !self.initialized {
343 return None;
344 }
345
346 let elapsed_ns = now_mono_ns.saturating_sub(self.anchor_mono_ns) as i128;
347 let max_adjust_ns = elapsed_ns.saturating_mul(self.max_slew_ppm as i128) / 1_000_000;
348 let applied_adjust_ns = if self.pending_adjust_ns >= 0 {
349 self.pending_adjust_ns.min(max_adjust_ns)
350 } else {
351 -(-self.pending_adjust_ns).min(max_adjust_ns)
352 };
353
354 Some(
355 self.anchor_unix_ns
356 .saturating_add(elapsed_ns)
357 .saturating_add(applied_adjust_ns),
358 )
359 }
360
361 pub fn read_unix_ms(&self, now_mono_ns: u64) -> Option<u64> {
363 let unix_ns = self.read_unix_ns(now_mono_ns)?;
364 if unix_ns < 0 {
365 return None;
366 }
367 u64::try_from(unix_ns / 1_000_000).ok()
368 }
369
370 pub fn steer_unix_ms(&mut self, now_mono_ns: u64, target_unix_ms: u64) {
372 let target_unix_ns = (target_unix_ms as i128) * 1_000_000;
373 if !self.initialized {
374 self.anchor_mono_ns = now_mono_ns;
375 self.anchor_unix_ns = target_unix_ns;
376 self.pending_adjust_ns = 0;
377 self.initialized = true;
378 return;
379 }
380
381 let current_unix_ns = self.read_unix_ns(now_mono_ns).unwrap_or(target_unix_ns);
382 self.anchor_mono_ns = now_mono_ns;
383 self.anchor_unix_ns = current_unix_ns;
384 self.pending_adjust_ns = target_unix_ns.saturating_sub(current_unix_ns);
387 self.initialized = true;
388 }
389}
390
391#[derive(Debug, Clone)]
392struct NetworkTimeSourceState {
393 priority: u64,
394 updated_mono_ms: u64,
395 anchor_mono_ns: u64,
396 ttl_ms: Option<u64>,
397 time: PartialNetworkTime,
398}
399
400pub(crate) struct PreparedClockSource<'a>(crate::peer_record::Prepared<'a, NetworkTimeSourceState>);
401
402pub(crate) struct PreparedTrackerSource<'a> {
403 ignore: bool,
404 sender: &'a str,
405 priority: u64,
406 recv_ms: u64,
407 time_ms: u64,
408 insert: Option<crate::peer_record::Prepared<'a, TimeSyncSource>>,
409}
410
411#[derive(Debug, Clone, Default)]
413pub struct NetworkClock {
414 sources: BTreeMap<String, Box<NetworkTimeSourceState>>,
415}
416
417impl NetworkClock {
418 pub fn update_source(
420 &mut self,
421 source: &str,
422 priority: u64,
423 time: PartialNetworkTime,
424 updated_mono_ms: u64,
425 anchor_mono_ns: u64,
426 ttl_ms: Option<u64>,
427 ) {
428 let _ = self.try_update_source(
431 source,
432 priority,
433 time,
434 updated_mono_ms,
435 anchor_mono_ns,
436 ttl_ms,
437 );
438 }
439
440 pub fn try_update_source(
442 &mut self,
443 source: &str,
444 priority: u64,
445 time: PartialNetworkTime,
446 updated_mono_ms: u64,
447 anchor_mono_ns: u64,
448 ttl_ms: Option<u64>,
449 ) -> TelemetryResult<()> {
450 let update = self.prepare_source(
451 source,
452 priority,
453 time,
454 updated_mono_ms,
455 anchor_mono_ns,
456 ttl_ms,
457 )?;
458 self.commit_source(update);
459 Ok(())
460 }
461
462 pub(crate) fn prepare_source<'a>(
463 &self,
464 source: &'a str,
465 priority: u64,
466 time: PartialNetworkTime,
467 updated_mono_ms: u64,
468 anchor_mono_ns: u64,
469 ttl_ms: Option<u64>,
470 ) -> TelemetryResult<PreparedClockSource<'a>> {
471 crate::peer_record::prepare(
472 &self.sources,
473 source,
474 NetworkTimeSourceState {
475 priority,
476 updated_mono_ms,
477 anchor_mono_ns,
478 ttl_ms,
479 time,
480 },
481 )
482 .map(PreparedClockSource)
483 }
484
485 pub(crate) fn commit_source(&mut self, update: PreparedClockSource<'_>) {
486 update.0.commit(&mut self.sources);
487 }
488
489 pub fn remove_source(&mut self, source: &str) {
491 self.sources.remove(source);
492 }
493
494 pub fn prune_expired(&mut self, now_mono_ms: u64) {
496 self.sources.retain(|_, src| {
497 src.ttl_ms
498 .map(|ttl| now_mono_ms.saturating_sub(src.updated_mono_ms) <= ttl)
499 .unwrap_or(true)
500 });
501 }
502
503 pub fn current_time(&self, now_mono_ns: u64) -> Option<NetworkTimeReading> {
505 let sources: Vec<(&str, &NetworkTimeSourceState)> = self
506 .sources
507 .iter()
508 .map(|(name, src)| (name.as_str(), src.as_ref()))
509 .collect();
510 if sources.is_empty() {
511 return None;
512 }
513
514 let full = best_source(&sources, |src| src.time.to_network_time().is_some());
515 if let Some((_, src)) = full
516 && let Some(base) = src.time.to_network_time()
517 {
518 let elapsed_ns = now_mono_ns.saturating_sub(src.anchor_mono_ns);
519 if let Some(advanced) = advance_network_time(base, elapsed_ns) {
520 return Some(NetworkTimeReading {
521 unix_time_ms: advanced.as_unix_ms(),
522 time: PartialNetworkTime::from(advanced),
523 });
524 }
525 }
526
527 let year = best_source(&sources, |src| src.time.year.is_some()).map(|(_, src)| src);
528 let month = best_source(&sources, |src| src.time.month.is_some()).map(|(_, src)| src);
529 let day = best_source(&sources, |src| src.time.day.is_some()).map(|(_, src)| src);
530 let hour = best_source(&sources, |src| src.time.hour.is_some()).map(|(_, src)| src);
531 let minute = best_source(&sources, |src| src.time.minute.is_some()).map(|(_, src)| src);
532 let second = best_source(&sources, |src| src.time.second.is_some()).map(|(_, src)| src);
533 let subsec = best_source(&sources, |src| src.time.nanosecond.is_some()).map(|(_, src)| src);
534
535 let mut merged = PartialNetworkTime {
536 year: year.and_then(|src| src.time.year),
537 month: month.and_then(|src| src.time.month),
538 day: day.and_then(|src| src.time.day),
539 hour: hour.and_then(|src| src.time.hour),
540 minute: minute.and_then(|src| src.time.minute),
541 second: second.and_then(|src| src.time.second),
542 ..Default::default()
543 };
544 if let Some(src) = subsec {
545 merged.nanosecond = src.time.nanosecond;
546 }
547
548 if let Some(time_src) = second.or(minute).or(hour)
549 && merged.is_complete_date()
550 && merged.is_complete_time()
551 {
552 merged.nanosecond = merged.nanosecond.or(Some(0));
553 let base = merged.to_network_time()?;
554 let elapsed_ns = now_mono_ns.saturating_sub(time_src.anchor_mono_ns);
555 if let Some(advanced) = advance_network_time(base, elapsed_ns) {
556 return Some(NetworkTimeReading {
557 unix_time_ms: advanced.as_unix_ms(),
558 time: PartialNetworkTime::from(advanced),
559 });
560 }
561 }
562
563 Some(NetworkTimeReading {
564 unix_time_ms: None,
565 time: merged,
566 })
567 }
568}
569
570fn best_source<'a, F>(
571 sources: &'a [(&'a str, &'a NetworkTimeSourceState)],
572 predicate: F,
573) -> Option<(&'a str, &'a NetworkTimeSourceState)>
574where
575 F: Fn(&NetworkTimeSourceState) -> bool,
576{
577 let mut best: Option<(&str, &NetworkTimeSourceState)> = None;
578 for (name, src) in sources.iter().copied() {
579 if !predicate(src) {
580 continue;
581 }
582 best = match best {
583 None => Some((name, src)),
584 Some((best_name, best_src)) => {
585 if src.priority < best_src.priority
586 || (src.priority == best_src.priority && name < best_name)
587 {
588 Some((name, src))
589 } else {
590 Some((best_name, best_src))
591 }
592 }
593 };
594 }
595 best
596}
597
598pub(crate) fn advance_network_time(base: NetworkTime, elapsed_ns: u64) -> Option<NetworkTime> {
600 let unix_ns = base.as_unix_ns()?;
601 NetworkTime::from_unix_ns(unix_ns + elapsed_ns as i128)
602}
603
604fn days_from_civil(year: i32, month: u32, day: u32) -> i64 {
605 let mut era = year.div_euclid(400);
606 let mut yoe = year.rem_euclid(400) - (month <= 2) as i32;
607 if yoe < 0 {
608 era -= 1;
609 yoe += 400;
610 }
611 let mp = month as i32 + if month > 2 { -3 } else { 9 };
612 let doy = (153 * mp + 2) / 5 + day as i32 - 1;
613 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
614 era as i64 * 146_097 + doe as i64 - 719_468
615}
616
617fn civil_from_days(mut z: u64) -> Option<(i32, u32, u32)> {
618 z += 719_468;
619 let era = z / 146_097;
620 let doe = (z - era * 146_097) as u32;
624 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
625 let y = yoe as u64 + era * 400;
626 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
627 let mp = (5 * doy + 2) / 153;
628 let d = doy - (153 * mp + 2) / 5 + 1;
629 let m = if mp < 10 { mp + 3 } else { mp - 9 };
630 let y = i32::try_from(y + (m <= 2) as u64).ok()?;
631 Some((y, m, d))
632}
633
634#[derive(Debug, Clone)]
636pub struct TimeSyncTracker {
637 cfg: TimeSyncConfig,
638 local_id: String,
639 sources: BTreeMap<String, Box<TimeSyncSource>>,
640 current_source: Option<String>,
641}
642
643impl TimeSyncTracker {
644 pub fn new(cfg: TimeSyncConfig) -> Self {
646 Self {
647 cfg,
648 local_id: runtime_device_identifier(),
649 sources: BTreeMap::new(),
650 current_source: None,
651 }
652 }
653
654 pub fn config(&self) -> TimeSyncConfig {
656 self.cfg
657 }
658
659 pub fn current_source(&self) -> Option<&TimeSyncSource> {
661 self.current_source
662 .as_ref()
663 .and_then(|sender| self.sources.get(sender).map(AsRef::as_ref))
664 }
665
666 pub fn refresh(&mut self, now_ms: u64) -> TimeSyncUpdate {
668 self.sources
669 .retain(|_, src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms));
670 self.reselect_source(now_ms)
671 }
672
673 pub fn best_active_source(&self, now_ms: u64) -> Option<&TimeSyncSource> {
675 self.sources
676 .values()
677 .filter(|src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms))
678 .min_by(|a, b| {
679 a.priority
680 .cmp(&b.priority)
681 .then_with(|| a.sender.cmp(&b.sender))
682 })
683 .map(AsRef::as_ref)
684 }
685
686 pub fn local_candidate_priority(&self, now_ms: u64, has_usable_time: bool) -> Option<u64> {
688 match self.cfg.role {
689 TimeSyncRole::Consumer => {
690 if has_usable_time
691 && self.cfg.consumer_promotion_enabled
692 && self.best_active_source(now_ms).is_none()
693 {
694 Some(self.cfg.priority)
695 } else {
696 None
697 }
698 }
699 TimeSyncRole::Source => Some(self.cfg.priority),
700 TimeSyncRole::Auto => {
701 if has_usable_time && self.best_active_source(now_ms).is_none() {
702 Some(self.cfg.priority)
703 } else {
704 None
705 }
706 }
707 }
708 }
709
710 pub fn leader(&self, now_ms: u64, has_usable_time: bool) -> Option<TimeSyncLeader> {
712 let local_priority = self.local_candidate_priority(now_ms, has_usable_time);
713 let remote = self.best_active_source(now_ms).cloned();
714 match (local_priority, remote) {
715 (Some(priority), Some(remote)) => {
716 if priority < remote.priority
717 || (priority == remote.priority
718 && self.local_id.as_str() < remote.sender.as_str())
719 {
720 Some(TimeSyncLeader::Local { priority })
721 } else {
722 Some(TimeSyncLeader::Remote(remote))
723 }
724 }
725 (Some(priority), None) => Some(TimeSyncLeader::Local { priority }),
726 (None, Some(remote)) => Some(TimeSyncLeader::Remote(remote)),
727 (None, None) => None,
728 }
729 }
730
731 pub fn local_announce_priority(&self, now_ms: u64, has_usable_time: bool) -> Option<u64> {
733 let Some(TimeSyncLeader::Local { priority }) = self.leader(now_ms, has_usable_time) else {
734 return None;
735 };
736 let tie_exists = self
737 .sources
738 .values()
739 .filter(|src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms))
740 .any(|src| src.priority == priority);
741 Some(if tie_exists {
742 priority.saturating_sub(1)
743 } else {
744 priority
745 })
746 }
747
748 pub fn should_announce(&self, now_ms: u64, has_usable_time: bool) -> bool {
750 self.local_announce_priority(now_ms, has_usable_time)
751 .is_some()
752 }
753
754 pub fn should_serve(&self, now_ms: u64, has_usable_time: bool) -> bool {
756 self.should_announce(now_ms, has_usable_time)
757 }
758
759 pub fn handle_announce(
761 &mut self,
762 pkt: &Packet,
763 recv_ms: u64,
764 ) -> TelemetryResult<TimeSyncUpdate> {
765 let ann = decode_timesync_announce(pkt)?;
766 if pkt.sender() == self.local_id.as_str() {
767 return Ok(TimeSyncUpdate::NoChange);
768 }
769
770 let update = self.prepare_source(pkt.sender(), ann, recv_ms)?;
771 Ok(self.commit_source(update))
772 }
773
774 pub(crate) fn prepare_source<'a>(
775 &self,
776 sender: &'a str,
777 ann: TimeSyncAnnounceFields,
778 recv_ms: u64,
779 ) -> TelemetryResult<PreparedTrackerSource<'a>> {
780 let ignore = sender == self.local_id.as_str();
781 let insert = if ignore || self.sources.contains_key(sender) {
782 None
783 } else {
784 let incoming = TimeSyncSource {
785 sender: crate::peer_record::try_string(sender)?,
786 priority: ann.priority,
787 last_announce_ms: recv_ms,
788 last_time_ms: ann.time_ms,
789 };
790 Some(crate::peer_record::prepare(
791 &self.sources,
792 sender,
793 incoming,
794 )?)
795 };
796 Ok(PreparedTrackerSource {
797 ignore,
798 sender,
799 priority: ann.priority,
800 recv_ms,
801 time_ms: ann.time_ms,
802 insert,
803 })
804 }
805
806 pub(crate) fn commit_source(&mut self, update: PreparedTrackerSource<'_>) -> TimeSyncUpdate {
807 if update.ignore {
808 return TimeSyncUpdate::NoChange;
809 }
810 if let Some(insert) = update.insert {
811 insert.commit(&mut self.sources);
812 } else {
813 let source = self
814 .sources
815 .get_mut(update.sender)
816 .expect("prepared source remains present");
817 source.priority = update.priority;
818 source.last_announce_ms = update.recv_ms;
819 source.last_time_ms = update.time_ms;
820 }
821 self.reselect_source(update.recv_ms)
822 }
823
824 pub fn is_source_active(&self, now_ms: u64) -> bool {
826 self.current_source()
827 .map(|s| Self::source_is_active(s, now_ms, self.cfg.source_timeout_ms))
828 .unwrap_or(false)
829 }
830
831 fn source_is_active(src: &TimeSyncSource, now_ms: u64, timeout_ms: u64) -> bool {
832 now_ms.saturating_sub(src.last_announce_ms) <= timeout_ms
833 }
834
835 fn best_active_source_id(&self, now_ms: u64) -> Option<String> {
836 self.best_active_source(now_ms)
837 .map(|src| src.sender.clone())
838 }
839
840 fn reselect_source(&mut self, now_ms: u64) -> TimeSyncUpdate {
841 let prev = self.current_source.clone();
842 self.current_source = self.best_active_source_id(now_ms);
843 if self.current_source == prev {
844 TimeSyncUpdate::NoChange
845 } else {
846 TimeSyncUpdate::SourceChanged
847 }
848 }
849}
850
851pub fn compute_offset_delay(t1_ms: u64, t2_ms: u64, t3_ms: u64, t4_ms: u64) -> TimeSyncSample {
853 let t1 = t1_ms as i128;
854 let t2 = t2_ms as i128;
855 let t3 = t3_ms as i128;
856 let t4 = t4_ms as i128;
857
858 let offset = ((t2 - t1) + (t3 - t4)) / 2;
859 let delay = (t4 - t1) - (t3 - t2);
860 let delay_ms = if delay < 0 { 0 } else { delay as u64 };
861
862 TimeSyncSample {
863 offset_ms: offset as i64,
864 delay_ms,
865 }
866}
867
868pub fn compute_network_time_sample(
870 t1_monotonic_ms: u64,
871 t2_network_ms: u64,
872 t3_network_ms: u64,
873 t4_monotonic_ms: u64,
874) -> (u64, u64) {
875 let round_trip = t4_monotonic_ms.saturating_sub(t1_monotonic_ms);
876 let server_processing = t3_network_ms.saturating_sub(t2_network_ms);
877 let one_way_delay = round_trip.saturating_sub(server_processing) / 2;
878 let estimated_network_ms = t3_network_ms.saturating_add(one_way_delay);
879 (estimated_network_ms, one_way_delay)
880}
881
882pub fn build_timesync_announce(priority: u64, time_ms: u64) -> TelemetryResult<Packet> {
884 let meta = message_meta(DataType::TimeSyncAnnounce);
885 Packet::from_u64_slice(
886 DataType::TimeSyncAnnounce,
887 &[priority, time_ms],
888 meta.endpoints_ref(),
889 time_ms,
890 )
891}
892
893pub fn build_timesync_announce_with_sender(
895 sender: &'static str,
896 priority: u64,
897 time_ms: u64,
898) -> TelemetryResult<Packet> {
899 let payload = encode_slice_le(&[priority, time_ms]);
900 Packet::new(
901 DataType::TimeSyncAnnounce,
902 &[DataEndpoint::TimeSync],
903 sender,
904 time_ms,
905 payload,
906 )
907}
908
909pub fn send_timesync_announce(router: &Router, priority: u64, time_ms: u64) -> TelemetryResult<()> {
911 router.log_ts(DataType::TimeSyncAnnounce, time_ms, &[priority, time_ms])
912}
913
914pub fn build_timesync_request(seq: u64, t1_ms: u64) -> TelemetryResult<Packet> {
916 let meta = message_meta(DataType::TimeSyncRequest);
917 Packet::from_u64_slice(
918 DataType::TimeSyncRequest,
919 &[seq, t1_ms],
920 meta.endpoints_ref(),
921 t1_ms,
922 )
923}
924
925pub fn send_timesync_request(router: &Router, seq: u64, t1_ms: u64) -> TelemetryResult<()> {
927 router.log_ts(DataType::TimeSyncRequest, t1_ms, &[seq, t1_ms])
928}
929
930pub fn build_timesync_response(
932 seq: u64,
933 t1_ms: u64,
934 t2_ms: u64,
935 t3_ms: u64,
936) -> TelemetryResult<Packet> {
937 let meta = message_meta(DataType::TimeSyncResponse);
938 Packet::from_u64_slice(
939 DataType::TimeSyncResponse,
940 &[seq, t1_ms, t2_ms, t3_ms],
941 meta.endpoints_ref(),
942 t3_ms,
943 )
944}
945
946pub fn send_timesync_response(
948 router: &Router,
949 seq: u64,
950 t1_ms: u64,
951 t2_ms: u64,
952 t3_ms: u64,
953) -> TelemetryResult<()> {
954 router.log_ts(
955 DataType::TimeSyncResponse,
956 t3_ms,
957 &[seq, t1_ms, t2_ms, t3_ms],
958 )
959}
960
961fn decode_u64_payload(
962 pkt: &Packet,
963 expected_ty: DataType,
964 expected_words: usize,
965) -> TelemetryResult<Vec<u64>> {
966 if pkt.data_type() != expected_ty {
967 return Err(TelemetryError::InvalidType);
968 }
969
970 let vals = pkt.data_as_u64()?;
971 if vals.len() != expected_words {
972 return Err(TelemetryError::SizeMismatch {
973 expected: expected_words * size_of::<u64>(),
974 got: vals.len() * size_of::<u64>(),
975 });
976 }
977
978 Ok(vals)
979}
980
981pub fn decode_timesync_announce(pkt: &Packet) -> TelemetryResult<TimeSyncAnnounceFields> {
983 let vals = decode_u64_payload(pkt, DataType::TimeSyncAnnounce, TIMESYNC_ANNOUNCE_WORDS)?;
984 Ok(TimeSyncAnnounceFields {
985 priority: vals[0],
986 time_ms: vals[1],
987 })
988}
989
990pub fn decode_timesync_request(pkt: &Packet) -> TelemetryResult<TimeSyncRequestFields> {
992 let vals = decode_u64_payload(pkt, DataType::TimeSyncRequest, TIMESYNC_REQUEST_WORDS)?;
993 Ok(TimeSyncRequestFields {
994 seq: vals[0],
995 t1_ms: vals[1],
996 })
997}
998
999pub fn decode_timesync_response(pkt: &Packet) -> TelemetryResult<TimeSyncResponseFields> {
1001 let vals = decode_u64_payload(pkt, DataType::TimeSyncResponse, TIMESYNC_RESPONSE_WORDS)?;
1002 Ok(TimeSyncResponseFields {
1003 seq: vals[0],
1004 t1_ms: vals[1],
1005 t2_ms: vals[2],
1006 t3_ms: vals[3],
1007 })
1008}
1009
1010#[cfg(test)]
1011mod calendar_tests {
1012 use super::NetworkTime;
1013
1014 #[test]
1015 fn calendar_roundtrips_era_and_year_boundaries() {
1016 for year in [1970, 1999, 2000, 2024, 2100, 2400, 9999, i32::MAX] {
1017 for (month, day) in [(1, 1), (2, 28), (3, 1), (12, 31)] {
1018 let expected = NetworkTime {
1019 year,
1020 month,
1021 day,
1022 hour: 23,
1023 minute: 59,
1024 second: 59,
1025 nanosecond: 999_999_999,
1026 };
1027 assert_eq!(
1028 NetworkTime::from_unix_ns(expected.as_unix_ns().unwrap()),
1029 Some(expected)
1030 );
1031 }
1032 }
1033 let leap = NetworkTime {
1034 year: 2024,
1035 month: 2,
1036 day: 29,
1037 hour: 0,
1038 minute: 0,
1039 second: 0,
1040 nanosecond: 123,
1041 };
1042 assert_eq!(
1043 NetworkTime::from_unix_ns(leap.as_unix_ns().unwrap()),
1044 Some(leap)
1045 );
1046 assert_eq!(NetworkTime::from_unix_ns(0).unwrap().year, 1970);
1047 }
1048
1049 #[test]
1050 fn out_of_range_calendar_timestamps_are_rejected_without_wraparound() {
1051 assert_eq!(NetworkTime::from_unix_ns(-1), None);
1052 assert_eq!(NetworkTime::from_unix_ns(i128::MAX), None);
1053 let last = NetworkTime {
1054 year: i32::MAX,
1055 month: 12,
1056 day: 31,
1057 hour: 23,
1058 minute: 59,
1059 second: 59,
1060 nanosecond: 999_999_999,
1061 }
1062 .as_unix_ns()
1063 .unwrap();
1064 assert_eq!(NetworkTime::from_unix_ns(last + 1), None);
1065 }
1066}
1067
1068#[cfg(test)]
1069mod slew_absolute_target_tests {
1070 use super::SlewedNetworkClock;
1071
1072 #[test]
1073 fn remote_source_projection_survives_embedded_polling_and_source_updates() {
1074 use super::{TimeSyncConfig, TimeSyncRole, build_timesync_announce_with_sender};
1075 use crate::router::{Router, RouterConfig};
1076 use alloc::sync::Arc;
1077 use core::sync::atomic::{AtomicU64, Ordering};
1078 const EPOCH: u64 = 1_767_225_600_000;
1079 let now = Arc::new(AtomicU64::new(0));
1080 let clock = now.clone();
1081 let router = Router::new_with_clock(
1082 RouterConfig::default().with_timesync(TimeSyncConfig {
1083 role: TimeSyncRole::Consumer,
1084 priority: 50,
1085 source_timeout_ms: 10_000,
1086 consumer_promotion_enabled: false,
1087 ..Default::default()
1088 }),
1089 alloc::boxed::Box::new(move || clock.load(Ordering::SeqCst)),
1090 );
1091 for elapsed_ms in (0..=20_000).step_by(10) {
1092 now.store(elapsed_ms, Ordering::SeqCst);
1093 if elapsed_ms % 5_000 == 0 {
1094 let (source, priority) = if elapsed_ms < 10_000 {
1096 ("SOURCE_A", 2)
1097 } else {
1098 ("SOURCE_B", 1)
1099 };
1100 router
1101 .rx(
1102 &build_timesync_announce_with_sender(source, priority, EPOCH + elapsed_ms)
1103 .unwrap(),
1104 )
1105 .unwrap();
1106 }
1107 for _ in 0..5 {
1108 router.poll_timesync().unwrap();
1109 assert_eq!(router.network_time_ms(), Some(EPOCH + elapsed_ms));
1110 }
1111 }
1112 }
1113
1114 #[test]
1115 fn repeated_absolute_target_does_not_accumulate_phase_error() {
1116 for target in [9_900, 10_100] {
1117 let mut once = SlewedNetworkClock::new(50_000);
1118 once.steer_unix_ms(0, 10_000);
1119 once.steer_unix_ms(0, target);
1120 let mut repeated = once;
1121 for _ in 0..100 {
1122 repeated.steer_unix_ms(0, target);
1123 }
1124 for elapsed_ms in [1, 100, 1_000, 2_000, 3_000, 10_000] {
1125 assert_eq!(
1126 repeated.read_unix_ms(elapsed_ms * 1_000_000),
1127 once.read_unix_ms(elapsed_ms * 1_000_000)
1128 );
1129 }
1130 }
1131 }
1132
1133 #[test]
1134 fn advancing_absolute_targets_converge_with_bounded_monotonic_slew() {
1135 for offset_ms in [-100i64, 100] {
1136 let mut clock = SlewedNetworkClock::new(50_000);
1137 clock.steer_unix_ms(0, 10_000);
1138 let mut previous = 10_000_000_000i128;
1139 for elapsed_ms in 1..=5_000u64 {
1140 let now_ns = elapsed_ms * 1_000_000;
1141 let target_ms = (10_000 + elapsed_ms as i64 + offset_ms) as u64;
1142 clock.steer_unix_ms(now_ns, target_ms);
1143 let current = clock.read_unix_ns(now_ns).unwrap();
1144 assert!((950_000..=1_050_000).contains(&(current - previous)));
1145 previous = current;
1146 }
1147 assert_eq!(
1148 clock.read_unix_ms(5_000_000_000),
1149 Some((15_000 + offset_ms) as u64)
1150 );
1151 }
1152 }
1153}