Skip to main content

laser_dac/protocols/ether_dream/
protocol.rs

1//! Types and constants that precisely match the Ether Dream protocol specification.
2
3use byteorder::{ReadBytesExt, WriteBytesExt, LE};
4use std::io;
5
6use crate::point::LaserPoint;
7
8pub use self::command::Command;
9
10/// Communication with the DAC happens over TCP on port 7765.
11pub const COMMUNICATION_PORT: u16 = 7765;
12
13/// The DAC sends UDP broadcast messages on port 7654.
14pub const BROADCAST_PORT: u16 = 7654;
15
16/// A trait for writing any of the Ether Dream protocol types to little-endian bytes.
17pub trait WriteBytes {
18    fn write_bytes<P: WriteToBytes>(&mut self, protocol: P) -> io::Result<()>;
19}
20
21/// A trait for reading any of the Ether Dream protocol types from little-endian bytes.
22pub trait ReadBytes {
23    fn read_bytes<P: ReadFromBytes>(&mut self) -> io::Result<P>;
24}
25
26/// Protocol types that may be written to little endian bytes.
27pub trait WriteToBytes {
28    fn write_to_bytes<W: WriteBytesExt>(&self, writer: W) -> io::Result<()>;
29}
30
31/// Protocol types that may be read from little endian bytes.
32pub trait ReadFromBytes: Sized {
33    fn read_from_bytes<R: ReadBytesExt>(reader: R) -> io::Result<Self>;
34}
35
36/// Types that have a constant size when written to or read from bytes.
37pub trait SizeBytes {
38    const SIZE_BYTES: usize;
39}
40
41/// Periodically, and as part of ACK packets, the DAC sends its current playback status.
42#[repr(C)]
43#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
44pub struct DacStatus {
45    pub protocol: u8,
46    pub light_engine_state: u8,
47    pub playback_state: u8,
48    pub source: u8,
49    pub light_engine_flags: u16,
50    pub playback_flags: u16,
51    pub source_flags: u16,
52    pub buffer_fullness: u16,
53    pub point_rate: u32,
54    pub point_count: u32,
55}
56
57/// Each DAC broadcasts a status/ID datagram over UDP once per second.
58#[repr(C)]
59#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
60pub struct DacBroadcast {
61    pub mac_address: [u8; 6],
62    pub hw_revision: u16,
63    pub sw_revision: u16,
64    pub buffer_capacity: u16,
65    pub max_point_rate: u32,
66    pub dac_status: DacStatus,
67}
68
69/// A point with position and color values.
70#[repr(C)]
71#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
72pub struct DacPoint {
73    pub control: u16,
74    pub x: i16,
75    pub y: i16,
76    pub r: u16,
77    pub g: u16,
78    pub b: u16,
79    pub i: u16,
80    pub u1: u16,
81    pub u2: u16,
82}
83
84/// A response from a DAC.
85#[repr(C)]
86#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
87pub struct DacResponse {
88    pub response: u8,
89    pub command: u8,
90    pub dac_status: DacStatus,
91}
92
93impl DacStatus {
94    pub const LIGHT_ENGINE_READY: u8 = 0;
95    pub const LIGHT_ENGINE_WARMUP: u8 = 1;
96    pub const LIGHT_ENGINE_COOLDOWN: u8 = 2;
97    pub const LIGHT_ENGINE_EMERGENCY_STOP: u8 = 3;
98
99    pub const PLAYBACK_IDLE: u8 = 0;
100    pub const PLAYBACK_PREPARED: u8 = 1;
101    pub const PLAYBACK_PLAYING: u8 = 2;
102
103    pub const SOURCE_NETWORK_STREAMING: u8 = 0;
104    pub const SOURCE_ILDA_PLAYBACK_SD: u8 = 1;
105    pub const SOURCE_INTERNAL_ABSTRACT_GENERATOR: u8 = 2;
106}
107
108impl DacResponse {
109    pub const ACK: u8 = 0x61;
110    pub const NAK_FULL: u8 = 0x46;
111    pub const NAK_INVALID: u8 = 0x49;
112    pub const NAK_STOP_CONDITION: u8 = 0x21;
113}
114
115impl WriteToBytes for DacStatus {
116    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
117        writer.write_u8(self.protocol)?;
118        writer.write_u8(self.light_engine_state)?;
119        writer.write_u8(self.playback_state)?;
120        writer.write_u8(self.source)?;
121        writer.write_u16::<LE>(self.light_engine_flags)?;
122        writer.write_u16::<LE>(self.playback_flags)?;
123        writer.write_u16::<LE>(self.source_flags)?;
124        writer.write_u16::<LE>(self.buffer_fullness)?;
125        writer.write_u32::<LE>(self.point_rate)?;
126        writer.write_u32::<LE>(self.point_count)?;
127        Ok(())
128    }
129}
130
131impl WriteToBytes for DacBroadcast {
132    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
133        for &byte in &self.mac_address {
134            writer.write_u8(byte)?;
135        }
136        writer.write_u16::<LE>(self.hw_revision)?;
137        writer.write_u16::<LE>(self.sw_revision)?;
138        writer.write_u16::<LE>(self.buffer_capacity)?;
139        writer.write_u32::<LE>(self.max_point_rate)?;
140        writer.write_bytes(self.dac_status)?;
141        Ok(())
142    }
143}
144
145impl WriteToBytes for DacPoint {
146    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
147        writer.write_u16::<LE>(self.control)?;
148        writer.write_i16::<LE>(self.x)?;
149        writer.write_i16::<LE>(self.y)?;
150        writer.write_u16::<LE>(self.r)?;
151        writer.write_u16::<LE>(self.g)?;
152        writer.write_u16::<LE>(self.b)?;
153        writer.write_u16::<LE>(self.i)?;
154        writer.write_u16::<LE>(self.u1)?;
155        writer.write_u16::<LE>(self.u2)?;
156        Ok(())
157    }
158}
159
160impl WriteToBytes for DacResponse {
161    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
162        writer.write_u8(self.response)?;
163        writer.write_u8(self.command)?;
164        writer.write_bytes(self.dac_status)?;
165        Ok(())
166    }
167}
168
169impl ReadFromBytes for DacStatus {
170    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
171        Ok(DacStatus {
172            protocol: reader.read_u8()?,
173            light_engine_state: reader.read_u8()?,
174            playback_state: reader.read_u8()?,
175            source: reader.read_u8()?,
176            light_engine_flags: reader.read_u16::<LE>()?,
177            playback_flags: reader.read_u16::<LE>()?,
178            source_flags: reader.read_u16::<LE>()?,
179            buffer_fullness: reader.read_u16::<LE>()?,
180            point_rate: reader.read_u32::<LE>()?,
181            point_count: reader.read_u32::<LE>()?,
182        })
183    }
184}
185
186impl ReadFromBytes for DacBroadcast {
187    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
188        let mac_address = [
189            reader.read_u8()?,
190            reader.read_u8()?,
191            reader.read_u8()?,
192            reader.read_u8()?,
193            reader.read_u8()?,
194            reader.read_u8()?,
195        ];
196        Ok(DacBroadcast {
197            mac_address,
198            hw_revision: reader.read_u16::<LE>()?,
199            sw_revision: reader.read_u16::<LE>()?,
200            buffer_capacity: reader.read_u16::<LE>()?,
201            max_point_rate: reader.read_u32::<LE>()?,
202            dac_status: reader.read_bytes::<DacStatus>()?,
203        })
204    }
205}
206
207impl ReadFromBytes for DacPoint {
208    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
209        Ok(DacPoint {
210            control: reader.read_u16::<LE>()?,
211            x: reader.read_i16::<LE>()?,
212            y: reader.read_i16::<LE>()?,
213            r: reader.read_u16::<LE>()?,
214            g: reader.read_u16::<LE>()?,
215            b: reader.read_u16::<LE>()?,
216            i: reader.read_u16::<LE>()?,
217            u1: reader.read_u16::<LE>()?,
218            u2: reader.read_u16::<LE>()?,
219        })
220    }
221}
222
223impl ReadFromBytes for DacResponse {
224    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
225        Ok(DacResponse {
226            response: reader.read_u8()?,
227            command: reader.read_u8()?,
228            dac_status: reader.read_bytes::<DacStatus>()?,
229        })
230    }
231}
232
233impl SizeBytes for DacStatus {
234    const SIZE_BYTES: usize = 20;
235}
236
237impl SizeBytes for DacBroadcast {
238    const SIZE_BYTES: usize = DacStatus::SIZE_BYTES + 16;
239}
240
241impl SizeBytes for DacPoint {
242    const SIZE_BYTES: usize = 18;
243}
244
245impl From<&LaserPoint> for DacPoint {
246    /// Convert a LaserPoint to an Ether Dream DacPoint.
247    ///
248    /// LaserPoint uses f32 coordinates (-1.0 to 1.0) and u16 colors (0-65535).
249    /// Ether Dream uses i16 signed coordinates and u16 colors (direct mapping).
250    /// Coordinates are inverted to match hardware orientation.
251    fn from(p: &LaserPoint) -> Self {
252        DacPoint {
253            control: 0,
254            x: LaserPoint::coord_to_i16_inverted(p.x),
255            y: LaserPoint::coord_to_i16_inverted(p.y),
256            r: p.r,
257            g: p.g,
258            b: p.b,
259            i: p.intensity,
260            u1: 0,
261            u2: 0,
262        }
263    }
264}
265
266impl SizeBytes for DacResponse {
267    const SIZE_BYTES: usize = DacStatus::SIZE_BYTES + 2;
268}
269
270impl<P> WriteToBytes for &P
271where
272    P: WriteToBytes,
273{
274    fn write_to_bytes<W: WriteBytesExt>(&self, writer: W) -> io::Result<()> {
275        (*self).write_to_bytes(writer)
276    }
277}
278
279impl<W> WriteBytes for W
280where
281    W: WriteBytesExt,
282{
283    fn write_bytes<P: WriteToBytes>(&mut self, protocol: P) -> io::Result<()> {
284        protocol.write_to_bytes(self)
285    }
286}
287
288impl<R> ReadBytes for R
289where
290    R: ReadBytesExt,
291{
292    fn read_bytes<P: ReadFromBytes>(&mut self) -> io::Result<P> {
293        P::read_from_bytes(self)
294    }
295}
296
297/// Commands that can be sent to the DAC.
298pub mod command {
299    use super::{DacPoint, ReadBytes, ReadFromBytes, SizeBytes, WriteBytes, WriteToBytes};
300    use byteorder::{ReadBytesExt, WriteBytesExt, LE};
301    use std::borrow::Cow;
302    use std::io;
303
304    /// Types that may be submitted as commands to the DAC.
305    pub trait Command {
306        const START_BYTE: u8;
307        fn start_byte(&self) -> u8 {
308            Self::START_BYTE
309        }
310    }
311
312    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
313    pub struct PrepareStream;
314
315    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
316    pub struct Begin {
317        pub low_water_mark: u16,
318        pub point_rate: u32,
319    }
320
321    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
322    pub struct Update {
323        pub low_water_mark: u16,
324        pub point_rate: u32,
325    }
326
327    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
328    pub struct PointRate(pub u32);
329
330    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
331    pub struct Data<'a> {
332        pub points: Cow<'a, [DacPoint]>,
333    }
334
335    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
336    pub struct Stop;
337
338    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
339    pub struct EmergencyStop;
340
341    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
342    pub struct EmergencyStopAlt;
343
344    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
345    pub struct ClearEmergencyStop;
346
347    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
348    pub struct Ping;
349
350    impl Begin {
351        pub fn read_fields<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
352            Ok(Begin {
353                low_water_mark: reader.read_u16::<LE>()?,
354                point_rate: reader.read_u32::<LE>()?,
355            })
356        }
357    }
358
359    impl Update {
360        pub fn read_fields<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
361            Ok(Update {
362                low_water_mark: reader.read_u16::<LE>()?,
363                point_rate: reader.read_u32::<LE>()?,
364            })
365        }
366    }
367
368    impl PointRate {
369        pub fn read_fields<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
370            Ok(PointRate(reader.read_u32::<LE>()?))
371        }
372    }
373
374    impl<'a> Data<'a> {
375        pub fn read_n_points<R: ReadBytesExt>(mut reader: R) -> io::Result<u16> {
376            reader.read_u16::<LE>()
377        }
378
379        pub fn read_points<R: ReadBytesExt>(
380            mut reader: R,
381            n_points: u16,
382            points: &mut Vec<DacPoint>,
383        ) -> io::Result<()> {
384            for _ in 0..n_points {
385                points.push(reader.read_bytes::<DacPoint>()?);
386            }
387            Ok(())
388        }
389    }
390
391    impl Data<'static> {
392        pub fn read_fields<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
393            let n_points = Self::read_n_points(&mut reader)?;
394            let mut data = Vec::with_capacity(n_points as _);
395            Self::read_points(reader, n_points, &mut data)?;
396            Ok(Data {
397                points: Cow::Owned(data),
398            })
399        }
400    }
401
402    impl<C> Command for &C
403    where
404        C: Command,
405    {
406        const START_BYTE: u8 = C::START_BYTE;
407    }
408
409    impl Command for PrepareStream {
410        const START_BYTE: u8 = 0x70;
411    }
412    impl Command for Begin {
413        const START_BYTE: u8 = 0x62;
414    }
415    impl Command for Update {
416        const START_BYTE: u8 = 0x75;
417    }
418    impl Command for PointRate {
419        const START_BYTE: u8 = 0x74;
420    }
421    impl<'a> Command for Data<'a> {
422        const START_BYTE: u8 = 0x64;
423    }
424    impl Command for Stop {
425        const START_BYTE: u8 = 0x73;
426    }
427    impl Command for EmergencyStop {
428        const START_BYTE: u8 = 0x00;
429    }
430    impl Command for EmergencyStopAlt {
431        const START_BYTE: u8 = 0xff;
432    }
433    impl Command for ClearEmergencyStop {
434        const START_BYTE: u8 = 0x63;
435    }
436    impl Command for Ping {
437        const START_BYTE: u8 = 0x3f;
438    }
439
440    impl SizeBytes for PrepareStream {
441        const SIZE_BYTES: usize = 1;
442    }
443    impl SizeBytes for Begin {
444        const SIZE_BYTES: usize = 7;
445    }
446    impl SizeBytes for Update {
447        const SIZE_BYTES: usize = 7;
448    }
449    impl SizeBytes for PointRate {
450        const SIZE_BYTES: usize = 5;
451    }
452    impl SizeBytes for Stop {
453        const SIZE_BYTES: usize = 1;
454    }
455    impl SizeBytes for EmergencyStop {
456        const SIZE_BYTES: usize = 1;
457    }
458    impl SizeBytes for ClearEmergencyStop {
459        const SIZE_BYTES: usize = 1;
460    }
461    impl SizeBytes for Ping {
462        const SIZE_BYTES: usize = 1;
463    }
464
465    macro_rules! impl_unit_command_bytes {
466        ($($cmd:ident),* $(,)?) => {
467            $(
468                impl WriteToBytes for $cmd {
469                    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
470                        writer.write_u8(Self::START_BYTE)
471                    }
472                }
473
474                impl ReadFromBytes for $cmd {
475                    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
476                        if reader.read_u8()? != Self::START_BYTE {
477                            return Err(io::Error::new(
478                                io::ErrorKind::InvalidData,
479                                "invalid command",
480                            ));
481                        }
482                        Ok($cmd)
483                    }
484                }
485            )*
486        };
487    }
488
489    impl_unit_command_bytes!(PrepareStream, Stop, ClearEmergencyStop, Ping);
490
491    impl WriteToBytes for Begin {
492        fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
493            writer.write_u8(Self::START_BYTE)?;
494            writer.write_u16::<LE>(self.low_water_mark)?;
495            writer.write_u32::<LE>(self.point_rate)?;
496            Ok(())
497        }
498    }
499
500    impl WriteToBytes for Update {
501        fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
502            writer.write_u8(Self::START_BYTE)?;
503            writer.write_u16::<LE>(self.low_water_mark)?;
504            writer.write_u32::<LE>(self.point_rate)?;
505            Ok(())
506        }
507    }
508
509    impl WriteToBytes for PointRate {
510        fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
511            writer.write_u8(Self::START_BYTE)?;
512            writer.write_u32::<LE>(self.0)?;
513            Ok(())
514        }
515    }
516
517    impl<'a> WriteToBytes for Data<'a> {
518        fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
519            if self.points.len() > u16::MAX as usize {
520                return Err(io::Error::new(
521                    io::ErrorKind::InvalidData,
522                    "too many points",
523                ));
524            }
525            writer.write_u8(Self::START_BYTE)?;
526            writer.write_u16::<LE>(self.points.len() as u16)?;
527            for point in self.points.iter() {
528                writer.write_bytes(point)?;
529            }
530            Ok(())
531        }
532    }
533
534    impl WriteToBytes for EmergencyStop {
535        fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
536            writer.write_u8(Self::START_BYTE)
537        }
538    }
539
540    impl WriteToBytes for EmergencyStopAlt {
541        fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
542            writer.write_u8(Self::START_BYTE)
543        }
544    }
545
546    impl ReadFromBytes for Begin {
547        fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
548            if reader.read_u8()? != Self::START_BYTE {
549                return Err(io::Error::new(
550                    io::ErrorKind::InvalidData,
551                    "invalid command",
552                ));
553            }
554            Self::read_fields(reader)
555        }
556    }
557
558    impl ReadFromBytes for Update {
559        fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
560            if reader.read_u8()? != Self::START_BYTE {
561                return Err(io::Error::new(
562                    io::ErrorKind::InvalidData,
563                    "invalid command",
564                ));
565            }
566            Self::read_fields(reader)
567        }
568    }
569
570    impl ReadFromBytes for PointRate {
571        fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
572            if reader.read_u8()? != Self::START_BYTE {
573                return Err(io::Error::new(
574                    io::ErrorKind::InvalidData,
575                    "invalid command",
576                ));
577            }
578            Self::read_fields(reader)
579        }
580    }
581
582    impl ReadFromBytes for Data<'static> {
583        fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
584            if reader.read_u8()? != Self::START_BYTE {
585                return Err(io::Error::new(
586                    io::ErrorKind::InvalidData,
587                    "invalid command",
588                ));
589            }
590            Self::read_fields(reader)
591        }
592    }
593
594    impl ReadFromBytes for EmergencyStop {
595        fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
596            let command = reader.read_u8()?;
597            if command != Self::START_BYTE && command != EmergencyStopAlt::START_BYTE {
598                return Err(io::Error::new(
599                    io::ErrorKind::InvalidData,
600                    "invalid command",
601                ));
602            }
603            Ok(EmergencyStop)
604        }
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611    use crate::point::LaserPoint;
612
613    // ==========================================================================
614    // DacPoint Conversion Tests
615    // These test the From<&LaserPoint> implementation which handles:
616    // - 16-bit signed coordinate conversion (f32 -1..1 to i16 -32767..32767)
617    // - Direct u16 color pass-through (no scaling needed)
618    // - Out-of-range clamping
619    // ==========================================================================
620
621    #[test]
622    fn test_ether_dream_conversion_center() {
623        // Center point (0, 0) should map to (0, 0)
624        let laser_point = LaserPoint::new(0.0, 0.0, 128 * 257, 64 * 257, 32 * 257, 200 * 257);
625        let dac_point: DacPoint = (&laser_point).into();
626
627        assert_eq!(dac_point.x, 0);
628        assert_eq!(dac_point.y, 0);
629        // Colors: direct u16 pass-through
630        assert_eq!(dac_point.r, 128 * 257);
631        assert_eq!(dac_point.g, 64 * 257);
632        assert_eq!(dac_point.b, 32 * 257);
633        assert_eq!(dac_point.i, 200 * 257);
634    }
635
636    #[test]
637    fn test_ether_dream_conversion_boundaries() {
638        // Min point (-1, -1) should map to (32767, 32767) due to axis inversion
639        let min = LaserPoint::new(-1.0, -1.0, 0, 0, 0, 0);
640        let min_dac: DacPoint = (&min).into();
641        assert_eq!(min_dac.x, 32767);
642        assert_eq!(min_dac.y, 32767);
643
644        // Max point (1, 1) should map to (-32767, -32767) due to axis inversion
645        let max = LaserPoint::new(1.0, 1.0, 65535, 65535, 65535, 65535);
646        let max_dac: DacPoint = (&max).into();
647        assert_eq!(max_dac.x, -32767);
648        assert_eq!(max_dac.y, -32767);
649    }
650
651    #[test]
652    fn test_ether_dream_conversion_clamps_out_of_range() {
653        // Out of range values should clamp, then invert
654        let laser_point = LaserPoint::new(2.0, -3.0, 65535, 65535, 65535, 65535);
655        let dac_point: DacPoint = (&laser_point).into();
656
657        assert_eq!(dac_point.x, -32767);
658        assert_eq!(dac_point.y, 32767);
659    }
660
661    #[test]
662    fn test_ether_dream_color_direct_passthrough() {
663        // Colors should pass through directly without scaling
664        let laser_point = LaserPoint::new(0.0, 0.0, 0, 32639, 65535, 257);
665        let dac_point: DacPoint = (&laser_point).into();
666
667        assert_eq!(dac_point.r, 0);
668        assert_eq!(dac_point.g, 32639);
669        assert_eq!(dac_point.b, 65535);
670        assert_eq!(dac_point.i, 257);
671    }
672
673    #[test]
674    fn test_ether_dream_coordinate_symmetry() {
675        // Verify that x and -x produce symmetric results around 0
676        let p1 = LaserPoint::new(0.5, 0.0, 0, 0, 0, 0);
677        let p2 = LaserPoint::new(-0.5, 0.0, 0, 0, 0, 0);
678        let d1: DacPoint = (&p1).into();
679        let d2: DacPoint = (&p2).into();
680
681        assert_eq!(d1.x, -d2.x);
682    }
683
684    #[test]
685    fn test_ether_dream_conversion_infinity_clamps() {
686        let laser_point = LaserPoint::new(f32::INFINITY, f32::NEG_INFINITY, 0, 0, 0, 0);
687        let dac_point: DacPoint = (&laser_point).into();
688
689        assert_eq!(dac_point.x, -32767);
690        assert_eq!(dac_point.y, 32767);
691    }
692}