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}