From c817a8ab8774859bd186e05419caa676ed106267 Mon Sep 17 00:00:00 2001 From: ivmarkov Date: Sun, 2 Jun 2024 23:36:33 +0300 Subject: [PATCH] rs-matter-stack (#1) * WIP: rs-matter-stack * Fix CI * Create IPv6 address * Put extra warnings for stack blowups * Box EspWifi to avoid stack blowups * Multicast no longer needed * Bugfix * Fix async-io bug * Update to latest edge-net --- Cargo.toml | 23 ++- README.md | 79 +++----- clippy.toml | 3 + examples/light.rs | 46 +++-- examples/light_eth.rs | 84 ++++---- src/ble.rs | 36 ++-- src/error.rs | 63 ------ src/lib.rs | 24 +-- src/multicast.rs | 110 ----------- src/netif.rs | 235 ---------------------- src/nvs.rs | 175 ----------------- src/stack.rs | 398 ++----------------------------------- src/stack/eth.rs | 103 +--------- src/stack/netif.rs | 135 +++++++++++++ src/stack/persist.rs | 91 +++++++++ src/stack/wifible.rs | 419 ++++++++++++++++++++------------------- src/udp.rs | 40 ---- src/wifi.rs | 192 ------------------ src/wifi/comm.rs | 447 ------------------------------------------ src/wifi/mgmt.rs | 243 ----------------------- 20 files changed, 606 insertions(+), 2340 deletions(-) delete mode 100644 src/error.rs delete mode 100644 src/multicast.rs delete mode 100644 src/netif.rs delete mode 100644 src/nvs.rs create mode 100644 src/stack/netif.rs create mode 100644 src/stack/persist.rs delete mode 100644 src/udp.rs delete mode 100644 src/wifi.rs delete mode 100644 src/wifi/comm.rs delete mode 100644 src/wifi/mgmt.rs diff --git a/Cargo.toml b/Cargo.toml index 161f004..4ab6be4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,13 @@ embedded-svc = { git = "https://github.com/esp-rs/embedded-svc" } esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc", branch = "gatt" } #esp-idf-svc = { path = "../esp-idf-svc" } rs-matter = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" } -rs-matter-macros = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" } #rs-matter = { path = "../rs-matter/rs-matter" } +rs-matter-macros = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" } #rs-matter-macros = { path = "../rs-matter/rs-matter-macros" } +edge-nal = { git = "https://github.com/ivmarkov/edge-net" } +#edge-nal = { path = "../edge-net/edge-nal" } +edge-nal-std = { git = "https://github.com/ivmarkov/edge-net" } +#edge-nal-std = { path = "../edge-net/edge-nal-std" } [profile.release] opt-level = "s" @@ -34,9 +38,12 @@ debug = true opt-level = "z" [features] -default = ["std", "async-io-mini"] -std = ["rs-matter/std", "esp-idf-svc/std"] -examples = ["std", "async-io-mini", "esp-idf-svc/binstart", "esp-idf-svc/critical-section"] # Enable only when building the examples +#default = ["rs-matter-stack"] +default = ["rs-matter-stack", "async-io-mini"] +rs-matter-stack = ["dep:rs-matter-stack", "std"] +async-io-mini = ["std", "edge-nal-std/async-io-mini"] +std = ["esp-idf-svc/std", "edge-nal-std"] +examples = ["default", "esp-idf-svc/binstart", "esp-idf-svc/critical-section"] # Enable only when building the examples [dependencies] log = { version = "0.4", default-features = false } @@ -50,8 +57,11 @@ esp-idf-svc = { version = "0.48", default-features = false, features = ["alloc", embedded-svc = { version = "0.27", default-features = false } rs-matter = { version = "0.1", default-features = false, features = ["rustcrypto"] } rs-matter-macros = "0.1" -async-io = { version = "=2.0.0", optional = true, default-features = false } # Workaround for https://github.com/smol-rs/async-lock/issues/84 -async-io-mini = { git = "https://github.com/ivmarkov/async-io-mini", optional = true } +async-io = { version = "=2.0.0", default-features = false } # Workaround for https://github.com/smol-rs/async-lock/issues/84 +rs-matter-stack = { git = "https://github.com/ivmarkov/rs-matter-stack", default-features = false, features = ["std"], optional = true } +#rs-matter-stack = { path = "../rs-matter-stack", default-features = false, features = ["std"], optional = true } +edge-nal = "0.2" +edge-nal-std = { version = "0.2", default-features = false, optional = true } [build-dependencies] embuild = "0.31.3" @@ -59,6 +69,7 @@ embuild = "0.31.3" [dev-dependencies] embassy-time = { version = "0.3", features = ["generic-queue"] } static_cell = "2.1" +anyhow = "1" [[example]] name = "light" diff --git a/README.md b/README.md index 237ab76..63de409 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,19 @@ ## Overview -Configuring and running the [`rs-matter`](https://github.com/project-chip/rs-matter) crate is not trivial. +Everything necessary to run [`rs-matter`](https://github.com/project-chip/rs-matter) on the ESP-IDF: +* Bluedroid implementation of `rs-matter`'s `GattPeripheral` for BLE comissioning support. +* [`rs-matter-stack`](https://github.com/ivmarkov/rs-matter-stack) support with `Netif`, `KvBlobStore` and `Modem` implementations. -Users are expected to provide implementations for various `rs-matter` abstractions, like a UDP stack, BLE stack, randomizer, epoch time, responder and so on and so forth. +Since ESP-IDF does support the Rust Standard Library, UDP networking just works. -Furthermore, _operating_ the assembled Matter stack is also challenging, as various features might need to be switched on or off depending on whether Matter is running in commissioning or operating mode, and also depending on the current network connectivity (as in e.g. Wifi signal lost). - -**This crate addresses these issues by providing an all-in-one [`MatterStack`](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/stack.rs) assembly that configures `rs-matter` for reliably operating on top of the ESP IDF SDK.** - -Instantiate it and then call `MatterStack::run(...)`. +## Example ```rust -//! An example utilizing the `WifiBleMatterStack` struct. +//! An example utilizing the `EspWifiBleMatterStack` struct. //! As the name suggests, this Matter stack assembly uses Wifi as the main transport, //! and BLE for commissioning. -//! If you want to use Ethernet, utilize `EthMatterStack` instead. +//! If you want to use Ethernet, utilize `EspEthMatterStack` instead. //! //! The example implements a fictitious Light device (an On-Off Matter cluster). @@ -32,7 +30,7 @@ use core::pin::pin; use embassy_futures::select::select; use embassy_time::{Duration, Timer}; -use esp_idf_matter::{init_async_io, Error, MdnsType, WifiBleMatterStack}; +use esp_idf_matter::{init_async_io, EspWifiBleMatterStack, EspKvBlobStore, EspPersist, EspModem}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::peripherals::Peripherals; @@ -57,7 +55,7 @@ use static_cell::ConstStaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; -fn main() -> Result<(), Error> { +fn main() -> Result<(), anyhow::Error> { EspLogger::initialize_default(); info!("Starting..."); @@ -80,7 +78,7 @@ fn main() -> Result<(), Error> { #[inline(never)] #[cold] -fn run() -> Result<(), Error> { +fn run() -> Result<(), anyhow::Error> { let result = block_on(matter()); if let Err(e) = &result { @@ -93,11 +91,17 @@ fn run() -> Result<(), Error> { result } -async fn matter() -> Result<(), Error> { +async fn matter() -> Result<(), anyhow::Error> { // Take the Matter stack (can be done only once), // as we'll run it in this thread let stack = MATTER_STACK.take(); + // Take some generic ESP-IDF stuff we'll need later + let sysloop = EspSystemEventLoop::take()?; + let timers = EspTaskTimerService::new()?; + let nvs = EspDefaultNvsPartition::take()?; + let peripherals = Peripherals::take()?; + // Our "light" on-off cluster. // Can be anything implementing `rs_matter::data_model::AsyncHandler` let on_off = cluster_on_off::OnOffCluster::new(*stack.matter().borrow()); @@ -124,17 +128,15 @@ async fn matter() -> Result<(), Error> { // Using `pin!` is completely optional, but saves some memory due to `rustc` // not being very intelligent w.r.t. stack usage in async functions let mut matter = pin!(stack.run( - // The Matter stack needs (a clone of) the system event loop - EspSystemEventLoop::take()?, - // The Matter stack needs (a clone of) the timer service - EspTaskTimerService::new()?, - // The Matter stack needs (a clone of) the default ESP IDF NVS partition - EspDefaultNvsPartition::take()?, + // The Matter stack needs a persister to store its state + // `EspPersist`+`EspKvBlobStore` saves to a user-supplied NVS partition + // under namespace `esp-idf-matter` + EspPersist::new_wifi_ble(EspKvBlobStore::new_default(nvs.clone())?, stack), // The Matter stack needs the BT/Wifi modem peripheral - and in general - // the Bluetooth / Wifi connections will be managed by the Matter stack itself // For finer-grained control, call `MatterStack::is_commissioned`, // `MatterStack::commission` and `MatterStack::operate` - Peripherals::take()?.modem, + EspModem::new(peripherals.modem, sysloop, timers, nvs, stack), // Hard-coded for demo purposes CommissioningData { verifier: VerifierData::new_with_pw(123456, *stack.matter().borrow()), @@ -166,14 +168,16 @@ async fn matter() -> Result<(), Error> { }); // Schedule the Matter run & the device loop together - select(&mut matter, &mut device).coalesce().await + select(&mut matter, &mut device).coalesce().await?; + + Ok(()) } /// The Matter stack is allocated statically to avoid /// program stack blowups. /// It is also a mandatory requirement when the `WifiBle` stack variation is used. -static MATTER_STACK: ConstStaticCell = - ConstStaticCell::new(WifiBleMatterStack::new( +static MATTER_STACK: ConstStaticCell> = + ConstStaticCell::new(EspWifiBleMatterStack::new_default( &BasicInfoConfig { vid: 0xFFF1, pid: 0x8000, @@ -186,7 +190,6 @@ static MATTER_STACK: ConstStaticCell = vendor_name: "ACME", }, &dev_att::HardCodedDevAtt::new(), - MdnsType::default(), )); /// Endpoint 0 (the root endpoint) always runs @@ -197,7 +200,7 @@ const LIGHT_ENDPOINT_ID: u16 = 1; const NODE: Node = Node { id: 0, endpoints: &[ - WifiBleMatterStack::root_metadata(), + EspWifiBleMatterStack::<()>::root_metadata(), Endpoint { id: LIGHT_ENDPOINT_ID, device_type: DEV_TYPE_ON_OFF_LIGHT, @@ -207,41 +210,21 @@ const NODE: Node = Node { }; ``` -(See also [Examples](#examples)) +(See also [All examples](#all-examples)) -### Advanced use cases -If the provided `MatterStack` does not cut it, users can implement their own stacks because the building blocks are also exposed as a public API. - -#### Building blocks - -* [Bluetooth commissioning support](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/ble.rs) with the ESP IDF Bluedroid stack (not necessary if you plan to run Matter over Ethernet) -* WiFi provisioning support via an [ESP IDF specific Matter Network Commissioning Cluster implementation](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/wifi/comm.rs) -* [Non-volatile storage for Matter persistent data (fabrics, ACLs and network connections)](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/nvs.rs) on top of the ESP IDF NVS flash API -* mDNS: - * Optional [Matter mDNS responder implementation](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/mdns.rs) based on the ESP IDF mDNS responder (use if you need to register other services besides Matter in mDNS) - * [UDP-multicast workarounds](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/multicast.rs) for `rs-matter`'s built-in mDNS responder, addressing bugs in the Rust STD wrappers of ESP IDF - -#### Future +## Future * Device Attestation data support using secure flash storage * Setting system time via Matter * Matter OTA support based on the ESP IDF OTA API * Thread networking (for ESP32H2 and ESP32C6) * Wifi Access-Point based commissioning (for ESP32S2 which does not have Bluetooth support) -#### Additional building blocks provided by `rs-matter` directly and compatible with ESP IDF: -* UDP and (in future) TCP support - * Enable the `async-io` and `std` features on `rs-matter` and use `async-io` sockets. The [`async-io`](https://github.com/smol-rs/async-io) crate has support for ESP IDF out of the box -* Random number generator - * Enable the `std` feature on `rs-matter`. This way, the [`rand`](https://github.com/rust-random/rand) crate will be utilized, which has support for ESP IDF out of the box -* UNIX epoch - * Enable the `std` feature on `rs-matter`. This way, `rs-matter` will utilize `std::time::SystemTime` which is supported by ESP IDF out of the box - ## Build Prerequisites Follow the [Prerequisites](https://github.com/esp-rs/esp-idf-template#prerequisites) section in the `esp-idf-template` crate. -## Examples +## All examples The examples could be built and flashed conveniently with [`cargo-espflash`](https://github.com/esp-rs/espflash/). To run e.g. `light` on an e.g. ESP32-C3: (Swap the Rust target and example name with the target corresponding for your ESP32 MCU and with the example you would like to build) diff --git a/clippy.toml b/clippy.toml index 44608f1..c5af450 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,4 @@ future-size-threshold = 2048 +stack-size-threshold = 2048 +pass-by-value-size-limit = 16 +large-error-threshold = 64 diff --git a/examples/light.rs b/examples/light.rs index 890ec92..f58533c 100644 --- a/examples/light.rs +++ b/examples/light.rs @@ -1,7 +1,7 @@ -//! An example utilizing the `WifiBleMatterStack` struct. +//! An example utilizing the `EspWifiBleMatterStack` struct. //! As the name suggests, this Matter stack assembly uses Wifi as the main transport, //! and BLE for commissioning. -//! If you want to use Ethernet, utilize `EthMatterStack` instead. +//! If you want to use Ethernet, utilize `EspEthMatterStack` instead. //! //! The example implements a fictitious Light device (an On-Off Matter cluster). @@ -11,7 +11,7 @@ use core::pin::pin; use embassy_futures::select::select; use embassy_time::{Duration, Timer}; -use esp_idf_matter::{init_async_io, Error, MdnsType, WifiBleMatterStack}; +use esp_idf_matter::{init_async_io, EspKvBlobStore, EspModem, EspPersist, EspWifiBleMatterStack}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::peripherals::Peripherals; @@ -31,12 +31,14 @@ use rs_matter::secure_channel::spake2p::VerifierData; use rs_matter::utils::select::Coalesce; use rs_matter::CommissioningData; +use rs_matter_stack::persist::DummyPersist; + use static_cell::ConstStaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; -fn main() -> Result<(), Error> { +fn main() -> Result<(), anyhow::Error> { EspLogger::initialize_default(); info!("Starting..."); @@ -45,7 +47,7 @@ fn main() -> Result<(), Error> { // confused by the low priority of the ESP IDF main task // Also allocate a very large stack (for now) as `rs-matter` futures do occupy quite some space let thread = std::thread::Builder::new() - .stack_size(65 * 1024) + .stack_size(70 * 1024) .spawn(|| { // Eagerly initialize `async-io` to minimize the risk of stack blowups later on init_async_io()?; @@ -59,7 +61,7 @@ fn main() -> Result<(), Error> { #[inline(never)] #[cold] -fn run() -> Result<(), Error> { +fn run() -> Result<(), anyhow::Error> { let result = block_on(matter()); if let Err(e) = &result { @@ -72,11 +74,17 @@ fn run() -> Result<(), Error> { result } -async fn matter() -> Result<(), Error> { +async fn matter() -> Result<(), anyhow::Error> { // Take the Matter stack (can be done only once), // as we'll run it in this thread let stack = MATTER_STACK.take(); + // Take some generic ESP-IDF stuff we'll need later + let sysloop = EspSystemEventLoop::take()?; + let timers = EspTaskTimerService::new()?; + let nvs = EspDefaultNvsPartition::take()?; + let peripherals = Peripherals::take()?; + // Our "light" on-off cluster. // Can be anything implementing `rs_matter::data_model::AsyncHandler` let on_off = cluster_on_off::OnOffCluster::new(*stack.matter().borrow()); @@ -103,17 +111,16 @@ async fn matter() -> Result<(), Error> { // Using `pin!` is completely optional, but saves some memory due to `rustc` // not being very intelligent w.r.t. stack usage in async functions let mut matter = pin!(stack.run( - // The Matter stack needs (a clone of) the system event loop - EspSystemEventLoop::take()?, - // The Matter stack needs (a clone of) the timer service - EspTaskTimerService::new()?, - // The Matter stack needs (a clone of) the default ESP IDF NVS partition - EspDefaultNvsPartition::take()?, + // The Matter stack needs a persister to store its state + // `EspPersist`+`EspKvBlobStore` saves to a user-supplied NVS partition + // under namespace `esp-idf-matter` + DummyPersist, + //EspPersist::new_wifi_ble(EspKvBlobStore::new_default(nvs.clone())?, stack), // The Matter stack needs the BT/Wifi modem peripheral - and in general - // the Bluetooth / Wifi connections will be managed by the Matter stack itself // For finer-grained control, call `MatterStack::is_commissioned`, // `MatterStack::commission` and `MatterStack::operate` - Peripherals::take()?.modem, + EspModem::new(peripherals.modem, sysloop, timers, nvs, stack), // Hard-coded for demo purposes CommissioningData { verifier: VerifierData::new_with_pw(123456, *stack.matter().borrow()), @@ -145,14 +152,16 @@ async fn matter() -> Result<(), Error> { }); // Schedule the Matter run & the device loop together - select(&mut matter, &mut device).coalesce().await + select(&mut matter, &mut device).coalesce().await?; + + Ok(()) } /// The Matter stack is allocated statically to avoid /// program stack blowups. /// It is also a mandatory requirement when the `WifiBle` stack variation is used. -static MATTER_STACK: ConstStaticCell = - ConstStaticCell::new(WifiBleMatterStack::new( +static MATTER_STACK: ConstStaticCell> = + ConstStaticCell::new(EspWifiBleMatterStack::new_default( &BasicInfoConfig { vid: 0xFFF1, pid: 0x8000, @@ -165,7 +174,6 @@ static MATTER_STACK: ConstStaticCell = vendor_name: "ACME", }, &dev_att::HardCodedDevAtt::new(), - MdnsType::default(), )); /// Endpoint 0 (the root endpoint) always runs @@ -176,7 +184,7 @@ const LIGHT_ENDPOINT_ID: u16 = 1; const NODE: Node = Node { id: 0, endpoints: &[ - WifiBleMatterStack::root_metadata(), + EspWifiBleMatterStack::<()>::root_metadata(), Endpoint { id: LIGHT_ENDPOINT_ID, device_type: DEV_TYPE_ON_OFF_LIGHT, diff --git a/examples/light_eth.rs b/examples/light_eth.rs index 15f7b43..d59ee4e 100644 --- a/examples/light_eth.rs +++ b/examples/light_eth.rs @@ -1,4 +1,4 @@ -//! An example utilizing the `EthMatterStack` struct. +//! An example utilizing the `EspEthMatterStack` struct. //! As the name suggests, this Matter stack assembly uses Ethernet as the main transport, as well as for commissioning. //! //! Notice thart we actually don't use Ethernet for real, as ESP32s don't have Ethernet ports out of the box. @@ -13,7 +13,9 @@ use core::pin::pin; use embassy_futures::select::select; use embassy_time::{Duration, Timer}; -use esp_idf_matter::{init_async_io, Error, EthMatterStack, MdnsType}; +use esp_idf_matter::{ + init_async_io, EspEthMatterStack, EspKvBlobStore, EspMatterNetif, EspPersist, +}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::peripherals::Peripherals; @@ -36,6 +38,8 @@ use rs_matter::secure_channel::spake2p::VerifierData; use rs_matter::utils::select::Coalesce; use rs_matter::CommissioningData; +use rs_matter_stack::persist::DummyPersist; + use static_cell::ConstStaticCell; #[path = "dev_att/dev_att.rs"] @@ -44,7 +48,7 @@ mod dev_att; const WIFI_SSID: &str = env!("WIFI_SSID"); const WIFI_PASS: &str = env!("WIFI_PASS"); -fn main() -> Result<(), Error> { +fn main() -> Result<(), anyhow::Error> { EspLogger::initialize_default(); info!("Starting..."); @@ -67,7 +71,7 @@ fn main() -> Result<(), Error> { #[inline(never)] #[cold] -fn run() -> Result<(), Error> { +fn run() -> Result<(), anyhow::Error> { let result = block_on(matter()); if let Err(e) = &result { @@ -80,17 +84,19 @@ fn run() -> Result<(), Error> { result } -async fn matter() -> Result<(), Error> { +async fn matter() -> Result<(), anyhow::Error> { + // Take the Matter stack (can be done only once), + // as we'll run it in this thread + let stack = MATTER_STACK.take(); + + // Take some generic ESP-IDF stuff we'll need later let sysloop = EspSystemEventLoop::take()?; let nvs = EspDefaultNvsPartition::take()?; + let peripherals = Peripherals::take()?; // Configure and start the Wifi first let mut wifi = Box::new(AsyncWifi::wrap( - EspWifi::new( - Peripherals::take()?.modem, - sysloop.clone(), - Some(nvs.clone()), - )?, + EspWifi::new(peripherals.modem, sysloop.clone(), Some(nvs.clone()))?, sysloop.clone(), EspTaskTimerService::new()?, )?); @@ -105,10 +111,6 @@ async fn matter() -> Result<(), Error> { // Matter needs an IPv6 address to work esp!(unsafe { esp_netif_create_ip6_linklocal(wifi.wifi().sta_netif().handle() as _) })?; - // Take the Matter stack (can be done only once), - // as we'll run it in this thread - let stack = MATTER_STACK.take(); - // Our "light" on-off cluster. // Can be anything implementing `rs_matter::data_model::AsyncHandler` let on_off = cluster_on_off::OnOffCluster::new(*stack.matter().borrow()); @@ -135,15 +137,15 @@ async fn matter() -> Result<(), Error> { // Using `pin!` is completely optional, but saves some memory due to `rustc` // not being very intelligent w.r.t. stack usage in async functions let mut matter = pin!(stack.run( - // The Matter stack needs (a clone of) the system event loop - sysloop, - // The Matter stack needs (a clone of) the default ESP IDF NVS partition - nvs, - // For the Ethernet case, the Matter stack needs access to the network - // interface (i.e. something that implements the `NetifAccess` trait) - // so that it can track when the interface goes up/down. `EspNetif` obviously - // does implement this trait - wifi.wifi().sta_netif(), + // The Matter stack needs a persister to store its state + // `EspPersist`+`EspKvBlobStore` saves to a user-supplied NVS partition + // under namespace `esp-idf-matter` + DummyPersist, + //EspPersist::new_eth(EspKvBlobStore::new_default(nvs.clone())?, stack), + // The Matter stack need access to the netif on which we'll operate + // Since we are pretending to use a wired Ethernet connection - yet - + // we are using a Wifi STA, provide the Wifi netif here + EspMatterNetif::new(wifi.wifi().sta_netif(), sysloop), // Hard-coded for demo purposes CommissioningData { verifier: VerifierData::new_with_pw(123456, *stack.matter().borrow()), @@ -175,26 +177,28 @@ async fn matter() -> Result<(), Error> { }); // Schedule the Matter run & the device loop together - select(&mut matter, &mut device).coalesce().await + select(&mut matter, &mut device).coalesce().await?; + + Ok(()) } /// The Matter stack is allocated statically to avoid /// program stack blowups. -static MATTER_STACK: ConstStaticCell = ConstStaticCell::new(EthMatterStack::new( - &BasicInfoConfig { - vid: 0xFFF1, - pid: 0x8000, - hw_ver: 2, - sw_ver: 1, - sw_ver_str: "1", - serial_no: "aabbccdd", - device_name: "MyLight", - product_name: "ACME Light", - vendor_name: "ACME", - }, - &dev_att::HardCodedDevAtt::new(), - MdnsType::default(), -)); +static MATTER_STACK: ConstStaticCell> = + ConstStaticCell::new(EspEthMatterStack::new_default( + &BasicInfoConfig { + vid: 0xFFF1, + pid: 0x8000, + hw_ver: 2, + sw_ver: 1, + sw_ver_str: "1", + serial_no: "aabbccdd", + device_name: "MyLight", + product_name: "ACME Light", + vendor_name: "ACME", + }, + &dev_att::HardCodedDevAtt::new(), + )); /// Endpoint 0 (the root endpoint) always runs /// the hidden Matter system clusters, so we pick ID=1 @@ -204,7 +208,7 @@ const LIGHT_ENDPOINT_ID: u16 = 1; const NODE: Node = Node { id: 0, endpoints: &[ - EthMatterStack::root_metadata(), + EspEthMatterStack::<()>::root_metadata(), Endpoint { id: LIGHT_ENDPOINT_ID, device_type: DEV_TYPE_ON_OFF_LIGHT, diff --git a/src/ble.rs b/src/ble.rs index e65679c..8f518f3 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -22,7 +22,7 @@ use esp_idf_svc::bt::ble::gatt::{ }; use esp_idf_svc::bt::{BdAddr, BleEnabled, BtDriver, BtStatus, BtUuid}; use esp_idf_svc::hal::task::embassy_sync::EspRawMutex; -use esp_idf_svc::sys::{EspError, ESP_FAIL}; +use esp_idf_svc::sys::{EspError, ESP_ERR_INVALID_STATE, ESP_FAIL}; use log::{debug, info, warn}; @@ -35,8 +35,6 @@ use rs_matter::transport::network::BtAddr; use rs_matter::utils::ifmutex::IfMutex; use rs_matter::utils::signal::Signal; -use crate::error::Error; - const MAX_CONNECTIONS: usize = MAX_BTP_SESSIONS; const MAX_MTU_SIZE: usize = 512; @@ -58,7 +56,7 @@ struct State { response: GattResponse, } -#[derive(Debug, Clone)] +#[derive(Debug)] struct IndBuffer { addr: BtAddr, data: heapless::Vec, @@ -75,6 +73,8 @@ pub struct BtpGattContext { impl BtpGattContext { /// Create a new instance. + #[allow(clippy::large_stack_frames)] + #[inline(always)] pub const fn new() -> Self { Self { state: Mutex::new(RefCell::new(State { @@ -94,7 +94,7 @@ impl BtpGattContext { } } - pub(crate) fn reset(&self) -> Result<(), Error> { + pub(crate) fn reset(&self) -> Result<(), EspError> { self.state.lock(|state| { let mut state = state.borrow_mut(); @@ -110,15 +110,21 @@ impl BtpGattContext { (false, ()) }); - self.ind.try_lock().map(|mut ind| { - ind.data.clear(); - })?; + self.ind + .try_lock() + .map(|mut ind| { + ind.data.clear(); + }) + .map_err(|_| EspError::from_infallible::<{ ESP_ERR_INVALID_STATE }>())?; Ok(()) } } impl Default for BtpGattContext { + // TODO + #[allow(clippy::large_stack_frames)] + #[inline(always)] fn default() -> Self { Self::new() } @@ -131,7 +137,7 @@ where M: BleEnabled, { app_id: u16, - driver: &'a BtDriver<'d, M>, + driver: BtDriver<'d, M>, context: &'a BtpGattContext, } @@ -145,9 +151,9 @@ where /// that there are no other GATT peripherals running before calling this function. pub fn new( app_id: u16, - driver: &'a BtDriver<'d, M>, + driver: BtDriver<'d, M>, context: &'a BtpGattContext, - ) -> Result { + ) -> Result { context.reset()?; Ok(Self { @@ -163,14 +169,14 @@ where service_name: &str, service_adv_data: &AdvData, mut callback: F, - ) -> Result<(), Error> + ) -> Result<(), EspError> where F: FnMut(GattPeripheralEvent) + Send + 'd, { let _pin = service_adv_data.pin(); - let gap = EspBleGap::new(self.driver)?; - let gatts = EspGatts::new(self.driver)?; + let gap = EspBleGap::new(&self.driver)?; + let gatts = EspGatts::new(&self.driver)?; info!("BLE Gap and Gatts initialized"); @@ -235,7 +241,7 @@ where } /// Indicate new data on characteristic `C2` to a remote peer. - pub async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error> { + pub async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), EspError> { self.context .ind .with(|ind| { diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index bf42ef9..0000000 --- a/src/error.rs +++ /dev/null @@ -1,63 +0,0 @@ -use core::fmt::{self, Display}; - -use embassy_sync::mutex::TryLockError; -use esp_idf_svc::sys::EspError; - -/// The error used throughout this crate. -/// A composition of `rs_matter::error::Error` and `EspError`. -#[derive(Debug)] -pub enum Error { - Matter(rs_matter::error::Error), - Esp(EspError), - InvalidState, -} - -impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Matter(e) => write!(f, "Matter error: {}", e), - Error::Esp(e) => write!(f, "ESP error: {}", e), - Error::InvalidState => write!(f, "Invalid state"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for Error {} - -impl From for Error { - fn from(e: rs_matter::error::Error) -> Self { - Error::Matter(e) - } -} - -impl From for Error { - fn from(e: rs_matter::error::ErrorCode) -> Self { - Error::Matter(e.into()) - } -} - -impl From for Error { - fn from(e: EspError) -> Self { - Error::Esp(e) - } -} - -impl From for Error { - fn from(_: TryLockError) -> Self { - Error::InvalidState - } -} - -impl From for Error { - fn from(_: rs_matter::utils::ifmutex::TryLockError) -> Self { - Error::InvalidState - } -} - -#[cfg(feature = "std")] -impl From for Error { - fn from(e: std::io::Error) -> Self { - Error::Matter(e.into()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 85fa1eb..0a71214 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ #![allow(unexpected_cfgs)] #![allow(clippy::declare_interior_mutable_const)] #![warn(clippy::large_futures)] +#![warn(clippy::large_stack_frames)] +#![warn(clippy::large_types_passed_by_value)] #[cfg(feature = "std")] #[allow(unused_imports)] @@ -15,29 +17,9 @@ extern crate std; #[macro_use] extern crate alloc; -pub use error::*; -#[cfg(all( - feature = "std", - any(feature = "async-io", feature = "async-io-mini"), - esp_idf_comp_nvs_flash_enabled, - esp_idf_comp_esp_netif_enabled, - esp_idf_comp_esp_event_enabled -))] +#[cfg(feature = "rs-matter-stack")] pub use stack::*; pub mod ble; -mod error; pub mod mdns; -pub mod multicast; -pub mod netif; -pub mod nvs; -#[cfg(all( - feature = "std", - any(feature = "async-io", feature = "async-io-mini"), - esp_idf_comp_nvs_flash_enabled, - esp_idf_comp_esp_netif_enabled, - esp_idf_comp_esp_event_enabled -))] mod stack; -mod udp; -pub mod wifi; diff --git a/src/multicast.rs b/src/multicast.rs deleted file mode 100644 index 094ab48..0000000 --- a/src/multicast.rs +++ /dev/null @@ -1,110 +0,0 @@ -#![cfg(all(feature = "std", any(feature = "async-io", feature = "async-io-mini")))] - -use core::net::{Ipv4Addr, Ipv6Addr}; - -use std::net::UdpSocket; - -#[cfg(feature = "async-io-mini")] -use async_io_mini as async_io; - -use log::info; - -use rs_matter::error::{Error, ErrorCode}; - -/// Join an IPV6 multicast group on a specific interface -pub fn join_multicast_v6( - socket: &async_io::Async, - multiaddr: Ipv6Addr, - interface: u32, -) -> Result<(), Error> { - #[cfg(not(target_os = "espidf"))] - socket.as_ref().join_multicast_v6(&multiaddr, interface)?; - - // join_multicast_v6() is broken for ESP-IDF due to mismatch w.r.t. sizes - // (u8 expected but u32 passed to setsockopt() and sometimes the other way around) - #[cfg(target_os = "espidf")] - { - let mreq = esp_idf_svc::sys::ipv6_mreq { - ipv6mr_multiaddr: esp_idf_svc::sys::in6_addr { - un: esp_idf_svc::sys::in6_addr__bindgen_ty_1 { - u8_addr: multiaddr.octets(), - }, - }, - ipv6mr_interface: interface, - }; - - esp_setsockopt( - socket, - esp_idf_svc::sys::IPPROTO_IPV6, - esp_idf_svc::sys::IPV6_ADD_MEMBERSHIP, - mreq, - )?; - } - - info!("Joined IPV6 multicast {}/{}", multiaddr, interface); - - Ok(()) -} - -/// Join an IPV4 multicast group on a specific interface -pub fn join_multicast_v4( - socket: &async_io::Async, - multiaddr: Ipv4Addr, - interface: Ipv4Addr, -) -> Result<(), Error> { - #[cfg(not(target_os = "espidf"))] - self.socket - .as_ref() - .join_multicast_v4(multiaddr, interface)?; - - // join_multicast_v4() is broken for ESP-IDF, most likely due to wrong `ip_mreq` signature in the `libc` crate - // Note that also most *_multicast_v4 and *_multicast_v6 methods are broken as well in Rust STD for the ESP-IDF - // due to mismatch w.r.t. sizes (u8 expected but u32 passed to setsockopt() and sometimes the other way around) - #[cfg(target_os = "espidf")] - { - let mreq = esp_idf_svc::sys::ip_mreq { - imr_multiaddr: esp_idf_svc::sys::in_addr { - s_addr: u32::from_ne_bytes(multiaddr.octets()), - }, - imr_interface: esp_idf_svc::sys::in_addr { - s_addr: u32::from_ne_bytes(interface.octets()), - }, - }; - - esp_setsockopt( - socket, - esp_idf_svc::sys::IPPROTO_IP, - esp_idf_svc::sys::IP_ADD_MEMBERSHIP, - mreq, - )?; - } - - info!("Joined IP multicast {}/{}", multiaddr, interface); - - Ok(()) -} - -// Most *_multicast_v4 and *_multicast_v6 methods are broken in Rust STD for the ESP-IDF -// due to mismatch w.r.t. sizes (u8 expected but u32 passed to setsockopt() and sometimes the other way around) -#[cfg(target_os = "espidf")] -fn esp_setsockopt( - socket: &async_io::Async, - proto: u32, - option: u32, - value: T, -) -> Result<(), Error> { - use std::os::fd::AsRawFd; - - esp_idf_svc::sys::esp!(unsafe { - esp_idf_svc::sys::lwip_setsockopt( - socket.as_raw_fd(), - proto as _, - option as _, - &value as *const _ as *const _, - core::mem::size_of::() as _, - ) - }) - .map_err(|_| ErrorCode::StdIoError)?; - - Ok(()) -} diff --git a/src/netif.rs b/src/netif.rs deleted file mode 100644 index c73fe4f..0000000 --- a/src/netif.rs +++ /dev/null @@ -1,235 +0,0 @@ -#![cfg(all(esp_idf_comp_esp_netif_enabled, esp_idf_comp_esp_event_enabled))] - -use core::fmt; -use core::net::{Ipv4Addr, Ipv6Addr}; -use core::pin::pin; - -use alloc::sync::Arc; - -use embassy_futures::select::select; -use embassy_sync::blocking_mutex::raw::RawMutex; -use embassy_sync::mutex::Mutex; -use embassy_time::{Duration, Timer}; - -use esp_idf_svc::eventloop::EspSystemEventLoop; -use esp_idf_svc::hal::task::embassy_sync::EspRawMutex; -use esp_idf_svc::handle::RawHandle; -use esp_idf_svc::netif::{EspNetif, IpEvent}; -use esp_idf_svc::sys::{esp, esp_netif_get_ip6_linklocal, EspError, ESP_FAIL}; - -use rs_matter::utils::notification::Notification; - -use crate::error::Error; - -const TIMEOUT_PERIOD_SECS: u8 = 5; - -/// Async trait for accessing the `EspNetif` network interface (netif) of a driver. -/// -/// Allows sharing the network interface between multiple tasks, where one task -/// may be waiting for the network interface to be ready, while the other might -/// be mutably operating on the L2 driver below the netif, or on the netif itself. -pub trait NetifAccess { - /// Waits until the network interface is available and then - /// calls the provided closure with a reference to the network interface. - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R; - - /// Waits until a certain condition `f` becomes `Some` for a network interface - /// and then returns the result. - /// - /// The condition is checked every 5 seconds and every time a new `IpEvent` is - /// generated on the ESP IDF system event loop. - /// - /// The main use case of this method is to wait and listen the netif for changes - /// (netif up/down, IP address changes, etc.) - async fn wait(&self, sysloop: EspSystemEventLoop, mut f: F) -> Result - where - F: FnMut(&EspNetif) -> Result, Error>, - { - let notification = Arc::new(Notification::::new()); - - let _subscription = { - let notification = notification.clone(); - - sysloop.subscribe::(move |_| { - notification.notify(); - }) - }?; - - loop { - if let Some(result) = self.with_netif(&mut f).await? { - break Ok(result); - } - - let mut events = pin!(notification.wait()); - let mut timer = pin!(Timer::after(Duration::from_secs(TIMEOUT_PERIOD_SECS as _))); - - select(&mut events, &mut timer).await; - } - } -} - -impl NetifAccess for &T -where - T: NetifAccess, -{ - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R, - { - (**self).with_netif(f).await - } -} - -impl NetifAccess for &mut T -where - T: NetifAccess, -{ - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R, - { - (**self).with_netif(f).await - } -} - -impl NetifAccess for EspNetif { - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R, - { - f(self) - } -} - -impl NetifAccess for Mutex -where - M: RawMutex, - T: NetifAccess, -{ - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R, - { - let netif = self.lock().await; - - netif.with_netif(f).await - } -} - -/// The current configuration of a network interface (if the netif is configured and up) -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct NetifInfo { - /// Ipv4 address - pub ipv4: Ipv4Addr, - // Ipv6 address - pub ipv6: Ipv6Addr, - // Interface index - pub interface: u32, - // MAC address - pub mac: [u8; 6], -} - -impl fmt::Display for NetifInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "IPv4: {}, IPv6: {}, Interface: {}, MAC: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", - self.ipv4, - self.ipv6, - self.interface, - self.mac[0], - self.mac[1], - self.mac[2], - self.mac[3], - self.mac[4], - self.mac[5] - ) - } -} - -/// Get the MAC, IP addresses and the interface index of the network interface. -/// -/// Return an error when some of the IP addresses are unspecified. -pub fn get_info(netif: &EspNetif) -> Result { - let ip_info = netif.get_ip_info()?; - - let ipv4: Ipv4Addr = ip_info.ip.octets().into(); - if ipv4.is_unspecified() { - return Err(EspError::from_infallible::().into()); - } - - let mut ipv6: esp_idf_svc::sys::esp_ip6_addr_t = Default::default(); - - esp!(unsafe { esp_netif_get_ip6_linklocal(netif.handle() as _, &mut ipv6) })?; - - let ipv6: Ipv6Addr = [ - ipv6.addr[0].to_le_bytes()[0], - ipv6.addr[0].to_le_bytes()[1], - ipv6.addr[0].to_le_bytes()[2], - ipv6.addr[0].to_le_bytes()[3], - ipv6.addr[1].to_le_bytes()[0], - ipv6.addr[1].to_le_bytes()[1], - ipv6.addr[1].to_le_bytes()[2], - ipv6.addr[1].to_le_bytes()[3], - ipv6.addr[2].to_le_bytes()[0], - ipv6.addr[2].to_le_bytes()[1], - ipv6.addr[2].to_le_bytes()[2], - ipv6.addr[2].to_le_bytes()[3], - ipv6.addr[3].to_le_bytes()[0], - ipv6.addr[3].to_le_bytes()[1], - ipv6.addr[3].to_le_bytes()[2], - ipv6.addr[3].to_le_bytes()[3], - ] - .into(); - - let interface = netif.get_index(); - - let mac = netif.get_mac()?; - - Ok(NetifInfo { - ipv4, - ipv6, - interface, - mac, - }) -} - -/// Implementation of `NetifAccess` for the `EspEth` and `AsyncEth` drivers. -#[cfg(esp_idf_comp_esp_eth_enabled)] -#[cfg(any( - all(esp32, esp_idf_eth_use_esp32_emac), - any( - esp_idf_eth_spi_ethernet_dm9051, - esp_idf_eth_spi_ethernet_w5500, - esp_idf_eth_spi_ethernet_ksz8851snl - ), - esp_idf_eth_use_openeth -))] -pub mod eth { - use esp_idf_svc::{ - eth::{AsyncEth, EspEth}, - netif::EspNetif, - }; - - use super::NetifAccess; - - impl<'d, T> NetifAccess for EspEth<'d, T> { - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R, - { - f(self.netif()) - } - } - - impl<'d, T> NetifAccess for AsyncEth> { - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R, - { - f(self.eth().netif()) - } - } -} diff --git a/src/nvs.rs b/src/nvs.rs deleted file mode 100644 index f77f08d..0000000 --- a/src/nvs.rs +++ /dev/null @@ -1,175 +0,0 @@ -#![cfg(esp_idf_comp_nvs_flash_enabled)] - -use embassy_sync::blocking_mutex::raw::RawMutex; - -use esp_idf_svc::nvs::{EspNvs, NvsPartitionId}; -use esp_idf_svc::sys::EspError; - -use log::info; - -use rs_matter::Matter; - -use crate::{error::Error, wifi::WifiContext}; - -/// Represents the netowkr type currently in use. -pub enum Network<'a, const N: usize, M> -where - M: RawMutex, -{ - /// The Matter stack uses an Ethernet network for operating - /// or in general, a network that is not managed by the stack - /// and therefore does not need to be stored in the NVS. - Eth, - /// The Matter stack uses Wifi for operating. - Wifi(&'a WifiContext), -} - -impl<'a, const N: usize, M> Network<'a, N, M> -where - M: RawMutex, -{ - const fn key(&self) -> Option<&str> { - match self { - Self::Eth => None, - Self::Wifi(_) => Some("wifi"), - } - } -} - -/// A persistent storage manager (PSM) for the Matter stack. -/// Uses the ESP IDF NVS API to store and load the stack's state. -pub struct Psm<'a, T, const N: usize, M> -where - T: NvsPartitionId, - M: RawMutex, -{ - matter: &'a Matter<'a>, - network: Network<'a, N, M>, - nvs: EspNvs, - buf: &'a mut [u8], -} - -impl<'a, T, const N: usize, M> Psm<'a, T, N, M> -where - T: NvsPartitionId, - M: RawMutex, -{ - /// Create a new PSM instance. - #[inline(always)] - pub fn new( - matter: &'a Matter<'a>, - network: Network<'a, N, M>, - nvs: EspNvs, - buf: &'a mut [u8], - ) -> Result { - Ok(Self { - matter, - network, - nvs, - buf, - }) - } - - /// Run the PSM instance, listening for changes in the Matter stack's state - /// and persisting these, as well as the network state, to the NVS. - pub async fn run(&mut self) -> Result<(), Error> { - self.load().await?; - - loop { - self.matter.wait_changed().await; - self.store().await?; - } - } - - /// Reset the PSM instance, removing all stored data from the NVS. - pub async fn reset(&mut self) -> Result<(), Error> { - Self::remove_blob(&mut self.nvs, "acls").await?; - Self::remove_blob(&mut self.nvs, "fabrics").await?; - - if let Some(nw_key) = self.network.key() { - Self::remove_blob(&mut self.nvs, nw_key).await?; - } - - // TODO: Reset the Matter state - - Ok(()) - } - - /// Load the stored data from the NVS into the Matter stack and the network. - pub async fn load(&mut self) -> Result<(), Error> { - if let Some(data) = Self::load_blob(&mut self.nvs, "acls", self.buf).await? { - self.matter.load_acls(data)?; - } - - if let Some(data) = Self::load_blob(&mut self.nvs, "fabrics", self.buf).await? { - self.matter.load_fabrics(data)?; - } - - if let Network::Wifi(wifi_comm) = self.network { - if let Some(data) = - Self::load_blob(&mut self.nvs, self.network.key().unwrap(), self.buf).await? - { - wifi_comm.load(data)?; - } - } - - Ok(()) - } - - /// Store the Matter stack's state and the network state to the NVS. - pub async fn store(&mut self) -> Result<(), Error> { - if self.matter.is_changed() { - if let Some(data) = self.matter.store_acls(self.buf)? { - Self::store_blob(&mut self.nvs, "acls", data).await?; - } - - if let Some(data) = self.matter.store_fabrics(self.buf)? { - Self::store_blob(&mut self.nvs, "fabrics", data).await?; - } - } - - if let Network::Wifi(wifi_comm) = self.network { - if let Some(data) = wifi_comm.store(self.buf)? { - Self::store_blob(&mut self.nvs, self.network.key().unwrap(), data).await?; - } - } - - Ok(()) - } - - async fn load_blob<'b>( - nvs: &mut EspNvs, - key: &str, - buf: &'b mut [u8], - ) -> Result, EspError> { - // TODO: Not really async - - let data = nvs.get_blob(key, buf)?; - info!( - "Blob {key}: loaded {:?} bytes {data:?}", - data.map(|data| data.len()) - ); - - Ok(data) - } - - async fn store_blob(nvs: &mut EspNvs, key: &str, data: &[u8]) -> Result<(), EspError> { - // TODO: Not really async - - nvs.set_blob(key, data)?; - - info!("Blob {key}: stored {} bytes {data:?}", data.len()); - - Ok(()) - } - - async fn remove_blob(nvs: &mut EspNvs, key: &str) -> Result<(), EspError> { - // TODO: Not really async - - nvs.remove(key)?; - - info!("Blob {key}: removed"); - - Ok(()) - } -} diff --git a/src/stack.rs b/src/stack.rs index c137572..ebbe700 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -1,40 +1,14 @@ -use core::fmt::Write as _; -use core::net::{Ipv6Addr, SocketAddr, SocketAddrV6}; -use core::pin::pin; - -#[cfg(feature = "async-io-mini")] -use async_io_mini as async_io; - -use embassy_futures::select::select3; -use embassy_sync::blocking_mutex::raw::{NoopRawMutex, RawMutex}; - -use esp_idf_svc::eventloop::EspSystemEventLoop; -use esp_idf_svc::hal::task::block_on; -use esp_idf_svc::nvs::{EspNvs, EspNvsPartition, NvsPartitionId}; - -use log::info; - -use rs_matter::data_model::cluster_basic_information::BasicInfoConfig; -use rs_matter::data_model::core::IMBuffer; -use rs_matter::data_model::objects::{AsyncHandler, AsyncMetadata}; -use rs_matter::data_model::sdm::dev_att::DevAttDataFetcher; -use rs_matter::data_model::subscriptions::Subscriptions; -use rs_matter::error::ErrorCode; -use rs_matter::pairing::DiscoveryCapabilities; -use rs_matter::respond::DefaultResponder; -use rs_matter::transport::network::{NetworkReceive, NetworkSend}; -use rs_matter::utils::buf::{BufferAccess, PooledBuffers}; -use rs_matter::utils::select::Coalesce; -use rs_matter::utils::signal::Signal; -use rs_matter::{CommissioningData, Matter, MATTER_PORT}; - -use crate::error::Error; -use crate::multicast::{join_multicast_v4, join_multicast_v6}; -use crate::netif::{get_info, NetifAccess, NetifInfo}; -use crate::nvs; -use crate::udp; +#![cfg(feature = "rs-matter-stack")] pub use eth::*; +#[cfg(all( + esp_idf_comp_esp_netif_enabled, + esp_idf_comp_esp_event_enabled, + feature = "std" +))] +pub use netif::*; +#[cfg(esp_idf_comp_nvs_flash_enabled)] +pub use persist::*; #[cfg(all( not(esp32h2), not(esp32s2), @@ -42,352 +16,16 @@ pub use eth::*; esp_idf_comp_esp_event_enabled, not(esp_idf_btdm_ctrl_mode_br_edr_only), esp_idf_bt_enabled, - esp_idf_bt_bluedroid_enabled + esp_idf_bt_bluedroid_enabled, + feature = "std" ))] pub use wifible::*; -const MAX_SUBSCRIPTIONS: usize = 3; -const MAX_IM_BUFFERS: usize = 10; -const PSM_BUFFER_SIZE: usize = 4096; -const MAX_RESPONDERS: usize = 4; -const MAX_BUSY_RESPONDERS: usize = 2; - mod eth; +mod netif; +mod persist; mod wifible; -/// A trait modeling a specific network type. -/// `MatterStack` is parameterized by a network type implementing this trait. -pub trait Network { - const INIT: Self; -} - -/// An enum modeling the mDNS service to be used. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] -pub enum MdnsType { - /// The mDNS service provided by the `rs-matter` crate. - #[default] - Builtin, -} - -impl MdnsType { - pub const fn default() -> Self { - Self::Builtin - } -} - -/// The `MatterStack` struct is the main entry point for the Matter stack. -/// -/// It wraps the actual `rs-matter` Matter instance and provides a simplified API for running the stack. -pub struct MatterStack<'a, N> -where - N: Network, -{ - matter: Matter<'a>, - buffers: PooledBuffers, - psm_buffer: PooledBuffers<1, NoopRawMutex, heapless::Vec>, - subscriptions: Subscriptions, - #[allow(unused)] - network: N, - #[allow(unused)] - mdns: MdnsType, - netif_info: Signal>, -} - -impl<'a, N> MatterStack<'a, N> -where - N: Network, -{ - /// Create a new `MatterStack` instance. - pub const fn new( - dev_det: &'a BasicInfoConfig, - dev_att: &'a dyn DevAttDataFetcher, - mdns: MdnsType, - ) -> Self { - Self { - matter: Matter::new_default( - dev_det, - dev_att, - rs_matter::mdns::MdnsService::Builtin, - MATTER_PORT, - ), - buffers: PooledBuffers::new(0), - psm_buffer: PooledBuffers::new(0), - subscriptions: Subscriptions::new(), - network: N::INIT, - mdns, - netif_info: Signal::new(None), - } - } - - /// Get a reference to the `Matter` instance. - pub const fn matter(&self) -> &Matter<'a> { - &self.matter - } - - /// Get a reference to the `Network` instance. - /// Useful when the user instantiates `MatterStack` with a custom network type. - pub const fn network(&self) -> &N { - &self.network - } - - /// Notifies the Matter instance that there is a change in the state - /// of one of the clusters. - /// - /// User is expected to call this method when user-provided clusters - /// change their state. - /// - /// This is necessary so as the Matter instance can notify clients - /// that have active subscriptions to some of the changed clusters. - pub fn notify_changed(&self) { - self.subscriptions.notify_changed(); - } - - /// User code hook to get the state of the netif passed to the - /// `run_with_netif` method. - /// - /// Useful when user code needs to bring up/down its own IP services depending on - /// when the netif controlled by Matter goes up, down or changes its IP configuration. - pub async fn get_netif_info(&self) -> Option { - self.netif_info - .wait(|netif_info| Some(netif_info.clone())) - .await - } - - /// User code hook to detect changes to the IP state of the netif passed to the - /// `run_with_netif` method. - /// - /// Useful when user code needs to bring up/down its own IP services depending on - /// when the netif controlled by Matter goes up, down or changes its IP configuration. - pub async fn wait_netif_changed( - &self, - prev_netif_info: Option<&NetifInfo>, - ) -> Option { - self.netif_info - .wait(|netif_info| (netif_info.as_ref() != prev_netif_info).then(|| netif_info.clone())) - .await - } - - /// This method is a specialization of `run_with_transport` over the UDP transport (both IPv4 and IPv6). - /// It calls `run_with_transport` and in parallel runs the mDNS service. - /// - /// The netif instance is necessary, so that the loop can monitor the network and bring up/down - /// the main UDP transport and the mDNS service when the netif goes up/down or changes its IP addresses. - pub async fn run_with_netif<'d, H, P, E>( - &self, - sysloop: EspSystemEventLoop, - nvs: EspNvsPartition

, - netif: E, - dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, - handler: H, - ) -> Result<(), Error> - where - H: AsyncHandler + AsyncMetadata, - P: NvsPartitionId, - E: NetifAccess, - { - loop { - info!("Waiting for the network to come up..."); - - let reset_netif_info = || { - self.netif_info.modify(|global_netif_info| { - if global_netif_info.is_some() { - *global_netif_info = None; - (true, ()) - } else { - (false, ()) - } - }); - }; - - let _guard = scopeguard::guard((), |_| reset_netif_info()); - - reset_netif_info(); - - let netif_info = netif - .wait(sysloop.clone(), |netif| Ok(get_info(netif).ok())) - .await?; - - info!("Got IP network: {netif_info}"); - - self.netif_info.modify(|global_netif_info| { - *global_netif_info = Some(netif_info.clone()); - - (true, ()) - }); - - let socket = async_io::Async::::bind(SocketAddr::V6( - SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, MATTER_PORT, 0, netif_info.interface), - ))?; - - let mut main = pin!(self.run_with_transport( - udp::Udp(&socket), - udp::Udp(&socket), - nvs.clone(), - dev_comm.clone(), - &handler - )); - let mut mdns = pin!(self.run_builtin_mdns(&netif_info)); - let mut down = pin!(netif.wait(sysloop.clone(), |netif| { - let next = get_info(netif).ok(); - let next = next.as_ref(); - - Ok((Some(&netif_info) != next).then_some(())) - })); - - select3(&mut main, &mut mdns, &mut down).coalesce().await?; - - info!("Network change detected"); - } - } - - /// A transport-agnostic method to run the main Matter loop. - /// The user is expected to provide a transport implementation in the form of - /// `NetworkSend` and `NetworkReceive` implementations. - /// - /// The utility runs the following tasks: - /// - The main Matter transport via the user-provided traits - /// - The PSM task handling changes to fabrics and ACLs as well as initial load of these from NVS - /// - The Matter responder (i.e. handling incoming exchanges) - /// - /// Unlike `run_with_netif`, this utility method does _not_ run the mDNS service, as the - /// user-provided transport might not be IP-based (i.e. BLE). - /// - /// It also has no facilities for monitoring the transport network state. - pub async fn run_with_transport<'d, S, R, H, P>( - &self, - send: S, - recv: R, - nvs: EspNvsPartition

, - dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, - handler: H, - ) -> Result<(), Error> - where - S: NetworkSend, - R: NetworkReceive, - H: AsyncHandler + AsyncMetadata, - P: NvsPartitionId, - { - // Reset the Matter transport buffers and all sessions first - self.matter().reset_transport()?; - - let mut psm = pin!(self.run_psm(nvs, nvs::Network::<0, NoopRawMutex>::Eth)); - let mut respond = pin!(self.run_responder(handler)); - let mut transport = pin!(self.run_transport(send, recv, dev_comm)); - - select3(&mut psm, &mut respond, &mut transport) - .coalesce() - .await?; - - Ok(()) - } - - async fn run_psm( - &self, - nvs: EspNvsPartition

, - network: nvs::Network<'_, W, R>, - ) -> Result<(), Error> - where - P: NvsPartitionId, - R: RawMutex, - { - if false { - let mut psm_buf = self - .psm_buffer - .get() - .await - .ok_or(ErrorCode::ResourceExhausted)?; - psm_buf.resize_default(4096).unwrap(); - - let nvs = EspNvs::new(nvs, "rs_matter", true)?; - - let mut psm = nvs::Psm::new(self.matter(), network, nvs, &mut psm_buf)?; - - psm.run().await - } else { - core::future::pending().await - } - } - - async fn run_responder(&self, handler: H) -> Result<(), Error> - where - H: AsyncHandler + AsyncMetadata, - { - let responder = - DefaultResponder::new(self.matter(), &self.buffers, &self.subscriptions, handler); - - info!( - "Responder memory: Responder={}B, Runner={}B", - core::mem::size_of_val(&responder), - core::mem::size_of_val(&responder.run::()) - ); - - // Run the responder with up to MAX_RESPONDERS handlers (i.e. MAX_RESPONDERS exchanges can be handled simultenously) - // Clients trying to open more exchanges than the ones currently running will get "I'm busy, please try again later" - responder - .run::() - .await?; - - Ok(()) - } - - async fn run_builtin_mdns(&self, netif_info: &NetifInfo) -> Result<(), Error> { - use rs_matter::mdns::{ - Host, MDNS_IPV4_BROADCAST_ADDR, MDNS_IPV6_BROADCAST_ADDR, MDNS_PORT, - }; - - let socket = async_io::Async::::bind(SocketAddr::V6( - SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, MDNS_PORT, 0, netif_info.interface), - ))?; - - join_multicast_v4(&socket, MDNS_IPV4_BROADCAST_ADDR, netif_info.ipv4)?; - join_multicast_v6(&socket, MDNS_IPV6_BROADCAST_ADDR, netif_info.interface)?; - - let mut hostname = heapless::String::<12>::new(); - write!( - hostname, - "{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}", - netif_info.mac[0], - netif_info.mac[1], - netif_info.mac[2], - netif_info.mac[3], - netif_info.mac[4], - netif_info.mac[5] - ) - .unwrap(); - - self.matter() - .run_builtin_mdns( - udp::Udp(&socket), - udp::Udp(&socket), - &Host { - id: 0, - hostname: &hostname, - ip: netif_info.ipv4.octets(), - ipv6: Some(netif_info.ipv6.octets()), - }, - Some(netif_info.interface), - ) - .await?; - - Ok(()) - } - - async fn run_transport( - &self, - send: S, - recv: R, - dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, - ) -> Result<(), Error> - where - S: NetworkSend, - R: NetworkReceive, - { - self.matter().run(send, recv, dev_comm).await?; - - Ok(()) - } -} - /// A utility function to initialize the `async-io` Reactor which is /// used for IP-based networks (UDP and TCP). /// @@ -396,29 +34,31 @@ where /// consumes > 10KB of stack space, so it has to be done with care. #[inline(never)] #[cold] -pub fn init_async_io() -> Result<(), Error> { +#[cfg(feature = "std")] +pub fn init_async_io() -> Result<(), esp_idf_svc::sys::EspError> { // We'll use `async-io` for networking, so ESP IDF VFS needs to be initialized esp_idf_svc::io::vfs::initialize_eventfd(3)?; - block_on(init_async_io_async()); + esp_idf_svc::hal::task::block_on(init_async_io_async()); Ok(()) } #[inline(never)] #[cold] +#[cfg(feature = "std")] async fn init_async_io_async() { #[cfg(not(feature = "async-io-mini"))] { // Force the `async-io` lazy initialization to trigger earlier rather than later, // as it consumes a lot of temp stack memory async_io::Timer::after(core::time::Duration::from_millis(100)).await; - info!("Async IO initialized; using `async-io`"); + ::log::info!("Async IO initialized; using `async-io`"); } #[cfg(feature = "async-io-mini")] { // Nothing to initialize for `async-io-mini` - info!("Async IO initialized; using `async-io-mini`"); + ::log::info!("Async IO initialized; using `async-io-mini`"); } } diff --git a/src/stack/eth.rs b/src/stack/eth.rs index c2b112b..bf9b698 100644 --- a/src/stack/eth.rs +++ b/src/stack/eth.rs @@ -1,101 +1,4 @@ -use core::borrow::Borrow; +use rs_matter_stack::{persist::KvBlobBuf, Eth, MatterStack}; -use esp_idf_svc::eventloop::EspSystemEventLoop; -use esp_idf_svc::nvs::{EspNvsPartition, NvsPartitionId}; - -use log::info; - -use rs_matter::data_model::objects::{AsyncHandler, AsyncMetadata, Endpoint, HandlerCompat}; -use rs_matter::data_model::root_endpoint; -use rs_matter::data_model::root_endpoint::{handler, OperNwType, RootEndpointHandler}; -use rs_matter::data_model::sdm::ethernet_nw_diagnostics::EthNwDiagCluster; -use rs_matter::data_model::sdm::nw_commissioning::EthNwCommCluster; -use rs_matter::data_model::sdm::{ethernet_nw_diagnostics, nw_commissioning}; -use rs_matter::pairing::DiscoveryCapabilities; -use rs_matter::CommissioningData; - -use crate::error::Error; -use crate::netif::NetifAccess; -use crate::{MatterStack, Network}; - -/// An implementation of the `Network` trait for Ethernet. -/// -/// Note that "Ethernet" - in the context of this crate - means -/// not just the Ethernet transport, but also any other IP-based transport -/// (like Wifi or Thread), where the Matter stack would not be concerned -/// with the management of the network transport (as in re-connecting to the -/// network on lost signal, managing network credentials and so on). -/// -/// The expectation is nevertheless that for production use-cases -/// the `Eth` network would really only be used for Ethernet. -pub struct Eth(()); - -impl Network for Eth { - const INIT: Self = Self(()); -} - -pub type EthMatterStack<'a> = MatterStack<'a, Eth>; - -/// A specialization of the `MatterStack` for Ethernet. -impl<'a> MatterStack<'a, Eth> { - /// Return a metadata for the root (Endpoint 0) of the Matter Node - /// configured for Ethernet network. - pub const fn root_metadata() -> Endpoint<'static> { - root_endpoint::endpoint(0, OperNwType::Ethernet) - } - - /// Return a handler for the root (Endpoint 0) of the Matter Node - /// configured for Ethernet network. - pub fn root_handler(&self) -> EthRootEndpointHandler<'_> { - handler( - 0, - self.matter(), - HandlerCompat(EthNwCommCluster::new(*self.matter().borrow())), - ethernet_nw_diagnostics::ID, - HandlerCompat(EthNwDiagCluster::new(*self.matter().borrow())), - true, - ) - } - - /// Resets the Matter instance to the factory defaults putting it into a - /// Commissionable mode. - pub fn reset(&self) -> Result<(), Error> { - // TODO: Reset fabrics and ACLs - - Ok(()) - } - - /// Run the Matter stack for Ethernet network. - pub async fn run<'d, H, P, E>( - &self, - sysloop: EspSystemEventLoop, - nvs: EspNvsPartition

, - eth: E, - dev_comm: CommissioningData, - handler: H, - ) -> Result<(), Error> - where - H: AsyncHandler + AsyncMetadata, - P: NvsPartitionId, - E: NetifAccess, - { - info!("Matter Stack memory: {}B", core::mem::size_of_val(self)); - - self.run_with_netif( - sysloop, - nvs, - eth, - Some((dev_comm, DiscoveryCapabilities::new(true, false, false))), - handler, - ) - .await - } -} - -/// The type of the handler for the root (Endpoint 0) of the Matter Node -/// when configured for Ethernet network. -pub type EthRootEndpointHandler<'a> = RootEndpointHandler< - 'a, - HandlerCompat, - HandlerCompat, ->; +pub type EspEthMatterStack<'a, E> = MatterStack<'a, EspEth>; +pub type EspEth = Eth>; diff --git a/src/stack/netif.rs b/src/stack/netif.rs new file mode 100644 index 0000000..dc879ad --- /dev/null +++ b/src/stack/netif.rs @@ -0,0 +1,135 @@ +#![cfg(all( + esp_idf_comp_esp_netif_enabled, + esp_idf_comp_esp_event_enabled, + feature = "std" +))] + +use core::net::{Ipv4Addr, Ipv6Addr}; +use core::pin::pin; + +use alloc::sync::Arc; +use rs_matter::error::{Error, ErrorCode}; + +use std::io; + +use edge_nal::UdpBind; +use edge_nal_std::{Stack, UdpSocket}; + +use embassy_futures::select::select; +use embassy_time::{Duration, Timer}; + +use esp_idf_svc::eventloop::EspSystemEventLoop; +use esp_idf_svc::hal::task::embassy_sync::EspRawMutex; +use esp_idf_svc::handle::RawHandle; +use esp_idf_svc::netif::{EspNetif, IpEvent}; +use esp_idf_svc::sys::{esp, esp_netif_get_ip6_linklocal, EspError, ESP_FAIL}; + +use rs_matter::utils::notification::Notification; +use rs_matter_stack::netif::{Netif, NetifConf}; + +const TIMEOUT_PERIOD_SECS: u8 = 5; + +pub struct EspMatterNetif<'a> { + netif: &'a EspNetif, + sysloop: EspSystemEventLoop, +} + +impl<'a> EspMatterNetif<'a> { + pub const fn new(netif: &'a EspNetif, sysloop: EspSystemEventLoop) -> Self { + Self { netif, sysloop } + } + + fn get_conf(&self) -> Result { + Self::get_netif_conf(self.netif) + } + + async fn wait_conf_change(&self) -> Result<(), EspError> { + Self::wait_any_conf_change(&self.sysloop).await + } + + pub(crate) fn get_netif_conf(netif: &EspNetif) -> Result { + let ip_info = netif.get_ip_info()?; + + let ipv4: Ipv4Addr = ip_info.ip.octets().into(); + if ipv4.is_unspecified() { + return Err(EspError::from_infallible::()); + } + + let mut ipv6: esp_idf_svc::sys::esp_ip6_addr_t = Default::default(); + + esp!(unsafe { esp_netif_get_ip6_linklocal(netif.handle() as _, &mut ipv6) })?; + + let ipv6: Ipv6Addr = [ + ipv6.addr[0].to_le_bytes()[0], + ipv6.addr[0].to_le_bytes()[1], + ipv6.addr[0].to_le_bytes()[2], + ipv6.addr[0].to_le_bytes()[3], + ipv6.addr[1].to_le_bytes()[0], + ipv6.addr[1].to_le_bytes()[1], + ipv6.addr[1].to_le_bytes()[2], + ipv6.addr[1].to_le_bytes()[3], + ipv6.addr[2].to_le_bytes()[0], + ipv6.addr[2].to_le_bytes()[1], + ipv6.addr[2].to_le_bytes()[2], + ipv6.addr[2].to_le_bytes()[3], + ipv6.addr[3].to_le_bytes()[0], + ipv6.addr[3].to_le_bytes()[1], + ipv6.addr[3].to_le_bytes()[2], + ipv6.addr[3].to_le_bytes()[3], + ] + .into(); + + let interface = netif.get_index(); + + let mac = netif.get_mac()?; + + Ok(NetifConf { + ipv4, + ipv6, + interface, + mac, + }) + } + + pub(crate) async fn wait_any_conf_change(sysloop: &EspSystemEventLoop) -> Result<(), EspError> { + let notification = Arc::new(Notification::::new()); + + let _subscription = { + let notification = notification.clone(); + + sysloop.subscribe::(move |_| { + notification.notify(); + }) + }?; + + let mut events = pin!(notification.wait()); + let mut timer = pin!(Timer::after(Duration::from_secs(TIMEOUT_PERIOD_SECS as _))); + + select(&mut events, &mut timer).await; + + Ok(()) + } +} + +impl<'a> Netif for EspMatterNetif<'a> { + async fn get_conf(&self) -> Result, Error> { + Ok(EspMatterNetif::get_conf(self).ok()) + } + + async fn wait_conf_change(&self) -> Result<(), Error> { + EspMatterNetif::wait_conf_change(self) + .await + .map_err(|_| ErrorCode::NoNetworkInterface)?; // TODO + + Ok(()) + } +} + +impl<'a> UdpBind for EspMatterNetif<'a> { + type Error = io::Error; + type Socket<'b> = UdpSocket where Self: 'b; + + async fn bind(&self, local: core::net::SocketAddr) -> Result, Self::Error> { + Stack::new().bind(local).await + } +} diff --git a/src/stack/persist.rs b/src/stack/persist.rs new file mode 100644 index 0000000..cf1a686 --- /dev/null +++ b/src/stack/persist.rs @@ -0,0 +1,91 @@ +#![cfg(esp_idf_comp_nvs_flash_enabled)] + +use esp_idf_svc::nvs::{EspNvs, EspNvsPartition, NvsPartitionId}; +use esp_idf_svc::sys::EspError; + +use log::info; + +use rs_matter::error::{Error, ErrorCode}; + +use rs_matter_stack::persist::{KvBlobStore, KvPersist}; + +pub type EspPersist<'a, T, const N: usize, M> = KvPersist<'a, EspKvBlobStore, N, M>; + +/// A `KvBlobStore`` implementation that uses the ESP IDF NVS API +/// to store and load the BLOBs. +/// +/// NOTE: Not async (yet) +pub struct EspKvBlobStore(EspNvs) +where + T: NvsPartitionId; + +impl EspKvBlobStore +where + T: NvsPartitionId, +{ + /// Create a new PSM instance that would persist in namespace `esp-idf-matter`. + pub fn new_default(nvs: EspNvsPartition) -> Result { + Self::new(nvs, "esp-idf-matter") + } + + /// Create a new PSM instance. + pub fn new(nvs: EspNvsPartition, namespace: &str) -> Result { + Ok(Self(EspNvs::new(nvs, namespace, true)?)) + } + + fn load_blob<'b>(&self, key: &str, buf: &'b mut [u8]) -> Result, EspError> { + // TODO: Not really async + + let data = self.0.get_blob(key, buf)?; + info!( + "Blob {key}: loaded {:?} bytes {data:?}", + data.map(|data| data.len()) + ); + + Ok(data) + } + + fn store_blob(&mut self, key: &str, data: &[u8]) -> Result<(), EspError> { + // TODO: Not really async + + self.0.set_blob(key, data)?; + + info!("Blob {key}: stored {} bytes {data:?}", data.len()); + + Ok(()) + } + + fn remove_blob(&mut self, key: &str) -> Result<(), EspError> { + // TODO: Not really async + + self.0.remove(key)?; + + info!("Blob {key}: removed"); + + Ok(()) + } +} + +impl KvBlobStore for EspKvBlobStore +where + T: NvsPartitionId, +{ + async fn load<'a>(&mut self, key: &str, buf: &'a mut [u8]) -> Result, Error> { + Ok(self + .load_blob(key, buf) + .map_err(|_| ErrorCode::StdIoError)?) // TODO: We need a dedicated PersistError code here + } + + async fn store(&mut self, key: &str, value: &[u8]) -> Result<(), Error> { + self.store_blob(key, value) + .map_err(|_| ErrorCode::StdIoError)?; + + Ok(()) + } + + async fn remove(&mut self, key: &str) -> Result<(), Error> { + self.remove_blob(key).map_err(|_| ErrorCode::StdIoError)?; + + Ok(()) + } +} diff --git a/src/stack/wifible.rs b/src/stack/wifible.rs index 84bcfd6..3799b2e 100644 --- a/src/stack/wifible.rs +++ b/src/stack/wifible.rs @@ -5,268 +5,273 @@ esp_idf_comp_esp_event_enabled, not(esp_idf_btdm_ctrl_mode_br_edr_only), esp_idf_bt_enabled, - esp_idf_bt_bluedroid_enabled + esp_idf_bt_bluedroid_enabled, + feature = "std" ))] -use core::borrow::Borrow; -use core::cell::RefCell; -use core::pin::pin; +use core::net::SocketAddr; + +use std::io; use alloc::boxed::Box; -use embassy_futures::select::select; +use edge_nal::UdpBind; +use edge_nal_std::{Stack, UdpSocket}; + use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::mutex::Mutex; -use esp_idf_svc::bt::{Ble, BleEnabled, BtDriver}; +use embedded_svc::wifi::asynch::Wifi; + +use enumset::EnumSet; + +use esp_idf_svc::bt::{Ble, BtDriver}; use esp_idf_svc::eventloop::EspSystemEventLoop; -use esp_idf_svc::hal::modem::Modem; -use esp_idf_svc::hal::peripheral::Peripheral; +use esp_idf_svc::hal::peripheral::{Peripheral, PeripheralRef}; use esp_idf_svc::hal::task::embassy_sync::EspRawMutex; +use esp_idf_svc::hal::{into_ref, modem}; +use esp_idf_svc::handle::RawHandle; use esp_idf_svc::nvs::EspDefaultNvsPartition; +use esp_idf_svc::sys::{esp, esp_netif_create_ip6_linklocal, EspError}; use esp_idf_svc::timer::EspTaskTimerService; -use esp_idf_svc::wifi::{AsyncWifi, EspWifi}; - -use log::info; - -use rs_matter::data_model::objects::{AsyncHandler, AsyncMetadata, Endpoint, HandlerCompat}; -use rs_matter::data_model::root_endpoint; -use rs_matter::data_model::root_endpoint::{handler, OperNwType, RootEndpointHandler}; -use rs_matter::data_model::sdm::failsafe::FailSafe; -use rs_matter::data_model::sdm::wifi_nw_diagnostics; -use rs_matter::data_model::sdm::wifi_nw_diagnostics::{ - WiFiSecurity, WiFiVersion, WifiNwDiagCluster, WifiNwDiagData, -}; -use rs_matter::pairing::DiscoveryCapabilities; -use rs_matter::transport::network::btp::{Btp, BtpContext}; -use rs_matter::utils::select::Coalesce; -use rs_matter::CommissioningData; +use esp_idf_svc::wifi::{AccessPointInfo, AsyncWifi, Capability, Configuration, EspWifi}; + +use rs_matter::error::{Error, ErrorCode}; +use rs_matter_stack::modem::{Modem, WifiDevice}; +use rs_matter_stack::netif::{Netif, NetifConf}; +use rs_matter_stack::network::{Embedding, Network}; +use rs_matter_stack::persist::KvBlobBuf; +use rs_matter_stack::{MatterStack, WifiBle}; use crate::ble::{BtpGattContext, BtpGattPeripheral}; -use crate::error::Error; -use crate::wifi::mgmt::WifiManager; -use crate::wifi::{comm, WifiContext}; -use crate::{MatterStack, Network}; -const MAX_WIFI_NETWORKS: usize = 2; -const GATTS_APP_ID: u16 = 0; +use super::netif::EspMatterNetif; -/// An implementation of the `Network` trait for a Matter stack running over -/// BLE during commissioning, and then over WiFi when operating. +pub type EspWifiBleMatterStack<'a, E> = MatterStack<'a, EspWifiBle>; +pub type EspWifiBle = WifiBle>>; + +/// An embedding of the ESP IDF Bluedroid Gatt peripheral context for the `WifiBle` network type from `rs-matter-stack`. +/// Allows the memory of this context to be statically allocated and cost-initialized. /// -/// The supported commissioning is of the non-concurrent type (as per the Matter Core spec), -/// where the device - at any point in time - either runs Bluetooth or Wifi, but not both. -/// This is done to save memory and to avoid the usage of the ESP IDF Co-exist driver. +/// Usage: +/// ```no_run +/// MatterStack>>::new(); +/// ``` /// -/// The BLE implementation used is the ESP IDF Bluedroid stack (not NimBLE). -pub struct WifiBle { - btp_context: BtpContext, +/// ... where `E` can be a next-level, user-supplied embedding or just `()` if the user does not need to embed anything. +pub struct EspGatt { btp_gatt_context: BtpGattContext, - wifi_context: WifiContext, + embedding: E, } -impl WifiBle { +impl EspGatt +where + E: Embedding, +{ + #[allow(clippy::large_stack_frames)] + #[inline(always)] const fn new() -> Self { Self { - btp_context: BtpContext::new(), btp_gatt_context: BtpGattContext::new(), - wifi_context: WifiContext::new(), + embedding: E::INIT, } } + + pub fn context(&self) -> &BtpGattContext { + &self.btp_gatt_context + } + + pub fn embedding(&self) -> &E { + &self.embedding + } } -impl Network for WifiBle { +impl Embedding for EspGatt +where + E: Embedding, +{ const INIT: Self = Self::new(); } -pub type WifiBleMatterStack<'a> = MatterStack<'a, WifiBle>; +const GATTS_APP_ID: u16 = 0; + +pub struct EspModem<'a, 'd> { + context: &'a BtpGattContext, + modem: PeripheralRef<'d, modem::Modem>, + sysloop: EspSystemEventLoop, + timers: EspTaskTimerService, + nvs: EspDefaultNvsPartition, +} -impl<'a> MatterStack<'a, WifiBle> { - /// Return a metadata for the root (Endpoint 0) of the Matter Node - /// configured for BLE+Wifi network. - pub const fn root_metadata() -> Endpoint<'static> { - root_endpoint::endpoint(0, OperNwType::Wifi) +impl<'a, 'd> EspModem<'a, 'd> { + pub fn new( + modem: impl Peripheral

+ 'd, + sysloop: EspSystemEventLoop, + timers: EspTaskTimerService, + nvs: EspDefaultNvsPartition, + stack: &'a EspWifiBleMatterStack, + ) -> Self + where + E: Embedding + 'static, + { + Self::wrap( + modem, + sysloop, + timers, + nvs, + stack.network().embedding().embedding().context(), + ) } - /// Return a handler for the root (Endpoint 0) of the Matter Node - /// configured for BLE+Wifi network. - pub fn root_handler(&self) -> WifiBleRootEndpointHandler<'_> { - handler( - 0, - self.matter(), - comm::WifiNwCommCluster::new(*self.matter().borrow(), &self.network.wifi_context), - wifi_nw_diagnostics::ID, - HandlerCompat(WifiNwDiagCluster::new( - *self.matter().borrow(), - // TODO: Update with actual information - WifiNwDiagData { - bssid: [0; 6], - security_type: WiFiSecurity::Unspecified, - wifi_version: WiFiVersion::B, - channel_number: 20, - rssi: 0, - }, + pub fn wrap( + modem: impl Peripheral

+ 'd, + sysloop: EspSystemEventLoop, + timers: EspTaskTimerService, + nvs: EspDefaultNvsPartition, + context: &'a BtpGattContext, + ) -> Self { + into_ref!(modem); + + Self { + context, + modem, + sysloop, + timers, + nvs, + } + } +} + +impl<'a, 'd> Modem for EspModem<'a, 'd> { + type BleDevice<'t> = BtpGattPeripheral<'t, 't, Ble> where Self: 't; + + type WifiDevice<'t> = EspWifiDevice<'t> where Self: 't; + + async fn ble(&mut self) -> Self::BleDevice<'_> { + let bt = BtDriver::new(&mut self.modem, Some(self.nvs.clone())).unwrap(); + + let peripheral = BtpGattPeripheral::new(GATTS_APP_ID, bt, self.context).unwrap(); + + peripheral + } + + async fn wifi(&mut self) -> Self::WifiDevice<'_> { + EspWifiDevice { + sysloop: self.sysloop.clone(), + wifi: Mutex::new(Box::new( + AsyncWifi::wrap( + EspWifi::new( + &mut self.modem, + self.sysloop.clone(), + Some(self.nvs.clone()), + ) + .unwrap(), + self.sysloop.clone(), + self.timers.clone(), + ) + .unwrap(), )), - false, - ) + } } +} - /// Resets the Matter instance to the factory defaults putting it into a - /// Commissionable mode. - pub fn reset(&self) -> Result<(), Error> { - // TODO: Reset fabrics and ACLs - self.network.btp_gatt_context.reset()?; - // TODO self.network.btp_context.reset(); - self.network.wifi_context.reset(); +pub struct EspWifiDevice<'d> { + sysloop: EspSystemEventLoop, + wifi: Mutex>>>, +} - Ok(()) +impl<'d> WifiDevice for EspWifiDevice<'d> { + type L2<'t> = EspL2<'t, 'd> where Self: 't; + + type L3<'t> = EspL3<'t, 'd> where Self: 't; + + async fn split(&mut self) -> (Self::L2<'_>, Self::L3<'_>) { + (EspL2(self), EspL3(self)) } +} + +pub struct EspL2<'a, 'd>(&'a EspWifiDevice<'d>); - /// Return information whether the Matter instance is already commissioned. - /// - /// User might need to reach out to this method only when it needs finer-grained control - /// and utilizes the `commission` and `operate` methods rather than the all-in-one `run` loop. - pub async fn is_commissioned(&self, _nvs: EspDefaultNvsPartition) -> Result { - // TODO - Ok(false) +impl<'a, 'd> Wifi for EspL2<'a, 'd> { + type Error = EspError; + + async fn get_capabilities(&self) -> Result, Self::Error> { + self.0.wifi.lock().await.get_capabilities() } - /// A utility method to run the Matter stack in Operating mode (as per the Matter Core spec) over Wifi. - /// - /// This method assumes that the Matter instance is already commissioned and therefore - /// does not take a `CommissioningData` parameter. - /// - /// It is just a composition of the `MatterStack::run_with_netif` method, and the `WifiManager::run` method, - /// where the former takes care of running the main Matter loop, while the latter takes care of ensuring - /// that the Matter instance stays connected to the Wifi network. - pub async fn operate<'d, H>( - &self, - sysloop: EspSystemEventLoop, - timer_service: EspTaskTimerService, - nvs: EspDefaultNvsPartition, - wifi: &mut EspWifi<'d>, - handler: H, - ) -> Result<(), Error> - where - H: AsyncHandler + AsyncMetadata, - { - info!("Running Matter in operating mode (Wifi)"); + async fn get_configuration(&self) -> Result { + self.0.wifi.lock().await.get_configuration() + } + + async fn set_configuration(&mut self, conf: &Configuration) -> Result<(), Self::Error> { + self.0.wifi.lock().await.set_configuration(conf) + } + + async fn start(&mut self) -> Result<(), Self::Error> { + self.0.wifi.lock().await.start().await + } + + async fn stop(&mut self) -> Result<(), Self::Error> { + self.0.wifi.lock().await.stop().await + } - let wifi = - Mutex::::new(AsyncWifi::wrap(wifi, sysloop.clone(), timer_service)?); + async fn connect(&mut self) -> Result<(), Self::Error> { + let mut wifi = self.0.wifi.lock().await; - let mgr = WifiManager::new(&wifi, &self.network.wifi_context, sysloop.clone()); + wifi.connect().await?; - let mut main = pin!(self.run_with_netif(sysloop, nvs, &wifi, None, handler)); - let mut wifi = pin!(mgr.run()); + esp!(unsafe { esp_netif_create_ip6_linklocal(wifi.wifi().sta_netif().handle() as _) })?; - select(&mut wifi, &mut main).coalesce().await + Ok(()) } - /// A utility method to run the Matter stack in Commissioning mode (as per the Matter Core spec) over BLE. - /// Essentially an instantiation of `MatterStack::run_with_transport` with the BLE transport. - /// - /// Note: make sure to call `MatterStack::reset` before calling this method, as all fabrics and ACLs, as well as all - /// transport state should be reset. - pub async fn commission<'d, H, B>( - &'static self, - nvs: EspDefaultNvsPartition, - bt: &BtDriver<'d, B>, - dev_comm: CommissioningData, - handler: H, - ) -> Result<(), Error> - where - H: AsyncHandler + AsyncMetadata, - B: BleEnabled, - { - info!("Running Matter in commissioning mode (BLE)"); + async fn disconnect(&mut self) -> Result<(), Self::Error> { + self.0.wifi.lock().await.disconnect().await + } - let peripheral = BtpGattPeripheral::new(GATTS_APP_ID, bt, &self.network.btp_gatt_context)?; + async fn is_started(&self) -> Result { + self.0.wifi.lock().await.is_started() + } - let btp = Btp::new(peripheral, &self.network.btp_context); + async fn is_connected(&self) -> Result { + self.0.wifi.lock().await.is_connected() + } - let mut ble = pin!(async { - btp.run("BT", self.matter().dev_det(), &dev_comm) - .await - .map_err(Into::into) - }); + async fn scan_n( + &mut self, + ) -> Result<(heapless::Vec, usize), Self::Error> { + self.0.wifi.lock().await.scan_n().await + } - let mut main = pin!(self.run_with_transport( - &btp, - &btp, - nvs, - Some(( - dev_comm.clone(), - DiscoveryCapabilities::new(false, true, false) - )), - &handler - )); + async fn scan(&mut self) -> Result, Self::Error> { + self.0.wifi.lock().await.scan().await + } +} + +pub struct EspL3<'a, 'd>(&'a EspWifiDevice<'d>); - select(&mut ble, &mut main).coalesce().await +impl<'a, 'd> Netif for EspL3<'a, 'd> { + async fn get_conf(&self) -> Result, Error> { + let wifi = self.0.wifi.lock().await; + + Ok(EspMatterNetif::get_netif_conf(wifi.wifi().sta_netif()).ok()) } - /// An all-in-one "run everything" method that automatically - /// places the Matter stack either in Commissioning or in Operating mode, depending - /// on the state of the device as persisted in the NVS storage. - pub async fn run<'d, H>( - &'static self, - sysloop: EspSystemEventLoop, - timer_service: EspTaskTimerService, - nvs: EspDefaultNvsPartition, - mut modem: impl Peripheral

+ 'd, - dev_comm: CommissioningData, - handler: H, - ) -> Result<(), Error> - where - H: AsyncHandler + AsyncMetadata, - { - info!("Matter Stack memory: {}B", core::mem::size_of_val(self)); - - loop { - if !self.is_commissioned(nvs.clone()).await? { - // Reset to factory defaults everything, as we'll do commissioning all over - self.reset()?; - - let bt = BtDriver::::new(&mut modem, Some(nvs.clone()))?; - - info!("BLE driver initialized"); - - let mut main = pin!(self.commission(nvs.clone(), &bt, dev_comm.clone(), &handler)); - let mut wait_network_connect = - pin!(self.network.wifi_context.wait_network_connect()); - - select(&mut main, &mut wait_network_connect) - .coalesce() - .await?; - } - - // As per spec, we need to indicate the expectation of a re-arm with a CASE session - // even if the current session is a PASE one (this is specific for non-concurrent commissiioning flows) - let failsafe: &RefCell = self.matter().borrow(); - failsafe.borrow_mut().expect_case_rearm()?; - - let mut wifi = Box::new(EspWifi::new( - &mut modem, - sysloop.clone(), - Some(nvs.clone()), - )?); - - info!("Wifi driver initialized"); - - self.operate( - sysloop.clone(), - timer_service.clone(), - nvs.clone(), - &mut wifi, - &handler, - ) - .await?; - } + async fn wait_conf_change(&self) -> Result<(), Error> { + EspMatterNetif::wait_any_conf_change(&self.0.sysloop) + .await + .map_err(|_| ErrorCode::NoNetworkInterface)?; // TODO + + Ok(()) } } -pub type WifiBleRootEndpointHandler<'a> = RootEndpointHandler< - 'a, - comm::WifiNwCommCluster<'a, MAX_WIFI_NETWORKS, NoopRawMutex>, - HandlerCompat, ->; +impl<'a, 'd> UdpBind for EspL3<'a, 'd> { + type Error = io::Error; + + type Socket<'t> = UdpSocket where Self: 't; + + async fn bind(&self, local: SocketAddr) -> Result, Self::Error> { + Stack::new().bind(local).await + } +} diff --git a/src/udp.rs b/src/udp.rs deleted file mode 100644 index f10141e..0000000 --- a/src/udp.rs +++ /dev/null @@ -1,40 +0,0 @@ -#![cfg(all(feature = "std", any(feature = "async-io", feature = "async-io-mini")))] - -//! UDP transport implementation for async-io and async-io-mini - -use std::net::UdpSocket; - -#[cfg(feature = "async-io-mini")] -use async_io_mini as async_io; - -use rs_matter::error::{Error, ErrorCode}; -use rs_matter::transport::network::{Address, NetworkReceive, NetworkSend}; - -pub struct Udp<'a>(pub &'a async_io::Async); - -impl NetworkSend for Udp<'_> { - async fn send_to(&mut self, data: &[u8], addr: Address) -> Result<(), Error> { - async_io::Async::::send_to( - self.0, - data, - addr.udp().ok_or(ErrorCode::NoNetworkInterface)?, - ) - .await?; - - Ok(()) - } -} - -impl NetworkReceive for Udp<'_> { - async fn wait_available(&mut self) -> Result<(), Error> { - async_io::Async::::readable(self.0).await?; - - Ok(()) - } - - async fn recv_from(&mut self, buffer: &mut [u8]) -> Result<(usize, Address), Error> { - let (len, addr) = async_io::Async::::recv_from(self.0, buffer).await?; - - Ok((len, Address::Udp(addr))) - } -} diff --git a/src/wifi.rs b/src/wifi.rs deleted file mode 100644 index 05a16ac..0000000 --- a/src/wifi.rs +++ /dev/null @@ -1,192 +0,0 @@ -use core::cell::RefCell; - -use embassy_sync::blocking_mutex::{self, raw::RawMutex}; -use embassy_time::{Duration, Timer}; - -use log::{info, warn}; - -use rs_matter::data_model::sdm::nw_commissioning::NetworkCommissioningStatus; -use rs_matter::error::{Error, ErrorCode}; -use rs_matter::tlv::{self, FromTLV, TLVList, TLVWriter, TagType, ToTLV}; -use rs_matter::utils::notification::Notification; -use rs_matter::utils::writebuf::WriteBuf; - -pub mod comm; -pub mod mgmt; - -#[derive(Debug, Clone, ToTLV, FromTLV)] -struct WifiCredentials { - ssid: heapless::String<32>, - password: heapless::String<64>, -} - -struct WifiStatus { - ssid: heapless::String<32>, - status: NetworkCommissioningStatus, - value: i32, -} - -struct WifiState { - networks: heapless::Vec, - connected_once: bool, - connect_requested: Option>, - status: Option, - changed: bool, -} - -impl WifiState { - pub(crate) fn get_next_network(&mut self, last_ssid: Option<&str>) -> Option { - // Return the requested network with priority - if let Some(ssid) = self.connect_requested.take() { - let creds = self.networks.iter().find(|creds| creds.ssid == ssid); - - if let Some(creds) = creds { - info!("Trying with requested network first - SSID: {}", creds.ssid); - - return Some(creds.clone()); - } - } - - if let Some(last_ssid) = last_ssid { - info!("Looking for network after the one with SSID: {}", last_ssid); - - // Return the network positioned after the last one used - - let mut networks = self.networks.iter(); - - for network in &mut networks { - if network.ssid.as_str() == last_ssid { - break; - } - } - - let creds = networks.next(); - if let Some(creds) = creds { - info!("Trying with next network - SSID: {}", creds.ssid); - - return Some(creds.clone()); - } - } - - // Wrap over - info!("Wrapping over"); - - self.networks.first().cloned() - } - - fn reset(&mut self) { - self.networks.clear(); - self.connected_once = false; - self.connect_requested = None; - self.status = None; - self.changed = false; - } - - fn load(&mut self, data: &[u8]) -> Result<(), Error> { - let root = TLVList::new(data).iter().next().ok_or(ErrorCode::Invalid)?; - - tlv::from_tlv(&mut self.networks, &root)?; - - self.changed = false; - - Ok(()) - } - - fn store<'m>(&mut self, buf: &'m mut [u8]) -> Result, Error> { - if !self.changed { - return Ok(None); - } - - let mut wb = WriteBuf::new(buf); - let mut tw = TLVWriter::new(&mut wb); - - self.networks - .as_slice() - .to_tlv(&mut tw, TagType::Anonymous)?; - - self.changed = false; - - let len = tw.get_tail(); - - Ok(Some(&buf[..len])) - } -} - -/// The `'static` state of the Wifi module. -/// Isolated as a separate struct to allow for `const fn` construction -/// and static allocation. -pub struct WifiContext -where - M: RawMutex, -{ - state: blocking_mutex::Mutex>>, - network_connect_requested: Notification, -} - -impl WifiContext -where - M: RawMutex, -{ - /// Create a new instance. - pub const fn new() -> Self { - Self { - state: blocking_mutex::Mutex::new(RefCell::new(WifiState { - networks: heapless::Vec::new(), - connected_once: false, - connect_requested: None, - status: None, - changed: false, - })), - network_connect_requested: Notification::new(), - } - } - - /// Reset the state. - pub fn reset(&self) { - self.state.lock(|state| state.borrow_mut().reset()); - } - - /// Load the state from a byte slice. - pub fn load(&self, data: &[u8]) -> Result<(), Error> { - self.state.lock(|state| state.borrow_mut().load(data)) - } - - /// Store the state into a byte slice. - pub fn store<'m>(&self, buf: &'m mut [u8]) -> Result, Error> { - self.state.lock(|state| state.borrow_mut().store(buf)) - } - - /// Wait until signalled by the Matter stack that a network connect request is issued during commissioning. - /// - /// Typically, this is a signal that the BLE/BTP transport should be teared down and - /// the Wifi transport should be brought up. - pub async fn wait_network_connect(&self) -> Result<(), crate::error::Error> { - loop { - if self - .state - .lock(|state| state.borrow().connect_requested.is_some()) - { - break; - } - - self.network_connect_requested.wait().await; - } - - warn!( - "Giving BLE/BTP extra 4 seconds for any outstanding messages before switching to Wifi" - ); - - Timer::after(Duration::from_secs(4)).await; - - Ok(()) - } -} - -impl Default for WifiContext -where - M: RawMutex, -{ - fn default() -> Self { - Self::new() - } -} diff --git a/src/wifi/comm.rs b/src/wifi/comm.rs deleted file mode 100644 index 0c15970..0000000 --- a/src/wifi/comm.rs +++ /dev/null @@ -1,447 +0,0 @@ -use embassy_sync::blocking_mutex::raw::RawMutex; - -use log::{error, info, warn}; - -use rs_matter::data_model::objects::{ - AsyncHandler, AttrDataEncoder, AttrDataWriter, AttrDetails, AttrType, CmdDataEncoder, - CmdDetails, Dataver, -}; -use rs_matter::data_model::sdm::nw_commissioning::{ - AddWifiNetworkRequest, Attributes, Commands, ConnectNetworkRequest, ConnectNetworkResponse, - NetworkCommissioningStatus, NetworkConfigResponse, NwInfo, RemoveNetworkRequest, - ReorderNetworkRequest, ResponseCommands, ScanNetworksRequest, WIFI_CLUSTER, -}; -use rs_matter::error::{Error, ErrorCode}; -use rs_matter::interaction_model::core::IMStatusCode; -use rs_matter::interaction_model::messages::ib::Status; -use rs_matter::tlv::{FromTLV, OctetStr, TLVElement, TagType, ToTLV}; -use rs_matter::transport::exchange::Exchange; -use rs_matter::utils::rand::Rand; - -use super::{WifiContext, WifiCredentials}; - -/// A cluster implementing the Matter Network Commissioning Cluster -/// for managing WiFi networks. -/// -/// `N` is the maximum number of networks that can be stored. -pub struct WifiNwCommCluster<'a, const N: usize, M> -where - M: RawMutex, -{ - data_ver: Dataver, - networks: &'a WifiContext, -} - -impl<'a, const N: usize, M> WifiNwCommCluster<'a, N, M> -where - M: RawMutex, -{ - /// Create a new instance. - pub fn new(rand: Rand, networks: &'a WifiContext) -> Self { - Self { - data_ver: Dataver::new(rand), - networks, - } - } - - /// Read an attribute. - pub fn read( - &self, - attr: &AttrDetails<'_>, - encoder: AttrDataEncoder<'_, '_, '_>, - ) -> Result<(), Error> { - if let Some(mut writer) = encoder.with_dataver(self.data_ver.get())? { - if attr.is_system() { - WIFI_CLUSTER.read(attr.attr_id, writer) - } else { - match attr.attr_id.try_into()? { - Attributes::MaxNetworks => AttrType::::new().encode(writer, N as u8), - Attributes::Networks => { - writer.start_array(AttrDataWriter::TAG)?; - - self.networks.state.lock(|state| { - let state = state.borrow(); - - for network in &state.networks { - let nw_info = NwInfo { - network_id: OctetStr(network.ssid.as_str().as_bytes()), - connected: state - .status - .as_ref() - .map(|status| { - *status.ssid == network.ssid - && matches!( - status.status, - NetworkCommissioningStatus::Success - ) - }) - .unwrap_or(false), - }; - - nw_info.to_tlv(&mut writer, TagType::Anonymous)?; - } - - Ok::<_, Error>(()) - })?; - - writer.end_container()?; - writer.complete() - } - Attributes::ScanMaxTimeSecs => AttrType::new().encode(writer, 30_u8), - Attributes::ConnectMaxTimeSecs => AttrType::new().encode(writer, 60_u8), - Attributes::InterfaceEnabled => AttrType::new().encode(writer, true), - Attributes::LastNetworkingStatus => self.networks.state.lock(|state| { - AttrType::new().encode( - writer, - state.borrow().status.as_ref().map(|o| o.status as u8), - ) - }), - Attributes::LastNetworkID => self.networks.state.lock(|state| { - AttrType::new().encode( - writer, - state - .borrow() - .status - .as_ref() - .map(|o| OctetStr(o.ssid.as_str().as_bytes())), - ) - }), - Attributes::LastConnectErrorValue => self.networks.state.lock(|state| { - AttrType::new() - .encode(writer, state.borrow().status.as_ref().map(|o| o.value)) - }), - } - } - } else { - Ok(()) - } - } - - /// Invoke a command. - pub async fn invoke( - &self, - exchange: &Exchange<'_>, - cmd: &CmdDetails<'_>, - data: &TLVElement<'_>, - encoder: CmdDataEncoder<'_, '_, '_>, - ) -> Result<(), Error> { - match cmd.cmd_id.try_into()? { - Commands::ScanNetworks => { - info!("ScanNetworks"); - self.scan_networks(exchange, &ScanNetworksRequest::from_tlv(data)?, encoder)?; - } - Commands::AddOrUpdateWifiNetwork => { - info!("AddOrUpdateWifiNetwork"); - self.add_network(exchange, &AddWifiNetworkRequest::from_tlv(data)?, encoder)?; - } - Commands::RemoveNetwork => { - info!("RemoveNetwork"); - self.remove_network(exchange, &RemoveNetworkRequest::from_tlv(data)?, encoder)?; - } - Commands::ConnectNetwork => { - info!("ConnectNetwork"); - self.connect_network(exchange, &ConnectNetworkRequest::from_tlv(data)?, encoder) - .await?; - } - Commands::ReorderNetwork => { - info!("ReorderNetwork"); - self.reorder_network(exchange, &ReorderNetworkRequest::from_tlv(data)?, encoder)?; - } - other => { - error!("{other:?} (not supported)"); - Err(ErrorCode::CommandNotFound)? - } - } - - self.data_ver.changed(); - - Ok(()) - } - - fn scan_networks( - &self, - _exchange: &Exchange<'_>, - _req: &ScanNetworksRequest<'_>, - encoder: CmdDataEncoder<'_, '_, '_>, - ) -> Result<(), Error> { - let writer = encoder.with_command(ResponseCommands::ScanNetworksResponse as _)?; - - warn!("Scan network not supported"); - - writer.set(Status::new(IMStatusCode::Busy, 0))?; - - Ok(()) - } - - fn add_network( - &self, - _exchange: &Exchange<'_>, - req: &AddWifiNetworkRequest<'_>, - encoder: CmdDataEncoder<'_, '_, '_>, - ) -> Result<(), Error> { - // TODO: Check failsafe status - - self.networks.state.lock(|state| { - let mut state = state.borrow_mut(); - - let index = state - .networks - .iter() - .position(|conf| conf.ssid.as_str().as_bytes() == req.ssid.0); - - let writer = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?; - - if let Some(index) = index { - // Update - state.networks[index].ssid = core::str::from_utf8(req.ssid.0) - .unwrap() - .try_into() - .unwrap(); - state.networks[index].password = core::str::from_utf8(req.credentials.0) - .unwrap() - .try_into() - .unwrap(); - - state.changed = true; - - info!("Updated network with SSID {}", state.networks[index].ssid); - - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::Success, - debug_text: None, - network_index: Some(index as _), - })?; - } else { - // Add - let network = WifiCredentials { - // TODO - ssid: core::str::from_utf8(req.ssid.0) - .unwrap() - .try_into() - .unwrap(), - password: core::str::from_utf8(req.credentials.0) - .unwrap() - .try_into() - .unwrap(), - }; - - match state.networks.push(network) { - Ok(_) => { - state.changed = true; - - info!( - "Added network with SSID {}", - state.networks.last().unwrap().ssid - ); - - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::Success, - debug_text: None, - network_index: Some((state.networks.len() - 1) as _), - })?; - } - Err(network) => { - warn!("Adding network with SSID {} failed: too many", network.ssid); - - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::BoundsExceeded, - debug_text: None, - network_index: None, - })?; - } - } - } - - Ok(()) - }) - } - - fn remove_network( - &self, - _exchange: &Exchange<'_>, - req: &RemoveNetworkRequest<'_>, - encoder: CmdDataEncoder<'_, '_, '_>, - ) -> Result<(), Error> { - // TODO: Check failsafe status - - self.networks.state.lock(|state| { - let mut state = state.borrow_mut(); - - let index = state - .networks - .iter() - .position(|conf| conf.ssid.as_str().as_bytes() == req.network_id.0); - - let writer = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?; - - if let Some(index) = index { - // Found - let network = state.networks.remove(index); - state.changed = true; - - info!("Removed network with SSID {}", network.ssid); - - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::Success, - debug_text: None, - network_index: Some(index as _), - })?; - } else { - warn!( - "Network with SSID {} not found", - core::str::from_utf8(req.network_id.0).unwrap() - ); - - // Not found - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::NetworkIdNotFound, - debug_text: None, - network_index: None, - })?; - } - - Ok(()) - }) - } - - async fn connect_network( - &self, - _exchange: &Exchange<'_>, - req: &ConnectNetworkRequest<'_>, - encoder: CmdDataEncoder<'_, '_, '_>, - ) -> Result<(), Error> { - // TODO: Check failsafe status - - // Non-concurrent commissioning scenario (i.e. only BLE is active, and the ESP IDF co-exist mode is not enabled) - - let ssid = core::str::from_utf8(req.network_id.0).unwrap(); - - info!( - "Request to connect to network with SSID {} received", - core::str::from_utf8(req.network_id.0).unwrap(), - ); - - self.networks.state.lock(|state| { - let mut state = state.borrow_mut(); - - state.connect_requested = Some(ssid.try_into().unwrap()); - }); - - let writer = encoder.with_command(ResponseCommands::ConnectNetworkResponse as _)?; - - // As per spec, return success even though though whether we'll be able to connect to the network - // will become apparent later, once we switch to Wifi - writer.set(ConnectNetworkResponse { - status: NetworkCommissioningStatus::Success, - debug_text: None, - error_value: 0, - })?; - - // Notify that we have received a connect command - self.networks.network_connect_requested.notify(); - - Ok(()) - } - - fn reorder_network( - &self, - _exchange: &Exchange<'_>, - req: &ReorderNetworkRequest<'_>, - encoder: CmdDataEncoder<'_, '_, '_>, - ) -> Result<(), Error> { - // TODO: Check failsafe status - - self.networks.state.lock(|state| { - let mut state = state.borrow_mut(); - - let index = state - .networks - .iter() - .position(|conf| conf.ssid.as_str().as_bytes() == req.network_id.0); - - let writer = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?; - - if let Some(index) = index { - // Found - - if req.index < state.networks.len() as u8 { - let conf = state.networks.remove(index); - state - .networks - .insert(req.index as usize, conf) - .map_err(|_| ()) - .unwrap(); - - state.changed = true; - - info!( - "Network with SSID {} reordered to index {}", - core::str::from_utf8(req.network_id.0).unwrap(), - req.index - ); - - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::Success, - debug_text: None, - network_index: Some(req.index as _), - })?; - } else { - warn!( - "Reordering network with SSID {} to index {} failed: out of range", - core::str::from_utf8(req.network_id.0).unwrap(), - req.index - ); - - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::OutOfRange, - debug_text: None, - network_index: Some(req.index as _), - })?; - } - } else { - warn!( - "Network with SSID {} not found", - core::str::from_utf8(req.network_id.0).unwrap() - ); - - // Not found - writer.set(NetworkConfigResponse { - status: NetworkCommissioningStatus::NetworkIdNotFound, - debug_text: None, - network_index: None, - })?; - } - - Ok(()) - }) - } -} - -impl<'a, const N: usize, M> AsyncHandler for WifiNwCommCluster<'a, N, M> -where - M: RawMutex, -{ - async fn read<'m>( - &'m self, - attr: &'m AttrDetails<'_>, - encoder: AttrDataEncoder<'m, '_, '_>, - ) -> Result<(), Error> { - WifiNwCommCluster::read(self, attr, encoder) - } - - async fn invoke<'m>( - &'m self, - exchange: &'m Exchange<'_>, - cmd: &'m CmdDetails<'_>, - data: &'m TLVElement<'_>, - encoder: CmdDataEncoder<'m, '_, '_>, - ) -> Result<(), Error> { - WifiNwCommCluster::invoke(self, exchange, cmd, data, encoder).await - } -} - -// impl ChangeNotifier<()> for WifiCommCluster { -// fn consume_change(&mut self) -> Option<()> { -// self.data_ver.consume_change(()) -// } -// } diff --git a/src/wifi/mgmt.rs b/src/wifi/mgmt.rs deleted file mode 100644 index 57dcf8e..0000000 --- a/src/wifi/mgmt.rs +++ /dev/null @@ -1,243 +0,0 @@ -#![cfg(all( - not(esp32h2), - esp_idf_comp_esp_wifi_enabled, - esp_idf_comp_esp_event_enabled, -))] - -use core::pin::pin; - -use alloc::sync::Arc; - -use embassy_futures::select::select; -use embassy_sync::blocking_mutex::raw::RawMutex; -use embassy_sync::mutex::Mutex; -use embassy_time::{Duration, Timer}; - -use esp_idf_svc::eventloop::EspSystemEventLoop; -use esp_idf_svc::hal::task::embassy_sync::EspRawMutex; -use esp_idf_svc::handle::RawHandle; -use esp_idf_svc::netif::EspNetif; -use esp_idf_svc::sys::{esp, esp_netif_create_ip6_linklocal, EspError, ESP_ERR_INVALID_STATE}; -use esp_idf_svc::wifi::{self as wifi, AsyncWifi, AuthMethod, EspWifi, WifiEvent}; - -use log::{error, info, warn}; - -use rs_matter::data_model::sdm::nw_commissioning::NetworkCommissioningStatus; -use rs_matter::utils::notification::Notification; - -use crate::netif::NetifAccess; - -use super::{WifiContext, WifiCredentials, WifiStatus}; - -impl<'d, M> NetifAccess for &Mutex>> -where - M: RawMutex, -{ - async fn with_netif(&self, f: F) -> R - where - F: FnOnce(&EspNetif) -> R, - { - f(self.lock().await.wifi().sta_netif()) - } -} - -/// A generic Wifi manager. -/// -/// Utilizes the information w.r.t. Wifi networks that the -/// Matter stack pushes into the `WifiContext` struct to connect -/// to one of these networks, in preference order matching the order of the -/// networks there, and the connect request that might be provided by the -/// Matter stack. -/// -/// Also monitors the Wifi connection status and retries the connection -/// with a backoff strategy and in a round-robin fashion with the other -/// networks in case of a failure. -pub struct WifiManager<'a, 'd, const N: usize, M> -where - M: RawMutex, -{ - wifi: &'a Mutex>>, - context: &'a WifiContext, - sysloop: EspSystemEventLoop, -} - -impl<'a, 'd, const N: usize, M> WifiManager<'a, 'd, N, M> -where - M: RawMutex, -{ - /// Create a new Wifi manager. - pub fn new( - wifi: &'a Mutex>>, - context: &'a WifiContext, - sysloop: EspSystemEventLoop, - ) -> Self { - Self { - wifi, - context, - sysloop, - } - } - - /// Runs the Wifi manager. - /// - /// This function will try to connect to the networks in the `WifiContext` - /// and will retry the connection in case of a failure. - pub async fn run(&self) -> Result<(), crate::error::Error> { - let mut ssid = None; - - loop { - let creds = self.context.state.lock(|state| { - let mut state = state.borrow_mut(); - - state.get_next_network(ssid.as_deref()) - }); - - let Some(creds) = creds else { - // No networks, bail out - warn!("No networks found"); - return Err(EspError::from_infallible::().into()); - }; - - ssid = Some(creds.ssid.clone()); - - let _ = self.connect_with_retries(&creds).await; - } - } - - async fn connect_with_retries(&self, creds: &WifiCredentials) -> Result<(), EspError> { - loop { - let mut result = Ok(()); - - for delay in [2, 5, 10, 20, 30, 60].iter().copied() { - result = self.connect(creds).await; - - if result.is_ok() { - break; - } else { - warn!( - "Connection to SSID {} failed: {:?}, retrying in {delay}s", - creds.ssid, result - ); - } - - Timer::after(Duration::from_secs(delay)).await; - } - - self.context.state.lock(|state| { - let mut state = state.borrow_mut(); - - if result.is_ok() { - state.connected_once = true; - } - - state.status = Some(WifiStatus { - ssid: creds.ssid.clone(), - status: result - .map(|_| NetworkCommissioningStatus::Success) - .unwrap_or(NetworkCommissioningStatus::OtherConnectionFailure), - value: 0, - }); - }); - - if result.is_ok() { - info!("Connected to SSID {}", creds.ssid); - - self.wait_disconnect().await?; - } else { - error!("Failed to connect to SSID {}: {:?}", creds.ssid, result); - - break result; - } - } - } - - async fn wait_disconnect(&self) -> Result<(), EspError> { - let notification = Arc::new(Notification::::new()); - - let _subscription = { - let notification = notification.clone(); - - self.sysloop.subscribe::(move |_| { - notification.notify(); - }) - }?; - - loop { - { - let wifi = self.wifi.lock().await; - if !wifi.is_connected()? { - break Ok(()); - } - } - - let mut events = pin!(notification.wait()); - let mut timer = pin!(Timer::after(Duration::from_secs(5))); - - select(&mut events, &mut timer).await; - } - } - - async fn connect(&self, creds: &WifiCredentials) -> Result<(), EspError> { - info!("Connecting to SSID {}", creds.ssid); - - let auth_methods: &[AuthMethod] = if creds.password.is_empty() { - &[AuthMethod::None] - } else { - &[ - AuthMethod::WPAWPA2Personal, - AuthMethod::WPA2WPA3Personal, - AuthMethod::WEP, - ] - }; - - let mut result = Ok(()); - - for auth_method in auth_methods.iter().copied() { - let conf = wifi::Configuration::Client(wifi::ClientConfiguration { - ssid: creds.ssid.clone(), - auth_method, - password: creds.password.clone(), - ..Default::default() - }); - - result = self.connect_with(&conf).await; - - if result.is_ok() { - break; - } - } - - result - } - - async fn connect_with(&self, conf: &wifi::Configuration) -> Result<(), EspError> { - info!("Connecting with {:?}", conf); - - let mut wifi = self.wifi.lock().await; - - let _ = wifi.stop().await; - - wifi.set_configuration(conf)?; - - wifi.start().await?; - - let connect = matches!(conf, wifi::Configuration::Client(_)) - && !matches!( - conf, - wifi::Configuration::Client(wifi::ClientConfiguration { - auth_method: wifi::AuthMethod::None, - .. - }) - ); - - if connect { - wifi.connect().await?; - } - - info!("Successfully connected with {:?}", conf); - - esp!(unsafe { esp_netif_create_ip6_linklocal(wifi.wifi().sta_netif().handle() as _) })?; - - Ok(()) - } -}