Skip to main content

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}