diff --git a/.github/configs/sdkconfig.defaults b/.github/configs/sdkconfig.defaults index a4cd52a..771bd42 100644 --- a/.github/configs/sdkconfig.defaults +++ b/.github/configs/sdkconfig.defaults @@ -22,7 +22,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=y CONFIG_BT_CLASSIC_ENABLED=y CONFIG_BTDM_CTRL_MODE_BLE_ONLY=n -CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y +CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n CONFIG_BTDM_CTRL_MODE_BTDM=n CONFIG_BT_A2DP_ENABLE=y CONFIG_BT_HFP_ENABLE=y diff --git a/Cargo.toml b/Cargo.toml index c5c43b6..b80ef29 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,37 @@ 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" } +esp-idf-sys = { path = "../esp-idf-sys" } +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", "embassy-time-driver", "experimental"] } +embedded-svc = { version = "0.27", default-features = false } 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] +embassy-time = { version = "0.3", features = ["generic-queue"] } +static_cell = "2.1" + +[[example]] +name = "light" +path = "examples/light.rs" +required-features = ["std"] diff --git a/README.md b/README.md index 3e2fae8..8e4ae1b 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,178 @@ [![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 features might need to be switched on or off depending on whether Matter is running in commissioning or operating mode, and also depending on the current network connectivity (as in e.g. Wifi signal lost). + +This crate provides an all-in-one `MatterStack` assembly. + +Instantiate it and then call `MatterStack::run(...)`. + +```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). + +use core::borrow::Borrow; +use core::pin::pin; + +use embassy_futures::select::select; +use embassy_time::{Duration, Timer}; + +use esp_idf_matter::{error::Error, MatterStack, WifiBle}; + +use esp_idf_svc::eventloop::EspSystemEventLoop; +use esp_idf_svc::hal::peripherals::Peripherals; +use esp_idf_svc::nvs::EspDefaultNvsPartition; + +use log::info; + +use rs_matter::data_model::cluster_basic_information::BasicInfoConfig; +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::{Endpoint, HandlerCompat, Node}; +use rs_matter::data_model::system_model::descriptor; +use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::select::Coalesce; +use rs_matter::CommissioningData; + +use static_cell::ConstStaticCell; + +#[path = "dev_att/dev_att.rs"] +mod dev_att; + +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(); + + // 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 = HandlerCompat( + 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 + // 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 matter = pin!(stack.run( + EspSystemEventLoop::take()?, // The Matter stack needs (a clone of) the system event loop + EspDefaultNvsPartition::take()?, // The Matter stack needs (a clone of) the default ESP IDF NVS partition too + Peripherals::take()?.modem, // The Matter stack needs the BT/Wifi modem peripheral + CommissioningData { + // Hard-coded for demo purposes + verifier: VerifierData::new_with_pw(123456, *stack.matter().borrow()), + discriminator: 250, + }, + (NODE, handler), + )); + + // 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 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: ConstStaticCell> = + ConstStaticCell::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: u16 = 1; + +/// The Matter Light device Node +const NODE: Node<'static> = Node { + id: 0, + endpoints: &[ + MatterStack::::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/dev_att/dev_att.rs b/examples/dev_att/dev_att.rs new file mode 100644 index 0000000..9174ccb --- /dev/null +++ b/examples/dev_att/dev_att.rs @@ -0,0 +1,165 @@ +/* + * + * Copyright (c) 2020-2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use rs_matter::data_model::sdm::dev_att::{DataType, DevAttDataFetcher}; +use rs_matter::error::{Error, ErrorCode}; + +pub struct HardCodedDevAtt {} + +impl HardCodedDevAtt { + pub const fn new() -> Self { + Self {} + } +} + +// credentials/examples/ExamplePAI.cpp FFF1 +const PAI_CERT: [u8; 463] = [ + 0x30, 0x82, 0x01, 0xcb, 0x30, 0x82, 0x01, 0x71, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x08, 0x56, + 0xad, 0x82, 0x22, 0xad, 0x94, 0x5b, 0x64, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x04, 0x03, 0x02, 0x30, 0x30, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0f, + 0x4d, 0x61, 0x74, 0x74, 0x65, 0x72, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, 0x50, 0x41, 0x41, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x01, 0x0c, + 0x04, 0x46, 0x46, 0x46, 0x31, 0x30, 0x20, 0x17, 0x0d, 0x32, 0x32, 0x30, 0x32, 0x30, 0x35, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0x0f, 0x39, 0x39, 0x39, 0x39, 0x31, 0x32, 0x33, 0x31, + 0x32, 0x33, 0x35, 0x39, 0x35, 0x39, 0x5a, 0x30, 0x3d, 0x31, 0x25, 0x30, 0x23, 0x06, 0x03, 0x55, + 0x04, 0x03, 0x0c, 0x1c, 0x4d, 0x61, 0x74, 0x74, 0x65, 0x72, 0x20, 0x44, 0x65, 0x76, 0x20, 0x50, + 0x41, 0x49, 0x20, 0x30, 0x78, 0x46, 0x46, 0x46, 0x31, 0x20, 0x6e, 0x6f, 0x20, 0x50, 0x49, 0x44, + 0x31, 0x14, 0x30, 0x12, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x01, + 0x0c, 0x04, 0x46, 0x46, 0x46, 0x31, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, + 0x04, 0x41, 0x9a, 0x93, 0x15, 0xc2, 0x17, 0x3e, 0x0c, 0x8c, 0x87, 0x6d, 0x03, 0xcc, 0xfc, 0x94, + 0x48, 0x52, 0x64, 0x7f, 0x7f, 0xec, 0x5e, 0x50, 0x82, 0xf4, 0x05, 0x99, 0x28, 0xec, 0xa8, 0x94, + 0xc5, 0x94, 0x15, 0x13, 0x09, 0xac, 0x63, 0x1e, 0x4c, 0xb0, 0x33, 0x92, 0xaf, 0x68, 0x4b, 0x0b, + 0xaf, 0xb7, 0xe6, 0x5b, 0x3b, 0x81, 0x62, 0xc2, 0xf5, 0x2b, 0xf9, 0x31, 0xb8, 0xe7, 0x7a, 0xaa, + 0x82, 0xa3, 0x66, 0x30, 0x64, 0x30, 0x12, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, + 0x08, 0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x00, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, + 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x01, 0x06, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, + 0x04, 0x16, 0x04, 0x14, 0x63, 0x54, 0x0e, 0x47, 0xf6, 0x4b, 0x1c, 0x38, 0xd1, 0x38, 0x84, 0xa4, + 0x62, 0xd1, 0x6c, 0x19, 0x5d, 0x8f, 0xfb, 0x3c, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, + 0x18, 0x30, 0x16, 0x80, 0x14, 0x6a, 0xfd, 0x22, 0x77, 0x1f, 0x51, 0x1f, 0xec, 0xbf, 0x16, 0x41, + 0x97, 0x67, 0x10, 0xdc, 0xdc, 0x31, 0xa1, 0x71, 0x7e, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x21, 0x00, 0xb2, 0xef, 0x27, + 0xf4, 0x9a, 0xe9, 0xb5, 0x0f, 0xb9, 0x1e, 0xea, 0xc9, 0x4c, 0x4d, 0x0b, 0xdb, 0xb8, 0xd7, 0x92, + 0x9c, 0x6c, 0xb8, 0x8f, 0xac, 0xe5, 0x29, 0x36, 0x8d, 0x12, 0x05, 0x4c, 0x0c, 0x02, 0x20, 0x65, + 0x5d, 0xc9, 0x2b, 0x86, 0xbd, 0x90, 0x98, 0x82, 0xa6, 0xc6, 0x21, 0x77, 0xb8, 0x25, 0xd7, 0xd0, + 0x5e, 0xdb, 0xe7, 0xc2, 0x2f, 0x9f, 0xea, 0x71, 0x22, 0x0e, 0x7e, 0xa7, 0x03, 0xf8, 0x91, +]; + +// credentials/examples/ExampleDACs.cpp FFF1-8000-0002-Cert +const DAC_CERT: [u8; 492] = [ + 0x30, 0x82, 0x01, 0xe8, 0x30, 0x82, 0x01, 0x8e, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x08, 0x52, + 0x72, 0x4d, 0x21, 0xe2, 0xc1, 0x74, 0xaf, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x04, 0x03, 0x02, 0x30, 0x3d, 0x31, 0x25, 0x30, 0x23, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1c, + 0x4d, 0x61, 0x74, 0x74, 0x65, 0x72, 0x20, 0x44, 0x65, 0x76, 0x20, 0x50, 0x41, 0x49, 0x20, 0x30, + 0x78, 0x46, 0x46, 0x46, 0x31, 0x20, 0x6e, 0x6f, 0x20, 0x50, 0x49, 0x44, 0x31, 0x14, 0x30, 0x12, + 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x01, 0x0c, 0x04, 0x46, 0x46, + 0x46, 0x31, 0x30, 0x20, 0x17, 0x0d, 0x32, 0x32, 0x30, 0x32, 0x30, 0x35, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x5a, 0x18, 0x0f, 0x39, 0x39, 0x39, 0x39, 0x31, 0x32, 0x33, 0x31, 0x32, 0x33, 0x35, + 0x39, 0x35, 0x39, 0x5a, 0x30, 0x53, 0x31, 0x25, 0x30, 0x23, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, + 0x1c, 0x4d, 0x61, 0x74, 0x74, 0x65, 0x72, 0x20, 0x44, 0x65, 0x76, 0x20, 0x44, 0x41, 0x43, 0x20, + 0x30, 0x78, 0x46, 0x46, 0x46, 0x31, 0x2f, 0x30, 0x78, 0x38, 0x30, 0x30, 0x32, 0x31, 0x14, 0x30, + 0x12, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x01, 0x0c, 0x04, 0x46, + 0x46, 0x46, 0x31, 0x31, 0x14, 0x30, 0x12, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, + 0x7c, 0x02, 0x02, 0x0c, 0x04, 0x38, 0x30, 0x30, 0x32, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, + 0x03, 0x42, 0x00, 0x04, 0xda, 0x93, 0xf1, 0x67, 0x36, 0x25, 0x67, 0x50, 0xd9, 0x03, 0xb0, 0x34, + 0xba, 0x45, 0x88, 0xab, 0xaf, 0x58, 0x95, 0x4f, 0x77, 0xaa, 0x9f, 0xd9, 0x98, 0x9d, 0xfd, 0x40, + 0x0d, 0x7a, 0xb3, 0xfd, 0xc9, 0x75, 0x3b, 0x3b, 0x92, 0x1b, 0x29, 0x4c, 0x95, 0x0f, 0xd9, 0xd2, + 0x80, 0xd1, 0x4c, 0x43, 0x86, 0x2f, 0x16, 0xdc, 0x85, 0x4b, 0x00, 0xed, 0x39, 0xe7, 0x50, 0xba, + 0xbf, 0x1d, 0xc4, 0xca, 0xa3, 0x60, 0x30, 0x5e, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, + 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, + 0x04, 0x04, 0x03, 0x02, 0x07, 0x80, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, + 0x14, 0xef, 0x06, 0x56, 0x11, 0x9c, 0x1c, 0x91, 0xa7, 0x9a, 0x94, 0xe6, 0xdc, 0xf3, 0x79, 0x79, + 0xdb, 0xd0, 0x7f, 0xf8, 0xa3, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, + 0x80, 0x14, 0x63, 0x54, 0x0e, 0x47, 0xf6, 0x4b, 0x1c, 0x38, 0xd1, 0x38, 0x84, 0xa4, 0x62, 0xd1, + 0x6c, 0x19, 0x5d, 0x8f, 0xfb, 0x3c, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, + 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x20, 0x46, 0x86, 0x81, 0x07, 0x33, 0xbf, 0x0d, + 0xc8, 0xff, 0x4c, 0xb5, 0x14, 0x5a, 0x6b, 0xfa, 0x1a, 0xec, 0xff, 0xa8, 0xb6, 0xda, 0xb6, 0xc3, + 0x51, 0xaa, 0xee, 0xcd, 0xaf, 0xb8, 0xbe, 0x95, 0x7d, 0x02, 0x21, 0x00, 0xe8, 0xc2, 0x8d, 0x6b, + 0xfc, 0xc8, 0x7a, 0x7d, 0x54, 0x2e, 0xad, 0x6e, 0xda, 0xca, 0x14, 0x8d, 0x5f, 0xa5, 0x06, 0x1e, + 0x51, 0x7c, 0xbe, 0x4f, 0x24, 0xa7, 0x20, 0xe1, 0xc0, 0x59, 0xde, 0x1a, +]; + +const DAC_PUBKEY: [u8; 65] = [ + 0x04, 0xda, 0x93, 0xf1, 0x67, 0x36, 0x25, 0x67, 0x50, 0xd9, 0x03, 0xb0, 0x34, 0xba, 0x45, 0x88, + 0xab, 0xaf, 0x58, 0x95, 0x4f, 0x77, 0xaa, 0x9f, 0xd9, 0x98, 0x9d, 0xfd, 0x40, 0x0d, 0x7a, 0xb3, + 0xfd, 0xc9, 0x75, 0x3b, 0x3b, 0x92, 0x1b, 0x29, 0x4c, 0x95, 0x0f, 0xd9, 0xd2, 0x80, 0xd1, 0x4c, + 0x43, 0x86, 0x2f, 0x16, 0xdc, 0x85, 0x4b, 0x00, 0xed, 0x39, 0xe7, 0x50, 0xba, 0xbf, 0x1d, 0xc4, + 0xca, +]; + +const DAC_PRIVKEY: [u8; 32] = [ + 0xda, 0xf2, 0x1a, 0x7e, 0xa4, 0x7a, 0x70, 0x48, 0x02, 0xa7, 0xe6, 0x6c, 0x50, 0xeb, 0x10, 0xba, + 0xc3, 0xbd, 0xd1, 0x68, 0x80, 0x39, 0x80, 0x66, 0xff, 0xda, 0xd7, 0xf5, 0x20, 0x98, 0xb6, 0x85, +]; + +// +const CERT_DECLARATION: [u8; 541] = [ + 0x30, 0x82, 0x02, 0x19, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, + 0x82, 0x02, 0x0a, 0x30, 0x82, 0x02, 0x06, 0x02, 0x01, 0x03, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x09, + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x82, 0x01, 0x71, 0x06, 0x09, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0xa0, 0x82, 0x01, 0x62, 0x04, 0x82, 0x01, 0x5e, + 0x15, 0x24, 0x00, 0x01, 0x25, 0x01, 0xf1, 0xff, 0x36, 0x02, 0x05, 0x00, 0x80, 0x05, 0x01, 0x80, + 0x05, 0x02, 0x80, 0x05, 0x03, 0x80, 0x05, 0x04, 0x80, 0x05, 0x05, 0x80, 0x05, 0x06, 0x80, 0x05, + 0x07, 0x80, 0x05, 0x08, 0x80, 0x05, 0x09, 0x80, 0x05, 0x0a, 0x80, 0x05, 0x0b, 0x80, 0x05, 0x0c, + 0x80, 0x05, 0x0d, 0x80, 0x05, 0x0e, 0x80, 0x05, 0x0f, 0x80, 0x05, 0x10, 0x80, 0x05, 0x11, 0x80, + 0x05, 0x12, 0x80, 0x05, 0x13, 0x80, 0x05, 0x14, 0x80, 0x05, 0x15, 0x80, 0x05, 0x16, 0x80, 0x05, + 0x17, 0x80, 0x05, 0x18, 0x80, 0x05, 0x19, 0x80, 0x05, 0x1a, 0x80, 0x05, 0x1b, 0x80, 0x05, 0x1c, + 0x80, 0x05, 0x1d, 0x80, 0x05, 0x1e, 0x80, 0x05, 0x1f, 0x80, 0x05, 0x20, 0x80, 0x05, 0x21, 0x80, + 0x05, 0x22, 0x80, 0x05, 0x23, 0x80, 0x05, 0x24, 0x80, 0x05, 0x25, 0x80, 0x05, 0x26, 0x80, 0x05, + 0x27, 0x80, 0x05, 0x28, 0x80, 0x05, 0x29, 0x80, 0x05, 0x2a, 0x80, 0x05, 0x2b, 0x80, 0x05, 0x2c, + 0x80, 0x05, 0x2d, 0x80, 0x05, 0x2e, 0x80, 0x05, 0x2f, 0x80, 0x05, 0x30, 0x80, 0x05, 0x31, 0x80, + 0x05, 0x32, 0x80, 0x05, 0x33, 0x80, 0x05, 0x34, 0x80, 0x05, 0x35, 0x80, 0x05, 0x36, 0x80, 0x05, + 0x37, 0x80, 0x05, 0x38, 0x80, 0x05, 0x39, 0x80, 0x05, 0x3a, 0x80, 0x05, 0x3b, 0x80, 0x05, 0x3c, + 0x80, 0x05, 0x3d, 0x80, 0x05, 0x3e, 0x80, 0x05, 0x3f, 0x80, 0x05, 0x40, 0x80, 0x05, 0x41, 0x80, + 0x05, 0x42, 0x80, 0x05, 0x43, 0x80, 0x05, 0x44, 0x80, 0x05, 0x45, 0x80, 0x05, 0x46, 0x80, 0x05, + 0x47, 0x80, 0x05, 0x48, 0x80, 0x05, 0x49, 0x80, 0x05, 0x4a, 0x80, 0x05, 0x4b, 0x80, 0x05, 0x4c, + 0x80, 0x05, 0x4d, 0x80, 0x05, 0x4e, 0x80, 0x05, 0x4f, 0x80, 0x05, 0x50, 0x80, 0x05, 0x51, 0x80, + 0x05, 0x52, 0x80, 0x05, 0x53, 0x80, 0x05, 0x54, 0x80, 0x05, 0x55, 0x80, 0x05, 0x56, 0x80, 0x05, + 0x57, 0x80, 0x05, 0x58, 0x80, 0x05, 0x59, 0x80, 0x05, 0x5a, 0x80, 0x05, 0x5b, 0x80, 0x05, 0x5c, + 0x80, 0x05, 0x5d, 0x80, 0x05, 0x5e, 0x80, 0x05, 0x5f, 0x80, 0x05, 0x60, 0x80, 0x05, 0x61, 0x80, + 0x05, 0x62, 0x80, 0x05, 0x63, 0x80, 0x18, 0x24, 0x03, 0x16, 0x2c, 0x04, 0x13, 0x5a, 0x49, 0x47, + 0x32, 0x30, 0x31, 0x34, 0x32, 0x5a, 0x42, 0x33, 0x33, 0x30, 0x30, 0x30, 0x33, 0x2d, 0x32, 0x34, + 0x24, 0x05, 0x00, 0x24, 0x06, 0x00, 0x25, 0x07, 0x94, 0x26, 0x24, 0x08, 0x00, 0x18, 0x31, 0x7d, + 0x30, 0x7b, 0x02, 0x01, 0x03, 0x80, 0x14, 0x62, 0xfa, 0x82, 0x33, 0x59, 0xac, 0xfa, 0xa9, 0x96, + 0x3e, 0x1c, 0xfa, 0x14, 0x0a, 0xdd, 0xf5, 0x04, 0xf3, 0x71, 0x60, 0x30, 0x0b, 0x06, 0x09, 0x60, + 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x04, 0x03, 0x02, 0x04, 0x47, 0x30, 0x45, 0x02, 0x20, 0x24, 0xe5, 0xd1, 0xf4, 0x7a, 0x7d, + 0x7b, 0x0d, 0x20, 0x6a, 0x26, 0xef, 0x69, 0x9b, 0x7c, 0x97, 0x57, 0xb7, 0x2d, 0x46, 0x90, 0x89, + 0xde, 0x31, 0x92, 0xe6, 0x78, 0xc7, 0x45, 0xe7, 0xf6, 0x0c, 0x02, 0x21, 0x00, 0xf8, 0xaa, 0x2f, + 0xa7, 0x11, 0xfc, 0xb7, 0x9b, 0x97, 0xe3, 0x97, 0xce, 0xda, 0x66, 0x7b, 0xae, 0x46, 0x4e, 0x2b, + 0xd3, 0xff, 0xdf, 0xc3, 0xcc, 0xed, 0x7a, 0xa8, 0xca, 0x5f, 0x4c, 0x1a, 0x7c, +]; + +impl DevAttDataFetcher for HardCodedDevAtt { + fn get_devatt_data(&self, data_type: DataType, data: &mut [u8]) -> Result { + let src = match data_type { + DataType::CertDeclaration => &CERT_DECLARATION[..], + DataType::PAI => &PAI_CERT[..], + DataType::DAC => &DAC_CERT[..], + DataType::DACPubKey => &DAC_PUBKEY[..], + DataType::DACPrivKey => &DAC_PRIVKEY[..], + }; + if src.len() <= data.len() { + let data = &mut data[0..src.len()]; + data.copy_from_slice(src); + Ok(src.len()) + } else { + Err(ErrorCode::NoSpace.into()) + } + } +} diff --git a/examples/light.rs b/examples/light.rs new file mode 100644 index 0000000..75e3abd --- /dev/null +++ b/examples/light.rs @@ -0,0 +1,128 @@ +//! 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). + +use core::borrow::Borrow; +use core::pin::pin; + +use embassy_futures::select::select; +use embassy_time::{Duration, Timer}; + +use esp_idf_matter::{error::Error, MatterStack, WifiBle}; + +use esp_idf_svc::eventloop::EspSystemEventLoop; +use esp_idf_svc::hal::peripherals::Peripherals; +use esp_idf_svc::nvs::EspDefaultNvsPartition; + +use log::info; + +use rs_matter::data_model::cluster_basic_information::BasicInfoConfig; +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::{Endpoint, HandlerCompat, Node}; +use rs_matter::data_model::system_model::descriptor; +use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::select::Coalesce; +use rs_matter::CommissioningData; + +use static_cell::ConstStaticCell; + +#[path = "dev_att/dev_att.rs"] +mod dev_att; + +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(); + + // 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 = HandlerCompat( + 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 + // 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 matter = pin!(stack.run( + EspSystemEventLoop::take()?, // The Matter stack needs (a clone of) the system event loop + EspDefaultNvsPartition::take()?, // The Matter stack needs (a clone of) the default ESP IDF NVS partition too + Peripherals::take()?.modem, // The Matter stack needs the BT/Wifi modem peripheral + CommissioningData { + // Hard-coded for demo purposes + verifier: VerifierData::new_with_pw(123456, *stack.matter().borrow()), + discriminator: 250, + }, + (NODE, handler), + )); + + // 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 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 the Matter run & 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: ConstStaticCell> = + ConstStaticCell::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: u16 = 1; + +/// The Matter Light device Node +const NODE: Node<'static> = Node { + id: 0, + endpoints: &[ + MatterStack::::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..b34a678 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -1,8 +1,6 @@ use core::borrow::Borrow; use core::cell::RefCell; -use alloc::sync::Arc; - use embassy_sync::blocking_mutex::Mutex; use enumset::enum_set; @@ -14,17 +12,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; @@ -44,56 +44,68 @@ struct GattsState { connections: heapless::Vec, } +pub struct BtpGattContext(Mutex>); + +impl BtpGattContext { + pub const fn new() -> Self { + Self(Mutex::new(RefCell::new(GattsState { + gatt_if: None, + service_handle: None, + c1_handle: None, + c2_handle: None, + c2_cccd_handle: None, + connections: heapless::Vec::new(), + }))) + } + + pub fn reset(&self) { + self.0.lock(|state| { + let mut state = state.borrow_mut(); + + state.gatt_if = None; + state.service_handle = None; + state.c1_handle = None; + state.c2_handle = None; + state.c2_cccd_handle = None; + }); + } +} + /// Implements the `GattPeripheral` trait using the ESP-IDF Bluedroid GATT stack. -struct PeripheralState<'d, M, T> +struct PeripheralState<'a, 'd, M, T> where T: Borrow>, M: BleEnabled, { - gap: EspBleGap<'d, M, T>, - gatts: EspGatts<'d, M, T>, - state: Mutex>, + gap: &'a EspBleGap<'d, M, T>, + gatts: &'a EspGatts<'d, M, T>, + state: &'a BtpGattContext, } -impl<'d, M, T> PeripheralState<'d, M, T> +impl<'a, 'd, M, T> PeripheralState<'a, 'd, M, T> where T: Borrow> + Clone, M: BleEnabled, { - fn new(driver: T) -> Result { - Ok(Self { - gap: EspBleGap::new(driver.clone())?, - gatts: EspGatts::new(driver)?, - state: Mutex::new(RefCell::new(GattsState { - gatt_if: None, - service_handle: None, - c1_handle: None, - c2_handle: None, - c2_cccd_handle: None, - connections: heapless::Vec::new(), - })), - }) + fn new( + gap: &'a EspBleGap<'d, M, T>, + gatts: &'a EspGatts<'d, M, T>, + state: &'a BtpGattContext, + ) -> Self { + Self { gap, gatts, state } } fn indicate(&self, data: &[u8], address: BtAddr) -> Result { - let conn = self.state.lock(|state| { + let conn = self.state.0.lock(|state| { let state = state.borrow(); - let Some(gatts_if) = state.gatt_if else { - return None; - }; + let gatts_if = state.gatt_if?; + let c2_handle = state.c2_handle?; - let Some(c2_handle) = state.c2_handle else { - return None; - }; - - 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 +120,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(()) @@ -244,7 +253,7 @@ where service_name: &str, service_adv_data: &AdvData, ) -> Result<(), EspError> { - self.state.lock(|state| { + self.state.0.lock(|state| { state.borrow_mut().gatt_if = Some(gatt_if); }); @@ -270,7 +279,7 @@ where } fn delete_service(&self, service_handle: Handle) -> Result<(), EspError> { - self.state.lock(|state| { + self.state.0.lock(|state| { if state.borrow().service_handle == Some(service_handle) { state.borrow_mut().c1_handle = None; state.borrow_mut().c2_handle = None; @@ -282,7 +291,7 @@ where } fn unregister_service(&self, service_handle: Handle) -> Result<(), EspError> { - self.state.lock(|state| { + self.state.0.lock(|state| { if state.borrow().service_handle == Some(service_handle) { state.borrow_mut().gatt_if = None; state.borrow_mut().service_handle = None; @@ -293,7 +302,7 @@ where } fn configure_and_start_service(&self, service_handle: Handle) -> Result<(), EspError> { - self.state.lock(|state| { + self.state.0.lock(|state| { state.borrow_mut().service_handle = Some(service_handle); }); @@ -337,7 +346,7 @@ where attr_handle: Handle, char_uuid: BtUuid, ) -> Result<(), EspError> { - let c2 = self.state.lock(|state| { + let c2 = self.state.0.lock(|state| { if state.borrow().service_handle != Some(service_handle) { return false; } @@ -374,7 +383,7 @@ where attr_handle: Handle, descr_uuid: BtUuid, ) -> Result<(), EspError> { - self.state.lock(|state| { + self.state.0.lock(|state| { if descr_uuid == BtUuid::uuid16(0x2902) && state.borrow().c2_handle == Some(attr_handle) { state.borrow_mut().c2_cccd_handle = Some(service_handle); @@ -385,7 +394,7 @@ where } fn register_conn_mtu(&self, conn_id: ConnectionId, mtu: u16) -> Result<(), EspError> { - self.state.lock(|state| { + self.state.0.lock(|state| { let mut state = state.borrow_mut(); if let Some(conn) = state .connections @@ -408,7 +417,7 @@ where where F: FnMut(GattPeripheralEvent), { - let added = self.state.lock(|state| { + let added = self.state.0.lock(|state| { let mut state = state.borrow_mut(); if state.connections.len() < MAX_CONNECTIONS { state @@ -441,7 +450,7 @@ where where F: FnMut(GattPeripheralEvent), { - self.state.lock(|state| { + self.state.0.lock(|state| { let mut state = state.borrow_mut(); if let Some(index) = state .connections @@ -457,6 +466,7 @@ where Ok(()) } + #[allow(clippy::too_many_arguments)] fn write( &self, gatt_if: GattInterface, @@ -473,18 +483,15 @@ where where F: FnMut(GattPeripheralEvent), { - let event = self.state.lock(|state| { + let event = self.state.0.lock(|state| { let mut state = state.borrow_mut(); 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 +505,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); @@ -555,19 +554,21 @@ where } } -pub struct BluedroidGattPeripheral(Arc>) +pub struct BluedroidGattPeripheral<'a, 'd, M> where - T: Borrow>, - M: BleEnabled; + M: BleEnabled, +{ + context: &'a BtpGattContext, + driver: &'a BtDriver<'d, M>, +} -impl BluedroidGattPeripheral +impl<'a, 'd, M> BluedroidGattPeripheral<'a, 'd, M> where - T: Borrow> + Clone, M: BleEnabled, { /// Create a new instance. - pub fn new(driver: T) -> Result { - Ok(Self(Arc::new(PeripheralState::new(driver)?))) + pub fn new(context: &'a BtpGattContext, driver: &'a BtDriver<'d, M>) -> Self { + Self { context, driver } } pub fn run( @@ -575,49 +576,60 @@ 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, - M: BleEnabled + 'static, + F: FnMut(GattPeripheralEvent) + Send + 'd, { let _pin = service_adv_data.pin(); - let gap_state = self.0.clone(); - let gatts_state = self.0.clone(); + let gap = EspBleGap::new(self.driver)?; + let gatts = EspGatts::new(self.driver)?; - self.0.gap.subscribe(move |event| { - gap_state.check_esp_status(gap_state.on_gap_event(event)); - })?; + unsafe { + gap.subscribe_nonstatic(|event| { + let state = PeripheralState::new(&gap, &gatts, &self.context); + + state.check_esp_status(state.on_gap_event(event)); + })?; + } let adv_data = service_adv_data.clone(); let service_name = service_name.to_owned(); - self.0.gatts.subscribe(move |(gatt_if, event)| { - gatts_state.check_esp_status(gatts_state.on_gatts_event( - &service_name, - &adv_data, - gatt_if, - event, - &mut callback, - )) - })?; + unsafe { + gatts.subscribe_nonstatic(|(gatt_if, event)| { + let state = PeripheralState::new(&gap, &gatts, &self.context); + + state.check_esp_status(state.on_gatts_event( + &service_name, + &adv_data, + gatt_if, + event, + &mut callback, + )) + })?; + } Ok(()) } /// 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)?) + todo!() } } -impl GattPeripheral for BluedroidGattPeripheral +impl<'a, 'd, M> GattPeripheral for BluedroidGattPeripheral<'a, 'd, M> where - T: Borrow> + Clone + Send + 'static, - M: BleEnabled + 'static, + M: BleEnabled, { - 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 +639,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..82f7250 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,428 @@ +use core::net::{Ipv4Addr, Ipv6Addr}; +use core::pin::pin; + +use std::io; +use std::net::UdpSocket; + +use async_io::Async; +use ble::BtpGattContext; +use embassy_futures::select::{select, select3, select4}; +use embassy_sync::blocking_mutex::raw::{NoopRawMutex, RawMutex}; + +use embassy_sync::mutex::Mutex; +use esp_idf_svc::bt::{Ble, BleEnabled, BtDriver}; +use esp_idf_svc::eth::{AsyncEth, EspEth}; +use esp_idf_svc::eventloop::EspSystemEventLoop; +use esp_idf_svc::hal::modem::{ + BluetoothModem, BluetoothModemPeripheral, Modem, WifiModem, WifiModemPeripheral, +}; +use esp_idf_svc::hal::peripheral::Peripheral; +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::{EspDefaultNvsPartition, EspNvs, EspNvsPartition, NvsPartitionId}; + +use esp_idf_svc::sys::{esp, esp_netif_get_ip6_linklocal, EspError, ESP_FAIL}; + +use esp_idf_svc::timer::EspTaskTimerService; +use esp_idf_svc::wifi::{AsyncWifi, EspWifi}; +use log::info; + +use netif::{EthNetifAccess, NetifAccess}; +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::{self, RootEndpointHandler}; +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}; +use wifi::{Wifi, WifiManager}; + +extern crate alloc; + +use crate::ble::BluedroidGattPeripheral; +use crate::error::Error; +use crate::multicast::{join_multicast_v4, join_multicast_v6}; 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, + btp_gatt_context: BtpGattContext, + wifi_manager: WifiManager<3, NoopRawMutex>, +} + +impl WifiBle { + const fn new() -> Self { + Self { + btp_context: BtpContext::new(), + btp_gatt_context: BtpGattContext::new(), + wifi_manager: WifiManager::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!() + } + + pub async fn run_with_netif<'d, H, P, N>( + &self, + sysloop: EspSystemEventLoop, + nvs: EspNvsPartition

, + netif: N, + dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, + handler: H, + ) -> Result<(), Error> + where + H: AsyncHandler + AsyncMetadata, + P: NvsPartitionId, + N: NetifAccess, + { + loop { + let (ipv4, ipv6) = netif.wait_ips_up(sysloop.clone()).await?; + + let socket = async_io::Async::::bind(MATTER_SOCKET_BIND_ADDR)?; + + let mut main = + pin!(self.run_once(&socket, &socket, nvs.clone(), dev_comm.clone(), &handler)); + let mut mdns = pin!(self.run_builtin_mdns(ipv4, ipv6)); + let mut down = pin!(netif.wait_ips_down(sysloop.clone())); + + select3(&mut main, &mut mdns, &mut down).coalesce().await?; + } + } + + pub async fn run_once<'d, S, R, H, P>( + &self, + send: S, + recv: R, + nvs: EspNvsPartition

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

, + network: nvs::Network<'_, 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, ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Result<(), Error> { + use rs_matter::mdns::{ + Host, MDNS_IPV4_BROADCAST_ADDR, MDNS_IPV6_BROADCAST_ADDR, MDNS_SOCKET_BIND_ADDR, + }; + + 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)?; + + 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(()) + } + + async fn run_transport( + &self, + send: S, + recv: R, + dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, + ) -> Result<(), Error> + where + S: NetworkSend, + R: NetworkReceive, + { + self.matter().run(send, recv, dev_comm).await?; + + Ok(()) + } +} + +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, E>( + &self, + sysloop: EspSystemEventLoop, + nvs: EspNvsPartition

, + eth: E, + dev_comm: CommissioningData, + handler: T, + ) -> Result<(), Error> + where + T: AsyncHandler + AsyncMetadata, + P: NvsPartitionId, + E: NetifAccess, + { + self.run_with_netif( + sysloop, + nvs, + eth, + Some((dev_comm, DiscoveryCapabilities::new(true, false, false))), + handler, + ) + .await + } +} + +impl<'d, M> NetifAccess for &Mutex>> +where + M: RawMutex, +{ + async fn with_netif(&self, f: F) -> R + where + F: FnOnce(&EspNetif) -> R, + { + f(self.lock().await.wifi().sta_netif()) + } +} + +impl<'a> MatterStack<'a, WifiBle> { + pub const fn root_metadata() -> Endpoint<'static> { + root_endpoint::endpoint(0) + } + + pub fn root_handler(&self, wifi: &Wifi<'_, NoopRawMutex>) -> RootEndpointHandler<'_> { + root_endpoint::handler(0, self.matter()) + } + + pub async fn is_commissioned(&self, nvs: EspDefaultNvsPartition) -> Result { + todo!() + } + + pub async fn operate<'d, T>( + &self, + sysloop: EspSystemEventLoop, + timer_service: EspTaskTimerService, + nvs: EspDefaultNvsPartition, + wifi: &mut EspWifi<'d>, + handler: T, + ) -> Result<(), Error> + where + T: AsyncHandler + AsyncMetadata, + { + let wifi = + Mutex::::new(AsyncWifi::wrap(wifi, sysloop.clone(), timer_service)?); + + let mut main = pin!(self.run_with_netif(sysloop, nvs, &wifi, None, handler)); + let mut wifi = pin!(self.network.wifi_manager.run(&wifi)); + + select(&mut wifi, &mut main).coalesce().await + } + + pub async fn commission<'d, T, M>( + &'static self, + nvs: EspDefaultNvsPartition, + bt: &BtDriver<'d, M>, + dev_comm: CommissioningData, + handler: T, + ) -> Result<(), Error> + where + T: AsyncHandler + AsyncMetadata, + M: BleEnabled, + { + let peripheral = BluedroidGattPeripheral::new(&self.network.btp_gatt_context, bt); + + let btp = Btp::new(peripheral, &self.network.btp_context); + + let mut ble = pin!(async { + btp.run("BT", self.matter().dev_det(), &dev_comm) + .await + .map_err(Into::into) + }); + let mut main = pin!(self.run_once( + &btp, + &btp, + nvs, + Some(( + dev_comm.clone(), + DiscoveryCapabilities::new(false, true, false) + )), + &handler + )); + + select(&mut ble, &mut main).coalesce().await + } + + pub async fn run<'d, T>( + &'static self, + sysloop: EspSystemEventLoop, + timer_service: EspTaskTimerService, + nvs: EspDefaultNvsPartition, + mut modem: impl Peripheral

+ 'd, + dev_comm: CommissioningData, + handler: T, + ) -> Result<(), Error> + where + T: AsyncHandler + AsyncMetadata, + { + loop { + if !self.is_commissioned(nvs.clone()).await? { + let bt = BtDriver::::new(&mut modem, Some(nvs.clone()))?; + + let mut main = pin!(self.commission(nvs.clone(), &bt, dev_comm.clone(), &handler)); + let mut wait_network_connect = + pin!(self.network.wifi_manager.wait_network_connect()); + + select(&mut main, &mut wait_network_connect) + .coalesce() + .await?; + } + + let mut wifi = EspWifi::new(&mut modem, sysloop.clone(), Some(nvs.clone()))?; + + self.operate( + sysloop.clone(), + timer_service.clone(), + nvs.clone(), + &mut wifi, + &handler, + ) + .await?; + } + } +} 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..72bb2c7 --- /dev/null +++ b/src/netif.rs @@ -0,0 +1,172 @@ +use core::net::{Ipv4Addr, Ipv6Addr}; +use core::pin::pin; + +use embassy_futures::select::{select, Either}; +use embassy_sync::blocking_mutex::raw::RawMutex; +use embassy_sync::mutex::Mutex; + +use embedded_svc::wifi::asynch::Wifi; +use esp_idf_svc::eth::{AsyncEth, EspEth}; +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 esp_idf_svc::wifi::{AsyncWifi, EspWifi}; + +use log::info; + +use crate::error::Error; + +pub trait NetifAccess { + async fn with_netif(&self, f: F) -> R + where + F: FnOnce(&EspNetif) -> R; + + async fn wait_ips_up( + &self, + sysloop: EspSystemEventLoop, + ) -> Result<(Ipv4Addr, Ipv6Addr), Error> { + // TODO: Maybe wait on Wifi and Eth events as well + let mut subscription = sysloop.subscribe_async::()?; + + loop { + let events = pin!(subscription.recv()); + let ips = pin!(self.with_netif(get_ips)); + + if let Either::Second(Ok(result)) = select(events, ips).await { + break Ok(result); + } + } + } + + async fn wait_ips_down(&self, sysloop: EspSystemEventLoop) -> Result<(), Error> { + // TODO: Maybe wait on Wifi and Eth events as well + let mut subscription = sysloop.subscribe_async::()?; + + loop { + let events = pin!(subscription.recv()); + let ips = pin!(self.with_netif(get_ips)); + + if let Either::Second(Err(_)) = select(events, ips).await { + break Ok(()); + } + } + } +} + +impl NetifAccess for &T +where + T: NetifAccess, +{ + async fn with_netif(&self, f: F) -> R + where + F: FnOnce(&EspNetif) -> R, + { + (**self).with_netif(f).await + } +} + +impl NetifAccess for &mut T +where + T: NetifAccess, +{ + async fn with_netif(&self, f: F) -> R + where + F: FnOnce(&EspNetif) -> R, + { + (**self).with_netif(f).await + } +} + +impl<'d, T> NetifAccess for &mut EspEth<'d, T> { + async fn with_netif(&self, f: F) -> R + where + F: FnOnce(&EspNetif) -> R, + { + f(self.netif()) + } +} + +impl<'d, T> NetifAccess for AsyncEth> { + async fn with_netif(&self, f: F) -> R + where + F: FnOnce(&EspNetif) -> R, + { + f(self.eth().netif()) + } +} + +pub struct EthNetifAccess<'a, 'd, M, T>(pub &'a Mutex>>) +where + M: RawMutex; + +impl<'a, 'd, M, T> NetifAccess for EthNetifAccess<'a, 'd, M, T> +where + M: RawMutex, +{ + async fn with_netif(&self, f: F) -> R + where + F: FnOnce(&EspNetif) -> R, + { + let eth = self.0.lock().await; + + f(eth.eth().netif()) + } +} + +// pub struct WifiNetifAccess<'a, M, T>(pub &'a Mutex) +// where +// M: RawMutex, +// T: Wifi; + +// impl<'a, M, T> NetifAccess for WifiNetifAccess<'a, M, T> +// where +// M: RawMutex, +// T: Wifi, +// { +// async fn with_netif(&self, f: F) -> R +// where +// F: FnOnce(&EspNetif) -> R, +// { +// let wifi = self.0.lock().await; + +// f(wifi.wifi().sta_netif()) +// } +// } + +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..ae16fe7 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::WifiNetworks}; + +pub enum Network<'a, const N: usize, M> +where + M: RawMutex, +{ + None, + Wifi(&'a WifiNetworks), +} + +impl<'a, const N: usize, M> Network<'a, N, M> +where + M: RawMutex, +{ + const fn key(&self) -> Option<&str> { + match self { + Self::None => None, + Self::Wifi(_) => Some("wifi"), + } + } +} + +pub struct Psm<'a, T, const N: usize, M> +where T: NvsPartitionId, + M: RawMutex, { matter: &'a Matter<'a>, + network: Network<'a, N, M>, nvs: EspNvs, + buf: &'a mut [u8], } -impl<'a, T> Psm<'a, T> -where +impl<'a, T, const N: usize, M> Psm<'a, 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, 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..94390d8 --- /dev/null +++ b/src/wifi.rs @@ -0,0 +1,848 @@ +use core::cell::RefCell; + +use embassy_sync::blocking_mutex::{self, raw::RawMutex}; +use embassy_sync::mutex::Mutex; + +use esp_idf_svc::eventloop::EspSystemEventLoop; +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; + +pub struct Wifi<'d, M>(Mutex>>>) +where + M: RawMutex; + +impl<'d, M> Wifi<'d, M> +where + M: RawMutex, +{ + pub const fn new() -> Self { + Self(Mutex::new(None)) + } + + pub(crate) async fn init(&self, wifi: AsyncWifi>) { + *self.0.lock().await = Some(wifi); + } + + fn mutex(&self) -> &Mutex>>> { + &self.0 + } +} + +#[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 WifiStatus { + ssid: heapless::String<32>, + status: NetworkCommissioningStatus, + value: i32, +} + +pub struct WifiState0 { + pub(crate) networks: heapless::Vec, + pub(crate) connected_once: bool, + pub(crate) connect_requested: Option>, + pub(crate) status: Option, + pub(crate) changed: bool, +} + +impl WifiState0 { + fn load(&mut self, data: &[u8]) -> Result<(), Error> { + let root = TLVList::new(data).iter().next().ok_or(ErrorCode::Invalid)?; + + tlv::from_tlv(&mut self.networks, &root)?; + + self.changed = false; + + Ok(()) + } + + fn store<'m>(&mut self, buf: &'m mut [u8]) -> Result, Error> { + if !self.changed { + return Ok(None); + } + + let mut wb = WriteBuf::new(buf); + let mut tw = TLVWriter::new(&mut wb); + + self.networks + .as_slice() + .to_tlv(&mut tw, TagType::Anonymous)?; + + self.changed = false; + + let len = tw.get_tail(); + + Ok(Some(&buf[..len])) + } +} + +pub struct WifiManager(pub(crate) WifiNetworks) +where + M: RawMutex; + +impl WifiManager +where + M: RawMutex, +{ + pub const fn new() -> Self { + Self(WifiNetworks::new()) + } + + pub async fn run<'d, R>( + &self, + wifi: &Mutex>>, + ) -> Result<(), crate::error::Error> + where + R: RawMutex, + { + todo!() + } + + pub async fn wait_network_connect(&self) -> Result<(), crate::error::Error> { + todo!() + } + + // 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 + // } + // } + + // async fn do_scan( + // &self, + // ) -> Option, NetworkCommissioningStatus>> { + // // TODO: Use IfMutex instead of Mutex + // let mut wifi_mutex = self.wifi.mutex().lock().await; + // let wifi = wifi_mutex.as_mut()?; + + // let result = Self::wifi_scan(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(WifiStatus { + // status, + // ssid: "".try_into().unwrap(), + // value: 0, + // }); + // } + // }); + + // Some(result.map_err(|_| status)) + // } + + // async fn do_connect(&self, ssid: Option<&str>) -> Option> { + // 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_mutex = self.wifi.mutex().lock().await; + // let wifi = wifi_mutex.as_mut()?; + + // let result = Self::wifi_connect(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(WifiStatus { + // status: response.status, + // ssid: 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(()) + // } +} + +pub struct WifiNetworks +where + M: RawMutex, +{ + state: blocking_mutex::Mutex>>, + network_connect_requested: Notification, +} + +impl WifiNetworks +where + M: RawMutex, +{ + pub const fn new() -> Self { + Self { + state: blocking_mutex::Mutex::new(RefCell::new(WifiState0 { + networks: heapless::Vec::new(), + connected_once: false, + connect_requested: None, + status: None, + changed: false, + })), + network_connect_requested: Notification::new(), + } + } + + pub fn load(&self, data: &[u8]) -> Result<(), Error> { + self.state.lock(|state| state.borrow_mut().load(data)) + } + + pub fn store<'m>(&self, buf: &'m mut [u8]) -> Result, Error> { + self.state.lock(|state| state.borrow_mut().store(buf)) + } +} + +#[derive(Debug, Clone, ToTLV, FromTLV)] +pub 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, const N: usize, M> +where + M: RawMutex, +{ + data_ver: Dataver, + networks: &'a WifiNetworks, +} + +impl<'a, const N: usize, M> WifiCommCluster<'a, N, M> +where + M: RawMutex, +{ + pub fn new(rand: Rand, networks: &'a WifiNetworks) -> Self { + Self { + data_ver: Dataver::new(rand), + networks, + } + } + + 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.networks.state.lock(|state| { + let state = state.borrow(); + + for network in &state.networks { + let nw_info = NwInfo { + network_id: OctetStr(network.ssid.as_str().as_bytes()), + connected: state + .status + .as_ref() + .map(|status| { + *status.ssid == network.ssid + && matches!( + status.status, + NetworkCommissioningStatus::Success + ) + }) + .unwrap_or(false), + }; + + nw_info.to_tlv(&mut writer, TagType::Anonymous)?; + } + + Ok::<_, Error>(()) + })?; + + writer.end_container()?; + writer.complete() + } + Attributes::ScanMaxTimeSecs => AttrType::new().encode(writer, 30_u8), + Attributes::ConnectMaxTimeSecs => AttrType::new().encode(writer, 60_u8), + Attributes::InterfaceEnabled => AttrType::new().encode(writer, true), + Attributes::LastNetworkingStatus => self.networks.state.lock(|state| { + AttrType::new().encode( + writer, + state.borrow().status.as_ref().map(|o| o.status as u8), + ) + }), + Attributes::LastNetworkID => self.networks.state.lock(|state| { + AttrType::new().encode( + writer, + state + .borrow() + .status + .as_ref() + .map(|o| OctetStr(o.ssid.as_str().as_bytes())), + ) + }), + Attributes::LastConnectErrorValue => self.networks.state.lock(|state| { + AttrType::new() + .encode(writer, state.borrow().status.as_ref().map(|o| o.value)) + }), + } + } + } else { + Ok(()) + } + } + + 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 _)?; + + Status::new(IMStatusCode::Busy, 0).to_tlv(&mut tw, TagType::Anonymous)?; + + Ok(()) + } + + async fn add_network( + &self, + exchange: &Exchange<'_>, + req: &AddWifiNetworkRequest<'_>, + encoder: CmdDataEncoder<'_, '_, '_>, + ) -> Result<(), Error> { + // TODO: Check failsafe status + + self.networks.state.lock(|state| { + let mut state = state.borrow_mut(); + + let index = state + .networks + .iter() + .position(|conf| conf.ssid.as_str().as_bytes() == req.ssid.0); + + let 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.networks.state.lock(|state| { + let mut state = state.borrow_mut(); + + let index = state + .networks + .iter() + .position(|conf| conf.ssid.as_str().as_bytes() == req.network_id.0); + + let 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 + + // 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.networks.state.lock(|state| { + let mut state = state.borrow_mut(); + + state.connect_requested = Some( + core::str::from_utf8(req.network_id.0) + .unwrap() + .try_into() + .unwrap(), + ); + }); + + self.networks.network_connect_requested.notify(); + + // Block forever waitinng for the firware to restart + core::future::pending().await + } + + async fn reorder_network( + &self, + exchange: &Exchange<'_>, + req: &ReorderNetworkRequest<'_>, + encoder: CmdDataEncoder<'_, '_, '_>, + ) -> Result<(), Error> { + // TODO: Check failsafe status + + self.networks.state.lock(|state| { + let mut state = state.borrow_mut(); + + let index = state + .networks + .iter() + .position(|conf| conf.ssid.as_str().as_bytes() == req.network_id.0); + + let 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(()) + }) + } +} + +impl<'a, const N: usize, M> AsyncHandler for WifiCommCluster<'a, 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(()) +// } +// }