Skip to main content

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}