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 {}