use crate::crc::checksum;
use crate::dialect::Message;
use crate::error::{MavlinkError, Result};
pub const MAGIC_V1: u8 = 0xFE;
pub const MAGIC_V2: u8 = 0xFD;
pub const IFLAG_SIGNED: u8 = 0x01;
pub const MAX_PAYLOAD: usize = 255;
pub const SIGNATURE_LEN: usize = 13;
const HEADER_V1: usize = 6;
const HEADER_V2: usize = 10;
const CHECKSUM_LEN: usize = 2;
pub const MAX_FRAME: usize = HEADER_V2 + MAX_PAYLOAD + CHECKSUM_LEN + SIGNATURE_LEN;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Version {
V1,
V2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Header {
pub system_id: u8,
pub component_id: u8,
pub sequence: u8,
}
impl Header {
pub const fn new(system_id: u8, component_id: u8, sequence: u8) -> Self {
Header {
system_id,
component_id,
sequence,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Frame {
bytes: [u8; MAX_FRAME],
len: usize,
}
impl Frame {
const fn header_len(version: Version) -> usize {
match version {
Version::V1 => HEADER_V1,
Version::V2 => HEADER_V2,
}
}
pub fn encode_v2(header: Header, msgid: u32, payload: &[u8], crc_extra: u8) -> Result<Frame> {
Self::assemble_v2(header, msgid, payload, crc_extra, 0)
}
pub fn encode_message<M: Message>(header: Header, message: &M) -> Result<Frame> {
let mut payload = [0u8; MAX_PAYLOAD];
let len = message.encode(&mut payload);
Self::encode_v2(header, M::ID, &payload[..len], M::CRC_EXTRA)
}
pub fn decode_message<M: Message>(&self) -> Result<M> {
M::decode(self.payload())
}
pub(crate) fn assemble_v2(
header: Header,
msgid: u32,
payload: &[u8],
crc_extra: u8,
incompat_flags: u8,
) -> Result<Frame> {
if payload.len() > MAX_PAYLOAD {
return Err(MavlinkError::PayloadTooLong);
}
let plen = truncated_len(payload);
let signed = incompat_flags & IFLAG_SIGNED != 0;
let total = HEADER_V2 + plen + CHECKSUM_LEN + if signed { SIGNATURE_LEN } else { 0 };
let mut bytes = [0u8; MAX_FRAME];
bytes[0] = MAGIC_V2;
bytes[1] = plen as u8;
bytes[2] = incompat_flags;
bytes[3] = 0;
bytes[4] = header.sequence;
bytes[5] = header.system_id;
bytes[6] = header.component_id;
bytes[7] = msgid as u8;
bytes[8] = (msgid >> 8) as u8;
bytes[9] = (msgid >> 16) as u8;
bytes[HEADER_V2..HEADER_V2 + plen].copy_from_slice(&payload[..plen]);
let crc = checksum(&bytes[1..HEADER_V2 + plen], crc_extra);
bytes[HEADER_V2 + plen..HEADER_V2 + plen + CHECKSUM_LEN]
.copy_from_slice(&crc.to_le_bytes());
Ok(Frame { bytes, len: total })
}
pub fn encode_v1(header: Header, msgid: u32, payload: &[u8], crc_extra: u8) -> Result<Frame> {
if payload.len() > MAX_PAYLOAD {
return Err(MavlinkError::PayloadTooLong);
}
if msgid > 0xFF {
return Err(MavlinkError::UnknownMessage(msgid));
}
let plen = payload.len();
let total = HEADER_V1 + plen + CHECKSUM_LEN;
let mut bytes = [0u8; MAX_FRAME];
bytes[0] = MAGIC_V1;
bytes[1] = plen as u8;
bytes[2] = header.sequence;
bytes[3] = header.system_id;
bytes[4] = header.component_id;
bytes[5] = msgid as u8;
bytes[HEADER_V1..HEADER_V1 + plen].copy_from_slice(payload);
let crc = checksum(&bytes[1..HEADER_V1 + plen], crc_extra);
bytes[HEADER_V1 + plen..HEADER_V1 + plen + CHECKSUM_LEN]
.copy_from_slice(&crc.to_le_bytes());
Ok(Frame { bytes, len: total })
}
pub fn parse_with<F>(bytes: &[u8], crc_extra_for: F) -> Result<Frame>
where
F: FnOnce(u32) -> Option<u8>,
{
if bytes.is_empty() {
return Err(MavlinkError::FrameTooShort);
}
let version = match bytes[0] {
MAGIC_V1 => Version::V1,
MAGIC_V2 => Version::V2,
other => return Err(MavlinkError::BadMagic(other)),
};
let header_len = Self::header_len(version);
if bytes.len() < header_len {
return Err(MavlinkError::FrameTooShort);
}
let plen = bytes[1] as usize;
let signed = version == Version::V2 && bytes[2] & IFLAG_SIGNED != 0;
let total = header_len + plen + CHECKSUM_LEN + if signed { SIGNATURE_LEN } else { 0 };
if bytes.len() < total {
return Err(MavlinkError::Truncated);
}
let msgid = match version {
Version::V1 => u32::from(bytes[5]),
Version::V2 => {
u32::from(bytes[7]) | u32::from(bytes[8]) << 8 | u32::from(bytes[9]) << 16
}
};
let crc_extra = crc_extra_for(msgid).ok_or(MavlinkError::UnknownMessage(msgid))?;
let crc_at = header_len + plen;
let expected = checksum(&bytes[1..crc_at], crc_extra);
let found = u16::from_le_bytes([bytes[crc_at], bytes[crc_at + 1]]);
if expected != found {
return Err(MavlinkError::CrcMismatch { expected, found });
}
let mut buffer = [0u8; MAX_FRAME];
buffer[..total].copy_from_slice(&bytes[..total]);
Ok(Frame {
bytes: buffer,
len: total,
})
}
pub fn parse(bytes: &[u8], crc_extra: u8) -> Result<Frame> {
Self::parse_with(bytes, |_| Some(crc_extra))
}
pub fn version(&self) -> Version {
if self.bytes[0] == MAGIC_V2 {
Version::V2
} else {
Version::V1
}
}
pub fn sequence(&self) -> u8 {
match self.version() {
Version::V1 => self.bytes[2],
Version::V2 => self.bytes[4],
}
}
pub fn system_id(&self) -> u8 {
match self.version() {
Version::V1 => self.bytes[3],
Version::V2 => self.bytes[5],
}
}
pub fn component_id(&self) -> u8 {
match self.version() {
Version::V1 => self.bytes[4],
Version::V2 => self.bytes[6],
}
}
pub fn message_id(&self) -> u32 {
match self.version() {
Version::V1 => u32::from(self.bytes[5]),
Version::V2 => {
u32::from(self.bytes[7])
| u32::from(self.bytes[8]) << 8
| u32::from(self.bytes[9]) << 16
}
}
}
pub fn incompat_flags(&self) -> u8 {
match self.version() {
Version::V1 => 0,
Version::V2 => self.bytes[2],
}
}
pub fn is_signed(&self) -> bool {
self.version() == Version::V2 && self.incompat_flags() & IFLAG_SIGNED != 0
}
pub fn payload(&self) -> &[u8] {
let start = Self::header_len(self.version());
let plen = self.bytes[1] as usize;
&self.bytes[start..start + plen]
}
pub fn signature(&self) -> Option<&[u8; SIGNATURE_LEN]> {
if !self.is_signed() {
return None;
}
let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
self.bytes[start..start + SIGNATURE_LEN].try_into().ok()
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len]
}
pub(crate) fn signed_region(&self) -> &[u8] {
let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
&self.bytes[..start]
}
pub(crate) fn signature_mut(&mut self) -> &mut [u8; SIGNATURE_LEN] {
let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
(&mut self.bytes[start..start + SIGNATURE_LEN])
.try_into()
.expect("a signed frame reserves a full signature block")
}
}
fn truncated_len(payload: &[u8]) -> usize {
let mut len = payload.len();
while len > 1 && payload[len - 1] == 0 {
len -= 1;
}
len
}
#[cfg(test)]
mod tests {
use super::*;
const HEARTBEAT_ID: u32 = 0;
const HEARTBEAT_CRC_EXTRA: u8 = 50;
#[test]
fn a_v2_frame_round_trips_through_parse() {
let header = Header::new(1, 1, 7);
let payload = [0x06, 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0x59, 0x03];
let frame = Frame::encode_v2(header, HEARTBEAT_ID, &payload, HEARTBEAT_CRC_EXTRA).unwrap();
let parsed = Frame::parse(frame.as_bytes(), HEARTBEAT_CRC_EXTRA).unwrap();
assert_eq!(parsed.version(), Version::V2);
assert_eq!(parsed.message_id(), HEARTBEAT_ID);
assert_eq!(parsed.system_id(), 1);
assert_eq!(parsed.sequence(), 7);
assert_eq!(parsed.payload(), &payload);
}
#[test]
fn a_v1_frame_round_trips_through_parse() {
let header = Header::new(1, 1, 0);
let payload = [0x06, 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0x59, 0x03];
let frame = Frame::encode_v1(header, HEARTBEAT_ID, &payload, HEARTBEAT_CRC_EXTRA).unwrap();
let parsed = Frame::parse(frame.as_bytes(), HEARTBEAT_CRC_EXTRA).unwrap();
assert_eq!(parsed.version(), Version::V1);
assert_eq!(parsed.message_id(), HEARTBEAT_ID);
assert_eq!(parsed.payload(), &payload);
}
#[test]
fn the_v2_header_is_laid_out_as_the_spec_requires() {
let header = Header::new(0x2A, 0xBE, 0x10);
let frame = Frame::encode_v2(header, 0x0A0B0C, &[1, 2, 3], 0).unwrap();
let bytes = frame.as_bytes();
assert_eq!(bytes[0], MAGIC_V2);
assert_eq!(bytes[1], 3); assert_eq!(bytes[2], 0); assert_eq!(bytes[3], 0); assert_eq!(bytes[4], 0x10); assert_eq!(bytes[5], 0x2A); assert_eq!(bytes[6], 0xBE); assert_eq!(&bytes[7..10], &[0x0C, 0x0B, 0x0A]); }
#[test]
fn trailing_zero_bytes_are_truncated_but_the_first_is_kept() {
let header = Header::new(1, 1, 0);
let frame = Frame::encode_v2(header, 0, &[0, 0, 0, 0], 50).unwrap();
assert_eq!(frame.payload(), &[0]);
let frame = Frame::encode_v2(header, 0, &[1, 0, 2, 0, 0], 50).unwrap();
assert_eq!(frame.payload(), &[1, 0, 2]);
}
#[test]
fn a_corrupt_checksum_is_rejected() {
let header = Header::new(1, 1, 0);
let frame = Frame::encode_v2(header, 0, &[1, 2, 3], 50).unwrap();
let mut bytes = frame.as_bytes().to_vec();
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
assert!(matches!(
Frame::parse(&bytes, 50),
Err(MavlinkError::CrcMismatch { .. })
));
}
#[test]
fn the_wrong_crc_extra_is_rejected() {
let header = Header::new(1, 1, 0);
let frame = Frame::encode_v2(header, 0, &[1, 2, 3], 50).unwrap();
assert!(matches!(
Frame::parse(frame.as_bytes(), 51),
Err(MavlinkError::CrcMismatch { .. })
));
}
#[test]
fn an_unknown_message_is_rejected() {
let header = Header::new(1, 1, 0);
let frame = Frame::encode_v2(header, 999, &[1, 2, 3], 50).unwrap();
assert_eq!(
Frame::parse_with(frame.as_bytes(), |_| None),
Err(MavlinkError::UnknownMessage(999))
);
}
#[test]
fn a_truncated_frame_is_rejected() {
let header = Header::new(1, 1, 0);
let frame = Frame::encode_v2(header, 0, &[1, 2, 3, 4, 5], 50).unwrap();
let bytes = frame.as_bytes();
assert_eq!(
Frame::parse(&bytes[..bytes.len() - 1], 50),
Err(MavlinkError::Truncated)
);
}
#[test]
fn an_unrecognized_start_marker_is_rejected() {
assert_eq!(
Frame::parse(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], 50),
Err(MavlinkError::BadMagic(0x00))
);
}
}