esp-idf-matter 0.1.0

Run rs-matter on Espressif chips with ESP IDF
//! An example utilizing the `EspEthMatterStack` struct.
//! As the name suggests, this Matter stack assembly uses Ethernet as the main transport, as well as for commissioning.
//!
//! Notice thart we actually don't use Ethernet for real, as ESP32s don't have Ethernet ports out of the box.
//! Instead, we utilize Thread, which - from the POV of Matter - is indistinguishable from Ethernet as long as the Matter
//! stack is not concerned with connecting to the Thread network, managing its credentials etc. and can assume it "pre-exists".
//!
//! The example implements a fictitious Light device (an On-Off Matter cluster).
#![allow(unexpected_cfgs)]
#![recursion_limit = "256"]

fn main() -> Result<(), anyhow::Error> {
    #[cfg(any(esp32c6, esp32h2))]
    {
        example::main()
    }

    #[cfg(not(any(esp32c6, esp32h2)))]
    panic!("This example is only supported on ESP32-C6 and ESP32-H2 chips. Please select a different example or target.");
}

#[cfg(any(esp32c6, esp32h2))]
mod example {
    use core::pin::pin;

    use alloc::sync::Arc;

    use esp_idf_matter::eth::EspEthMatterStack;
    use esp_idf_matter::init_async_io;
    use esp_idf_matter::matter::crypto::{default_crypto, Crypto};
    use esp_idf_matter::matter::dm::clusters::app::on_off::test::TestOnOffDeviceLogic;
    use esp_idf_matter::matter::dm::clusters::app::on_off::{self, OnOffHandler, OnOffHooks};
    use esp_idf_matter::matter::dm::clusters::desc::{ClusterHandler as _, DescHandler};
    use esp_idf_matter::matter::dm::clusters::gen_diag::InterfaceTypeEnum;
    use esp_idf_matter::matter::dm::devices::test::{
        DAC_PRIVKEY, TEST_DEV_ATT, TEST_DEV_COMM, TEST_DEV_DET,
    };
    use esp_idf_matter::matter::dm::devices::DEV_TYPE_ON_OFF_LIGHT;
    use esp_idf_matter::matter::dm::endpoints::ROOT_ENDPOINT_ID;
    use esp_idf_matter::matter::dm::{Async, Dataver, EmptyHandler, Endpoint, Node};
    use esp_idf_matter::matter::persist::DummyKvBlobStore;
    use esp_idf_matter::matter::utils::init::InitMaybeUninit;
    use esp_idf_matter::matter::{clusters, devices};
    use esp_idf_matter::netif::{EspMatterNetStack, EspMatterNetif};
    use esp_idf_matter::thread::EspMatterThreadSrp;

    use esp_idf_svc::eventloop::EspSystemEventLoop;
    use esp_idf_svc::hal::peripherals::Peripherals;
    use esp_idf_svc::hal::task::block_on;
    use esp_idf_svc::hal::task::thread::ThreadSpawnConfiguration;
    use esp_idf_svc::io::vfs::MountedEventfs;
    use esp_idf_svc::nvs::EspDefaultNvsPartition;
    use esp_idf_svc::thread::EspThread;

    use log::{error, info};

    use static_cell::StaticCell;

    extern crate alloc;

    const STACK_SIZE: usize = 20 * 1024; // Can go down to 15K for esp32c6
    const BUMP_SIZE: usize = 17000;

    const THREAD_DATASET: &str = env!("THREAD_DATASET");

    pub fn main() -> Result<(), anyhow::Error> {
        esp_idf_svc::log::init_from_env();

        info!("Starting...");

        ThreadSpawnConfiguration::set(&ThreadSpawnConfiguration {
            name: Some(c"matter"),
            ..Default::default()
        })?;

        // Run in a higher-prio thread to avoid issues with `async-io` getting
        // confused by the low priority of the ESP IDF main task
        // Also allocate a very large stack (for now) as `rs-matter` futures do occupy quite some space
        let thread = std::thread::Builder::new()
            .stack_size(STACK_SIZE)
            .spawn(run)
            .unwrap();

        thread.join().unwrap()
    }

    #[inline(never)]
    #[cold]
    fn run() -> Result<(), anyhow::Error> {
        let result = block_on(matter());

        if let Err(e) = &result {
            error!("Matter aborted execution with error: {e:?}");
        }
        {
            info!("Matter finished execution successfully");
        }

        result
    }

    async fn matter() -> Result<(), anyhow::Error> {
        // Initialize the Matter stack (can be done only once),
        // as we'll run it in this thread
        let stack = MATTER_STACK.uninit().init_with(EspEthMatterStack::init(
            &TEST_DEV_DET,
            TEST_DEV_COMM,
            &TEST_DEV_ATT,
        ));

        // Take some generic ESP-IDF stuff we'll need later
        let sysloop = EspSystemEventLoop::take()?;
        let nvs = EspDefaultNvsPartition::take()?;
        let peripherals = Peripherals::take()?;

        let mounted_event_fs = Arc::new(MountedEventfs::mount(6)?);
        init_async_io(mounted_event_fs.clone())?;

        // Configure and start Thread first
        let mut thread = EspThread::new(
            peripherals.modem,
            sysloop.clone(),
            nvs.clone(),
            mounted_event_fs,
        )?;

        info!("Thread driver created");

        thread.set_tod_hexstr(THREAD_DATASET)?;

        info!("Thread dataset set to: {THREAD_DATASET}");

        thread.enable_ipv6(true)?;

        info!("Thread IPv6 enabled");

        thread.enable_thread(true)?;

        info!("Thread enabled");

        thread.start()?;

        info!("Thread started");

        // Create the default crypto provider using the STD CSPRNG provided by the `rand` crate
        let crypto = default_crypto(rand::rng(), DAC_PRIVKEY);

        let mut weak_rand = crypto.weak_rand()?;

        // Our "light" on-off cluster.
        // It will toggle the light state every 5 seconds
        let on_off = OnOffHandler::new_standalone(
            Dataver::new_rand(&mut weak_rand),
            LIGHT_ENDPOINT_ID,
            TestOnOffDeviceLogic::new(true),
        );

        // Chain our endpoint clusters with the
        // (root) Endpoint 0 system clusters in the final handler
        let handler = EmptyHandler
            // The Endpoint 0 system clusters that are ours to provide.
            // The stack adds the operational network clusters (Network Commissioning,
            // General Commissioning, General Diagnostics and Wifi/Thread/Ethernet
            // Diagnostics) on top, because only it knows the network driver state.
            // Chain any extra Endpoint 0 clusters of your own the same way.
            .chain(
                |e, _| e == ROOT_ENDPOINT_ID,
                Async(EspEthMatterStack::<0, ()>::root_handler(
                    &(),
                    &mut weak_rand,
                )),
            )
            // Our on-off cluster, on Endpoint 1
            .chain(
                |e, c| e == LIGHT_ENDPOINT_ID && c == TestOnOffDeviceLogic::CLUSTER.id,
                on_off::HandlerAsyncAdaptor(&on_off),
            )
            // Each Endpoint needs a Descriptor cluster too
            // Just use the one that `rs-matter` provides out of the box
            .chain(
                |e, c| e == LIGHT_ENDPOINT_ID && c == DescHandler::CLUSTER.id,
                Async(DescHandler::new(Dataver::new_rand(&mut weak_rand)).adapt()),
            );

        // Create a KV BLOB store and load any previously saved state of `rs-matter`
        // `EspKvBlobStore` saves to an ESP-IDF NVS namespace
        // However, for this demo and for simplicity, we use a dummy KV BLOB store that does nothing
        let mut store = DummyKvBlobStore;
        stack.startup(&crypto, &mut store).await?;

        // Wrap the KV BLOB store as a shared reference, so that it can be used both by `rs-matter` and the user
        let kv = stack.kv(store);

        // Run the Matter stack with our handler
        // Using `pin!` is completely optional, but reduces the size of the final future
        let matter = pin!(stack.run_preex(
            // The Matter stack needs UDP sockets to communicate with other Matter devices
            EspMatterNetStack::new(),
            // The Matter stack need access to the netif on which we'll operate
            // Since we are pretending to use a wired Ethernet connection - yet -
            // we are using Thread - provide the Thread netif here
            EspMatterNetif::new(thread.netif(), InterfaceTypeEnum::Thread, sysloop),
            // The Matter stack needs an mDNS service to advertise itself
            EspMatterThreadSrp::new(&thread),
            // The crypto provider
            &crypto,
            // Our `AsyncHandler` + `AsyncMetadata` impl
            (NODE, handler),
            // The Matter stack needs a blob store to store its state
            &kv,
            // No user future to run
            (),
        ));

        // Run Matter
        matter.await?;

        Ok(())
    }

    /// The Matter stack is allocated statically to avoid
    /// program stack blowups.
    static MATTER_STACK: StaticCell<EspEthMatterStack<BUMP_SIZE, ()>> = StaticCell::new();

    /// Endpoint 0 (the root endpoint) always runs
    /// the hidden Matter system clusters, so we pick ID=1
    const LIGHT_ENDPOINT_ID: u16 = 1;

    /// The Matter Light device Node
    const NODE: Node = Node {
        endpoints: &[
            EspEthMatterStack::<0, ()>::root_endpoint(),
            Endpoint::new(
                LIGHT_ENDPOINT_ID,
                devices!(DEV_TYPE_ON_OFF_LIGHT),
                clusters!(DescHandler::CLUSTER, TestOnOffDeviceLogic::CLUSTER),
            ),
        ],
    };
}