Skip to main content

pamoja_mavlink/
crc.rs

1//! The CRC-16/MCRF4XX checksum every MAVLink frame carries, and the `CRC_EXTRA`
2//! seed derived from a message's shape.
3//!
4//! MAVLink names its checksum "X.25", but the accumulator it actually uses omits the
5//! final inversion that true CRC-16/X-25 applies, which makes it CRC-16/MCRF4XX. The
6//! distinction is not cosmetic: the two share a polynomial, initial value, and
7//! reflection and differ only in that final XOR, so a frame checked with the wrong one
8//! is silently rejected. This module pins the MCRF4XX parameters and is anchored to the
9//! catalogue check value, so that trap is closed.
10
11// The reflected form of the CRC-16/CCITT polynomial 0x1021, used because the checksum
12// reflects its input and output and so processes each byte least-significant bit first.
13const POLY_REFLECTED: u16 = 0x8408;
14
15/// Folds more bytes into a running CRC-16/MCRF4XX.
16///
17/// # Arguments
18///
19/// * `crc` - the running value; start a fresh checksum from `0xFFFF`.
20/// * `data` - the bytes to fold in.
21///
22/// # Returns
23///
24/// The updated CRC.
25pub const fn accumulate(mut crc: u16, data: &[u8]) -> u16 {
26    let mut i = 0;
27    while i < data.len() {
28        crc ^= data[i] as u16;
29        let mut bit = 0;
30        while bit < 8 {
31            if crc & 1 != 0 {
32                crc = (crc >> 1) ^ POLY_REFLECTED;
33            } else {
34                crc >>= 1;
35            }
36            bit += 1;
37        }
38        i += 1;
39    }
40    crc
41}
42
43/// Computes the CRC-16/MCRF4XX of a byte slice.
44///
45/// The parameters are width 16, polynomial `0x1021`, initial value `0xFFFF`, input and
46/// output reflected, and no final inversion (`xorout = 0x0000`). True CRC-16/X-25 differs
47/// only in that final inversion, so this checks `0x6F91` over `"123456789"` where X-25
48/// checks `0x906E`.
49///
50/// # Arguments
51///
52/// * `data` - the bytes to check.
53///
54/// # Returns
55///
56/// The 16-bit CRC.
57///
58/// # Examples
59///
60/// ```
61/// use pamoja_mavlink::crc16_mcrf4xx;
62///
63/// // The catalogue check value for CRC-16/MCRF4XX.
64/// assert_eq!(crc16_mcrf4xx(b"123456789"), 0x6F91);
65/// ```
66pub const fn crc16_mcrf4xx(data: &[u8]) -> u16 {
67    accumulate(0xFFFF, data)
68}
69
70/// Computes the checksum a frame carries: the CRC over its bytes with the message's
71/// `CRC_EXTRA` folded in last.
72///
73/// # Arguments
74///
75/// * `frame` - the frame bytes the checksum covers: everything after the start marker up
76///   to but not including the two checksum bytes (and not the signature).
77/// * `crc_extra` - the `CRC_EXTRA` seed for the frame's message id.
78///
79/// # Returns
80///
81/// The 16-bit checksum to append, low byte first.
82pub const fn checksum(frame: &[u8], crc_extra: u8) -> u16 {
83    accumulate(accumulate(0xFFFF, frame), &[crc_extra])
84}
85
86/// Derives a message's `CRC_EXTRA` from its name and base fields.
87///
88/// MAVLink computes this over the message name, then each base (non-extension) field's
89/// type and name in wire order, with an array field's length folded in as one byte, and
90/// reduces the 16-bit result to a byte. A receiver folds the seed into every frame's
91/// checksum, so a sender and receiver that disagree about a message's shape reject each
92/// other's frames instead of silently misreading them. Extension fields are excluded,
93/// which is what lets a message gain extension fields without breaking compatibility.
94///
95/// # Arguments
96///
97/// * `name` - the message name, such as `"HEARTBEAT"`.
98/// * `fields` - the base fields in wire order, each as `(type, name, array_len)`, where
99///   `type` is the MAVLink C type such as `"uint16_t"` and `array_len` is `0` for a
100///   scalar field.
101///
102/// # Returns
103///
104/// The `CRC_EXTRA` byte.
105///
106/// # Examples
107///
108/// ```
109/// use pamoja_mavlink::message_crc_extra;
110///
111/// // HEARTBEAT, in wire order: the 4-byte field first, then the five bytes.
112/// let crc_extra = message_crc_extra(
113///     "HEARTBEAT",
114///     &[
115///         ("uint32_t", "custom_mode", 0),
116///         ("uint8_t", "type", 0),
117///         ("uint8_t", "autopilot", 0),
118///         ("uint8_t", "base_mode", 0),
119///         ("uint8_t", "system_status", 0),
120///         ("uint8_t", "mavlink_version", 0),
121///     ],
122/// );
123/// assert_eq!(crc_extra, 50);
124/// ```
125pub fn message_crc_extra(name: &str, fields: &[(&str, &str, u8)]) -> u8 {
126    crc_extra_of(name, fields.iter().copied())
127}
128
129// The seed derivation itself, over any sequence of `(type, name, array_len)` fields, so
130// the descriptor layer derives the same value without collecting its fields into a slice.
131pub(crate) fn crc_extra_of<'f>(
132    name: &str,
133    fields: impl IntoIterator<Item = (&'f str, &'f str, u8)>,
134) -> u8 {
135    let mut crc = accumulate(0xFFFF, name.as_bytes());
136    crc = accumulate(crc, b" ");
137    for (ty, field, array_len) in fields {
138        crc = accumulate(crc, ty.as_bytes());
139        crc = accumulate(crc, b" ");
140        crc = accumulate(crc, field.as_bytes());
141        crc = accumulate(crc, b" ");
142        if array_len != 0 {
143            crc = accumulate(crc, &[array_len]);
144        }
145    }
146    ((crc & 0xFF) ^ (crc >> 8)) as u8
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn matches_the_mcrf4xx_catalogue_check_value() {
155        assert_eq!(crc16_mcrf4xx(b"123456789"), 0x6F91);
156    }
157
158    #[test]
159    fn is_not_the_x25_check_value() {
160        // True CRC-16/X-25 applies a final XOR and checks 0x906E; using it would make
161        // every frame fail its checksum against a real autopilot.
162        assert_ne!(crc16_mcrf4xx(b"123456789"), 0x906E);
163    }
164
165    #[test]
166    fn an_empty_slice_is_the_initial_value() {
167        assert_eq!(crc16_mcrf4xx(&[]), 0xFFFF);
168    }
169
170    #[test]
171    fn heartbeat_crc_extra_matches_the_dialect() {
172        // The official common-dialect CRC_EXTRA for HEARTBEAT is 50.
173        let crc_extra = message_crc_extra(
174            "HEARTBEAT",
175            &[
176                ("uint32_t", "custom_mode", 0),
177                ("uint8_t", "type", 0),
178                ("uint8_t", "autopilot", 0),
179                ("uint8_t", "base_mode", 0),
180                ("uint8_t", "system_status", 0),
181                ("uint8_t", "mavlink_version", 0),
182            ],
183        );
184        assert_eq!(crc_extra, 50);
185    }
186}