donglora-protocol 0.1.0

DongLoRa wire protocol types and COBS framing — shared between firmware and host crates
Documentation
//! 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)
                    {
                        if 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
    }
}