1pub mod buffer;
7pub mod cmds;
8pub mod point;
9
10pub use buffer::BufferState;
12pub use cmds::{Command, CommandType, SampleData};
13pub use point::Point;
14use std::{convert::TryFrom, ffi::CStr, net::Ipv4Addr};
15use thiserror::Error;
16
17pub mod port {
19 pub const ALIVE: u16 = 45456;
21 pub const CMD: u16 = 45457;
23 pub const DATA: u16 = 45458;
25}
26
27pub const MAX_POINTS_PER_MESSAGE: usize = 140;
29
30pub const DEFAULT_BROADCAST_ADDR: &str = "255.255.255.255";
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(u8)]
36pub enum ConnectionType {
37 Unknown = 0,
39 Usb = 1,
41 Ethernet = 2,
43 Wifi = 3,
45}
46
47#[derive(Debug, Error)]
49pub enum LaserInfoParseError {
50 #[error("Response too short: expected at least {expected} bytes, got {actual}")]
51 ResponseTooShort { expected: usize, actual: usize },
52 #[error("Missing null terminator in model name: {0}")]
53 MissingNullTerminator(#[from] std::ffi::FromBytesUntilNulError),
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct LaserInfoHeader {
59 pub fw_major: u8,
61 pub fw_minor: u8,
63 pub output_enabled: bool,
65 pub dac_rate: u32,
67 pub max_dac_rate: u32,
69 pub rx_buffer_free: u16,
71 pub rx_buffer_size: u16,
73 pub battery_percent: u8,
75 pub temperature: u8,
77 pub model_number: u8,
79 pub serial_number: [u8; 6],
81 pub ip_addr: Ipv4Addr,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct LaserInfo {
88 pub header: LaserInfoHeader,
90 pub model_name: String,
92}
93
94impl LaserInfoHeader {
95 pub const SIZE: usize = 38;
97
98 pub fn connection_type(&self) -> ConnectionType {
100 ConnectionType::from(self.serial_number[0])
101 }
102}
103
104impl LaserInfo {
105 pub const MIN_SIZE: usize = LaserInfoHeader::SIZE;
107 pub const MAX_SIZE: usize = 64;
109 pub const MAX_MODEL_NAME_SIZE: usize = Self::MAX_SIZE - Self::MIN_SIZE;
111
112 pub fn firmware_version(&self) -> String {
114 format!("{}.{}", self.header.fw_major, self.header.fw_minor)
115 }
116
117 pub fn serial_number_string(&self) -> String {
119 let mut result = String::with_capacity(17);
120 for (i, byte) in self.header.serial_number.iter().enumerate() {
121 if i > 0 {
122 result.push(':');
123 }
124 use std::fmt::Write;
125 write!(result, "{:02x}", byte).unwrap();
126 }
127 result
128 }
129
130 pub fn connection_type(&self) -> ConnectionType {
132 self.header.connection_type()
133 }
134}
135
136impl From<u8> for ConnectionType {
137 fn from(value: u8) -> Self {
138 match value {
139 1 => ConnectionType::Usb,
140 2 => ConnectionType::Ethernet,
141 3 => ConnectionType::Wifi,
142 _ => ConnectionType::Unknown,
143 }
144 }
145}
146
147impl From<[u8; 38]> for LaserInfoHeader {
148 fn from(bytes: [u8; 38]) -> Self {
149 #[rustfmt::skip]
151 let [
152 _,
153 _,
154 _,
155 fw_major,
156 fw_minor,
157 output_enabled,
158 _,
159 _,
160 _,
161 _,
162 _,
163 dr0, dr1, dr2, dr3,
164 mdr0, mdr1, mdr2, mdr3,
165 _,
166 rxbf0, rxbf1,
167 rxbs0, rxbs1,
168 battery_percent,
169 temperature,
170 sn0, sn1, sn2, sn3, sn4, sn5,
171 ip0, ip1, ip2, ip3,
172 _,
173 model_number,
174 ] = bytes;
175 Self {
176 fw_major,
177 fw_minor,
178 output_enabled: output_enabled != 0,
179 dac_rate: u32::from_le_bytes([dr0, dr1, dr2, dr3]),
180 max_dac_rate: u32::from_le_bytes([mdr0, mdr1, mdr2, mdr3]),
181 rx_buffer_free: u16::from_le_bytes([rxbf0, rxbf1]),
182 rx_buffer_size: u16::from_le_bytes([rxbs0, rxbs1]),
183 battery_percent,
184 temperature,
185 serial_number: [sn0, sn1, sn2, sn3, sn4, sn5],
186 ip_addr: [ip0, ip1, ip2, ip3].into(),
187 model_number,
188 }
189 }
190}
191
192impl TryFrom<&[u8]> for LaserInfo {
193 type Error = LaserInfoParseError;
194
195 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
196 let header_bytes: &[u8; LaserInfoHeader::SIZE] = bytes
198 .get(0..LaserInfoHeader::SIZE)
199 .and_then(|slice| slice.try_into().ok())
200 .ok_or_else(|| LaserInfoParseError::ResponseTooShort {
201 expected: LaserInfoHeader::SIZE,
202 actual: bytes.len(),
203 })?;
204 let header = LaserInfoHeader::from(*header_bytes);
206 let model_name_start = LaserInfoHeader::SIZE;
208 let model_name_cstr = CStr::from_bytes_until_nul(&bytes[model_name_start..])?;
209 let model_name = String::from_utf8_lossy(model_name_cstr.to_bytes()).to_string();
210 Ok(LaserInfo { header, model_name })
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_parse_laser_info_header() {
220 let mut header = [0u8; LaserInfoHeader::SIZE];
222
223 header[3] = 1; header[4] = 2; header[5] = 1; header[11] = 0x70;
230 header[12] = 0x17;
231 header[13] = 0;
232 header[14] = 0;
233
234 header[15] = 0x70;
236 header[16] = 0x17;
237 header[17] = 0;
238 header[18] = 0;
239
240 header[20] = 0x88; header[21] = 0x13; header[22] = 0x70; header[23] = 0x17; header[24] = 100; header[25] = 31; header[26] = 2; header[27] = 2;
254 header[28] = 3;
255 header[29] = 4;
256 header[30] = 5;
257 header[31] = 6;
258
259 header[32] = 192;
261 header[33] = 168;
262 header[34] = 1;
263 header[35] = 100;
264
265 header[37] = 1;
267
268 let info_header = LaserInfoHeader::from(header);
269
270 assert_eq!(info_header.fw_major, 1);
271 assert_eq!(info_header.fw_minor, 2);
272 assert_eq!(info_header.output_enabled, true);
273 assert_eq!(info_header.dac_rate, 6000);
274 assert_eq!(info_header.max_dac_rate, 6000);
275 assert_eq!(info_header.rx_buffer_free, 5000);
276 assert_eq!(info_header.rx_buffer_size, 6000);
277 assert_eq!(info_header.battery_percent, 100);
278 assert_eq!(info_header.temperature, 31);
279 assert_eq!(info_header.connection_type(), ConnectionType::Ethernet);
280 assert_eq!(info_header.model_number, 1);
281 assert_eq!(info_header.serial_number, [2, 2, 3, 4, 5, 6]); assert_eq!(info_header.ip_addr, Ipv4Addr::from([192, 168, 1, 100]));
283 }
284
285 #[test]
286 fn test_parse_laser_info_with_header() {
287 let mut message = [0u8; 80]; message[0] = 0x77; message[3] = 1; message[4] = 2; message[5] = 1; message[11] = 0x70;
298 message[12] = 0x17;
299 message[13] = 0;
300 message[14] = 0;
301
302 message[15] = 0x70;
304 message[16] = 0x17;
305 message[17] = 0;
306 message[18] = 0;
307
308 message[20] = 0x88; message[21] = 0x13; message[22] = 0x70; message[23] = 0x17; message[24] = 100; message[25] = 31; message[26] = 2; message[27] = 2;
321 message[28] = 3;
322 message[29] = 4;
323 message[30] = 5;
324 message[31] = 6;
325
326 message[32] = 192;
328 message[33] = 168;
329 message[34] = 1;
330 message[35] = 100;
331
332 message[37] = 1;
334
335 let model_name = b"LaserCube Pro";
337 let name_offset = 38;
338 for (i, &byte) in model_name.iter().enumerate() {
339 message[name_offset + i] = byte;
340 }
341 message[name_offset + model_name.len()] = 0; let laser_info = LaserInfo::try_from(&message[..]).unwrap();
344
345 assert_eq!(laser_info.header.fw_major, 1);
346 assert_eq!(laser_info.header.fw_minor, 2);
347 assert_eq!(laser_info.header.output_enabled, true);
348 assert_eq!(laser_info.header.dac_rate, 6000);
349 assert_eq!(laser_info.header.max_dac_rate, 6000);
350 assert_eq!(laser_info.header.rx_buffer_free, 5000);
351 assert_eq!(laser_info.header.rx_buffer_size, 6000);
352 assert_eq!(laser_info.header.battery_percent, 100);
353 assert_eq!(laser_info.header.temperature, 31);
354 assert_eq!(
355 laser_info.header.connection_type(),
356 ConnectionType::Ethernet
357 );
358 assert_eq!(laser_info.header.model_number, 1);
359 assert_eq!(laser_info.header.serial_number, [2, 2, 3, 4, 5, 6]);
360 assert_eq!(laser_info.header.ip_addr, Ipv4Addr::from([192, 168, 1, 100]));
361 assert_eq!(laser_info.model_name, "LaserCube Pro");
362 }
363}