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, ThreadDataset, Value,
};
const ONOFF_ENDPOINT: u16 = 1;
const ONOFF_CLUSTER: u32 = 0x0006;
const ONOFF_ATTR: u32 = 0x0000;
const ONOFF_CMD_TOGGLE: u32 = 0x02;
#[derive(Parser)]
#[command(about = "Commission a Matter-over-Thread device over BLE and control it")]
struct Args {
#[arg(long, default_value = "matter-controller.bin")]
store: PathBuf,
#[arg(long)]
commission: String,
#[arg(long)]
dataset: String,
#[arg(long, requires = "cd_dir")]
paa_dir: Option<PathBuf>,
#[arg(long, requires = "paa_dir")]
cd_dir: Option<PathBuf>,
}
fn hex_decode(s: &str) -> Result<Vec<u8>> {
let s = s.trim();
if !s.len().is_multiple_of(2) {
bail!(
"dataset hex has an odd number of characters ({} chars)",
s.len()
);
}
(0..s.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&s[i..i + 2], 16)
.with_context(|| format!("invalid hex byte at offset {i}: {:?}", &s[i..i + 2]))
})
.collect()
}
#[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 production attestation roots")?
}
_ => AttestationTrust::example_device_roots(),
};
let dataset = ThreadDataset::new(hex_decode(&args.dataset)?)
.context("dataset is not a well-formed Thread operational dataset")?;
println!(
"Thread dataset accepted ({} bytes); expecting Ext-PAN-ID {:02x?}",
args.dataset.trim().len() / 2,
dataset.ext_pan_id()
);
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→Thread (this can take up to ~90 s at network-enable while the device attaches to the mesh)…");
let node_id = controller
.commission_ble(&args.commission, NetworkCredentials::Thread(dataset), None)
.await
.context("BLE→Thread 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 Thread not confirmed");
}
println!(
"SUCCESS — commissioned and controlled node 0x{node_id:016X} over Thread; 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))
))
}