1use crate::types::{TrackingMode, ZoneFilterType};
2
3const CMD_HEADER: [u8; 4] = [0xFD, 0xFC, 0xFB, 0xFA];
5const CMD_FOOTER: [u8; 4] = [0x04, 0x03, 0x02, 0x01];
7
8#[derive(Debug, Clone)]
10pub enum Command {
11 EnableConfig,
12 EndConfig,
13 SingleTargetTracking,
14 MultiTargetTracking,
15 QueryTrackingMode,
16 ReadFirmwareVersion,
17 SetBaudRate(BaudRateIndex),
18 RestoreFactory,
19 Restart,
20 SetBluetooth(bool),
21 GetMacAddress,
22 QueryZoneFilter,
23 SetZoneFilter {
24 filter_type: ZoneFilterType,
25 zones: [ZoneRect; 3],
26 },
27}
28
29#[derive(Debug, Clone, Copy, Default)]
31pub struct ZoneRect {
32 pub x1: i16,
33 pub y1: i16,
34 pub x2: i16,
35 pub y2: i16,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40#[repr(u16)]
41pub enum BaudRateIndex {
42 B9600 = 0x0001,
43 B19200 = 0x0002,
44 B38400 = 0x0003,
45 B57600 = 0x0004,
46 B115200 = 0x0005,
47 B230400 = 0x0006,
48 B256000 = 0x0007,
49 B460800 = 0x0008,
50}
51
52impl BaudRateIndex {
53 pub fn from_rate(rate: u32) -> Option<Self> {
54 match rate {
55 9600 => Some(Self::B9600),
56 19200 => Some(Self::B19200),
57 38400 => Some(Self::B38400),
58 57600 => Some(Self::B57600),
59 115200 => Some(Self::B115200),
60 230400 => Some(Self::B230400),
61 256000 => Some(Self::B256000),
62 460800 => Some(Self::B460800),
63 _ => None,
64 }
65 }
66
67 pub fn to_rate(self) -> u32 {
68 match self {
69 Self::B9600 => 9600,
70 Self::B19200 => 19200,
71 Self::B38400 => 38400,
72 Self::B57600 => 57600,
73 Self::B115200 => 115200,
74 Self::B230400 => 230400,
75 Self::B256000 => 256000,
76 Self::B460800 => 460800,
77 }
78 }
79}
80
81#[derive(Debug)]
83pub struct CommandFrame {
84 buf: [u8; 64],
85 len: usize,
86}
87
88impl CommandFrame {
89 pub fn build(cmd: &Command) -> Self {
91 let mut frame = Self {
92 buf: [0u8; 64],
93 len: 0,
94 };
95
96 frame.push_slice(&CMD_HEADER);
98
99 let len_pos = frame.len;
101 frame.push_slice(&[0x00, 0x00]);
102
103 let data_start = frame.len;
104
105 match cmd {
106 Command::EnableConfig => {
107 frame.push_le_u16(0x00FF); frame.push_le_u16(0x0001); }
110 Command::EndConfig => {
111 frame.push_le_u16(0x00FE);
112 }
113 Command::SingleTargetTracking => {
114 frame.push_le_u16(0x0080);
115 }
116 Command::MultiTargetTracking => {
117 frame.push_le_u16(0x0090);
118 }
119 Command::QueryTrackingMode => {
120 frame.push_le_u16(0x0091);
121 }
122 Command::ReadFirmwareVersion => {
123 frame.push_le_u16(0x00A0);
124 }
125 Command::SetBaudRate(idx) => {
126 frame.push_le_u16(0x00A1);
127 frame.push_le_u16(*idx as u16);
128 }
129 Command::RestoreFactory => {
130 frame.push_le_u16(0x00A2);
131 }
132 Command::Restart => {
133 frame.push_le_u16(0x00A3);
134 }
135 Command::SetBluetooth(on) => {
136 frame.push_le_u16(0x00A4);
137 frame.push_le_u16(if *on { 0x0100 } else { 0x0000 });
138 }
139 Command::GetMacAddress => {
140 frame.push_le_u16(0x00A5);
141 frame.push_le_u16(0x0001);
142 }
143 Command::QueryZoneFilter => {
144 frame.push_le_u16(0x00C1);
145 }
146 Command::SetZoneFilter { filter_type, zones } => {
147 frame.push_le_u16(0x00C2);
148 frame.push_le_u16(*filter_type as u16);
149 for zone in zones {
150 frame.push_le_i16(zone.x1);
151 frame.push_le_i16(zone.y1);
152 frame.push_le_i16(zone.x2);
153 frame.push_le_i16(zone.y2);
154 }
155 }
156 }
157
158 let data_len = (frame.len - data_start) as u16;
160 frame.buf[len_pos] = data_len as u8;
161 frame.buf[len_pos + 1] = (data_len >> 8) as u8;
162
163 frame.push_slice(&CMD_FOOTER);
165
166 frame
167 }
168
169 pub fn as_bytes(&self) -> &[u8] {
171 &self.buf[..self.len]
172 }
173
174 fn push_slice(&mut self, data: &[u8]) {
175 self.buf[self.len..self.len + data.len()].copy_from_slice(data);
176 self.len += data.len();
177 }
178
179 fn push_le_u16(&mut self, val: u16) {
180 let bytes = val.to_le_bytes();
181 self.push_slice(&bytes);
182 }
183
184 fn push_le_i16(&mut self, val: i16) {
185 let bytes = val.to_le_bytes();
186 self.push_slice(&bytes);
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum AckStatus {
193 Success,
194 Failure,
195}
196
197#[derive(Debug, Clone)]
199pub enum AckData {
200 Simple,
202 EnableConfig {
204 protocol_version: u16,
205 buffer_size: u16,
206 },
207 TrackingMode(TrackingMode),
209 FirmwareVersion {
211 fw_type: u16,
212 major: u16,
213 minor: u32,
214 },
215 MacAddress([u8; 6]),
217 ZoneFilter {
219 filter_type: ZoneFilterType,
220 zones: [ZoneRect; 3],
221 },
222}
223
224#[derive(Debug, Clone)]
226pub struct AckFrame {
227 pub command_word: u16,
228 pub status: AckStatus,
229 pub data: AckData,
230}
231
232pub fn parse_ack(buf: &[u8]) -> Option<(AckFrame, usize)> {
236 let header_pos = find_sequence(buf, &CMD_HEADER)?;
238 let buf = &buf[header_pos..];
239
240 if buf.len() < 14 {
242 return None;
243 }
244
245 let data_len = u16::from_le_bytes([buf[4], buf[5]]) as usize;
246 let frame_len = 4 + 2 + data_len + 4;
247
248 if buf.len() < frame_len {
249 return None;
250 }
251
252 if buf[frame_len - 4..frame_len] != CMD_FOOTER {
254 return None;
255 }
256
257 let inframe = &buf[6..6 + data_len];
258
259 if inframe.len() < 4 {
262 return None;
263 }
264
265 let ack_cmd_word = u16::from_le_bytes([inframe[0], inframe[1]]);
266 let status_val = u16::from_le_bytes([inframe[2], inframe[3]]);
267 let status = if status_val == 0 {
268 AckStatus::Success
269 } else {
270 AckStatus::Failure
271 };
272
273 let command_word = ack_cmd_word & !0x0100;
275 let extra = &inframe[4..];
276
277 let data = match command_word {
278 0x00FF if extra.len() >= 4 => AckData::EnableConfig {
279 protocol_version: u16::from_le_bytes([extra[0], extra[1]]),
280 buffer_size: u16::from_le_bytes([extra[2], extra[3]]),
281 },
282 0x0091 if extra.len() >= 2 => {
283 let mode_val = u16::from_le_bytes([extra[0], extra[1]]);
284 match TrackingMode::from_u16(mode_val) {
285 Some(mode) => AckData::TrackingMode(mode),
286 None => AckData::Simple,
287 }
288 }
289 0x00A0 if extra.len() >= 8 => AckData::FirmwareVersion {
290 fw_type: u16::from_le_bytes([extra[0], extra[1]]),
291 major: u16::from_le_bytes([extra[2], extra[3]]),
292 minor: u32::from_le_bytes([extra[4], extra[5], extra[6], extra[7]]),
293 },
294 0x00A5 if extra.len() >= 4 => {
295 let mut mac = [0u8; 6];
299 let mac_start = 1; let available = extra.len().saturating_sub(mac_start).min(6);
301 mac[..available].copy_from_slice(&extra[mac_start..mac_start + available]);
302 AckData::MacAddress(mac)
303 }
304 0x00C1 if extra.len() >= 26 => {
305 let ft = u16::from_le_bytes([extra[0], extra[1]]);
306 let filter_type = ZoneFilterType::from_u16(ft).unwrap_or(ZoneFilterType::Disabled);
307 let zones = parse_zones(&extra[2..26]);
308 AckData::ZoneFilter { filter_type, zones }
309 }
310 _ => AckData::Simple,
311 };
312
313 Some((
314 AckFrame {
315 command_word,
316 status,
317 data,
318 },
319 header_pos + frame_len,
320 ))
321}
322
323fn parse_zones(data: &[u8]) -> [ZoneRect; 3] {
324 let mut zones = [ZoneRect::default(); 3];
325 for (i, zone) in zones.iter_mut().enumerate() {
326 let off = i * 8;
327 zone.x1 = i16::from_le_bytes([data[off], data[off + 1]]);
328 zone.y1 = i16::from_le_bytes([data[off + 2], data[off + 3]]);
329 zone.x2 = i16::from_le_bytes([data[off + 4], data[off + 5]]);
330 zone.y2 = i16::from_le_bytes([data[off + 6], data[off + 7]]);
331 }
332 zones
333}
334
335fn find_sequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
336 haystack.windows(needle.len()).position(|w| w == needle)
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn build_enable_config() {
345 let frame = CommandFrame::build(&Command::EnableConfig);
346 let expected: &[u8] = &[
347 0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x01, 0x00, 0x04, 0x03, 0x02, 0x01, ];
353 assert_eq!(frame.as_bytes(), expected);
354 }
355
356 #[test]
357 fn build_end_config() {
358 let frame = CommandFrame::build(&Command::EndConfig);
359 let expected: &[u8] = &[
360 0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01, ];
365 assert_eq!(frame.as_bytes(), expected);
366 }
367
368 #[test]
369 fn build_single_target_tracking() {
370 let frame = CommandFrame::build(&Command::SingleTargetTracking);
371 let expected: &[u8] = &[
372 0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x80, 0x00, 0x04, 0x03, 0x02, 0x01,
373 ];
374 assert_eq!(frame.as_bytes(), expected);
375 }
376
377 #[test]
378 fn build_set_baud_256000() {
379 let frame = CommandFrame::build(&Command::SetBaudRate(BaudRateIndex::B256000));
380 let expected: &[u8] = &[
381 0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xA1, 0x00, 0x07, 0x00, 0x04, 0x03, 0x02, 0x01,
382 ];
383 assert_eq!(frame.as_bytes(), expected);
384 }
385
386 #[test]
387 fn parse_ack_enable_config() {
388 let ack_bytes: &[u8] = &[
389 0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x01, 0x00, 0x40, 0x00, 0x04, 0x03, 0x02, 0x01, ];
397
398 let (ack, consumed) = parse_ack(ack_bytes).unwrap();
399 assert_eq!(consumed, ack_bytes.len());
400 assert_eq!(ack.command_word, 0x00FF);
401 assert_eq!(ack.status, AckStatus::Success);
402 match ack.data {
403 AckData::EnableConfig {
404 protocol_version,
405 buffer_size,
406 } => {
407 assert_eq!(protocol_version, 0x0001);
408 assert_eq!(buffer_size, 0x0040);
409 }
410 _ => panic!("expected EnableConfig ack data"),
411 }
412 }
413
414 #[test]
415 fn parse_ack_end_config() {
416 let ack_bytes: &[u8] = &[
417 0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01,
418 ];
419
420 let (ack, _) = parse_ack(ack_bytes).unwrap();
421 assert_eq!(ack.command_word, 0x00FE);
422 assert_eq!(ack.status, AckStatus::Success);
423 }
424
425 #[test]
426 fn parse_ack_tracking_mode_single() {
427 let ack_bytes: &[u8] = &[
428 0xFD, 0xFC, 0xFB, 0xFA, 0x06, 0x00, 0x91, 0x01, 0x00, 0x00, 0x01,
429 0x00, 0x04, 0x03, 0x02, 0x01,
431 ];
432
433 let (ack, _) = parse_ack(ack_bytes).unwrap();
434 assert_eq!(ack.command_word, 0x0091);
435 match ack.data {
436 AckData::TrackingMode(mode) => assert_eq!(mode, TrackingMode::Single),
437 _ => panic!("expected TrackingMode"),
438 }
439 }
440
441 #[test]
442 fn parse_ack_with_garbage_prefix() {
443 let mut data = vec![0x00, 0xFF, 0x42]; data.extend_from_slice(&[
445 0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01,
446 ]);
447
448 let (ack, _) = parse_ack(&data).unwrap();
449 assert_eq!(ack.command_word, 0x00FE);
450 }
451}