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