1use alloc::collections::BTreeMap;
11use alloc::string::{String, ToString};
12use alloc::vec::Vec;
13use core::mem::size_of;
14
15use crate::router::{Router, encode_slice_le};
16use crate::{
17 DataEndpoint, DataType, TelemetryError, TelemetryResult, config::runtime_device_identifier,
18 message_meta, packet::Packet,
19};
20
21pub const TIMESYNC_ANNOUNCE_WORDS: usize = 2;
23pub const TIMESYNC_REQUEST_WORDS: usize = 2;
25pub const TIMESYNC_RESPONSE_WORDS: usize = 4;
27pub const INTERNAL_TIMESYNC_SOURCE_ID: &str = "__timesync_remote__";
29pub const LOCAL_TIMESYNC_FULL_SOURCE_ID: &str = "__timesync_local_full__";
31pub const LOCAL_TIMESYNC_DATE_SOURCE_ID: &str = "__timesync_local_date__";
33pub const LOCAL_TIMESYNC_TOD_SOURCE_ID: &str = "__timesync_local_tod__";
35pub const LOCAL_TIMESYNC_SUBSEC_SOURCE_ID: &str = "__timesync_local_subsec__";
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum TimeSyncRole {
41 Consumer,
43 Source,
45 Auto,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct TimeSyncConfig {
52 pub role: TimeSyncRole,
54 pub priority: u64,
56 pub source_timeout_ms: u64,
58 pub announce_interval_ms: u64,
60 pub request_interval_ms: u64,
62 pub consumer_promotion_enabled: bool,
64 pub max_slew_ppm: u32,
66}
67
68impl Default for TimeSyncConfig {
69 fn default() -> Self {
70 Self {
71 role: TimeSyncRole::Consumer,
72 priority: 100,
73 source_timeout_ms: 5_000,
74 announce_interval_ms: 1_000,
75 request_interval_ms: 1_000,
76 consumer_promotion_enabled: true,
77 max_slew_ppm: 50_000,
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum TimeSyncLeader {
85 Local { priority: u64 },
87 Remote(TimeSyncSource),
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct TimeSyncSource {
94 pub sender: String,
96 pub priority: u64,
98 pub last_announce_ms: u64,
100 pub last_time_ms: u64,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum TimeSyncUpdate {
107 NoChange,
109 SourceChanged,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct TimeSyncRequestFields {
116 pub seq: u64,
118 pub t1_ms: u64,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct TimeSyncResponseFields {
125 pub seq: u64,
127 pub t1_ms: u64,
129 pub t2_ms: u64,
131 pub t3_ms: u64,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct TimeSyncAnnounceFields {
138 pub priority: u64,
140 pub time_ms: u64,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct TimeSyncSample {
147 pub offset_ms: i64,
149 pub delay_ms: u64,
151}
152
153#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
155pub struct PartialNetworkTime {
156 pub year: Option<i32>,
158 pub month: Option<u8>,
160 pub day: Option<u8>,
162 pub hour: Option<u8>,
164 pub minute: Option<u8>,
166 pub second: Option<u8>,
168 pub nanosecond: Option<u32>,
170}
171
172impl PartialNetworkTime {
173 pub fn from_unix_ms(unix_ms: u64) -> Self {
175 let unix_ns = (unix_ms as i128) * 1_000_000;
176 if let Some(full) = NetworkTime::from_unix_ns(unix_ns) {
177 Self::from(full)
178 } else {
179 Self::default()
180 }
181 }
182
183 pub fn is_complete_date(&self) -> bool {
185 self.year.is_some() && self.month.is_some() && self.day.is_some()
186 }
187
188 pub fn is_complete_time(&self) -> bool {
190 self.hour.is_some() && self.minute.is_some() && self.second.is_some()
191 }
192
193 pub fn to_network_time(&self) -> Option<NetworkTime> {
195 Some(NetworkTime {
196 year: self.year?,
197 month: self.month?,
198 day: self.day?,
199 hour: self.hour?,
200 minute: self.minute?,
201 second: self.second?,
202 nanosecond: self.nanosecond.unwrap_or(0),
203 })
204 }
205}
206
207impl From<NetworkTime> for PartialNetworkTime {
208 fn from(value: NetworkTime) -> Self {
209 Self {
210 year: Some(value.year),
211 month: Some(value.month),
212 day: Some(value.day),
213 hour: Some(value.hour),
214 minute: Some(value.minute),
215 second: Some(value.second),
216 nanosecond: Some(value.nanosecond),
217 }
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct NetworkTime {
224 pub year: i32,
226 pub month: u8,
228 pub day: u8,
230 pub hour: u8,
232 pub minute: u8,
234 pub second: u8,
236 pub nanosecond: u32,
238}
239
240impl NetworkTime {
241 pub fn from_unix_ns(unix_ns: i128) -> Option<Self> {
243 if unix_ns < 0 {
244 return None;
245 }
246
247 let secs = u64::try_from(unix_ns / 1_000_000_000).ok()?;
248 let nanos = unix_ns.rem_euclid(1_000_000_000) as u32;
249 let days = secs / 86_400;
250 let sod = (secs % 86_400) as u32;
251 let (year, month, day) = civil_from_days(days)?;
252
253 Some(Self {
254 year,
255 month: month as u8,
256 day: day as u8,
257 hour: (sod / 3_600) as u8,
258 minute: ((sod % 3_600) / 60) as u8,
259 second: (sod % 60) as u8,
260 nanosecond: nanos,
261 })
262 }
263
264 pub fn as_unix_ns(&self) -> Option<i128> {
266 if !(1..=12).contains(&self.month)
267 || !(1..=31).contains(&self.day)
268 || self.hour > 23
269 || self.minute > 59
270 || self.second > 59
271 || self.nanosecond >= 1_000_000_000
272 {
273 return None;
274 }
275
276 let days = days_from_civil(self.year, self.month as u32, self.day as u32);
277 let secs = (days as i128) * 86_400
278 + (self.hour as i128) * 3_600
279 + (self.minute as i128) * 60
280 + (self.second as i128);
281 Some(secs * 1_000_000_000 + self.nanosecond as i128)
282 }
283
284 pub fn as_unix_ms(&self) -> Option<u64> {
286 let unix_ns = self.as_unix_ns()?;
287 if unix_ns < 0 {
288 return None;
289 }
290 u64::try_from(unix_ns / 1_000_000).ok()
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct NetworkTimeReading {
297 pub time: PartialNetworkTime,
299 pub unix_time_ms: Option<u64>,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub struct SlewedNetworkClock {
306 anchor_mono_ns: u64,
307 anchor_unix_ns: i128,
308 pending_adjust_ns: i128,
309 max_slew_ppm: u32,
310 initialized: bool,
311}
312
313impl Default for SlewedNetworkClock {
314 fn default() -> Self {
315 Self {
316 anchor_mono_ns: 0,
317 anchor_unix_ns: 0,
318 pending_adjust_ns: 0,
319 max_slew_ppm: 50_000,
320 initialized: false,
321 }
322 }
323}
324
325impl SlewedNetworkClock {
326 pub fn new(max_slew_ppm: u32) -> Self {
328 Self {
329 max_slew_ppm: max_slew_ppm.min(999_999),
330 ..Default::default()
331 }
332 }
333
334 pub fn is_initialized(&self) -> bool {
336 self.initialized
337 }
338
339 pub fn read_unix_ns(&self, now_mono_ns: u64) -> Option<i128> {
341 if !self.initialized {
342 return None;
343 }
344
345 let elapsed_ns = now_mono_ns.saturating_sub(self.anchor_mono_ns) as i128;
346 let max_adjust_ns = elapsed_ns.saturating_mul(self.max_slew_ppm as i128) / 1_000_000;
347 let applied_adjust_ns = if self.pending_adjust_ns >= 0 {
348 self.pending_adjust_ns.min(max_adjust_ns)
349 } else {
350 -(-self.pending_adjust_ns).min(max_adjust_ns)
351 };
352
353 Some(
354 self.anchor_unix_ns
355 .saturating_add(elapsed_ns)
356 .saturating_add(applied_adjust_ns),
357 )
358 }
359
360 pub fn read_unix_ms(&self, now_mono_ns: u64) -> Option<u64> {
362 let unix_ns = self.read_unix_ns(now_mono_ns)?;
363 if unix_ns < 0 {
364 return None;
365 }
366 u64::try_from(unix_ns / 1_000_000).ok()
367 }
368
369 pub fn steer_unix_ms(&mut self, now_mono_ns: u64, target_unix_ms: u64) {
371 let target_unix_ns = (target_unix_ms as i128) * 1_000_000;
372 if !self.initialized {
373 self.anchor_mono_ns = now_mono_ns;
374 self.anchor_unix_ns = target_unix_ns;
375 self.pending_adjust_ns = 0;
376 self.initialized = true;
377 return;
378 }
379
380 let current_unix_ns = self.read_unix_ns(now_mono_ns).unwrap_or(target_unix_ns);
381 let elapsed_ns = now_mono_ns.saturating_sub(self.anchor_mono_ns) as i128;
382 let max_adjust_ns = elapsed_ns.saturating_mul(self.max_slew_ppm as i128) / 1_000_000;
383 let applied_adjust_ns = if self.pending_adjust_ns >= 0 {
384 self.pending_adjust_ns.min(max_adjust_ns)
385 } else {
386 -(-self.pending_adjust_ns).min(max_adjust_ns)
387 };
388 let remaining_adjust_ns = self.pending_adjust_ns.saturating_sub(applied_adjust_ns);
389
390 self.anchor_mono_ns = now_mono_ns;
391 self.anchor_unix_ns = current_unix_ns;
392 self.pending_adjust_ns =
393 remaining_adjust_ns.saturating_add(target_unix_ns.saturating_sub(current_unix_ns));
394 self.initialized = true;
395 }
396}
397
398#[derive(Debug, Clone)]
399struct NetworkTimeSourceState {
400 priority: u64,
401 updated_mono_ms: u64,
402 anchor_mono_ns: u64,
403 ttl_ms: Option<u64>,
404 time: PartialNetworkTime,
405}
406
407#[derive(Debug, Clone, Default)]
409pub struct NetworkClock {
410 sources: BTreeMap<String, NetworkTimeSourceState>,
411}
412
413impl NetworkClock {
414 pub fn update_source(
416 &mut self,
417 source: &str,
418 priority: u64,
419 time: PartialNetworkTime,
420 updated_mono_ms: u64,
421 anchor_mono_ns: u64,
422 ttl_ms: Option<u64>,
423 ) {
424 self.sources.insert(
425 source.to_string(),
426 NetworkTimeSourceState {
427 priority,
428 updated_mono_ms,
429 anchor_mono_ns,
430 ttl_ms,
431 time,
432 },
433 );
434 }
435
436 pub fn remove_source(&mut self, source: &str) {
438 self.sources.remove(source);
439 }
440
441 pub fn prune_expired(&mut self, now_mono_ms: u64) {
443 self.sources.retain(|_, src| {
444 src.ttl_ms
445 .map(|ttl| now_mono_ms.saturating_sub(src.updated_mono_ms) <= ttl)
446 .unwrap_or(true)
447 });
448 }
449
450 pub fn current_time(&self, now_mono_ns: u64) -> Option<NetworkTimeReading> {
452 let sources: Vec<(&str, &NetworkTimeSourceState)> = self
453 .sources
454 .iter()
455 .map(|(name, src)| (name.as_str(), src))
456 .collect();
457 if sources.is_empty() {
458 return None;
459 }
460
461 let full = best_source(&sources, |src| src.time.to_network_time().is_some());
462 if let Some((_, src)) = full
463 && let Some(base) = src.time.to_network_time()
464 {
465 let elapsed_ns = now_mono_ns.saturating_sub(src.anchor_mono_ns);
466 if let Some(advanced) = advance_network_time(base, elapsed_ns) {
467 return Some(NetworkTimeReading {
468 unix_time_ms: advanced.as_unix_ms(),
469 time: PartialNetworkTime::from(advanced),
470 });
471 }
472 }
473
474 let year = best_source(&sources, |src| src.time.year.is_some()).map(|(_, src)| src);
475 let month = best_source(&sources, |src| src.time.month.is_some()).map(|(_, src)| src);
476 let day = best_source(&sources, |src| src.time.day.is_some()).map(|(_, src)| src);
477 let hour = best_source(&sources, |src| src.time.hour.is_some()).map(|(_, src)| src);
478 let minute = best_source(&sources, |src| src.time.minute.is_some()).map(|(_, src)| src);
479 let second = best_source(&sources, |src| src.time.second.is_some()).map(|(_, src)| src);
480 let subsec = best_source(&sources, |src| src.time.nanosecond.is_some()).map(|(_, src)| src);
481
482 let mut merged = PartialNetworkTime {
483 year: year.and_then(|src| src.time.year),
484 month: month.and_then(|src| src.time.month),
485 day: day.and_then(|src| src.time.day),
486 hour: hour.and_then(|src| src.time.hour),
487 minute: minute.and_then(|src| src.time.minute),
488 second: second.and_then(|src| src.time.second),
489 ..Default::default()
490 };
491 if let Some(src) = subsec {
492 merged.nanosecond = src.time.nanosecond;
493 }
494
495 if let Some(time_src) = second.or(minute).or(hour)
496 && merged.is_complete_date()
497 && merged.is_complete_time()
498 {
499 merged.nanosecond = merged.nanosecond.or(Some(0));
500 let base = merged.to_network_time()?;
501 let elapsed_ns = now_mono_ns.saturating_sub(time_src.anchor_mono_ns);
502 if let Some(advanced) = advance_network_time(base, elapsed_ns) {
503 return Some(NetworkTimeReading {
504 unix_time_ms: advanced.as_unix_ms(),
505 time: PartialNetworkTime::from(advanced),
506 });
507 }
508 }
509
510 Some(NetworkTimeReading {
511 unix_time_ms: None,
512 time: merged,
513 })
514 }
515}
516
517fn best_source<'a, F>(
518 sources: &'a [(&'a str, &'a NetworkTimeSourceState)],
519 predicate: F,
520) -> Option<(&'a str, &'a NetworkTimeSourceState)>
521where
522 F: Fn(&NetworkTimeSourceState) -> bool,
523{
524 let mut best: Option<(&str, &NetworkTimeSourceState)> = None;
525 for (name, src) in sources.iter().copied() {
526 if !predicate(src) {
527 continue;
528 }
529 best = match best {
530 None => Some((name, src)),
531 Some((best_name, best_src)) => {
532 if src.priority < best_src.priority
533 || (src.priority == best_src.priority && name < best_name)
534 {
535 Some((name, src))
536 } else {
537 Some((best_name, best_src))
538 }
539 }
540 };
541 }
542 best
543}
544
545pub(crate) fn advance_network_time(base: NetworkTime, elapsed_ns: u64) -> Option<NetworkTime> {
547 let unix_ns = base.as_unix_ns()?;
548 NetworkTime::from_unix_ns(unix_ns + elapsed_ns as i128)
549}
550
551fn days_from_civil(year: i32, month: u32, day: u32) -> i64 {
552 let mut era = year.div_euclid(400);
553 let mut yoe = year.rem_euclid(400) - (month <= 2) as i32;
554 if yoe < 0 {
555 era -= 1;
556 yoe += 400;
557 }
558 let mp = month as i32 + if month > 2 { -3 } else { 9 };
559 let doy = (153 * mp + 2) / 5 + day as i32 - 1;
560 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
561 era as i64 * 146_097 + doe as i64 - 719_468
562}
563
564fn civil_from_days(mut z: u64) -> Option<(i32, u32, u32)> {
565 z += 719_468;
566 let era = z / 146_097;
567 let doe = (z - era * 146_097) as u32;
571 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
572 let y = yoe as u64 + era * 400;
573 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
574 let mp = (5 * doy + 2) / 153;
575 let d = doy - (153 * mp + 2) / 5 + 1;
576 let m = if mp < 10 { mp + 3 } else { mp - 9 };
577 let y = i32::try_from(y + (m <= 2) as u64).ok()?;
578 Some((y, m, d))
579}
580
581#[derive(Debug, Clone)]
583pub struct TimeSyncTracker {
584 cfg: TimeSyncConfig,
585 local_id: String,
586 sources: BTreeMap<String, TimeSyncSource>,
587 current_source: Option<String>,
588}
589
590impl TimeSyncTracker {
591 pub fn new(cfg: TimeSyncConfig) -> Self {
593 Self {
594 cfg,
595 local_id: runtime_device_identifier(),
596 sources: BTreeMap::new(),
597 current_source: None,
598 }
599 }
600
601 pub fn config(&self) -> TimeSyncConfig {
603 self.cfg
604 }
605
606 pub fn current_source(&self) -> Option<&TimeSyncSource> {
608 self.current_source
609 .as_ref()
610 .and_then(|sender| self.sources.get(sender))
611 }
612
613 pub fn refresh(&mut self, now_ms: u64) -> TimeSyncUpdate {
615 self.sources
616 .retain(|_, src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms));
617 self.reselect_source(now_ms)
618 }
619
620 pub fn best_active_source(&self, now_ms: u64) -> Option<&TimeSyncSource> {
622 self.sources
623 .values()
624 .filter(|src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms))
625 .min_by(|a, b| {
626 a.priority
627 .cmp(&b.priority)
628 .then_with(|| a.sender.cmp(&b.sender))
629 })
630 }
631
632 pub fn local_candidate_priority(&self, now_ms: u64, has_usable_time: bool) -> Option<u64> {
634 match self.cfg.role {
635 TimeSyncRole::Consumer => {
636 if has_usable_time
637 && self.cfg.consumer_promotion_enabled
638 && self.best_active_source(now_ms).is_none()
639 {
640 Some(self.cfg.priority)
641 } else {
642 None
643 }
644 }
645 TimeSyncRole::Source => Some(self.cfg.priority),
646 TimeSyncRole::Auto => {
647 if has_usable_time && self.best_active_source(now_ms).is_none() {
648 Some(self.cfg.priority)
649 } else {
650 None
651 }
652 }
653 }
654 }
655
656 pub fn leader(&self, now_ms: u64, has_usable_time: bool) -> Option<TimeSyncLeader> {
658 let local_priority = self.local_candidate_priority(now_ms, has_usable_time);
659 let remote = self.best_active_source(now_ms).cloned();
660 match (local_priority, remote) {
661 (Some(priority), Some(remote)) => {
662 if priority < remote.priority
663 || (priority == remote.priority
664 && self.local_id.as_str() < remote.sender.as_str())
665 {
666 Some(TimeSyncLeader::Local { priority })
667 } else {
668 Some(TimeSyncLeader::Remote(remote))
669 }
670 }
671 (Some(priority), None) => Some(TimeSyncLeader::Local { priority }),
672 (None, Some(remote)) => Some(TimeSyncLeader::Remote(remote)),
673 (None, None) => None,
674 }
675 }
676
677 pub fn local_announce_priority(&self, now_ms: u64, has_usable_time: bool) -> Option<u64> {
679 let Some(TimeSyncLeader::Local { priority }) = self.leader(now_ms, has_usable_time) else {
680 return None;
681 };
682 let tie_exists = self
683 .sources
684 .values()
685 .filter(|src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms))
686 .any(|src| src.priority == priority);
687 Some(if tie_exists {
688 priority.saturating_sub(1)
689 } else {
690 priority
691 })
692 }
693
694 pub fn should_announce(&self, now_ms: u64, has_usable_time: bool) -> bool {
696 self.local_announce_priority(now_ms, has_usable_time)
697 .is_some()
698 }
699
700 pub fn should_serve(&self, now_ms: u64, has_usable_time: bool) -> bool {
702 self.should_announce(now_ms, has_usable_time)
703 }
704
705 pub fn handle_announce(
707 &mut self,
708 pkt: &Packet,
709 recv_ms: u64,
710 ) -> TelemetryResult<TimeSyncUpdate> {
711 let ann = decode_timesync_announce(pkt)?;
712 if pkt.sender() == self.local_id.as_str() {
713 return Ok(TimeSyncUpdate::NoChange);
714 }
715
716 let incoming = TimeSyncSource {
717 sender: pkt.sender().to_string(),
718 priority: ann.priority,
719 last_announce_ms: recv_ms,
720 last_time_ms: ann.time_ms,
721 };
722 self.sources.insert(incoming.sender.clone(), incoming);
723 Ok(self.reselect_source(recv_ms))
724 }
725
726 pub fn is_source_active(&self, now_ms: u64) -> bool {
728 self.current_source()
729 .map(|s| Self::source_is_active(s, now_ms, self.cfg.source_timeout_ms))
730 .unwrap_or(false)
731 }
732
733 fn source_is_active(src: &TimeSyncSource, now_ms: u64, timeout_ms: u64) -> bool {
734 now_ms.saturating_sub(src.last_announce_ms) <= timeout_ms
735 }
736
737 fn best_active_source_id(&self, now_ms: u64) -> Option<String> {
738 self.best_active_source(now_ms)
739 .map(|src| src.sender.clone())
740 }
741
742 fn reselect_source(&mut self, now_ms: u64) -> TimeSyncUpdate {
743 let prev = self.current_source.clone();
744 self.current_source = self.best_active_source_id(now_ms);
745 if self.current_source == prev {
746 TimeSyncUpdate::NoChange
747 } else {
748 TimeSyncUpdate::SourceChanged
749 }
750 }
751}
752
753pub fn compute_offset_delay(t1_ms: u64, t2_ms: u64, t3_ms: u64, t4_ms: u64) -> TimeSyncSample {
755 let t1 = t1_ms as i128;
756 let t2 = t2_ms as i128;
757 let t3 = t3_ms as i128;
758 let t4 = t4_ms as i128;
759
760 let offset = ((t2 - t1) + (t3 - t4)) / 2;
761 let delay = (t4 - t1) - (t3 - t2);
762 let delay_ms = if delay < 0 { 0 } else { delay as u64 };
763
764 TimeSyncSample {
765 offset_ms: offset as i64,
766 delay_ms,
767 }
768}
769
770pub fn compute_network_time_sample(
772 t1_monotonic_ms: u64,
773 t2_network_ms: u64,
774 t3_network_ms: u64,
775 t4_monotonic_ms: u64,
776) -> (u64, u64) {
777 let round_trip = t4_monotonic_ms.saturating_sub(t1_monotonic_ms);
778 let server_processing = t3_network_ms.saturating_sub(t2_network_ms);
779 let one_way_delay = round_trip.saturating_sub(server_processing) / 2;
780 let estimated_network_ms = t3_network_ms.saturating_add(one_way_delay);
781 (estimated_network_ms, one_way_delay)
782}
783
784pub fn build_timesync_announce(priority: u64, time_ms: u64) -> TelemetryResult<Packet> {
786 let meta = message_meta(DataType::TimeSyncAnnounce);
787 Packet::from_u64_slice(
788 DataType::TimeSyncAnnounce,
789 &[priority, time_ms],
790 meta.endpoints_ref(),
791 time_ms,
792 )
793}
794
795pub fn build_timesync_announce_with_sender(
797 sender: &'static str,
798 priority: u64,
799 time_ms: u64,
800) -> TelemetryResult<Packet> {
801 let payload = encode_slice_le(&[priority, time_ms]);
802 Packet::new(
803 DataType::TimeSyncAnnounce,
804 &[DataEndpoint::TimeSync],
805 sender,
806 time_ms,
807 payload,
808 )
809}
810
811pub fn send_timesync_announce(router: &Router, priority: u64, time_ms: u64) -> TelemetryResult<()> {
813 router.log_ts(DataType::TimeSyncAnnounce, time_ms, &[priority, time_ms])
814}
815
816pub fn build_timesync_request(seq: u64, t1_ms: u64) -> TelemetryResult<Packet> {
818 let meta = message_meta(DataType::TimeSyncRequest);
819 Packet::from_u64_slice(
820 DataType::TimeSyncRequest,
821 &[seq, t1_ms],
822 meta.endpoints_ref(),
823 t1_ms,
824 )
825}
826
827pub fn send_timesync_request(router: &Router, seq: u64, t1_ms: u64) -> TelemetryResult<()> {
829 router.log_ts(DataType::TimeSyncRequest, t1_ms, &[seq, t1_ms])
830}
831
832pub fn build_timesync_response(
834 seq: u64,
835 t1_ms: u64,
836 t2_ms: u64,
837 t3_ms: u64,
838) -> TelemetryResult<Packet> {
839 let meta = message_meta(DataType::TimeSyncResponse);
840 Packet::from_u64_slice(
841 DataType::TimeSyncResponse,
842 &[seq, t1_ms, t2_ms, t3_ms],
843 meta.endpoints_ref(),
844 t3_ms,
845 )
846}
847
848pub fn send_timesync_response(
850 router: &Router,
851 seq: u64,
852 t1_ms: u64,
853 t2_ms: u64,
854 t3_ms: u64,
855) -> TelemetryResult<()> {
856 router.log_ts(
857 DataType::TimeSyncResponse,
858 t3_ms,
859 &[seq, t1_ms, t2_ms, t3_ms],
860 )
861}
862
863fn decode_u64_payload(
864 pkt: &Packet,
865 expected_ty: DataType,
866 expected_words: usize,
867) -> TelemetryResult<Vec<u64>> {
868 if pkt.data_type() != expected_ty {
869 return Err(TelemetryError::InvalidType);
870 }
871
872 let vals = pkt.data_as_u64()?;
873 if vals.len() != expected_words {
874 return Err(TelemetryError::SizeMismatch {
875 expected: expected_words * size_of::<u64>(),
876 got: vals.len() * size_of::<u64>(),
877 });
878 }
879
880 Ok(vals)
881}
882
883pub fn decode_timesync_announce(pkt: &Packet) -> TelemetryResult<TimeSyncAnnounceFields> {
885 let vals = decode_u64_payload(pkt, DataType::TimeSyncAnnounce, TIMESYNC_ANNOUNCE_WORDS)?;
886 Ok(TimeSyncAnnounceFields {
887 priority: vals[0],
888 time_ms: vals[1],
889 })
890}
891
892pub fn decode_timesync_request(pkt: &Packet) -> TelemetryResult<TimeSyncRequestFields> {
894 let vals = decode_u64_payload(pkt, DataType::TimeSyncRequest, TIMESYNC_REQUEST_WORDS)?;
895 Ok(TimeSyncRequestFields {
896 seq: vals[0],
897 t1_ms: vals[1],
898 })
899}
900
901pub fn decode_timesync_response(pkt: &Packet) -> TelemetryResult<TimeSyncResponseFields> {
903 let vals = decode_u64_payload(pkt, DataType::TimeSyncResponse, TIMESYNC_RESPONSE_WORDS)?;
904 Ok(TimeSyncResponseFields {
905 seq: vals[0],
906 t1_ms: vals[1],
907 t2_ms: vals[2],
908 t3_ms: vals[3],
909 })
910}
911
912#[cfg(test)]
913mod calendar_tests {
914 use super::NetworkTime;
915
916 #[test]
917 fn calendar_roundtrips_era_and_year_boundaries() {
918 for year in [1970, 1999, 2000, 2024, 2100, 2400, 9999, i32::MAX] {
919 for (month, day) in [(1, 1), (2, 28), (3, 1), (12, 31)] {
920 let expected = NetworkTime {
921 year,
922 month,
923 day,
924 hour: 23,
925 minute: 59,
926 second: 59,
927 nanosecond: 999_999_999,
928 };
929 assert_eq!(
930 NetworkTime::from_unix_ns(expected.as_unix_ns().unwrap()),
931 Some(expected)
932 );
933 }
934 }
935 let leap = NetworkTime {
936 year: 2024,
937 month: 2,
938 day: 29,
939 hour: 0,
940 minute: 0,
941 second: 0,
942 nanosecond: 123,
943 };
944 assert_eq!(
945 NetworkTime::from_unix_ns(leap.as_unix_ns().unwrap()),
946 Some(leap)
947 );
948 assert_eq!(NetworkTime::from_unix_ns(0).unwrap().year, 1970);
949 }
950
951 #[test]
952 fn out_of_range_calendar_timestamps_are_rejected_without_wraparound() {
953 assert_eq!(NetworkTime::from_unix_ns(-1), None);
954 assert_eq!(NetworkTime::from_unix_ns(i128::MAX), None);
955 let last = NetworkTime {
956 year: i32::MAX,
957 month: 12,
958 day: 31,
959 hour: 23,
960 minute: 59,
961 second: 59,
962 nanosecond: 999_999_999,
963 }
964 .as_unix_ns()
965 .unwrap();
966 assert_eq!(NetworkTime::from_unix_ns(last + 1), None);
967 }
968}