1159 lines
35 KiB
Rust
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),
|
|
}
|
|
}
|