toes-matter/src/lib.rs

1159 lines
35 KiB
Rust

#![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<T> = core::result::Result<T, Error>;
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<WifiMatterStack<BUMP_SIZE, ()>> = 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<Path>) -> 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<BasicCommData> {
match read_comm_data(&config.creds_dir)? {
Some(comm) => Ok(comm),
None => Ok(TEST_DEV_COMM.clone()),
}
}
fn read_comm_data(path: &Path) -> Result<Option<BasicCommData>> {
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<u32> {
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 <RGBLED_ORDER|grb> <rrggbb>`.
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<C> DynBase for PersistingWifiCtl<'_, C> where C: DynBase {}
impl<C> 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<F>(&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<F>(&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<C> WirelessDiag for PersistingWifiCtl<'_, C>
where
C: WirelessDiag,
{
fn connected(&self) -> Result<bool> {
self.inner.connected()
}
}
impl<C> 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<Nullable<SecurityTypeEnum>> {
self.inner.security_type()
}
fn wi_fi_version(&self) -> Result<Nullable<WiFiVersionEnum>> {
self.inner.wi_fi_version()
}
fn channel_number(&self) -> Result<Nullable<u16>> {
self.inner.channel_number()
}
fn rssi(&self) -> Result<Nullable<i8>> {
self.inner.rssi()
}
}
impl<C> 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<Option<String>, 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<String, NetCtlError> {
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<String, NetCtlError> {
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<std::process::Output, NetCtlError> {
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<RefCell<RgbLightState>>,
color_dataver: Dataver,
rgbled_order: String,
}
struct RgbLightState {
on: bool,
level: Option<u8>,
hue: u8,
saturation: u8,
options: color_control::OptionsBitmap,
start_up_on_off: Nullable<on_off::StartUpOnOffEnum>,
start_up_level: Option<u8>,
}
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<u8>, saturation: Option<u8>) {
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<on_off::StartUpOnOffEnum> {
self.state
.lock(|state| state.borrow().start_up_on_off.clone())
}
fn set_start_up_on_off(&self, value: Nullable<on_off::StartUpOnOffEnum>) -> 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<Option<u8>, ()> {
self.state
.lock(|state| state.borrow_mut().level = Some(level));
self.apply();
Ok(Some(level))
}
fn current_level(&self) -> Option<u8> {
self.state.lock(|state| state.borrow().level)
}
fn set_current_level(&self, level: Option<u8>) {
self.state.lock(|state| state.borrow_mut().level = level);
}
fn start_up_current_level(&self) -> Result<Option<u8>> {
Ok(self.state.lock(|state| state.borrow().start_up_level))
}
fn set_start_up_current_level(&self, value: Option<u8>) -> 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<u8> {
Ok(self.state.lock(|state| state.borrow().hue))
}
async fn current_saturation(&self, _ctx: impl rs_matter::dm::ReadContext) -> Result<u8> {
Ok(self.state.lock(|state| state.borrow().saturation))
}
async fn color_mode(
&self,
_ctx: impl rs_matter::dm::ReadContext,
) -> Result<color_control::ColorModeEnum> {
Ok(color_control::ColorModeEnum::CurrentHueAndCurrentSaturation)
}
async fn options(
&self,
_ctx: impl rs_matter::dm::ReadContext,
) -> Result<color_control::OptionsBitmap> {
Ok(self.state.lock(|state| state.borrow().options))
}
async fn number_of_primaries(
&self,
_ctx: impl rs_matter::dm::ReadContext,
) -> Result<Nullable<u8>> {
Ok(Nullable::none())
}
async fn enhanced_color_mode(
&self,
_ctx: impl rs_matter::dm::ReadContext,
) -> Result<color_control::EnhancedColorModeEnum> {
Ok(color_control::EnhancedColorModeEnum::CurrentHueAndCurrentSaturation)
}
async fn color_capabilities(
&self,
_ctx: impl rs_matter::dm::ReadContext,
) -> Result<color_control::ColorCapabilitiesBitmap> {
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),
}
}