rs_matter/dm/clusters/fixed_label.rs
1/*
2 *
3 * Copyright (c) 2026 Project CHIP Authors
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18//! FixedLabel cluster handler (Matter Application Cluster spec).
19//!
20//! Per-endpoint **read-only** list of `(label, value)` string pairs
21//! that the manufacturer bakes into the device firmware — typical use
22//! is exposing immutable tags such as `"serial"` → `"abc123"` or
23//! `"hwrev"` → `"B"`. Compare with [`super::user_label`], the
24//! writable counterpart used by commissioners.
25//!
26//! The list is **fixed** in the F-quality sense (Matter Core spec):
27//! it never changes for the lifetime of the device firmware,
28//! so we don't need a persistence layer, a mutex, or any per-entry
29//! storage. [`FixedLabelHandler`] just borrows a static slice of
30//! [`FixedLabelEntry`] from the application and iterates it on read.
31//! Writes are rejected by the framework before they reach the handler
32//! because the cluster metadata declares `LabelList` as read-only
33//! (`Access::READ`), which the IM dispatch maps to `UnsupportedWrite`
34//! — exactly the behaviour `TC_FLABEL_2_1` step 3 expects.
35//!
36//! Application wiring:
37//!
38//! ```ignore
39//! const LABELS: &[FixedLabelEntry] = &[
40//! FixedLabelEntry { label: "room", value: "kitchen" },
41//! FixedLabelEntry { label: "hwrev", value: "B" },
42//! ];
43//!
44//! let handler = FixedLabelHandler::new(Dataver::new_rand(rand), LABELS);
45//! ```
46
47use crate::dm::{ArrayAttributeRead, Cluster, Dataver, ReadContext};
48use crate::error::{Error, ErrorCode};
49use crate::tlv::TLVBuilderParent;
50use crate::with;
51
52pub use crate::dm::clusters::decl::fixed_label::*;
53pub use crate::dm::clusters::decl::globals::{
54 LabelStruct, LabelStructArrayBuilder, LabelStructBuilder,
55};
56
57/// Cluster metadata exposed by [`FixedLabelHandler`].
58///
59/// Exposed as a free constant so callers can spell out
60/// `EpClMatcher::new(Some(ep), Some(fixed_label::CLUSTER.id))` without
61/// reaching for the lifetime-parameterised handler type.
62pub const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
63
64/// One entry in a `LabelList`.
65///
66/// Per Matter Application Cluster spec (`LabelStruct`), each
67/// field is at most 16 characters. We don't enforce this here — the
68/// application is responsible for supplying spec-compliant data, and
69/// `TC_FLABEL_2_1` step 2 sanity-checks the lengths on the read path.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct FixedLabelEntry<'a> {
72 pub label: &'a str,
73 pub value: &'a str,
74}
75
76/// The handler for the FixedLabel Matter cluster.
77///
78/// Per-endpoint instance: each endpoint that advertises FixedLabel
79/// must own its own handler so the per-cluster-instance `Dataver`
80/// stays granular (Matter Core spec). The entries slice is
81/// borrowed — typically a `&'static [FixedLabelEntry<'static>]` —
82/// because the list is part of the device's firmware identity and
83/// doesn't change at runtime.
84pub struct FixedLabelHandler<'a> {
85 dataver: Dataver,
86 entries: &'a [FixedLabelEntry<'a>],
87}
88
89impl<'a> FixedLabelHandler<'a> {
90 /// Construct a handler exposing `entries` as the cluster's
91 /// `LabelList` attribute.
92 pub const fn new(dataver: Dataver, entries: &'a [FixedLabelEntry<'a>]) -> Self {
93 Self { dataver, entries }
94 }
95
96 /// Adapt the handler instance to the generic `rs-matter` `Handler` trait.
97 pub const fn adapt(self) -> HandlerAdaptor<Self> {
98 HandlerAdaptor(self)
99 }
100}
101
102impl ClusterHandler for FixedLabelHandler<'_> {
103 const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
104
105 fn dataver(&self) -> u32 {
106 self.dataver.get()
107 }
108
109 fn dataver_changed(&self) {
110 self.dataver.changed();
111 }
112
113 fn label_list<P: TLVBuilderParent>(
114 &self,
115 _ctx: impl ReadContext,
116 builder: ArrayAttributeRead<LabelStructArrayBuilder<P>, LabelStructBuilder<P>>,
117 ) -> Result<P, Error> {
118 match builder {
119 ArrayAttributeRead::ReadAll(mut array) => {
120 for entry in self.entries {
121 array = array
122 .push()?
123 .label(entry.label)?
124 .value(entry.value)?
125 .end()?;
126 }
127 array.end()
128 }
129 ArrayAttributeRead::ReadOne(index, item) => {
130 let Some(entry) = self.entries.get(index as usize) else {
131 // List-element index out of bounds — IM convention
132 // is `ConstraintError`; mirrors `UserLabelHandler`'s
133 // out-of-range behaviour.
134 return Err(ErrorCode::ConstraintError.into());
135 };
136 item.label(entry.label)?.value(entry.value)?.end()
137 }
138 ArrayAttributeRead::ReadNone(array) => array.end(),
139 }
140 }
141}