Skip to main content

rs_matter/dm/networks/
wireless.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//! A module containing various types for managing Thread and Wifi networks.
19
20use core::fmt::{Debug, Display};
21
22use crate::dm::clusters::net_comm::{
23    self, NetCtlError, NetworkCommissioningStatusEnum, NetworkType, Networks, NetworksError,
24    ThreadCapabilitiesBitmap, WirelessCreds,
25};
26use crate::dm::clusters::{thread_diag, wifi_diag};
27use crate::error::{Error, ErrorCode};
28use crate::fmt::Bytes;
29use crate::persist::{KvBlobStore, NETWORKS_KEY};
30use crate::tlv::{FromTLV, TLVElement, TLVTag, TLVWrite, ToTLV};
31use crate::transport::network::btp::Btp;
32use crate::utils::cell::RefCell;
33use crate::utils::init::{init, Init};
34use crate::utils::storage::{Vec, WriteBuf};
35use crate::utils::sync::blocking;
36use crate::utils::sync::DynBase;
37
38use super::NetChangeNotif;
39
40pub use mgr::*;
41pub use thread::*;
42pub use wifi::*;
43
44mod mgr;
45mod thread;
46mod wifi;
47
48/// The maximum length of a wireless network ID.
49/// Coincides with the SSID maximum length because
50pub const MAX_WIRELESS_NETWORK_ID_LEN: usize = 32;
51
52/// A type alias for representing an owned ID of a wireless (Thread or Wifi) network.
53/// Both Thread and Wifi networks use the same ID type which is just an octet string.
54///
55/// For Thread networks, this is the Extended PAN ID (`u64` as 8 bytes, network order).
56/// For Wifi networks, this is the SSID (`u8` array of max length 32 bytes).
57pub type OwnedWirelessNetworkId = Vec<u8, MAX_WIRELESS_NETWORK_ID_LEN>;
58
59/// A trait representing the credentials of a wireless network (Wifi or Thread).
60///
61/// The trait has only two implementations: `Wifi` and `Thread`.
62pub trait WirelessNetwork: Send + for<'a> FromTLV<'a> + ToTLV {
63    /// Return the network ID
64    ///
65    /// For Wifi networks, this is the SSID
66    /// For Thread networks, this is the Extended PAN ID (`u64` as 8 bytes, network order)
67    fn id(&self) -> &[u8];
68
69    /// Return an in-place initializer for the type
70    ///
71    /// # Arguments
72    /// - `creds`: The credentials of the network with which to initialize the type
73    fn init_from<'a>(creds: &'a WirelessCreds<'a>) -> impl Init<Self, Error> + 'a;
74
75    /// Update the credentials of the network
76    ///
77    /// # Arguments
78    /// - `creds`: The new credentials to set
79    fn update(&mut self, creds: &WirelessCreds<'_>) -> Result<(), Error>;
80
81    /// Return the credentials of the network
82    fn creds(&self) -> WirelessCreds<'_>;
83
84    /// Return a displayable representation of the network
85    #[cfg(not(feature = "defmt"))]
86    fn display(&self) -> impl Display {
87        Self::display_id(self.id())
88    }
89
90    /// Return a displayable representation of the network
91    #[cfg(feature = "defmt")]
92    fn display(&self) -> impl Display + defmt::Format {
93        Self::display_id(self.id())
94    }
95
96    /// Return a displayable representation of the provided network ID
97    #[cfg(not(feature = "defmt"))]
98    fn display_id(id: &[u8]) -> impl Display;
99
100    /// Return a displayable representation of the provided network ID
101    #[cfg(feature = "defmt")]
102    fn display_id(id: &[u8]) -> impl Display + defmt::Format;
103}
104
105/// A fixed-size storage for wireless networks credentials.
106#[derive(Clone, Debug)]
107#[cfg_attr(feature = "defmt", derive(defmt::Format))]
108pub struct WirelessNetworks<const N: usize, T> {
109    networks: crate::utils::storage::Vec<T, N>,
110    commissioned: bool,
111}
112
113impl<const N: usize, T> Default for WirelessNetworks<N, T>
114where
115    T: WirelessNetwork,
116{
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl<const N: usize, T> WirelessNetworks<N, T>
123where
124    T: WirelessNetwork,
125{
126    pub const fn new() -> Self {
127        Self {
128            networks: crate::utils::storage::Vec::new(),
129            commissioned: false,
130        }
131    }
132
133    pub fn init() -> impl Init<Self> {
134        init!(Self {
135            networks <- crate::utils::storage::Vec::init(),
136            commissioned: false,
137        })
138    }
139
140    /// Reset the state
141    pub fn reset(&mut self) {
142        self.networks.clear();
143        self.commissioned = false;
144    }
145
146    /// Remove all networks from the provided BLOB store and from memory
147    ///
148    /// # Arguments
149    /// - `store`: the BLOB store to remove the networks from
150    /// - `buf`: a temporary buffer to use for removing the networks
151    pub async fn reset_persist<S: KvBlobStore>(
152        &mut self,
153        mut kv: S,
154        buf: &mut [u8],
155    ) -> Result<(), Error> {
156        self.reset();
157
158        kv.remove(NETWORKS_KEY, buf)?;
159
160        info!("Removed all wireless networks from storage");
161
162        Ok(())
163    }
164
165    /// Load all networks from the provided BLOB store
166    ///
167    /// # Arguments
168    /// - `store`: the BLOB store to load the networks from
169    /// - `buf`: a temporary buffer to use for loading the networks
170    pub async fn load_persist<S: KvBlobStore>(
171        &mut self,
172        mut kv: S,
173        buf: &mut [u8],
174    ) -> Result<(), Error> {
175        self.reset();
176
177        if let Some(data) = kv.load(NETWORKS_KEY, buf)? {
178            self.load(data)?;
179
180            info!(
181                "Loaded {} wireless networks from storage",
182                self.networks.len()
183            );
184        }
185
186        Ok(())
187    }
188
189    /// Load the state from a byte slice.
190    ///
191    /// # Arguments
192    /// - `data`: The byte slice to load the state from
193    pub fn load(&mut self, data: &[u8]) -> Result<(), Error> {
194        let root = TLVElement::new(data);
195
196        self.networks.clear();
197
198        // Try new format: struct { ctx(0): networks array, ctx(1): commissioned bool }
199        // Fall back to old format: bare TLV array (with commissioned defaulting to false)
200        if let Ok(structure) = root.structure() {
201            for network in structure.ctx(0)?.array()?.iter() {
202                let network = network?;
203
204                self.networks.push_init(T::init_from_tlv(network), || {
205                    ErrorCode::ResourceExhausted.into()
206                })?;
207            }
208
209            self.commissioned = structure.ctx(1)?.bool()?;
210        } else {
211            for network in root.array()?.iter() {
212                let network = network?;
213
214                self.networks.push_init(T::init_from_tlv(network), || {
215                    ErrorCode::ResourceExhausted.into()
216                })?;
217            }
218
219            self.commissioned = false;
220        }
221
222        Ok(())
223    }
224
225    /// Store the state into a byte slice.
226    ///
227    /// # Arguments
228    /// - `buf`: The byte slice to store the state into
229    ///
230    /// Returns the number of bytes written into the buffer.
231    pub fn store(&self, buf: &mut [u8]) -> Result<usize, Error> {
232        let mut wb = WriteBuf::new(buf);
233
234        wb.start_struct(&TLVTag::Anonymous)?;
235
236        self.networks.to_tlv(&TLVTag::Context(0), &mut wb)?;
237        self.commissioned.to_tlv(&TLVTag::Context(1), &mut wb)?;
238
239        wb.end_container()?;
240
241        let tail = wb.get_tail();
242
243        Ok(tail)
244    }
245
246    /// Iterate over the registered network credentials
247    ///
248    /// # Arguments
249    /// - `f`: A closure that will be called for each network registered in the storage
250    pub fn networks<F>(&self, mut f: F) -> Result<(), Error>
251    where
252        F: FnMut(&T) -> Result<(), Error>,
253    {
254        for network in self.networks.iter() {
255            f(network)?;
256        }
257
258        Ok(())
259    }
260
261    /// Get the credentials of a network by its ID
262    ///
263    /// # Arguments
264    /// - `network_id`: The ID of the network to get
265    /// - `f`: A closure that will be called with the credentials of the network, if the network exists
266    ///
267    /// Returns the index of the network in the storage if the network exists, `NetworkError::NetworkIdNotFound` otherwise
268    pub fn network<F>(&self, network_id: &[u8], f: F) -> Result<u8, NetworksError>
269    where
270        F: FnOnce(&T) -> Result<(), Error>,
271    {
272        let networks = self
273            .networks
274            .iter()
275            .enumerate()
276            .find(|(_, network)| network.id() == network_id);
277
278        if let Some((index, network)) = networks {
279            f(network)?;
280
281            Ok(index as _)
282        } else {
283            Err(NetworksError::NetworkIdNotFound)
284        }
285    }
286
287    /// Get the next network credentials after the one with the given ID
288    ///
289    /// # Arguments
290    /// - `after_network_id`: The ID of the network to get the next one after.
291    ///   If no network with the provided network ID exists, the first network in the storage will be returned.
292    pub fn next_network<F>(&self, last_network_id: Option<&[u8]>, f: F) -> Result<bool, Error>
293    where
294        F: FnOnce(&T) -> Result<(), Error>,
295    {
296        if let Some(last_network_id) = last_network_id {
297            info!(
298                "Looking for network after the one with ID: {}",
299                T::display_id(last_network_id)
300            );
301
302            // Return the network positioned after the last one used
303
304            let mut networks = self.networks.iter();
305
306            for network in &mut networks {
307                if network.id() == last_network_id {
308                    break;
309                }
310            }
311
312            let network = networks.next();
313            if let Some(network) = network {
314                info!("Trying with next network - ID: {}", network.display());
315
316                f(network)?;
317                return Ok(true);
318            }
319        }
320
321        // Wrap over
322        info!("Wrapping over");
323
324        if let Some(network) = self.networks.first() {
325            info!("Trying with first network - ID: {}", network.display());
326
327            f(network)?;
328            Ok(true)
329        } else {
330            info!("No networks available");
331            Ok(false)
332        }
333    }
334
335    /// Add or update a network in the storage
336    ///
337    /// # Arguments
338    /// - `network_id`: The ID of the network to add or update
339    /// - `add`: An in-place initializer for the network to add. The initializer will be used only if a network with the provided
340    ///   network ID does not exist in the storage
341    /// - `update`: A closure that will be called with the network to update. The closure will be called only if a network with the provided
342    ///   network ID exists in the storage
343    pub fn add_or_update<A, U>(
344        &mut self,
345        network_id: &[u8],
346        add: A,
347        update: U,
348    ) -> Result<u8, NetworksError>
349    where
350        A: Init<T, Error>,
351        U: FnOnce(&mut T) -> Result<(), Error>,
352    {
353        let unetwork = self
354            .networks
355            .iter_mut()
356            .enumerate()
357            .find(|(_, unetwork)| unetwork.id() == network_id);
358
359        if let Some((index, unetwork)) = unetwork {
360            // Update
361            update(unetwork)?;
362
363            info!("Updated network with ID {}", unetwork.display());
364
365            Ok(index as _)
366        } else if self.networks.len() >= N {
367            warn!(
368                "Adding network with ID {} failed: too many",
369                T::display_id(network_id)
370            );
371
372            Err(NetworksError::BoundsExceeded)
373        } else {
374            // Add
375            self.networks
376                .push_init(add, || ErrorCode::ResourceExhausted.into())?;
377
378            info!("Added network with ID {}", T::display_id(network_id));
379
380            Ok((self.networks.len() - 1) as _)
381        }
382    }
383
384    /// Reorder a network in the storage
385    ///
386    /// # Arguments
387    /// - `index`: The new index of the network
388    /// - `network_id`: The ID of the network to reorder
389    ///
390    /// Returns the new index of the network in the storage, if a network with the provided ID exists
391    /// or `NetworkError::NetworkIdNotFound` otherwise
392    pub fn reorder(&mut self, index: u8, network_id: &[u8]) -> Result<u8, NetworksError> {
393        let cur_index = self
394            .networks
395            .iter()
396            .position(|conf| conf.id() == network_id);
397
398        if let Some(cur_index) = cur_index {
399            // Found
400
401            if index < self.networks.len() as u8 {
402                let conf = self.networks.remove(cur_index);
403                unwrap!(self.networks.insert(index as usize, conf).map_err(|_| ()));
404
405                info!(
406                    "Network with ID {} reordered to index {}",
407                    T::display_id(network_id),
408                    index
409                );
410            } else {
411                warn!(
412                    "Reordering network with ID {} to index {} failed: out of range",
413                    T::display_id(network_id),
414                    index
415                );
416
417                Err(NetworksError::OutOfRange)?;
418            }
419        } else {
420            warn!("Network with ID {} not found", T::display_id(network_id));
421            Err(NetworksError::NetworkIdNotFound)?;
422        }
423
424        Ok(index)
425    }
426
427    /// Remove a network from the storage
428    ///
429    /// # Arguments
430    /// - `network_id`: The ID of the network to remove
431    ///
432    /// Returns the index of the network in the storage if the network exists and was removed, `NetworkError::NetworkIdNotFound` otherwise
433    pub fn remove(&mut self, network_id: &[u8]) -> Result<u8, NetworksError> {
434        let index = self
435            .networks
436            .iter()
437            .position(|conf| conf.id() == network_id);
438
439        if let Some(index) = index {
440            // Found
441            self.networks.remove(index);
442
443            info!("Removed network with ID {}", T::display_id(network_id));
444
445            Ok(index as _)
446        } else {
447            warn!("Network with ID {} not found", T::display_id(network_id));
448
449            Err(NetworksError::NetworkIdNotFound)
450        }
451    }
452
453    pub fn commissioned(&self) -> bool {
454        self.commissioned
455    }
456
457    pub fn set_commissioned(&mut self, commissioned: bool) {
458        self.commissioned = commissioned;
459    }
460}
461
462impl<const N: usize, T> Networks for WirelessNetworks<N, T>
463where
464    T: WirelessNetwork,
465{
466    fn max_networks(&self) -> Result<u8, Error> {
467        Ok(N as _)
468    }
469
470    fn networks(
471        &self,
472        f: &mut dyn FnMut(&net_comm::NetworkInfo) -> Result<(), Error>,
473    ) -> Result<(), Error> {
474        WirelessNetworks::networks(self, |network| {
475            let network_id = network.id();
476
477            let network_info = net_comm::NetworkInfo {
478                network_id,
479                connected: false, // TODO
480            };
481
482            f(&network_info)
483        })
484    }
485
486    fn creds(
487        &self,
488        network_id: &[u8],
489        f: &mut dyn FnMut(&net_comm::WirelessCreds) -> Result<(), Error>,
490    ) -> Result<u8, NetworksError> {
491        WirelessNetworks::network(self, network_id, |network| f(&network.creds()))
492    }
493
494    fn next_creds(
495        &self,
496        last_network_id: Option<&[u8]>,
497        f: &mut dyn FnMut(&WirelessCreds) -> Result<(), Error>,
498    ) -> Result<bool, Error> {
499        WirelessNetworks::next_network(self, last_network_id, |network| f(&network.creds()))
500    }
501
502    fn enabled(&self) -> Result<bool, Error> {
503        Ok(true)
504    }
505
506    fn set_enabled(&mut self, _enabled: bool) -> Result<(), Error> {
507        Ok(())
508    }
509
510    fn add_or_update(
511        &mut self,
512        creds: &net_comm::WirelessCreds<'_>,
513    ) -> Result<u8, net_comm::NetworksError> {
514        WirelessNetworks::add_or_update(self, creds.id()?, T::init_from(creds), |network| {
515            network.update(creds)
516        })
517    }
518
519    fn reorder(&mut self, index: u8, network_id: &[u8]) -> Result<u8, NetworksError> {
520        WirelessNetworks::reorder(self, index, network_id)
521    }
522
523    fn remove(&mut self, network_id: &[u8]) -> Result<u8, NetworksError> {
524        WirelessNetworks::remove(self, network_id)
525    }
526
527    fn commissioned(&self) -> Result<bool, Error> {
528        Ok(self.commissioned())
529    }
530
531    fn set_commissioned(&mut self, commissioned: bool) -> Result<(), Error> {
532        WirelessNetworks::set_commissioned(self, commissioned);
533
534        Ok(())
535    }
536
537    fn reset(&mut self) -> Result<(), Error> {
538        WirelessNetworks::reset(self);
539
540        Ok(())
541    }
542
543    fn load(&mut self, data: &[u8]) -> Result<(), Error> {
544        WirelessNetworks::load(self, data)
545    }
546
547    fn save(&self, buf: &mut [u8]) -> Result<Option<usize>, Error> {
548        WirelessNetworks::store(self, buf).map(Some)
549    }
550}
551
552/// An enum capable of displaying a network ID in a human-readable format.
553#[derive(Debug)]
554enum DisplayId<'a> {
555    Wifi(&'a [u8]),
556    Thread(&'a [u8]),
557}
558
559impl Display for DisplayId<'_> {
560    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
561        match self {
562            DisplayId::Wifi(id) => {
563                if let Ok(str) = core::str::from_utf8(id) {
564                    write!(f, "Wifi SSID({})", str)
565                } else {
566                    write!(f, "Wifi SSID({:?})", Bytes(id))
567                }
568            }
569            DisplayId::Thread(id) => write!(f, "Thread ExtPanID({:?})", Bytes(id)),
570        }
571    }
572}
573
574#[cfg(feature = "defmt")]
575impl defmt::Format for DisplayId<'_> {
576    fn format(&self, fmt: defmt::Formatter) {
577        match self {
578            DisplayId::Wifi(id) => {
579                if let Ok(str) = core::str::from_utf8(id) {
580                    defmt::write!(fmt, "Wifi SSID({})", str)
581                } else {
582                    defmt::write!(fmt, "Wifi SSID({:?})", Bytes(id))
583                }
584            }
585            DisplayId::Thread(id) => defmt::write!(fmt, "Thread ExtPanID({:?})", Bytes(id)),
586        }
587    }
588}
589
590/// A no-op implementation of the `net_comm::NetCtl` trait suitable when non-concurrent provisioning over BTP is used.
591///
592/// This implementation will throw `NetworkError::Other(ErrorCode::InvalidAction)` for the `scan` method
593/// and will silently return `Ok(())` for the `connect` method, which is meeting the non-concurrent provisioning expectations.
594pub struct NoopWirelessNetCtl(NetworkType);
595
596impl NoopWirelessNetCtl {
597    /// Create a new instance of `NoopWirelessNetCtl` for the provided network type.
598    ///
599    /// Note that it does not make any sense to use `NetworkType::Ethernet` here, as the Ethernet
600    /// network controller should return errors for both `scan` and `connect` methods.
601    ///
602    /// For Ethernet networks, use `EthNetctl` instead.
603    pub const fn new(net_type: NetworkType) -> Self {
604        Self(net_type)
605    }
606}
607
608impl net_comm::NetCtl for NoopWirelessNetCtl {
609    fn net_type(&self) -> NetworkType {
610        self.0
611    }
612
613    async fn scan<F>(&self, _network: Option<&[u8]>, _f: F) -> Result<(), NetCtlError>
614    where
615        F: FnOnce(&net_comm::NetworkScanInfo) -> Result<(), Error>,
616    {
617        Err(NetCtlError::Other(ErrorCode::InvalidAction.into()))
618    }
619
620    async fn connect(&self, creds: &WirelessCreds<'_>) -> Result<(), NetCtlError> {
621        Ok(creds.check_match(self.0)?)
622    }
623}
624
625impl NetChangeNotif for NoopWirelessNetCtl {
626    async fn wait_changed(&self) {
627        core::future::pending().await
628    }
629}
630
631impl DynBase for NoopWirelessNetCtl {}
632
633impl wifi_diag::WirelessDiag for NoopWirelessNetCtl {}
634
635impl wifi_diag::WifiDiag for NoopWirelessNetCtl {}
636
637impl thread_diag::ThreadDiag for NoopWirelessNetCtl {}
638
639/// A type holding the status of the last `connect` or `scan` operation for the `NetCtlWithStatus` `NetCtl` + `NetCtlStatus` implementation.
640pub struct NetCtlState {
641    /// The network ID used in the last scan or connect operation
642    pub network_id: OwnedWirelessNetworkId,
643    /// The status of the last scan or connect operation
644    pub networking_status: Option<NetworkCommissioningStatusEnum>,
645    /// The error code of the last scan or connect operation.
646    /// If the last operation was scan, this value is `None`.
647    pub connect_error_value: Option<i32>,
648}
649
650impl NetCtlState {
651    /// Create a new, empty instance of `NetCtlState`.
652    pub const fn new() -> Self {
653        Self {
654            network_id: OwnedWirelessNetworkId::new(),
655            networking_status: None,
656            connect_error_value: None,
657        }
658    }
659
660    /// Return an in-place initializer for a new, empty `NetCtlState`.
661    pub fn init() -> impl Init<Self> {
662        init!(Self {
663            network_id <- OwnedWirelessNetworkId::init(),
664            networking_status: None,
665            connect_error_value: None,
666        })
667    }
668
669    /// Create a new, empty instance of `NetCtlState` wrapped in a mutex.
670    pub const fn new_with_mutex() -> NetCtlStateMutex {
671        blocking::Mutex::new(RefCell::new(Self::new()))
672    }
673
674    /// Return an in-place initializer for a new, empty `NetCtlState` wrapped in a mutex.
675    pub fn init_with_mutex() -> impl Init<NetCtlStateMutex> {
676        blocking::Mutex::init(RefCell::init(init!(Self {
677            network_id <- OwnedWirelessNetworkId::init(),
678            networking_status: None,
679            connect_error_value: None,
680        })))
681    }
682
683    /// Return `true` if the network ID is set and the last operation was successful.
684    pub fn is_prov_ready(&self) -> bool {
685        !self.network_id.is_empty()
686            && matches!(
687                self.networking_status,
688                Some(NetworkCommissioningStatusEnum::Success)
689            )
690            && self.connect_error_value.is_none()
691    }
692
693    /// Update the state with the provided network ID and result of the last operation.
694    ///
695    /// Return the result of the last operation.
696    pub fn update<R>(
697        &mut self,
698        network_id: Option<&[u8]>,
699        result: Result<R, NetCtlError>,
700    ) -> Result<R, NetCtlError> {
701        self.network_id.clear();
702
703        if let Some(network_id) = network_id {
704            unwrap!(self.network_id.extend_from_slice(network_id));
705        }
706
707        if let Some((status, err_code)) = NetworkCommissioningStatusEnum::map_ctl_status(&result) {
708            self.networking_status = Some(status);
709            self.connect_error_value = err_code;
710        } else {
711            self.networking_status = None;
712            self.connect_error_value = None;
713        }
714
715        result
716    }
717
718    /// Update the state with the provided network ID and result of the last operation.
719    ///
720    /// Return the result of the last operation.
721    pub fn update_with_mutex<R>(
722        state: &NetCtlStateMutex,
723        network_id: Option<&[u8]>,
724        result: Result<R, NetCtlError>,
725    ) -> Result<R, NetCtlError> {
726        state.lock(|state| state.borrow_mut().update(network_id, result))
727    }
728
729    /// A utility to wait for provisioning over BTP to be ready.
730    /// Provisioning over BTP is considered complete when there is no longer an active connection
731    /// and the network ID is set (i.e. method `NetCtl::connect` was called successfully).
732    ///
733    /// This method is only useful for non-concurrent commisioning using wireless networks and BLE,
734    /// and is likely to be used together with `NoopWirelessNetCtl`.
735    pub async fn wait_prov_ready(state: &NetCtlStateMutex, _btp: &Btp) {
736        while !state.lock(|state| state.borrow().is_prov_ready()) {
737            // Provisioning over BTP is considered complete when there is no longer an active connection
738            // and the network ID is set (i.e. method `NetCtl::connect` was called successfully)
739
740            embassy_time::Timer::after_secs(1).await;
741        }
742    }
743}
744
745impl Default for NetCtlState {
746    fn default() -> Self {
747        Self::new()
748    }
749}
750
751/// A type alias for a `NetCtlState` instance wrapped in a mutex.
752pub type NetCtlStateMutex = blocking::Mutex<RefCell<NetCtlState>>;
753
754/// A wrapper around a `NetCtl` network controller that additionally implements the `NetCtlStatus`trait.
755pub struct NetCtlWithStatusImpl<'a, T> {
756    state: &'a NetCtlStateMutex,
757    net_ctl: T,
758}
759
760impl<'a, T> NetCtlWithStatusImpl<'a, T> {
761    /// Create a new instance of `NetCtlWithStatusImpl`.
762    ///
763    /// # Arguments
764    /// - `state`: A reference to a `NetCtlState` instance wrapped in a mutex
765    /// - `net_ctl`: A network controller that implements the `NetCtl` trait
766    pub const fn new(state: &'a NetCtlStateMutex, net_ctl: T) -> Self {
767        Self { state, net_ctl }
768    }
769}
770
771impl<T> net_comm::NetCtl for NetCtlWithStatusImpl<'_, T>
772where
773    T: net_comm::NetCtl,
774{
775    fn net_type(&self) -> NetworkType {
776        self.net_ctl.net_type()
777    }
778
779    fn connect_max_time_seconds(&self) -> u8 {
780        self.net_ctl.connect_max_time_seconds()
781    }
782
783    fn scan_max_time_seconds(&self) -> u8 {
784        self.net_ctl.scan_max_time_seconds()
785    }
786
787    fn supported_wifi_bands<F>(&self, f: F) -> Result<(), Error>
788    where
789        F: FnMut(net_comm::WiFiBandEnum) -> Result<(), Error>,
790    {
791        self.net_ctl.supported_wifi_bands(f)
792    }
793
794    fn supported_thread_features(&self) -> ThreadCapabilitiesBitmap {
795        self.net_ctl.supported_thread_features()
796    }
797
798    fn thread_version(&self) -> u16 {
799        self.net_ctl.thread_version()
800    }
801
802    async fn scan<F>(&self, network: Option<&[u8]>, f: F) -> Result<(), NetCtlError>
803    where
804        F: FnMut(&net_comm::NetworkScanInfo) -> Result<(), Error>,
805    {
806        self.net_ctl.scan(network, f).await
807    }
808
809    async fn connect(&self, creds: &WirelessCreds<'_>) -> Result<(), NetCtlError> {
810        NetCtlState::update_with_mutex(
811            self.state,
812            Some(creds.id()?),
813            self.net_ctl.connect(creds).await,
814        )
815    }
816}
817
818impl<T> net_comm::NetCtlStatus for NetCtlWithStatusImpl<'_, T>
819where
820    T: net_comm::NetCtl,
821{
822    fn last_networking_status(&self) -> Result<Option<NetworkCommissioningStatusEnum>, Error> {
823        Ok(self.state.lock(|state| state.borrow().networking_status))
824    }
825
826    fn last_network_id<F, R>(&self, f: F) -> Result<R, Error>
827    where
828        F: FnOnce(Option<&[u8]>) -> Result<R, Error>,
829    {
830        self.state.lock(|state| {
831            let state = state.borrow();
832
833            if state.network_id.is_empty() {
834                f(None)
835            } else {
836                f(Some(&state.network_id))
837            }
838        })
839    }
840
841    fn last_connect_error_value(&self) -> Result<Option<i32>, Error> {
842        Ok(self.state.lock(|state| state.borrow().connect_error_value))
843    }
844}
845
846impl<T> NetChangeNotif for NetCtlWithStatusImpl<'_, T>
847where
848    T: NetChangeNotif,
849{
850    async fn wait_changed(&self) {
851        self.net_ctl.wait_changed().await
852    }
853}
854
855#[cfg(feature = "sync-mutex")]
856impl<T> DynBase for NetCtlWithStatusImpl<'_, T> where T: Send + Sync {}
857
858#[cfg(not(feature = "sync-mutex"))]
859impl<T> DynBase for NetCtlWithStatusImpl<'_, T> {}
860
861impl<T> wifi_diag::WirelessDiag for NetCtlWithStatusImpl<'_, T>
862where
863    T: wifi_diag::WirelessDiag,
864{
865    fn connected(&self) -> Result<bool, Error> {
866        self.net_ctl.connected()
867    }
868}
869
870impl<T> wifi_diag::WifiDiag for NetCtlWithStatusImpl<'_, T>
871where
872    T: wifi_diag::WifiDiag,
873{
874    fn bssid(&self, f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>) -> Result<(), Error> {
875        self.net_ctl.bssid(f)
876    }
877
878    fn security_type(&self) -> Result<crate::tlv::Nullable<wifi_diag::SecurityTypeEnum>, Error> {
879        self.net_ctl.security_type()
880    }
881
882    fn wi_fi_version(&self) -> Result<crate::tlv::Nullable<wifi_diag::WiFiVersionEnum>, Error> {
883        self.net_ctl.wi_fi_version()
884    }
885
886    fn channel_number(&self) -> Result<crate::tlv::Nullable<u16>, Error> {
887        self.net_ctl.channel_number()
888    }
889
890    fn rssi(&self) -> Result<crate::tlv::Nullable<i8>, Error> {
891        self.net_ctl.rssi()
892    }
893}
894
895impl<T> thread_diag::ThreadDiag for NetCtlWithStatusImpl<'_, T>
896where
897    T: thread_diag::ThreadDiag,
898{
899    fn channel(&self) -> Result<Option<u16>, Error> {
900        self.net_ctl.channel()
901    }
902
903    fn routing_role(&self) -> Result<Option<thread_diag::RoutingRoleEnum>, Error> {
904        self.net_ctl.routing_role()
905    }
906
907    fn network_name(
908        &self,
909        f: &mut dyn FnMut(Option<&str>) -> Result<(), Error>,
910    ) -> Result<(), Error> {
911        self.net_ctl.network_name(f)
912    }
913
914    fn pan_id(&self) -> Result<Option<u16>, Error> {
915        self.net_ctl.pan_id()
916    }
917
918    fn extended_pan_id(&self) -> Result<Option<u64>, Error> {
919        self.net_ctl.extended_pan_id()
920    }
921
922    fn mesh_local_prefix(
923        &self,
924        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
925    ) -> Result<(), Error> {
926        self.net_ctl.mesh_local_prefix(f)
927    }
928
929    fn neighbor_table(
930        &self,
931        f: &mut dyn FnMut(&thread_diag::NeighborTable) -> Result<(), Error>,
932    ) -> Result<(), Error> {
933        self.net_ctl.neighbor_table(f)
934    }
935
936    fn route_table(
937        &self,
938        f: &mut dyn FnMut(&thread_diag::RouteTable) -> Result<(), Error>,
939    ) -> Result<(), Error> {
940        self.net_ctl.route_table(f)
941    }
942
943    fn partition_id(&self) -> Result<Option<u32>, Error> {
944        self.net_ctl.partition_id()
945    }
946
947    fn weighting(&self) -> Result<Option<u16>, Error> {
948        self.net_ctl.weighting()
949    }
950
951    fn data_version(&self) -> Result<Option<u16>, Error> {
952        self.net_ctl.data_version()
953    }
954
955    fn stable_data_version(&self) -> Result<Option<u16>, Error> {
956        self.net_ctl.stable_data_version()
957    }
958
959    fn leader_router_id(&self) -> Result<Option<u8>, Error> {
960        self.net_ctl.leader_router_id()
961    }
962
963    fn security_policy(&self) -> Result<Option<thread_diag::SecurityPolicy>, Error> {
964        self.net_ctl.security_policy()
965    }
966
967    fn channel_page0_mask(
968        &self,
969        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
970    ) -> Result<(), Error> {
971        self.net_ctl.channel_page0_mask(f)
972    }
973
974    fn operational_dataset_components(
975        &self,
976        f: &mut dyn FnMut(Option<&thread_diag::OperationalDatasetComponents>) -> Result<(), Error>,
977    ) -> Result<(), Error> {
978        self.net_ctl.operational_dataset_components(f)
979    }
980
981    fn active_network_faults_list(
982        &self,
983        f: &mut dyn FnMut(thread_diag::NetworkFaultEnum) -> Result<(), Error>,
984    ) -> Result<(), Error> {
985        self.net_ctl.active_network_faults_list(f)
986    }
987}
988
989#[cfg(test)]
990mod tests {
991    use crate::dm::clusters::net_comm::{
992        NetworksAccess, NetworksError, SharedNetworks, WirelessCreds,
993    };
994
995    use super::wifi::{Wifi, WifiNetworks};
996    use super::WirelessNetwork;
997
998    // ── Helper ──
999
1000    fn wifi_creds<'a>(ssid: &'a [u8], pass: &'a [u8]) -> WirelessCreds<'a> {
1001        WirelessCreds::Wifi { ssid, pass }
1002    }
1003
1004    fn collect_ssids(nets: &WifiNetworks<4>) -> Vec<Vec<u8>> {
1005        let mut ids = Vec::new();
1006        nets.networks(|n| {
1007            ids.push(n.id().to_vec());
1008            Ok(())
1009        })
1010        .unwrap();
1011        ids
1012    }
1013
1014    // ── WirelessNetworks: add / update / remove ──
1015
1016    #[test]
1017    fn add_networks() {
1018        let mut nets = WifiNetworks::<4>::new();
1019
1020        let idx = nets
1021            .add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"PassA")), |_| {
1022                Ok(())
1023            })
1024            .unwrap();
1025        assert_eq!(idx, 0);
1026
1027        let idx = nets
1028            .add_or_update(b"B", Wifi::init_from(&wifi_creds(b"B", b"PassB")), |_| {
1029                Ok(())
1030            })
1031            .unwrap();
1032        assert_eq!(idx, 1);
1033
1034        assert_eq!(collect_ssids(&nets), vec![b"A".to_vec(), b"B".to_vec()]);
1035    }
1036
1037    #[test]
1038    fn update_existing_network() {
1039        let mut nets = WifiNetworks::<4>::new();
1040        nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"Old")), |_| Ok(()))
1041            .unwrap();
1042
1043        // Update: same SSID, new password
1044        nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"New")), |wifi| {
1045            wifi.update(&wifi_creds(b"A", b"New"))
1046        })
1047        .unwrap();
1048
1049        // Still one network
1050        assert_eq!(collect_ssids(&nets).len(), 1);
1051
1052        // Verify updated password via creds
1053        let mut pass = Vec::new();
1054        nets.network(b"A", |w| {
1055            if let WirelessCreds::Wifi { pass: p, .. } = w.creds() {
1056                pass.extend_from_slice(p);
1057            }
1058            Ok(())
1059        })
1060        .unwrap();
1061        assert_eq!(pass, b"New");
1062    }
1063
1064    #[test]
1065    fn add_exceeds_capacity() {
1066        let mut nets = WifiNetworks::<2>::new();
1067        nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
1068            .unwrap();
1069        nets.add_or_update(b"B", Wifi::init_from(&wifi_creds(b"B", b"p")), |_| Ok(()))
1070            .unwrap();
1071
1072        let err = nets.add_or_update(b"C", Wifi::init_from(&wifi_creds(b"C", b"p")), |_| Ok(()));
1073        assert!(matches!(err, Err(NetworksError::BoundsExceeded)));
1074    }
1075
1076    #[test]
1077    fn remove_network() {
1078        let mut nets = WifiNetworks::<4>::new();
1079        nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
1080            .unwrap();
1081        nets.add_or_update(b"B", Wifi::init_from(&wifi_creds(b"B", b"p")), |_| Ok(()))
1082            .unwrap();
1083
1084        let idx = nets.remove(b"A").unwrap();
1085        assert_eq!(idx, 0);
1086        assert_eq!(collect_ssids(&nets), vec![b"B".to_vec()]);
1087    }
1088
1089    #[test]
1090    fn remove_nonexistent() {
1091        let mut nets = WifiNetworks::<4>::new();
1092        assert!(matches!(
1093            nets.remove(b"X"),
1094            Err(NetworksError::NetworkIdNotFound)
1095        ));
1096    }
1097
1098    // ── WirelessNetworks: reorder ──
1099
1100    #[test]
1101    fn reorder_moves_to_front() {
1102        let mut nets = WifiNetworks::<4>::new();
1103        for id in [b"A", b"B", b"C"] {
1104            nets.add_or_update(
1105                id.as_slice(),
1106                Wifi::init_from(&wifi_creds(id, b"p")),
1107                |_| Ok(()),
1108            )
1109            .unwrap();
1110        }
1111
1112        // Move C (index 2) to index 0
1113        nets.reorder(0, b"C").unwrap();
1114        assert_eq!(
1115            collect_ssids(&nets),
1116            vec![b"C".to_vec(), b"A".to_vec(), b"B".to_vec()]
1117        );
1118    }
1119
1120    #[test]
1121    fn reorder_out_of_range() {
1122        let mut nets = WifiNetworks::<4>::new();
1123        nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
1124            .unwrap();
1125
1126        assert!(matches!(
1127            nets.reorder(5, b"A"),
1128            Err(NetworksError::OutOfRange)
1129        ));
1130    }
1131
1132    #[test]
1133    fn reorder_nonexistent() {
1134        let mut nets = WifiNetworks::<4>::new();
1135        assert!(matches!(
1136            nets.reorder(0, b"X"),
1137            Err(NetworksError::NetworkIdNotFound)
1138        ));
1139    }
1140
1141    // ── WirelessNetworks: next_network round-robin ──
1142
1143    #[test]
1144    fn next_network_iterates_and_wraps() {
1145        let mut nets = WifiNetworks::<4>::new();
1146        for id in [b"A", b"B", b"C"] {
1147            nets.add_or_update(
1148                id.as_slice(),
1149                Wifi::init_from(&wifi_creds(id, b"p")),
1150                |_| Ok(()),
1151            )
1152            .unwrap();
1153        }
1154
1155        let get_next = |last: Option<&[u8]>| -> Option<Vec<u8>> {
1156            let mut result = None;
1157            let found = nets
1158                .next_network(last, |w| {
1159                    result = Some(w.id().to_vec());
1160                    Ok(())
1161                })
1162                .unwrap();
1163            if found {
1164                result
1165            } else {
1166                None
1167            }
1168        };
1169
1170        assert_eq!(get_next(None), Some(b"A".to_vec()));
1171        assert_eq!(get_next(Some(b"A")), Some(b"B".to_vec()));
1172        assert_eq!(get_next(Some(b"B")), Some(b"C".to_vec()));
1173        // After last → wraps to first
1174        assert_eq!(get_next(Some(b"C")), Some(b"A".to_vec()));
1175        // Unknown ID → first
1176        assert_eq!(get_next(Some(b"Z")), Some(b"A".to_vec()));
1177    }
1178
1179    #[test]
1180    fn next_network_empty_returns_false() {
1181        let nets = WifiNetworks::<4>::new();
1182        let found = nets.next_network(None, |_| Ok(())).unwrap();
1183        assert!(!found);
1184    }
1185
1186    // ── WirelessNetworks: store / load round-trip ──
1187
1188    #[test]
1189    fn store_load_round_trip() {
1190        let mut nets = WifiNetworks::<4>::new();
1191        nets.add_or_update(
1192            b"Net1",
1193            Wifi::init_from(&wifi_creds(b"Net1", b"P1")),
1194            |_| Ok(()),
1195        )
1196        .unwrap();
1197        nets.add_or_update(
1198            b"Net2",
1199            Wifi::init_from(&wifi_creds(b"Net2", b"P2")),
1200            |_| Ok(()),
1201        )
1202        .unwrap();
1203        nets.set_commissioned(true);
1204
1205        let mut buf = [0u8; 512];
1206        let len = nets.store(&mut buf).unwrap();
1207
1208        let mut loaded = WifiNetworks::<4>::new();
1209        loaded.load(&buf[..len]).unwrap();
1210
1211        assert_eq!(collect_ssids(&loaded), collect_ssids(&nets));
1212        assert!(loaded.commissioned());
1213    }
1214
1215    // ── WirelessNetworks: commissioned state ──
1216
1217    #[test]
1218    fn commissioned_default_false() {
1219        let nets = WifiNetworks::<4>::new();
1220        assert!(!nets.commissioned());
1221    }
1222
1223    #[test]
1224    fn set_commissioned() {
1225        let mut nets = WifiNetworks::<4>::new();
1226        nets.set_commissioned(true);
1227        assert!(nets.commissioned());
1228        nets.set_commissioned(false);
1229        assert!(!nets.commissioned());
1230    }
1231
1232    // ── WirelessNetworks: reset ──
1233
1234    #[test]
1235    fn reset_clears_all() {
1236        let mut nets = WifiNetworks::<4>::new();
1237        nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
1238            .unwrap();
1239        nets.set_commissioned(true);
1240
1241        nets.reset();
1242        assert!(collect_ssids(&nets).is_empty());
1243        assert!(!nets.commissioned());
1244    }
1245
1246    // ── SharedNetworks: delegates to inner WifiNetworks correctly ──
1247
1248    #[test]
1249    fn shared_networks_access_add_and_read() {
1250        let shared = SharedNetworks::new(WifiNetworks::<4>::new());
1251
1252        shared.access(|networks| {
1253            networks
1254                .add_or_update(&wifi_creds(b"SSID1", b"pass1"))
1255                .unwrap();
1256            networks
1257                .add_or_update(&wifi_creds(b"SSID2", b"pass2"))
1258                .unwrap();
1259        });
1260
1261        // Read back via Networks trait
1262        let count = shared.access(|networks| {
1263            let mut count = 0u8;
1264            networks
1265                .networks(&mut |_info| {
1266                    count += 1;
1267                    Ok(())
1268                })
1269                .unwrap();
1270            count
1271        });
1272
1273        assert_eq!(count, 2);
1274    }
1275
1276    #[test]
1277    fn shared_networks_commissioned_via_access() {
1278        let shared = SharedNetworks::new(WifiNetworks::<4>::new());
1279
1280        let commissioned = shared.access(|networks| networks.commissioned().unwrap());
1281        assert!(!commissioned);
1282
1283        shared.access(|networks| networks.set_commissioned(true).unwrap());
1284
1285        let commissioned = shared.access(|networks| networks.commissioned().unwrap());
1286        assert!(commissioned);
1287    }
1288
1289    #[test]
1290    fn shared_networks_next_creds_round_robin() {
1291        let shared = SharedNetworks::new(WifiNetworks::<4>::new());
1292
1293        shared.access(|networks| {
1294            for (ssid, pass) in [(b"A", b"pA"), (b"B", b"pB"), (b"C", b"pC")] {
1295                networks
1296                    .add_or_update(&wifi_creds(ssid.as_slice(), pass.as_slice()))
1297                    .unwrap();
1298            }
1299        });
1300
1301        let get_next_ssid = |last: Option<&[u8]>| -> Option<Vec<u8>> {
1302            shared.access(|networks| {
1303                let mut result = None;
1304                let found = networks
1305                    .next_creds(last, &mut |creds| {
1306                        if let WirelessCreds::Wifi { ssid, .. } = creds {
1307                            result = Some(ssid.to_vec());
1308                        }
1309                        Ok(())
1310                    })
1311                    .unwrap();
1312                if found {
1313                    result
1314                } else {
1315                    None
1316                }
1317            })
1318        };
1319
1320        assert_eq!(get_next_ssid(None), Some(b"A".to_vec()));
1321        assert_eq!(get_next_ssid(Some(b"A")), Some(b"B".to_vec()));
1322        assert_eq!(get_next_ssid(Some(b"C")), Some(b"A".to_vec()));
1323    }
1324
1325    // ── Regression: load old-format (bare TLV array, no commissioned field) ──
1326
1327    #[test]
1328    fn load_old_format_bare_array() {
1329        use crate::tlv::{TLVTag, TLVWrite};
1330        use crate::utils::storage::WriteBuf;
1331
1332        // Build old-format TLV data: bare anonymous array of Wifi structs.
1333        // Before the `commissioned` field was added, `store` just serialized the
1334        // networks Vec directly (without wrapping in a struct).
1335        let mut buf = [0u8; 512];
1336        let mut wb = WriteBuf::new(&mut buf);
1337
1338        wb.start_array(&TLVTag::Anonymous).unwrap();
1339
1340        // Wifi entry "A" with password "pA"
1341        wb.start_struct(&TLVTag::Anonymous).unwrap();
1342        wb.str(&TLVTag::Context(0), b"A").unwrap();
1343        wb.str(&TLVTag::Context(1), b"pA").unwrap();
1344        wb.end_container().unwrap();
1345
1346        // Wifi entry "B" with password "pB"
1347        wb.start_struct(&TLVTag::Anonymous).unwrap();
1348        wb.str(&TLVTag::Context(0), b"B").unwrap();
1349        wb.str(&TLVTag::Context(1), b"pB").unwrap();
1350        wb.end_container().unwrap();
1351
1352        wb.end_container().unwrap();
1353        let len = wb.get_tail();
1354
1355        // Load using new code (should handle old format gracefully)
1356        let mut nets = WifiNetworks::<4>::new();
1357        nets.load(&buf[..len]).unwrap();
1358
1359        assert_eq!(collect_ssids(&nets), vec![b"A".to_vec(), b"B".to_vec()]);
1360        assert!(
1361            !nets.commissioned(),
1362            "Old format should default commissioned to false"
1363        );
1364    }
1365
1366    // ── Regression: save() must not trigger a change notification ──
1367
1368    #[test]
1369    fn shared_networks_save_does_not_trigger_change() {
1370        use core::pin::pin;
1371        use embassy_futures::select::{select, Either};
1372
1373        let shared = SharedNetworks::new(WifiNetworks::<4>::new());
1374
1375        // Add a network → triggers notification
1376        shared.access(|n| n.add_or_update(&wifi_creds(b"A", b"p")).unwrap());
1377
1378        // Consume the notification
1379        embassy_futures::block_on(shared.wait_state_changed());
1380
1381        // Save (should NOT trigger notification)
1382        shared.access(|n| {
1383            let mut buf = [0u8; 512];
1384            n.save(&mut buf).unwrap();
1385        });
1386
1387        // Check: wait_state_changed should NOT be ready.
1388        // `select` is biased towards the first future, so if the notification was
1389        // triggered, it would resolve first. `ready(())` resolves immediately so
1390        // if the notification was NOT triggered, the second branch wins.
1391        let notified = embassy_futures::block_on(async {
1392            match select(
1393                pin!(shared.wait_state_changed()),
1394                pin!(core::future::ready(())),
1395            )
1396            .await
1397            {
1398                Either::First(_) => true,
1399                Either::Second(_) => false,
1400            }
1401        });
1402
1403        assert!(!notified, "save() must not trigger change notification");
1404    }
1405}