use std::io::Cursor;
use thiserror::Error;
pub use rust_mavlink::dialects::ardupilotmega::MavMessage;
pub use rust_mavlink::{MavHeader, MavlinkVersion, Message};
pub use rust_mavlink::dialects::ardupilotmega;
#[derive(Debug, Error)]
pub enum MavlinkError {
#[error("failed to read MAVLink v2 frame: {0}")]
Read(String),
#[error("failed to write MAVLink v2 frame: {0}")]
Write(String),
}
pub fn parse_v2(bytes: &[u8]) -> Result<(MavHeader, MavMessage), MavlinkError> {
let mut reader = rust_mavlink::peek_reader::PeekReader::new(Cursor::new(bytes));
rust_mavlink::read_v2_msg::<MavMessage, _>(&mut reader)
.map_err(|e| MavlinkError::Read(e.to_string()))
}
pub fn parse_v1(bytes: &[u8]) -> Result<(MavHeader, MavMessage), MavlinkError> {
let mut reader = rust_mavlink::peek_reader::PeekReader::new(Cursor::new(bytes));
rust_mavlink::read_v1_msg::<MavMessage, _>(&mut reader)
.map_err(|e| MavlinkError::Read(e.to_string()))
}
pub fn parse_any(bytes: &[u8]) -> Result<(MavHeader, MavMessage), MavlinkError> {
match bytes.first() {
Some(&0xFD) => parse_v2(bytes),
Some(&0xFE) => parse_v1(bytes),
Some(other) => Err(MavlinkError::Read(format!(
"unknown MAVLink start-of-frame byte 0x{other:02X}"
))),
None => Err(MavlinkError::Read("empty MAVLink frame".to_string())),
}
}
#[must_use]
pub fn message_id_from_name(name: &str) -> Option<u32> {
<MavMessage as rust_mavlink::Message>::message_id_from_name(name)
}
const IFLAG_SIGNED: u8 = 0x01;
pub fn decode_exact_frame(frame: &[u8]) -> Result<MavMessage, MavlinkError> {
let bad = |why: &str| MavlinkError::Read(why.to_string());
let (version, header_len, msg_id, signature_len) = match frame.first() {
Some(&0xFD) => {
if frame.len() < 10 {
return Err(bad("truncated MAVLink v2 header"));
}
let incompat = frame[2];
if incompat & !IFLAG_SIGNED != 0 {
return Err(bad("unsupported MAVLink v2 incompat flag"));
}
let signature = if incompat & IFLAG_SIGNED != 0 { 13 } else { 0 };
let id = u32::from_le_bytes([frame[7], frame[8], frame[9], 0]);
(MavlinkVersion::V2, 10usize, id, signature)
}
Some(&0xFE) => {
if frame.len() < 6 {
return Err(bad("truncated MAVLink v1 header"));
}
(MavlinkVersion::V1, 6usize, u32::from(frame[5]), 0usize)
}
Some(_) => return Err(bad("unknown MAVLink start-of-frame byte")),
None => return Err(bad("empty MAVLink frame")),
};
let payload_len = frame[1] as usize;
let crc_at = header_len + payload_len;
if frame.len() != crc_at + 2 + signature_len {
return Err(bad("declared MAVLink length does not match the frame"));
}
let Some(template) = <MavMessage as rust_mavlink::Message>::default_message_from_id(msg_id)
else {
return Err(bad("message id is not in the dialect"));
};
let mut scratch = [0u8; 255];
let base_len = rust_mavlink::Message::ser(&template, MavlinkVersion::V1, &mut scratch);
let max_len = match version {
MavlinkVersion::V2 => v2_extended_len(msg_id).map_or(base_len, |ext| ext.max(base_len)),
MavlinkVersion::V1 => base_len,
};
if payload_len > max_len {
return Err(bad("payload is longer than the message"));
}
let mut crc = X25_INIT;
for &b in &frame[1..crc_at] {
crc = x25_accumulate(b, crc);
}
crc = x25_accumulate(
<MavMessage as rust_mavlink::Message>::extra_crc(msg_id),
crc,
);
if frame[crc_at..crc_at + 2] != crc.to_le_bytes() {
return Err(bad("MAVLink checksum mismatch"));
}
<MavMessage as rust_mavlink::Message>::parse(version, msg_id, &frame[header_len..crc_at])
.map_err(|e| MavlinkError::Read(e.to_string()))
}
fn v2_extended_len(msg_id: u32) -> Option<usize> {
match msg_id {
100 => Some(34),
_ => None,
}
}
pub fn frame_checksum_ok(frame: &[u8]) -> bool {
let (header_len, msg_id) = match frame.first() {
Some(&0xFD) if frame.len() >= 10 => (
10usize,
u32::from_le_bytes([frame[7], frame[8], frame[9], 0]),
),
Some(&0xFE) if frame.len() >= 6 => (6usize, u32::from(frame[5])),
_ => return false,
};
let crc_at = header_len + frame[1] as usize;
if frame.len() < crc_at + 2 {
return false;
}
if <MavMessage as rust_mavlink::Message>::default_message_from_id(msg_id).is_none() {
return false;
}
let mut crc = X25_INIT;
for &b in &frame[1..crc_at] {
crc = x25_accumulate(b, crc);
}
crc = x25_accumulate(
<MavMessage as rust_mavlink::Message>::extra_crc(msg_id),
crc,
);
frame[crc_at..crc_at + 2] == crc.to_le_bytes()
}
pub fn serialize_v2(header: MavHeader, msg: &MavMessage) -> Result<Vec<u8>, MavlinkError> {
let mut buf = Vec::new();
rust_mavlink::write_v2_msg(&mut buf, header, msg)
.map_err(|e| MavlinkError::Write(e.to_string()))?;
Ok(buf)
}
pub fn serialize_v1(header: MavHeader, msg: &MavMessage) -> Result<Vec<u8>, MavlinkError> {
let mut buf = Vec::new();
rust_mavlink::write_v1_msg(&mut buf, header, msg)
.map_err(|e| MavlinkError::Write(e.to_string()))?;
Ok(buf)
}
pub const MSG_ID_COMMAND_LONG: u32 = 76;
pub const COMMAND_LONG_CRC_EXTRA: u8 = 152;
#[allow(clippy::too_many_arguments)]
pub fn build_command_long_v2(
header: MavHeader,
command: u16,
target_system: u8,
target_component: u8,
params: [f32; 7],
) -> Vec<u8> {
let mut payload = Vec::with_capacity(33);
for p in params {
payload.extend_from_slice(&p.to_le_bytes());
}
payload.extend_from_slice(&command.to_le_bytes());
payload.push(target_system);
payload.push(target_component);
payload.push(0u8);
truncate_trailing_zeros(&mut payload);
let mut frame = Vec::with_capacity(10 + payload.len() + 2);
frame.push(0xFD); frame.push(payload.len() as u8); frame.push(0x00); frame.push(0x00); frame.push(header.sequence);
frame.push(header.system_id);
frame.push(header.component_id);
frame.push((MSG_ID_COMMAND_LONG & 0xFF) as u8);
frame.push(((MSG_ID_COMMAND_LONG >> 8) & 0xFF) as u8);
frame.push(((MSG_ID_COMMAND_LONG >> 16) & 0xFF) as u8);
frame.extend_from_slice(&payload);
let mut crc = X25_INIT;
for &b in &frame[1..] {
crc = x25_accumulate(b, crc);
}
crc = x25_accumulate(COMMAND_LONG_CRC_EXTRA, crc);
frame.push((crc & 0xFF) as u8);
frame.push((crc >> 8) as u8);
frame
}
fn truncate_trailing_zeros(payload: &mut Vec<u8>) {
while payload.len() > 1 && *payload.last().unwrap() == 0 {
payload.pop();
}
}
const X25_INIT: u16 = 0xFFFF;
fn x25_accumulate(byte: u8, crc: u16) -> u16 {
let mut tmp = byte ^ (crc & 0xFF) as u8;
tmp ^= tmp << 4;
let tmp = tmp as u16;
(crc >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4)
}
pub const MSG_ID_SET_POSITION_TARGET_LOCAL_NED: u32 = 84;
pub const MSG_ID_SET_POSITION_TARGET_GLOBAL_INT: u32 = 86;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetpointKind {
LocalNed,
GlobalInt,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GuidedSetpoint {
pub kind: SetpointKind,
pub coordinate_frame: u8,
pub type_mask: u16,
pub x: f64,
pub y: f64,
pub z: f64,
pub vx: f32,
pub vy: f32,
pub vz: f32,
pub afx: f32,
pub afy: f32,
pub afz: f32,
pub yaw: f32,
pub yaw_rate: f32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetpointError(pub String);
impl std::fmt::Display for SetpointError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for SetpointError {}
const POSITION_TARGET_TYPEMASK_KNOWN_BITS: u16 = 0x0FFF;
const TYPEMASK_X_IGNORE: u16 = 1;
const TYPEMASK_Y_IGNORE: u16 = 2;
const TYPEMASK_Z_IGNORE: u16 = 4;
const TYPEMASK_VX_IGNORE: u16 = 8;
const TYPEMASK_VY_IGNORE: u16 = 16;
const TYPEMASK_VZ_IGNORE: u16 = 32;
const TYPEMASK_AX_IGNORE: u16 = 64;
const TYPEMASK_AY_IGNORE: u16 = 128;
const TYPEMASK_AZ_IGNORE: u16 = 256;
const TYPEMASK_YAW_IGNORE: u16 = 1024;
const TYPEMASK_YAW_RATE_IGNORE: u16 = 2048;
const LOCAL_FRAMES: &[u8] = &[1, 7, 8, 9, 12];
const GLOBAL_FRAMES: &[u8] = &[0, 3, 5, 6];
impl GuidedSetpoint {
pub fn validate(&self) -> Result<(), SetpointError> {
if self.type_mask & !POSITION_TARGET_TYPEMASK_KNOWN_BITS != 0 {
return Err(SetpointError(format!(
"type_mask 0x{:04X} sets a bit outside the defined position-target field",
self.type_mask
)));
}
let valid_frames = match self.kind {
SetpointKind::LocalNed => LOCAL_FRAMES,
SetpointKind::GlobalInt => GLOBAL_FRAMES,
};
if !valid_frames.contains(&self.coordinate_frame) {
return Err(SetpointError(format!(
"coordinate_frame {} is not valid for this setpoint kind",
self.coordinate_frame
)));
}
let active = |ignore_bit: u16| self.type_mask & ignore_bit == 0;
let check_f64 = |v: f64, name: &str| -> Result<(), SetpointError> {
if v.is_finite() {
Ok(())
} else {
Err(SetpointError(format!("{name} must be a finite number")))
}
};
let check_f64_narrows_to_f32 = |v: f64, name: &str| -> Result<(), SetpointError> {
if v.is_finite() && (v as f32).is_finite() {
Ok(())
} else {
Err(SetpointError(format!("{name} must be a finite number")))
}
};
let check_f32 = |v: f32, name: &str| -> Result<(), SetpointError> {
if v.is_finite() {
Ok(())
} else {
Err(SetpointError(format!("{name} must be a finite number")))
}
};
let position_narrows_to_f32 = matches!(self.kind, SetpointKind::LocalNed);
if active(TYPEMASK_X_IGNORE) {
if position_narrows_to_f32 {
check_f64_narrows_to_f32(self.x, "x")?;
} else {
check_f64(self.x, "x")?;
}
}
if active(TYPEMASK_Y_IGNORE) {
if position_narrows_to_f32 {
check_f64_narrows_to_f32(self.y, "y")?;
} else {
check_f64(self.y, "y")?;
}
}
if active(TYPEMASK_Z_IGNORE) {
check_f64_narrows_to_f32(self.z, "z")?;
}
if active(TYPEMASK_VX_IGNORE) {
check_f32(self.vx, "vx")?;
}
if active(TYPEMASK_VY_IGNORE) {
check_f32(self.vy, "vy")?;
}
if active(TYPEMASK_VZ_IGNORE) {
check_f32(self.vz, "vz")?;
}
if active(TYPEMASK_AX_IGNORE) {
check_f32(self.afx, "afx")?;
}
if active(TYPEMASK_AY_IGNORE) {
check_f32(self.afy, "afy")?;
}
if active(TYPEMASK_AZ_IGNORE) {
check_f32(self.afz, "afz")?;
}
if active(TYPEMASK_YAW_IGNORE) {
check_f32(self.yaw, "yaw")?;
}
if active(TYPEMASK_YAW_RATE_IGNORE) {
check_f32(self.yaw_rate, "yaw_rate")?;
}
Ok(())
}
pub fn build_message(
&self,
target_system: u8,
target_component: u8,
) -> Result<MavMessage, SetpointError> {
self.validate()?;
let type_mask = ardupilotmega::PositionTargetTypemask::from_bits_truncate(self.type_mask);
Ok(match self.kind {
SetpointKind::LocalNed => MavMessage::SET_POSITION_TARGET_LOCAL_NED(
ardupilotmega::SET_POSITION_TARGET_LOCAL_NED_DATA {
time_boot_ms: 0,
x: self.x as f32,
y: self.y as f32,
z: self.z as f32,
vx: self.vx,
vy: self.vy,
vz: self.vz,
afx: self.afx,
afy: self.afy,
afz: self.afz,
yaw: self.yaw,
yaw_rate: self.yaw_rate,
type_mask,
target_system,
target_component,
coordinate_frame: mav_frame_from_u8(self.coordinate_frame),
},
),
SetpointKind::GlobalInt => {
MavMessage::SET_POSITION_TARGET_GLOBAL_INT(
ardupilotmega::SET_POSITION_TARGET_GLOBAL_INT_DATA {
time_boot_ms: 0,
lat_int: clamp_to_i32(self.x),
lon_int: clamp_to_i32(self.y),
alt: self.z as f32,
vx: self.vx,
vy: self.vy,
vz: self.vz,
afx: self.afx,
afy: self.afy,
afz: self.afz,
yaw: self.yaw,
yaw_rate: self.yaw_rate,
type_mask,
target_system,
target_component,
coordinate_frame: mav_frame_from_u8(self.coordinate_frame),
},
)
}
})
}
}
fn clamp_to_i32(v: f64) -> i32 {
if v.is_nan() {
return 0;
}
v.clamp(i32::MIN as f64, i32::MAX as f64) as i32
}
#[allow(deprecated)]
fn mav_frame_from_u8(frame: u8) -> ardupilotmega::MavFrame {
use ardupilotmega::MavFrame::*;
match frame {
0 => MAV_FRAME_GLOBAL,
1 => MAV_FRAME_LOCAL_NED,
3 => MAV_FRAME_GLOBAL_RELATIVE_ALT,
5 => MAV_FRAME_GLOBAL_INT,
6 => MAV_FRAME_GLOBAL_RELATIVE_ALT_INT,
7 => MAV_FRAME_LOCAL_OFFSET_NED,
8 => MAV_FRAME_BODY_NED,
9 => MAV_FRAME_BODY_OFFSET_NED,
12 => MAV_FRAME_BODY_FRD,
_ => ardupilotmega::MavFrame::DEFAULT,
}
}
pub const MSG_ID_SET_ATTITUDE_TARGET: u32 = 82;
pub const ATTITUDE_TARGET_CRC_EXTRA: u8 = 49;
const ATTMASK_BODY_ROLL_RATE_IGNORE: u8 = 1;
const ATTMASK_BODY_PITCH_RATE_IGNORE: u8 = 2;
const ATTMASK_BODY_YAW_RATE_IGNORE: u8 = 4;
const ATTMASK_THRUST_BODY_SET: u8 = 32;
const ATTMASK_THROTTLE_IGNORE: u8 = 64;
const ATTMASK_ATTITUDE_IGNORE: u8 = 128;
const ATTITUDE_TARGET_TYPEMASK_KNOWN_BITS: u8 = ATTMASK_BODY_ROLL_RATE_IGNORE
| ATTMASK_BODY_PITCH_RATE_IGNORE
| ATTMASK_BODY_YAW_RATE_IGNORE
| ATTMASK_THRUST_BODY_SET
| ATTMASK_THROTTLE_IGNORE
| ATTMASK_ATTITUDE_IGNORE;
const QUAT_NORM_TOLERANCE: f32 = 1e-3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AttitudeSetpoint {
pub type_mask: u8,
pub q: [f32; 4],
pub body_roll_rate: f32,
pub body_pitch_rate: f32,
pub body_yaw_rate: f32,
pub thrust: f32,
}
impl AttitudeSetpoint {
pub fn validate(&self) -> Result<(), SetpointError> {
if self.type_mask & !ATTITUDE_TARGET_TYPEMASK_KNOWN_BITS != 0 {
return Err(SetpointError(format!(
"type_mask 0x{:02X} sets a bit outside the defined attitude-target field",
self.type_mask
)));
}
let active = |bit: u8| self.type_mask & bit == 0;
let check_f32 = |v: f32, name: &str| -> Result<(), SetpointError> {
if v.is_finite() {
Ok(())
} else {
Err(SetpointError(format!("{name} must be a finite number")))
}
};
if active(ATTMASK_ATTITUDE_IGNORE) {
for (i, c) in self.q.iter().enumerate() {
check_f32(*c, &format!("q[{i}]"))?;
}
let norm = (self.q[0] * self.q[0]
+ self.q[1] * self.q[1]
+ self.q[2] * self.q[2]
+ self.q[3] * self.q[3])
.sqrt();
if (norm - 1.0).abs() > QUAT_NORM_TOLERANCE {
return Err(SetpointError(format!(
"q must be a unit quaternion (norm {norm})"
)));
}
}
if active(ATTMASK_BODY_ROLL_RATE_IGNORE) {
check_f32(self.body_roll_rate, "body_roll_rate")?;
}
if active(ATTMASK_BODY_PITCH_RATE_IGNORE) {
check_f32(self.body_pitch_rate, "body_pitch_rate")?;
}
if active(ATTMASK_BODY_YAW_RATE_IGNORE) {
check_f32(self.body_yaw_rate, "body_yaw_rate")?;
}
if active(ATTMASK_THROTTLE_IGNORE) {
check_f32(self.thrust, "thrust")?;
if !(-1.0..=1.0).contains(&self.thrust) {
return Err(SetpointError(format!(
"thrust {} must be in -1..=1",
self.thrust
)));
}
}
Ok(())
}
pub fn build_message(
&self,
target_system: u8,
target_component: u8,
) -> Result<MavMessage, SetpointError> {
self.validate()?;
let type_mask = ardupilotmega::AttitudeTargetTypemask::from_bits_truncate(self.type_mask);
Ok(MavMessage::SET_ATTITUDE_TARGET(
ardupilotmega::SET_ATTITUDE_TARGET_DATA {
time_boot_ms: 0,
q: self.q,
body_roll_rate: self.body_roll_rate,
body_pitch_rate: self.body_pitch_rate,
body_yaw_rate: self.body_yaw_rate,
thrust: self.thrust,
target_system,
target_component,
type_mask,
},
))
}
}
pub const MSG_ID_TUNNEL: u32 = 385;
pub const TUNNEL_CRC_EXTRA: u8 = 147;
pub const TUNNEL_MAX_PAYLOAD: usize = 128;
pub const TUNNEL_RESERVED_PAYLOAD_TYPE_MAX: u16 = 32767;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TunnelError(pub String);
impl std::fmt::Display for TunnelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for TunnelError {}
pub fn build_tunnel_v2(
header: MavHeader,
payload_type: u16,
target_system: u8,
target_component: u8,
payload: &[u8],
) -> Result<Vec<u8>, TunnelError> {
if payload_type <= TUNNEL_RESERVED_PAYLOAD_TYPE_MAX {
return Err(TunnelError(format!(
"payload_type {payload_type} must be a private type greater than {TUNNEL_RESERVED_PAYLOAD_TYPE_MAX}"
)));
}
if payload.len() > TUNNEL_MAX_PAYLOAD {
return Err(TunnelError(format!(
"payload is {} bytes, exceeds the {TUNNEL_MAX_PAYLOAD}-byte TUNNEL limit",
payload.len()
)));
}
let mut body = Vec::with_capacity(5 + TUNNEL_MAX_PAYLOAD);
body.extend_from_slice(&payload_type.to_le_bytes());
body.push(target_system);
body.push(target_component);
body.push(payload.len() as u8);
body.extend_from_slice(payload);
body.resize(5 + TUNNEL_MAX_PAYLOAD, 0);
truncate_trailing_zeros(&mut body);
let mut frame = Vec::with_capacity(10 + body.len() + 2);
frame.push(0xFD); frame.push(body.len() as u8); frame.push(0x00); frame.push(0x00); frame.push(header.sequence);
frame.push(header.system_id);
frame.push(header.component_id);
frame.push((MSG_ID_TUNNEL & 0xFF) as u8);
frame.push(((MSG_ID_TUNNEL >> 8) & 0xFF) as u8);
frame.push(((MSG_ID_TUNNEL >> 16) & 0xFF) as u8);
frame.extend_from_slice(&body);
let mut crc = X25_INIT;
for &b in &frame[1..] {
crc = x25_accumulate(b, crc);
}
crc = x25_accumulate(TUNNEL_CRC_EXTRA, crc);
frame.push((crc & 0xFF) as u8);
frame.push((crc >> 8) as u8);
Ok(frame)
}
pub fn tunnel_payload_type(frame: &[u8]) -> Option<u16> {
if frame.first() != Some(&0xFD) || frame.len() < 10 {
return None;
}
let mut id = [0u8; 4];
id[..3].copy_from_slice(&frame[7..10]);
if u32::from_le_bytes(id) != MSG_ID_TUNNEL {
return None;
}
if frame.len() < 12 {
return None;
}
Some(u16::from_le_bytes([frame[10], frame[11]]))
}
#[must_use]
pub fn tunnel_payload(frame: &[u8]) -> Option<Vec<u8>> {
tunnel_payload_type(frame)?;
let body_len = frame[1] as usize;
let body_end = 10usize.checked_add(body_len)?;
if frame.len() < body_end + 2 {
return None;
}
let body = &frame[10..body_end];
let declared = body.get(4).copied().unwrap_or(0) as usize;
let present = body.get(5..).unwrap_or(&[]);
if present.len() > declared {
return None;
}
let mut payload = present.to_vec();
payload.resize(declared, 0);
Some(payload)
}
#[cfg(test)]
mod tests {
use super::*;
use rust_mavlink::dialects::ardupilotmega::HEARTBEAT_DATA;
fn heartbeat() -> MavMessage {
MavMessage::HEARTBEAT(HEARTBEAT_DATA {
custom_mode: 0,
mavtype: ardupilotmega::MavType::MAV_TYPE_QUADROTOR,
autopilot: ardupilotmega::MavAutopilot::MAV_AUTOPILOT_ARDUPILOTMEGA,
base_mode: ardupilotmega::MavModeFlag::empty(),
system_status: ardupilotmega::MavState::MAV_STATE_STANDBY,
mavlink_version: 3,
})
}
#[test]
fn heartbeat_round_trips_through_v2_frame() {
let header = MavHeader {
system_id: 1,
component_id: 1,
sequence: 42,
};
let frame = serialize_v2(header, &heartbeat()).unwrap();
assert_eq!(frame[0], 0xFD);
let (got_header, got_msg) = parse_v2(&frame).unwrap();
assert_eq!(got_header.system_id, 1);
assert_eq!(got_header.component_id, 1);
assert_eq!(got_header.sequence, 42);
match got_msg {
MavMessage::HEARTBEAT(hb) => {
assert_eq!(hb.mavtype, ardupilotmega::MavType::MAV_TYPE_QUADROTOR);
assert_eq!(hb.mavlink_version, 3);
}
other => panic!("expected HEARTBEAT, got {other:?}"),
}
}
#[test]
fn truncated_frame_is_a_read_error() {
let header = MavHeader::default();
let frame = serialize_v2(header, &heartbeat()).unwrap();
assert!(matches!(
parse_v2(&frame[..frame.len() - 3]),
Err(MavlinkError::Read(_))
));
}
#[test]
fn heartbeat_round_trips_through_v1_frame() {
let header = MavHeader {
system_id: 1,
component_id: 1,
sequence: 7,
};
let frame = serialize_v1(header, &heartbeat()).unwrap();
assert_eq!(frame[0], 0xFE);
let (got_header, got_msg) = parse_v1(&frame).unwrap();
assert_eq!(got_header.system_id, 1);
assert_eq!(got_header.component_id, 1);
assert_eq!(got_header.sequence, 7);
assert!(matches!(got_msg, MavMessage::HEARTBEAT(_)));
}
#[test]
fn outbound_frame_is_length_prefixed_and_decode_recovers_it() {
use crate::frame::{decode_len, encode_frame, HEADER_SIZE, MAVLINK_MAX_FRAME};
let header = MavHeader {
system_id: 1,
component_id: 1,
sequence: 5,
};
let raw = serialize_v2(header, &heartbeat()).unwrap();
let framed = encode_frame(&raw, MAVLINK_MAX_FRAME).unwrap();
let prefix: [u8; HEADER_SIZE] = framed[..HEADER_SIZE].try_into().unwrap();
let len = decode_len(prefix, MAVLINK_MAX_FRAME, false).unwrap();
assert_eq!(len, raw.len());
assert_eq!(&framed[HEADER_SIZE..], raw.as_slice());
let (_, msg) = parse_any(&framed[HEADER_SIZE..]).unwrap();
assert!(matches!(msg, MavMessage::HEARTBEAT(_)));
}
#[test]
fn parse_any_dispatches_on_start_of_frame_byte() {
let header = MavHeader {
system_id: 1,
component_id: 1,
sequence: 3,
};
let v2_frame = serialize_v2(header, &heartbeat()).unwrap();
let v1_frame = serialize_v1(header, &heartbeat()).unwrap();
assert_eq!(v2_frame[0], 0xFD);
assert_eq!(v1_frame[0], 0xFE);
let (_, m2) = parse_any(&v2_frame).unwrap();
let (_, m1) = parse_any(&v1_frame).unwrap();
assert!(matches!(m2, MavMessage::HEARTBEAT(_)));
assert!(matches!(m1, MavMessage::HEARTBEAT(_)));
assert!(matches!(
parse_any(&[0x00, 0x01, 0x02]),
Err(MavlinkError::Read(_))
));
assert!(matches!(parse_any(&[]), Err(MavlinkError::Read(_))));
}
#[test]
fn command_long_builder_matches_the_golden_set_camera_source_frame() {
let header = MavHeader {
system_id: 255,
component_id: 190,
sequence: 0,
};
let frame = build_command_long_v2(header, 534, 1, 1, [0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let golden = hex_to_bytes(
"fd20000000ffbe4c000000000000000000400000000000000000000000000000000000000000160201019b45",
);
assert_eq!(
frame, golden,
"the built frame must be byte-identical to the golden SET_CAMERA_SOURCE frame"
);
}
#[test]
fn command_long_builder_truncates_trailing_zeros_and_is_44_bytes() {
let header = MavHeader {
system_id: 255,
component_id: 190,
sequence: 0,
};
let frame = build_command_long_v2(header, 534, 1, 1, [0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
assert_eq!(frame.len(), 44);
assert_eq!(frame[0], 0xFD); assert_eq!(frame[1], 32); assert_eq!(frame[2], 0x00); assert_eq!(frame[3], 0x00); assert_eq!(frame[7], 76);
assert_eq!(frame[8], 0);
assert_eq!(frame[9], 0);
}
#[test]
fn command_long_builder_crc_matches_rust_mavlink_for_a_named_id() {
use rust_mavlink::dialects::ardupilotmega::{MavCmd, COMMAND_LONG_DATA};
let header = MavHeader {
system_id: 1,
component_id: 191,
sequence: 7,
};
let typed = MavMessage::COMMAND_LONG(COMMAND_LONG_DATA {
target_system: 1,
target_component: 1,
command: MavCmd::MAV_CMD_COMPONENT_ARM_DISARM,
confirmation: 0,
param1: 1.0,
param2: 0.0,
param3: 0.0,
param4: 0.0,
param5: 0.0,
param6: 0.0,
param7: 0.0,
});
let typed_frame = serialize_v2(header, &typed).unwrap();
let raw_frame =
build_command_long_v2(header, 400, 1, 1, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
assert_eq!(
&raw_frame[..raw_frame.len() - 2],
&typed_frame[..typed_frame.len() - 2],
"header + payload must match the typed serializer for the same command"
);
}
#[test]
fn command_long_builder_zero_truncation_keeps_one_byte() {
let mut payload = vec![0u8, 0, 0, 0];
truncate_trailing_zeros(&mut payload);
assert_eq!(payload, vec![0u8]);
}
fn hex_to_bytes(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn velocity_setpoint(kind: SetpointKind, frame: u8) -> GuidedSetpoint {
let ignore = TYPEMASK_X_IGNORE
| TYPEMASK_Y_IGNORE
| TYPEMASK_Z_IGNORE
| TYPEMASK_AX_IGNORE
| TYPEMASK_AY_IGNORE
| TYPEMASK_AZ_IGNORE
| TYPEMASK_YAW_IGNORE
| TYPEMASK_YAW_RATE_IGNORE;
GuidedSetpoint {
kind,
coordinate_frame: frame,
type_mask: ignore,
x: 0.0,
y: 0.0,
z: 0.0,
vx: 2.5,
vy: -1.0,
vz: 0.5,
afx: 0.0,
afy: 0.0,
afz: 0.0,
yaw: 0.0,
yaw_rate: 0.0,
}
}
#[test]
#[allow(deprecated)]
fn local_ned_setpoint_builds_and_round_trips() {
let sp = velocity_setpoint(SetpointKind::LocalNed, 8); let msg = sp.build_message(1, 1).expect("valid setpoint builds");
let header = MavHeader {
system_id: 1,
component_id: 191,
sequence: 0,
};
let frame = serialize_v2(header, &msg).expect("serialize succeeds");
assert_eq!(frame[7], 84, "message id low byte is 84 (LOCAL_NED)");
assert_eq!(frame[8], 0);
assert_eq!(frame[9], 0);
let (_h, decoded) = parse_v2(&frame).expect("decode succeeds");
match decoded {
MavMessage::SET_POSITION_TARGET_LOCAL_NED(d) => {
assert_eq!(d.vx, 2.5);
assert_eq!(d.vy, -1.0);
assert_eq!(d.vz, 0.5);
assert_eq!(d.target_system, 1);
assert_eq!(d.target_component, 1);
assert_eq!(
d.coordinate_frame,
ardupilotmega::MavFrame::MAV_FRAME_BODY_NED
);
assert!(d.type_mask.contains(
ardupilotmega::PositionTargetTypemask::POSITION_TARGET_TYPEMASK_X_IGNORE
));
}
other => panic!("expected SET_POSITION_TARGET_LOCAL_NED, got {other:?}"),
}
}
#[test]
#[allow(deprecated)]
fn global_int_setpoint_builds_and_round_trips() {
let sp = velocity_setpoint(SetpointKind::GlobalInt, 6); let msg = sp.build_message(1, 1).expect("valid setpoint builds");
let header = MavHeader {
system_id: 1,
component_id: 191,
sequence: 7,
};
let frame = serialize_v2(header, &msg).expect("serialize succeeds");
assert_eq!(frame[7], 86, "message id low byte is 86 (GLOBAL_INT)");
assert_eq!(frame[8], 0);
assert_eq!(frame[9], 0);
let (_h, decoded) = parse_v2(&frame).expect("decode succeeds");
match decoded {
MavMessage::SET_POSITION_TARGET_GLOBAL_INT(d) => {
assert_eq!(d.vx, 2.5);
assert_eq!(d.vy, -1.0);
assert_eq!(d.vz, 0.5);
assert_eq!(
d.coordinate_frame,
ardupilotmega::MavFrame::MAV_FRAME_GLOBAL_RELATIVE_ALT_INT
);
}
other => panic!("expected SET_POSITION_TARGET_GLOBAL_INT, got {other:?}"),
}
}
#[test]
fn global_int_carries_scaled_lat_lon_and_clamps() {
let mut sp = velocity_setpoint(SetpointKind::GlobalInt, 5); sp.type_mask &= !(TYPEMASK_X_IGNORE | TYPEMASK_Y_IGNORE | TYPEMASK_Z_IGNORE);
sp.x = 37.422_408 * 1e7; sp.y = -122.084_270 * 1e7; sp.z = 30.0; let msg = sp.build_message(1, 1).expect("valid setpoint builds");
match msg {
MavMessage::SET_POSITION_TARGET_GLOBAL_INT(d) => {
assert_eq!(d.lat_int, 374_224_080);
assert_eq!(d.lon_int, -1_220_842_700);
assert_eq!(d.alt, 30.0);
}
other => panic!("expected GLOBAL_INT, got {other:?}"),
}
let mut huge = velocity_setpoint(SetpointKind::GlobalInt, 5);
huge.type_mask &= !TYPEMASK_X_IGNORE;
huge.x = 5e10;
match huge.build_message(1, 1).unwrap() {
MavMessage::SET_POSITION_TARGET_GLOBAL_INT(d) => assert_eq!(d.lat_int, i32::MAX),
other => panic!("expected GLOBAL_INT, got {other:?}"),
}
}
#[test]
fn setpoint_rejects_a_nan_on_an_active_axis() {
let mut sp = velocity_setpoint(SetpointKind::LocalNed, 1);
sp.vx = f32::NAN;
let err = sp.build_message(1, 1).unwrap_err();
assert_eq!(err.0, "vx must be a finite number");
let mut inf = velocity_setpoint(SetpointKind::LocalNed, 1);
inf.vz = f32::INFINITY;
assert_eq!(
inf.build_message(1, 1).unwrap_err().0,
"vz must be a finite number"
);
}
#[test]
fn setpoint_rejects_a_finite_f64_over_f32_range_on_an_active_position_axis() {
let over = (f32::MAX as f64) * 2.0; assert!(over.is_finite());
assert!((over as f32).is_infinite());
let mut sp = velocity_setpoint(SetpointKind::LocalNed, 1);
sp.type_mask &= !TYPEMASK_X_IGNORE; sp.x = over;
let err = sp.build_message(1, 1).unwrap_err();
assert_eq!(err.0, "x must be a finite number");
let mut alt = velocity_setpoint(SetpointKind::GlobalInt, 5);
alt.type_mask &= !TYPEMASK_Z_IGNORE;
alt.z = over;
assert_eq!(
alt.build_message(1, 1).unwrap_err().0,
"z must be a finite number"
);
}
#[test]
fn setpoint_ignores_a_nan_on_an_ignored_axis() {
let mut sp = velocity_setpoint(SetpointKind::GlobalInt, 5);
sp.x = f64::NAN; let msg = sp
.build_message(1, 1)
.expect("ignored-axis NaN is accepted");
match msg {
MavMessage::SET_POSITION_TARGET_GLOBAL_INT(d) => assert_eq!(d.lat_int, 0),
other => panic!("expected GLOBAL_INT, got {other:?}"),
}
}
#[test]
fn setpoint_rejects_an_unknown_type_mask_bit() {
let mut sp = velocity_setpoint(SetpointKind::LocalNed, 1);
sp.type_mask |= 0x8000; let err = sp.build_message(1, 1).unwrap_err();
assert!(
err.0.contains("outside the defined position-target field"),
"got: {}",
err.0
);
}
#[test]
fn setpoint_rejects_a_frame_wrong_for_the_kind() {
let mut local = velocity_setpoint(SetpointKind::LocalNed, 1);
local.coordinate_frame = 5; assert!(local
.build_message(1, 1)
.unwrap_err()
.0
.contains("not valid for this setpoint kind"));
let mut global = velocity_setpoint(SetpointKind::GlobalInt, 5);
global.coordinate_frame = 1; assert!(global
.build_message(1, 1)
.unwrap_err()
.0
.contains("not valid for this setpoint kind"));
}
fn body_rate_command() -> AttitudeSetpoint {
AttitudeSetpoint {
type_mask: ATTMASK_ATTITUDE_IGNORE,
q: [1.0, 0.0, 0.0, 0.0],
body_roll_rate: 0.5,
body_pitch_rate: -0.2,
body_yaw_rate: 0.1,
thrust: 0.6,
}
}
#[test]
fn attitude_setpoint_builds_and_round_trips() {
let sp = body_rate_command();
let msg = sp.build_message(1, 1).expect("valid setpoint builds");
let header = MavHeader {
system_id: 1,
component_id: 191,
sequence: 0,
};
let frame = serialize_v2(header, &msg).expect("serialize succeeds");
assert_eq!(
frame[7], 82,
"message id low byte is 82 (SET_ATTITUDE_TARGET)"
);
assert_eq!(frame[8], 0);
assert_eq!(frame[9], 0);
let (_h, decoded) = parse_v2(&frame).expect("decode succeeds");
match decoded {
MavMessage::SET_ATTITUDE_TARGET(d) => {
assert_eq!(d.body_roll_rate, 0.5);
assert_eq!(d.body_pitch_rate, -0.2);
assert_eq!(d.body_yaw_rate, 0.1);
assert_eq!(d.thrust, 0.6);
assert_eq!(d.target_system, 1);
assert_eq!(d.target_component, 1);
assert!(d.type_mask.contains(
ardupilotmega::AttitudeTargetTypemask::ATTITUDE_TARGET_TYPEMASK_ATTITUDE_IGNORE
));
}
other => panic!("expected SET_ATTITUDE_TARGET, got {other:?}"),
}
}
#[test]
fn attitude_setpoint_commands_a_quaternion_and_round_trips() {
let mut sp = body_rate_command();
sp.type_mask &= !ATTMASK_ATTITUDE_IGNORE;
sp.q = [0.923_879_5, 0.0, 0.0, 0.382_683_43]; let msg = sp.build_message(1, 1).expect("valid setpoint builds");
let header = MavHeader::default();
let frame = serialize_v2(header, &msg).expect("serialize succeeds");
let (_h, decoded) = parse_v2(&frame).expect("decode succeeds");
match decoded {
MavMessage::SET_ATTITUDE_TARGET(d) => {
assert_eq!(d.q, sp.q);
assert!(!d.type_mask.contains(
ardupilotmega::AttitudeTargetTypemask::ATTITUDE_TARGET_TYPEMASK_ATTITUDE_IGNORE
));
}
other => panic!("expected SET_ATTITUDE_TARGET, got {other:?}"),
}
}
#[test]
fn attitude_setpoint_rejects_a_non_unit_quaternion() {
let mut sp = body_rate_command();
sp.type_mask &= !ATTMASK_ATTITUDE_IGNORE;
sp.q = [1.0, 0.0, 0.0, 0.0];
sp.q[3] = 0.5; let err = sp.build_message(1, 1).unwrap_err();
assert!(err.0.contains("unit quaternion"), "got: {}", err.0);
}
#[test]
fn attitude_setpoint_rejects_a_nan_on_an_active_rate_axis() {
let mut sp = body_rate_command();
sp.body_pitch_rate = f32::NAN;
let err = sp.build_message(1, 1).unwrap_err();
assert_eq!(err.0, "body_pitch_rate must be a finite number");
}
#[test]
fn attitude_setpoint_rejects_thrust_out_of_range() {
let mut sp = body_rate_command();
sp.thrust = 1.5;
let err = sp.build_message(1, 1).unwrap_err();
assert!(err.0.contains("must be in -1..=1"), "got: {}", err.0);
sp.thrust = -0.9;
assert!(sp.build_message(1, 1).is_ok());
}
#[test]
fn attitude_setpoint_ignores_a_nan_on_an_ignored_axis() {
let mut sp = body_rate_command();
sp.body_roll_rate = f32::NAN; sp.type_mask |= ATTMASK_BODY_ROLL_RATE_IGNORE;
assert!(
sp.build_message(1, 1).is_ok(),
"ignored-axis NaN is accepted"
);
}
#[test]
fn attitude_setpoint_rejects_an_unknown_type_mask_bit() {
let mut sp = body_rate_command();
sp.type_mask |= 0x10; let err = sp.build_message(1, 1).unwrap_err();
assert!(
err.0.contains("outside the defined attitude-target field"),
"got: {}",
err.0
);
}
#[test]
fn opponent_attitude_changes_not_rejected_but_crc_const_is_49() {
assert_eq!(MSG_ID_SET_ATTITUDE_TARGET, 82);
assert_eq!(ATTITUDE_TARGET_CRC_EXTRA, 49);
assert_eq!(ardupilotmega::SET_ATTITUDE_TARGET_DATA::ENCODED_LEN, 39);
}
#[test]
fn tunnel_frame_round_trips_the_payload_and_private_type() {
let header = MavHeader {
system_id: 42,
component_id: 191,
sequence: 3,
};
let payload_type: u16 = 40001;
let app_payload = b"hello-from-a-plugin";
let frame =
build_tunnel_v2(header, payload_type, 1, 1, app_payload).expect("private type builds");
let mut id = [0u8; 4];
id[..3].copy_from_slice(&frame[7..10]);
assert_eq!(u32::from_le_bytes(id), MSG_ID_TUNNEL);
assert_eq!(tunnel_payload_type(&frame), Some(payload_type));
assert_eq!(frame[14] as usize, app_payload.len());
assert_eq!(&frame[15..15 + app_payload.len()], app_payload);
}
#[test]
fn tunnel_payload_recovers_the_exact_bytes_including_trailing_zeros() {
let header = MavHeader::default();
for app in [
&b"chunk-header-and-body"[..],
&b""[..],
&[0xAD, 0x03, 1, 0, 9, 0, 0, 0, 0, 0, 0, 0][..], &vec![0u8; TUNNEL_MAX_PAYLOAD][..], ] {
let frame = build_tunnel_v2(header, 44480, 7, 1, app).expect("builds");
assert_eq!(tunnel_payload(&frame).as_deref(), Some(app));
}
let hb = serialize_v2(header, &heartbeat()).unwrap();
assert_eq!(tunnel_payload(&hb), None);
}
#[test]
fn tunnel_rejects_a_registered_payload_type() {
let header = MavHeader::default();
let err = build_tunnel_v2(header, TUNNEL_RESERVED_PAYLOAD_TYPE_MAX, 1, 1, b"x")
.expect_err("a registered type is refused");
assert!(err.0.contains("private type"));
assert!(build_tunnel_v2(header, TUNNEL_RESERVED_PAYLOAD_TYPE_MAX + 1, 1, 1, b"x").is_ok());
}
#[test]
fn tunnel_rejects_an_oversized_payload() {
let header = MavHeader::default();
let big = vec![0xABu8; TUNNEL_MAX_PAYLOAD + 1];
let err = build_tunnel_v2(header, 40002, 1, 1, &big)
.expect_err("an oversized payload is refused");
assert!(err.0.contains("exceeds"));
let exact = vec![0xCDu8; TUNNEL_MAX_PAYLOAD];
assert!(build_tunnel_v2(header, 40002, 1, 1, &exact).is_ok());
}
#[test]
fn tunnel_classifier_ignores_a_non_tunnel_frame() {
let header = MavHeader::default();
let frame = serialize_v2(header, &heartbeat()).unwrap();
assert_eq!(tunnel_payload_type(&frame), None);
}
fn v2_frame_with(msg_id: u32, payload: &[u8], incompat: u8) -> Vec<u8> {
let id = msg_id.to_le_bytes();
let mut frame = vec![
0xFD,
payload.len() as u8,
incompat,
0,
0,
1,
1,
id[0],
id[1],
id[2],
];
frame.extend_from_slice(payload);
let mut crc = X25_INIT;
for &b in &frame[1..] {
crc = x25_accumulate(b, crc);
}
crc = x25_accumulate(
<MavMessage as rust_mavlink::Message>::extra_crc(msg_id),
crc,
);
frame.extend_from_slice(&crc.to_le_bytes());
frame
}
#[test]
fn exact_decode_accepts_whole_v1_and_v2_frames() {
let header = MavHeader::default();
let v2 = serialize_v2(header, &heartbeat()).unwrap();
assert!(matches!(
decode_exact_frame(&v2),
Ok(MavMessage::HEARTBEAT(_))
));
let v1 = serialize_v1(header, &heartbeat()).unwrap();
assert!(matches!(
decode_exact_frame(&v1),
Ok(MavMessage::HEARTBEAT(_))
));
assert!(decode_exact_frame(&v2_frame_with(0, &[0; 9], 0)).is_ok());
}
#[test]
fn exact_decode_refuses_an_unsupported_incompat_flag() {
let frame = v2_frame_with(0, &[0; 9], 0x02);
assert!(decode_exact_frame(&frame).is_err());
}
#[test]
fn exact_decode_refuses_a_bad_checksum_without_resyncing() {
let odometry = <MavMessage as rust_mavlink::Message>::default_message_from_id(331).unwrap();
let inner = serialize_v2(MavHeader::default(), &odometry).unwrap();
let mut outer = v2_frame_with(0, &[0; 9], 0);
let last = outer.len() - 1;
outer[last] ^= 0xFF;
outer.extend_from_slice(&inner);
assert!(parse_v2(&outer).is_ok(), "the stream reader resyncs");
assert!(decode_exact_frame(&outer).is_err());
let mut bad_crc = v2_frame_with(0, &[0; 9], 0);
bad_crc[last] ^= 0xFF;
assert!(decode_exact_frame(&bad_crc).is_err());
}
#[test]
fn exact_decode_refuses_unknown_ids_overlong_payloads_and_length_mismatch() {
assert!(decode_exact_frame(&v2_frame_with(0x00AB_CDEF, &[1, 2, 3], 0)).is_err());
assert!(decode_exact_frame(&v2_frame_with(0, &[0; 10], 0)).is_err());
let whole = v2_frame_with(0, &[0; 9], 0);
assert!(decode_exact_frame(&whole[..whole.len() - 1]).is_err());
let mut trailing = whole.clone();
trailing.push(0);
assert!(decode_exact_frame(&trailing).is_err());
}
#[test]
fn exact_decode_accepts_v2_extension_length_only_up_to_extended_size() {
assert!(matches!(
decode_exact_frame(&v2_frame_with(100, &[0; 34], 0)),
Ok(MavMessage::OPTICAL_FLOW(_))
));
assert!(decode_exact_frame(&v2_frame_with(100, &[0; 35], 0)).is_err());
}
#[test]
fn message_names_resolve_to_dialect_ids() {
assert_eq!(message_id_from_name("HEARTBEAT"), Some(0));
assert_eq!(message_id_from_name("GLOBAL_POSITION_INT"), Some(33));
assert_eq!(message_id_from_name("ODOMETRY"), Some(331));
assert_eq!(message_id_from_name("NOT_A_MESSAGE"), None);
}
}