#![cfg(feature = "device")] #![recursion_limit = "256"] #![allow(async_fn_in_trait)] use core::cell::RefCell; use core::pin::pin; use std::path::{Path, PathBuf}; use std::process::Command; use log::{info, warn}; use rand::RngCore; use static_cell::StaticCell; use rs_matter_stack::ble::GattPeripheral; use rs_matter_stack::matter as rs_matter; use rs_matter_stack::matter::crypto::{default_crypto, Crypto}; use rs_matter_stack::matter::dm::clusters::app::level_control::LevelControlHooks; use rs_matter_stack::matter::dm::clusters::app::on_off::OnOffHooks; use rs_matter_stack::matter::dm::clusters::app::{level_control, on_off}; use rs_matter_stack::matter::dm::clusters::decl::color_control; use rs_matter_stack::matter::dm::clusters::decl::color_control::ClusterAsyncHandler as ColorControlHandler; use rs_matter_stack::matter::dm::clusters::desc::{ClusterHandler as _, DescHandler}; use rs_matter_stack::matter::dm::clusters::dev_att::DeviceAttestation; use rs_matter_stack::matter::dm::clusters::gen_diag::{NetifDiag, NetifInfo}; use rs_matter_stack::matter::dm::clusters::net_comm::{ NetCtl, NetCtlError, NetworkScanInfo, NetworkType, ThreadCapabilitiesBitmap, WiFiBandEnum, WirelessCreds, }; use rs_matter_stack::matter::dm::clusters::wifi_diag::{ SecurityTypeEnum, WiFiVersionEnum, WifiDiag, WirelessDiag, }; use rs_matter_stack::matter::dm::devices::test::{ DAC_PRIVKEY, TEST_DEV_ATT, TEST_DEV_COMM, TEST_DEV_DET, }; use rs_matter_stack::matter::dm::networks::unix::UnixNetifs; use rs_matter_stack::matter::dm::networks::NetChangeNotif; use rs_matter_stack::matter::dm::{ Async, Cluster, Dataver, EmptyHandler, Endpoint, EpClMatcher, Node, }; use rs_matter_stack::matter::error::{Error, ErrorCode}; use rs_matter_stack::matter::pairing::qr::{no_optional_data, CommFlowType, QrPayload}; use rs_matter_stack::matter::pairing::DiscoveryCapabilities; use rs_matter_stack::matter::persist::DirKvBlobStore; use rs_matter_stack::matter::sc::pase::{Spake2pVerifierPassword, Spake2pVerifierPasswordRef}; use rs_matter_stack::matter::tlv::Nullable; use rs_matter_stack::matter::transport::network::btp::{AdvData, Btp}; use rs_matter_stack::matter::transport::network::mdns::zeroconf::ZeroconfMdnsResponder; use rs_matter_stack::matter::transport::network::wifi::wpa_supp::unix::DhClientCtl; use rs_matter_stack::matter::transport::network::wifi::wpa_supp::WpaSuppCtl; use rs_matter_stack::matter::utils::init::InitMaybeUninit; use rs_matter_stack::matter::utils::sync::blocking::Mutex; use rs_matter_stack::matter::utils::sync::DynBase; use rs_matter_stack::matter::utils::zbus::Connection; use rs_matter_stack::matter::{clusters, devices, with, BasicCommData}; use rs_matter_stack::nal::std::Stack as StdNetStack; use rs_matter_stack::wireless::{PreexistingWireless, WifiMatterStack}; pub type Result = core::result::Result; const DEFAULT_CREDS_DIR: &str = "./creds"; const DEFAULT_WIFI_IFACE: &str = "wlan0"; const LIGHT_ENDPOINT_ID: u16 = 1; const BLE_SERVICE_NAME: &str = "TOES"; const BUMP_SIZE: usize = 48_000; const DEV_TYPE_EXTENDED_COLOR_LIGHT: rs_matter::dm::DeviceType = rs_matter::dm::DeviceType { dtype: 0x010d, drev: 3, }; const NODE: Node = Node { endpoints: &[ WifiMatterStack::<0, ()>::root_endpoint(), Endpoint { id: LIGHT_ENDPOINT_ID, device_types: devices!(DEV_TYPE_EXTENDED_COLOR_LIGHT), clusters: clusters!( DescHandler::CLUSTER, RgbLight::ON_OFF_CLUSTER, RgbLight::LEVEL_CLUSTER, RgbLight::COLOR_CLUSTER ), }, ], }; static MATTER_STACK: StaticCell> = StaticCell::new(); /// Runtime configuration used by [`run_with_config`]. #[derive(Clone, Debug)] pub struct Config { pub creds_dir: PathBuf, pub state_dir: PathBuf, pub wifi_iface: String, pub rgbled_order: String, } impl Config { pub fn from_env() -> Self { let creds_dir = std::env::var_os("TOES_MATTER_CREDS_DIR") .or_else(|| std::env::var_os("MATTER_CREDS_DIR")) .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from(DEFAULT_CREDS_DIR)); let state_dir = std::env::var_os("TOES_MATTER_STATE_DIR") .or_else(|| std::env::var_os("MATTER_KV_DIR")) .map(PathBuf::from) .unwrap_or_else(|| creds_dir.join("state")); let wifi_iface = std::env::var("TOES_MATTER_WIFI_IFACE") .or_else(|_| std::env::var("MATTER_WIFI_IFACE")) .unwrap_or_else(|_| DEFAULT_WIFI_IFACE.into()); let rgbled_order = std::env::var("RGBLED_ORDER").unwrap_or_else(|_| "grb".into()); Self { creds_dir, state_dir, wifi_iface, rgbled_order, } } } /// Write the development Matter credentials and setup metadata used by this crate. /// /// This intentionally uses `rs-matter`'s upstream development/test attestation /// credentials. These are good for local development, not production. pub async fn generate_credentials(path: impl AsRef) -> Result<()> { let path = path.as_ref(); std::fs::create_dir_all(path)?; std::fs::write(path.join("dac.der"), TEST_DEV_ATT.dac())?; std::fs::write(path.join("pai.der"), TEST_DEV_ATT.pai())?; std::fs::write( path.join("certification-declaration.der"), TEST_DEV_ATT.cert_declaration(), )?; std::fs::write( path.join("dac-private-key.raw"), TEST_DEV_ATT.dac_priv_key().access(), )?; let comm = read_comm_data(path)?.unwrap_or_else(random_comm_data); let payload = QrPayload::new_from_basic_info( DiscoveryCapabilities::BLE, CommFlowType::Standard, comm.clone(), &TEST_DEV_DET, no_optional_data, ); let mut qr_buf = [0_u8; 512]; let (qr_text, _) = payload.as_str(&mut qr_buf)?; let setup = format!( "setup_passcode={}\n\ discriminator={}\n\ manual_code={}\n\ qr_code={}\n", comm_passcode(&comm), comm.discriminator, comm.compute_pretty_pairing_code(), qr_text, ); std::fs::write(path.join("setup.txt"), setup)?; Ok(()) } fn load_comm_data(config: &Config) -> Result { match read_comm_data(&config.creds_dir)? { Some(comm) => Ok(comm), None => Ok(TEST_DEV_COMM.clone()), } } fn read_comm_data(path: &Path) -> Result> { let setup_path = path.join("setup.txt"); let setup = match std::fs::read_to_string(setup_path) { Ok(setup) => setup, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(err) => return Err(err.into()), }; let passcode = parse_setup_u32(&setup, "setup_passcode")?; let discriminator = parse_setup_u32(&setup, "discriminator")?; if discriminator > 0x0fff || !valid_setup_passcode(passcode) { return Err(ErrorCode::InvalidData.into()); } Ok(Some(make_comm_data(passcode, discriminator as u16))) } fn parse_setup_u32(setup: &str, key: &str) -> Result { let value = setup .lines() .find_map(|line| line.strip_prefix(key)?.strip_prefix('=')) .ok_or(ErrorCode::InvalidData)?; value.parse().map_err(|_| ErrorCode::InvalidData.into()) } fn random_comm_data() -> BasicCommData { let mut rng = rand::thread_rng(); let passcode = loop { let passcode = (rng.next_u32() % 99_999_998) + 1; if valid_setup_passcode(passcode) { break passcode; } }; let discriminator = (rng.next_u32() & 0x0fff) as u16; make_comm_data(passcode, discriminator) } fn make_comm_data(passcode: u32, discriminator: u16) -> BasicCommData { BasicCommData { password: Spake2pVerifierPassword::new_from_ref(Spake2pVerifierPasswordRef::new( &passcode.to_le_bytes(), )), discriminator, } } fn comm_passcode(comm: &BasicCommData) -> u32 { u32::from_le_bytes(*comm.password.access()) } fn valid_setup_passcode(passcode: u32) -> bool { const INVALID: &[u32] = &[ 0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888, 99999999, 12345678, 87654321, ]; (1..=99_999_998).contains(&passcode) && !INVALID.contains(&passcode) } /// Run the combined rs-matter-stack runtime. /// /// This runtime handles both first-boot BLE/Wi-Fi commissioning and normal /// operational Matter traffic. It runs until the process is stopped or an /// unrecoverable transport error is returned. pub async fn run() -> Result<()> { run_with_config(Config::from_env()).await } pub async fn run_with_config(config: Config) -> Result<()> { run_stack(config).await } /// Legacy entrypoint kept for callers that used the earlier split API. /// /// With `rs-matter-stack`, provisioning and listening are one runtime, so this /// does not return after commissioning; it continues serving Matter traffic. pub async fn provision() -> Result<()> { info!("Starting combined Matter provisioning/listening runtime"); run().await } pub async fn provision_with_config(config: Config) -> Result<()> { info!("Starting combined Matter provisioning/listening runtime"); run_with_config(config).await } /// Listen forever for Matter commands, opening BLE commissioning when needed. /// /// RGB commands are applied by invoking `rgbled --order `. pub async fn listen() -> Result<()> { run().await } pub async fn listen_with_config(config: Config) -> Result<()> { run_with_config(config).await } async fn run_stack(config: Config) -> Result<()> { std::fs::create_dir_all(&config.state_dir)?; let comm = load_comm_data(&config)?; let stack = MATTER_STACK .uninit() .init_with(WifiMatterStack::init_default( &TEST_DEV_DET, comm, &TEST_DEV_ATT, )); let crypto = default_crypto(rand::thread_rng(), DAC_PRIVKEY); let mut rand = crypto.weak_rand()?; let rgb_light = RgbLight::new(Dataver::new_rand(&mut rand), config.rgbled_order.clone()); let on_off = on_off::OnOffHandler::new(Dataver::new_rand(&mut rand), LIGHT_ENDPOINT_ID, &rgb_light); let level = level_control::LevelControlHandler::new( Dataver::new_rand(&mut rand), LIGHT_ENDPOINT_ID, &rgb_light, level_control::AttributeDefaults::default(), ); on_off.init(Some(&level)); level.init(Some(&on_off)); let handler = rgb_handler(&mut rand, &rgb_light, &on_off, &level); let mut kv = DirKvBlobStore::new(config.state_dir.clone()); stack.startup(&crypto, &mut kv).await?; let kv = stack.create_shared_kv(kv)?; let connection = Connection::system().await?; let net_ctl = PersistingWifiCtl::new( WpaSuppCtl::new( &connection, &config.wifi_iface, DhClientCtl::new(&config.wifi_iface, true), ), &config.wifi_iface, ); info!( "Matter Network Commissioning will manage Wi-Fi interface {}", config.wifi_iface ); info!("Running Matter via rs-matter-stack with BLE name {BLE_SERVICE_NAME}"); let matter = pin!(stack.run_coex( PreexistingWireless::new( StdNetStack::new(), PreferredNetifs::new(&config.wifi_iface), net_ctl, ZeroconfMdnsResponder::new(), ToesBluezGattPeripheral::new(&connection, None), ), &crypto, (NODE, handler), &kv, (), )); matter.await } fn rgb_handler<'a, OH, LH>( rand: &mut impl RngCore, rgb_light: &'a RgbLight, on_off: &'a on_off::OnOffHandler<'a, OH, LH>, level: &'a level_control::LevelControlHandler<'a, LH, OH>, ) -> impl rs_matter::dm::AsyncHandler + 'a where OH: OnOffHooks + 'a, LH: LevelControlHooks + 'a, { EmptyHandler .chain( EpClMatcher::new(Some(LIGHT_ENDPOINT_ID), Some(RgbLight::ON_OFF_CLUSTER.id)), on_off::HandlerAsyncAdaptor(on_off), ) .chain( EpClMatcher::new(Some(LIGHT_ENDPOINT_ID), Some(RgbLight::LEVEL_CLUSTER.id)), level_control::HandlerAsyncAdaptor(level), ) .chain( EpClMatcher::new(Some(LIGHT_ENDPOINT_ID), Some(RgbLight::COLOR_CLUSTER.id)), color_control::HandlerAsyncAdaptor(rgb_light), ) .chain( EpClMatcher::new(Some(LIGHT_ENDPOINT_ID), Some(DescHandler::CLUSTER.id)), Async(DescHandler::new(Dataver::new_rand(rand)).adapt()), ) } struct ToesBluezGattPeripheral<'a> { connection: &'a Connection, adapter_name: Option<&'a str>, } impl<'a> ToesBluezGattPeripheral<'a> { const fn new(connection: &'a Connection, adapter_name: Option<&'a str>) -> Self { Self { connection, adapter_name, } } } impl GattPeripheral for ToesBluezGattPeripheral<'_> { async fn run(&mut self, btp: &Btp, _service_name: &str, service_adv: &AdvData) -> Result<()> { rs_matter::transport::network::btp::bluez::run_peripheral( self.connection, self.adapter_name, BLE_SERVICE_NAME, service_adv, btp, ) .await } } struct PreferredNetifs<'a> { ifname: &'a str, } impl<'a> PreferredNetifs<'a> { const fn new(ifname: &'a str) -> Self { Self { ifname } } } impl DynBase for PreferredNetifs<'_> {} impl NetifDiag for PreferredNetifs<'_> { fn netifs(&self, f: &mut dyn FnMut(&NetifInfo) -> Result<()>) -> Result<()> { UnixNetifs.netifs(&mut |netif| { if netif.name == self.ifname { f(netif)?; } Ok(()) }) } } impl NetChangeNotif for PreferredNetifs<'_> { async fn wait_changed(&self) { UnixNetifs.wait_changed().await; } } struct PersistingWifiCtl<'a, C> { inner: C, ifname: &'a str, } impl<'a, C> PersistingWifiCtl<'a, C> { const fn new(inner: C, ifname: &'a str) -> Self { Self { inner, ifname } } } impl DynBase for PersistingWifiCtl<'_, C> where C: DynBase {} impl NetCtl for PersistingWifiCtl<'_, C> where C: NetCtl, { fn net_type(&self) -> NetworkType { self.inner.net_type() } fn connect_max_time_seconds(&self) -> u8 { self.inner.connect_max_time_seconds() } fn scan_max_time_seconds(&self) -> u8 { self.inner.scan_max_time_seconds() } fn supported_wifi_bands(&self, f: F) -> Result<()> where F: FnMut(WiFiBandEnum) -> Result<()>, { self.inner.supported_wifi_bands(f) } fn supported_thread_features(&self) -> ThreadCapabilitiesBitmap { self.inner.supported_thread_features() } fn thread_version(&self) -> u16 { self.inner.thread_version() } async fn scan(&self, network: Option<&[u8]>, f: F) -> core::result::Result<(), NetCtlError> where F: FnMut(&NetworkScanInfo) -> Result<()>, { self.inner.scan(network, f).await } async fn connect(&self, creds: &WirelessCreds<'_>) -> core::result::Result<(), NetCtlError> { self.inner.connect(creds).await?; if let Err(err) = persist_wifi_creds_to_wpa_supplicant(self.ifname, creds) { warn!("failed to persist Wi-Fi credentials to wpa_supplicant: {err:?}"); } Ok(()) } } impl WirelessDiag for PersistingWifiCtl<'_, C> where C: WirelessDiag, { fn connected(&self) -> Result { self.inner.connected() } } impl WifiDiag for PersistingWifiCtl<'_, C> where C: WifiDiag, { fn bssid(&self, f: &mut dyn FnMut(Option<&[u8]>) -> Result<()>) -> Result<()> { self.inner.bssid(f) } fn security_type(&self) -> Result> { self.inner.security_type() } fn wi_fi_version(&self) -> Result> { self.inner.wi_fi_version() } fn channel_number(&self) -> Result> { self.inner.channel_number() } fn rssi(&self) -> Result> { self.inner.rssi() } } impl NetChangeNotif for PersistingWifiCtl<'_, C> where C: NetChangeNotif, { async fn wait_changed(&self) { self.inner.wait_changed().await; } } fn persist_wifi_creds_to_wpa_supplicant( ifname: &str, creds: &WirelessCreds<'_>, ) -> core::result::Result<(), NetCtlError> { let WirelessCreds::Wifi { ssid, pass } = creds else { return Ok(()); }; let ssid = std::str::from_utf8(ssid).map_err(|_| NetCtlError::Other(ErrorCode::InvalidData.into()))?; let pass = std::str::from_utf8(pass).map_err(|_| NetCtlError::Other(ErrorCode::InvalidData.into()))?; let network_id = match find_wpa_network_id(ifname, ssid)? { Some(network_id) => network_id, None => add_wpa_network(ifname)?, }; wpa_cli( ifname, &["set_network", &network_id, "ssid", &wpa_cli_string(ssid)], )?; if pass.is_empty() { wpa_cli(ifname, &["set_network", &network_id, "key_mgmt", "NONE"])?; } else { wpa_cli( ifname, &["set_network", &network_id, "psk", &wpa_cli_string(pass)], )?; } wpa_cli(ifname, &["enable_network", &network_id])?; wpa_cli(ifname, &["save_config"])?; info!("Persisted Wi-Fi credentials for SSID {ssid} to wpa_supplicant"); Ok(()) } fn find_wpa_network_id( ifname: &str, ssid: &str, ) -> core::result::Result, NetCtlError> { let output = wpa_cli_output(ifname, &["list_networks"])?; Ok(output.lines().skip(1).find_map(|line| { let mut fields = line.split('\t'); let id = fields.next()?; let listed_ssid = fields.next()?; (listed_ssid == ssid).then(|| id.to_owned()) })) } fn add_wpa_network(ifname: &str) -> core::result::Result { let output = wpa_cli_output(ifname, &["add_network"])?; Ok(output.trim().to_owned()) } fn wpa_cli(ifname: &str, args: &[&str]) -> core::result::Result<(), NetCtlError> { let output = wpa_cli_raw(ifname, args)?; if output.status.success() && String::from_utf8_lossy(&output.stdout).trim() != "FAIL" { Ok(()) } else { warn!( "wpa_cli -i {ifname} {} failed: status={:?}, stdout={}, stderr={}", wpa_cli_desc(args), output.status.code(), String::from_utf8_lossy(&output.stdout).trim(), String::from_utf8_lossy(&output.stderr).trim(), ); Err(NetCtlError::Other(ErrorCode::Invalid.into())) } } fn wpa_cli_output(ifname: &str, args: &[&str]) -> core::result::Result { let output = wpa_cli_raw(ifname, args)?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).into_owned()) } else { warn!( "wpa_cli -i {ifname} {} failed: status={:?}, stdout={}, stderr={}", wpa_cli_desc(args), output.status.code(), String::from_utf8_lossy(&output.stdout).trim(), String::from_utf8_lossy(&output.stderr).trim(), ); Err(NetCtlError::Other(ErrorCode::Invalid.into())) } } fn wpa_cli_desc(args: &[&str]) -> String { if matches!(args, ["set_network", _, "psk", _]) { format!("set_network {} psk ***", args[1]) } else { args.join(" ") } } fn wpa_cli_raw( ifname: &str, args: &[&str], ) -> core::result::Result { Command::new("wpa_cli") .arg("-i") .arg(ifname) .args(args) .output() .map_err(|err| NetCtlError::Other(err.into())) } fn wpa_cli_string(value: &str) -> String { let mut escaped = String::with_capacity(value.len() + 2); escaped.push('"'); for ch in value.chars() { match ch { '"' | '\\' => { escaped.push('\\'); escaped.push(ch); } _ => escaped.push(ch), } } escaped.push('"'); escaped } struct RgbLight { state: Mutex>, color_dataver: Dataver, rgbled_order: String, } struct RgbLightState { on: bool, level: Option, hue: u8, saturation: u8, options: color_control::OptionsBitmap, start_up_on_off: Nullable, start_up_level: Option, } impl RgbLight { const ON_OFF_CLUSTER: Cluster<'static> = on_off::FULL_CLUSTER .with_revision(6) .with_features(on_off::Feature::LIGHTING.bits()) .with_attrs(with!( required; on_off::AttributeId::OnOff | on_off::AttributeId::GlobalSceneControl | on_off::AttributeId::OnTime | on_off::AttributeId::OffWaitTime | on_off::AttributeId::StartUpOnOff )) .with_cmds(with!( on_off::CommandId::Off | on_off::CommandId::On | on_off::CommandId::Toggle | on_off::CommandId::OffWithEffect | on_off::CommandId::OnWithRecallGlobalScene | on_off::CommandId::OnWithTimedOff )); const LEVEL_CLUSTER: Cluster<'static> = level_control::FULL_CLUSTER .with_revision(6) .with_features(level_control::Feature::ON_OFF.bits()) .with_attrs(with!( required; level_control::AttributeId::CurrentLevel | level_control::AttributeId::MinLevel | level_control::AttributeId::MaxLevel | level_control::AttributeId::OnLevel | level_control::AttributeId::Options )) .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 )); const COLOR_CLUSTER: Cluster<'static> = color_control::FULL_CLUSTER .with_revision(7) .with_features(color_control::Feature::HUE_AND_SATURATION.bits()) .with_attrs(with!( required; color_control::AttributeId::CurrentHue | color_control::AttributeId::CurrentSaturation | color_control::AttributeId::ColorMode | color_control::AttributeId::Options | color_control::AttributeId::NumberOfPrimaries | color_control::AttributeId::EnhancedColorMode | color_control::AttributeId::ColorCapabilities )) .with_cmds(with!( color_control::CommandId::MoveToHue | color_control::CommandId::MoveHue | color_control::CommandId::StepHue | color_control::CommandId::MoveToSaturation | color_control::CommandId::MoveSaturation | color_control::CommandId::StepSaturation | color_control::CommandId::MoveToHueAndSaturation | color_control::CommandId::StopMoveStep )); fn new(color_dataver: Dataver, rgbled_order: String) -> Self { Self { state: Mutex::new(RefCell::new(RgbLightState { on: false, level: Some(254), hue: 0, saturation: 254, options: color_control::OptionsBitmap::from_bits(0).unwrap(), start_up_on_off: Nullable::none(), start_up_level: None, })), color_dataver, rgbled_order, } } fn apply(&self) { let (on, level, hue, saturation) = self.state.lock(|state| { let state = state.borrow(); ( state.on, state.level.unwrap_or(254), state.hue, state.saturation, ) }); let (r, g, b) = if on { hsv_to_rgb(hue, saturation, level) } else { (0, 0, 0) }; let color = format!("{r:02x}{g:02x}{b:02x}"); info!("Setting RGB LED to #{color}"); match Command::new("rgbled") .arg("--order") .arg(&self.rgbled_order) .arg(&color) .status() { Ok(status) if status.success() => {} Ok(status) => warn!( "rgbled --order {} exited with status {status}", self.rgbled_order ), Err(err) => warn!("failed to run rgbled: {err}"), } } fn set_hue_sat(&self, hue: Option, saturation: Option) { self.state.lock(|state| { let mut state = state.borrow_mut(); if let Some(hue) = hue { state.hue = hue; } if let Some(saturation) = saturation { state.saturation = saturation; } }); self.color_dataver.changed(); self.apply(); } } impl DynBase for RgbLight {} impl OnOffHooks for RgbLight { const CLUSTER: Cluster<'static> = Self::ON_OFF_CLUSTER; fn on_off(&self) -> bool { self.state.lock(|state| state.borrow().on) } fn set_on_off(&self, on: bool) { self.state.lock(|state| state.borrow_mut().on = on); self.apply(); } fn start_up_on_off(&self) -> Nullable { self.state .lock(|state| state.borrow().start_up_on_off.clone()) } fn set_start_up_on_off(&self, value: Nullable) -> Result<()> { self.state .lock(|state| state.borrow_mut().start_up_on_off = value); Ok(()) } async fn handle_off_with_effect(&self, _effect: on_off::EffectVariantEnum) { self.set_on_off(false); } } impl LevelControlHooks for RgbLight { const MIN_LEVEL: u8 = 1; const MAX_LEVEL: u8 = 254; const FASTEST_RATE: u8 = 50; const CLUSTER: Cluster<'static> = Self::LEVEL_CLUSTER; fn set_device_level(&self, level: u8) -> core::result::Result, ()> { self.state .lock(|state| state.borrow_mut().level = Some(level)); self.apply(); Ok(Some(level)) } fn current_level(&self) -> Option { self.state.lock(|state| state.borrow().level) } fn set_current_level(&self, level: Option) { self.state.lock(|state| state.borrow_mut().level = level); } fn start_up_current_level(&self) -> Result> { Ok(self.state.lock(|state| state.borrow().start_up_level)) } fn set_start_up_current_level(&self, value: Option) -> Result<()> { self.state .lock(|state| state.borrow_mut().start_up_level = value); Ok(()) } } impl ColorControlHandler for RgbLight { const CLUSTER: Cluster<'static> = Self::COLOR_CLUSTER; fn dataver(&self) -> u32 { self.color_dataver.get() } fn dataver_changed(&self) { self.color_dataver.changed(); } async fn current_hue(&self, _ctx: impl rs_matter::dm::ReadContext) -> Result { Ok(self.state.lock(|state| state.borrow().hue)) } async fn current_saturation(&self, _ctx: impl rs_matter::dm::ReadContext) -> Result { Ok(self.state.lock(|state| state.borrow().saturation)) } async fn color_mode( &self, _ctx: impl rs_matter::dm::ReadContext, ) -> Result { Ok(color_control::ColorModeEnum::CurrentHueAndCurrentSaturation) } async fn options( &self, _ctx: impl rs_matter::dm::ReadContext, ) -> Result { Ok(self.state.lock(|state| state.borrow().options)) } async fn number_of_primaries( &self, _ctx: impl rs_matter::dm::ReadContext, ) -> Result> { Ok(Nullable::none()) } async fn enhanced_color_mode( &self, _ctx: impl rs_matter::dm::ReadContext, ) -> Result { Ok(color_control::EnhancedColorModeEnum::CurrentHueAndCurrentSaturation) } async fn color_capabilities( &self, _ctx: impl rs_matter::dm::ReadContext, ) -> Result { Ok(color_control::ColorCapabilitiesBitmap::HUE_SATURATION) } async fn set_options( &self, _ctx: impl rs_matter::dm::WriteContext, value: color_control::OptionsBitmap, ) -> Result<()> { self.state.lock(|state| state.borrow_mut().options = value); self.color_dataver.changed(); Ok(()) } async fn handle_move_to_hue( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::MoveToHueRequest<'_>, ) -> Result<()> { self.set_hue_sat(Some(request.hue()?), None); Ok(()) } async fn handle_move_hue( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::MoveHueRequest<'_>, ) -> Result<()> { let hue = self.state.lock(|state| state.borrow().hue); let rate = request.rate().unwrap_or(8); let hue = match request.move_mode()? { color_control::MoveModeEnum::Up => hue.wrapping_add(rate), color_control::MoveModeEnum::Down => hue.wrapping_sub(rate), color_control::MoveModeEnum::Stop => hue, }; self.set_hue_sat(Some(hue), None); Ok(()) } async fn handle_step_hue( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::StepHueRequest<'_>, ) -> Result<()> { let hue = self.state.lock(|state| state.borrow().hue); let hue = match request.step_mode()? { color_control::StepModeEnum::Up => hue.wrapping_add(request.step_size()?), color_control::StepModeEnum::Down => hue.wrapping_sub(request.step_size()?), }; self.set_hue_sat(Some(hue), None); Ok(()) } async fn handle_move_to_saturation( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::MoveToSaturationRequest<'_>, ) -> Result<()> { self.set_hue_sat(None, Some(request.saturation()?)); Ok(()) } async fn handle_move_saturation( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::MoveSaturationRequest<'_>, ) -> Result<()> { let saturation = self.state.lock(|state| state.borrow().saturation); let rate = request.rate().unwrap_or(8); let saturation = match request.move_mode()? { color_control::MoveModeEnum::Up => saturation.saturating_add(rate), color_control::MoveModeEnum::Down => saturation.saturating_sub(rate), color_control::MoveModeEnum::Stop => saturation, }; self.set_hue_sat(None, Some(saturation)); Ok(()) } async fn handle_step_saturation( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::StepSaturationRequest<'_>, ) -> Result<()> { let saturation = self.state.lock(|state| state.borrow().saturation); let saturation = match request.step_mode()? { color_control::StepModeEnum::Up => saturation.saturating_add(request.step_size()?), color_control::StepModeEnum::Down => saturation.saturating_sub(request.step_size()?), }; self.set_hue_sat(None, Some(saturation)); Ok(()) } async fn handle_move_to_hue_and_saturation( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::MoveToHueAndSaturationRequest<'_>, ) -> Result<()> { self.set_hue_sat(Some(request.hue()?), Some(request.saturation()?)); Ok(()) } async fn handle_move_to_color( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::MoveToColorRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_move_color( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::MoveColorRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_step_color( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::StepColorRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_move_to_color_temperature( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::MoveToColorTemperatureRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_enhanced_move_to_hue( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::EnhancedMoveToHueRequest<'_>, ) -> Result<()> { self.set_hue_sat(Some((request.enhanced_hue()? >> 8) as u8), None); Ok(()) } async fn handle_enhanced_move_hue( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::EnhancedMoveHueRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_enhanced_step_hue( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::EnhancedStepHueRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_enhanced_move_to_hue_and_saturation( &self, _ctx: impl rs_matter::dm::InvokeContext, request: color_control::EnhancedMoveToHueAndSaturationRequest<'_>, ) -> Result<()> { self.set_hue_sat( Some((request.enhanced_hue()? >> 8) as u8), Some(request.saturation()?), ); Ok(()) } async fn handle_color_loop_set( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::ColorLoopSetRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_stop_move_step( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::StopMoveStepRequest<'_>, ) -> Result<()> { Ok(()) } async fn handle_move_color_temperature( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::MoveColorTemperatureRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } async fn handle_step_color_temperature( &self, _ctx: impl rs_matter::dm::InvokeContext, _request: color_control::StepColorTemperatureRequest<'_>, ) -> Result<()> { Err(ErrorCode::InvalidAction.into()) } } fn hsv_to_rgb(hue: u8, saturation: u8, value: u8) -> (u8, u8, u8) { if saturation == 0 { return (value, value, value); } let region = hue as u16 * 6 / 256; let remainder = (hue as u16 * 6) % 256; let value = value as u16; let saturation = saturation as u16; let p = value * (255 - saturation) / 255; let q = value * (255 - (saturation * remainder / 255)) / 255; let t = value * (255 - (saturation * (255 - remainder) / 255)) / 255; match region { 0 => (value as u8, t as u8, p as u8), 1 => (q as u8, value as u8, p as u8), 2 => (p as u8, value as u8, t as u8), 3 => (p as u8, q as u8, value as u8), 4 => (t as u8, p as u8, value as u8), _ => (value as u8, p as u8, q as u8), } }