Skip to main content

rs_matter/dm/clusters/
desc.rs

1/*
2 *
3 *    Copyright (c) 2025-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 Descriptor cluster and its handler.
19
20use core::fmt::Debug;
21
22use crate::dm::{
23    ArrayAttributeRead, Cluster, Dataver, Endpoint, EndptId, Metadata, ReadContext, SemanticTag,
24};
25use crate::error::{Error, ErrorCode};
26use crate::tlv::{Nullable, TLVBuilderParent, ToTLVArrayBuilder, ToTLVBuilder, Utf8StrBuilder};
27use crate::utils::sync::DynBase;
28use crate::with;
29
30pub use crate::dm::clusters::decl::descriptor::*;
31pub use crate::dm::clusters::decl::globals::{
32    SemanticTagStructArrayBuilder, SemanticTagStructBuilder,
33};
34
35/// A parts matcher suitable for regular Matter devices
36#[derive(Debug)]
37#[cfg_attr(feature = "defmt", derive(defmt::Format))]
38struct StandardPartsMatcher;
39
40impl DynBase for StandardPartsMatcher {}
41
42impl PartsMatcher for StandardPartsMatcher {
43    fn matches(&self, our_endpoint: EndptId, endpoint: EndptId) -> bool {
44        our_endpoint == 0 && endpoint != our_endpoint
45    }
46}
47
48/// A parts matcher suitable for the aggregator endpoints of bridged Matter devices
49///
50/// This matcher matches ALL endpoints that are not the root endpoint (0) and not the aggregator endpoint itself.
51///
52/// For more complex scenarios, where the node needs to contain multiple aggregators, or where the node
53/// might contain non-bridged endpoints, user needs to supply its own `PartsMatcher` implementation.
54#[derive(Debug)]
55#[cfg_attr(feature = "defmt", derive(defmt::Format))]
56struct AggregatorPartsMatcher;
57
58impl DynBase for AggregatorPartsMatcher {}
59
60impl PartsMatcher for AggregatorPartsMatcher {
61    fn matches(&self, our_endpoint: EndptId, endpoint: EndptId) -> bool {
62        endpoint != our_endpoint && endpoint != 0
63    }
64}
65
66/// A trait for describing which endpoints (parts) should be returned
67/// from the POV of our endpoint
68///
69/// For standard Matter devices, all endpoints should be returned as parts.
70/// However - for queries on aggregator endpoints (i.e. those present in Matter bridges) -
71/// only endpoints different from the aggregator and from the root endpoint should be returned.
72pub trait PartsMatcher: DynBase + Debug {
73    /// Return `true` if the endpoint should be returned as a part
74    ///
75    /// # Arguments
76    /// - `our_endpoint`: The endpoint ID of the endpoint that is being queried
77    /// - `endpoint`: The endpoint ID of the endpoint that is being checked
78    fn matches(&self, our_endpoint: EndptId, endpoint: EndptId) -> bool;
79}
80
81impl<T> PartsMatcher for &T
82where
83    T: PartsMatcher,
84{
85    fn matches(&self, our_endpoint: EndptId, endpoint: EndptId) -> bool {
86        (**self).matches(our_endpoint, endpoint)
87    }
88}
89
90/// The system implementation of a handler for the Descriptor Matter cluster.
91#[derive(Clone, Debug)]
92#[cfg_attr(feature = "defmt", derive(defmt::Format))]
93pub struct DescHandler<'a> {
94    dataver: Dataver,
95    matcher: &'a dyn PartsMatcher,
96}
97
98impl DescHandler<'static> {
99    /// Create a new instance of `DescHandler` with the given `Dataver`
100    /// and a matcher suitable for regular Matter devices
101    pub const fn new(dataver: Dataver) -> Self {
102        Self::new_matching(dataver, &StandardPartsMatcher)
103    }
104
105    /// Create a new instance of `DescHandler` with the given `Dataver`
106    /// and a matcher suitable for aggregator endpoints
107    pub const fn new_aggregator(dataver: Dataver) -> Self {
108        Self::new_matching(dataver, &AggregatorPartsMatcher)
109    }
110}
111
112impl<'a> DescHandler<'a> {
113    /// Create a new instance of `DescHandler` with the given `Dataver`
114    /// and a custom matcher
115    pub const fn new_matching(dataver: Dataver, matcher: &'a dyn PartsMatcher) -> DescHandler<'a> {
116        Self { dataver, matcher }
117    }
118
119    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
120    pub const fn adapt(self) -> HandlerAdaptor<Self> {
121        HandlerAdaptor(self)
122    }
123
124    /// Emit a single `SemanticTagStruct` into `builder`.
125    fn push_tag<P: TLVBuilderParent>(
126        builder: SemanticTagStructBuilder<P>,
127        tag: &SemanticTag<'_>,
128    ) -> Result<P, Error> {
129        builder
130            .mfg_code(Nullable::new(tag.mfg_code))?
131            .namespace_id(tag.namespace_id)?
132            .tag(tag.tag)?
133            .label(tag.label.map(Nullable::some))?
134            .end()
135    }
136
137    fn with_endpoint<F, R>(ctx: impl ReadContext, f: F) -> Result<R, Error>
138    where
139        F: FnOnce(&Endpoint) -> Result<R, Error>,
140    {
141        let metadata = ctx.metadata();
142
143        metadata.access(|node| {
144            let endpoint = node
145                .endpoint(ctx.attr().endpoint_id)
146                .ok_or_else(|| Error::new(ErrorCode::EndpointNotFound))?;
147
148            f(endpoint)
149        })
150    }
151}
152
153/// `Descriptor` cluster metadata that additionally advertises the optional
154/// `TagList` attribute and the `TagList` feature.
155///
156/// Use this in place of [`DescHandler::CLUSTER`] on endpoints carrying
157/// [`Endpoint::semantic_tags`]. It is required whenever a node exposes two or
158/// more endpoints with the same device type under one parent: Matter Core spec
159/// 9.5 then demands each of them report a non-empty, mutually distinct
160/// `TagList`, and `TC_DESC_2_2` checks precisely that (it fails an endpoint
161/// whose `TagList` is absent *or* empty, and separately flags a missing
162/// feature bit).
163///
164/// It is deliberately not the default: on a node with no duplicated device
165/// types the attribute is optional, and advertising an always-empty `TagList`
166/// would be worse than not advertising it at all.
167pub const CLUSTER_TAG_LIST: Cluster<'static> = FULL_CLUSTER
168    .with_attrs(with!(required; AttributeId::TagList))
169    .with_cmds(with!())
170    .with_features(Feature::TAG_LIST.bits());
171
172/// `Descriptor` cluster metadata that additionally advertises the optional
173/// `EndpointUniqueID` attribute.
174///
175/// Use this in place of [`DescHandler::CLUSTER`] on endpoints carrying
176/// [`Endpoint::unique_id`]. It is deliberately not the default: the attribute
177/// is optional, and advertising it on an endpoint whose [`Endpoint::unique_id`]
178/// is `None` would turn every read into an error.
179pub const CLUSTER_ENDPOINT_UNIQUE_ID: Cluster<'static> = FULL_CLUSTER
180    .with_attrs(with!(required; AttributeId::EndpointUniqueID))
181    .with_cmds(with!());
182
183impl ClusterHandler for DescHandler<'_> {
184    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required)).with_cmds(with!());
185
186    fn dataver(&self) -> u32 {
187        self.dataver.get()
188    }
189
190    fn dataver_changed(&self) {
191        self.dataver.changed();
192    }
193
194    fn device_type_list<P: TLVBuilderParent>(
195        &self,
196        ctx: impl ReadContext,
197        builder: ArrayAttributeRead<DeviceTypeStructArrayBuilder<P>, DeviceTypeStructBuilder<P>>,
198    ) -> Result<P, Error> {
199        Self::with_endpoint(ctx, |endpoint| match builder {
200            ArrayAttributeRead::ReadAll(mut builder) => {
201                for dev_type in endpoint.device_types {
202                    builder = builder
203                        .push()?
204                        .device_type(dev_type.dtype as _)?
205                        .revision(dev_type.drev)?
206                        .end()?;
207                }
208
209                builder.end()
210            }
211            ArrayAttributeRead::ReadOne(index, builder) => {
212                let Some(dev_type) = endpoint.device_types.get(index as usize) else {
213                    return Err(ErrorCode::ConstraintError.into());
214                };
215
216                builder
217                    .device_type(dev_type.dtype as _)?
218                    .revision(dev_type.drev)?
219                    .end()
220            }
221            ArrayAttributeRead::ReadNone(builder) => builder.end(),
222        })
223    }
224
225    fn server_list<P: TLVBuilderParent>(
226        &self,
227        ctx: impl ReadContext,
228        builder: ArrayAttributeRead<ToTLVArrayBuilder<P, u32>, ToTLVBuilder<P, u32>>,
229    ) -> Result<P, Error> {
230        Self::with_endpoint(ctx, |endpoint| match builder {
231            ArrayAttributeRead::ReadAll(mut builder) => {
232                for cluster in endpoint.clusters {
233                    builder = builder.push(&cluster.id)?;
234                }
235
236                builder.end()
237            }
238            ArrayAttributeRead::ReadOne(index, builder) => {
239                let Some(cluster) = endpoint.clusters.get(index as usize) else {
240                    return Err(ErrorCode::ConstraintError.into());
241                };
242
243                builder.set(&cluster.id)
244            }
245            ArrayAttributeRead::ReadNone(builder) => builder.end(),
246        })
247    }
248
249    fn client_list<P: TLVBuilderParent>(
250        &self,
251        ctx: impl ReadContext,
252        builder: ArrayAttributeRead<ToTLVArrayBuilder<P, u32>, ToTLVBuilder<P, u32>>,
253    ) -> Result<P, Error> {
254        Self::with_endpoint(ctx, |endpoint| match builder {
255            ArrayAttributeRead::ReadAll(mut builder) => {
256                for client_id in endpoint.client_clusters {
257                    builder = builder.push(client_id)?;
258                }
259                builder.end()
260            }
261            ArrayAttributeRead::ReadOne(index, builder) => {
262                let Some(client_id) = endpoint.client_clusters.get(index as usize) else {
263                    return Err(ErrorCode::ConstraintError.into());
264                };
265                builder.set(client_id)
266            }
267            ArrayAttributeRead::ReadNone(builder) => builder.end(),
268        })
269    }
270
271    fn tag_list<P: TLVBuilderParent>(
272        &self,
273        ctx: impl ReadContext,
274        builder: ArrayAttributeRead<SemanticTagStructArrayBuilder<P>, SemanticTagStructBuilder<P>>,
275    ) -> Result<P, Error> {
276        Self::with_endpoint(ctx, |endpoint| match builder {
277            ArrayAttributeRead::ReadAll(mut builder) => {
278                for tag in endpoint.semantic_tags {
279                    builder = Self::push_tag(builder.push()?, tag)?;
280                }
281
282                builder.end()
283            }
284            ArrayAttributeRead::ReadOne(index, builder) => {
285                let Some(tag) = endpoint.semantic_tags.get(index as usize) else {
286                    return Err(ErrorCode::ConstraintError.into());
287                };
288
289                Self::push_tag(builder, tag)
290            }
291            ArrayAttributeRead::ReadNone(builder) => builder.end(),
292        })
293    }
294
295    // Deliberately outlined (`inline(never)`): inlining duplicates the body
296    // in every read-dispatch instantiation (flash size)
297    #[inline(never)]
298    fn endpoint_unique_id<P: TLVBuilderParent>(
299        &self,
300        ctx: impl ReadContext,
301        builder: Utf8StrBuilder<P>,
302    ) -> Result<P, Error> {
303        Self::with_endpoint(ctx, |endpoint| {
304            // Reaching here means the endpoint's `Descriptor` metadata
305            // advertises the attribute (`CLUSTER_ENDPOINT_UNIQUE_ID`), so a
306            // missing `Endpoint::unique_id` is a composition error.
307            let unique_id = endpoint
308                .unique_id
309                .ok_or_else(|| Error::new(ErrorCode::AttributeNotFound))?;
310
311            builder.set(unique_id)
312        })
313    }
314
315    fn parts_list<P: TLVBuilderParent>(
316        &self,
317        ctx: impl ReadContext,
318        builder: ArrayAttributeRead<ToTLVArrayBuilder<P, u16>, ToTLVBuilder<P, u16>>,
319    ) -> Result<P, Error> {
320        let metadata = ctx.metadata();
321
322        metadata.access(|node| {
323            let mut ep_ids = node
324                .endpoints
325                .iter()
326                .map(|e| e.id)
327                .filter(|e| self.matcher.matches(ctx.attr().endpoint_id, *e));
328
329            match builder {
330                ArrayAttributeRead::ReadAll(mut builder) => {
331                    for id in ep_ids {
332                        builder = builder.push(&id)?;
333                    }
334
335                    builder.end()
336                }
337                ArrayAttributeRead::ReadOne(index, builder) => {
338                    let Some(ep_id) = ep_ids.nth(index as usize) else {
339                        return Err(ErrorCode::ConstraintError.into());
340                    };
341
342                    builder.set(&ep_id)
343                }
344                ArrayAttributeRead::ReadNone(builder) => builder.end(),
345            }
346        })
347    }
348}