Skip to main content

laser_dac/protocols/lasercube_usb/
protocol.rs

1//! Low-level USB protocol types and constants for LaserCube/LaserDock DAC communication.
2
3use crate::point::LaserPoint;
4use std::time::Duration;
5
6/// Timeout for USB control transfers (shared across modules).
7pub const CONTROL_TIMEOUT: Duration = Duration::from_millis(1000);
8
9/// USB Vendor ID for LaserDock/LaserCube USB devices.
10pub const LASERDOCK_VID: u16 = 0x1fc9;
11
12/// USB Product ID for LaserDock/LaserCube USB devices.
13pub const LASERDOCK_PID: u16 = 0x04d8;
14
15/// Control interface number.
16pub const CONTROL_INTERFACE: u8 = 0;
17
18/// Data interface number.
19pub const DATA_INTERFACE: u8 = 1;
20
21/// Alternate setting for data interface.
22pub const DATA_ALT_SETTING: u8 = 1;
23
24/// Control endpoint (bulk out).
25pub const ENDPOINT_CONTROL_OUT: u8 = 0x01;
26
27/// Control endpoint (bulk in).
28pub const ENDPOINT_CONTROL_IN: u8 = 0x81;
29
30/// Data endpoint (bulk out).
31pub const ENDPOINT_DATA_OUT: u8 = 0x03;
32
33/// Control packet size.
34pub const CONTROL_PACKET_SIZE: usize = 64;
35
36// Command bytes
37/// Enable or disable laser output.
38pub const CMD_SET_OUTPUT: u8 = 0x80;
39/// Get output enable status.
40pub const CMD_GET_OUTPUT: u8 = 0x81;
41/// Set DAC sample rate.
42pub const CMD_SET_DAC_RATE: u8 = 0x82;
43/// Get DAC sample rate.
44pub const CMD_GET_DAC_RATE: u8 = 0x83;
45/// Get maximum DAC sample rate.
46pub const CMD_GET_MAX_DAC_RATE: u8 = 0x84;
47/// Get sample element count.
48pub const CMD_GET_SAMPLE_ELEMENT_COUNT: u8 = 0x85;
49/// Get ISO packet sample count.
50pub const CMD_GET_ISO_PACKET_SAMPLE_COUNT: u8 = 0x86;
51/// Get minimum DAC value.
52pub const CMD_GET_MIN_DAC_VALUE: u8 = 0x87;
53/// Get maximum DAC value.
54pub const CMD_GET_MAX_DAC_VALUE: u8 = 0x88;
55/// Get ring buffer sample count.
56pub const CMD_GET_RINGBUFFER_SAMPLE_COUNT: u8 = 0x89;
57/// Get ring buffer empty sample count.
58pub const CMD_GET_RINGBUFFER_EMPTY_SAMPLE_COUNT: u8 = 0x8A;
59/// Get firmware major version.
60pub const CMD_GET_VERSION_MAJOR: u8 = 0x8B;
61/// Get firmware minor version.
62pub const CMD_GET_VERSION_MINOR: u8 = 0x8C;
63/// Clear ring buffer.
64pub const CMD_CLEAR_RINGBUFFER: u8 = 0x8D;
65/// Get bulk packet sample count.
66pub const CMD_GET_BULK_PACKET_SAMPLE_COUNT: u8 = 0x8E;
67
68/// Runner mode command.
69pub const CMD_RUNNER_MODE: u8 = 0xC0;
70/// Runner mode sub-command: enable.
71pub const RUNNER_MODE_SUB_ENABLE: u8 = 0x01;
72/// Runner mode sub-command: run.
73pub const RUNNER_MODE_SUB_RUN: u8 = 0x09;
74
75/// Maximum coordinate value (12-bit).
76pub const MAX_COORDINATE_VALUE: u16 = 4095;
77
78/// Size of a single sample in bytes.
79pub const SAMPLE_SIZE_BYTES: usize = 8;
80
81/// A laser sample with position and color.
82///
83/// The LaserDock uses 12-bit coordinates (0-4095) where:
84/// - X: 0 = left edge, 2047 = center, 4095 = right edge
85/// - Y: 0 = bottom edge, 2047 = center, 4095 = top edge
86///
87/// Colors are 8-bit values (0-255).
88#[repr(C)]
89#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
90pub struct Sample {
91    /// Red (low byte) and Green (high byte) combined.
92    pub rg: u16,
93    /// Blue (low byte), high byte unused.
94    pub b: u16,
95    /// X coordinate (12-bit, 0-4095).
96    pub x: u16,
97    /// Y coordinate (12-bit, 0-4095).
98    pub y: u16,
99}
100
101impl Sample {
102    /// Create a new sample from individual components.
103    ///
104    /// # Arguments
105    /// * `x` - X coordinate (0-4095)
106    /// * `y` - Y coordinate (0-4095)
107    /// * `r` - Red intensity (0-255)
108    /// * `g` - Green intensity (0-255)
109    /// * `b` - Blue intensity (0-255)
110    pub fn new(x: u16, y: u16, r: u8, g: u8, b: u8) -> Self {
111        Self {
112            rg: (r as u16) | ((g as u16) << 8),
113            b: b as u16,
114            x: x.min(MAX_COORDINATE_VALUE),
115            y: y.min(MAX_COORDINATE_VALUE),
116        }
117    }
118
119    /// Create a blank sample (laser off) at the center position.
120    pub fn blank() -> Self {
121        Self::new(2048, 2048, 0, 0, 0)
122    }
123
124    /// Get the red component.
125    pub fn red(&self) -> u8 {
126        (self.rg & 0xFF) as u8
127    }
128
129    /// Get the green component.
130    pub fn green(&self) -> u8 {
131        ((self.rg >> 8) & 0xFF) as u8
132    }
133
134    /// Get the blue component.
135    pub fn blue(&self) -> u8 {
136        (self.b & 0xFF) as u8
137    }
138
139    /// Flip the X coordinate.
140    pub fn flip_x(&mut self) {
141        self.x = MAX_COORDINATE_VALUE - self.x;
142    }
143
144    /// Flip the Y coordinate.
145    pub fn flip_y(&mut self) {
146        self.y = MAX_COORDINATE_VALUE - self.y;
147    }
148
149    /// Convert the sample to raw bytes for USB transmission.
150    pub fn to_bytes(&self) -> [u8; SAMPLE_SIZE_BYTES] {
151        let mut bytes = [0u8; SAMPLE_SIZE_BYTES];
152        bytes[0..2].copy_from_slice(&self.rg.to_le_bytes());
153        bytes[2..4].copy_from_slice(&self.b.to_le_bytes());
154        bytes[4..6].copy_from_slice(&self.x.to_le_bytes());
155        bytes[6..8].copy_from_slice(&self.y.to_le_bytes());
156        bytes
157    }
158}
159
160impl From<&LaserPoint> for Sample {
161    /// Convert a LaserPoint to a LaserCube USB Sample.
162    ///
163    /// LaserPoint uses f32 coordinates (-1.0 to 1.0) and u16 colors (0-65535).
164    /// LaserCube USB uses 12-bit unsigned coordinates (0-4095) and u8 colors.
165    fn from(p: &LaserPoint) -> Self {
166        Sample::new(
167            LaserPoint::coord_to_u12(p.x),
168            LaserPoint::coord_to_u12(p.y),
169            LaserPoint::color_to_u8(p.r),
170            LaserPoint::color_to_u8(p.g),
171            LaserPoint::color_to_u8(p.b),
172        )
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_sample_new() {
182        let sample = Sample::new(2048, 2048, 255, 128, 64);
183        assert_eq!(sample.x, 2048);
184        assert_eq!(sample.y, 2048);
185        assert_eq!(sample.red(), 255);
186        assert_eq!(sample.green(), 128);
187        assert_eq!(sample.blue(), 64);
188    }
189
190    #[test]
191    fn test_sample_blank() {
192        let blank = Sample::blank();
193        assert_eq!(blank.x, 2048);
194        assert_eq!(blank.y, 2048);
195        assert_eq!(blank.red(), 0);
196        assert_eq!(blank.green(), 0);
197        assert_eq!(blank.blue(), 0);
198    }
199
200    #[test]
201    fn test_sample_clamps_coordinates() {
202        let sample = Sample::new(5000, 6000, 0, 0, 0);
203        assert_eq!(sample.x, MAX_COORDINATE_VALUE);
204        assert_eq!(sample.y, MAX_COORDINATE_VALUE);
205    }
206
207    #[test]
208    fn test_sample_flip() {
209        let mut sample = Sample::new(1000, 3000, 0, 0, 0);
210        sample.flip_x();
211        sample.flip_y();
212        assert_eq!(sample.x, 4095 - 1000);
213        assert_eq!(sample.y, 4095 - 3000);
214    }
215
216    #[test]
217    fn test_sample_size() {
218        assert_eq!(std::mem::size_of::<Sample>(), SAMPLE_SIZE_BYTES);
219    }
220}