From faed0e3d4597ced23d3d17efa807f19639c6b2d0 Mon Sep 17 00:00:00 2001 From: ivmarkov Date: Sat, 27 Apr 2024 18:51:46 +0000 Subject: [PATCH] (WIP) Wifi provisioning --- Cargo.toml | 20 +- README.md | 146 ++++++- examples/on_off_wifi_ble.rs | 96 +++++ src/ble.rs | 132 +++--- src/error.rs | 46 ++ src/lib.rs | 287 +++++++++++++ src/mdns.rs | 1 + src/multicast.rs | 82 ++++ src/netif.rs | 77 ++++ src/nvs.rs | 155 +++++-- src/wifi.rs | 834 ++++++++++++++++++++++++++++++++++++ 11 files changed, 1752 insertions(+), 124 deletions(-) create mode 100644 examples/on_off_wifi_ble.rs create mode 100644 src/error.rs create mode 100644 src/mdns.rs create mode 100644 src/multicast.rs create mode 100644 src/netif.rs create mode 100644 src/wifi.rs diff --git a/Cargo.toml b/Cargo.toml index c5c43b6..6273a08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["ivmarkov "] edition = "2021" resolver = "2" categories = ["embedded", "hardware-support"] -keywords = ["embedded", "svc", "idf", "esp-idf", "esp32"] +keywords = ["matter", "embedded", "esp-idf", "esp32"] description = "Implementation of the embedded-svc traits for ESP-IDF (Espressif's IoT Development Framework)" repository = "https://github.com/ivmarkov/esp-idf-matter" license = "MIT OR Apache-2.0" @@ -15,15 +15,29 @@ build = "build.rs" rust-version = "1.77" [patch.crates-io] +embedded-svc = { git = "https://github.com/esp-rs/embedded-svc" } esp-idf-svc = { path = "../esp-idf-svc" } -rs-matter = { path = "../rs-matter" } +rs-matter = { path = "../rs-matter/rs-matter" } +rs-matter-macros = { path = "../rs-matter/rs-matter-macros" } [features] +default = ["std"] +std = ["async-io", "rs-matter/std", "rs-matter/async-io"] [dependencies] log = { version = "0.4", default-features = false } -esp-idf-svc = { version = "0.48", default-features = false, fatures = ["experimental"] } +heapless = "0.8" +enumset = { version = "1", default-features = false } +strum = { version = "0.26", default-features = false, features = ["derive"] } +embassy-futures = "0.1" +embassy-sync = "0.5" +esp-idf-svc = { version = "0.48", default-features = false, features = ["alloc", "embassy-sync", "experimental"] } rs-matter = { version = "0.1", default-features = false } +rs-matter-macros = "0.1" +async-io = { version = "2", optional = true, default-features = false } [build-dependencies] embuild = "0.31.3" + +[dev-dependencies] +static_cell = "2.1" diff --git a/README.md b/README.md index 3e2fae8..85a6694 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Run [rs-matter](https://github.com/project-chip/rs-matter) on top of the [Rust ESP IDF SDK wrappers](https://github.com/esp-rs/esp-idf-svc) +# (WIP) Run [rs-matter](https://github.com/project-chip/rs-matter) on Espressif chips with [ESP IDF](https://github.com/esp-rs/esp-idf-svc) [![CI](https://github.com/ivmarkov/esp-idf-matter/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/esp-idf-matter/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/esp-idf-matter.svg)](https://crates.io/crates/esp-idf-matter) @@ -6,19 +6,149 @@ [![Matrix](https://img.shields.io/matrix/ivmarkov:matrix.org?label=join%20matrix&color=BEC5C9&logo=matrix)](https://matrix.to/#/#esp-rs:matrix.org) [![Wokwi](https://img.shields.io/endpoint?url=https%3A%2F%2Fwokwi.com%2Fbadge%2Fclick-to-simulate.json)](https://wokwi.com/projects/332188235906155092) -## Highlights +## Overview -This boring crate provides the necessary glue to operate the [`rs-matter` Rust Matter stack]() on Espressif chips with the ESP IDF SDK. +Configuring and running the [`rs-matter`]() stack is not trivial. -In particular: -* [Bluetooth commissioning support]() with the ESP IDF Bluedroid stack +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. + +Furthermore, _operating_ the assembled Matter stack is also challenging, as various networking stacks might need to be brought up or down 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 provides an easy-to-use all-in-one `MatterStack` assembly. +Instantiate it and then call the `MatterStack::run(...)` method. + +```rust +//! An example utilizing the `MatterStack` struct. +//! As the name suggests, this Matter stack assembly uses Wifi as the main transport, and BLE for commissioning. +//! +//! The example implements a fictitious Light device (an on-off cluster). + +fn main() -> anyhow::Result() { + // Take the Matter stack (can be done only once), as we'll run it in this thread + let stack = MATTER_STACK.take().unwrap(); + + // We need to pass the `modem` peripheral to the Matter stack, as it manages Bluetooth & Wifi by itself + // + // If we want to use Bluetooth for our own needs, that's possible post-commissioning, as long as the + // coexist ESP IDF driver is loaded. We just need to then split the `modem` peripheral into BT and Wifi, + // and only pass the Wifi half. + let peripherals = Peripherals::take()?; + + // Matter needs (a clone of) the ESP IDF NVS service too + let nvs = EspDefaultNvs::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()); + + // Chain our endpoint clusters with the (root) Endpoint 0 system clusters in the final handler + let handler = stack.root_handler() + // Our on-off cluster, on Endpoint 1 + .chain( + LIGHT_ENDPOINT_ID, + cluster_on_off::ID, + on_off, + ) + // Each endpoint needs a Descriptor cluster too, use the one + // that `rs-matter` provides out of the box + .chain( + LIGHT_ENDPOINT_ID, + descriptor::ID, + descriptor::DescriptorCluster::new(stack.matter().borrow()), + ) + + // Run the Matter stack with our handler + let mut matter = pin!(async { + stack.run(peripherals.modem, nvs, (NODE, handler)).await + }); + + // Just for demoing purposes: + // + // Run a sample loop that simulates state changes triggered by the HAL + // Changes will be properly communicated to the Matter controllers (i.e. Google Home, Alexa) + // and other Matter devices thanks to subscriptions + let mut device = pin!(async { + loop { + // Simulate user toggling the light with a physical switch every 5 seconds + Timer::after(Duration::from_secs(5)).await; + + // Toggle + on_off.set(!on_off.get()); + + // Let the Matter stack know that we have changed the state of our Lamp device + stack.notify_changed(); + + info!("Lamp toggled"); + } + }); + + // Schedule both the Matter loop & the device loop together + esp_idf_svc::hal::task::block_on(select(matter, device).coalesce())?; + + Ok(()) +} + +/// The Matter stack is allocated statically to avoid program stack blowups +static StaticConstCell> MATTER_STACK: StaticConstCell::new(MatterStack::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(), +)); + +/// Endpoint 0 (the root endpoint) always runs the hidden Matter system clusters, so we pick ID=1 +const LIGHT_ENDPOINT_ID: usize = 1; + +/// The Matter Light device Node +const NODE: Node<'static> = Node { + id: 0, + endpoints: &[ + Matter::::root_metadata(), + Endpoint { + id: LIGHT_ENDPOINT_ID, + device_type: DEV_TYPE_ON_OFF_LIGHT, + clusters: &[descriptor::CLUSTER, cluster_on_off::CLUSTER], + }, + ], +}; +``` + +(See also [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]() 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]() -* [Non-volatile storage for Matter data (fabrics and ACLs)]() on top of the ESP IDF NVS flash API +* [Non-volatile storage for Matter persistent data (fabrics, ACLs and network connections)]() on top of the ESP IDF NVS flash API * mDNS: * Optional [Matter mDNS responder implementation]() based on the ESP IDF mDNS responder (use if you need to register other services besides Matter in mDNS) - * [UDP-multicast workarounds]() for `rs-matter`'s built-in mDNS responder, specifc to the Rust wrappers of ESP IDF + * [UDP-multicast workarounds]() for `rs-matter`'s built-in mDNS responder, addressing bugs in the Rust STD wrappers of ESP IDF + +#### 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) -For enabling UDP and TCP networking in `rs-matter`, just use the [`async-io`]() crate which has support for ESP IDF out of the box. +#### Additional building blocks provided by `rs-matter` 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` crate has support for ESP IDF out of the box +* Random number generator + * Enable the [`std`] feature on `rs-matter`. This way, the [`rng`]() 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, the [`rng`]() crate will be utilized, which has support for ESP IDF out of the box ## Build Prerequisites diff --git a/examples/on_off_wifi_ble.rs b/examples/on_off_wifi_ble.rs new file mode 100644 index 0000000..9ec2a52 --- /dev/null +++ b/examples/on_off_wifi_ble.rs @@ -0,0 +1,96 @@ +//! An example utilizing the `MatterStack` struct. +//! As the name suggests, this Matter stack assembly uses Wifi as the main transport, and BLE for commissioning. +//! +//! The example implements a fictitious Light device (an on-off cluster). + +fn main() -> Result<(), Error> { + // Take the Matter stack (can be done only once), as we'll run it in this thread + let stack = MATTER_STACK.take().unwrap(); + + // We need to pass the `modem` peripheral to the Matter stack, as it manages Bluetooth & Wifi by itself + // + // If we want to use Bluetooth for our own needs, that's possible post-commissioning, as long as the + // coexist ESP IDF driver is loaded. We just need to then split the `modem` peripheral into BT and Wifi, + // and only pass the Wifi half. + let peripherals = Peripherals::take()?; + + // Matter needs (a clone of) the ESP IDF NVS service too + let nvs = EspDefaultNvs::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()); + + // Chain our endpoint clusters with the (root) Endpoint 0 system clusters in the final handler + let handler = stack + .root_handler() + // Our on-off cluster, on Endpoint 1 + .chain(LIGHT_ENDPOINT_ID, cluster_on_off::ID, on_off) + // Each Endpoint needs a Descriptor cluster too + // Just use the one that `rs-matter` provides out of the box + .chain( + LIGHT_ENDPOINT_ID, + descriptor::ID, + descriptor::DescriptorCluster::new(stack.matter().borrow()), + ); + + // Run the Matter stack with our handler + let mut matter = pin!(async { stack.run(peripherals.modem, nvs, (NODE, handler)).await }); + + // Just for demoing purposes: + // + // Run a sample loop that simulates state changes triggered by the HAL + // Changes will be properly communicated to the Matter controllers (i.e. Google Home, Alexa) + // and other Matter devices thanks to subscriptions + let mut device = pin!(async { + loop { + // Simulate user toggling the light with a physical switch every 5 seconds + Timer::after(Duration::from_secs(5)).await; + + // Toggle + on_off.set(!on_off.get()); + + // Let the Matter stack know that we have changed the state of our Lamp device + stack.notify_changed(); + + info!("Lamp toggled"); + } + }); + + // Schedule both the Matter loop & the device loop together + esp_idf_svc::hal::task::block_on(select(matter, device).coalesce())?; + + Ok(()) +} + +/// The Matter stack is allocated statically to avoid program stack blowups +static MATTER_STACK: StaticConstCell> = + StaticConstCell::new(MatterStack::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(), + )); + +/// Endpoint 0 (the root endpoint) always runs the hidden Matter system clusters, so we pick ID=1 +const LIGHT_ENDPOINT_ID: usize = 1; + +/// The Matter Light device Node +const NODE: Node<'static> = Node { + id: 0, + endpoints: &[ + Matter::::root_metadata(), + Endpoint { + id: LIGHT_ENDPOINT_ID, + device_type: DEV_TYPE_ON_OFF_LIGHT, + clusters: &[descriptor::CLUSTER, cluster_on_off::CLUSTER], + }, + ], +}; diff --git a/src/ble.rs b/src/ble.rs index 8dbde01..66b35fa 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -14,17 +14,19 @@ use esp_idf_svc::bt::ble::gatt::{ GattServiceId, GattStatus, Handle, Permission, Property, }; 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 log::{info, trace, warn}; +use log::warn; -use rs_matter::error::{Error, ErrorCode}; +use rs_matter::error::ErrorCode; use rs_matter::transport::network::btp::{ AdvData, GattPeripheral, GattPeripheralEvent, C1_CHARACTERISTIC_UUID, C1_MAX_LEN, C2_CHARACTERISTIC_UUID, C2_MAX_LEN, MATTER_BLE_SERVICE_UUID16, MAX_BTP_SESSIONS, }; use rs_matter::transport::network::BtAddr; -use rs_matter::utils::std_mutex::StdRawMutex; + +use crate::error::Error; const MAX_CONNECTIONS: usize = MAX_BTP_SESSIONS; @@ -52,7 +54,7 @@ where { gap: EspBleGap<'d, M, T>, gatts: EspGatts<'d, M, T>, - state: Mutex>, + state: Mutex>, } impl<'d, M, T> PeripheralState<'d, M, T> @@ -79,21 +81,13 @@ where let conn = self.state.lock(|state| { let state = state.borrow(); - let Some(gatts_if) = state.gatt_if else { - return None; - }; - - let Some(c2_handle) = state.c2_handle else { - return None; - }; + let gatts_if = state.gatt_if?; + let c2_handle = state.c2_handle?; - let Some(conn) = state + let conn = state .connections .iter() - .find(|conn| conn.peer.addr() == address.0 && conn.subscribed) - else { - return None; - }; + .find(|conn| conn.peer.addr() == address.0 && conn.subscribed)?; Some((gatts_if, conn.conn_id, c2_handle)) }); @@ -108,12 +102,9 @@ where } fn on_gap_event(&self, event: BleGapEvent) -> Result<(), EspError> { - match event { - BleGapEvent::RawAdvertisingConfigured(status) => { - self.check_bt_status(status)?; - self.gap.start_advertising()?; - } - _ => (), + if let BleGapEvent::RawAdvertisingConfigured(status) = event { + self.check_bt_status(status)?; + self.gap.start_advertising()?; } Ok(()) @@ -457,6 +448,7 @@ where Ok(()) } + #[allow(clippy::too_many_arguments)] fn write( &self, gatt_if: GattInterface, @@ -478,13 +470,10 @@ where let c2_handle = state.c2_handle; let c2_cccd_handle = state.c2_cccd_handle; - let Some(conn) = state + let conn = state .connections .iter_mut() - .find(|conn| conn.conn_id == conn_id) - else { - return None; - }; + .find(|conn| conn.conn_id == conn_id)?; if c2_cccd_handle == Some(handle) { // TODO: What if this needs a response? @@ -498,54 +487,46 @@ where addr.into(), ))); } - } else { - if conn.subscribed { - conn.subscribed = false; - return Some(GattPeripheralEvent::NotifyUnsubscribed(BtAddr( - addr.into(), - ))); - } + } else if conn.subscribed { + conn.subscribed = false; + return Some(GattPeripheralEvent::NotifyUnsubscribed(BtAddr(addr.into()))); } } - } else if c2_handle == Some(handle) { - if offset == 0 { - // TODO: Is it safe to report the write before it was confirmed? - return Some(GattPeripheralEvent::Write { - address: BtAddr(addr.into()), - data: value, - }); - } + } else if c2_handle == Some(handle) && offset == 0 { + // TODO: Is it safe to report the write before it was confirmed? + return Some(GattPeripheralEvent::Write { + address: BtAddr(addr.into()), + data: value, + }); } None }); if let Some(event) = event { - if matches!(event, GattPeripheralEvent::Write { .. }) { - if need_rsp { - let response = if is_prep { - // TODO: Do not allocate on-stack - let mut response = GattResponse::new(); - - response - .attr_handle(handle) - .auth_req(0) - .offset(0) - .value(value)?; - - Some(response) - } else { - None - }; - - self.gatts.send_response( - gatt_if, - conn_id, - trans_id, - GattStatus::Ok, - response.as_ref(), - )?; - } + if matches!(event, GattPeripheralEvent::Write { .. }) && need_rsp { + let response = if is_prep { + // TODO: Do not allocate on-stack + let mut response = GattResponse::new(); + + response + .attr_handle(handle) + .auth_req(0) + .offset(0) + .value(value)?; + + Some(response) + } else { + None + }; + + self.gatts.send_response( + gatt_if, + conn_id, + trans_id, + GattStatus::Ok, + response.as_ref(), + )?; } callback(event); @@ -566,7 +547,7 @@ where M: BleEnabled, { /// Create a new instance. - pub fn new(driver: T) -> Result { + pub fn new(driver: T) -> Result { Ok(Self(Arc::new(PeripheralState::new(driver)?))) } @@ -575,7 +556,7 @@ where service_name: &str, service_adv_data: &AdvData, mut callback: F, - ) -> Result<(), EspError> + ) -> Result<(), Error> where F: FnMut(GattPeripheralEvent) + Send + Clone + 'static, T: Send + 'static, @@ -607,8 +588,8 @@ where } /// Indicate new data on characteristic `C2` to a remote peer. - pub fn indicate(&self, data: &[u8], address: BtAddr) -> Result { - self.0.indicate(data, address) + pub fn indicate(&self, data: &[u8], address: BtAddr) -> Result { + Ok(self.0.indicate(data, address)?) } } @@ -617,7 +598,12 @@ where T: Borrow> + Clone + Send + 'static, M: BleEnabled + 'static, { - async fn run(&self, service_name: &str, adv_data: &AdvData, callback: F) -> Result<(), Error> + async fn run( + &self, + service_name: &str, + adv_data: &AdvData, + callback: F, + ) -> Result<(), rs_matter::error::Error> where F: FnMut(GattPeripheralEvent) + Send + Clone + 'static, { @@ -627,7 +613,7 @@ where core::future::pending().await } - async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error> { + async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), rs_matter::error::Error> { // TODO: Is indicate blocking? if BluedroidGattPeripheral::indicate(self, data, address) .map_err(|_| ErrorCode::BtpError)? diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..73f8819 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,46 @@ +use core::fmt::{self, Display}; + +use esp_idf_svc::sys::EspError; + +#[derive(Debug)] +pub enum Error { + Matter(rs_matter::error::Error), + Esp(EspError), +} + +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), + } + } +} + +#[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) + } +} + +#[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 b9b47d6..c934331 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,290 @@ +use core::net::{Ipv4Addr, Ipv6Addr}; +use core::pin::pin; + +use std::io; +use std::net::UdpSocket; + +use async_io::Async; +use embassy_futures::select::{select, select4}; +use embassy_sync::blocking_mutex::raw::{NoopRawMutex, RawMutex}; + +use esp_idf_svc::bt::{BleEnabled, BtDriver}; +use esp_idf_svc::eth::{AsyncEth, EspEth}; +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, NetifConfiguration, NetifStack}; +use esp_idf_svc::nvs::{EspNvs, EspNvsPartition, NvsPartitionId}; + +use esp_idf_svc::sys::{esp, esp_netif_get_ip6_linklocal, EspError, ESP_FAIL}; + +use esp_idf_svc::timer::EspTaskTimerService; +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, Endpoint, HandlerCompat, Metadata, +}; +use rs_matter::data_model::root_endpoint; +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::core::MATTER_SOCKET_BIND_ADDR; +use rs_matter::transport::network::btp::{Btp, BtpContext}; +use rs_matter::transport::network::{NetworkReceive, NetworkSend}; +use rs_matter::utils::buf::{BufferAccess, PooledBuffers}; +use rs_matter::utils::select::Coalesce; +use rs_matter::{CommissioningData, Matter, MATTER_PORT}; + +extern crate alloc; + +use crate::ble::BluedroidGattPeripheral; +use crate::error::Error; +use crate::multicast::{join_multicast_v4, join_multicast_v6}; +use crate::netif::{wait_ips_down, wait_ips_up}; pub mod ble; +pub mod error; +pub mod mdns; +pub mod multicast; +pub mod netif; pub mod nvs; +pub mod wifi; + +pub trait Network { + const INIT: Self; +} + +pub struct Eth(()); + +impl Network for Eth { + const INIT: Self = Self(()); +} + +pub struct WifiBle { + btp_context: BtpContext, +} + +impl WifiBle { + const fn new() -> Self { + Self { + btp_context: BtpContext::new(), + } + } +} + +impl Network for WifiBle { + const INIT: Self = Self::new(); +} + +pub struct MatterStack<'a, T> +where + T: Network, +{ + matter: Matter<'a>, + buffers: PooledBuffers<10, NoopRawMutex, IMBuffer>, + psm_buffer: PooledBuffers<1, NoopRawMutex, heapless::Vec>, + subscriptions: Subscriptions<3>, + #[allow(unused)] + network: T, +} + +impl<'a, T> MatterStack<'a, T> +where + T: Network, +{ + pub const fn new( + dev_det: &'static BasicInfoConfig, + dev_att: &'static dyn DevAttDataFetcher, + ) -> 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: T::INIT, + } + } + + pub const fn matter(&self) -> &Matter<'a> { + &self.matter + } + + pub fn notify_changed(&self) { + self.subscriptions.notify_changed(); + } + + pub fn reset(&self) { + todo!() + } + + async fn run_psm( + &self, + nvs: EspNvsPartition

, + network: nvs::Network<'_, '_, N, M>, + ) -> Result<(), Error> + where + P: NvsPartitionId, + M: RawMutex, + { + 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 + } + + 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::<4, 4>()) + ); + + // Run the responder with up to 4 handlers (i.e. 4 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::<4, 4>().await?; + + Ok(()) + } + + async fn run_builtin_mdns( + &self, + netif: &EspNetif, + sysloop: EspSystemEventLoop, + ) -> Result<(), Error> { + use rs_matter::mdns::{ + Host, MDNS_IPV4_BROADCAST_ADDR, MDNS_IPV6_BROADCAST_ADDR, MDNS_SOCKET_BIND_ADDR, + }; + + loop { + let (ipv4, ipv6) = wait_ips_up(netif, sysloop.clone()).await?; + + let socket = async_io::Async::::bind(MDNS_SOCKET_BIND_ADDR)?; + + join_multicast_v4(&socket, MDNS_IPV4_BROADCAST_ADDR, Ipv4Addr::UNSPECIFIED)?; + join_multicast_v6(&socket, MDNS_IPV6_BROADCAST_ADDR, 0)?; + + let mut mdns = pin!(async { + self.matter() + .run_builtin_mdns( + &socket, + &socket, + Host { + id: 0, + hostname: self.matter().dev_det().device_name, + ip: ipv4.octets(), + ipv6: Some(ipv6.octets()), + }, + Some(0), + ) + .await?; + + Ok(()) + }); + + let mut ips = pin!(wait_ips_down(netif, sysloop.clone())); + + select(&mut ips, &mut mdns).coalesce().await?; + } + } + + async fn run_transport( + &self, + send: S, + recv: R, + dev_comm: CommissioningData, + disc_caps: DiscoveryCapabilities, + ) -> Result<(), Error> + where + S: NetworkSend, + R: NetworkReceive, + { + self.matter() + .run(send, recv, Some((dev_comm, disc_caps))) + .await?; + + Ok(()) + } + + fn create_udp(&self) -> Result, Error> { + let socket = async_io::Async::::bind(MATTER_SOCKET_BIND_ADDR)?; + + Ok(socket) + } + + fn create_btp(&self) -> Result, Error> { + let socket = async_io::Async::::bind(MATTER_SOCKET_BIND_ADDR)?; + + Ok(socket) + } +} + +impl<'a> MatterStack<'a, Eth> { + pub const fn root_metadata() -> Endpoint<'static> { + root_endpoint::endpoint(0) + } + + pub fn root_handler(&self) -> impl AsyncHandler + '_ { + HandlerCompat(root_endpoint::handler(0, self.matter())) + } + + pub async fn run<'d, T, P, D>( + &self, + sysloop: EspSystemEventLoop, + nvs: EspNvsPartition

, + mut eth: EspEth<'d, D>, + dev_comm: CommissioningData, + handler: T, + ) -> Result<(), Error> + where + T: AsyncHandler + AsyncMetadata, + P: NvsPartitionId, + { + let _ = eth.stop()?; + + eth.swap_netif(EspNetif::new_with_conf( + &NetifConfiguration::eth_default_client(), + )?)?; + eth.start()?; + + let udp = self.create_udp()?; + + let mut psm = pin!(self.run_psm(nvs, nvs::Network::<0, NoopRawMutex>::None)); + let mut mdns = pin!(self.run_builtin_mdns(eth.netif(), sysloop)); + let mut transport = pin!(self.run_transport( + &udp, + &udp, + dev_comm, + DiscoveryCapabilities::new(true, false, false) + )); + let mut respond = pin!(self.run_responder(handler)); + + select4(&mut psm, &mut mdns, &mut transport, &mut respond) + .coalesce() + .await?; + + Ok(()) + } +} diff --git a/src/mdns.rs b/src/mdns.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/mdns.rs @@ -0,0 +1 @@ + diff --git a/src/multicast.rs b/src/multicast.rs new file mode 100644 index 0000000..a050942 --- /dev/null +++ b/src/multicast.rs @@ -0,0 +1,82 @@ +#![cfg(feature = "std")] + +use core::net::{Ipv4Addr, Ipv6Addr}; + +use std::net::UdpSocket; + +use async_io::Async; + +use log::info; + +use rs_matter::error::{Error, ErrorCode}; + +pub fn join_multicast_v6( + socket: &Async, + multiaddr: Ipv6Addr, + interface: u32, +) -> Result<(), Error> { + socket.as_ref().join_multicast_v6(&multiaddr, interface)?; + + info!("Joined IPV6 multicast {}/{}", multiaddr, interface); + + Ok(()) +} + +pub fn join_multicast_v4( + socket: &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")] + { + fn esp_setsockopt( + socket: &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(()) + } + + 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(()) +} diff --git a/src/netif.rs b/src/netif.rs new file mode 100644 index 0000000..411c691 --- /dev/null +++ b/src/netif.rs @@ -0,0 +1,77 @@ +use core::net::{Ipv4Addr, Ipv6Addr}; + +use esp_idf_svc::eventloop::EspSystemEventLoop; +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 log::info; + +use crate::error::Error; + +pub async fn wait_ips_up( + netif: &EspNetif, + sysloop: EspSystemEventLoop, +) -> Result<(Ipv4Addr, Ipv6Addr), Error> { + // TODO: Maybe wait on Wifi and Eth events as well + let mut subscription = sysloop.subscribe_async::()?; + + loop { + if let Ok((ipv4, ipv6)) = get_ips(netif) { + break Ok((ipv4, ipv6)); + } + + subscription.recv().await?; + } +} + +pub async fn wait_ips_down(netif: &EspNetif, sysloop: EspSystemEventLoop) -> Result<(), Error> { + // TODO: Maybe wait on Wifi and Eth events as well + let mut subscription = sysloop.subscribe_async::()?; + + loop { + if get_ips(netif).is_err() { + break Ok(()); + } + + subscription.recv().await?; + } +} + +pub fn get_ips(netif: &EspNetif) -> Result<(Ipv4Addr, Ipv6Addr), Error> { + 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(); + + info!("Waiting for IPv6 address"); + + 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(); + + Ok((ipv4, ipv6)) +} diff --git a/src/nvs.rs b/src/nvs.rs index 45b1f13..7173343 100644 --- a/src/nvs.rs +++ b/src/nvs.rs @@ -1,84 +1,159 @@ -use esp_idf_svc::{nvs::{EspNvs, NvsPartitionId}, sys::EspError}; +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; -pub struct Psm<'a, T> -where +use crate::{error::Error, wifi::WifiCommCluster}; + +pub enum Network<'a, 'd, const N: usize, M> +where + M: RawMutex, +{ + None, + Wifi(&'a WifiCommCluster<'a, 'd, N, M>), +} + +impl<'a, 'd, const N: usize, M> Network<'a, 'd, N, M> +where + M: RawMutex, +{ + const fn key(&self) -> Option<&str> { + match self { + Self::None => None, + Self::Wifi(_) => Some("wifi"), + } + } +} + +pub struct Psm<'a, 'd, T, const N: usize, M> +where T: NvsPartitionId, + M: RawMutex, { matter: &'a Matter<'a>, + network: Network<'a, 'd, N, M>, nvs: EspNvs, + buf: &'a mut [u8], } -impl<'a, T> Psm<'a, T> -where +impl<'a, 'd, T, const N: usize, M> Psm<'a, 'd, T, N, M> +where T: NvsPartitionId, + M: RawMutex, { #[inline(always)] - pub fn new(matter: &'a Matter<'a>, nvs: EspNvs, buf: &mut [u8]) -> Result { + pub fn new( + matter: &'a Matter<'a>, + network: Network<'a, 'd, N, M>, + nvs: EspNvs, + buf: &'a mut [u8], + ) -> Result { Ok(Self { matter, + network, nvs, + buf, }) } - pub async fn run(&mut self) -> Result<(), EspError> { + pub async fn run(&mut self) -> Result<(), Error> { + self.load().await?; + loop { self.matter.wait_changed().await; + self.store().await?; + } + } - if self.matter.is_changed() { - if let Some(data) = self.matter.store_acls(&mut self.buf)? { - Self::store(&self.dir, "acls", data)?; - } + 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(data) = self.matter.store_fabrics(&mut self.buf)? { - Self::store(&self.dir, "fabrics", data)?; - } - } + if let Some(nw_key) = self.network.key() { + Self::remove_blob(&mut self.nvs, nw_key).await?; } - } - fn load<'b>(dir: &Path, key: &str, buf: &'b mut [u8]) -> Result, EspError> { - let path = dir.join(key); + // TODO: Reset the Matter state - match fs::File::open(path) { - Ok(mut file) => { - let mut offset = 0; + Ok(()) + } - loop { - if offset == buf.len() { - Err(ErrorCode::NoSpace)?; - } + 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)?; + } - let len = file.read(&mut buf[offset..])?; + if let Some(data) = Self::load_blob(&mut self.nvs, "fabrics", self.buf).await? { + self.matter.load_fabrics(data)?; + } - if len == 0 { - break; - } + 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)?; + } + } - offset += len; - } + Ok(()) + } - let data = &buf[..offset]; + 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?; + } - info!("Key {}: loaded {} bytes {:?}", key, data.len(), data); + if let Some(data) = self.matter.store_fabrics(self.buf)? { + Self::store_blob(&mut self.nvs, "fabrics", data).await?; + } + } - Ok(Some(data)) + 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?; } - Err(_) => Ok(None), } + + Ok(()) } - fn store(dir: &Path, key: &str, data: &[u8]) -> Result<(), EspError> { - let path = dir.join(key); + 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(()) + } - let mut file = fs::File::create(path)?; + async fn remove_blob(nvs: &mut EspNvs, key: &str) -> Result<(), EspError> { + // TODO: Not really async - file.write_all(data)?; + nvs.remove(key)?; - info!("Key {}: stored {} bytes {:?}", key, data.len(), data); + info!("Blob {key}: removed"); Ok(()) } diff --git a/src/wifi.rs b/src/wifi.rs new file mode 100644 index 0000000..4ffc09f --- /dev/null +++ b/src/wifi.rs @@ -0,0 +1,834 @@ +use core::cell::RefCell; + +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::{self, raw::RawMutex}; +use embassy_sync::mutex::Mutex; + +use esp_idf_svc::sys::{EspError, ESP_ERR_INVALID_STATE, ESP_FAIL}; +use esp_idf_svc::wifi::{self as wifi, AccessPointInfo, AsyncWifi, AuthMethod, EspWifi}; + +use log::{error, info}; + +use rs_matter::data_model::objects::{ + AsyncHandler, AttrDataEncoder, AttrDataWriter, AttrDetails, AttrType, CmdDataEncoder, + CmdDetails, Dataver, +}; +use rs_matter::data_model::sdm::nw_commissioning::{ + Attributes, Commands, NetworkCommissioningStatus, NwInfo, ResponseCommands, 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::{ + self, FromTLV, OctetStr, TLVArray, TLVElement, TLVList, TLVWriter, TagType, ToTLV, +}; +use rs_matter::transport::exchange::Exchange; +use rs_matter::utils::notification::Notification; +use rs_matter::utils::{rand::Rand, writebuf::WriteBuf}; + +use strum::FromRepr; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, FromTLV, ToTLV, FromRepr)] +pub enum WiFiSecurity { + Unencrypted = 0x01, + Wep = 0x02, + WpaPersonal = 0x04, + Wpa2Personal = 0x08, + Wpa3Personal = 0x10, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, FromTLV, ToTLV, FromRepr)] +pub enum WifiBand { + B3G4 = 0x01, + B3G65 = 0x02, + B5G = 0x04, + B6G = 0x08, + B60G = 0x10, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct WiFiInterfaceScanResult<'a> { + pub security: WiFiSecurity, + pub ssid: OctetStr<'a>, + pub bssid: OctetStr<'a>, + pub channel: u16, + pub band: Option, + pub rssi: Option, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct ThreadInterfaceScanResult<'a> { + pub pan_id: u16, + pub extended_pan_id: u64, + pub network_name: OctetStr<'a>, + pub channel: u16, + pub version: u8, + pub extended_address: OctetStr<'a>, + pub rssi: i8, + pub lqi: u8, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct WifiNetwork<'a> { + ssid: OctetStr<'a>, + bssid: OctetStr<'a>, + channel: u16, + security: WiFiSecurity, + band: WifiBand, + rssi: u8, +} +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct ScanNetworksRequest<'a> { + pub ssid: Option>, + pub breadcrumb: Option, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct ScanNetworksResponse<'a> { + pub status: NetworkCommissioningStatus, + pub debug_text: Option>, + pub wifi_scan_results: Option>>, + pub thread_scan_results: Option>>, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ScanNetworksResponseTag { + Status = 0, + DebugText = 1, + WifiScanResults = 2, + ThreadScanResults = 3, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct AddWifiNetworkRequest<'a> { + pub ssid: OctetStr<'a>, + pub credentials: OctetStr<'a>, + pub breadcrumb: Option, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct AddThreadNetworkRequest<'a> { + pub op_dataset: OctetStr<'a>, + pub breadcrumb: Option, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct RemoveNetworkRequest<'a> { + pub network_id: OctetStr<'a>, + pub breadcrumb: Option, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct NetworkConfigResponse<'a> { + pub status: NetworkCommissioningStatus, + pub debug_text: Option>, + pub network_index: Option, +} + +pub type ConnectNetworkRequest<'a> = RemoveNetworkRequest<'a>; + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct ReorderNetworkRequest<'a> { + pub network_id: OctetStr<'a>, + pub index: u8, + pub breadcrumb: Option, +} + +#[derive(Debug, Clone, FromTLV, ToTLV)] +#[tlvargs(lifetime = "'a")] +pub struct ConnectNetworkResponse<'a> { + pub status: NetworkCommissioningStatus, + pub debug_text: Option>, + pub error_value: i32, +} + +struct LastNetworkOutcome { + status: NetworkCommissioningStatus, + id: heapless::String<32>, + value: i32, +} + +#[derive(Debug, Clone, ToTLV, FromTLV)] +struct WifiCredentials { + ssid: heapless::String<32>, + password: heapless::String<64>, +} + +struct WifiState { + networks: heapless::Vec, + last_network_outcome: Option, + changed: bool, +} + +pub struct WifiCommCluster<'a, 'd, const N: usize, M> +where + M: RawMutex, +{ + data_ver: Dataver, + state: blocking_mutex::Mutex>>, + wifi: Option>>>, + connected: &'a Notification, +} + +impl<'a, 'd, const N: usize, M> WifiCommCluster<'a, 'd, N, M> +where + M: RawMutex, +{ + pub fn new( + rand: Rand, + wifi: Option>>, + connected: &'a Notification, + ) -> Self { + Self { + data_ver: Dataver::new(rand), + state: blocking_mutex::Mutex::new(RefCell::new(WifiState { + networks: heapless::Vec::new(), + last_network_outcome: None, + changed: false, + })), + wifi: wifi.map(Mutex::new), + connected, + } + } + + pub async fn connect(&self) -> Result<(), EspError> { + let response = self + .do_connect(None) + .await + .ok_or(EspError::from_infallible::())?; + + if matches!(response.status, NetworkCommissioningStatus::Success) { + Ok(()) + } else { + Err(EspError::from_infallible::()) // TODO + } + } + + pub fn load(&self, data: &[u8]) -> Result<(), Error> { + self.state.lock(|state| { + let mut state = state.borrow_mut(); + + let root = TLVList::new(data).iter().next().ok_or(ErrorCode::Invalid)?; + + tlv::from_tlv(&mut state.networks, &root)?; + + state.changed = false; + + Ok(()) + }) + } + + pub fn store<'m>(&self, buf: &'m mut [u8]) -> Result, Error> { + self.state.lock(|state| { + let mut state = state.borrow_mut(); + + if !state.changed { + return Ok(None); + } + + let mut wb = WriteBuf::new(buf); + let mut tw = TLVWriter::new(&mut wb); + + state + .networks + .as_slice() + .to_tlv(&mut tw, TagType::Anonymous)?; + + state.changed = false; + + let len = tw.get_tail(); + + Ok(Some(&buf[..len])) + }) + } + + async 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.state.lock(|state| { + for network in &state.borrow().networks { + let nw_info = NwInfo { + network_id: OctetStr(network.ssid.as_str().as_bytes()), + connected: false, // TODO + }; + + 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.state.lock(|state| { + AttrType::new().encode( + writer, + state + .borrow() + .last_network_outcome + .as_ref() + .map(|o| o.status as u8), + ) + }), + Attributes::LastNetworkID => self.state.lock(|state| { + AttrType::new().encode( + writer, + state + .borrow() + .last_network_outcome + .as_ref() + .map(|o| OctetStr(o.id.as_str().as_bytes())), + ) + }), + Attributes::LastConnectErrorValue => self.state.lock(|state| { + AttrType::new().encode( + writer, + state + .borrow() + .last_network_outcome + .as_ref() + .map(|o| o.value), + ) + }), + } + } + } else { + Ok(()) + } + } + + 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) + .await?; + } + Commands::AddOrUpdateWifiNetwork => { + info!("AddOrUpdateWifiNetwork"); + self.add_network(exchange, &AddWifiNetworkRequest::from_tlv(data)?, encoder) + .await?; + } + Commands::RemoveNetwork => { + info!("RemoveNetwork"); + self.remove_network(exchange, &RemoveNetworkRequest::from_tlv(data)?, encoder) + .await?; + } + 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) + .await?; + } + other => { + error!("{other:?} (not supported)"); + todo!() + } + } + + self.data_ver.changed(); + + Ok(()) + } + + async fn scan_networks( + &self, + _exchange: &Exchange<'_>, + req: &ScanNetworksRequest<'_>, + encoder: CmdDataEncoder<'_, '_, '_>, + ) -> Result<(), Error> { + let mut tw = encoder.with_command(ResponseCommands::ScanNetworksResponse as _)?; + + let Some(result) = self.do_scan().await else { + // Non-concurrent commissioning - we don't have Wifi, hence we can't do scan + + Status::new(IMStatusCode::Busy, 0).to_tlv(&mut tw, TagType::Anonymous)?; + + return Ok(()); + }; + + tw.start_struct(TagType::Anonymous)?; + + match result { + Ok(aps) => { + tw.u8( + TagType::Context(ScanNetworksResponseTag::Status as _), + NetworkCommissioningStatus::Success as _, + )?; + + tw.start_array(TagType::Context( + ScanNetworksResponseTag::WifiScanResults as _, + ))?; + + for ap in &aps { + if req + .ssid + .map(|ssid| ssid.0 == ap.ssid.as_bytes()) + .unwrap_or(true) + { + let scan_result = WiFiInterfaceScanResult { + security: WiFiSecurity::Wpa2Personal, + ssid: OctetStr(ap.ssid.as_bytes()), + bssid: OctetStr(&ap.bssid), + channel: ap.channel as _, + band: None, + rssi: None, + }; + + scan_result.to_tlv(&mut tw, TagType::Anonymous)?; + } + } + + tw.end_container()?; + + info!("ScanNetworks success"); + } + Err(status) => { + tw.u8( + TagType::Context(ScanNetworksResponseTag::Status as _), + status as _, + )?; + + error!("ScanNetworks failed"); + } + } + + tw.end_container() + } + + async fn add_network( + &self, + exchange: &Exchange<'_>, + req: &AddWifiNetworkRequest<'_>, + encoder: CmdDataEncoder<'_, '_, '_>, + ) -> Result<(), Error> { + // TODO: Check failsafe status + + self.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 mut tw = 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; + exchange.matter().notify_changed(); + + NetworkConfigResponse { + status: NetworkCommissioningStatus::Success, + debug_text: None, + network_index: Some(index as _), + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } 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(), + }; + + if state.networks.push(network).is_ok() { + state.changed = true; + exchange.matter().notify_changed(); + + NetworkConfigResponse { + status: NetworkCommissioningStatus::Success, + debug_text: None, + network_index: Some(state.networks.len() as _), + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } else { + NetworkConfigResponse { + status: NetworkCommissioningStatus::BoundsExceeded, + debug_text: None, + network_index: None, + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } + } + + Ok(()) + }) + } + + async fn remove_network( + &self, + exchange: &Exchange<'_>, + req: &RemoveNetworkRequest<'_>, + encoder: CmdDataEncoder<'_, '_, '_>, + ) -> Result<(), Error> { + // TODO: Check failsafe status + + self.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 mut tw = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?; + + if let Some(index) = index { + // Found + state.networks.remove(index); + state.changed = true; + exchange.matter().notify_changed(); + + NetworkConfigResponse { + status: NetworkCommissioningStatus::Success, + debug_text: None, + network_index: Some(index as _), + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } else { + // Not found + NetworkConfigResponse { + status: NetworkCommissioningStatus::NetworkIdNotFound, + debug_text: None, + network_index: None, + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } + + Ok(()) + }) + } + + async fn connect_network( + &self, + _exchange: &Exchange<'_>, + req: &ConnectNetworkRequest<'_>, + encoder: CmdDataEncoder<'_, '_, '_>, + ) -> Result<(), Error> { + // TODO: Check failsafe status + let Some(response) = self + .do_connect(Some(core::str::from_utf8(req.network_id.0).unwrap())) + .await + else { + // Non-concurrent commissioning scenario (i.e. only BLE is active, and the ESP IDF co-exist mode is not enabled) + // Notify that we have received a connect command + + self.connected.notify(); + + // Block forever waitinng for the firware to restart + core::future::pending().await + }; + + // We have a Wifi, so we must be running in a concurrent commissioning mode + + let mut tw = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?; + + response.to_tlv(&mut tw, TagType::Anonymous)?; + + if matches!(response.status, NetworkCommissioningStatus::Success) { + self.data_ver.changed(); + } + + Ok(()) + } + + async fn reorder_network( + &self, + exchange: &Exchange<'_>, + req: &ReorderNetworkRequest<'_>, + encoder: CmdDataEncoder<'_, '_, '_>, + ) -> Result<(), Error> { + // TODO: Check failsafe status + + self.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 mut tw = 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; + exchange.matter().notify_changed(); + + NetworkConfigResponse { + status: NetworkCommissioningStatus::Success, + debug_text: None, + network_index: Some(req.index as _), + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } else { + NetworkConfigResponse { + status: NetworkCommissioningStatus::OutOfRange, + debug_text: None, + network_index: Some(req.index as _), + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } + } else { + // Not found + NetworkConfigResponse { + status: NetworkCommissioningStatus::NetworkIdNotFound, + debug_text: None, + network_index: None, + } + .to_tlv(&mut tw, TagType::Anonymous)?; + } + + Ok(()) + }) + } + + async fn do_scan( + &self, + ) -> Option, NetworkCommissioningStatus>> { + let wifi = self.wifi.as_ref()?; + + // TODO: Use IfMutex instead of Mutex + let mut wifi = wifi.lock().await; + + let result = Self::wifi_scan(&mut wifi).await; + + let status = if result.is_ok() { + NetworkCommissioningStatus::Success + } else { + NetworkCommissioningStatus::OtherConnectionFailure + }; + + self.state.lock(|state| { + let mut state = state.borrow_mut(); + + if let Some(last_network_outcome) = state.last_network_outcome.as_mut() { + last_network_outcome.status = status; + } else { + state.last_network_outcome = Some(LastNetworkOutcome { + status, + id: "".try_into().unwrap(), + value: 0, + }); + } + }); + + Some(result.map_err(|_| status)) + } + + async fn do_connect(&self, ssid: Option<&str>) -> Option> { + let wifi = self.wifi.as_ref()?; + + let creds = self.state.lock(|state| { + let state = state.borrow(); + + let creds = if let Some(ssid) = ssid { + state + .networks + .iter() + .find(|creds| creds.ssid.as_str().as_bytes() == ssid.as_bytes()) + } else { + state.networks.first() + }; + + creds.cloned() + }); + + let ssid = ssid + .map(|ssid| ssid.try_into().unwrap()) + .or_else(|| creds.as_ref().map(|creds| creds.ssid.clone())); + + let response = if let Some(creds) = creds { + // Found + + // TODO: Use IfMutex instead of Mutex + let mut wifi = wifi.lock().await; + + let result = Self::wifi_connect(&mut wifi, creds).await; + + ConnectNetworkResponse { + status: if result.is_ok() { + NetworkCommissioningStatus::Success + } else { + NetworkCommissioningStatus::OtherConnectionFailure + }, + debug_text: None, + error_value: 0, + } + } else { + // Not found + ConnectNetworkResponse { + status: NetworkCommissioningStatus::NetworkIdNotFound, + debug_text: None, + error_value: 1, // TODO + } + }; + + self.state.lock(|state| { + let mut state = state.borrow_mut(); + + state.last_network_outcome = Some(LastNetworkOutcome { + status: response.status, + id: ssid.unwrap(), + value: response.error_value, + }); + }); + + Some(response) + } + + async fn wifi_scan( + wifi: &mut AsyncWifi>, + ) -> Result, EspError> { + let _ = wifi.stop().await; + + wifi.set_configuration(&wifi::Configuration::Client( + wifi::ClientConfiguration::default(), + ))?; + wifi.start().await?; + + wifi.scan().await + } + + async fn wifi_connect( + wifi: &mut AsyncWifi>, + creds: WifiCredentials, + ) -> Result<(), EspError> { + let auth_methods: &[AuthMethod] = if creds.password.is_empty() { + &[AuthMethod::None] + } else { + &[ + AuthMethod::WPA2WPA3Personal, + AuthMethod::WPAWPA2Personal, + AuthMethod::WEP, + ] + }; + + let mut result = Ok(()); + + for auth_method in auth_methods.iter().copied() { + let connect = !matches!(auth_method, wifi::AuthMethod::None); + let conf = wifi::Configuration::Client(wifi::ClientConfiguration { + ssid: creds.ssid.clone(), + auth_method, + password: creds.password.clone(), + ..Default::default() + }); + + result = Self::wifi_connect_with(wifi, &conf, connect).await; + + if result.is_ok() { + break; + } + } + + result + } + + async fn wifi_connect_with( + wifi: &mut AsyncWifi>, + conf: &wifi::Configuration, + connect: bool, + ) -> Result<(), EspError> { + let _ = wifi.stop().await; + + wifi.set_configuration(conf)?; + wifi.start().await?; + + if connect { + wifi.connect().await?; + } + + Ok(()) + } +} + +impl<'a, 'd, const N: usize, M> AsyncHandler for WifiCommCluster<'a, 'd, N, M> +where + M: RawMutex, +{ + async fn read<'m>( + &'m self, + attr: &'m AttrDetails<'_>, + encoder: AttrDataEncoder<'m, '_, '_>, + ) -> Result<(), Error> { + WifiCommCluster::read(self, attr, encoder).await + } + + async fn invoke<'m>( + &'m self, + exchange: &'m Exchange<'_>, + cmd: &'m CmdDetails<'_>, + data: &'m TLVElement<'_>, + encoder: CmdDataEncoder<'m, '_, '_>, + ) -> Result<(), Error> { + WifiCommCluster::invoke(self, exchange, cmd, data, encoder).await + } +} + +// impl ChangeNotifier<()> for WifiCommCluster { +// fn consume_change(&mut self) -> Option<()> { +// self.data_ver.consume_change(()) +// } +// }