Skip to main content

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            ..Default::default()
59        }
60    }
61}
62
63/// Types of laser DAC hardware supported.
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
66pub enum DacType {
67    /// Helios laser DAC (USB connection).
68    Helios,
69    /// Ether Dream laser DAC (network connection).
70    EtherDream,
71    /// IDN laser DAC (ILDA Digital Network, network connection).
72    Idn,
73    /// LaserCube WiFi laser DAC (network connection).
74    LasercubeWifi,
75    /// LaserCube USB laser DAC (USB connection, also known as LaserDock).
76    LasercubeUsb,
77    /// AVB audio device backend.
78    Avb,
79    /// Custom DAC implementation (for external/third-party backends).
80    Custom(String),
81}
82
83impl DacType {
84    /// Returns all available DAC types.
85    pub fn all() -> &'static [DacType] {
86        &[
87            DacType::Helios,
88            DacType::EtherDream,
89            DacType::Idn,
90            DacType::LasercubeWifi,
91            DacType::LasercubeUsb,
92            DacType::Avb,
93        ]
94    }
95
96    /// Returns the display name for this DAC type.
97    pub fn display_name(&self) -> &str {
98        match self {
99            DacType::Helios => "Helios",
100            DacType::EtherDream => "Ether Dream",
101            DacType::Idn => "IDN",
102            DacType::LasercubeWifi => "LaserCube WiFi",
103            DacType::LasercubeUsb => "LaserCube USB (Laserdock)",
104            DacType::Avb => "AVB Audio Device",
105            DacType::Custom(name) => name,
106        }
107    }
108
109    /// Returns a description of this DAC type.
110    pub fn description(&self) -> &'static str {
111        match self {
112            DacType::Helios => "USB laser DAC",
113            DacType::EtherDream => "Network laser DAC",
114            DacType::Idn => "ILDA Digital Network laser DAC",
115            DacType::LasercubeWifi => "WiFi laser DAC",
116            DacType::LasercubeUsb => "USB laser DAC",
117            DacType::Avb => "AVB audio network output",
118            DacType::Custom(_) => "Custom DAC",
119        }
120    }
121}
122
123impl fmt::Display for DacType {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "{}", self.display_name())
126    }
127}
128
129/// Set of enabled DAC types for discovery.
130#[derive(Debug, Clone, PartialEq, Eq)]
131#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
132pub struct EnabledDacTypes {
133    types: HashSet<DacType>,
134}
135
136impl EnabledDacTypes {
137    /// Creates a new set with all DAC types enabled.
138    pub fn all() -> Self {
139        Self {
140            types: DacType::all().iter().cloned().collect(),
141        }
142    }
143
144    /// Creates an empty set (no DAC types enabled).
145    #[allow(dead_code)]
146    pub fn none() -> Self {
147        Self {
148            types: HashSet::new(),
149        }
150    }
151
152    /// Returns true if the given DAC type is enabled.
153    pub fn is_enabled(&self, dac_type: DacType) -> bool {
154        self.types.contains(&dac_type)
155    }
156
157    /// Enables a DAC type for discovery.
158    ///
159    /// Returns `&mut Self` to allow method chaining.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use laser_dac::{EnabledDacTypes, DacType};
165    ///
166    /// let mut enabled = EnabledDacTypes::none();
167    /// enabled.enable(DacType::Helios).enable(DacType::EtherDream);
168    ///
169    /// assert!(enabled.is_enabled(DacType::Helios));
170    /// assert!(enabled.is_enabled(DacType::EtherDream));
171    /// ```
172    pub fn enable(&mut self, dac_type: DacType) -> &mut Self {
173        self.types.insert(dac_type);
174        self
175    }
176
177    /// Disables a DAC type for discovery.
178    ///
179    /// Returns `&mut Self` to allow method chaining.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use laser_dac::{EnabledDacTypes, DacType};
185    ///
186    /// let mut enabled = EnabledDacTypes::all();
187    /// enabled.disable(DacType::Helios).disable(DacType::EtherDream);
188    ///
189    /// assert!(!enabled.is_enabled(DacType::Helios));
190    /// assert!(!enabled.is_enabled(DacType::EtherDream));
191    /// ```
192    pub fn disable(&mut self, dac_type: DacType) -> &mut Self {
193        self.types.remove(&dac_type);
194        self
195    }
196
197    /// Returns an iterator over enabled DAC types.
198    #[allow(dead_code)]
199    pub fn iter(&self) -> impl Iterator<Item = DacType> + '_ {
200        self.types.iter().cloned()
201    }
202
203    /// Returns true if no DAC types are enabled.
204    #[allow(dead_code)]
205    pub fn is_empty(&self) -> bool {
206        self.types.is_empty()
207    }
208}
209
210impl Default for EnabledDacTypes {
211    fn default() -> Self {
212        Self::all()
213    }
214}
215
216impl std::iter::FromIterator<DacType> for EnabledDacTypes {
217    fn from_iter<I: IntoIterator<Item = DacType>>(iter: I) -> Self {
218        Self {
219            types: iter.into_iter().collect(),
220        }
221    }
222}
223
224impl Extend<DacType> for EnabledDacTypes {
225    fn extend<I: IntoIterator<Item = DacType>>(&mut self, iter: I) {
226        self.types.extend(iter);
227    }
228}
229
230/// Information about a discovered DAC device.
231/// The name is the unique identifier for the device.
232#[derive(Debug, Clone, PartialEq, Eq, Hash)]
233#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
234pub struct DacDevice {
235    pub name: String,
236    pub dac_type: DacType,
237}
238
239impl DacDevice {
240    pub fn new(name: String, dac_type: DacType) -> Self {
241        Self { name, dac_type }
242    }
243}
244
245/// Connection state for a single DAC device.
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
247#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
248pub enum DacConnectionState {
249    /// Successfully connected and ready to receive frames.
250    Connected { name: String },
251    /// Worker stopped normally (callback returned None or stop() was called).
252    Stopped { name: String },
253    /// Connection was lost due to an error.
254    Lost { name: String, error: Option<String> },
255}
256
257// =============================================================================
258// Streaming Types
259// =============================================================================
260
261/// DAC capabilities that inform the stream scheduler about safe chunk sizes and behaviors.
262#[derive(Clone, Debug)]
263#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
264pub struct DacCapabilities {
265    /// Minimum points-per-second (hardware/protocol limit where known).
266    ///
267    /// A value of 1 means no known protocol constraint. Helios (7) and
268    /// Ether Dream (1) have true hardware minimums. Note that very low PPS
269    /// increases point dwell time and can produce flickery output.
270    pub pps_min: u32,
271    /// Maximum supported points-per-second (hardware limit).
272    pub pps_max: u32,
273    /// Maximum number of points allowed per chunk submission.
274    pub max_points_per_chunk: usize,
275    /// The scheduler-relevant output model.
276    pub output_model: OutputModel,
277}
278
279impl Default for DacCapabilities {
280    fn default() -> Self {
281        Self {
282            pps_min: 1,
283            pps_max: 100_000,
284            max_points_per_chunk: 4096,
285            output_model: OutputModel::NetworkFifo,
286        }
287    }
288}
289
290/// Get default capabilities for a DAC type.
291///
292/// This delegates to each protocol's `default_capabilities()` function.
293/// For optimal performance, backends should query actual device capabilities
294/// at runtime where the protocol supports it (e.g., LaserCube's `max_dac_rate`
295/// and ringbuffer queries).
296pub fn caps_for_dac_type(dac_type: &DacType) -> DacCapabilities {
297    match dac_type {
298        #[cfg(feature = "helios")]
299        DacType::Helios => crate::protocols::helios::default_capabilities(),
300        #[cfg(not(feature = "helios"))]
301        DacType::Helios => DacCapabilities::default(),
302
303        #[cfg(feature = "ether-dream")]
304        DacType::EtherDream => crate::protocols::ether_dream::default_capabilities(),
305        #[cfg(not(feature = "ether-dream"))]
306        DacType::EtherDream => DacCapabilities::default(),
307
308        #[cfg(feature = "idn")]
309        DacType::Idn => crate::protocols::idn::default_capabilities(),
310        #[cfg(not(feature = "idn"))]
311        DacType::Idn => DacCapabilities::default(),
312
313        #[cfg(feature = "lasercube-wifi")]
314        DacType::LasercubeWifi => crate::protocols::lasercube_wifi::default_capabilities(),
315        #[cfg(not(feature = "lasercube-wifi"))]
316        DacType::LasercubeWifi => DacCapabilities::default(),
317
318        #[cfg(feature = "lasercube-usb")]
319        DacType::LasercubeUsb => crate::protocols::lasercube_usb::default_capabilities(),
320        #[cfg(not(feature = "lasercube-usb"))]
321        DacType::LasercubeUsb => DacCapabilities::default(),
322
323        #[cfg(feature = "avb")]
324        DacType::Avb => crate::protocols::avb::default_capabilities(),
325        #[cfg(not(feature = "avb"))]
326        DacType::Avb => DacCapabilities::default(),
327
328        DacType::Custom(_) => DacCapabilities::default(),
329    }
330}
331
332/// The scheduler-relevant output model for a DAC.
333#[derive(Clone, Debug, PartialEq, Eq)]
334#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
335pub enum OutputModel {
336    /// Frame swap / limited queue depth (e.g., Helios-style double-buffering).
337    UsbFrameSwap,
338    /// FIFO-ish buffer where "top up" is natural (e.g., Ether Dream-style).
339    NetworkFifo,
340    /// Timed UDP chunks where OS send may not reflect hardware pacing.
341    UdpTimed,
342}
343
344/// Represents a point in stream time, anchored to estimated playback position.
345///
346/// `StreamInstant` represents the **estimated playback time** of points, not merely
347/// "points sent so far." When used in `ChunkRequest::start`, it represents:
348///
349/// `start` = playhead + buffered
350///
351/// Where:
352/// - `playhead` = stream_epoch + estimated_consumed_points
353/// - `buffered` = points sent but not yet played
354///
355/// This allows callbacks to generate content for the exact time it will be displayed,
356/// enabling accurate audio synchronization.
357#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
358#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
359pub struct StreamInstant(pub u64);
360
361impl StreamInstant {
362    /// Create a new stream instant from a point count.
363    pub fn new(points: u64) -> Self {
364        Self(points)
365    }
366
367    /// Returns the number of points since stream start.
368    pub fn points(&self) -> u64 {
369        self.0
370    }
371
372    /// Convert this instant to seconds at the given points-per-second rate.
373    pub fn as_seconds(&self, pps: u32) -> f64 {
374        self.0 as f64 / pps as f64
375    }
376
377    /// Convert to seconds at the given PPS.
378    ///
379    /// This is an alias for `as_seconds()` for consistency with standard Rust
380    /// duration naming conventions (e.g., `Duration::as_secs_f64()`).
381    #[inline]
382    pub fn as_secs_f64(&self, pps: u32) -> f64 {
383        self.as_seconds(pps)
384    }
385
386    /// Create a stream instant from a duration in seconds at the given PPS.
387    pub fn from_seconds(seconds: f64, pps: u32) -> Self {
388        Self((seconds * pps as f64) as u64)
389    }
390
391    /// Add a number of points to this instant.
392    pub fn add_points(&self, points: u64) -> Self {
393        Self(self.0.saturating_add(points))
394    }
395
396    /// Subtract a number of points from this instant (saturating at 0).
397    pub fn sub_points(&self, points: u64) -> Self {
398        Self(self.0.saturating_sub(points))
399    }
400}
401
402impl std::ops::Add<u64> for StreamInstant {
403    type Output = Self;
404    fn add(self, rhs: u64) -> Self::Output {
405        self.add_points(rhs)
406    }
407}
408
409impl std::ops::Sub<u64> for StreamInstant {
410    type Output = Self;
411    fn sub(self, rhs: u64) -> Self::Output {
412        self.sub_points(rhs)
413    }
414}
415
416impl std::ops::AddAssign<u64> for StreamInstant {
417    fn add_assign(&mut self, rhs: u64) {
418        self.0 = self.0.saturating_add(rhs);
419    }
420}
421
422impl std::ops::SubAssign<u64> for StreamInstant {
423    fn sub_assign(&mut self, rhs: u64) {
424        self.0 = self.0.saturating_sub(rhs);
425    }
426}
427
428/// Configuration for starting a stream.
429///
430/// # Buffer-Driven Timing
431///
432/// The streaming API uses pure buffer-driven timing:
433/// - `target_buffer`: Target buffer level to maintain (default: 20ms)
434/// - `min_buffer`: Minimum buffer before requesting urgent fill (default: 8ms)
435///
436/// The callback is invoked when `buffered < target_buffer`. The callback receives
437/// a `ChunkRequest` with `min_points` and `target_points` calculated from these
438/// durations and the current buffer state.
439///
440/// To reduce perceived latency, reduce `target_buffer`.
441#[derive(Clone, Debug)]
442#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
443pub struct StreamConfig {
444    /// Points per second output rate.
445    pub pps: u32,
446
447    /// Target buffer level to maintain (default: 20ms).
448    ///
449    /// The callback's `target_points` is calculated to bring the buffer to this level.
450    /// The callback is invoked when the buffer drops below this level.
451    #[cfg_attr(feature = "serde", serde(with = "duration_millis"))]
452    pub target_buffer: std::time::Duration,
453
454    /// Minimum buffer before requesting urgent fill (default: 8ms).
455    ///
456    /// When buffer drops below this, `min_points` in `ChunkRequest` will be non-zero.
457    #[cfg_attr(feature = "serde", serde(with = "duration_millis"))]
458    pub min_buffer: std::time::Duration,
459
460    /// What to do when the producer can't keep up.
461    pub underrun: UnderrunPolicy,
462
463    /// Maximum time to wait for queued points to drain on graceful shutdown (default: 1s).
464    ///
465    /// When the producer returns `ChunkResult::End`, the stream waits for buffered
466    /// points to play out before returning. This timeout caps that wait to prevent
467    /// blocking forever if the DAC stalls or queue depth is unknown.
468    #[cfg_attr(feature = "serde", serde(with = "duration_millis"))]
469    pub drain_timeout: std::time::Duration,
470
471    /// Initial color delay for scanner sync compensation (default: disabled).
472    ///
473    /// Delays RGB+intensity channels relative to XY coordinates by this duration,
474    /// allowing galvo mirrors time to settle before the laser fires. The delay is
475    /// implemented as a FIFO: output colors lag input colors by `ceil(color_delay * pps)` points.
476    ///
477    /// Can be changed at runtime via [`crate::StreamControl::set_color_delay`] /
478    /// [`crate::SessionControl::set_color_delay`].
479    ///
480    /// Typical values: 50–200µs depending on scanner speed.
481    /// `Duration::ZERO` disables the delay (default).
482    #[cfg_attr(feature = "serde", serde(with = "duration_micros"))]
483    pub color_delay: std::time::Duration,
484
485    /// Duration of forced blanking after arming (default: 1ms).
486    ///
487    /// After the stream is armed, the first `ceil(startup_blank * pps)` points
488    /// will have their color channels forced to zero, regardless of what the
489    /// producer writes. This prevents the "flash on start" artifact where
490    /// the laser fires before mirrors reach position.
491    ///
492    /// Note: when `color_delay` is also active, the delay line provides
493    /// `color_delay` worth of natural startup blanking. This `startup_blank`
494    /// setting adds blanking *beyond* that duration.
495    ///
496    /// Set to `Duration::ZERO` to disable explicit startup blanking.
497    #[cfg_attr(feature = "serde", serde(with = "duration_micros"))]
498    pub startup_blank: std::time::Duration,
499}
500
501#[cfg(feature = "serde")]
502mod duration_millis {
503    use serde::{Deserialize, Deserializer, Serialize, Serializer};
504    use std::time::Duration;
505
506    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
507    where
508        S: Serializer,
509    {
510        // Use u64 for both serialize and deserialize to ensure round-trip compatibility.
511        // Clamp to u64::MAX for durations > ~584 million years (practically never hit).
512        let millis = duration.as_millis().min(u64::MAX as u128) as u64;
513        millis.serialize(serializer)
514    }
515
516    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
517    where
518        D: Deserializer<'de>,
519    {
520        let millis = u64::deserialize(deserializer)?;
521        Ok(Duration::from_millis(millis))
522    }
523}
524
525#[cfg(feature = "serde")]
526mod duration_micros {
527    use serde::{Deserialize, Deserializer, Serialize, Serializer};
528    use std::time::Duration;
529
530    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
531    where
532        S: Serializer,
533    {
534        let micros = duration.as_micros().min(u64::MAX as u128) as u64;
535        micros.serialize(serializer)
536    }
537
538    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
539    where
540        D: Deserializer<'de>,
541    {
542        let micros = u64::deserialize(deserializer)?;
543        Ok(Duration::from_micros(micros))
544    }
545}
546
547impl Default for StreamConfig {
548    fn default() -> Self {
549        use std::time::Duration;
550        Self {
551            pps: 30_000,
552            target_buffer: Duration::from_millis(20),
553            min_buffer: Duration::from_millis(8),
554            underrun: UnderrunPolicy::default(),
555            drain_timeout: Duration::from_secs(1),
556            color_delay: Duration::ZERO,
557            startup_blank: Duration::from_millis(1),
558        }
559    }
560}
561
562impl StreamConfig {
563    /// Create a new stream configuration with the given PPS.
564    pub fn new(pps: u32) -> Self {
565        Self {
566            pps,
567            ..Default::default()
568        }
569    }
570
571    /// Set the target buffer level to maintain (builder pattern).
572    ///
573    /// Default: 20ms. Higher values provide more safety margin against underruns.
574    /// Lower values reduce perceived latency.
575    pub fn with_target_buffer(mut self, duration: std::time::Duration) -> Self {
576        self.target_buffer = duration;
577        self
578    }
579
580    /// Set the minimum buffer level before urgent fill (builder pattern).
581    ///
582    /// Default: 8ms. When buffer drops below this, `min_points` will be non-zero.
583    pub fn with_min_buffer(mut self, duration: std::time::Duration) -> Self {
584        self.min_buffer = duration;
585        self
586    }
587
588    /// Set the underrun policy (builder pattern).
589    pub fn with_underrun(mut self, policy: UnderrunPolicy) -> Self {
590        self.underrun = policy;
591        self
592    }
593
594    /// Set the drain timeout for graceful shutdown (builder pattern).
595    ///
596    /// Default: 1 second. Set to `Duration::ZERO` to skip drain entirely.
597    pub fn with_drain_timeout(mut self, timeout: std::time::Duration) -> Self {
598        self.drain_timeout = timeout;
599        self
600    }
601
602    /// Set the color delay for scanner sync compensation (builder pattern).
603    ///
604    /// Default: `Duration::ZERO` (disabled). Typical values: 50–200µs.
605    pub fn with_color_delay(mut self, delay: std::time::Duration) -> Self {
606        self.color_delay = delay;
607        self
608    }
609
610    /// Set the startup blanking duration after arming (builder pattern).
611    ///
612    /// Default: 1ms. Set to `Duration::ZERO` to disable.
613    pub fn with_startup_blank(mut self, duration: std::time::Duration) -> Self {
614        self.startup_blank = duration;
615        self
616    }
617}
618
619/// Policy for what to do when the producer can't keep up with the stream.
620#[derive(Clone, Debug, PartialEq)]
621#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
622#[derive(Default)]
623pub enum UnderrunPolicy {
624    /// Repeat the last chunk of points.
625    RepeatLast,
626    /// Output blanked points (laser off).
627    #[default]
628    Blank,
629    /// Park the beam at a specific position with laser off.
630    Park { x: f32, y: f32 },
631    /// Stop the stream entirely on underrun.
632    Stop,
633}
634
635/// A request to fill a buffer with points for streaming.
636///
637/// This is the streaming API with pure buffer-driven timing.
638/// The callback receives a `ChunkRequest` describing buffer state and requirements,
639/// and fills points into a library-owned buffer.
640///
641/// # Point Tiers
642///
643/// - `min_points`: Minimum points needed to avoid imminent underrun (ceiling rounded)
644/// - `target_points`: Ideal number of points to reach target buffer level (clamped to buffer length)
645/// - `buffer.len()` (passed separately): Maximum points the callback may write
646///
647/// # Rounding Rules
648///
649/// - `min_points`: Always **ceiling** (underrun prevention)
650/// - `target_points`: **ceiling**, then clamped to buffer length
651#[derive(Clone, Debug)]
652pub struct ChunkRequest {
653    /// Estimated playback time when this chunk starts.
654    ///
655    /// Calculated as: playhead + buffered_points
656    /// Use this for audio synchronization.
657    pub start: StreamInstant,
658
659    /// Points per second (fixed for stream duration).
660    pub pps: u32,
661
662    /// Minimum points needed to avoid imminent underrun.
663    ///
664    /// Calculated with ceiling to prevent underrun: `ceil((min_buffer - buffered) * pps)`
665    /// If 0, buffer is healthy.
666    pub min_points: usize,
667
668    /// Ideal number of points to reach target buffer level.
669    ///
670    /// Calculated as: `ceil((target_buffer - buffered) * pps)`, clamped to buffer length.
671    pub target_points: usize,
672
673    /// Current buffer level in points (for diagnostics/adaptive content).
674    pub buffered_points: u64,
675
676    /// Current buffer level as duration (for audio sync convenience).
677    pub buffered: std::time::Duration,
678
679    /// Raw device queue if available (best-effort, may differ from buffered_points).
680    pub device_queued_points: Option<u64>,
681}
682
683/// Result returned by the fill callback indicating how the buffer was filled.
684///
685/// This enum allows the callback to communicate three distinct states:
686/// - Successfully filled some number of points
687/// - Temporarily unable to provide data (underrun policy applies)
688/// - Stream should end gracefully
689///
690/// # `Filled(0)` Semantics
691///
692/// - If `target_points == 0`: Buffer is full, nothing needed. This is fine.
693/// - If `target_points > 0`: We needed points but got none. Treated as `Starved`.
694#[derive(Clone, Copy, Debug, PartialEq, Eq)]
695pub enum ChunkResult {
696    /// Wrote n points to the buffer.
697    ///
698    /// `n` must be <= `buffer.len()`.
699    /// Partial fills (`n < min_points`) are accepted without padding - useful when
700    /// content is legitimately ending. Return `End` on the next call to signal completion.
701    Filled(usize),
702
703    /// No data available right now.
704    ///
705    /// Underrun policy is applied (repeat last chunk or blank).
706    /// Stream continues; callback will be called again when buffer needs filling.
707    Starved,
708
709    /// Stream is finished. Shutdown sequence:
710    /// 1. Stop calling callback
711    /// 2. Let queued points drain (play out)
712    /// 3. Blank/park the laser at last position
713    /// 4. Return from stream() with `RunExit::ProducerEnded`
714    End,
715}
716
717/// Current status of a stream.
718#[derive(Clone, Debug)]
719pub struct StreamStatus {
720    /// Whether the device is connected.
721    pub connected: bool,
722    /// Library-owned scheduled amount.
723    pub scheduled_ahead_points: u64,
724    /// Best-effort device/backend estimate.
725    pub device_queued_points: Option<u64>,
726    /// Optional statistics for diagnostics.
727    pub stats: Option<StreamStats>,
728}
729
730/// Stream statistics for diagnostics and debugging.
731#[derive(Clone, Debug, Default)]
732pub struct StreamStats {
733    /// Number of times the stream underran.
734    pub underrun_count: u64,
735    /// Number of chunks that arrived late.
736    pub late_chunk_count: u64,
737    /// Number of times the device reconnected.
738    pub reconnect_count: u64,
739    /// Total chunks written since stream start.
740    pub chunks_written: u64,
741    /// Total points written since stream start.
742    pub points_written: u64,
743}
744
745/// How a callback-mode stream run ended.
746#[derive(Clone, Debug, PartialEq, Eq)]
747pub enum RunExit {
748    /// Stream was stopped via `StreamControl::stop()`.
749    Stopped,
750    /// Producer returned `None` (graceful completion).
751    ProducerEnded,
752    /// Device disconnected. No auto-reconnect; new streams start disarmed.
753    Disconnected,
754}
755
756/// Information about a discovered DAC before connection.
757#[derive(Clone, Debug)]
758#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
759pub struct DacInfo {
760    /// Stable, unique identifier used for (re)selecting DACs.
761    pub id: String,
762    /// Human-readable name for the DAC.
763    pub name: String,
764    /// The type of DAC hardware.
765    pub kind: DacType,
766    /// DAC capabilities.
767    pub caps: DacCapabilities,
768}
769
770impl DacInfo {
771    /// Create a new DAC info.
772    pub fn new(
773        id: impl Into<String>,
774        name: impl Into<String>,
775        kind: DacType,
776        caps: DacCapabilities,
777    ) -> Self {
778        Self {
779            id: id.into(),
780            name: name.into(),
781            kind,
782            caps,
783        }
784    }
785}
786
787#[cfg(test)]
788mod tests {
789    use super::*;
790
791    // ==========================================================================
792    // LaserPoint Tests
793    // ==========================================================================
794
795    #[test]
796    fn test_laser_point_blanked_sets_all_colors_to_zero() {
797        // blanked() should set all color channels to 0 while preserving position
798        let point = LaserPoint::blanked(0.25, 0.75);
799        assert_eq!(point.x, 0.25);
800        assert_eq!(point.y, 0.75);
801        assert_eq!(point.r, 0);
802        assert_eq!(point.g, 0);
803        assert_eq!(point.b, 0);
804        assert_eq!(point.intensity, 0);
805    }
806
807    // ==========================================================================
808    // DacType Tests
809    // ==========================================================================
810
811    #[test]
812    fn test_dac_type_all_returns_all_builtin_types() {
813        let all_types = DacType::all();
814        assert_eq!(all_types.len(), 6);
815        assert!(all_types.contains(&DacType::Helios));
816        assert!(all_types.contains(&DacType::EtherDream));
817        assert!(all_types.contains(&DacType::Idn));
818        assert!(all_types.contains(&DacType::LasercubeWifi));
819        assert!(all_types.contains(&DacType::LasercubeUsb));
820        assert!(all_types.contains(&DacType::Avb));
821    }
822
823    #[test]
824    fn test_dac_type_display_uses_display_name() {
825        // Display trait should delegate to display_name
826        assert_eq!(
827            format!("{}", DacType::Helios),
828            DacType::Helios.display_name()
829        );
830        assert_eq!(
831            format!("{}", DacType::EtherDream),
832            DacType::EtherDream.display_name()
833        );
834    }
835
836    #[test]
837    fn test_dac_type_can_be_used_in_hashset() {
838        use std::collections::HashSet;
839
840        let mut set = HashSet::new();
841        set.insert(DacType::Helios);
842        set.insert(DacType::Helios); // Duplicate should not increase count
843
844        assert_eq!(set.len(), 1);
845    }
846
847    // ==========================================================================
848    // EnabledDacTypes Tests
849    // ==========================================================================
850
851    #[test]
852    fn test_enabled_dac_types_all_enables_everything() {
853        let enabled = EnabledDacTypes::all();
854        for dac_type in DacType::all() {
855            assert!(
856                enabled.is_enabled(dac_type.clone()),
857                "{:?} should be enabled",
858                dac_type
859            );
860        }
861        assert!(!enabled.is_empty());
862    }
863
864    #[test]
865    fn test_enabled_dac_types_none_disables_everything() {
866        let enabled = EnabledDacTypes::none();
867        for dac_type in DacType::all() {
868            assert!(
869                !enabled.is_enabled(dac_type.clone()),
870                "{:?} should be disabled",
871                dac_type
872            );
873        }
874        assert!(enabled.is_empty());
875    }
876
877    #[test]
878    fn test_enabled_dac_types_enable_disable_toggles_correctly() {
879        let mut enabled = EnabledDacTypes::none();
880
881        // Enable one
882        enabled.enable(DacType::Helios);
883        assert!(enabled.is_enabled(DacType::Helios));
884        assert!(!enabled.is_enabled(DacType::EtherDream));
885
886        // Enable another
887        enabled.enable(DacType::EtherDream);
888        assert!(enabled.is_enabled(DacType::Helios));
889        assert!(enabled.is_enabled(DacType::EtherDream));
890
891        // Disable first
892        enabled.disable(DacType::Helios);
893        assert!(!enabled.is_enabled(DacType::Helios));
894        assert!(enabled.is_enabled(DacType::EtherDream));
895    }
896
897    #[test]
898    fn test_enabled_dac_types_iter_only_returns_enabled() {
899        let mut enabled = EnabledDacTypes::none();
900        enabled.enable(DacType::Helios);
901        enabled.enable(DacType::Idn);
902
903        let types: Vec<DacType> = enabled.iter().collect();
904        assert_eq!(types.len(), 2);
905        assert!(types.contains(&DacType::Helios));
906        assert!(types.contains(&DacType::Idn));
907        assert!(!types.contains(&DacType::EtherDream));
908    }
909
910    #[test]
911    fn test_enabled_dac_types_default_enables_all() {
912        let enabled = EnabledDacTypes::default();
913        // Default should be same as all()
914        for dac_type in DacType::all() {
915            assert!(enabled.is_enabled(dac_type.clone()));
916        }
917    }
918
919    #[test]
920    fn test_enabled_dac_types_idempotent_operations() {
921        let mut enabled = EnabledDacTypes::none();
922
923        // Enabling twice should have same effect as once
924        enabled.enable(DacType::Helios);
925        enabled.enable(DacType::Helios);
926        assert!(enabled.is_enabled(DacType::Helios));
927
928        // Disabling twice should have same effect as once
929        enabled.disable(DacType::Helios);
930        enabled.disable(DacType::Helios);
931        assert!(!enabled.is_enabled(DacType::Helios));
932    }
933
934    #[test]
935    fn test_enabled_dac_types_chaining() {
936        let mut enabled = EnabledDacTypes::none();
937        enabled
938            .enable(DacType::Helios)
939            .enable(DacType::EtherDream)
940            .disable(DacType::Helios);
941
942        assert!(!enabled.is_enabled(DacType::Helios));
943        assert!(enabled.is_enabled(DacType::EtherDream));
944    }
945
946    // ==========================================================================
947    // DacConnectionState Tests
948    // ==========================================================================
949
950    #[test]
951    fn test_dac_connection_state_equality() {
952        let s1 = DacConnectionState::Connected {
953            name: "DAC1".to_string(),
954        };
955        let s2 = DacConnectionState::Connected {
956            name: "DAC1".to_string(),
957        };
958        let s3 = DacConnectionState::Connected {
959            name: "DAC2".to_string(),
960        };
961        let s4 = DacConnectionState::Lost {
962            name: "DAC1".to_string(),
963            error: None,
964        };
965
966        assert_eq!(s1, s2);
967        assert_ne!(s1, s3); // Different name
968        assert_ne!(s1, s4); // Different variant
969    }
970
971    // ==========================================================================
972    // StreamConfig Serde Tests
973    // ==========================================================================
974
975    #[cfg(feature = "serde")]
976    #[test]
977    fn test_stream_config_serde_roundtrip() {
978        use std::time::Duration;
979
980        let config = StreamConfig {
981            pps: 45000,
982            target_buffer: Duration::from_millis(50),
983            min_buffer: Duration::from_millis(12),
984            underrun: UnderrunPolicy::Park { x: 0.5, y: -0.3 },
985            drain_timeout: Duration::from_secs(2),
986            color_delay: Duration::from_micros(150),
987            startup_blank: Duration::from_micros(800),
988        };
989
990        // Round-trip through JSON
991        let json = serde_json::to_string(&config).expect("serialize to JSON");
992        let restored: StreamConfig = serde_json::from_str(&json).expect("deserialize from JSON");
993
994        assert_eq!(restored.pps, config.pps);
995        assert_eq!(restored.target_buffer, config.target_buffer);
996        assert_eq!(restored.min_buffer, config.min_buffer);
997        assert_eq!(restored.drain_timeout, config.drain_timeout);
998        assert_eq!(restored.color_delay, config.color_delay);
999        assert_eq!(restored.startup_blank, config.startup_blank);
1000
1001        // Verify underrun policy
1002        match restored.underrun {
1003            UnderrunPolicy::Park { x, y } => {
1004                assert!((x - 0.5).abs() < f32::EPSILON);
1005                assert!((y - (-0.3)).abs() < f32::EPSILON);
1006            }
1007            _ => panic!("Expected Park policy"),
1008        }
1009    }
1010
1011    #[cfg(feature = "serde")]
1012    #[test]
1013    fn test_duration_millis_roundtrip_consistency() {
1014        use std::time::Duration;
1015
1016        // Test various duration values round-trip correctly
1017        let test_durations = [
1018            Duration::from_millis(0),
1019            Duration::from_millis(1),
1020            Duration::from_millis(10),
1021            Duration::from_millis(100),
1022            Duration::from_millis(1000),
1023            Duration::from_millis(u64::MAX / 1000), // Large but valid
1024        ];
1025
1026        for &duration in &test_durations {
1027            let config = StreamConfig {
1028                target_buffer: duration,
1029                ..StreamConfig::default()
1030            };
1031
1032            let json = serde_json::to_string(&config).expect("serialize");
1033            let restored: StreamConfig = serde_json::from_str(&json).expect("deserialize");
1034
1035            assert_eq!(
1036                restored.target_buffer, duration,
1037                "Duration {:?} did not round-trip correctly",
1038                duration
1039            );
1040        }
1041    }
1042}