Skip to content

Commit

Permalink
Merge pull request #155 from ivmarkov/ble
Browse files Browse the repository at this point in the history
Support for BLE and BTP
andy31415 authored Jun 28, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 3054a32 + 22d0454 commit 90412d7
Showing 23 changed files with 4,271 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci-tlv-tool.yml
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@ jobs:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy, rust-src

- name: Install libdbus
run: sudo apt-get install -y libdbus-1-dev

- name: Checkout
uses: actions/checkout@v3

3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -29,6 +29,9 @@ jobs:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy, rust-src

- name: Install libdbus
run: sudo apt-get install -y libdbus-1-dev

- name: Checkout
uses: actions/checkout@v3

3 changes: 3 additions & 0 deletions .github/workflows/publish-dry-run.yml
Original file line number Diff line number Diff line change
@@ -17,6 +17,9 @@ jobs:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy, rust-src

- name: Install libdbus
run: sudo apt-get install -y libdbus-1-dev

- name: Checkout
uses: actions/checkout@v3

3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -17,6 +17,9 @@ jobs:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy, rust-src

- name: Install libdbus
run: sudo apt-get install -y libdbus-1-dev

- name: Checkout
uses: actions/checkout@v3

4 changes: 0 additions & 4 deletions examples/Cargo.toml

This file was deleted.

264 changes: 264 additions & 0 deletions examples/onoff_light_bt/src/comm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
*
* 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 embassy_sync::blocking_mutex::raw::NoopRawMutex;
use log::{error, info, warn};

use rs_matter::data_model::objects::{
AttrDataEncoder, AttrDataWriter, AttrDetails, AttrType, CmdDataEncoder, CmdDetails, Dataver,
Handler, NonBlockingHandler,
};
use rs_matter::data_model::sdm::nw_commissioning::{
AddWifiNetworkRequest, Attributes, Commands, ConnectNetworkRequest, ConnectNetworkResponse,
NetworkCommissioningStatus, NetworkConfigResponse, RemoveNetworkRequest, 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};
use rs_matter::transport::exchange::Exchange;
use rs_matter::utils::notification::Notification;

/// A _fake_ cluster implementing the Matter Network Commissioning Cluster
/// for managing WiFi networks.
///
/// We only pretend to manage these for the purposes of the BT demo.
pub struct WifiNwCommCluster<'a> {
data_ver: Dataver,
nw_setup_complete: &'a Notification<NoopRawMutex>,
}

impl<'a> WifiNwCommCluster<'a> {
/// Create a new instance.
pub const fn new(data_ver: Dataver, nw_setup_complete: &'a Notification<NoopRawMutex>) -> Self {
Self {
data_ver,
nw_setup_complete,
}
}

/// Read an attribute.
pub fn read(
&self,
_exchange: &Exchange,
attr: &AttrDetails<'_>,
encoder: AttrDataEncoder<'_, '_, '_>,
) -> Result<(), Error> {
let Some(mut writer) = encoder.with_dataver(self.data_ver.get())? else {
return Ok(());
};

if attr.is_system() {
return WIFI_CLUSTER.read(attr.attr_id, writer);
}

match attr.attr_id.try_into()? {
Attributes::MaxNetworks => AttrType::<u8>::new().encode(writer, 1_u8),
Attributes::Networks => {
writer.start_array(AttrDataWriter::TAG)?;

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 => AttrType::new().encode(writer, 0_u8),
Attributes::LastNetworkID => {
AttrType::new().encode(writer, OctetStr("ssid".as_bytes()))
}
Attributes::LastConnectErrorValue => AttrType::new().encode(writer, 0),
}
}

/// Invoke a command.
pub 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)?;
}
Commands::AddOrUpdateWifiNetwork => {
info!("AddOrUpdateWifiNetwork");
self.add_network(exchange, &AddWifiNetworkRequest::from_tlv(data)?, encoder)?;
}
Commands::RemoveNetwork => {
info!("RemoveNetwork");
self.remove_network(exchange, &RemoveNetworkRequest::from_tlv(data)?, encoder)?;
}
Commands::ConnectNetwork => {
info!("ConnectNetwork");
self.connect_network(exchange, &ConnectNetworkRequest::from_tlv(data)?, encoder)?;
}
Commands::ReorderNetwork => {
info!("ReorderNetwork");
self.reorder_network(exchange, &ReorderNetworkRequest::from_tlv(data)?, encoder)?;
}
other => {
error!("{other:?} (not supported)");
Err(ErrorCode::CommandNotFound)?
}
}

self.data_ver.changed();

Ok(())
}

fn scan_networks(
&self,
_exchange: &Exchange<'_>,
_req: &ScanNetworksRequest<'_>,
encoder: CmdDataEncoder<'_, '_, '_>,
) -> Result<(), Error> {
let writer = encoder.with_command(ResponseCommands::ScanNetworksResponse as _)?;

warn!("Scan network not supported");

writer.set(Status::new(IMStatusCode::Busy, 0))?;

Ok(())
}

fn add_network(
&self,
_exchange: &Exchange<'_>,
req: &AddWifiNetworkRequest<'_>,
encoder: CmdDataEncoder<'_, '_, '_>,
) -> Result<(), Error> {
let writer = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?;

info!(
"Updated network with SSID {}",
core::str::from_utf8(req.ssid.0).unwrap()
);

writer.set(NetworkConfigResponse {
status: NetworkCommissioningStatus::Success,
debug_text: None,
network_index: Some(0 as _),
})?;

Ok(())
}

fn remove_network(
&self,
_exchange: &Exchange<'_>,
req: &RemoveNetworkRequest<'_>,
encoder: CmdDataEncoder<'_, '_, '_>,
) -> Result<(), Error> {
let writer = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?;

info!(
"Removed network with SSID {}",
core::str::from_utf8(req.network_id.0).unwrap()
);

writer.set(NetworkConfigResponse {
status: NetworkCommissioningStatus::Success,
debug_text: None,
network_index: Some(0 as _),
})?;

Ok(())
}

fn connect_network(
&self,
_exchange: &Exchange<'_>,
req: &ConnectNetworkRequest<'_>,
encoder: CmdDataEncoder<'_, '_, '_>,
) -> Result<(), Error> {
// Non-concurrent commissioning scenario
// (i.e. only BLE is active, and the device BLE+Wifi co-exist
// driver is not running, or does not even exist)

info!(
"Request to connect to network with SSID {} received",
core::str::from_utf8(req.network_id.0).unwrap(),
);

let writer = encoder.with_command(ResponseCommands::ConnectNetworkResponse as _)?;

// As per spec, return success even though though whether we'll be able to connect to the network
// will become apparent later, once we switch to Wifi
writer.set(ConnectNetworkResponse {
status: NetworkCommissioningStatus::Success,
debug_text: None,
error_value: 0,
})?;

// Wifi setup is complete, UDP stack can run now
self.nw_setup_complete.notify();

Ok(())
}

fn reorder_network(
&self,
_exchange: &Exchange<'_>,
req: &ReorderNetworkRequest<'_>,
encoder: CmdDataEncoder<'_, '_, '_>,
) -> Result<(), Error> {
let writer = encoder.with_command(ResponseCommands::NetworkConfigResponse as _)?;

info!(
"Network with SSID {} reordered to index {}",
core::str::from_utf8(req.network_id.0).unwrap(),
req.index
);

writer.set(NetworkConfigResponse {
status: NetworkCommissioningStatus::Success,
debug_text: None,
network_index: Some(req.index as _),
})?;

Ok(())
}
}

impl<'a> Handler for WifiNwCommCluster<'a> {
fn read(
&self,
exchange: &Exchange,
attr: &AttrDetails,
encoder: AttrDataEncoder,
) -> Result<(), Error> {
WifiNwCommCluster::read(self, exchange, attr, encoder)
}

fn invoke(
&self,
exchange: &Exchange<'_>,
cmd: &CmdDetails,
data: &TLVElement,
encoder: CmdDataEncoder,
) -> Result<(), Error> {
WifiNwCommCluster::invoke(self, exchange, cmd, data, encoder)
}
}

impl<'a> NonBlockingHandler for WifiNwCommCluster<'a> {}
165 changes: 165 additions & 0 deletions examples/onoff_light_bt/src/dev_att.rs
Original file line number Diff line number Diff line change
@@ -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<usize, Error> {
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())
}
}
}
385 changes: 385 additions & 0 deletions examples/onoff_light_bt/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,385 @@
/*
*
* 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.
*/

//! On/Off Light Example with provisioning over Bluetooth (Linux only)
//!
//! Build with:
//! `cargo build --features os,async-io,async-compat,zeroconf --example onoff_light_bt`
//! or - if you don't use Avahi:
//! `cargo build --features os,async-io,async-compat --example onoff_light_bt`
//!
//! Note that - in the absence of capabilities in the `rs-matter` core to setup and control
//! Wifi networks - this example implements a _fake_ NwCommCluster which only pretends to manage
//! Wifi networks, but in reality expects a pre-existing connection over Ethernet and/or Wifi on
//! the host machine where the example would run.
//!
//! In real-world scenarios, the user is expected to provide an actual NwCommCluster implementation
//! that can manage Wifi networks on the device by using the device-specific APIs.
//! (For (embedded) Linux, this could be done using `nmcli` or `wpa_supplicant`.)
use core::pin::pin;

use std::net::UdpSocket;

use comm::WifiNwCommCluster;
use embassy_futures::select::{select, select4};

use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_time::{Duration, Timer};
use log::{info, warn};

use rs_matter::core::{CommissioningData, Matter};
use rs_matter::data_model::cluster_basic_information::BasicInfoConfig;
use rs_matter::data_model::cluster_on_off;
use rs_matter::data_model::core::IMBuffer;
use rs_matter::data_model::device_types::DEV_TYPE_ON_OFF_LIGHT;
use rs_matter::data_model::objects::*;
use rs_matter::data_model::root_endpoint;
use rs_matter::data_model::sdm::wifi_nw_diagnostics::{
self, WiFiSecurity, WiFiVersion, WifiNwDiagCluster, WifiNwDiagData,
};
use rs_matter::data_model::subscriptions::Subscriptions;
use rs_matter::data_model::system_model::descriptor;
use rs_matter::error::Error;
use rs_matter::mdns::MdnsService;
use rs_matter::pairing::DiscoveryCapabilities;
use rs_matter::persist::Psm;
use rs_matter::respond::DefaultResponder;
use rs_matter::secure_channel::spake2p::VerifierData;
use rs_matter::transport::core::MATTER_SOCKET_BIND_ADDR;
use rs_matter::transport::network::btp::{Btp, BtpContext};
use rs_matter::utils::buf::PooledBuffers;
use rs_matter::utils::notification::Notification;
use rs_matter::utils::select::Coalesce;
use rs_matter::utils::std_mutex::StdRawMutex;
use rs_matter::MATTER_PORT;

mod comm;
// TODO: Now that we have two examples, move common stuff to a `common` filder
// The `dev_att` module would be a prime candidate for this.
mod dev_att;

static BTP_CONTEXT: BtpContext<StdRawMutex> = BtpContext::<StdRawMutex>::new();

fn main() -> Result<(), Error> {
let thread = std::thread::Builder::new()
// Increase the stack size until the example can work without stack blowups.
// Note that the used stack size increases exponentially by lowering the level of compiler optimizations,
// as lower optimization settings prevent the Rust compiler from inlining constructor functions
// which often results in (unnecessary) memory moves and increased stack utilization:
// e.g., an opt-level of "0" will require a several times' larger stack.
//
// Optimizing/lowering `rs-matter` memory consumption is an ongoing topic.
.stack_size(200 * 1024)
.spawn(run)
.unwrap();

thread.join().unwrap()
}

fn run() -> Result<(), Error> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);

info!(
"Matter memory: Matter={}B, IM Buffers={}B",
core::mem::size_of::<Matter>(),
core::mem::size_of::<PooledBuffers<10, NoopRawMutex, IMBuffer>>()
);

let dev_det = BasicInfoConfig {
vid: 0xFFF1,
pid: 0x8000,
hw_ver: 2,
sw_ver: 1,
sw_ver_str: "1",
serial_no: "aabbccdd",
device_name: "OnOff Light",
product_name: "Light123",
vendor_name: "Vendor PQR",
};

let dev_att = dev_att::HardCodedDevAtt::new();

let matter = Matter::new(
&dev_det,
&dev_att,
// NOTE:
// For `no_std` environments, provide your own epoch and rand functions here
MdnsService::Builtin,
rs_matter::utils::epoch::sys_epoch,
rs_matter::utils::rand::sys_rand,
MATTER_PORT,
);

let dev_comm = CommissioningData {
// TODO: Hard-coded for now
verifier: VerifierData::new_with_pw(123456, matter.rand()),
discriminator: 250,
};

let discovery_caps = DiscoveryCapabilities::new(false, true, false);

matter.initialize_transport_buffers()?;

info!("Matter initialized");

let buffers = PooledBuffers::<10, NoopRawMutex, _>::new(0);

info!("IM buffers initialized");

let mut mdns = pin!(run_mdns(&matter));

let on_off = cluster_on_off::OnOffCluster::new(Dataver::new_rand(matter.rand()));

let subscriptions = Subscriptions::<3>::new();

let wifi_complete = Notification::new();

// Assemble our Data Model handler by composing the predefined Root Endpoint handler with our custom On/Off clusters
let dm_handler = HandlerCompat(dm_handler(&matter, &on_off, &wifi_complete));

// Create a default responder capable of handling up to 3 subscriptions
// All other subscription requests will be turned down with "resource exhausted"
let responder = DefaultResponder::new(&matter, &buffers, &subscriptions, dm_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"
let mut respond = pin!(responder.run::<4, 4>());

// This is a sample code that simulates state changes triggered by the HAL
// Changes will be properly communicated to the Matter controllers and other Matter apps (i.e. Google Home, Alexa), thanks to subscriptions
let mut device = pin!(async {
loop {
Timer::after(Duration::from_secs(5)).await;

on_off.set(!on_off.get());
subscriptions.notify_changed();

info!("Lamp toggled");
}
});

// NOTE:
// Replace with your own persister for e.g. `no_std` environments
let mut psm = Psm::new(&matter, std::env::temp_dir().join("rs-matter"))?;
let mut persist = pin!(psm.run());

if !matter.is_commissioned() {
// Not commissioned yet, start commissioning first

let btp = Btp::new_builtin(&BTP_CONTEXT);
let mut bluetooth = pin!(btp.run("MT", &dev_det, &dev_comm));

let mut transport = pin!(matter.run(&btp, &btp, Some((dev_comm, discovery_caps))));

let mut wifi_complete_task = pin!(async {
wifi_complete.wait().await;
warn!(
"Wifi setup complete, giving 4 seconds to BTP to finish any outstanding messages"
);

Timer::after(Duration::from_secs(4)).await;

Ok(())
});

let all = select4(
&mut transport,
&mut bluetooth,
select(&mut wifi_complete_task, &mut persist).coalesce(),
select(&mut respond, &mut device).coalesce(),
);

// NOTE:
// Replace with a different executor for e.g. `no_std` environments
futures_lite::future::block_on(async_compat::Compat::new(all.coalesce()))?;

matter.reset_transport()?;
}

// NOTE:
// When using a custom UDP stack (e.g. for `no_std` environments), replace with a UDP socket bind for your custom UDP stack
// The returned socket should be splittable into two halves, where each half implements `UdpSend` and `UdpReceive` respectively
let udp = async_io::Async::<UdpSocket>::bind(MATTER_SOCKET_BIND_ADDR)?;

// Run the Matter transport
let mut transport = pin!(matter.run(&udp, &udp, None));

// Combine all async tasks in a single one
let all = select4(
&mut transport,
&mut mdns,
&mut persist,
select(&mut respond, &mut device).coalesce(),
);

// NOTE:
// Replace with a different executor for e.g. `no_std` environments
futures_lite::future::block_on(async_compat::Compat::new(all.coalesce()))
}

const NODE: Node<'static> = Node {
id: 0,
endpoints: &[
root_endpoint::endpoint(0, root_endpoint::OperNwType::Wifi),
Endpoint {
id: 1,
device_type: DEV_TYPE_ON_OFF_LIGHT,
clusters: &[descriptor::CLUSTER, cluster_on_off::CLUSTER],
},
],
};

fn dm_handler<'a>(
matter: &'a Matter<'a>,
on_off: &'a cluster_on_off::OnOffCluster,
wifi_complete: &'a Notification<NoopRawMutex>,
) -> impl Metadata + NonBlockingHandler + 'a {
(
NODE,
root_endpoint::handler(
0,
HandlerCompat(WifiNwCommCluster::new(
Dataver::new_rand(matter.rand()),
&wifi_complete,
)),
wifi_nw_diagnostics::ID,
HandlerCompat(WifiNwDiagCluster::new(
Dataver::new_rand(matter.rand()),
WifiNwDiagData {
bssid: [0; 6],
security_type: WiFiSecurity::Wpa2Personal,
wifi_version: WiFiVersion::B,
channel_number: 20,
rssi: 0,
},
)),
false,
matter.rand(),
)
.chain(
1,
descriptor::ID,
descriptor::DescriptorCluster::new(Dataver::new_rand(matter.rand())),
)
.chain(1, cluster_on_off::ID, on_off),
)
}

#[cfg(all(
feature = "std",
any(target_os = "macos", all(feature = "zeroconf", target_os = "linux"))
))]
async fn run_mdns(_matter: &Matter<'_>) -> Result<(), Error> {
// Nothing to run
core::future::pending().await
}

#[cfg(not(all(
feature = "std",
any(target_os = "macos", all(feature = "zeroconf", target_os = "linux"))
)))]
async fn run_mdns(matter: &Matter<'_>) -> Result<(), Error> {
use rs_matter::transport::network::{Ipv4Addr, Ipv6Addr};

// NOTE:
// Replace with your own network initialization for e.g. `no_std` environments
fn initialize_network() -> Result<(Ipv4Addr, Ipv6Addr, u32), Error> {
use log::error;
use nix::{net::if_::InterfaceFlags, sys::socket::SockaddrIn6};
use rs_matter::error::ErrorCode;
let interfaces = || {
nix::ifaddrs::getifaddrs().unwrap().filter(|ia| {
ia.flags
.contains(InterfaceFlags::IFF_UP | InterfaceFlags::IFF_BROADCAST)
&& !ia
.flags
.intersects(InterfaceFlags::IFF_LOOPBACK | InterfaceFlags::IFF_POINTOPOINT)
})
};

// 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)
let (iname, ip, ipv6) = interfaces()
.filter_map(|ia| {
ia.address
.and_then(|addr| addr.as_sockaddr_in6().map(SockaddrIn6::ip))
.filter(|ip| ip.octets()[..2] == [0xfe, 0x80])
.map(|ipv6| (ia.interface_name, ipv6))
})
.filter_map(|(iname, ipv6)| {
interfaces()
.filter(|ia2| ia2.interface_name == iname)
.find_map(|ia2| {
ia2.address
.and_then(|addr| addr.as_sockaddr_in().map(|addr| addr.ip().into()))
.map(|ip: std::net::Ipv4Addr| (iname.clone(), ip, ipv6))
})
})
.next()
.ok_or_else(|| {
error!("Cannot find network interface suitable for mDNS broadcasting");
ErrorCode::StdIoError
})?;

info!(
"Will use network interface {} with {}/{} for mDNS",
iname, ip, ipv6
);

Ok((ip.octets().into(), ipv6.octets().into(), 0 as _))
}

let (ipv4_addr, ipv6_addr, interface) = initialize_network()?;

use rs_matter::mdns::{
Host, MDNS_IPV4_BROADCAST_ADDR, MDNS_IPV6_BROADCAST_ADDR, MDNS_SOCKET_BIND_ADDR,
};

// NOTE:
// When using a custom UDP stack (e.g. for `no_std` environments), replace with a UDP socket bind + multicast join for your custom UDP stack
// The returned socket should be splittable into two halves, where each half implements `UdpSend` and `UdpReceive` respectively
let socket = async_io::Async::<UdpSocket>::bind(MDNS_SOCKET_BIND_ADDR)?;
socket
.get_ref()
.join_multicast_v6(&MDNS_IPV6_BROADCAST_ADDR, interface)?;
socket
.get_ref()
.join_multicast_v4(&MDNS_IPV4_BROADCAST_ADDR, &ipv4_addr)?;

matter
.run_builtin_mdns(
&socket,
&socket,
&Host {
id: 0,
hostname: "rs-matter-demo",
ip: ipv4_addr.octets(),
ipv6: Some(ipv6_addr.octets()),
},
Some(interface),
)
.await
}
10 changes: 10 additions & 0 deletions rs-matter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -67,23 +67,33 @@ x509-cert = { version = "0.2", default-features = false, features = ["pem"], opt
# STD
rand = { version = "0.8", optional = true, default-features = false, features = ["std", "std_rng"] }
async-io = { version = "2", optional = true, default-features = false }
async-compat = { version = "0.2", optional = true, default-features = false }

[target.'cfg(target_os = "macos")'.dependencies]
astro-dnssd = { version = "0.3" }

[target.'cfg(target_os = "linux")'.dependencies]
zeroconf = { version = "0.12", optional = true }
bluer = { version = "0.17", features = ["bluetoothd"] }
tokio = { version = "1" }
tokio-stream = { version = "0.1" }

[dev-dependencies]
env_logger = "0.11"
nix = { version = "0.27", features = ["net"] }
futures-lite = "1"
async-channel = "2"

[[example]]
name = "onoff_light"
path = "../examples/onoff_light/src/main.rs"
required-features = ["std", "async-io"]

[[example]]
name = "onoff_light_bt"
path = "../examples/onoff_light_bt/src/main.rs"
required-features = ["std", "async-io", "async-compat"]

# [[example]]
# name = "speaker"
# path = "../examples/speaker/src/main.rs"
17 changes: 17 additions & 0 deletions rs-matter/src/error.rs
Original file line number Diff line number Diff line change
@@ -204,6 +204,23 @@ impl From<ccm::aead::Error> for Error {
}
}

#[cfg(all(feature = "std", target_os = "linux", not(feature = "backtrace")))]
impl From<bluer::Error> for Error {
fn from(e: bluer::Error) -> Self {
// Log the error given that we lose all context from the
// original error here
::log::error!("Error in BTP: {e}");
Self::new(ErrorCode::BtpError)
}
}

#[cfg(all(feature = "std", target_os = "linux", feature = "backtrace"))]
impl From<bluer::Error> for Error {
fn from(e: bluer::Error) -> Self {
Self::new_with_details(ErrorCode::BtpError, Box::new(e))
}
}

#[cfg(feature = "std")]
impl From<std::time::SystemTimeError> for Error {
fn from(_e: std::time::SystemTimeError) -> Self {
1 change: 1 addition & 0 deletions rs-matter/src/pairing/mod.rs
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ use self::{
qr::{compute_qr_code_text, print_qr_code},
};

// TODO: Rework as a `bitflags!` enum
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct DiscoveryCapabilities {
on_ip_network: bool,
1 change: 1 addition & 0 deletions rs-matter/src/transport/core.rs
Original file line number Diff line number Diff line change
@@ -80,6 +80,7 @@ pub struct TransportMgr<'m> {
pub(crate) session_removed: Notification<NoopRawMutex>,
pub session_mgr: RefCell<SessionMgr>, // For testing
pub(crate) mdns: MdnsImpl<'m>,
#[allow(dead_code)]
rand: Rand,
}

1 change: 1 addition & 0 deletions rs-matter/src/transport/network.rs
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ use embassy_futures::select::{select, Either};

use crate::error::{Error, ErrorCode};

pub mod btp;
pub mod udp;

// Maximum UDP RX packet size per Matter spec
359 changes: 359 additions & 0 deletions rs-matter/src/transport/network/btp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
/*
*
* 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 core::borrow::Borrow;
use core::future::Future;
use core::marker::PhantomData;
use core::ops::DerefMut;

use embassy_futures::select::select4;
use embassy_sync::blocking_mutex::raw::{NoopRawMutex, RawMutex};
use embassy_time::{Duration, Instant, Timer};

use log::trace;

use context::LockError;
use session::{BTP_ACK_TIMEOUT_SECS, BTP_CONN_IDLE_TIMEOUT_SECS};

use crate::data_model::cluster_basic_information::BasicInfoConfig;
use crate::error::{Error, ErrorCode};
use crate::transport::network::{Address, BtAddr, NetworkReceive, NetworkSend};
use crate::utils::ifmutex::IfMutex;
use crate::utils::select::Coalesce;
use crate::CommissioningData;

pub use context::{BtpContext, MAX_BTP_SESSIONS};
pub use gatt::*;

use self::context::SessionSendLock;

mod context;
mod gatt;
mod session;
#[cfg(test)]
mod test;

/// The maximum size of a BTP segment.
pub(crate) const MAX_BTP_SEGMENT_SIZE: usize = 244;
/// The size of the GATT header. `MAX_BTP_SEGMENT_SIZE` + `GATT_HEADER_SIZE` is 247 bytes, which is the maximum ATT MTU size supported by the BTP protocol.
pub(crate) const GATT_HEADER_SIZE: usize = 3;

/// The minimum MTU that can be used as per specification.
pub(crate) const MIN_MTU: u16 = (20 + GATT_HEADER_SIZE) as u16;
/// The maximum MTU that can be used as per specification.
pub(crate) const MAX_MTU: u16 = (MAX_BTP_SEGMENT_SIZE + GATT_HEADER_SIZE) as u16;

/// An implementation of the Matter BTP protocol.
/// This is a low-level protocol that is used to send and receive Matter messages over BLE.
///
/// The implementation needs a `Gatt` trait implementation which is OS/platform-specific.
/// All aspects of the BTP protocol however are implemented in platform-neutral way.
pub struct Btp<C, M, T> {
gatt: T,
context: C,
send_buf: IfMutex<NoopRawMutex, heapless::Vec<u8, MAX_BTP_SEGMENT_SIZE>>,
ack_timeout_secs: u16,
conn_idle_timeout_secs: u16,
_mutex: PhantomData<M>,
}

#[cfg(all(feature = "std", target_os = "linux"))]
impl<C, M> Btp<C, M, BuiltinGattPeripheral>
where
C: Borrow<BtpContext<M>> + Clone + Send + Sync + 'static,
M: RawMutex + Send + Sync,
{
#[inline(always)]
pub fn new_builtin(context: C) -> Self {
Self::new(BuiltinGattPeripheral::new(None), context)
}
}

impl<C, M, T> Btp<C, M, T>
where
C: Borrow<BtpContext<M>> + Clone + Send + Sync + 'static,
M: RawMutex + Send + Sync,
T: GattPeripheral,
{
/// Construct a new BTP object with the provided `GattPeripheral` trait implementation and with the
/// provided BTP `context`.
#[inline(always)]
pub const fn new(gatt: T, context: C) -> Self {
Self::new_internal(
gatt,
context,
BTP_ACK_TIMEOUT_SECS,
BTP_CONN_IDLE_TIMEOUT_SECS,
)
}

#[inline(always)]
const fn new_internal(
gatt: T,
context: C,
ack_timeout_secs: u16,
conn_idle_timeout_secs: u16,
) -> Self {
Self {
gatt,
context,
send_buf: IfMutex::new(heapless::Vec::new()),
ack_timeout_secs,
conn_idle_timeout_secs,
_mutex: PhantomData,
}
}

/// Run the BTP protocol
///
/// While all sending and receiving of Matter packets (a.k.a. BTP SDUs) is done via the `recv` and `send` methods
/// on the `Btp` struct, this method is responsible for managing internal implementation aspects of
/// the BTP protocol implementation, like e.g. the sessions' keepalive logic.
///
/// Therefore, user is expected to call this method in order to run the BTP protocol.
pub fn run<'a>(
&'a self,
service_name: &'a str,
dev_det: &BasicInfoConfig<'_>,
dev_comm: &CommissioningData,
) -> impl Future<Output = Result<(), Error>> + 'a {
let adv_data = AdvData::new(dev_det, dev_comm);

let context = self.context.clone();

async move {
select4(
self.gatt.run(service_name, &adv_data, move |event| {
context.borrow().on_event(event)
}),
self.handshake(),
self.ack(),
self.remove_expired(),
)
.coalesce()
.await
}
}

/// Wait until there is at least one Matter (a.k.a. BTP SDU) packet available for consumption.
pub async fn wait_available(&self) -> Result<(), Error> {
self.context.borrow().wait_available().await
}

/// Receive a Matter (a.k.a. BTP SDU) packet.
///
/// If there is no packet available, this method will block asynchronously until a packet is available.
/// Returns the size of the received packet, as well as the address of the BLE peer from where the packet originates.
pub async fn recv(&self, buf: &mut [u8]) -> Result<(usize, BtAddr), Error> {
self.context.borrow().recv(buf).await
}

/// Send a Matter (a.k.a. BTP SDU) packet to the specified BLE peer.
///
/// The `data` parameter is the data to be sent.
/// The `address` parameter is the BLE address of the peer to which the data should be sent.
///
/// If the peer is not connected, this method will return an error.
/// If the BTP stack is busy sending data to another peer, this method will block asynchronously until the stack is ready to send the data.
pub async fn send(&self, data: &[u8], address: BtAddr) -> Result<(), Error> {
let context = self.context.borrow();

let session_lock = loop {
match SessionSendLock::try_lock(context, |session| session.address() == address) {
Ok(session_lock) => break session_lock,
Err(LockError::NoMatch) => Err(ErrorCode::NoNetworkInterface)?,
Err(LockError::AlreadyLocked) => (),
}

context.send_notif.wait().await;
};

self.do_send(&session_lock, data).await?;

Ok(())
}

/// Internal utility method that sends a BTP SDU packet on behalf of a session which is locked for sending.
///
/// The `session_lock` parameter represents a session which had been locked for sending.
/// The `data` parameter is the data to be sent as part of the BTP SDU packet.
async fn do_send(
&self,
session_lock: &SessionSendLock<'_, M>,
data: &[u8],
) -> Result<(), Error> {
let mut offset = 0;

loop {
let mut buf = self.send_buf().await;

let packet = session_lock
.with_session(|session| session.prep_tx_data(data, offset, &mut buf))?;

if let Some((slice, new_offset)) = packet {
self.gatt.indicate(slice, session_lock.address()).await?;
offset = new_offset;

trace!(
"Sent {slice:02x?} bytes to address {}",
session_lock.address()
);

if offset == data.len() {
break;
}
} else {
drop(buf);

self.context.borrow().send_notif.wait().await;
}
}

Ok(())
}

/// A job that is responsible for removing all sessions, which are considered expired due to
/// the remote peers not sending an ACK packet on time.
async fn remove_expired(&self) -> Result<(), Error> {
let context = self.context.borrow();

loop {
Timer::after(Duration::from_secs(1)).await;

// Remove all timed-out sessions
context.remove(|session| {
session.is_timed_out(Instant::now(), self.conn_idle_timeout_secs)
})?;

// Notify ack() below that maybe it is time to send an ACK packet
context.ack_notif.notify();
}
}

/// A job that is responsible for sending ACK on behalf of all sessions, which
/// either have their receive windows full, or which would otherwise expire due to inactivity.
async fn ack(&self) -> Result<(), Error> {
let context = self.context.borrow();

loop {
while let Some(session_lock) = SessionSendLock::lock_any(context, |session| {
session.is_ack_due(Instant::now(), self.ack_timeout_secs)
}) {
self.do_send(&session_lock, &[]).await?;
}

context.ack_notif.wait().await;
}
}

/// A job that is resposible for sending the Handshake Response packet to all remote peers that
/// in the meantime have connected to the peripheral, subscribed to chracteristic `C2` and had
/// written the Handshake Request packet to characteristic `C1`.
async fn handshake(&self) -> Result<(), Error> {
let context = self.context.borrow();

loop {
while let Some(session_lock) =
SessionSendLock::lock_any(context, session::Session::is_handshake_resp_due)
{
let mut buf = self.send_buf().await;

let slice =
session_lock.with_session(|session| session.prep_tx_handshake(&mut buf))?;

self.gatt.indicate(slice, session_lock.address()).await?;

trace!(
"Sent {slice:02x?} bytes to address {}",
session_lock.address()
);
}

context.handshake_notif.wait().await;
}
}

/// Get a mutable reference to the send buffer, asybchronously waiting for the buffer to become available,
/// in case it is used by another operation.
async fn send_buf(
&self,
) -> impl DerefMut<Target = heapless::Vec<u8, MAX_BTP_SEGMENT_SIZE>> + '_ {
let mut buf = self.send_buf.lock().await;

// Unwrap is safe because the max size of the buffer is MAX_PDU_SIZE
buf.resize_default(MAX_BTP_SEGMENT_SIZE).unwrap();

buf
}
}

impl<C, M, T> NetworkSend for &Btp<C, M, T>
where
C: Borrow<BtpContext<M>> + Clone + Send + Sync + 'static,
M: RawMutex + Send + Sync,
T: GattPeripheral,
{
async fn send_to(&mut self, data: &[u8], addr: Address) -> Result<(), Error> {
(*self)
.send(data, addr.btp().ok_or(ErrorCode::NoNetworkInterface)?)
.await
}
}

impl<C, M, T> NetworkReceive for &Btp<C, M, T>
where
C: Borrow<BtpContext<M>> + Clone + Send + Sync + 'static,
M: RawMutex + Send + Sync,
T: GattPeripheral,
{
async fn wait_available(&mut self) -> Result<(), Error> {
(*self).wait_available().await
}

async fn recv_from(&mut self, buffer: &mut [u8]) -> Result<(usize, Address), Error> {
(*self)
.recv(buffer)
.await
.map(|(len, addr)| (len, Address::Btp(addr)))
}
}

impl<C, M, T> NetworkSend for Btp<C, M, T>
where
C: Borrow<BtpContext<M>> + Clone + Send + Sync + 'static,
M: RawMutex + Send + Sync,
T: GattPeripheral,
{
async fn send_to(&mut self, data: &[u8], addr: Address) -> Result<(), Error> {
(&*self).send_to(data, addr).await
}
}

impl<C, M, T> NetworkReceive for Btp<C, M, T>
where
C: Borrow<BtpContext<M>> + Clone + Send + Sync + 'static,
M: RawMutex + Send + Sync,
T: GattPeripheral,
{
async fn wait_available(&mut self) -> Result<(), Error> {
(*self).wait_available().await
}

async fn recv_from(&mut self, buffer: &mut [u8]) -> Result<(usize, Address), Error> {
(&*self).recv_from(buffer).await
}
}
388 changes: 388 additions & 0 deletions rs-matter/src/transport/network/btp/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
/*
*
* 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 core::cell::RefCell;

use embassy_sync::blocking_mutex::{raw::RawMutex, Mutex};
use log::{error, info, trace, warn};

use crate::error::{Error, ErrorCode};
use crate::transport::network::BtAddr;
use crate::utils::notification::Notification;

use super::{session::Session, GattPeripheralEvent};

/// The maximum number of BTP sessions that can be active at any given time.
/// This is an `rs-matter` specific limit, and is not a requirement of the Matter BTP spec, and which in future should be configurable.
///
/// The `GattPeripheral` implementation is expected to enforce this limit as well,
/// i.e. it should not allow more than `MAX_BTP_SESSIONS` active subscriptions to characteristic `C2`.
pub const MAX_BTP_SESSIONS: usize = 2;

/// Represents an error that occurred while trying to lock a session for sending.
#[derive(Debug)]
pub(crate) enum LockError {
/// Session for the specified condition was not found.
NoMatch,
/// Session for the specified condition was found, but it was already locked for sending.
AlreadyLocked,
}

/// An internal utility for representing a session which is locked for sending.
///
/// This type is used to ensure that at any moment in time, a session either is not sending anything,
/// or is sending the BTP PDUs of a single BTP SDU, which is a requirement of the Matter BTP spec.
///
/// The send lock is removed once this object is dropped.
pub(crate) struct SessionSendLock<'a, M>
where
M: RawMutex,
{
context: &'a BtpContext<M>,
address: BtAddr,
}

impl<'a, M> SessionSendLock<'a, M>
where
M: RawMutex,
{
/// Try to find a session that matches the given condition and lock it for sending.
///
/// - If there is no session matching the given condition, the method will return `LockError::NoMatch`.
/// - If the first session matching the given condition is already locked for sending, the method will return `LockError::AlreadyLocked`.
///
/// Due to the above semantics, the condition is expected to uniquely identify a session, by - say - matching on
/// the session peer BLE address.
pub fn try_lock<F>(context: &'a BtpContext<M>, condition: F) -> Result<Self, LockError>
where
F: Fn(&Session) -> bool,
{
context.sessions.lock(move |sessions| {
let mut sessions = sessions.borrow_mut();

let Some(session) = sessions.iter_mut().find(|session| condition(session)) else {
return Err(LockError::NoMatch);
};

if !session.set_sending(true) {
Err(LockError::AlreadyLocked)?;
}

Ok(Self {
context,
address: session.address(),
})
})
}

/// Lock one (out of potentially many) sessions matcing the provided condition for sending.
///
/// If all sessions matching the provided condition are already locked for sending, or if there is no
/// session matching the provided condition, the method will return `None`.
pub fn lock_any<F>(context: &'a BtpContext<M>, condition: F) -> Option<Self>
where
F: Fn(&Session) -> bool,
{
context.sessions.lock(move |sessions| {
sessions.borrow_mut().iter_mut().find_map(|session| {
if condition(session) && session.set_sending(true) {
Some(Self {
context,
address: session.address(),
})
} else {
None
}
})
})
}

/// Return the peer BLE address.
pub fn address(&self) -> BtAddr {
self.address
}

/// Execute the provided closure with a mutable reference to the session locked for sending.
///
/// If the session is no longer present, the method will return `ErrorCode::NoNetworkInterface`.
pub fn with_session<F, R>(&self, f: F) -> Result<R, Error>
where
F: FnOnce(&mut Session) -> Result<R, Error>,
{
self.context.sessions.lock(|sessions| {
let mut sessions = sessions.borrow_mut();
let session = sessions
.iter_mut()
.find(|session| session.address() == self.address)
.ok_or(ErrorCode::NoNetworkInterface)?;

f(session)
})
}
}

impl<'a, M> Drop for SessionSendLock<'a, M>
where
M: RawMutex,
{
fn drop(&mut self) {
self.context.sessions.lock(|sessions| {
if let Some(session) = sessions
.borrow_mut()
.iter_mut()
.find(|session| session.address() == self.address)
{
if !session.set_sending(false) || !session.set_running() {
// If we reach here this is a bug, because
// - a `SessionSendLock` cannot be acqired unless the session is
// either in `Subscribed` or `Running` state already, during the
// lock acqusition.
// - The session is set to `seding` state when the lock is acquired,
// and is unset when the lock is dropped.
unreachable!("Should not happen")
}
}
});

self.context.send_notif.notify();
}
}

/// A structure representing a BTP "context".
///
/// The BTP protocol implementation is split into two structures:
/// - `Btp` - the main BTP protocol implementation, which is responsible for handling the BTP protocol itself. This structure is not `Send` and `Sync`
/// and is overall a typical future-based protocol implementation, like the others in the `rs-matter` stack.
/// - `BtpContext` - a structure that holds the state of the BTP protocol shared between itself and the Gatt peripheral implementation.
/// In terms of ownership, The `Btp` instance holds a `'static` reference to the context, i.e. a `&'static BtpContext<M>` reference,
/// or an `Arc<BtpContext<M>>` instance for platforms where the Rust `alloc::sync` module is available.
/// Furthermore, the state kept in `BtpContext` is safe to share amongst multiple threads.
///
/// The need to split the BTP implementation into two structures is due to the fact that the `GattPeripheral` trait uses a
/// `'static + Send + Sync` callback closure so as to report subscribe, unsubscribe and write events back to the BTP protocol implementation.
///
/// While this simplifies the implementation of the `GattPeripheral` trait (as MCU-based Gatt peripheral stacks often expect a closure with these
/// precise restrictions), it complicates the implementation of the BTP protocol and necessiates the isolation of the shared state in the
/// `BtpContext` structure.
pub struct BtpContext<M>
where
M: RawMutex,
{
pub(crate) sessions: Mutex<M, RefCell<heapless::Vec<Session, MAX_BTP_SESSIONS>>>,
pub(crate) handshake_notif: Notification<M>,
pub(crate) available_notif: Notification<M>,
pub(crate) recv_notif: Notification<M>,
pub(crate) ack_notif: Notification<M>,
pub(crate) send_notif: Notification<M>,
}

impl<M> Default for BtpContext<M>
where
M: RawMutex,
{
fn default() -> Self {
Self::new()
}
}

impl<M> BtpContext<M>
where
M: RawMutex,
{
/// Create a new BTP context.
#[inline(always)]
pub const fn new() -> Self {
Self {
sessions: Mutex::new(RefCell::new(heapless::Vec::new())),
handshake_notif: Notification::new(),
available_notif: Notification::new(),
recv_notif: Notification::new(),
ack_notif: Notification::new(),
send_notif: Notification::new(),
}
}
}

impl<M> BtpContext<M>
where
M: RawMutex,
{
/// The `Btp` instance passes a closure of this method to the `GattPeripheral` implementation which is in use
/// so that the peripheral can report to it subscribe, unsubscribe and write events.
pub(crate) fn on_event(&self, event: GattPeripheralEvent) {
let result = match event {
GattPeripheralEvent::NotifySubscribed(address) => self.on_subscribe(address),
GattPeripheralEvent::NotifyUnsubscribed(address) => self.on_unsubscribe(address),
GattPeripheralEvent::Write {
address,
data,
gatt_mtu,
} => self.on_write(address, data, gatt_mtu),
};

if let Err(e) = result {
error!("Unexpected error in GATT callback: {e:?}");
}
}

/// Handles a write event to characteristic `C1` from the GATT peripheral.
fn on_write(&self, address: BtAddr, data: &[u8], gatt_mtu: Option<u16>) -> Result<(), Error> {
trace!("Received {data:02x?} bytes from {address}");

self.sessions.lock(|sessions| {
let mut sessions = sessions.borrow_mut();

if Session::is_handshake(data)? {
if sessions.len() >= MAX_BTP_SESSIONS {
warn!("Too many BTP sessions, dropping a handshake request from address {address}");
} else {
// Unwrap is safe because we checked the length above
sessions
.push(Session::process_rx_handshake(address, data, gatt_mtu)?)
.unwrap();
}

Ok(())
} else {
let Some(index) = sessions
.iter_mut()
.position(|session| session.address() == address)
else {
warn!("Dropping data from address {address} because there is no session for it");
return Ok(());
};

let session = &mut sessions[index];
let result = session.process_rx_data(data);

if result.is_err() {
sessions.swap_remove(index);
error!("Dropping session {address} because of an error: {result:?}");
}

self.available_notif.notify();
self.recv_notif.notify();
self.ack_notif.notify();
self.send_notif.notify();

result
}
})
}

/// Handles a subscribe event to characteristic `C2` from the GATT peripheral.
fn on_subscribe(&self, address: BtAddr) -> Result<(), Error> {
info!("Subscribe request from {address}");

self.sessions.lock(|sessions| {
let mut sessions = sessions.borrow_mut();
if let Some(session) = sessions
.iter_mut()
.find(|session| session.address() == address)
{
if !session.set_subscribed() {
warn!("Got a second subscribe request for an address which is already subscribed: {address}");
Err(ErrorCode::InvalidState)?;
}

self.handshake_notif.notify();
} else {
warn!("No session for address {address}");
}

Ok(())
})
}

/// Handles an unsubscribe event to characteristic `C2` from the GATT peripheral.
fn on_unsubscribe(&self, address: BtAddr) -> Result<(), Error> {
info!("Unsubscribe request from {address}");

self.remove(|session| session.address() == address)
}

/// Removes all sesssions that match the provided condition.
pub(crate) fn remove<F>(&self, condition: F) -> Result<(), Error>
where
F: Fn(&Session) -> bool,
{
self.sessions.lock(|sessions| {
let mut sessions = sessions.borrow_mut();
while let Some(index) = sessions.iter().position(&condition) {
let session = sessions.swap_remove(index);
info!("Session {} removed", session.address());

self.send_notif.notify();
}

Ok(())
})
}

/// Will wait until there is at least one session which has a BTP SDU packet ready for consumption by the Matter stack.
///
/// `Btp::wait_available` internally delegates to this method.
pub(crate) async fn wait_available(&self) -> Result<(), Error> {
loop {
let available = self.sessions.lock(|sessions| {
sessions
.borrow()
.iter()
.any(|session| session.message_available())
});

if available {
break;
}

self.available_notif.wait().await;
}

Ok(())
}

/// Receive a Matter (a.k.a. BTP SDU) packet.
///
/// If there is no packet available, this method will block asynchronously until a packet is available.
/// Returns the size of the received packet, as well as the address of the BLE peer from where the packet originates.
///
/// `Btp::recv` internally delegates to this method.
pub(crate) async fn recv(&self, buf: &mut [u8]) -> Result<(usize, BtAddr), Error> {
loop {
let result = self.sessions.lock(|sessions| {
let mut sessions = sessions.borrow_mut();

let Some(session) = sessions
.iter_mut()
.find(|session| session.message_available())
else {
return Ok::<_, Error>(None);
};

let len = session.fetch_message(buf)?;

Ok(Some((len, session.address())))
})?;

if let Some(result) = result {
break Ok(result);
}

self.recv_notif.wait().await;
}
}
}
252 changes: 252 additions & 0 deletions rs-matter/src/transport/network/btp/gatt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
*
* 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 core::iter::{empty, once};

use crate::data_model::cluster_basic_information::BasicInfoConfig;
use crate::error::Error;
use crate::transport::network::BtAddr;
use crate::CommissioningData;

#[cfg(all(feature = "std", target_os = "linux"))]
pub use builtin::BluerGattPeripheral as BuiltinGattPeripheral;

use super::{GATT_HEADER_SIZE, MAX_BTP_SEGMENT_SIZE};

#[cfg(all(feature = "std", target_os = "linux"))]
#[path = "gatt/bluer.rs"]
mod builtin;

// The 16-bit, registered Matter Service UUID, as per the Matter Core spec.
pub const MATTER_BLE_SERVICE_UUID16: u16 = 0xFFF6;
// A 128-bit expanded representation of the Matter Service UUID.
pub const MATTER_BLE_SERVICE_UUID: u128 = 0x0000FFF600001000800000805F9B34FB;

/// `C1` characteristic UUID, as per the Matter Core spec.
pub const C1_CHARACTERISTIC_UUID: u128 = 0x18EE2EF5263D4559959F4F9C429F9D11;
/// `C2` characteristic UUID, as per the Matter Core spec.
pub const C2_CHARACTERISTIC_UUID: u128 = 0x18EE2EF5263D4559959F4F9C429F9D12;
/// `C3` characteristic UUID, as per the Matter Core spec.
pub const C3_CHARACTERISTIC_UUID: u128 = 0x64630238877245F2B87D748A83218F04;

/// The maximum length of packet data written to the `C1` characteristic, as per the Matter Core spec, and as advertised in the GATT service.
pub const C1_MAX_LEN: usize = MAX_BTP_SEGMENT_SIZE + GATT_HEADER_SIZE;
/// The maximum length of packet data indicated via the `C2` characteristic, as per the Matter Core spec, and as advertised in the GATT service.
pub const C2_MAX_LEN: usize = MAX_BTP_SEGMENT_SIZE + GATT_HEADER_SIZE;
/// The maximum length of data read from the `C3` characteristic, as per the Matter Core spec, and as advertised in the GATT service.
pub const C3_MAX_LEN: usize = 512;

/// Encapsulates the advertising data for the Matter BTP protocol.
///
/// See section "5.4.2.5.6. Advertising Data" in the Core Matter spec
#[derive(Clone)]
pub struct AdvData {
vid: u16,
pid: u16,
discriminator: u16,
}

impl AdvData {
/// Create a new instance by using the provided `BasicInfoConfig` and `CommissioningData`.
pub const fn new(dev_det: &BasicInfoConfig, comm_data: &CommissioningData) -> Self {
Self {
vid: dev_det.vid,
pid: dev_det.pid,
discriminator: comm_data.discriminator,
}
}

/// Return an iterator over the binary representation of the advertising data.
///
/// As per the Matter Core spec, the advertising data consists of
/// an AD1 record which is of Flags type, and an AD2 record, which is of type UUID16+Service Data
pub fn iter(&self) -> impl Iterator<Item = u8> + '_ {
self.flags_iter().chain(self.service_iter())
}

/// Return an iterator over the binary representation of the AD1 advertising data (Flags).
/// Useful with GATT stacks that require the advertising data to be reported as separate AD records
pub fn flags_iter(&self) -> impl Iterator<Item = u8> + '_ {
empty()
.chain(once(self.flags_payload_iter().count() as u8 + 1)) // 1-byte type
.chain(once(self.flags_adv_type()))
.chain(self.flags_payload_iter())
}

/// The AD1 advertising data type (Flags).
pub const fn flags_adv_type(&self) -> u8 {
0x01
}

/// Return an iterator over the binary representation of the AD1 advertising data _payload_.
/// Useful with GATT stacks that require the advertising data to be reported as separate AD records
pub fn flags_payload_iter(&self) -> impl Iterator<Item = u8> + '_ {
once(0x06)
}

/// Return an iterator over the binary representation of the AD2 advertising data (UUID16+Service Data).
pub fn service_iter(&self) -> impl Iterator<Item = u8> + '_ {
empty()
.chain(once(self.service_payload_iter().count() as u8 + 3)) // + 1-byte type and 2-bytes Matter UUID16 Service
.chain(once(self.service_adv_type()))
.chain(MATTER_BLE_SERVICE_UUID16.to_le_bytes())
.chain(self.service_payload_iter())
}

/// The AD2 advertising data type (UUID16+Service Data).
pub const fn service_adv_type(&self) -> u8 {
0x16
}

/// Return an iterator over the binary representation of the AD2 advertising data _payload_.
/// Useful with GATT stacks that require the advertising data to be reported as separate AD records
pub fn service_payload_iter(&self) -> impl Iterator<Item = u8> + '_ {
[
0, // Always 0 = "Commissionable"
self.discriminator.to_le_bytes()[0],
self.discriminator.to_le_bytes()[1],
self.vid.to_le_bytes()[0],
self.vid.to_le_bytes()[1],
self.pid.to_le_bytes()[0],
self.pid.to_le_bytes()[1],
0, // No additional data
]
.into_iter()
}
}

/// A minimal GATT peripheral event.
/// This enum is used to abstract the platform-specific GATT peripheral events.
///
/// The abstraction is "minimal" in the sense that it is good enough for the purposes of
/// the Matter BTP protocol, but is otherwise not really having the ambition to model all
/// possible events of a generic GATT peripheral, which would result in a much larger API surface.
#[derive(Debug, Clone)]
pub enum GattPeripheralEvent<'a> {
/// A GATT central has subscribed for notifications from characteristic `C2`.
/// In other words, the GATT central is now ready to receive BTP packets.
///
/// See the Matter Core spec w.r.t. details on characteristic `C2`.
NotifySubscribed(BtAddr),
/// A GATT central has unsubscribed for notifications from characteristic `C2`.
/// In other words, the GATT central is closing the BTP session.
///
/// See the Matter Core spec w.r.t. details on characteristic `C2`.
NotifyUnsubscribed(BtAddr),
/// A GATT central has requested a Write to characteristic `C1`.
/// In other words, the GATT central had sent a BTP packet.
///
/// See the Matter Core spec w.r.t. details on characteristic `C1`.
///
/// `gatt_mtu` is the ATT MTU (contains +3 bytes for the GATT header)
/// as negotiated between the GATT central and the GATT peripheral.
/// Might be `None` if a concrete GATT peripheral implementation does
/// not provide access to this value. In that case, the minimum MTU
/// will be used (23 bytes, including the GATT header).
Write {
address: BtAddr,
data: &'a [u8],
gatt_mtu: Option<u16>,
},
}

/// A minimal GATT peripheral trait.
/// This trait is used to abstract the platform-specific GATT peripheral implementation.
///
/// The abstraction is "minimal" in the sense that it is good enough for the purposes of
/// the Matter BTP protocol, but is otherwise not really having the ambition to model all
/// the aspects of a generic GATT peripheral, which would result in a much larger trait.
///
/// The design of this trait is deliberately chosen to be simple and easy to implement
/// on top of MCU-based GATT peripherals; hence the blocking, callback-based approach for modeling
/// notification subscriptions and chracteristic writes, which - while making the BTP protocol
/// implementation more complex - is a good fit for MCUs where these operations might also be
/// implemented via a callback which cannot await.
pub trait GattPeripheral {
/// Run the GATT peripheral.
///
/// The implementation of this method is expected to do the following:
/// - GATT peripheral lifecycle:
/// - Start avertising a GATT service with UUID `MATTER_BLE_SERVICE_UUID16`, by utilizing
/// the provided `service_name` and `adv_data` parameters.
/// - Possibly stop advertising the GATT service when the first notification subscription is received.
/// - Stop advertising and tear down the GATT service when the future of this method is dropped.
/// - Gatt peripheral incoming data:
/// - Handle incoming GATT events, and call the provided `callback` function for each event.
/// See `GattPeripheralEvent` for the possible events and their semantics.
///
/// The callback is constraned to be `Send`, `Sync`, `Clone` and `'static` on purpose, as it might be
/// the case that the GATT implementation needs to invoke the callback from a different thread than the Matter thread,
/// as well as it might need multiple instances of it.
/// Therefore, this constraint is not a problem but an advantage for `GattPeripheral` trait implementors (it is a deliberate design decision).
async fn run<F>(
&self,
service_name: &str,
adv_data: &AdvData,
callback: F,
) -> Result<(), Error>
where
F: Fn(GattPeripheralEvent) + Send + Sync + Clone + 'static;

/// Indicate data changes in characteristics `C2` to to a GATT central.
/// In other words, send a BTP packet to a GATT central.
///
/// See the Matter Core spec w.r.t. details on characteristic C2.
async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error>;
}

impl<T> GattPeripheral for &T
where
T: GattPeripheral,
{
fn run<F>(
&self,
service_name: &str,
adv_data: &AdvData,
callback: F,
) -> impl core::future::Future<Output = Result<(), Error>>
where
F: Fn(GattPeripheralEvent) + Send + Sync + Clone + 'static,
{
(*self).run(service_name, adv_data, callback)
}

async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error> {
(*self).indicate(data, address).await
}
}

impl<T> GattPeripheral for &mut T
where
T: GattPeripheral,
{
fn run<F>(
&self,
service_name: &str,
adv_data: &AdvData,
callback: F,
) -> impl core::future::Future<Output = Result<(), Error>>
where
F: Fn(GattPeripheralEvent) + Send + Sync + Clone + 'static,
{
(**self).run(service_name, adv_data, callback)
}

async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error> {
(**self).indicate(data, address).await
}
}
373 changes: 373 additions & 0 deletions rs-matter/src/transport/network/btp/gatt/bluer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
/*
*
* 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 core::iter::once;

use alloc::sync::Arc;

use bluer::adv::Advertisement;
use bluer::agent::Agent;
use bluer::gatt::local::{
characteristic_control, Application, Characteristic, CharacteristicControl,
CharacteristicControlEvent, CharacteristicNotify, CharacteristicNotifyMethod,
CharacteristicWrite, CharacteristicWriteMethod, Service,
};
use bluer::gatt::CharacteristicWriter;
use bluer::Uuid;

use embassy_futures::select::{select, select_slice, Either};

use log::{info, trace, warn};

use tokio::io::AsyncWriteExt;
use tokio_stream::StreamExt;

use crate::transport::network::btp::MIN_MTU;
use crate::{
error::{Error, ErrorCode},
transport::network::{btp::context::MAX_BTP_SESSIONS, BtAddr},
utils::{ifmutex::IfMutex, select::Coalesce, signal::Signal, std_mutex::StdRawMutex},
};

use super::{AdvData, GattPeripheral, GattPeripheralEvent};
use super::{C1_CHARACTERISTIC_UUID, C2_CHARACTERISTIC_UUID, MATTER_BLE_SERVICE_UUID};

const MAX_CONNECTIONS: usize = MAX_BTP_SESSIONS;

/// The internal state of the peripheral.
/// Arc-ed so as to be thread-safe and to have `'static` interior, as demanded by the BlueR bindings.
struct GattState {
/// The name of the bluetooth adapter to use. If `None`, the default adapter is used.
adapter_name: Option<String>,
/// The list of active notifiers on characteristic `C2`.
notifiers: IfMutex<StdRawMutex, heapless::Vec<CharacteristicWriter, MAX_CONNECTIONS>>,
/// A signal necessary so that we can switch between two states:
/// - Indicating data to a notifier
/// - Listening all notifiers for a closed one (i.e. a remote peer had unsubscribed from characteristic `C2`)
notifiers_listen_allowed: Signal<StdRawMutex, bool>,
}

/// Implements the `GattPeripheral` trait using the BlueZ GATT stack.
#[derive(Clone)]
pub struct BluerGattPeripheral(Arc<GattState>);

impl Default for BluerGattPeripheral {
fn default() -> Self {
Self::new(None)
}
}

impl BluerGattPeripheral {
/// Create a new instance.
pub fn new(adapter_name: Option<&str>) -> Self {
Self(Arc::new(GattState {
adapter_name: adapter_name.map(|name| name.into()),
notifiers: IfMutex::new(heapless::Vec::new()),
notifiers_listen_allowed: Signal::new(true),
}))
}

/// Runs the GATT peripheral service.
/// What this means in details:
/// - Advertises the service with the provided name and advertising data, where the advertising data
/// contains the elements specified in the Matter Core spec.
/// - Serves a GATT peripheral service with the `C1`, `C2` and `C3` characteristics, as specified
/// in the Matter Core spec.
/// - Calls the provided callback with the events that occur during the service lifetime, on the `C1`
/// and `C2` characteristics.
pub async fn run<F>(
&self,
service_name: &str,
service_adv_data: &AdvData,
callback: F,
) -> Result<(), Error>
where
F: Fn(GattPeripheralEvent) + Send + Sync + 'static,
{
let session = bluer::Session::new().await?;

// Register a "NoInputNoOutput" agent that will accept all incoming requests.
let _handle = session.register_agent(Agent::default()).await?;

let adapter = if let Some(adapter_name) = self.0.adapter_name.as_ref() {
session.adapter(adapter_name)?
} else {
session.default_adapter().await?
};

adapter.set_powered(true).await?;

info!(
"Advertising on Bluetooth adapter {} with address {}",
adapter.name(),
BtAddr(adapter.address().await?.0)
);

let le_advertisement = Advertisement {
discoverable: Some(true),
local_name: Some(service_name.into()),
service_data: once((
Uuid::from_u128(MATTER_BLE_SERVICE_UUID),
service_adv_data.service_payload_iter().collect(),
))
.collect(),
..Default::default()
};

// TODO: Stop advertizing after the first connection?
let _adv_handle = adapter.advertise(le_advertisement).await?;

info!(
"Serving GATT echo service on Bluetooth adapter {}",
adapter.name()
);

let callback_w = Arc::new(callback);
let callback_n = callback_w.clone();
let callback_s = callback_w.clone();

let (notify, notify_handle) = characteristic_control();

// Service and characteristics as per the Matter Core spec
let app = Application {
services: vec![Service {
uuid: Uuid::from_u128(MATTER_BLE_SERVICE_UUID),
primary: true,
characteristics: vec![
Characteristic {
uuid: Uuid::from_u128(C1_CHARACTERISTIC_UUID),
write: Some(CharacteristicWrite {
write: true,
method: CharacteristicWriteMethod::Fun(Box::new(
move |new_value, req| {
let address = BtAddr(req.device_address.0);
let data = &new_value;

trace!("Got write request from {address}: {data:02x?}");

// Notify the BTP protocol implementation for the write
callback_w(GattPeripheralEvent::Write {
gatt_mtu: (req.mtu > MIN_MTU).then_some(req.mtu),
address,
data,
});

// We don't need a future because the callback is synchronous
Box::pin(core::future::ready(Ok(())))
},
)),
..Default::default()
}),
..Default::default()
},
Characteristic {
uuid: Uuid::from_u128(C2_CHARACTERISTIC_UUID),
notify: Some(CharacteristicNotify {
indicate: true,
// Reason why we don't use the (simpler) callback-based approach here:
// The callback approach does not provide us with access to the remote peer address
// when a notification subscription is received. This is necessary for the Matter BTP protocol
// to work correctly.
//
// Restriction seems to come from BlueZ dBus bindings, where their `StartNotify` method does not
// provide the address of the remote peer, nor any other peer properties thereof.
method: CharacteristicNotifyMethod::Io,
..Default::default()
}),
control_handle: notify_handle,
..Default::default()
},
// Characteristic {
// uuid: Uuid::from_u128(C3_CHARACTERISTIC_UUID),
// read: Some(CharacteristicRead {
// method: CharacteristicReadMethod::Io,
// ..Default::default()
// }),
// control_handle: write_handle,
// ..Default::default()
// },
],
..Default::default()
}],
..Default::default()
};

let _app_handle = adapter.serve_gatt_application(app).await?;

select(
self.closed(callback_s),
self.pull_notify(notify, callback_n),
)
.coalesce()
.await
}

/// Indicate new data on characteristic `C2` to a remote peer.
pub async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error> {
self.0.notifiers_listen_allowed.modify(|listen| {
*listen = false;

(true, ())
});

let mut notifiers = self.0.notifiers.lock().await;

let result = if let Some(notifier) = notifiers
.iter_mut()
.find(|notifier| notifier.device_address().0 == address.0)
{
notifier.write_all(data).await.map_err(|e| e.into())
} else {
Err(Error::new(ErrorCode::NoNetworkInterface))
};

self.0.notifiers_listen_allowed.modify(|listen| {
*listen = true;

(true, ())
});

result?;

trace!("Indicated {data:02x?} bytes to address {address}");

Ok(())
}

/// Handle a new subscription to the `C2` characteristic
/// by registering the notifier in the internal state.
async fn add_notifier(&self, notifier: CharacteristicWriter) {
// Tell the `Self::closed` method to unlock the `notifiers` mutex
self.0.notifiers_listen_allowed.modify(|listen| {
*listen = false;

(true, ())
});

let mut notifiers = self.0.notifiers.lock().await;

let address = BtAddr(notifier.device_address().0);

if notifiers.len() < MAX_CONNECTIONS {
// Unwraping is safe because we just checked the length
notifiers.push(notifier).map_err(|_| ()).unwrap();
trace!("Notify connection from address {address} started");
} else {
warn!("Notifiers limit reached; ignoring notifier from address {address}");
}

drop(notifiers);

// `Self::close` can listen again for closed connections
self.0.notifiers_listen_allowed.modify(|listen| {
*listen = true;

(true, ())
});
}

/// Pull new subscription notifications from the `C2` characteristic.
async fn pull_notify<F>(
&self,
mut notify: CharacteristicControl,
callback: Arc<F>,
) -> Result<(), Error>
where
F: Fn(GattPeripheralEvent) + Send + Sync + 'static,
{
while let Some(event) = notify.next().await {
match event {
// Should never happen, as characteristic `C2` is not marked as capable of taking writes.
CharacteristicControlEvent::Write(_) => unreachable!(),
CharacteristicControlEvent::Notify(writer) => {
let address = BtAddr(writer.device_address().0);

self.add_notifier(writer).await;

// Notify the BTP protocol implementation
callback(GattPeripheralEvent::NotifySubscribed(address));
}
}
}

Ok(())
}

/// Listen for stopped connections (i.e. unsubscriptions from characteristic `C2`).
async fn closed<F>(&self, callback: Arc<F>) -> Result<(), Error>
where
F: Fn(GattPeripheralEvent) + Send + Sync + 'static,
{
loop {
// Wait until we are allowed to listen for closed connections
self.0
.notifiers_listen_allowed
.wait(|allowed| (*allowed).then_some(()))
.await;

{
let mut notifiers = self.0.notifiers.lock().await;

let notifiers_listen_allowed = self
.0
.notifiers_listen_allowed
.wait(|allowed| (!*allowed).then_some(()));

let mut closed = notifiers
.iter()
.map(|notifier| notifier.closed())
.collect::<heapless::Vec<_, MAX_CONNECTIONS>>();

// Await until we are no longer allowed to await (future notifiers_listen_allowed)
// or until we have a closed notifier
let result = select(notifiers_listen_allowed, select_slice(&mut closed)).await;

match result {
// No longer allowed to await for closed connections, wait until we are allowed again
Either::First(_) => continue,
Either::Second((_, index)) => {
// Remove the closed notifier

let address = BtAddr(notifiers[index].device_address().0);

drop(closed);

notifiers.swap_remove(index);

// Notify the BTP protocol implementation
callback(GattPeripheralEvent::NotifyUnsubscribed(address));

trace!("Notify connection from address {address} stopped");
}
}
}
}
}
}

impl GattPeripheral for BluerGattPeripheral {
async fn run<F>(&self, service_name: &str, adv_data: &AdvData, callback: F) -> Result<(), Error>
where
F: Fn(GattPeripheralEvent) + Send + Sync + 'static,
{
BluerGattPeripheral::run(self, service_name, adv_data, callback).await
}

async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error> {
BluerGattPeripheral::indicate(self, data, address).await
}
}
660 changes: 660 additions & 0 deletions rs-matter/src/transport/network/btp/session.rs

Large diffs are not rendered by default.

461 changes: 461 additions & 0 deletions rs-matter/src/transport/network/btp/session/packet.rs

Large diffs are not rendered by default.

620 changes: 620 additions & 0 deletions rs-matter/src/transport/network/btp/test.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions rs-matter/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ pub mod ifmutex;
pub mod notification;
pub mod parsebuf;
pub mod rand;
pub mod ringbuf;
pub mod select;
pub mod signal;
pub mod std_mutex;
pub mod writebuf;
258 changes: 258 additions & 0 deletions rs-matter/src/utils/ringbuf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/*
*
* 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 core::cmp::min;

/// A ring buffer of a fixed capacity `N` using owned storage.
#[derive(Debug)]
pub struct RingBuf<const N: usize> {
buf: heapless::Vec<u8, N>,
start: usize,
end: usize,
empty: bool,
}

impl<const N: usize> Default for RingBuf<N> {
fn default() -> Self {
Self::new()
}
}

impl<const N: usize> RingBuf<N> {
/// Create a new ring buffer.
#[inline(always)]
pub const fn new() -> Self {
Self {
buf: heapless::Vec::new(),
start: 0,
end: 0,
empty: true,
}
}

/// Push new data to the end of the buffer.
/// If the data does not fit in the buffer, the oldest data is dropped to make room for the new one.
///
/// Return the new length of data in the buffer.
#[inline(always)]
pub fn push(&mut self, data: &[u8]) -> usize {
// Unwrap is safe because the max size of the buffer is N
self.buf.resize_default(N).unwrap();

let mut offset = 0;

while offset < data.len() {
let len = min(self.buf.len() - self.end, data.len() - offset);

self.buf[self.end..self.end + len].copy_from_slice(&data[offset..offset + len]);

offset += len;

if !self.empty && self.start >= self.end && self.start < self.end + len {
// Dropping oldest data
self.start = self.end + len;
}

self.end += len;

self.wrap();

self.empty = false;
}

self.len()
}

/// Push a single byte to the end of the buffer.
/// If the buffer is full, the oldest byte is dropped to make room for the new one.
///
/// Return the new length of data in the buffer.
#[inline(always)]
pub fn push_byte(&mut self, data: u8) -> usize {
// Unwrap is safe because the max size of the buffer is N
self.buf.resize_default(N).unwrap();

self.buf[self.end] = data;

if !self.empty && self.start == self.end {
// Dropping oldest data
self.start = self.end + 1;
}

self.end += 1;

self.wrap();

self.empty = false;

self.len()
}

/// Pop one byte from the start of the buffer.
/// If the bufer is empty, return `None`.
#[inline(always)]
pub fn pop_byte(&mut self) -> Option<u8> {
let mut buf = [0; 1];

if self.pop(&mut buf) == 1 {
Some(buf[0])
} else {
None
}
}

/// Pop data from the start of the buffer.
/// Return the number of bytes copied to the output buffer.
#[inline(always)]
pub fn pop(&mut self, out_buf: &mut [u8]) -> usize {
let mut offset = 0;

while offset < out_buf.len() && !self.empty {
let len = min(
if self.start < self.end {
self.end
} else {
self.buf.len()
} - self.start,
out_buf.len() - offset,
);

out_buf[offset..offset + len].copy_from_slice(&self.buf[self.start..self.start + len]);

self.start += len;

self.wrap();

if self.start == self.end {
self.empty = true
}

offset += len;
}

offset
}

/// Return `true` when the buffer is full.
#[inline(always)]
pub fn is_full(&self) -> bool {
self.start == self.end && !self.empty
}

/// Return `true` when the buffer is empty.
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.empty
}

/// Return the current size of the data in the buffer.
#[inline(always)]
#[allow(unused)]
pub fn len(&self) -> usize {
if self.empty {
0
} else if self.start < self.end {
self.end - self.start
} else {
self.buf.len() + self.end - self.start
}
}

/// Return the free space in the buffer.
#[inline(always)]
#[allow(unused)]
pub fn free(&self) -> usize {
N - self.len()
}

/// Clear the buffer.
#[inline(always)]
pub fn clear(&mut self) {
self.start = 0;
self.end = 0;
self.empty = true;
}

#[inline(always)]
fn wrap(&mut self) {
if self.start == self.buf.len() {
self.start = 0;
}

if self.end == self.buf.len() {
self.end = 0;
}
}
}

impl<const N: usize> Iterator for RingBuf<N> {
type Item = u8;

fn next(&mut self) -> Option<Self::Item> {
self.pop_byte()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn push_pop() {
let mut rb = RingBuf::<4>::new();
assert!(rb.is_empty());

rb.push(&[0, 1, 2]);
assert_eq!(3, rb.len());
assert!(!rb.is_empty());
assert!(!rb.is_full());

rb.push(&[3]);
assert_eq!(4, rb.len());
assert!(!rb.is_empty());
assert!(rb.is_full());

let mut buf = [0; 256];

let len = rb.pop(&mut buf);
assert_eq!(4, len);
assert_eq!(&buf[0..4], &[0, 1, 2, 3]);
assert!(rb.is_empty());

rb.push(&[0, 1, 2, 3, 4, 5]);
assert_eq!(4, rb.len());
assert!(!rb.is_empty());
assert!(rb.is_full());

let len = rb.pop(&mut buf[..3]);
assert_eq!(3, len);
assert_eq!(&buf[0..len], &[2, 3, 4]);
assert!(!rb.is_empty());
assert!(!rb.is_full());

let len = rb.pop(&mut buf);
assert_eq!(1, len);
assert_eq!(&buf[0..len], &[5]);
assert!(rb.is_empty());
assert!(!rb.is_full());

let len = rb.pop(&mut buf);
assert_eq!(0, len);
assert!(rb.is_empty());
assert!(!rb.is_full());
}
}
42 changes: 42 additions & 0 deletions rs-matter/src/utils/std_mutex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
*
* 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.
*/

#![cfg(feature = "std")]

use embassy_sync::blocking_mutex::raw::RawMutex;

/// An `embassy-sync` `RawMutex` implementation using `std::sync::Mutex`.
/// TODO: Upstream into `embassy-sync` itself.
#[derive(Default)]
pub struct StdRawMutex(std::sync::Mutex<()>);

impl StdRawMutex {
pub const fn new() -> Self {
Self(std::sync::Mutex::new(()))
}
}

unsafe impl RawMutex for StdRawMutex {
#[allow(clippy::declare_interior_mutable_const)]
const INIT: Self = StdRawMutex(std::sync::Mutex::new(()));

fn lock<R>(&self, f: impl FnOnce() -> R) -> R {
let _guard = self.0.lock().unwrap();

f()
}
}

0 comments on commit 90412d7

Please sign in to comment.