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 scope_id = matter_info.scope_id;
293 let ips = matter_info.ips;
294 let port = matter_info.port.unwrap_or(5540);
295
296 if ips.is_empty() {
297 anyhow::bail!("discovered device with discriminator {} but no IPs returned", discriminator);
298 }
299
300 let mut last_err = anyhow::anyhow!("no IPs to try");
301 for ip in &ips {
302 let address = crate::discover::addr_string(ip, port, scope_id);
303 match self.commission_at(&address, passcode, node_id, name, mrp_ms).await {
304 Ok(conn) => return Ok(conn),
305 Err(e) => {
306 log::debug!("Commission attempt at {} failed: {}", address, e);
307 last_err = e;
308 }
309 }
310 }
311 Err(last_err).context(format!("commissioning failed on all IPs for discriminator {}", discriminator))
312 }
313
314 #[cfg(feature = "ble")]
323 pub async fn commission_ble_with_code(
324 &self,
325 pairing_code: &str,
326 node_id: u64,
327 name: &str,
328 network_creds: crate::commission::NetworkCreds,
329 ) -> Result<controller::Connection> {
330 let info = if pairing_code.starts_with("MT:") || pairing_code.starts_with("mt:") {
331 crate::onboarding::decode_qr_payload(pairing_code)
332 } else {
333 crate::onboarding::decode_manual_pairing_code(pairing_code)
334 }
335 .context("decoding pairing code")?;
336
337 let connection = self
338 .controller
339 .commission_ble(
340 info.discriminator,
341 info.is_short_discriminator,
342 info.passcode,
343 node_id,
344 self.config.controller_id,
345 network_creds,
346 &self.mdns,
347 )
348 .await?;
349
350 let device = crate::devman::Device {
351 node_id,
352 address: String::new(), name: name.to_owned(),
354 ..Default::default()
355 };
356 self.registry
357 .lock()
358 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
359 .add(device)?;
360
361 Ok(connection)
362 }
363
364 pub async fn discover_device(&self, node_id: u64, timeout: Duration) -> Result<String> {
368 let (address, _) = self.discover_device_info(node_id, timeout).await?;
369 Ok(address)
370 }
371
372 async fn discover_device_info(
375 &self,
376 node_id: u64,
377 timeout: Duration,
378 ) -> Result<(String, MatterDeviceInfo)> {
379 let ca_public_key = self.certmanager.get_ca_public_key()?;
380 let fabric = Fabric::new(self.config.fabric_id, 0, &ca_public_key, &self.certmanager.get_ipk_epoch_key());
381 let compressed = fabric.compressed().context("computing compressed fabric ID")?;
382 let instance_name = format!("{}-{:016X}", hex::encode_upper(&compressed), node_id);
383 let expected_target = format!("{}._matter._tcp.local.", instance_name);
384
385 log::info!("Operational discovery for instance {}...", instance_name);
386
387 let (_, matter_info) = discover::discover_one(
388 &self.mdns,
389 "_matter._tcp.local",
390 "_matter._tcp.local.",
391 timeout,
392 move |target, _| target == expected_target,
393 ).await.context(format!("operational discovery for node {}", node_id))?;
394
395 let ip = matter_info.ips.first()
396 .context(format!("discovered {} but no IPs in response", instance_name))?;
397 let port = matter_info.port.unwrap_or(5540);
398 let address = crate::discover::addr_string(ip, port, matter_info.scope_id);
399
400 self.update_device_address(node_id, &address)?;
401 if let Err(e) = self.registry
402 .lock()
403 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
404 .update_mrp(
405 node_id,
406 matter_info.session_idle_interval_ms,
407 matter_info.session_active_interval_ms,
408 matter_info.session_active_threshold_ms,
409 )
410 {
411 log::debug!("failed to persist MRP intervals for node {}: {}", node_id, e);
412 }
413 Ok((address, matter_info))
414 }
415
416 pub async fn discover_commissionable_devices(&self, timeout: Duration) -> Result<Vec<(String, MatterDeviceInfo)>> {
417 discover::discover_all(
418 &self.mdns,
419 "_matterc._udp.local",
420 "_matterc._udp.local.",
421 timeout,
422 ).await
423 }
424
425 pub fn list_devices(&self) -> Result<Vec<Device>> {
427 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
428 Ok(reg.list().to_vec())
429 }
430
431 pub fn get_device(&self, node_id: u64) -> Result<Option<Device>> {
433 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
434 Ok(reg.get(node_id).cloned())
435 }
436
437 pub fn get_device_by_name(&self, name: &str) -> Result<Option<Device>> {
439 let reg = self.registry.lock().map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
440 Ok(reg.get_by_name(name).cloned())
441 }
442
443 pub fn remove_device(&self, node_id: u64) -> Result<()> {
445 self.registry
446 .lock()
447 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
448 .remove(node_id)
449 }
450
451 pub fn rename_device(&self, node_id: u64, name: &str) -> Result<()> {
453 self.registry
454 .lock()
455 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
456 .rename(node_id, name)
457 }
458
459 pub fn update_device_address(&self, node_id: u64, address: &str) -> Result<()> {
461 self.registry
462 .lock()
463 .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?
464 .update_address(node_id, address)
465 }
466
467 pub fn mdns(&self) -> &Arc<mdns2::MdnsService> {
469 &self.mdns
470 }
471
472 pub fn controller(&self) -> &Arc<controller::Controller> {
474 &self.controller
475 }
476
477 pub fn transport(&self) -> &Arc<transport::Transport> {
479 &self.transport
480 }
481
482 pub fn certmanager(&self) -> &Arc<dyn certmanager::CertManager> {
484 &self.certmanager
485 }
486
487 pub fn config(&self) -> &ManagerConfig {
489 &self.config
490 }
491
492 pub fn base_path(&self) -> &str {
494 &self.base_path
495 }
496}