Skip to main content

rs_matter_stack/
wireless.rs

1use core::marker::PhantomData;
2use core::pin::pin;
3
4use embassy_futures::select::{select, select3, select4};
5
6use rs_matter::crypto::Crypto;
7use rs_matter::dm::clusters::gen_diag::NetifDiag;
8use rs_matter::dm::clusters::net_comm::{self, NetCtlError, NetworkType, WirelessCreds};
9use rs_matter::dm::clusters::wifi_diag::WirelessDiag;
10use rs_matter::dm::clusters::{thread_diag, wifi_diag};
11use rs_matter::dm::networks::wireless::{
12    NetCtlState, NoopWirelessNetCtl, WirelessNetwork, WirelessNetworks,
13};
14use rs_matter::dm::networks::NetChangeNotif;
15use rs_matter::dm::DataModel;
16use rs_matter::error::Error;
17use rs_matter::pairing::DiscoveryCapabilities;
18use rs_matter::persist::KvBlobStore;
19use rs_matter::sc::pase::CommWindowState;
20use rs_matter::transport::network::btp::{AdvData, Btp};
21use rs_matter::transport::network::NoNetwork;
22use rs_matter::utils::cell::RefCell;
23use rs_matter::utils::init::{init, Init};
24use rs_matter::utils::select::Coalesce;
25use rs_matter::utils::sync::blocking;
26use rs_matter::utils::sync::DynBase;
27
28use crate::ble::GattPeripheral;
29use crate::mdns::Mdns;
30use crate::nal::NetStack;
31use crate::network::{Embedding, Network};
32use crate::private::Sealed;
33use crate::{pin_alloc, DummyAttrNotifier, MatterStack};
34
35pub use gatt::*;
36pub use thread::*;
37pub use wifi::*;
38
39mod gatt;
40mod thread;
41mod wifi;
42
43pub const MAX_WIRELESS_NETWORKS: usize = 2;
44
45/// A type alias for a Matter stack running over either Wifi or Thread (and BLE, during commissioning).
46pub type WirelessMatterStack<'a, const B: usize, T, E = ()> = MatterStack<'a, B, WirelessBle<T, E>>;
47
48/// An implementation of the `Network` trait for a Matter stack running over
49/// BLE during commissioning, and then over either WiFi or Thread when operating.
50///
51/// The supported commissioning is either concurrent or non-concurrent (as per the Matter Core spec),
52/// where one over the other is decided at runtime with the concrete wireless implementation
53/// (`WirelessCoex` or `Wireless` + `Gatt`).
54///
55/// Non-concurrent commissioning means that the device - at any point in time - either runs Bluetooth
56/// or Wifi/Thread, but not both.
57///
58/// This is done to save memory and to avoid the usage of BLE+Wifi/Thread co-exist drivers on
59/// devices which share a single wireless radio for both BLE and Wifi/Thread.
60pub struct WirelessBle<T, E = ()>
61where
62    T: WirelessNetwork,
63{
64    btp: Btp,
65    net_state: blocking::Mutex<RefCell<NetCtlState>>,
66    embedding: E,
67    // The wireless network type is no longer stored here (the networks store lives
68    // in the stack's `InteractionModelState`), but it still parameterizes the
69    // `Network::Networks` associated type, so keep it as a phantom marker.
70    _network: PhantomData<fn() -> T>,
71}
72
73impl<T, E> WirelessBle<T, E>
74where
75    T: WirelessNetwork,
76    E: Embedding,
77{
78    /// Creates a new instance of the `WirelessBle` network type.
79    pub const fn new() -> Self {
80        Self {
81            btp: Btp::new(),
82            net_state: NetCtlState::new_with_mutex(),
83            embedding: E::INIT,
84            _network: PhantomData,
85        }
86    }
87
88    /// Return an in-place initializer for the `WirelessBle` network type.
89    pub fn init() -> impl Init<Self> {
90        init!(Self {
91            btp <- Btp::init(),
92            net_state <- NetCtlState::init_with_mutex(),
93            embedding <- E::init(),
94            _network: PhantomData,
95        })
96    }
97}
98
99/// A composite wireless network controller that is the SAME concrete type during
100/// both the (BLE) commissioning phase and the operational (Thread/Wifi) phase.
101///
102/// This exists purely to avoid building two structurally-different Matter handler
103/// chains per device. `InteractionModel::handle` and every generated cluster handler
104/// adaptor are monomorphized over the whole handler-chain tuple type; if the
105/// net-ctl slot has a different type per phase, the entire dispatch tree is
106/// compiled twice (~tens of KiB of duplicated `.text` on embedded targets).
107///
108/// By using `WirelessNetCtl<Q>` in *both* phases — `Commissioning` before the
109/// operational controller exists, `Operational(&Q)` afterwards — the chain type
110/// is identical across phases and the dispatch tree monomorphizes once.
111///
112/// The `Commissioning` variant reproduces the behavior of the former
113/// `NoopWirelessNetCtl`: `scan` errors with `InvalidAction`, `connect` only
114/// checks the creds match the network type, and every diag returns its default.
115/// The `Operational` variant delegates to the real controller `Q`.
116///
117/// `Q` is named identically at every phase via the wireless driver's associated
118/// net-ctl type (see the `Thread`/`Gatt` driver traits), so even the
119/// commissioning phase — which has no controller value — can still name the type.
120pub enum WirelessNetCtl<'a, Q> {
121    /// Commissioning phase: no operational controller yet (BLE only).
122    Commissioning(NetworkType),
123    /// Operational phase: delegate to the real controller.
124    Operational(&'a Q),
125}
126
127impl<Q> net_comm::NetCtl for WirelessNetCtl<'_, Q>
128where
129    Q: net_comm::NetCtl,
130{
131    fn net_type(&self) -> NetworkType {
132        match self {
133            Self::Commissioning(net_type) => *net_type,
134            Self::Operational(q) => q.net_type(),
135        }
136    }
137
138    fn connect_max_time_seconds(&self) -> u8 {
139        match self {
140            Self::Commissioning(_) => 0,
141            Self::Operational(q) => q.connect_max_time_seconds(),
142        }
143    }
144
145    fn scan_max_time_seconds(&self) -> u8 {
146        match self {
147            Self::Commissioning(_) => 0,
148            Self::Operational(q) => q.scan_max_time_seconds(),
149        }
150    }
151
152    fn supported_wifi_bands<F>(&self, f: F) -> Result<(), Error>
153    where
154        F: FnMut(net_comm::WiFiBandEnum) -> Result<(), Error>,
155    {
156        match self {
157            Self::Commissioning(_) => Ok(()),
158            Self::Operational(q) => q.supported_wifi_bands(f),
159        }
160    }
161
162    fn supported_thread_features(&self) -> net_comm::ThreadCapabilitiesBitmap {
163        match self {
164            Self::Commissioning(_) => net_comm::ThreadCapabilitiesBitmap::empty(),
165            Self::Operational(q) => q.supported_thread_features(),
166        }
167    }
168
169    fn thread_version(&self) -> u16 {
170        match self {
171            Self::Commissioning(_) => 0,
172            Self::Operational(q) => q.thread_version(),
173        }
174    }
175
176    async fn scan<F>(&self, network: Option<&[u8]>, f: F) -> Result<(), NetCtlError>
177    where
178        F: FnMut(&net_comm::NetworkScanInfo) -> Result<(), Error>,
179    {
180        match self {
181            // Matches the former `NoopWirelessNetCtl::scan`.
182            Self::Commissioning(_) => Err(NetCtlError::Other(
183                rs_matter::error::ErrorCode::InvalidAction.into(),
184            )),
185            Self::Operational(q) => q.scan(network, f).await,
186        }
187    }
188
189    async fn connect(&self, creds: &WirelessCreds<'_>) -> Result<(), NetCtlError> {
190        match self {
191            // Matches the former `NoopWirelessNetCtl::connect`.
192            Self::Commissioning(net_type) => Ok(creds.check_match(*net_type)?),
193            Self::Operational(q) => q.connect(creds).await,
194        }
195    }
196}
197
198impl<Q> NetChangeNotif for WirelessNetCtl<'_, Q>
199where
200    Q: NetChangeNotif,
201{
202    async fn wait_changed(&self) {
203        match self {
204            Self::Commissioning(_) => core::future::pending().await,
205            Self::Operational(q) => q.wait_changed().await,
206        }
207    }
208}
209
210#[cfg(feature = "sync-mutex")]
211impl<Q> DynBase for WirelessNetCtl<'_, Q> where Q: Send + Sync {}
212
213#[cfg(not(feature = "sync-mutex"))]
214impl<Q> DynBase for WirelessNetCtl<'_, Q> {}
215
216impl<Q> WirelessDiag for WirelessNetCtl<'_, Q>
217where
218    Q: WirelessDiag,
219{
220    fn connected(&self) -> Result<bool, Error> {
221        match self {
222            Self::Commissioning(_) => Ok(false),
223            Self::Operational(q) => q.connected(),
224        }
225    }
226}
227
228// For `WifiDiag`/`ThreadDiag`, the `Commissioning` variant reproduces each
229// method's trait default (matching the former `NoopWirelessNetCtl`, which impl'd
230// both traits empty), and the `Operational` variant delegates to the real
231// controller. The defaults are: `Ok(None)` for the scalar accessors, `Ok(())` /
232// `f(None)` for the closure-based accessors, and `Nullable::none()` for the
233// `WifiDiag` nullable accessors — kept in sync with rs-matter's trait defaults.
234impl<Q> wifi_diag::WifiDiag for WirelessNetCtl<'_, Q>
235where
236    Q: wifi_diag::WifiDiag,
237{
238    fn bssid(&self, f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>) -> Result<(), Error> {
239        match self {
240            Self::Commissioning(_) => f(None),
241            Self::Operational(q) => q.bssid(f),
242        }
243    }
244
245    fn security_type(
246        &self,
247    ) -> Result<rs_matter::tlv::Nullable<wifi_diag::SecurityTypeEnum>, Error> {
248        match self {
249            Self::Commissioning(_) => Ok(rs_matter::tlv::Nullable::none()),
250            Self::Operational(q) => q.security_type(),
251        }
252    }
253
254    fn wi_fi_version(&self) -> Result<rs_matter::tlv::Nullable<wifi_diag::WiFiVersionEnum>, Error> {
255        match self {
256            Self::Commissioning(_) => Ok(rs_matter::tlv::Nullable::none()),
257            Self::Operational(q) => q.wi_fi_version(),
258        }
259    }
260
261    fn channel_number(&self) -> Result<rs_matter::tlv::Nullable<u16>, Error> {
262        match self {
263            Self::Commissioning(_) => Ok(rs_matter::tlv::Nullable::none()),
264            Self::Operational(q) => q.channel_number(),
265        }
266    }
267
268    fn rssi(&self) -> Result<rs_matter::tlv::Nullable<i8>, Error> {
269        match self {
270            Self::Commissioning(_) => Ok(rs_matter::tlv::Nullable::none()),
271            Self::Operational(q) => q.rssi(),
272        }
273    }
274}
275
276impl<Q> thread_diag::ThreadDiag for WirelessNetCtl<'_, Q>
277where
278    Q: thread_diag::ThreadDiag,
279{
280    fn channel(&self) -> Result<Option<u16>, Error> {
281        match self {
282            Self::Commissioning(_) => Ok(None),
283            Self::Operational(q) => q.channel(),
284        }
285    }
286    fn routing_role(&self) -> Result<Option<thread_diag::RoutingRoleEnum>, Error> {
287        match self {
288            Self::Commissioning(_) => Ok(None),
289            Self::Operational(q) => q.routing_role(),
290        }
291    }
292    fn network_name(
293        &self,
294        f: &mut dyn FnMut(Option<&str>) -> Result<(), Error>,
295    ) -> Result<(), Error> {
296        match self {
297            Self::Commissioning(_) => f(None),
298            Self::Operational(q) => q.network_name(f),
299        }
300    }
301    fn pan_id(&self) -> Result<Option<u16>, Error> {
302        match self {
303            Self::Commissioning(_) => Ok(None),
304            Self::Operational(q) => q.pan_id(),
305        }
306    }
307    fn extended_pan_id(&self) -> Result<Option<u64>, Error> {
308        match self {
309            Self::Commissioning(_) => Ok(None),
310            Self::Operational(q) => q.extended_pan_id(),
311        }
312    }
313    fn mesh_local_prefix(
314        &self,
315        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
316    ) -> Result<(), Error> {
317        match self {
318            Self::Commissioning(_) => f(None),
319            Self::Operational(q) => q.mesh_local_prefix(f),
320        }
321    }
322    fn neighbor_table(
323        &self,
324        f: &mut dyn FnMut(&thread_diag::NeighborTable) -> Result<(), Error>,
325    ) -> Result<(), Error> {
326        match self {
327            Self::Commissioning(_) => Ok(()),
328            Self::Operational(q) => q.neighbor_table(f),
329        }
330    }
331    fn route_table(
332        &self,
333        f: &mut dyn FnMut(&thread_diag::RouteTable) -> Result<(), Error>,
334    ) -> Result<(), Error> {
335        match self {
336            Self::Commissioning(_) => Ok(()),
337            Self::Operational(q) => q.route_table(f),
338        }
339    }
340    fn partition_id(&self) -> Result<Option<u32>, Error> {
341        match self {
342            Self::Commissioning(_) => Ok(None),
343            Self::Operational(q) => q.partition_id(),
344        }
345    }
346    fn weighting(&self) -> Result<Option<u16>, Error> {
347        match self {
348            Self::Commissioning(_) => Ok(None),
349            Self::Operational(q) => q.weighting(),
350        }
351    }
352    fn data_version(&self) -> Result<Option<u16>, Error> {
353        match self {
354            Self::Commissioning(_) => Ok(None),
355            Self::Operational(q) => q.data_version(),
356        }
357    }
358    fn stable_data_version(&self) -> Result<Option<u16>, Error> {
359        match self {
360            Self::Commissioning(_) => Ok(None),
361            Self::Operational(q) => q.stable_data_version(),
362        }
363    }
364    fn leader_router_id(&self) -> Result<Option<u8>, Error> {
365        match self {
366            Self::Commissioning(_) => Ok(None),
367            Self::Operational(q) => q.leader_router_id(),
368        }
369    }
370    fn ext_address(&self) -> Result<Option<u64>, Error> {
371        match self {
372            Self::Commissioning(_) => Ok(None),
373            Self::Operational(q) => q.ext_address(),
374        }
375    }
376    fn rloc_16(&self) -> Result<Option<u16>, Error> {
377        match self {
378            Self::Commissioning(_) => Ok(None),
379            Self::Operational(q) => q.rloc_16(),
380        }
381    }
382    fn security_policy(&self) -> Result<Option<thread_diag::SecurityPolicy>, Error> {
383        match self {
384            Self::Commissioning(_) => Ok(None),
385            Self::Operational(q) => q.security_policy(),
386        }
387    }
388    fn channel_page0_mask(
389        &self,
390        f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
391    ) -> Result<(), Error> {
392        match self {
393            Self::Commissioning(_) => f(None),
394            Self::Operational(q) => q.channel_page0_mask(f),
395        }
396    }
397    fn operational_dataset_components(
398        &self,
399        f: &mut dyn FnMut(Option<&thread_diag::OperationalDatasetComponents>) -> Result<(), Error>,
400    ) -> Result<(), Error> {
401        match self {
402            Self::Commissioning(_) => f(None),
403            Self::Operational(q) => q.operational_dataset_components(f),
404        }
405    }
406    fn active_network_faults_list(
407        &self,
408        f: &mut dyn FnMut(thread_diag::NetworkFaultEnum) -> Result<(), Error>,
409    ) -> Result<(), Error> {
410        match self {
411            Self::Commissioning(_) => Ok(()),
412            Self::Operational(q) => q.active_network_faults_list(f),
413        }
414    }
415}
416
417impl<T, E> Default for WirelessBle<T, E>
418where
419    T: WirelessNetwork,
420    E: Embedding,
421{
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427impl<T, E> Sealed for WirelessBle<T, E>
428where
429    T: WirelessNetwork,
430    E: Embedding,
431{
432}
433
434impl<T, E> Network for WirelessBle<T, E>
435where
436    T: WirelessNetwork,
437    E: Embedding,
438{
439    const INIT: Self = Self::new();
440
441    type Embedding<'a>
442        = E
443    where
444        Self: 'a;
445
446    // The wireless networks store, owned by the stack's `InteractionModelState`.
447    type Networks = WirelessNetworks<MAX_WIRELESS_NETWORKS, T>;
448
449    const NETWORKS: Self::Networks = WirelessNetworks::new();
450
451    fn init() -> impl Init<Self> {
452        WirelessBle::init()
453    }
454
455    fn init_networks() -> impl Init<Self::Networks> {
456        WirelessNetworks::init()
457    }
458
459    fn discovery_capabilities(&self) -> DiscoveryCapabilities {
460        DiscoveryCapabilities::BLE
461    }
462
463    fn embedding(&self) -> &Self::Embedding<'_> {
464        &self.embedding
465    }
466}
467
468impl<const B: usize, T, E> MatterStack<'_, B, WirelessBle<T, E>>
469where
470    T: WirelessNetwork,
471    E: Embedding,
472{
473    /// Reset the Matter instance to the factory defaults by removing all fabrics and basic info settings
474    ///
475    /// `handler` is the same data model handler that is passed to `run`: the
476    /// Interaction Model broadcasts a `FactoryReset` lifecycle op to it, so
477    /// cluster handlers owning persisted state of their own can drop it too.
478    pub async fn reset<C, H, S>(&mut self, crypto: C, handler: H, store: S) -> Result<(), Error>
479    where
480        C: Crypto,
481        H: DataModel,
482        S: KvBlobStore,
483    {
484        let kv = self.matter.kv(store);
485
486        self.matter.factory_reset(&kv)?;
487
488        // Reset the events counter and the wireless networks store so we don't
489        // carry stale state across a factory reset (Matter Core spec R1.5.1,
490        // §7.14.1.1 for the events watermark; the networks store holds the
491        // commissioned Wifi/Thread credentials).
492        // The net-ctl is inert here - a factory reset never drives the
493        // wireless connection manager.
494        self.im(
495            crypto,
496            handler,
497            &kv,
498            NoopWirelessNetCtl::new(NetworkType::Ethernet),
499        )
500        .factory_reset()
501        .await
502    }
503
504    /// Run the startup sequence of the stack: re-hydrate the persisted state and
505    /// open the basic communication window if the device is not commissioned yet.
506    ///
507    /// This is the `Matter`-level half of the startup (fabrics, basic info, RTC,
508    /// sessions). The Interaction Model half - the events watermark, the networks
509    /// store and the persisted subscriptions - is re-hydrated by `run`, because
510    /// `InteractionModel::startup` has to run on the very Interaction Model
511    /// instance that is then run: a resumed subscription borrows that instance's
512    /// IM buffers, and constructing an `InteractionModel` clears the
513    /// subscriptions table.
514    pub async fn startup<C, S>(&mut self, crypto: C, store: S) -> Result<(), Error>
515    where
516        C: Crypto,
517        S: KvBlobStore,
518    {
519        let kv = self.matter.kv(store);
520
521        self.matter.startup(&kv)?;
522
523        if !self.matter().has_fabrics() {
524            info!("Device is not commissioned yet, opening commissioning window...");
525
526            self.open_basic_comm_window(crypto, &DummyAttrNotifier)?;
527        } else {
528            info!("Device is already commissioned");
529        }
530
531        Ok(())
532    }
533
534    /// Run the concurrent (BLE + Wireless) commissioning transport.
535    ///
536    /// The operational wireless connection manager is no longer run here; it is
537    /// driven by the data model engine (`InteractionModel::run`), which was built
538    /// with the operational `net_ctl` and the stack's networks store. This method
539    /// therefore only runs the BTP coexistence transport.
540    async fn run_net_coex<C, S, N, D, G>(
541        &self,
542        crypto: C,
543        net_stack: S,
544        netif: N,
545        mut mdns: D,
546        mut gatt: G,
547    ) -> Result<(), Error>
548    where
549        C: Crypto,
550        S: NetStack,
551        N: NetifDiag + NetChangeNotif,
552        D: Mdns,
553        G: GattPeripheral,
554    {
555        self.run_btp_coex(&crypto, &net_stack, &netif, &mut mdns, &mut gatt)
556            .await
557    }
558
559    async fn run_btp_coex<C, S, N, D, P>(
560        &self,
561        crypto: C,
562        net_stack: S,
563        netif: N,
564        mut mdns: D,
565        peripheral: P,
566    ) -> Result<(), Error>
567    where
568        C: Crypto,
569        S: NetStack,
570        N: NetifDiag + NetChangeNotif,
571        D: Mdns,
572        P: GattPeripheral,
573    {
574        info!("BLE driver started");
575
576        info!("Running in concurrent commissioning mode (BLE and Wireless)");
577
578        let adv_data = AdvData::new(
579            self.matter().dev_det(),
580            self.matter().dev_comm().discriminator,
581        );
582
583        let mut btp_task = pin_alloc!(
584            self.bump,
585            self.run_gatt_while_ble_commissionable(peripheral, &adv_data)
586        );
587
588        let mut net_task = pin_alloc!(
589            self.bump,
590            self.run_oper_net(
591                &crypto,
592                &net_stack,
593                0, // TODO
594                core::future::pending(),
595                Some((&self.network.btp, &self.network.btp))
596            )
597        );
598
599        let mut mdns_task = pin_alloc!(
600            self.bump,
601            self.run_oper_netif_mdns(&crypto, &net_stack, &netif, &mut mdns)
602        );
603
604        select3(&mut btp_task, &mut net_task, &mut mdns_task)
605            .coalesce()
606            .await
607    }
608
609    async fn run_btp<C, P>(&self, crypto: C, mut peripheral: P) -> Result<(), Error>
610    where
611        C: Crypto,
612        P: GattPeripheral,
613    {
614        info!("BLE driver started");
615
616        info!("Running in non-concurrent commissioning mode (BLE only)");
617
618        let adv_data = AdvData::new(
619            self.matter().dev_det(),
620            self.matter().dev_comm().discriminator,
621        );
622
623        let mut btp_task = pin_alloc!(
624            self.bump,
625            peripheral.run(&self.network.btp, "BT", &adv_data)
626        );
627
628        let mut net_task =
629            pin!(self.run_transport_net(&crypto, &self.network.btp, &self.network.btp, NoNetwork));
630
631        // The window this phase is advertising can also simply go away - it expires, or an
632        // administrator revokes it - without the commissioner ever getting as far as handing
633        // over the network credentials. Then there is nothing left to advertise, and BLE
634        // should stop rather than keep the peripheral up for a window that no longer exists.
635        let mut comm_window_task = pin!(async {
636            self.wait_comm_window(|state| !state.is_open_on_all_transports())
637                .await;
638
639            Ok(())
640        });
641
642        let mut oper_net_act_task = pin!(async {
643            NetCtlState::wait_prov_ready(&self.network.net_state, &self.network.btp).await;
644
645            // TODO: Workaround for a bug in the `esp-wifi` BLE stack:
646            // ====================== PANIC ======================
647            // panicked at /home/ivan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/esp-wifi-0.12.0/src/ble/npl.rs:914:9:
648            // timed eventq_get not yet supported - go implement it!
649            embassy_time::Timer::after(embassy_time::Duration::from_secs(2)).await;
650
651            Ok(())
652        });
653
654        select4(
655            &mut btp_task,
656            &mut net_task,
657            &mut oper_net_act_task,
658            &mut comm_window_task,
659        )
660        .coalesce()
661        .await
662    }
663
664    /// Run the GATT peripheral (and with it the BTP transport), but only for as long as the
665    /// device is actually commissionable over BLE. Never returns, unless the peripheral itself
666    /// fails.
667    async fn run_gatt_while_ble_commissionable<P>(
668        &self,
669        mut peripheral: P,
670        adv_data: &AdvData,
671    ) -> Result<(), Error>
672    where
673        P: GattPeripheral,
674    {
675        loop {
676            // BLE carries a window the device opened for itself - initial commissioning.
677            // A window re-opened by an administrator over CASE (`opener: Some`) is advertised
678            // on the operational IP network alone, so the peripheral stays down for it.
679            //
680            // The opener is part of the window state, so one read answers both "is a window
681            // open" and "is it ours". It also does not change while the window is open - in
682            // particular the fabric that appears at `AddNOC`, in the middle of the very
683            // commissioning this peripheral is carrying, leaves it `None`.
684            let state = self.wait_comm_window(CommWindowState::is_open).await;
685
686            if !state.is_open_on_all_transports() {
687                info!("Commissioning window opened over CASE; BLE peripheral not started");
688
689                self.wait_comm_window(|state| !state.is_open()).await;
690
691                continue;
692            }
693
694            info!("Commissioning window opened; BLE peripheral started");
695
696            {
697                let mut peripheral_task = pin!(peripheral.run(&self.network.btp, "BT", adv_data));
698                let mut closed_task = pin!(async {
699                    self.wait_comm_window(|state| !state.is_open()).await;
700                    Ok(())
701                });
702
703                select(&mut peripheral_task, &mut closed_task)
704                    .coalesce()
705                    .await?;
706            } // <- dropping the peripheral future here is what stops BLE
707
708            info!("Commissioning window closed; BLE peripheral stopped");
709        }
710    }
711
712    /// Resolve once a commissioning window that has to be advertised on every transport
713    /// *appears* - i.e. once the radio is needed for BLE again.
714    ///
715    /// A window that is already open on entry does not count. The non-concurrent BLE -> wireless
716    /// handover happens with the window still open - it is closed by `CommissioningComplete`,
717    /// which the commissioner sends over the operational network, after the handover - so
718    /// treating the current window as an event would bounce the radio straight back to BLE in
719    /// the middle of the commissioning it is there to finish. Hence: let the current window go
720    /// away first, and only then wait for the next one. When nothing is open on entry - every
721    /// case other than that handover - the first wait resolves on its first poll.
722    async fn wait_next_comm_window(&self) {
723        self.wait_comm_window(|state| !state.is_open_on_all_transports())
724            .await;
725        self.wait_comm_window(CommWindowState::is_open_on_all_transports)
726            .await;
727    }
728
729    /// Forget the outcome of the last `NetworkCommissioning` scan / connect.
730    ///
731    /// The BLE phase of non-concurrent commissioning ends when `NetCtlState` reports that the
732    /// commissioner has handed the network credentials over, and that verdict is sticky: nothing
733    /// clears it, it is only ever overwritten by the next scan / connect. Clearing it as the BLE
734    /// phase starts scopes it to the commissioning attempt that this window represents. Without
735    /// it, a device commissioned earlier in this same boot would leave a freshly entered BLE
736    /// phase again after one poll interval, on the strength of a verdict from the previous
737    /// commissioning.
738    fn reset_net_ctl_state(&self) {
739        self.network.net_state.lock(|state| {
740            let mut state = state.borrow_mut();
741
742            state.network_id.clear();
743            state.networking_status = None;
744            state.connect_error_value = None;
745        });
746    }
747
748    /// Resolve once `until` accepts the current commissioning window state, and return that
749    /// state - so a caller that needs more than the predicate (typically the window's opener)
750    /// does not have to read it a second time.
751    ///
752    /// Polled rather than driven by a notification: `rs-matter` signals a commissioning window
753    /// change only through the mDNS notification, and that is a single-slot `Notification`
754    /// already consumed by the mDNS task, so a second waiter would race it. Reacting a couple
755    /// of seconds late is of no consequence for what this drives.
756    async fn wait_comm_window<F>(&self, until: F) -> CommWindowState
757    where
758        F: Fn(&CommWindowState) -> bool,
759    {
760        const POLL_INTERVAL_SECS: u64 = 2;
761
762        loop {
763            let state = self.matter().comm_window_state();
764
765            if until(&state) {
766                break state;
767            }
768
769            embassy_time::Timer::after(embassy_time::Duration::from_secs(POLL_INTERVAL_SECS)).await;
770        }
771    }
772}
773
774/// A utility type for running a wireless task with a pre-existing wireless interface
775/// rather than bringing up / tearing down the wireless interface for the task.
776///
777/// This utility can only be used with hardware that implements wireless coexist mode
778/// (i.e. the Thread/Wifi interface as well as the BLE GATT peripheral are available at the same time).
779pub struct PreexistingWireless<S, N, C, M, G> {
780    pub(crate) net_stack: S,
781    pub(crate) netif: N,
782    pub(crate) net_ctl: C,
783    pub(crate) mdns: M,
784    pub(crate) gatt: G,
785}
786
787impl<S, N, C, M, G> PreexistingWireless<S, N, C, M, G> {
788    /// Create a new `PreexistingWireless` instance with the given network stack,
789    /// network interface, network controller and GATT peripheral.
790    pub const fn new(net_stack: S, netif: N, net_ctl: C, mdns: M, gatt: G) -> Self {
791        Self {
792            net_stack,
793            netif,
794            net_ctl,
795            mdns,
796            gatt,
797        }
798    }
799}
800
801pub(crate) struct MatterStackWirelessTask<'a, const B: usize, T, E, C, H, K, U, Q>
802where
803    T: WirelessNetwork,
804    E: Embedding,
805{
806    stack: &'a MatterStack<'a, B, WirelessBle<T, E>>,
807    crypto: C,
808    handler: H,
809    kv: K,
810    user_task: U,
811    _net_ctl: PhantomData<fn() -> Q>,
812}