A pure-Rust, no_std
, no-alloc, async-first, extensible and safe implementation of the Matter protocol.
Scales from bare-metal MCUs with 1MB flash and 256KB RAM to ARM embedded Linux and bigger iron!
Rather than a shrink-wrapped solution, it is first and formeost - a toolkit. Users are free to consume all of the APIs, including the provided system clusters, or only pick up bits and pieces. As in:
- ... re-using the transport layer and Secure Channel, but implementing their own Data Model;
- ... custom Exchange responders;
- ... custom mDNS provider;
- ... custom IP network implementation and BLE GATT device implementation;
- ... flexible polling of the
rs-matter
futures as e.g. separate tasks in their async executor of choice; - ... or just using the shrink-wrapped
rs-matter-stack
arrangement and its down-stream crates; - ... and so on.
- To run
rs-matter
on baremetal MCUs with Embassy, look atrs-matter-embassy
. Currently supported MCUs:- Espressif ESP32XX
- Nordic NRF52840
- RP2040 Pico and RP2040 Pico W
- To run
rs-matter
on top of the ESP-IDF with Espressif MCUs, look atesp-idf-matter
We'll have an rs-matter
Rust Book in future, but in the meantime - look at the examples, as well as the code documentation.
Use the discussions to ask questions.
rs-matter
is still in development, and APIs are likely to see backwards-incompatible changes still, though the blast radius should be more limited now.
With that said, provisioning and operating under the major Smart Home controllers, that is:
- Google Home
- Apple HomeKit
- Alexa
- Home Assistant
... does work without issues.
- Enable more ConnectedHomeIP YAML tests;
- More intelligent reporting on subscriptions;
- Support for Events.
Also look at all open issues.
rs-matter
includes comprehensive CI testing:
- Standard CI: Runs on every push and PR with build, test, linting across multiple feature combinations
- ConnectedHomeIP Integration: Native Rust tooling via
xtask
for running official Matter test cases- Run locally during development:
cargo xtask itest
- Automated nightly CI execution
- Iterative test enablement workflow for developers
- Run locally during development:
rs-matter
provides nix files for setting up reproducible shells on systems using the nix package manager.
There are two different environments for different use-cases.
This is used for normal development of applications.
To enter this shell, run devenv shell
in the project root.
You can optionally follow this with your preferred shell e.g. devenv shell zsh
.
This requires devenv to be installed on your system.
For nix managed systems, add pkgs.devenv
to environment.systemPackages
.
This is for cases where tools expect a standard Linux filesystem layout (FHS).
Use this shell when running cargo xtask itest
as it sets up connectedhomeip
which expects a FHS layout.
To enter this shell, run nix-shell
in the project root.
See the examples.
Note that using the "Matter Stack" metaphor of rs-matter-stack
/ rs-matter-embassy
/ esp-idf-matter
results in less bootstrapping boilerplate.
/*
*
* 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.
*/
//! An example Matter device that implements a Speaker device over Ethernet.
//! Demonstrates how to make use of the `rs_matter::import` macro for `LevelControl`.
use core::cell::Cell;
use core::pin::pin;
use std::net::UdpSocket;
use embassy_futures::select::select4;
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use log::info;
use level_control::{
ClusterAsyncHandler as _, MoveRequest, MoveToClosestFrequencyRequest, MoveToLevelRequest,
MoveToLevelWithOnOffRequest, MoveWithOnOffRequest, OptionsBitmap, StepRequest,
StepWithOnOffRequest, StopRequest, StopWithOnOffRequest,
};
use rs_matter::dm::clusters::desc::{self, ClusterHandler as _};
use rs_matter::dm::clusters::net_comm::NetworkType;
use rs_matter::dm::clusters::on_off::{ClusterHandler as _, OnOffHandler};
use rs_matter::dm::devices::test::{TEST_DEV_ATT, TEST_DEV_COMM, TEST_DEV_DET};
use rs_matter::dm::devices::DEV_TYPE_SMART_SPEAKER;
use rs_matter::dm::endpoints;
use rs_matter::dm::networks::unix::UnixNetifs;
use rs_matter::dm::subscriptions::Subscriptions;
use rs_matter::dm::{
Async, AsyncHandler, AsyncMetadata, Cluster, Dataver, EmptyHandler, Endpoint, EpClMatcher,
InvokeContext, Node, ReadContext, WriteContext,
};
use rs_matter::error::{Error, ErrorCode};
use rs_matter::pairing::DiscoveryCapabilities;
use rs_matter::persist::Psm;
use rs_matter::respond::DefaultResponder;
use rs_matter::tlv::Nullable;
use rs_matter::transport::MATTER_SOCKET_BIND_ADDR;
use rs_matter::utils::select::Coalesce;
use rs_matter::utils::storage::pooled::PooledBuffers;
use rs_matter::{clusters, devices, with, Matter, MATTER_PORT};
// Import the LevelControl cluster from `rs-matter`.
//
// This will auto-generate all Rust types related to the LevelControl cluster
// in a module named `level_control`.
//
// User needs to implement the `ClusterAsyncHandler` trait or the `ClusterHandler` trait
// so as to handle the requests from the controller.
rs_matter::import!(LevelControl);
#[path = "../common/mdns.rs"]
mod mdns;
fn main() -> Result<(), Error> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
// Create the Matter object
let matter = Matter::new_default(&TEST_DEV_DET, TEST_DEV_COMM, &TEST_DEV_ATT, MATTER_PORT);
// Need to call this once
matter.initialize_transport_buffers()?;
// Create the transport buffers
let buffers = PooledBuffers::<10, NoopRawMutex, _>::new(0);
// Create the subscriptions
let subscriptions = Subscriptions::<3>::new();
// Assemble our Data Model handler by composing the predefined Root Endpoint handler with our custom Speaker handler
let dm_handler = dm_handler(&matter);
// 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);
// Run the responder with up to 4 handlers (i.e. 4 exchanges can be handled simultaneously)
// 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>());
// Create the Matter UDP socket
let socket = async_io::Async::<UdpSocket>::bind(MATTER_SOCKET_BIND_ADDR)?;
// Run the Matter and mDNS transports
let mut mdns = pin!(mdns::run_mdns(&matter));
let mut transport = pin!(matter.run(&socket, &socket, DiscoveryCapabilities::IP));
// Create, load and run the persister
let mut psm: Psm<4096> = Psm::new();
let dir = std::env::temp_dir().join("rs-matter");
psm.load(&dir, &matter)?;
let mut persist = pin!(psm.run(dir, &matter));
// Combine all async tasks in a single one
let all = select4(&mut transport, &mut mdns, &mut persist, &mut respond);
// Run with a simple `block_on`. Any local executor would do.
futures_lite::future::block_on(all.coalesce())
}
/// The Node meta-data describing our Matter device.
const NODE: Node<'static> = Node {
id: 0,
endpoints: &[
endpoints::root_endpoint(NetworkType::Ethernet),
Endpoint {
id: 1,
device_types: devices!(DEV_TYPE_SMART_SPEAKER),
clusters: clusters!(
desc::DescHandler::CLUSTER,
OnOffHandler::CLUSTER,
LevelControlHandler::CLUSTER
),
},
],
};
/// The Data Model handler + meta-data for our Matter device.
/// The handler is the root endpoint 0 handler plus the Speaker handler.
fn dm_handler(matter: &Matter<'_>) -> impl AsyncMetadata + AsyncHandler + 'static {
(
NODE,
endpoints::with_eth(
&(),
&UnixNetifs,
matter.rand(),
endpoints::with_sys(
&false,
matter.rand(),
EmptyHandler
.chain(
EpClMatcher::new(Some(1), Some(desc::DescHandler::CLUSTER.id)),
Async(desc::DescHandler::new(Dataver::new_rand(matter.rand())).adapt()),
)
.chain(
EpClMatcher::new(Some(1), Some(LevelControlHandler::CLUSTER.id)),
LevelControlHandler::new(Dataver::new_rand(matter.rand())).adapt(),
)
.chain(
EpClMatcher::new(Some(1), Some(OnOffHandler::CLUSTER.id)),
Async(OnOffHandler::new(Dataver::new_rand(matter.rand())).adapt()),
),
),
),
)
}
/// A sample NOOP handler for the LevelControl cluster.
pub struct LevelControlHandler {
dataver: Dataver,
level: Cell<u8>,
}
impl LevelControlHandler {
/// Create a new instance of the handler
pub const fn new(dataver: Dataver) -> Self {
Self {
dataver,
level: Cell::new(0),
}
}
/// Adapt the handler instance to the generic `rs-matter` `AsyncHandler` trait
pub const fn adapt(self) -> level_control::HandlerAsyncAdaptor<Self> {
level_control::HandlerAsyncAdaptor(self)
}
/// Update the volume level of the handler
fn set_level(&self, state: u8, ctx: &InvokeContext<'_>) {
let old_state = self.level.replace(state);
if old_state != state {
// Update the cluster data version and notify potential subscribers
self.dataver.changed();
ctx.notify_changed();
}
}
}
impl level_control::ClusterAsyncHandler for LevelControlHandler {
/// The metadata cluster definition corresponding to the handler
const CLUSTER: Cluster<'static> = level_control::FULL_CLUSTER
.with_revision(1)
.with_attrs(with!(required))
.with_cmds(with!(
level_control::CommandId::MoveToLevel
| level_control::CommandId::Move
| level_control::CommandId::Step
| level_control::CommandId::Stop
| level_control::CommandId::MoveToLevelWithOnOff
| level_control::CommandId::MoveWithOnOff
| level_control::CommandId::StepWithOnOff
| level_control::CommandId::StopWithOnOff
));
fn dataver(&self) -> u32 {
self.dataver.get()
}
fn dataver_changed(&self) {
self.dataver.changed();
}
async fn current_level(&self, _ctx: &ReadContext<'_>) -> Result<Nullable<u8>, Error> {
Ok(Nullable::some(self.level.get()))
}
async fn options(&self, _ctx: &ReadContext<'_>) -> Result<OptionsBitmap, Error> {
Ok(OptionsBitmap::empty())
}
async fn set_options(
&self,
_ctx: &WriteContext<'_>,
_value: OptionsBitmap,
) -> Result<(), Error> {
Ok(())
}
async fn on_level(&self, _ctx: &ReadContext<'_>) -> Result<Nullable<u8>, Error> {
Ok(Nullable::none())
}
async fn set_on_level(
&self,
_ctx: &WriteContext<'_>,
_value: Nullable<u8>,
) -> Result<(), Error> {
Ok(())
}
async fn handle_move_to_level(
&self,
ctx: &InvokeContext<'_>,
request: MoveToLevelRequest<'_>,
) -> Result<(), Error> {
info!("Moving to level: {}", request.level()?);
self.set_level(request.level()?, ctx);
Ok(())
}
async fn handle_move(
&self,
_ctx: &InvokeContext<'_>,
request: MoveRequest<'_>,
) -> Result<(), Error> {
info!(
"Moving {:?} with rate: {:?}",
request.move_mode()?,
request.rate()?
);
Ok(())
}
async fn handle_step(
&self,
_ctx: &InvokeContext<'_>,
request: StepRequest<'_>,
) -> Result<(), Error> {
info!(
"Stepping {:?} with step size: {} and transition time: {:?}",
request.step_mode()?,
request.step_size()?,
request.transition_time()?
);
Ok(())
}
async fn handle_stop(
&self,
_ctx: &InvokeContext<'_>,
_request: StopRequest<'_>,
) -> Result<(), Error> {
info!("Stopping");
Ok(())
}
async fn handle_move_to_level_with_on_off(
&self,
ctx: &InvokeContext<'_>,
request: MoveToLevelWithOnOffRequest<'_>,
) -> Result<(), Error> {
info!("Moving to level with on/off: {}", request.level()?);
self.set_level(request.level()?, ctx);
Ok(())
}
async fn handle_move_with_on_off(
&self,
_ctx: &InvokeContext<'_>,
request: MoveWithOnOffRequest<'_>,
) -> Result<(), Error> {
info!(
"Moving with on/off: {:?} with rate: {:?}",
request.move_mode()?,
request.rate()?
);
Ok(())
}
async fn handle_step_with_on_off(
&self,
_ctx: &InvokeContext<'_>,
request: StepWithOnOffRequest<'_>,
) -> Result<(), Error> {
info!(
"Stepping with on/off: {:?} with step size: {} and transition time: {:?}",
request.step_mode()?,
request.step_size()?,
request.transition_time()?
);
Ok(())
}
async fn handle_stop_with_on_off(
&self,
_ctx: &InvokeContext<'_>,
_request: StopWithOnOffRequest<'_>,
) -> Result<(), Error> {
info!("Stopping with on/off");
Ok(())
}
async fn handle_move_to_closest_frequency(
&self,
_ctx: &InvokeContext<'_>,
_request: MoveToClosestFrequencyRequest<'_>,
) -> Result<(), Error> {
Err(ErrorCode::InvalidAction.into())
}
}
$ cargo build --features zeroconf
These days mDNS is still a PITA especially on Linux, but not only.
rs-matter
currently offers no less than 5 (five!) mDNS implementations (three well-supported ones - avahi
, resolve
and builtin
-
for production use-cases on embedded MCUs and embedded Linux, as well as two legacy ones - zeroconf
and astro-dnssd
- which are kept around
for compatibility with Windows and MacOSX; look inside rs-matter/rs-matter/src/transport/network/mdns
for more details on those).
The TL;DR is, rather than building with --features zeroconf
everywhere, you can try:
- On Linux, all of the above, but
--features avahi
and--features zeroconf
are your best bet; - On MacOSX,
--features astro-dnssd
is known to work fine; - On Windows, try
--features astro-dnssd
or--features zeroconf
.
$ cargo test -- --test-threads 1
With the chip-tool
(the current tool for testing Matter) use the Ethernet commissioning mechanism:
$ chip-tool pairing code 12344321 <Pairing-Code>
Or alternatively:
$ chip-tool pairing ethernet 12344321 123456 0 <IP-Address> 5540
Interact with the device
# Read server-list
$ chip-tool descriptor read server-list 12344321 0
# Read On/Off status
$ chip-tool onoff read on-off 12344321 1
# Toggle On/Off by invoking the command
$ chip-tool onoff on 12344321 1
All of these should work. Follow the instructions in your controller phone app.