lasercube_core/
cmds.rs

1//! Command definitions for LaserCube protocol.
2
3use crate::{LaserInfo, LaserInfoParseError, Point};
4use std::convert::TryFrom;
5use thiserror::Error;
6
7/// Command types supported by the LaserCube protocol.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[repr(u8)]
10pub enum CommandType {
11    /// Get detailed device information.
12    GetFullInfo = 0x77,
13    /// Enable/disable buffer size responses on data packets.
14    EnableBufferSizeResponseOnData = 0x78,
15    /// Enable/disable laser output.
16    SetOutput = 0x80,
17    /// Get the number of free samples in the device's ring buffer.
18    GetRingbufferEmptySampleCount = 0x8a,
19    /// Send point data to render.
20    SampleData = 0xa9,
21}
22
23/// Command structure for the LaserCube protocol.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Command {
26    /// Get detailed device information.
27    GetFullInfo,
28    /// Enable/disable buffer size responses on data packets.
29    EnableBufferSizeResponseOnData(bool),
30    /// Enable/disable laser output.
31    SetOutput(bool),
32    /// Get the number of free samples in the device's ring buffer.
33    GetRingbufferEmptySampleCount,
34    /// Send point data to render.
35    SampleData(SampleData),
36}
37
38/// Send point data to render.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct SampleData {
41    /// Message sequence number (0-255)
42    pub message_num: u8,
43    /// Frame sequence number (0-255)
44    pub frame_num: u8,
45    /// Point data to render
46    pub points: Vec<Point>,
47}
48
49/// Responses from LaserCube device
50#[derive(Debug, Clone, PartialEq)]
51pub enum Response {
52    /// Full device information
53    FullInfo(LaserInfo),
54    /// Buffer free space
55    BufferFree(u16),
56    /// Simple acknowledgment
57    Ack,
58}
59
60/// Error types that can occur when parsing command responses
61#[derive(Debug, Error)]
62pub enum ResponseParseError {
63    #[error("Empty response")]
64    EmptyResponse,
65    #[error("Unknown command type: {0}")]
66    UnknownCommandType(u8),
67    #[error("Response too short for {command_type:?} command: expected at least {expected} bytes, got {actual}")]
68    ResponseTooShort {
69        command_type: CommandType,
70        expected: usize,
71        actual: usize,
72    },
73    #[error("Failed to parse LaserInfo: {0}")]
74    LaserInfoError(#[from] LaserInfoParseError),
75}
76
77impl TryFrom<&[u8]> for Response {
78    type Error = ResponseParseError;
79
80    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
81        if bytes.is_empty() {
82            return Err(ResponseParseError::EmptyResponse);
83        }
84
85        // First byte is the command type
86        let cmd_type = match CommandType::try_from(bytes[0]) {
87            Ok(cmd) => cmd,
88            Err(_) => return Err(ResponseParseError::UnknownCommandType(bytes[0])),
89        };
90
91        match cmd_type {
92            CommandType::GetFullInfo => {
93                // Parse the LaserInfo using its TryFrom implementation
94                let laser_info = LaserInfo::try_from(bytes)?;
95                Ok(Response::FullInfo(laser_info))
96            }
97
98            CommandType::GetRingbufferEmptySampleCount => {
99                let minimum_len = 4;
100                if bytes.len() < minimum_len {
101                    return Err(ResponseParseError::ResponseTooShort {
102                        command_type: cmd_type,
103                        expected: minimum_len,
104                        actual: bytes.len(),
105                    });
106                }
107
108                let buffer_free = u16::from_le_bytes([bytes[2], bytes[3]]);
109                Ok(Response::BufferFree(buffer_free))
110            }
111
112            // Data packets can respond with buffer info when enabled
113            CommandType::SampleData => {
114                let minimum_len = 3;
115                if bytes.len() < minimum_len {
116                    return Err(ResponseParseError::ResponseTooShort {
117                        command_type: cmd_type,
118                        expected: minimum_len,
119                        actual: bytes.len(),
120                    });
121                }
122
123                // The response includes the free buffer space
124                let buffer_free = u16::from_le_bytes([bytes[1], bytes[2]]);
125                Ok(Response::BufferFree(buffer_free))
126            }
127
128            // Acknowledgment responses
129            CommandType::EnableBufferSizeResponseOnData | CommandType::SetOutput => {
130                Ok(Response::Ack)
131            }
132        }
133    }
134}
135
136impl TryFrom<u8> for CommandType {
137    type Error = ();
138    fn try_from(value: u8) -> Result<Self, Self::Error> {
139        match value {
140            0x77 => Ok(CommandType::GetFullInfo),
141            0x78 => Ok(CommandType::EnableBufferSizeResponseOnData),
142            0x80 => Ok(CommandType::SetOutput),
143            0x8a => Ok(CommandType::GetRingbufferEmptySampleCount),
144            0xa9 => Ok(CommandType::SampleData),
145            _ => Err(()),
146        }
147    }
148}
149
150impl Command {
151    /// Get the command type associated with this command.
152    pub fn command_type(&self) -> CommandType {
153        match self {
154            Command::GetFullInfo => CommandType::GetFullInfo,
155            Command::EnableBufferSizeResponseOnData(_) => {
156                CommandType::EnableBufferSizeResponseOnData
157            }
158            Command::SetOutput(_) => CommandType::SetOutput,
159            Command::GetRingbufferEmptySampleCount => CommandType::GetRingbufferEmptySampleCount,
160            Command::SampleData { .. } => CommandType::SampleData,
161        }
162    }
163
164    /// Estimate the size in bytes this command will take when serialized.
165    pub fn size(&self) -> usize {
166        match self {
167            Command::GetFullInfo => 1,
168            Command::EnableBufferSizeResponseOnData(_) => 2,
169            Command::SetOutput(_) => 2,
170            Command::GetRingbufferEmptySampleCount => 1,
171            Command::SampleData(SampleData { points, .. }) => {
172                // 1 byte command
173                // + 1 byte padding
174                // + 1 byte message num
175                // + 1 byte frame num
176                4 + (points.len() * 10) // Each point is 10 bytes
177            }
178        }
179    }
180
181    /// Write this command into the provided byte buffer.
182    ///
183    /// Returns the number of bytes written.
184    pub fn write_bytes(&self, buffer: &mut Vec<u8>) -> usize {
185        let start_len = buffer.len();
186
187        match self {
188            Command::GetFullInfo => {
189                buffer.push(CommandType::GetFullInfo as u8);
190            }
191
192            Command::EnableBufferSizeResponseOnData(enable) => {
193                buffer.push(CommandType::EnableBufferSizeResponseOnData as u8);
194                buffer.push(if *enable { 1 } else { 0 });
195            }
196
197            Command::SetOutput(enable) => {
198                buffer.push(CommandType::SetOutput as u8);
199                buffer.push(if *enable { 1 } else { 0 });
200            }
201
202            Command::GetRingbufferEmptySampleCount => {
203                buffer.push(CommandType::GetRingbufferEmptySampleCount as u8);
204            }
205
206            Command::SampleData(data) => {
207                // Header: command byte, 0x00, message_num, frame_num
208                buffer.push(CommandType::SampleData as u8);
209                buffer.push(0x00); // Always 0x00 according to protocol
210                buffer.push(data.message_num);
211                buffer.push(data.frame_num);
212
213                // Append each point's serialized bytes
214                for point in &data.points {
215                    let point_bytes: [u8; Point::SIZE] = (*point).into();
216                    buffer.extend_from_slice(&point_bytes);
217                }
218            }
219        }
220
221        buffer.len() - start_len
222    }
223
224    /// Convenience method to get command bytes as a new Vec<u8>
225    pub fn to_bytes(&self) -> Vec<u8> {
226        let mut buffer = Vec::with_capacity(self.size());
227        self.write_bytes(&mut buffer);
228        buffer
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_parse_buffer_free_response() {
238        // Sample response for GetRingbufferEmptySampleCount with 1000 free samples
239        let response = [0x8a, 0x00, 0xe8, 0x03]; // 0x03e8 = 1000 in little-endian
240
241        let parsed = Response::try_from(&response[..]).unwrap();
242
243        match parsed {
244            Response::BufferFree(free) => assert_eq!(free, 1000),
245            _ => panic!("Wrong response type parsed"),
246        }
247    }
248
249    #[test]
250    fn test_parse_ack_response() {
251        // Sample response for SetOutput
252        let response = [0x80];
253
254        let parsed = Response::try_from(&response[..]).unwrap();
255
256        match parsed {
257            Response::Ack => {}
258            _ => panic!("Wrong response type parsed"),
259        }
260    }
261
262    #[test]
263    fn test_parse_error_handling() {
264        // Empty response
265        let result = Response::try_from(&[][..]);
266        assert!(matches!(result, Err(ResponseParseError::EmptyResponse)));
267
268        // Unknown command type
269        let result = Response::try_from(&[0xFF][..]);
270        assert!(matches!(
271            result,
272            Err(ResponseParseError::UnknownCommandType(0xFF))
273        ));
274
275        // Response too short
276        let result = Response::try_from(&[0x8a, 0x00][..]);
277        assert!(matches!(
278            result,
279            Err(ResponseParseError::ResponseTooShort {
280                command_type: CommandType::GetRingbufferEmptySampleCount,
281                ..
282            })
283        ));
284    }
285}