Skip to content

sysgrok/rs-matter

 
 

Repository files navigation

alt text rs-matter

license CI crates.io Matrix

What is it exactly?

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.

I just want to run Matter on my MCU!

  • To run rs-matter on baremetal MCUs with Embassy, look at rs-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 at esp-idf-matter

Documentation

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.

Status Quo

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.

Next steps

  • Enable more ConnectedHomeIP YAML tests;
  • More intelligent reporting on subscriptions;
  • Support for Events.

Also look at all open issues.

Continuous Integration

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

Nix support

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.

devenv.nix

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.

shell.nix

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.

How does it look like?

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())
    }
}

Build

Building the library and all examples

$ cargo build --features zeroconf

NOTE: mDNS issues (e.g. "running the example fails with strange error messages mentioning mDNS").

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.

Unit Tests

$ cargo test -- --test-threads 1

Test

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

Test with Google/Apple/Alexa controllers

All of these should work. Follow the instructions in your controller phone app.

About

Rust implementation of the Matter protocol. Status: Experimental

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Rust 100.0%