Skip to main content

rs_matter/dm/clusters/
gen_comm.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 General Commissioning cluster and its handler.
19
20use core::fmt::Debug;
21
22use either::Either;
23
24use crate::dm::clusters::net_comm::NetworksAccess;
25use crate::dm::{Cluster, Dataver, InvokeContext, OperationContext, ReadContext, WriteContext};
26use crate::error::{Error, ErrorCode};
27use crate::fabric::FabricPersist;
28use crate::persist::{Persist, BASIC_INFO_KEY, NETWORKS_KEY};
29use crate::sc::pase::MAX_COMM_WINDOW_TIMEOUT_SECS;
30use crate::tlv::TLVBuilderParent;
31use crate::transport::session::SessionMode;
32use crate::utils::sync::DynBase;
33use crate::{except, with, MatterState};
34
35pub use crate::dm::clusters::decl::general_commissioning::*;
36
37impl CommissioningErrorEnum {
38    fn map(result: Result<(), Error>) -> Result<Self, Error> {
39        Self::map_result(result).map(Self::ok)
40    }
41
42    fn map_result<T>(result: Result<T, Error>) -> Result<Either<T, Self>, Error> {
43        match result {
44            Ok(value) => Ok(Either::Left(value)),
45            Err(err) => match err.code() {
46                ErrorCode::Busy | ErrorCode::NocInvalidFabricIndex => {
47                    Ok(Either::Right(Self::BusyWithOtherAdmin))
48                }
49                ErrorCode::GennCommInvalidAuthentication => {
50                    Ok(Either::Right(Self::InvalidAuthentication))
51                }
52                ErrorCode::FailSafeRequired => Ok(Either::Right(Self::NoFailSafe)),
53                _ => Err(err),
54            },
55        }
56    }
57
58    fn ok<T>(value: Either<T, Self>) -> Self {
59        match value {
60            Either::Left(_) => Self::OK,
61            Either::Right(code) => code,
62        }
63    }
64}
65
66/// A trait indicating the commissioning policy supported by `rs-matter`.
67/// (i.e. co-existence of the BLE/BTP network and the operational network during commissioning).
68pub trait CommPolicy: DynBase {
69    /// Return true if the device supports concurrent connection
70    /// (i.e. co-existence of the BLE/BTP network and the operational network during commissioning).
71    fn concurrent_connection_supported(&self) -> bool;
72
73    /// Return the expiry length of the fail-safe in seconds.
74    fn failsafe_expiry_len_secs(&self) -> u16;
75
76    /// Return the maximum cumulative fail-safe time in seconds.
77    fn failsafe_max_cml_secs(&self) -> u16;
78
79    /// Return the regulatory configuration of the device.
80    fn regulatory_config(&self) -> RegulatoryLocationTypeEnum;
81
82    /// Return the location capability of the device.
83    fn location_cap(&self) -> RegulatoryLocationTypeEnum;
84}
85
86impl<T> CommPolicy for &T
87where
88    T: CommPolicy,
89{
90    fn concurrent_connection_supported(&self) -> bool {
91        (*self).concurrent_connection_supported()
92    }
93
94    fn failsafe_expiry_len_secs(&self) -> u16 {
95        (*self).failsafe_expiry_len_secs()
96    }
97
98    fn failsafe_max_cml_secs(&self) -> u16 {
99        (*self).failsafe_max_cml_secs()
100    }
101
102    fn regulatory_config(&self) -> RegulatoryLocationTypeEnum {
103        (*self).regulatory_config()
104    }
105
106    fn location_cap(&self) -> RegulatoryLocationTypeEnum {
107        (*self).location_cap()
108    }
109}
110
111impl DynBase for bool {}
112
113impl CommPolicy for bool {
114    fn concurrent_connection_supported(&self) -> bool {
115        *self
116    }
117
118    fn failsafe_expiry_len_secs(&self) -> u16 {
119        120
120    }
121
122    fn failsafe_max_cml_secs(&self) -> u16 {
123        // Aligned with the Matter reference SDK example implementations and
124        // with `MAX_COMM_WINDOW_TIMEOUT_SECS` in `sc::pase`. Some Python tests
125        // (e.g. TC_ACL_2_9) read this attribute and reuse it as the
126        // `commissioning_timeout` for `OpenCommissioningWindow`, which the spec
127        // bounds at [180, 900] seconds; reporting 900 keeps such tests within
128        // the valid range while still being a reasonable upper bound.
129        MAX_COMM_WINDOW_TIMEOUT_SECS
130    }
131
132    fn regulatory_config(&self) -> RegulatoryLocationTypeEnum {
133        RegulatoryLocationTypeEnum::IndoorOutdoor
134    }
135
136    fn location_cap(&self) -> RegulatoryLocationTypeEnum {
137        RegulatoryLocationTypeEnum::IndoorOutdoor
138    }
139}
140
141/// The system implementation of a handler for the General Commissioning Matter cluster.
142pub struct GenCommHandler<'a> {
143    dataver: Dataver,
144    commissioning_policy: &'a dyn CommPolicy,
145}
146
147impl<'a> GenCommHandler<'a> {
148    /// Create a new instance of `GenCommHandler` with the given `Dataver` and `CommissioningPolicy`.
149    pub const fn new(dataver: Dataver, commissioning_policy: &'a dyn CommPolicy) -> Self {
150        Self {
151            dataver,
152            commissioning_policy,
153        }
154    }
155
156    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
157    pub const fn adapt(self) -> HandlerAdaptor<Self> {
158        HandlerAdaptor(self)
159    }
160
161    /// Execute the provided closure after checking that the failsafe is armed for the
162    /// fabric of this session.
163    ///
164    /// If the check fail, an appropriate error is returned.
165    pub(crate) fn with_armed_failsafe<F, T>(ctx: impl OperationContext, f: F) -> Result<T, Error>
166    where
167        F: FnOnce(&mut MatterState, &mut dyn FnMut()) -> Result<T, Error>,
168    {
169        Self::with_armed_failsafe_ex(ctx, f)
170    }
171
172    /// Return whether the supplied `NewRegulatoryConfig` value is allowed
173    /// given the device's `LocationCapability`. Mirrors the matrix in Matter
174    /// Core spec.
175    fn is_regulatory_config_supported(
176        policy: &dyn CommPolicy,
177        new_config: RegulatoryLocationTypeEnum,
178    ) -> bool {
179        match policy.location_cap() {
180            RegulatoryLocationTypeEnum::Indoor => {
181                matches!(new_config, RegulatoryLocationTypeEnum::Indoor)
182            }
183            RegulatoryLocationTypeEnum::Outdoor => {
184                matches!(new_config, RegulatoryLocationTypeEnum::Outdoor)
185            }
186            RegulatoryLocationTypeEnum::IndoorOutdoor => true,
187        }
188    }
189
190    /// Execute the provided closure after checking that the failsafe is armed for the
191    /// fabric of this session.
192    ///
193    /// If the check fail, an appropriate error is returned.
194    pub(crate) fn with_armed_failsafe_ex<F, T, E>(ctx: impl OperationContext, f: F) -> Result<T, E>
195    where
196        F: FnOnce(&mut MatterState, &mut dyn FnMut()) -> Result<T, E>,
197        E: From<Error>,
198    {
199        let mut notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
200
201        ctx.exchange().with_state_ex(|state| {
202            let sess = ctx.exchange().id().session(&mut state.sessions);
203
204            state
205                .failsafe
206                .check_armed(sess.get_session_mode())
207                .map_err(|err| match err.code() {
208                    ErrorCode::NocInvalidFabricIndex => {
209                        Error::new(ErrorCode::GennCommInvalidAuthentication)
210                    }
211                    _ => err,
212                })?;
213
214            f(state, &mut notify_mdns)
215        })
216    }
217}
218
219impl ClusterHandler for GenCommHandler<'_> {
220    const CLUSTER: Cluster<'static> = FULL_CLUSTER
221        .with_attrs(with!(required))
222        .with_cmds(except!(CommandId::SetTCAcknowledgements));
223
224    fn dataver(&self) -> u32 {
225        self.dataver.get()
226    }
227
228    fn dataver_changed(&self) {
229        self.dataver.changed();
230    }
231
232    fn breadcrumb(&self, ctx: impl ReadContext) -> Result<u64, Error> {
233        ctx.exchange()
234            .with_state(|state| Ok(state.failsafe.breadcrumb()))
235    }
236
237    fn set_breadcrumb(&self, ctx: impl WriteContext, value: u64) -> Result<(), Error> {
238        ctx.exchange().with_state(|state| {
239            state.failsafe.set_breadcrumb(value);
240
241            Ok(())
242        })
243    }
244
245    fn basic_commissioning_info<P: TLVBuilderParent>(
246        &self,
247        _ctx: impl ReadContext,
248        builder: BasicCommissioningInfoBuilder<P>,
249    ) -> Result<P, Error> {
250        builder
251            .fail_safe_expiry_length_seconds(self.commissioning_policy.failsafe_expiry_len_secs())?
252            .max_cumulative_failsafe_seconds(self.commissioning_policy.failsafe_max_cml_secs())?
253            .end()
254    }
255
256    fn regulatory_config(
257        &self,
258        ctx: impl ReadContext,
259    ) -> Result<RegulatoryLocationTypeEnum, Error> {
260        ctx.exchange()
261            .with_state(|state| Ok(state.basic_info_settings.location_type))
262    }
263
264    fn location_capability(
265        &self,
266        _ctx: impl ReadContext,
267    ) -> Result<RegulatoryLocationTypeEnum, Error> {
268        Ok(self.commissioning_policy.location_cap())
269    }
270
271    fn supports_concurrent_connection(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
272        Ok(self.commissioning_policy.concurrent_connection_supported())
273    }
274
275    fn handle_arm_fail_safe<P: TLVBuilderParent>(
276        &self,
277        ctx: impl InvokeContext,
278        request: ArmFailSafeRequest<'_>,
279        response: ArmFailSafeResponseBuilder<P>,
280    ) -> Result<P, Error> {
281        let expiry_length_seconds = request.expiry_length_seconds()?;
282
283        info!(
284            "Got Arm Fail Safe Request, expiry {}s",
285            expiry_length_seconds
286        );
287
288        // `ArmFailSafe(0)` means "force-expire the fail-safe context" per
289        // Matter Core spec: if the fail-safe is armed,
290        // the device SHALL roll back any uncommitted fabric / network state
291        // and reset the breadcrumb. Route through `force_expiry` so that
292        // in-flight `AddNOC` / `SetRegulatoryConfig` changes are reverted —
293        // the bare `failsafe.arm(0, ...)` path only flips the state to
294        // `Idle` and would leave the staged fabric committed.
295        let status = if expiry_length_seconds == 0 {
296            let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
297            let notify_change = |endpt_id, clust_id| ctx.notify_cluster_changed(endpt_id, clust_id);
298
299            CommissioningErrorEnum::map(ctx.exchange().with_state(|state| {
300                let sess = ctx.exchange().id().session(&mut state.sessions);
301                let pase_sess_id =
302                    matches!(sess.get_session_mode(), SessionMode::Pase { .. }).then(|| sess.id());
303
304                state.failsafe.expire(
305                    &mut state.fabrics,
306                    &mut state.sessions,
307                    pase_sess_id,
308                    ctx.networks(),
309                    ctx.kv(),
310                    notify_mdns,
311                    notify_change,
312                )?;
313
314                Ok(())
315            }))?
316        } else {
317            CommissioningErrorEnum::map(ctx.exchange().with_state(|state| {
318                let sess = ctx.exchange().id().session(&mut state.sessions);
319
320                state.failsafe.arm(
321                    expiry_length_seconds,
322                    request.breadcrumb()?,
323                    sess.get_session_mode(),
324                    &mut state.pase,
325                )
326            }))?
327        };
328
329        // Breadcrumb (and possibly failsafe-arm state) may have changed
330        ctx.notify_own_cluster_changed();
331
332        response.error_code(status)?.debug_text("")?.end()
333    }
334
335    fn handle_set_regulatory_config<P: TLVBuilderParent>(
336        &self,
337        ctx: impl InvokeContext,
338        request: SetRegulatoryConfigRequest<'_>,
339        response: SetRegulatoryConfigResponseBuilder<P>,
340    ) -> Result<P, Error> {
341        info!("Got Set Regulatory Config Request");
342
343        let country_code = request.country_code()?;
344        if country_code.len() != 2 {
345            return Err(ErrorCode::ConstraintError.into());
346        }
347
348        // Per Matter Core spec, `NewRegulatoryConfig`
349        // SHALL be one of the values supported by the device's
350        // `LocationCapability`:
351        //
352        //   * `LocationCapability::Indoor`         -> only `Indoor`
353        //   * `LocationCapability::Outdoor`        -> only `Outdoor`
354        //   * `LocationCapability::IndoorOutdoor`  -> any of the three
355        //
356        // A request that violates this — including an enum value the device
357        // doesn't even recognise — must be rejected with the cluster-level
358        // `ValueOutsideRange` rather than a generic IM `Failure`. Decode the
359        // enum defensively because TLV decoding will reject an unknown
360        // variant before we ever see it (the test sends `3`).
361        let location_type = request.new_regulatory_config();
362        let breadcrumb = request.breadcrumb()?;
363
364        let location_type = match location_type {
365            Ok(loc) if Self::is_regulatory_config_supported(self.commissioning_policy, loc) => loc,
366            _ => {
367                return response
368                    .error_code(CommissioningErrorEnum::ValueOutsideRange)?
369                    .debug_text("")?
370                    .end();
371            }
372        };
373
374        let mut persist = Persist::new(ctx.kv());
375
376        let status = CommissioningErrorEnum::map(ctx.exchange().with_state(|state| {
377            state.basic_info_settings.set_location(country_code);
378            state.basic_info_settings.location_type = location_type;
379
380            state.failsafe.set_breadcrumb(breadcrumb);
381
382            persist.store_tlv(BASIC_INFO_KEY, &state.basic_info_settings)?;
383
384            Ok(())
385        }))?;
386
387        persist.run()?;
388
389        // Regulatory config mutates both this cluster (RegulatoryConfig, Breadcrumb)
390        // and Basic Information (Location) on the same endpoint
391        ctx.notify_own_endpoint_changed();
392
393        response.error_code(status)?.debug_text("")?.end()
394    }
395
396    fn handle_commissioning_complete<P: TLVBuilderParent>(
397        &self,
398        ctx: impl InvokeContext,
399        response: CommissioningCompleteResponseBuilder<P>,
400    ) -> Result<P, Error> {
401        info!("Got Commissioning Complete Request");
402
403        let notify_change = |endpt_id, clust_id| ctx.notify_cluster_changed(endpt_id, clust_id);
404
405        let mut persist = FabricPersist::new(ctx.kv());
406
407        let status =
408            CommissioningErrorEnum::map(Self::with_armed_failsafe(&ctx, |state, notify_mdns| {
409                let sess = ctx.exchange().id().session(&mut state.sessions);
410                // Spec: on
411                // `CommissioningComplete` the PASE session SHALL be
412                // terminated. The current command is being delivered over
413                // that PASE, so mark it `expired` (response can still go
414                // out, no further exchanges accepted) and let the LRU
415                // eviction reclaim the slot. Without this the promoted
416                // PASE leaks across commissioning rounds and eventually
417                // exhausts the session table — visible as `BUSY` on the
418                // next round's `PBKDFParamRequest` (TC_CADMIN_1_19 hit
419                // this on the 4th round of `SupportedFabrics` rounds).
420                // Modern controllers (CHIP SDK 1.4+) run a
421                // `FindOperationalForCommissioningComplete` step before
422                // sending `CommissioningComplete`, so this command
423                // typically arrives over the new operational CASE session
424                // rather than over PASE. In that case the current session
425                // doesn't need preserving and we just drop every PASE
426                // session unconditionally. (For legacy behaviour where the
427                // command does come over PASE we still preserve the
428                // current one.)
429                let pase_sess_id =
430                    matches!(sess.get_session_mode(), SessionMode::Pase { .. }).then(|| sess.id());
431
432                let fabric = state
433                    .failsafe
434                    .disarm(sess.get_session_mode(), &mut state.fabrics)?;
435
436                state.pase.close_comm_window(notify_mdns, notify_change)?;
437                state.sessions.remove_pase(pase_sess_id);
438                ctx.exchange().matter().transport().notify_session_removed();
439
440                // Finally, persist the fabric and the network settings, prior to sending the other party a "success" status
441                persist.store(fabric)?;
442                ctx.networks().access(|networks| {
443                    networks.set_commissioned(true)?;
444
445                    persist
446                        .persist_mut()
447                        .store(NETWORKS_KEY, |buf| networks.save(buf))
448                })?;
449
450                info!("Commissioning complete, fabric and network settings persisted");
451
452                Ok(())
453            }))?;
454
455        persist.run()?;
456
457        // Commissioning-complete mutates many clusters on the root endpoint:
458        // breadcrumb (this cluster), fabrics (NOC), networks (NetCommissioning).
459        // The closed commissioning window was already notified via `notify_change`.
460        ctx.notify_own_endpoint_changed();
461
462        response.error_code(status)?.debug_text("")?.end()
463    }
464
465    fn handle_set_tc_acknowledgements<P: TLVBuilderParent>(
466        &self,
467        _ctx: impl InvokeContext,
468        _request: SetTCAcknowledgementsRequest<'_>,
469        _response: SetTCAcknowledgementsResponseBuilder<P>,
470    ) -> Result<P, Error> {
471        Err(ErrorCode::CommandNotFound.into())
472    }
473}
474
475impl Debug for GenCommHandler<'_> {
476    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
477        f.debug_struct("GenCommHandler")
478            .field("dataver", &self.dataver)
479            .finish()
480    }
481}
482
483#[cfg(feature = "defmt")]
484impl defmt::Format for GenCommHandler<'_> {
485    fn format(&self, fmt: defmt::Formatter) {
486        defmt::write!(fmt, "GenCommHandler {{ dataver: {} }}", self.dataver);
487    }
488}