Skip to main content

ld2450_proto/
frame.rs

1use crate::types::RadarFrame;
2
3/// Data frame header: AA FF 03 00
4const DATA_HEADER: [u8; 4] = [0xAA, 0xFF, 0x03, 0x00];
5/// Data frame footer: 55 CC
6const DATA_FOOTER: [u8; 2] = [0x55, 0xCC];
7
8/// Total payload size: 3 targets × 8 bytes each
9const PAYLOAD_LEN: usize = 24;
10/// Events emitted by the frame parser.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ParseEvent {
13    /// A complete, valid radar data frame was parsed.
14    Frame(RadarFrame),
15}
16
17/// Parser states.
18#[derive(Debug, Clone, Copy)]
19enum State {
20    /// Scanning for the header sequence.
21    SearchHeader { matched: u8 },
22    /// Reading payload bytes.
23    ReadPayload { pos: u8 },
24    /// Reading footer bytes.
25    ReadFooter { matched: u8 },
26}
27
28/// Streaming frame parser for LD2450 radar data output.
29///
30/// Feed bytes one at a time or in chunks. The parser emits `ParseEvent::Frame`
31/// for each valid frame found in the byte stream.
32///
33/// This is a zero-allocation state machine suitable for `no_std` environments.
34#[derive(Debug)]
35pub struct FrameParser {
36    state: State,
37    buf: [u8; PAYLOAD_LEN],
38}
39
40impl FrameParser {
41    pub fn new() -> Self {
42        Self {
43            state: State::SearchHeader { matched: 0 },
44            buf: [0u8; PAYLOAD_LEN],
45        }
46    }
47
48    /// Feed a single byte to the parser. Returns `Some(ParseEvent)` if a
49    /// complete frame was parsed.
50    #[inline]
51    pub fn feed(&mut self, byte: u8) -> Option<ParseEvent> {
52        match self.state {
53            State::SearchHeader { matched } => {
54                if byte == DATA_HEADER[matched as usize] {
55                    let next = matched + 1;
56                    if next as usize == DATA_HEADER.len() {
57                        self.state = State::ReadPayload { pos: 0 };
58                    } else {
59                        self.state = State::SearchHeader { matched: next };
60                    }
61                } else if byte == DATA_HEADER[0] {
62                    // Could be start of a new header
63                    self.state = State::SearchHeader { matched: 1 };
64                } else {
65                    self.state = State::SearchHeader { matched: 0 };
66                }
67                None
68            }
69            State::ReadPayload { pos } => {
70                self.buf[pos as usize] = byte;
71                let next = pos + 1;
72                if next as usize == PAYLOAD_LEN {
73                    self.state = State::ReadFooter { matched: 0 };
74                } else {
75                    self.state = State::ReadPayload { pos: next };
76                }
77                None
78            }
79            State::ReadFooter { matched } => {
80                if byte == DATA_FOOTER[matched as usize] {
81                    let next = matched + 1;
82                    if next as usize == DATA_FOOTER.len() {
83                        // Complete frame!
84                        self.state = State::SearchHeader { matched: 0 };
85                        let frame = RadarFrame::from_bytes(&self.buf);
86                        return Some(ParseEvent::Frame(frame));
87                    } else {
88                        self.state = State::ReadFooter { matched: next };
89                    }
90                } else {
91                    // Invalid footer — discard and resync
92                    self.state = State::SearchHeader { matched: 0 };
93                }
94                None
95            }
96        }
97    }
98
99    /// Feed a slice of bytes, collecting all parsed events.
100    /// For performance-critical paths, prefer calling `feed()` in a loop
101    /// and handling events inline.
102    #[cfg(feature = "std")]
103    pub fn feed_slice(&mut self, data: &[u8]) -> Vec<ParseEvent> {
104        let mut events = Vec::new();
105        for &b in data {
106            if let Some(ev) = self.feed(b) {
107                events.push(ev);
108            }
109        }
110        events
111    }
112}
113
114impl Default for FrameParser {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn make_frame(payload: &[u8; 24]) -> Vec<u8> {
125        let mut frame = Vec::with_capacity(30);
126        frame.extend_from_slice(&DATA_HEADER);
127        frame.extend_from_slice(payload);
128        frame.extend_from_slice(&DATA_FOOTER);
129        frame
130    }
131
132    #[test]
133    fn parse_datasheet_example() {
134        // From protocol doc example
135        let raw: [u8; 30] = [
136            0xAA, 0xFF, 0x03, 0x00, // header
137            0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01, // target 1
138            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // target 2 (empty)
139            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // target 3 (empty)
140            0x55, 0xCC, // footer
141        ];
142
143        let mut parser = FrameParser::new();
144        let events = parser.feed_slice(&raw);
145        assert_eq!(events.len(), 1);
146
147        let ParseEvent::Frame(frame) = &events[0];
148        assert_eq!(frame.targets[0].x, -782);
149        assert_eq!(frame.targets[0].y, 1713);
150        assert_eq!(frame.targets[0].speed, -16);
151        assert_eq!(frame.targets[0].distance_resolution, 320);
152        assert!(frame.targets[1].is_empty());
153        assert!(frame.targets[2].is_empty());
154        assert_eq!(frame.active_count(), 1);
155    }
156
157    #[test]
158    fn parse_with_garbage_prefix() {
159        let mut data = vec![0xFF, 0x00, 0x42, 0x13]; // garbage
160        let mut payload = [0u8; 24];
161        payload[..8].copy_from_slice(&[0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01]);
162        data.extend_from_slice(&make_frame(&payload));
163
164        let mut parser = FrameParser::new();
165        let events = parser.feed_slice(&data);
166        assert_eq!(events.len(), 1);
167    }
168
169    #[test]
170    fn parse_multiple_frames() {
171        let mut payload = [0u8; 24];
172        payload[..8].copy_from_slice(&[0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01]);
173        let frame = make_frame(&payload);
174
175        let mut data = Vec::new();
176        data.extend_from_slice(&frame);
177        data.extend_from_slice(&frame);
178        data.extend_from_slice(&frame);
179
180        let mut parser = FrameParser::new();
181        let events = parser.feed_slice(&data);
182        assert_eq!(events.len(), 3);
183    }
184
185    #[test]
186    fn bad_footer_resyncs() {
187        let mut data = Vec::new();
188        // Frame with bad footer
189        data.extend_from_slice(&DATA_HEADER);
190        data.extend_from_slice(&[0u8; 24]);
191        data.extend_from_slice(&[0x55, 0xDD]); // wrong footer
192
193        // Followed by a good frame
194        let mut payload = [0u8; 24];
195        payload[..8].copy_from_slice(&[0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01]);
196        data.extend_from_slice(&make_frame(&payload));
197
198        let mut parser = FrameParser::new();
199        let events = parser.feed_slice(&data);
200        assert_eq!(events.len(), 1); // only the good frame
201    }
202
203    #[test]
204    fn byte_by_byte_feeding() {
205        let mut payload = [0u8; 24];
206        payload[..8].copy_from_slice(&[0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01]);
207        let data = make_frame(&payload);
208
209        let mut parser = FrameParser::new();
210        let mut events = Vec::new();
211        for &b in &data {
212            if let Some(ev) = parser.feed(b) {
213                events.push(ev);
214            }
215        }
216        assert_eq!(events.len(), 1);
217    }
218}