1use core::num::NonZeroU8;
55
56use bitflags::bitflags;
57
58use heapless::String;
59
60use embassy_futures::select::select;
61
62use crate::dm::endpoints::ROOT_ENDPOINT_ID;
63use crate::dm::{
64 ArrayAttributeRead, AttrChangeNotifier, Attribute, Cluster, Command, Dataver, EndptId,
65 EventEmitter, HandlerContext, InvokeContext, NodeId, Quality, ReadContext,
66};
67use crate::error::{Error, ErrorCode};
68use crate::persist::{
69 KvBlobStore, KvBlobStoreAccess, Persist, LKG_UTC_KEY, TIME_ZONE_KEY, TRUSTED_TIME_SOURCE_KEY,
70};
71use crate::tlv::{
72 FromTLV, Nullable, NullableBuilder, TLVBuilderParent, TLVElement, TLVTag, TLVWrite, ToTLV,
73 Utf8StrBuilder, TLV,
74};
75use crate::utils::cell::RefCell;
76use crate::utils::epoch::FIRMWARE_BUILD_MATTER_US;
77use crate::utils::init::{init, into_init, try_init, Init};
78use crate::utils::storage::Vec;
79use crate::utils::sync::blocking::Mutex;
80use crate::utils::sync::Notification;
81
82pub use crate::dm::clusters::decl::time_synchronization::*;
83
84pub mod client;
85
86#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
90#[cfg_attr(feature = "defmt", derive(defmt::Format))]
91pub enum UtcTime {
92 Reliable(u64),
94 LastKnown(u64),
100}
101
102impl UtcTime {
103 pub const fn reliable(&self) -> Option<u64> {
105 match self {
106 UtcTime::Reliable(utc) => Some(*utc),
107 UtcTime::LastKnown(_) => None,
108 }
109 }
110
111 pub const fn any(&self) -> u64 {
113 match self {
114 UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc,
115 }
116 }
117
118 pub const fn reliable_secs(&self) -> Option<u64> {
120 match self {
121 UtcTime::Reliable(utc) => Some(*utc / 1_000_000),
122 UtcTime::LastKnown(_) => None,
123 }
124 }
125
126 pub const fn any_secs(&self) -> u64 {
128 match self {
129 UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc / 1_000_000,
130 }
131 }
132}
133
134pub struct Rtc {
150 utc_us: u64,
152 utc_us_persisted: u64,
154 granularity: GranularityEnum,
160 source: TimeSourceEnum,
163 anchor: Option<embassy_time::Instant>,
166 trusted_time_source: Option<TrustedTimeSource>,
171}
172
173impl Rtc {
174 #[inline(always)]
175 pub(crate) const fn new() -> Self {
176 Self {
177 utc_us: FIRMWARE_BUILD_MATTER_US,
178 utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
179 granularity: GranularityEnum::NoTimeGranularity,
180 source: TimeSourceEnum::None,
181 anchor: None,
182 trusted_time_source: None,
183 }
184 }
185
186 pub(crate) fn init() -> impl Init<Self> {
188 init!(Self {
189 utc_us: FIRMWARE_BUILD_MATTER_US,
190 utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
191 granularity: GranularityEnum::NoTimeGranularity,
192 source: TimeSourceEnum::None,
193 anchor: None,
194 trusted_time_source: None,
195 })
196 }
197
198 fn reset(&mut self) {
199 self.utc_us = FIRMWARE_BUILD_MATTER_US;
200 self.utc_us_persisted = FIRMWARE_BUILD_MATTER_US;
201 self.granularity = GranularityEnum::NoTimeGranularity;
202 self.source = TimeSourceEnum::None;
203 self.anchor = None;
204 self.trusted_time_source = None;
205 }
206
207 pub fn reset_persist<S: KvBlobStore>(
208 &mut self,
209 mut store: S,
210 buf: &mut [u8],
211 ) -> Result<(), Error> {
212 self.reset();
213
214 store.remove(LKG_UTC_KEY, buf)?;
215 store.remove(TRUSTED_TIME_SOURCE_KEY, buf)?;
216 Ok(())
217 }
218
219 pub fn load_persist<S: KvBlobStore>(&mut self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
220 self.reset();
221
222 if let Some(data) = kv.load(LKG_UTC_KEY, buf)? {
228 let stored = u64::from_tlv(&TLVElement::new(data))?;
229 let floor = FIRMWARE_BUILD_MATTER_US;
230
231 self.utc_us_persisted = stored;
232 self.utc_us = stored.max(floor);
233 }
234
235 if let Some(data) = kv.load(TRUSTED_TIME_SOURCE_KEY, buf)? {
237 self.trusted_time_source = Some(TrustedTimeSource::from_tlv(&TLVElement::new(data))?);
238 }
239
240 Ok(())
241 }
242
243 pub fn trusted_time_source(&self) -> Option<TrustedTimeSource> {
246 self.trusted_time_source
247 }
248
249 pub fn set_trusted_time_source<E: EventEmitter>(
253 &mut self,
254 source: Option<TrustedTimeSource>,
255 change_notifier: &dyn AttrChangeNotifier,
256 event_emitter: E,
257 ) -> Result<(), Error> {
258 if self.trusted_time_source != source {
259 let previous = self.trusted_time_source;
260
261 self.trusted_time_source = source;
262
263 change_notifier.notify_attr_changed(
264 ROOT_ENDPOINT_ID,
265 TimeSyncHandler::CLUSTER.id,
266 AttributeId::TrustedTimeSource as _,
267 );
268
269 if self.trusted_time_source.is_none() && previous.is_some() {
273 MissingTrustedTimeSource::emit_for(event_emitter, ROOT_ENDPOINT_ID, |b| b.end())?;
274 }
275 }
276
277 Ok(())
278 }
279
280 pub fn set_trusted_time_source_persist<S: KvBlobStoreAccess, E: EventEmitter>(
289 &mut self,
290 source: Option<TrustedTimeSource>,
291 persist: &mut Persist<S>,
292 change_notifier: &dyn AttrChangeNotifier,
293 event_emitter: E,
294 ) -> Result<(), Error> {
295 if self.trusted_time_source != source {
296 self.set_trusted_time_source(source, change_notifier, event_emitter)?;
297
298 match source {
299 Some(source) => {
300 persist.store_tlv(TRUSTED_TIME_SOURCE_KEY, source)?;
301 }
302 None => {
303 persist.remove(TRUSTED_TIME_SOURCE_KEY)?;
304 }
305 }
306 }
307
308 Ok(())
309 }
310
311 pub fn utc_time(&self) -> UtcTime {
313 if let Some(anchor) = self.anchor {
314 let elapsed_us = embassy_time::Instant::now()
315 .checked_duration_since(anchor)
316 .map(|d| d.as_micros())
317 .unwrap_or(0);
318
319 UtcTime::Reliable(self.utc_us.saturating_add(elapsed_us))
320 } else {
321 UtcTime::LastKnown(self.utc_us)
322 }
323 }
324
325 pub fn utc_time_granularity(&self) -> GranularityEnum {
333 if self.anchor.is_some() {
334 self.granularity
335 } else {
336 GranularityEnum::NoTimeGranularity
337 }
338 }
339
340 pub fn utc_time_source(&self) -> TimeSourceEnum {
344 if self.anchor.is_some() {
345 self.source
346 } else {
347 TimeSourceEnum::None
348 }
349 }
350
351 pub fn set_utc_time(
367 &mut self,
368 utc_us: u64,
369 granularity: GranularityEnum,
370 source: TimeSourceEnum,
371 change_notifier: &dyn AttrChangeNotifier,
372 ) -> bool {
373 let stepped = match granularity {
374 GranularityEnum::MicrosecondsGranularity => GranularityEnum::MillisecondsGranularity,
375 GranularityEnum::MillisecondsGranularity => GranularityEnum::SecondsGranularity,
376 GranularityEnum::SecondsGranularity => GranularityEnum::MinutesGranularity,
377 _ => GranularityEnum::MinutesGranularity,
380 };
381
382 let changed = self.utc_us != utc_us || self.granularity != stepped || self.source != source;
383
384 if changed || self.anchor.is_none() {
385 self.utc_us = utc_us;
386 self.granularity = stepped;
387 self.source = source;
388 self.anchor = Some(embassy_time::Instant::now());
389
390 change_notifier.notify_attr_changed(
391 ROOT_ENDPOINT_ID,
392 TimeSyncHandler::CLUSTER.id,
393 AttributeId::UTCTime as _,
394 );
395 change_notifier.notify_attr_changed(
396 ROOT_ENDPOINT_ID,
397 TimeSyncHandler::CLUSTER.id,
398 AttributeId::Granularity as _,
399 );
400 change_notifier.notify_attr_changed(
401 ROOT_ENDPOINT_ID,
402 TimeSyncHandler::CLUSTER.id,
403 AttributeId::TimeSource as _,
404 );
405 }
406
407 changed
408 }
409
410 pub fn set_utc_time_persist<S: KvBlobStoreAccess>(
411 &mut self,
412 utc_us: u64,
413 granularity: GranularityEnum,
414 source: TimeSourceEnum,
415 persist: &mut Persist<S>,
416 change_notifier: &dyn AttrChangeNotifier,
417 ) -> Result<(), Error> {
418 const DELTA: u64 = 24 * 60 * 60 * 1_000_000; let delta = self.utc_us_persisted.abs_diff(utc_us);
421
422 self.set_utc_time(utc_us, granularity, source, change_notifier);
423
424 if delta >= DELTA {
425 info!("TimeSync: UTC time changed by more than a day, persisting");
431
432 persist.store_tlv(LKG_UTC_KEY, utc_us.to_le_bytes())?;
433 self.utc_us_persisted = utc_us;
434 }
435
436 Ok(())
437 }
438}
439
440#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromTLV, ToTLV)]
444#[cfg_attr(feature = "defmt", derive(defmt::Format))]
445pub struct TrustedTimeSource {
446 pub fab_idx: NonZeroU8,
449 pub node_id: NodeId,
451 pub endpoint: EndptId,
454}
455
456bitflags! {
457 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
468 pub struct Options: u8 {
469 const TIME_ZONE = 0x1;
474 const NTP_CLIENT = 0x2;
478 const NTP_SERVER = 0x4;
481 const TIME_SYNC_CLIENT = 0x8;
485 }
486}
487
488#[derive(Debug, Clone, Eq, PartialEq, Hash)]
494pub struct TimeZoneEntry<'a> {
495 pub offset: i32,
497 pub valid_at: u64,
499 pub name: Option<&'a str>,
502}
503
504#[derive(Debug, Clone, Eq, PartialEq, Hash, FromTLV, ToTLV)]
507pub struct DSTOffsetEntry {
508 pub offset: i32,
511 pub valid_starting: u64,
513 pub valid_until: Option<u64>,
516}
517
518#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
522pub struct TrustedTimeSourceData {
523 pub fabric_index: u8,
525 pub node_id: u64,
527 pub endpoint: u16,
529}
530
531pub trait TimeZones {
547 fn time_zone(
549 &self,
550 visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
551 ) -> Result<(), Error>;
552
553 fn dst_offset(
555 &self,
556 visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
557 ) -> Result<(), Error>;
558
559 fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error>;
561
562 fn time_zone_list_max_size(&self) -> Result<u8, Error>;
564
565 fn dst_offset_list_max_size(&self) -> Result<u8, Error>;
567
568 fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error>;
571
572 fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error>;
574}
575
576impl<T> TimeZones for &T
577where
578 T: TimeZones,
579{
580 fn time_zone(
581 &self,
582 visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
583 ) -> Result<(), Error> {
584 (*self).time_zone(visit)
585 }
586
587 fn dst_offset(
588 &self,
589 visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
590 ) -> Result<(), Error> {
591 (*self).dst_offset(visit)
592 }
593
594 fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
595 (*self).time_zone_database()
596 }
597
598 fn time_zone_list_max_size(&self) -> Result<u8, Error> {
599 (*self).time_zone_list_max_size()
600 }
601
602 fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
603 (*self).dst_offset_list_max_size()
604 }
605
606 fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
607 (*self).set_time_zone(request)
608 }
609
610 fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
611 (*self).set_dst_offset(request)
612 }
613}
614
615pub trait NtpClient {
617 fn default_ntp(&self) -> Result<Nullable<&str>, Error>;
620
621 fn supports_dns_resolve(&self) -> Result<bool, Error>;
624
625 fn set_default_ntp(&self, request: &SetDefaultNTPRequest<'_>) -> Result<(), Error>;
627}
628
629impl<T> NtpClient for &T
630where
631 T: NtpClient,
632{
633 fn default_ntp(&self) -> Result<Nullable<&str>, Error> {
634 (*self).default_ntp()
635 }
636
637 fn supports_dns_resolve(&self) -> Result<bool, Error> {
638 (*self).supports_dns_resolve()
639 }
640
641 fn set_default_ntp(&self, request: &SetDefaultNTPRequest<'_>) -> Result<(), Error> {
642 (*self).set_default_ntp(request)
643 }
644}
645
646pub trait NtpServer {
648 fn ntp_server_available(&self) -> Result<bool, Error>;
650}
651
652impl<T> NtpServer for &T
653where
654 T: NtpServer,
655{
656 fn ntp_server_available(&self) -> Result<bool, Error> {
657 (*self).ntp_server_available()
658 }
659}
660
661pub const TIME_ZONE_NAME_MAX: usize = 64;
663
664#[derive(FromTLV, ToTLV)]
666struct TimeZoneOwned {
667 offset: i32,
668 valid_at: u64,
669 name: Option<String<TIME_ZONE_NAME_MAX>>,
670}
671
672struct TimeZoneStoreData<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> {
675 time_zone: Vec<TimeZoneOwned, TIME_ZONE_MAX>,
676 dst_offset: Vec<DSTOffsetEntry, DST_OFFSET_MAX>,
677}
678
679impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize>
680 TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>
681{
682 const fn new() -> Self {
683 Self {
684 time_zone: Vec::new(),
685 dst_offset: Vec::new(),
686 }
687 }
688
689 fn init() -> impl Init<Self> {
690 init!(Self {
691 time_zone <- Vec::init(),
692 dst_offset <- Vec::init(),
693 })
694 }
695}
696
697impl<'a, const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> FromTLV<'a>
698 for TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>
699{
700 fn from_tlv(tlv: &TLVElement<'a>) -> Result<Self, Error> {
701 let tlv = tlv.structure()?;
702
703 Ok(Self {
704 time_zone: FromTLV::from_tlv(&tlv.ctx(0)?)?,
705 dst_offset: FromTLV::from_tlv(&tlv.ctx(1)?)?,
706 })
707 }
708
709 fn init_from_tlv(tlv: TLVElement<'a>) -> impl Init<Self, Error> {
710 into_init(move || {
711 let seq = tlv.structure()?;
712
713 let init = try_init!(Self {
714 time_zone <- Vec::<TimeZoneOwned, TIME_ZONE_MAX>::init_from_tlv(seq.ctx(0)?),
715 dst_offset <- Vec::<DSTOffsetEntry, DST_OFFSET_MAX>::init_from_tlv(seq.ctx(1)?),
716 }? Error);
717
718 Ok(init)
719 })
720 }
721}
722
723impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> ToTLV
724 for TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>
725{
726 fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
727 tw.start_struct(tag)?;
728
729 self.time_zone.to_tlv(&TLVTag::Context(0), &mut tw)?;
730 self.dst_offset.to_tlv(&TLVTag::Context(1), &mut tw)?;
731
732 tw.end_container()
733 }
734
735 fn tlv_iter(&self, tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
736 use crate::tlv::TLVIter;
737
738 core::iter::empty()
739 .start_struct(tag)
740 .chain_iter(self.time_zone.tlv_iter(TLVTag::Context(0)))
741 .chain_iter(self.dst_offset.tlv_iter(TLVTag::Context(1)))
742 .end_container()
743 }
744}
745
746struct TimeZoneStoreState<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> {
748 data: TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>,
749 generation: u32,
752}
753
754impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize>
755 TimeZoneStoreState<TIME_ZONE_MAX, DST_OFFSET_MAX>
756{
757 const fn new() -> Self {
758 Self {
759 data: TimeZoneStoreData::new(),
760 generation: 0,
761 }
762 }
763
764 fn init() -> impl Init<Self> {
765 init!(Self {
766 data <- TimeZoneStoreData::init(),
767 generation: 0,
768 })
769 }
770}
771
772pub struct TimeZoneStore<const TIME_ZONE_MAX: usize = 2, const DST_OFFSET_MAX: usize = 2> {
783 state: Mutex<RefCell<TimeZoneStoreState<TIME_ZONE_MAX, DST_OFFSET_MAX>>>,
784 changed: Notification,
788}
789
790impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize>
791 TimeZoneStore<TIME_ZONE_MAX, DST_OFFSET_MAX>
792{
793 pub const fn new() -> Self {
796 Self {
797 state: Mutex::new(RefCell::new(TimeZoneStoreState::new())),
798 changed: Notification::new(),
799 }
800 }
801
802 pub fn init() -> impl Init<Self> {
804 init!(Self {
805 state <- Mutex::init(RefCell::init(TimeZoneStoreState::init())),
806 changed <- Notification::init(),
807 })
808 }
809
810 pub async fn wait_changed(&self) {
813 self.changed.wait().await
814 }
815
816 pub fn note_changed(&self) {
820 self.changed.notify();
821 }
822
823 pub fn load_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
827 let Some(data) = store.load(TIME_ZONE_KEY, buf)? else {
828 return Ok(());
829 };
830
831 let loaded = TimeZoneStoreData::from_tlv(&TLVElement::new(data))?;
833
834 self.state.lock(|state| {
835 let mut state = state.borrow_mut();
836
837 state.data = loaded;
838 });
839
840 info!("Loaded TimeZone / DSTOffset lists from storage");
841
842 Ok(())
843 }
844
845 fn store_persist<S: KvBlobStoreAccess>(&self, kv: S) -> Result<(), Error> {
849 let mut persist = Persist::new(kv);
850
851 self.state.lock(|state| {
852 let state = state.borrow();
853
854 persist.store_tlv(TIME_ZONE_KEY, &state.data)
855 })?;
856
857 persist.run()
858 }
859
860 pub fn generation(&self) -> u32 {
862 self.state.lock(|state| state.borrow().generation)
863 }
864
865 pub fn active_time_zone(&self, now: u64) -> (i32, Option<String<TIME_ZONE_NAME_MAX>>) {
869 self.state.lock(|state| {
870 let state = state.borrow();
871
872 state
873 .data
874 .time_zone
875 .iter()
876 .rfind(|entry| entry.valid_at <= now)
877 .map(|entry| (entry.offset, entry.name.clone()))
878 .unwrap_or((0, None))
879 })
880 }
881
882 pub fn active_dst_offset(&self, now: u64) -> Option<i32> {
884 self.state.lock(|state| {
885 let state = state.borrow();
886
887 state
888 .data
889 .dst_offset
890 .iter()
891 .find(|entry| {
892 entry.valid_starting <= now
893 && entry.valid_until.map(|until| now < until).unwrap_or(true)
894 })
895 .map(|entry| entry.offset)
896 })
897 }
898
899 pub fn dst_table_empty(&self) -> bool {
901 self.state
902 .lock(|state| state.borrow().data.dst_offset.is_empty())
903 }
904
905 pub fn dst_usable(&self, now: u64) -> bool {
917 self.state.lock(|state| {
918 let state = state.borrow();
919
920 state
921 .data
922 .dst_offset
923 .iter()
924 .any(|entry| entry.valid_until.map(|until| now < until).unwrap_or(true))
925 })
926 }
927
928 pub fn next_transition(&self, now: u64) -> Option<u64> {
933 self.state.lock(|state| {
934 let state = state.borrow();
935
936 let tz = state
937 .data
938 .time_zone
939 .iter()
940 .map(|entry| entry.valid_at)
941 .filter(|at| *at > now)
942 .min();
943
944 let dst = state
945 .data
946 .dst_offset
947 .iter()
948 .flat_map(|entry| {
949 [Some(entry.valid_starting), entry.valid_until]
950 .into_iter()
951 .flatten()
952 })
953 .filter(|at| *at > now)
954 .min();
955
956 match (tz, dst) {
957 (Some(a), Some(b)) => Some(a.min(b)),
958 (a, b) => a.or(b),
959 }
960 })
961 }
962
963 fn validate_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
973 let mut prev_starting: Option<u64> = None;
974 let mut prev_until: Option<u64> = None;
975 let mut seen_null_until = false;
976
977 for (index, entry) in request.dst_offset()?.iter().enumerate() {
978 let entry = entry?;
979
980 if index == DST_OFFSET_MAX {
981 return Err(ErrorCode::ResourceExhausted.into());
982 }
983
984 if seen_null_until {
987 Err(ErrorCode::ConstraintError)?;
988 }
989
990 let starting = entry.valid_starting()?;
991
992 if let Some(prev) = prev_starting {
993 if starting <= prev {
994 Err(ErrorCode::ConstraintError)?;
995 }
996 }
997
998 if let Some(until) = prev_until {
999 if starting < until {
1000 Err(ErrorCode::ConstraintError)?;
1002 }
1003 }
1004
1005 match entry.valid_until()?.into_option() {
1006 Some(until) => {
1007 if starting >= until {
1008 Err(ErrorCode::ConstraintError)?;
1009 }
1010
1011 prev_until = Some(until);
1012 }
1013 None => seen_null_until = true,
1014 }
1015
1016 prev_starting = Some(starting);
1017 }
1018
1019 Ok(())
1020 }
1021}
1022
1023impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> Default
1024 for TimeZoneStore<TIME_ZONE_MAX, DST_OFFSET_MAX>
1025{
1026 fn default() -> Self {
1027 Self::new()
1028 }
1029}
1030
1031impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> TimeZones
1032 for TimeZoneStore<TIME_ZONE_MAX, DST_OFFSET_MAX>
1033{
1034 fn time_zone(
1035 &self,
1036 visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
1037 ) -> Result<(), Error> {
1038 self.state.lock(|state| {
1039 let state = state.borrow();
1040
1041 if state.data.time_zone.is_empty() {
1042 return visit(&TimeZoneEntry {
1045 offset: 0,
1046 valid_at: 0,
1047 name: None,
1048 });
1049 }
1050
1051 for entry in state.data.time_zone.iter() {
1052 visit(&TimeZoneEntry {
1053 offset: entry.offset,
1054 valid_at: entry.valid_at,
1055 name: entry.name.as_deref(),
1056 })?;
1057 }
1058
1059 Ok(())
1060 })
1061 }
1062
1063 fn dst_offset(
1064 &self,
1065 visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
1066 ) -> Result<(), Error> {
1067 self.state.lock(|state| {
1068 let state = state.borrow();
1069
1070 for entry in state.data.dst_offset.iter() {
1071 visit(entry)?;
1072 }
1073
1074 Ok(())
1075 })
1076 }
1077
1078 fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
1079 Ok(TimeZoneDatabaseEnum::None)
1080 }
1081
1082 fn time_zone_list_max_size(&self) -> Result<u8, Error> {
1083 Ok(TIME_ZONE_MAX as u8)
1084 }
1085
1086 fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
1087 Ok(DST_OFFSET_MAX as u8)
1088 }
1089
1090 fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
1091 let mut prev_valid_at: Option<u64> = None;
1112
1113 for (index, entry) in request.time_zone()?.iter().enumerate() {
1114 let entry = entry?;
1115
1116 if index == TIME_ZONE_MAX {
1117 return Err(ErrorCode::ResourceExhausted.into());
1118 }
1119
1120 let valid_at = entry.valid_at()?;
1121
1122 if (index == 0) != (valid_at == 0) {
1123 Err(ErrorCode::ConstraintError)?;
1124 }
1125
1126 if let Some(prev) = prev_valid_at {
1127 if valid_at <= prev {
1128 Err(ErrorCode::ConstraintError)?;
1129 }
1130 }
1131
1132 prev_valid_at = Some(valid_at);
1133
1134 let offset = entry.offset()?;
1135
1136 if !(-12 * 3600..=14 * 3600).contains(&offset) {
1137 Err(ErrorCode::ConstraintError)?;
1138 }
1139
1140 if let Some(name) = entry.name()? {
1141 if name.len() > TIME_ZONE_NAME_MAX {
1142 Err(ErrorCode::ConstraintError)?;
1143 }
1144 }
1145 }
1146
1147 self.state.lock(|state| {
1150 let mut state = state.borrow_mut();
1151
1152 state.data.time_zone.clear();
1153
1154 for entry in request.time_zone()?.iter() {
1155 let entry = entry?;
1156
1157 let name = match entry.name()? {
1158 Some(name) => {
1159 Some(String::try_from(name).map_err(|_| ErrorCode::ConstraintError)?)
1160 }
1161 None => None,
1162 };
1163
1164 unwrap!(state
1166 .data
1167 .time_zone
1168 .push(TimeZoneOwned {
1169 offset: entry.offset()?,
1170 valid_at: entry.valid_at()?,
1171 name,
1172 })
1173 .ok());
1174 }
1175
1176 state.data.dst_offset.clear();
1177 state.generation = state.generation.wrapping_add(1);
1178
1179 Ok::<_, Error>(())
1180 })?;
1181
1182 self.note_changed();
1183
1184 Ok(true)
1187 }
1188
1189 fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
1190 let validated = self.validate_dst_offset(request);
1203
1204 if let Err(e) = validated {
1205 self.state.lock(|state| {
1206 let mut state = state.borrow_mut();
1207
1208 state.data.dst_offset.clear();
1209 state.generation = state.generation.wrapping_add(1);
1210 });
1211
1212 self.note_changed();
1213
1214 return Err(e);
1215 }
1216
1217 self.state.lock(|state| {
1218 let mut state = state.borrow_mut();
1219
1220 state.data.dst_offset.clear();
1221
1222 for entry in request.dst_offset()?.iter() {
1223 let entry = entry?;
1224
1225 unwrap!(state
1227 .data
1228 .dst_offset
1229 .push(DSTOffsetEntry {
1230 offset: entry.offset()?,
1231 valid_starting: entry.valid_starting()?,
1232 valid_until: entry.valid_until()?.into_option(),
1233 })
1234 .ok());
1235 }
1236
1237 state.generation = state.generation.wrapping_add(1);
1238
1239 Ok::<_, Error>(())
1240 })?;
1241
1242 self.note_changed();
1243
1244 Ok(())
1245 }
1246}
1247
1248const fn time_sync_attrs<const OPTS: u8>(attr: &Attribute, _: u16, _: u32) -> bool {
1251 use AttributeId as A;
1252
1253 if !attr.quality.contains(Quality::OPTIONAL) {
1255 return true;
1256 }
1257
1258 if attr.id == A::TimeSource as u32 {
1261 return true;
1262 }
1263
1264 let opts = Options::from_bits_truncate(OPTS);
1265 if opts.contains(Options::TIME_ZONE)
1266 && (attr.id == A::TimeZone as u32
1267 || attr.id == A::DSTOffset as u32
1268 || attr.id == A::LocalTime as u32
1269 || attr.id == A::TimeZoneDatabase as u32
1270 || attr.id == A::TimeZoneListMaxSize as u32
1271 || attr.id == A::DSTOffsetListMaxSize as u32)
1272 {
1273 return true;
1274 }
1275
1276 if opts.contains(Options::NTP_CLIENT)
1277 && (attr.id == A::DefaultNTP as u32 || attr.id == A::SupportsDNSResolve as u32)
1278 {
1279 return true;
1280 }
1281
1282 if opts.contains(Options::NTP_SERVER) && attr.id == A::NTPServerAvailable as u32 {
1283 return true;
1284 }
1285
1286 if opts.contains(Options::TIME_SYNC_CLIENT) && attr.id == A::TrustedTimeSource as u32 {
1287 return true;
1288 }
1289
1290 false
1291}
1292
1293const fn time_sync_cmds<const OPTS: u8>(cmd: &Command, _: u16, _: u32) -> bool {
1294 use CommandId as C;
1295
1296 if cmd.id == C::SetUTCTime as u32 {
1301 return true;
1302 }
1303
1304 let opts = Options::from_bits_truncate(OPTS);
1305
1306 if opts.contains(Options::TIME_ZONE)
1307 && (cmd.id == C::SetTimeZone as u32 || cmd.id == C::SetDSTOffset as u32)
1308 {
1309 return true;
1310 }
1311
1312 if opts.contains(Options::NTP_CLIENT) && cmd.id == C::SetDefaultNTP as u32 {
1313 return true;
1314 }
1315
1316 if opts.contains(Options::TIME_SYNC_CLIENT) && cmd.id == C::SetTrustedTimeSource as u32 {
1317 return true;
1318 }
1319
1320 false
1321}
1322
1323pub const fn cluster<const OPTS: u8>() -> Cluster<'static> {
1330 let opts = Options::from_bits_truncate(OPTS);
1331
1332 let mut features = 0u32;
1333
1334 if opts.contains(Options::TIME_ZONE) {
1335 features |= Feature::TIME_ZONE.bits();
1336 }
1337
1338 if opts.contains(Options::NTP_CLIENT) {
1339 features |= Feature::NTP_CLIENT.bits();
1340 }
1341
1342 if opts.contains(Options::NTP_SERVER) {
1343 features |= Feature::NTP_SERVER.bits();
1344 }
1345
1346 if opts.contains(Options::TIME_SYNC_CLIENT) {
1347 features |= Feature::TIME_SYNC_CLIENT.bits();
1348 }
1349
1350 Cluster {
1351 feature_map: features,
1352 with_attrs: time_sync_attrs::<OPTS>,
1353 with_cmds: time_sync_cmds::<OPTS>,
1354 ..FULL_CLUSTER
1355 }
1356}
1357
1358#[derive(Clone)]
1375pub struct TimeSyncHandler<'a> {
1376 dataver: Dataver,
1377 time_zones: Option<&'a dyn TimeZones>,
1379 ntp_client: Option<&'a dyn NtpClient>,
1381 ntp_server: Option<&'a dyn NtpServer>,
1383 tz_store: Option<&'a TimeZoneStore>,
1389}
1390
1391impl<'a> TimeSyncHandler<'a> {
1392 pub const fn new(dataver: Dataver) -> Self {
1396 Self {
1397 dataver,
1398 time_zones: None,
1399 ntp_client: None,
1400 ntp_server: None,
1401 tz_store: None,
1402 }
1403 }
1404
1405 pub const fn new_with_time_zone(dataver: Dataver, store: &'a TimeZoneStore) -> Self {
1410 Self {
1411 dataver,
1412 time_zones: Some(store),
1413 ntp_client: None,
1414 ntp_server: None,
1415 tz_store: Some(store),
1416 }
1417 }
1418
1419 pub const fn with_time_zones(mut self, time_zones: &'a dyn TimeZones) -> Self {
1421 self.time_zones = Some(time_zones);
1422 self
1423 }
1424
1425 pub const fn with_ntp_client(mut self, ntp_client: &'a dyn NtpClient) -> Self {
1427 self.ntp_client = Some(ntp_client);
1428 self
1429 }
1430
1431 pub const fn with_ntp_server(mut self, ntp_server: &'a dyn NtpServer) -> Self {
1433 self.ntp_server = Some(ntp_server);
1434 self
1435 }
1436
1437 pub const fn adapt(self) -> HandlerAdaptor<Self> {
1439 HandlerAdaptor(self)
1440 }
1441}
1442
1443impl ClusterHandler for TimeSyncHandler<'_> {
1444 const CLUSTER: Cluster<'static> = cluster::<0>();
1445
1446 fn dataver(&self) -> u32 {
1447 self.dataver.get()
1448 }
1449
1450 fn dataver_changed(&self) {
1451 self.dataver.changed();
1452 }
1453
1454 async fn run(&self, ctx: impl HandlerContext) -> Result<(), Error> {
1458 let Some(store) = self.tz_store else {
1461 return core::future::pending().await;
1462 };
1463
1464 let mut last_tz: Option<i32> = None;
1468 let mut last_dst_active: Option<bool> = None;
1469 let mut last_dst_usable: Option<bool> = None;
1470
1471 loop {
1472 let now = ctx
1473 .matter()
1474 .with_state(|state| state.rtc.utc_time())
1475 .reliable();
1476
1477 let next = if let Some(now) = now {
1478 let (tz_offset, tz_name) = store.active_time_zone(now);
1480
1481 if last_tz != Some(tz_offset) {
1482 if last_tz.is_some() {
1483 let emitted = TimeZoneStatus::emit_for(&ctx, ROOT_ENDPOINT_ID, |event| {
1484 event.offset(tz_offset)?.name(tz_name.as_deref())?.end()
1485 });
1486
1487 if let Err(e) = emitted {
1488 warn!("Failed to emit TimeZoneStatus: {:?}", e);
1489 }
1490 }
1491
1492 last_tz = Some(tz_offset);
1493 }
1494
1495 let dst_active = store.active_dst_offset(now).is_some();
1497
1498 if last_dst_active != Some(dst_active) {
1499 if last_dst_active.is_some() || dst_active {
1500 let emitted = DSTStatus::emit_for(&ctx, ROOT_ENDPOINT_ID, |event| {
1501 event.dst_offset_active(dst_active)?.end()
1502 });
1503
1504 if let Err(e) = emitted {
1505 warn!("Failed to emit DSTStatus: {:?}", e);
1506 }
1507 }
1508
1509 last_dst_active = Some(dst_active);
1510 }
1511
1512 let dst_usable = store.dst_usable(now);
1517
1518 if last_dst_usable != Some(dst_usable) {
1519 if !dst_usable && last_dst_usable == Some(true) {
1520 let emitted =
1521 DSTTableEmpty::emit_for(&ctx, ROOT_ENDPOINT_ID, |event| event.end());
1522
1523 if let Err(e) = emitted {
1524 warn!("Failed to emit DSTTableEmpty: {:?}", e);
1525 }
1526 }
1527
1528 last_dst_usable = Some(dst_usable);
1529 }
1530
1531 store.next_transition(now)
1532 } else {
1533 None
1536 };
1537
1538 let boundary = async {
1541 match (next, now) {
1542 (Some(at), Some(now)) => {
1543 let delta_us = at.saturating_sub(now).saturating_add(100_000);
1544
1545 embassy_time::Timer::after(embassy_time::Duration::from_micros(delta_us))
1546 .await
1547 }
1548 _ => core::future::pending().await,
1549 }
1550 };
1551
1552 select(boundary, store.wait_changed()).await;
1553 }
1554 }
1555
1556 fn utc_time(&self, ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
1557 Ok(Nullable::new(
1558 ctx.matter()
1559 .with_state(|state| state.rtc.utc_time())
1560 .reliable(),
1561 ))
1562 }
1563
1564 fn granularity(&self, ctx: impl ReadContext) -> Result<GranularityEnum, Error> {
1565 Ok(ctx
1566 .matter()
1567 .with_state(|state| state.rtc.utc_time_granularity()))
1568 }
1569
1570 fn time_source(&self, ctx: impl ReadContext) -> Result<TimeSourceEnum, Error> {
1571 Ok(ctx.matter().with_state(|state| state.rtc.utc_time_source()))
1572 }
1573
1574 fn trusted_time_source<P: TLVBuilderParent>(
1580 &self,
1581 ctx: impl ReadContext,
1582 builder: NullableBuilder<P, TrustedTimeSourceStructBuilder<P>>,
1583 ) -> Result<P, Error> {
1584 match ctx
1585 .matter()
1586 .with_state(|state| state.rtc.trusted_time_source())
1587 {
1588 Some(tts) => builder
1589 .non_null()?
1590 .fabric_index(tts.fab_idx.get())?
1591 .node_id(tts.node_id)?
1592 .endpoint(tts.endpoint)?
1593 .end(),
1594 None => builder.null(),
1595 }
1596 }
1597
1598 fn default_ntp<P: TLVBuilderParent>(
1599 &self,
1600 _ctx: impl ReadContext,
1601 builder: NullableBuilder<P, Utf8StrBuilder<P>>,
1602 ) -> Result<P, Error> {
1603 match self
1604 .ntp_client
1605 .ok_or(ErrorCode::AttributeNotFound)?
1606 .default_ntp()?
1607 .into_option()
1608 {
1609 Some(s) => builder.non_null()?.set(s),
1610 None => builder.null(),
1611 }
1612 }
1613
1614 fn supports_dns_resolve(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
1615 self.ntp_client
1616 .ok_or(ErrorCode::AttributeNotFound)?
1617 .supports_dns_resolve()
1618 }
1619
1620 fn ntp_server_available(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
1621 self.ntp_server
1622 .ok_or(ErrorCode::AttributeNotFound)?
1623 .ntp_server_available()
1624 }
1625
1626 fn time_zone<P: TLVBuilderParent>(
1627 &self,
1628 _ctx: impl ReadContext,
1629 builder: ArrayAttributeRead<TimeZoneStructArrayBuilder<P>, TimeZoneStructBuilder<P>>,
1630 ) -> Result<P, Error> {
1631 match builder {
1632 ArrayAttributeRead::ReadAll(array) => {
1633 let mut array_opt = Some(array);
1634 self.time_zones
1635 .ok_or(ErrorCode::AttributeNotFound)?
1636 .time_zone(&mut |entry| {
1637 let array = unwrap!(array_opt.take());
1638 let next = array
1639 .push()?
1640 .offset(entry.offset)?
1641 .valid_at(entry.valid_at)?
1642 .name(entry.name)?
1643 .end()?;
1644 array_opt = Some(next);
1645 Ok(())
1646 })?;
1647 unwrap!(array_opt.take()).end()
1648 }
1649 ArrayAttributeRead::ReadOne(index, item_builder) => {
1650 let mut item_opt = Some(item_builder);
1651 let mut returned: Option<P> = None;
1652 let mut current = 0u16;
1653 self.time_zones
1654 .ok_or(ErrorCode::AttributeNotFound)?
1655 .time_zone(&mut |entry| {
1656 if returned.is_none() && current == index {
1657 let b = unwrap!(item_opt.take());
1658 returned = Some(
1659 b.offset(entry.offset)?
1660 .valid_at(entry.valid_at)?
1661 .name(entry.name)?
1662 .end()?,
1663 );
1664 }
1665 current = current.saturating_add(1);
1666 Ok(())
1667 })?;
1668 returned.ok_or_else(|| ErrorCode::ConstraintError.into())
1669 }
1670 ArrayAttributeRead::ReadNone(array) => array.end(),
1671 }
1672 }
1673
1674 fn dst_offset<P: TLVBuilderParent>(
1675 &self,
1676 _ctx: impl ReadContext,
1677 builder: ArrayAttributeRead<DSTOffsetStructArrayBuilder<P>, DSTOffsetStructBuilder<P>>,
1678 ) -> Result<P, Error> {
1679 match builder {
1680 ArrayAttributeRead::ReadAll(array) => {
1681 let mut array_opt = Some(array);
1682 self.time_zones
1683 .ok_or(ErrorCode::AttributeNotFound)?
1684 .dst_offset(&mut |entry| {
1685 let array = unwrap!(array_opt.take());
1686 let next = array
1687 .push()?
1688 .offset(entry.offset)?
1689 .valid_starting(entry.valid_starting)?
1690 .valid_until(Nullable::new(entry.valid_until))?
1691 .end()?;
1692 array_opt = Some(next);
1693 Ok(())
1694 })?;
1695 unwrap!(array_opt.take()).end()
1696 }
1697 ArrayAttributeRead::ReadOne(index, item_builder) => {
1698 let mut item_opt = Some(item_builder);
1699 let mut returned: Option<P> = None;
1700 let mut current = 0u16;
1701 self.time_zones
1702 .ok_or(ErrorCode::AttributeNotFound)?
1703 .dst_offset(&mut |entry| {
1704 if returned.is_none() && current == index {
1705 let b = unwrap!(item_opt.take());
1706 returned = Some(
1707 b.offset(entry.offset)?
1708 .valid_starting(entry.valid_starting)?
1709 .valid_until(Nullable::new(entry.valid_until))?
1710 .end()?,
1711 );
1712 }
1713 current = current.saturating_add(1);
1714 Ok(())
1715 })?;
1716 returned.ok_or_else(|| ErrorCode::ConstraintError.into())
1717 }
1718 ArrayAttributeRead::ReadNone(array) => array.end(),
1719 }
1720 }
1721
1722 fn local_time(&self, ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
1723 let Some(utc) = ctx
1729 .matter()
1730 .with_state(|state| state.rtc.utc_time())
1731 .reliable()
1732 else {
1733 return Ok(Nullable::none());
1734 };
1735
1736 let mut offset_secs: i64 = 0;
1737
1738 self.time_zones
1740 .ok_or(ErrorCode::AttributeNotFound)?
1741 .time_zone(&mut |entry| {
1742 if entry.valid_at <= utc {
1743 offset_secs = entry.offset as i64;
1744 }
1745 Ok(())
1746 })?;
1747
1748 let mut usable = false;
1755 let mut active: Option<i32> = None;
1756
1757 self.time_zones
1758 .ok_or(ErrorCode::AttributeNotFound)?
1759 .dst_offset(&mut |entry| {
1760 let unexpired = entry.valid_until.map(|u| utc < u).unwrap_or(true);
1761
1762 if unexpired {
1763 usable = true;
1764
1765 if entry.valid_starting <= utc {
1766 active = Some(entry.offset);
1767 }
1768 }
1769
1770 Ok(())
1771 })?;
1772
1773 if !usable {
1774 return Ok(Nullable::none());
1775 }
1776
1777 offset_secs += active.unwrap_or(0) as i64;
1778
1779 Ok(Nullable::some(utc.saturating_add_signed(
1780 offset_secs.saturating_mul(1_000_000),
1781 )))
1782 }
1783
1784 fn time_zone_database(&self, _ctx: impl ReadContext) -> Result<TimeZoneDatabaseEnum, Error> {
1785 self.time_zones
1786 .ok_or(ErrorCode::AttributeNotFound)?
1787 .time_zone_database()
1788 }
1789
1790 fn time_zone_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
1791 self.time_zones
1792 .ok_or(ErrorCode::AttributeNotFound)?
1793 .time_zone_list_max_size()
1794 }
1795
1796 fn dst_offset_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
1797 self.time_zones
1798 .ok_or(ErrorCode::AttributeNotFound)?
1799 .dst_offset_list_max_size()
1800 }
1801
1802 fn handle_set_utc_time(
1805 &self,
1806 ctx: impl InvokeContext,
1807 request: SetUTCTimeRequest<'_>,
1808 ) -> Result<(), Error> {
1809 let utc_us = request.utc_time()?;
1813 let granularity = request.granularity()?;
1814 ctx.matter().with_state(|state| {
1815 state
1816 .rtc
1817 .set_utc_time(utc_us, granularity, TimeSourceEnum::Admin, &ctx)
1818 });
1819
1820 if let Some(store) = self.tz_store {
1823 store.note_changed();
1824 }
1825
1826 Ok(())
1827 }
1828
1829 fn handle_set_trusted_time_source(
1834 &self,
1835 ctx: impl InvokeContext,
1836 request: SetTrustedTimeSourceRequest<'_>,
1837 ) -> Result<(), Error> {
1838 let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::InvalidCommand)?;
1839
1840 let source = request
1841 .trusted_time_source()?
1842 .into_option()
1843 .map(|tts| {
1844 Ok::<_, Error>(TrustedTimeSource {
1845 fab_idx,
1846 node_id: tts.node_id()?,
1847 endpoint: tts.endpoint()?,
1848 })
1849 })
1850 .transpose()?;
1851
1852 let mut persist = Persist::new(ctx.kv());
1853
1854 ctx.matter().with_state(|state| {
1855 state
1856 .rtc
1857 .set_trusted_time_source_persist(source, &mut persist, &ctx, &ctx)
1858 })?;
1859
1860 persist.run()?;
1861
1862 Ok(())
1863 }
1864
1865 fn handle_set_time_zone<P: TLVBuilderParent>(
1866 &self,
1867 ctx: impl InvokeContext,
1868 request: SetTimeZoneRequest<'_>,
1869 response: SetTimeZoneResponseBuilder<P>,
1870 ) -> Result<P, Error> {
1871 let dst_offset_required = self
1872 .time_zones
1873 .ok_or(ErrorCode::CommandNotFound)?
1874 .set_time_zone(&request)?;
1875
1876 if let Some(store) = self.tz_store {
1882 store.store_persist(ctx.kv())?;
1883 }
1884
1885 ctx.notify_own_attr_changed(AttributeId::TimeZone as _);
1886 ctx.notify_own_attr_changed(AttributeId::DSTOffset as _);
1887
1888 response.dst_offset_required(dst_offset_required)?.end()
1889 }
1890
1891 fn handle_set_dst_offset(
1892 &self,
1893 ctx: impl InvokeContext,
1894 request: SetDSTOffsetRequest<'_>,
1895 ) -> Result<(), Error> {
1896 self.time_zones
1897 .ok_or(ErrorCode::CommandNotFound)?
1898 .set_dst_offset(&request)?;
1899
1900 if let Some(store) = self.tz_store {
1901 store.store_persist(ctx.kv())?;
1902 }
1903
1904 ctx.notify_own_attr_changed(AttributeId::DSTOffset as _);
1905
1906 Ok(())
1907 }
1908
1909 fn handle_set_default_ntp(
1910 &self,
1911 _ctx: impl InvokeContext,
1912 request: SetDefaultNTPRequest<'_>,
1913 ) -> Result<(), Error> {
1914 self.ntp_client
1915 .ok_or(ErrorCode::CommandNotFound)?
1916 .set_default_ntp(&request)
1917 }
1918}
1919
1920impl core::fmt::Debug for TimeSyncHandler<'_> {
1921 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1922 f.debug_struct("TimeSyncHandler")
1923 .field("dataver", &self.dataver)
1924 .finish()
1925 }
1926}
1927
1928#[cfg(feature = "defmt")]
1929impl defmt::Format for TimeSyncHandler<'_> {
1930 fn format(&self, f: defmt::Formatter) {
1931 defmt::write!(f, "TimeSyncHandler {{ dataver: {} }}", self.dataver.get());
1932 }
1933}