Skip to main content

ld2450_proto/
command.rs

1use crate::types::{TrackingMode, ZoneFilterType};
2
3/// Command frame header
4const CMD_HEADER: [u8; 4] = [0xFD, 0xFC, 0xFB, 0xFA];
5/// Command frame footer
6const CMD_FOOTER: [u8; 4] = [0x04, 0x03, 0x02, 0x01];
7
8/// Commands that can be sent to the LD2450.
9#[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/// A rectangular zone defined by two diagonal vertices (mm).
30#[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/// Baud rate selection index per protocol spec.
39#[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/// A serialized command frame ready to send over UART.
82#[derive(Debug)]
83pub struct CommandFrame {
84    buf: [u8; 64],
85    len: usize,
86}
87
88impl CommandFrame {
89    /// Build the wire-format frame for a command.
90    pub fn build(cmd: &Command) -> Self {
91        let mut frame = Self {
92            buf: [0u8; 64],
93            len: 0,
94        };
95
96        // Header
97        frame.push_slice(&CMD_HEADER);
98
99        // Reserve 2 bytes for length, fill in after
100        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); // command word
108                frame.push_le_u16(0x0001); // command value
109            }
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        // Fill in data length
159        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        // Footer
164        frame.push_slice(&CMD_FOOTER);
165
166        frame
167    }
168
169    /// The serialized bytes to send over UART.
170    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/// Status code in ACK responses.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum AckStatus {
193    Success,
194    Failure,
195}
196
197/// Parsed ACK data for different commands.
198#[derive(Debug, Clone)]
199pub enum AckData {
200    /// Generic ACK with just a status (EndConfig, SetMode, SetBaud, etc.)
201    Simple,
202    /// EnableConfig response: protocol version + buffer size
203    EnableConfig {
204        protocol_version: u16,
205        buffer_size: u16,
206    },
207    /// Query tracking mode response
208    TrackingMode(TrackingMode),
209    /// Firmware version
210    FirmwareVersion {
211        fw_type: u16,
212        major: u16,
213        minor: u32,
214    },
215    /// MAC address (6 bytes)
216    MacAddress([u8; 6]),
217    /// Zone filter configuration
218    ZoneFilter {
219        filter_type: ZoneFilterType,
220        zones: [ZoneRect; 3],
221    },
222}
223
224/// A parsed ACK frame from the radar.
225#[derive(Debug, Clone)]
226pub struct AckFrame {
227    pub command_word: u16,
228    pub status: AckStatus,
229    pub data: AckData,
230}
231
232/// ACK frame parser — scans a byte buffer for a complete ACK frame.
233///
234/// Returns `Some((ack_frame, bytes_consumed))` if successful.
235pub fn parse_ack(buf: &[u8]) -> Option<(AckFrame, usize)> {
236    // Find header
237    let header_pos = find_sequence(buf, &CMD_HEADER)?;
238    let buf = &buf[header_pos..];
239
240    // Need at least header(4) + length(2) + cmd(2) + status(2) + footer(4) = 14
241    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    // Verify footer
253    if buf[frame_len - 4..frame_len] != CMD_FOOTER {
254        return None;
255    }
256
257    let inframe = &buf[6..6 + data_len];
258
259    // ACK inframe: command_word(2) | status(2) | [extra data...]
260    // Note: ACK command word has bit 0 of high byte set (e.g. 0xFF01 for cmd 0x00FF)
261    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    // Original command word: clear the ACK bit (bit 8)
274    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            // 1 byte type (0x00) + 3 bytes MAC... actually protocol says 6 bytes for MAC
296            // The doc shows: type(1) + mac(3) but real devices use 6 byte MAC
297            // Let's handle both cases
298            let mut mac = [0u8; 6];
299            let mac_start = 1; // skip type byte
300            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, // header
348            0x04, 0x00, // data length = 4
349            0xFF, 0x00, // command word
350            0x01, 0x00, // command value
351            0x04, 0x03, 0x02, 0x01, // footer
352        ];
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, // header
361            0x02, 0x00, // data length = 2
362            0xFE, 0x00, // command word
363            0x04, 0x03, 0x02, 0x01, // footer
364        ];
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, // header
390            0x08, 0x00, // data length = 8
391            0xFF, 0x01, // ack command word (0x00FF | 0x0100)
392            0x00, 0x00, // status: success
393            0x01, 0x00, // protocol version
394            0x40, 0x00, // buffer size
395            0x04, 0x03, 0x02, 0x01, // footer
396        ];
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, // single target
430            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]; // garbage
444        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}