From 548cf722c2ef2c91fab9e60194aa40ae0e03e71f Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 13 Apr 2026 09:00:56 -0600 Subject: [PATCH] feat: add COBS framing module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Imports CobsDecoder and cobs_encode_response from firmware's src/host/framing.rs, adjusted so the defmt::warn on encode overflow is gated on the crate's defmt feature rather than an unconditional import. The route_command helper stays in firmware — it depends on embassy channels, which have no business in a transport-agnostic crate. --- src/framing.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ 2 files changed, 95 insertions(+) create mode 100644 src/framing.rs diff --git a/src/framing.rs b/src/framing.rs new file mode 100644 index 0000000..0f4f2ac --- /dev/null +++ b/src/framing.rs @@ -0,0 +1,93 @@ +//! COBS framing for the DongLoRa wire protocol. +//! +//! Every frame sent between host and firmware is a COBS-encoded +//! [`Command`] or [`Response`] payload followed by a single `0x00` +//! sentinel byte. [`CobsDecoder`] accumulates an inbound byte stream +//! and yields parsed [`Command`]s; [`cobs_encode_response`] produces a +//! wire-ready byte slice from a [`Response`]. +//! +//! Kept tiny and alloc-free so both the firmware (no_std, bounded +//! buffers) and host crates can share it. + +use crate::{Command, Response}; + +/// Maximum COBS frame size (bytes). +/// +/// Worst case: tag(1) + rssi(2) + snr(2) + len(2) + payload(256) = 263 +/// raw bytes. COBS adds `ceil(263/254)+1 = 3` bytes. 266 < 512. +pub const MAX_FRAME: usize = 512; + +const _: () = assert!( + MAX_FRAME >= crate::MAX_PAYLOAD + 64, + "MAX_FRAME too small for max payload + COBS overhead" +); + +/// Decodes COBS-framed commands from a byte stream. +pub struct CobsDecoder { + buf: [u8; MAX_FRAME], + len: usize, +} + +impl Default for CobsDecoder { + fn default() -> Self { + Self::new() + } +} + +impl CobsDecoder { + pub const fn new() -> Self { + Self { + buf: [0u8; MAX_FRAME], + len: 0, + } + } + + /// Reset the accumulator (discard partial frame). + pub fn reset(&mut self) { + self.len = 0; + } + + /// Feed a chunk of bytes, calling `on_command` for each decoded command. + pub fn feed(&mut self, data: &[u8], mut on_command: impl FnMut(Command)) { + for &byte in data { + if byte == 0x00 { + // End of COBS frame — decode and dispatch + if self.len > 0 { + let mut decode_buf = [0u8; MAX_FRAME]; + if let Some(decoded_len) = ucobs::decode(&self.buf[..self.len], &mut decode_buf) + && let Some(cmd) = Command::from_bytes(&decode_buf[..decoded_len]) + { + on_command(cmd); + } + } + self.len = 0; + } else if self.len < MAX_FRAME { + self.buf[self.len] = byte; + self.len += 1; + } else { + // Frame too large — discard + self.len = 0; + } + } + } +} + +/// COBS-encode a response into `encode_buf` with trailing 0x00 sentinel. +/// +/// Returns the slice to send (encoded frame + sentinel), or `None` on overflow. +pub fn cobs_encode_response<'a>( + response: Response, + write_buf: &mut [u8; MAX_FRAME], + encode_buf: &'a mut [u8; MAX_FRAME], +) -> Option<&'a [u8]> { + let raw_len = response.write_to(write_buf); + let encoded_len = ucobs::encode(&write_buf[..raw_len], encode_buf).unwrap_or(0); + if encoded_len < encode_buf.len() { + encode_buf[encoded_len] = 0x00; + Some(&encode_buf[..encoded_len + 1]) + } else { + #[cfg(feature = "defmt")] + defmt::warn!("COBS encode buffer overflow"); + None + } +} diff --git a/src/lib.rs b/src/lib.rs index 2113fe7..707c691 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ #![no_std] +pub mod framing; + use heapless::Vec; /// Maximum LoRa payload size in bytes.