1use std::any::Any;
7use std::io;
8use std::net::IpAddr;
9use std::time::Duration;
10
11use crate::backend::{Error, Result, StreamBackend};
12use crate::types::{DacType, EnabledDacTypes};
13
14pub trait ExternalDiscoverer: Send {
51 fn dac_type(&self) -> DacType;
53
54 fn scan(&mut self) -> Vec<ExternalDevice>;
56
57 fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>>;
60}
61
62pub struct ExternalDevice {
68 pub ip_address: Option<IpAddr>,
70 pub mac_address: Option<[u8; 6]>,
72 pub hostname: Option<String>,
74 pub usb_address: Option<String>,
76 pub hardware_name: Option<String>,
78 pub opaque_data: Box<dyn Any + Send>,
81}
82
83impl ExternalDevice {
84 pub fn new<T: Any + Send + 'static>(opaque_data: T) -> Self {
88 Self {
89 ip_address: None,
90 mac_address: None,
91 hostname: None,
92 usb_address: None,
93 hardware_name: None,
94 opaque_data: Box::new(opaque_data),
95 }
96 }
97}
98
99#[cfg(feature = "helios")]
102use crate::backend::HeliosBackend;
103#[cfg(feature = "helios")]
104use crate::protocols::helios::{HeliosDac, HeliosDacController};
105
106#[cfg(feature = "ether-dream")]
107use crate::backend::EtherDreamBackend;
108#[cfg(feature = "ether-dream")]
109use crate::protocols::ether_dream::protocol::DacBroadcast as EtherDreamBroadcast;
110#[cfg(feature = "ether-dream")]
111use crate::protocols::ether_dream::recv_dac_broadcasts;
112
113#[cfg(feature = "idn")]
114use crate::backend::IdnBackend;
115#[cfg(feature = "idn")]
116use crate::protocols::idn::dac::ServerInfo as IdnServerInfo;
117#[cfg(feature = "idn")]
118use crate::protocols::idn::dac::ServiceInfo as IdnServiceInfo;
119#[cfg(feature = "idn")]
120use crate::protocols::idn::scan_for_servers;
121#[cfg(all(feature = "idn", feature = "testutils"))]
122use crate::protocols::idn::ServerScanner;
123#[cfg(all(feature = "idn", feature = "testutils"))]
124use std::net::SocketAddr;
125
126#[cfg(feature = "lasercube-wifi")]
127use crate::backend::LasercubeWifiBackend;
128#[cfg(feature = "lasercube-wifi")]
129use crate::protocols::lasercube_wifi::dac::Addressed as LasercubeAddressed;
130#[cfg(feature = "lasercube-wifi")]
131use crate::protocols::lasercube_wifi::discover_dacs as discover_lasercube_wifi;
132#[cfg(feature = "lasercube-wifi")]
133use crate::protocols::lasercube_wifi::protocol::DeviceInfo as LasercubeDeviceInfo;
134
135#[cfg(feature = "lasercube-usb")]
136use crate::backend::LasercubeUsbBackend;
137#[cfg(feature = "lasercube-usb")]
138use crate::protocols::lasercube_usb::rusb;
139#[cfg(feature = "lasercube-usb")]
140use crate::protocols::lasercube_usb::DacController as LasercubeUsbController;
141
142pub struct DiscoveredDevice {
150 dac_type: DacType,
151 ip_address: Option<IpAddr>,
152 mac_address: Option<[u8; 6]>,
153 hostname: Option<String>,
154 usb_address: Option<String>,
155 hardware_name: Option<String>,
156 inner: DiscoveredDeviceInner,
157}
158
159impl DiscoveredDevice {
160 pub fn name(&self) -> String {
164 self.ip_address
165 .map(|ip| ip.to_string())
166 .or_else(|| self.hardware_name.clone())
167 .or_else(|| self.usb_address.clone())
168 .unwrap_or_else(|| "Unknown".into())
169 }
170
171 pub fn dac_type(&self) -> DacType {
173 self.dac_type.clone()
174 }
175
176 pub fn info(&self) -> DiscoveredDeviceInfo {
178 DiscoveredDeviceInfo {
179 dac_type: self.dac_type.clone(),
180 ip_address: self.ip_address,
181 mac_address: self.mac_address,
182 hostname: self.hostname.clone(),
183 usb_address: self.usb_address.clone(),
184 hardware_name: self.hardware_name.clone(),
185 }
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Hash)]
194pub struct DiscoveredDeviceInfo {
195 pub dac_type: DacType,
197 pub ip_address: Option<IpAddr>,
199 pub mac_address: Option<[u8; 6]>,
201 pub hostname: Option<String>,
203 pub usb_address: Option<String>,
205 pub hardware_name: Option<String>,
207}
208
209impl DiscoveredDeviceInfo {
210 pub fn name(&self) -> String {
214 self.ip_address
215 .map(|ip| ip.to_string())
216 .or_else(|| self.hardware_name.clone())
217 .or_else(|| self.usb_address.clone())
218 .unwrap_or_else(|| "Unknown".into())
219 }
220
221 pub fn stable_id(&self) -> String {
232 match &self.dac_type {
233 DacType::EtherDream => {
234 if let Some(mac) = self.mac_address {
235 return format!(
236 "etherdream:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
237 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
238 );
239 }
240 if let Some(ip) = self.ip_address {
241 return format!("etherdream:{}", ip);
242 }
243 }
244 DacType::Idn => {
245 if let Some(ref hostname) = self.hostname {
246 return format!("idn:{}", hostname);
247 }
248 if let Some(ip) = self.ip_address {
249 return format!("idn:{}", ip);
250 }
251 }
252 DacType::Helios => {
253 if let Some(ref hw_name) = self.hardware_name {
254 return format!("helios:{}", hw_name);
255 }
256 if let Some(ref usb_addr) = self.usb_address {
257 return format!("helios:{}", usb_addr);
258 }
259 }
260 DacType::LasercubeUsb => {
261 if let Some(ref hw_name) = self.hardware_name {
262 return format!("lasercube-usb:{}", hw_name);
263 }
264 if let Some(ref usb_addr) = self.usb_address {
265 return format!("lasercube-usb:{}", usb_addr);
266 }
267 }
268 DacType::LasercubeWifi => {
269 if let Some(ip) = self.ip_address {
270 return format!("lasercube-wifi:{}", ip);
271 }
272 }
273 DacType::Custom(name) => {
274 if let Some(ip) = self.ip_address {
276 return format!("{}:{}", name.to_lowercase(), ip);
277 }
278 if let Some(ref hw_name) = self.hardware_name {
279 return format!("{}:{}", name.to_lowercase(), hw_name);
280 }
281 }
282 }
283
284 format!("unknown:{:?}", self.dac_type)
286 }
287}
288
289enum DiscoveredDeviceInner {
291 #[cfg(feature = "helios")]
292 Helios(HeliosDac),
293 #[cfg(feature = "ether-dream")]
294 EtherDream {
295 broadcast: EtherDreamBroadcast,
296 ip: IpAddr,
297 },
298 #[cfg(feature = "idn")]
299 Idn {
300 server: IdnServerInfo,
301 service: IdnServiceInfo,
302 },
303 #[cfg(feature = "lasercube-wifi")]
304 LasercubeWifi {
305 info: LasercubeDeviceInfo,
306 source_addr: std::net::SocketAddr,
307 },
308 #[cfg(feature = "lasercube-usb")]
309 LasercubeUsb(rusb::Device<rusb::Context>),
310 External {
312 discoverer_index: usize,
314 opaque_data: Box<dyn Any + Send>,
316 },
317 #[cfg(not(any(
319 feature = "helios",
320 feature = "ether-dream",
321 feature = "idn",
322 feature = "lasercube-wifi",
323 feature = "lasercube-usb"
324 )))]
325 _Placeholder,
326}
327
328#[cfg(feature = "helios")]
334pub struct HeliosDiscovery {
335 controller: HeliosDacController,
336}
337
338#[cfg(feature = "helios")]
339impl HeliosDiscovery {
340 pub fn new() -> Option<Self> {
344 HeliosDacController::new()
345 .ok()
346 .map(|controller| Self { controller })
347 }
348
349 pub fn scan(&self) -> Vec<DiscoveredDevice> {
351 let Ok(devices) = self.controller.list_devices() else {
352 return Vec::new();
353 };
354
355 let mut discovered = Vec::new();
356 for device in devices {
357 let HeliosDac::Idle(_) = &device else {
359 continue;
360 };
361
362 let opened = match device.open() {
364 Ok(o) => o,
365 Err(_) => continue,
366 };
367
368 let hardware_name = opened.name().unwrap_or_else(|_| "Unknown Helios".into());
369 discovered.push(DiscoveredDevice {
370 dac_type: DacType::Helios,
371 ip_address: None,
372 mac_address: None,
373 hostname: None,
374 usb_address: None,
375 hardware_name: Some(hardware_name),
376 inner: DiscoveredDeviceInner::Helios(opened),
377 });
378 }
379 discovered
380 }
381
382 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
384 let DiscoveredDeviceInner::Helios(dac) = device.inner else {
385 return Err(Error::invalid_config("Invalid device type for Helios"));
386 };
387 Ok(Box::new(HeliosBackend::from_dac(dac)))
388 }
389}
390
391#[cfg(feature = "ether-dream")]
393pub struct EtherDreamDiscovery {
394 timeout: Duration,
395}
396
397#[cfg(feature = "ether-dream")]
398impl EtherDreamDiscovery {
399 pub fn new() -> Self {
401 Self {
402 timeout: Duration::from_millis(1500),
405 }
406 }
407
408 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
410 let Ok(mut rx) = recv_dac_broadcasts() else {
411 return Vec::new();
412 };
413
414 if rx.set_timeout(Some(self.timeout)).is_err() {
415 return Vec::new();
416 }
417
418 let mut discovered = Vec::new();
419 let mut seen_macs = std::collections::HashSet::new();
420
421 for _ in 0..3 {
423 let (broadcast, source_addr) = match rx.next_broadcast() {
424 Ok(b) => b,
425 Err(_) => break,
426 };
427
428 let ip = source_addr.ip();
429
430 if seen_macs.contains(&broadcast.mac_address) {
432 continue;
433 }
434 seen_macs.insert(broadcast.mac_address);
435
436 discovered.push(DiscoveredDevice {
437 dac_type: DacType::EtherDream,
438 ip_address: Some(ip),
439 mac_address: Some(broadcast.mac_address),
440 hostname: None,
441 usb_address: None,
442 hardware_name: None,
443 inner: DiscoveredDeviceInner::EtherDream { broadcast, ip },
444 });
445 }
446 discovered
447 }
448
449 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
451 let DiscoveredDeviceInner::EtherDream { broadcast, ip } = device.inner else {
452 return Err(Error::invalid_config("Invalid device type for EtherDream"));
453 };
454
455 let backend = EtherDreamBackend::new(broadcast, ip);
456 Ok(Box::new(backend))
457 }
458}
459
460#[cfg(feature = "ether-dream")]
461impl Default for EtherDreamDiscovery {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467#[cfg(feature = "idn")]
469pub struct IdnDiscovery {
470 scan_timeout: Duration,
471}
472
473#[cfg(feature = "idn")]
474impl IdnDiscovery {
475 pub fn new() -> Self {
477 Self {
478 scan_timeout: Duration::from_millis(500),
479 }
480 }
481
482 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
484 let Ok(servers) = scan_for_servers(self.scan_timeout) else {
485 return Vec::new();
486 };
487
488 let mut discovered = Vec::new();
489 for server in servers {
490 let Some(service) = server.find_laser_projector().cloned() else {
491 continue;
492 };
493
494 let ip_address = server.addresses.first().map(|addr| addr.ip());
495 let hostname = server.hostname.clone();
496
497 discovered.push(DiscoveredDevice {
498 dac_type: DacType::Idn,
499 ip_address,
500 mac_address: None,
501 hostname: Some(hostname),
502 usb_address: None,
503 hardware_name: None,
504 inner: DiscoveredDeviceInner::Idn { server, service },
505 });
506 }
507 discovered
508 }
509
510 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
512 let DiscoveredDeviceInner::Idn { server, service } = device.inner else {
513 return Err(Error::invalid_config("Invalid device type for IDN"));
514 };
515
516 Ok(Box::new(IdnBackend::new(server, service)))
517 }
518
519 #[cfg(feature = "testutils")]
526 pub fn scan_address(&mut self, addr: SocketAddr) -> Vec<DiscoveredDevice> {
527 let Ok(mut scanner) = ServerScanner::new(0) else {
528 return Vec::new();
529 };
530
531 let Ok(servers) = scanner.scan_address(addr, self.scan_timeout) else {
532 return Vec::new();
533 };
534
535 let mut discovered = Vec::new();
536 for server in servers {
537 let Some(service) = server.find_laser_projector().cloned() else {
538 continue;
539 };
540
541 let ip_address = server.addresses.first().map(|addr| addr.ip());
542 let hostname = server.hostname.clone();
543
544 discovered.push(DiscoveredDevice {
545 dac_type: DacType::Idn,
546 ip_address,
547 mac_address: None,
548 hostname: Some(hostname),
549 usb_address: None,
550 hardware_name: None,
551 inner: DiscoveredDeviceInner::Idn { server, service },
552 });
553 }
554 discovered
555 }
556}
557
558#[cfg(feature = "idn")]
559impl Default for IdnDiscovery {
560 fn default() -> Self {
561 Self::new()
562 }
563}
564
565#[cfg(feature = "lasercube-wifi")]
567pub struct LasercubeWifiDiscovery {
568 timeout: Duration,
569}
570
571#[cfg(feature = "lasercube-wifi")]
572impl LasercubeWifiDiscovery {
573 pub fn new() -> Self {
575 Self {
576 timeout: Duration::from_millis(100),
577 }
578 }
579
580 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
582 let Ok(mut discovery) = discover_lasercube_wifi() else {
583 return Vec::new();
584 };
585
586 if discovery.set_timeout(Some(self.timeout)).is_err() {
587 return Vec::new();
588 }
589
590 let mut discovered = Vec::new();
591 for _ in 0..10 {
592 let (device_info, source_addr) = match discovery.next_device() {
593 Ok(d) => d,
594 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
595 Err(e) if e.kind() == io::ErrorKind::TimedOut => break,
596 Err(_) => continue,
597 };
598
599 let ip_address = source_addr.ip();
600
601 discovered.push(DiscoveredDevice {
602 dac_type: DacType::LasercubeWifi,
603 ip_address: Some(ip_address),
604 mac_address: None,
605 hostname: None,
606 usb_address: None,
607 hardware_name: None,
608 inner: DiscoveredDeviceInner::LasercubeWifi {
609 info: device_info,
610 source_addr,
611 },
612 });
613 }
614 discovered
615 }
616
617 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
619 let DiscoveredDeviceInner::LasercubeWifi { info, source_addr } = device.inner else {
620 return Err(Error::invalid_config(
621 "Invalid device type for LaserCube WiFi",
622 ));
623 };
624
625 let addressed = LasercubeAddressed::from_discovery(&info, source_addr);
626 Ok(Box::new(LasercubeWifiBackend::new(addressed)))
627 }
628}
629
630#[cfg(feature = "lasercube-wifi")]
631impl Default for LasercubeWifiDiscovery {
632 fn default() -> Self {
633 Self::new()
634 }
635}
636
637#[cfg(feature = "lasercube-usb")]
639pub struct LasercubeUsbDiscovery {
640 controller: LasercubeUsbController,
641}
642
643#[cfg(feature = "lasercube-usb")]
644impl LasercubeUsbDiscovery {
645 pub fn new() -> Option<Self> {
649 LasercubeUsbController::new()
650 .ok()
651 .map(|controller| Self { controller })
652 }
653
654 pub fn scan(&self) -> Vec<DiscoveredDevice> {
656 let Ok(devices) = self.controller.list_devices() else {
657 return Vec::new();
658 };
659
660 let mut discovered = Vec::new();
661 for device in devices {
662 let usb_address = format!("{}:{}", device.bus_number(), device.address());
663 let serial = crate::protocols::lasercube_usb::get_serial_number(&device);
664
665 discovered.push(DiscoveredDevice {
666 dac_type: DacType::LasercubeUsb,
667 ip_address: None,
668 mac_address: None,
669 hostname: None,
670 usb_address: Some(usb_address),
671 hardware_name: serial,
672 inner: DiscoveredDeviceInner::LasercubeUsb(device),
673 });
674 }
675 discovered
676 }
677
678 pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
680 let DiscoveredDeviceInner::LasercubeUsb(usb_device) = device.inner else {
681 return Err(Error::invalid_config(
682 "Invalid device type for LaserCube USB",
683 ));
684 };
685
686 let backend = LasercubeUsbBackend::new(usb_device);
687 Ok(Box::new(backend))
688 }
689}
690
691pub struct DacDiscovery {
700 #[cfg(feature = "helios")]
701 helios: Option<HeliosDiscovery>,
702 #[cfg(feature = "ether-dream")]
703 etherdream: EtherDreamDiscovery,
704 #[cfg(feature = "idn")]
705 idn: IdnDiscovery,
706 #[cfg(all(feature = "idn", feature = "testutils"))]
707 idn_scan_addresses: Vec<SocketAddr>,
708 #[cfg(feature = "lasercube-wifi")]
709 lasercube_wifi: LasercubeWifiDiscovery,
710 #[cfg(feature = "lasercube-usb")]
711 lasercube_usb: Option<LasercubeUsbDiscovery>,
712 enabled: EnabledDacTypes,
713 external: Vec<Box<dyn ExternalDiscoverer>>,
715}
716
717impl DacDiscovery {
718 pub fn new(enabled: EnabledDacTypes) -> Self {
724 Self {
725 #[cfg(feature = "helios")]
726 helios: HeliosDiscovery::new(),
727 #[cfg(feature = "ether-dream")]
728 etherdream: EtherDreamDiscovery::new(),
729 #[cfg(feature = "idn")]
730 idn: IdnDiscovery::new(),
731 #[cfg(all(feature = "idn", feature = "testutils"))]
732 idn_scan_addresses: Vec::new(),
733 #[cfg(feature = "lasercube-wifi")]
734 lasercube_wifi: LasercubeWifiDiscovery::new(),
735 #[cfg(feature = "lasercube-usb")]
736 lasercube_usb: LasercubeUsbDiscovery::new(),
737 enabled,
738 external: Vec::new(),
739 }
740 }
741
742 #[cfg(all(feature = "idn", feature = "testutils"))]
749 pub fn set_idn_scan_addresses(&mut self, addresses: Vec<SocketAddr>) {
750 self.idn_scan_addresses = addresses;
751 }
752
753 pub fn set_enabled(&mut self, enabled: EnabledDacTypes) {
755 self.enabled = enabled;
756 }
757
758 pub fn enabled(&self) -> &EnabledDacTypes {
760 &self.enabled
761 }
762
763 pub fn register(&mut self, discoverer: Box<dyn ExternalDiscoverer>) {
779 self.external.push(discoverer);
780 }
781
782 pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
787 let mut devices = Vec::new();
788
789 #[cfg(feature = "helios")]
791 if self.enabled.is_enabled(DacType::Helios) {
792 if let Some(ref discovery) = self.helios {
793 devices.extend(discovery.scan());
794 }
795 }
796
797 #[cfg(feature = "ether-dream")]
799 if self.enabled.is_enabled(DacType::EtherDream) {
800 devices.extend(self.etherdream.scan());
801 }
802
803 #[cfg(feature = "idn")]
805 if self.enabled.is_enabled(DacType::Idn) {
806 #[cfg(feature = "testutils")]
807 {
808 if self.idn_scan_addresses.is_empty() {
809 devices.extend(self.idn.scan());
811 } else {
812 for addr in &self.idn_scan_addresses {
814 devices.extend(self.idn.scan_address(*addr));
815 }
816 }
817 }
818 #[cfg(not(feature = "testutils"))]
819 {
820 devices.extend(self.idn.scan());
821 }
822 }
823
824 #[cfg(feature = "lasercube-wifi")]
826 if self.enabled.is_enabled(DacType::LasercubeWifi) {
827 devices.extend(self.lasercube_wifi.scan());
828 }
829
830 #[cfg(feature = "lasercube-usb")]
832 if self.enabled.is_enabled(DacType::LasercubeUsb) {
833 if let Some(ref discovery) = self.lasercube_usb {
834 devices.extend(discovery.scan());
835 }
836 }
837
838 for (index, discoverer) in self.external.iter_mut().enumerate() {
840 let dac_type = discoverer.dac_type();
841 for ext_device in discoverer.scan() {
842 devices.push(DiscoveredDevice {
843 dac_type: dac_type.clone(),
844 ip_address: ext_device.ip_address,
845 mac_address: ext_device.mac_address,
846 hostname: ext_device.hostname,
847 usb_address: ext_device.usb_address,
848 hardware_name: ext_device.hardware_name,
849 inner: DiscoveredDeviceInner::External {
850 discoverer_index: index,
851 opaque_data: ext_device.opaque_data,
852 },
853 });
854 }
855 }
856
857 devices
858 }
859
860 #[allow(unreachable_patterns)]
862 pub fn connect(&mut self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
863 if let DiscoveredDeviceInner::External {
865 discoverer_index,
866 opaque_data,
867 } = device.inner
868 {
869 return self
870 .external
871 .get_mut(discoverer_index)
872 .ok_or_else(|| Error::invalid_config("External discoverer not found"))?
873 .connect(opaque_data);
874 }
875
876 match device.dac_type {
878 #[cfg(feature = "helios")]
879 DacType::Helios => self
880 .helios
881 .as_ref()
882 .ok_or_else(|| Error::disconnected("Helios discovery not available"))?
883 .connect(device),
884 #[cfg(feature = "ether-dream")]
885 DacType::EtherDream => self.etherdream.connect(device),
886 #[cfg(feature = "idn")]
887 DacType::Idn => self.idn.connect(device),
888 #[cfg(feature = "lasercube-wifi")]
889 DacType::LasercubeWifi => self.lasercube_wifi.connect(device),
890 #[cfg(feature = "lasercube-usb")]
891 DacType::LasercubeUsb => self
892 .lasercube_usb
893 .as_ref()
894 .ok_or_else(|| Error::disconnected("LaserCube USB discovery not available"))?
895 .connect(device),
896 _ => Err(Error::invalid_config(format!(
897 "DAC type {:?} not supported in this build",
898 device.dac_type
899 ))),
900 }
901 }
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907
908 #[test]
909 fn test_stable_id_etherdream_with_mac() {
910 let info = DiscoveredDeviceInfo {
911 dac_type: DacType::EtherDream,
912 ip_address: Some("192.168.1.100".parse().unwrap()),
913 mac_address: Some([0x01, 0x23, 0x45, 0x67, 0x89, 0xab]),
914 hostname: None,
915 usb_address: None,
916 hardware_name: None,
917 };
918 assert_eq!(info.stable_id(), "etherdream:01:23:45:67:89:ab");
919 }
920
921 #[test]
922 fn test_stable_id_idn_with_hostname() {
923 let info = DiscoveredDeviceInfo {
924 dac_type: DacType::Idn,
925 ip_address: Some("192.168.1.100".parse().unwrap()),
926 mac_address: None,
927 hostname: Some("laser-projector.local".to_string()),
928 usb_address: None,
929 hardware_name: None,
930 };
931 assert_eq!(info.stable_id(), "idn:laser-projector.local");
932 }
933
934 #[test]
935 fn test_stable_id_helios_with_hardware_name() {
936 let info = DiscoveredDeviceInfo {
937 dac_type: DacType::Helios,
938 ip_address: None,
939 mac_address: None,
940 hostname: None,
941 usb_address: Some("1:5".to_string()),
942 hardware_name: Some("Helios DAC".to_string()),
943 };
944 assert_eq!(info.stable_id(), "helios:Helios DAC");
945 }
946
947 #[test]
948 fn test_stable_id_lasercube_usb_with_address() {
949 let info = DiscoveredDeviceInfo {
950 dac_type: DacType::LasercubeUsb,
951 ip_address: None,
952 mac_address: None,
953 hostname: None,
954 usb_address: Some("2:3".to_string()),
955 hardware_name: None,
956 };
957 assert_eq!(info.stable_id(), "lasercube-usb:2:3");
958 }
959
960 #[test]
961 fn test_stable_id_lasercube_wifi_with_ip() {
962 let info = DiscoveredDeviceInfo {
963 dac_type: DacType::LasercubeWifi,
964 ip_address: Some("192.168.1.50".parse().unwrap()),
965 mac_address: None,
966 hostname: None,
967 usb_address: None,
968 hardware_name: None,
969 };
970 assert_eq!(info.stable_id(), "lasercube-wifi:192.168.1.50");
971 }
972
973 #[test]
974 fn test_stable_id_custom_fallback() {
975 let info = DiscoveredDeviceInfo {
976 dac_type: DacType::Custom("MyDAC".to_string()),
977 ip_address: None,
978 mac_address: None,
979 hostname: None,
980 usb_address: None,
981 hardware_name: None,
982 };
983 assert_eq!(info.stable_id(), "unknown:Custom(\"MyDAC\")");
985 }
986
987 #[test]
988 fn test_stable_id_custom_with_ip() {
989 let info = DiscoveredDeviceInfo {
990 dac_type: DacType::Custom("MyDAC".to_string()),
991 ip_address: Some("10.0.0.1".parse().unwrap()),
992 mac_address: None,
993 hostname: None,
994 usb_address: None,
995 hardware_name: None,
996 };
997 assert_eq!(info.stable_id(), "mydac:10.0.0.1");
998 }
999
1000 use crate::types::{DacCapabilities, LaserPoint};
1005 use crate::WriteOutcome;
1006 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1007 use std::sync::Arc;
1008
1009 #[derive(Debug, Clone)]
1011 struct MockConnectionInfo {
1012 _device_id: u32,
1013 }
1014
1015 struct MockBackend {
1017 connected: bool,
1018 }
1019
1020 impl StreamBackend for MockBackend {
1021 fn dac_type(&self) -> DacType {
1022 DacType::Custom("MockDAC".into())
1023 }
1024
1025 fn caps(&self) -> &DacCapabilities {
1026 static CAPS: DacCapabilities = DacCapabilities {
1027 pps_min: 1,
1028 pps_max: 100_000,
1029 max_points_per_chunk: 4096,
1030 prefers_constant_pps: false,
1031 can_estimate_queue: false,
1032 output_model: crate::types::OutputModel::NetworkFifo,
1033 };
1034 &CAPS
1035 }
1036
1037 fn connect(&mut self) -> Result<()> {
1038 self.connected = true;
1039 Ok(())
1040 }
1041
1042 fn disconnect(&mut self) -> Result<()> {
1043 self.connected = false;
1044 Ok(())
1045 }
1046
1047 fn is_connected(&self) -> bool {
1048 self.connected
1049 }
1050
1051 fn try_write_chunk(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
1052 Ok(WriteOutcome::Written)
1053 }
1054
1055 fn stop(&mut self) -> Result<()> {
1056 Ok(())
1057 }
1058
1059 fn set_shutter(&mut self, _open: bool) -> Result<()> {
1060 Ok(())
1061 }
1062 }
1063
1064 struct MockExternalDiscoverer {
1066 scan_count: Arc<AtomicUsize>,
1067 connect_called: Arc<AtomicBool>,
1068 devices_to_return: Vec<(u32, Option<IpAddr>)>,
1069 }
1070
1071 impl MockExternalDiscoverer {
1072 fn new(devices: Vec<(u32, Option<IpAddr>)>) -> Self {
1073 Self {
1074 scan_count: Arc::new(AtomicUsize::new(0)),
1075 connect_called: Arc::new(AtomicBool::new(false)),
1076 devices_to_return: devices,
1077 }
1078 }
1079 }
1080
1081 impl ExternalDiscoverer for MockExternalDiscoverer {
1082 fn dac_type(&self) -> DacType {
1083 DacType::Custom("MockDAC".into())
1084 }
1085
1086 fn scan(&mut self) -> Vec<ExternalDevice> {
1087 self.scan_count.fetch_add(1, Ordering::SeqCst);
1088 self.devices_to_return
1089 .iter()
1090 .map(|(id, ip)| {
1091 let mut device = ExternalDevice::new(MockConnectionInfo { _device_id: *id });
1092 device.ip_address = *ip;
1093 device.hardware_name = Some(format!("Mock Device {}", id));
1094 device
1095 })
1096 .collect()
1097 }
1098
1099 fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>> {
1100 self.connect_called.store(true, Ordering::SeqCst);
1101 let _info = opaque_data
1102 .downcast::<MockConnectionInfo>()
1103 .map_err(|_| Error::invalid_config("wrong device type"))?;
1104 Ok(Box::new(MockBackend { connected: false }))
1105 }
1106 }
1107
1108 #[test]
1109 fn test_external_discoverer_scan_is_called() {
1110 let discoverer = MockExternalDiscoverer::new(vec![(1, Some("10.0.0.1".parse().unwrap()))]);
1111 let scan_count = discoverer.scan_count.clone();
1112
1113 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1114 discovery.register(Box::new(discoverer));
1115
1116 assert_eq!(scan_count.load(Ordering::SeqCst), 0);
1117 let devices = discovery.scan();
1118 assert_eq!(scan_count.load(Ordering::SeqCst), 1);
1119 assert_eq!(devices.len(), 1);
1120 }
1121
1122 #[test]
1123 fn test_external_discoverer_device_info() {
1124 let discoverer =
1125 MockExternalDiscoverer::new(vec![(42, Some("192.168.1.100".parse().unwrap()))]);
1126
1127 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1128 discovery.register(Box::new(discoverer));
1129
1130 let devices = discovery.scan();
1131 assert_eq!(devices.len(), 1);
1132
1133 let device = &devices[0];
1134 assert_eq!(device.dac_type(), DacType::Custom("MockDAC".into()));
1135 assert_eq!(
1136 device.info().ip_address,
1137 Some("192.168.1.100".parse().unwrap())
1138 );
1139 assert_eq!(device.info().hardware_name, Some("Mock Device 42".into()));
1140 }
1141
1142 #[test]
1143 fn test_external_discoverer_connect() {
1144 let discoverer = MockExternalDiscoverer::new(vec![(99, None)]);
1145 let connect_called = discoverer.connect_called.clone();
1146
1147 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1148 discovery.register(Box::new(discoverer));
1149
1150 let devices = discovery.scan();
1151 assert_eq!(devices.len(), 1);
1152 assert!(!connect_called.load(Ordering::SeqCst));
1153
1154 let backend = discovery.connect(devices.into_iter().next().unwrap());
1155 assert!(backend.is_ok());
1156 assert!(connect_called.load(Ordering::SeqCst));
1157
1158 let backend = backend.unwrap();
1159 assert_eq!(backend.dac_type(), DacType::Custom("MockDAC".into()));
1160 }
1161
1162 #[test]
1163 fn test_external_discoverer_multiple_devices() {
1164 let discoverer = MockExternalDiscoverer::new(vec![
1165 (1, Some("10.0.0.1".parse().unwrap())),
1166 (2, Some("10.0.0.2".parse().unwrap())),
1167 (3, None),
1168 ]);
1169
1170 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1171 discovery.register(Box::new(discoverer));
1172
1173 let devices = discovery.scan();
1174 assert_eq!(devices.len(), 3);
1175
1176 for device in devices {
1178 let backend = discovery.connect(device);
1179 assert!(backend.is_ok());
1180 }
1181 }
1182
1183 #[test]
1184 fn test_multiple_external_discoverers() {
1185 let discoverer1 = MockExternalDiscoverer::new(vec![(1, None)]);
1186 let discoverer2 = MockExternalDiscoverer::new(vec![(2, None), (3, None)]);
1187
1188 let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1189 discovery.register(Box::new(discoverer1));
1190 discovery.register(Box::new(discoverer2));
1191
1192 let devices = discovery.scan();
1193 assert_eq!(devices.len(), 3);
1194 }
1195}