Skip to main content

rs_matter/dm/clusters/
thread_diag.rs

1/*
2 *
3 *    Copyright (c) 2025-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//! This module contains the implementation of the Thread Network Diagnostics cluster and its handler.
19
20use core::fmt::Debug;
21
22use rs_matter_macros::{FromTLV, ToTLV};
23
24use crate::dm::{ArrayAttributeRead, Dataver, InvokeContext, ReadContext};
25use crate::error::{Error, ErrorCode};
26use crate::tlv::{
27    Nullable, NullableBuilder, Octets, OctetsBuilder, TLVBuilderParent, ToTLVArrayBuilder,
28    ToTLVBuilder, Utf8StrBuilder,
29};
30use crate::with;
31
32pub use crate::dm::clusters::decl::thread_network_diagnostics::*;
33
34use super::wifi_diag::WirelessDiag;
35
36/// Thread Neighbor Table as returned by the `ThreadDiag` trait
37#[derive(Debug, Clone, Eq, PartialEq, Hash, FromTLV, ToTLV)]
38#[cfg_attr(feature = "defmt", derive(defmt::Format))]
39pub struct NeighborTable {
40    pub ext_address: u64,
41    pub age: u32,
42    pub rloc16: u16,
43    pub link_frame_counter: u32,
44    pub mle_frame_counter: u32,
45    pub lqi: u8,
46    pub average_rssi: Option<i8>,
47    pub last_rssi: Option<i8>,
48    pub frame_error_rate: u8,
49    pub message_error_rate: u8,
50    pub rx_on_when_idle: bool,
51    pub full_thread_device: bool,
52    pub full_network_data: bool,
53    pub is_child: bool,
54}
55
56impl NeighborTable {
57    /// Reads the `NeighborTable` into the provided `NeighborTableStructBuilder`.
58    fn read_into<P: TLVBuilderParent>(
59        &self,
60        builder: NeighborTableStructBuilder<P>,
61    ) -> Result<P, Error> {
62        builder
63            .ext_address(self.ext_address)?
64            .age(self.age)?
65            .rloc_16(self.rloc16)?
66            .link_frame_counter(self.link_frame_counter)?
67            .mle_frame_counter(self.mle_frame_counter)?
68            .lqi(self.lqi)?
69            .average_rssi(Nullable::new(self.average_rssi))?
70            .last_rssi(Nullable::new(self.last_rssi))?
71            .frame_error_rate(self.frame_error_rate)?
72            .message_error_rate(self.message_error_rate)?
73            .rx_on_when_idle(self.rx_on_when_idle)?
74            .full_thread_device(self.full_thread_device)?
75            .full_network_data(self.full_network_data)?
76            .is_child(self.is_child)?
77            .end()
78    }
79}
80
81/// Thread Route Table as returned by the `ThreadDiag` trait
82#[derive(Debug, Clone, Eq, PartialEq, Hash, FromTLV, ToTLV)]
83#[cfg_attr(feature = "defmt", derive(defmt::Format))]
84pub struct RouteTable {
85    pub ext_address: u64,
86    pub rloc16: u16,
87    pub router_id: u8,
88    pub next_hop: u8,
89    pub path_cost: u8,
90    pub lqi_in: u8,
91    pub lqi_out: u8,
92    pub age: u8,
93    pub allocated: bool,
94    pub link_established: bool,
95}
96
97impl RouteTable {
98    /// Reads the `RouteTable` into the provided `RouteTableStructBuilder`.
99    fn read_into<P: TLVBuilderParent>(
100        &self,
101        builder: RouteTableStructBuilder<P>,
102    ) -> Result<P, Error> {
103        builder
104            .ext_address(self.ext_address)?
105            .rloc_16(self.rloc16)?
106            .router_id(self.router_id)?
107            .next_hop(self.next_hop)?
108            .path_cost(self.path_cost)?
109            .lqi_in(self.lqi_in)?
110            .lqi_out(self.lqi_out)?
111            .age(self.age)?
112            .allocated(self.allocated)?
113            .link_established(self.link_established)?
114            .end()
115    }
116}
117
118/// Thread Routing Role as returned by the `ThreadDiag` trait
119#[derive(Debug, Clone, Eq, PartialEq, Hash, FromTLV, ToTLV)]
120#[cfg_attr(feature = "defmt", derive(defmt::Format))]
121pub struct SecurityPolicy {
122    pub rotation_time: u16,
123    pub flags: u16,
124}
125
126/// Thread Operational Dataset Components as returned by the `ThreadDiag` trait
127#[derive(Debug, Clone, Eq, PartialEq, Hash, FromTLV, ToTLV)]
128#[cfg_attr(feature = "defmt", derive(defmt::Format))]
129pub struct OperationalDatasetComponents {
130    pub active_timestamp_present: bool,
131    pub pending_timestamp_present: bool,
132    pub master_key_present: bool,
133    pub network_name_present: bool,
134    pub extended_pan_id_present: bool,
135    pub mesh_local_prefix_present: bool,
136    pub delay_present: bool,
137    pub pan_id_present: bool,
138    pub channel_present: bool,
139    pub pskc_present: bool,
140    pub security_policy_present: bool,
141    pub channel_mask_present: bool,
142}
143
144impl OperationalDatasetComponents {
145    /// Reads the `OperationalDatasetComponents` into the provided `OperationalDatasetComponentsBuilder`.
146    fn read_into<P: TLVBuilderParent>(
147        &self,
148        builder: OperationalDatasetComponentsBuilder<P>,
149    ) -> Result<P, Error> {
150        builder
151            .active_timestamp_present(self.active_timestamp_present)?
152            .pending_timestamp_present(self.pending_timestamp_present)?
153            .master_key_present(self.master_key_present)?
154            .network_name_present(self.network_name_present)?
155            .extended_pan_id_present(self.extended_pan_id_present)?
156            .mesh_local_prefix_present(self.mesh_local_prefix_present)?
157            .delay_present(self.delay_present)?
158            .pan_id_present(self.pan_id_present)?
159            .channel_present(self.channel_present)?
160            .pskc_present(self.pskc_present)?
161            .security_policy_present(self.security_policy_present)?
162            .channel_mask_present(self.channel_mask_present)?
163            .end()
164    }
165}
166
167/// The minimal set of data required to implement the Thread Network Diagnostics Cluster
168///
169/// The names of the methods in this trait are matching 1:1 the mandatory attributes of the
170/// Thread Network Diagnostics Cluster.
171pub trait ThreadDiag: WirelessDiag {
172    fn channel(&self) -> Result<Option<u16>, Error> {
173        Ok(None)
174    }
175
176    fn routing_role(&self) -> Result<Option<RoutingRoleEnum>, Error> {
177        Ok(None)
178    }
179
180    fn network_name(
181        &self,
182        f: &mut dyn FnMut(Option<&str>) -> Result<(), Error>,
183    ) -> Result<(), Error> {
184        f(None)
185    }
186
187    fn pan_id(&self) -> Result<Option<u16>, Error> {
188        Ok(None)
189    }
190
191    fn extended_pan_id(&self) -> Result<Option<u64>, Error> {
192        Ok(None)
193    }
194
195    #[allow(clippy::type_complexity)]
196    fn mesh_local_prefix(
197        &self,
198        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
199    ) -> Result<(), Error> {
200        f(None)
201    }
202
203    fn neighbor_table(
204        &self,
205        _f: &mut dyn FnMut(&NeighborTable) -> Result<(), Error>,
206    ) -> Result<(), Error> {
207        Ok(())
208    }
209
210    fn route_table(
211        &self,
212        _f: &mut dyn FnMut(&RouteTable) -> Result<(), Error>,
213    ) -> Result<(), Error> {
214        Ok(())
215    }
216
217    fn partition_id(&self) -> Result<Option<u32>, Error> {
218        Ok(None)
219    }
220
221    fn weighting(&self) -> Result<Option<u16>, Error> {
222        Ok(None)
223    }
224
225    fn data_version(&self) -> Result<Option<u16>, Error> {
226        Ok(None)
227    }
228
229    fn stable_data_version(&self) -> Result<Option<u16>, Error> {
230        Ok(None)
231    }
232
233    fn leader_router_id(&self) -> Result<Option<u8>, Error> {
234        Ok(None)
235    }
236
237    fn ext_address(&self) -> Result<Option<u64>, Error> {
238        Ok(None)
239    }
240
241    fn rloc_16(&self) -> Result<Option<u16>, Error> {
242        Ok(None)
243    }
244
245    fn security_policy(&self) -> Result<Option<SecurityPolicy>, Error> {
246        Ok(None)
247    }
248
249    #[allow(clippy::type_complexity)]
250    fn channel_page0_mask(
251        &self,
252        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
253    ) -> Result<(), Error> {
254        f(None)
255    }
256
257    #[allow(clippy::type_complexity)]
258    fn operational_dataset_components(
259        &self,
260        f: &mut dyn FnMut(Option<&OperationalDatasetComponents>) -> Result<(), Error>,
261    ) -> Result<(), Error> {
262        f(None)
263    }
264
265    #[allow(clippy::type_complexity)]
266    fn active_network_faults_list(
267        &self,
268        _f: &mut dyn FnMut(NetworkFaultEnum) -> Result<(), Error>,
269    ) -> Result<(), Error> {
270        Ok(())
271    }
272}
273
274impl<T> ThreadDiag for &T
275where
276    T: ThreadDiag,
277{
278    fn channel(&self) -> Result<Option<u16>, Error> {
279        (*self).channel()
280    }
281
282    fn routing_role(&self) -> Result<Option<RoutingRoleEnum>, Error> {
283        (*self).routing_role()
284    }
285
286    fn network_name(
287        &self,
288        f: &mut dyn FnMut(Option<&str>) -> Result<(), Error>,
289    ) -> Result<(), Error> {
290        (*self).network_name(f)
291    }
292
293    fn pan_id(&self) -> Result<Option<u16>, Error> {
294        (*self).pan_id()
295    }
296
297    fn extended_pan_id(&self) -> Result<Option<u64>, Error> {
298        (*self).extended_pan_id()
299    }
300
301    fn mesh_local_prefix(
302        &self,
303        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
304    ) -> Result<(), Error> {
305        (*self).mesh_local_prefix(f)
306    }
307
308    fn neighbor_table(
309        &self,
310        f: &mut dyn FnMut(&NeighborTable) -> Result<(), Error>,
311    ) -> Result<(), Error> {
312        (*self).neighbor_table(f)
313    }
314
315    fn route_table(
316        &self,
317        f: &mut dyn FnMut(&RouteTable) -> Result<(), Error>,
318    ) -> Result<(), Error> {
319        (*self).route_table(f)
320    }
321
322    fn partition_id(&self) -> Result<Option<u32>, Error> {
323        (*self).partition_id()
324    }
325
326    fn weighting(&self) -> Result<Option<u16>, Error> {
327        (*self).weighting()
328    }
329
330    fn data_version(&self) -> Result<Option<u16>, Error> {
331        (*self).data_version()
332    }
333
334    fn stable_data_version(&self) -> Result<Option<u16>, Error> {
335        (*self).stable_data_version()
336    }
337
338    fn leader_router_id(&self) -> Result<Option<u8>, Error> {
339        (*self).leader_router_id()
340    }
341
342    fn ext_address(&self) -> Result<Option<u64>, Error> {
343        (*self).ext_address()
344    }
345
346    fn rloc_16(&self) -> Result<Option<u16>, Error> {
347        (*self).rloc_16()
348    }
349
350    fn security_policy(&self) -> Result<Option<SecurityPolicy>, Error> {
351        (*self).security_policy()
352    }
353
354    fn channel_page0_mask(
355        &self,
356        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
357    ) -> Result<(), Error> {
358        (*self).channel_page0_mask(f)
359    }
360
361    fn operational_dataset_components(
362        &self,
363        f: &mut dyn FnMut(Option<&OperationalDatasetComponents>) -> Result<(), Error>,
364    ) -> Result<(), Error> {
365        (*self).operational_dataset_components(f)
366    }
367
368    fn active_network_faults_list(
369        &self,
370        f: &mut dyn FnMut(NetworkFaultEnum) -> Result<(), Error>,
371    ) -> Result<(), Error> {
372        (*self).active_network_faults_list(f)
373    }
374}
375
376impl ThreadDiag for () {}
377
378/// A cluster implementing the Matter Thread Diagnostics Cluster.
379#[derive(Clone)]
380pub struct ThreadDiagHandler<'a> {
381    dataver: Dataver,
382    diag: &'a dyn ThreadDiag,
383}
384
385impl<'a> ThreadDiagHandler<'a> {
386    /// Create a new instance.
387    pub const fn new(dataver: Dataver, diag: &'a dyn ThreadDiag) -> Self {
388        Self { dataver, diag }
389    }
390
391    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
392    pub const fn adapt(self) -> HandlerAdaptor<Self> {
393        HandlerAdaptor(self)
394    }
395}
396
397impl ClusterHandler for ThreadDiagHandler<'_> {
398    const CLUSTER: crate::dm::Cluster<'static> =
399        FULL_CLUSTER.with_attrs(with!(required)).with_cmds(with!());
400
401    fn dataver(&self) -> u32 {
402        self.dataver.get()
403    }
404
405    fn dataver_changed(&self) {
406        self.dataver.changed();
407    }
408
409    fn channel(&self, _ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
410        Ok(Nullable::new(self.diag.channel()?))
411    }
412
413    fn routing_role(&self, _ctx: impl ReadContext) -> Result<Nullable<RoutingRoleEnum>, Error> {
414        Ok(Nullable::new(self.diag.routing_role()?))
415    }
416
417    fn network_name<P: TLVBuilderParent>(
418        &self,
419        _ctx: impl ReadContext,
420        builder: NullableBuilder<P, Utf8StrBuilder<P>>,
421    ) -> Result<P, Error> {
422        let mut builder = Some(builder);
423        let mut parent = None;
424
425        self.diag.network_name(&mut |name| {
426            if let Some(name) = name {
427                parent = Some(unwrap!(builder.take()).non_null()?.set(name)?);
428            } else {
429                parent = Some(unwrap!(builder.take()).null()?);
430            }
431
432            Ok(())
433        })?;
434
435        Ok(unwrap!(parent))
436    }
437
438    fn pan_id(&self, _ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
439        Ok(Nullable::new(self.diag.pan_id()?))
440    }
441
442    fn extended_pan_id(&self, _ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
443        Ok(Nullable::new(self.diag.extended_pan_id()?))
444    }
445
446    fn mesh_local_prefix<P: TLVBuilderParent>(
447        &self,
448        _ctx: impl ReadContext,
449        builder: NullableBuilder<P, OctetsBuilder<P>>,
450    ) -> Result<P, Error> {
451        let mut builder = Some(builder);
452        let mut parent = None;
453
454        self.diag.mesh_local_prefix(&mut |prefix| {
455            if let Some(prefix) = prefix {
456                parent = Some(
457                    unwrap!(builder.take())
458                        .non_null()?
459                        .set(Octets::new(prefix))?,
460                );
461            } else {
462                parent = Some(unwrap!(builder.take()).null()?);
463            }
464
465            Ok(())
466        })?;
467
468        Ok(unwrap!(parent))
469    }
470
471    fn neighbor_table<P: TLVBuilderParent>(
472        &self,
473        _ctx: impl ReadContext,
474        builder: ArrayAttributeRead<
475            NeighborTableStructArrayBuilder<P>,
476            NeighborTableStructBuilder<P>,
477        >,
478    ) -> Result<P, Error> {
479        match builder {
480            ArrayAttributeRead::ReadAll(builder) => {
481                let mut builder = Some(builder);
482
483                self.diag.neighbor_table(&mut |item| {
484                    builder = Some(item.read_into(unwrap!(builder.take()).push()?)?);
485
486                    Ok(())
487                })?;
488
489                unwrap!(builder).end()
490            }
491            ArrayAttributeRead::ReadOne(index, builder) => {
492                let mut builder = Some(builder);
493                let mut parent = None;
494                let mut current = 0;
495
496                self.diag.neighbor_table(&mut |item| {
497                    if index == current {
498                        parent = Some(item.read_into(unwrap!(builder.take()))?);
499                    }
500
501                    current += 1;
502
503                    Ok(())
504                })?;
505
506                if let Some(parent) = parent {
507                    Ok(parent)
508                } else {
509                    Err(ErrorCode::InvalidAction.into())
510                }
511            }
512            ArrayAttributeRead::ReadNone(builder) => builder.end(),
513        }
514    }
515
516    fn route_table<P: TLVBuilderParent>(
517        &self,
518        _ctx: impl ReadContext,
519        builder: ArrayAttributeRead<RouteTableStructArrayBuilder<P>, RouteTableStructBuilder<P>>,
520    ) -> Result<P, Error> {
521        match builder {
522            ArrayAttributeRead::ReadAll(builder) => {
523                let mut builder = Some(builder);
524
525                self.diag.route_table(&mut |item| {
526                    builder = Some(item.read_into(unwrap!(builder.take()).push()?)?);
527
528                    Ok(())
529                })?;
530
531                unwrap!(builder).end()
532            }
533            ArrayAttributeRead::ReadOne(index, builder) => {
534                let mut builder = Some(builder);
535                let mut parent = None;
536                let mut current = 0;
537
538                self.diag.route_table(&mut |item| {
539                    if index == current {
540                        parent = Some(item.read_into(unwrap!(builder.take()))?);
541                    }
542
543                    current += 1;
544
545                    Ok(())
546                })?;
547
548                if let Some(parent) = parent {
549                    Ok(parent)
550                } else {
551                    Err(ErrorCode::InvalidAction.into())
552                }
553            }
554            ArrayAttributeRead::ReadNone(builder) => builder.end(),
555        }
556    }
557
558    fn partition_id(&self, _ctx: impl ReadContext) -> Result<Nullable<u32>, Error> {
559        Ok(Nullable::new(self.diag.partition_id()?))
560    }
561
562    fn weighting(&self, _ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
563        Ok(Nullable::new(self.diag.weighting()?))
564    }
565
566    fn data_version(&self, _ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
567        Ok(Nullable::new(self.diag.data_version()?))
568    }
569
570    fn stable_data_version(&self, _ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
571        Ok(Nullable::new(self.diag.stable_data_version()?))
572    }
573
574    fn leader_router_id(&self, _ctx: impl ReadContext) -> Result<Nullable<u8>, Error> {
575        Ok(Nullable::new(self.diag.leader_router_id()?))
576    }
577
578    fn security_policy<P: TLVBuilderParent>(
579        &self,
580        _ctx: impl ReadContext,
581        builder: NullableBuilder<P, SecurityPolicyBuilder<P>>,
582    ) -> Result<P, Error> {
583        let security_policy = self.diag.security_policy()?;
584        if let Some(security_policy) = security_policy {
585            builder
586                .non_null()?
587                .rotation_time(security_policy.rotation_time)?
588                .flags(security_policy.flags)?
589                .end()
590        } else {
591            builder.null()
592        }
593    }
594
595    fn channel_page_0_mask<P: TLVBuilderParent>(
596        &self,
597        _ctx: impl ReadContext,
598        builder: NullableBuilder<P, OctetsBuilder<P>>,
599    ) -> Result<P, Error> {
600        let mut builder = Some(builder);
601        let mut parent = None;
602
603        self.diag.channel_page0_mask(&mut |mask| {
604            if let Some(mask) = mask {
605                parent = Some(unwrap!(builder.take()).non_null()?.set(Octets::new(mask))?);
606            } else {
607                parent = Some(unwrap!(builder.take()).null()?);
608            }
609
610            Ok(())
611        })?;
612
613        Ok(unwrap!(parent.take()))
614    }
615
616    fn operational_dataset_components<P: TLVBuilderParent>(
617        &self,
618        _ctx: impl ReadContext,
619        builder: NullableBuilder<P, OperationalDatasetComponentsBuilder<P>>,
620    ) -> Result<P, Error> {
621        let mut builder = Some(builder);
622        let mut parent = None;
623
624        self.diag.operational_dataset_components(&mut |dsc| {
625            if let Some(dsc) = dsc {
626                parent = Some(dsc.read_into(unwrap!(builder.take()).non_null()?)?);
627            } else {
628                parent = Some(unwrap!(builder.take()).null()?);
629            }
630
631            Ok(())
632        })?;
633
634        Ok(unwrap!(parent))
635    }
636
637    fn active_network_faults_list<P: TLVBuilderParent>(
638        &self,
639        _ctx: impl ReadContext,
640        builder: ArrayAttributeRead<
641            ToTLVArrayBuilder<P, NetworkFaultEnum>,
642            ToTLVBuilder<P, NetworkFaultEnum>,
643        >,
644    ) -> Result<P, Error> {
645        match builder {
646            ArrayAttributeRead::ReadAll(builder) => {
647                let mut builder = Some(builder);
648
649                self.diag.active_network_faults_list(&mut |fault| {
650                    builder = Some(unwrap!(builder.take()).push(&fault)?);
651
652                    Ok(())
653                })?;
654
655                unwrap!(builder.take()).end()
656            }
657            ArrayAttributeRead::ReadOne(index, builder) => {
658                let mut builder = Some(builder);
659                let mut parent = None;
660                let mut current = 0;
661
662                self.diag.active_network_faults_list(&mut |fault| {
663                    if index == current {
664                        parent = Some(unwrap!(builder.take()).set(&fault)?);
665                    }
666
667                    current += 1;
668
669                    Ok(())
670                })?;
671
672                if let Some(parent) = parent {
673                    Ok(parent)
674                } else {
675                    Err(ErrorCode::InvalidAction.into())
676                }
677            }
678            ArrayAttributeRead::ReadNone(builder) => builder.end(),
679        }
680    }
681    fn ext_address(&self, _ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
682        Ok(Nullable::new(self.diag.ext_address()?))
683    }
684
685    fn rloc_16(&self, _ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
686        Ok(Nullable::new(self.diag.rloc_16()?))
687    }
688
689    fn handle_reset_counts(&self, _ctx: impl InvokeContext) -> Result<(), Error> {
690        Err(ErrorCode::CommandNotFound.into())
691    }
692}
693
694impl Debug for ThreadDiagHandler<'_> {
695    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
696        f.debug_struct("ThreadDiagHandler")
697            .field("dataver", &self.dataver)
698            .finish()
699    }
700}
701
702#[cfg(feature = "defmt")]
703impl defmt::Format for ThreadDiagHandler<'_> {
704    fn format(&self, f: defmt::Formatter) {
705        defmt::write!(f, "ThreadDiagHandler {{ dataver: {} }}", self.dataver.get());
706    }
707}