rs_matter/dm/clusters/time_sync/client.rs
1/*
2 *
3 * Copyright (c) 2026 Project CHIP Authors
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18//! Time Synchronization initiator-side client (Matter Core spec).
19//!
20//! Refreshes the device's
21//! [Last-Known-Good UTC Time](crate::Matter::last_known_utc_time) by
22//! opening a CASE-secured exchange to the configured
23//! [Trusted Time Source](crate::Matter::trusted_time_source) (set via
24//! the `SetTrustedTimeSource` command, Matter Core spec),
25//! reading its `UTCTime` attribute, and calling
26//! [`Matter::set_utc_time`] with the result.
27//!
28//! Drive it from your application's async runtime:
29//!
30//! ```ignore
31//! let client = TimeSyncClient::new(&matter, &crypto);
32//! let _ = client.run(
33//! embassy_time::Duration::from_secs(60 * 60),
34//! persist_access,
35//! &dm,
36//! ).await;
37//! ```
38
39use embassy_time::{Duration, Timer};
40
41use crate::crypto::Crypto;
42use crate::dm::clusters::decl::time_synchronization::{
43 GranularityEnum, TimeSourceEnum, TimeSynchronizationClient as _,
44};
45use crate::dm::AttrChangeNotifier;
46use crate::error::Error;
47use crate::persist::{KvBlobStoreAccess, Persist};
48use crate::transport::exchange::Exchange;
49use crate::Matter;
50
51/// Initiator-side TimeSync client. Periodically reads `UTCTime` from
52/// the configured trusted source and stores it as the device's new
53/// Last-Known-Good UTC Time (Matter Core spec).
54///
55/// Holds a borrow of the [`Matter`] instance for its lifetime; the
56/// `run` / `refresh_once` methods take the KV-store handle and the
57/// attribute-change notifier (typically your `InteractionModel`) by reference
58/// so the same client struct can be re-used across refresh cycles.
59pub struct TimeSyncClient<'a, C> {
60 matter: &'a Matter<'a>,
61 crypto: C,
62}
63
64impl<'a, C: Crypto> TimeSyncClient<'a, C> {
65 /// Create a new client bound to `matter`.
66 ///
67 /// `crypto` is needed because reading the trusted time source may require
68 /// establishing a fresh CASE session (via [`Exchange::initiate`]) when none
69 /// is cached.
70 pub const fn new(matter: &'a Matter<'a>, crypto: C) -> Self {
71 Self { matter, crypto }
72 }
73
74 /// Run the periodic refresh loop. Calls
75 /// [`Self::refresh_once`] every `period`; logs and continues on
76 /// any per-cycle error so a single bad exchange doesn't take the
77 /// task down. Never returns under normal operation.
78 pub async fn run<S, N>(&self, period: Duration, kv: S, notify: &N) -> Result<(), Error>
79 where
80 S: KvBlobStoreAccess,
81 N: AttrChangeNotifier,
82 {
83 loop {
84 let period = if let Err(e) = self.refresh_once(&kv, notify).await {
85 warn!("TimeSync client: refresh failed: {}", e);
86
87 // On error, retry sooner than the normal period to avoid
88 // long outages if the trusted source is temporarily
89 // unavailable.
90 Duration::from_secs(60).min(period)
91 } else {
92 period
93 };
94
95 Timer::after(period).await;
96 }
97 }
98
99 /// Perform a single refresh cycle.
100 ///
101 /// - If no Trusted Time Source is configured, returns `Ok(())` with
102 /// no action.
103 /// - Otherwise opens a CASE-secured initiator exchange to the
104 /// configured `(fab_idx, node_id)`, reads the `UTCTime` attribute
105 /// on the configured `endpoint`, and on a non-null result calls
106 /// [`Matter::set_utc_time`] with
107 /// `Granularity = SecondsGranularity` and
108 /// `TimeSource = NodeTimeCluster` (per spec — the source
109 /// that this device used to sync its time was another node's
110 /// TimeSync cluster).
111 pub async fn refresh_once<S, N>(&self, kv: S, notify: &N) -> Result<(), Error>
112 where
113 S: KvBlobStoreAccess,
114 N: AttrChangeNotifier,
115 {
116 let Some(tts) = self
117 .matter
118 .with_state(|state| state.rtc.trusted_time_source())
119 else {
120 // No trusted source configured — nothing to do.
121 return Ok(());
122 };
123
124 info!(
125 "TimeSync client: refreshing from fabric {}, node 0x{:016x}, endpoint {}",
126 tts.fab_idx, tts.node_id, tts.endpoint
127 );
128
129 let exchange =
130 Exchange::initiate(self.matter, &self.crypto, tts.fab_idx, tts.node_id).await?;
131
132 let result = exchange
133 .time_synchronization()
134 .utc_time_read(tts.endpoint)
135 .await?;
136
137 if let Some(utc_us) = result.into_option() {
138 let mut persist = Persist::new(kv);
139
140 self.matter.with_rtc(|rtc| {
141 rtc.set_utc_time_persist(
142 utc_us,
143 GranularityEnum::SecondsGranularity,
144 TimeSourceEnum::NodeTimeCluster,
145 &mut persist,
146 notify,
147 )
148 })?;
149
150 persist.run()?;
151
152 info!(
153 "TimeSync client: applied UTCTime = {} \u{00b5}s from trusted source",
154 utc_us
155 );
156 } else {
157 warn!("TimeSync client: trusted source returned null UTCTime");
158 }
159
160 Ok(())
161 }
162}