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::UnknownIncompatFlags`] if a v2 frame sets an incompatibility flag other
260    /// than signing, [`MavlinkError::Truncated`] if the frame is shorter than its length field
261    /// promises, [`MavlinkError::UnknownMessage`] if the message id has no `CRC_EXTRA`,
262    /// or [`MavlinkError::CrcMismatch`] if the checksum does not verify.
263    pub fn parse_with<F>(bytes: &[u8], crc_extra_for: F) -> Result<Frame>
264    where
265        F: FnOnce(u32) -> Option<u8>,
266    {
267        if bytes.is_empty() {
268            return Err(MavlinkError::FrameTooShort);
269        }
270        let version = match bytes[0] {
271            MAGIC_V1 => Version::V1,
272            MAGIC_V2 => Version::V2,
273            other => return Err(MavlinkError::BadMagic(other)),
274        };
275        let header_len = Self::header_len(version);
276        if bytes.len() < header_len {
277            return Err(MavlinkError::FrameTooShort);
278        }
279        if version == Version::V2 && bytes[2] & !IFLAG_SIGNED != 0 {
280            return Err(MavlinkError::UnknownIncompatFlags(bytes[2]));
281        }
282        let plen = bytes[1] as usize;
283        let signed = version == Version::V2 && bytes[2] & IFLAG_SIGNED != 0;
284        let total = header_len + plen + CHECKSUM_LEN + if signed { SIGNATURE_LEN } else { 0 };
285        if bytes.len() < total {
286            return Err(MavlinkError::Truncated);
287        }
288
289        let msgid = match version {
290            Version::V1 => u32::from(bytes[5]),
291            Version::V2 => {
292                u32::from(bytes[7]) | u32::from(bytes[8]) << 8 | u32::from(bytes[9]) << 16
293            }
294        };
295        let crc_extra = crc_extra_for(msgid).ok_or(MavlinkError::UnknownMessage(msgid))?;
296
297        let crc_at = header_len + plen;
298        let expected = checksum(&bytes[1..crc_at], crc_extra);
299        let found = u16::from_le_bytes([bytes[crc_at], bytes[crc_at + 1]]);
300        if expected != found {
301            return Err(MavlinkError::CrcMismatch { expected, found });
302        }
303
304        let mut buffer = [0u8; MAX_FRAME];
305        buffer[..total].copy_from_slice(&bytes[..total]);
306        Ok(Frame {
307            bytes: buffer,
308            len: total,
309        })
310    }
311
312    /// Parses a received frame whose `CRC_EXTRA` is already known.
313    ///
314    /// # Arguments
315    ///
316    /// * `bytes` - the raw frame as it came off the link.
317    /// * `crc_extra` - the `CRC_EXTRA` for the frame's message id.
318    ///
319    /// # Returns
320    ///
321    /// The validated frame.
322    ///
323    /// # Errors
324    ///
325    /// As [`parse_with`](Frame::parse_with), except the message id is always resolvable.
326    pub fn parse(bytes: &[u8], crc_extra: u8) -> Result<Frame> {
327        Self::parse_with(bytes, |_| Some(crc_extra))
328    }
329
330    /// Returns which wire format the frame uses.
331    ///
332    /// # Returns
333    ///
334    /// [`Version::V1`] or [`Version::V2`].
335    pub fn version(&self) -> Version {
336        if self.bytes[0] == MAGIC_V2 {
337            Version::V2
338        } else {
339            Version::V1
340        }
341    }
342
343    /// Returns the sequence number the sender stamped on the frame.
344    ///
345    /// # Returns
346    ///
347    /// The sequence number.
348    pub fn sequence(&self) -> u8 {
349        match self.version() {
350            Version::V1 => self.bytes[2],
351            Version::V2 => self.bytes[4],
352        }
353    }
354
355    /// Returns the sending system's id.
356    ///
357    /// # Returns
358    ///
359    /// The system id.
360    pub fn system_id(&self) -> u8 {
361        match self.version() {
362            Version::V1 => self.bytes[3],
363            Version::V2 => self.bytes[5],
364        }
365    }
366
367    /// Returns the sending component's id.
368    ///
369    /// # Returns
370    ///
371    /// The component id.
372    pub fn component_id(&self) -> u8 {
373        match self.version() {
374            Version::V1 => self.bytes[4],
375            Version::V2 => self.bytes[6],
376        }
377    }
378
379    /// Returns the message id the frame carries.
380    ///
381    /// # Returns
382    ///
383    /// The message id: 0-255 for v1, up to 24 bits for v2.
384    pub fn message_id(&self) -> u32 {
385        match self.version() {
386            Version::V1 => u32::from(self.bytes[5]),
387            Version::V2 => {
388                u32::from(self.bytes[7])
389                    | u32::from(self.bytes[8]) << 8
390                    | u32::from(self.bytes[9]) << 16
391            }
392        }
393    }
394
395    /// Returns the incompatibility flags of a v2 frame, or `0` for a v1 frame.
396    ///
397    /// # Returns
398    ///
399    /// The incompatibility flags byte.
400    pub fn incompat_flags(&self) -> u8 {
401        match self.version() {
402            Version::V1 => 0,
403            Version::V2 => self.bytes[2],
404        }
405    }
406
407    /// Reports whether the frame is a signed v2 frame.
408    ///
409    /// # Returns
410    ///
411    /// `true` if the frame carries a signature.
412    pub fn is_signed(&self) -> bool {
413        self.version() == Version::V2 && self.incompat_flags() & IFLAG_SIGNED != 0
414    }
415
416    /// Returns the payload as it was carried, after any MAVLink 2 truncation.
417    ///
418    /// # Returns
419    ///
420    /// The payload bytes.
421    pub fn payload(&self) -> &[u8] {
422        let start = Self::header_len(self.version());
423        let plen = self.bytes[1] as usize;
424        &self.bytes[start..start + plen]
425    }
426
427    /// Returns the 13-byte signature block of a signed frame.
428    ///
429    /// # Returns
430    ///
431    /// The signature block, or [`None`] if the frame is not signed.
432    pub fn signature(&self) -> Option<&[u8; SIGNATURE_LEN]> {
433        if !self.is_signed() {
434            return None;
435        }
436        let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
437        self.bytes[start..start + SIGNATURE_LEN].try_into().ok()
438    }
439
440    /// Returns the whole frame, ready for the link.
441    ///
442    /// # Returns
443    ///
444    /// The frame as a byte slice, signature included if present.
445    pub fn as_bytes(&self) -> &[u8] {
446        &self.bytes[..self.len]
447    }
448
449    // The bytes a signature is computed over, with the message-specific CRC: the header
450    // (start marker included), the payload, and the two checksum bytes.
451    pub(crate) fn signed_region(&self) -> &[u8] {
452        let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
453        &self.bytes[..start]
454    }
455
456    // Writes the signature block of a signed frame, after the checksum.
457    pub(crate) fn signature_mut(&mut self) -> &mut [u8; SIGNATURE_LEN] {
458        let start = HEADER_V2 + self.bytes[1] as usize + CHECKSUM_LEN;
459        (&mut self.bytes[start..start + SIGNATURE_LEN])
460            .try_into()
461            .expect("a signed frame reserves a full signature block")
462    }
463}
464
465// The payload length after dropping trailing zero bytes. A non-empty payload keeps at
466// least one byte, as MAVLink 2 requires; an empty payload stays empty. The `len > 1`
467// guard supplies both, and never exceeds the payload length.
468fn truncated_len(payload: &[u8]) -> usize {
469    let mut len = payload.len();
470    while len > 1 && payload[len - 1] == 0 {
471        len -= 1;
472    }
473    len
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    // HEARTBEAT, the frame every MAVLink node emits, used to anchor the layout.
481    const HEARTBEAT_ID: u32 = 0;
482    const HEARTBEAT_CRC_EXTRA: u8 = 50;
483
484    // The serialization guide: an implementation must discard a packet if it does not
485    // understand any flag in its incompatibility flags. The checksum is good, so only the
486    // flag refuses it.
487    #[test]
488    fn a_frame_with_an_incompatibility_flag_it_does_not_know_is_dropped() {
489        let payload = [0x06, 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0x59, 0x03];
490        let flagged = Frame::assemble_v2(
491            Header::new(1, 1, 0),
492            HEARTBEAT_ID,
493            &payload,
494            HEARTBEAT_CRC_EXTRA,
495            0x02,
496        )
497        .unwrap();
498        assert_eq!(
499            Frame::parse(flagged.as_bytes(), HEARTBEAT_CRC_EXTRA),
500            Err(MavlinkError::UnknownIncompatFlags(0x02))
501        );
502        let good = Frame::encode_v2(
503            Header::new(1, 1, 1),
504            HEARTBEAT_ID,
505            &payload,
506            HEARTBEAT_CRC_EXTRA,
507        )
508        .unwrap();
509        let mut parser = crate::Parser::new();
510        let heard: Vec<Frame> = [flagged.as_bytes(), good.as_bytes()]
511            .concat()
512            .iter()
513            .filter_map(|&byte| parser.push_byte(byte, &|_| Some(HEARTBEAT_CRC_EXTRA)))
514            .collect();
515        assert_eq!(heard.len(), 1, "the parser passes over the flagged frame");
516        assert_eq!(heard[0].sequence(), 1);
517    }
518
519    #[test]
520    fn a_v2_frame_round_trips_through_parse() {
521        let header = Header::new(1, 1, 7);
522        let payload = [0x06, 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0x59, 0x03];
523        let frame = Frame::encode_v2(header, HEARTBEAT_ID, &payload, HEARTBEAT_CRC_EXTRA).unwrap();
524        let parsed = Frame::parse(frame.as_bytes(), HEARTBEAT_CRC_EXTRA).unwrap();
525        assert_eq!(parsed.version(), Version::V2);
526        assert_eq!(parsed.message_id(), HEARTBEAT_ID);
527        assert_eq!(parsed.system_id(), 1);
528        assert_eq!(parsed.sequence(), 7);
529        assert_eq!(parsed.payload(), &payload);
530    }
531
532    #[test]
533    fn a_v1_frame_round_trips_through_parse() {
534        let header = Header::new(1, 1, 0);
535        let payload = [0x06, 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0x59, 0x03];
536        let frame = Frame::encode_v1(header, HEARTBEAT_ID, &payload, HEARTBEAT_CRC_EXTRA).unwrap();
537        let parsed = Frame::parse(frame.as_bytes(), HEARTBEAT_CRC_EXTRA).unwrap();
538        assert_eq!(parsed.version(), Version::V1);
539        assert_eq!(parsed.message_id(), HEARTBEAT_ID);
540        assert_eq!(parsed.payload(), &payload);
541    }
542
543    #[test]
544    fn the_v2_header_is_laid_out_as_the_spec_requires() {
545        let header = Header::new(0x2A, 0xBE, 0x10);
546        let frame = Frame::encode_v2(header, 0x0A0B0C, &[1, 2, 3], 0).unwrap();
547        let bytes = frame.as_bytes();
548        assert_eq!(bytes[0], MAGIC_V2);
549        assert_eq!(bytes[1], 3); // payload length
550        assert_eq!(bytes[2], 0); // incompat flags
551        assert_eq!(bytes[3], 0); // compat flags
552        assert_eq!(bytes[4], 0x10); // sequence
553        assert_eq!(bytes[5], 0x2A); // system id
554        assert_eq!(bytes[6], 0xBE); // component id
555        assert_eq!(&bytes[7..10], &[0x0C, 0x0B, 0x0A]); // msgid, little-endian
556    }
557
558    #[test]
559    fn trailing_zero_bytes_are_truncated_but_the_first_is_kept() {
560        let header = Header::new(1, 1, 0);
561        // A payload of all zeros truncates to a single byte, never to nothing.
562        let frame = Frame::encode_v2(header, 0, &[0, 0, 0, 0], 50).unwrap();
563        assert_eq!(frame.payload(), &[0]);
564
565        // Trailing zeros are dropped; an interior zero is preserved.
566        let frame = Frame::encode_v2(header, 0, &[1, 0, 2, 0, 0], 50).unwrap();
567        assert_eq!(frame.payload(), &[1, 0, 2]);
568    }
569
570    #[test]
571    fn a_corrupt_checksum_is_rejected() {
572        let header = Header::new(1, 1, 0);
573        let frame = Frame::encode_v2(header, 0, &[1, 2, 3], 50).unwrap();
574        let mut bytes = frame.as_bytes().to_vec();
575        let last = bytes.len() - 1;
576        bytes[last] ^= 0xFF;
577        assert!(matches!(
578            Frame::parse(&bytes, 50),
579            Err(MavlinkError::CrcMismatch { .. })
580        ));
581    }
582
583    #[test]
584    fn the_wrong_crc_extra_is_rejected() {
585        // A receiver that disagrees about the message shape folds in a different seed and
586        // rejects the frame, which is the whole point of CRC_EXTRA.
587        let header = Header::new(1, 1, 0);
588        let frame = Frame::encode_v2(header, 0, &[1, 2, 3], 50).unwrap();
589        assert!(matches!(
590            Frame::parse(frame.as_bytes(), 51),
591            Err(MavlinkError::CrcMismatch { .. })
592        ));
593    }
594
595    #[test]
596    fn an_unknown_message_is_rejected() {
597        let header = Header::new(1, 1, 0);
598        let frame = Frame::encode_v2(header, 999, &[1, 2, 3], 50).unwrap();
599        assert_eq!(
600            Frame::parse_with(frame.as_bytes(), |_| None),
601            Err(MavlinkError::UnknownMessage(999))
602        );
603    }
604
605    #[test]
606    fn a_truncated_frame_is_rejected() {
607        let header = Header::new(1, 1, 0);
608        let frame = Frame::encode_v2(header, 0, &[1, 2, 3, 4, 5], 50).unwrap();
609        let bytes = frame.as_bytes();
610        assert_eq!(
611            Frame::parse(&bytes[..bytes.len() - 1], 50),
612            Err(MavlinkError::Truncated)
613        );
614    }
615
616    #[test]
617    fn an_unrecognized_start_marker_is_rejected() {
618        assert_eq!(
619            Frame::parse(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], 50),
620            Err(MavlinkError::BadMagic(0x00))
621        );
622    }
623}