Skip to main content

rs_matter/dm/clusters/
sw_diag.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//! Implementation of the Software Diagnostics cluster.
19//!
20//! # Cluster-shape selection — endpoint-side, via [`Options`]
21//!
22//! Matter features (`WATERMARKS`) and spec-independent-optional
23//! attribute toggles (`HEAP` for the heap counters, `THREAD` for the
24//! `ThreadMetrics` list) are unified into a single [`Options`]
25//! bitflags type, consumed by the [`cluster`] `const fn` which
26//! returns the matching `Cluster<'static>` metadata.
27//!
28//! The shape is picked **endpoint-side**, on the `clusters!` /
29//! `root_endpoint!` macros — e.g. `clusters!(sys, sw_diag(heap,
30//! watermarks); …)` — not on the handler. [`SwDiagHandler`] itself
31//! is non-generic and its [`Self::CLUSTER`](ClusterHandler::CLUSTER)
32//! is pinned to the empty-options shape; only `CLUSTER.id` is
33//! actually consulted by the dispatcher, and the per-attribute /
34//! per-command dispatch is driven by what the endpoint advertises.
35//!
36//! This decoupling means a single handler instance can be installed
37//! against any cluster shape — what gets dispatched to it is decided
38//! by the endpoint metadata, and the [`SwDiag`] trait carries methods
39//! for every option. Methods corresponding to un-advertised options
40//! are simply never called.
41//!
42//! # Pluggable data source — [`SwDiag`]
43//!
44//! [`SwDiagHandler`] borrows a `&dyn SwDiag` data provider and
45//! forwards every attribute read / command invoke to it. The trait
46//! is intentionally abstract (no default methods) — the implementor
47//! is forced to make an explicit choice for each method, paired
48//! with the [`Feature`] set they've picked.
49//!
50//! [`impl SwDiag for ()`] is the canonical "we don't track
51//! anything" provider (heap counters return `0`, thread iteration
52//! emits nothing, `ResetWatermarks` refuses with `UnsupportedAccess`).
53//! Pass `&()` when no real telemetry is available.
54//!
55//! Thread metrics use a visitor-style callback
56//! ([`SwDiag::thread_metrics`]) rather than returning an allocated
57//! `Vec` so MCU implementations can stream the list straight out of
58//! their internal thread table.
59
60use bitflags::bitflags;
61
62use crate::dm::{ArrayAttributeRead, Cluster, Dataver, InvokeContext, ReadContext};
63use crate::error::{Error, ErrorCode};
64use crate::tlv::TLVBuilderParent;
65use crate::with;
66
67pub use crate::dm::clusters::decl::software_diagnostics::*;
68
69bitflags! {
70    /// Cluster-shape selectors for the [`SwDiagHandler`]. Each bit
71    /// turns on one orthogonal piece of the cluster surface — Matter
72    /// `Feature` bits (`WATERMARKS`) and spec-independent-optional
73    /// attribute toggles (`HEAP`, `THREAD`) are unified into a single
74    /// enumset so the user picks the whole shape with one literal.
75    ///
76    /// Used as the argument to [`cluster`] to compute the matching
77    /// `Cluster<'static>` metadata, which is then installed onto the
78    /// endpoint via the `clusters!` / `root_endpoint!` macros (e.g.
79    /// `clusters!(sys, sw_diag(heap, watermarks); …)`).
80    ///
81    /// `WATERMARKS` is the Matter `WATERMARKS` feature — it exposes
82    /// `CurrentHeapHighWatermark` + the `ResetWatermarks` command and
83    /// implies `HEAP` (the watermark tracks heap usage).
84    #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
85    pub struct Options: u8 {
86        /// Advertise the heap counters `CurrentHeapFree` and
87        /// `CurrentHeapUsed`. Independently optional per Matter Core
88        /// spec.
89        const HEAP = 0x1;
90        /// Claim the Matter `WATERMARKS` feature — adds
91        /// `CurrentHeapHighWatermark` + the `ResetWatermarks`
92        /// command, and surfaces the `WATERMARKS` bit in `FeatureMap`.
93        /// Implies [`Options::HEAP`].
94        const WATERMARKS = 0x2;
95        /// Advertise the `ThreadMetrics` list attribute. Set only on
96        /// devices that actually run multiple threads; a single-task
97        /// Wi-Fi MCU should leave this off so the cluster doesn't
98        /// misadvertise non-existent threads.
99        const THREAD = 0x4;
100    }
101}
102
103/// One thread's snapshot for the `ThreadMetrics` attribute. Yielded
104/// by the implementor of [`SwDiag::thread_metrics`] via a visitor
105/// closure; the lifetime `'a` is the borrow of the implementor's
106/// internal storage for the duration of the visit, so `name` can
107/// point straight into the implementor's thread-control-block
108/// without copying.
109#[derive(Debug, Clone, Eq, PartialEq, Hash)]
110pub struct ThreadMetric<'a> {
111    /// OS-assigned identifier for this thread. Mandatory per spec.
112    pub id: u64,
113    /// Human-readable thread name. `None` if the runtime doesn't
114    /// name threads.
115    pub name: Option<&'a str>,
116    /// Free stack bytes right now.
117    pub stack_free_current: Option<u32>,
118    /// Lowest free-stack value observed since boot (or since the
119    /// last `ResetWatermarks` if the `WATERMARKS` feature is claimed).
120    pub stack_free_minimum: Option<u32>,
121    /// Total stack size in bytes.
122    pub stack_size: Option<u32>,
123}
124
125/// Pluggable data source for the Software Diagnostics cluster
126/// handler. All methods have sensible defaults so an implementor
127/// can opt in to just the bits the device can actually report;
128/// `impl SwDiag for ()` lets `&()` stand in as the no-op provider.
129pub trait SwDiag {
130    /// Free bytes on the device's heap right now. Default `0` for
131    /// "not tracked".
132    fn current_heap_free(&self) -> Result<u64, Error>;
133
134    /// Used bytes on the device's heap right now. Default `0`.
135    fn current_heap_used(&self) -> Result<u64, Error>;
136
137    /// Maximum used bytes observed since boot or since the last
138    /// `ResetWatermarks` invocation. Default `0`. Only meaningful
139    /// when the handler is configured to claim the `WATERMARKS` feature.
140    fn current_heap_high_watermark(&self) -> Result<u64, Error>;
141
142    /// Stream per-thread metrics into `visit`. The implementor calls
143    /// `visit(&ThreadMetric { … })` once per thread it wants to
144    /// report; the handler relays each call into the on-wire
145    /// `ThreadMetrics` array.
146    ///
147    /// `&ThreadMetric<'_>` is borrowed per-call: the implementor can
148    /// build the metric from references into its own thread-control
149    /// blocks (no `Vec` / `String` allocation needed).
150    ///
151    /// Default: emit nothing (no threads tracked) — the wire-side
152    /// `ThreadMetrics` attribute reads as an empty list.
153    fn thread_metrics(
154        &self,
155        _visit: &mut dyn FnMut(&ThreadMetric<'_>) -> Result<(), Error>,
156    ) -> Result<(), Error>;
157
158    /// Reset the high-watermark tracker. Default refuses with
159    /// `UnsupportedAccess` — `ResetWatermarks` is gated on the
160    /// `WATERMARKS` feature, which the current handler doesn't claim.
161    fn reset_watermarks(&self) -> Result<(), Error>;
162}
163
164impl<T> SwDiag for &T
165where
166    T: SwDiag,
167{
168    fn current_heap_free(&self) -> Result<u64, Error> {
169        (*self).current_heap_free()
170    }
171
172    fn current_heap_used(&self) -> Result<u64, Error> {
173        (*self).current_heap_used()
174    }
175
176    fn current_heap_high_watermark(&self) -> Result<u64, Error> {
177        (*self).current_heap_high_watermark()
178    }
179
180    fn thread_metrics(
181        &self,
182        visit: &mut dyn FnMut(&ThreadMetric<'_>) -> Result<(), Error>,
183    ) -> Result<(), Error> {
184        (*self).thread_metrics(visit)
185    }
186
187    fn reset_watermarks(&self) -> Result<(), Error> {
188        (*self).reset_watermarks()
189    }
190}
191
192/// No-op `SwDiag` provider used as `&()` for "no heap / thread
193/// telemetry" — matches the convention used by [`crate::dm::clusters::wifi_diag::WifiDiag`].
194impl SwDiag for () {
195    fn current_heap_free(&self) -> Result<u64, Error> {
196        Ok(0)
197    }
198
199    fn current_heap_used(&self) -> Result<u64, Error> {
200        Ok(0)
201    }
202
203    fn current_heap_high_watermark(&self) -> Result<u64, Error> {
204        Ok(0)
205    }
206
207    fn thread_metrics(
208        &self,
209        _visit: &mut dyn FnMut(&ThreadMetric<'_>) -> Result<(), Error>,
210    ) -> Result<(), Error> {
211        Ok(())
212    }
213
214    fn reset_watermarks(&self) -> Result<(), Error> {
215        Err(ErrorCode::UnsupportedAccess.into())
216    }
217}
218
219/// Compute the `Cluster<'static>` metadata for a SwDiag handler
220/// whose advertised surface is described by `options`. See the
221/// [`Options`] flags for the per-bit detail.
222///
223/// `Options::WATERMARKS` implies `Options::HEAP` — the watermark
224/// tracks heap usage, so requesting a watermark without claiming
225/// the heap counters is meaningless and the implication is folded
226/// in here. Pair the returned shape with a [`SwDiag`]
227/// implementation whose corresponding methods return real values
228/// for the chosen options; the handler forwards every trait method
229/// unconditionally, and methods for un-advertised attributes /
230/// commands are simply never dispatched by the DM.
231pub const fn cluster(options: Options) -> Cluster<'static> {
232    let heap = options.bits() & Options::HEAP.bits() != 0
233        || options.bits() & Options::WATERMARKS.bits() != 0;
234    let watermarks = options.bits() & Options::WATERMARKS.bits() != 0;
235    let thread = options.bits() & Options::THREAD.bits() != 0;
236
237    let matter_features = if watermarks {
238        Feature::WATERMARKS.bits()
239    } else {
240        0
241    };
242
243    let cluster = FULL_CLUSTER.with_features(matter_features);
244
245    match (heap, watermarks, thread) {
246        (false, _, false) => cluster.with_attrs(with!(required)).with_cmds(with!()),
247        (false, _, true) => cluster
248            .with_attrs(with!(required; AttributeId::ThreadMetrics))
249            .with_cmds(with!()),
250        (true, false, false) => cluster
251            .with_attrs(with!(required;
252                AttributeId::CurrentHeapFree | AttributeId::CurrentHeapUsed))
253            .with_cmds(with!()),
254        (true, false, true) => cluster
255            .with_attrs(with!(required;
256                AttributeId::ThreadMetrics
257                    | AttributeId::CurrentHeapFree
258                    | AttributeId::CurrentHeapUsed))
259            .with_cmds(with!()),
260        (true, true, false) => cluster
261            .with_attrs(with!(required;
262                AttributeId::CurrentHeapFree
263                    | AttributeId::CurrentHeapUsed
264                    | AttributeId::CurrentHeapHighWatermark))
265            .with_cmds(with!(CommandId::ResetWatermarks)),
266        (true, true, true) => cluster
267            .with_attrs(with!(required;
268                AttributeId::ThreadMetrics
269                    | AttributeId::CurrentHeapFree
270                    | AttributeId::CurrentHeapUsed
271                    | AttributeId::CurrentHeapHighWatermark))
272            .with_cmds(with!(CommandId::ResetWatermarks)),
273    }
274}
275
276/// Handler for the Software Diagnostics Matter cluster.
277///
278/// Borrows a `&dyn SwDiag` data provider for the lifetime `'a` and
279/// forwards every attribute read / command invoke to it.
280///
281/// The handler is **not** parameterized by cluster shape:
282/// [`Self::CLUSTER`](ClusterHandler::CLUSTER) is pinned to the
283/// empty-options form and only its `id` is consulted by the
284/// dispatcher. The on-wire shape — which optional attributes /
285/// commands / features are advertised — is decided by the cluster
286/// metadata supplied on the endpoint side (e.g. `clusters!(sys,
287/// sw_diag(heap, watermarks); …)`); per-attribute dispatch follows
288/// the endpoint's metadata, so the handler answers exactly what
289/// the endpoint exposes.
290#[derive(Clone)]
291pub struct SwDiagHandler<'a> {
292    dataver: Dataver,
293    sw_diag: &'a dyn SwDiag,
294}
295
296impl<'a> SwDiagHandler<'a> {
297    /// Create a new handler bound to `sw_diag` for its lifetime.
298    /// Pass `&()` (the no-op [`SwDiag`] impl) when no real telemetry
299    /// source is available.
300    pub const fn new(dataver: Dataver, sw_diag: &'a dyn SwDiag) -> Self {
301        Self { dataver, sw_diag }
302    }
303
304    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
305    pub const fn adapt(self) -> HandlerAdaptor<Self> {
306        HandlerAdaptor(self)
307    }
308}
309
310impl ClusterHandler for SwDiagHandler<'_> {
311    const CLUSTER: Cluster<'static> = cluster(Options::empty());
312
313    fn dataver(&self) -> u32 {
314        self.dataver.get()
315    }
316
317    fn dataver_changed(&self) {
318        self.dataver.changed();
319    }
320
321    fn current_heap_free(&self, _ctx: impl ReadContext) -> Result<u64, Error> {
322        self.sw_diag.current_heap_free()
323    }
324
325    fn current_heap_used(&self, _ctx: impl ReadContext) -> Result<u64, Error> {
326        self.sw_diag.current_heap_used()
327    }
328
329    fn current_heap_high_watermark(&self, _ctx: impl ReadContext) -> Result<u64, Error> {
330        self.sw_diag.current_heap_high_watermark()
331    }
332
333    fn thread_metrics<P: TLVBuilderParent>(
334        &self,
335        _ctx: impl ReadContext,
336        builder: ArrayAttributeRead<
337            ThreadMetricsStructArrayBuilder<P>,
338            ThreadMetricsStructBuilder<P>,
339        >,
340    ) -> Result<P, Error> {
341        match builder {
342            ArrayAttributeRead::ReadAll(array) => {
343                // Stream the implementor's thread metrics into the
344                // wire-side array via the visitor closure. The
345                // `Option` dance is the same pattern used by
346                // `WifiDiagHandler::bssid` to thread the moved-by-value
347                // typestate builder through a `&mut dyn FnMut` closure.
348                let mut array_opt = Some(array);
349                self.sw_diag.thread_metrics(&mut |m| {
350                    let array = unwrap!(array_opt.take());
351                    let next = array
352                        .push()?
353                        .id(m.id)?
354                        .name(m.name)?
355                        .stack_free_current(m.stack_free_current)?
356                        .stack_free_minimum(m.stack_free_minimum)?
357                        .stack_size(m.stack_size)?
358                        .end()?;
359                    array_opt = Some(next);
360                    Ok(())
361                })?;
362                unwrap!(array_opt.take()).end()
363            }
364            ArrayAttributeRead::ReadOne(index, item_builder) => {
365                // Walk the implementor's thread list looking for the
366                // requested index; on hit, emit that one entry into
367                // the per-element builder. Consume `item_builder` at
368                // most once; if the index is out of range, return
369                // `ConstraintError` (same convention as
370                // `desc::DescHandler::device_type_list`).
371                let mut item_opt = Some(item_builder);
372                let mut returned: Option<P> = None;
373                let mut current = 0u16;
374                self.sw_diag.thread_metrics(&mut |m| {
375                    if returned.is_none() && current == index {
376                        let b = unwrap!(item_opt.take());
377                        returned = Some(
378                            b.id(m.id)?
379                                .name(m.name)?
380                                .stack_free_current(m.stack_free_current)?
381                                .stack_free_minimum(m.stack_free_minimum)?
382                                .stack_size(m.stack_size)?
383                                .end()?,
384                        );
385                    }
386                    current = current.saturating_add(1);
387                    Ok(())
388                })?;
389                returned.ok_or_else(|| ErrorCode::ConstraintError.into())
390            }
391            ArrayAttributeRead::ReadNone(array) => array.end(),
392        }
393    }
394
395    fn handle_reset_watermarks(&self, _ctx: impl InvokeContext) -> Result<(), Error> {
396        self.sw_diag.reset_watermarks()
397    }
398}
399
400impl core::fmt::Debug for SwDiagHandler<'_> {
401    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
402        f.debug_struct("SwDiagHandler")
403            .field("dataver", &self.dataver)
404            .finish()
405    }
406}
407
408#[cfg(feature = "defmt")]
409impl defmt::Format for SwDiagHandler<'_> {
410    fn format(&self, f: defmt::Formatter) {
411        defmt::write!(f, "SwDiagHandler {{ dataver: {} }}", self.dataver.get());
412    }
413}