pamoja_mavlink/frame.rs
1//! The MAVLink frame on the wire: the v1 and v2 packet layouts, with the checksum
2//! verified on the way in so a corrupt frame never reaches the application.
3
4use crate::crc::checksum;
5use crate::dialect::Message;
6use crate::error::{MavlinkError, Result};
7
8/// The start marker of a MAVLink v1 frame.
9pub const MAGIC_V1: u8 = 0xFE;
10/// The start marker of a MAVLink v2 frame.
11pub const MAGIC_V2: u8 = 0xFD;
12
13/// The incompatibility-flag bit that marks a v2 frame as signed.
14pub const IFLAG_SIGNED: u8 = 0x01;
15
16/// The largest payload, in bytes, a frame can carry.
17pub const MAX_PAYLOAD: usize = 255;
18/// The length of a v2 signature block: a link id, a timestamp, and the signature.
19pub const SIGNATURE_LEN: usize = 13;
20
21// Header lengths, magic byte included.
22const HEADER_V1: usize = 6;
23const HEADER_V2: usize = 10;
24const CHECKSUM_LEN: usize = 2;
25
26/// The largest a complete frame can be: a v2 header, the largest payload, the checksum,
27/// and a signature.
28pub const MAX_FRAME: usize = HEADER_V2 + MAX_PAYLOAD + CHECKSUM_LEN + SIGNATURE_LEN;
29
30/// Which MAVLink wire format a frame uses.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum Version {
33 /// The original 6-byte-header format, start marker `0xFE`.
34 V1,
35 /// The current format, start marker `0xFD`: a 24-bit message id, flag bytes, and
36 /// optional signing.
37 V2,
38}
39
40/// The addressing fields a sender stamps on every frame.
41///
42/// A frame says who sent it (a system and a component) and where it sits in that
43/// sender's stream (a sequence number that wraps at 256), which lets a receiver detect
44/// dropped frames.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct Header {
47 /// The sending system's id, such as a vehicle's.
48 pub system_id: u8,
49 /// The sending component's id within the system, such as an autopilot or a camera.
50 pub component_id: u8,
51 /// The sender's per-link sequence number for this frame.
52 pub sequence: u8,
53}
54
55impl Header {
56 /// Creates a header for a system, a component, and a sequence number.
57 ///
58 /// # Arguments
59 ///
60 /// * `system_id` - the sending system's id.
61 /// * `component_id` - the sending component's id.
62 /// * `sequence` - the sequence number to stamp on the frame.
63 ///
64 /// # Returns
65 ///
66 /// The header.
67 pub const fn new(system_id: u8, component_id: u8, sequence: u8) -> Self {
68 Header {
69 system_id,
70 component_id,
71 sequence,
72 }
73 }
74}
75
76/// An encoded MAVLink frame, held in a fixed buffer so encoding never allocates.
77///
78/// [`encode_v2`](Frame::encode_v2) and [`encode_v1`](Frame::encode_v1) build a frame to
79/// send; [`parse`](Frame::parse) reads one received, verifying its checksum so a frame
80/// mangled in transit is rejected rather than misread. Signing a v2 frame is a separate
81/// step in [`signing`](crate::signing), since it needs a key and the timestamp the
82/// sender chooses.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub struct Frame {
85 bytes: [u8; MAX_FRAME],
86 len: usize,
87}
88
89impl Frame {
90 // The byte offset of the payload, by version.
91 const fn header_len(version: Version) -> usize {
92 match version {
93 Version::V1 => HEADER_V1,
94 Version::V2 => HEADER_V2,
95 }
96 }
97
98 /// Builds a v2 frame for a message, computing and appending the checksum.
99 ///
100 /// Trailing zero bytes of the payload are dropped before transmission as MAVLink 2
101 /// requires, except that the first payload byte is always kept.
102 ///
103 /// # Arguments
104 ///
105 /// * `header` - the addressing fields to stamp on the frame.
106 /// * `msgid` - the 24-bit message id.
107 /// * `payload` - the serialized message payload, full length.
108 /// * `crc_extra` - the `CRC_EXTRA` seed for `msgid`.
109 ///
110 /// # Returns
111 ///
112 /// The frame ready to send.
113 ///
114 /// # Errors
115 ///
116 /// Returns [`MavlinkError::PayloadTooLong`] if `payload` is longer than
117 /// [`MAX_PAYLOAD`].
118 pub fn encode_v2(header: Header, msgid: u32, payload: &[u8], crc_extra: u8) -> Result<Frame> {
119 Self::assemble_v2(header, msgid, payload, crc_extra, 0)
120 }
121
122 /// Builds a v2 frame carrying a typed message.
123 ///
124 /// The message knows its own id and `CRC_EXTRA` and serializes itself, so a caller
125 /// hands over the message rather than a payload buffer and two constants that have to
126 /// agree with it. The payload is built on the stack, so this allocates nothing.
127 ///
128 /// # Arguments
129 ///
130 /// * `header` - the addressing fields to stamp on the frame.
131 /// * `message` - the message to carry.
132 ///
133 /// # Returns
134 ///
135 /// The frame ready to send.
136 ///
137 /// # Errors
138 ///
139 /// Returns [`MavlinkError::PayloadTooLong`] if the message does not fit a frame.
140 pub fn encode_message<M: Message>(header: Header, message: &M) -> Result<Frame> {
141 let mut payload = [0u8; MAX_PAYLOAD];
142 let len = message.encode(&mut payload);
143 Self::encode_v2(header, M::ID, &payload[..len], M::CRC_EXTRA)
144 }
145
146 /// Reads the payload back as a typed message.
147 ///
148 /// # Returns
149 ///
150 /// The decoded message.
151 ///
152 /// # Errors
153 ///
154 /// Returns [`MavlinkError::PayloadTooLong`] if the payload is longer than the message
155 /// describes.
156 pub fn decode_message<M: Message>(&self) -> Result<M> {
157 M::decode(self.payload())
158 }
159
160 // Builds a v2 frame with the given incompatibility flags. Signing sets IFLAG_SIGNED
161 // here so the flag is covered by the checksum, then fills the signature block.
162 pub(crate) fn assemble_v2(
163 header: Header,
164 msgid: u32,
165 payload: &[u8],
166 crc_extra: u8,
167 incompat_flags: u8,
168 ) -> Result<Frame> {
169 if payload.len() > MAX_PAYLOAD {
170 return Err(MavlinkError::PayloadTooLong);
171 }
172 let plen = truncated_len(payload);
173 let signed = incompat_flags & IFLAG_SIGNED != 0;
174 let total = HEADER_V2 + plen + CHECKSUM_LEN + if signed { SIGNATURE_LEN } else { 0 };
175
176 let mut bytes = [0u8; MAX_FRAME];
177 bytes[0] = MAGIC_V2;
178 bytes[1] = plen as u8;
179 bytes[2] = incompat_flags;
180 bytes[3] = 0;
181 bytes[4] = header.sequence;
182 bytes[5] = header.system_id;
183 bytes[6] = header.component_id;
184 bytes[7] = msgid as u8;
185 bytes[8] = (msgid >> 8) as u8;
186 bytes[9] = (msgid >> 16) as u8;
187 bytes[HEADER_V2..HEADER_V2 + plen].copy_from_slice(&payload[..plen]);
188
189 let crc = checksum(&bytes[1..HEADER_V2 + plen], crc_extra);
190 bytes[HEADER_V2 + plen..HEADER_V2 + plen + CHECKSUM_LEN]
191 .copy_from_slice(&crc.to_le_bytes());
192
193 Ok(Frame { bytes, len: total })
194 }
195
196 /// Builds a v1 frame for a message, computing and appending the checksum.
197 ///
198 /// # Arguments
199 ///
200 /// * `header` - the addressing fields to stamp on the frame.
201 /// * `msgid` - the message id, which must fit a single byte for v1.
202 /// * `payload` - the serialized message payload.
203 /// * `crc_extra` - the `CRC_EXTRA` seed for `msgid`.
204 ///
205 /// # Returns
206 ///
207 /// The frame ready to send.
208 ///
209 /// # Errors
210 ///
211 /// Returns [`MavlinkError::PayloadTooLong`] if `payload` is longer than [`MAX_PAYLOAD`],
212 /// or [`MavlinkError::UnknownMessage`] if `msgid` does not fit a single byte.
213 pub fn encode_v1(header: Header, msgid: u32, payload: &[u8], crc_extra: u8) -> Result<Frame> {
214 if payload.len() > MAX_PAYLOAD {
215 return Err(MavlinkError::PayloadTooLong);
216 }
217 if msgid > 0xFF {
218 return Err(MavlinkError::UnknownMessage(msgid));
219 }
220 let plen = payload.len();
221 let total = HEADER_V1 + plen + CHECKSUM_LEN;
222
223 let mut bytes = [0u8; MAX_FRAME];
224 bytes[0] = MAGIC_V1;
225 bytes[1] = plen as u8;
226 bytes[2] = header.sequence;
227 bytes[3] = header.system_id;
228 bytes[4] = header.component_id;
229 bytes[5] = msgid as u8;
230 bytes[HEADER_V1..HEADER_V1 + plen].copy_from_slice(payload);
231
232 let crc = checksum(&bytes[1..HEADER_V1 + plen], crc_extra);
233 bytes[HEADER_V1 + plen..HEADER_V1 + plen + CHECKSUM_LEN]
234 .copy_from_slice(&crc.to_le_bytes());
235
236 Ok(Frame { bytes, len: total })
237 }
238
239 /// Parses a received frame, verifying its checksum.
240 ///
241 /// The message id is read from the header and its `CRC_EXTRA` is resolved through
242 /// `crc_extra_for`, so a frame whose message is unknown is rejected rather than
243 /// accepted unchecked. The signature of a signed v2 frame is preserved but not
244 /// verified here; pass the parsed frame to a [`Verifier`](crate::signing::Verifier).
245 ///
246 /// # Arguments
247 ///
248 /// * `bytes` - the raw frame as it came off the link, from the start marker onward.
249 /// * `crc_extra_for` - resolves a message id to its `CRC_EXTRA`, or `None` if unknown.
250 ///
251 /// # Returns
252 ///
253 /// The validated frame.
254 ///
255 /// # Errors
256 ///
257 /// Returns [`MavlinkError::FrameTooShort`] if the bytes cannot hold a header,
258 /// [`MavlinkError::BadMagic`] if the start marker is unrecognized,
259 /// [`MavlinkError::Truncated`] if the frame is shorter than its length field
260 /// promises, [`MavlinkError::UnknownMessage`] if the message id has no `CRC_EXTRA`,
261 /// or [`MavlinkError::CrcMismatch`] if the checksum does not verify.
262 pub fn parse_with<F>(bytes: &[u8], crc_extra_for: F) -> Result<Frame>
263 where
264 F: FnOnce(u32) -> Option<u8>,
265 {
266 if bytes.is_empty() {
267 return Err(MavlinkError::FrameTooShort);
268 }
269 let version = match bytes[0] {
270 MAGIC_V1 => Version::V1,
271 MAGIC_V2 => Version::V2,
272 other => return Err(MavlinkError::BadMagic(other)),
273 };
274 let header_len = Self::header_len(version);
275 if bytes.len() < header_len {
276 return Err(MavlinkError::FrameTooShort);
277 }
278 let plen = bytes[1] as usize;
279 let signed = version == Version::V2 && bytes[2] & IFLAG_SIGNED != 0;
280 let total = header_len + plen + CHECKSUM_LEN + if signed { SIGNATURE_LEN } else { 0 };
281 if bytes.len() < total {
282 return Err(MavlinkError::Truncated);
283 }
284
285 let msgid = match version {
286 Version::V1 => u32::from(bytes[5]),
287 Version::V2 => {
288 u32::from(bytes[7]) | u32::from(bytes[8]) << 8 | u32::from(bytes[9]) << 16
289 }
290 };
291 let crc_extra = crc_extra_for(msgid).ok_or(MavlinkError::UnknownMessage(msgid))?;
292
293 let crc_at = header_len + plen;
294 let expected = checksum(&bytes[1..crc_at], crc_extra);
295 let found = u16::from_le_bytes([bytes[crc_at], bytes[crc_at + 1]]);
296 if expected != found {
297 return Err(MavlinkError::CrcMismatch { expected, found });
298 }
299
300 let mut buffer = [0u8; MAX_FRAME];
301 buffer[..total].copy_from_slice(&bytes[..total]);
302 Ok(Frame {
303 bytes: buffer,
304 len: total,
305 })
306 }
307
308 /// Parses a received frame whose `CRC_EXTRA` is already known.
309 ///
310 /// # Arguments
311 ///
312 /// * `bytes` - the raw frame as it came off the link.
313 /// * `crc_extra` - the `CRC_EXTRA` for the frame's message id.
314 ///
315 /// # Returns
316 ///
317 /// The validated frame.
318 ///
319 /// # Errors
320 ///
321 /// As [`parse_with`](Frame::parse_with), except the message id is always resolvable.
322 pub fn parse(bytes: &[u8], crc_extra: u8) -> Result<Frame> {
323 Self::parse_with(bytes, |_| Some(crc_extra))
324 }
325
326 /// Returns which wire format the frame uses.
327 ///
328 /// # Returns
329 ///
330 /// [`Version::V1`] or [`Version::V2`].
331 pub fn version(&self) -> Version {
332 if self.bytes[0] == MAGIC_V2 {
333 Version::V2
334 } else {
335 Version::V1
336 }
337 }
338
339 /// Returns the sequence number the sender stamped on the frame.
340 ///
341 /// # Returns
342 ///
343 /// The sequence number.
344 pub fn sequence(&self) -> u8 {
345 match self.version() {
346 Version::V1 => self.bytes[2],
347 Version::V2 => self.bytes[4],
348 }
349 }
350
351 /// Returns the sending system's id.
352 ///
353 /// # Returns
354 ///
355 /// The system id.
356 pub fn system_id(&self) -> u8 {
357 match self.version() {
358 Version::V1 => self.bytes[3],
359 Version::V2 => self.bytes[5],
360 }
361 }
362
363 /// Returns the sending component's id.
364 ///
365 /// # Returns
366 ///
367 /// The component id.
368 pub fn component_id(&self) -> u8 {
369 match self.version() {
370 Version::V1 => self.bytes[4],
371 Version::V2 => self.bytes[6],
372 }
373 }
374
375 /// Returns the message id the frame carries.
376 ///
377 /// # Returns
378 ///
379 /// The message id: 0-255 for v1, up to 24 bits for v2.
380 pub fn message_id(&self) -> u32 {
381 match self.version() {
382 Version::V1 => u32::from(self.bytes[5]),
383 Version::V2 => {
384 u32::from(self.bytes[7])
385 | u32::from(self.bytes[8]) << 8
386 | u32::from(self.bytes[9]) << 16
387 }
388 }
389 }
390
391 /// Returns the incompatibility flags of a v2 frame, or `0` for a v1 frame.
392 ///
393 /// # Returns
394 ///
395 /// The incompatibility flags byte.
396 pub fn incompat_flags(&self) -> u8 {
397 match self.version() {
398 Version::V1 => 0,
399 Version::V2 => self.bytes[2],
400 }
401 }
402
403 /// Reports whether the frame is a signed v2 frame.
404 ///
405 /// # Returns
406 ///
407 /// `true` if the frame carries a signature.
408 pub fn is_signed(&self) -> bool {
409 self.version() == Version::V2 && self.incompat_flags() & IFLAG_SIGNED != 0
410 }
411
412 /// Returns the payload as it was carried, after any MAVLink 2 truncation.
413 ///
414 /// # Returns
415 ///
416 /// The payload bytes.
417 pub fn payload(&self) -> &[u8] {
418 let start = Self::header_len(self.version());
419 let plen = self.bytes[1] as usize;
420 &self.bytes[start..start + plen]
421 }
422
423 /// Returns the 13-byte signature block of a signed frame.
424 ///
425 /// # Returns
426 ///
427 /// The signature block, or [`None`] if the frame is not signed.
428 pub fn signature(&self) -> Option<&[u8; SIGNATURE_LEN]> {
429 if !self.is_signed() {
430 return None;
431 }
432 let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
433 self.bytes[start..start + SIGNATURE_LEN].try_into().ok()
434 }
435
436 /// Returns the whole frame, ready for the link.
437 ///
438 /// # Returns
439 ///
440 /// The frame as a byte slice, signature included if present.
441 pub fn as_bytes(&self) -> &[u8] {
442 &self.bytes[..self.len]
443 }
444
445 // The bytes a signature is computed over, with the message-specific CRC: the header
446 // (start marker included), the payload, and the two checksum bytes.
447 pub(crate) fn signed_region(&self) -> &[u8] {
448 let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
449 &self.bytes[..start]
450 }
451
452 // Writes the signature block of a signed frame, after the checksum.
453 pub(crate) fn signature_mut(&mut self) -> &mut [u8; SIGNATURE_LEN] {
454 let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
455 (&mut self.bytes[start..start + SIGNATURE_LEN])
456 .try_into()
457 .expect("a signed frame reserves a full signature block")
458 }
459}
460
461// The payload length after dropping trailing zero bytes. A non-empty payload keeps at
462// least one byte, as MAVLink 2 requires; an empty payload stays empty. The `len > 1`
463// guard supplies both, and never exceeds the payload length.
464fn truncated_len(payload: &[u8]) -> usize {
465 let mut len = payload.len();
466 while len > 1 && payload[len - 1] == 0 {
467 len -= 1;
468 }
469 len
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 // HEARTBEAT, the frame every MAVLink node emits, used to anchor the layout.
477 const HEARTBEAT_ID: u32 = 0;
478 const HEARTBEAT_CRC_EXTRA: u8 = 50;
479
480 #[test]
481 fn a_v2_frame_round_trips_through_parse() {
482 let header = Header::new(1, 1, 7);
483 let payload = [0x06, 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0x59, 0x03];
484 let frame = Frame::encode_v2(header, HEARTBEAT_ID, &payload, HEARTBEAT_CRC_EXTRA).unwrap();
485 let parsed = Frame::parse(frame.as_bytes(), HEARTBEAT_CRC_EXTRA).unwrap();
486 assert_eq!(parsed.version(), Version::V2);
487 assert_eq!(parsed.message_id(), HEARTBEAT_ID);
488 assert_eq!(parsed.system_id(), 1);
489 assert_eq!(parsed.sequence(), 7);
490 assert_eq!(parsed.payload(), &payload);
491 }
492
493 #[test]
494 fn a_v1_frame_round_trips_through_parse() {
495 let header = Header::new(1, 1, 0);
496 let payload = [0x06, 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0x59, 0x03];
497 let frame = Frame::encode_v1(header, HEARTBEAT_ID, &payload, HEARTBEAT_CRC_EXTRA).unwrap();
498 let parsed = Frame::parse(frame.as_bytes(), HEARTBEAT_CRC_EXTRA).unwrap();
499 assert_eq!(parsed.version(), Version::V1);
500 assert_eq!(parsed.message_id(), HEARTBEAT_ID);
501 assert_eq!(parsed.payload(), &payload);
502 }
503
504 #[test]
505 fn the_v2_header_is_laid_out_as_the_spec_requires() {
506 let header = Header::new(0x2A, 0xBE, 0x10);
507 let frame = Frame::encode_v2(header, 0x0A0B0C, &[1, 2, 3], 0).unwrap();
508 let bytes = frame.as_bytes();
509 assert_eq!(bytes[0], MAGIC_V2);
510 assert_eq!(bytes[1], 3); // payload length
511 assert_eq!(bytes[2], 0); // incompat flags
512 assert_eq!(bytes[3], 0); // compat flags
513 assert_eq!(bytes[4], 0x10); // sequence
514 assert_eq!(bytes[5], 0x2A); // system id
515 assert_eq!(bytes[6], 0xBE); // component id
516 assert_eq!(&bytes[7..10], &[0x0C, 0x0B, 0x0A]); // msgid, little-endian
517 }
518
519 #[test]
520 fn trailing_zero_bytes_are_truncated_but_the_first_is_kept() {
521 let header = Header::new(1, 1, 0);
522 // A payload of all zeros truncates to a single byte, never to nothing.
523 let frame = Frame::encode_v2(header, 0, &[0, 0, 0, 0], 50).unwrap();
524 assert_eq!(frame.payload(), &[0]);
525
526 // Trailing zeros are dropped; an interior zero is preserved.
527 let frame = Frame::encode_v2(header, 0, &[1, 0, 2, 0, 0], 50).unwrap();
528 assert_eq!(frame.payload(), &[1, 0, 2]);
529 }
530
531 #[test]
532 fn a_corrupt_checksum_is_rejected() {
533 let header = Header::new(1, 1, 0);
534 let frame = Frame::encode_v2(header, 0, &[1, 2, 3], 50).unwrap();
535 let mut bytes = frame.as_bytes().to_vec();
536 let last = bytes.len() - 1;
537 bytes[last] ^= 0xFF;
538 assert!(matches!(
539 Frame::parse(&bytes, 50),
540 Err(MavlinkError::CrcMismatch { .. })
541 ));
542 }
543
544 #[test]
545 fn the_wrong_crc_extra_is_rejected() {
546 // A receiver that disagrees about the message shape folds in a different seed and
547 // rejects the frame, which is the whole point of CRC_EXTRA.
548 let header = Header::new(1, 1, 0);
549 let frame = Frame::encode_v2(header, 0, &[1, 2, 3], 50).unwrap();
550 assert!(matches!(
551 Frame::parse(frame.as_bytes(), 51),
552 Err(MavlinkError::CrcMismatch { .. })
553 ));
554 }
555
556 #[test]
557 fn an_unknown_message_is_rejected() {
558 let header = Header::new(1, 1, 0);
559 let frame = Frame::encode_v2(header, 999, &[1, 2, 3], 50).unwrap();
560 assert_eq!(
561 Frame::parse_with(frame.as_bytes(), |_| None),
562 Err(MavlinkError::UnknownMessage(999))
563 );
564 }
565
566 #[test]
567 fn a_truncated_frame_is_rejected() {
568 let header = Header::new(1, 1, 0);
569 let frame = Frame::encode_v2(header, 0, &[1, 2, 3, 4, 5], 50).unwrap();
570 let bytes = frame.as_bytes();
571 assert_eq!(
572 Frame::parse(&bytes[..bytes.len() - 1], 50),
573 Err(MavlinkError::Truncated)
574 );
575 }
576
577 #[test]
578 fn an_unrecognized_start_marker_is_rejected() {
579 assert_eq!(
580 Frame::parse(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], 50),
581 Err(MavlinkError::BadMagic(0x00))
582 );
583 }
584}