1use super::transport::Transport;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum DeviceRuntime {
20 MicroPython,
22 CircuitPython,
24 Arduino,
26 Nucleus,
28 Linux,
30 Aardvark,
32}
33
34impl DeviceRuntime {
35 pub fn from_kind(kind: &DeviceKind) -> Self {
37 match kind {
38 DeviceKind::Pico | DeviceKind::Esp32 | DeviceKind::Generic => Self::MicroPython,
39 DeviceKind::Arduino => Self::Arduino,
40 DeviceKind::Nucleo => Self::Nucleus,
41 DeviceKind::Aardvark => Self::Aardvark,
42 }
43 }
44}
45
46impl std::fmt::Display for DeviceRuntime {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::MicroPython => write!(f, "MicroPython"),
50 Self::CircuitPython => write!(f, "CircuitPython"),
51 Self::Arduino => write!(f, "Arduino"),
52 Self::Nucleus => write!(f, "Nucleus"),
53 Self::Linux => write!(f, "Linux"),
54 Self::Aardvark => write!(f, "Aardvark"),
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum DeviceKind {
67 Pico,
69 Arduino,
71 Esp32,
73 Nucleo,
75 Generic,
77 Aardvark,
79}
80
81impl DeviceKind {
82 pub fn from_vid(vid: u16) -> Option<Self> {
85 match vid {
86 0x2e8a => Some(Self::Pico),
87 0x2341 => Some(Self::Arduino),
88 0x10c4 => Some(Self::Esp32),
89 0x0483 => Some(Self::Nucleo),
90 0x2b76 => Some(Self::Aardvark),
91 _ => None,
92 }
93 }
94}
95
96impl std::fmt::Display for DeviceKind {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 match self {
99 Self::Pico => write!(f, "pico"),
100 Self::Arduino => write!(f, "arduino"),
101 Self::Esp32 => write!(f, "esp32"),
102 Self::Nucleo => write!(f, "nucleo"),
103 Self::Generic => write!(f, "generic"),
104 Self::Aardvark => write!(f, "aardvark"),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Default)]
114#[allow(clippy::struct_excessive_bools)]
115pub struct DeviceCapabilities {
116 pub gpio: bool,
117 pub i2c: bool,
118 pub spi: bool,
119 pub swd: bool,
120 pub uart: bool,
121 pub adc: bool,
122 pub pwm: bool,
123}
124
125#[derive(Debug, Clone)]
127pub struct Device {
128 pub alias: String,
130 pub board_name: String,
132 pub kind: DeviceKind,
134 pub runtime: DeviceRuntime,
136 pub vid: Option<u16>,
138 pub pid: Option<u16>,
140 pub device_path: Option<String>,
143 pub architecture: Option<String>,
145 pub firmware: Option<String>,
147}
148
149impl Device {
150 pub fn port(&self) -> Option<&str> {
152 self.device_path.as_deref()
153 }
154}
155
156pub struct DeviceContext {
161 pub device: Arc<Device>,
163 pub transport: Arc<dyn Transport>,
165 pub capabilities: DeviceCapabilities,
167}
168
169struct RegisteredDevice {
171 device: Arc<Device>,
172 transport: Option<Arc<dyn Transport>>,
173 capabilities: DeviceCapabilities,
174}
175
176pub const NO_HW_DEVICES_SUMMARY: &str = "No hardware devices connected.";
180
181pub struct DeviceRegistry {
188 devices: HashMap<String, RegisteredDevice>,
189 alias_counters: HashMap<String, u32>,
190}
191
192impl DeviceRegistry {
193 pub fn new() -> Self {
195 Self {
196 devices: HashMap::new(),
197 alias_counters: HashMap::new(),
198 }
199 }
200
201 pub fn register(
205 &mut self,
206 board_name: &str,
207 vid: Option<u16>,
208 pid: Option<u16>,
209 device_path: Option<String>,
210 architecture: Option<String>,
211 ) -> String {
212 let prefix = alias_prefix(board_name);
213 let counter = self.alias_counters.entry(prefix.clone()).or_insert(0);
214 let alias = format!("{}{}", prefix, counter);
215 *counter += 1;
216
217 let kind = vid
218 .and_then(DeviceKind::from_vid)
219 .unwrap_or(DeviceKind::Generic);
220 let runtime = DeviceRuntime::from_kind(&kind);
221
222 let device = Arc::new(Device {
223 alias: alias.clone(),
224 board_name: board_name.to_string(),
225 kind,
226 runtime,
227 vid,
228 pid,
229 device_path,
230 architecture,
231 firmware: None,
232 });
233
234 self.devices.insert(
235 alias.clone(),
236 RegisteredDevice {
237 device,
238 transport: None,
239 capabilities: DeviceCapabilities::default(),
240 },
241 );
242
243 alias
244 }
245
246 pub fn attach_transport(
251 &mut self,
252 alias: &str,
253 transport: Arc<dyn Transport>,
254 capabilities: DeviceCapabilities,
255 ) -> anyhow::Result<()> {
256 if let Some(entry) = self.devices.get_mut(alias) {
257 entry.transport = Some(transport);
258 entry.capabilities = capabilities;
259 Ok(())
260 } else {
261 ::zeroclaw_log::record!(
262 WARN,
263 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
264 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
265 .with_attrs(::serde_json::json!({"device_alias": alias})),
266 "device registry attach refused: unknown alias"
267 );
268 Err(anyhow::Error::msg(format!("unknown device alias: {alias}")))
269 }
270 }
271
272 pub fn get_device(&self, alias: &str) -> Option<Arc<Device>> {
274 self.devices.get(alias).map(|e| e.device.clone())
275 }
276
277 pub fn context(&self, alias: &str) -> Option<DeviceContext> {
281 self.devices.get(alias).and_then(|e| {
282 e.transport.as_ref().map(|t| DeviceContext {
283 device: e.device.clone(),
284 transport: t.clone(),
285 capabilities: e.capabilities.clone(),
286 })
287 })
288 }
289
290 pub fn aliases(&self) -> Vec<&str> {
292 self.devices.keys().map(|s| s.as_str()).collect()
293 }
294
295 pub fn prompt_summary(&self) -> String {
297 if self.devices.is_empty() {
298 return NO_HW_DEVICES_SUMMARY.to_string();
299 }
300
301 let mut lines = vec!["Connected devices:".to_string()];
302 let mut sorted_aliases: Vec<&String> = self.devices.keys().collect();
303 sorted_aliases.sort();
304 for alias in sorted_aliases {
305 let entry = &self.devices[alias];
306 let status = entry
307 .transport
308 .as_ref()
309 .map(|t| {
310 if t.is_connected() {
311 "connected"
312 } else {
313 "disconnected"
314 }
315 })
316 .unwrap_or("no transport");
317 let arch = entry
318 .device
319 .architecture
320 .as_deref()
321 .unwrap_or("unknown arch");
322 lines.push(format!(
323 " {} — {} ({}) [{}]",
324 alias, entry.device.board_name, arch, status
325 ));
326 }
327 lines.join("\n")
328 }
329
330 pub fn resolve_gpio_device(
339 &self,
340 args: &serde_json::Value,
341 ) -> Result<(String, DeviceContext), String> {
342 let device_alias: String = match args.get("device").and_then(|v| v.as_str()) {
343 Some(a) => a.to_string(),
344 None => {
345 let gpio_aliases: Vec<String> = self
346 .aliases()
347 .into_iter()
348 .filter(|a| {
349 self.context(a)
350 .map(|c| c.capabilities.gpio)
351 .unwrap_or(false)
352 })
353 .map(|a| a.to_string())
354 .collect();
355 match gpio_aliases.as_slice() {
356 [single] => single.clone(),
357 [] => {
358 return Err("no GPIO-capable device found; specify \"device\" parameter"
359 .to_string());
360 }
361 _ => {
362 return Err(format!(
363 "multiple devices available ({}); specify \"device\" parameter",
364 gpio_aliases.join(", ")
365 ));
366 }
367 }
368 }
369 };
370
371 let ctx = self.context(&device_alias).ok_or_else(|| {
372 format!(
373 "device '{}' not found or has no transport attached",
374 device_alias
375 )
376 })?;
377
378 if !ctx.capabilities.gpio {
380 return Err(format!(
381 "device '{}' does not support GPIO; specify a GPIO-capable device",
382 device_alias
383 ));
384 }
385
386 Ok((device_alias, ctx))
387 }
388
389 pub fn has_aardvark(&self) -> bool {
391 self.devices
392 .values()
393 .any(|e| e.device.kind == DeviceKind::Aardvark)
394 }
395
396 pub fn resolve_aardvark_device(
405 &self,
406 args: &serde_json::Value,
407 ) -> Result<(String, DeviceContext), String> {
408 let device_alias: String = match args.get("device").and_then(|v| v.as_str()) {
409 Some(a) => a.to_string(),
410 None => {
411 let aardvark_aliases: Vec<String> = self
412 .aliases()
413 .into_iter()
414 .filter(|a| {
415 self.devices
416 .get(*a)
417 .map(|e| e.device.kind == DeviceKind::Aardvark)
418 .unwrap_or(false)
419 })
420 .map(|a| a.to_string())
421 .collect();
422 match aardvark_aliases.as_slice() {
423 [single] => single.clone(),
424 [] => {
425 return Err("no Aardvark adapter found; is it plugged in?".to_string());
426 }
427 _ => {
428 return Err(format!(
429 "multiple Aardvark adapters available ({}); \
430 specify \"device\" parameter",
431 aardvark_aliases.join(", ")
432 ));
433 }
434 }
435 }
436 };
437
438 let ctx = self.context(&device_alias).ok_or_else(|| {
439 format!("device '{device_alias}' not found or has no transport attached")
440 })?;
441
442 Ok((device_alias, ctx))
443 }
444
445 pub fn len(&self) -> usize {
447 self.devices.len()
448 }
449
450 pub fn is_empty(&self) -> bool {
452 self.devices.is_empty()
453 }
454
455 pub fn get(&self, alias: &str) -> Option<Arc<Device>> {
457 self.get_device(alias)
458 }
459
460 pub fn all(&self) -> Vec<Arc<Device>> {
462 self.devices.values().map(|e| e.device.clone()).collect()
463 }
464
465 pub fn summary(&self) -> String {
469 if self.devices.is_empty() {
470 return String::new();
471 }
472 let mut lines: Vec<String> = self
473 .devices
474 .values()
475 .map(|e| {
476 let path = e.device.port().unwrap_or("(native)");
477 format!("{}: {} {}", e.device.alias, e.device.board_name, path)
478 })
479 .collect();
480 lines.sort(); lines.join("\n")
482 }
483
484 #[cfg(feature = "hardware")]
496 pub async fn discover() -> Self {
497 use super::{
498 discover::scan_serial_devices,
499 serial::{DEFAULT_BAUD, HardwareSerialTransport},
500 };
501
502 let mut registry = Self::new();
503
504 for info in scan_serial_devices() {
505 let is_known_vid = info.vid != 0;
506
507 let probe_transport = if !is_known_vid {
511 let probe = HardwareSerialTransport::new(&info.port_path, DEFAULT_BAUD);
512 if !probe.ping_handshake().await {
513 ::zeroclaw_log::record!(
514 DEBUG,
515 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
516 .with_attrs(::serde_json::json!({"port": info.port_path})),
517 "skipping unknown device: no ZeroClaw firmware response"
518 );
519 continue;
520 }
521 Some(probe)
522 } else {
523 None
524 };
525
526 let board_name = info.board_name.as_deref().unwrap_or("unknown").to_string();
527
528 let alias = registry.register(
529 &board_name,
530 if info.vid != 0 { Some(info.vid) } else { None },
531 if info.pid != 0 { Some(info.pid) } else { None },
532 Some(info.port_path.clone()),
533 info.architecture,
534 );
535
536 let transport: Arc<dyn super::transport::Transport> =
540 if let Some(probe) = probe_transport {
541 Arc::new(probe)
542 } else {
543 Arc::new(HardwareSerialTransport::new(&info.port_path, DEFAULT_BAUD))
544 };
545 let caps = DeviceCapabilities {
546 gpio: true, ..DeviceCapabilities::default()
548 };
549 registry
550 .attach_transport(&alias, transport, caps)
551 .unwrap_or_else(|e| {
552 ::zeroclaw_log::record!(
553 WARN,
554 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
555 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
556 .with_attrs(
557 ::serde_json::json!({"alias": alias, "err": e.to_string()})
558 ),
559 "attach_transport: unexpected unknown alias"
560 )
561 });
562
563 ::zeroclaw_log::record!(INFO, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(::serde_json::json!({"alias": alias, "port": info.port_path, "vid": info.vid})), "device registered");
564 }
565
566 registry
567 }
568}
569
570impl DeviceRegistry {
571 #[cfg(feature = "hardware")]
580 pub async fn reconnect(&mut self, alias: &str, new_port: Option<&str>) -> anyhow::Result<()> {
581 use super::serial::{DEFAULT_BAUD, HardwareSerialTransport};
582
583 let entry = self.devices.get_mut(alias).ok_or_else(|| {
584 ::zeroclaw_log::record!(
585 WARN,
586 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
587 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
588 .with_attrs(::serde_json::json!({"device_alias": alias})),
589 "device registry reconnect refused: unknown alias"
590 );
591 anyhow::Error::msg(format!("unknown device alias: {alias}"))
592 })?;
593
594 let port_path = match new_port {
596 Some(p) => {
597 let mut updated = (*entry.device).clone();
599 updated.device_path = Some(p.to_string());
600 entry.device = Arc::new(updated);
601 p.to_string()
602 }
603 None => entry.device.device_path.clone().ok_or_else(|| {
604 ::zeroclaw_log::record!(
605 WARN,
606 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
607 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
608 .with_attrs(::serde_json::json!({"device_alias": alias})),
609 "device registry reconnect refused: no recorded port path"
610 );
611 anyhow::Error::msg(format!("device {alias} has no port path"))
612 })?,
613 };
614
615 entry.transport = None;
617
618 let transport = HardwareSerialTransport::new(&port_path, DEFAULT_BAUD);
620 if !transport.ping_handshake().await {
621 anyhow::bail!(
622 "ping handshake failed after reconnect on {port_path} — \
623 firmware may not be running"
624 );
625 }
626
627 entry.transport = Some(Arc::new(transport) as Arc<dyn super::transport::Transport>);
628 entry.capabilities.gpio = true;
629
630 ::zeroclaw_log::record!(
631 INFO,
632 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
633 .with_attrs(::serde_json::json!({"alias": alias, "port": port_path})),
634 "device reconnected"
635 );
636 Ok(())
637 }
638}
639
640impl Default for DeviceRegistry {
641 fn default() -> Self {
642 Self::new()
643 }
644}
645
646fn alias_prefix(board_name: &str) -> String {
648 match board_name {
649 s if s.starts_with("raspberry-pi-pico") || s.starts_with("pico") => "pico".to_string(),
650 s if s.starts_with("arduino") => "arduino".to_string(),
651 s if s.starts_with("esp32") || s.starts_with("esp") => "esp".to_string(),
652 s if s.starts_with("nucleo") || s.starts_with("stm32") => "nucleo".to_string(),
653 s if s.starts_with("rpi") || s == "raspberry-pi" => "rpi".to_string(),
654 _ => "device".to_string(),
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn alias_prefix_pico_variants() {
664 assert_eq!(alias_prefix("raspberry-pi-pico"), "pico");
665 assert_eq!(alias_prefix("pico-w"), "pico");
666 assert_eq!(alias_prefix("pico"), "pico");
667 }
668
669 #[test]
670 fn alias_prefix_arduino() {
671 assert_eq!(alias_prefix("arduino-uno"), "arduino");
672 assert_eq!(alias_prefix("arduino-mega"), "arduino");
673 }
674
675 #[test]
676 fn alias_prefix_esp() {
677 assert_eq!(alias_prefix("esp32"), "esp");
678 assert_eq!(alias_prefix("esp32-s3"), "esp");
679 }
680
681 #[test]
682 fn alias_prefix_nucleo() {
683 assert_eq!(alias_prefix("nucleo-f401re"), "nucleo");
684 assert_eq!(alias_prefix("stm32-discovery"), "nucleo");
685 }
686
687 #[test]
688 fn alias_prefix_rpi() {
689 assert_eq!(alias_prefix("rpi-gpio"), "rpi");
690 assert_eq!(alias_prefix("raspberry-pi"), "rpi");
691 }
692
693 #[test]
694 fn alias_prefix_unknown() {
695 assert_eq!(alias_prefix("custom-board"), "device");
696 }
697
698 #[test]
699 fn registry_assigns_sequential_aliases() {
700 let mut reg = DeviceRegistry::new();
701 let a1 = reg.register("raspberry-pi-pico", Some(0x2E8A), Some(0x000A), None, None);
702 let a2 = reg.register("raspberry-pi-pico", Some(0x2E8A), Some(0x000A), None, None);
703 let a3 = reg.register("arduino-uno", Some(0x2341), Some(0x0043), None, None);
704
705 assert_eq!(a1, "pico0");
706 assert_eq!(a2, "pico1");
707 assert_eq!(a3, "arduino0");
708 assert_eq!(reg.len(), 3);
709 }
710
711 #[test]
712 fn registry_get_device_by_alias() {
713 let mut reg = DeviceRegistry::new();
714 let alias = reg.register(
715 "nucleo-f401re",
716 Some(0x0483),
717 Some(0x374B),
718 Some("/dev/ttyACM0".to_string()),
719 Some("ARM Cortex-M4".to_string()),
720 );
721
722 let device = reg.get_device(&alias).unwrap();
723 assert_eq!(device.alias, "nucleo0");
724 assert_eq!(device.board_name, "nucleo-f401re");
725 assert_eq!(device.vid, Some(0x0483));
726 assert_eq!(device.architecture.as_deref(), Some("ARM Cortex-M4"));
727 }
728
729 #[test]
730 fn registry_unknown_alias_returns_none() {
731 let reg = DeviceRegistry::new();
732 assert!(reg.get_device("nonexistent").is_none());
733 assert!(reg.context("nonexistent").is_none());
734 }
735
736 #[test]
737 fn registry_context_none_without_transport() {
738 let mut reg = DeviceRegistry::new();
739 let alias = reg.register("pico", None, None, None, None);
740 assert!(reg.context(&alias).is_none());
742 }
743
744 #[test]
745 fn registry_prompt_summary_empty() {
746 let reg = DeviceRegistry::new();
747 assert_eq!(reg.prompt_summary(), NO_HW_DEVICES_SUMMARY);
748 }
749
750 #[test]
751 fn registry_prompt_summary_with_devices() {
752 let mut reg = DeviceRegistry::new();
753 reg.register(
754 "raspberry-pi-pico",
755 Some(0x2E8A),
756 None,
757 None,
758 Some("ARM Cortex-M0+".to_string()),
759 );
760 let summary = reg.prompt_summary();
761 assert!(summary.contains("pico0"));
762 assert!(summary.contains("raspberry-pi-pico"));
763 assert!(summary.contains("ARM Cortex-M0+"));
764 assert!(summary.contains("no transport"));
765 }
766
767 #[test]
768 fn device_capabilities_default_all_false() {
769 let caps = DeviceCapabilities::default();
770 assert!(!caps.gpio);
771 assert!(!caps.i2c);
772 assert!(!caps.spi);
773 assert!(!caps.swd);
774 assert!(!caps.uart);
775 assert!(!caps.adc);
776 assert!(!caps.pwm);
777 }
778
779 #[test]
780 fn registry_default_is_empty() {
781 let reg = DeviceRegistry::default();
782 assert!(reg.is_empty());
783 assert_eq!(reg.len(), 0);
784 }
785
786 #[test]
787 fn registry_aliases_returns_all() {
788 let mut reg = DeviceRegistry::new();
789 reg.register("pico", None, None, None, None);
790 reg.register("arduino-uno", None, None, None, None);
791 let mut aliases = reg.aliases();
792 aliases.sort_unstable();
793 assert_eq!(aliases, vec!["arduino0", "pico0"]);
794 }
795
796 #[test]
799 fn device_kind_from_vid_known() {
800 assert_eq!(DeviceKind::from_vid(0x2e8a), Some(DeviceKind::Pico));
801 assert_eq!(DeviceKind::from_vid(0x2341), Some(DeviceKind::Arduino));
802 assert_eq!(DeviceKind::from_vid(0x10c4), Some(DeviceKind::Esp32));
803 assert_eq!(DeviceKind::from_vid(0x0483), Some(DeviceKind::Nucleo));
804 }
805
806 #[test]
807 fn device_kind_from_vid_unknown() {
808 assert_eq!(DeviceKind::from_vid(0x0000), None);
809 assert_eq!(DeviceKind::from_vid(0xffff), None);
810 }
811
812 #[test]
813 fn device_kind_display() {
814 assert_eq!(DeviceKind::Pico.to_string(), "pico");
815 assert_eq!(DeviceKind::Arduino.to_string(), "arduino");
816 assert_eq!(DeviceKind::Esp32.to_string(), "esp32");
817 assert_eq!(DeviceKind::Nucleo.to_string(), "nucleo");
818 assert_eq!(DeviceKind::Generic.to_string(), "generic");
819 }
820
821 #[test]
822 fn register_sets_kind_from_vid() {
823 let mut reg = DeviceRegistry::new();
824 let a = reg.register("raspberry-pi-pico", Some(0x2e8a), Some(0x000a), None, None);
825 assert_eq!(reg.get(&a).unwrap().kind, DeviceKind::Pico);
826
827 let b = reg.register("arduino-uno", Some(0x2341), Some(0x0043), None, None);
828 assert_eq!(reg.get(&b).unwrap().kind, DeviceKind::Arduino);
829
830 let c = reg.register("unknown-device", None, None, None, None);
831 assert_eq!(reg.get(&c).unwrap().kind, DeviceKind::Generic);
832 }
833
834 #[test]
835 fn device_port_returns_device_path() {
836 let mut reg = DeviceRegistry::new();
837 let alias = reg.register(
838 "raspberry-pi-pico",
839 Some(0x2e8a),
840 None,
841 Some("/dev/ttyACM0".to_string()),
842 None,
843 );
844 let device = reg.get(&alias).unwrap();
845 assert_eq!(device.port(), Some("/dev/ttyACM0"));
846 }
847
848 #[test]
849 fn device_port_none_without_path() {
850 let mut reg = DeviceRegistry::new();
851 let alias = reg.register("pico", None, None, None, None);
852 assert!(reg.get(&alias).unwrap().port().is_none());
853 }
854
855 #[test]
856 fn registry_get_is_alias_for_get_device() {
857 let mut reg = DeviceRegistry::new();
858 let alias = reg.register("raspberry-pi-pico", Some(0x2e8a), None, None, None);
859 let via_get = reg.get(&alias);
860 let via_get_device = reg.get_device(&alias);
861 assert!(via_get.is_some());
862 assert!(via_get_device.is_some());
863 assert_eq!(via_get.unwrap().alias, via_get_device.unwrap().alias);
864 }
865
866 #[test]
867 fn registry_all_returns_every_device() {
868 let mut reg = DeviceRegistry::new();
869 reg.register("raspberry-pi-pico", Some(0x2e8a), None, None, None);
870 reg.register("arduino-uno", Some(0x2341), None, None, None);
871 assert_eq!(reg.all().len(), 2);
872 }
873
874 #[test]
875 fn registry_summary_one_liner_per_device() {
876 let mut reg = DeviceRegistry::new();
877 reg.register(
878 "raspberry-pi-pico",
879 Some(0x2e8a),
880 None,
881 Some("/dev/ttyACM0".to_string()),
882 None,
883 );
884 let s = reg.summary();
885 assert!(s.contains("pico0"));
886 assert!(s.contains("raspberry-pi-pico"));
887 assert!(s.contains("/dev/ttyACM0"));
888 }
889
890 #[test]
891 fn registry_summary_empty_when_no_devices() {
892 let reg = DeviceRegistry::new();
893 assert_eq!(reg.summary(), "");
894 }
895}