Skip to content

Commit

Permalink
rs-matter-stack (#1)
Browse files Browse the repository at this point in the history
* WIP: rs-matter-stack

* Fix CI

* Create IPv6 address

* Put extra warnings for stack blowups

* Box EspWifi to avoid stack blowups

* Multicast no longer needed

* Bugfix

* Fix async-io bug

* Update to latest edge-net
  • Loading branch information
ivmarkov authored Jun 2, 2024
1 parent f6cea01 commit c817a8a
Show file tree
Hide file tree
Showing 20 changed files with 606 additions and 2,340 deletions.
23 changes: 17 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ embedded-svc = { git = "https://github.com/esp-rs/embedded-svc" }
esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc", branch = "gatt" }
#esp-idf-svc = { path = "../esp-idf-svc" }
rs-matter = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" }
rs-matter-macros = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" }
#rs-matter = { path = "../rs-matter/rs-matter" }
rs-matter-macros = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" }
#rs-matter-macros = { path = "../rs-matter/rs-matter-macros" }
edge-nal = { git = "https://github.com/ivmarkov/edge-net" }
#edge-nal = { path = "../edge-net/edge-nal" }
edge-nal-std = { git = "https://github.com/ivmarkov/edge-net" }
#edge-nal-std = { path = "../edge-net/edge-nal-std" }

[profile.release]
opt-level = "s"
Expand All @@ -34,9 +38,12 @@ debug = true
opt-level = "z"

[features]
default = ["std", "async-io-mini"]
std = ["rs-matter/std", "esp-idf-svc/std"]
examples = ["std", "async-io-mini", "esp-idf-svc/binstart", "esp-idf-svc/critical-section"] # Enable only when building the examples
#default = ["rs-matter-stack"]
default = ["rs-matter-stack", "async-io-mini"]
rs-matter-stack = ["dep:rs-matter-stack", "std"]
async-io-mini = ["std", "edge-nal-std/async-io-mini"]
std = ["esp-idf-svc/std", "edge-nal-std"]
examples = ["default", "esp-idf-svc/binstart", "esp-idf-svc/critical-section"] # Enable only when building the examples

[dependencies]
log = { version = "0.4", default-features = false }
Expand All @@ -50,15 +57,19 @@ esp-idf-svc = { version = "0.48", default-features = false, features = ["alloc",
embedded-svc = { version = "0.27", default-features = false }
rs-matter = { version = "0.1", default-features = false, features = ["rustcrypto"] }
rs-matter-macros = "0.1"
async-io = { version = "=2.0.0", optional = true, default-features = false } # Workaround for https://github.com/smol-rs/async-lock/issues/84
async-io-mini = { git = "https://github.com/ivmarkov/async-io-mini", optional = true }
async-io = { version = "=2.0.0", default-features = false } # Workaround for https://github.com/smol-rs/async-lock/issues/84
rs-matter-stack = { git = "https://github.com/ivmarkov/rs-matter-stack", default-features = false, features = ["std"], optional = true }
#rs-matter-stack = { path = "../rs-matter-stack", default-features = false, features = ["std"], optional = true }
edge-nal = "0.2"
edge-nal-std = { version = "0.2", default-features = false, optional = true }

[build-dependencies]
embuild = "0.31.3"

[dev-dependencies]
embassy-time = { version = "0.3", features = ["generic-queue"] }
static_cell = "2.1"
anyhow = "1"

[[example]]
name = "light"
Expand Down
79 changes: 31 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@

## Overview

Configuring and running the [`rs-matter`](https://github.com/project-chip/rs-matter) crate is not trivial.
Everything necessary to run [`rs-matter`](https://github.com/project-chip/rs-matter) on the ESP-IDF:
* Bluedroid implementation of `rs-matter`'s `GattPeripheral` for BLE comissioning support.
* [`rs-matter-stack`](https://github.com/ivmarkov/rs-matter-stack) support with `Netif`, `KvBlobStore` and `Modem` implementations.

Users are expected to provide implementations for various `rs-matter` abstractions, like a UDP stack, BLE stack, randomizer, epoch time, responder and so on and so forth.
Since ESP-IDF does support the Rust Standard Library, UDP networking just works.

Furthermore, _operating_ the assembled Matter stack is also challenging, as various features might need to be switched on or off depending on whether Matter is running in commissioning or operating mode, and also depending on the current network connectivity (as in e.g. Wifi signal lost).

**This crate addresses these issues by providing an all-in-one [`MatterStack`](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/stack.rs) assembly that configures `rs-matter` for reliably operating on top of the ESP IDF SDK.**

Instantiate it and then call `MatterStack::run(...)`.
## Example

```rust
//! An example utilizing the `WifiBleMatterStack` struct.
//! An example utilizing the `EspWifiBleMatterStack` struct.
//! As the name suggests, this Matter stack assembly uses Wifi as the main transport,
//! and BLE for commissioning.
//! If you want to use Ethernet, utilize `EthMatterStack` instead.
//! If you want to use Ethernet, utilize `EspEthMatterStack` instead.
//!
//! The example implements a fictitious Light device (an On-Off Matter cluster).

Expand All @@ -32,7 +30,7 @@ use core::pin::pin;
use embassy_futures::select::select;
use embassy_time::{Duration, Timer};

use esp_idf_matter::{init_async_io, Error, MdnsType, WifiBleMatterStack};
use esp_idf_matter::{init_async_io, EspWifiBleMatterStack, EspKvBlobStore, EspPersist, EspModem};

use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::hal::peripherals::Peripherals;
Expand All @@ -57,7 +55,7 @@ use static_cell::ConstStaticCell;
#[path = "dev_att/dev_att.rs"]
mod dev_att;

fn main() -> Result<(), Error> {
fn main() -> Result<(), anyhow::Error> {
EspLogger::initialize_default();

info!("Starting...");
Expand All @@ -80,7 +78,7 @@ fn main() -> Result<(), Error> {

#[inline(never)]
#[cold]
fn run() -> Result<(), Error> {
fn run() -> Result<(), anyhow::Error> {
let result = block_on(matter());

if let Err(e) = &result {
Expand All @@ -93,11 +91,17 @@ fn run() -> Result<(), Error> {
result
}

async fn matter() -> Result<(), Error> {
async fn matter() -> Result<(), anyhow::Error> {
// Take the Matter stack (can be done only once),
// as we'll run it in this thread
let stack = MATTER_STACK.take();

// Take some generic ESP-IDF stuff we'll need later
let sysloop = EspSystemEventLoop::take()?;
let timers = EspTaskTimerService::new()?;
let nvs = EspDefaultNvsPartition::take()?;
let peripherals = Peripherals::take()?;

// Our "light" on-off cluster.
// Can be anything implementing `rs_matter::data_model::AsyncHandler`
let on_off = cluster_on_off::OnOffCluster::new(*stack.matter().borrow());
Expand All @@ -124,17 +128,15 @@ async fn matter() -> Result<(), Error> {
// Using `pin!` is completely optional, but saves some memory due to `rustc`
// not being very intelligent w.r.t. stack usage in async functions
let mut matter = pin!(stack.run(
// The Matter stack needs (a clone of) the system event loop
EspSystemEventLoop::take()?,
// The Matter stack needs (a clone of) the timer service
EspTaskTimerService::new()?,
// The Matter stack needs (a clone of) the default ESP IDF NVS partition
EspDefaultNvsPartition::take()?,
// The Matter stack needs a persister to store its state
// `EspPersist`+`EspKvBlobStore` saves to a user-supplied NVS partition
// under namespace `esp-idf-matter`
EspPersist::new_wifi_ble(EspKvBlobStore::new_default(nvs.clone())?, stack),
// The Matter stack needs the BT/Wifi modem peripheral - and in general -
// the Bluetooth / Wifi connections will be managed by the Matter stack itself
// For finer-grained control, call `MatterStack::is_commissioned`,
// `MatterStack::commission` and `MatterStack::operate`
Peripherals::take()?.modem,
EspModem::new(peripherals.modem, sysloop, timers, nvs, stack),
// Hard-coded for demo purposes
CommissioningData {
verifier: VerifierData::new_with_pw(123456, *stack.matter().borrow()),
Expand Down Expand Up @@ -166,14 +168,16 @@ async fn matter() -> Result<(), Error> {
});

// Schedule the Matter run & the device loop together
select(&mut matter, &mut device).coalesce().await
select(&mut matter, &mut device).coalesce().await?;

Ok(())
}

/// The Matter stack is allocated statically to avoid
/// program stack blowups.
/// It is also a mandatory requirement when the `WifiBle` stack variation is used.
static MATTER_STACK: ConstStaticCell<WifiBleMatterStack> =
ConstStaticCell::new(WifiBleMatterStack::new(
static MATTER_STACK: ConstStaticCell<EspWifiBleMatterStack<()>> =
ConstStaticCell::new(EspWifiBleMatterStack::new_default(
&BasicInfoConfig {
vid: 0xFFF1,
pid: 0x8000,
Expand All @@ -186,7 +190,6 @@ static MATTER_STACK: ConstStaticCell<WifiBleMatterStack> =
vendor_name: "ACME",
},
&dev_att::HardCodedDevAtt::new(),
MdnsType::default(),
));

/// Endpoint 0 (the root endpoint) always runs
Expand All @@ -197,7 +200,7 @@ const LIGHT_ENDPOINT_ID: u16 = 1;
const NODE: Node = Node {
id: 0,
endpoints: &[
WifiBleMatterStack::root_metadata(),
EspWifiBleMatterStack::<()>::root_metadata(),
Endpoint {
id: LIGHT_ENDPOINT_ID,
device_type: DEV_TYPE_ON_OFF_LIGHT,
Expand All @@ -207,41 +210,21 @@ const NODE: Node = Node {
};
```

(See also [Examples](#examples))
(See also [All examples](#all-examples))

### Advanced use cases

If the provided `MatterStack` does not cut it, users can implement their own stacks because the building blocks are also exposed as a public API.

#### Building blocks

* [Bluetooth commissioning support](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/ble.rs) with the ESP IDF Bluedroid stack (not necessary if you plan to run Matter over Ethernet)
* WiFi provisioning support via an [ESP IDF specific Matter Network Commissioning Cluster implementation](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/wifi/comm.rs)
* [Non-volatile storage for Matter persistent data (fabrics, ACLs and network connections)](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/nvs.rs) on top of the ESP IDF NVS flash API
* mDNS:
* Optional [Matter mDNS responder implementation](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/mdns.rs) based on the ESP IDF mDNS responder (use if you need to register other services besides Matter in mDNS)
* [UDP-multicast workarounds](https://github.com/ivmarkov/esp-idf-matter/blob/master/src/multicast.rs) for `rs-matter`'s built-in mDNS responder, addressing bugs in the Rust STD wrappers of ESP IDF

#### Future
## Future
* Device Attestation data support using secure flash storage
* Setting system time via Matter
* Matter OTA support based on the ESP IDF OTA API
* Thread networking (for ESP32H2 and ESP32C6)
* Wifi Access-Point based commissioning (for ESP32S2 which does not have Bluetooth support)

#### Additional building blocks provided by `rs-matter` directly and compatible with ESP IDF:
* UDP and (in future) TCP support
* Enable the `async-io` and `std` features on `rs-matter` and use `async-io` sockets. The [`async-io`](https://github.com/smol-rs/async-io) crate has support for ESP IDF out of the box
* Random number generator
* Enable the `std` feature on `rs-matter`. This way, the [`rand`](https://github.com/rust-random/rand) crate will be utilized, which has support for ESP IDF out of the box
* UNIX epoch
* Enable the `std` feature on `rs-matter`. This way, `rs-matter` will utilize `std::time::SystemTime` which is supported by ESP IDF out of the box

## Build Prerequisites

Follow the [Prerequisites](https://github.com/esp-rs/esp-idf-template#prerequisites) section in the `esp-idf-template` crate.

## Examples
## All examples

The examples could be built and flashed conveniently with [`cargo-espflash`](https://github.com/esp-rs/espflash/). To run e.g. `light` on an e.g. ESP32-C3:
(Swap the Rust target and example name with the target corresponding for your ESP32 MCU and with the example you would like to build)
Expand Down
3 changes: 3 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
future-size-threshold = 2048
stack-size-threshold = 2048
pass-by-value-size-limit = 16
large-error-threshold = 64
46 changes: 27 additions & 19 deletions examples/light.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! An example utilizing the `WifiBleMatterStack` struct.
//! An example utilizing the `EspWifiBleMatterStack` struct.
//! As the name suggests, this Matter stack assembly uses Wifi as the main transport,
//! and BLE for commissioning.
//! If you want to use Ethernet, utilize `EthMatterStack` instead.
//! If you want to use Ethernet, utilize `EspEthMatterStack` instead.
//!
//! The example implements a fictitious Light device (an On-Off Matter cluster).

Expand All @@ -11,7 +11,7 @@ use core::pin::pin;
use embassy_futures::select::select;
use embassy_time::{Duration, Timer};

use esp_idf_matter::{init_async_io, Error, MdnsType, WifiBleMatterStack};
use esp_idf_matter::{init_async_io, EspKvBlobStore, EspModem, EspPersist, EspWifiBleMatterStack};

use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::hal::peripherals::Peripherals;
Expand All @@ -31,12 +31,14 @@ use rs_matter::secure_channel::spake2p::VerifierData;
use rs_matter::utils::select::Coalesce;
use rs_matter::CommissioningData;

use rs_matter_stack::persist::DummyPersist;

use static_cell::ConstStaticCell;

#[path = "dev_att/dev_att.rs"]
mod dev_att;

fn main() -> Result<(), Error> {
fn main() -> Result<(), anyhow::Error> {
EspLogger::initialize_default();

info!("Starting...");
Expand All @@ -45,7 +47,7 @@ fn main() -> Result<(), Error> {
// confused by the low priority of the ESP IDF main task
// Also allocate a very large stack (for now) as `rs-matter` futures do occupy quite some space
let thread = std::thread::Builder::new()
.stack_size(65 * 1024)
.stack_size(70 * 1024)
.spawn(|| {
// Eagerly initialize `async-io` to minimize the risk of stack blowups later on
init_async_io()?;
Expand All @@ -59,7 +61,7 @@ fn main() -> Result<(), Error> {

#[inline(never)]
#[cold]
fn run() -> Result<(), Error> {
fn run() -> Result<(), anyhow::Error> {
let result = block_on(matter());

if let Err(e) = &result {
Expand All @@ -72,11 +74,17 @@ fn run() -> Result<(), Error> {
result
}

async fn matter() -> Result<(), Error> {
async fn matter() -> Result<(), anyhow::Error> {
// Take the Matter stack (can be done only once),
// as we'll run it in this thread
let stack = MATTER_STACK.take();

// Take some generic ESP-IDF stuff we'll need later
let sysloop = EspSystemEventLoop::take()?;
let timers = EspTaskTimerService::new()?;
let nvs = EspDefaultNvsPartition::take()?;
let peripherals = Peripherals::take()?;

// Our "light" on-off cluster.
// Can be anything implementing `rs_matter::data_model::AsyncHandler`
let on_off = cluster_on_off::OnOffCluster::new(*stack.matter().borrow());
Expand All @@ -103,17 +111,16 @@ async fn matter() -> Result<(), Error> {
// Using `pin!` is completely optional, but saves some memory due to `rustc`
// not being very intelligent w.r.t. stack usage in async functions
let mut matter = pin!(stack.run(
// The Matter stack needs (a clone of) the system event loop
EspSystemEventLoop::take()?,
// The Matter stack needs (a clone of) the timer service
EspTaskTimerService::new()?,
// The Matter stack needs (a clone of) the default ESP IDF NVS partition
EspDefaultNvsPartition::take()?,
// The Matter stack needs a persister to store its state
// `EspPersist`+`EspKvBlobStore` saves to a user-supplied NVS partition
// under namespace `esp-idf-matter`
DummyPersist,
//EspPersist::new_wifi_ble(EspKvBlobStore::new_default(nvs.clone())?, stack),
// The Matter stack needs the BT/Wifi modem peripheral - and in general -
// the Bluetooth / Wifi connections will be managed by the Matter stack itself
// For finer-grained control, call `MatterStack::is_commissioned`,
// `MatterStack::commission` and `MatterStack::operate`
Peripherals::take()?.modem,
EspModem::new(peripherals.modem, sysloop, timers, nvs, stack),
// Hard-coded for demo purposes
CommissioningData {
verifier: VerifierData::new_with_pw(123456, *stack.matter().borrow()),
Expand Down Expand Up @@ -145,14 +152,16 @@ async fn matter() -> Result<(), Error> {
});

// Schedule the Matter run & the device loop together
select(&mut matter, &mut device).coalesce().await
select(&mut matter, &mut device).coalesce().await?;

Ok(())
}

/// The Matter stack is allocated statically to avoid
/// program stack blowups.
/// It is also a mandatory requirement when the `WifiBle` stack variation is used.
static MATTER_STACK: ConstStaticCell<WifiBleMatterStack> =
ConstStaticCell::new(WifiBleMatterStack::new(
static MATTER_STACK: ConstStaticCell<EspWifiBleMatterStack<()>> =
ConstStaticCell::new(EspWifiBleMatterStack::new_default(
&BasicInfoConfig {
vid: 0xFFF1,
pid: 0x8000,
Expand All @@ -165,7 +174,6 @@ static MATTER_STACK: ConstStaticCell<WifiBleMatterStack> =
vendor_name: "ACME",
},
&dev_att::HardCodedDevAtt::new(),
MdnsType::default(),
));

/// Endpoint 0 (the root endpoint) always runs
Expand All @@ -176,7 +184,7 @@ const LIGHT_ENDPOINT_ID: u16 = 1;
const NODE: Node = Node {
id: 0,
endpoints: &[
WifiBleMatterStack::root_metadata(),
EspWifiBleMatterStack::<()>::root_metadata(),
Endpoint {
id: LIGHT_ENDPOINT_ID,
device_type: DEV_TYPE_ON_OFF_LIGHT,
Expand Down
Loading

0 comments on commit c817a8a

Please sign in to comment.