Skip to main content

laser_dac/
discovery.rs

1//! DAC device discovery.
2//!
3//! Provides a DAC interface for discovering and connecting to laser DAC devices
4//! from multiple manufacturers.
5
6use 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
14// =============================================================================
15// External Discoverer Support
16// =============================================================================
17
18/// Trait for external DAC discovery implementations.
19///
20/// External crates implement this to integrate their DAC discovery
21/// with the unified `DacDiscovery` system.
22///
23/// # Example
24///
25/// ```ignore
26/// use laser_dac::{
27///     DacDiscovery, ExternalDiscoverer, ExternalDevice,
28///     StreamBackend, DacType, EnabledDacTypes,
29/// };
30/// use std::any::Any;
31///
32/// struct MyClosedDacDiscoverer { /* ... */ }
33///
34/// impl ExternalDiscoverer for MyClosedDacDiscoverer {
35///     fn dac_type(&self) -> DacType {
36///         DacType::Custom("MyClosedDAC".into())
37///     }
38///
39///     fn scan(&mut self) -> Vec<ExternalDevice> {
40///         // Your discovery logic here
41///         vec![]
42///     }
43///
44///     fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>> {
45///         // Your connection logic here
46///         todo!()
47///     }
48/// }
49/// ```
50pub trait ExternalDiscoverer: Send {
51    /// Returns the DAC type this discoverer handles.
52    fn dac_type(&self) -> DacType;
53
54    /// Scan for devices. Called during `DacDiscovery::scan()`.
55    fn scan(&mut self) -> Vec<ExternalDevice>;
56
57    /// Connect to a previously discovered device.
58    /// The `opaque_data` is the same data returned in `ExternalDevice`.
59    fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>>;
60}
61
62/// Device info returned by external discoverers.
63///
64/// This struct contains the common fields that `DacDiscovery` uses to create
65/// a `DiscoveredDevice`. The `opaque_data` field stores protocol-specific
66/// connection information that will be passed back to `connect()`.
67pub struct ExternalDevice {
68    /// IP address for network devices.
69    pub ip_address: Option<IpAddr>,
70    /// MAC address if available.
71    pub mac_address: Option<[u8; 6]>,
72    /// Hostname if available.
73    pub hostname: Option<String>,
74    /// USB address (e.g., "bus:device") for USB devices.
75    pub usb_address: Option<String>,
76    /// Hardware/device name if available.
77    pub hardware_name: Option<String>,
78    /// Opaque data passed back to `connect()`.
79    /// Store whatever your protocol needs to establish a connection.
80    pub opaque_data: Box<dyn Any + Send>,
81}
82
83impl ExternalDevice {
84    /// Create a new external device with the given opaque data.
85    ///
86    /// All other fields default to `None`.
87    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// Feature-gated imports from internal protocol modules
100
101#[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
142// =============================================================================
143// DiscoveredDevice
144// =============================================================================
145
146/// A discovered but not-yet-connected DAC device.
147///
148/// Use `DacDiscovery::connect()` to establish a connection and get a backend.
149pub 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    /// Returns the device name (unique identifier).
161    /// For network devices: IP address.
162    /// For USB devices: hardware name or bus:address.
163    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    /// Returns the DAC type.
172    pub fn dac_type(&self) -> DacType {
173        self.dac_type.clone()
174    }
175
176    /// Returns a lightweight, cloneable info struct for this device.
177    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/// Lightweight info about a discovered device.
190///
191/// This struct is Clone-able and can be used for filtering and reporting
192/// without consuming the original `DiscoveredDevice`.
193#[derive(Debug, Clone, PartialEq, Eq, Hash)]
194pub struct DiscoveredDeviceInfo {
195    /// The DAC type.
196    pub dac_type: DacType,
197    /// IP address for network devices, None for USB devices.
198    pub ip_address: Option<IpAddr>,
199    /// MAC address (Ether Dream only).
200    pub mac_address: Option<[u8; 6]>,
201    /// Hostname (IDN only).
202    pub hostname: Option<String>,
203    /// USB bus:address (LaserCube USB only).
204    pub usb_address: Option<String>,
205    /// Device name from hardware (Helios only).
206    pub hardware_name: Option<String>,
207}
208
209impl DiscoveredDeviceInfo {
210    /// Returns the device name (human-readable).
211    /// For network devices: IP address.
212    /// For USB devices: hardware name or bus:address.
213    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    /// Returns a stable, namespaced identifier for the device.
222    ///
223    /// The ID is prefixed with the protocol name to avoid cross-protocol collisions:
224    /// - Ether Dream: `etherdream:<mac>` (survives IP changes)
225    /// - IDN: `idn:<hostname>` (mDNS name, survives IP changes)
226    /// - Helios: `helios:<hardware_name>` (USB serial if available)
227    /// - LaserCube USB: `lasercube-usb:<serial|bus:addr>`
228    /// - LaserCube WiFi: `lasercube-wifi:<ip>` (best available)
229    ///
230    /// This is used for device tracking/deduplication.
231    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                // For custom types, use the custom name as prefix
275                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        // Fallback for unknown configurations
285        format!("unknown:{:?}", self.dac_type)
286    }
287}
288
289/// Internal data needed for connection (opaque to callers).
290enum 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 discoverer device.
311    External {
312        /// Index into `DacDiscovery.external` for the discoverer that found this device.
313        discoverer_index: usize,
314        /// Opaque data passed back to `ExternalDiscoverer::connect()`.
315        opaque_data: Box<dyn Any + Send>,
316    },
317    /// Placeholder variant to ensure enum is not empty when no features are enabled
318    #[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// =============================================================================
329// Per-DAC Discovery Implementations
330// =============================================================================
331
332/// Discovery for Helios USB DACs.
333#[cfg(feature = "helios")]
334pub struct HeliosDiscovery {
335    controller: HeliosDacController,
336}
337
338#[cfg(feature = "helios")]
339impl HeliosDiscovery {
340    /// Create a new Helios discovery instance.
341    ///
342    /// Returns None if the USB controller fails to initialize.
343    pub fn new() -> Option<Self> {
344        HeliosDacController::new()
345            .ok()
346            .map(|controller| Self { controller })
347    }
348
349    /// Scan for available Helios devices.
350    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            // Only process idle (unopened) devices
358            let HeliosDac::Idle(_) = &device else {
359                continue;
360            };
361
362            // Try to open to get name
363            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    /// Connect to a discovered Helios device.
383    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/// Discovery for Ether Dream network DACs.
392#[cfg(feature = "ether-dream")]
393pub struct EtherDreamDiscovery {
394    timeout: Duration,
395}
396
397#[cfg(feature = "ether-dream")]
398impl EtherDreamDiscovery {
399    /// Create a new Ether Dream discovery instance.
400    pub fn new() -> Self {
401        Self {
402            // Ether Dream DACs broadcast once per second, so we need
403            // at least 1.5s to reliably catch a broadcast
404            timeout: Duration::from_millis(1500),
405        }
406    }
407
408    /// Scan for available Ether Dream devices.
409    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        // Only try 3 iterations max - we just need one device
422        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            // Skip duplicate MACs - but keep polling to find other devices
431            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    /// Connect to a discovered Ether Dream device.
450    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/// Discovery for IDN (ILDA Digital Network) DACs.
468#[cfg(feature = "idn")]
469pub struct IdnDiscovery {
470    scan_timeout: Duration,
471}
472
473#[cfg(feature = "idn")]
474impl IdnDiscovery {
475    /// Create a new IDN discovery instance.
476    pub fn new() -> Self {
477        Self {
478            scan_timeout: Duration::from_millis(500),
479        }
480    }
481
482    /// Scan for available IDN devices.
483    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    /// Connect to a discovered IDN device.
511    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    /// Scan a specific address for IDN devices.
520    ///
521    /// This is useful for testing with mock servers on localhost where
522    /// broadcast won't work.
523    ///
524    /// This method is only available with the `testutils` feature.
525    #[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/// Discovery for LaserCube WiFi DACs.
566#[cfg(feature = "lasercube-wifi")]
567pub struct LasercubeWifiDiscovery {
568    timeout: Duration,
569}
570
571#[cfg(feature = "lasercube-wifi")]
572impl LasercubeWifiDiscovery {
573    /// Create a new LaserCube WiFi discovery instance.
574    pub fn new() -> Self {
575        Self {
576            timeout: Duration::from_millis(100),
577        }
578    }
579
580    /// Scan for available LaserCube WiFi devices.
581    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    /// Connect to a discovered LaserCube WiFi device.
618    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/// Discovery for LaserCube USB DACs (LaserDock).
638#[cfg(feature = "lasercube-usb")]
639pub struct LasercubeUsbDiscovery {
640    controller: LasercubeUsbController,
641}
642
643#[cfg(feature = "lasercube-usb")]
644impl LasercubeUsbDiscovery {
645    /// Create a new LaserCube USB discovery instance.
646    ///
647    /// Returns None if the USB controller fails to initialize.
648    pub fn new() -> Option<Self> {
649        LasercubeUsbController::new()
650            .ok()
651            .map(|controller| Self { controller })
652    }
653
654    /// Scan for available LaserCube USB devices.
655    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    /// Connect to a discovered LaserCube USB device.
679    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
691// =============================================================================
692// DAC Discovery
693// =============================================================================
694
695/// DAC discovery coordinator for all DAC types.
696///
697/// This provides a single entry point for discovering and connecting to any
698/// supported DAC hardware.
699pub 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 discoverers registered by external crates.
714    external: Vec<Box<dyn ExternalDiscoverer>>,
715}
716
717impl DacDiscovery {
718    /// Create a new DAC discovery instance.
719    ///
720    /// This initializes USB controllers, so it should be called from the main thread.
721    /// If a USB controller fails to initialize, that DAC type will be unavailable
722    /// but other types will still work.
723    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    /// Set specific addresses to scan for IDN servers.
743    ///
744    /// When set, the scanner will scan these specific addresses instead of
745    /// using broadcast discovery. This is useful for testing with mock servers.
746    ///
747    /// This method is only available with the `testutils` feature.
748    #[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    /// Update which DAC types to scan for.
754    pub fn set_enabled(&mut self, enabled: EnabledDacTypes) {
755        self.enabled = enabled;
756    }
757
758    /// Returns the currently enabled DAC types.
759    pub fn enabled(&self) -> &EnabledDacTypes {
760        &self.enabled
761    }
762
763    /// Register an external discoverer.
764    ///
765    /// External discoverers are called during `scan()` to find additional devices
766    /// beyond the built-in DAC types. This allows external crates to integrate
767    /// their own DAC discovery with the unified discovery system.
768    ///
769    /// # Example
770    ///
771    /// ```ignore
772    /// let mut discovery = DacDiscovery::new(EnabledDacTypes::all());
773    /// discovery.register(Box::new(MyClosedDacDiscoverer::new()));
774    ///
775    /// // Now scan() will include devices from the external discoverer
776    /// let devices = discovery.scan();
777    /// ```
778    pub fn register(&mut self, discoverer: Box<dyn ExternalDiscoverer>) {
779        self.external.push(discoverer);
780    }
781
782    /// Scan for available DAC devices of all enabled types.
783    ///
784    /// Returns a list of discovered devices. Each device can be connected
785    /// using `connect()`.
786    pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
787        let mut devices = Vec::new();
788
789        // Helios
790        #[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        // Ether Dream
798        #[cfg(feature = "ether-dream")]
799        if self.enabled.is_enabled(DacType::EtherDream) {
800            devices.extend(self.etherdream.scan());
801        }
802
803        // IDN
804        #[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                    // Use broadcast discovery
810                    devices.extend(self.idn.scan());
811                } else {
812                    // Scan specific addresses (for testing with mock servers)
813                    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        // LaserCube WiFi
825        #[cfg(feature = "lasercube-wifi")]
826        if self.enabled.is_enabled(DacType::LasercubeWifi) {
827            devices.extend(self.lasercube_wifi.scan());
828        }
829
830        // LaserCube USB
831        #[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        // External discoverers
839        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    /// Connect to a discovered device and return a streaming backend.
861    #[allow(unreachable_patterns)]
862    pub fn connect(&mut self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
863        // Handle external devices first (check inner variant)
864        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        // Handle built-in DAC types
877        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        // Custom with no identifiers falls back to unknown format
984        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    // =========================================================================
1001    // External Discoverer Tests
1002    // =========================================================================
1003
1004    use crate::types::{DacCapabilities, LaserPoint};
1005    use crate::WriteOutcome;
1006    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1007    use std::sync::Arc;
1008
1009    /// Mock connection info for testing.
1010    #[derive(Debug, Clone)]
1011    struct MockConnectionInfo {
1012        _device_id: u32,
1013    }
1014
1015    /// Mock backend for testing external discoverers.
1016    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    /// Mock external discoverer for testing.
1065    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        // Verify we can connect to any of them
1177        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}