pub const FLAG: u8 = 0x7e;
pub const ESCAPE: u8 = 0x7d;
pub const ESCAPE_XOR: u8 = 0x20;
pub const XON: u8 = 0x11;
pub const XOFF: u8 = 0x13;
pub const SPECIAL_ESCAPE: u8 = 0xf8;
const FCS_INIT: u16 = 0xffff;
const FCS_GOOD: u16 = 0xf0b8;
const FCS_POLY: u16 = 0x8408;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SpinelHdlcError {
BufferTooSmall,
FrameTooLong,
UnexpectedEscape,
FrameTooShort,
BadFcs,
}
pub fn encode_frame(frame: &[u8], out: &mut [u8]) -> Result<usize, SpinelHdlcError> {
let mut written = 0;
push_raw(FLAG, out, &mut written)?;
let mut fcs = FCS_INIT;
for byte in frame.iter().copied() {
fcs = update_fcs(fcs, byte);
push_escaped(byte, out, &mut written)?;
}
let fcs = !fcs;
push_escaped((fcs & 0x00ff) as u8, out, &mut written)?;
push_escaped((fcs >> 8) as u8, out, &mut written)?;
push_raw(FLAG, out, &mut written)?;
Ok(written)
}
pub struct SpinelHdlcDecoder<const N: usize> {
frame: [u8; N],
len: usize,
escaped: bool,
in_frame: bool,
}
impl<const N: usize> SpinelHdlcDecoder<N> {
pub const fn new() -> Self {
Self {
frame: [0; N],
len: 0,
escaped: false,
in_frame: false,
}
}
pub fn reset(&mut self) {
self.len = 0;
self.escaped = false;
self.in_frame = false;
}
pub fn push_byte(
&mut self,
byte: u8,
out: &mut [u8],
) -> Result<Option<usize>, SpinelHdlcError> {
if byte == FLAG {
return self.finish_or_start(out);
}
if !self.in_frame {
return Ok(None);
}
if byte == ESCAPE {
self.escaped = true;
return Ok(None);
}
let decoded = if self.escaped {
self.escaped = false;
byte ^ ESCAPE_XOR
} else {
byte
};
if self.len == N {
self.reset();
return Err(SpinelHdlcError::FrameTooLong);
}
self.frame[self.len] = decoded;
self.len += 1;
Ok(None)
}
fn finish_or_start(&mut self, out: &mut [u8]) -> Result<Option<usize>, SpinelHdlcError> {
if !self.in_frame || self.len == 0 {
self.in_frame = true;
self.escaped = false;
self.len = 0;
return Ok(None);
}
if self.escaped {
self.reset();
return Err(SpinelHdlcError::UnexpectedEscape);
}
if self.len < 2 {
self.reset();
return Err(SpinelHdlcError::FrameTooShort);
}
let mut fcs = FCS_INIT;
let mut index = 0;
while index < self.len {
fcs = update_fcs(fcs, self.frame[index]);
index += 1;
}
if fcs != FCS_GOOD {
self.reset();
return Err(SpinelHdlcError::BadFcs);
}
let payload_len = self.len - 2;
if out.len() < payload_len {
self.reset();
return Err(SpinelHdlcError::BufferTooSmall);
}
out[..payload_len].copy_from_slice(&self.frame[..payload_len]);
self.len = 0;
self.escaped = false;
self.in_frame = true;
Ok(Some(payload_len))
}
}
impl<const N: usize> Default for SpinelHdlcDecoder<N> {
fn default() -> Self {
Self::new()
}
}
const fn should_escape(byte: u8) -> bool {
matches!(byte, FLAG | ESCAPE | XON | XOFF | SPECIAL_ESCAPE)
}
fn push_raw(byte: u8, out: &mut [u8], written: &mut usize) -> Result<(), SpinelHdlcError> {
if *written == out.len() {
return Err(SpinelHdlcError::BufferTooSmall);
}
out[*written] = byte;
*written += 1;
Ok(())
}
fn push_escaped(byte: u8, out: &mut [u8], written: &mut usize) -> Result<(), SpinelHdlcError> {
if should_escape(byte) {
push_raw(ESCAPE, out, written)?;
push_raw(byte ^ ESCAPE_XOR, out, written)
} else {
push_raw(byte, out, written)
}
}
fn update_fcs(mut fcs: u16, byte: u8) -> u16 {
fcs ^= u16::from(byte);
let mut bit = 0;
while bit < 8 {
if (fcs & 1) != 0 {
fcs = (fcs >> 1) ^ FCS_POLY;
} else {
fcs >>= 1;
}
bit += 1;
}
fcs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_and_decodes_spinel_payload() {
let payload = [0x80, 0x06, 0x00, 0x7e, 0x7d, 0x11, 0x13, 0xf8];
let mut encoded = [0u8; 64];
let len = encode_frame(&payload, &mut encoded).unwrap();
assert_eq!(encoded[0], FLAG);
assert_eq!(encoded[len - 1], FLAG);
assert!(encoded[..len].windows(2).any(|pair| pair == [ESCAPE, 0x5e]));
assert!(encoded[..len].windows(2).any(|pair| pair == [ESCAPE, 0x5d]));
let mut decoder = SpinelHdlcDecoder::<32>::new();
let mut decoded = [0u8; 32];
let mut out_len = None;
for byte in encoded[..len].iter().copied() {
out_len = decoder.push_byte(byte, &mut decoded).unwrap().or(out_len);
}
assert_eq!(out_len, Some(payload.len()));
assert_eq!(&decoded[..payload.len()], payload);
}
#[test]
fn rejects_bad_fcs() {
let payload = [0x80, 0x06, 0x00];
let mut encoded = [0u8; 32];
let len = encode_frame(&payload, &mut encoded).unwrap();
encoded[len - 2] ^= 0x01;
let mut decoder = SpinelHdlcDecoder::<16>::new();
let mut decoded = [0u8; 16];
let mut error = None;
for byte in encoded[..len].iter().copied() {
match decoder.push_byte(byte, &mut decoded) {
Ok(_) => {}
Err(err) => error = Some(err),
}
}
assert_eq!(error, Some(SpinelHdlcError::BadFcs));
}
#[test]
fn reports_output_buffer_too_small_without_truncating() {
let payload = [1, 2, 3, 4];
let mut encoded = [0u8; 32];
let len = encode_frame(&payload, &mut encoded).unwrap();
let mut decoder = SpinelHdlcDecoder::<16>::new();
let mut decoded = [0u8; 3];
let mut error = None;
for byte in encoded[..len].iter().copied() {
match decoder.push_byte(byte, &mut decoded) {
Ok(_) => {}
Err(err) => error = Some(err),
}
}
assert_eq!(error, Some(SpinelHdlcError::BufferTooSmall));
}
#[test]
fn ignores_bytes_until_first_flag() {
let mut decoder = SpinelHdlcDecoder::<16>::new();
let mut decoded = [0u8; 16];
assert_eq!(decoder.push_byte(0x80, &mut decoded), Ok(None));
assert_eq!(decoder.push_byte(FLAG, &mut decoded), Ok(None));
}
}