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 pub fn name(&self) -> String {
175 self.ip_address
176 .map(|ip| ip.to_string())
177 .or_else(|| self.hardware_name.clone())
178 .or_else(|| self.usb_address.clone())
179 .unwrap_or_else(|| "Unknown".into())
180 }
181
182 pub fn dac_type(&self) -> DacType {
184 self.dac_type.clone()
185 }
186
187 pub fn info(&self) -> DiscoveredDeviceInfo {
189 DiscoveredDeviceInfo {
190 dac_type: self.dac_type.clone(),
191 ip_address: self.ip_address,
192 mac_address: self.mac_address,
193 hostname: self.hostname.clone(),
194 usb_address: self.usb_address.clone(),
195 hardware_name: self.hardware_name.clone(),
196 device_index: self.device_index,
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Hash)]
206pub struct DiscoveredDeviceInfo {
207 pub dac_type: DacType,
209 pub ip_address: Option<IpAddr>,
211 pub mac_address: Option<[u8; 6]>,
213 pub hostname: Option<String>,
215 pub usb_address: Option<String>,
217 pub hardware_name: Option<String>,
219 pub device_index: Option<u16>,
221}
222
223impl DiscoveredDeviceInfo {
224 pub fn name(&self) -> String {
228 self.ip_address
229 .map(|ip| ip.to_string())
230 .or_else(|| self.hardware_name.clone())
231 .or_else(|| self.usb_address.clone())
232 .unwrap_or_else(|| "Unknown".into())
233 }
234
235 pub fn stable_id(&self) -> String {
247 match &self.dac_type {
248 DacType::EtherDream => {
249 if let Some(mac) = self.mac_address {
250 return format!(
251 "etherdream:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
252 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
253 );
254 }
255 if let Some(ip) = self.ip_address {
256 return format!("etherdream:{}", ip);
257 }
258 }
259 DacType::Idn => {
260 if let Some(ref hostname) = self.hostname {
261 return format!("idn:{}", hostname);
262 }
263 if let Some(ip) = self.ip_address {
264 return format!("idn:{}", ip);
265 }
266 }
267 DacType::Helios => {
268 if let Some(ref hw_name) = self.hardware_name {
269 return format!("helios:{}", hw_name);
270 }
271 if let Some(ref usb_addr) = self.usb_address {
272 return format!("helios:{}", usb_addr);
273 }
274 }
275 DacType::LasercubeUsb => {
276 if let Some(ref hw_name) = self.hardware_name {
277 return format!("lasercube-usb:{}", hw_name);
278 }
279 if let Some(ref usb_addr) = self.usb_address {
280 return format!("lasercube-usb:{}", usb_addr);
281 }
282 }
283 DacType::LasercubeWifi => {
284 if let Some(ip) = self.ip_address {
285 return format!("lasercube-wifi:{}", ip);
286 }
287 }
288 DacType::Avb => {
289 if let Some(ref hw_name) = self.hardware_name {
290 let slug = slugify_device_id(hw_name);
291 if let Some(index) = self.device_index {
292 return format!("avb:{}:{}", slug, index);
293 }
294 return format!("avb:{}", slug);
295 }
296 }
297 DacType::Custom(name) => {
298 if let Some(ip) = self.ip_address {
300 return format!("{}:{}", name.to_lowercase(), ip);
301 }
302 if let Some(ref hw_name) = self.hardware_name {
303 return format!("{}:{}", name.to_lowercase(), hw_name);
304 }
305 }
306 }
307
308 format!("unknown:{:?}", self.dac_type)
310 }
311}
312
313fn slugify_device_id(name: &str) -> String {
314 let normalized = name
315 .split_whitespace()
316 .collect::<Vec<_>>()
317 .join(" ")
318 .trim()
319 .to_ascii_lowercase();
320 let mut slug = String::with_capacity(normalized.len());
321 let mut prev_dash = false;
322
323 for ch in normalized.chars() {
324 if ch.is_ascii_alphanumeric() {
325 slug.push(ch);
326 prev_dash = false;
327 } else if !prev_dash {
328 slug.push('-');
329 prev_dash = true;
330 }
331 }
332
333 slug.trim_matches('-').to_string()
334}
335
336enum DiscoveredDeviceInner {
338 #[cfg(feature = "helios")]
339 Helios(HeliosDac),
340 #[cfg(feature = "ether-dream")]
341 EtherDream {
342 broadcast: EtherDreamBroadcast,
343 ip: IpAddr,
344 },
345 #[cfg(feature = "idn")]
346 Idn {
347 server: IdnServerInfo,
348 service: IdnServiceInfo,
349 },
350 #[cfg(feature = "lasercube-wifi")]
351 LasercubeWifi {
352 info: LasercubeDeviceInfo,
353 source_addr: std::net::SocketAddr,
354 },
355 #[cfg(feature = "lasercube-usb")]
356 LasercubeUsb(rusb::Device<rusb::Context>),
357 #[cfg(feature = "avb")]
358 Avb(AvbSelector),
359 External {
361 discoverer_index: usize,
363 opaque_data: Box<dyn Any + Send>,
365 },
366 #[cfg(not(any(
368 feature = "helios",
369 feature = "ether-dream",
370 feature = "idn",
371 feature = "lasercube-wifi",
372 feature = "lasercube-usb",
373 feature = "avb"
374 )))]
375 _Placeholder,
376}
377
378#[cfg(feature = "helios")]
384pub struct HeliosDiscovery {
385 controller: HeliosDacController,
386}
387
388#[cfg(feature = "helios")]
389impl HeliosDiscovery {
390 pub fn new() -> Option<Self> {
394 HeliosDacController::new()
395 .ok()
396 .map(|controller| Self { controller })
397 }
398
399 pub fn scan(&self) -> Vec<DiscoveredDevice> {
401 let Ok(devices) = self.controller.list_devices() else {
402 return Vec::new();
403 };
404
405 let mut discovered = Vec::new();
406 for device in devices {
407 let HeliosDac::Idle(_) = &device else {
409 continue;
410 };
411
412 let opened = match device.open() {
414 Ok(o) => o,
415 Err(_) => continue,
416 };
417
418 let hardware_name = opened.name().unwrap_or_else(|_| "Unknown Helios".into());
419 discovered.push(DiscoveredDevice {
420 dac_type: DacType::Helios,
421 ip_address: None,
422 mac_address: None,
423 hostname: None,
424 usb_address: None,
425 hardware_name: Some(hardware_name),
426 device_index: None,
427 inner: DiscoveredDeviceInner::Helios(opened),
428 });
429 }
430 discovered
431 }
432
433 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
435 let DiscoveredDeviceInner::Helios(dac) = device.inner else {
436 return Err(Error::invalid_config("Invalid device type for Helios"));
437 };
438 Ok(Box::new(HeliosBackend::from_dac(dac)))
439 }
440}
441
442#[cfg(feature = "ether-dream")]
444pub struct EtherDreamDiscovery {
445 timeout: Duration,
446}
447
448#[cfg(feature = "ether-dream")]
449impl EtherDreamDiscovery {
450 pub fn new() -> Self {
452 Self {
453 timeout: Duration::from_millis(1500),
456 }
457 }
458
459 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
461 let Ok(mut rx) = recv_dac_broadcasts() else {
462 return Vec::new();
463 };
464
465 if rx.set_timeout(Some(self.timeout)).is_err() {
466 return Vec::new();
467 }
468
469 let mut discovered = Vec::new();
470 let mut seen_macs = std::collections::HashSet::new();
471
472 for _ in 0..3 {
474 let (broadcast, source_addr) = match rx.next_broadcast() {
475 Ok(b) => b,
476 Err(_) => break,
477 };
478
479 let ip = source_addr.ip();
480
481 if seen_macs.contains(&broadcast.mac_address) {
483 continue;
484 }
485 seen_macs.insert(broadcast.mac_address);
486
487 discovered.push(DiscoveredDevice {
488 dac_type: DacType::EtherDream,
489 ip_address: Some(ip),
490 mac_address: Some(broadcast.mac_address),
491 hostname: None,
492 usb_address: None,
493 hardware_name: None,
494 device_index: None,
495 inner: DiscoveredDeviceInner::EtherDream { broadcast, ip },
496 });
497 }
498 discovered
499 }
500
501 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
503 let DiscoveredDeviceInner::EtherDream { broadcast, ip } = device.inner else {
504 return Err(Error::invalid_config("Invalid device type for EtherDream"));
505 };
506
507 let backend = EtherDreamBackend::new(broadcast, ip);
508 Ok(Box::new(backend))
509 }
510}
511
512#[cfg(feature = "ether-dream")]
513impl Default for EtherDreamDiscovery {
514 fn default() -> Self {
515 Self::new()
516 }
517}
518
519#[cfg(feature = "idn")]
521pub struct IdnDiscovery {
522 scan_timeout: Duration,
523}
524
525#[cfg(feature = "idn")]
526impl IdnDiscovery {
527 pub fn new() -> Self {
529 Self {
530 scan_timeout: Duration::from_millis(500),
531 }
532 }
533
534 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
536 let Ok(servers) = scan_for_servers(self.scan_timeout) else {
537 return Vec::new();
538 };
539
540 let mut discovered = Vec::new();
541 for server in servers {
542 let Some(service) = server.find_laser_projector().cloned() else {
543 continue;
544 };
545
546 let ip_address = server.addresses.first().map(|addr| addr.ip());
547 let hostname = server.hostname.clone();
548
549 discovered.push(DiscoveredDevice {
550 dac_type: DacType::Idn,
551 ip_address,
552 mac_address: None,
553 hostname: Some(hostname),
554 usb_address: None,
555 hardware_name: None,
556 device_index: None,
557 inner: DiscoveredDeviceInner::Idn { server, service },
558 });
559 }
560 discovered
561 }
562
563 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
565 let DiscoveredDeviceInner::Idn { server, service } = device.inner else {
566 return Err(Error::invalid_config("Invalid device type for IDN"));
567 };
568
569 Ok(Box::new(IdnBackend::new(server, service)))
570 }
571
572 #[cfg(feature = "testutils")]
579 pub fn scan_address(&mut self, addr: SocketAddr) -> Vec<DiscoveredDevice> {
580 let Ok(mut scanner) = ServerScanner::new(0) else {
581 return Vec::new();
582 };
583
584 let Ok(servers) = scanner.scan_address(addr, self.scan_timeout) else {
585 return Vec::new();
586 };
587
588 let mut discovered = Vec::new();
589 for server in servers {
590 let Some(service) = server.find_laser_projector().cloned() else {
591 continue;
592 };
593
594 let ip_address = server.addresses.first().map(|addr| addr.ip());
595 let hostname = server.hostname.clone();
596
597 discovered.push(DiscoveredDevice {
598 dac_type: DacType::Idn,
599 ip_address,
600 mac_address: None,
601 hostname: Some(hostname),
602 usb_address: None,
603 hardware_name: None,
604 device_index: None,
605 inner: DiscoveredDeviceInner::Idn { server, service },
606 });
607 }
608 discovered
609 }
610}
611
612#[cfg(feature = "idn")]
613impl Default for IdnDiscovery {
614 fn default() -> Self {
615 Self::new()
616 }
617}
618
619#[cfg(feature = "lasercube-wifi")]
621pub struct LasercubeWifiDiscovery {
622 timeout: Duration,
623}
624
625#[cfg(feature = "lasercube-wifi")]
626impl LasercubeWifiDiscovery {
627 pub fn new() -> Self {
629 Self {
630 timeout: Duration::from_millis(100),
631 }
632 }
633
634 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
636 let Ok(mut discovery) = discover_lasercube_wifi() else {
637 return Vec::new();
638 };
639
640 if discovery.set_timeout(Some(self.timeout)).is_err() {
641 return Vec::new();
642 }
643
644 let mut discovered = Vec::new();
645 for _ in 0..10 {
646 let (device_info, source_addr) = match discovery.next_device() {
647 Ok(d) => d,
648 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
649 Err(e) if e.kind() == io::ErrorKind::TimedOut => break,
650 Err(_) => continue,
651 };
652
653 let ip_address = source_addr.ip();
654
655 discovered.push(DiscoveredDevice {
656 dac_type: DacType::LasercubeWifi,
657 ip_address: Some(ip_address),
658 mac_address: None,
659 hostname: None,
660 usb_address: None,
661 hardware_name: None,
662 device_index: None,
663 inner: DiscoveredDeviceInner::LasercubeWifi {
664 info: device_info,
665 source_addr,
666 },
667 });
668 }
669 discovered
670 }
671
672 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
674 let DiscoveredDeviceInner::LasercubeWifi { info, source_addr } = device.inner else {
675 return Err(Error::invalid_config(
676 "Invalid device type for LaserCube WiFi",
677 ));
678 };
679
680 let addressed = LasercubeAddressed::from_discovery(&info, source_addr);
681 Ok(Box::new(LasercubeWifiBackend::new(addressed)))
682 }
683}
684
685#[cfg(feature = "lasercube-wifi")]
686impl Default for LasercubeWifiDiscovery {
687 fn default() -> Self {
688 Self::new()
689 }
690}
691
692#[cfg(feature = "lasercube-usb")]
694pub struct LasercubeUsbDiscovery {
695 controller: LasercubeUsbController,
696}
697
698#[cfg(feature = "lasercube-usb")]
699impl LasercubeUsbDiscovery {
700 pub fn new() -> Option<Self> {
704 LasercubeUsbController::new()
705 .ok()
706 .map(|controller| Self { controller })
707 }
708
709 pub fn scan(&self) -> Vec<DiscoveredDevice> {
711 let Ok(devices) = self.controller.list_devices() else {
712 return Vec::new();
713 };
714
715 let mut discovered = Vec::new();
716 for device in devices {
717 let usb_address = format!("{}:{}", device.bus_number(), device.address());
718 let serial = crate::protocols::lasercube_usb::get_serial_number(&device);
719
720 discovered.push(DiscoveredDevice {
721 dac_type: DacType::LasercubeUsb,
722 ip_address: None,
723 mac_address: None,
724 hostname: None,
725 usb_address: Some(usb_address),
726 hardware_name: serial,
727 device_index: None,
728 inner: DiscoveredDeviceInner::LasercubeUsb(device),
729 });
730 }
731 discovered
732 }
733
734 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
736 let DiscoveredDeviceInner::LasercubeUsb(usb_device) = device.inner else {
737 return Err(Error::invalid_config(
738 "Invalid device type for LaserCube USB",
739 ));
740 };
741
742 let backend = LasercubeUsbBackend::new(usb_device);
743 Ok(Box::new(backend))
744 }
745}
746
747#[cfg(feature = "avb")]
749pub struct AvbDiscovery;
750
751#[cfg(feature = "avb")]
752impl AvbDiscovery {
753 pub fn new() -> Self {
755 Self
756 }
757
758 pub fn scan(&self) -> Vec<DiscoveredDevice> {
760 let Ok(selectors) = discover_avb_selectors() else {
761 return Vec::new();
762 };
763
764 selectors
765 .into_iter()
766 .map(|selector| {
767 let hardware_name = selector.name.clone();
768 let index = selector.duplicate_index;
769 DiscoveredDevice {
770 dac_type: DacType::Avb,
771 ip_address: None,
772 mac_address: None,
773 hostname: None,
774 usb_address: None,
775 hardware_name: Some(hardware_name),
776 device_index: Some(index),
777 inner: DiscoveredDeviceInner::Avb(selector),
778 }
779 })
780 .collect()
781 }
782
783 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
785 let DiscoveredDeviceInner::Avb(selector) = device.inner else {
786 return Err(Error::invalid_config("Invalid device type for AVB"));
787 };
788
789 Ok(Box::new(AvbBackend::from_selector(selector)))
790 }
791}
792
793#[cfg(feature = "avb")]
794impl Default for AvbDiscovery {
795 fn default() -> Self {
796 Self::new()
797 }
798}
799
800pub struct DacDiscovery {
809 #[cfg(feature = "helios")]
810 helios: Option<HeliosDiscovery>,
811 #[cfg(feature = "ether-dream")]
812 etherdream: EtherDreamDiscovery,
813 #[cfg(feature = "idn")]
814 idn: IdnDiscovery,
815 #[cfg(all(feature = "idn", feature = "testutils"))]
816 idn_scan_addresses: Vec<SocketAddr>,
817 #[cfg(feature = "lasercube-wifi")]
818 lasercube_wifi: LasercubeWifiDiscovery,
819 #[cfg(feature = "lasercube-usb")]
820 lasercube_usb: Option<LasercubeUsbDiscovery>,
821 #[cfg(feature = "avb")]
822 avb: AvbDiscovery,
823 enabled: EnabledDacTypes,
824 external: Vec<Box<dyn ExternalDiscoverer>>,
826}
827
828impl DacDiscovery {
829 pub fn new(enabled: EnabledDacTypes) -> Self {
835 Self {
836 #[cfg(feature = "helios")]
837 helios: HeliosDiscovery::new(),
838 #[cfg(feature = "ether-dream")]
839 etherdream: EtherDreamDiscovery::new(),
840 #[cfg(feature = "idn")]
841 idn: IdnDiscovery::new(),
842 #[cfg(all(feature = "idn", feature = "testutils"))]
843 idn_scan_addresses: Vec::new(),
844 #[cfg(feature = "lasercube-wifi")]
845 lasercube_wifi: LasercubeWifiDiscovery::new(),
846 #[cfg(feature = "lasercube-usb")]
847 lasercube_usb: LasercubeUsbDiscovery::new(),
848 #[cfg(feature = "avb")]
849 avb: AvbDiscovery::new(),
850 enabled,
851 external: Vec::new(),
852 }
853 }
854
855 #[cfg(all(feature = "idn", feature = "testutils"))]
862 pub fn set_idn_scan_addresses(&mut self, addresses: Vec<SocketAddr>) {
863 self.idn_scan_addresses = addresses;
864 }
865
866 pub fn set_enabled(&mut self, enabled: EnabledDacTypes) {
868 self.enabled = enabled;
869 }
870
871 pub fn enabled(&self) -> &EnabledDacTypes {
873 &self.enabled
874 }
875
876 pub fn register(&mut self, discoverer: Box<dyn ExternalDiscoverer>) {
894 self.external.push(discoverer);
895 }
896
897 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
902 let mut devices = Vec::new();
903
904 #[cfg(feature = "helios")]
906 if self.enabled.is_enabled(DacType::Helios) {
907 if let Some(ref discovery) = self.helios {
908 devices.extend(discovery.scan());
909 }
910 }
911
912 #[cfg(feature = "ether-dream")]
914 if self.enabled.is_enabled(DacType::EtherDream) {
915 devices.extend(self.etherdream.scan());
916 }
917
918 #[cfg(feature = "idn")]
920 if self.enabled.is_enabled(DacType::Idn) {
921 #[cfg(feature = "testutils")]
922 {
923 if self.idn_scan_addresses.is_empty() {
924 devices.extend(self.idn.scan());
926 } else {
927 for addr in &self.idn_scan_addresses {
929 devices.extend(self.idn.scan_address(*addr));
930 }
931 }
932 }
933 #[cfg(not(feature = "testutils"))]
934 {
935 devices.extend(self.idn.scan());
936 }
937 }
938
939 #[cfg(feature = "lasercube-wifi")]
941 if self.enabled.is_enabled(DacType::LasercubeWifi) {
942 devices.extend(self.lasercube_wifi.scan());
943 }
944
945 #[cfg(feature = "lasercube-usb")]
947 if self.enabled.is_enabled(DacType::LasercubeUsb) {
948 if let Some(ref discovery) = self.lasercube_usb {
949 devices.extend(discovery.scan());
950 }
951 }
952
953 #[cfg(feature = "avb")]
955 if self.enabled.is_enabled(DacType::Avb) {
956 devices.extend(self.avb.scan());
957 }
958
959 for (index, discoverer) in self.external.iter_mut().enumerate() {
961 let dac_type = discoverer.dac_type();
962 for ext_device in discoverer.scan() {
963 devices.push(DiscoveredDevice {
964 dac_type: dac_type.clone(),
965 ip_address: ext_device.ip_address,
966 mac_address: ext_device.mac_address,
967 hostname: ext_device.hostname,
968 usb_address: ext_device.usb_address,
969 hardware_name: ext_device.hardware_name,
970 device_index: ext_device.device_index,
971 inner: DiscoveredDeviceInner::External {
972 discoverer_index: index,
973 opaque_data: ext_device.opaque_data,
974 },
975 });
976 }
977 }
978
979 devices
980 }
981
982 #[allow(unreachable_patterns)]
984 pub fn connect(&mut self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
985 if let DiscoveredDeviceInner::External {
987 discoverer_index,
988 opaque_data,
989 } = device.inner
990 {
991 return self
992 .external
993 .get_mut(discoverer_index)
994 .ok_or_else(|| Error::invalid_config("External discoverer not found"))?
995 .connect(opaque_data);
996 }
997
998 match device.dac_type {
1000 #[cfg(feature = "helios")]
1001 DacType::Helios => self
1002 .helios
1003 .as_ref()
1004 .ok_or_else(|| Error::disconnected("Helios discovery not available"))?
1005 .connect(device),
1006 #[cfg(feature = "ether-dream")]
1007 DacType::EtherDream => self.etherdream.connect(device),
1008 #[cfg(feature = "idn")]
1009 DacType::Idn => self.idn.connect(device),
1010 #[cfg(feature = "lasercube-wifi")]
1011 DacType::LasercubeWifi => self.lasercube_wifi.connect(device),
1012 #[cfg(feature = "lasercube-usb")]
1013 DacType::LasercubeUsb => self
1014 .lasercube_usb
1015 .as_ref()
1016 .ok_or_else(|| Error::disconnected("LaserCube USB discovery not available"))?
1017 .connect(device),
1018 #[cfg(feature = "avb")]
1019 DacType::Avb => self.avb.connect(device),
1020 _ => Err(Error::invalid_config(format!(
1021 "DAC type {:?} not supported in this build",
1022 device.dac_type
1023 ))),
1024 }
1025 }
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use super::*;
1031
1032 #[test]
1033 fn test_stable_id_etherdream_with_mac() {
1034 let info = DiscoveredDeviceInfo {
1035 dac_type: DacType::EtherDream,
1036 ip_address: Some("192.168.1.100".parse().unwrap()),
1037 mac_address: Some([0x01, 0x23, 0x45, 0x67, 0x89, 0xab]),
1038 hostname: None,
1039 usb_address: None,
1040 hardware_name: None,
1041 device_index: None,
1042 };
1043 assert_eq!(info.stable_id(), "etherdream:01:23:45:67:89:ab");
1044 }
1045
1046 #[test]
1047 fn test_stable_id_idn_with_hostname() {
1048 let info = DiscoveredDeviceInfo {
1049 dac_type: DacType::Idn,
1050 ip_address: Some("192.168.1.100".parse().unwrap()),
1051 mac_address: None,
1052 hostname: Some("laser-projector.local".to_string()),
1053 usb_address: None,
1054 hardware_name: None,
1055 device_index: None,
1056 };
1057 assert_eq!(info.stable_id(), "idn:laser-projector.local");
1058 }
1059
1060 #[test]
1061 fn test_stable_id_helios_with_hardware_name() {
1062 let info = DiscoveredDeviceInfo {
1063 dac_type: DacType::Helios,
1064 ip_address: None,
1065 mac_address: None,
1066 hostname: None,
1067 usb_address: Some("1:5".to_string()),
1068 hardware_name: Some("Helios DAC".to_string()),
1069 device_index: None,
1070 };
1071 assert_eq!(info.stable_id(), "helios:Helios DAC");
1072 }
1073
1074 #[test]
1075 fn test_stable_id_lasercube_usb_with_address() {
1076 let info = DiscoveredDeviceInfo {
1077 dac_type: DacType::LasercubeUsb,
1078 ip_address: None,
1079 mac_address: None,
1080 hostname: None,
1081 usb_address: Some("2:3".to_string()),
1082 hardware_name: None,
1083 device_index: None,
1084 };
1085 assert_eq!(info.stable_id(), "lasercube-usb:2:3");
1086 }
1087
1088 #[test]
1089 fn test_stable_id_lasercube_wifi_with_ip() {
1090 let info = DiscoveredDeviceInfo {
1091 dac_type: DacType::LasercubeWifi,
1092 ip_address: Some("192.168.1.50".parse().unwrap()),
1093 mac_address: None,
1094 hostname: None,
1095 usb_address: None,
1096 hardware_name: None,
1097 device_index: None,
1098 };
1099 assert_eq!(info.stable_id(), "lasercube-wifi:192.168.1.50");
1100 }
1101
1102 #[test]
1103 fn test_stable_id_avb_with_index() {
1104 let info = DiscoveredDeviceInfo {
1105 dac_type: DacType::Avb,
1106 ip_address: None,
1107 mac_address: None,
1108 hostname: None,
1109 usb_address: None,
1110 hardware_name: Some("MOTU AVB Main".to_string()),
1111 device_index: Some(1),
1112 };
1113 assert_eq!(info.stable_id(), "avb:motu-avb-main:1");
1114 }
1115
1116 #[test]
1117 fn test_stable_id_custom_fallback() {
1118 let info = DiscoveredDeviceInfo {
1119 dac_type: DacType::Custom("MyDAC".to_string()),
1120 ip_address: None,
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(), "unknown:Custom(\"MyDAC\")");
1129 }
1130
1131 #[test]
1132 fn test_stable_id_custom_with_ip() {
1133 let info = DiscoveredDeviceInfo {
1134 dac_type: DacType::Custom("MyDAC".to_string()),
1135 ip_address: Some("10.0.0.1".parse().unwrap()),
1136 mac_address: None,
1137 hostname: None,
1138 usb_address: None,
1139 hardware_name: None,
1140 device_index: None,
1141 };
1142 assert_eq!(info.stable_id(), "mydac:10.0.0.1");
1143 }
1144
1145 use crate::types::{DacCapabilities, LaserPoint};
1150 use crate::WriteOutcome;
1151 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1152 use std::sync::Arc;
1153
1154 #[derive(Debug, Clone)]
1156 struct MockConnectionInfo {
1157 _device_id: u32,
1158 }
1159
1160 struct MockBackend {
1162 connected: bool,
1163 }
1164
1165 impl StreamBackend for MockBackend {
1166 fn dac_type(&self) -> DacType {
1167 DacType::Custom("MockDAC".into())
1168 }
1169
1170 fn caps(&self) -> &DacCapabilities {
1171 static CAPS: DacCapabilities = DacCapabilities {
1172 pps_min: 1,
1173 pps_max: 100_000,
1174 max_points_per_chunk: 4096,
1175 output_model: crate::types::OutputModel::NetworkFifo,
1176 };
1177 &CAPS
1178 }
1179
1180 fn connect(&mut self) -> Result<()> {
1181 self.connected = true;
1182 Ok(())
1183 }
1184
1185 fn disconnect(&mut self) -> Result<()> {
1186 self.connected = false;
1187 Ok(())
1188 }
1189
1190 fn is_connected(&self) -> bool {
1191 self.connected
1192 }
1193
1194 fn try_write_chunk(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
1195 Ok(WriteOutcome::Written)
1196 }
1197
1198 fn stop(&mut self) -> Result<()> {
1199 Ok(())
1200 }
1201
1202 fn set_shutter(&mut self, _open: bool) -> Result<()> {
1203 Ok(())
1204 }
1205 }
1206
1207 struct MockExternalDiscoverer {
1209 scan_count: Arc<AtomicUsize>,
1210 connect_called: Arc<AtomicBool>,
1211 devices_to_return: Vec<(u32, Option<IpAddr>)>,
1212 }
1213
1214 impl MockExternalDiscoverer {
1215 fn new(devices: Vec<(u32, Option<IpAddr>)>) -> Self {
1216 Self {
1217 scan_count: Arc::new(AtomicUsize::new(0)),
1218 connect_called: Arc::new(AtomicBool::new(false)),
1219 devices_to_return: devices,
1220 }
1221 }
1222 }
1223
1224 impl ExternalDiscoverer for MockExternalDiscoverer {
1225 fn dac_type(&self) -> DacType {
1226 DacType::Custom("MockDAC".into())
1227 }
1228
1229 fn scan(&mut self) -> Vec<ExternalDevice> {
1230 self.scan_count.fetch_add(1, Ordering::SeqCst);
1231 self.devices_to_return
1232 .iter()
1233 .map(|(id, ip)| {
1234 let mut device = ExternalDevice::new(MockConnectionInfo { _device_id: *id });
1235 device.ip_address = *ip;
1236 device.hardware_name = Some(format!("Mock Device {}", id));
1237 device
1238 })
1239 .collect()
1240 }
1241
1242 fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>> {
1243 self.connect_called.store(true, Ordering::SeqCst);
1244 let _info = opaque_data
1245 .downcast::<MockConnectionInfo>()
1246 .map_err(|_| Error::invalid_config("wrong device type"))?;
1247 Ok(Box::new(MockBackend { connected: false }))
1248 }
1249 }
1250
1251 #[test]
1252 fn test_external_discoverer_scan_is_called() {
1253 let discoverer = MockExternalDiscoverer::new(vec![(1, Some("10.0.0.1".parse().unwrap()))]);
1254 let scan_count = discoverer.scan_count.clone();
1255
1256 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1257 discovery.register(Box::new(discoverer));
1258
1259 assert_eq!(scan_count.load(Ordering::SeqCst), 0);
1260 let devices = discovery.scan();
1261 assert_eq!(scan_count.load(Ordering::SeqCst), 1);
1262 assert_eq!(devices.len(), 1);
1263 }
1264
1265 #[test]
1266 fn test_external_discoverer_device_info() {
1267 let discoverer =
1268 MockExternalDiscoverer::new(vec![(42, Some("192.168.1.100".parse().unwrap()))]);
1269
1270 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1271 discovery.register(Box::new(discoverer));
1272
1273 let devices = discovery.scan();
1274 assert_eq!(devices.len(), 1);
1275
1276 let device = &devices[0];
1277 assert_eq!(device.dac_type(), DacType::Custom("MockDAC".into()));
1278 assert_eq!(
1279 device.info().ip_address,
1280 Some("192.168.1.100".parse().unwrap())
1281 );
1282 assert_eq!(device.info().hardware_name, Some("Mock Device 42".into()));
1283 }
1284
1285 #[test]
1286 fn test_external_discoverer_connect() {
1287 let discoverer = MockExternalDiscoverer::new(vec![(99, None)]);
1288 let connect_called = discoverer.connect_called.clone();
1289
1290 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1291 discovery.register(Box::new(discoverer));
1292
1293 let devices = discovery.scan();
1294 assert_eq!(devices.len(), 1);
1295 assert!(!connect_called.load(Ordering::SeqCst));
1296
1297 let backend = discovery.connect(devices.into_iter().next().unwrap());
1298 assert!(backend.is_ok());
1299 assert!(connect_called.load(Ordering::SeqCst));
1300
1301 let backend = backend.unwrap();
1302 assert_eq!(backend.dac_type(), DacType::Custom("MockDAC".into()));
1303 }
1304
1305 #[test]
1306 fn test_external_discoverer_multiple_devices() {
1307 let discoverer = MockExternalDiscoverer::new(vec![
1308 (1, Some("10.0.0.1".parse().unwrap())),
1309 (2, Some("10.0.0.2".parse().unwrap())),
1310 (3, None),
1311 ]);
1312
1313 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1314 discovery.register(Box::new(discoverer));
1315
1316 let devices = discovery.scan();
1317 assert_eq!(devices.len(), 3);
1318
1319 for device in devices {
1321 let backend = discovery.connect(device);
1322 assert!(backend.is_ok());
1323 }
1324 }
1325
1326 #[test]
1327 fn test_multiple_external_discoverers() {
1328 let discoverer1 = MockExternalDiscoverer::new(vec![(1, None)]);
1329 let discoverer2 = MockExternalDiscoverer::new(vec![(2, None), (3, None)]);
1330
1331 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1332 discovery.register(Box::new(discoverer1));
1333 discovery.register(Box::new(discoverer2));
1334
1335 let devices = discovery.scan();
1336 assert_eq!(devices.len(), 3);
1337 }
1338}