From 5533b1876691b7f34d6231e7a86ceca7c1f7dd98 Mon Sep 17 00:00:00 2001 From: ivmarkov Date: Sat, 5 Oct 2024 23:12:37 +0300 Subject: [PATCH] Concurrent commissioning, WIP Thread support (#7) * In-place init * Build with Rust stable * Build with Rust stable * Update to the tip of my fork * Update to latest edge-net; proper init for GattResponse * Concurrent commissioning * Concurrent commissioning * fmt * Update example * fmt * Fix CI * Bugfixing * Switch to the main branch --- Cargo.toml | 17 +- README.md | 84 ++++---- examples/light.rs | 80 +++---- examples/light_eth.rs | 69 +++--- src/ble.rs | 150 +++++++++---- src/error.rs | 10 + src/{stack => }/eth.rs | 0 src/lib.rs | 85 +++++++- src/mdns.rs | 2 - src/{stack => }/netif.rs | 51 +++-- src/{stack => }/persist.rs | 4 +- src/stack.rs | 64 ------ src/stack/wifible.rs | 277 ------------------------ src/wireless.rs | 430 +++++++++++++++++++++++++++++++++++++ 14 files changed, 799 insertions(+), 524 deletions(-) create mode 100644 src/error.rs rename src/{stack => }/eth.rs (100%) rename src/{stack => }/netif.rs (76%) rename src/{stack => }/persist.rs (94%) delete mode 100644 src/stack.rs delete mode 100644 src/stack/wifible.rs create mode 100644 src/wireless.rs diff --git a/Cargo.toml b/Cargo.toml index 107d688..eed6899 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,16 @@ readme = "README.md" build = "build.rs" rust-version = "1.78" +#[lib] +#harness = false + #[patch.'https://github.com/ivmarkov/async-io-mini'] #async-io-mini = { path = "../async-io-mini" } #[patch.'https://github.com/ivmarkov/rs-matter-stack'] -#rs-matter-stack = { path = "../rs-matter-stack", default-features = false, features = ["std"], optional = true } +#rs-matter-stack = { path = "../rs-matter-stack" } [patch.crates-io] -rs-matter = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" } +rs-matter = { git = "https://github.com/ivmarkov/rs-matter" } #rs-matter = { path = "../rs-matter/rs-matter" } #edge-nal = { git = "https://github.com/ivmarkov/edge-net" } #edge-nal = { path = "../edge-net/edge-nal" } @@ -34,18 +37,16 @@ debug = true opt-level = "z" [features] -#default = ["rs-matter-stack"] -default = ["rs-matter-stack", "async-io-mini"] -rs-matter-stack = ["dep:rs-matter-stack", "std"] +#default = ["std", "rs-matter-stack"] +default = ["std", "rs-matter-stack", "async-io-mini"] async-io-mini = ["std", "edge-nal-std/async-io-mini"] -std = ["esp-idf-svc/std", "edge-nal-std"] +std = ["esp-idf-svc/std", "edge-nal-std", "rs-matter-stack?/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 } heapless = "0.8" enumset = { version = "1", default-features = false } -scopeguard = { version = "1", default-features = false } embassy-futures = "0.1" embassy-sync = "0.6" embassy-time = { version = "0.3", features = ["generic-queue"] } @@ -53,7 +54,7 @@ esp-idf-svc = { version = "0.49.1", default-features = false, features = ["alloc embedded-svc = { version = "0.28", default-features = false } rs-matter = { version = "0.1", default-features = false, features = ["rustcrypto"] } 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 = { git = "https://github.com/ivmarkov/rs-matter-stack", default-features = false, optional = true } edge-nal = "0.3" edge-nal-std = { version = "0.3", default-features = false, optional = true } diff --git a/README.md b/README.md index cc9770e..1001977 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,15 @@ Since ESP-IDF does support the Rust Standard Library, UDP networking just works. (See also [All examples](#all-examples)) ```rust -//! An example utilizing the `EspWifiBleMatterStack` struct. +//! An example utilizing the `EspNCWifiMatterStack` struct. +//! //! As the name suggests, this Matter stack assembly uses Wifi as the main transport, -//! and BLE for commissioning. +//! (and thus BLE for commissioning), where `NC` stands for non-concurrent commisisoning +//! (i.e., the stack will not run the BLE and Wifi radio simultaneously, which saves memory). +//! //! If you want to use Ethernet, utilize `EspEthMatterStack` instead. +//! If you want to use concurrent commissioning, utilize `EspWifiMatterStack` instead +//! (Alexa does not work (yet) with non-concurrent commissioning). //! //! The example implements a fictitious Light device (an On-Off Matter cluster). @@ -31,7 +36,7 @@ use core::pin::pin; use embassy_futures::select::select; use embassy_time::{Duration, Timer}; -use esp_idf_matter::{init_async_io, EspKvBlobStore, EspModem, EspPersist, EspWifiBleMatterStack}; +use esp_idf_matter::{init_async_io, EspMatterBle, EspMatterWifi, EspWifiMatterStack}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::peripherals::Peripherals; @@ -47,13 +52,13 @@ use rs_matter::data_model::cluster_on_off; use rs_matter::data_model::device_types::DEV_TYPE_ON_OFF_LIGHT; use rs_matter::data_model::objects::{Dataver, Endpoint, HandlerCompat, Node}; use rs_matter::data_model::system_model::descriptor; -use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::init::InitMaybeUninit; use rs_matter::utils::select::Coalesce; -use rs_matter::CommissioningData; +use rs_matter::BasicCommData; use rs_matter_stack::persist::DummyPersist; -use static_cell::ConstStaticCell; +use static_cell::StaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; @@ -67,7 +72,7 @@ fn main() -> Result<(), anyhow::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(70 * 1024) + .stack_size(55 * 1024) .spawn(|| { // Eagerly initialize `async-io` to minimize the risk of stack blowups later on init_async_io()?; @@ -95,9 +100,28 @@ fn run() -> Result<(), anyhow::Error> { } async fn matter() -> Result<(), anyhow::Error> { - // Take the Matter stack (can be done only once), + // Initialize the Matter stack (can be done only once), // as we'll run it in this thread - let stack = MATTER_STACK.take(); + let stack = MATTER_STACK + .uninit() + .init_with(EspWifiMatterStack::init_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", + }, + BasicCommData { + password: 20202021, + discriminator: 3840, + }, + &DEV_ATT, + )); // Take some generic ESP-IDF stuff we'll need later let sysloop = EspSystemEventLoop::take()?; @@ -124,28 +148,26 @@ async fn matter() -> Result<(), anyhow::Error> { .chain( LIGHT_ENDPOINT_ID, descriptor::ID, - HandlerCompat(descriptor::DescriptorCluster::new(Dataver::new_rand(stack.matter().rand()))), + HandlerCompat(descriptor::DescriptorCluster::new(Dataver::new_rand( + stack.matter().rand(), + ))), ); + let (mut wifi_modem, mut bt_modem) = peripherals.modem.split(); + // Run the Matter stack with our handler // 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 the Wifi modem peripheral + EspMatterWifi::new(&mut wifi_modem, sysloop, timers, nvs.clone()), + // The Matter stack needs the BT modem peripheral + EspMatterBle::new(&mut bt_modem, nvs, stack), // 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` - EspModem::new(peripherals.modem, sysloop, timers, nvs, stack), - // Hard-coded for demo purposes - CommissioningData { - verifier: VerifierData::new_with_pw(123456, stack.matter().rand()), - discriminator: 250, - }, // Our `AsyncHandler` + `AsyncMetadata` impl (NODE, handler), // No user future to run @@ -182,21 +204,9 @@ async fn matter() -> Result<(), anyhow::Error> { /// 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(EspWifiBleMatterStack::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(), - )); +static MATTER_STACK: StaticCell> = StaticCell::new(); + +static DEV_ATT: dev_att::HardCodedDevAtt = dev_att::HardCodedDevAtt::new(); /// Endpoint 0 (the root endpoint) always runs /// the hidden Matter system clusters, so we pick ID=1 @@ -206,10 +216,10 @@ const LIGHT_ENDPOINT_ID: u16 = 1; const NODE: Node = Node { id: 0, endpoints: &[ - EspWifiBleMatterStack::<()>::root_metadata(), + EspWifiMatterStack::<()>::root_metadata(), Endpoint { id: LIGHT_ENDPOINT_ID, - device_type: DEV_TYPE_ON_OFF_LIGHT, + device_types: &[DEV_TYPE_ON_OFF_LIGHT], clusters: &[descriptor::CLUSTER, cluster_on_off::CLUSTER], }, ], diff --git a/examples/light.rs b/examples/light.rs index 544487c..f8fe7c0 100644 --- a/examples/light.rs +++ b/examples/light.rs @@ -1,7 +1,12 @@ -//! An example utilizing the `EspWifiBleMatterStack` struct. +//! An example utilizing the `EspWifiNCMatterStack` struct. +//! //! As the name suggests, this Matter stack assembly uses Wifi as the main transport, -//! and BLE for commissioning. +//! (and thus BLE for commissioning), where `NC` stands for non-concurrent commisisoning +//! (i.e., the stack will not run the BLE and Wifi radio simultaneously, which saves memory). +//! //! If you want to use Ethernet, utilize `EspEthMatterStack` instead. +//! If you want to use concurrent commissioning, utilize `EspWifiMatterStack` instead +//! (Alexa does not work (yet) with non-concurrent commissioning). //! //! The example implements a fictitious Light device (an On-Off Matter cluster). @@ -10,7 +15,7 @@ use core::pin::pin; use embassy_futures::select::select; use embassy_time::{Duration, Timer}; -use esp_idf_matter::{init_async_io, EspKvBlobStore, EspModem, EspPersist, EspWifiBleMatterStack}; +use esp_idf_matter::{init_async_io, EspMatterBle, EspMatterWifi, EspWifiNCMatterStack}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::peripherals::Peripherals; @@ -26,13 +31,13 @@ use rs_matter::data_model::cluster_on_off; use rs_matter::data_model::device_types::DEV_TYPE_ON_OFF_LIGHT; use rs_matter::data_model::objects::{Dataver, Endpoint, HandlerCompat, Node}; use rs_matter::data_model::system_model::descriptor; -use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::init::InitMaybeUninit; use rs_matter::utils::select::Coalesce; -use rs_matter::CommissioningData; +use rs_matter::BasicCommData; use rs_matter_stack::persist::DummyPersist; -use static_cell::ConstStaticCell; +use static_cell::StaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; @@ -46,7 +51,7 @@ fn main() -> Result<(), anyhow::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(70 * 1024) + .stack_size(75 * 1024) .spawn(|| { // Eagerly initialize `async-io` to minimize the risk of stack blowups later on init_async_io()?; @@ -74,9 +79,28 @@ fn run() -> Result<(), anyhow::Error> { } async fn matter() -> Result<(), anyhow::Error> { - // Take the Matter stack (can be done only once), + // Initialize the Matter stack (can be done only once), // as we'll run it in this thread - let stack = MATTER_STACK.take(); + let stack = MATTER_STACK + .uninit() + .init_with(EspWifiNCMatterStack::init_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", + }, + BasicCommData { + password: 20202021, + discriminator: 3840, + }, + &DEV_ATT, + )); // Take some generic ESP-IDF stuff we'll need later let sysloop = EspSystemEventLoop::take()?; @@ -108,25 +132,21 @@ async fn matter() -> Result<(), anyhow::Error> { ))), ); + let (mut wifi_modem, mut bt_modem) = peripherals.modem.split(); + // Run the Matter stack with our handler // 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 the Wifi modem peripheral + EspMatterWifi::new(&mut wifi_modem, sysloop, timers, nvs.clone()), + // The Matter stack needs the BT modem peripheral + EspMatterBle::new(&mut bt_modem, nvs, stack), // 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` - EspModem::new(peripherals.modem, sysloop, timers, nvs, stack), - // Hard-coded for demo purposes - CommissioningData { - verifier: VerifierData::new_with_pw(123456, stack.matter().rand()), - discriminator: 250, - }, // Our `AsyncHandler` + `AsyncMetadata` impl (NODE, handler), // No user future to run @@ -163,21 +183,9 @@ async fn matter() -> Result<(), anyhow::Error> { /// 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(EspWifiBleMatterStack::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(), - )); +static MATTER_STACK: StaticCell> = StaticCell::new(); + +static DEV_ATT: dev_att::HardCodedDevAtt = dev_att::HardCodedDevAtt::new(); /// Endpoint 0 (the root endpoint) always runs /// the hidden Matter system clusters, so we pick ID=1 @@ -187,10 +195,10 @@ const LIGHT_ENDPOINT_ID: u16 = 1; const NODE: Node = Node { id: 0, endpoints: &[ - EspWifiBleMatterStack::<()>::root_metadata(), + EspWifiNCMatterStack::<()>::root_metadata(), Endpoint { id: LIGHT_ENDPOINT_ID, - device_type: DEV_TYPE_ON_OFF_LIGHT, + device_types: &[DEV_TYPE_ON_OFF_LIGHT], clusters: &[descriptor::CLUSTER, cluster_on_off::CLUSTER], }, ], diff --git a/examples/light_eth.rs b/examples/light_eth.rs index 332e0c3..9d112f1 100644 --- a/examples/light_eth.rs +++ b/examples/light_eth.rs @@ -12,9 +12,8 @@ use core::pin::pin; use embassy_futures::select::select; use embassy_time::{Duration, Timer}; -use esp_idf_matter::{ - init_async_io, EspEthMatterStack, EspKvBlobStore, EspMatterNetif, EspPersist, -}; +use esp_idf_matter::netif::EspMatterNetif; +use esp_idf_matter::{init_async_io, EspEthMatterStack}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::peripherals::Peripherals; @@ -33,13 +32,13 @@ use rs_matter::data_model::cluster_on_off; use rs_matter::data_model::device_types::DEV_TYPE_ON_OFF_LIGHT; use rs_matter::data_model::objects::{Dataver, Endpoint, HandlerCompat, Node}; use rs_matter::data_model::system_model::descriptor; -use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::init::InitMaybeUninit; use rs_matter::utils::select::Coalesce; -use rs_matter::CommissioningData; +use rs_matter::BasicCommData; use rs_matter_stack::persist::DummyPersist; -use static_cell::ConstStaticCell; +use static_cell::StaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; @@ -56,7 +55,7 @@ fn main() -> Result<(), anyhow::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(60 * 1024) .spawn(|| { // Eagerly initialize `async-io` to minimize the risk of stack blowups later on init_async_io()?; @@ -84,9 +83,28 @@ fn run() -> Result<(), anyhow::Error> { } async fn matter() -> Result<(), anyhow::Error> { - // Take the Matter stack (can be done only once), + // Initialize the Matter stack (can be done only once), // as we'll run it in this thread - let stack = MATTER_STACK.take(); + let stack = MATTER_STACK + .uninit() + .init_with(EspEthMatterStack::init_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", + }, + BasicCommData { + password: 20202021, + discriminator: 3840, + }, + &DEV_ATT, + )); // Take some generic ESP-IDF stuff we'll need later let sysloop = EspSystemEventLoop::take()?; @@ -138,20 +156,15 @@ async fn matter() -> Result<(), anyhow::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 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), // 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().rand()), - discriminator: 250, - }, // Our `AsyncHandler` + `AsyncMetadata` impl (NODE, handler), // No user future to run @@ -187,21 +200,9 @@ async fn matter() -> Result<(), anyhow::Error> { /// The Matter stack is allocated statically to avoid /// program stack blowups. -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(), - )); +static MATTER_STACK: StaticCell> = StaticCell::new(); + +static DEV_ATT: dev_att::HardCodedDevAtt = dev_att::HardCodedDevAtt::new(); /// Endpoint 0 (the root endpoint) always runs /// the hidden Matter system clusters, so we pick ID=1 @@ -214,7 +215,7 @@ const NODE: Node = Node { EspEthMatterStack::<()>::root_metadata(), Endpoint { id: LIGHT_ENDPOINT_ID, - device_type: DEV_TYPE_ON_OFF_LIGHT, + device_types: &[DEV_TYPE_ON_OFF_LIGHT], clusters: &[descriptor::CLUSTER, cluster_on_off::CLUSTER], }, ], diff --git a/src/ble.rs b/src/ble.rs index 0b6db4e..bfd6a44 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -1,17 +1,7 @@ -#![cfg(all( - not(esp_idf_btdm_ctrl_mode_br_edr_only), - esp_idf_bt_enabled, - esp_idf_bt_bluedroid_enabled, - not(esp32s2) -))] - use core::borrow::Borrow; -use core::cell::RefCell; use alloc::borrow::ToOwned; -use embassy_sync::blocking_mutex::Mutex; - use enumset::enum_set; use esp_idf_svc::bt::ble::gap::{BleGapEvent, EspBleGap}; @@ -32,8 +22,10 @@ use rs_matter::transport::network::btp::{ C2_CHARACTERISTIC_UUID, C2_MAX_LEN, MATTER_BLE_SERVICE_UUID16, MAX_BTP_SESSIONS, }; use rs_matter::transport::network::BtAddr; -use rs_matter::utils::ifmutex::IfMutex; -use rs_matter::utils::signal::Signal; +use rs_matter::utils::cell::RefCell; +use rs_matter::utils::init::{init, Init}; +use rs_matter::utils::sync::blocking::Mutex; +use rs_matter::utils::sync::{IfMutex, Signal}; const MAX_CONNECTIONS: usize = MAX_BTP_SESSIONS; const MAX_MTU_SIZE: usize = 512; @@ -52,48 +44,91 @@ struct State { c1_handle: Option, c2_handle: Option, c2_cccd_handle: Option, - connections: heapless::Vec, + connections: rs_matter::utils::storage::Vec, response: GattResponse, } +impl State { + #[inline(always)] + const fn new() -> Self { + Self { + gatt_if: None, + service_handle: None, + c1_handle: None, + c2_handle: None, + c2_cccd_handle: None, + connections: rs_matter::utils::storage::Vec::new(), + response: GattResponse::new(), + } + } + + fn init() -> impl Init { + init!(Self { + gatt_if: None, + service_handle: None, + c1_handle: None, + c2_handle: None, + c2_cccd_handle: None, + connections <- rs_matter::utils::storage::Vec::init(), + response <- gatt_response::init(), + }) + } +} + #[derive(Debug)] struct IndBuffer { addr: BtAddr, - data: heapless::Vec, + data: rs_matter::utils::storage::Vec, +} + +impl IndBuffer { + #[inline(always)] + const fn new() -> Self { + Self { + addr: BtAddr([0; 6]), + data: rs_matter::utils::storage::Vec::new(), + } + } + + fn init() -> impl Init { + init!(Self { + addr: BtAddr([0; 6]), + data <- rs_matter::utils::storage::Vec::init(), + }) + } } -/// The `'static` state of the `BtpGattPeripheral` struct. +/// The `'static` state of the `EspBtpGattPeripheral` struct. /// Isolated as a separate struct to allow for `const fn` construction /// and static allocation. -pub struct BtpGattContext { +pub struct EspBtpGattContext { state: Mutex>, ind: IfMutex, ind_in_flight: Signal, } -impl BtpGattContext { +impl EspBtpGattContext { /// Create a new instance. #[allow(clippy::large_stack_frames)] #[inline(always)] pub const fn new() -> Self { Self { - state: Mutex::new(RefCell::new(State { - gatt_if: None, - service_handle: None, - c1_handle: None, - c2_handle: None, - c2_cccd_handle: None, - connections: heapless::Vec::new(), - response: GattResponse::new(), - })), - ind: IfMutex::new(IndBuffer { - addr: BtAddr([0; 6]), - data: heapless::Vec::new(), - }), + state: Mutex::new(RefCell::new(State::new())), + ind: IfMutex::new(IndBuffer::new()), ind_in_flight: Signal::new(false), } } + /// Return an in-place initializer for `EspBtpGattContext`. + #[allow(clippy::large_stack_frames)] + pub fn init() -> impl Init { + init!(Self { + state <- Mutex::init(RefCell::init(State::init())), + ind <- IfMutex::init(IndBuffer::init()), + ind_in_flight: Signal::new(false), + }) + } + pub(crate) fn reset(&self) -> Result<(), EspError> { self.state.lock(|state| { let mut state = state.borrow_mut(); @@ -121,7 +156,7 @@ impl BtpGattContext { } } -impl Default for BtpGattContext { +impl Default for EspBtpGattContext { // TODO #[allow(clippy::large_stack_frames)] #[inline(always)] @@ -130,18 +165,18 @@ impl Default for BtpGattContext { } } -/// A GATT peripheral implementation for the BTP protocol in `rs-matter`. +/// A GATT peripheral implementation for the BTP protocol in `rs-matter` via ESP-IDF. /// Implements the `GattPeripheral` trait. -pub struct BtpGattPeripheral<'a, 'd, M> +pub struct EspBtpGattPeripheral<'a, 'd, M> where M: BleEnabled, { app_id: u16, driver: BtDriver<'d, M>, - context: &'a BtpGattContext, + context: &'a EspBtpGattContext, } -impl<'a, 'd, M> BtpGattPeripheral<'a, 'd, M> +impl<'a, 'd, M> EspBtpGattPeripheral<'a, 'd, M> where M: BleEnabled, { @@ -152,7 +187,7 @@ where pub fn new( app_id: u16, driver: BtDriver<'d, M>, - context: &'a BtpGattContext, + context: &'a EspBtpGattContext, ) -> Result { context.reset()?; @@ -258,7 +293,7 @@ where } } -impl<'a, 'd, M> GattPeripheral for BtpGattPeripheral<'a, 'd, M> +impl<'a, 'd, M> GattPeripheral for EspBtpGattPeripheral<'a, 'd, M> where M: BleEnabled, { @@ -271,7 +306,7 @@ where where F: FnMut(GattPeripheralEvent) + Send + Clone + 'static, { - BtpGattPeripheral::run(self, service_name, adv_data, callback) + EspBtpGattPeripheral::run(self, service_name, adv_data, callback) .await .map_err(|_| ErrorCode::BtpError)?; @@ -279,7 +314,7 @@ where } async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), rs_matter::error::Error> { - BtpGattPeripheral::indicate(self, data, address) + EspBtpGattPeripheral::indicate(self, data, address) .await .map_err(|_| ErrorCode::BtpError)?; @@ -295,7 +330,7 @@ where app_id: u16, gap: &'a EspBleGap<'d, M, T>, gatts: &'a EspGatts<'d, M, T>, - ctx: &'a BtpGattContext, + ctx: &'a EspBtpGattContext, } impl<'a, 'd, M, T> GattExecContext<'a, 'd, M, T> @@ -307,7 +342,7 @@ where app_id: u16, gap: &'a EspBleGap<'d, M, T>, gatts: &'a EspGatts<'d, M, T>, - ctx: &'a BtpGattContext, + ctx: &'a EspBtpGattContext, ) -> Self { Self { app_id, @@ -804,3 +839,36 @@ where Ok(()) } } + +mod gatt_response { + use esp_idf_svc::bt::ble::gatt::GattResponse; + use esp_idf_svc::sys::{esp_gatt_rsp_t, esp_gatt_value_t}; + + use rs_matter::utils::init::{init, init_from_closure, zeroed, Init}; + + /// Return an in-place initializer for `GattResponse`. + /// + /// Works by initializing the `GattResponse` struct in-place using the `esp_gatt_rsp_t` type, + /// which is possible because `GattResponse` is a `#[repr(transparent)]` newtype over `esp_gatt_rsp_t`. + pub fn init() -> impl Init { + unsafe { + init_from_closure(|slot: *mut GattResponse| { + let slot = slot as *mut esp_gatt_rsp_t; + + init_esp_gatt_response().__init(slot) + }) + } + } + + fn init_esp_gatt_response() -> impl Init { + init!(esp_gatt_rsp_t { + attr_value <- init!(esp_gatt_value_t { + len: 0, + value <- zeroed(), + handle: 0, + offset: 0, + auth_req: 0, + }), + }) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..a993b93 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,10 @@ +use esp_idf_svc::sys::EspError; + +use rs_matter::error::{Error, ErrorCode}; + +/// Converts an ESP network error to an `rs-matter` error +pub fn to_net_error(_err: EspError) -> Error { + // TODO: The `rs-matter` error code is too generic + // TODO: Capture the backtrace and the original error + ErrorCode::NoNetworkInterface.into() +} diff --git a/src/stack/eth.rs b/src/eth.rs similarity index 100% rename from src/stack/eth.rs rename to src/eth.rs diff --git a/src/lib.rs b/src/lib.rs index 0a71214..4861257 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,9 +17,88 @@ extern crate std; #[macro_use] extern crate alloc; -#[cfg(feature = "rs-matter-stack")] -pub use stack::*; +#[cfg(all(feature = "std", feature = "rs-matter-stack"))] +pub use eth::*; +#[cfg(all( + not(esp32h2), + not(esp32s2), + esp_idf_comp_esp_event_enabled, + not(esp_idf_btdm_ctrl_mode_br_edr_only), + esp_idf_bt_enabled, + esp_idf_bt_bluedroid_enabled, + feature = "std", + feature = "rs-matter-stack" +))] +pub use wireless::*; +#[cfg(all( + not(esp_idf_btdm_ctrl_mode_br_edr_only), + esp_idf_bt_enabled, + esp_idf_bt_bluedroid_enabled, + not(esp32s2) +))] pub mod ble; +pub mod error; +#[cfg(all(feature = "std", feature = "rs-matter-stack"))] +pub mod eth; +#[cfg(any(esp_idf_comp_mdns_enabled, esp_idf_comp_espressif__mdns_enabled))] pub mod mdns; -mod stack; +#[cfg(all( + esp_idf_comp_esp_netif_enabled, + esp_idf_comp_esp_event_enabled, + feature = "std", + feature = "rs-matter-stack" +))] +pub mod netif; +#[cfg(esp_idf_comp_nvs_flash_enabled)] +#[cfg(feature = "rs-matter-stack")] +pub mod persist; +#[cfg(all( + not(esp32h2), + not(esp32s2), + esp_idf_comp_esp_wifi_enabled, + esp_idf_comp_esp_event_enabled, + not(esp_idf_btdm_ctrl_mode_br_edr_only), + esp_idf_bt_enabled, + esp_idf_bt_bluedroid_enabled, + feature = "std", + feature = "rs-matter-stack" +))] +pub mod wireless; + +/// A utility function to initialize the `async-io` Reactor which is +/// used for IP-based networks (UDP and TCP). +/// +/// User is expected to call this method early in the application's lifecycle +/// when there is plenty of task stack space available, as the initialization +/// consumes > 10KB of stack space, so it has to be done with care. +#[inline(never)] +#[cold] +#[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)?; + + 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; + ::log::info!("Async IO initialized; using `async-io`"); + } + + #[cfg(feature = "async-io-mini")] + { + // Nothing to initialize for `async-io-mini` + ::log::info!("Async IO initialized; using `async-io-mini`"); + } +} diff --git a/src/mdns.rs b/src/mdns.rs index 22fab38..817ccdd 100644 --- a/src/mdns.rs +++ b/src/mdns.rs @@ -1,5 +1,3 @@ -#![cfg(any(esp_idf_comp_mdns_enabled, esp_idf_comp_espressif__mdns_enabled))] - // /// A type representing the ESP IDF mDNS service. // pub struct EspIdfMdns(()); diff --git a/src/stack/netif.rs b/src/netif.rs similarity index 76% rename from src/stack/netif.rs rename to src/netif.rs index dc879ad..9893e59 100644 --- a/src/stack/netif.rs +++ b/src/netif.rs @@ -1,14 +1,9 @@ -#![cfg(all( - esp_idf_comp_esp_netif_enabled, - esp_idf_comp_esp_event_enabled, - feature = "std" -))] - +use core::borrow::Borrow; use core::net::{Ipv4Addr, Ipv6Addr}; use core::pin::pin; use alloc::sync::Arc; -use rs_matter::error::{Error, ErrorCode}; +use rs_matter::error::Error; use std::io; @@ -24,30 +19,38 @@ 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::utils::sync::Notification; use rs_matter_stack::netif::{Netif, NetifConf}; +use crate::error::to_net_error; + const TIMEOUT_PERIOD_SECS: u8 = 5; -pub struct EspMatterNetif<'a> { - netif: &'a EspNetif, +/// A `Netif` and `UdpBind` traits implementation via ESP-IDF +pub struct EspMatterNetif { + netif: T, sysloop: EspSystemEventLoop, } -impl<'a> EspMatterNetif<'a> { - pub const fn new(netif: &'a EspNetif, sysloop: EspSystemEventLoop) -> Self { +impl EspMatterNetif +where + T: Borrow, +{ + /// Create a new `EspMatterNetif` instance + pub const fn new(netif: T, sysloop: EspSystemEventLoop) -> Self { Self { netif, sysloop } } fn get_conf(&self) -> Result { - Self::get_netif_conf(self.netif) + Self::get_netif_conf(self.netif.borrow()) } 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 { + /// Get the network interface configuration + pub fn get_netif_conf(netif: &EspNetif) -> Result { let ip_info = netif.get_ip_info()?; let ipv4: Ipv4Addr = ip_info.ip.octets().into(); @@ -91,7 +94,8 @@ impl<'a> EspMatterNetif<'a> { }) } - pub(crate) async fn wait_any_conf_change(sysloop: &EspSystemEventLoop) -> Result<(), EspError> { + /// Wait for any IP configuration change + pub async fn wait_any_conf_change(sysloop: &EspSystemEventLoop) -> Result<(), EspError> { let notification = Arc::new(Notification::::new()); let _subscription = { @@ -111,7 +115,10 @@ impl<'a> EspMatterNetif<'a> { } } -impl<'a> Netif for EspMatterNetif<'a> { +impl Netif for EspMatterNetif +where + T: Borrow, +{ async fn get_conf(&self) -> Result, Error> { Ok(EspMatterNetif::get_conf(self).ok()) } @@ -119,15 +126,21 @@ impl<'a> Netif for EspMatterNetif<'a> { async fn wait_conf_change(&self) -> Result<(), Error> { EspMatterNetif::wait_conf_change(self) .await - .map_err(|_| ErrorCode::NoNetworkInterface)?; // TODO + .map_err(to_net_error)?; Ok(()) } } -impl<'a> UdpBind for EspMatterNetif<'a> { +impl UdpBind for EspMatterNetif +where + T: Borrow, +{ type Error = io::Error; - type Socket<'b> = UdpSocket where Self: 'b; + 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/persist.rs similarity index 94% rename from src/stack/persist.rs rename to src/persist.rs index cf1a686..12c08ad 100644 --- a/src/stack/persist.rs +++ b/src/persist.rs @@ -1,5 +1,3 @@ -#![cfg(esp_idf_comp_nvs_flash_enabled)] - use esp_idf_svc::nvs::{EspNvs, EspNvsPartition, NvsPartitionId}; use esp_idf_svc::sys::EspError; @@ -9,7 +7,7 @@ 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>; +pub type EspMatterPersist<'a, T, C> = KvPersist<'a, EspKvBlobStore, C>; /// A `KvBlobStore`` implementation that uses the ESP IDF NVS API /// to store and load the BLOBs. diff --git a/src/stack.rs b/src/stack.rs deleted file mode 100644 index ebbe700..0000000 --- a/src/stack.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![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), - esp_idf_comp_esp_wifi_enabled, - esp_idf_comp_esp_event_enabled, - not(esp_idf_btdm_ctrl_mode_br_edr_only), - esp_idf_bt_enabled, - esp_idf_bt_bluedroid_enabled, - feature = "std" -))] -pub use wifible::*; - -mod eth; -mod netif; -mod persist; -mod wifible; - -/// A utility function to initialize the `async-io` Reactor which is -/// used for IP-based networks (UDP and TCP). -/// -/// User is expected to call this method early in the application's lifecycle -/// when there is plenty of task stack space available, as the initialization -/// consumes > 10KB of stack space, so it has to be done with care. -#[inline(never)] -#[cold] -#[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)?; - - 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; - ::log::info!("Async IO initialized; using `async-io`"); - } - - #[cfg(feature = "async-io-mini")] - { - // Nothing to initialize for `async-io-mini` - ::log::info!("Async IO initialized; using `async-io-mini`"); - } -} diff --git a/src/stack/wifible.rs b/src/stack/wifible.rs deleted file mode 100644 index 3799b2e..0000000 --- a/src/stack/wifible.rs +++ /dev/null @@ -1,277 +0,0 @@ -#![cfg(all( - not(esp32h2), - not(esp32s2), - esp_idf_comp_esp_wifi_enabled, - esp_idf_comp_esp_event_enabled, - not(esp_idf_btdm_ctrl_mode_br_edr_only), - esp_idf_bt_enabled, - esp_idf_bt_bluedroid_enabled, - feature = "std" -))] - -use core::net::SocketAddr; - -use std::io; - -use alloc::boxed::Box; - -use edge_nal::UdpBind; -use edge_nal_std::{Stack, UdpSocket}; - -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_sync::mutex::Mutex; - -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::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::{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 super::netif::EspMatterNetif; - -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. -/// -/// Usage: -/// ```no_run -/// MatterStack>>::new(); -/// ``` -/// -/// ... 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, - embedding: E, -} - -impl EspGatt -where - E: Embedding, -{ - #[allow(clippy::large_stack_frames)] - #[inline(always)] - const fn new() -> Self { - Self { - btp_gatt_context: BtpGattContext::new(), - embedding: E::INIT, - } - } - - pub fn context(&self) -> &BtpGattContext { - &self.btp_gatt_context - } - - pub fn embedding(&self) -> &E { - &self.embedding - } -} - -impl Embedding for EspGatt -where - E: Embedding, -{ - const INIT: Self = Self::new(); -} - -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, '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(), - ) - } - - 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(), - )), - } - } -} - -pub struct EspWifiDevice<'d> { - sysloop: EspSystemEventLoop, - wifi: Mutex>>>, -} - -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>); - -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() - } - - 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 - } - - async fn connect(&mut self) -> Result<(), Self::Error> { - let mut wifi = self.0.wifi.lock().await; - - wifi.connect().await?; - - esp!(unsafe { esp_netif_create_ip6_linklocal(wifi.wifi().sta_netif().handle() as _) })?; - - Ok(()) - } - - async fn disconnect(&mut self) -> Result<(), Self::Error> { - self.0.wifi.lock().await.disconnect().await - } - - async fn is_started(&self) -> Result { - self.0.wifi.lock().await.is_started() - } - - async fn is_connected(&self) -> Result { - self.0.wifi.lock().await.is_connected() - } - - async fn scan_n( - &mut self, - ) -> Result<(heapless::Vec, usize), Self::Error> { - self.0.wifi.lock().await.scan_n().await - } - - async fn scan(&mut self) -> Result, Self::Error> { - self.0.wifi.lock().await.scan().await - } -} - -pub struct EspL3<'a, 'd>(&'a EspWifiDevice<'d>); - -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()) - } - - async fn wait_conf_change(&self) -> Result<(), Error> { - EspMatterNetif::wait_any_conf_change(&self.0.sysloop) - .await - .map_err(|_| ErrorCode::NoNetworkInterface)?; // TODO - - Ok(()) - } -} - -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/wireless.rs b/src/wireless.rs new file mode 100644 index 0000000..d1bd56d --- /dev/null +++ b/src/wireless.rs @@ -0,0 +1,430 @@ +use esp_idf_svc::bt::{self, BtDriver}; +use esp_idf_svc::hal::into_ref; +use esp_idf_svc::hal::modem::BluetoothModemPeripheral; +use esp_idf_svc::hal::peripheral::{Peripheral, PeripheralRef}; +use esp_idf_svc::hal::task::embassy_sync::EspRawMutex; +use esp_idf_svc::nvs::EspDefaultNvsPartition; + +use rs_matter::error::Error; +use rs_matter::tlv::{FromTLV, ToTLV}; +use rs_matter::utils::init::{init, Init}; + +use rs_matter_stack::network::{Embedding, Network}; +use rs_matter_stack::persist::KvBlobBuf; +use rs_matter_stack::wireless::traits::{Ble, WirelessConfig, WirelessData}; +use rs_matter_stack::{MatterStack, WirelessBle}; + +use crate::ble::{EspBtpGattContext, EspBtpGattPeripheral}; + +#[cfg(all( + esp_idf_comp_openthread_enabled, + esp_idf_openthread_enabled, + esp_idf_comp_vfs_enabled, +))] +pub use thread::*; + +#[cfg(esp_idf_comp_esp_wifi_enabled)] +pub use wifi::*; + +/// A type alias for an ESP-IDF Matter stack running over a wireless network (Wifi or Thread) and BLE. +pub type EspWirelessMatterStack<'a, T, E> = MatterStack<'a, EspWirelessBle>; + +/// A type alias for an ESP-IDF implementation of the `Network` trait for a Matter stack running over +/// BLE during commissioning, and then over either WiFi or Thread when operating. +pub type EspWirelessBle = WirelessBle>>; + +/// An embedding of the ESP IDF Bluedroid Gatt peripheral context for the `WirelessBle` network type from `rs-matter-stack`. +/// +/// Allows the memory of this context to be statically allocated and cost-initialized. +/// +/// Usage: +/// ```no_run +/// MatterStack>>>::new(...); +/// ``` +/// +/// ... 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: EspBtpGattContext, + embedding: E, +} + +impl EspGatt +where + E: Embedding, +{ + /// Creates a new instance of the `EspGatt` embedding. + #[allow(clippy::large_stack_frames)] + #[inline(always)] + const fn new() -> Self { + Self { + btp_gatt_context: EspBtpGattContext::new(), + embedding: E::INIT, + } + } + + /// Return an in-place initializer for the `EspGatt` embedding. + fn init() -> impl Init { + init!(Self { + btp_gatt_context <- EspBtpGattContext::init(), + embedding <- E::init(), + }) + } + + /// Return a reference to the Bluedroid Gatt peripheral context. + pub fn context(&self) -> &EspBtpGattContext { + &self.btp_gatt_context + } + + /// Return a reference to the embedding. + pub fn embedding(&self) -> &E { + &self.embedding + } +} + +impl Embedding for EspGatt +where + E: Embedding, +{ + const INIT: Self = Self::new(); + + fn init() -> impl Init { + EspGatt::init() + } +} + +const GATTS_APP_ID: u16 = 0; + +/// A `Ble` trait implementation via ESP-IDF +pub struct EspMatterBle<'a, 'd, T> { + context: &'a EspBtpGattContext, + modem: PeripheralRef<'d, T>, + nvs: EspDefaultNvsPartition, +} + +impl<'a, 'd, T> EspMatterBle<'a, 'd, T> +where + T: BluetoothModemPeripheral, +{ + /// Create a new instance of the `EspBle` type. + pub fn new( + modem: impl Peripheral

+ 'd, + nvs: EspDefaultNvsPartition, + stack: &'a EspWirelessMatterStack, + ) -> Self + where + C: WirelessConfig, + ::NetworkCredentials: Clone + for<'t> FromTLV<'t> + ToTLV, + E: Embedding + 'static, + { + Self::wrap( + modem, + nvs, + stack.network().embedding().embedding().context(), + ) + } + + /// Wrap an existing `EspBtpGattContext` and `BluetoothModemPeripheral` into a new instance of the `EspBle` type. + pub fn wrap( + modem: impl Peripheral

+ 'd, + nvs: EspDefaultNvsPartition, + context: &'a EspBtpGattContext, + ) -> Self { + into_ref!(modem); + + Self { + context, + modem, + nvs, + } + } +} + +impl<'a, 'd, T> Ble for EspMatterBle<'a, 'd, T> +where + T: BluetoothModemPeripheral, +{ + type Peripheral<'t> + = EspBtpGattPeripheral<'a, 't, bt::Ble> + where + Self: 't; + + async fn start(&mut self) -> Result, Error> { + let bt = BtDriver::new(&mut self.modem, Some(self.nvs.clone())).unwrap(); + + let peripheral = EspBtpGattPeripheral::new(GATTS_APP_ID, bt, self.context).unwrap(); + + Ok(peripheral) + } +} + +#[cfg(all( + esp_idf_comp_openthread_enabled, + esp_idf_openthread_enabled, + esp_idf_comp_vfs_enabled, +))] +mod thread { + use rs_matter_stack::wireless::traits::{Thread, NC}; + + use super::EspWirelessMatterStack; + + /// A type alias for an ESP-IDF Matter stack running over Thread (and BLE, during commissioning). + pub type EspThreadMatterStack<'a, E> = EspWirelessMatterStack<'a, Thread, E>; + + /// A type alias for an ESP-IDF Matter stack running over Thread (and BLE, during commissioning). + /// + /// Unlike `EspThreadMatterStack`, this type alias runs the commissioning in a non-concurrent mode, + /// where the device runs either BLE or Thread, but not both at the same time. + /// + /// This is useful to save memory by only having one of the stacks active at any point in time. + /// + /// Note that Alexa does not (yet) work with non-concurrent commissioning. + pub type EspThreadNCMatterStack<'a, E> = EspWirelessMatterStack<'a, Thread, E>; +} + +#[cfg(esp_idf_comp_esp_wifi_enabled)] +mod wifi { + use std::io; + + use alloc::sync::Arc; + + use edge_nal::UdpBind; + use edge_nal_std::{Stack, UdpSocket}; + + use embassy_sync::mutex::Mutex; + + use embedded_svc::wifi::asynch::Wifi as WifiSvc; + + use enumset::EnumSet; + + use esp_idf_svc::eventloop::EspSystemEventLoop; + use esp_idf_svc::hal::into_ref; + use esp_idf_svc::hal::modem::WifiModemPeripheral; + use esp_idf_svc::hal::peripheral::{Peripheral, PeripheralRef}; + 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::nvs::EspDefaultNvsPartition; + use esp_idf_svc::sys::{esp, EspError}; + use esp_idf_svc::timer::EspTaskTimerService; + use esp_idf_svc::wifi::{AccessPointInfo, AsyncWifi, Capability, Configuration, EspWifi}; + + use rs_matter::error::Error; + + use rs_matter_stack::netif::{Netif, NetifConf, NetifRun}; + use rs_matter_stack::wireless::svc::SvcWifiController; + use rs_matter_stack::wireless::traits::{Wifi, WifiData, Wireless, NC}; + + use crate::error::to_net_error; + use crate::netif::EspMatterNetif; + + use super::EspWirelessMatterStack; + + /// A type alias for an ESP-IDF Matter stack running over Wifi (and BLE, during commissioning). + pub type EspWifiMatterStack<'a, E> = EspWirelessMatterStack<'a, Wifi, E>; + + /// A type alias for an ESP-IDF Matter stack running over Wifi (and BLE, during commissioning). + /// + /// Unlike `EspWifiMatterStack`, this type alias runs the commissioning in a non-concurrent mode, + /// where the device runs either BLE or Wifi, but not both at the same time. + /// + /// This is useful to save memory by only having one of the stacks active at any point in time. + /// + /// Note that Alexa does not (yet) work with non-concurrent commissioning. + pub type EspWifiNCMatterStack<'a, E> = EspWirelessMatterStack<'a, Wifi, E>; + + /// The relation between a network interface and a controller is slightly different + /// in the ESP-IDF crates compared to what `rs-matter-stack` wants, hence we need this helper type. + pub struct EspWifiSplit<'a>( + Arc>>>, + EspSystemEventLoop, + ); + + impl<'a> WifiSvc for EspWifiSplit<'a> { + type Error = EspError; + + async fn get_capabilities(&self) -> Result, Self::Error> { + let wifi = self.0.lock().await; + + wifi.get_capabilities() + } + + async fn get_configuration(&self) -> Result { + let wifi = self.0.lock().await; + + wifi.get_configuration() + } + + async fn set_configuration(&mut self, conf: &Configuration) -> Result<(), Self::Error> { + let mut wifi = self.0.lock().await; + + wifi.set_configuration(conf) + } + + async fn start(&mut self) -> Result<(), Self::Error> { + let mut wifi = self.0.lock().await; + + wifi.start().await + } + + async fn stop(&mut self) -> Result<(), Self::Error> { + let mut wifi = self.0.lock().await; + + wifi.stop().await + } + + async fn connect(&mut self) -> Result<(), Self::Error> { + let mut wifi = self.0.lock().await; + + wifi.connect().await?; + + // Matter needs an IPv6 address to work + esp!(unsafe { + esp_idf_svc::sys::esp_netif_create_ip6_linklocal( + wifi.wifi().sta_netif().handle() as _ + ) + })?; + + Ok(()) + } + + async fn disconnect(&mut self) -> Result<(), Self::Error> { + let mut wifi = self.0.lock().await; + + wifi.disconnect().await + } + + async fn is_started(&self) -> Result { + let wifi = self.0.lock().await; + + wifi.is_started() + } + + async fn is_connected(&self) -> Result { + let wifi = self.0.lock().await; + + wifi.is_connected() + } + + async fn scan_n( + &mut self, + ) -> Result<(heapless::Vec, usize), Self::Error> { + let mut wifi = self.0.lock().await; + + wifi.scan_n().await + } + + async fn scan(&mut self) -> Result, Self::Error> { + let mut wifi = self.0.lock().await; + + wifi.scan().await + } + } + + impl<'a> Netif for EspWifiSplit<'a> { + async fn get_conf(&self) -> Result, Error> { + let wifi = self.0.lock().await; + + EspMatterNetif::new(wifi.wifi().sta_netif(), self.1.clone()) + .get_conf() + .await + } + + async fn wait_conf_change(&self) -> Result<(), Error> { + // Wait on any conf change + // We anyway cannot lock the wifi mutex here (would be a deadlock), so we just wait for the event + + EspMatterNetif::::wait_any_conf_change(&self.1) + .await + .map_err(to_net_error)?; + + Ok(()) + } + } + + impl<'a> NetifRun for EspWifiSplit<'a> { + async fn run(&self) -> Result<(), Error> { + core::future::pending().await + } + } + + impl<'a> UdpBind for EspWifiSplit<'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 + } + } + + /// A `Wireless` trait implementation via ESP-IDF's Wifi modem + pub struct EspMatterWifi<'d, T> { + modem: PeripheralRef<'d, T>, + sysloop: EspSystemEventLoop, + timer: EspTaskTimerService, + nvs: EspDefaultNvsPartition, + } + + impl<'d, T> EspMatterWifi<'d, T> + where + T: WifiModemPeripheral, + { + /// Create a new instance of the `EspMatterWifi` type. + pub fn new( + modem: impl Peripheral

+ 'd, + sysloop: EspSystemEventLoop, + timer: EspTaskTimerService, + nvs: EspDefaultNvsPartition, + ) -> Self { + into_ref!(modem); + + Self { + modem, + sysloop, + timer, + nvs, + } + } + } + + impl<'d, T> Wireless for EspMatterWifi<'d, T> + where + T: WifiModemPeripheral, + { + type Data = WifiData; + + type Netif<'a> + = EspWifiSplit<'a> + where + Self: 'a; + + type Controller<'a> + = SvcWifiController> + where + Self: 'a; + + async fn start(&mut self) -> Result<(Self::Netif<'_>, Self::Controller<'_>), Error> { + let wifi = EspWifi::new( + &mut self.modem, + self.sysloop.clone(), + Some(self.nvs.clone()), + ) + .map_err(to_net_error)?; + + let wifi = Arc::new(Mutex::new( + AsyncWifi::wrap(wifi, self.sysloop.clone(), self.timer.clone()) + .map_err(to_net_error)?, + )); + + Ok(( + EspWifiSplit(wifi.clone(), self.sysloop.clone()), + SvcWifiController::new(EspWifiSplit(wifi.clone(), self.sysloop.clone())), + )) + } + } +}