use alloc::vec::Vec;
pub const ADDR_FLIGHT_CONTROLLER: u8 = 0xC8;
pub const ADDR_TRANSMITTER_MODULE: u8 = 0xEE;
pub const ADDR_HANDSET: u8 = 0xEA;
pub const TYPE_RC_CHANNELS_PACKED: u8 = 0x16;
pub const TYPE_GPS: u8 = 0x02;
pub const TYPE_BATTERY: u8 = 0x08;
pub const TYPE_LINK_STATISTICS: u8 = 0x14;
pub const TYPE_ATTITUDE: u8 = 0x1E;
pub const TYPE_FLIGHT_MODE: u8 = 0x21;
pub const TYPE_PARAMETER_SETTINGS_ENTRY: u8 = 0x2D;
pub const TYPE_PARAMETER_READ: u8 = 0x2E;
pub const TYPE_PARAMETER_WRITE: u8 = 0x2F;
pub const MAX_LEN_BYTE: u8 = 62;
pub const MIN_LEN_BYTE: u8 = 2;
pub const MAX_PAYLOAD: usize = 60;
pub const MAX_FRAME_BYTES: usize = 64;
pub fn crc8_dvb_s2(data: &[u8]) -> u8 {
let mut crc: u8 = 0;
for &byte in data {
crc ^= byte;
for _ in 0..8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0xD5;
} else {
crc <<= 1;
}
}
}
crc
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawFrame {
pub sync: u8,
pub frame_type: u8,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameError {
BadSync(u8),
BadLength(u8),
Truncated,
CrcMismatch {
expected: u8,
got: u8,
},
PayloadSizeMismatch {
frame_type: u8,
expected: usize,
got: usize,
},
PayloadTooLarge(usize),
}
impl core::fmt::Display for FrameError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FrameError::BadSync(b) => write!(f, "bad sync byte 0x{b:02X}"),
FrameError::BadLength(l) => write!(f, "bad length byte {l}"),
FrameError::Truncated => write!(f, "truncated frame"),
FrameError::CrcMismatch { expected, got } => {
write!(
f,
"crc mismatch: expected 0x{expected:02X}, got 0x{got:02X}"
)
}
FrameError::PayloadSizeMismatch {
frame_type,
expected,
got,
} => write!(
f,
"payload size mismatch for type 0x{frame_type:02X}: expected {expected}, got {got}"
),
FrameError::PayloadTooLarge(n) => write!(f, "payload too large: {n} bytes"),
}
}
}
impl core::error::Error for FrameError {}
pub fn is_sync_byte(b: u8) -> bool {
matches!(
b,
ADDR_FLIGHT_CONTROLLER | ADDR_TRANSMITTER_MODULE | ADDR_HANDSET
)
}
pub fn fixed_payload_size(frame_type: u8) -> Option<usize> {
match frame_type {
TYPE_RC_CHANNELS_PACKED => Some(22),
TYPE_LINK_STATISTICS => Some(10),
TYPE_GPS => Some(15),
TYPE_BATTERY => Some(8),
TYPE_ATTITUDE => Some(6),
_ => None,
}
}
pub fn build_frame(sync: u8, frame_type: u8, payload: &[u8]) -> Result<Vec<u8>, FrameError> {
if payload.len() > MAX_PAYLOAD {
return Err(FrameError::PayloadTooLarge(payload.len()));
}
let mut out = Vec::with_capacity(4 + payload.len());
out.push(sync);
out.push((payload.len() + 2) as u8); out.push(frame_type);
out.extend_from_slice(payload);
out.push(crc8_dvb_s2(&out[2..]));
Ok(out)
}
pub fn parse_frame(buf: &[u8]) -> Result<(RawFrame, usize), FrameError> {
if buf.len() < 2 {
return Err(FrameError::Truncated);
}
let sync = buf[0];
if !is_sync_byte(sync) {
return Err(FrameError::BadSync(sync));
}
let len = buf[1];
if !(MIN_LEN_BYTE..=MAX_LEN_BYTE).contains(&len) {
return Err(FrameError::BadLength(len));
}
let total = 2 + len as usize;
if buf.len() < total {
return Err(FrameError::Truncated);
}
let frame_type = buf[2];
let payload = &buf[3..total - 1];
let got_crc = buf[total - 1];
let expected_crc = crc8_dvb_s2(&buf[2..total - 1]);
if got_crc != expected_crc {
return Err(FrameError::CrcMismatch {
expected: expected_crc,
got: got_crc,
});
}
if let Some(expected) = fixed_payload_size(frame_type) {
if payload.len() != expected {
return Err(FrameError::PayloadSizeMismatch {
frame_type,
expected,
got: payload.len(),
});
}
}
Ok((
RawFrame {
sync,
frame_type,
payload: payload.to_vec(),
},
total,
))
}
#[derive(Debug, Default)]
pub struct Parser {
buf: Vec<u8>,
pub frames_ok: u64,
pub crc_errors: u64,
pub resync_bytes: u64,
}
impl Parser {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, bytes: &[u8]) -> Vec<RawFrame> {
self.buf.extend_from_slice(bytes);
let mut out = Vec::new();
loop {
match parse_frame(&self.buf) {
Ok((frame, consumed)) => {
self.buf.drain(..consumed);
self.frames_ok += 1;
out.push(frame);
}
Err(FrameError::Truncated) => break,
Err(e) => {
if matches!(e, FrameError::CrcMismatch { .. }) {
self.crc_errors += 1;
} else {
self.resync_bytes += 1;
}
self.buf.drain(..1);
}
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crc8_catalog_check_value() {
assert_eq!(crc8_dvb_s2(b"123456789"), 0xBC);
}
#[test]
fn crc8_single_byte_vectors() {
assert_eq!(crc8_dvb_s2(&[0x00]), 0x00);
assert_eq!(crc8_dvb_s2(&[0x01]), 0xD5);
}
#[test]
fn crc8_empty_input_is_zero() {
assert_eq!(crc8_dvb_s2(&[]), 0x00);
}
#[test]
fn build_frame_layout_and_roundtrip() {
let payload = [0xAA, 0xBB, 0xCC];
let wire = build_frame(ADDR_FLIGHT_CONTROLLER, 0x7F, &payload).unwrap();
assert_eq!(wire.len(), 2 + 2 + payload.len());
assert_eq!(wire[0], 0xC8);
assert_eq!(wire[1], (payload.len() + 2) as u8);
assert_eq!(wire[2], 0x7F);
let (frame, consumed) = parse_frame(&wire).unwrap();
assert_eq!(consumed, wire.len());
assert_eq!(frame.sync, 0xC8);
assert_eq!(frame.frame_type, 0x7F);
assert_eq!(frame.payload, payload);
}
#[test]
fn build_frame_rejects_oversize_payload() {
let payload = vec![0u8; MAX_PAYLOAD + 1];
assert_eq!(
build_frame(ADDR_FLIGHT_CONTROLLER, 0x10, &payload),
Err(FrameError::PayloadTooLarge(MAX_PAYLOAD + 1))
);
let max = vec![0u8; MAX_PAYLOAD];
let wire = build_frame(ADDR_FLIGHT_CONTROLLER, 0x10, &max).unwrap();
assert_eq!(wire[1], MAX_LEN_BYTE);
assert!(parse_frame(&wire).is_ok());
}
#[test]
fn parse_rejects_corrupted_crc() {
let mut wire = build_frame(ADDR_FLIGHT_CONTROLLER, 0x7F, &[1, 2, 3]).unwrap();
let last = wire.len() - 1;
wire[last] ^= 0xFF;
assert!(matches!(
parse_frame(&wire),
Err(FrameError::CrcMismatch { .. })
));
}
#[test]
fn parse_rejects_bad_sync_and_bad_length() {
assert_eq!(parse_frame(&[0x55, 0x04]), Err(FrameError::BadSync(0x55)));
assert_eq!(
parse_frame(&[ADDR_FLIGHT_CONTROLLER, 0x01, 0x00]),
Err(FrameError::BadLength(0x01))
);
assert_eq!(
parse_frame(&[ADDR_FLIGHT_CONTROLLER, 63, 0x00]),
Err(FrameError::BadLength(63))
);
}
#[test]
fn parse_accepts_all_three_device_addresses() {
for sync in [
ADDR_FLIGHT_CONTROLLER,
ADDR_TRANSMITTER_MODULE,
ADDR_HANDSET,
] {
let wire = build_frame(sync, 0x7F, &[9]).unwrap();
assert_eq!(parse_frame(&wire).unwrap().0.sync, sync);
}
}
#[test]
fn parse_rejects_known_type_size_mismatch() {
let wire = build_frame(ADDR_FLIGHT_CONTROLLER, TYPE_LINK_STATISTICS, &[0; 9]).unwrap();
assert_eq!(
parse_frame(&wire),
Err(FrameError::PayloadSizeMismatch {
frame_type: TYPE_LINK_STATISTICS,
expected: 10,
got: 9,
})
);
}
#[test]
fn parser_reassembles_split_frames() {
let wire = build_frame(ADDR_FLIGHT_CONTROLLER, 0x7F, &[1, 2, 3, 4]).unwrap();
let mut p = Parser::new();
for &b in &wire[..wire.len() - 1] {
assert!(p.push(&[b]).is_empty());
}
let frames = p.push(&[wire[wire.len() - 1]]);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].payload, vec![1, 2, 3, 4]);
assert_eq!(p.frames_ok, 1);
assert_eq!(p.crc_errors, 0);
}
#[test]
fn parser_recovers_after_garbage_and_counts_crc_errors() {
let good = build_frame(ADDR_FLIGHT_CONTROLLER, 0x7F, &[7, 8]).unwrap();
let mut corrupt = good.clone();
let last = corrupt.len() - 1;
corrupt[last] ^= 0x01;
let mut stream = Vec::new();
stream.extend_from_slice(&[0x00, 0x11, 0x22]); stream.extend_from_slice(&corrupt);
stream.extend_from_slice(&[0xFF]); stream.extend_from_slice(&good);
let mut p = Parser::new();
let frames = p.push(&stream);
assert_eq!(frames.len(), 1, "only the intact frame survives");
assert_eq!(frames[0].payload, vec![7, 8]);
assert!(p.crc_errors >= 1, "the corrupted frame is counted");
assert!(p.resync_bytes >= 3, "garbage bytes are counted");
}
#[test]
fn parser_buffer_stays_bounded_under_garbage_floods() {
let mut p = Parser::new();
for i in 0..100u32 {
let chunk: Vec<u8> = (0..512u32)
.map(|j| ((i * 31 + j * 7) % 251) as u8)
.collect();
p.push(&chunk);
assert!(p.buf.len() < MAX_FRAME_BYTES);
}
}
}