use std::time::Duration;
use hap_ble::ThreadDataset;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "hap_ble=info,ble_thread_provision=info".into()),
)
.init();
let Some(setup_code) = std::env::args().nth(1) else {
eprintln!("usage: ble_thread_provision <setup-code> (dataset via THREAD_* env vars)");
return Ok(());
};
let dataset = dataset_from_env()?;
println!("Provisioning target network: {dataset:?}");
let mut target = None;
for attempt in 1..=8 {
let found = hap_ble::scan(Duration::from_secs(8)).await?;
if let Some(t) = found.into_iter().find(|a| !a.paired) {
target = Some(t);
break;
}
println!("scan {attempt}: no unpaired accessory yet, retrying...");
}
let Some(target) = target else {
println!("No unpaired accessory found. Factory-reset the accessory and retry.");
return Ok(());
};
println!("Pairing with {}...", target.device_id);
let gatt: std::sync::Arc<dyn hap_ble::GattConnection> = hap_ble::connect_gatt(&target).await?;
let controller = hap_ble::BleController::generate("hap-ble-thread-provision".into());
let hap_ble::Paired { mut accessory, .. } = controller.pair(gatt, &target, &setup_code).await?;
println!("Paired.");
let has_thread = accessory.accessories().iter().any(|a| {
a.services.iter().any(|s| {
s.characteristics
.iter()
.any(|c| c.iid != 0 && is_thread_cp(&format!("{:?}", c.char_type)))
})
});
if !has_thread {
println!("note: no obvious Thread Control Point in the database dump — attempting anyway");
}
println!("Writing Thread credentials...");
accessory.thread_provision(&dataset).await?;
println!(
"Provision sent. The accessory should now join '{}' and appear over Thread \
(discover _hap._udp / read via hap-thread). Verify on the mesh, not here.",
dataset.network_name
);
Ok(())
}
fn is_thread_cp(dbg: &str) -> bool {
let d = dbg.to_ascii_lowercase();
d.contains("threadcontrolpoint") || d.contains("0704")
}
fn dataset_from_env() -> Result<ThreadDataset, String> {
let network_name = env("THREAD_NETWORK_NAME")?;
let channel: u8 = env("THREAD_CHANNEL")?
.trim()
.parse()
.map_err(|_| "THREAD_CHANNEL must be a number 0..=255".to_string())?;
let pan_raw = env("THREAD_PANID")?;
let pan_id = u16::from_str_radix(pan_raw.trim().trim_start_matches("0x"), 16)
.map_err(|_| "THREAD_PANID must be hex, e.g. 0x89d7".to_string())?;
let ext_pan_id: [u8; 8] = hex_array(&env("THREAD_EXTPANID")?)
.ok_or("THREAD_EXTPANID must be 16 hex chars (8 bytes)")?;
let network_key: [u8; 16] = hex_array(&env("THREAD_NETWORKKEY")?)
.ok_or("THREAD_NETWORKKEY must be 32 hex chars (16 bytes)")?;
Ok(ThreadDataset {
network_name,
channel,
pan_id,
ext_pan_id,
network_key,
})
}
fn env(key: &str) -> Result<String, String> {
std::env::var(key).map_err(|_| format!("missing environment variable {key}"))
}
fn hex_array<const N: usize>(s: &str) -> Option<[u8; N]> {
let s = s.trim();
if s.len() != N * 2 {
return None;
}
let mut out = [0u8; N];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok()?;
}
Some(out)
}