donglora_protocol/framing.rs
1//! COBS framing for the DongLoRa wire protocol.
2//!
3//! Every frame sent between host and firmware is a COBS-encoded
4//! [`Command`] or [`Response`] payload followed by a single `0x00`
5//! sentinel byte. [`CobsDecoder`] accumulates an inbound byte stream
6//! and yields parsed [`Command`]s; [`cobs_encode_response`] produces a
7//! wire-ready byte slice from a [`Response`].
8//!
9//! Kept tiny and alloc-free so both the firmware (no_std, bounded
10//! buffers) and host crates can share it.
11
12use crate::{Command, Response};
13
14/// Maximum COBS frame size (bytes).
15///
16/// Worst case: tag(1) + rssi(2) + snr(2) + len(2) + payload(256) = 263
17/// raw bytes. COBS adds `ceil(263/254)+1 = 3` bytes. 266 < 512.
18pub const MAX_FRAME: usize = 512;
19
20const _: () = assert!(
21 MAX_FRAME >= crate::MAX_PAYLOAD + 64,
22 "MAX_FRAME too small for max payload + COBS overhead"
23);
24
25/// Decodes COBS-framed commands from a byte stream.
26pub struct CobsDecoder {
27 buf: [u8; MAX_FRAME],
28 len: usize,
29}
30
31impl Default for CobsDecoder {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl CobsDecoder {
38 pub const fn new() -> Self {
39 Self {
40 buf: [0u8; MAX_FRAME],
41 len: 0,
42 }
43 }
44
45 /// Reset the accumulator (discard partial frame).
46 pub fn reset(&mut self) {
47 self.len = 0;
48 }
49
50 /// Feed a chunk of bytes, calling `on_command` for each decoded command.
51 pub fn feed(&mut self, data: &[u8], mut on_command: impl FnMut(Command)) {
52 for &byte in data {
53 if byte == 0x00 {
54 // End of COBS frame — decode and dispatch
55 if self.len > 0 {
56 let mut decode_buf = [0u8; MAX_FRAME];
57 if let Some(decoded_len) = ucobs::decode(&self.buf[..self.len], &mut decode_buf)
58 {
59 if let Some(cmd) = Command::from_bytes(&decode_buf[..decoded_len]) {
60 on_command(cmd);
61 }
62 }
63 }
64 self.len = 0;
65 } else if self.len < MAX_FRAME {
66 self.buf[self.len] = byte;
67 self.len += 1;
68 } else {
69 // Frame too large — discard
70 self.len = 0;
71 }
72 }
73 }
74}
75
76/// COBS-encode a response into `encode_buf` with trailing 0x00 sentinel.
77///
78/// Returns the slice to send (encoded frame + sentinel), or `None` on overflow.
79pub fn cobs_encode_response<'a>(
80 response: Response,
81 write_buf: &mut [u8; MAX_FRAME],
82 encode_buf: &'a mut [u8; MAX_FRAME],
83) -> Option<&'a [u8]> {
84 let raw_len = response.write_to(write_buf);
85 let encoded_len = ucobs::encode(&write_buf[..raw_len], encode_buf).unwrap_or(0);
86 if encoded_len < encode_buf.len() {
87 encode_buf[encoded_len] = 0x00;
88 Some(&encode_buf[..encoded_len + 1])
89 } else {
90 #[cfg(feature = "defmt")]
91 defmt::warn!("COBS encode buffer overflow");
92 None
93 }
94}