use crate::{
FRAME_HEADER_SIZE, FRAME_TRAILER_SIZE, FrameDecodeError, FrameEncodeError, MAX_PAYLOAD_FIELD,
MAX_PRE_COBS_FRAME, MAX_WIRE_FRAME, crc::crc16,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FrameResult<'a> {
Ok {
type_id: u8,
tag: u16,
payload: &'a [u8],
},
Err(FrameDecodeError),
}
pub const MAX_COBS_ENCODED: usize = MAX_WIRE_FRAME - 1;
pub fn encode_frame(
type_id: u8,
tag: u16,
payload: &[u8],
out: &mut [u8],
) -> Result<usize, FrameEncodeError> {
if payload.len() > MAX_PAYLOAD_FIELD {
return Err(FrameEncodeError::PayloadTooLarge);
}
let pre_cobs_len = FRAME_HEADER_SIZE + payload.len() + FRAME_TRAILER_SIZE;
let mut scratch = [0u8; MAX_PRE_COBS_FRAME];
scratch[0] = type_id;
scratch[1..3].copy_from_slice(&tag.to_le_bytes());
scratch[3..3 + payload.len()].copy_from_slice(payload);
let crc = crc16(&scratch[..FRAME_HEADER_SIZE + payload.len()]);
scratch[FRAME_HEADER_SIZE + payload.len()..pre_cobs_len].copy_from_slice(&crc.to_le_bytes());
let mut cobs_scratch = [0u8; MAX_COBS_ENCODED];
let encoded_len = ucobs::encode(&scratch[..pre_cobs_len], &mut cobs_scratch)
.ok_or(FrameEncodeError::CobsEncode)?;
let total = encoded_len + 1;
if out.len() < total {
return Err(FrameEncodeError::BufferTooSmall);
}
out[..encoded_len].copy_from_slice(&cobs_scratch[..encoded_len]);
out[encoded_len] = 0x00;
Ok(total)
}
pub struct FrameDecoder {
buf: [u8; MAX_COBS_ENCODED],
len: usize,
overflowed: bool,
}
impl FrameDecoder {
pub const fn new() -> Self {
Self {
buf: [0u8; MAX_COBS_ENCODED],
len: 0,
overflowed: false,
}
}
pub fn reset(&mut self) {
self.len = 0;
self.overflowed = false;
}
pub fn feed<F: FnMut(FrameResult<'_>)>(&mut self, data: &[u8], mut on_frame: F) {
for &byte in data {
if byte == 0x00 {
if self.overflowed || self.len == 0 {
self.len = 0;
self.overflowed = false;
continue;
}
let mut decoded = [0u8; MAX_PRE_COBS_FRAME];
match ucobs::decode(&self.buf[..self.len], &mut decoded) {
Some(n) => {
emit_decoded(&decoded[..n], &mut on_frame);
}
None => on_frame(FrameResult::Err(FrameDecodeError::Cobs)),
}
self.len = 0;
continue;
}
if self.overflowed {
continue;
}
if self.len < self.buf.len() {
self.buf[self.len] = byte;
self.len += 1;
} else {
self.overflowed = true;
self.len = 0;
}
}
}
}
impl Default for FrameDecoder {
fn default() -> Self {
Self::new()
}
}
fn emit_decoded<F: FnMut(FrameResult<'_>)>(decoded: &[u8], on_frame: &mut F) {
if decoded.len() < FRAME_HEADER_SIZE + FRAME_TRAILER_SIZE {
on_frame(FrameResult::Err(FrameDecodeError::TooShort));
return;
}
let crc_start = decoded.len() - FRAME_TRAILER_SIZE;
let body = &decoded[..crc_start];
let expected = crc16(body);
let got = u16::from_le_bytes([decoded[crc_start], decoded[crc_start + 1]]);
if expected != got {
on_frame(FrameResult::Err(FrameDecodeError::Crc));
return;
}
let type_id = body[0];
let tag = u16::from_le_bytes([body[1], body[2]]);
let payload = &body[FRAME_HEADER_SIZE..];
on_frame(FrameResult::Ok {
type_id,
tag,
payload,
});
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn max_cobs_encoded_is_wire_frame_minus_one() {
assert_eq!(MAX_COBS_ENCODED, 283);
assert_eq!(MAX_COBS_ENCODED, MAX_WIRE_FRAME - 1);
}
fn roundtrip(type_id: u8, tag: u16, payload: &[u8]) {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(type_id, tag, payload, &mut wire).unwrap();
let mut decoder = FrameDecoder::new();
let mut emitted: heapless::Vec<(u8, u16, heapless::Vec<u8, 300>), 4> = heapless::Vec::new();
decoder.feed(&wire[..n], |res| match res {
FrameResult::Ok {
type_id,
tag,
payload,
} => {
let mut p = heapless::Vec::new();
p.extend_from_slice(payload).unwrap();
emitted.push((type_id, tag, p)).unwrap();
}
FrameResult::Err(e) => panic!("decode error: {:?}", e),
});
assert_eq!(emitted.len(), 1);
assert_eq!(emitted[0].0, type_id);
assert_eq!(emitted[0].1, tag);
assert_eq!(emitted[0].2.as_slice(), payload);
}
#[test]
fn empty_payload() {
roundtrip(0x01, 0x0001, &[]);
}
#[test]
fn one_byte_payload() {
roundtrip(0x04, 0x1234, &[0xAA]);
}
#[test]
fn max_payload() {
let big = [0x42u8; MAX_PAYLOAD_FIELD];
roundtrip(0xC0, 0x0000, &big);
}
#[test]
fn rejects_too_large_payload() {
let big = [0u8; MAX_PAYLOAD_FIELD + 1];
let mut wire = [0u8; MAX_WIRE_FRAME];
assert!(matches!(
encode_frame(0x01, 1, &big, &mut wire),
Err(FrameEncodeError::PayloadTooLarge)
));
}
#[test]
fn rejects_small_output_buffer() {
let mut tiny = [0u8; 4];
assert!(matches!(
encode_frame(0x01, 1, b"hello", &mut tiny),
Err(FrameEncodeError::BufferTooSmall)
));
}
#[test]
fn detects_crc_corruption() {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x04, 0x0042, b"hello", &mut wire).unwrap();
wire[4] ^= 0xFF;
let mut decoder = FrameDecoder::new();
let mut got_err = None;
decoder.feed(&wire[..n], |res| {
if let FrameResult::Err(e) = res {
got_err = Some(e);
}
});
assert!(matches!(
got_err,
Some(FrameDecodeError::Crc | FrameDecodeError::Cobs)
));
}
#[test]
fn resync_after_garbage() {
let mut decoder = FrameDecoder::new();
decoder.feed(&[0xAA, 0xBB, 0xCC], |res| {
panic!("unexpected frame: {:?}", res);
});
let mut errs = 0;
let mut oks = 0;
decoder.feed(&[0x00], |res| match res {
FrameResult::Err(_) => errs += 1,
FrameResult::Ok { .. } => oks += 1,
});
assert!(errs >= 1);
assert_eq!(oks, 0);
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x80, 0x0001, b"", &mut wire).unwrap();
let mut got_type = None;
decoder.feed(&wire[..n], |res| {
if let FrameResult::Ok { type_id, .. } = res {
got_type = Some(type_id);
}
});
assert_eq!(got_type, Some(0x80));
}
#[test]
fn overflow_resets_and_continues() {
let mut decoder = FrameDecoder::new();
let bomb = [0x55u8; MAX_COBS_ENCODED + 20];
decoder.feed(&bomb, |res| panic!("unexpected frame: {:?}", res));
decoder.feed(&[0x00], |res| panic!("unexpected frame: {:?}", res));
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x01, 0x0007, &[], &mut wire).unwrap();
let mut got_type_tag = None;
decoder.feed(&wire[..n], |res| {
if let FrameResult::Ok { type_id, tag, .. } = res {
got_type_tag = Some((type_id, tag));
}
});
assert_eq!(got_type_tag, Some((0x01, 0x0007)));
}
#[test]
fn multiple_frames_in_one_feed() {
let mut wire = [0u8; MAX_WIRE_FRAME * 3];
let a = encode_frame(0x01, 1, &[], &mut wire).unwrap();
let b = encode_frame(0x02, 2, &[], &mut wire[a..]).unwrap();
let c = encode_frame(0x03, 3, &[], &mut wire[a + b..]).unwrap();
let total = a + b + c;
let mut decoder = FrameDecoder::new();
let mut tags: heapless::Vec<u16, 4> = heapless::Vec::new();
decoder.feed(&wire[..total], |res| {
if let FrameResult::Ok { tag, .. } = res {
tags.push(tag).unwrap();
}
});
assert_eq!(tags.as_slice(), &[1, 2, 3]);
}
#[test]
fn detects_short_decoded_frame() {
let wire = [0x02, 0x42, 0x00];
let mut decoder = FrameDecoder::new();
let mut got_err = None;
decoder.feed(&wire, |res| {
if let FrameResult::Err(e) = res {
got_err = Some(e);
}
});
assert_eq!(got_err, Some(FrameDecodeError::TooShort));
}
#[test]
fn spec_ping_example() {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x01, 0x0001, &[], &mut wire).unwrap();
assert_eq!(&wire[..n], &[0x03, 0x01, 0x01, 0x03, 0x9D, 0xC8, 0x00]);
}
#[test]
fn spec_ok_example() {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x80, 0x0001, &[], &mut wire).unwrap();
assert_eq!(&wire[..n], &[0x03, 0x80, 0x01, 0x03, 0xF7, 0xC4, 0x00]);
}
}