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