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::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 = unix_ns.div_euclid(1_000_000_000);
248 let nanos = unix_ns.rem_euclid(1_000_000_000) as u32;
249 let days = secs.div_euclid(86_400) as i64;
250 let sod = secs.rem_euclid(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 year = year - (month <= 2) as i32;
553 let era = if year >= 0 { year } else { year - 399 } / 400;
554 let yoe = year - era * 400;
555 let mp = month as i32 + if month > 2 { -3 } else { 9 };
556 let doy = (153 * mp + 2) / 5 + day as i32 - 1;
557 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
558 (era * 146_097 + doe - 719_468) as i64
559}
560
561fn civil_from_days(mut z: i64) -> (i32, u32, u32) {
562 z += 719_468;
563 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
564 let doe = z - era * 146_097;
565 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
566 let y = yoe as i32 + era as i32 * 400;
567 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
568 let mp = (5 * doy + 2) / 153;
569 let d = doy - (153 * mp + 2) / 5 + 1;
570 let m = mp + if mp < 10 { 3 } else { -9 };
571 let y = y + (m <= 2) as i32;
572 (y, m as u32, d as u32)
573}
574
575#[derive(Debug, Clone)]
577pub struct TimeSyncTracker {
578 cfg: TimeSyncConfig,
579 local_id: &'static str,
580 sources: BTreeMap<String, TimeSyncSource>,
581 current_source: Option<String>,
582}
583
584impl TimeSyncTracker {
585 pub fn new(cfg: TimeSyncConfig) -> Self {
587 Self {
588 cfg,
589 local_id: DEVICE_IDENTIFIER,
590 sources: BTreeMap::new(),
591 current_source: None,
592 }
593 }
594
595 pub fn config(&self) -> TimeSyncConfig {
597 self.cfg
598 }
599
600 pub fn current_source(&self) -> Option<&TimeSyncSource> {
602 self.current_source
603 .as_ref()
604 .and_then(|sender| self.sources.get(sender))
605 }
606
607 pub fn refresh(&mut self, now_ms: u64) -> TimeSyncUpdate {
609 self.sources
610 .retain(|_, src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms));
611 self.reselect_source(now_ms)
612 }
613
614 pub fn best_active_source(&self, now_ms: u64) -> Option<&TimeSyncSource> {
616 self.sources
617 .values()
618 .filter(|src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms))
619 .min_by(|a, b| {
620 a.priority
621 .cmp(&b.priority)
622 .then_with(|| a.sender.cmp(&b.sender))
623 })
624 }
625
626 pub fn local_candidate_priority(&self, now_ms: u64, has_usable_time: bool) -> Option<u64> {
628 match self.cfg.role {
629 TimeSyncRole::Consumer => {
630 if has_usable_time
631 && self.cfg.consumer_promotion_enabled
632 && self.best_active_source(now_ms).is_none()
633 {
634 Some(self.cfg.priority)
635 } else {
636 None
637 }
638 }
639 TimeSyncRole::Source => Some(self.cfg.priority),
640 TimeSyncRole::Auto => {
641 if has_usable_time && self.best_active_source(now_ms).is_none() {
642 Some(self.cfg.priority)
643 } else {
644 None
645 }
646 }
647 }
648 }
649
650 pub fn leader(&self, now_ms: u64, has_usable_time: bool) -> Option<TimeSyncLeader> {
652 let local_priority = self.local_candidate_priority(now_ms, has_usable_time);
653 let remote = self.best_active_source(now_ms).cloned();
654 match (local_priority, remote) {
655 (Some(priority), Some(remote)) => {
656 if priority < remote.priority
657 || (priority == remote.priority && self.local_id < remote.sender.as_str())
658 {
659 Some(TimeSyncLeader::Local { priority })
660 } else {
661 Some(TimeSyncLeader::Remote(remote))
662 }
663 }
664 (Some(priority), None) => Some(TimeSyncLeader::Local { priority }),
665 (None, Some(remote)) => Some(TimeSyncLeader::Remote(remote)),
666 (None, None) => None,
667 }
668 }
669
670 pub fn local_announce_priority(&self, now_ms: u64, has_usable_time: bool) -> Option<u64> {
672 let Some(TimeSyncLeader::Local { priority }) = self.leader(now_ms, has_usable_time) else {
673 return None;
674 };
675 let tie_exists = self
676 .sources
677 .values()
678 .filter(|src| Self::source_is_active(src, now_ms, self.cfg.source_timeout_ms))
679 .any(|src| src.priority == priority);
680 Some(if tie_exists {
681 priority.saturating_sub(1)
682 } else {
683 priority
684 })
685 }
686
687 pub fn should_announce(&self, now_ms: u64, has_usable_time: bool) -> bool {
689 self.local_announce_priority(now_ms, has_usable_time)
690 .is_some()
691 }
692
693 pub fn should_serve(&self, now_ms: u64, has_usable_time: bool) -> bool {
695 self.should_announce(now_ms, has_usable_time)
696 }
697
698 pub fn handle_announce(
700 &mut self,
701 pkt: &Packet,
702 recv_ms: u64,
703 ) -> TelemetryResult<TimeSyncUpdate> {
704 let ann = decode_timesync_announce(pkt)?;
705 if pkt.sender() == self.local_id {
706 return Ok(TimeSyncUpdate::NoChange);
707 }
708
709 let incoming = TimeSyncSource {
710 sender: pkt.sender().to_string(),
711 priority: ann.priority,
712 last_announce_ms: recv_ms,
713 last_time_ms: ann.time_ms,
714 };
715 self.sources.insert(incoming.sender.clone(), incoming);
716 Ok(self.reselect_source(recv_ms))
717 }
718
719 pub fn is_source_active(&self, now_ms: u64) -> bool {
721 self.current_source()
722 .map(|s| Self::source_is_active(s, now_ms, self.cfg.source_timeout_ms))
723 .unwrap_or(false)
724 }
725
726 fn source_is_active(src: &TimeSyncSource, now_ms: u64, timeout_ms: u64) -> bool {
727 now_ms.saturating_sub(src.last_announce_ms) <= timeout_ms
728 }
729
730 fn best_active_source_id(&self, now_ms: u64) -> Option<String> {
731 self.best_active_source(now_ms)
732 .map(|src| src.sender.clone())
733 }
734
735 fn reselect_source(&mut self, now_ms: u64) -> TimeSyncUpdate {
736 let prev = self.current_source.clone();
737 self.current_source = self.best_active_source_id(now_ms);
738 if self.current_source == prev {
739 TimeSyncUpdate::NoChange
740 } else {
741 TimeSyncUpdate::SourceChanged
742 }
743 }
744}
745
746pub fn compute_offset_delay(t1_ms: u64, t2_ms: u64, t3_ms: u64, t4_ms: u64) -> TimeSyncSample {
748 let t1 = t1_ms as i128;
749 let t2 = t2_ms as i128;
750 let t3 = t3_ms as i128;
751 let t4 = t4_ms as i128;
752
753 let offset = ((t2 - t1) + (t3 - t4)) / 2;
754 let delay = (t4 - t1) - (t3 - t2);
755 let delay_ms = if delay < 0 { 0 } else { delay as u64 };
756
757 TimeSyncSample {
758 offset_ms: offset as i64,
759 delay_ms,
760 }
761}
762
763pub fn compute_network_time_sample(
765 t1_monotonic_ms: u64,
766 t2_network_ms: u64,
767 t3_network_ms: u64,
768 t4_monotonic_ms: u64,
769) -> (u64, u64) {
770 let round_trip = t4_monotonic_ms.saturating_sub(t1_monotonic_ms);
771 let server_processing = t3_network_ms.saturating_sub(t2_network_ms);
772 let one_way_delay = round_trip.saturating_sub(server_processing) / 2;
773 let estimated_network_ms = t3_network_ms.saturating_add(one_way_delay);
774 (estimated_network_ms, one_way_delay)
775}
776
777pub fn build_timesync_announce(priority: u64, time_ms: u64) -> TelemetryResult<Packet> {
779 let meta = message_meta(DataType::TimeSyncAnnounce);
780 Packet::from_u64_slice(
781 DataType::TimeSyncAnnounce,
782 &[priority, time_ms],
783 meta.endpoints,
784 time_ms,
785 )
786}
787
788pub fn build_timesync_announce_with_sender(
790 sender: &'static str,
791 priority: u64,
792 time_ms: u64,
793) -> TelemetryResult<Packet> {
794 let payload = encode_slice_le(&[priority, time_ms]);
795 Packet::new(
796 DataType::TimeSyncAnnounce,
797 &[DataEndpoint::TimeSync],
798 sender,
799 time_ms,
800 payload,
801 )
802}
803
804pub fn send_timesync_announce(router: &Router, priority: u64, time_ms: u64) -> TelemetryResult<()> {
806 router.log_ts(DataType::TimeSyncAnnounce, time_ms, &[priority, time_ms])
807}
808
809pub fn build_timesync_request(seq: u64, t1_ms: u64) -> TelemetryResult<Packet> {
811 let meta = message_meta(DataType::TimeSyncRequest);
812 Packet::from_u64_slice(
813 DataType::TimeSyncRequest,
814 &[seq, t1_ms],
815 meta.endpoints,
816 t1_ms,
817 )
818}
819
820pub fn send_timesync_request(router: &Router, seq: u64, t1_ms: u64) -> TelemetryResult<()> {
822 router.log_ts(DataType::TimeSyncRequest, t1_ms, &[seq, t1_ms])
823}
824
825pub fn build_timesync_response(
827 seq: u64,
828 t1_ms: u64,
829 t2_ms: u64,
830 t3_ms: u64,
831) -> TelemetryResult<Packet> {
832 let meta = message_meta(DataType::TimeSyncResponse);
833 Packet::from_u64_slice(
834 DataType::TimeSyncResponse,
835 &[seq, t1_ms, t2_ms, t3_ms],
836 meta.endpoints,
837 t3_ms,
838 )
839}
840
841pub fn send_timesync_response(
843 router: &Router,
844 seq: u64,
845 t1_ms: u64,
846 t2_ms: u64,
847 t3_ms: u64,
848) -> TelemetryResult<()> {
849 router.log_ts(
850 DataType::TimeSyncResponse,
851 t3_ms,
852 &[seq, t1_ms, t2_ms, t3_ms],
853 )
854}
855
856fn decode_u64_payload(
857 pkt: &Packet,
858 expected_ty: DataType,
859 expected_words: usize,
860) -> TelemetryResult<Vec<u64>> {
861 if pkt.data_type() != expected_ty {
862 return Err(TelemetryError::InvalidType);
863 }
864
865 let vals = pkt.data_as_u64()?;
866 if vals.len() != expected_words {
867 return Err(TelemetryError::SizeMismatch {
868 expected: expected_words * size_of::<u64>(),
869 got: vals.len() * size_of::<u64>(),
870 });
871 }
872
873 Ok(vals)
874}
875
876pub fn decode_timesync_announce(pkt: &Packet) -> TelemetryResult<TimeSyncAnnounceFields> {
878 let vals = decode_u64_payload(pkt, DataType::TimeSyncAnnounce, TIMESYNC_ANNOUNCE_WORDS)?;
879 Ok(TimeSyncAnnounceFields {
880 priority: vals[0],
881 time_ms: vals[1],
882 })
883}
884
885pub fn decode_timesync_request(pkt: &Packet) -> TelemetryResult<TimeSyncRequestFields> {
887 let vals = decode_u64_payload(pkt, DataType::TimeSyncRequest, TIMESYNC_REQUEST_WORDS)?;
888 Ok(TimeSyncRequestFields {
889 seq: vals[0],
890 t1_ms: vals[1],
891 })
892}
893
894pub fn decode_timesync_response(pkt: &Packet) -> TelemetryResult<TimeSyncResponseFields> {
896 let vals = decode_u64_payload(pkt, DataType::TimeSyncResponse, TIMESYNC_RESPONSE_WORDS)?;
897 Ok(TimeSyncResponseFields {
898 seq: vals[0],
899 t1_ms: vals[1],
900 t2_ms: vals[2],
901 t3_ms: vals[3],
902 })
903}