pub const AUX_HEADER_LEN: usize = 6;
pub const AUX_MAGIC: [u8; 2] = [0xAD, 0x02];
pub const AUX_VERSION: u8 = 0x01;
pub const AUX_MAX_PAYLOAD: usize = 1200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum AuxChannel {
Mavlink = 1,
Status = 2,
Identity = 3,
Request = 4,
Response = 5,
LinkFeedback = 6,
ConfigTunnel = 7,
AppStream = 8,
AppCommand = 9,
}
impl AuxChannel {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
1 => Some(Self::Mavlink),
2 => Some(Self::Status),
3 => Some(Self::Identity),
4 => Some(Self::Request),
5 => Some(Self::Response),
6 => Some(Self::LinkFeedback),
7 => Some(Self::ConfigTunnel),
8 => Some(Self::AppStream),
9 => Some(Self::AppCommand),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuxDecodeError {
TooShort,
BadMagic,
UnsupportedVersion(u8),
UnknownChannel(u8),
LengthMismatch { declared: usize, actual: usize },
TooLong(usize),
}
pub fn encode(channel: AuxChannel, payload: &[u8]) -> Option<Vec<u8>> {
if payload.len() > AUX_MAX_PAYLOAD {
return None;
}
let mut out = Vec::with_capacity(AUX_HEADER_LEN + payload.len());
out.extend_from_slice(&AUX_MAGIC);
out.push(AUX_VERSION);
out.push(channel as u8);
out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
out.extend_from_slice(payload);
Some(out)
}
pub fn decode(frame: &[u8]) -> Result<(AuxChannel, &[u8]), AuxDecodeError> {
if frame.len() < AUX_HEADER_LEN {
return Err(AuxDecodeError::TooShort);
}
if frame[0..2] != AUX_MAGIC {
return Err(AuxDecodeError::BadMagic);
}
if frame[2] != AUX_VERSION {
return Err(AuxDecodeError::UnsupportedVersion(frame[2]));
}
let channel = AuxChannel::from_u8(frame[3]).ok_or(AuxDecodeError::UnknownChannel(frame[3]))?;
let declared = u16::from_be_bytes([frame[4], frame[5]]) as usize;
if declared > AUX_MAX_PAYLOAD {
return Err(AuxDecodeError::TooLong(declared));
}
let actual = frame.len() - AUX_HEADER_LEN;
if declared != actual {
return Err(AuxDecodeError::LengthMismatch { declared, actual });
}
Ok((channel, &frame[AUX_HEADER_LEN..]))
}
pub fn frame_len(bytes: &[u8]) -> Option<usize> {
match bytes.first()? {
0xFD => {
let payload = *bytes.get(1)? as usize;
let signed = (*bytes.get(2)? & 0x01) != 0;
Some(12 + payload + if signed { 13 } else { 0 })
}
0xFE => Some(8 + *bytes.get(1)? as usize),
_ => None,
}
}
pub fn split_frames(buf: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut at = 0usize;
while at < buf.len() {
let Some(len) = frame_len(&buf[at..]) else {
break;
};
if len == 0 || at + len > buf.len() {
break;
}
out.push(&buf[at..at + len]);
at += len;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_every_channel() {
for ch in [
AuxChannel::Mavlink,
AuxChannel::Status,
AuxChannel::Identity,
AuxChannel::Request,
AuxChannel::Response,
] {
let body = b"payload bytes";
let framed = encode(ch, body).expect("within budget");
assert_eq!(framed.len(), AUX_HEADER_LEN + body.len());
let (got_ch, got) = decode(&framed).expect("decodes");
assert_eq!(got_ch, ch);
assert_eq!(got, body);
}
}
#[test]
fn round_trips_an_empty_payload() {
let framed = encode(AuxChannel::Status, &[]).unwrap();
let (ch, body) = decode(&framed).unwrap();
assert_eq!(ch, AuxChannel::Status);
assert!(body.is_empty());
}
#[test]
fn refuses_to_truncate_an_oversized_payload() {
let too_big = vec![0u8; AUX_MAX_PAYLOAD + 1];
assert!(encode(AuxChannel::Mavlink, &too_big).is_none());
assert!(encode(AuxChannel::Mavlink, &vec![0u8; AUX_MAX_PAYLOAD]).is_some());
}
#[test]
fn drops_foreign_traffic_rather_than_guessing() {
assert_eq!(decode(b"hello!").unwrap_err(), AuxDecodeError::BadMagic);
assert_eq!(decode(b"\xAD").unwrap_err(), AuxDecodeError::TooShort);
}
#[test]
fn rejects_an_unsupported_version_and_unknown_channel() {
let mut f = encode(AuxChannel::Mavlink, b"x").unwrap();
f[2] = 0x99;
assert_eq!(
decode(&f).unwrap_err(),
AuxDecodeError::UnsupportedVersion(0x99)
);
let mut f = encode(AuxChannel::Mavlink, b"x").unwrap();
f[3] = 0x7F;
assert_eq!(
decode(&f).unwrap_err(),
AuxDecodeError::UnknownChannel(0x7F)
);
}
#[test]
fn catches_a_truncated_frame_of_ours() {
let mut f = encode(AuxChannel::Mavlink, b"twelve bytes").unwrap();
f.truncate(f.len() - 3);
assert_eq!(
decode(&f).unwrap_err(),
AuxDecodeError::LengthMismatch {
declared: 12,
actual: 9
}
);
}
fn v2(payload_len: u8, signed: bool) -> Vec<u8> {
let mut f = vec![
0xFD,
payload_len,
if signed { 0x01 } else { 0x00 },
0,
0,
1,
1,
0,
0,
0,
];
f.extend(std::iter::repeat_n(0xAB, payload_len as usize));
f.extend_from_slice(&[0, 0]); if signed {
f.extend(std::iter::repeat_n(0u8, 13));
}
f
}
fn v1(payload_len: u8) -> Vec<u8> {
let mut f = vec![0xFE, payload_len, 0, 1, 1, 0];
f.extend(std::iter::repeat_n(0xCD, payload_len as usize));
f.extend_from_slice(&[0, 0]);
f
}
#[test]
fn frame_len_reads_v1_v2_and_the_signature_block() {
assert_eq!(frame_len(&v2(10, false)), Some(22));
assert_eq!(frame_len(&v2(10, true)), Some(35));
assert_eq!(frame_len(&v1(10)), Some(18));
assert_eq!(frame_len(b"\x00junk"), None);
assert_eq!(frame_len(&[]), None);
}
#[test]
fn splits_a_batch_back_into_the_frames_that_went_in() {
let frames = vec![v2(5, false), v1(9), v2(0, false), v2(3, true)];
let mut batch = Vec::new();
for f in &frames {
batch.extend_from_slice(f);
}
let out = split_frames(&batch);
assert_eq!(out.len(), frames.len());
for (got, want) in out.iter().zip(frames.iter()) {
assert_eq!(*got, want.as_slice());
}
}
#[test]
fn a_single_frame_batch_is_unchanged() {
let f = v2(12, false);
assert_eq!(split_frames(&f), vec![f.as_slice()]);
}
#[test]
fn drops_a_truncated_tail_and_keeps_the_whole_frames() {
let good = v2(8, false);
let mut batch = good.clone();
let partial = v2(20, false);
batch.extend_from_slice(&partial[..6]);
let out = split_frames(&batch);
assert_eq!(out, vec![good.as_slice()]);
}
#[test]
fn stops_at_an_unrecognisable_byte_rather_than_scanning_on() {
let mut batch = v2(4, false);
batch.push(0x00);
batch.extend_from_slice(&v1(4));
assert_eq!(split_frames(&batch).len(), 1);
}
#[test]
fn an_empty_buffer_yields_no_frames() {
assert!(split_frames(&[]).is_empty());
}
#[test]
fn channel_numbers_are_pinned() {
assert_eq!(AuxChannel::Mavlink as u8, 1);
assert_eq!(AuxChannel::Status as u8, 2);
assert_eq!(AuxChannel::Identity as u8, 3);
assert_eq!(AuxChannel::Request as u8, 4);
assert_eq!(AuxChannel::Response as u8, 5);
assert_eq!(AuxChannel::LinkFeedback as u8, 6);
assert_eq!(AuxChannel::ConfigTunnel as u8, 7);
assert_eq!(AuxChannel::from_u8(1), Some(AuxChannel::Mavlink));
assert_eq!(AuxChannel::from_u8(7), Some(AuxChannel::ConfigTunnel));
assert_eq!(AuxChannel::from_u8(8), Some(AuxChannel::AppStream));
assert_eq!(AuxChannel::from_u8(9), Some(AuxChannel::AppCommand));
assert_eq!(AuxChannel::from_u8(0), None);
assert_eq!(AuxChannel::from_u8(10), None);
}
}