1use std::any::Any;
7#[cfg(feature = "lasercube-wifi")]
8use std::io;
9use std::net::IpAddr;
10#[cfg(any(feature = "ether-dream", feature = "idn", feature = "lasercube-wifi"))]
11use std::time::Duration;
12
13use crate::backend::{Error, Result, StreamBackend};
14use crate::types::{DacType, EnabledDacTypes};
15
16pub trait ExternalDiscoverer: Send {
53 fn dac_type(&self) -> DacType;
55
56 fn scan(&mut self) -> Vec<ExternalDevice>;
58
59 fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>>;
62}
63
64pub struct ExternalDevice {
70 pub ip_address: Option<IpAddr>,
72 pub mac_address: Option<[u8; 6]>,
74 pub hostname: Option<String>,
76 pub usb_address: Option<String>,
78 pub hardware_name: Option<String>,
80 pub device_index: Option<u16>,
82 pub opaque_data: Box<dyn Any + Send>,
85}
86
87impl ExternalDevice {
88 pub fn new<T: Any + Send + 'static>(opaque_data: T) -> Self {
92 Self {
93 ip_address: None,
94 mac_address: None,
95 hostname: None,
96 usb_address: None,
97 hardware_name: None,
98 device_index: None,
99 opaque_data: Box::new(opaque_data),
100 }
101 }
102}
103
104#[cfg(feature = "helios")]
107use crate::backend::HeliosBackend;
108#[cfg(feature = "helios")]
109use crate::protocols::helios::{HeliosDac, HeliosDacController};
110
111#[cfg(feature = "ether-dream")]
112use crate::backend::EtherDreamBackend;
113#[cfg(feature = "ether-dream")]
114use crate::protocols::ether_dream::protocol::DacBroadcast as EtherDreamBroadcast;
115#[cfg(feature = "ether-dream")]
116use crate::protocols::ether_dream::recv_dac_broadcasts;
117
118#[cfg(feature = "idn")]
119use crate::backend::IdnBackend;
120#[cfg(feature = "idn")]
121use crate::protocols::idn::dac::ServerInfo as IdnServerInfo;
122#[cfg(feature = "idn")]
123use crate::protocols::idn::dac::ServiceInfo as IdnServiceInfo;
124#[cfg(feature = "idn")]
125use crate::protocols::idn::scan_for_servers;
126#[cfg(all(feature = "idn", feature = "testutils"))]
127use crate::protocols::idn::ServerScanner;
128#[cfg(all(feature = "idn", feature = "testutils"))]
129use std::net::SocketAddr;
130
131#[cfg(feature = "lasercube-wifi")]
132use crate::backend::LasercubeWifiBackend;
133#[cfg(feature = "lasercube-wifi")]
134use crate::protocols::lasercube_wifi::dac::Addressed as LasercubeAddressed;
135#[cfg(feature = "lasercube-wifi")]
136use crate::protocols::lasercube_wifi::discover_dacs as discover_lasercube_wifi;
137#[cfg(feature = "lasercube-wifi")]
138use crate::protocols::lasercube_wifi::protocol::DeviceInfo as LasercubeDeviceInfo;
139
140#[cfg(feature = "lasercube-usb")]
141use crate::backend::LasercubeUsbBackend;
142#[cfg(feature = "lasercube-usb")]
143use crate::protocols::lasercube_usb::rusb;
144#[cfg(feature = "lasercube-usb")]
145use crate::protocols::lasercube_usb::DacController as LasercubeUsbController;
146
147#[cfg(feature = "avb")]
148use crate::backend::AvbBackend;
149#[cfg(feature = "avb")]
150use crate::protocols::avb::{discover_device_selectors as discover_avb_selectors, AvbSelector};
151
152pub struct DiscoveredDevice {
160 dac_type: DacType,
161 ip_address: Option<IpAddr>,
162 mac_address: Option<[u8; 6]>,
163 hostname: Option<String>,
164 usb_address: Option<String>,
165 hardware_name: Option<String>,
166 device_index: Option<u16>,
167 inner: DiscoveredDeviceInner,
168}
169
170impl DiscoveredDevice {
171 fn new(dac_type: DacType, inner: DiscoveredDeviceInner) -> Self {
175 Self {
176 dac_type,
177 ip_address: None,
178 mac_address: None,
179 hostname: None,
180 usb_address: None,
181 hardware_name: None,
182 device_index: None,
183 inner,
184 }
185 }
186
187 pub fn name(&self) -> String {
191 self.info().name()
192 }
193
194 pub fn dac_type(&self) -> DacType {
196 self.dac_type.clone()
197 }
198
199 pub fn info(&self) -> DiscoveredDeviceInfo {
201 DiscoveredDeviceInfo {
202 dac_type: self.dac_type.clone(),
203 ip_address: self.ip_address,
204 mac_address: self.mac_address,
205 hostname: self.hostname.clone(),
206 usb_address: self.usb_address.clone(),
207 hardware_name: self.hardware_name.clone(),
208 device_index: self.device_index,
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Hash)]
218pub struct DiscoveredDeviceInfo {
219 pub dac_type: DacType,
221 pub ip_address: Option<IpAddr>,
223 pub mac_address: Option<[u8; 6]>,
225 pub hostname: Option<String>,
227 pub usb_address: Option<String>,
229 pub hardware_name: Option<String>,
231 pub device_index: Option<u16>,
233}
234
235impl DiscoveredDeviceInfo {
236 pub fn name(&self) -> String {
240 self.ip_address
241 .map(|ip| ip.to_string())
242 .or_else(|| self.hardware_name.clone())
243 .or_else(|| self.usb_address.clone())
244 .unwrap_or_else(|| "Unknown".into())
245 }
246
247 pub fn stable_id(&self) -> String {
259 match &self.dac_type {
260 DacType::EtherDream => {
261 if let Some(mac) = self.mac_address {
262 return format!(
263 "etherdream:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
264 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
265 );
266 }
267 if let Some(ip) = self.ip_address {
268 return format!("etherdream:{}", ip);
269 }
270 }
271 DacType::Idn => {
272 if let Some(ref hostname) = self.hostname {
273 return format!("idn:{}", hostname);
274 }
275 if let Some(ip) = self.ip_address {
276 return format!("idn:{}", ip);
277 }
278 }
279 DacType::Helios => {
280 if let Some(ref hw_name) = self.hardware_name {
281 return format!("helios:{}", hw_name);
282 }
283 if let Some(ref usb_addr) = self.usb_address {
284 return format!("helios:{}", usb_addr);
285 }
286 }
287 DacType::LasercubeUsb => {
288 if let Some(ref hw_name) = self.hardware_name {
289 return format!("lasercube-usb:{}", hw_name);
290 }
291 if let Some(ref usb_addr) = self.usb_address {
292 return format!("lasercube-usb:{}", usb_addr);
293 }
294 }
295 DacType::LasercubeWifi => {
296 if let Some(ip) = self.ip_address {
297 return format!("lasercube-wifi:{}", ip);
298 }
299 }
300 DacType::Avb => {
301 if let Some(ref hw_name) = self.hardware_name {
302 let slug = slugify_device_id(hw_name);
303 if let Some(index) = self.device_index {
304 return format!("avb:{}:{}", slug, index);
305 }
306 return format!("avb:{}", slug);
307 }
308 }
309 DacType::Custom(name) => {
310 if let Some(ip) = self.ip_address {
312 return format!("{}:{}", name.to_lowercase(), ip);
313 }
314 if let Some(ref hw_name) = self.hardware_name {
315 return format!("{}:{}", name.to_lowercase(), hw_name);
316 }
317 }
318 }
319
320 format!("unknown:{:?}", self.dac_type)
322 }
323}
324
325fn slugify_device_id(name: &str) -> String {
326 let normalized = name
327 .split_whitespace()
328 .collect::<Vec<_>>()
329 .join(" ")
330 .trim()
331 .to_ascii_lowercase();
332 let mut slug = String::with_capacity(normalized.len());
333 let mut prev_dash = false;
334
335 for ch in normalized.chars() {
336 if ch.is_ascii_alphanumeric() {
337 slug.push(ch);
338 prev_dash = false;
339 } else if !prev_dash {
340 slug.push('-');
341 prev_dash = true;
342 }
343 }
344
345 slug.trim_matches('-').to_string()
346}
347
348enum DiscoveredDeviceInner {
350 #[cfg(feature = "helios")]
351 Helios(HeliosDac),
352 #[cfg(feature = "ether-dream")]
353 EtherDream {
354 broadcast: EtherDreamBroadcast,
355 ip: IpAddr,
356 },
357 #[cfg(feature = "idn")]
358 Idn {
359 server: IdnServerInfo,
360 service: IdnServiceInfo,
361 },
362 #[cfg(feature = "lasercube-wifi")]
363 LasercubeWifi {
364 info: LasercubeDeviceInfo,
365 source_addr: std::net::SocketAddr,
366 },
367 #[cfg(feature = "lasercube-usb")]
368 LasercubeUsb(rusb::Device<rusb::Context>),
369 #[cfg(feature = "avb")]
370 Avb(AvbSelector),
371 External {
373 discoverer_index: usize,
375 opaque_data: Box<dyn Any + Send>,
377 },
378 #[cfg(not(any(
380 feature = "helios",
381 feature = "ether-dream",
382 feature = "idn",
383 feature = "lasercube-wifi",
384 feature = "lasercube-usb",
385 feature = "avb"
386 )))]
387 _Placeholder,
388}
389
390#[cfg(feature = "helios")]
396pub struct HeliosDiscovery {
397 controller: HeliosDacController,
398}
399
400#[cfg(feature = "helios")]
401impl HeliosDiscovery {
402 pub fn new() -> Option<Self> {
406 HeliosDacController::new()
407 .ok()
408 .map(|controller| Self { controller })
409 }
410
411 pub fn scan(&self) -> Vec<DiscoveredDevice> {
413 let Ok(devices) = self.controller.list_devices() else {
414 return Vec::new();
415 };
416
417 let mut discovered = Vec::new();
418 for device in devices {
419 let HeliosDac::Idle(_) = &device else {
421 continue;
422 };
423
424 let opened = match device.open() {
426 Ok(o) => o,
427 Err(_) => continue,
428 };
429
430 let hardware_name = opened.name().unwrap_or_else(|_| "Unknown Helios".into());
431 let mut device =
432 DiscoveredDevice::new(DacType::Helios, DiscoveredDeviceInner::Helios(opened));
433 device.hardware_name = Some(hardware_name);
434 discovered.push(device);
435 }
436 discovered
437 }
438
439 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
441 let DiscoveredDeviceInner::Helios(dac) = device.inner else {
442 return Err(Error::invalid_config("Invalid device type for Helios"));
443 };
444 Ok(Box::new(HeliosBackend::from_dac(dac)))
445 }
446}
447
448#[cfg(feature = "ether-dream")]
450pub struct EtherDreamDiscovery {
451 timeout: Duration,
452}
453
454#[cfg(feature = "ether-dream")]
455impl EtherDreamDiscovery {
456 pub fn new() -> Self {
458 Self {
459 timeout: Duration::from_millis(1500),
462 }
463 }
464
465 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
467 let Ok(mut rx) = recv_dac_broadcasts() else {
468 return Vec::new();
469 };
470
471 if rx.set_timeout(Some(self.timeout)).is_err() {
472 return Vec::new();
473 }
474
475 let mut discovered = Vec::new();
476 let mut seen_macs = std::collections::HashSet::new();
477
478 for _ in 0..3 {
480 let (broadcast, source_addr) = match rx.next_broadcast() {
481 Ok(b) => b,
482 Err(_) => break,
483 };
484
485 let ip = source_addr.ip();
486
487 let device_mac = broadcast.mac_address;
489 if seen_macs.contains(&device_mac) {
490 continue;
491 }
492 seen_macs.insert(device_mac);
493
494 let mut device = DiscoveredDevice::new(
495 DacType::EtherDream,
496 DiscoveredDeviceInner::EtherDream { broadcast, ip },
497 );
498 device.ip_address = Some(ip);
499 device.mac_address = Some(device_mac);
500 discovered.push(device);
501 }
502 discovered
503 }
504
505 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
507 let DiscoveredDeviceInner::EtherDream { broadcast, ip } = device.inner else {
508 return Err(Error::invalid_config("Invalid device type for EtherDream"));
509 };
510
511 let backend = EtherDreamBackend::new(broadcast, ip);
512 Ok(Box::new(backend))
513 }
514}
515
516#[cfg(feature = "ether-dream")]
517impl Default for EtherDreamDiscovery {
518 fn default() -> Self {
519 Self::new()
520 }
521}
522
523#[cfg(feature = "idn")]
525pub struct IdnDiscovery {
526 scan_timeout: Duration,
527}
528
529#[cfg(feature = "idn")]
530impl IdnDiscovery {
531 pub fn new() -> Self {
533 Self {
534 scan_timeout: Duration::from_millis(500),
535 }
536 }
537
538 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
540 let Ok(servers) = scan_for_servers(self.scan_timeout) else {
541 return Vec::new();
542 };
543 Self::servers_to_devices(servers)
544 }
545
546 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
548 let DiscoveredDeviceInner::Idn { server, service } = device.inner else {
549 return Err(Error::invalid_config("Invalid device type for IDN"));
550 };
551
552 Ok(Box::new(IdnBackend::new(server, service)))
553 }
554
555 #[cfg(feature = "testutils")]
562 pub fn scan_address(&mut self, addr: SocketAddr) -> Vec<DiscoveredDevice> {
563 let Ok(mut scanner) = ServerScanner::new(0) else {
564 return Vec::new();
565 };
566
567 let Ok(servers) = scanner.scan_address(addr, self.scan_timeout) else {
568 return Vec::new();
569 };
570
571 Self::servers_to_devices(servers)
572 }
573
574 fn servers_to_devices(servers: Vec<IdnServerInfo>) -> Vec<DiscoveredDevice> {
576 servers
577 .into_iter()
578 .filter_map(|server| {
579 let service = server.find_laser_projector().cloned()?;
580 let ip_address = server.addresses.first().map(|addr| addr.ip());
581 let hostname = server.hostname.clone();
582 let mut device = DiscoveredDevice::new(
583 DacType::Idn,
584 DiscoveredDeviceInner::Idn { server, service },
585 );
586 device.ip_address = ip_address;
587 device.hostname = Some(hostname);
588 Some(device)
589 })
590 .collect()
591 }
592}
593
594#[cfg(feature = "idn")]
595impl Default for IdnDiscovery {
596 fn default() -> Self {
597 Self::new()
598 }
599}
600
601#[cfg(feature = "lasercube-wifi")]
603pub struct LasercubeWifiDiscovery {
604 timeout: Duration,
605}
606
607#[cfg(feature = "lasercube-wifi")]
608impl LasercubeWifiDiscovery {
609 pub fn new() -> Self {
611 Self {
612 timeout: Duration::from_millis(100),
613 }
614 }
615
616 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
618 let Ok(mut discovery) = discover_lasercube_wifi() else {
619 return Vec::new();
620 };
621
622 if discovery.set_timeout(Some(self.timeout)).is_err() {
623 return Vec::new();
624 }
625
626 let mut discovered = Vec::new();
627 for _ in 0..10 {
628 let (device_info, source_addr) = match discovery.next_device() {
629 Ok(d) => d,
630 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
631 Err(e) if e.kind() == io::ErrorKind::TimedOut => break,
632 Err(_) => continue,
633 };
634
635 let ip_address = source_addr.ip();
636
637 let mut device = DiscoveredDevice::new(
638 DacType::LasercubeWifi,
639 DiscoveredDeviceInner::LasercubeWifi {
640 info: device_info,
641 source_addr,
642 },
643 );
644 device.ip_address = Some(ip_address);
645 discovered.push(device);
646 }
647 discovered
648 }
649
650 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
652 let DiscoveredDeviceInner::LasercubeWifi { info, source_addr } = device.inner else {
653 return Err(Error::invalid_config(
654 "Invalid device type for LaserCube WiFi",
655 ));
656 };
657
658 let addressed = LasercubeAddressed::from_discovery(&info, source_addr);
659 Ok(Box::new(LasercubeWifiBackend::new(addressed)))
660 }
661}
662
663#[cfg(feature = "lasercube-wifi")]
664impl Default for LasercubeWifiDiscovery {
665 fn default() -> Self {
666 Self::new()
667 }
668}
669
670#[cfg(feature = "lasercube-usb")]
672pub struct LasercubeUsbDiscovery {
673 controller: LasercubeUsbController,
674}
675
676#[cfg(feature = "lasercube-usb")]
677impl LasercubeUsbDiscovery {
678 pub fn new() -> Option<Self> {
682 LasercubeUsbController::new()
683 .ok()
684 .map(|controller| Self { controller })
685 }
686
687 pub fn scan(&self) -> Vec<DiscoveredDevice> {
689 let Ok(devices) = self.controller.list_devices() else {
690 return Vec::new();
691 };
692
693 let mut discovered = Vec::new();
694 for device in devices {
695 let usb_address = format!("{}:{}", device.bus_number(), device.address());
696 let serial = crate::protocols::lasercube_usb::get_serial_number(&device);
697
698 let mut discovered_device = DiscoveredDevice::new(
699 DacType::LasercubeUsb,
700 DiscoveredDeviceInner::LasercubeUsb(device),
701 );
702 discovered_device.usb_address = Some(usb_address);
703 discovered_device.hardware_name = serial;
704 discovered.push(discovered_device);
705 }
706 discovered
707 }
708
709 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
711 let DiscoveredDeviceInner::LasercubeUsb(usb_device) = device.inner else {
712 return Err(Error::invalid_config(
713 "Invalid device type for LaserCube USB",
714 ));
715 };
716
717 let backend = LasercubeUsbBackend::new(usb_device);
718 Ok(Box::new(backend))
719 }
720}
721
722#[cfg(feature = "avb")]
724pub struct AvbDiscovery;
725
726#[cfg(feature = "avb")]
727impl AvbDiscovery {
728 pub fn new() -> Self {
730 Self
731 }
732
733 pub fn scan(&self) -> Vec<DiscoveredDevice> {
735 let Ok(selectors) = discover_avb_selectors() else {
736 return Vec::new();
737 };
738
739 selectors
740 .into_iter()
741 .map(|selector| {
742 let hardware_name = selector.name.clone();
743 let index = selector.duplicate_index;
744 let mut device =
745 DiscoveredDevice::new(DacType::Avb, DiscoveredDeviceInner::Avb(selector));
746 device.hardware_name = Some(hardware_name);
747 device.device_index = Some(index);
748 device
749 })
750 .collect()
751 }
752
753 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
755 let DiscoveredDeviceInner::Avb(selector) = device.inner else {
756 return Err(Error::invalid_config("Invalid device type for AVB"));
757 };
758
759 Ok(Box::new(AvbBackend::from_selector(selector)))
760 }
761}
762
763#[cfg(feature = "avb")]
764impl Default for AvbDiscovery {
765 fn default() -> Self {
766 Self::new()
767 }
768}
769
770pub struct DacDiscovery {
779 #[cfg(feature = "helios")]
780 helios: Option<HeliosDiscovery>,
781 #[cfg(feature = "ether-dream")]
782 etherdream: EtherDreamDiscovery,
783 #[cfg(feature = "idn")]
784 idn: IdnDiscovery,
785 #[cfg(all(feature = "idn", feature = "testutils"))]
786 idn_scan_addresses: Vec<SocketAddr>,
787 #[cfg(feature = "lasercube-wifi")]
788 lasercube_wifi: LasercubeWifiDiscovery,
789 #[cfg(feature = "lasercube-usb")]
790 lasercube_usb: Option<LasercubeUsbDiscovery>,
791 #[cfg(feature = "avb")]
792 avb: AvbDiscovery,
793 enabled: EnabledDacTypes,
794 external: Vec<Box<dyn ExternalDiscoverer>>,
796}
797
798impl DacDiscovery {
799 pub fn new(enabled: EnabledDacTypes) -> Self {
805 Self {
806 #[cfg(feature = "helios")]
807 helios: HeliosDiscovery::new(),
808 #[cfg(feature = "ether-dream")]
809 etherdream: EtherDreamDiscovery::new(),
810 #[cfg(feature = "idn")]
811 idn: IdnDiscovery::new(),
812 #[cfg(all(feature = "idn", feature = "testutils"))]
813 idn_scan_addresses: Vec::new(),
814 #[cfg(feature = "lasercube-wifi")]
815 lasercube_wifi: LasercubeWifiDiscovery::new(),
816 #[cfg(feature = "lasercube-usb")]
817 lasercube_usb: LasercubeUsbDiscovery::new(),
818 #[cfg(feature = "avb")]
819 avb: AvbDiscovery::new(),
820 enabled,
821 external: Vec::new(),
822 }
823 }
824
825 #[cfg(all(feature = "idn", feature = "testutils"))]
832 pub fn set_idn_scan_addresses(&mut self, addresses: Vec<SocketAddr>) {
833 self.idn_scan_addresses = addresses;
834 }
835
836 pub fn set_enabled(&mut self, enabled: EnabledDacTypes) {
838 self.enabled = enabled;
839 }
840
841 pub fn enabled(&self) -> &EnabledDacTypes {
843 &self.enabled
844 }
845
846 pub fn register(&mut self, discoverer: Box<dyn ExternalDiscoverer>) {
864 self.external.push(discoverer);
865 }
866
867 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
872 let mut devices = Vec::new();
873
874 #[cfg(feature = "helios")]
876 if self.enabled.is_enabled(DacType::Helios) {
877 if let Some(ref discovery) = self.helios {
878 devices.extend(discovery.scan());
879 }
880 }
881
882 #[cfg(feature = "ether-dream")]
884 if self.enabled.is_enabled(DacType::EtherDream) {
885 devices.extend(self.etherdream.scan());
886 }
887
888 #[cfg(feature = "idn")]
890 if self.enabled.is_enabled(DacType::Idn) {
891 #[cfg(feature = "testutils")]
892 {
893 if self.idn_scan_addresses.is_empty() {
894 devices.extend(self.idn.scan());
896 } else {
897 for addr in &self.idn_scan_addresses {
899 devices.extend(self.idn.scan_address(*addr));
900 }
901 }
902 }
903 #[cfg(not(feature = "testutils"))]
904 {
905 devices.extend(self.idn.scan());
906 }
907 }
908
909 #[cfg(feature = "lasercube-wifi")]
911 if self.enabled.is_enabled(DacType::LasercubeWifi) {
912 devices.extend(self.lasercube_wifi.scan());
913 }
914
915 #[cfg(feature = "lasercube-usb")]
917 if self.enabled.is_enabled(DacType::LasercubeUsb) {
918 if let Some(ref discovery) = self.lasercube_usb {
919 devices.extend(discovery.scan());
920 }
921 }
922
923 #[cfg(feature = "avb")]
925 if self.enabled.is_enabled(DacType::Avb) {
926 devices.extend(self.avb.scan());
927 }
928
929 for (index, discoverer) in self.external.iter_mut().enumerate() {
931 let dac_type = discoverer.dac_type();
932 for ext_device in discoverer.scan() {
933 let mut device = DiscoveredDevice::new(
934 dac_type.clone(),
935 DiscoveredDeviceInner::External {
936 discoverer_index: index,
937 opaque_data: ext_device.opaque_data,
938 },
939 );
940 device.ip_address = ext_device.ip_address;
941 device.mac_address = ext_device.mac_address;
942 device.hostname = ext_device.hostname;
943 device.usb_address = ext_device.usb_address;
944 device.hardware_name = ext_device.hardware_name;
945 device.device_index = ext_device.device_index;
946 devices.push(device);
947 }
948 }
949
950 devices
951 }
952
953 fn dac_type_from_id_prefix(id: &str) -> Option<DacType> {
958 let prefix = id.split(':').next()?;
961 match prefix {
962 "etherdream" => Some(DacType::EtherDream),
963 "idn" => Some(DacType::Idn),
964 "helios" => Some(DacType::Helios),
965 "lasercube-usb" => Some(DacType::LasercubeUsb),
966 "lasercube-wifi" => Some(DacType::LasercubeWifi),
967 "avb" => Some(DacType::Avb),
968 _ => None,
969 }
970 }
971
972 pub(crate) fn open_by_id(&mut self, id: &str) -> Result<crate::stream::Dac> {
980 let discovered = if let Some(dac_type) = Self::dac_type_from_id_prefix(id) {
982 let saved = self.enabled.clone();
983 self.enabled = std::iter::once(dac_type).collect();
984 let result = self.scan();
985 self.enabled = saved;
986 result
987 } else {
988 self.scan()
989 };
990
991 let device = discovered
992 .into_iter()
993 .find(|d| d.info().stable_id() == id)
994 .ok_or_else(|| Error::disconnected(format!("DAC not found: {}", id)))?;
995
996 let name = device.info().name();
997 let dac_type = device.dac_type();
998 let stream_backend = self.connect(device)?;
999
1000 let dac_info = crate::types::DacInfo {
1001 id: id.to_string(),
1002 name,
1003 kind: dac_type,
1004 caps: stream_backend.caps().clone(),
1005 };
1006
1007 Ok(crate::stream::Dac::new(dac_info, stream_backend))
1008 }
1009
1010 #[allow(unreachable_patterns)]
1012 pub fn connect(&mut self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
1013 if let DiscoveredDeviceInner::External {
1015 discoverer_index,
1016 opaque_data,
1017 } = device.inner
1018 {
1019 return self
1020 .external
1021 .get_mut(discoverer_index)
1022 .ok_or_else(|| Error::invalid_config("External discoverer not found"))?
1023 .connect(opaque_data);
1024 }
1025
1026 match device.dac_type {
1028 #[cfg(feature = "helios")]
1029 DacType::Helios => self
1030 .helios
1031 .as_ref()
1032 .ok_or_else(|| Error::disconnected("Helios discovery not available"))?
1033 .connect(device),
1034 #[cfg(feature = "ether-dream")]
1035 DacType::EtherDream => self.etherdream.connect(device),
1036 #[cfg(feature = "idn")]
1037 DacType::Idn => self.idn.connect(device),
1038 #[cfg(feature = "lasercube-wifi")]
1039 DacType::LasercubeWifi => self.lasercube_wifi.connect(device),
1040 #[cfg(feature = "lasercube-usb")]
1041 DacType::LasercubeUsb => self
1042 .lasercube_usb
1043 .as_ref()
1044 .ok_or_else(|| Error::disconnected("LaserCube USB discovery not available"))?
1045 .connect(device),
1046 #[cfg(feature = "avb")]
1047 DacType::Avb => self.avb.connect(device),
1048 _ => Err(Error::invalid_config(format!(
1049 "DAC type {:?} not supported in this build",
1050 device.dac_type
1051 ))),
1052 }
1053 }
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use super::*;
1059
1060 #[test]
1061 fn test_stable_id_etherdream_with_mac() {
1062 let info = DiscoveredDeviceInfo {
1063 dac_type: DacType::EtherDream,
1064 ip_address: Some("192.168.1.100".parse().unwrap()),
1065 mac_address: Some([0x01, 0x23, 0x45, 0x67, 0x89, 0xab]),
1066 hostname: None,
1067 usb_address: None,
1068 hardware_name: None,
1069 device_index: None,
1070 };
1071 assert_eq!(info.stable_id(), "etherdream:01:23:45:67:89:ab");
1072 }
1073
1074 #[test]
1075 fn test_stable_id_idn_with_hostname() {
1076 let info = DiscoveredDeviceInfo {
1077 dac_type: DacType::Idn,
1078 ip_address: Some("192.168.1.100".parse().unwrap()),
1079 mac_address: None,
1080 hostname: Some("laser-projector.local".to_string()),
1081 usb_address: None,
1082 hardware_name: None,
1083 device_index: None,
1084 };
1085 assert_eq!(info.stable_id(), "idn:laser-projector.local");
1086 }
1087
1088 #[test]
1089 fn test_stable_id_helios_with_hardware_name() {
1090 let info = DiscoveredDeviceInfo {
1091 dac_type: DacType::Helios,
1092 ip_address: None,
1093 mac_address: None,
1094 hostname: None,
1095 usb_address: Some("1:5".to_string()),
1096 hardware_name: Some("Helios DAC".to_string()),
1097 device_index: None,
1098 };
1099 assert_eq!(info.stable_id(), "helios:Helios DAC");
1100 }
1101
1102 #[test]
1103 fn test_stable_id_lasercube_usb_with_address() {
1104 let info = DiscoveredDeviceInfo {
1105 dac_type: DacType::LasercubeUsb,
1106 ip_address: None,
1107 mac_address: None,
1108 hostname: None,
1109 usb_address: Some("2:3".to_string()),
1110 hardware_name: None,
1111 device_index: None,
1112 };
1113 assert_eq!(info.stable_id(), "lasercube-usb:2:3");
1114 }
1115
1116 #[test]
1117 fn test_stable_id_lasercube_wifi_with_ip() {
1118 let info = DiscoveredDeviceInfo {
1119 dac_type: DacType::LasercubeWifi,
1120 ip_address: Some("192.168.1.50".parse().unwrap()),
1121 mac_address: None,
1122 hostname: None,
1123 usb_address: None,
1124 hardware_name: None,
1125 device_index: None,
1126 };
1127 assert_eq!(info.stable_id(), "lasercube-wifi:192.168.1.50");
1128 }
1129
1130 #[test]
1131 fn test_stable_id_avb_with_index() {
1132 let info = DiscoveredDeviceInfo {
1133 dac_type: DacType::Avb,
1134 ip_address: None,
1135 mac_address: None,
1136 hostname: None,
1137 usb_address: None,
1138 hardware_name: Some("MOTU AVB Main".to_string()),
1139 device_index: Some(1),
1140 };
1141 assert_eq!(info.stable_id(), "avb:motu-avb-main:1");
1142 }
1143
1144 #[test]
1145 fn test_stable_id_custom_fallback() {
1146 let info = DiscoveredDeviceInfo {
1147 dac_type: DacType::Custom("MyDAC".to_string()),
1148 ip_address: None,
1149 mac_address: None,
1150 hostname: None,
1151 usb_address: None,
1152 hardware_name: None,
1153 device_index: None,
1154 };
1155 assert_eq!(info.stable_id(), "unknown:Custom(\"MyDAC\")");
1157 }
1158
1159 #[test]
1160 fn test_stable_id_custom_with_ip() {
1161 let info = DiscoveredDeviceInfo {
1162 dac_type: DacType::Custom("MyDAC".to_string()),
1163 ip_address: Some("10.0.0.1".parse().unwrap()),
1164 mac_address: None,
1165 hostname: None,
1166 usb_address: None,
1167 hardware_name: None,
1168 device_index: None,
1169 };
1170 assert_eq!(info.stable_id(), "mydac:10.0.0.1");
1171 }
1172
1173 use crate::types::{DacCapabilities, LaserPoint};
1178 use crate::WriteOutcome;
1179 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1180 use std::sync::Arc;
1181
1182 #[derive(Debug, Clone)]
1184 struct MockConnectionInfo {
1185 _device_id: u32,
1186 }
1187
1188 struct MockBackend {
1190 connected: bool,
1191 }
1192
1193 impl StreamBackend for MockBackend {
1194 fn dac_type(&self) -> DacType {
1195 DacType::Custom("MockDAC".into())
1196 }
1197
1198 fn caps(&self) -> &DacCapabilities {
1199 static CAPS: DacCapabilities = DacCapabilities {
1200 pps_min: 1,
1201 pps_max: 100_000,
1202 max_points_per_chunk: 4096,
1203 output_model: crate::types::OutputModel::NetworkFifo,
1204 };
1205 &CAPS
1206 }
1207
1208 fn connect(&mut self) -> Result<()> {
1209 self.connected = true;
1210 Ok(())
1211 }
1212
1213 fn disconnect(&mut self) -> Result<()> {
1214 self.connected = false;
1215 Ok(())
1216 }
1217
1218 fn is_connected(&self) -> bool {
1219 self.connected
1220 }
1221
1222 fn try_write_chunk(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
1223 Ok(WriteOutcome::Written)
1224 }
1225
1226 fn stop(&mut self) -> Result<()> {
1227 Ok(())
1228 }
1229
1230 fn set_shutter(&mut self, _open: bool) -> Result<()> {
1231 Ok(())
1232 }
1233 }
1234
1235 struct MockExternalDiscoverer {
1237 scan_count: Arc<AtomicUsize>,
1238 connect_called: Arc<AtomicBool>,
1239 devices_to_return: Vec<(u32, Option<IpAddr>)>,
1240 }
1241
1242 impl MockExternalDiscoverer {
1243 fn new(devices: Vec<(u32, Option<IpAddr>)>) -> Self {
1244 Self {
1245 scan_count: Arc::new(AtomicUsize::new(0)),
1246 connect_called: Arc::new(AtomicBool::new(false)),
1247 devices_to_return: devices,
1248 }
1249 }
1250 }
1251
1252 impl ExternalDiscoverer for MockExternalDiscoverer {
1253 fn dac_type(&self) -> DacType {
1254 DacType::Custom("MockDAC".into())
1255 }
1256
1257 fn scan(&mut self) -> Vec<ExternalDevice> {
1258 self.scan_count.fetch_add(1, Ordering::SeqCst);
1259 self.devices_to_return
1260 .iter()
1261 .map(|(id, ip)| {
1262 let mut device = ExternalDevice::new(MockConnectionInfo { _device_id: *id });
1263 device.ip_address = *ip;
1264 device.hardware_name = Some(format!("Mock Device {}", id));
1265 device
1266 })
1267 .collect()
1268 }
1269
1270 fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>> {
1271 self.connect_called.store(true, Ordering::SeqCst);
1272 let _info = opaque_data
1273 .downcast::<MockConnectionInfo>()
1274 .map_err(|_| Error::invalid_config("wrong device type"))?;
1275 Ok(Box::new(MockBackend { connected: false }))
1276 }
1277 }
1278
1279 #[test]
1280 fn test_external_discoverer_scan_is_called() {
1281 let discoverer = MockExternalDiscoverer::new(vec![(1, Some("10.0.0.1".parse().unwrap()))]);
1282 let scan_count = discoverer.scan_count.clone();
1283
1284 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1285 discovery.register(Box::new(discoverer));
1286
1287 assert_eq!(scan_count.load(Ordering::SeqCst), 0);
1288 let devices = discovery.scan();
1289 assert_eq!(scan_count.load(Ordering::SeqCst), 1);
1290 assert_eq!(devices.len(), 1);
1291 }
1292
1293 #[test]
1294 fn test_external_discoverer_device_info() {
1295 let discoverer =
1296 MockExternalDiscoverer::new(vec![(42, Some("192.168.1.100".parse().unwrap()))]);
1297
1298 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1299 discovery.register(Box::new(discoverer));
1300
1301 let devices = discovery.scan();
1302 assert_eq!(devices.len(), 1);
1303
1304 let device = &devices[0];
1305 assert_eq!(device.dac_type(), DacType::Custom("MockDAC".into()));
1306 assert_eq!(
1307 device.info().ip_address,
1308 Some("192.168.1.100".parse().unwrap())
1309 );
1310 assert_eq!(device.info().hardware_name, Some("Mock Device 42".into()));
1311 }
1312
1313 #[test]
1314 fn test_external_discoverer_connect() {
1315 let discoverer = MockExternalDiscoverer::new(vec![(99, None)]);
1316 let connect_called = discoverer.connect_called.clone();
1317
1318 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1319 discovery.register(Box::new(discoverer));
1320
1321 let devices = discovery.scan();
1322 assert_eq!(devices.len(), 1);
1323 assert!(!connect_called.load(Ordering::SeqCst));
1324
1325 let backend = discovery.connect(devices.into_iter().next().unwrap());
1326 assert!(backend.is_ok());
1327 assert!(connect_called.load(Ordering::SeqCst));
1328
1329 let backend = backend.unwrap();
1330 assert_eq!(backend.dac_type(), DacType::Custom("MockDAC".into()));
1331 }
1332
1333 #[test]
1334 fn test_external_discoverer_multiple_devices() {
1335 let discoverer = MockExternalDiscoverer::new(vec![
1336 (1, Some("10.0.0.1".parse().unwrap())),
1337 (2, Some("10.0.0.2".parse().unwrap())),
1338 (3, None),
1339 ]);
1340
1341 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1342 discovery.register(Box::new(discoverer));
1343
1344 let devices = discovery.scan();
1345 assert_eq!(devices.len(), 3);
1346
1347 for device in devices {
1349 let backend = discovery.connect(device);
1350 assert!(backend.is_ok());
1351 }
1352 }
1353
1354 #[test]
1355 fn test_multiple_external_discoverers() {
1356 let discoverer1 = MockExternalDiscoverer::new(vec![(1, None)]);
1357 let discoverer2 = MockExternalDiscoverer::new(vec![(2, None), (3, None)]);
1358
1359 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1360 discovery.register(Box::new(discoverer1));
1361 discovery.register(Box::new(discoverer2));
1362
1363 let devices = discovery.scan();
1364 assert_eq!(devices.len(), 3);
1365 }
1366}