use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use clap::Parser;
use matter_controller::{
AttestationTrust, CommandPath, FabricConfig, FileStore, MatterController, MatterTime,
NetworkCredentials, ReadPath, Value, WiFiCredentials,
};
const ONOFF_ENDPOINT: u16 = 1;
const ONOFF_CLUSTER: u32 = 0x0006;
const ONOFF_ATTR: u32 = 0x0000;
const ONOFF_CMD_TOGGLE: u32 = 0x02;
const SSID_MAX: usize = 32;
const PASSWORD_MAX: usize = 64;
#[derive(Parser)]
#[command(about = "Commission a Matter-over-Wi-Fi device over BLE and control it")]
struct Args {
#[arg(long, default_value = "matter-controller.bin")]
store: PathBuf,
#[arg(long)]
commission: String,
#[arg(long)]
ssid: String,
#[arg(long, default_value = "")]
password: String,
#[arg(long, conflicts_with = "password")]
password_env: Option<String>,
#[arg(long, requires = "cd_dir")]
paa_dir: Option<PathBuf>,
#[arg(long, requires = "paa_dir")]
cd_dir: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let trust = match (&args.paa_dir, &args.cd_dir) {
(Some(paa), Some(cd)) => {
AttestationTrust::from_dirs(paa, cd).context("loading attestation roots")?
}
_ => AttestationTrust::example_device_roots(),
};
let password = match &args.password_env {
Some(var) => {
std::env::var(var).with_context(|| format!("reading Wi-Fi passphrase from ${var}"))?
}
None => args.password.clone(),
};
if args.ssid.is_empty() || args.ssid.len() > SSID_MAX {
bail!("SSID must be 1–{SSID_MAX} bytes (got {})", args.ssid.len());
}
if password.len() > PASSWORD_MAX {
bail!(
"Wi-Fi passphrase must be 0–{PASSWORD_MAX} bytes (got {})",
password.len()
);
}
println!(
"Wi-Fi credentials accepted (SSID {:?}, passphrase <{} bytes>)",
args.ssid,
password.len()
);
let wifi = WiFiCredentials {
ssid: args.ssid.clone().into_bytes(),
credentials: password.into_bytes(),
};
let fresh = !args.store.exists();
let store = Arc::new(FileStore::new(&args.store));
let controller = MatterController::builder(store)
.attestation_trust(trust)
.build()
.await
.context("opening controller")?;
if fresh {
let now_unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(1_700_000_000, |d| d.as_secs());
controller
.create_fabric(FabricConfig::new(
1,
1,
1,
(
MatterTime::from_unix_secs(now_unix.saturating_sub(3600)),
MatterTime::NO_EXPIRY,
),
))
.await
.context("creating fabric")?;
println!("created a new fabric (stable commissioner identity persisted)");
}
println!(
"commissioning over BLE→Wi-Fi (network-enable waits while the device associates \
and registers over mDNS)…"
);
let node_id = controller
.commission_ble(&args.commission, NetworkCredentials::WiFi(wifi), None)
.await
.context("BLE→Wi-Fi commissioning")?
.node_id;
println!("commissioned over BLE as node 0x{node_id:016X}");
let node = controller.node(node_id);
let before = read_onoff(&node).await.context("reading OnOff")?;
println!("OnOff = {before}");
node.invoke(
CommandPath {
endpoint: ONOFF_ENDPOINT,
cluster: ONOFF_CLUSTER,
command: ONOFF_CMD_TOGGLE,
},
Value::Structure(vec![]),
)
.await
.context("invoking Toggle")?;
let after = read_onoff(&node).await.context("re-reading OnOff")?;
println!(
"OnOff after Toggle = {after} (flipped: {})",
before != after
);
if before == after {
bail!("OnOff did not change after Toggle — operational control over Wi-Fi not confirmed");
}
println!(
"SUCCESS — commissioned and controlled node 0x{node_id:016X} over Wi-Fi; persisted to {}",
args.store.display()
);
Ok(())
}
async fn read_onoff(node: &matter_controller::Node) -> Result<bool> {
let report = node
.read(&[ReadPath::concrete(
ONOFF_ENDPOINT,
ONOFF_CLUSTER,
ONOFF_ATTR,
)])
.await?;
Ok(matches!(
report.first().map(|(_, v)| v),
Some(Value::Bool(true))
))
}