1mod config;
58mod device;
59
60pub use config::ManagerConfig;
61pub use device::Device;
62
63use std::sync::Arc;
64
65use anyhow::{Context, Result};
66
67use std::time::Duration;
68
69use crate::{certmanager, controller, discover::{self, MatterDeviceInfo}, fabric::Fabric, mdns2, onboarding, transport};
70
71pub struct DeviceManager {
72 base_path: String,
73 config: ManagerConfig,
74 transport: Arc<transport::Transport>,
75 controller: Arc<controller::Controller>,
76 certmanager: Arc<dyn certmanager::CertManager>,
77 registry: std::sync::Mutex<device::DeviceRegistry>,
78 mdns: Arc<mdns2::MdnsService>,
79}
80
81impl DeviceManager {
82 pub async fn create(base_path: &str, config: ManagerConfig) -> Result<Self> {
85 std::fs::create_dir_all(base_path)
86 .context(format!("creating base directory {}", base_path))?;
87 config::save_config(base_path, &config)?;
88
89 let pem = config::pem_path(base_path);
90 let cm = certmanager::FileCertManager::new(config.fabric_id, &pem);
91 cm.bootstrap()?;
92 cm.create_user(config.controller_id)?;
93
94 let cm: Arc<dyn certmanager::CertManager> = certmanager::FileCertManager::load(&pem)?;
95 let transport = transport::Transport::new(&config.local_address).await?;
96 let controller = controller::Controller::new(&cm, &transport, config.fabric_id)?;
97 let registry = device::DeviceRegistry::load(&config::devices_path(base_path))?;
98 let mdns = mdns2::MdnsService::new().await?;
99
100 Ok(Self {
101 base_path: base_path.to_owned(),
102 config,
103 transport,
104 controller,
105 certmanager: cm,
106 registry: std::sync::Mutex::new(registry),
107 mdns,
108 })
109 }
110
111 pub async fn load(base_path: &str) -> Result<Self> {
113 let config = config::load_config(base_path)?;
114 let pem = config::pem_path(base_path);
115 let cm: Arc<dyn certmanager::CertManager> = certmanager::FileCertManager::load(&pem)?;
116 let transport = transport::Transport::new(&config.local_address).await?;
117 let controller = controller::Controller::new(&cm, &transport, config.fabric_id)?;
118 let registry = device::DeviceRegistry::load(&config::devices_path(base_path))?;
119 let mdns = mdns2::MdnsService::new().await?;
120
121 Ok(Self {
122 base_path: base_path.to_owned(),
123 config,
124 transport,
125 controller,
126 certmanager: cm,
127 registry: std::sync::Mutex::new(registry),
128 mdns,
129 })
130 }
131
132 pub async fn commission(
135 &self,
136 address: &str,
137 pin: u32,
138 node_id: u64,
139 name: &str,
140 ) -> Result<controller::Connection> {
141 self.commission_at(address, pin, node_id, name, (None, None, None)).await
142 }
143
144 async fn commission_at(
148 &self,
149 address: &str,
150 pin: u32,
151 node_id: u64,
152 name: &str,
153 mrp_ms: (Option<u32>, Option<u32>, Option<u32>),
154 ) -> Result<controller::Connection> {
155 let conn = self.transport.create_connection(address).await;
156 conn.set_mrp_params(crate::mrp::MrpParameters::from_txt_ms(mrp_ms.0, mrp_ms.1, mrp_ms.2));
157 let connection = self
158 .controller
159 .commission(&conn, pin, node_id, self.config.controller_id)
160 .await?;
161
162 let device = Device {
163 node_id,
164 address: address.to_owned(),
165 name: name.to_owned(),
166 sii_ms: mrp_ms.0,
167 sai_ms: mrp_ms.1,
168 sat_ms: mrp_ms.2,
169 };
170 self.registry
171 .lock()
172 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
173 .add(device)?;
174
175 Ok(connection)
176 }
177
178 pub async fn connect(&self, node_id: u64) -> Result<controller::Connection> {
181 let address = {
182 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
183 reg.get(node_id)
184 .context(format!("device {} not found in registry", node_id))?
185 .address
186 .clone()
187 };
188 self.connect_with_rediscovery(node_id, &address).await
189 }
190
191 pub async fn connect_by_name(&self, name: &str) -> Result<controller::Connection> {
194 let (node_id, address) = {
195 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
196 let dev = reg
197 .get_by_name(name)
198 .context(format!("device '{}' not found in registry", name))?;
199 (dev.node_id, dev.address.clone())
200 };
201 self.connect_with_rediscovery(node_id, &address).await
202 }
203
204 async fn connect_with_rediscovery(&self, node_id: u64, address: &str) -> Result<controller::Connection> {
208 let stored_mrp = self
209 .registry
210 .lock()
211 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
212 .get(node_id)
213 .map(|d| d.mrp_params())
214 .unwrap_or_default();
215 let mut current_address = address.to_string();
216 let mut conn = self.transport.create_connection(¤t_address).await;
218 conn.set_mrp_params(stored_mrp);
219
220 match self.controller.auth_sigma_with_busy_retry(&conn, node_id, self.config.controller_id).await {
221 Ok(ses) => Ok(controller::Connection::from_parts(conn, ses)),
222 Err(e) => {
223 log::info!(
225 "Connection to {} failed ({}), attempting operational rediscovery...",
226 current_address, e
227 );
228 let (new_address, matter_info) = self
229 .discover_device_info(node_id, Duration::from_secs(10))
230 .await
231 .context(format!("rediscovery for node {} after connect failure", node_id))?;
232 current_address = new_address;
233 conn = self.transport.create_connection(¤t_address).await;
234 conn.set_mrp_params(matter_info.mrp_params());
235 let ses = self
236 .controller
237 .auth_sigma_with_busy_retry(&conn, node_id, self.config.controller_id)
238 .await
239 .context(format!(
240 "connection still failed after rediscovery at {}", current_address
241 ))?;
242 Ok(controller::Connection::from_parts(conn, ses))
243 }
244 }
245 }
246
247 pub async fn reauth(&self, conn: &controller::Connection, node_id: u64) -> Result<()> {
251 conn.reauth(&self.controller, node_id, self.config.controller_id).await
252 }
253
254 pub async fn commission_with_code(
258 &self,
259 pairing_code: &str,
260 node_id: u64,
261 name: &str,
262 ) -> Result<controller::Connection> {
263 let info = onboarding::decode_manual_pairing_code(pairing_code)
264 .context("decoding manual pairing code")?;
265 let discriminator = info.discriminator;
266 let passcode = info.passcode;
267
268 log::info!("Discovering device with discriminator {}...", discriminator);
269
270 let is_short = info.is_short_discriminator;
271 let (_, matter_info) = discover::discover_one(
272 &self.mdns,
273 "_matterc._udp.local",
274 "_matterc._udp.local.",
275 Duration::from_secs(10),
276 move |_, i| {
277 if let Some(ref d) = i.discriminator {
278 if let Ok(mut disc) = d.parse::<u16>() {
279 if is_short { disc &= 0xf00; }
280 return disc == discriminator;
281 }
282 }
283 false
284 },
285 ).await.context(format!("discovering device with discriminator {}", discriminator))?;
286
287 let mrp_ms = (
288 matter_info.session_idle_interval_ms,
289 matter_info.session_active_interval_ms,
290 matter_info.session_active_threshold_ms,
291 );
292 let ips = matter_info.ips;
293 let port = matter_info.port.unwrap_or(5540);
294
295 if ips.is_empty() {
296 anyhow::bail!("discovered device with discriminator {} but no IPs returned", discriminator);
297 }
298
299 let mut last_err = anyhow::anyhow!("no IPs to try");
300 for ip in &ips {
301 let address = if ip.is_ipv6() {
302 format!("[{}]:{}", ip, port)
303 } else {
304 format!("{}:{}", ip, port)
305 };
306 match self.commission_at(&address, passcode, node_id, name, mrp_ms).await {
307 Ok(conn) => return Ok(conn),
308 Err(e) => {
309 log::debug!("Commission attempt at {} failed: {}", address, e);
310 last_err = e;
311 }
312 }
313 }
314 Err(last_err).context(format!("commissioning failed on all IPs for discriminator {}", discriminator))
315 }
316
317 #[cfg(feature = "ble")]
326 pub async fn commission_ble_with_code(
327 &self,
328 pairing_code: &str,
329 node_id: u64,
330 name: &str,
331 network_creds: crate::commission::NetworkCreds,
332 ) -> Result<controller::Connection> {
333 let info = if pairing_code.starts_with("MT:") || pairing_code.starts_with("mt:") {
334 crate::onboarding::decode_qr_payload(pairing_code)
335 } else {
336 crate::onboarding::decode_manual_pairing_code(pairing_code)
337 }
338 .context("decoding pairing code")?;
339
340 let connection = self
341 .controller
342 .commission_ble(
343 info.discriminator,
344 info.is_short_discriminator,
345 info.passcode,
346 node_id,
347 self.config.controller_id,
348 network_creds,
349 &self.mdns,
350 )
351 .await?;
352
353 let device = crate::devman::Device {
354 node_id,
355 address: String::new(), name: name.to_owned(),
357 ..Default::default()
358 };
359 self.registry
360 .lock()
361 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
362 .add(device)?;
363
364 Ok(connection)
365 }
366
367 pub async fn discover_device(&self, node_id: u64, timeout: Duration) -> Result<String> {
371 let (address, _) = self.discover_device_info(node_id, timeout).await?;
372 Ok(address)
373 }
374
375 async fn discover_device_info(
378 &self,
379 node_id: u64,
380 timeout: Duration,
381 ) -> Result<(String, MatterDeviceInfo)> {
382 let ca_public_key = self.certmanager.get_ca_public_key()?;
383 let fabric = Fabric::new(self.config.fabric_id, 0, &ca_public_key, &self.certmanager.get_ipk_epoch_key());
384 let compressed = fabric.compressed().context("computing compressed fabric ID")?;
385 let instance_name = format!("{}-{:016X}", hex::encode_upper(&compressed), node_id);
386 let expected_target = format!("{}._matter._tcp.local.", instance_name);
387
388 log::info!("Operational discovery for instance {}...", instance_name);
389
390 let (_, matter_info) = discover::discover_one(
391 &self.mdns,
392 "_matter._tcp.local",
393 "_matter._tcp.local.",
394 timeout,
395 move |target, _| target == expected_target,
396 ).await.context(format!("operational discovery for node {}", node_id))?;
397
398 let ip = matter_info.ips.first()
399 .context(format!("discovered {} but no IPs in response", instance_name))?;
400 let port = matter_info.port.unwrap_or(5540);
401 let address = if ip.is_ipv6() {
402 format!("[{}]:{}", ip, port)
403 } else {
404 format!("{}:{}", ip, port)
405 };
406
407 self.update_device_address(node_id, &address)?;
408 if let Err(e) = self.registry
409 .lock()
410 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
411 .update_mrp(
412 node_id,
413 matter_info.session_idle_interval_ms,
414 matter_info.session_active_interval_ms,
415 matter_info.session_active_threshold_ms,
416 )
417 {
418 log::debug!("failed to persist MRP intervals for node {}: {}", node_id, e);
419 }
420 Ok((address, matter_info))
421 }
422
423 pub async fn discover_commissionable_devices(&self, timeout: Duration) -> Result<Vec<(String, MatterDeviceInfo)>> {
424 discover::discover_all(
425 &self.mdns,
426 "_matterc._udp.local",
427 "_matterc._udp.local.",
428 timeout,
429 ).await
430 }
431
432 pub fn list_devices(&self) -> Result<Vec<Device>> {
434 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
435 Ok(reg.list().to_vec())
436 }
437
438 pub fn get_device(&self, node_id: u64) -> Result<Option<Device>> {
440 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
441 Ok(reg.get(node_id).cloned())
442 }
443
444 pub fn get_device_by_name(&self, name: &str) -> Result<Option<Device>> {
446 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
447 Ok(reg.get_by_name(name).cloned())
448 }
449
450 pub fn remove_device(&self, node_id: u64) -> Result<()> {
452 self.registry
453 .lock()
454 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
455 .remove(node_id)
456 }
457
458 pub fn rename_device(&self, node_id: u64, name: &str) -> Result<()> {
460 self.registry
461 .lock()
462 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
463 .rename(node_id, name)
464 }
465
466 pub fn update_device_address(&self, node_id: u64, address: &str) -> Result<()> {
468 self.registry
469 .lock()
470 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
471 .update_address(node_id, address)
472 }
473
474 pub fn mdns(&self) -> &Arc<mdns2::MdnsService> {
476 &self.mdns
477 }
478
479 pub fn controller(&self) -> &Arc<controller::Controller> {
481 &self.controller
482 }
483
484 pub fn transport(&self) -> &Arc<transport::Transport> {
486 &self.transport
487 }
488
489 pub fn certmanager(&self) -> &Arc<dyn certmanager::CertManager> {
491 &self.certmanager
492 }
493
494 pub fn config(&self) -> &ManagerConfig {
496 &self.config
497 }
498
499 pub fn base_path(&self) -> &str {
501 &self.base_path
502 }
503}