rs_matter/lib.rs
1/*
2 *
3 * Copyright (c) 2022-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//! Native Rust implementation of the Matter protocol by CSA-IOT.
19//!
20//! This crate implements the Matter specification that can be run on embedded devices
21//! to build Matter-compatible smart-home/IoT devices.
22//!
23//! Look at the examples project in the workspace for example applications built using this crate, and the tests directory for unit tests.
24//!
25//! Start off exploring by going to the [Matter] object.
26#![cfg_attr(not(feature = "std"), no_std)]
27#![allow(async_fn_in_trait)]
28#![allow(unknown_lints)]
29#![allow(clippy::uninlined_format_args)]
30#![recursion_limit = "1024"]
31
32use crate::crypto::Crypto;
33use crate::dm::clusters::basic_info::{
34 self, BasicInfoConfig, BasicInfoSettings, FULL_CLUSTER as BASIC_INFO_CLUSTER,
35};
36use crate::dm::clusters::dev_att::DeviceAttestation;
37use crate::dm::clusters::icd_mgmt::OperatingModeEnum;
38use crate::dm::clusters::time_sync::Rtc;
39use crate::dm::endpoints::ROOT_ENDPOINT_ID;
40use crate::dm::AttrChangeNotifier;
41use crate::error::{Error, ErrorCode};
42use crate::fabric::Fabrics;
43use crate::failsafe::FailSafe;
44use crate::pairing::qr::{
45 no_optional_data, CommFlowType, NoOptionalData, Qr, QrPayload, QrTextType,
46};
47use crate::pairing::DiscoveryCapabilities;
48use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist};
49#[cfg(feature = "case-resumption")]
50use crate::sc::case::ResumableSessions;
51use crate::sc::pase::spake2p::{Spake2pVerifierPassword, SPAKE2P_VERIFIER_SALT_ZEROED};
52use crate::sc::pase::{CommWindowState, Pase};
53use crate::transport::network::MatterLocalService;
54use crate::transport::network::{NetworkMulticast, NetworkReceive, NetworkSend};
55use crate::transport::session::Sessions;
56use crate::transport::{
57 PacketBufferExternalAccess, Transport, TransportRunner, MAX_RX_BUF_SIZE, MAX_TX_BUF_SIZE,
58};
59use crate::utils::cell::RefCell;
60use crate::utils::init::{init, Init};
61use crate::utils::storage::pooled::Buffers;
62use crate::utils::sync::blocking::Mutex;
63
64use rand_core::RngCore;
65
66#[cfg(feature = "alloc")]
67extern crate alloc;
68
69// This mod MUST go first, so that the others see its macros.
70pub(crate) mod fmt;
71
72pub mod acl;
73pub mod attest;
74pub mod bdx;
75pub mod cert;
76pub mod crypto;
77pub mod dm;
78pub mod error;
79pub mod fabric;
80pub mod failsafe;
81pub mod group_keys;
82pub mod im;
83pub mod onboard;
84pub mod pairing;
85pub mod persist;
86pub mod respond;
87pub mod sc;
88pub mod tlv;
89pub mod transport;
90pub mod utils;
91
92/// Re-export several crates
93///
94/// This is necessary for crates used in the code generated by the proc-macros
95pub mod reexport {
96 pub use bitflags;
97 #[cfg(feature = "defmt")]
98 pub use defmt;
99 #[cfg(feature = "log")]
100 pub use log;
101 pub use strum;
102}
103
104#[cfg(feature = "alloc")]
105#[macro_export]
106macro_rules! alloc {
107 ($val:expr) => {
108 alloc::boxed::Box::new($val)
109 };
110}
111
112#[cfg(not(feature = "alloc"))]
113#[macro_export]
114macro_rules! alloc {
115 ($val:expr) => {
116 $val
117 };
118}
119
120/// The Matter UDP port
121pub const MATTER_PORT: u16 = 5540;
122
123/// Device basic commissioning data
124#[derive(Debug, Clone)]
125#[cfg_attr(feature = "defmt", derive(defmt::Format))]
126pub struct BasicCommData {
127 /// The password which is necessary to authenticate the device in either
128 /// initial commissioning, or when the basic commissioning window is opened
129 pub password: Spake2pVerifierPassword,
130 /// The 12-bit discriminator used to differentiate between multiple devices
131 pub discriminator: u16,
132}
133
134/// The primary Matter Object
135pub struct Matter<'a> {
136 /// The internal state of the Matter Object, protected by a mutex for concurrent access from different threads and async tasks.
137 state: Mutex<RefCell<MatterState>>,
138 /// The transport state of the Matter Object
139 transport: Transport,
140 /// The basic information configuration for this Matter device
141 dev_det: &'a BasicInfoConfig<'a>,
142 /// The basic commissioning data for this Matter device
143 dev_comm: BasicCommData,
144 /// The device attestation data fetcher for this Matter device
145 dev_att: &'a dyn DeviceAttestation,
146 /// The port number on which the Matter stack will listen for incoming connections
147 port: u16,
148 /// The Groupcast testing-mode bridge - shared between the transport RX
149 /// path, the Interaction Model's invoke processing and the Groupcast
150 /// cluster handler. See `dm::clusters::groupcast::TestingBridge`.
151 #[cfg(feature = "groups")]
152 groupcast_testing: crate::dm::clusters::groupcast::TestingBridge,
153 /// The scratch buffer used by the key-value persistence machinery for
154 /// (de)serializing BLOBs. Behind a blocking mutex so [`Matter::kv`] can
155 /// recombine it with the user's raw [`KvBlobStore`](crate::persist::KvBlobStore)
156 /// into a full [`KvBlobStoreAccess`](crate::persist::KvBlobStoreAccess). Its
157 /// size is set by the `kv-blob-store-*` Cargo features (see
158 /// [`KV_BUF_SIZE`](crate::persist::KV_BUF_SIZE)).
159 kv_buf: Mutex<RefCell<[u8; crate::persist::KV_BUF_SIZE]>>,
160}
161
162impl<'a> Matter<'a> {
163 /// Create a new Matter object.
164 ///
165 /// # Parameters
166 /// * dev_det: An object of type [BasicInfoConfig].
167 /// * dev_comm: An object of type [BasicCommData]. This object contains the basic commissioning
168 /// data required for the device.
169 /// * dev_att: An object that implements the trait [DevAttDataFetcher]. Any Matter device
170 /// requires a set of device attestation certificates and keys. It is the responsibility of
171 /// this object to return the device attestation details when queried upon.
172 /// * port: The port number on which the Matter stack will listen for incoming connections.
173 #[inline(always)]
174 pub const fn new(
175 dev_det: &'a BasicInfoConfig<'a>,
176 dev_comm: BasicCommData,
177 dev_att: &'a dyn DeviceAttestation,
178 port: u16,
179 ) -> Self {
180 Self {
181 state: Mutex::new(RefCell::new(MatterState::new())),
182 transport: Transport::new(dev_det),
183 dev_det,
184 dev_comm,
185 dev_att,
186 port,
187 #[cfg(feature = "groups")]
188 groupcast_testing: crate::dm::clusters::groupcast::TestingBridge::new(),
189 kv_buf: Mutex::new(RefCell::new([0; crate::persist::KV_BUF_SIZE])),
190 }
191 }
192
193 /// Create an in-place initializer for a Matter object.
194 ///
195 /// # Parameters
196 /// * dev_det: An object of type [BasicInfoConfig].
197 /// * dev_comm: An object of type [BasicCommData]. This object contains the basic commissioning
198 /// data required for the device.
199 /// * dev_att: An object that implements the trait [DevAttDataFetcher]. Any Matter device
200 /// requires a set of device attestation certificates and keys. It is the responsibility of
201 /// this object to return the device attestation details when queried upon.
202 /// * port: The port number on which the Matter stack will listen for incoming connections.
203 pub fn init(
204 dev_det: &'a BasicInfoConfig<'a>,
205 dev_comm: BasicCommData,
206 dev_att: &'a dyn DeviceAttestation,
207 port: u16,
208 ) -> impl Init<Self> {
209 // Two variants because the `init!` macro does not accept `#[cfg]`
210 // on fields
211 #[cfg(feature = "groups")]
212 {
213 init!(
214 Self {
215 state <- Mutex::init(RefCell::init(MatterState::init())),
216 transport <- Transport::init(dev_det),
217 dev_det,
218 dev_comm,
219 dev_att,
220 port,
221 groupcast_testing <- crate::dm::clusters::groupcast::TestingBridge::init(),
222 kv_buf <- Mutex::init(RefCell::init(crate::utils::init::zeroed())),
223 }
224 )
225 }
226
227 #[cfg(not(feature = "groups"))]
228 {
229 init!(
230 Self {
231 state <- Mutex::init(RefCell::init(MatterState::init())),
232 transport <- Transport::init(dev_det),
233 dev_det,
234 dev_comm,
235 dev_att,
236 port,
237 kv_buf <- Mutex::init(RefCell::init(crate::utils::init::zeroed())),
238 }
239 )
240 }
241 }
242
243 pub fn dev_det(&self) -> &BasicInfoConfig<'_> {
244 self.dev_det
245 }
246
247 pub fn dev_att(&self) -> &dyn DeviceAttestation {
248 self.dev_att
249 }
250
251 pub fn dev_comm(&self) -> &BasicCommData {
252 &self.dev_comm
253 }
254
255 pub fn port(&self) -> u16 {
256 self.port
257 }
258
259 /// The ICD operating mode to advertise in the operational `ICD` DNS-SD TXT
260 /// key, or `None` when the device is not a Long-Idle-Time ICD.
261 pub fn icd_mode(&self) -> Option<OperatingModeEnum> {
262 self.with_state(|state| state.icd_mode)
263 }
264
265 /// Set the ICD operating mode advertised in mDNS and, if it changed, signal
266 /// the mDNS layer to re-publish.
267 pub fn set_icd_mode(&self, mode: Option<OperatingModeEnum>) {
268 let changed = self.with_state(|state| {
269 let changed = state.icd_mode != mode;
270 state.icd_mode = mode;
271 changed
272 });
273
274 if changed {
275 self.transport.notify_mdns_changed();
276 }
277 }
278
279 /// Combine a user-provided raw [`KvBlobStore`] with the scratch buffer owned
280 /// by this `Matter` object to obtain a full [`KvBlobStoreAccess`].
281 ///
282 /// This is the single entry point for persistence: the application passes its
283 /// raw store (sync `load`/`store`/`remove`) and gets back an access object
284 /// that recombines it with `Matter`'s feature-sized scratch buffer (see
285 /// [`KV_BUF_SIZE`](crate::persist::KV_BUF_SIZE)). The returned value is then
286 /// lent (by `&`) to [`Matter::startup`], [`Matter::factory_reset`]
287 /// and [`InteractionModel::new`](crate::im::InteractionModel::new).
288 ///
289 /// # Arguments
290 /// - `store` - the raw [`KvBlobStore`] implementation to wrap
291 pub fn kv<'s, S: KvBlobStore + 's>(&'s self, store: S) -> impl KvBlobStoreAccess + 's {
292 crate::persist::SharedKvBlobStore::new(store, &self.kv_buf)
293 }
294
295 /// Get a reference to the transport state of this Matter object.
296 ///
297 /// All transport-related state and operations (mDNS change/resolve
298 /// rendezvous, session/group notifications, RX/TX buffers, exchange
299 /// initiation/acceptance) live on [`Transport`].
300 #[inline(always)]
301 pub const fn transport(&self) -> &Transport {
302 &self.transport
303 }
304
305 pub fn transport_rx_buffer(&self) -> PacketBufferExternalAccess<'_, MAX_RX_BUF_SIZE> {
306 self.transport().rx_buffer()
307 }
308
309 pub fn transport_tx_buffer(&self) -> PacketBufferExternalAccess<'_, MAX_TX_BUF_SIZE> {
310 self.transport().tx_buffer()
311 }
312
313 /// A utility method to replace the initial Device Attestation with another one.
314 pub fn replace_dev_att(&mut self, dev_att: &'a dyn DeviceAttestation) {
315 self.dev_att = dev_att;
316 }
317
318 /// Print the standard QR code text to the console
319 ///
320 /// The printed QR code text corresponds to the standard commissioning flow (i.e. `CommFlowType::Standard`)
321 /// and contains no optional data.
322 ///
323 /// This method is useful primarily during development, when the Matter device is
324 /// attached to a console. It is expected that the developer will call this method prior to running the Matter transport.
325 ///
326 /// # Arguments
327 /// - `disc_caps`: The discovery capabilities to be used in the QR code payload
328 pub fn print_standard_qr_text(&self, disc_caps: DiscoveryCapabilities) -> Result<(), Error> {
329 let rx_buf = self.transport().rx_buffer();
330
331 let mut buf = rx_buf.get_immediate().ok_or(ErrorCode::NoMemory)?;
332 let buf = &mut *buf;
333
334 let payload = self.standard_qr_payload(disc_caps)?;
335
336 let (text, _) = payload.as_str(buf)?;
337
338 // Do not remove this logging line or change its formatting.
339 // C++ E2E tests rely on this log line to grep the QR code
340 info!("SetupQRCode: [{}]", text);
341
342 Ok(())
343 }
344
345 /// Print the standard QR code to the console
346 ///
347 /// The printed QR code corresponds to the standard commissioning flow (i.e. `CommFlowType::Standard`)
348 /// and contains no optional data.
349 ///
350 /// This method is useful primarily during development, when the Matter device is
351 /// attached to a console. It is expected that the developer will call this method prior to running the Matter transport.
352 ///
353 /// # Arguments
354 /// - `text_type`: The type of text representation to use when printing the QR code
355 /// - `disc_caps`: The discovery capabilities to be used in the QR code payload
356 pub fn print_standard_qr_code(
357 &self,
358 text_type: QrTextType,
359 disc_caps: DiscoveryCapabilities,
360 ) -> Result<(), Error> {
361 // Also print the pairing code for convenience
362 info!(
363 "PairingCode: [{}]",
364 self.dev_comm.compute_pretty_pairing_code()
365 );
366
367 let rx_buf = self.transport().rx_buffer();
368
369 let mut buf = rx_buf.get_immediate().ok_or(ErrorCode::NoMemory)?;
370 let buf = &mut *buf;
371
372 let payload = self.standard_qr_payload(disc_caps)?;
373
374 let (text, buf) = payload.as_str(buf)?;
375
376 let (tmp_buf, out_buf) = buf.split_at_mut(buf.len() / 2);
377
378 let qr = Qr::compute(text, tmp_buf, out_buf)?;
379
380 const BORDER_SIZE: u8 = 4;
381
382 for y in qr.lines_range(text_type, BORDER_SIZE) {
383 info!(
384 "{}",
385 qr.line_as_str(text_type, BORDER_SIZE, false, false, y, tmp_buf)?
386 .0
387 );
388 }
389
390 Ok(())
391 }
392
393 /// Return the standard QR code payload
394 ///
395 /// The returned QR code payload corresponds to the standard commissioning flow (i.e. `CommFlowType::Standard`)
396 /// and contains no optional data.
397 ///
398 /// # Arguments
399 /// - `disc_caps`: The discovery capabilities to be used in the QR code payload
400 fn standard_qr_payload(
401 &self,
402 disc_caps: DiscoveryCapabilities,
403 ) -> Result<QrPayload<'_, NoOptionalData>, Error> {
404 let payload = QrPayload::new_from_basic_info(
405 disc_caps,
406 CommFlowType::Standard,
407 self.dev_comm.clone(),
408 self.dev_det,
409 no_optional_data as _,
410 );
411
412 Ok(payload)
413 }
414
415 /// Return `true` if there is at least one fabric.
416 ///
417 /// Note that this is emphatically *not* "the device is commissioned": a fabric is created
418 /// as soon as `AddNOC` is received, which is well before the commissioner has established a
419 /// CASE session over the operational network and sent `CommissioningComplete`. Code that
420 /// needs to know whether commissioning is still in progress should use
421 /// [`Matter::is_comm_window_open`] instead, as the commissioning window stays open for exactly
422 /// that long.
423 pub fn has_fabrics(&self) -> bool {
424 self.with_state(|state| state.fabrics.iter().next().is_some())
425 }
426
427 /// Return the state of the commissioning window - whether one is open, and if so who
428 /// opened it. See [`CommWindowState`] for what the answer is good for.
429 pub fn comm_window_state(&self) -> CommWindowState {
430 self.with_state(|state| state.pase.comm_window_state())
431 }
432
433 /// Open a basic commissioning window
434 ///
435 /// The method will return an error if the commissioning window cannot be opened
436 /// (due to another window already being opened, for example).
437 ///
438 /// # Arguments
439 /// - `timeout_secs`: The timeout in seconds for the basic commissioning window
440 ///
441 /// **Note:** This is the low-level building block that mutates PASE
442 /// state and routes a `notify_cluster_changed(...)` to subscribers
443 /// via `notify`, but does **not** bump the per-cluster `Dataver` of
444 /// `AdministratorCommissioning` — a subsequent dataver-filtered
445 /// read could therefore cache-hit and miss the change. Application
446 /// code that holds a `InteractionModel` should prefer
447 /// [`crate::im::InteractionModel::open_basic_comm_window`], which delegates
448 /// here and additionally bumps dataver via its
449 /// [`AttrChangeNotifier`] impl.
450 pub fn open_basic_comm_window<C: Crypto>(
451 &self,
452 timeout_secs: u16,
453 crypto: C,
454 notify: &dyn AttrChangeNotifier,
455 ) -> Result<(), Error> {
456 let notify_mdns = || self.transport().notify_mdns_changed();
457 let notify_change = |endpt_id, clust_id| notify.notify_cluster_changed(endpt_id, clust_id);
458
459 self.with_state(|state| {
460 let mut rand = crypto.rand()?;
461
462 let mdns_id = rand.next_u64();
463
464 let mut salt = SPAKE2P_VERIFIER_SALT_ZEROED;
465 rand.fill_bytes(salt.access_mut());
466
467 state.pase.open_basic_comm_window(
468 mdns_id,
469 salt.access(),
470 self.dev_comm.password.reference(),
471 self.dev_comm.discriminator,
472 timeout_secs,
473 None,
474 notify_mdns,
475 notify_change,
476 )
477 })
478 }
479
480 /// Close the commissioning window (basic or other)
481 ///
482 /// The method will return Ok(false) if there is no active PASE commissioning window to close.
483 ///
484 /// **Note:** As with [`Matter::open_basic_comm_window`], this does
485 /// not bump the per-cluster `Dataver` of
486 /// `AdministratorCommissioning`. Prefer
487 /// [`crate::im::InteractionModel::close_comm_window`] when a `InteractionModel`
488 /// is available.
489 pub fn close_comm_window(&self, notify: &dyn AttrChangeNotifier) -> Result<bool, Error> {
490 let notify_mdns = || self.transport().notify_mdns_changed();
491 let notify_change = |endpt_id, clust_id| notify.notify_cluster_changed(endpt_id, clust_id);
492
493 self.with_state(|state| state.pase.close_comm_window(notify_mdns, notify_change))
494 }
495
496 /// Bump `BasicInformation::ConfigurationVersion` by one, persist
497 /// the new value via `kv`, and route an attribute-change
498 /// notification to subscribers via `notify`.
499 ///
500 /// Per Matter Core Spec, the device MUST bump
501 /// this attribute on any change to its exposed fixed-quality
502 /// surface (a firmware update that adds or removes functionality,
503 /// internal reconfiguration that changes any `F`-quality attribute,
504 /// bridged-node add/remove on a bridge). `rs-matter` cannot detect
505 /// such events on its own — the application drives the bump.
506 ///
507 /// **Note:** Like the other low-level mutators on `Matter`, this
508 /// does **not** bump the per-cluster `Dataver` of
509 /// `BasicInformation`. A subsequent dataver-filtered read could
510 /// therefore cache-hit and miss the change. Application code that
511 /// holds a `InteractionModel` should prefer
512 /// [`crate::im::InteractionModel::bump_configuration_version`], which
513 /// delegates here and additionally bumps dataver via its
514 /// [`AttrChangeNotifier`] impl.
515 ///
516 /// Returns the new `ConfigurationVersion` value.
517 pub fn bump_configuration_version<S: KvBlobStoreAccess>(
518 &self,
519 kv: S,
520 notify: &dyn AttrChangeNotifier,
521 ) -> Result<u32, Error> {
522 let mut persist = Persist::new(kv);
523
524 let new_version = self.with_state(|state| {
525 let new_version = state.basic_info_settings.bump_configuration_version();
526
527 state.basic_info_settings.store_persist(&mut persist)?;
528
529 notify.notify_attr_changed(
530 ROOT_ENDPOINT_ID,
531 BASIC_INFO_CLUSTER.id,
532 basic_info::AttributeId::ConfigurationVersion as _,
533 );
534
535 Ok::<_, Error>(new_version)
536 })?;
537
538 persist.run()?;
539
540 Ok(new_version)
541 }
542
543 /// Create a new transport runner instance
544 pub fn transport_runner<C: Crypto>(&self, crypto: C) -> TransportRunner<'_, C> {
545 TransportRunner::new(self, crypto)
546 }
547
548 /// Run the Matter transport layer.
549 ///
550 /// # Arguments
551 /// - `crypto`: The crypto backend
552 /// - `send`: The network send interface
553 /// - `recv`: The network receive interface
554 /// - `multicast`: The multicast network interface (for receiving groupcast messages)
555 /// When running on top of non-IP networks like BLE pass a no-op implementation like `NoNetwork` here and the multicast functionality will be disabled.
556 pub async fn run<C, S, R, M>(
557 &self,
558 crypto: C,
559 send: S,
560 recv: R,
561 multicast: M,
562 ) -> Result<(), Error>
563 where
564 C: Crypto,
565 S: NetworkSend,
566 R: NetworkReceive,
567 M: NetworkMulticast,
568 {
569 let mut transport_runner = self.transport_runner(crypto);
570
571 transport_runner.run(send, recv, multicast).await
572 }
573
574 /// Access the Matter state by invoking a closure with a mutable reference to the state.
575 pub fn with_state<F, R>(&self, f: F) -> R
576 where
577 F: FnOnce(&mut MatterState) -> R,
578 {
579 self.state.lock(|state| {
580 let mut state = state.borrow_mut();
581 f(&mut state)
582 })
583 }
584
585 /// Return the Groupcast testing-mode bridge.
586 #[cfg(feature = "groups")]
587 pub(crate) fn groupcast_testing(&self) -> &crate::dm::clusters::groupcast::TestingBridge {
588 &self.groupcast_testing
589 }
590
591 /// Access the Real-Time-clock by invoking a closure with a mutable reference to it.
592 pub fn with_rtc<F, R>(&self, f: F) -> R
593 where
594 F: FnOnce(&mut Rtc) -> R,
595 {
596 self.with_state(|state| f(&mut state.rtc))
597 }
598
599 /// Reset the transport layer by clearing all sessions, exchanges, the RX buffer and the TX buffer
600 /// NOTE: User should be careful _not_ to call this method while the transport layer and/or the built-in mDNS is running.
601 pub fn reset_transport(&self) -> Result<(), Error> {
602 self.with_state(|state| {
603 state.sessions.reset();
604
605 self.transport().reset()
606 })
607 }
608
609 /// Factory-reset the `Matter` persistable state by removing all fabrics and
610 /// resetting the basic info settings, the RTC state and (if compiled in) the
611 /// CASE resumption cache and the group data message counter - both in-memory
612 /// and in the provided KV store.
613 ///
614 /// The counterpart of [`Matter::startup`]. Call when the node is
615 /// factory-reset, alongside
616 /// [`InteractionModel::factory_reset`](crate::im::InteractionModel::factory_reset).
617 ///
618 /// Arguments:
619 /// - `kv`: The key-value store access (obtained via [`Matter::kv`]) to remove the fabrics
620 /// and basic info settings from. Provides both the store and the scratch buffer.
621 pub fn factory_reset<K: KvBlobStoreAccess>(&self, kv: K) -> Result<(), Error> {
622 self.with_state(|state| {
623 // The KV ops are sync, so do them all inside a single `access` closure.
624 kv.access(|mut store, buf| {
625 state.fabrics.reset_persist(&mut store, buf)?;
626 state.basic_info_settings.reset_persist(&mut store, buf)?;
627 state.rtc.reset_persist(&mut store, buf)?;
628 #[cfg(feature = "case-resumption")]
629 state.resumption.reset_persist(&mut store, buf)?;
630 #[cfg(feature = "groups")]
631 state.sessions.reset_persist(&mut store, buf)?;
632
633 Ok::<_, Error>(())
634 })
635 })?;
636
637 self.transport().notify_mdns_changed();
638
639 Ok(())
640 }
641
642 /// Re-hydrate the `Matter` persistable state - the fabrics, the basic info
643 /// settings, the RTC state and (if compiled in) the CASE resumption cache
644 /// and the group data message counter - from the provided KV store.
645 ///
646 /// Call once at startup, before the transport starts serving traffic. The
647 /// Data-Model counterpart is
648 /// [`InteractionModel::startup`](crate::im::InteractionModel::startup).
649 ///
650 /// Arguments:
651 /// - `kv`: The key-value store access (obtained via [`Matter::kv`]) to load the fabrics
652 /// and basic info settings from. Provides both the store and the scratch buffer.
653 pub fn startup<K: KvBlobStoreAccess>(&self, kv: K) -> Result<(), Error> {
654 self.with_state(|state| {
655 // The KV ops are sync, so do them all inside a single `access` closure.
656 kv.access(|mut store, buf| {
657 state.fabrics.load_persist(&mut store, buf)?;
658 state.basic_info_settings.load_persist(&mut store, buf)?;
659 state.rtc.load_persist(&mut store, buf)?;
660 #[cfg(feature = "case-resumption")]
661 state.resumption.load_persist(&mut store, buf)?;
662 #[cfg(feature = "groups")]
663 state.sessions.load_persist(&mut store, buf)?;
664
665 Ok::<_, Error>(())
666 })
667 })?;
668
669 self.transport().notify_mdns_changed();
670
671 Ok(())
672 }
673
674 /// Background task that flushes the CASE session resumption cache
675 /// to persistent storage whenever it is mutated.
676 ///
677 /// The task waits for a mutation event fired by the CASE handshake
678 /// paths (both full handshake and resumption), sleeps for
679 /// `min_interval` so a burst of handshakes coalesces into one
680 /// write, and then serialises the current cache to `kv` under
681 /// [`crate::persist::CASE_RESUMPTION_KEY`].
682 ///
683 /// `min_interval` therefore doubles as (a) a burst-coalescing
684 /// debounce and (b) a hard lower bound on the time between two
685 /// consecutive persists — a mutation‑storm cannot drive more than
686 /// one write per `min_interval` regardless of arrival rate. Pick
687 /// something appropriate for the underlying medium:
688 ///
689 /// - POSIX / dev builds: anything from a few hundred ms upwards is
690 /// fine; `Duration::from_millis(500)` matches the pre‑existing
691 /// hard‑coded behaviour.
692 /// - MCU firmwares writing to internal flash: prefer tens of
693 /// seconds (e.g. `Duration::from_secs(30)`) so a
694 /// commissioner-driven wave of CASE handshakes doesn't churn the
695 /// flash write endurance budget. The cache is a soft cache — a
696 /// power cut that loses the most recent entry only forces a
697 /// full CASE handshake the next time the peer connects, which
698 /// is the same fallback any resumption failure produces.
699 ///
700 /// Intended to be spawned alongside [`Matter::run`], for example
701 /// via `embassy_futures::select` or a dedicated executor task. It
702 /// never returns on the happy path — a `Result` is only produced
703 /// if the KV backend errors.
704 ///
705 /// # Arguments
706 /// - `kv`: The key-value store access (obtained via [`Matter::kv`])
707 /// used to persist the cache. Provides both the store and the
708 /// scratch buffer.
709 /// - `min_interval`: Minimum time between two consecutive persists
710 /// (see above).
711 #[cfg(feature = "case-resumption")]
712 pub async fn run_persist_resumption<K: KvBlobStoreAccess>(
713 &self,
714 kv: K,
715 min_interval: embassy_time::Duration,
716 ) -> Result<(), Error> {
717 loop {
718 self.transport().wait_resumption_dirty().await;
719 embassy_time::Timer::after(min_interval).await;
720
721 self.with_state(|state| {
722 kv.access(|mut store, buf| state.resumption.store_persist(&mut store, buf))
723 })?;
724
725 debug!("CASE session resumption cache persisted");
726 }
727 }
728
729 /// Invoke the given closure for each currently published Matter mDNS service.
730 pub fn mdns_services<F>(&self, mut f: F) -> Result<(), Error>
731 where
732 F: FnMut(MatterLocalService) -> Result<(), Error>,
733 {
734 debug!("=== Currently published mDNS services");
735
736 self.with_state(|state| {
737 if let Some(comm_window) = state.pase.comm_window() {
738 // Do not remove this logging line or change its formatting.
739 // C++ E2E tests rely on this log line to determine when the mDNS service is published
740 debug!("mDNS service published: {:?}", comm_window.mdns_service());
741
742 f(comm_window.mdns_service())?;
743 }
744
745 for fabric in state.fabrics.iter() {
746 if let Some(service) = fabric.mdns_service() {
747 // Do not remove this logging line or change its formatting.
748 // C++ E2E tests rely on this log line to determine when the mDNS service is published
749 debug!("mDNS service published: {:?}", service);
750
751 f(service)?;
752 }
753 }
754
755 debug!("===");
756
757 Ok(())
758 })
759 }
760}
761
762/// The internal state of the Matter Object
763///
764/// Public for unit tests.
765pub struct MatterState {
766 /// All fabrics
767 ///
768 /// Public for unit tests
769 pub fabrics: Fabrics,
770 /// All sessions
771 sessions: Sessions,
772 /// CASE session resumption cache
773 ///
774 /// Public for unit tests
775 #[cfg(feature = "case-resumption")]
776 pub resumption: ResumableSessions,
777 /// The PASE session state
778 pase: Pase,
779 /// The Failsafe state
780 failsafe: FailSafe,
781 /// The mutable basic information settings
782 basic_info_settings: BasicInfoSettings,
783 /// Real Time Clock state and Last-Known-Good UTC Time tracking (Matter Core spec).
784 rtc: Rtc,
785 /// The ICD operating mode advertised in the operational `ICD` DNS-SD TXT key.
786 /// The ICD Management handler keeps this in sync with its registration set.
787 icd_mode: Option<OperatingModeEnum>,
788}
789
790impl MatterState {
791 /// Create a new instance of MatterState
792 #[inline(always)]
793 const fn new() -> Self {
794 Self {
795 fabrics: Fabrics::new(),
796 sessions: Sessions::new(),
797 #[cfg(feature = "case-resumption")]
798 resumption: ResumableSessions::new(),
799 pase: Pase::new(),
800 failsafe: FailSafe::new(),
801 basic_info_settings: BasicInfoSettings::new(),
802 rtc: Rtc::new(),
803 icd_mode: None,
804 }
805 }
806
807 /// Return an in-place initializer for MatterState
808 // NOTE: `init!` (pinned-init) rejects `#[cfg]` on its fields, so the
809 // `case-resumption` field forces two full variants of this initializer.
810 #[cfg(feature = "case-resumption")]
811 fn init() -> impl Init<Self> {
812 init!(Self {
813 fabrics <- Fabrics::init(),
814 sessions <- Sessions::init(),
815 resumption <- crate::sc::case::ResumableSessions::init(),
816 pase <- Pase::init(),
817 failsafe <- FailSafe::init(),
818 basic_info_settings <- BasicInfoSettings::init(),
819 rtc <- Rtc::init(),
820 icd_mode: None,
821 })
822 }
823
824 /// Return an in-place initializer for MatterState
825 #[cfg(not(feature = "case-resumption"))]
826 fn init() -> impl Init<Self> {
827 init!(Self {
828 fabrics <- Fabrics::init(),
829 sessions <- Sessions::init(),
830 pase <- Pase::init(),
831 failsafe <- FailSafe::init(),
832 basic_info_settings <- BasicInfoSettings::init(),
833 rtc <- Rtc::init(),
834 icd_mode: None,
835 })
836 }
837}
838
839#[cfg(test)]
840pub mod test {
841 use crate::Matter;
842
843 pub fn test_matter() -> Matter<'static> {
844 Matter::new(
845 &crate::dm::devices::test::TEST_DEV_DET,
846 crate::dm::devices::test::TEST_DEV_COMM,
847 &crate::dm::devices::test::TEST_DEV_ATT,
848 0,
849 )
850 }
851}