rs-matter 0.2.0

Native Rust implementation of the Matter (Smart-Home) ecosystem
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
/*
 *
 *    Copyright (c) 2026 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

//! Binding cluster handler (Matter Core spec).
//!
//! Per-endpoint, fabric-scoped, **persistent** list of `TargetStruct`
//! entries each describing a unicast `(node, endpoint, cluster?)` or
//! a groupcast `(group, cluster?)` destination. The Binding cluster
//! is the *device-side address book* a client cluster reads when it
//! wants to send a command somewhere: a wall switch with `OnOff` in
//! its client list, for instance, reads its Binding list to find the
//! bulb(s) it's been paired with.
//!
//! See the spec's [`9.6` summary][spec], or the in-tree write-up in
//! `super::user_label` for the analogous (per-endpoint, persistent,
//! shared-registry) shape.
//!
//! # Persistence
//!
//! Spec marks the `Binding` attribute with the `N` quality
//! bit (Non-Volatile, Matter Core) — values **SHALL** survive
//! reboots. We re-serialise the whole registry under [`BINDINGS_KEY`]
//! after every successful write, and re-hydrate on startup via
//! [`Bindings::load_persist`].
//!
//! # Fabric scoping
//!
//! `TargetStruct` carries an implicit `FabricIndex` field (id 254)
//! that the IM dispatch auto-injects from the writing accessor. On
//! reads, `attr.fab_filter` + `attr.fab_idx` constrain results to the
//! reading fabric. We store `fab_idx` alongside each entry and apply
//! the same filter manually on read paths.
//!
//! # Validation
//!
//! Per spec:
//! - `Group` and `Endpoint` are mutually exclusive (one of the two
//!   identifies the target).
//! - `Node` is required when `Endpoint` is present.
//! - `Cluster` is optional.
//!
//! We reject malformed entries with `ConstraintError`.

use core::num::NonZeroU8;

use crate::dm::{
    ArrayAttributeRead, ArrayAttributeWrite, Cluster, ClusterId, Dataver, EndptId, ReadContext,
    WriteContext,
};
use crate::error::{Error, ErrorCode};
use crate::persist::{KvBlobStore, Persist};
use crate::tlv::{FromTLV, TLVArray, TLVBuilderParent, TLVElement, ToTLV};
use crate::utils::cell::RefCell;
use crate::utils::init::{init, Init};
use crate::utils::storage::Vec;
use crate::utils::sync::blocking::Mutex;
use crate::with;

pub use crate::dm::clusters::decl::binding::*;
pub use crate::persist::BINDINGS_KEY;

/// Cluster metadata exposed by [`BindingHandler`].
///
/// Exposed as a free constant so callers can spell out
/// `EpClMatcher::new(Some(ep), Some(binding::CLUSTER.id))` without
/// reaching for the lifetime-parameterised handler type.
pub const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));

/// One binding entry: the *local* endpoint it is attached to, the fabric it
/// belongs to, and the destination it points at.
///
/// This is the single entry type used for storage, persistence, and the
/// application-facing read API ([`Bindings::get`]). Application code that *acts*
/// on the device's bindings (e.g. a switch reading its binding list to decide
/// which node(s) to send a command to) receives it cloned, so the registry lock
/// is never held across the `await` of the subsequent remote invoke. It is small
/// but not trivially `Copy`, hence `Clone`.
///
/// `local_endpoint` is **this** device's endpoint that hosts the binding — not
/// to be confused with the remote target's `endpoint`. A *unicast* target has
/// both `node` and `endpoint` set (`cluster` optional); a *groupcast* target has
/// `group` set instead. The destination fields mirror the spec's `TargetStruct`.
///
/// Wire layout follows the standard `derive(FromTLV, ToTLV)` shape — a TLV
/// struct with positional context-tagged fields. `Option<T>` fields are
/// omit-if-`None`. The encoding is decoupled from the wire-level `TargetStruct`
/// (which puts `FabricIndex` at ctx 254): we own the persisted layout and only
/// need it to be self-consistent.
#[derive(Debug, Clone, FromTLV, ToTLV)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Binding {
    /// The *local* endpoint this binding is attached to.
    pub local_endpoint: EndptId,
    /// The fabric this binding belongs to.
    pub fab_idx: NonZeroU8,
    /// The remote node id (set for a unicast target).
    pub node: Option<u64>,
    /// The group id (set for a groupcast target).
    pub group: Option<u16>,
    /// The remote endpoint (set for a unicast target).
    pub endpoint: Option<EndptId>,
    /// The cluster to address (optional).
    pub cluster: Option<ClusterId>,
}

/// Shared registry of [`Binding`] entries across every endpoint and
/// fabric. Persisted as a single TLV blob under [`BINDINGS_KEY`].
///
/// `N` bounds the total number of entries the device can hold. Per
/// Matter Core spec, device-type definitions may prescribe a
/// minimum-per-fabric; the spec also says the total must be
/// `min_per_fabric × supported_fabrics` — pick `N` accordingly.
pub struct Bindings<const N: usize> {
    state: Mutex<RefCell<Vec<Binding, N>>>,
}

impl<const N: usize> Bindings<N> {
    /// Create an empty registry. Prefer [`Self::init`] for non-trivial
    /// `N` so the storage is initialised in BSS.
    pub const fn new() -> Self {
        Self {
            state: Mutex::new(RefCell::new(Vec::new())),
        }
    }

    /// Return an in-place initialiser for an empty registry.
    pub fn init() -> impl Init<Self> {
        init!(Self {
            state <- Mutex::init(RefCell::init(Vec::init())),
        })
    }

    /// Re-hydrate the registry from `store` under [`BINDINGS_KEY`].
    /// Call once at startup, before exposing the data model.
    pub async fn load_persist<S: KvBlobStore>(
        &self,
        mut store: S,
        buf: &mut [u8],
    ) -> Result<(), Error> {
        let Some(data) = store.load(BINDINGS_KEY, buf)? else {
            self.state.lock(|cell| cell.borrow_mut().clear());
            return Ok(());
        };

        let loaded = Vec::<Binding, N>::from_tlv(&TLVElement::new(data))?;
        self.state.lock(|cell| *cell.borrow_mut() = loaded);

        info!("Loaded Binding entries for all endpoints from storage");
        Ok(())
    }

    /// Serialise the registry to `ctx.kv()` under [`BINDINGS_KEY`].
    fn store_persist<C: WriteContext>(&self, ctx: &C) -> Result<(), Error> {
        let mut persist = Persist::new(ctx.kv());

        self.state.lock(|cell| {
            let state = cell.borrow();
            persist.store_tlv(BINDINGS_KEY, &*state)
        })?;

        persist.run()
    }

    /// Validate a `TargetStruct` against spec and return a fully-built
    /// [`Binding`]. `local_endpoint` and `fab_idx` come from the dispatch
    /// context (they are not on the wire entry for this attribute write — the
    /// framework auto-injects fab_idx).
    fn parse_target(
        local_endpoint: EndptId,
        fab_idx: NonZeroU8,
        t: &TargetStruct<'_>,
    ) -> Result<Binding, Error> {
        let node = t.node()?;
        let group = t.group()?;
        let endpoint = t.endpoint()?;
        let cluster = t.cluster()?;

        // Spec — the conformance columns say Group is
        // `!Endpoint` and Endpoint is `!Group`, so **exactly one** of
        // them must identify the target. Node's conformance is
        // `Endpoint`, i.e. Node is mandatory whenever Endpoint is
        // present (it names the remote node for the unicast target).
        if group.is_some() && endpoint.is_some() {
            return Err(ErrorCode::ConstraintError.into());
        }
        if group.is_none() && endpoint.is_none() {
            // Rejects both "all fields missing" and "only Node
            // present" (no destination at all).
            return Err(ErrorCode::ConstraintError.into());
        }
        if endpoint.is_some() && node.is_none() {
            return Err(ErrorCode::ConstraintError.into());
        }

        Ok(Binding {
            local_endpoint,
            fab_idx,
            node,
            group,
            endpoint,
            cluster,
        })
    }

    /// Replace every entry on `(endpoint_id, fab_idx)` with the
    /// supplied list. Other endpoints / fabrics are untouched.
    fn replace_entries<'a, C: WriteContext>(
        &self,
        ctx: &C,
        endpoint_id: EndptId,
        fab_idx: NonZeroU8,
        list: &TLVArray<'a, TargetStruct<'a>>,
    ) -> Result<(), Error> {
        // Two-pass validation: parse every supplied target *before*
        // mutating state so a malformed input never partially clears
        // the existing entries on this fabric.
        let mut parsed: Vec<Binding, N> = Vec::new();
        for t in list {
            let t = t?;
            let sb = Self::parse_target(endpoint_id, fab_idx, &t)?;
            parsed.push(sb).map_err(|_| ErrorCode::ResourceExhausted)?;
        }

        let count = parsed.len();

        self.state.lock(|cell| {
            let mut state = cell.borrow_mut();
            // Drop every existing entry on this (endpoint, fabric)
            // in O(N) — one pass, in-place shift.
            state.retain(|e| !(e.local_endpoint == endpoint_id && e.fab_idx == fab_idx));
            // Now bulk-insert the validated list. Capacity-exhausted
            // here means the *combined* fabric counts exceeded `N`.
            for sb in parsed {
                state.push(sb).map_err(|_| ErrorCode::ResourceExhausted)?;
            }
            Ok::<_, Error>(())
        })?;

        if count == 0 {
            info!(
                "Binding: cleared all targets on endpoint {}, fabric {}",
                endpoint_id, fab_idx
            );
        } else {
            info!(
                "Binding: replaced list on endpoint {}, fabric {} with {} target(s)",
                endpoint_id, fab_idx, count
            );
        }

        self.store_persist(ctx)
    }

    /// Append one entry for `(endpoint_id, fab_idx)`.
    fn add_entry<'a, C: WriteContext>(
        &self,
        ctx: &C,
        endpoint_id: EndptId,
        fab_idx: NonZeroU8,
        entry: &TargetStruct<'a>,
    ) -> Result<(), Error> {
        let sb = Self::parse_target(endpoint_id, fab_idx, entry)?;

        info!(
            "Binding: add on endpoint {}, fabric {} -> node {:?}, group {:?}, endpoint {:?}, cluster {:?}",
            endpoint_id, fab_idx, sb.node, sb.group, sb.endpoint, sb.cluster
        );

        self.state.lock(|cell| {
            cell.borrow_mut()
                .push(sb)
                .map_err(|_| -> Error { ErrorCode::ResourceExhausted.into() })?;
            Ok::<_, Error>(())
        })?;

        self.store_persist(ctx)
    }

    /// The total number of [`Binding`] entries in the registry, across **all**
    /// local endpoints and fabrics. See [`Bindings::get`].
    pub fn len(&self) -> usize {
        self.state.lock(|cell| cell.borrow().len())
    }

    /// Whether the registry holds no bindings.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// The `index`-th [`Binding`] in the registry (across all endpoints and
    /// fabrics), cloned out so the registry lock is not held by the caller, or
    /// `None` if `index` is out of range.
    ///
    /// This flat, index-based accessor lets application code iterate the bindings
    /// and perform async work (e.g. a remote invoke) per entry without holding
    /// the lock across the `await`. Each [`Binding`] carries its `local_endpoint`
    /// and `fab_idx`, so the caller filters on those itself:
    ///
    /// ```ignore
    /// for i in 0..bindings.len() {
    ///     let Some(b) = bindings.get(i) else { break };
    ///     if b.local_endpoint != MY_ENDPOINT { continue; }
    ///     if let (Some(node), Some(ep)) = (b.node, b.endpoint) {
    ///         // lock already released here - safe to await
    ///         let exchange = Exchange::initiate(matter, &crypto, b.fab_idx, node).await?;
    ///         exchange.on_off().toggle(ep).await?;
    ///     }
    /// }
    /// ```
    pub fn get(&self, index: usize) -> Option<Binding> {
        self.state.lock(|cell| cell.borrow().get(index).cloned())
    }

    /// Render every entry on `endpoint_id` matching the read filter
    /// into the provided builder. `fab_filter = Some(idx)` constrains
    /// the output to one fabric; `None` returns every fabric's
    /// entries (used when the reading accessor opted out of fabric
    /// filtering via `attr.fab_filter == false`).
    fn render<P: TLVBuilderParent>(
        &self,
        endpoint_id: EndptId,
        fab_filter: Option<NonZeroU8>,
        builder: ArrayAttributeRead<TargetStructArrayBuilder<P>, TargetStructBuilder<P>>,
    ) -> Result<P, Error> {
        self.state.lock(|cell| {
            let state = cell.borrow();
            let mut iter = state
                .iter()
                .filter(|e| e.local_endpoint == endpoint_id)
                .filter(|e| fab_filter.is_none_or(|f| e.fab_idx == f));

            match builder {
                ArrayAttributeRead::ReadAll(mut array) => {
                    for e in iter {
                        let item = array.push()?;
                        let item = item.node(e.node)?;
                        let item = item.group(e.group)?;
                        let item = item.endpoint(e.endpoint)?;
                        let item = item.cluster(e.cluster)?;
                        array = item.fabric_index(Some(e.fab_idx.get()))?.end()?;
                    }
                    array.end()
                }
                ArrayAttributeRead::ReadOne(index, item) => {
                    let Some(e) = iter.nth(index as usize) else {
                        return Err(ErrorCode::ConstraintError.into());
                    };
                    let item = item.node(e.node)?;
                    let item = item.group(e.group)?;
                    let item = item.endpoint(e.endpoint)?;
                    let item = item.cluster(e.cluster)?;
                    item.fabric_index(Some(e.fab_idx.get()))?.end()
                }
                ArrayAttributeRead::ReadNone(array) => array.end(),
            }
        })
    }
}

impl<const N: usize> Default for Bindings<N> {
    fn default() -> Self {
        Self::new()
    }
}

/// Per-`(endpoint, Binding)`-instance handler facade. Holds only a
/// `Dataver`, the endpoint id it serves, and a borrow of the shared
/// [`Bindings`] registry. All persisted state lives in the registry.
pub struct BindingHandler<'a, const N: usize> {
    dataver: Dataver,
    endpoint_id: EndptId,
    bindings: &'a Bindings<N>,
}

impl<'a, const N: usize> BindingHandler<'a, N> {
    /// Construct a facade for `(endpoint_id, Binding)` backed by the
    /// shared `bindings` registry.
    pub const fn new(dataver: Dataver, endpoint_id: EndptId, bindings: &'a Bindings<N>) -> Self {
        Self {
            dataver,
            endpoint_id,
            bindings,
        }
    }

    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait.
    pub const fn adapt(self) -> HandlerAdaptor<Self> {
        HandlerAdaptor(self)
    }
}

impl<const N: usize> ClusterHandler for BindingHandler<'_, N> {
    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));

    fn dataver(&self) -> u32 {
        self.dataver.get()
    }

    fn dataver_changed(&self) {
        self.dataver.changed();
    }

    fn binding<P: TLVBuilderParent>(
        &self,
        ctx: impl ReadContext,
        builder: ArrayAttributeRead<TargetStructArrayBuilder<P>, TargetStructBuilder<P>>,
    ) -> Result<P, Error> {
        let attr = ctx.attr();
        // Translate the framework's `(fab_filter: bool, fab_idx: u8)` pair
        // into `Option<NonZeroU8>`. A reader that opted into fabric
        // filtering but presents an unaccredited fab_idx of 0 gets the
        // empty list — that's how the spec describes the "no accessing
        // fabric" state for fabric-scoped attrs.
        let fab_filter = if attr.fab_filter {
            Some(NonZeroU8::new(attr.fab_idx).ok_or(ErrorCode::UnsupportedAccess)?)
        } else {
            None
        };
        self.bindings.render(self.endpoint_id, fab_filter, builder)
    }

    fn set_binding(
        &self,
        ctx: impl WriteContext,
        value: ArrayAttributeWrite<TLVArray<'_, TargetStruct<'_>>, TargetStruct<'_>>,
    ) -> Result<(), Error> {
        // Fabric-scoped writes require a valid accessor fabric — the
        // `NonZeroU8::new` conversion is the type-system encoding of
        // that requirement.
        let fab_idx = NonZeroU8::new(ctx.attr().fab_idx).ok_or(ErrorCode::UnsupportedAccess)?;

        match value {
            ArrayAttributeWrite::Replace(list) => {
                self.bindings
                    .replace_entries(&ctx, self.endpoint_id, fab_idx, &list)
            }
            ArrayAttributeWrite::Add(entry) => {
                self.bindings
                    .add_entry(&ctx, self.endpoint_id, fab_idx, &entry)
            }
            // Per-element list update / remove on fabric-scoped attrs:
            // the framework converts these to InvalidAction before
            // reaching us, but match exhaustively to be safe.
            ArrayAttributeWrite::Update(_, _) | ArrayAttributeWrite::Remove(_) => {
                Err(ErrorCode::InvalidAction.into())
            }
        }
    }
}