Skip to main content

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}