Skip to content

project-chip/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 a CI that runs rs-matter against the Python/XML TCK tests of the Matter C++ SDK;
  • More intelligent reporting on subscriptions;
  • Support for Events.

Also look at all open issues.

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::mdns::MdnsService;
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,
        MdnsService::Builtin,
        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

NOTE: If you are on Linux and are running the Avahi daemon, you might want to build with:

$ cargo build --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

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 27

Languages