use core::num::NonZeroU8;
use bitflags::bitflags;
use crate::dm::endpoints::ROOT_ENDPOINT_ID;
use crate::dm::{
ArrayAttributeRead, AttrChangeNotifier, Attribute, Cluster, Command, Dataver, EndptId,
EventEmitter, InvokeContext, NodeId, Quality, ReadContext,
};
use crate::error::{Error, ErrorCode};
use crate::persist::{
KvBlobStore, KvBlobStoreAccess, Persist, LKG_UTC_KEY, TRUSTED_TIME_SOURCE_KEY,
};
use crate::tlv::{
FromTLV, Nullable, NullableBuilder, TLVBuilderParent, TLVElement, ToTLV, Utf8StrBuilder,
};
use crate::utils::epoch::FIRMWARE_BUILD_MATTER_US;
use crate::utils::init::{init, Init};
pub use crate::dm::clusters::decl::time_synchronization::*;
pub mod client;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum UtcTime {
Reliable(u64),
LastKnown(u64),
}
impl UtcTime {
pub const fn reliable(&self) -> Option<u64> {
match self {
UtcTime::Reliable(utc) => Some(*utc),
UtcTime::LastKnown(_) => None,
}
}
pub const fn any(&self) -> u64 {
match self {
UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc,
}
}
pub const fn reliable_secs(&self) -> Option<u64> {
match self {
UtcTime::Reliable(utc) => Some(*utc / 1_000_000),
UtcTime::LastKnown(_) => None,
}
}
pub const fn any_secs(&self) -> u64 {
match self {
UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc / 1_000_000,
}
}
}
pub struct Rtc {
utc_us: u64,
utc_us_persisted: u64,
granularity: GranularityEnum,
source: TimeSourceEnum,
anchor: Option<embassy_time::Instant>,
trusted_time_source: Option<TrustedTimeSource>,
}
impl Rtc {
#[inline(always)]
pub(crate) const fn new() -> Self {
Self {
utc_us: FIRMWARE_BUILD_MATTER_US,
utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
granularity: GranularityEnum::NoTimeGranularity,
source: TimeSourceEnum::None,
anchor: None,
trusted_time_source: None,
}
}
pub(crate) fn init() -> impl Init<Self> {
init!(Self {
utc_us: FIRMWARE_BUILD_MATTER_US,
utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
granularity: GranularityEnum::NoTimeGranularity,
source: TimeSourceEnum::None,
anchor: None,
trusted_time_source: None,
})
}
fn reset(&mut self) {
self.utc_us = FIRMWARE_BUILD_MATTER_US;
self.utc_us_persisted = FIRMWARE_BUILD_MATTER_US;
self.granularity = GranularityEnum::NoTimeGranularity;
self.source = TimeSourceEnum::None;
self.anchor = None;
self.trusted_time_source = None;
}
pub fn reset_persist<S: KvBlobStore>(
&mut self,
mut store: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
store.remove(LKG_UTC_KEY, buf)?;
store.remove(TRUSTED_TIME_SOURCE_KEY, buf)?;
Ok(())
}
pub fn load_persist<S: KvBlobStore>(&mut self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
self.reset();
if let Some(data) = kv.load(LKG_UTC_KEY, buf)? {
let stored = u64::from_tlv(&TLVElement::new(data))?;
let floor = FIRMWARE_BUILD_MATTER_US;
self.utc_us_persisted = stored;
self.utc_us = stored.max(floor);
}
if let Some(data) = kv.load(TRUSTED_TIME_SOURCE_KEY, buf)? {
self.trusted_time_source = Some(TrustedTimeSource::from_tlv(&TLVElement::new(data))?);
}
Ok(())
}
pub fn trusted_time_source(&self) -> Option<TrustedTimeSource> {
self.trusted_time_source
}
pub fn set_trusted_time_source<E: EventEmitter>(
&mut self,
source: Option<TrustedTimeSource>,
change_notifier: &dyn AttrChangeNotifier,
event_emitter: E,
) -> Result<(), Error> {
if self.trusted_time_source != source {
let previous = self.trusted_time_source;
self.trusted_time_source = source;
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::TrustedTimeSource as _,
);
if self.trusted_time_source.is_none() && previous.is_some() {
MissingTrustedTimeSource::emit_for(event_emitter, ROOT_ENDPOINT_ID, |b| b.end())?;
}
}
Ok(())
}
pub fn set_trusted_time_source_persist<S: KvBlobStoreAccess, E: EventEmitter>(
&mut self,
source: Option<TrustedTimeSource>,
persist: &mut Persist<S>,
change_notifier: &dyn AttrChangeNotifier,
event_emitter: E,
) -> Result<(), Error> {
if self.trusted_time_source != source {
self.set_trusted_time_source(source, change_notifier, event_emitter)?;
match source {
Some(source) => {
persist.store_tlv(TRUSTED_TIME_SOURCE_KEY, source)?;
}
None => {
persist.remove(TRUSTED_TIME_SOURCE_KEY)?;
}
}
}
Ok(())
}
pub fn utc_time(&self) -> UtcTime {
if let Some(anchor) = self.anchor {
let elapsed_us = embassy_time::Instant::now()
.checked_duration_since(anchor)
.map(|d| d.as_micros())
.unwrap_or(0);
UtcTime::Reliable(self.utc_us.saturating_add(elapsed_us))
} else {
UtcTime::LastKnown(self.utc_us)
}
}
pub fn utc_time_granularity(&self) -> GranularityEnum {
if self.anchor.is_some() {
self.granularity
} else {
GranularityEnum::NoTimeGranularity
}
}
pub fn utc_time_source(&self) -> TimeSourceEnum {
if self.anchor.is_some() {
self.source
} else {
TimeSourceEnum::None
}
}
pub fn set_utc_time(
&mut self,
utc_us: u64,
granularity: GranularityEnum,
source: TimeSourceEnum,
change_notifier: &dyn AttrChangeNotifier,
) -> bool {
let stepped = match granularity {
GranularityEnum::MicrosecondsGranularity => GranularityEnum::MillisecondsGranularity,
GranularityEnum::MillisecondsGranularity => GranularityEnum::SecondsGranularity,
GranularityEnum::SecondsGranularity => GranularityEnum::MinutesGranularity,
_ => GranularityEnum::MinutesGranularity,
};
let changed = self.utc_us != utc_us || self.granularity != stepped || self.source != source;
if changed || self.anchor.is_none() {
self.utc_us = utc_us;
self.granularity = stepped;
self.source = source;
self.anchor = Some(embassy_time::Instant::now());
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::UTCTime as _,
);
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::Granularity as _,
);
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::TimeSource as _,
);
}
changed
}
pub fn set_utc_time_persist<S: KvBlobStoreAccess>(
&mut self,
utc_us: u64,
granularity: GranularityEnum,
source: TimeSourceEnum,
persist: &mut Persist<S>,
change_notifier: &dyn AttrChangeNotifier,
) -> Result<(), Error> {
const DELTA: u64 = 24 * 60 * 60 * 1_000_000;
let delta = self.utc_us_persisted.abs_diff(utc_us);
self.set_utc_time(utc_us, granularity, source, change_notifier);
if delta >= DELTA {
info!("TimeSync: UTC time changed by more than a day, persisting");
persist.store_tlv(LKG_UTC_KEY, utc_us.to_le_bytes())?;
self.utc_us_persisted = utc_us;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromTLV, ToTLV)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TrustedTimeSource {
pub fab_idx: NonZeroU8,
pub node_id: NodeId,
pub endpoint: EndptId,
}
bitflags! {
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Options: u8 {
const TIME_ZONE = 0x1;
const NTP_CLIENT = 0x2;
const NTP_SERVER = 0x4;
const TIME_SYNC_CLIENT = 0x8;
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct TimeZoneEntry<'a> {
pub offset: i32,
pub valid_at: u64,
pub name: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct DSTOffsetEntry {
pub offset: i32,
pub valid_starting: u64,
pub valid_until: Option<u64>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct TrustedTimeSourceData {
pub fabric_index: u8,
pub node_id: u64,
pub endpoint: u16,
}
pub trait TimeSync {
fn default_ntp(&self) -> Result<Nullable<&str>, Error>;
fn supports_dns_resolve(&self) -> Result<bool, Error>;
fn ntp_server_available(&self) -> Result<bool, Error>;
fn time_zone(
&self,
_visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
) -> Result<(), Error>;
fn dst_offset(
&self,
_visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
) -> Result<(), Error>;
fn local_time(&self) -> Result<Nullable<u64>, Error>;
fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error>;
fn time_zone_list_max_size(&self) -> Result<u8, Error>;
fn dst_offset_list_max_size(&self) -> Result<u8, Error>;
fn set_time_zone(&self, _request: &SetTimeZoneRequest<'_>) -> Result<bool, Error>;
fn set_dst_offset(&self, _request: &SetDSTOffsetRequest<'_>) -> Result<(), Error>;
fn set_default_ntp(&self, _request: &SetDefaultNTPRequest<'_>) -> Result<(), Error>;
}
impl<T> TimeSync for &T
where
T: TimeSync,
{
fn default_ntp(&self) -> Result<Nullable<&str>, Error> {
(*self).default_ntp()
}
fn supports_dns_resolve(&self) -> Result<bool, Error> {
(*self).supports_dns_resolve()
}
fn ntp_server_available(&self) -> Result<bool, Error> {
(*self).ntp_server_available()
}
fn time_zone(
&self,
visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
) -> Result<(), Error> {
(*self).time_zone(visit)
}
fn dst_offset(
&self,
visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
) -> Result<(), Error> {
(*self).dst_offset(visit)
}
fn local_time(&self) -> Result<Nullable<u64>, Error> {
(*self).local_time()
}
fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
(*self).time_zone_database()
}
fn time_zone_list_max_size(&self) -> Result<u8, Error> {
(*self).time_zone_list_max_size()
}
fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
(*self).dst_offset_list_max_size()
}
fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
(*self).set_time_zone(request)
}
fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
(*self).set_dst_offset(request)
}
fn set_default_ntp(&self, request: &SetDefaultNTPRequest<'_>) -> Result<(), Error> {
(*self).set_default_ntp(request)
}
}
impl TimeSync for () {
fn default_ntp(&self) -> Result<Nullable<&str>, Error> {
Ok(Nullable::none())
}
fn supports_dns_resolve(&self) -> Result<bool, Error> {
Ok(false)
}
fn ntp_server_available(&self) -> Result<bool, Error> {
Ok(false)
}
fn time_zone(
&self,
_visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
) -> Result<(), Error> {
Ok(())
}
fn dst_offset(
&self,
_visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
) -> Result<(), Error> {
Ok(())
}
fn local_time(&self) -> Result<Nullable<u64>, Error> {
Ok(Nullable::none())
}
fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
Ok(TimeZoneDatabaseEnum::None)
}
fn time_zone_list_max_size(&self) -> Result<u8, Error> {
Ok(0)
}
fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
Ok(0)
}
fn set_time_zone(&self, _request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
Err(ErrorCode::CommandNotFound.into())
}
fn set_dst_offset(&self, _request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
Err(ErrorCode::CommandNotFound.into())
}
fn set_default_ntp(&self, _request: &SetDefaultNTPRequest<'_>) -> Result<(), Error> {
Err(ErrorCode::CommandNotFound.into())
}
}
const fn time_sync_attrs<const OPTS: u8>(attr: &Attribute, _: u16, _: u32) -> bool {
use AttributeId as A;
if !attr.quality.contains(Quality::OPTIONAL) {
return true;
}
if attr.id == A::TimeSource as u32 {
return true;
}
let opts = Options::from_bits_truncate(OPTS);
if opts.contains(Options::TIME_ZONE)
&& (attr.id == A::TimeZone as u32
|| attr.id == A::DSTOffset as u32
|| attr.id == A::LocalTime as u32
|| attr.id == A::TimeZoneDatabase as u32
|| attr.id == A::TimeZoneListMaxSize as u32
|| attr.id == A::DSTOffsetListMaxSize as u32)
{
return true;
}
if opts.contains(Options::NTP_CLIENT)
&& (attr.id == A::DefaultNTP as u32 || attr.id == A::SupportsDNSResolve as u32)
{
return true;
}
if opts.contains(Options::NTP_SERVER) && attr.id == A::NTPServerAvailable as u32 {
return true;
}
if opts.contains(Options::TIME_SYNC_CLIENT) && attr.id == A::TrustedTimeSource as u32 {
return true;
}
false
}
const fn time_sync_cmds<const OPTS: u8>(cmd: &Command, _: u16, _: u32) -> bool {
use CommandId as C;
if cmd.id == C::SetUTCTime as u32 {
return true;
}
let opts = Options::from_bits_truncate(OPTS);
if opts.contains(Options::TIME_ZONE)
&& (cmd.id == C::SetTimeZone as u32 || cmd.id == C::SetDSTOffset as u32)
{
return true;
}
if opts.contains(Options::NTP_CLIENT) && cmd.id == C::SetDefaultNTP as u32 {
return true;
}
if opts.contains(Options::TIME_SYNC_CLIENT) && cmd.id == C::SetTrustedTimeSource as u32 {
return true;
}
false
}
pub const fn cluster<const OPTS: u8>() -> Cluster<'static> {
let opts = Options::from_bits_truncate(OPTS);
let mut features = 0u32;
if opts.contains(Options::TIME_ZONE) {
features |= Feature::TIME_ZONE.bits();
}
if opts.contains(Options::NTP_CLIENT) {
features |= Feature::NTP_CLIENT.bits();
}
if opts.contains(Options::NTP_SERVER) {
features |= Feature::NTP_SERVER.bits();
}
if opts.contains(Options::TIME_SYNC_CLIENT) {
features |= Feature::TIME_SYNC_CLIENT.bits();
}
Cluster {
feature_map: features,
with_attrs: time_sync_attrs::<OPTS>,
with_cmds: time_sync_cmds::<OPTS>,
..FULL_CLUSTER
}
}
#[derive(Clone)]
pub struct TimeSyncHandler<'a> {
dataver: Dataver,
time_sync: &'a dyn TimeSync,
}
impl<'a> TimeSyncHandler<'a> {
pub const fn new(dataver: Dataver, time_sync: &'a dyn TimeSync) -> Self {
Self { dataver, time_sync }
}
pub const fn adapt(self) -> HandlerAdaptor<Self> {
HandlerAdaptor(self)
}
}
impl ClusterHandler for TimeSyncHandler<'_> {
const CLUSTER: Cluster<'static> = cluster::<0>();
fn dataver(&self) -> u32 {
self.dataver.get()
}
fn dataver_changed(&self) {
self.dataver.changed();
}
fn utc_time(&self, ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
Ok(Nullable::new(
ctx.matter()
.with_state(|state| state.rtc.utc_time())
.reliable(),
))
}
fn granularity(&self, ctx: impl ReadContext) -> Result<GranularityEnum, Error> {
Ok(ctx
.matter()
.with_state(|state| state.rtc.utc_time_granularity()))
}
fn time_source(&self, ctx: impl ReadContext) -> Result<TimeSourceEnum, Error> {
Ok(ctx.matter().with_state(|state| state.rtc.utc_time_source()))
}
fn trusted_time_source<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: NullableBuilder<P, TrustedTimeSourceStructBuilder<P>>,
) -> Result<P, Error> {
match ctx
.matter()
.with_state(|state| state.rtc.trusted_time_source())
{
Some(tts) => builder
.non_null()?
.fabric_index(tts.fab_idx.get())?
.node_id(tts.node_id)?
.endpoint(tts.endpoint)?
.end(),
None => builder.null(),
}
}
fn default_ntp<P: TLVBuilderParent>(
&self,
_ctx: impl ReadContext,
builder: NullableBuilder<P, Utf8StrBuilder<P>>,
) -> Result<P, Error> {
match self.time_sync.default_ntp()?.into_option() {
Some(s) => builder.non_null()?.set(s),
None => builder.null(),
}
}
fn supports_dns_resolve(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
self.time_sync.supports_dns_resolve()
}
fn ntp_server_available(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
self.time_sync.ntp_server_available()
}
fn time_zone<P: TLVBuilderParent>(
&self,
_ctx: impl ReadContext,
builder: ArrayAttributeRead<TimeZoneStructArrayBuilder<P>, TimeZoneStructBuilder<P>>,
) -> Result<P, Error> {
match builder {
ArrayAttributeRead::ReadAll(array) => {
let mut array_opt = Some(array);
self.time_sync.time_zone(&mut |entry| {
let array = unwrap!(array_opt.take());
let next = array
.push()?
.offset(entry.offset)?
.valid_at(entry.valid_at)?
.name(entry.name)?
.end()?;
array_opt = Some(next);
Ok(())
})?;
unwrap!(array_opt.take()).end()
}
ArrayAttributeRead::ReadOne(index, item_builder) => {
let mut item_opt = Some(item_builder);
let mut returned: Option<P> = None;
let mut current = 0u16;
self.time_sync.time_zone(&mut |entry| {
if returned.is_none() && current == index {
let b = unwrap!(item_opt.take());
returned = Some(
b.offset(entry.offset)?
.valid_at(entry.valid_at)?
.name(entry.name)?
.end()?,
);
}
current = current.saturating_add(1);
Ok(())
})?;
returned.ok_or_else(|| ErrorCode::ConstraintError.into())
}
ArrayAttributeRead::ReadNone(array) => array.end(),
}
}
fn dst_offset<P: TLVBuilderParent>(
&self,
_ctx: impl ReadContext,
builder: ArrayAttributeRead<DSTOffsetStructArrayBuilder<P>, DSTOffsetStructBuilder<P>>,
) -> Result<P, Error> {
match builder {
ArrayAttributeRead::ReadAll(array) => {
let mut array_opt = Some(array);
self.time_sync.dst_offset(&mut |entry| {
let array = unwrap!(array_opt.take());
let next = array
.push()?
.offset(entry.offset)?
.valid_starting(entry.valid_starting)?
.valid_until(Nullable::new(entry.valid_until))?
.end()?;
array_opt = Some(next);
Ok(())
})?;
unwrap!(array_opt.take()).end()
}
ArrayAttributeRead::ReadOne(index, item_builder) => {
let mut item_opt = Some(item_builder);
let mut returned: Option<P> = None;
let mut current = 0u16;
self.time_sync.dst_offset(&mut |entry| {
if returned.is_none() && current == index {
let b = unwrap!(item_opt.take());
returned = Some(
b.offset(entry.offset)?
.valid_starting(entry.valid_starting)?
.valid_until(Nullable::new(entry.valid_until))?
.end()?,
);
}
current = current.saturating_add(1);
Ok(())
})?;
returned.ok_or_else(|| ErrorCode::ConstraintError.into())
}
ArrayAttributeRead::ReadNone(array) => array.end(),
}
}
fn local_time(&self, _ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
self.time_sync.local_time()
}
fn time_zone_database(&self, _ctx: impl ReadContext) -> Result<TimeZoneDatabaseEnum, Error> {
self.time_sync.time_zone_database()
}
fn time_zone_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
self.time_sync.time_zone_list_max_size()
}
fn dst_offset_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
self.time_sync.dst_offset_list_max_size()
}
fn handle_set_utc_time(
&self,
ctx: impl InvokeContext,
request: SetUTCTimeRequest<'_>,
) -> Result<(), Error> {
let utc_us = request.utc_time()?;
let granularity = request.granularity()?;
ctx.matter().with_state(|state| {
state
.rtc
.set_utc_time(utc_us, granularity, TimeSourceEnum::Admin, &ctx)
});
Ok(())
}
fn handle_set_trusted_time_source(
&self,
ctx: impl InvokeContext,
request: SetTrustedTimeSourceRequest<'_>,
) -> Result<(), Error> {
let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::InvalidCommand)?;
let source = request
.trusted_time_source()?
.into_option()
.map(|tts| {
Ok::<_, Error>(TrustedTimeSource {
fab_idx,
node_id: tts.node_id()?,
endpoint: tts.endpoint()?,
})
})
.transpose()?;
let mut persist = Persist::new(ctx.kv());
ctx.matter().with_state(|state| {
state
.rtc
.set_trusted_time_source_persist(source, &mut persist, &ctx, &ctx)
})?;
persist.run()?;
Ok(())
}
fn handle_set_time_zone<P: TLVBuilderParent>(
&self,
_ctx: impl InvokeContext,
request: SetTimeZoneRequest<'_>,
response: SetTimeZoneResponseBuilder<P>,
) -> Result<P, Error> {
let dst_offset_required = self.time_sync.set_time_zone(&request)?;
response.dst_offset_required(dst_offset_required)?.end()
}
fn handle_set_dst_offset(
&self,
_ctx: impl InvokeContext,
request: SetDSTOffsetRequest<'_>,
) -> Result<(), Error> {
self.time_sync.set_dst_offset(&request)
}
fn handle_set_default_ntp(
&self,
_ctx: impl InvokeContext,
request: SetDefaultNTPRequest<'_>,
) -> Result<(), Error> {
self.time_sync.set_default_ntp(&request)
}
}
impl core::fmt::Debug for TimeSyncHandler<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("TimeSyncHandler")
.field("dataver", &self.dataver)
.finish()
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for TimeSyncHandler<'_> {
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "TimeSyncHandler {{ dataver: {} }}", self.dataver.get());
}
}