diff --git a/Cargo.toml b/Cargo.toml index cb6c6a9..f6bdf53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" rust-version = "1.77" [patch.crates-io] -rs-matter = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" } +rs-matter = { git = "https://github.com/ivmarkov/rs-matter", branch = "tip" } #rs-matter = { path = "../rs-matter/rs-matter" } #edge-nal = { git = "https://github.com/ivmarkov/edge-net" } #edge-nal = { path = "../edge-net/edge-nal" } @@ -25,7 +25,7 @@ rs-matter = { git = "https://github.com/ivmarkov/rs-matter", branch = "wifi" } [features] default = [] zeroconf = ["os", "rs-matter/zeroconf"] -os = ["backtrace", "rs-matter/os", "rs-matter/mbedtls", "embassy-time/std", "embassy-time/generic-queue"] +os = ["backtrace", "rs-matter/os", "rs-matter/rustcrypto", "embassy-time/std", "embassy-time/generic-queue"] backtrace = ["std", "rs-matter/backtrace"] std = ["alloc", "rs-matter/std", "edge-nal-std"] alloc = ["embedded-svc/alloc"] @@ -45,6 +45,7 @@ edge-nal-std = { version = "0.3", optional = true } edge-mdns = { version = "0.3", optional = true } [target.'cfg(all(unix, not(target_os = "espidf")))'.dependencies] +bitflags = "2" nix = { version = "0.27", features = ["net"], optional = true } [dev-dependencies] diff --git a/README.md b/README.md index 1807c8b..40052f0 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,7 @@ Instantiate it and then call `MatterStack::<...>::run(...)`. **Flexibility**. -Using `MatterStack<...>` hard-codes the following: -* _One large future_: The Matter stack is assembled as one large future which is not `Send`. Using an executor to poll that future together with others is still possible, but the executor should be a local one (i.e. Tokio's `LocalSet`, `async_executor::LocalExecutor` and so on). -* _Allocation strategy_: a number of large-ish buffers are const-allocated inside the `MatterStack` struct. This allows the whole stack to be statically-allocated with `ConstStaticCell` - yet - that would eat up 20 to 60K of your flash size, depending on the selected max number of subscriptions, exchange buffers and so on. A different allocation strategy might be provided in future. +The Matter stack is assembled as one large future which is not `Send`. Using an executor to poll that future together with others is still possible, but the executor should be a local one (i.e. Tokio's `LocalSet`, `async_executor::LocalExecutor` and so on). ## The examples are STD-only? @@ -69,6 +67,7 @@ use rs_matter::data_model::objects::{Dataver, Endpoint, HandlerCompat, Node}; use rs_matter::data_model::system_model::descriptor; use rs_matter::error::Error; use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::init::InitMaybeUninit; use rs_matter::utils::select::Coalesce; use rs_matter::CommissioningData; @@ -76,7 +75,7 @@ use rs_matter_stack::netif::UnixNetif; use rs_matter_stack::persist::{DirKvBlobStore, KvBlobBuf, KvPersist}; use rs_matter_stack::EthMatterStack; -use static_cell::ConstStaticCell; +use static_cell::StaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; @@ -88,9 +87,24 @@ fn main() -> Result<(), Error> { info!("Starting..."); - // Take the Matter stack (can be done only once), + // Initialize the Matter stack (can be done only once), // as we'll run it in this thread - let stack = MATTER_STACK.take(); + let stack = MATTER_STACK + .uninit() + .init_with(EthMatterStack::init_default( + &BasicInfoConfig { + vid: 0xFFF1, + pid: 0x8000, + hw_ver: 2, + sw_ver: 1, + sw_ver_str: "1", + serial_no: "aabbccdd", + device_name: "MyLight", + product_name: "ACME Light", + vendor_name: "ACME", + }, + &DEV_ATT, + )); // Our "light" on-off cluster. // Can be anything implementing `rs_matter::data_model::AsyncHandler` @@ -162,21 +176,9 @@ fn main() -> Result<(), Error> { /// The Matter stack is allocated statically to avoid /// program stack blowups. -static MATTER_STACK: ConstStaticCell>> = - ConstStaticCell::new(EthMatterStack::new_default( - &BasicInfoConfig { - vid: 0xFFF1, - pid: 0x8000, - hw_ver: 2, - sw_ver: 1, - sw_ver_str: "1", - serial_no: "aabbccdd", - device_name: "MyLight", - product_name: "ACME Light", - vendor_name: "ACME", - }, - &dev_att::HardCodedDevAtt::new(), - )); +static MATTER_STACK: StaticCell>> = StaticCell::new(); + +const DEV_ATT: dev_att::HardCodedDevAtt = dev_att::HardCodedDevAtt::new(); /// Endpoint 0 (the root endpoint) always runs /// the hidden Matter system clusters, so we pick ID=1 diff --git a/examples/light.rs b/examples/light.rs index 5985517..3781907 100644 --- a/examples/light.rs +++ b/examples/light.rs @@ -18,16 +18,16 @@ use rs_matter::data_model::device_types::DEV_TYPE_ON_OFF_LIGHT; use rs_matter::data_model::objects::{Dataver, Endpoint, HandlerCompat, Node}; use rs_matter::data_model::system_model::descriptor; use rs_matter::error::Error; -use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::init::InitMaybeUninit; use rs_matter::utils::select::Coalesce; -use rs_matter::utils::std_mutex::StdRawMutex; -use rs_matter::CommissioningData; +use rs_matter::utils::sync::blocking::raw::StdRawMutex; +use rs_matter::BasicCommData; use rs_matter_stack::modem::DummyLinuxModem; use rs_matter_stack::persist::{DirKvBlobStore, KvBlobBuf, KvPersist}; use rs_matter_stack::WifiBleMatterStack; -use static_cell::ConstStaticCell; +use static_cell::StaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; @@ -39,9 +39,28 @@ fn main() -> Result<(), Error> { info!("Starting..."); - // Take the Matter stack (can be done only once), + // Initialize the Matter stack (can be done only once), // as we'll run it in this thread - let stack = MATTER_STACK.take(); + let stack = MATTER_STACK + .uninit() + .init_with(WifiBleMatterStack::init_default( + &BasicInfoConfig { + vid: 0xFFF1, + pid: 0x8001, + hw_ver: 2, + sw_ver: 1, + sw_ver_str: "1", + serial_no: "aabbccdd", + device_name: "MyLight", + product_name: "ACME Light", + vendor_name: "ACME", + }, + BasicCommData { + password: 20202021, + discriminator: 3840, + }, + &DEV_ATT, + )); // Our "light" on-off cluster. // Can be anything implementing `rs_matter::data_model::AsyncHandler` @@ -75,11 +94,6 @@ fn main() -> Result<(), Error> { KvPersist::new_wifi_ble(DirKvBlobStore::new_default(), stack), // A Linux-specific modem using BlueZ DummyLinuxModem::default(), - // Hard-coded for demo purposes - CommissioningData { - verifier: VerifierData::new_with_pw(123456, stack.matter().rand()), - discriminator: 250, - }, // Our `AsyncHandler` + `AsyncMetadata` impl (NODE, handler), // No user future to run @@ -116,21 +130,9 @@ fn main() -> Result<(), Error> { /// The Matter stack is allocated statically to avoid /// program stack blowups. /// It is also a mandatory requirement when the `WifiBle` stack variation is used. -static MATTER_STACK: ConstStaticCell>> = - ConstStaticCell::new(WifiBleMatterStack::new_default( - &BasicInfoConfig { - vid: 0xFFF1, - pid: 0x8000, - hw_ver: 2, - sw_ver: 1, - sw_ver_str: "1", - serial_no: "aabbccdd", - device_name: "MyLight", - product_name: "ACME Light", - vendor_name: "ACME", - }, - &dev_att::HardCodedDevAtt::new(), - )); +static MATTER_STACK: StaticCell>> = StaticCell::new(); + +const DEV_ATT: dev_att::HardCodedDevAtt = dev_att::HardCodedDevAtt::new(); /// Endpoint 0 (the root endpoint) always runs /// the hidden Matter system clusters, so we pick ID=1 diff --git a/examples/light_eth.rs b/examples/light_eth.rs index 0a5aa41..31e6047 100644 --- a/examples/light_eth.rs +++ b/examples/light_eth.rs @@ -14,6 +14,7 @@ use core::pin::pin; use embassy_futures::select::select; use embassy_time::{Duration, Timer}; +use env_logger::Target; use log::info; use rs_matter::data_model::cluster_basic_information::BasicInfoConfig; @@ -22,29 +23,50 @@ use rs_matter::data_model::device_types::DEV_TYPE_ON_OFF_LIGHT; use rs_matter::data_model::objects::{Dataver, Endpoint, HandlerCompat, Node}; use rs_matter::data_model::system_model::descriptor; use rs_matter::error::Error; -use rs_matter::secure_channel::spake2p::VerifierData; +use rs_matter::utils::init::InitMaybeUninit; use rs_matter::utils::select::Coalesce; -use rs_matter::CommissioningData; +use rs_matter::BasicCommData; use rs_matter_stack::netif::UnixNetif; use rs_matter_stack::persist::{DirKvBlobStore, KvBlobBuf, KvPersist}; use rs_matter_stack::EthMatterStack; -use static_cell::ConstStaticCell; +use static_cell::StaticCell; #[path = "dev_att/dev_att.rs"] mod dev_att; fn main() -> Result<(), Error> { - env_logger::init_from_env( + env_logger::Builder::from_env( env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), - ); + ) + .target(Target::Stdout) + .init(); info!("Starting..."); - // Take the Matter stack (can be done only once), + // Initialize the Matter stack (can be done only once), // as we'll run it in this thread - let stack = MATTER_STACK.take(); + let stack = MATTER_STACK + .uninit() + .init_with(EthMatterStack::init_default( + &BasicInfoConfig { + vid: 0xFFF1, + pid: 0x8001, + hw_ver: 2, + sw_ver: 1, + sw_ver_str: "1", + serial_no: "aabbccdd", + device_name: "MyLight", + product_name: "ACME Light", + vendor_name: "ACME", + }, + BasicCommData { + password: 20202021, + discriminator: 3840, + }, + &DEV_ATT, + )); // Our "light" on-off cluster. // Can be anything implementing `rs_matter::data_model::AsyncHandler` @@ -78,11 +100,6 @@ fn main() -> Result<(), Error> { KvPersist::new_eth(DirKvBlobStore::new_default(), stack), // Will try to find a default network interface UnixNetif::default(), - // Hard-coded for demo purposes - CommissioningData { - verifier: VerifierData::new_with_pw(123456, stack.matter().rand()), - discriminator: 250, - }, // Our `AsyncHandler` + `AsyncMetadata` impl (NODE, handler), // No user future to run @@ -116,21 +133,9 @@ fn main() -> Result<(), Error> { /// The Matter stack is allocated statically to avoid /// program stack blowups. -static MATTER_STACK: ConstStaticCell>> = - ConstStaticCell::new(EthMatterStack::new_default( - &BasicInfoConfig { - vid: 0xFFF1, - pid: 0x8000, - hw_ver: 2, - sw_ver: 1, - sw_ver_str: "1", - serial_no: "aabbccdd", - device_name: "MyLight", - product_name: "ACME Light", - vendor_name: "ACME", - }, - &dev_att::HardCodedDevAtt::new(), - )); +static MATTER_STACK: StaticCell>> = StaticCell::new(); + +const DEV_ATT: dev_att::HardCodedDevAtt = dev_att::HardCodedDevAtt::new(); /// Endpoint 0 (the root endpoint) always runs /// the hidden Matter system clusters, so we pick ID=1 diff --git a/src/eth.rs b/src/eth.rs index 41746ff..6e359d2 100644 --- a/src/eth.rs +++ b/src/eth.rs @@ -15,7 +15,7 @@ use rs_matter::data_model::sdm::nw_commissioning::EthNwCommCluster; use rs_matter::data_model::sdm::{ethernet_nw_diagnostics, nw_commissioning}; use rs_matter::error::Error; use rs_matter::pairing::DiscoveryCapabilities; -use rs_matter::CommissioningData; +use rs_matter::utils::init::{init, Init}; use crate::netif::Netif; use crate::network::{Embedding, Network}; @@ -32,18 +32,26 @@ use crate::MatterStack; /// /// The expectation is nevertheless that for production use-cases /// the `Eth` network would really only be used for Ethernet. -pub struct Eth(E); +pub struct Eth { + embedding: E, +} impl Network for Eth where E: Embedding + 'static, { - const INIT: Self = Self(E::INIT); + const INIT: Self = Self { embedding: E::INIT }; type Embedding = E; fn embedding(&self) -> &Self::Embedding { - &self.0 + &self.embedding + } + + fn init() -> impl Init { + init!(Self { + embedding <- E::init(), + }) } } @@ -85,6 +93,16 @@ where Ok(()) } + /// Enable basic commissioning over IP (mDNS) by setting up a PASE session and printing the pairing code and QR code. + /// + /// The method will return an error if there is not enough space in the buffer to print the pairing code and QR code + /// or if the PASE session could not be set up (due to another PASE session already being active, for example). + pub async fn enable_basic_commissioning(&self) -> Result<(), Error> { + self.matter() + .enable_basic_commissioning(DiscoveryCapabilities::IP, 0) + .await // TODO + } + /// Run the Matter stack for Ethernet network. /// /// Parameters: @@ -97,7 +115,6 @@ where &self, persist: P, netif: I, - dev_comm: CommissioningData, handler: H, user: U, ) -> Result<(), Error> @@ -111,14 +128,14 @@ where let mut user = pin!(user); - self.run_with_netif( - persist, - netif, - Some((dev_comm, DiscoveryCapabilities::new(true, false, false))), - handler, - &mut user, - ) - .await + // TODO persist.load().await?; + + if !self.is_commissioned().await? { + self.enable_basic_commissioning().await?; + } + + self.run_with_netif(persist, netif, handler, &mut user) + .await } } diff --git a/src/lib.rs b/src/lib.rs index 0e0cb24..aba6a6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,15 +26,15 @@ use rs_matter::data_model::sdm::dev_att::DevAttDataFetcher; use rs_matter::data_model::subscriptions::Subscriptions; use rs_matter::error::{Error, ErrorCode}; use rs_matter::mdns::{Mdns, MdnsService}; -use rs_matter::pairing::DiscoveryCapabilities; use rs_matter::respond::DefaultResponder; use rs_matter::transport::network::{NetworkReceive, NetworkSend}; -use rs_matter::utils::buf::PooledBuffers; use rs_matter::utils::epoch::Epoch; +use rs_matter::utils::init::{init, Init}; use rs_matter::utils::rand::Rand; use rs_matter::utils::select::Coalesce; -use rs_matter::utils::signal::Signal; -use rs_matter::{CommissioningData, Matter, MATTER_PORT}; +use rs_matter::utils::storage::pooled::PooledBuffers; +use rs_matter::utils::sync::Signal; +use rs_matter::{BasicCommData, Matter, MATTER_PORT}; use crate::netif::{Netif, NetifConf}; use crate::network::Network; @@ -124,10 +124,12 @@ where #[inline(always)] pub const fn new_default( dev_det: &'a BasicInfoConfig, + dev_comm: BasicCommData, dev_att: &'a dyn DevAttDataFetcher, ) -> Self { Self::new( dev_det, + dev_comm, dev_att, MdnsType::default(), rs_matter::utils::epoch::sys_epoch, @@ -140,6 +142,7 @@ where #[inline(always)] pub const fn new( dev_det: &'a BasicInfoConfig, + dev_comm: BasicCommData, dev_att: &'a dyn DevAttDataFetcher, mdns: MdnsType<'a>, epoch: Epoch, @@ -148,6 +151,7 @@ where Self { matter: Matter::new( dev_det, + dev_comm, dev_att, mdns.mdns_service(), epoch, @@ -162,6 +166,51 @@ where } } + /// Create a new `MatterStack` instance. + #[cfg(feature = "std")] + #[allow(clippy::large_stack_frames)] + pub fn init_default( + dev_det: &'a BasicInfoConfig, + dev_comm: BasicCommData, + dev_att: &'a dyn DevAttDataFetcher, + ) -> impl Init { + Self::init( + dev_det, + dev_comm, + dev_att, + MdnsType::default(), + rs_matter::utils::epoch::sys_epoch, + rs_matter::utils::rand::sys_rand, + ) + } + + #[allow(clippy::large_stack_frames)] + pub fn init( + dev_det: &'a BasicInfoConfig, + dev_comm: BasicCommData, + dev_att: &'a dyn DevAttDataFetcher, + mdns: MdnsType<'a>, + epoch: Epoch, + rand: Rand, + ) -> impl Init { + init!(Self { + matter <- Matter::init( + dev_det, + dev_comm, + dev_att, + mdns.mdns_service(), + epoch, + rand, + MATTER_PORT, + ), + buffers <- PooledBuffers::init(0), + subscriptions <- Subscriptions::init(), + network <- N::init(), + mdns, + netif_conf: Signal::new(None), + }) + } + /// A utility method to replace the initial mDNS implementation with another one. /// /// Useful in particular with `MdnsType::Provided()`, where the user would still like @@ -235,6 +284,11 @@ where .await } + /// Return information whether the Matter instance is already commissioned. + pub async fn is_commissioned(&self) -> Result { + Ok(self.matter().is_commissioned()) + } + /// This method is a specialization of `run_with_transport` over the UDP transport (both IPv4 and IPv6). /// It calls `run_with_transport` and in parallel runs the mDNS service. /// @@ -244,14 +298,12 @@ where /// Parameters: /// - `persist` - a user-provided `Persist` implementation /// - `netif` - a user-provided `Netif` implementation - /// - `dev_comm` - the commissioning data and discovery capabilities /// - `handler` - a user-provided DM handler implementation /// - `user` - a user-provided future that will be polled only when the netif interface is up pub async fn run_with_netif<'d, H, P, I, U>( &self, mut persist: P, netif: I, - dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, handler: H, user: U, ) -> Result<(), Error> @@ -313,7 +365,6 @@ where udp::Udp(send), udp::Udp(recv), &mut persist, - dev_comm.clone(), &handler )); let mut mdns = pin!(self.run_builtin_mdns(&netif, &netif_conf)); @@ -358,7 +409,6 @@ where send: S, recv: R, persist: P, - dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, handler: H, ) -> Result<(), Error> where @@ -372,7 +422,7 @@ where let mut psm = pin!(self.run_psm(persist)); let mut respond = pin!(self.run_responder(handler)); - let mut transport = pin!(self.run_transport(send, recv, dev_comm)); + let mut transport = pin!(self.run_transport(send, recv)); select3(&mut psm, &mut respond, &mut transport) .coalesce() @@ -473,9 +523,10 @@ where &Host { id: 0, hostname: &hostname, - ip: _netif_conf.ipv4.octets(), - ipv6: Some(_netif_conf.ipv6.octets()), + ip: _netif_conf.ipv4, + ipv6: _netif_conf.ipv6, }, + Some(_netif_conf.ipv4), Some(_netif_conf.interface), ) .await?; @@ -493,17 +544,12 @@ where Ok(()) } - async fn run_transport( - &self, - send: S, - recv: R, - dev_comm: Option<(CommissioningData, DiscoveryCapabilities)>, - ) -> Result<(), Error> + async fn run_transport(&self, send: S, recv: R) -> Result<(), Error> where S: NetworkSend, R: NetworkReceive, { - self.matter().run(send, recv, dev_comm).await?; + self.matter().run_transport(send, recv).await?; Ok(()) } diff --git a/src/mdns.rs b/src/mdns.rs index b562dc2..7cea48f 100644 --- a/src/mdns.rs +++ b/src/mdns.rs @@ -14,18 +14,18 @@ //! Using `edge-mdns` solves this problem by providing a general-purpose mDNS which can be //! shared between the `rs-matter` stack and other - user-specific use cases. -use core::cell::RefCell; - use edge_mdns::host::Service; use embassy_sync::blocking_mutex::raw::RawMutex; -use embassy_sync::blocking_mutex::Mutex; use embassy_sync::signal::Signal; use rs_matter::data_model::cluster_basic_information::BasicInfoConfig; use rs_matter::error::{Error, ErrorCode}; use rs_matter::mdns::{Mdns, ServiceMode}; +use rs_matter::utils::blmutex::Mutex; use rs_matter::utils::buf::BufferAccess; +use rs_matter::utils::init::{init, Init}; +use rs_matter::utils::refcell::RefCell; const MAX_MATTER_SERVICES: usize = 4; const MAX_MATTER_SERVICE_NAME_LEN: usize = 40; @@ -68,7 +68,7 @@ where services: Mutex< M, RefCell< - heapless::Vec< + rs_matter::utils::vec::Vec< (heapless::String, ServiceMode), MAX_MATTER_SERVICES, >, @@ -87,11 +87,21 @@ where Self { dev_det, matter_port, - services: Mutex::new(RefCell::new(heapless::Vec::new())), + services: Mutex::new(RefCell::new(rs_matter::utils::vec::Vec::new())), broadcast_signal: Signal::new(), } } + /// Create an in-place initializer for `MatterServices` + pub fn init(dev_det: &'a BasicInfoConfig<'a>, matter_port: u16) -> impl Init { + init!(Self { + dev_det, + matter_port, + services <- Mutex::init(RefCell::init(rs_matter::utils::vec::Vec::init())), + broadcast_signal: Signal::new(), + }) + } + fn reset(&self) { self.services.lock(|services| { services.borrow_mut().clear(); diff --git a/src/netif.rs b/src/netif.rs index 7cd5327..7077609 100644 --- a/src/netif.rs +++ b/src/netif.rs @@ -151,14 +151,87 @@ where mod unix { use alloc::string::String; + use bitflags::bitflags; + use embassy_time::{Duration, Timer}; - use nix::{ - net::if_::InterfaceFlags, - sys::socket::{SockaddrIn6, SockaddrStorage}, - }; + + use nix::net::if_::InterfaceFlags; + use nix::sys::socket::{SockaddrIn6, SockaddrStorage}; use super::{Netif, NetifConf}; + bitflags! { + /// DefaultNetif is a set of flags that can be used to filter network interfaces + /// when calling `default_if` + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct NetifSearchFlags: u16 { + /// Consider only interfaces which have the Loopback flag set + const LOOPBACK = 0x0001; + /// Consider only interfaces which do not have the Loopback flag set + const NON_LOOPBACK = 0x0002; + /// Consider only interfaces which have the Point-to-Point flag set + const PEER_TO_PEER = 0x0004; + /// Consider only interfaces which do not have the Point-to-Point flag set + const NON_PEER_TO_PEER = 0x0008; + /// Consider only interfaces which have the Broadcast flag set + const BROADCAST = 0x0010; + /// Consider only interfaces which do have an IPv4 address assigned + const IPV4 = 0x0020; + /// Consider only interfaces which do have an IPv6 address assigned + const IPV6 = 0x0040; + /// Consider only interfaces which do have a Link-Local IPv6 address assigned + const IPV6_LINK_LOCAL = 0x0080; + /// Consider only interfaces which do have a non-Link-Local IPv6 address assigned + const IPV6_NON_LINK_LOCAL = 0x0100; + } + } + + impl NetifSearchFlags { + fn contains_if_flags(&self) -> InterfaceFlags { + let mut flags = InterfaceFlags::empty(); + + if self.contains(NetifSearchFlags::LOOPBACK) { + flags |= InterfaceFlags::IFF_LOOPBACK; + } + + if self.contains(NetifSearchFlags::PEER_TO_PEER) { + flags |= InterfaceFlags::IFF_POINTOPOINT; + } + + if self.contains(NetifSearchFlags::BROADCAST) { + flags |= InterfaceFlags::IFF_BROADCAST; + } + + flags + } + + fn not_contains_if_flags(&self) -> InterfaceFlags { + let mut flags = InterfaceFlags::empty(); + + if self.contains(NetifSearchFlags::NON_LOOPBACK) { + flags |= InterfaceFlags::IFF_LOOPBACK; + } + + if self.contains(NetifSearchFlags::NON_PEER_TO_PEER) { + flags |= InterfaceFlags::IFF_POINTOPOINT; + } + + flags + } + } + + impl Default for NetifSearchFlags { + fn default() -> Self { + NetifSearchFlags::NON_LOOPBACK + | NetifSearchFlags::NON_PEER_TO_PEER + | NetifSearchFlags::BROADCAST + | NetifSearchFlags::IPV4 + | NetifSearchFlags::IPV6 + | NetifSearchFlags::IPV6_LINK_LOCAL + } + } + /// UnixNetif works on any Unix-like OS /// /// It is a simple implementation of the `Netif` trait that uses polling instead of actual notifications @@ -169,7 +242,7 @@ mod unix { /// Create a new `UnixNetif`. The implementation will try /// to find and use a suitable interface automatically. pub fn new_default() -> Self { - Self(default_if().unwrap()) + Self::search(NetifSearchFlags::default()).next().unwrap() } /// Create a new `UnixNetif` for the given interface name @@ -180,6 +253,72 @@ mod unix { pub fn get_conf(&self) -> Option { get_if_conf(&self.0) } + + /// Search for network interfaces that match the given flags + pub fn search(flags: NetifSearchFlags) -> impl Iterator { + nix::ifaddrs::getifaddrs() + .ok() + .into_iter() + .flatten() + .filter(move |ia| { + ia.flags.contains(flags.contains_if_flags()) + && !ia.flags.intersects(flags.not_contains_if_flags()) + }) + .filter(move |ia| { + !flags.contains(NetifSearchFlags::IPV4) + || ia + .address + .map(|addr| addr.as_sockaddr_in().is_some()) + .unwrap_or(false) + }) + .map(|ia| ia.interface_name) + .filter(move |ifname| { + // Now also check the Ipv6 conditions + + if !flags.intersects( + NetifSearchFlags::IPV6 + | NetifSearchFlags::IPV6_LINK_LOCAL + | NetifSearchFlags::IPV6_NON_LINK_LOCAL, + ) { + return true; + } + + let Ok(iter) = nix::ifaddrs::getifaddrs() else { + return false; + }; + + let mut ipv6 = false; + let mut ipv6_link_local = !flags.contains(NetifSearchFlags::IPV6_LINK_LOCAL); + let mut ipv6_non_link_local = + !flags.contains(NetifSearchFlags::IPV6_NON_LINK_LOCAL); + + for ia2 in iter { + if &ia2.interface_name == ifname { + if let Some(ip) = ia2 + .address + .and_then(|addr| addr.as_sockaddr_in6().map(SockaddrIn6::ip)) + { + ipv6 = true; + + if flags.contains(NetifSearchFlags::IPV6_LINK_LOCAL) + && ip.octets()[..2] == [0xfe, 0x80] + { + ipv6_link_local = true; + } + + if flags.contains(NetifSearchFlags::IPV6_NON_LINK_LOCAL) + && ip.octets()[..2] != [0xfe, 0x80] + { + ipv6_non_link_local = true; + } + } + } + } + + ipv6 && ipv6_link_local && ipv6_non_link_local + }) + .map(UnixNetif) + } } impl Default for UnixNetif { @@ -211,40 +350,6 @@ mod unix { } } - fn default_if() -> Option { - // A quick and dirty way to get a network interface that has a link-local IPv6 address assigned as well as a non-loopback IPv4 - // Most likely, this is the interface we need - // (as opposed to all the docker and libvirt interfaces that might be assigned on the machine and which seem by default to be IPv4 only) - nix::ifaddrs::getifaddrs() - .unwrap() - .filter(|ia| { - // Only take interfaces which are up, which are not PTP or loopback - // and which have IPv4 IP assigned - ia.flags - .contains(InterfaceFlags::IFF_UP | InterfaceFlags::IFF_BROADCAST) - && !ia - .flags - .intersects(InterfaceFlags::IFF_LOOPBACK | InterfaceFlags::IFF_POINTOPOINT) - && ia - .address - .and_then(|addr| addr.as_sockaddr_in().map(|_| ())) - .is_some() - }) - .map(|ia| ia.interface_name) - .find(|ifname| { - // Only take interfaces which have an IPv4 address - nix::ifaddrs::getifaddrs() - .unwrap() - .filter(|ia2| &ia2.interface_name == ifname) - .any(|ia2| { - ia2.address - .and_then(|addr| addr.as_sockaddr_in6().map(SockaddrIn6::ip)) - .filter(|ip| ip.octets()[..2] == [0xfe, 0x80]) - .is_some() - }) - }) - } - fn get_if_conf(if_name: &str) -> Option { extract_if_conf( nix::ifaddrs::getifaddrs() diff --git a/src/network.rs b/src/network.rs index 23651c1..710125a 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,10 +1,18 @@ +use rs_matter::utils::init::{init_from_closure, Init}; + /// User data that can be embedded in the stack network pub trait Embedding { const INIT: Self; + + fn init() -> impl Init; } impl Embedding for () { const INIT: Self = (); + + fn init() -> impl Init { + unsafe { init_from_closure(|_| Ok(())) } + } } /// A trait modeling a specific network type. @@ -15,4 +23,6 @@ pub trait Network { type Embedding: Embedding + 'static; fn embedding(&self) -> &Self::Embedding; + + fn init() -> impl Init; } diff --git a/src/persist.rs b/src/persist.rs index f9488ff..be922a5 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -2,7 +2,8 @@ use embassy_futures::select::select; use embassy_sync::blocking_mutex::raw::{NoopRawMutex, RawMutex}; use rs_matter::error::Error; -use rs_matter::utils::buf::{BufferAccess, PooledBuffers}; +use rs_matter::utils::init::{init, Init}; +use rs_matter::utils::storage::pooled::{BufferAccess, PooledBuffers}; use rs_matter::Matter; use crate::network::{Embedding, Network}; @@ -48,7 +49,6 @@ pub struct DummyPersist; impl Persist for DummyPersist { async fn reset(&mut self) -> Result<(), Error> { - // TODO Ok(()) } @@ -157,10 +157,6 @@ where let mut buf = self.buf.get().await.unwrap(); buf.resize_default(KV_BLOB_BUF_SIZE).unwrap(); - if let Some(data) = self.store.load("acls", &mut buf).await? { - self.matter.load_acls(data)?; - } - if let Some(data) = self.store.load("fabrics", &mut buf).await? { self.matter.load_fabrics(data)?; } @@ -177,7 +173,7 @@ where /// Run the persist instance, listening for changes in the Matter stack's state. pub async fn run(&mut self) -> Result<(), Error> { loop { - let wait_matter = self.matter.wait_changed(); + let wait_fabrics = self.matter.wait_fabrics_changed(); let wait_wifi = async { if let Some(wifi_networks) = self.wifi_networks { wifi_networks.wait_state_changed().await; @@ -186,16 +182,12 @@ where } }; - select(wait_matter, wait_wifi).await; + select(wait_fabrics, wait_wifi).await; let mut buf = self.buf.get().await.unwrap(); buf.resize_default(KV_BLOB_BUF_SIZE).unwrap(); - if self.matter.is_changed() { - if let Some(data) = self.matter.store_acls(&mut buf)? { - self.store.store("acls", data).await?; - } - + if self.matter.fabrics_changed() { if let Some(data) = self.matter.store_fabrics(&mut buf)? { self.store.store("fabrics", data).await?; } @@ -410,6 +402,13 @@ where } } + fn init() -> impl Init { + init!(Self { + buf <- PooledBuffers::init(0), + embedding <- E::init(), + }) + } + pub fn buf(&self) -> &PooledBuffers<1, NoopRawMutex, KvBlobBuffer> { &self.buf } @@ -424,4 +423,8 @@ where E: Embedding, { const INIT: Self = Self::new(); + + fn init() -> impl Init { + KvBlobBuf::init() + } } diff --git a/src/wifi.rs b/src/wifi.rs index d312825..d736c21 100644 --- a/src/wifi.rs +++ b/src/wifi.rs @@ -1,15 +1,16 @@ -use core::cell::RefCell; - -use embassy_sync::blocking_mutex::{self, raw::RawMutex}; +use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_time::{Duration, Timer}; use log::{info, warn}; use rs_matter::data_model::sdm::nw_commissioning::NetworkCommissioningStatus; use rs_matter::error::{Error, ErrorCode}; -use rs_matter::tlv::{self, FromTLV, TLVList, TLVWriter, TagType, ToTLV}; -use rs_matter::utils::notification::Notification; -use rs_matter::utils::writebuf::WriteBuf; +use rs_matter::tlv::{FromTLV, TLVElement, TLVTag, ToTLV}; +use rs_matter::utils::cell::RefCell; +use rs_matter::utils::init::{init, Init}; +use rs_matter::utils::storage::WriteBuf; +use rs_matter::utils::sync::blocking::Mutex; +use rs_matter::utils::sync::Notification; pub mod comm; pub mod mgmt; @@ -27,7 +28,7 @@ struct WifiStatus { } struct WifiState { - networks: heapless::Vec, + networks: rs_matter::utils::storage::Vec, connected_once: bool, connect_requested: Option>, status: Option, @@ -35,6 +36,26 @@ struct WifiState { } impl WifiState { + const fn new() -> Self { + Self { + networks: rs_matter::utils::storage::Vec::new(), + connected_once: false, + connect_requested: None, + status: None, + changed: false, + } + } + + fn init() -> impl Init { + init!(Self { + networks <- rs_matter::utils::storage::Vec::init(), + connected_once: false, + connect_requested: None, + status: None, + changed: false, + }) + } + pub(crate) fn get_next_network(&mut self, last_ssid: Option<&str>) -> Option { // Return the requested network with priority if let Some(ssid) = self.connect_requested.take() { @@ -83,9 +104,20 @@ impl WifiState { } fn load(&mut self, data: &[u8]) -> Result<(), Error> { - let root = TLVList::new(data).iter().next().ok_or(ErrorCode::Invalid)?; + let root = TLVElement::new(data); + + let iter = root.array()?.iter(); - tlv::from_tlv(&mut self.networks, &root)?; + self.networks.clear(); + + for creds in iter { + let creds = creds?; + + self.networks + .push_init(WifiCredentials::init_from_tlv(creds), || { + ErrorCode::NoSpace.into() + })?; + } self.changed = false; @@ -98,15 +130,12 @@ impl WifiState { } let mut wb = WriteBuf::new(buf); - let mut tw = TLVWriter::new(&mut wb); - self.networks - .as_slice() - .to_tlv(&mut tw, TagType::Anonymous)?; + self.networks.to_tlv(&TLVTag::Anonymous, &mut wb)?; self.changed = false; - let len = tw.get_tail(); + let len = wb.get_tail(); Ok(Some(&buf[..len])) } @@ -119,7 +148,7 @@ pub struct WifiContext where M: RawMutex, { - state: blocking_mutex::Mutex>>, + state: Mutex>>, state_changed: Notification, network_connect_requested: Notification, } @@ -131,18 +160,20 @@ where /// Create a new instance. pub const fn new() -> Self { Self { - state: blocking_mutex::Mutex::new(RefCell::new(WifiState { - networks: heapless::Vec::new(), - connected_once: false, - connect_requested: None, - status: None, - changed: false, - })), + state: Mutex::new(RefCell::new(WifiState::new())), state_changed: Notification::new(), network_connect_requested: Notification::new(), } } + pub fn init() -> impl Init { + init!(Self { + state <- Mutex::init(RefCell::init(WifiState::init())), + state_changed: Notification::new(), + network_connect_requested: Notification::new(), + }) + } + /// Reset the state. pub fn reset(&self) { self.state.lock(|state| state.borrow_mut().reset()); diff --git a/src/wifi/comm.rs b/src/wifi/comm.rs index 403ab8a..bf3af64 100644 --- a/src/wifi/comm.rs +++ b/src/wifi/comm.rs @@ -12,9 +12,7 @@ use rs_matter::data_model::sdm::nw_commissioning::{ ReorderNetworkRequest, ResponseCommands, ScanNetworksRequest, WIFI_CLUSTER, }; use rs_matter::error::{Error, ErrorCode}; -use rs_matter::interaction_model::core::IMStatusCode; -use rs_matter::interaction_model::messages::ib::Status; -use rs_matter::tlv::{FromTLV, OctetStr, TLVElement, TagType, ToTLV}; +use rs_matter::tlv::{FromTLV, Octets, TLVElement, TLVTag, TLVWrite, ToTLV}; use rs_matter::transport::exchange::Exchange; use super::{WifiContext, WifiCredentials}; @@ -54,14 +52,14 @@ where match attr.attr_id.try_into()? { Attributes::MaxNetworks => AttrType::::new().encode(writer, N as u8), Attributes::Networks => { - writer.start_array(AttrDataWriter::TAG)?; + 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()), + network_id: Octets(network.ssid.as_str().as_bytes()), connected: state .status .as_ref() @@ -75,7 +73,7 @@ where .unwrap_or(false), }; - nw_info.to_tlv(&mut writer, TagType::Anonymous)?; + nw_info.to_tlv(&TLVTag::Anonymous, &mut *writer)?; } Ok::<_, Error>(()) @@ -100,7 +98,7 @@ where .borrow() .status .as_ref() - .map(|o| OctetStr(o.ssid.as_str().as_bytes())), + .map(|o| Octets(o.ssid.as_str().as_bytes())), ) }), Attributes::LastConnectErrorValue => self.networks.state.lock(|state| { @@ -157,16 +155,45 @@ where fn scan_networks( &self, _exchange: &Exchange<'_>, - _req: &ScanNetworksRequest<'_>, - encoder: CmdDataEncoder<'_, '_, '_>, + req: &ScanNetworksRequest<'_>, + _encoder: CmdDataEncoder<'_, '_, '_>, ) -> Result<(), Error> { - let writer = encoder.with_command(ResponseCommands::ScanNetworksResponse as _)?; - + info!("ScanNetworks req: {:?}", req); warn!("Scan network not supported"); - writer.set(Status::new(IMStatusCode::Busy, 0))?; + // Unfortunately Alexa calls `ScanNetworks` even if we have explicitly communicated + // that we do not support concurrent commissioning + // + // Cheat and declare that the SSID it is asking for is found - Ok(()) + // let mut writer = encoder.with_command(ResponseCommands::ScanNetworksResponse as _)?; + + // writer.start_struct(&CmdDataWriter::TAG)?; + + // NetworkCommissioningStatus::Success.to_tlv(&TLVTag::Context(ScanNetworksResponseTag::Status as _), &mut *writer)?; + + // writer.utf8(&TLVTag::Context(ScanNetworksResponseTag::DebugText as _), "")?; + + // writer.start_array(&TLVTag::Context(ScanNetworksResponseTag::WifiScanResults as _))?; + + // WiFiInterfaceScanResult { + // security: WiFiSecurity::Wpa2Personal, + // ssid: Octets(b"test\0"), + // bssid: Octets(&[0xF4, 0x6A, 0xDD, 0xF4, 0xF2, 0xB5]), + // channel: 20, + // band: None, + // rssi: None, + // } + // .to_tlv(&TLVTag::Anonymous, &mut *writer)?; + + // writer.end_container()?; + // writer.end_container()?; + + // writer.complete()?; + + // Ok(()) + + Err(ErrorCode::Busy)? } fn add_network( diff --git a/src/wifible.rs b/src/wifible.rs index 8b16cf7..c351482 100644 --- a/src/wifible.rs +++ b/src/wifible.rs @@ -22,8 +22,8 @@ use rs_matter::data_model::sdm::wifi_nw_diagnostics::{ use rs_matter::error::Error; use rs_matter::pairing::DiscoveryCapabilities; use rs_matter::transport::network::btp::{Btp, BtpContext, GattPeripheral}; +use rs_matter::utils::init::{init, Init}; use rs_matter::utils::select::Coalesce; -use rs_matter::CommissioningData; use crate::modem::{Modem, WifiDevice}; use crate::netif::Netif; @@ -76,6 +76,14 @@ where fn embedding(&self) -> &Self::Embedding { &self.embedding } + + fn init() -> impl Init { + init!(Self { + btp_context <- BtpContext::init(), + wifi_context <- WifiContext::init(), + embedding <- E::init(), + }) + } } pub type WifiBleMatterStack<'a, M, E> = MatterStack<'a, WifiBle>; @@ -128,15 +136,6 @@ where Ok(()) } - /// Return information whether the Matter instance is already commissioned. - /// - /// User might need to reach out to this method only when it needs finer-grained control - /// and utilizes the `commission` and `operate` methods rather than the all-in-one `run` loop. - pub async fn is_commissioned(&self) -> Result { - // TODO - Ok(false) - } - /// A utility method to run the Matter stack in Operating mode (as per the Matter Core spec) over Wifi. /// /// This method assumes that the Matter instance is already commissioned and therefore @@ -174,7 +173,7 @@ where let mut mgr = WifiManager::new(wifi, &self.network.wifi_context); - let mut main = pin!(self.run_with_netif(persist, netif, None, handler, &mut user)); + let mut main = pin!(self.run_with_netif(persist, netif, handler, &mut user)); let mut wifi = pin!(mgr.run()); select(&mut wifi, &mut main).coalesce().await @@ -195,7 +194,6 @@ where &'static self, persist: P, gatt: G, - dev_comm: CommissioningData, handler: H, ) -> Result<(), Error> where @@ -205,24 +203,23 @@ where { info!("Running Matter in commissioning mode (BLE)"); + self.matter() + .enable_basic_commissioning(DiscoveryCapabilities::BLE, 0) + .await?; // TODO + let btp = Btp::new(gatt, &self.network.btp_context); let mut ble = pin!(async { - btp.run("BT", self.matter().dev_det(), &dev_comm) - .await - .map_err(Into::into) + btp.run( + "BT", + self.matter().dev_det(), + self.matter().dev_comm().discriminator, + ) + .await + .map_err(Into::into) }); - let mut main = pin!(self.run_with_transport( - &btp, - &btp, - persist, - Some(( - dev_comm.clone(), - DiscoveryCapabilities::new(false, true, false) - )), - &handler - )); + let mut main = pin!(self.run_with_transport(&btp, &btp, persist, &handler)); select(&mut ble, &mut main).coalesce().await } @@ -241,7 +238,6 @@ where &'static self, mut persist: P, mut modem: O, - dev_comm: CommissioningData, handler: H, user: U, ) -> Result<(), Error> @@ -256,16 +252,14 @@ where let mut user = pin!(user); loop { - if !self.is_commissioned().await? { - // Reset to factory defaults everything, as we'll do commissioning all over - self.reset()?; + // TODO persist.load().await?; + if !self.is_commissioned().await? { let gatt = modem.ble().await; info!("BLE driver initialized"); - let mut main = - pin!(self.commission(&mut persist, gatt, dev_comm.clone(), &handler)); + let mut main = pin!(self.commission(&mut persist, gatt, &handler)); let mut wait_network_connect = pin!(async { self.network.wifi_context.wait_network_connect().await; Ok(())