Skip to main content

laser_dac/protocols/helios/
frame.rs

1//! Helios frame and point types.
2
3use bitflags::bitflags;
4
5use crate::point::LaserPoint;
6
7/// A frame to be sent to the Helios DAC.
8#[derive(Debug, Clone, PartialEq)]
9pub struct Frame {
10    /// Rate of output in points per second
11    pub pps: u32,
12    /// Frame flags (default is empty)
13    pub flags: WriteFrameFlags,
14    /// Points in this frame
15    pub points: Vec<Point>,
16}
17
18impl Frame {
19    /// Create a new frame with the given point rate and points.
20    ///
21    /// Defaults to `SINGLE_MODE` (play once, don't repeat), matching the
22    /// official Helios SDK's `HELIOS_FLAGS_DEFAULT`. This prevents the DAC
23    /// from repeating the last frame indefinitely if the host stops sending.
24    pub fn new(pps: u32, points: Vec<Point>) -> Self {
25        Frame {
26            pps,
27            points,
28            flags: WriteFrameFlags::SINGLE_MODE,
29        }
30    }
31
32    /// Create a new frame with specific flags.
33    pub fn new_with_flags(pps: u32, points: Vec<Point>, flags: WriteFrameFlags) -> Self {
34        Frame { pps, points, flags }
35    }
36}
37
38/// A single laser point.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct Point {
41    /// X/Y coordinate
42    pub coordinate: Coordinate,
43    /// RGB color
44    pub color: Color,
45    /// Intensity (0-255)
46    pub intensity: u8,
47}
48
49/// Coordinates (x, y) for Helios DAC.
50///
51/// 12 bit (from 0 to 0xFFF)
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct Coordinate {
54    pub x: u16,
55    pub y: u16,
56}
57
58impl From<(u16, u16)> for Coordinate {
59    fn from((x, y): (u16, u16)) -> Self {
60        Coordinate { x, y }
61    }
62}
63
64/// RGB color for a laser point.
65#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct Color {
67    /// Red channel (0-255)
68    pub r: u8,
69    /// Green channel (0-255)
70    pub g: u8,
71    /// Blue channel (0-255)
72    pub b: u8,
73}
74
75impl Color {
76    /// Create a new color.
77    pub fn new(r: u8, g: u8, b: u8) -> Self {
78        Color { r, g, b }
79    }
80}
81
82bitflags! {
83    /// Flags for WriteFrame operation.
84    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
85    pub struct WriteFrameFlags: u8 {
86        /// Bit 0 (LSB) = if 1, start output immediately, instead of waiting for current frame (if there is one) to finish playing
87        const START_IMMEDIATELY = 0b0000_0001;
88        /// Bit 1 = if 1, play frame only once, instead of repeating until another frame is written
89        const SINGLE_MODE = 0b0000_0010;
90        /// Bit 2 = if 1, don't let WriteFrame() block execution while waiting for the transfer to finish
91        const DONT_BLOCK = 0b0000_0100;
92    }
93}
94
95impl From<&LaserPoint> for Point {
96    /// Convert a [`LaserPoint`] to a Helios [`Point`].
97    ///
98    /// [`LaserPoint`] uses f32 coordinates (-1.0 to 1.0) and u16 colors (0-65535).
99    /// Helios uses u16 12-bit coordinates (0-4095) with inverted axes and u8 colors.
100    fn from(p: &LaserPoint) -> Self {
101        Point {
102            coordinate: Coordinate {
103                x: LaserPoint::coord_to_u12_inverted(p.x),
104                y: LaserPoint::coord_to_u12_inverted(p.y),
105            },
106            color: Color::new(
107                LaserPoint::color_to_u8(p.r),
108                LaserPoint::color_to_u8(p.g),
109                LaserPoint::color_to_u8(p.b),
110            ),
111            intensity: LaserPoint::color_to_u8(p.intensity),
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    // ==========================================================================
121    // LaserPoint to Helios Point Conversion Tests
122    // These test the From<&LaserPoint> implementation which handles:
123    // - Coordinate inversion (Helios has inverted axes)
124    // - 12-bit conversion (f32 -1..1 to u16 0..4095)
125    // - Out-of-range clamping
126    // ==========================================================================
127
128    #[test]
129    fn test_helios_conversion_center() {
130        // Center point (0, 0) should map to (2047, 2047) due to inversion
131        // Colors: u16 values that downscale to expected u8 values (128, 64, 32, 200)
132        let laser_point = LaserPoint::new(0.0, 0.0, 128 * 257, 64 * 257, 32 * 257, 200 * 257);
133        let helios_point: Point = (&laser_point).into();
134
135        // (1.0 - (0.0 + 1.0) / 2.0) * 4095 = (1.0 - 0.5) * 4095 = 2047.5 -> 2048
136        assert_eq!(helios_point.coordinate.x, 2048);
137        assert_eq!(helios_point.coordinate.y, 2048);
138        // Colors should downscale from u16 to u8 (>> 8)
139        assert_eq!(helios_point.color.r, 128);
140        assert_eq!(helios_point.color.g, 64);
141        assert_eq!(helios_point.color.b, 32);
142        assert_eq!(helios_point.intensity, 200);
143    }
144
145    #[test]
146    fn test_helios_conversion_boundaries() {
147        // Min point (-1, -1) should map to (4095, 4095) due to inversion
148        let min = LaserPoint::new(-1.0, -1.0, 0, 0, 0, 0);
149        let min_helios: Point = (&min).into();
150        assert_eq!(min_helios.coordinate.x, 4095);
151        assert_eq!(min_helios.coordinate.y, 4095);
152
153        // Max point (1, 1) should map to (0, 0) due to inversion
154        let max = LaserPoint::new(1.0, 1.0, 0, 0, 0, 0);
155        let max_helios: Point = (&max).into();
156        assert_eq!(max_helios.coordinate.x, 0);
157        assert_eq!(max_helios.coordinate.y, 0);
158    }
159
160    #[test]
161    fn test_helios_conversion_asymmetric() {
162        // Test that x and y convert independently with different values
163        let laser_point = LaserPoint::new(-0.5, 0.5, 0, 0, 0, 0);
164        let helios_point: Point = (&laser_point).into();
165
166        // x: (1.0 - (-0.5 + 1.0) / 2.0) * 4095 = (1.0 - 0.25) * 4095 = 3071.25 -> 3071
167        // y: (1.0 - (0.5 + 1.0) / 2.0) * 4095 = (1.0 - 0.75) * 4095 = 1023.75 -> 1024
168        assert_eq!(helios_point.coordinate.x, 3071);
169        assert_eq!(helios_point.coordinate.y, 1024);
170    }
171
172    #[test]
173    fn test_helios_conversion_clamps_out_of_range() {
174        // Out of range positive values should clamp to 0 (due to inversion)
175        let positive = LaserPoint::new(2.0, 3.0, 0, 0, 0, 0);
176        let positive_helios: Point = (&positive).into();
177        assert_eq!(positive_helios.coordinate.x, 0);
178        assert_eq!(positive_helios.coordinate.y, 0);
179
180        // Out of range negative values should clamp to 4095 (due to inversion)
181        let negative = LaserPoint::new(-2.0, -3.0, 0, 0, 0, 0);
182        let negative_helios: Point = (&negative).into();
183        assert_eq!(negative_helios.coordinate.x, 4095);
184        assert_eq!(negative_helios.coordinate.y, 4095);
185    }
186
187    #[test]
188    fn test_helios_inversion_symmetry() {
189        // Verify that x and -x produce symmetric results around center
190        // This validates the inversion formula is mathematically correct
191        let p1 = LaserPoint::new(0.5, 0.0, 0, 0, 0, 0);
192        let p2 = LaserPoint::new(-0.5, 0.0, 0, 0, 0, 0);
193        let h1: Point = (&p1).into();
194        let h2: Point = (&p2).into();
195
196        // Due to 12-bit resolution, h1.x + h2.x should equal ~4095
197        let sum = h1.coordinate.x as i32 + h2.coordinate.x as i32;
198        assert!((sum - 4095).abs() <= 1, "Sum was {}, expected ~4095", sum);
199    }
200
201    #[test]
202    fn test_helios_conversion_infinity_clamps() {
203        let laser_point = LaserPoint::new(f32::INFINITY, f32::NEG_INFINITY, 0, 0, 0, 0);
204        let helios_point: Point = (&laser_point).into();
205
206        // Infinity clamps like out-of-range values
207        assert_eq!(helios_point.coordinate.x, 0);
208        assert_eq!(helios_point.coordinate.y, 4095);
209    }
210
211    #[test]
212    fn test_helios_conversion_nan_does_not_panic() {
213        // NaN should produce some valid output without panicking
214        let laser_point = LaserPoint::new(
215            f32::NAN,
216            f32::NAN,
217            100 * 257,
218            100 * 257,
219            100 * 257,
220            100 * 257,
221        );
222        let helios_point: Point = (&laser_point).into();
223
224        // Just verify it's within valid range
225        assert!(helios_point.coordinate.x <= 4095);
226        assert!(helios_point.coordinate.y <= 4095);
227    }
228}