Skip to main content

laser_dac/
device.rs

1//! DAC identity, capabilities, and discovery filtering.
2//!
3//! Holds the types that describe a DAC as a *kind of device*:
4//!
5//! - [`DacType`] — the kind of DAC hardware (Helios, EtherDream, IDN, …)
6//! - [`DacCapabilities`] + [`OutputModel`] — scheduler-relevant capabilities
7//! - [`caps_for_dac_type`] — default capabilities per [`DacType`]
8//! - [`DacInfo`] — identity record for a discovered DAC before connection
9//! - [`DacDevice`], [`DacConnectionState`] — legacy identity / connection state
10//! - [`EnabledDacTypes`] — discovery filter selecting which kinds to scan
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14use std::collections::HashSet;
15use std::fmt;
16
17/// Types of laser DAC hardware supported.
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
20pub enum DacType {
21    /// Helios laser DAC (USB connection).
22    Helios,
23    /// Ether Dream laser DAC (network connection).
24    EtherDream,
25    /// IDN laser DAC (ILDA Digital Network, network connection).
26    Idn,
27    /// LaserCube WiFi laser DAC (network connection).
28    LasercubeWifi,
29    /// LaserCube USB laser DAC (USB connection, also known as LaserDock).
30    LasercubeUsb,
31    /// Oscilloscope XY output via stereo audio interface.
32    /// Maps LaserPoint.x → Left channel, LaserPoint.y → Right channel.
33    #[cfg(feature = "oscilloscope")]
34    Oscilloscope,
35    /// AVB audio device backend.
36    Avb,
37    /// Custom DAC implementation (for external/third-party backends).
38    Custom(String),
39}
40
41impl DacType {
42    /// Returns all available DAC types.
43    #[cfg(not(feature = "oscilloscope"))]
44    pub fn all() -> &'static [DacType] {
45        &[
46            DacType::Helios,
47            DacType::EtherDream,
48            DacType::Idn,
49            DacType::LasercubeWifi,
50            DacType::LasercubeUsb,
51            DacType::Avb,
52        ]
53    }
54
55    /// Returns all available DAC types.
56    #[cfg(feature = "oscilloscope")]
57    pub fn all() -> &'static [DacType] {
58        &[
59            DacType::Helios,
60            DacType::EtherDream,
61            DacType::Idn,
62            DacType::LasercubeWifi,
63            DacType::LasercubeUsb,
64            DacType::Avb,
65            DacType::Oscilloscope,
66        ]
67    }
68
69    /// Returns the display name for this DAC type.
70    pub fn display_name(&self) -> &str {
71        match self {
72            DacType::Helios => "Helios",
73            DacType::EtherDream => "Ether Dream",
74            DacType::Idn => "IDN",
75            DacType::LasercubeWifi => "LaserCube WiFi",
76            DacType::LasercubeUsb => "LaserCube USB (Laserdock)",
77            #[cfg(feature = "oscilloscope")]
78            DacType::Oscilloscope => "Oscilloscope",
79            DacType::Avb => "AVB Audio Device",
80            DacType::Custom(name) => name,
81        }
82    }
83
84    /// Returns a description of this DAC type.
85    pub fn description(&self) -> &'static str {
86        match self {
87            DacType::Helios => "USB laser DAC",
88            DacType::EtherDream => "Network laser DAC",
89            DacType::Idn => "ILDA Digital Network laser DAC",
90            DacType::LasercubeWifi => "WiFi laser DAC",
91            DacType::LasercubeUsb => "USB laser DAC",
92            #[cfg(feature = "oscilloscope")]
93            DacType::Oscilloscope => "Oscilloscope XY output via stereo audio",
94            DacType::Avb => "AVB audio network output",
95            DacType::Custom(_) => "Custom DAC",
96        }
97    }
98}
99
100impl fmt::Display for DacType {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{}", self.display_name())
103    }
104}
105
106/// Set of enabled DAC types for discovery.
107#[derive(Debug, Clone, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109pub struct EnabledDacTypes {
110    types: HashSet<DacType>,
111}
112
113impl EnabledDacTypes {
114    /// Creates a new set with all DAC types enabled.
115    pub fn all() -> Self {
116        Self {
117            types: DacType::all().iter().cloned().collect(),
118        }
119    }
120
121    /// Creates an empty set (no DAC types enabled).
122    pub fn none() -> Self {
123        Self {
124            types: HashSet::new(),
125        }
126    }
127
128    /// Returns true if the given DAC type is enabled.
129    pub fn is_enabled(&self, dac_type: DacType) -> bool {
130        self.types.contains(&dac_type)
131    }
132
133    /// Enables a DAC type for discovery.
134    ///
135    /// Returns `&mut Self` to allow method chaining.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use laser_dac::{EnabledDacTypes, DacType};
141    ///
142    /// let mut enabled = EnabledDacTypes::none();
143    /// enabled.enable(DacType::Helios).enable(DacType::EtherDream);
144    ///
145    /// assert!(enabled.is_enabled(DacType::Helios));
146    /// assert!(enabled.is_enabled(DacType::EtherDream));
147    /// ```
148    pub fn enable(&mut self, dac_type: DacType) -> &mut Self {
149        self.types.insert(dac_type);
150        self
151    }
152
153    /// Disables a DAC type for discovery.
154    ///
155    /// Returns `&mut Self` to allow method chaining.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use laser_dac::{EnabledDacTypes, DacType};
161    ///
162    /// let mut enabled = EnabledDacTypes::all();
163    /// enabled.disable(DacType::Helios).disable(DacType::EtherDream);
164    ///
165    /// assert!(!enabled.is_enabled(DacType::Helios));
166    /// assert!(!enabled.is_enabled(DacType::EtherDream));
167    /// ```
168    pub fn disable(&mut self, dac_type: DacType) -> &mut Self {
169        self.types.remove(&dac_type);
170        self
171    }
172
173    /// Returns an iterator over enabled DAC types.
174    pub fn iter(&self) -> impl Iterator<Item = DacType> + '_ {
175        self.types.iter().cloned()
176    }
177
178    /// Returns a copy with the given DAC type removed.
179    ///
180    /// Useful in conjunction with `DacDiscovery::register` to replace a
181    /// built-in discoverer with a custom-configured one (e.g., IDN with
182    /// specific scan addresses for testing).
183    pub fn without(mut self, dac_type: DacType) -> Self {
184        self.types.remove(&dac_type);
185        self
186    }
187
188    /// Returns true if no DAC types are enabled.
189    pub fn is_empty(&self) -> bool {
190        self.types.is_empty()
191    }
192}
193
194impl Default for EnabledDacTypes {
195    fn default() -> Self {
196        Self::all()
197    }
198}
199
200impl std::iter::FromIterator<DacType> for EnabledDacTypes {
201    fn from_iter<I: IntoIterator<Item = DacType>>(iter: I) -> Self {
202        Self {
203            types: iter.into_iter().collect(),
204        }
205    }
206}
207
208impl Extend<DacType> for EnabledDacTypes {
209    fn extend<I: IntoIterator<Item = DacType>>(&mut self, iter: I) {
210        self.types.extend(iter);
211    }
212}
213
214/// Information about a discovered DAC device.
215/// The name is the unique identifier for the device.
216#[derive(Debug, Clone, PartialEq, Eq, Hash)]
217#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
218pub struct DacDevice {
219    pub name: String,
220    pub dac_type: DacType,
221}
222
223impl DacDevice {
224    pub fn new(name: String, dac_type: DacType) -> Self {
225        Self { name, dac_type }
226    }
227}
228
229/// Connection state for a single DAC device.
230#[derive(Debug, Clone, PartialEq, Eq, Hash)]
231#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
232pub enum DacConnectionState {
233    /// Successfully connected and ready to receive frames.
234    Connected { name: String },
235    /// Worker stopped normally (callback returned None or stop() was called).
236    Stopped { name: String },
237    /// Connection was lost due to an error.
238    Lost { name: String, error: Option<String> },
239}
240
241/// DAC capabilities that inform the stream scheduler about safe chunk sizes and behaviors.
242#[derive(Clone, Debug)]
243#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
244pub struct DacCapabilities {
245    /// Minimum points-per-second (hardware/protocol limit where known).
246    ///
247    /// A value of 1 means no known protocol constraint. Helios (7) and
248    /// Ether Dream (1) have true hardware minimums. Note that very low PPS
249    /// increases point dwell time and can produce flickery output.
250    pub pps_min: u32,
251    /// Maximum supported points-per-second (hardware limit).
252    pub pps_max: u32,
253    /// Maximum number of points allowed per chunk submission.
254    pub max_points_per_chunk: usize,
255    /// The scheduler-relevant output model.
256    pub output_model: OutputModel,
257}
258
259impl Default for DacCapabilities {
260    fn default() -> Self {
261        Self {
262            pps_min: 1,
263            pps_max: 100_000,
264            max_points_per_chunk: 4096,
265            output_model: OutputModel::NetworkFifo,
266        }
267    }
268}
269
270/// Get default capabilities for a DAC type.
271///
272/// This delegates to each protocol's `default_capabilities()` function.
273/// For optimal performance, backends should query actual device capabilities
274/// at runtime where the protocol supports it (e.g., LaserCube's `max_dac_rate`
275/// and ringbuffer queries).
276pub fn caps_for_dac_type(dac_type: &DacType) -> DacCapabilities {
277    match dac_type {
278        #[cfg(feature = "helios")]
279        DacType::Helios => crate::protocols::helios::default_capabilities(),
280        #[cfg(not(feature = "helios"))]
281        DacType::Helios => DacCapabilities::default(),
282
283        #[cfg(feature = "ether-dream")]
284        DacType::EtherDream => crate::protocols::ether_dream::default_capabilities(),
285        #[cfg(not(feature = "ether-dream"))]
286        DacType::EtherDream => DacCapabilities::default(),
287
288        #[cfg(feature = "idn")]
289        DacType::Idn => crate::protocols::idn::default_capabilities(),
290        #[cfg(not(feature = "idn"))]
291        DacType::Idn => DacCapabilities::default(),
292
293        #[cfg(feature = "lasercube-wifi")]
294        DacType::LasercubeWifi => crate::protocols::lasercube_wifi::default_capabilities(),
295        #[cfg(not(feature = "lasercube-wifi"))]
296        DacType::LasercubeWifi => DacCapabilities::default(),
297
298        #[cfg(feature = "lasercube-usb")]
299        DacType::LasercubeUsb => crate::protocols::lasercube_usb::default_capabilities(),
300        #[cfg(not(feature = "lasercube-usb"))]
301        DacType::LasercubeUsb => DacCapabilities::default(),
302
303        #[cfg(feature = "oscilloscope")]
304        DacType::Oscilloscope => DacCapabilities::default(), // Caps depend on sample rate, use backend's actual caps
305
306        #[cfg(feature = "avb")]
307        DacType::Avb => crate::protocols::avb::default_capabilities(),
308        #[cfg(not(feature = "avb"))]
309        DacType::Avb => DacCapabilities::default(),
310
311        DacType::Custom(_) => DacCapabilities::default(),
312    }
313}
314
315/// The scheduler-relevant output model for a DAC.
316#[derive(Clone, Debug, PartialEq, Eq)]
317#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
318pub enum OutputModel {
319    /// Frame swap / limited queue depth (e.g., Helios-style double-buffering).
320    UsbFrameSwap,
321    /// FIFO-ish buffer where "top up" is natural (e.g., Ether Dream-style).
322    NetworkFifo,
323    /// Timed UDP chunks where OS send may not reflect hardware pacing.
324    UdpTimed,
325}
326
327/// Information about a discovered DAC before connection.
328#[derive(Clone, Debug)]
329#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
330pub struct DacInfo {
331    /// Stable, unique identifier used for (re)selecting DACs.
332    pub id: String,
333    /// Human-readable name for the DAC.
334    pub name: String,
335    /// The type of DAC hardware.
336    pub kind: DacType,
337    /// DAC capabilities.
338    pub caps: DacCapabilities,
339}
340
341impl DacInfo {
342    /// Create a new DAC info.
343    pub fn new(
344        id: impl Into<String>,
345        name: impl Into<String>,
346        kind: DacType,
347        caps: DacCapabilities,
348    ) -> Self {
349        Self {
350            id: id.into(),
351            name: name.into(),
352            kind,
353            caps,
354        }
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    // ==========================================================================
363    // DacType Tests
364    // ==========================================================================
365
366    #[test]
367    fn test_dac_type_all_returns_all_builtin_types() {
368        let all_types = DacType::all();
369        #[cfg(not(feature = "oscilloscope"))]
370        assert_eq!(all_types.len(), 6);
371        #[cfg(feature = "oscilloscope")]
372        assert_eq!(all_types.len(), 7);
373        assert!(all_types.contains(&DacType::Helios));
374        assert!(all_types.contains(&DacType::EtherDream));
375        assert!(all_types.contains(&DacType::Idn));
376        assert!(all_types.contains(&DacType::LasercubeWifi));
377        assert!(all_types.contains(&DacType::LasercubeUsb));
378        assert!(all_types.contains(&DacType::Avb));
379        #[cfg(feature = "oscilloscope")]
380        assert!(all_types.contains(&DacType::Oscilloscope));
381    }
382
383    #[test]
384    fn test_dac_type_display_uses_display_name() {
385        // Display trait should delegate to display_name
386        assert_eq!(
387            format!("{}", DacType::Helios),
388            DacType::Helios.display_name()
389        );
390        assert_eq!(
391            format!("{}", DacType::EtherDream),
392            DacType::EtherDream.display_name()
393        );
394    }
395
396    #[test]
397    fn test_dac_type_can_be_used_in_hashset() {
398        use std::collections::HashSet;
399
400        let mut set = HashSet::new();
401        set.insert(DacType::Helios);
402        set.insert(DacType::Helios); // Duplicate should not increase count
403
404        assert_eq!(set.len(), 1);
405    }
406
407    // ==========================================================================
408    // EnabledDacTypes Tests
409    // ==========================================================================
410
411    #[test]
412    fn test_enabled_dac_types_all_enables_everything() {
413        let enabled = EnabledDacTypes::all();
414        for dac_type in DacType::all() {
415            assert!(
416                enabled.is_enabled(dac_type.clone()),
417                "{:?} should be enabled",
418                dac_type
419            );
420        }
421        assert!(!enabled.is_empty());
422    }
423
424    #[test]
425    fn test_enabled_dac_types_none_disables_everything() {
426        let enabled = EnabledDacTypes::none();
427        for dac_type in DacType::all() {
428            assert!(
429                !enabled.is_enabled(dac_type.clone()),
430                "{:?} should be disabled",
431                dac_type
432            );
433        }
434        assert!(enabled.is_empty());
435    }
436
437    #[test]
438    fn test_enabled_dac_types_enable_disable_toggles_correctly() {
439        let mut enabled = EnabledDacTypes::none();
440
441        // Enable one
442        enabled.enable(DacType::Helios);
443        assert!(enabled.is_enabled(DacType::Helios));
444        assert!(!enabled.is_enabled(DacType::EtherDream));
445
446        // Enable another
447        enabled.enable(DacType::EtherDream);
448        assert!(enabled.is_enabled(DacType::Helios));
449        assert!(enabled.is_enabled(DacType::EtherDream));
450
451        // Disable first
452        enabled.disable(DacType::Helios);
453        assert!(!enabled.is_enabled(DacType::Helios));
454        assert!(enabled.is_enabled(DacType::EtherDream));
455    }
456
457    #[test]
458    fn test_enabled_dac_types_iter_only_returns_enabled() {
459        let mut enabled = EnabledDacTypes::none();
460        enabled.enable(DacType::Helios);
461        enabled.enable(DacType::Idn);
462
463        let types: Vec<DacType> = enabled.iter().collect();
464        assert_eq!(types.len(), 2);
465        assert!(types.contains(&DacType::Helios));
466        assert!(types.contains(&DacType::Idn));
467        assert!(!types.contains(&DacType::EtherDream));
468    }
469
470    #[test]
471    fn test_enabled_dac_types_default_enables_all() {
472        let enabled = EnabledDacTypes::default();
473        // Default should be same as all()
474        for dac_type in DacType::all() {
475            assert!(enabled.is_enabled(dac_type.clone()));
476        }
477    }
478
479    #[test]
480    fn test_enabled_dac_types_idempotent_operations() {
481        let mut enabled = EnabledDacTypes::none();
482
483        // Enabling twice should have same effect as once
484        enabled.enable(DacType::Helios);
485        enabled.enable(DacType::Helios);
486        assert!(enabled.is_enabled(DacType::Helios));
487
488        // Disabling twice should have same effect as once
489        enabled.disable(DacType::Helios);
490        enabled.disable(DacType::Helios);
491        assert!(!enabled.is_enabled(DacType::Helios));
492    }
493
494    #[test]
495    fn test_enabled_dac_types_chaining() {
496        let mut enabled = EnabledDacTypes::none();
497        enabled
498            .enable(DacType::Helios)
499            .enable(DacType::EtherDream)
500            .disable(DacType::Helios);
501
502        assert!(!enabled.is_enabled(DacType::Helios));
503        assert!(enabled.is_enabled(DacType::EtherDream));
504    }
505
506    // ==========================================================================
507    // DacConnectionState Tests
508    // ==========================================================================
509
510    #[test]
511    fn test_dac_connection_state_equality() {
512        let s1 = DacConnectionState::Connected {
513            name: "DAC1".to_string(),
514        };
515        let s2 = DacConnectionState::Connected {
516            name: "DAC1".to_string(),
517        };
518        let s3 = DacConnectionState::Connected {
519            name: "DAC2".to_string(),
520        };
521        let s4 = DacConnectionState::Lost {
522            name: "DAC1".to_string(),
523            error: None,
524        };
525
526        assert_eq!(s1, s2);
527        assert_ne!(s1, s3); // Different name
528        assert_ne!(s1, s4); // Different variant
529    }
530}