Skip to main content

donglora_protocol/
frame.rs

1//! DongLoRa Protocol frame format and streaming decoder (`PROTOCOL.md §2`).
2//!
3//! Every DongLoRa Protocol frame on the wire is `COBS(type || tag_le || payload ||
4//! crc_le) || 0x00`. This module owns the wire-level codec: it does
5//! nothing protocol-semantic (no enum matching, no tag validation) —
6//! just framing, CRC, and COBS.
7//!
8//! - `encode_frame` builds a complete wire-ready frame (COBS-encoded,
9//!   `0x00`-terminated) given `(type_id, tag, payload)`.
10//! - `FrameDecoder` accumulates bytes, splitting on `0x00`, and emits
11//!   `FrameResult` values through a caller-provided closure. `FrameResult`
12//!   is `Ok` with borrowed pre-payload-and-tag slices on success, or
13//!   `Err` with a `FrameError` variant on CRC/COBS/length failure.
14//!
15//! The decoder's internal scratch borrows into the callback, so parsing
16//! is zero-copy above the framing layer. Typed parsing (`Command`,
17//! `Response`, `Event`) consumes those borrowed slices.
18
19use crate::{
20    FRAME_HEADER_SIZE, FRAME_TRAILER_SIZE, FrameDecodeError, FrameEncodeError, MAX_PAYLOAD_FIELD,
21    MAX_PRE_COBS_FRAME, MAX_WIRE_FRAME, crc::crc16,
22};
23
24/// Outcome of attempting to decode one frame delimited by a `0x00` byte.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[cfg_attr(feature = "defmt", derive(defmt::Format))]
27pub enum FrameResult<'a> {
28    Ok {
29        type_id: u8,
30        tag: u16,
31        payload: &'a [u8],
32    },
33    Err(FrameDecodeError),
34}
35
36/// Worst-case COBS-encoded size for a maximum-length pre-COBS frame.
37/// Equal to `MAX_WIRE_FRAME - 1` (the `-1` accounts for the trailing
38/// `0x00` sentinel). Public so tests can pin its exact value against
39/// the derivation in `lib.rs`.
40pub const MAX_COBS_ENCODED: usize = MAX_WIRE_FRAME - 1;
41
42/// Encode `(type_id, tag, payload)` as a complete DongLoRa Protocol frame (COBS-encoded,
43/// `0x00`-terminated). Returns the number of bytes written to `out`.
44///
45/// `out` must be at least `MAX_WIRE_FRAME` bytes to satisfy the worst
46/// case across all frame shapes. Smaller buffers succeed when the actual
47/// frame fits.
48pub fn encode_frame(
49    type_id: u8,
50    tag: u16,
51    payload: &[u8],
52    out: &mut [u8],
53) -> Result<usize, FrameEncodeError> {
54    if payload.len() > MAX_PAYLOAD_FIELD {
55        return Err(FrameEncodeError::PayloadTooLarge);
56    }
57    let pre_cobs_len = FRAME_HEADER_SIZE + payload.len() + FRAME_TRAILER_SIZE;
58
59    let mut scratch = [0u8; MAX_PRE_COBS_FRAME];
60    scratch[0] = type_id;
61    scratch[1..3].copy_from_slice(&tag.to_le_bytes());
62    scratch[3..3 + payload.len()].copy_from_slice(payload);
63    let crc = crc16(&scratch[..FRAME_HEADER_SIZE + payload.len()]);
64    scratch[FRAME_HEADER_SIZE + payload.len()..pre_cobs_len].copy_from_slice(&crc.to_le_bytes());
65
66    let mut cobs_scratch = [0u8; MAX_COBS_ENCODED];
67    let encoded_len = ucobs::encode(&scratch[..pre_cobs_len], &mut cobs_scratch)
68        .ok_or(FrameEncodeError::CobsEncode)?;
69    let total = encoded_len + 1;
70    if out.len() < total {
71        return Err(FrameEncodeError::BufferTooSmall);
72    }
73    out[..encoded_len].copy_from_slice(&cobs_scratch[..encoded_len]);
74    out[encoded_len] = 0x00;
75    Ok(total)
76}
77
78/// Streaming accumulator for inbound bytes. Emits one `FrameResult` per
79/// `0x00`-delimited frame encountered in the feed.
80///
81/// Overflow policy: if accumulated bytes exceed the maximum COBS-encoded
82/// size without a delimiter, the buffer is reset and the next `0x00` is
83/// treated as the start of a fresh frame. A `FrameResult::Err` is NOT
84/// emitted for the dropped bytes — the bytes are simply never classified
85/// (the device or host will observe a dropped response and time out at
86/// the command level).
87pub struct FrameDecoder {
88    buf: [u8; MAX_COBS_ENCODED],
89    len: usize,
90    overflowed: bool,
91}
92
93impl FrameDecoder {
94    pub const fn new() -> Self {
95        Self {
96            buf: [0u8; MAX_COBS_ENCODED],
97            len: 0,
98            overflowed: false,
99        }
100    }
101
102    /// Discard any partial frame currently buffered.
103    pub fn reset(&mut self) {
104        self.len = 0;
105        self.overflowed = false;
106    }
107
108    /// Feed `data` into the accumulator. For every complete frame
109    /// detected, call `on_frame` with the result.
110    pub fn feed<F: FnMut(FrameResult<'_>)>(&mut self, data: &[u8], mut on_frame: F) {
111        for &byte in data {
112            if byte == 0x00 {
113                if self.overflowed || self.len == 0 {
114                    // Overflow recovery or stray sentinel: drop silently.
115                    self.len = 0;
116                    self.overflowed = false;
117                    continue;
118                }
119                let mut decoded = [0u8; MAX_PRE_COBS_FRAME];
120                match ucobs::decode(&self.buf[..self.len], &mut decoded) {
121                    Some(n) => {
122                        emit_decoded(&decoded[..n], &mut on_frame);
123                    }
124                    None => on_frame(FrameResult::Err(FrameDecodeError::Cobs)),
125                }
126                self.len = 0;
127                continue;
128            }
129            if self.overflowed {
130                // Still waiting for the next 0x00 to resync; drop this byte.
131                continue;
132            }
133            if self.len < self.buf.len() {
134                self.buf[self.len] = byte;
135                self.len += 1;
136            } else {
137                self.overflowed = true;
138                self.len = 0;
139            }
140        }
141    }
142}
143
144impl Default for FrameDecoder {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150fn emit_decoded<F: FnMut(FrameResult<'_>)>(decoded: &[u8], on_frame: &mut F) {
151    if decoded.len() < FRAME_HEADER_SIZE + FRAME_TRAILER_SIZE {
152        on_frame(FrameResult::Err(FrameDecodeError::TooShort));
153        return;
154    }
155    let crc_start = decoded.len() - FRAME_TRAILER_SIZE;
156    let body = &decoded[..crc_start];
157    let expected = crc16(body);
158    let got = u16::from_le_bytes([decoded[crc_start], decoded[crc_start + 1]]);
159    if expected != got {
160        on_frame(FrameResult::Err(FrameDecodeError::Crc));
161        return;
162    }
163    let type_id = body[0];
164    let tag = u16::from_le_bytes([body[1], body[2]]);
165    let payload = &body[FRAME_HEADER_SIZE..];
166    on_frame(FrameResult::Ok {
167        type_id,
168        tag,
169        payload,
170    });
171}
172
173#[cfg(test)]
174#[allow(clippy::panic, clippy::unwrap_used)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn max_cobs_encoded_is_wire_frame_minus_one() {
180        // Kills `MAX_COBS_ENCODED = MAX_WIRE_FRAME - 1` with `+1` / `/1`
181        // mutants (285 and 284 respectively, both ≠ 283).
182        assert_eq!(MAX_COBS_ENCODED, 283);
183        assert_eq!(MAX_COBS_ENCODED, MAX_WIRE_FRAME - 1);
184    }
185
186    fn roundtrip(type_id: u8, tag: u16, payload: &[u8]) {
187        let mut wire = [0u8; MAX_WIRE_FRAME];
188        let n = encode_frame(type_id, tag, payload, &mut wire).unwrap();
189
190        let mut decoder = FrameDecoder::new();
191        let mut emitted: heapless::Vec<(u8, u16, heapless::Vec<u8, 300>), 4> = heapless::Vec::new();
192        decoder.feed(&wire[..n], |res| match res {
193            FrameResult::Ok {
194                type_id,
195                tag,
196                payload,
197            } => {
198                let mut p = heapless::Vec::new();
199                p.extend_from_slice(payload).unwrap();
200                emitted.push((type_id, tag, p)).unwrap();
201            }
202            FrameResult::Err(e) => panic!("decode error: {:?}", e),
203        });
204        assert_eq!(emitted.len(), 1);
205        assert_eq!(emitted[0].0, type_id);
206        assert_eq!(emitted[0].1, tag);
207        assert_eq!(emitted[0].2.as_slice(), payload);
208    }
209
210    #[test]
211    fn empty_payload() {
212        roundtrip(0x01, 0x0001, &[]);
213    }
214
215    #[test]
216    fn one_byte_payload() {
217        roundtrip(0x04, 0x1234, &[0xAA]);
218    }
219
220    #[test]
221    fn max_payload() {
222        let big = [0x42u8; MAX_PAYLOAD_FIELD];
223        roundtrip(0xC0, 0x0000, &big);
224    }
225
226    #[test]
227    fn rejects_too_large_payload() {
228        let big = [0u8; MAX_PAYLOAD_FIELD + 1];
229        let mut wire = [0u8; MAX_WIRE_FRAME];
230        assert!(matches!(
231            encode_frame(0x01, 1, &big, &mut wire),
232            Err(FrameEncodeError::PayloadTooLarge)
233        ));
234    }
235
236    #[test]
237    fn rejects_small_output_buffer() {
238        let mut tiny = [0u8; 4];
239        assert!(matches!(
240            encode_frame(0x01, 1, b"hello", &mut tiny),
241            Err(FrameEncodeError::BufferTooSmall)
242        ));
243    }
244
245    #[test]
246    fn detects_crc_corruption() {
247        let mut wire = [0u8; MAX_WIRE_FRAME];
248        let n = encode_frame(0x04, 0x0042, b"hello", &mut wire).unwrap();
249        // Flip a non-sentinel byte mid-frame. This will corrupt the CRC.
250        wire[4] ^= 0xFF;
251
252        let mut decoder = FrameDecoder::new();
253        let mut got_err = None;
254        decoder.feed(&wire[..n], |res| {
255            if let FrameResult::Err(e) = res {
256                got_err = Some(e);
257            }
258        });
259        assert!(matches!(
260            got_err,
261            Some(FrameDecodeError::Crc | FrameDecodeError::Cobs)
262        ));
263    }
264
265    #[test]
266    fn resync_after_garbage() {
267        let mut decoder = FrameDecoder::new();
268
269        // Leading garbage with no 0x00 is accumulated silently.
270        decoder.feed(&[0xAA, 0xBB, 0xCC], |res| {
271            panic!("unexpected frame: {:?}", res);
272        });
273
274        // A first 0x00 closes the bogus frame — expect a decode error.
275        let mut errs = 0;
276        let mut oks = 0;
277        decoder.feed(&[0x00], |res| match res {
278            FrameResult::Err(_) => errs += 1,
279            FrameResult::Ok { .. } => oks += 1,
280        });
281        assert!(errs >= 1);
282        assert_eq!(oks, 0);
283
284        // Now a clean frame decodes normally.
285        let mut wire = [0u8; MAX_WIRE_FRAME];
286        let n = encode_frame(0x80, 0x0001, b"", &mut wire).unwrap();
287        let mut got_type = None;
288        decoder.feed(&wire[..n], |res| {
289            if let FrameResult::Ok { type_id, .. } = res {
290                got_type = Some(type_id);
291            }
292        });
293        assert_eq!(got_type, Some(0x80));
294    }
295
296    #[test]
297    fn overflow_resets_and_continues() {
298        let mut decoder = FrameDecoder::new();
299        // Overrun the buffer with a long non-zero run.
300        let bomb = [0x55u8; MAX_COBS_ENCODED + 20];
301        decoder.feed(&bomb, |res| panic!("unexpected frame: {:?}", res));
302
303        // The next 0x00 clears the overflow state without emitting a
304        // spurious error (we never saw a sane frame candidate).
305        decoder.feed(&[0x00], |res| panic!("unexpected frame: {:?}", res));
306
307        // A clean frame still decodes.
308        let mut wire = [0u8; MAX_WIRE_FRAME];
309        let n = encode_frame(0x01, 0x0007, &[], &mut wire).unwrap();
310        let mut got_type_tag = None;
311        decoder.feed(&wire[..n], |res| {
312            if let FrameResult::Ok { type_id, tag, .. } = res {
313                got_type_tag = Some((type_id, tag));
314            }
315        });
316        assert_eq!(got_type_tag, Some((0x01, 0x0007)));
317    }
318
319    #[test]
320    fn multiple_frames_in_one_feed() {
321        let mut wire = [0u8; MAX_WIRE_FRAME * 3];
322        let a = encode_frame(0x01, 1, &[], &mut wire).unwrap();
323        let b = encode_frame(0x02, 2, &[], &mut wire[a..]).unwrap();
324        let c = encode_frame(0x03, 3, &[], &mut wire[a + b..]).unwrap();
325        let total = a + b + c;
326
327        let mut decoder = FrameDecoder::new();
328        let mut tags: heapless::Vec<u16, 4> = heapless::Vec::new();
329        decoder.feed(&wire[..total], |res| {
330            if let FrameResult::Ok { tag, .. } = res {
331                tags.push(tag).unwrap();
332            }
333        });
334        assert_eq!(tags.as_slice(), &[1, 2, 3]);
335    }
336
337    #[test]
338    fn detects_short_decoded_frame() {
339        // Craft a COBS-encoded 1-byte pre-COBS frame (below 5-byte min).
340        // COBS of [0x42] is [0x02, 0x42].
341        let wire = [0x02, 0x42, 0x00];
342        let mut decoder = FrameDecoder::new();
343        let mut got_err = None;
344        decoder.feed(&wire, |res| {
345            if let FrameResult::Err(e) = res {
346                got_err = Some(e);
347            }
348        });
349        assert_eq!(got_err, Some(FrameDecodeError::TooShort));
350    }
351
352    #[test]
353    fn spec_ping_example() {
354        // PROTOCOL.md §C.2.1 — H→D PING, tag=0x0001.
355        // Expected on-wire: 03 01 01 03 9D C8 00
356        let mut wire = [0u8; MAX_WIRE_FRAME];
357        let n = encode_frame(0x01, 0x0001, &[], &mut wire).unwrap();
358        assert_eq!(&wire[..n], &[0x03, 0x01, 0x01, 0x03, 0x9D, 0xC8, 0x00]);
359    }
360
361    #[test]
362    fn spec_ok_example() {
363        // PROTOCOL.md §C.2.1 — D→H OK, tag=0x0001.
364        // Expected on-wire: 03 80 01 03 F7 C4 00
365        let mut wire = [0u8; MAX_WIRE_FRAME];
366        let n = encode_frame(0x80, 0x0001, &[], &mut wire).unwrap();
367        assert_eq!(&wire[..n], &[0x03, 0x80, 0x01, 0x03, 0xF7, 0xC4, 0x00]);
368    }
369}