Skip to main content

rs_matter/dm/clusters/
basic_info.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//! This module contains the implementation of the Basic Information cluster and its handler.
19
20use core::str::FromStr;
21
22use crate::dm::{Cluster, Dataver, InvokeContext, ReadContext, WriteContext};
23use crate::error::{Error, ErrorCode};
24use crate::fabric::MAX_FABRICS;
25use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist, BASIC_INFO_KEY};
26use crate::tlv::{
27    FromTLV, Nullable, NullableBuilder, TLVBuilderParent, TLVElement, ToTLV, Utf8StrBuilder,
28};
29use crate::transport::exchange::Exchange;
30use crate::transport::session::MAX_SESSIONS;
31use crate::utils::bitflags::bitflags;
32use crate::utils::init::{init, Init};
33use crate::{except, with};
34
35pub use crate::dm::clusters::decl::basic_information::*;
36pub use crate::dm::clusters::decl::general_commissioning::RegulatoryLocationTypeEnum;
37pub use crate::dm::clusters::decl::globals::{
38    AreaTypeTag, LocationDescriptorStruct, LocationDescriptorStructBuilder,
39};
40
41/// The default Matter App Clusters specification version
42///
43/// Currently set to V1.6.0.0.
44pub const DEFAULT_MATTER_SPEC_VERSION: u32 = 0x01060000;
45
46/// The default Matter Data Model revision
47///
48/// Currently set to V21, which was released with Matter Core spec V1.6
49pub const DEFAULT_DATA_MODEL_REVISION: u16 = 21;
50
51/// The default maximum number of paths that can be included in an Invoke request
52///
53/// Set to 5, which is enough to support typical batched invokes while
54/// keeping the in-memory CommandRef tracking buffer in `dm::invoke()` small.
55pub const DEFAULT_MAX_PATHS_PER_INVOKE: u16 = 5;
56
57bitflags! {
58    #[repr(transparent)]
59    #[derive(Default)]
60    #[cfg_attr(not(feature = "defmt"), derive(Debug, Copy, Clone, Eq, PartialEq, Hash))]
61    pub struct PairingHintFlags: u32 {
62        /// Power Cycle False The Device will automatically enter Commissioning Mode upon
63        /// power cycle (unplug/replug, remove/re-insert batteries).
64        /// This bit SHALL be set to 1 for devices using Standard Commissioning Flow,
65        /// and set to 0 otherwise.
66        const POWER_CYCLE = 0x0000_0001;
67        /// This SHALL be set to 1 for devices requiring Custom Commissioning
68        /// Flow before they can be available for Commissioning by any Commissioner.
69        /// For such a flow, the user SHOULD be sent to the URL specified in the
70        /// CommissioningCustomFlowUrl of the DeviceModel schema entry indexed by the
71        /// Vendor ID and Product ID (e.g., as found in the announcement) in the
72        /// Distributed Compliance Ledger.
73        const DEV_MANUFACTURER_URL = 0x0000_0002;
74        /// The Device has been commissioned. Any Administrator that commissioned the
75        /// device provides a user interface that may be used to put the device
76        /// into Commissioning Mode.
77        const ADMINISTRATOR = 0x0000_0004;
78        /// The settings menu on the Device provides instructions to put it
79        /// into Commissioning Mode.
80        const SETTINGS_MENU = 0x0000_0008;
81        /// The PI key/value pair describes a custom way to put the Device into
82        /// Commissioning Mode. This Custom Instruction option is NOT recommended
83        /// for use by a Device that does not have knowledge of the user's language preference.
84        const CUSTOM_INSTRUCTION = 0x0000_0010;
85        /// The Device Manual provides special instructions to put the Device
86        /// into Commissioning Mode (see "UserManualUrl" in the Core Spec).
87        /// This is a catchall option to capture user interactions that are not codified by
88        /// other options in this flags type.
89        const DEVICE_MANUAL = 0x0000_0020;
90        /// The Device will enter Commissioning Mode when reset button is pressed.
91        const PRESS_RESET_BUTTON = 0x0000_0040;
92        /// The Device will enter Commissioning Mode when reset button is pressed when applying power to it.
93        const PRESS_RESET_BUTTON_WITH_POWER = 0x0000_0080;
94        /// The Device will enter Commissioning Mode when reset button is pressed for N seconds.
95        /// The exact value of N SHALL be made available via PI key.
96        const PRESS_RESET_BUTTON_FOR_N_SECONDS = 0x0000_0100;
97        /// The Device will enter Commissioning Mode when reset button is pressed until associated light blinks.
98        /// Information on color of light MAY be made available via PI key.
99        const PRESS_RESET_BUTTON_UNTIL_LIGHT_BLINKS = 0x0000_0200;
100        /// The Device will enter Commissioning Mode when reset button is pressed for N seconds
101        /// when applying power to it. The exact value of N SHALL be made available via PI key.
102        const PRESS_RESET_BUTTON_FOR_N_SECONDS_WITH_POWER = 0x0000_0400;
103        /// The Device will enter Commissioning Mode when reset button is pressed until associated
104        /// light blinks when applying power to the Device. Information on color of light MAY be
105        /// made available via PI key.
106        const PRESS_RESET_BUTTON_UNTIL_LIGHT_BLINKS_WITH_POWER = 0x0000_0800;
107        /// The Device will enter Commissioning Mode when reset button is pressed N times
108        /// with maximum 1 second between each press. The exact value of N SHALL be made available via PI key.
109        const PRESS_RESET_BUTTON_N_TIMES = 0x0000_1000;
110        /// The Device will enter Commissioning Mode when setup button is pressed.
111        const PRESS_SETUP_BUTTON = 0x0000_2000;
112        /// The Device will enter Commissioning Mode when setup button is pressed when applying power to it.
113        const PRESS_SETUP_BUTTON_WITH_POWER = 0x0000_4000;
114        /// The Device will enter Commissioning Mode when setup button is pressed for N seconds.
115        /// The exact value of N SHALL be made available via PI key.
116        const PRESS_SETUP_BUTTON_FOR_N_SECONDS = 0x0000_8000;
117        /// The Device will enter Commissioning Mode when setup button is pressed until associated
118        /// light blinks. Information on color of light MAY be made available via PI key.
119        const PRESS_SETUP_BUTTON_UNTIL_LIGHT_BLINKS = 0x0001_0000;
120        /// The Device will enter Commissioning Mode when setup button is pressed for N seconds
121        /// when applying power to it. The exact value of N SHALL be made available via PI key.
122        const PRESS_SETUP_BUTTON_FOR_N_SECONDS_WITH_POWER = 0x0002_0000;
123        /// The Device will enter Commissioning Mode when setup button is pressed until associated
124        /// light blinks when applying power to the Device. Information on color of light MAY be
125        /// made available via PI key.
126        const PRESS_SETUP_BUTTON_UNTIL_LIGHT_BLINKS_WITH_POWER = 0x0004_0000;
127        /// The Device will enter Commissioning Mode when setup button is pressed N times with
128        /// maximum 1 second between each press. The exact value of N SHALL be made available via PI key.
129        const PRESS_SETUP_BUTTON_N_TIMES = 0x0008_0000;
130    }
131}
132
133/// Factory-default value for the `BasicInformation::DeviceLocation`
134/// attribute - the borrowed, const-constructible counterpart of
135/// [`DeviceLocation`], suitable for embedding in [`BasicInfoConfig`].
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
137#[cfg_attr(feature = "defmt", derive(defmt::Format))]
138pub struct DeviceLocationConfig<'a> {
139    /// Free-form location description, at most 128 bytes.
140    pub location_name: &'a str,
141    /// Floor number, when applicable.
142    pub floor_number: Option<i16>,
143    /// Area type from the common Area namespace.
144    pub area_type: Option<AreaTypeTag>,
145}
146
147/// Basic information which is immutable
148/// (i.e. valid for the lifetime of the device firmware)
149///
150/// Note that some of the fields will be reported only if their corresponding optional attributes are enabled.
151///
152/// By default, `BasicInfoHandler::CLUSTER` enables ALL optional attributes except `reachable` which is only valid for
153/// bridged devices.
154#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
155#[cfg_attr(feature = "defmt", derive(defmt::Format))]
156pub struct BasicInfoConfig<'a> {
157    /// Vendor name (up to 32 characters)
158    pub vendor_name: &'a str,
159    /// Vendor ID
160    pub vid: u16,
161    /// Product name (up to 32 characters)
162    pub product_name: &'a str,
163    /// Product ID
164    pub pid: u16,
165    /// Hardware version
166    pub hw_ver: u16,
167    /// Hardware version string (up to 64 characters)
168    pub hw_ver_str: &'a str,
169    /// Software version
170    pub sw_ver: u32,
171    /// Software version string (up to 64 characters)
172    pub sw_ver_str: &'a str,
173    /// Manufacturing date (up to 16 characters)
174    pub manufacturing_date: &'a str,
175    /// Part number (up to 32 characters)
176    pub part_number: &'a str,
177    /// Product URL (up to 256 characters)
178    pub product_url: &'a str,
179    /// Product label (up to 64 characters)
180    pub product_label: &'a str,
181    /// Serial number (up to 32 characters)
182    pub serial_no: &'a str,
183    /// Unique ID (up to 64 characters)
184    pub unique_id: &'a str,
185    /// Factory default for the runtime-mutable, persisted
186    /// `BasicInformation::Location` attribute
187    pub location: Option<&'a str>,
188    /// Factory default for the runtime-mutable, persisted
189    /// `BasicInformation::DeviceLocation` attribute
190    pub device_location: Option<DeviceLocationConfig<'a>>,
191    /// Capability Minima
192    pub capability_minima: CapabilityMinima,
193    /// Product Appearance
194    pub product_appearance: ProductAppearance,
195    /// Specification Version
196    pub specification_version: u32,
197    /// Data Model Revision
198    pub data_model_revision: u16,
199    /// Max Paths Per Invoke
200    pub max_paths_per_invoke: u16,
201    /// Device Name
202    ///
203    /// Not a real attribute; used in the mDNS commissioning advertisement
204    pub device_name: &'a str,
205    /// Device Type
206    ///
207    /// Not a real attribute; used in the mDNS commissioning advertisement
208    pub device_type: Option<u16>,
209    /// Pairing Hint
210    ///
211    /// Not a real attribute; used in the mDNS commissioning advertisement
212    pub pairing_hint: PairingHintFlags,
213    /// Pairing Instruction
214    ///
215    /// Not a real attribute; used in the mDNS commissioning advertisement
216    pub pairing_instruction: &'a str,
217    /// Session Active Interval in ms
218    /// If not specified, defaults to 300
219    ///
220    /// Per the Matter Core Spec, the value is a 32-bit unsigned integer and
221    /// SHALL NOT exceed 3,600,000 (1 hour in milliseconds).
222    ///
223    /// Not a real attribute, just used to configure the session timeouts
224    pub sai: Option<u32>,
225    /// Session Idle Interval in ms
226    /// If not specified, defaults to 5000
227    ///
228    /// Per the Matter Core Spec, the value is a 32-bit unsigned integer and
229    /// SHALL NOT exceed 3,600,000 (1 hour in milliseconds).
230    ///
231    /// Not a real attribute, just used to configure the session timeouts
232    pub sii: Option<u32>,
233    /// Whether the device supports TCP transport.
234    ///
235    /// Not a real attribute; advertised via the `T` TXT record key in mDNS.
236    /// Per the Matter Core Spec, `T` is a bitmap: bit 2 (value 4)
237    /// indicates TCP server support. Required for large payloads such as WebRTC SDP
238    /// exchanges and camera snapshots.
239    pub tcp_supported: bool,
240}
241
242impl BasicInfoConfig<'_> {
243    pub const fn new() -> Self {
244        Self {
245            vid: 0,
246            pid: 0,
247            hw_ver: 0,
248            hw_ver_str: "",
249            sw_ver: 0,
250            sw_ver_str: "",
251            serial_no: "",
252            product_name: "",
253            vendor_name: "",
254            manufacturing_date: "",
255            part_number: "",
256            product_url: "",
257            product_label: "",
258            unique_id: "",
259            location: None,
260            device_location: None,
261            capability_minima: CapabilityMinima::new(),
262            product_appearance: ProductAppearance::new(),
263            specification_version: DEFAULT_MATTER_SPEC_VERSION,
264            data_model_revision: DEFAULT_DATA_MODEL_REVISION,
265            max_paths_per_invoke: DEFAULT_MAX_PATHS_PER_INVOKE,
266            device_name: "",
267            device_type: None,
268            pairing_hint: PairingHintFlags::empty(),
269            pairing_instruction: "",
270            sai: None,
271            sii: None,
272            tcp_supported: false,
273        }
274    }
275}
276
277impl Default for BasicInfoConfig<'_> {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283/// Capability Minima as reported in the Basic Information cluster
284#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
285#[cfg_attr(feature = "defmt", derive(defmt::Format))]
286pub struct CapabilityMinima {
287    /// Maximum CASE sessions per fabric
288    pub case_sessions_per_fabric: u16,
289    /// Maximum subscriptions per fabric
290    pub subscriptions_per_fabric: u16,
291    /// Maximum concurrent Invoke interactions processed before the node may
292    /// start answering with `BUSY`
293    pub simultaneous_invocations_supported: u16,
294    /// Minimum concurrent Write interactions the node can process
295    pub simultaneous_writes_supported: u16,
296    /// Maximum number of read paths (`AttributePathIB` + `EventPathIB`) the
297    /// node guarantees to process in a single Read Request
298    pub read_paths_supported: u16,
299    /// Maximum number of subscribe paths (`AttributePathIB` + `EventPathIB`)
300    /// the node guarantees to process in a single Subscribe Request
301    pub subscribe_paths_supported: u16,
302}
303
304/// The Matter spec mandates `CapabilityMinima.SubscriptionsPerFabric >= 3`.
305/// rs-matter sizes its default subscription table as `MAX_FABRICS * 3` (see
306/// [`DEFAULT_MAX_SUBSCRIPTIONS`](crate::im::subscriptions::DEFAULT_MAX_SUBSCRIPTIONS)),
307/// so this per-fabric minimum is what the device guarantees.
308const SUBSCRIPTIONS_PER_FABRIC: u16 = 3;
309
310/// Constraint is `1 to 10000`.
311const SIMULTANEOUS_INVOCATIONS_SUPPORTED: u16 = 1;
312/// Constraint is `1 to 10000`.
313const SIMULTANEOUS_WRITES_SUPPORTED: u16 = 1;
314/// Constraint is `9 to 10000`; see Core spec 2.11.2.1 "Read Interaction Limits".
315const READ_PATHS_SUPPORTED: u16 = 9;
316/// Constraint is `3 to 10000`; see Core spec 2.11.2.2 "Subscribe Interaction Limits".
317const SUBSCRIBE_PATHS_SUPPORTED: u16 = 3;
318
319impl CapabilityMinima {
320    /// Create a default instance of `CapabilityMinima`, with CASE sessions per
321    /// fabric derived from the session table and the spec-minimum subscriptions
322    /// per fabric.
323    pub const fn new() -> Self {
324        Self {
325            case_sessions_per_fabric: (MAX_SESSIONS / MAX_FABRICS) as _,
326            subscriptions_per_fabric: SUBSCRIPTIONS_PER_FABRIC,
327            simultaneous_invocations_supported: SIMULTANEOUS_INVOCATIONS_SUPPORTED,
328            simultaneous_writes_supported: SIMULTANEOUS_WRITES_SUPPORTED,
329            read_paths_supported: READ_PATHS_SUPPORTED,
330            subscribe_paths_supported: SUBSCRIBE_PATHS_SUPPORTED,
331        }
332    }
333}
334
335impl Default for CapabilityMinima {
336    fn default() -> Self {
337        Self::new()
338    }
339}
340
341/// Product Appearance as reported in the Basic Information cluster
342#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
343#[cfg_attr(feature = "defmt", derive(defmt::Format))]
344pub struct ProductAppearance {
345    /// Product finish type
346    pub finish: ProductFinishEnum,
347    /// Product primary color
348    pub color: Option<ColorEnum>,
349}
350
351impl ProductAppearance {
352    /// Create a default instance of `ProductAppearance`,
353    /// with `Other` finish and no color.
354    pub const fn new() -> Self {
355        Self {
356            finish: ProductFinishEnum::Other,
357            color: None,
358        }
359    }
360}
361
362impl Default for ProductAppearance {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368/// Owned mirror of the `LocationDescriptorStruct` global type, as stored for
369/// the `BasicInformation::DeviceLocation` attribute.
370#[derive(Debug, Clone, Eq, PartialEq, Hash, ToTLV, FromTLV)]
371#[cfg_attr(feature = "defmt", derive(defmt::Format))]
372pub struct DeviceLocation {
373    /// Free-form location description, at most 128 bytes.
374    pub location_name: heapless::String<128>,
375    /// Floor number, when applicable; `Null` on the wire when `None`.
376    pub floor_number: Option<i16>,
377    /// Area type from the common Area namespace; `Null` on the wire when
378    /// `None`.
379    pub area_type: Option<AreaTypeTag>,
380}
381
382impl DeviceLocation {
383    /// Create an empty `DeviceLocation` (empty location name, no floor
384    /// number, no area type).
385    pub const fn new() -> Self {
386        Self {
387            location_name: heapless::String::new(),
388            floor_number: None,
389            area_type: None,
390        }
391    }
392}
393
394impl Default for DeviceLocation {
395    fn default() -> Self {
396        Self::new()
397    }
398}
399
400/// Mutable basic information
401#[derive(Debug, Clone, Eq, PartialEq, Hash, ToTLV, FromTLV)]
402#[cfg_attr(feature = "defmt", derive(defmt::Format))]
403pub struct BasicInfoSettings {
404    pub node_label: heapless::String<32>, // Max node-label as per the spec
405    pub location: Option<heapless::String<2>>, // Max location as per the spec
406    /// The regulatory location type, as set via
407    /// `GeneralCommissioning::SetRegulatoryConfig`.
408    pub location_type: Option<RegulatoryLocationTypeEnum>,
409    pub local_config_disabled: bool,
410    /// `BasicInformation::ConfigurationVersion` (Matter Core Spec).
411    /// Non-volatile, monotonically increasing, minimum 1.
412    /// Bumped by application code via
413    /// `InteractionModel::bump_configuration_version` whenever the node's
414    /// fixed-quality surface (Server/Parts list, device types, software
415    /// version, …) changes — see Matter Core Spec.
416    pub configuration_version: u32,
417    /// `BasicInformation::DeviceLocation` (provisional in the Matter 1.6
418    /// IDL): where the device is installed, admin-writable.
419    pub device_location: Option<Nullable<DeviceLocation>>,
420    /// `GeneralCommissioning::RecoveryIdentifier` (provisional in Matter 1.6):
421    /// a random 64-bit value that identifies this node during the
422    /// Network Recovery flow without revealing its Node ID.
423    ///
424    /// NOTE: keep this field *last* - the persisted-blob TLV tags are
425    /// positional, and appending preserves compatibility with blobs written
426    /// before the field existed.
427    pub recovery_identifier: Option<u64>,
428}
429
430impl BasicInfoSettings {
431    /// Create a new instance of `BasicInfoSettings`
432    pub const fn new() -> Self {
433        Self {
434            node_label: heapless::String::new(),
435            location: None,
436            location_type: None,
437            local_config_disabled: false,
438            // Spec fallback for `ConfigurationVersion` is 1 (`min 1`,
439            // Core Spec).
440            configuration_version: 1,
441            device_location: None,
442            recovery_identifier: None,
443        }
444    }
445
446    /// Return an in-place initializer for `BasicInfoSettings`
447    pub fn init() -> impl Init<Self> {
448        init!(Self {
449            node_label: heapless::String::new(),
450            location: None,
451            location_type: None,
452            local_config_disabled: false,
453            configuration_version: 1,
454            device_location: None,
455            recovery_identifier: None,
456        })
457    }
458
459    /// Resets the basic info to initial values
460    ///
461    /// # Arguments
462    /// - `flag_changed`: whether to mark the basic info settings as changed
463    pub fn reset(&mut self) {
464        self.node_label.clear();
465        self.location = None;
466        self.location_type = None;
467        self.local_config_disabled = false;
468        self.configuration_version = 1;
469        self.device_location = None;
470        self.recovery_identifier = None;
471    }
472
473    /// Bump `ConfigurationVersion` by one and return the new value.
474    ///
475    /// Saturates at `u32::MAX` (the spec gives no wrap semantics, so
476    /// staying at the max is safer than rolling over to 0 which would
477    /// violate the `min 1` constraint).
478    ///
479    /// This routine only mutates the in-memory value. Persistence and
480    /// subscriber notification are the caller's responsibility — use
481    /// `InteractionModel::bump_configuration_version` for the full
482    /// "bump + persist + notify + dataver-bump" pass.
483    pub fn bump_configuration_version(&mut self) -> u32 {
484        self.configuration_version = self.configuration_version.saturating_add(1);
485        self.configuration_version
486    }
487
488    /// Set the location (country code) to the given (2-character) value.
489    pub fn set_location(&mut self, location: &str) {
490        self.location = Some(unwrap!(heapless::String::<2>::from_str(location)));
491    }
492
493    /// Remove all basic info settings from the provided BLOB store as well as from memory
494    ///
495    /// # Arguments
496    /// - `store`: the BLOB store to remove the settings from
497    /// - `buf`: a temporary buffer to use for removing the settings
498    pub fn reset_persist<S: KvBlobStore>(
499        &mut self,
500        mut store: S,
501        buf: &mut [u8],
502    ) -> Result<(), Error> {
503        self.reset();
504
505        store.remove(BASIC_INFO_KEY, buf)?;
506
507        info!("Removed basic info settings from storage");
508
509        Ok(())
510    }
511
512    /// Load basic info settings from the provided byte slice
513    pub fn load(&mut self, data: &[u8]) -> Result<(), Error> {
514        let info = Self::from_tlv(&TLVElement::new(data))?;
515
516        self.node_label = info.node_label;
517        self.location = info.location;
518        self.location_type = info.location_type;
519        self.local_config_disabled = info.local_config_disabled;
520        self.configuration_version = info.configuration_version;
521        self.device_location = info.device_location;
522        self.recovery_identifier = info.recovery_identifier;
523
524        Ok(())
525    }
526
527    /// Store the basic info settings via the provided `Persist` instance
528    ///
529    /// # Arguments
530    /// - `persist`: the `Persist` instance to serialize the settings into
531    ///
532    /// Deliberately outlined (`inline(never)`): the settings' TLV
533    /// serialization is sizeable, and every runtime-mutable-attribute setter
534    /// ends with this call - sharing a single copy keeps it out of each
535    /// attribute-dispatch path (flash size).
536    #[inline(never)]
537    pub fn store_persist<S: KvBlobStoreAccess>(
538        &self,
539        persist: &mut Persist<S>,
540    ) -> Result<(), Error> {
541        persist.store_tlv(BASIC_INFO_KEY, self)
542    }
543
544    /// Load all basic info settings from the provided BLOB store
545    ///
546    /// # Arguments
547    /// - `store`: the BLOB store to load the fabrics from
548    /// - `buf`: a temporary buffer to use for loading the fabrics
549    pub fn load_persist<S: KvBlobStore>(
550        &mut self,
551        mut store: S,
552        buf: &mut [u8],
553    ) -> Result<(), Error> {
554        self.reset();
555
556        if let Some(data) = store.load(BASIC_INFO_KEY, buf)? {
557            self.load(data)?;
558
559            info!("Loaded basic info settings from storage");
560        }
561
562        Ok(())
563    }
564}
565
566impl Default for BasicInfoSettings {
567    fn default() -> Self {
568        Self::new()
569    }
570}
571
572/// The system implementation of a handler for the Basic Information Matter cluster.
573#[derive(Clone, Debug)]
574#[cfg_attr(feature = "defmt", derive(defmt::Format))]
575pub struct BasicInfoHandler(Dataver);
576
577impl BasicInfoHandler {
578    /// Create a new instance of `BasicInfoHandler` with the given `Dataver`
579    pub fn new(dataver: Dataver) -> Self {
580        Self(dataver)
581    }
582
583    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
584    pub const fn adapt(self) -> HandlerAdaptor<Self> {
585        HandlerAdaptor(self)
586    }
587
588    fn config<'a>(exchange: &'a Exchange) -> &'a BasicInfoConfig<'a> {
589        exchange.matter().dev_det()
590    }
591
592    fn with_settings<F, R>(exchange: &Exchange, f: F) -> Result<R, Error>
593    where
594        F: FnOnce(&mut BasicInfoSettings) -> Result<R, Error>,
595    {
596        exchange.with_state(|state| f(&mut state.basic_info_settings))
597    }
598}
599
600/// `BasicInformation` cluster metadata that additionally advertises the
601/// provisional `DeviceLocation` attribute (admin-writable, persisted in
602/// [`BasicInfoSettings`]).
603///
604/// Use this in place of [`BasicInfoHandler::CLUSTER`] to serve the attribute -
605/// the handler itself always implements it. It is deliberately not the
606/// default: the attribute is provisional, and reference test suites that pin
607/// `BasicInformation::AttributeList` to an exact set reject its presence.
608///
609/// The initial value is `Null`; set [`BasicInfoConfig::device_location`] for
610/// a device whose location is factory-provisioned.
611pub const CLUSTER_DEVICE_LOCATION: Cluster<'static> = FULL_CLUSTER
612    .with_attrs(except!(AttributeId::Reachable))
613    .with_cmds(with!());
614
615impl ClusterHandler for BasicInfoHandler {
616    const CLUSTER: Cluster<'static> = FULL_CLUSTER
617        // Hide `Reachable` (TODO) from the default metadata.
618        //
619        // `DeviceLocation` IS implemented (see `device_location` /
620        // `set_device_location` below) but stays out of the default metadata
621        // because it is provisional: upstream's reference apps do not
622        // advertise it, and test suites that pin `AttributeList` to an exact
623        // set (e.g. the `TestBasicInformation` YAML) reject its presence.
624        // Opt in with [`CLUSTER_DEVICE_LOCATION`].
625        .with_attrs(except!(
626            AttributeId::Reachable | AttributeId::DeviceLocation
627        ))
628        .with_cmds(with!());
629
630    fn dataver(&self) -> u32 {
631        self.0.get()
632    }
633
634    fn dataver_changed(&self) {
635        self.0.changed();
636    }
637
638    fn data_model_revision(&self, ctx: impl ReadContext) -> Result<u16, Error> {
639        Ok(Self::config(ctx.exchange()).data_model_revision)
640    }
641
642    fn vendor_id(&self, ctx: impl ReadContext) -> Result<u16, Error> {
643        Ok(Self::config(ctx.exchange()).vid)
644    }
645
646    fn vendor_name<P: TLVBuilderParent>(
647        &self,
648        ctx: impl ReadContext,
649        out: Utf8StrBuilder<P>,
650    ) -> Result<P, Error> {
651        out.set(Self::config(ctx.exchange()).vendor_name)
652    }
653
654    fn product_id(&self, ctx: impl ReadContext) -> Result<u16, Error> {
655        Ok(Self::config(ctx.exchange()).pid)
656    }
657
658    fn product_name<P: TLVBuilderParent>(
659        &self,
660        ctx: impl ReadContext,
661        out: Utf8StrBuilder<P>,
662    ) -> Result<P, Error> {
663        out.set(Self::config(ctx.exchange()).product_name)
664    }
665
666    fn hardware_version(&self, ctx: impl ReadContext) -> Result<u16, Error> {
667        Ok(Self::config(ctx.exchange()).hw_ver)
668    }
669
670    fn hardware_version_string<P: TLVBuilderParent>(
671        &self,
672        ctx: impl ReadContext,
673        out: Utf8StrBuilder<P>,
674    ) -> Result<P, Error> {
675        out.set(Self::config(ctx.exchange()).hw_ver_str)
676    }
677
678    fn software_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
679        Ok(Self::config(ctx.exchange()).sw_ver)
680    }
681
682    fn software_version_string<P: TLVBuilderParent>(
683        &self,
684        ctx: impl ReadContext,
685        out: Utf8StrBuilder<P>,
686    ) -> Result<P, Error> {
687        out.set(Self::config(ctx.exchange()).sw_ver_str)
688    }
689
690    fn node_label<P: TLVBuilderParent>(
691        &self,
692        ctx: impl ReadContext,
693        out: Utf8StrBuilder<P>,
694    ) -> Result<P, Error> {
695        Self::with_settings(ctx.exchange(), |settings| {
696            out.set(settings.node_label.as_str())
697        })
698    }
699
700    fn set_node_label(&self, ctx: impl WriteContext, label: &str) -> Result<(), Error> {
701        if label.len() > 32 {
702            return Err(ErrorCode::ConstraintError.into());
703        }
704
705        let mut persist = Persist::new(ctx.kv());
706
707        Self::with_settings(ctx.exchange(), |settings| {
708            settings.node_label.clear();
709            settings
710                .node_label
711                .push_str(label)
712                .map_err(|_| ErrorCode::ConstraintError)?;
713
714            settings.store_persist(&mut persist)
715        })?;
716
717        persist.run()
718    }
719
720    fn location<P: TLVBuilderParent>(
721        &self,
722        ctx: impl ReadContext,
723        out: Utf8StrBuilder<P>,
724    ) -> Result<P, Error> {
725        // Until a value is set at runtime (`Location` write /
726        // `SetRegulatoryConfig`), report the factory default from
727        // `BasicInfoConfig`, falling back to the "unknown" country code.
728        let config = Self::config(ctx.exchange());
729
730        Self::with_settings(ctx.exchange(), |settings| {
731            out.set(
732                settings
733                    .location
734                    .as_deref()
735                    .or(config.location)
736                    .unwrap_or("XX"),
737            )
738        })
739    }
740
741    fn set_location(&self, ctx: impl WriteContext, location: &str) -> Result<(), Error> {
742        if location.len() != 2 {
743            return Err(ErrorCode::ConstraintError.into());
744        }
745
746        let mut persist = Persist::new(ctx.kv());
747
748        Self::with_settings(ctx.exchange(), |settings| {
749            settings.set_location(location);
750
751            settings.store_persist(&mut persist)
752        })?;
753
754        persist.run()
755    }
756
757    fn capability_minima<P: TLVBuilderParent>(
758        &self,
759        ctx: impl ReadContext,
760        builder: CapabilityMinimaStructBuilder<P>,
761    ) -> Result<P, Error> {
762        let cm = Self::config(ctx.exchange()).capability_minima;
763
764        builder
765            .case_sessions_per_fabric(cm.case_sessions_per_fabric)?
766            .subscriptions_per_fabric(cm.subscriptions_per_fabric)?
767            .simultaneous_invocations_supported(Some(cm.simultaneous_invocations_supported))?
768            .simultaneous_writes_supported(Some(cm.simultaneous_writes_supported))?
769            .read_paths_supported(Some(cm.read_paths_supported))?
770            .subscribe_paths_supported(Some(cm.subscribe_paths_supported))?
771            .end()
772    }
773
774    fn specification_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
775        Ok(Self::config(ctx.exchange()).specification_version)
776    }
777
778    fn max_paths_per_invoke(&self, ctx: impl ReadContext) -> Result<u16, Error> {
779        Ok(Self::config(ctx.exchange()).max_paths_per_invoke)
780    }
781
782    fn configuration_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
783        // Non-volatile, runtime-mutable. Lives in `BasicInfoSettings`,
784        // bumped via `InteractionModel::bump_configuration_version`.
785        Self::with_settings(
786            ctx.exchange(),
787            |settings| Ok(settings.configuration_version),
788        )
789    }
790
791    // Deliberately outlined (`inline(never)`): inlining duplicates the
792    // builder chain in every read-dispatch instantiation (flash size)
793    #[inline(never)]
794    fn device_location<P: TLVBuilderParent>(
795        &self,
796        ctx: impl ReadContext,
797        builder: NullableBuilder<P, LocationDescriptorStructBuilder<P>>,
798    ) -> Result<P, Error> {
799        let config = Self::config(ctx.exchange());
800
801        Self::with_settings(ctx.exchange(), |settings| {
802            // An admin-written value - including an explicitly-written `Null`
803            // - takes precedence; until one exists, report the factory
804            // default from `BasicInfoConfig`, falling back to `Null`.
805            let (location_name, floor_number, area_type) = match settings.device_location.as_ref() {
806                Some(location) => match location.as_opt_ref() {
807                    Some(location) => (
808                        location.location_name.as_str(),
809                        location.floor_number,
810                        location.area_type,
811                    ),
812                    None => return builder.null(),
813                },
814                None => match config.device_location {
815                    Some(location) => (
816                        location.location_name,
817                        location.floor_number,
818                        location.area_type,
819                    ),
820                    None => return builder.null(),
821                },
822            };
823
824            builder
825                .non_null()?
826                .location_name(location_name)?
827                .floor_number(Nullable::new(floor_number))?
828                .area_type(Nullable::new(area_type))?
829                .end()
830        })
831    }
832
833    // Deliberately outlined (`inline(never)`): cold path with sizeable
834    // TLV-parsing and persistence code (flash size)
835    #[inline(never)]
836    fn set_device_location(
837        &self,
838        ctx: impl WriteContext,
839        value: Nullable<LocationDescriptorStruct<'_>>,
840    ) -> Result<(), Error> {
841        let mut persist = Persist::new(ctx.kv());
842
843        Self::with_settings(ctx.exchange(), |settings| {
844            if let Some(value) = value.as_opt_ref() {
845                // Parse and validate everything up-front, so that a failed
846                // write leaves the stored value intact
847                let location_name = value.location_name()?;
848                if location_name.len() > 128 {
849                    return Err(ErrorCode::ConstraintError.into());
850                }
851
852                let floor_number = value.floor_number()?.into_option();
853                let area_type = value.area_type()?.into_option();
854
855                // ... and then update the stored value in-place: going
856                // through an owned `DeviceLocation` temporary would move the
857                // ~150-byte value several times through the stack (flash and
858                // stack size)
859                let location = settings.device_location.get_or_insert_with(Nullable::none);
860                if location.is_none() {
861                    *location = Nullable::some(DeviceLocation::new());
862                }
863
864                let location = unwrap!(location.as_opt_mut());
865
866                location.location_name.clear();
867                unwrap!(location.location_name.push_str(location_name));
868                location.floor_number = floor_number;
869                location.area_type = area_type;
870            } else {
871                settings.device_location = Some(Nullable::none());
872            }
873
874            settings.store_persist(&mut persist)
875        })?;
876
877        persist.run()
878    }
879
880    fn handle_mfg_specific_ping(&self, _ctx: impl InvokeContext) -> Result<(), Error> {
881        Err(ErrorCode::CommandNotFound.into())
882    }
883
884    fn manufacturing_date<P: TLVBuilderParent>(
885        &self,
886        ctx: impl ReadContext,
887        builder: Utf8StrBuilder<P>,
888    ) -> Result<P, Error> {
889        builder.set(Self::config(ctx.exchange()).manufacturing_date)
890    }
891
892    fn part_number<P: TLVBuilderParent>(
893        &self,
894        ctx: impl ReadContext,
895        builder: Utf8StrBuilder<P>,
896    ) -> Result<P, Error> {
897        builder.set(Self::config(ctx.exchange()).part_number)
898    }
899
900    fn product_url<P: TLVBuilderParent>(
901        &self,
902        ctx: impl ReadContext,
903        builder: Utf8StrBuilder<P>,
904    ) -> Result<P, Error> {
905        builder.set(Self::config(ctx.exchange()).product_url)
906    }
907
908    fn product_label<P: TLVBuilderParent>(
909        &self,
910        ctx: impl ReadContext,
911        builder: Utf8StrBuilder<P>,
912    ) -> Result<P, Error> {
913        builder.set(Self::config(ctx.exchange()).product_label)
914    }
915
916    fn serial_number<P: TLVBuilderParent>(
917        &self,
918        ctx: impl ReadContext,
919        builder: Utf8StrBuilder<P>,
920    ) -> Result<P, Error> {
921        builder.set(Self::config(ctx.exchange()).serial_no)
922    }
923
924    fn local_config_disabled(&self, ctx: impl ReadContext) -> Result<bool, Error> {
925        Self::with_settings(
926            ctx.exchange(),
927            |settings| Ok(settings.local_config_disabled),
928        )
929    }
930
931    fn set_local_config_disabled(&self, ctx: impl WriteContext, value: bool) -> Result<(), Error> {
932        let mut persist = Persist::new(ctx.kv());
933
934        Self::with_settings(ctx.exchange(), |settings| {
935            settings.local_config_disabled = value;
936
937            settings.store_persist(&mut persist)
938        })?;
939
940        persist.run()
941    }
942
943    fn unique_id<P: TLVBuilderParent>(
944        &self,
945        ctx: impl ReadContext,
946        builder: Utf8StrBuilder<P>,
947    ) -> Result<P, Error> {
948        builder.set(Self::config(ctx.exchange()).unique_id)
949    }
950
951    fn product_appearance<P: TLVBuilderParent>(
952        &self,
953        ctx: impl ReadContext,
954        builder: ProductAppearanceStructBuilder<P>,
955    ) -> Result<P, Error> {
956        let appearance = Self::config(ctx.exchange()).product_appearance;
957
958        builder
959            .finish(appearance.finish)?
960            .primary_color(Nullable::new(appearance.color))?
961            .end()
962    }
963}
964
965#[cfg(test)]
966mod tests {
967    use super::{BasicInfoSettings, DeviceLocation};
968
969    use crate::tlv::{Nullable, TLVElement, TLVTag, ToTLV};
970    use crate::utils::storage::WriteBuf;
971
972    /// Round-trip `settings` through the persisted-blob TLV representation.
973    fn round_trip(settings: &BasicInfoSettings) -> BasicInfoSettings {
974        let mut buf = [0; 512];
975        let mut wb = WriteBuf::new(&mut buf);
976        settings.to_tlv(&TLVTag::Anonymous, &mut wb).unwrap();
977
978        let mut loaded = BasicInfoSettings::new();
979        loaded.load(wb.as_slice()).unwrap();
980
981        loaded
982    }
983
984    /// An explicit `XX` ("unknown country") write must be stored - and
985    /// persisted - verbatim: `None` means "never configured" (and lets the
986    /// `Location` attribute report the `BasicInfoConfig::location` factory
987    /// default), which an explicit `XX` write is not.
988    #[test]
989    fn explicit_xx_location_is_kept() {
990        let mut settings = BasicInfoSettings::new();
991        assert!(settings.location.is_none());
992
993        settings.set_location("XX");
994        assert_eq!(settings.location.as_deref(), Some("XX"));
995        assert_eq!(round_trip(&settings).location.as_deref(), Some("XX"));
996    }
997
998    /// The three `device_location` states - never-written / explicit `Null` /
999    /// value - must survive the persisted-blob round-trip, or an admin
1000    /// -written `Null` would resurrect the factory default after a reboot.
1001    #[test]
1002    fn device_location_states_survive_persistence() {
1003        let mut settings = BasicInfoSettings::new();
1004        assert!(round_trip(&settings).device_location.is_none());
1005
1006        settings.device_location = Some(Nullable::none());
1007        let loaded = round_trip(&settings);
1008        assert!(matches!(&loaded.device_location, Some(l) if l.is_none()));
1009
1010        settings.device_location = Some(Nullable::some(DeviceLocation {
1011            location_name: "Basement".try_into().unwrap(),
1012            floor_number: Some(-1),
1013            area_type: None,
1014        }));
1015        let loaded = round_trip(&settings);
1016        let location = loaded.device_location.unwrap().into_option().unwrap();
1017        assert_eq!(location.location_name, "Basement");
1018        assert_eq!(location.floor_number, Some(-1));
1019        assert_eq!(location.area_type, None);
1020    }
1021
1022    /// Settings predating the `device_location` field (or a factory-fresh
1023    /// blob) must load as "never written".
1024    #[test]
1025    fn missing_device_location_field_loads_as_unset() {
1026        // A blob serialized without the trailing `device_location` field:
1027        // emulate by truncating... simpler - serialize a fresh settings
1028        // (which encodes the field as absent) and check the tri-state.
1029        let settings = BasicInfoSettings::new();
1030        let loaded = round_trip(&settings);
1031        assert!(loaded.device_location.is_none());
1032
1033        // And `reset()` returns every runtime-configured value to
1034        // "never written", so the factory defaults apply again.
1035        let mut settings = BasicInfoSettings::new();
1036        settings.set_location("US");
1037        settings.location_type = Some(super::RegulatoryLocationTypeEnum::Indoor);
1038        settings.device_location = Some(Nullable::none());
1039        settings.reset();
1040        assert!(settings.location.is_none());
1041        assert!(settings.location_type.is_none());
1042        assert!(settings.device_location.is_none());
1043    }
1044
1045    /// The `GeneralCommissioning::RecoveryIdentifier` value must survive the
1046    /// persisted-blob round-trip (stable across reboots, per Matter Core Spec
1047    /// 11.10.6.11), load as "never minted" from a blob that predates the field,
1048    /// and be cleared by `reset()` so a factory reset regenerates it.
1049    #[test]
1050    fn recovery_identifier_survives_persistence_and_resets() {
1051        // Factory-fresh (and blobs predating the field) load as "never minted".
1052        let mut settings = BasicInfoSettings::new();
1053        assert!(settings.recovery_identifier.is_none());
1054        assert!(round_trip(&settings).recovery_identifier.is_none());
1055
1056        // A minted value round-trips verbatim.
1057        settings.recovery_identifier = Some(0x1122_3344_5566_7788);
1058        assert_eq!(
1059            round_trip(&settings).recovery_identifier,
1060            Some(0x1122_3344_5566_7788)
1061        );
1062
1063        // Factory reset clears it so the next read mints a fresh one.
1064        settings.reset();
1065        assert!(settings.recovery_identifier.is_none());
1066    }
1067
1068    // Silence unused-import lint on no-test builds
1069    #[allow(unused)]
1070    fn _t(_: TLVElement) {}
1071}