Skip to main content

rs_matter/dm/clusters/
binding.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//! Binding cluster handler (Matter Core spec).
19//!
20//! Per-endpoint, fabric-scoped, **persistent** list of `TargetStruct`
21//! entries each describing a unicast `(node, endpoint, cluster?)` or
22//! a groupcast `(group, cluster?)` destination. The Binding cluster
23//! is the *device-side address book* a client cluster reads when it
24//! wants to send a command somewhere: a wall switch with `OnOff` in
25//! its client list, for instance, reads its Binding list to find the
26//! bulb(s) it's been paired with.
27//!
28//! See the spec's [`9.6` summary][spec], or the in-tree write-up in
29//! `super::user_label` for the analogous (per-endpoint, persistent,
30//! shared-registry) shape.
31//!
32//! # Persistence
33//!
34//! Spec marks the `Binding` attribute with the `N` quality
35//! bit (Non-Volatile, Matter Core) — values **SHALL** survive
36//! reboots. We re-serialise the whole registry under [`BINDINGS_KEY`]
37//! after every successful write, and re-hydrate on startup via
38//! [`Bindings::load_persist`].
39//!
40//! # Fabric scoping
41//!
42//! `TargetStruct` carries an implicit `FabricIndex` field (id 254)
43//! that the IM dispatch auto-injects from the writing accessor. On
44//! reads, `attr.fab_filter` + `attr.fab_idx` constrain results to the
45//! reading fabric. We store `fab_idx` alongside each entry and apply
46//! the same filter manually on read paths.
47//!
48//! # Validation
49//!
50//! Per spec:
51//! - `Group` and `Endpoint` are mutually exclusive (one of the two
52//!   identifies the target).
53//! - `Node` is required when `Endpoint` is present.
54//! - `Cluster` is optional.
55//!
56//! We reject malformed entries with `ConstraintError`.
57
58use core::num::NonZeroU8;
59
60use crate::dm::{
61    ArrayAttributeRead, ArrayAttributeWrite, Cluster, ClusterId, Dataver, EndptId, ReadContext,
62    WriteContext,
63};
64use crate::error::{Error, ErrorCode};
65use crate::persist::{KvBlobStore, Persist};
66use crate::tlv::{FromTLV, TLVArray, TLVBuilderParent, TLVElement, ToTLV};
67use crate::utils::cell::RefCell;
68use crate::utils::init::{init, Init};
69use crate::utils::storage::Vec;
70use crate::utils::sync::blocking::Mutex;
71use crate::with;
72
73pub use crate::dm::clusters::decl::binding::*;
74pub use crate::persist::BINDINGS_KEY;
75
76/// Cluster metadata exposed by [`BindingHandler`].
77///
78/// Exposed as a free constant so callers can spell out
79/// `EpClMatcher::new(Some(ep), Some(binding::CLUSTER.id))` without
80/// reaching for the lifetime-parameterised handler type.
81pub const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
82
83/// One binding entry: the *local* endpoint it is attached to, the fabric it
84/// belongs to, and the destination it points at.
85///
86/// This is the single entry type used for storage, persistence, and the
87/// application-facing read API ([`Bindings::get`]). Application code that *acts*
88/// on the device's bindings (e.g. a switch reading its binding list to decide
89/// which node(s) to send a command to) receives it cloned, so the registry lock
90/// is never held across the `await` of the subsequent remote invoke. It is small
91/// but not trivially `Copy`, hence `Clone`.
92///
93/// `local_endpoint` is **this** device's endpoint that hosts the binding — not
94/// to be confused with the remote target's `endpoint`. A *unicast* target has
95/// both `node` and `endpoint` set (`cluster` optional); a *groupcast* target has
96/// `group` set instead. The destination fields mirror the spec's `TargetStruct`.
97///
98/// Wire layout follows the standard `derive(FromTLV, ToTLV)` shape — a TLV
99/// struct with positional context-tagged fields. `Option<T>` fields are
100/// omit-if-`None`. The encoding is decoupled from the wire-level `TargetStruct`
101/// (which puts `FabricIndex` at ctx 254): we own the persisted layout and only
102/// need it to be self-consistent.
103#[derive(Debug, Clone, FromTLV, ToTLV)]
104#[cfg_attr(feature = "defmt", derive(defmt::Format))]
105pub struct Binding {
106    /// The *local* endpoint this binding is attached to.
107    pub local_endpoint: EndptId,
108    /// The fabric this binding belongs to.
109    pub fab_idx: NonZeroU8,
110    /// The remote node id (set for a unicast target).
111    pub node: Option<u64>,
112    /// The group id (set for a groupcast target).
113    pub group: Option<u16>,
114    /// The remote endpoint (set for a unicast target).
115    pub endpoint: Option<EndptId>,
116    /// The cluster to address (optional).
117    pub cluster: Option<ClusterId>,
118}
119
120/// Shared registry of [`Binding`] entries across every endpoint and
121/// fabric. Persisted as a single TLV blob under [`BINDINGS_KEY`].
122///
123/// `N` bounds the total number of entries the device can hold. Per
124/// Matter Core spec, device-type definitions may prescribe a
125/// minimum-per-fabric; the spec also says the total must be
126/// `min_per_fabric × supported_fabrics` — pick `N` accordingly.
127pub struct Bindings<const N: usize> {
128    state: Mutex<RefCell<Vec<Binding, N>>>,
129}
130
131impl<const N: usize> Bindings<N> {
132    /// Create an empty registry. Prefer [`Self::init`] for non-trivial
133    /// `N` so the storage is initialised in BSS.
134    pub const fn new() -> Self {
135        Self {
136            state: Mutex::new(RefCell::new(Vec::new())),
137        }
138    }
139
140    /// Return an in-place initialiser for an empty registry.
141    pub fn init() -> impl Init<Self> {
142        init!(Self {
143            state <- Mutex::init(RefCell::init(Vec::init())),
144        })
145    }
146
147    /// Re-hydrate the registry from `store` under [`BINDINGS_KEY`].
148    /// Call once at startup, before exposing the data model.
149    pub async fn load_persist<S: KvBlobStore>(
150        &self,
151        mut store: S,
152        buf: &mut [u8],
153    ) -> Result<(), Error> {
154        let Some(data) = store.load(BINDINGS_KEY, buf)? else {
155            self.state.lock(|cell| cell.borrow_mut().clear());
156            return Ok(());
157        };
158
159        let loaded = Vec::<Binding, N>::from_tlv(&TLVElement::new(data))?;
160        self.state.lock(|cell| *cell.borrow_mut() = loaded);
161
162        info!("Loaded Binding entries for all endpoints from storage");
163        Ok(())
164    }
165
166    /// Serialise the registry to `ctx.kv()` under [`BINDINGS_KEY`].
167    fn store_persist<C: WriteContext>(&self, ctx: &C) -> Result<(), Error> {
168        let mut persist = Persist::new(ctx.kv());
169
170        self.state.lock(|cell| {
171            let state = cell.borrow();
172            persist.store_tlv(BINDINGS_KEY, &*state)
173        })?;
174
175        persist.run()
176    }
177
178    /// Validate a `TargetStruct` against spec and return a fully-built
179    /// [`Binding`]. `local_endpoint` and `fab_idx` come from the dispatch
180    /// context (they are not on the wire entry for this attribute write — the
181    /// framework auto-injects fab_idx).
182    fn parse_target(
183        local_endpoint: EndptId,
184        fab_idx: NonZeroU8,
185        t: &TargetStruct<'_>,
186    ) -> Result<Binding, Error> {
187        let node = t.node()?;
188        let group = t.group()?;
189        let endpoint = t.endpoint()?;
190        let cluster = t.cluster()?;
191
192        // Spec — the conformance columns say Group is
193        // `!Endpoint` and Endpoint is `!Group`, so **exactly one** of
194        // them must identify the target. Node's conformance is
195        // `Endpoint`, i.e. Node is mandatory whenever Endpoint is
196        // present (it names the remote node for the unicast target).
197        if group.is_some() && endpoint.is_some() {
198            return Err(ErrorCode::ConstraintError.into());
199        }
200        if group.is_none() && endpoint.is_none() {
201            // Rejects both "all fields missing" and "only Node
202            // present" (no destination at all).
203            return Err(ErrorCode::ConstraintError.into());
204        }
205        if endpoint.is_some() && node.is_none() {
206            return Err(ErrorCode::ConstraintError.into());
207        }
208
209        Ok(Binding {
210            local_endpoint,
211            fab_idx,
212            node,
213            group,
214            endpoint,
215            cluster,
216        })
217    }
218
219    /// Replace every entry on `(endpoint_id, fab_idx)` with the
220    /// supplied list. Other endpoints / fabrics are untouched.
221    fn replace_entries<'a, C: WriteContext>(
222        &self,
223        ctx: &C,
224        endpoint_id: EndptId,
225        fab_idx: NonZeroU8,
226        list: &TLVArray<'a, TargetStruct<'a>>,
227    ) -> Result<(), Error> {
228        // Two-pass validation: parse every supplied target *before*
229        // mutating state so a malformed input never partially clears
230        // the existing entries on this fabric.
231        let mut parsed: Vec<Binding, N> = Vec::new();
232        for t in list {
233            let t = t?;
234            let sb = Self::parse_target(endpoint_id, fab_idx, &t)?;
235            parsed.push(sb).map_err(|_| ErrorCode::ResourceExhausted)?;
236        }
237
238        let count = parsed.len();
239
240        self.state.lock(|cell| {
241            let mut state = cell.borrow_mut();
242            // Drop every existing entry on this (endpoint, fabric)
243            // in O(N) — one pass, in-place shift.
244            state.retain(|e| !(e.local_endpoint == endpoint_id && e.fab_idx == fab_idx));
245            // Now bulk-insert the validated list. Capacity-exhausted
246            // here means the *combined* fabric counts exceeded `N`.
247            for sb in parsed {
248                state.push(sb).map_err(|_| ErrorCode::ResourceExhausted)?;
249            }
250            Ok::<_, Error>(())
251        })?;
252
253        if count == 0 {
254            info!(
255                "Binding: cleared all targets on endpoint {}, fabric {}",
256                endpoint_id, fab_idx
257            );
258        } else {
259            info!(
260                "Binding: replaced list on endpoint {}, fabric {} with {} target(s)",
261                endpoint_id, fab_idx, count
262            );
263        }
264
265        self.store_persist(ctx)
266    }
267
268    /// Append one entry for `(endpoint_id, fab_idx)`.
269    fn add_entry<'a, C: WriteContext>(
270        &self,
271        ctx: &C,
272        endpoint_id: EndptId,
273        fab_idx: NonZeroU8,
274        entry: &TargetStruct<'a>,
275    ) -> Result<(), Error> {
276        let sb = Self::parse_target(endpoint_id, fab_idx, entry)?;
277
278        info!(
279            "Binding: add on endpoint {}, fabric {} -> node {:?}, group {:?}, endpoint {:?}, cluster {:?}",
280            endpoint_id, fab_idx, sb.node, sb.group, sb.endpoint, sb.cluster
281        );
282
283        self.state.lock(|cell| {
284            cell.borrow_mut()
285                .push(sb)
286                .map_err(|_| -> Error { ErrorCode::ResourceExhausted.into() })?;
287            Ok::<_, Error>(())
288        })?;
289
290        self.store_persist(ctx)
291    }
292
293    /// The total number of [`Binding`] entries in the registry, across **all**
294    /// local endpoints and fabrics. See [`Bindings::get`].
295    pub fn len(&self) -> usize {
296        self.state.lock(|cell| cell.borrow().len())
297    }
298
299    /// Whether the registry holds no bindings.
300    pub fn is_empty(&self) -> bool {
301        self.len() == 0
302    }
303
304    /// The `index`-th [`Binding`] in the registry (across all endpoints and
305    /// fabrics), cloned out so the registry lock is not held by the caller, or
306    /// `None` if `index` is out of range.
307    ///
308    /// This flat, index-based accessor lets application code iterate the bindings
309    /// and perform async work (e.g. a remote invoke) per entry without holding
310    /// the lock across the `await`. Each [`Binding`] carries its `local_endpoint`
311    /// and `fab_idx`, so the caller filters on those itself:
312    ///
313    /// ```ignore
314    /// for i in 0..bindings.len() {
315    ///     let Some(b) = bindings.get(i) else { break };
316    ///     if b.local_endpoint != MY_ENDPOINT { continue; }
317    ///     if let (Some(node), Some(ep)) = (b.node, b.endpoint) {
318    ///         // lock already released here - safe to await
319    ///         let exchange = Exchange::initiate(matter, &crypto, b.fab_idx, node).await?;
320    ///         exchange.on_off().toggle(ep).await?;
321    ///     }
322    /// }
323    /// ```
324    pub fn get(&self, index: usize) -> Option<Binding> {
325        self.state.lock(|cell| cell.borrow().get(index).cloned())
326    }
327
328    /// Render every entry on `endpoint_id` matching the read filter
329    /// into the provided builder. `fab_filter = Some(idx)` constrains
330    /// the output to one fabric; `None` returns every fabric's
331    /// entries (used when the reading accessor opted out of fabric
332    /// filtering via `attr.fab_filter == false`).
333    fn render<P: TLVBuilderParent>(
334        &self,
335        endpoint_id: EndptId,
336        fab_filter: Option<NonZeroU8>,
337        builder: ArrayAttributeRead<TargetStructArrayBuilder<P>, TargetStructBuilder<P>>,
338    ) -> Result<P, Error> {
339        self.state.lock(|cell| {
340            let state = cell.borrow();
341            let mut iter = state
342                .iter()
343                .filter(|e| e.local_endpoint == endpoint_id)
344                .filter(|e| fab_filter.is_none_or(|f| e.fab_idx == f));
345
346            match builder {
347                ArrayAttributeRead::ReadAll(mut array) => {
348                    for e in iter {
349                        let item = array.push()?;
350                        let item = item.node(e.node)?;
351                        let item = item.group(e.group)?;
352                        let item = item.endpoint(e.endpoint)?;
353                        let item = item.cluster(e.cluster)?;
354                        array = item.fabric_index(Some(e.fab_idx.get()))?.end()?;
355                    }
356                    array.end()
357                }
358                ArrayAttributeRead::ReadOne(index, item) => {
359                    let Some(e) = iter.nth(index as usize) else {
360                        return Err(ErrorCode::ConstraintError.into());
361                    };
362                    let item = item.node(e.node)?;
363                    let item = item.group(e.group)?;
364                    let item = item.endpoint(e.endpoint)?;
365                    let item = item.cluster(e.cluster)?;
366                    item.fabric_index(Some(e.fab_idx.get()))?.end()
367                }
368                ArrayAttributeRead::ReadNone(array) => array.end(),
369            }
370        })
371    }
372}
373
374impl<const N: usize> Default for Bindings<N> {
375    fn default() -> Self {
376        Self::new()
377    }
378}
379
380/// Per-`(endpoint, Binding)`-instance handler facade. Holds only a
381/// `Dataver`, the endpoint id it serves, and a borrow of the shared
382/// [`Bindings`] registry. All persisted state lives in the registry.
383pub struct BindingHandler<'a, const N: usize> {
384    dataver: Dataver,
385    endpoint_id: EndptId,
386    bindings: &'a Bindings<N>,
387}
388
389impl<'a, const N: usize> BindingHandler<'a, N> {
390    /// Construct a facade for `(endpoint_id, Binding)` backed by the
391    /// shared `bindings` registry.
392    pub const fn new(dataver: Dataver, endpoint_id: EndptId, bindings: &'a Bindings<N>) -> Self {
393        Self {
394            dataver,
395            endpoint_id,
396            bindings,
397        }
398    }
399
400    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait.
401    pub const fn adapt(self) -> HandlerAdaptor<Self> {
402        HandlerAdaptor(self)
403    }
404}
405
406impl<const N: usize> ClusterHandler for BindingHandler<'_, N> {
407    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
408
409    fn dataver(&self) -> u32 {
410        self.dataver.get()
411    }
412
413    fn dataver_changed(&self) {
414        self.dataver.changed();
415    }
416
417    fn binding<P: TLVBuilderParent>(
418        &self,
419        ctx: impl ReadContext,
420        builder: ArrayAttributeRead<TargetStructArrayBuilder<P>, TargetStructBuilder<P>>,
421    ) -> Result<P, Error> {
422        let attr = ctx.attr();
423        // Translate the framework's `(fab_filter: bool, fab_idx: u8)` pair
424        // into `Option<NonZeroU8>`. A reader that opted into fabric
425        // filtering but presents an unaccredited fab_idx of 0 gets the
426        // empty list — that's how the spec describes the "no accessing
427        // fabric" state for fabric-scoped attrs.
428        let fab_filter = if attr.fab_filter {
429            Some(NonZeroU8::new(attr.fab_idx).ok_or(ErrorCode::UnsupportedAccess)?)
430        } else {
431            None
432        };
433        self.bindings.render(self.endpoint_id, fab_filter, builder)
434    }
435
436    fn set_binding(
437        &self,
438        ctx: impl WriteContext,
439        value: ArrayAttributeWrite<TLVArray<'_, TargetStruct<'_>>, TargetStruct<'_>>,
440    ) -> Result<(), Error> {
441        // Fabric-scoped writes require a valid accessor fabric — the
442        // `NonZeroU8::new` conversion is the type-system encoding of
443        // that requirement.
444        let fab_idx = NonZeroU8::new(ctx.attr().fab_idx).ok_or(ErrorCode::UnsupportedAccess)?;
445
446        match value {
447            ArrayAttributeWrite::Replace(list) => {
448                self.bindings
449                    .replace_entries(&ctx, self.endpoint_id, fab_idx, &list)
450            }
451            ArrayAttributeWrite::Add(entry) => {
452                self.bindings
453                    .add_entry(&ctx, self.endpoint_id, fab_idx, &entry)
454            }
455            // Per-element list update / remove on fabric-scoped attrs:
456            // the framework converts these to InvalidAction before
457            // reaching us, but match exhaustively to be safe.
458            ArrayAttributeWrite::Update(_, _) | ArrayAttributeWrite::Remove(_) => {
459                Err(ErrorCode::InvalidAction.into())
460            }
461        }
462    }
463}