laser_dac/
types.rs

1//! DAC types for laser output.
2//!
3//! Provides DAC-agnostic types for laser frames and points,
4//! as well as device enumeration types.
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use std::fmt;
10
11/// A DAC-agnostic laser point with full-precision f32 coordinates.
12///
13/// Coordinates are normalized:
14/// - x: -1.0 (left) to 1.0 (right)
15/// - y: -1.0 (bottom) to 1.0 (top)
16///
17/// Colors are 16-bit (0-65535) to support high-resolution DACs.
18/// DACs with lower resolution (8-bit) will downscale automatically.
19///
20/// This allows each DAC to convert to its native format:
21/// - Helios: 12-bit unsigned (0-4095), inverted
22/// - EtherDream: 16-bit signed (-32768 to 32767)
23#[derive(Debug, Clone, Copy, PartialEq, Default)]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25pub struct LaserPoint {
26    /// X coordinate, -1.0 to 1.0
27    pub x: f32,
28    /// Y coordinate, -1.0 to 1.0
29    pub y: f32,
30    /// Red channel (0-65535)
31    pub r: u16,
32    /// Green channel (0-65535)
33    pub g: u16,
34    /// Blue channel (0-65535)
35    pub b: u16,
36    /// Intensity (0-65535)
37    pub intensity: u16,
38}
39
40impl LaserPoint {
41    /// Creates a new laser point.
42    pub fn new(x: f32, y: f32, r: u16, g: u16, b: u16, intensity: u16) -> Self {
43        Self {
44            x,
45            y,
46            r,
47            g,
48            b,
49            intensity,
50        }
51    }
52
53    /// Creates a blanked point (laser off) at the given position.
54    pub fn blanked(x: f32, y: f32) -> Self {
55        Self {
56            x,
57            y,
58            r: 0,
59            g: 0,
60            b: 0,
61            intensity: 0,
62        }
63    }
64}
65
66/// A DAC-agnostic laser frame with full-precision coordinates.
67#[derive(Debug, Clone, PartialEq, Default)]
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69pub struct LaserFrame {
70    /// Points per second output rate
71    pub pps: u32,
72    /// Points in this frame
73    pub points: Vec<LaserPoint>,
74}
75
76impl LaserFrame {
77    /// Creates a new laser frame.
78    pub fn new(pps: u32, points: Vec<LaserPoint>) -> Self {
79        Self { pps, points }
80    }
81}
82
83/// Types of laser DAC hardware supported.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
86pub enum DacType {
87    /// Helios laser DAC (USB connection).
88    Helios,
89    /// Ether Dream laser DAC (network connection).
90    EtherDream,
91    /// IDN laser DAC (ILDA Digital Network, network connection).
92    Idn,
93    /// LaserCube WiFi laser DAC (network connection).
94    LasercubeWifi,
95    /// LaserCube USB laser DAC (USB connection, also known as LaserDock).
96    LasercubeUsb,
97}
98
99impl DacType {
100    /// Returns all available DAC types.
101    pub fn all() -> &'static [DacType] {
102        &[
103            DacType::Helios,
104            DacType::EtherDream,
105            DacType::Idn,
106            DacType::LasercubeWifi,
107            DacType::LasercubeUsb,
108        ]
109    }
110
111    /// Returns the display name for this DAC type.
112    pub fn display_name(&self) -> &'static str {
113        match self {
114            DacType::Helios => "Helios",
115            DacType::EtherDream => "Ether Dream",
116            DacType::Idn => "IDN",
117            DacType::LasercubeWifi => "LaserCube WiFi",
118            DacType::LasercubeUsb => "LaserCube USB (Laserdock)",
119        }
120    }
121
122    /// Returns a description of this DAC type.
123    pub fn description(&self) -> &'static str {
124        match self {
125            DacType::Helios => "USB laser DAC",
126            DacType::EtherDream => "Network laser DAC",
127            DacType::Idn => "ILDA Digital Network laser DAC",
128            DacType::LasercubeWifi => "WiFi laser DAC",
129            DacType::LasercubeUsb => "USB laser DAC",
130        }
131    }
132}
133
134impl fmt::Display for DacType {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "{}", self.display_name())
137    }
138}
139
140/// Set of enabled DAC types for discovery.
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143pub struct EnabledDacTypes {
144    types: HashSet<DacType>,
145}
146
147impl EnabledDacTypes {
148    /// Creates a new set with all DAC types enabled.
149    pub fn all() -> Self {
150        Self {
151            types: DacType::all().iter().copied().collect(),
152        }
153    }
154
155    /// Creates an empty set (no DAC types enabled).
156    #[allow(dead_code)]
157    pub fn none() -> Self {
158        Self {
159            types: HashSet::new(),
160        }
161    }
162
163    /// Returns true if the given DAC type is enabled.
164    pub fn is_enabled(&self, dac_type: DacType) -> bool {
165        self.types.contains(&dac_type)
166    }
167
168    /// Enables a DAC type for discovery.
169    ///
170    /// Returns `&mut Self` to allow method chaining.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use laser_dac::{EnabledDacTypes, DacType};
176    ///
177    /// let mut enabled = EnabledDacTypes::none();
178    /// enabled.enable(DacType::Helios).enable(DacType::EtherDream);
179    ///
180    /// assert!(enabled.is_enabled(DacType::Helios));
181    /// assert!(enabled.is_enabled(DacType::EtherDream));
182    /// ```
183    pub fn enable(&mut self, dac_type: DacType) -> &mut Self {
184        self.types.insert(dac_type);
185        self
186    }
187
188    /// Disables a DAC type for discovery.
189    ///
190    /// Returns `&mut Self` to allow method chaining.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use laser_dac::{EnabledDacTypes, DacType};
196    ///
197    /// let mut enabled = EnabledDacTypes::all();
198    /// enabled.disable(DacType::Helios).disable(DacType::EtherDream);
199    ///
200    /// assert!(!enabled.is_enabled(DacType::Helios));
201    /// assert!(!enabled.is_enabled(DacType::EtherDream));
202    /// ```
203    pub fn disable(&mut self, dac_type: DacType) -> &mut Self {
204        self.types.remove(&dac_type);
205        self
206    }
207
208    /// Returns an iterator over enabled DAC types.
209    #[allow(dead_code)]
210    pub fn iter(&self) -> impl Iterator<Item = DacType> + '_ {
211        self.types.iter().copied()
212    }
213
214    /// Returns true if no DAC types are enabled.
215    #[allow(dead_code)]
216    pub fn is_empty(&self) -> bool {
217        self.types.is_empty()
218    }
219}
220
221impl Default for EnabledDacTypes {
222    fn default() -> Self {
223        Self::all()
224    }
225}
226
227impl std::iter::FromIterator<DacType> for EnabledDacTypes {
228    fn from_iter<I: IntoIterator<Item = DacType>>(iter: I) -> Self {
229        Self {
230            types: iter.into_iter().collect(),
231        }
232    }
233}
234
235impl Extend<DacType> for EnabledDacTypes {
236    fn extend<I: IntoIterator<Item = DacType>>(&mut self, iter: I) {
237        self.types.extend(iter);
238    }
239}
240
241/// Information about a discovered DAC device.
242/// The name is the unique identifier for the device.
243#[derive(Debug, Clone, PartialEq, Eq, Hash)]
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245pub struct DacDevice {
246    pub name: String,
247    pub dac_type: DacType,
248}
249
250impl DacDevice {
251    pub fn new(name: String, dac_type: DacType) -> Self {
252        Self { name, dac_type }
253    }
254}
255
256/// Connection state for a single DAC device.
257#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
259pub enum DacConnectionState {
260    /// Successfully connected and ready to receive frames.
261    Connected { name: String },
262    /// Connection was lost, waiting for reconnect.
263    Lost { name: String, error: Option<String> },
264}
265
266/// Information about a discovered DAC device.
267#[derive(Debug, Clone, PartialEq, Eq, Hash)]
268#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
269pub struct DiscoveredDac {
270    /// The type of DAC.
271    pub dac_type: DacType,
272    /// Unique identifier for this DAC.
273    pub id: String,
274    /// Human-readable name for this DAC.
275    pub name: String,
276    /// Network address (if applicable).
277    pub address: Option<String>,
278    /// Additional metadata about the DAC.
279    pub metadata: Option<String>,
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    // ==========================================================================
287    // LaserPoint Tests
288    // ==========================================================================
289
290    #[test]
291    fn test_laser_point_blanked_sets_all_colors_to_zero() {
292        // blanked() should set all color channels to 0 while preserving position
293        let point = LaserPoint::blanked(0.25, 0.75);
294        assert_eq!(point.x, 0.25);
295        assert_eq!(point.y, 0.75);
296        assert_eq!(point.r, 0);
297        assert_eq!(point.g, 0);
298        assert_eq!(point.b, 0);
299        assert_eq!(point.intensity, 0);
300    }
301
302    // ==========================================================================
303    // DacType Tests
304    // ==========================================================================
305
306    #[test]
307    fn test_dac_type_all_returns_all_five_types() {
308        let all_types = DacType::all();
309        assert_eq!(all_types.len(), 5);
310        assert!(all_types.contains(&DacType::Helios));
311        assert!(all_types.contains(&DacType::EtherDream));
312        assert!(all_types.contains(&DacType::Idn));
313        assert!(all_types.contains(&DacType::LasercubeWifi));
314        assert!(all_types.contains(&DacType::LasercubeUsb));
315    }
316
317    #[test]
318    fn test_dac_type_display_uses_display_name() {
319        // Display trait should delegate to display_name
320        assert_eq!(
321            format!("{}", DacType::Helios),
322            DacType::Helios.display_name()
323        );
324        assert_eq!(
325            format!("{}", DacType::EtherDream),
326            DacType::EtherDream.display_name()
327        );
328    }
329
330    #[test]
331    fn test_dac_type_can_be_used_in_hashset() {
332        use std::collections::HashSet;
333
334        let mut set = HashSet::new();
335        set.insert(DacType::Helios);
336        set.insert(DacType::Helios); // Duplicate should not increase count
337
338        assert_eq!(set.len(), 1);
339    }
340
341    // ==========================================================================
342    // EnabledDacTypes Tests
343    // ==========================================================================
344
345    #[test]
346    fn test_enabled_dac_types_all_enables_everything() {
347        let enabled = EnabledDacTypes::all();
348        for dac_type in DacType::all() {
349            assert!(
350                enabled.is_enabled(*dac_type),
351                "{:?} should be enabled",
352                dac_type
353            );
354        }
355        assert!(!enabled.is_empty());
356    }
357
358    #[test]
359    fn test_enabled_dac_types_none_disables_everything() {
360        let enabled = EnabledDacTypes::none();
361        for dac_type in DacType::all() {
362            assert!(
363                !enabled.is_enabled(*dac_type),
364                "{:?} should be disabled",
365                dac_type
366            );
367        }
368        assert!(enabled.is_empty());
369    }
370
371    #[test]
372    fn test_enabled_dac_types_enable_disable_toggles_correctly() {
373        let mut enabled = EnabledDacTypes::none();
374
375        // Enable one
376        enabled.enable(DacType::Helios);
377        assert!(enabled.is_enabled(DacType::Helios));
378        assert!(!enabled.is_enabled(DacType::EtherDream));
379
380        // Enable another
381        enabled.enable(DacType::EtherDream);
382        assert!(enabled.is_enabled(DacType::Helios));
383        assert!(enabled.is_enabled(DacType::EtherDream));
384
385        // Disable first
386        enabled.disable(DacType::Helios);
387        assert!(!enabled.is_enabled(DacType::Helios));
388        assert!(enabled.is_enabled(DacType::EtherDream));
389    }
390
391    #[test]
392    fn test_enabled_dac_types_iter_only_returns_enabled() {
393        let mut enabled = EnabledDacTypes::none();
394        enabled.enable(DacType::Helios);
395        enabled.enable(DacType::Idn);
396
397        let types: Vec<DacType> = enabled.iter().collect();
398        assert_eq!(types.len(), 2);
399        assert!(types.contains(&DacType::Helios));
400        assert!(types.contains(&DacType::Idn));
401        assert!(!types.contains(&DacType::EtherDream));
402    }
403
404    #[test]
405    fn test_enabled_dac_types_default_enables_all() {
406        let enabled = EnabledDacTypes::default();
407        // Default should be same as all()
408        for dac_type in DacType::all() {
409            assert!(enabled.is_enabled(*dac_type));
410        }
411    }
412
413    #[test]
414    fn test_enabled_dac_types_idempotent_operations() {
415        let mut enabled = EnabledDacTypes::none();
416
417        // Enabling twice should have same effect as once
418        enabled.enable(DacType::Helios);
419        enabled.enable(DacType::Helios);
420        assert!(enabled.is_enabled(DacType::Helios));
421
422        // Disabling twice should have same effect as once
423        enabled.disable(DacType::Helios);
424        enabled.disable(DacType::Helios);
425        assert!(!enabled.is_enabled(DacType::Helios));
426    }
427
428    #[test]
429    fn test_enabled_dac_types_chaining() {
430        let mut enabled = EnabledDacTypes::none();
431        enabled
432            .enable(DacType::Helios)
433            .enable(DacType::EtherDream)
434            .disable(DacType::Helios);
435
436        assert!(!enabled.is_enabled(DacType::Helios));
437        assert!(enabled.is_enabled(DacType::EtherDream));
438    }
439
440    // ==========================================================================
441    // DacConnectionState Tests
442    // ==========================================================================
443
444    #[test]
445    fn test_dac_connection_state_equality() {
446        let s1 = DacConnectionState::Connected {
447            name: "DAC1".to_string(),
448        };
449        let s2 = DacConnectionState::Connected {
450            name: "DAC1".to_string(),
451        };
452        let s3 = DacConnectionState::Connected {
453            name: "DAC2".to_string(),
454        };
455        let s4 = DacConnectionState::Lost {
456            name: "DAC1".to_string(),
457            error: None,
458        };
459
460        assert_eq!(s1, s2);
461        assert_ne!(s1, s3); // Different name
462        assert_ne!(s1, s4); // Different variant
463    }
464}