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