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;
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
16// =============================================================================
17// External Discoverer Support
18// =============================================================================
19
20/// Trait for external DAC discovery implementations.
21///
22/// External crates implement this to integrate their DAC discovery
23/// with the unified `DacDiscovery` system.
24///
25/// # Example
26///
27/// ```ignore
28/// use laser_dac::{
29///     DacDiscovery, ExternalDiscoverer, ExternalDevice,
30///     StreamBackend, DacType, EnabledDacTypes, Result,
31/// };
32/// use std::any::Any;
33///
34/// struct MyClosedDacDiscoverer { /* ... */ }
35///
36/// impl ExternalDiscoverer for MyClosedDacDiscoverer {
37///     fn dac_type(&self) -> DacType {
38///         DacType::Custom("MyClosedDAC".into())
39///     }
40///
41///     fn scan(&mut self) -> Vec<ExternalDevice> {
42///         // Your discovery logic here
43///         vec![]
44///     }
45///
46///     fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>> {
47///         // Your connection logic here
48///         todo!()
49///     }
50/// }
51/// ```
52pub trait ExternalDiscoverer: Send {
53    /// Returns the DAC type this discoverer handles.
54    fn dac_type(&self) -> DacType;
55
56    /// Scan for devices. Called during `DacDiscovery::scan()`.
57    fn scan(&mut self) -> Vec<ExternalDevice>;
58
59    /// Connect to a previously discovered device.
60    /// The `opaque_data` is the same data returned in `ExternalDevice`.
61    fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>>;
62}
63
64/// Device info returned by external discoverers.
65///
66/// This struct contains the common fields that `DacDiscovery` uses to create
67/// a `DiscoveredDevice`. The `opaque_data` field stores protocol-specific
68/// connection information that will be passed back to `connect()`.
69pub struct ExternalDevice {
70    /// IP address for network devices.
71    pub ip_address: Option<IpAddr>,
72    /// MAC address if available.
73    pub mac_address: Option<[u8; 6]>,
74    /// Hostname if available.
75    pub hostname: Option<String>,
76    /// USB address (e.g., "bus:device") for USB devices.
77    pub usb_address: Option<String>,
78    /// Hardware/device name if available.
79    pub hardware_name: Option<String>,
80    /// Disambiguation index when multiple identical devices are present.
81    pub device_index: Option<u16>,
82    /// Opaque data passed back to `connect()`.
83    /// Store whatever your protocol needs to establish a connection.
84    pub opaque_data: Box<dyn Any + Send>,
85}
86
87impl ExternalDevice {
88    /// Create a new external device with the given opaque data.
89    ///
90    /// All other fields default to `None`.
91    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// Feature-gated imports from internal protocol modules
105
106#[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
152// =============================================================================
153// DiscoveredDevice
154// =============================================================================
155
156/// A discovered but not-yet-connected DAC device.
157///
158/// Use `DacDiscovery::connect()` to establish a connection and get a backend.
159pub 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    /// Create a new discovered device with the given type and inner data.
172    ///
173    /// All identification fields default to `None`.
174    fn new(dac_type: DacType, inner: DiscoveredDeviceInner) -> Self {
175        Self {
176            dac_type,
177            ip_address: None,
178            mac_address: None,
179            hostname: None,
180            usb_address: None,
181            hardware_name: None,
182            device_index: None,
183            inner,
184        }
185    }
186
187    /// Returns the device name (unique identifier).
188    /// For network devices: IP address.
189    /// For USB devices: hardware name or bus:address.
190    pub fn name(&self) -> String {
191        self.info().name()
192    }
193
194    /// Returns the DAC type.
195    pub fn dac_type(&self) -> DacType {
196        self.dac_type.clone()
197    }
198
199    /// Returns a lightweight, cloneable info struct for this device.
200    pub fn info(&self) -> DiscoveredDeviceInfo {
201        DiscoveredDeviceInfo {
202            dac_type: self.dac_type.clone(),
203            ip_address: self.ip_address,
204            mac_address: self.mac_address,
205            hostname: self.hostname.clone(),
206            usb_address: self.usb_address.clone(),
207            hardware_name: self.hardware_name.clone(),
208            device_index: self.device_index,
209        }
210    }
211}
212
213/// Lightweight info about a discovered device.
214///
215/// This struct is Clone-able and can be used for filtering and reporting
216/// without consuming the original `DiscoveredDevice`.
217#[derive(Debug, Clone, PartialEq, Eq, Hash)]
218pub struct DiscoveredDeviceInfo {
219    /// The DAC type.
220    pub dac_type: DacType,
221    /// IP address for network devices, None for USB devices.
222    pub ip_address: Option<IpAddr>,
223    /// MAC address (Ether Dream only).
224    pub mac_address: Option<[u8; 6]>,
225    /// Hostname (IDN only).
226    pub hostname: Option<String>,
227    /// USB bus:address (LaserCube USB only).
228    pub usb_address: Option<String>,
229    /// Device name from hardware (Helios only).
230    pub hardware_name: Option<String>,
231    /// Disambiguation index when multiple identical devices are present.
232    pub device_index: Option<u16>,
233}
234
235impl DiscoveredDeviceInfo {
236    /// Returns the device name (human-readable).
237    /// For network devices: IP address.
238    /// For USB devices: hardware name or bus:address.
239    pub fn name(&self) -> String {
240        self.ip_address
241            .map(|ip| ip.to_string())
242            .or_else(|| self.hardware_name.clone())
243            .or_else(|| self.usb_address.clone())
244            .unwrap_or_else(|| "Unknown".into())
245    }
246
247    /// Returns a stable, namespaced identifier for the device.
248    ///
249    /// The ID is prefixed with the protocol name to avoid cross-protocol collisions:
250    /// - Ether Dream: `etherdream:<mac>` (survives IP changes)
251    /// - IDN: `idn:<hostname>` (mDNS name, survives IP changes)
252    /// - Helios: `helios:<hardware_name>` (USB serial if available)
253    /// - LaserCube USB: `lasercube-usb:<serial|bus:addr>`
254    /// - LaserCube WiFi: `lasercube-wifi:<ip>` (best available)
255    /// - AVB: `avb:<device-slug>:<n>` (slugged audio device name + duplicate index)
256    ///
257    /// This is used for device tracking/deduplication.
258    pub fn stable_id(&self) -> String {
259        match &self.dac_type {
260            DacType::EtherDream => {
261                if let Some(mac) = self.mac_address {
262                    return format!(
263                        "etherdream:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
264                        mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
265                    );
266                }
267                if let Some(ip) = self.ip_address {
268                    return format!("etherdream:{}", ip);
269                }
270            }
271            DacType::Idn => {
272                if let Some(ref hostname) = self.hostname {
273                    return format!("idn:{}", hostname);
274                }
275                if let Some(ip) = self.ip_address {
276                    return format!("idn:{}", ip);
277                }
278            }
279            DacType::Helios => {
280                if let Some(ref hw_name) = self.hardware_name {
281                    return format!("helios:{}", hw_name);
282                }
283                if let Some(ref usb_addr) = self.usb_address {
284                    return format!("helios:{}", usb_addr);
285                }
286            }
287            DacType::LasercubeUsb => {
288                if let Some(ref hw_name) = self.hardware_name {
289                    return format!("lasercube-usb:{}", hw_name);
290                }
291                if let Some(ref usb_addr) = self.usb_address {
292                    return format!("lasercube-usb:{}", usb_addr);
293                }
294            }
295            DacType::LasercubeWifi => {
296                if let Some(ip) = self.ip_address {
297                    return format!("lasercube-wifi:{}", ip);
298                }
299            }
300            DacType::Avb => {
301                if let Some(ref hw_name) = self.hardware_name {
302                    let slug = slugify_device_id(hw_name);
303                    if let Some(index) = self.device_index {
304                        return format!("avb:{}:{}", slug, index);
305                    }
306                    return format!("avb:{}", slug);
307                }
308            }
309            DacType::Custom(name) => {
310                // For custom types, use the custom name as prefix
311                if let Some(ip) = self.ip_address {
312                    return format!("{}:{}", name.to_lowercase(), ip);
313                }
314                if let Some(ref hw_name) = self.hardware_name {
315                    return format!("{}:{}", name.to_lowercase(), hw_name);
316                }
317            }
318        }
319
320        // Fallback for unknown configurations
321        format!("unknown:{:?}", self.dac_type)
322    }
323}
324
325fn slugify_device_id(name: &str) -> String {
326    let normalized = name
327        .split_whitespace()
328        .collect::<Vec<_>>()
329        .join(" ")
330        .trim()
331        .to_ascii_lowercase();
332    let mut slug = String::with_capacity(normalized.len());
333    let mut prev_dash = false;
334
335    for ch in normalized.chars() {
336        if ch.is_ascii_alphanumeric() {
337            slug.push(ch);
338            prev_dash = false;
339        } else if !prev_dash {
340            slug.push('-');
341            prev_dash = true;
342        }
343    }
344
345    slug.trim_matches('-').to_string()
346}
347
348/// Internal data needed for connection (opaque to callers).
349enum DiscoveredDeviceInner {
350    #[cfg(feature = "helios")]
351    Helios(HeliosDac),
352    #[cfg(feature = "ether-dream")]
353    EtherDream {
354        broadcast: EtherDreamBroadcast,
355        ip: IpAddr,
356    },
357    #[cfg(feature = "idn")]
358    Idn {
359        server: IdnServerInfo,
360        service: IdnServiceInfo,
361    },
362    #[cfg(feature = "lasercube-wifi")]
363    LasercubeWifi {
364        info: LasercubeDeviceInfo,
365        source_addr: std::net::SocketAddr,
366    },
367    #[cfg(feature = "lasercube-usb")]
368    LasercubeUsb(rusb::Device<rusb::Context>),
369    #[cfg(feature = "avb")]
370    Avb(AvbSelector),
371    /// External discoverer device.
372    External {
373        /// Index into `DacDiscovery.external` for the discoverer that found this device.
374        discoverer_index: usize,
375        /// Opaque data passed back to `ExternalDiscoverer::connect()`.
376        opaque_data: Box<dyn Any + Send>,
377    },
378    /// Placeholder variant to ensure enum is not empty when no features are enabled
379    #[cfg(not(any(
380        feature = "helios",
381        feature = "ether-dream",
382        feature = "idn",
383        feature = "lasercube-wifi",
384        feature = "lasercube-usb",
385        feature = "avb"
386    )))]
387    _Placeholder,
388}
389
390// =============================================================================
391// Per-DAC Discovery Implementations
392// =============================================================================
393
394/// Discovery for Helios USB DACs.
395#[cfg(feature = "helios")]
396pub struct HeliosDiscovery {
397    controller: HeliosDacController,
398}
399
400#[cfg(feature = "helios")]
401impl HeliosDiscovery {
402    /// Create a new Helios discovery instance.
403    ///
404    /// Returns None if the USB controller fails to initialize.
405    pub fn new() -> Option<Self> {
406        HeliosDacController::new()
407            .ok()
408            .map(|controller| Self { controller })
409    }
410
411    /// Scan for available Helios devices.
412    pub fn scan(&self) -> Vec<DiscoveredDevice> {
413        let Ok(devices) = self.controller.list_devices() else {
414            return Vec::new();
415        };
416
417        let mut discovered = Vec::new();
418        for device in devices {
419            // Only process idle (unopened) devices
420            let HeliosDac::Idle(_) = &device else {
421                continue;
422            };
423
424            // Try to open to get name
425            let opened = match device.open() {
426                Ok(o) => o,
427                Err(_) => continue,
428            };
429
430            let hardware_name = opened.name().unwrap_or_else(|_| "Unknown Helios".into());
431            let mut device =
432                DiscoveredDevice::new(DacType::Helios, DiscoveredDeviceInner::Helios(opened));
433            device.hardware_name = Some(hardware_name);
434            discovered.push(device);
435        }
436        discovered
437    }
438
439    /// Connect to a discovered Helios device.
440    pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
441        let DiscoveredDeviceInner::Helios(dac) = device.inner else {
442            return Err(Error::invalid_config("Invalid device type for Helios"));
443        };
444        Ok(Box::new(HeliosBackend::from_dac(dac)))
445    }
446}
447
448/// Discovery for Ether Dream network DACs.
449#[cfg(feature = "ether-dream")]
450pub struct EtherDreamDiscovery {
451    timeout: Duration,
452}
453
454#[cfg(feature = "ether-dream")]
455impl EtherDreamDiscovery {
456    /// Create a new Ether Dream discovery instance.
457    pub fn new() -> Self {
458        Self {
459            // Ether Dream DACs broadcast once per second, so we need
460            // at least 1.5s to reliably catch a broadcast
461            timeout: Duration::from_millis(1500),
462        }
463    }
464
465    /// Scan for available Ether Dream devices.
466    pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
467        let Ok(mut rx) = recv_dac_broadcasts() else {
468            return Vec::new();
469        };
470
471        if rx.set_timeout(Some(self.timeout)).is_err() {
472            return Vec::new();
473        }
474
475        let mut discovered = Vec::new();
476        let mut seen_macs = std::collections::HashSet::new();
477
478        // Only try 3 iterations max - we just need one device
479        for _ in 0..3 {
480            let (broadcast, source_addr) = match rx.next_broadcast() {
481                Ok(b) => b,
482                Err(_) => break,
483            };
484
485            let ip = source_addr.ip();
486
487            // Skip duplicate MACs - but keep polling to find other devices
488            let device_mac = broadcast.mac_address;
489            if seen_macs.contains(&device_mac) {
490                continue;
491            }
492            seen_macs.insert(device_mac);
493
494            let mut device = DiscoveredDevice::new(
495                DacType::EtherDream,
496                DiscoveredDeviceInner::EtherDream { broadcast, ip },
497            );
498            device.ip_address = Some(ip);
499            device.mac_address = Some(device_mac);
500            discovered.push(device);
501        }
502        discovered
503    }
504
505    /// Connect to a discovered Ether Dream device.
506    pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
507        let DiscoveredDeviceInner::EtherDream { broadcast, ip } = device.inner else {
508            return Err(Error::invalid_config("Invalid device type for EtherDream"));
509        };
510
511        let backend = EtherDreamBackend::new(broadcast, ip);
512        Ok(Box::new(backend))
513    }
514}
515
516#[cfg(feature = "ether-dream")]
517impl Default for EtherDreamDiscovery {
518    fn default() -> Self {
519        Self::new()
520    }
521}
522
523/// Discovery for IDN (ILDA Digital Network) DACs.
524#[cfg(feature = "idn")]
525pub struct IdnDiscovery {
526    scan_timeout: Duration,
527}
528
529#[cfg(feature = "idn")]
530impl IdnDiscovery {
531    /// Create a new IDN discovery instance.
532    pub fn new() -> Self {
533        Self {
534            scan_timeout: Duration::from_millis(500),
535        }
536    }
537
538    /// Scan for available IDN devices.
539    pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
540        let Ok(servers) = scan_for_servers(self.scan_timeout) else {
541            return Vec::new();
542        };
543        Self::servers_to_devices(servers)
544    }
545
546    /// Connect to a discovered IDN device.
547    pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
548        let DiscoveredDeviceInner::Idn { server, service } = device.inner else {
549            return Err(Error::invalid_config("Invalid device type for IDN"));
550        };
551
552        Ok(Box::new(IdnBackend::new(server, service)))
553    }
554
555    /// Scan a specific address for IDN devices.
556    ///
557    /// This is useful for testing with mock servers on localhost where
558    /// broadcast won't work.
559    ///
560    /// This method is only available with the `testutils` feature.
561    #[cfg(feature = "testutils")]
562    pub fn scan_address(&mut self, addr: SocketAddr) -> Vec<DiscoveredDevice> {
563        let Ok(mut scanner) = ServerScanner::new(0) else {
564            return Vec::new();
565        };
566
567        let Ok(servers) = scanner.scan_address(addr, self.scan_timeout) else {
568            return Vec::new();
569        };
570
571        Self::servers_to_devices(servers)
572    }
573
574    /// Convert discovered IDN servers into `DiscoveredDevice` entries.
575    fn servers_to_devices(servers: Vec<IdnServerInfo>) -> Vec<DiscoveredDevice> {
576        servers
577            .into_iter()
578            .filter_map(|server| {
579                let service = server.find_laser_projector().cloned()?;
580                let ip_address = server.addresses.first().map(|addr| addr.ip());
581                let hostname = server.hostname.clone();
582                let mut device = DiscoveredDevice::new(
583                    DacType::Idn,
584                    DiscoveredDeviceInner::Idn { server, service },
585                );
586                device.ip_address = ip_address;
587                device.hostname = Some(hostname);
588                Some(device)
589            })
590            .collect()
591    }
592}
593
594#[cfg(feature = "idn")]
595impl Default for IdnDiscovery {
596    fn default() -> Self {
597        Self::new()
598    }
599}
600
601/// Discovery for LaserCube WiFi DACs.
602#[cfg(feature = "lasercube-wifi")]
603pub struct LasercubeWifiDiscovery {
604    timeout: Duration,
605}
606
607#[cfg(feature = "lasercube-wifi")]
608impl LasercubeWifiDiscovery {
609    /// Create a new LaserCube WiFi discovery instance.
610    pub fn new() -> Self {
611        Self {
612            timeout: Duration::from_millis(100),
613        }
614    }
615
616    /// Scan for available LaserCube WiFi devices.
617    pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
618        let Ok(mut discovery) = discover_lasercube_wifi() else {
619            return Vec::new();
620        };
621
622        if discovery.set_timeout(Some(self.timeout)).is_err() {
623            return Vec::new();
624        }
625
626        let mut discovered = Vec::new();
627        for _ in 0..10 {
628            let (device_info, source_addr) = match discovery.next_device() {
629                Ok(d) => d,
630                Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
631                Err(e) if e.kind() == io::ErrorKind::TimedOut => break,
632                Err(_) => continue,
633            };
634
635            let ip_address = source_addr.ip();
636
637            let mut device = DiscoveredDevice::new(
638                DacType::LasercubeWifi,
639                DiscoveredDeviceInner::LasercubeWifi {
640                    info: device_info,
641                    source_addr,
642                },
643            );
644            device.ip_address = Some(ip_address);
645            discovered.push(device);
646        }
647        discovered
648    }
649
650    /// Connect to a discovered LaserCube WiFi device.
651    pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
652        let DiscoveredDeviceInner::LasercubeWifi { info, source_addr } = device.inner else {
653            return Err(Error::invalid_config(
654                "Invalid device type for LaserCube WiFi",
655            ));
656        };
657
658        let addressed = LasercubeAddressed::from_discovery(&info, source_addr);
659        Ok(Box::new(LasercubeWifiBackend::new(addressed)))
660    }
661}
662
663#[cfg(feature = "lasercube-wifi")]
664impl Default for LasercubeWifiDiscovery {
665    fn default() -> Self {
666        Self::new()
667    }
668}
669
670/// Discovery for LaserCube USB DACs (LaserDock).
671#[cfg(feature = "lasercube-usb")]
672pub struct LasercubeUsbDiscovery {
673    controller: LasercubeUsbController,
674}
675
676#[cfg(feature = "lasercube-usb")]
677impl LasercubeUsbDiscovery {
678    /// Create a new LaserCube USB discovery instance.
679    ///
680    /// Returns None if the USB controller fails to initialize.
681    pub fn new() -> Option<Self> {
682        LasercubeUsbController::new()
683            .ok()
684            .map(|controller| Self { controller })
685    }
686
687    /// Scan for available LaserCube USB devices.
688    pub fn scan(&self) -> Vec<DiscoveredDevice> {
689        let Ok(devices) = self.controller.list_devices() else {
690            return Vec::new();
691        };
692
693        let mut discovered = Vec::new();
694        for device in devices {
695            let usb_address = format!("{}:{}", device.bus_number(), device.address());
696            let serial = crate::protocols::lasercube_usb::get_serial_number(&device);
697
698            let mut discovered_device = DiscoveredDevice::new(
699                DacType::LasercubeUsb,
700                DiscoveredDeviceInner::LasercubeUsb(device),
701            );
702            discovered_device.usb_address = Some(usb_address);
703            discovered_device.hardware_name = serial;
704            discovered.push(discovered_device);
705        }
706        discovered
707    }
708
709    /// Connect to a discovered LaserCube USB device.
710    pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
711        let DiscoveredDeviceInner::LasercubeUsb(usb_device) = device.inner else {
712            return Err(Error::invalid_config(
713                "Invalid device type for LaserCube USB",
714            ));
715        };
716
717        let backend = LasercubeUsbBackend::new(usb_device);
718        Ok(Box::new(backend))
719    }
720}
721
722/// Discovery for AVB audio-output DACs.
723#[cfg(feature = "avb")]
724pub struct AvbDiscovery;
725
726#[cfg(feature = "avb")]
727impl AvbDiscovery {
728    /// Create a new AVB discovery instance.
729    pub fn new() -> Self {
730        Self
731    }
732
733    /// Scan for available AVB output devices.
734    pub fn scan(&self) -> Vec<DiscoveredDevice> {
735        let Ok(selectors) = discover_avb_selectors() else {
736            return Vec::new();
737        };
738
739        selectors
740            .into_iter()
741            .map(|selector| {
742                let hardware_name = selector.name.clone();
743                let index = selector.duplicate_index;
744                let mut device =
745                    DiscoveredDevice::new(DacType::Avb, DiscoveredDeviceInner::Avb(selector));
746                device.hardware_name = Some(hardware_name);
747                device.device_index = Some(index);
748                device
749            })
750            .collect()
751    }
752
753    /// Connect to a discovered AVB output device.
754    pub fn connect(&self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
755        let DiscoveredDeviceInner::Avb(selector) = device.inner else {
756            return Err(Error::invalid_config("Invalid device type for AVB"));
757        };
758
759        Ok(Box::new(AvbBackend::from_selector(selector)))
760    }
761}
762
763#[cfg(feature = "avb")]
764impl Default for AvbDiscovery {
765    fn default() -> Self {
766        Self::new()
767    }
768}
769
770// =============================================================================
771// DAC Discovery
772// =============================================================================
773
774/// DAC discovery coordinator for all DAC types.
775///
776/// This provides a single entry point for discovering and connecting to any
777/// supported DAC hardware.
778pub struct DacDiscovery {
779    #[cfg(feature = "helios")]
780    helios: Option<HeliosDiscovery>,
781    #[cfg(feature = "ether-dream")]
782    etherdream: EtherDreamDiscovery,
783    #[cfg(feature = "idn")]
784    idn: IdnDiscovery,
785    #[cfg(all(feature = "idn", feature = "testutils"))]
786    idn_scan_addresses: Vec<SocketAddr>,
787    #[cfg(feature = "lasercube-wifi")]
788    lasercube_wifi: LasercubeWifiDiscovery,
789    #[cfg(feature = "lasercube-usb")]
790    lasercube_usb: Option<LasercubeUsbDiscovery>,
791    #[cfg(feature = "avb")]
792    avb: AvbDiscovery,
793    enabled: EnabledDacTypes,
794    /// External discoverers registered by external crates.
795    external: Vec<Box<dyn ExternalDiscoverer>>,
796}
797
798impl DacDiscovery {
799    /// Create a new DAC discovery instance.
800    ///
801    /// This initializes USB controllers, so it should be called from the main thread.
802    /// If a USB controller fails to initialize, that DAC type will be unavailable
803    /// but other types will still work.
804    pub fn new(enabled: EnabledDacTypes) -> Self {
805        Self {
806            #[cfg(feature = "helios")]
807            helios: HeliosDiscovery::new(),
808            #[cfg(feature = "ether-dream")]
809            etherdream: EtherDreamDiscovery::new(),
810            #[cfg(feature = "idn")]
811            idn: IdnDiscovery::new(),
812            #[cfg(all(feature = "idn", feature = "testutils"))]
813            idn_scan_addresses: Vec::new(),
814            #[cfg(feature = "lasercube-wifi")]
815            lasercube_wifi: LasercubeWifiDiscovery::new(),
816            #[cfg(feature = "lasercube-usb")]
817            lasercube_usb: LasercubeUsbDiscovery::new(),
818            #[cfg(feature = "avb")]
819            avb: AvbDiscovery::new(),
820            enabled,
821            external: Vec::new(),
822        }
823    }
824
825    /// Set specific addresses to scan for IDN servers.
826    ///
827    /// When set, the scanner will scan these specific addresses instead of
828    /// using broadcast discovery. This is useful for testing with mock servers.
829    ///
830    /// This method is only available with the `testutils` feature.
831    #[cfg(all(feature = "idn", feature = "testutils"))]
832    pub fn set_idn_scan_addresses(&mut self, addresses: Vec<SocketAddr>) {
833        self.idn_scan_addresses = addresses;
834    }
835
836    /// Update which DAC types to scan for.
837    pub fn set_enabled(&mut self, enabled: EnabledDacTypes) {
838        self.enabled = enabled;
839    }
840
841    /// Returns the currently enabled DAC types.
842    pub fn enabled(&self) -> &EnabledDacTypes {
843        &self.enabled
844    }
845
846    /// Register an external discoverer.
847    ///
848    /// External discoverers are called during `scan()` to find additional devices
849    /// beyond the built-in DAC types. This allows external crates to integrate
850    /// their own DAC discovery with the unified discovery system.
851    ///
852    /// # Example
853    ///
854    /// ```ignore
855    /// use laser_dac::{DacDiscovery, EnabledDacTypes};
856    ///
857    /// let mut discovery = DacDiscovery::new(EnabledDacTypes::all());
858    /// discovery.register(Box::new(MyClosedDacDiscoverer::new()));
859    ///
860    /// // Now scan() will include devices from the external discoverer
861    /// let devices = discovery.scan();
862    /// ```
863    pub fn register(&mut self, discoverer: Box<dyn ExternalDiscoverer>) {
864        self.external.push(discoverer);
865    }
866
867    /// Scan for available DAC devices of all enabled types.
868    ///
869    /// Returns a list of discovered devices. Each device can be connected
870    /// using `connect()`.
871    pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
872        let mut devices = Vec::new();
873
874        // Helios
875        #[cfg(feature = "helios")]
876        if self.enabled.is_enabled(DacType::Helios) {
877            if let Some(ref discovery) = self.helios {
878                devices.extend(discovery.scan());
879            }
880        }
881
882        // Ether Dream
883        #[cfg(feature = "ether-dream")]
884        if self.enabled.is_enabled(DacType::EtherDream) {
885            devices.extend(self.etherdream.scan());
886        }
887
888        // IDN
889        #[cfg(feature = "idn")]
890        if self.enabled.is_enabled(DacType::Idn) {
891            #[cfg(feature = "testutils")]
892            {
893                if self.idn_scan_addresses.is_empty() {
894                    // Use broadcast discovery
895                    devices.extend(self.idn.scan());
896                } else {
897                    // Scan specific addresses (for testing with mock servers)
898                    for addr in &self.idn_scan_addresses {
899                        devices.extend(self.idn.scan_address(*addr));
900                    }
901                }
902            }
903            #[cfg(not(feature = "testutils"))]
904            {
905                devices.extend(self.idn.scan());
906            }
907        }
908
909        // LaserCube WiFi
910        #[cfg(feature = "lasercube-wifi")]
911        if self.enabled.is_enabled(DacType::LasercubeWifi) {
912            devices.extend(self.lasercube_wifi.scan());
913        }
914
915        // LaserCube USB
916        #[cfg(feature = "lasercube-usb")]
917        if self.enabled.is_enabled(DacType::LasercubeUsb) {
918            if let Some(ref discovery) = self.lasercube_usb {
919                devices.extend(discovery.scan());
920            }
921        }
922
923        // AVB
924        #[cfg(feature = "avb")]
925        if self.enabled.is_enabled(DacType::Avb) {
926            devices.extend(self.avb.scan());
927        }
928
929        // External discoverers
930        for (index, discoverer) in self.external.iter_mut().enumerate() {
931            let dac_type = discoverer.dac_type();
932            for ext_device in discoverer.scan() {
933                let mut device = DiscoveredDevice::new(
934                    dac_type.clone(),
935                    DiscoveredDeviceInner::External {
936                        discoverer_index: index,
937                        opaque_data: ext_device.opaque_data,
938                    },
939                );
940                device.ip_address = ext_device.ip_address;
941                device.mac_address = ext_device.mac_address;
942                device.hostname = ext_device.hostname;
943                device.usb_address = ext_device.usb_address;
944                device.hardware_name = ext_device.hardware_name;
945                device.device_index = ext_device.device_index;
946                devices.push(device);
947            }
948        }
949
950        devices
951    }
952
953    /// Parse the protocol prefix from a stable ID and return the corresponding `DacType`.
954    ///
955    /// Stable IDs are formatted as `<protocol>:<identifier>`, e.g. `etherdream:01:23:...`.
956    /// Returns `None` if the prefix doesn't match a known built-in type.
957    fn dac_type_from_id_prefix(id: &str) -> Option<DacType> {
958        // Find the first ':' — the prefix is everything before it.
959        // Note: some prefixes contain hyphens (lasercube-usb, lasercube-wifi).
960        let prefix = id.split(':').next()?;
961        match prefix {
962            "etherdream" => Some(DacType::EtherDream),
963            "idn" => Some(DacType::Idn),
964            "helios" => Some(DacType::Helios),
965            "lasercube-usb" => Some(DacType::LasercubeUsb),
966            "lasercube-wifi" => Some(DacType::LasercubeWifi),
967            "avb" => Some(DacType::Avb),
968            _ => None,
969        }
970    }
971
972    /// Scan for a device by stable ID, connect, and return a `Dac`.
973    ///
974    /// This is a convenience method combining `scan()`, lookup by `stable_id()`,
975    /// and `connect()` into a single call.
976    ///
977    /// When the ID has a known protocol prefix (e.g. `etherdream:`, `idn:`),
978    /// only that protocol is scanned, avoiding unnecessary network timeouts.
979    pub(crate) fn open_by_id(&mut self, id: &str) -> Result<crate::stream::Dac> {
980        // Narrow the scan to just the relevant protocol if we can parse the prefix.
981        let discovered = if let Some(dac_type) = Self::dac_type_from_id_prefix(id) {
982            let saved = self.enabled.clone();
983            self.enabled = std::iter::once(dac_type).collect();
984            let result = self.scan();
985            self.enabled = saved;
986            result
987        } else {
988            self.scan()
989        };
990
991        let device = discovered
992            .into_iter()
993            .find(|d| d.info().stable_id() == id)
994            .ok_or_else(|| Error::disconnected(format!("DAC not found: {}", id)))?;
995
996        let name = device.info().name();
997        let dac_type = device.dac_type();
998        let stream_backend = self.connect(device)?;
999
1000        let dac_info = crate::types::DacInfo {
1001            id: id.to_string(),
1002            name,
1003            kind: dac_type,
1004            caps: stream_backend.caps().clone(),
1005        };
1006
1007        Ok(crate::stream::Dac::new(dac_info, stream_backend))
1008    }
1009
1010    /// Connect to a discovered device and return a streaming backend.
1011    #[allow(unreachable_patterns)]
1012    pub fn connect(&mut self, device: DiscoveredDevice) -> Result<Box<dyn StreamBackend>> {
1013        // Handle external devices first (check inner variant)
1014        if let DiscoveredDeviceInner::External {
1015            discoverer_index,
1016            opaque_data,
1017        } = device.inner
1018        {
1019            return self
1020                .external
1021                .get_mut(discoverer_index)
1022                .ok_or_else(|| Error::invalid_config("External discoverer not found"))?
1023                .connect(opaque_data);
1024        }
1025
1026        // Handle built-in DAC types
1027        match device.dac_type {
1028            #[cfg(feature = "helios")]
1029            DacType::Helios => self
1030                .helios
1031                .as_ref()
1032                .ok_or_else(|| Error::disconnected("Helios discovery not available"))?
1033                .connect(device),
1034            #[cfg(feature = "ether-dream")]
1035            DacType::EtherDream => self.etherdream.connect(device),
1036            #[cfg(feature = "idn")]
1037            DacType::Idn => self.idn.connect(device),
1038            #[cfg(feature = "lasercube-wifi")]
1039            DacType::LasercubeWifi => self.lasercube_wifi.connect(device),
1040            #[cfg(feature = "lasercube-usb")]
1041            DacType::LasercubeUsb => self
1042                .lasercube_usb
1043                .as_ref()
1044                .ok_or_else(|| Error::disconnected("LaserCube USB discovery not available"))?
1045                .connect(device),
1046            #[cfg(feature = "avb")]
1047            DacType::Avb => self.avb.connect(device),
1048            _ => Err(Error::invalid_config(format!(
1049                "DAC type {:?} not supported in this build",
1050                device.dac_type
1051            ))),
1052        }
1053    }
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058    use super::*;
1059
1060    #[test]
1061    fn test_stable_id_etherdream_with_mac() {
1062        let info = DiscoveredDeviceInfo {
1063            dac_type: DacType::EtherDream,
1064            ip_address: Some("192.168.1.100".parse().unwrap()),
1065            mac_address: Some([0x01, 0x23, 0x45, 0x67, 0x89, 0xab]),
1066            hostname: None,
1067            usb_address: None,
1068            hardware_name: None,
1069            device_index: None,
1070        };
1071        assert_eq!(info.stable_id(), "etherdream:01:23:45:67:89:ab");
1072    }
1073
1074    #[test]
1075    fn test_stable_id_idn_with_hostname() {
1076        let info = DiscoveredDeviceInfo {
1077            dac_type: DacType::Idn,
1078            ip_address: Some("192.168.1.100".parse().unwrap()),
1079            mac_address: None,
1080            hostname: Some("laser-projector.local".to_string()),
1081            usb_address: None,
1082            hardware_name: None,
1083            device_index: None,
1084        };
1085        assert_eq!(info.stable_id(), "idn:laser-projector.local");
1086    }
1087
1088    #[test]
1089    fn test_stable_id_helios_with_hardware_name() {
1090        let info = DiscoveredDeviceInfo {
1091            dac_type: DacType::Helios,
1092            ip_address: None,
1093            mac_address: None,
1094            hostname: None,
1095            usb_address: Some("1:5".to_string()),
1096            hardware_name: Some("Helios DAC".to_string()),
1097            device_index: None,
1098        };
1099        assert_eq!(info.stable_id(), "helios:Helios DAC");
1100    }
1101
1102    #[test]
1103    fn test_stable_id_lasercube_usb_with_address() {
1104        let info = DiscoveredDeviceInfo {
1105            dac_type: DacType::LasercubeUsb,
1106            ip_address: None,
1107            mac_address: None,
1108            hostname: None,
1109            usb_address: Some("2:3".to_string()),
1110            hardware_name: None,
1111            device_index: None,
1112        };
1113        assert_eq!(info.stable_id(), "lasercube-usb:2:3");
1114    }
1115
1116    #[test]
1117    fn test_stable_id_lasercube_wifi_with_ip() {
1118        let info = DiscoveredDeviceInfo {
1119            dac_type: DacType::LasercubeWifi,
1120            ip_address: Some("192.168.1.50".parse().unwrap()),
1121            mac_address: None,
1122            hostname: None,
1123            usb_address: None,
1124            hardware_name: None,
1125            device_index: None,
1126        };
1127        assert_eq!(info.stable_id(), "lasercube-wifi:192.168.1.50");
1128    }
1129
1130    #[test]
1131    fn test_stable_id_avb_with_index() {
1132        let info = DiscoveredDeviceInfo {
1133            dac_type: DacType::Avb,
1134            ip_address: None,
1135            mac_address: None,
1136            hostname: None,
1137            usb_address: None,
1138            hardware_name: Some("MOTU AVB Main".to_string()),
1139            device_index: Some(1),
1140        };
1141        assert_eq!(info.stable_id(), "avb:motu-avb-main:1");
1142    }
1143
1144    #[test]
1145    fn test_stable_id_custom_fallback() {
1146        let info = DiscoveredDeviceInfo {
1147            dac_type: DacType::Custom("MyDAC".to_string()),
1148            ip_address: None,
1149            mac_address: None,
1150            hostname: None,
1151            usb_address: None,
1152            hardware_name: None,
1153            device_index: None,
1154        };
1155        // Custom with no identifiers falls back to unknown format
1156        assert_eq!(info.stable_id(), "unknown:Custom(\"MyDAC\")");
1157    }
1158
1159    #[test]
1160    fn test_stable_id_custom_with_ip() {
1161        let info = DiscoveredDeviceInfo {
1162            dac_type: DacType::Custom("MyDAC".to_string()),
1163            ip_address: Some("10.0.0.1".parse().unwrap()),
1164            mac_address: None,
1165            hostname: None,
1166            usb_address: None,
1167            hardware_name: None,
1168            device_index: None,
1169        };
1170        assert_eq!(info.stable_id(), "mydac:10.0.0.1");
1171    }
1172
1173    // =========================================================================
1174    // External Discoverer Tests
1175    // =========================================================================
1176
1177    use crate::types::{DacCapabilities, LaserPoint};
1178    use crate::WriteOutcome;
1179    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1180    use std::sync::Arc;
1181
1182    /// Mock connection info for testing.
1183    #[derive(Debug, Clone)]
1184    struct MockConnectionInfo {
1185        _device_id: u32,
1186    }
1187
1188    /// Mock backend for testing external discoverers.
1189    struct MockBackend {
1190        connected: bool,
1191    }
1192
1193    impl StreamBackend for MockBackend {
1194        fn dac_type(&self) -> DacType {
1195            DacType::Custom("MockDAC".into())
1196        }
1197
1198        fn caps(&self) -> &DacCapabilities {
1199            static CAPS: DacCapabilities = DacCapabilities {
1200                pps_min: 1,
1201                pps_max: 100_000,
1202                max_points_per_chunk: 4096,
1203                output_model: crate::types::OutputModel::NetworkFifo,
1204            };
1205            &CAPS
1206        }
1207
1208        fn connect(&mut self) -> Result<()> {
1209            self.connected = true;
1210            Ok(())
1211        }
1212
1213        fn disconnect(&mut self) -> Result<()> {
1214            self.connected = false;
1215            Ok(())
1216        }
1217
1218        fn is_connected(&self) -> bool {
1219            self.connected
1220        }
1221
1222        fn try_write_chunk(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
1223            Ok(WriteOutcome::Written)
1224        }
1225
1226        fn stop(&mut self) -> Result<()> {
1227            Ok(())
1228        }
1229
1230        fn set_shutter(&mut self, _open: bool) -> Result<()> {
1231            Ok(())
1232        }
1233    }
1234
1235    /// Mock external discoverer for testing.
1236    struct MockExternalDiscoverer {
1237        scan_count: Arc<AtomicUsize>,
1238        connect_called: Arc<AtomicBool>,
1239        devices_to_return: Vec<(u32, Option<IpAddr>)>,
1240    }
1241
1242    impl MockExternalDiscoverer {
1243        fn new(devices: Vec<(u32, Option<IpAddr>)>) -> Self {
1244            Self {
1245                scan_count: Arc::new(AtomicUsize::new(0)),
1246                connect_called: Arc::new(AtomicBool::new(false)),
1247                devices_to_return: devices,
1248            }
1249        }
1250    }
1251
1252    impl ExternalDiscoverer for MockExternalDiscoverer {
1253        fn dac_type(&self) -> DacType {
1254            DacType::Custom("MockDAC".into())
1255        }
1256
1257        fn scan(&mut self) -> Vec<ExternalDevice> {
1258            self.scan_count.fetch_add(1, Ordering::SeqCst);
1259            self.devices_to_return
1260                .iter()
1261                .map(|(id, ip)| {
1262                    let mut device = ExternalDevice::new(MockConnectionInfo { _device_id: *id });
1263                    device.ip_address = *ip;
1264                    device.hardware_name = Some(format!("Mock Device {}", id));
1265                    device
1266                })
1267                .collect()
1268        }
1269
1270        fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<Box<dyn StreamBackend>> {
1271            self.connect_called.store(true, Ordering::SeqCst);
1272            let _info = opaque_data
1273                .downcast::<MockConnectionInfo>()
1274                .map_err(|_| Error::invalid_config("wrong device type"))?;
1275            Ok(Box::new(MockBackend { connected: false }))
1276        }
1277    }
1278
1279    #[test]
1280    fn test_external_discoverer_scan_is_called() {
1281        let discoverer = MockExternalDiscoverer::new(vec![(1, Some("10.0.0.1".parse().unwrap()))]);
1282        let scan_count = discoverer.scan_count.clone();
1283
1284        let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1285        discovery.register(Box::new(discoverer));
1286
1287        assert_eq!(scan_count.load(Ordering::SeqCst), 0);
1288        let devices = discovery.scan();
1289        assert_eq!(scan_count.load(Ordering::SeqCst), 1);
1290        assert_eq!(devices.len(), 1);
1291    }
1292
1293    #[test]
1294    fn test_external_discoverer_device_info() {
1295        let discoverer =
1296            MockExternalDiscoverer::new(vec![(42, Some("192.168.1.100".parse().unwrap()))]);
1297
1298        let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1299        discovery.register(Box::new(discoverer));
1300
1301        let devices = discovery.scan();
1302        assert_eq!(devices.len(), 1);
1303
1304        let device = &devices[0];
1305        assert_eq!(device.dac_type(), DacType::Custom("MockDAC".into()));
1306        assert_eq!(
1307            device.info().ip_address,
1308            Some("192.168.1.100".parse().unwrap())
1309        );
1310        assert_eq!(device.info().hardware_name, Some("Mock Device 42".into()));
1311    }
1312
1313    #[test]
1314    fn test_external_discoverer_connect() {
1315        let discoverer = MockExternalDiscoverer::new(vec![(99, None)]);
1316        let connect_called = discoverer.connect_called.clone();
1317
1318        let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1319        discovery.register(Box::new(discoverer));
1320
1321        let devices = discovery.scan();
1322        assert_eq!(devices.len(), 1);
1323        assert!(!connect_called.load(Ordering::SeqCst));
1324
1325        let backend = discovery.connect(devices.into_iter().next().unwrap());
1326        assert!(backend.is_ok());
1327        assert!(connect_called.load(Ordering::SeqCst));
1328
1329        let backend = backend.unwrap();
1330        assert_eq!(backend.dac_type(), DacType::Custom("MockDAC".into()));
1331    }
1332
1333    #[test]
1334    fn test_external_discoverer_multiple_devices() {
1335        let discoverer = MockExternalDiscoverer::new(vec![
1336            (1, Some("10.0.0.1".parse().unwrap())),
1337            (2, Some("10.0.0.2".parse().unwrap())),
1338            (3, None),
1339        ]);
1340
1341        let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1342        discovery.register(Box::new(discoverer));
1343
1344        let devices = discovery.scan();
1345        assert_eq!(devices.len(), 3);
1346
1347        // Verify we can connect to any of them
1348        for device in devices {
1349            let backend = discovery.connect(device);
1350            assert!(backend.is_ok());
1351        }
1352    }
1353
1354    #[test]
1355    fn test_multiple_external_discoverers() {
1356        let discoverer1 = MockExternalDiscoverer::new(vec![(1, None)]);
1357        let discoverer2 = MockExternalDiscoverer::new(vec![(2, None), (3, None)]);
1358
1359        let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
1360        discovery.register(Box::new(discoverer1));
1361        discovery.register(Box::new(discoverer2));
1362
1363        let devices = discovery.scan();
1364        assert_eq!(devices.len(), 3);
1365    }
1366}