Skip to main content

rs_matter/dm/clusters/
identify.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//! Identify cluster handler (Matter Application Cluster spec).
19//!
20//! The Identify cluster lets a controller put an endpoint into an
21//! identification state — typically blinking an LED, beeping, or playing a
22//! lighting effect — so a human can pick out which physical device they
23//! just commissioned among several visually-identical ones. It is a
24//! mandatory cluster on most application device types (e.g. On/Off Light,
25//! Dimmable Light, Color Temperature Light), see Matter Device Library
26//!  for the On/Off Light requirements.
27//!
28//! [`IdentifyHandler`] is generic over an [`IdentifyHooks`] hardware-hook
29//! trait. The library handler owns all the boring bookkeeping —
30//! `IdentifyTime` storage, the deadline-driven countdown, attribute-change
31//! notifications — and dispatches a single sync [`IdentifyAction`]
32//! callback to the application whenever the identification state
33//! transitions. Applications with real hardware just implement
34//! [`IdentifyHooks::identify`] (and optionally
35//! [`IdentifyHooks::identify_type`]) and let the library handle the rest;
36//! applications with no hardware can use the default `()` impl and ship
37//! `IdentifyHandler::new(dataver)` without further ceremony.
38
39use core::cell::Cell;
40use core::pin::pin;
41
42use embassy_futures::select::{select, Either};
43use embassy_time::{Duration, Instant, Timer};
44
45use crate::dm::types::EndptId;
46use crate::dm::{Cluster, Dataver, HandlerContext, InvokeContext, ReadContext, WriteContext};
47use crate::error::Error;
48use crate::utils::sync::blocking::Mutex;
49use crate::utils::sync::Notification;
50use crate::with;
51
52pub use crate::dm::clusters::decl::identify::*;
53
54/// Cluster metadata exposed by [`IdentifyHandler`] regardless of hooks.
55///
56/// Equivalent to `<IdentifyHandler<H> as ClusterHandler>::CLUSTER` for
57/// any `H: IdentifyHooks`, exposed here as a free constant so callers
58/// don't have to spell out the generic parameter when they just want the
59/// cluster ID for an `EpClMatcher` or a `clusters!(...)` literal.
60pub const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
61
62/// The kind of identification action requested by a controller, dispatched
63/// by [`IdentifyHandler`] to the application's [`IdentifyHooks::identify`]
64/// method. Hooks see exactly one of these per state transition.
65#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
66#[cfg_attr(feature = "defmt", derive(crate::reexport::defmt::Format))]
67pub enum IdentifyAction {
68    /// `IdentifyTime` write (or `Identify` command) with a non-zero
69    /// duration: start (or re-arm) identifying for the supplied seconds.
70    /// The handler tracks the deadline and will dispatch a follow-up
71    /// [`IdentifyAction::Cancel`] action when the timer expires.
72    Time(u16),
73    /// `TriggerEffect` command: trigger a named effect with the supplied
74    /// variant. The handler does not track effect duration — applications
75    /// own the effect lifecycle from this point. The `StopEffect` /
76    /// `FinishEffect` effect identifiers arrive here as
77    /// [`IdentifyAction::Effect`] just like every other effect, so the
78    /// application can dispatch them to the appropriate hardware sequence.
79    Effect(EffectIdentifierEnum, EffectVariantEnum),
80    /// Stop any in-progress identification. Dispatched on:
81    /// - `IdentifyTime` write (or `Identify` command) with value `0`,
82    /// - the countdown deadline being reached,
83    ///
84    /// **not** dispatched on a re-arm: the application instead receives a
85    /// fresh [`IdentifyAction::Time`] with the new duration and can decide
86    /// whether to keep its current visual pattern running or restart.
87    Cancel,
88}
89
90/// Application-level hooks for the Identify cluster.
91///
92/// Implementations override this trait to drive their hardware (LED,
93/// buzzer, display, …) in response to identify requests from a Matter
94/// controller. The default `()` implementation is a no-op and is suitable
95/// for headless test fixtures or applications that observe identification
96/// state via attribute subscriptions instead.
97pub trait IdentifyHooks {
98    /// Return the kind of identification mechanism this endpoint provides.
99    /// Reported as the `IdentifyType` attribute (per App Cluster spec).
100    fn identify_type(&self) -> IdentifyTypeEnum;
101
102    /// Drive the application's identify hardware in response to a state
103    /// transition.
104    fn identify(&self, action: IdentifyAction);
105}
106
107impl<T> IdentifyHooks for &T
108where
109    T: IdentifyHooks,
110{
111    fn identify_type(&self) -> IdentifyTypeEnum {
112        (*self).identify_type()
113    }
114
115    fn identify(&self, action: IdentifyAction) {
116        (*self).identify(action)
117    }
118}
119
120impl IdentifyHooks for () {
121    fn identify_type(&self) -> IdentifyTypeEnum {
122        IdentifyTypeEnum::None
123    }
124
125    fn identify(&self, action: IdentifyAction) {
126        let _ = action;
127    }
128}
129
130/// Read-only view over an endpoint's identification state, for sibling
131/// clusters whose behavior depends on it — notably the Groups cluster,
132/// whose `AddGroupIfIdentifying` command only takes effect while the
133/// endpoint is identifying (App Cluster spec).
134///
135/// Implemented by [`IdentifyHandler`]; couple it to the sibling handler
136/// serving the *same* endpoint (identification state is per-endpoint).
137pub trait IdentifyStatus {
138    /// Whether the endpoint is currently identifying (`IdentifyTime > 0`).
139    fn is_identifying(&self) -> bool;
140}
141
142impl<T> IdentifyStatus for &T
143where
144    T: IdentifyStatus + ?Sized,
145{
146    fn is_identifying(&self) -> bool {
147        (*self).is_identifying()
148    }
149}
150
151/// The captured identify session. Reads of `IdentifyTime` compute
152/// `duration.saturating_sub(elapsed)` from this on-demand; the run task
153/// uses `endpoint_id` to target its `notify_attr_changed` at the right
154/// path when the deadline expires.
155#[derive(Copy, Clone, Debug, Eq, PartialEq)]
156#[cfg_attr(feature = "defmt", derive(crate::reexport::defmt::Format))]
157struct Session {
158    /// The endpoint that requested the identify — captured at write/invoke
159    /// time from the surrounding `OperationContext`, since the
160    /// `HandlerContext` available in the run task does not carry it.
161    endpoint_id: EndptId,
162    /// The originally-requested duration in seconds. Together with `start`
163    /// drives the on-demand "remaining seconds" computation.
164    duration: u16,
165    /// The `Instant` at which the request landed.
166    start: Instant,
167}
168
169/// The handler for the Identify Matter cluster.
170///
171/// Per-endpoint instance: each endpoint that advertises the Identify
172/// cluster must own a separate `IdentifyHandler` so countdown state and
173/// attribute reports stay segregated. The endpoint ID is captured *lazily*
174/// from the first write/command that lands on this instance — no
175/// constructor argument required, and the same handler type can therefore
176/// serve any endpoint without per-endpoint specialization.
177///
178/// State model: rather than physically decrement an `IdentifyTime` counter
179/// at 1 Hz, the handler captures the originally-requested
180/// `(endpoint_id, duration, start_instant)` when an `Identify` command (or
181/// `IdentifyTime` write) arrives, and computes the *remaining* seconds
182/// on-demand whenever the framework reads the attribute. This is
183/// observably equivalent to a physical countdown per Matter App Cluster
184/// spec (which constrains the attribute's observable value, not
185/// the implementation's internal storage), but it avoids 60 wakeups for
186/// a 60-second identify — the [run task](Handler::run) only schedules a
187/// single `Timer::at(deadline)` per identify cycle, fires the final-zero
188/// `notify_attr_changed`, and parks. On battery-powered targets that's the
189/// difference between a measurable wake-and-radio-on hit per second and a
190/// non-event.
191///
192/// Concurrency: the session cell is wrapped in a blocking [`Mutex`] so the
193/// handler is sound under a future work-stealing executor configuration of
194/// rs-matter. Lock holds are bounded to a single `Cell::get` / `Cell::set`
195/// each — the lock is *never* held across `.await` points.
196///
197/// Dataver is bumped automatically by the framework after each write/invoke
198/// (via the cluster handler chain's `bump_dataver(MatchContext)`), and
199/// again whenever a `notify_attr_changed` is dispatched. This handler
200/// therefore never calls `self.dataver.changed()` directly — it only
201/// signals attribute-changed notifications, and the framework takes care
202/// of dataver progression.
203///
204/// The handler implements the (sync) `ClusterHandler` trait — which keeps
205/// the read/write/invoke surface as cheap sync calls (smaller footprint
206/// than `ClusterAsyncHandler`'s all-async state machines) — and provides
207/// the deadline-timer task via `ClusterHandler::run`. Adapt it to the
208/// generic `rs-matter` `Handler` trait with [`IdentifyHandler::adapt`].
209pub struct IdentifyHandler<H = ()> {
210    dataver: Dataver,
211    /// `Some(session)` while identifying, `None` when idle. Reads of
212    /// `IdentifyTime` compute `duration.saturating_sub(elapsed)` from the
213    /// captured `(endpoint_id, duration, start_instant)` on-demand.
214    session: Mutex<Cell<Option<Session>>>,
215    /// Wakes the run loop on writes/commands so it can re-arm its
216    /// `Timer::at(deadline)` against the new session, instead of letting
217    /// the previous deadline complete with stale parameters.
218    state_change: Notification,
219    hooks: H,
220}
221
222impl IdentifyHandler<()> {
223    /// Creates a new `IdentifyHandler` with the no-hardware default
224    /// `()` hooks. Suitable for headless test fixtures.
225    ///
226    /// The endpoint ID is *not* a constructor argument: it is captured at
227    /// the first write/command from the surrounding `OperationContext`.
228    pub const fn new(dataver: Dataver) -> Self {
229        Self::new_with(dataver, ())
230    }
231}
232
233impl<H> IdentifyHandler<H>
234where
235    H: IdentifyHooks,
236{
237    /// Creates a new `IdentifyHandler` with application-supplied hooks.
238    pub const fn new_with(dataver: Dataver, hooks: H) -> Self {
239        Self {
240            dataver,
241            session: Mutex::new(Cell::new(None)),
242            state_change: Notification::new(),
243            hooks,
244        }
245    }
246
247    /// Compute the current `IdentifyTime` value from the captured session,
248    /// saturating at zero if the deadline has already passed (the
249    /// background `run` task may not yet have observed expiry).
250    fn remaining(&self) -> u16 {
251        let Some(Session {
252            duration, start, ..
253        }) = self.session.lock(|cell| cell.get())
254        else {
255            return 0;
256        };
257        let elapsed = start.elapsed().as_secs();
258        // `duration as u64` is in [0, u16::MAX]; `saturating_sub` returns a
259        // value in [0, lhs]; therefore the result is in [0, u16::MAX] and
260        // the narrowing cast back to `u16` is lossless.
261        (duration as u64).saturating_sub(elapsed) as u16
262    }
263
264    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait.
265    pub const fn adapt(self) -> HandlerAdaptor<Self> {
266        HandlerAdaptor(self)
267    }
268
269    /// Capture a new identify session (or clear the existing one),
270    /// dispatch the matching [`IdentifyAction`] to the hooks, and wake the
271    /// run loop so it can re-arm its deadline timer. Caller is responsible
272    /// for the subsequent attribute-changed notification — the
273    /// WriteContext path uses `ctx.notify_changed()` and the InvokeContext
274    /// path uses `ctx.notify_own_attr_changed(...)`.
275    fn set_identify_time_internal(&self, endpoint_id: EndptId, value: u16) {
276        self.session.lock(|cell| {
277            if value == 0 {
278                cell.set(None);
279            } else {
280                cell.set(Some(Session {
281                    endpoint_id,
282                    duration: value,
283                    start: Instant::now(),
284                }));
285            }
286        });
287        self.state_change.notify();
288        self.hooks.identify(if value == 0 {
289            IdentifyAction::Cancel
290        } else {
291            IdentifyAction::Time(value)
292        });
293    }
294}
295
296impl<H> IdentifyStatus for IdentifyHandler<H>
297where
298    H: IdentifyHooks,
299{
300    fn is_identifying(&self) -> bool {
301        self.remaining() > 0
302    }
303}
304
305impl<H> ClusterHandler for IdentifyHandler<H>
306where
307    H: IdentifyHooks,
308{
309    // No optional features supported; default cluster metadata covers the
310    // mandatory `IdentifyTime` / `IdentifyType` attributes and the
311    // mandatory `Identify` / `TriggerEffect` commands. Same as the
312    // module-level `identify::CLUSTER` constant — the duplication exists
313    // because trait-impl associated consts can't reference free items
314    // by short path inside the impl body without trips through name
315    // resolution that depend on glob re-export ordering.
316    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
317
318    fn dataver(&self) -> u32 {
319        self.dataver.get()
320    }
321
322    fn dataver_changed(&self) {
323        self.dataver.changed();
324    }
325
326    fn identify_time(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
327        Ok(self.remaining())
328    }
329
330    fn identify_type(&self, _ctx: impl ReadContext) -> Result<IdentifyTypeEnum, Error> {
331        Ok(self.hooks.identify_type())
332    }
333
334    fn set_identify_time(&self, ctx: impl WriteContext, value: u16) -> Result<(), Error> {
335        self.set_identify_time_internal(ctx.attr().endpoint_id, value);
336        // The shortcut for "the attribute the framework just wrote": fans
337        // out a `notify_attr_changed` for `IdentifyTime`, which
338        // simultaneously triggers the `bump_dataver(MatchContext)` chain
339        // and notifies any subscribers.
340        ctx.notify_changed();
341        Ok(())
342    }
343
344    fn handle_identify(
345        &self,
346        ctx: impl InvokeContext,
347        request: IdentifyRequest<'_>,
348    ) -> Result<(), Error> {
349        let time = request.identify_time()?;
350        self.set_identify_time_internal(ctx.cmd().endpoint_id, time);
351        // The Identify command mutates an attribute that lives on the
352        // *same* cluster as the command (App Cluster), so use the
353        // `OwnAttrChangeNotifier` shortcut to notify against
354        // `(ctx.cmd().endpoint_id, ctx.cmd().cluster_id, IdentifyTime)`.
355        ctx.notify_own_attr_changed(AttributeId::IdentifyTime as _);
356        Ok(())
357    }
358
359    fn handle_trigger_effect(
360        &self,
361        _ctx: impl InvokeContext,
362        request: TriggerEffectRequest<'_>,
363    ) -> Result<(), Error> {
364        let effect = request.effect_identifier()?;
365        let variant = request.effect_variant()?;
366        self.hooks.identify(IdentifyAction::Effect(effect, variant));
367        Ok(())
368    }
369
370    async fn run(&self, ctx: impl HandlerContext) -> Result<(), Error> {
371        loop {
372            let Some(Session {
373                endpoint_id,
374                duration,
375                start,
376            }) = self.session.lock(|cell| cell.get())
377            else {
378                // Idle: wait for a write/command to start a session.
379                self.state_change.wait().await;
380                continue;
381            };
382
383            // Compute the absolute deadline once and race a single timer
384            // against the wakeup signal — no per-second polling, and no
385            // accumulating sub-second drift across multiple ticks.
386            let deadline = start + Duration::from_secs(duration as u64);
387
388            match select(Timer::at(deadline), pin!(self.state_change.wait())).await {
389                Either::First(_) => {
390                    // Deadline reached: clear the session so subsequent
391                    // reads of `IdentifyTime` return 0, dispatch the
392                    // application-visible `Cancel` action, and notify
393                    // subscribers of the final-zero transition (Q-quality
394                    // reportable per App Cluster spec). The
395                    // framework auto-bumps the cluster dataver through
396                    // the `bump_dataver` chain as a side effect.
397                    self.session.lock(|cell| cell.set(None));
398                    self.hooks.identify(IdentifyAction::Cancel);
399                    ctx.notify_attr_changed(
400                        endpoint_id,
401                        <Self as ClusterHandler>::CLUSTER.id,
402                        AttributeId::IdentifyTime as _,
403                    );
404                }
405                Either::Second(_) => {
406                    // Re-armed via write/command (or cancelled with `0`);
407                    // loop and re-read the session.
408                }
409            }
410        }
411    }
412}