Skip to main content

pamoja_mavlink/dialect/
mod.rs

1//! The typed message layer: a broad slice of the MAVLink common dialect, plus the seam
2//! that lets any message id be carried and checked.
3//!
4//! A [`Frame`] moves opaque payload bytes; this module gives those bytes
5//! meaning. Each typed message knows its id, its `CRC_EXTRA` seed, and how its fields are
6//! laid out on the wire, so a sender fills named fields instead of hand-packing a buffer
7//! and a receiver reads them back. The set covers what a ground station and an autopilot
8//! actually exchange: the periodic [`Heartbeat`], system status, the command, parameter,
9//! and mission protocols, and the core position and attitude telemetry.
10//!
11//! Messages are declared from one source of truth and their byte layout is derived from
12//! it, including the field reordering MAVLink applies (largest field first). Each message
13//! carries the official `CRC_EXTRA` for its shape, and a test re-derives that seed from
14//! the field definitions, so a wrong field type, name, or order is caught against the
15//! published dialect rather than only by a round-trip.
16//!
17//! A message this crate does not type is still reachable two ways: [`RawMessage`] carries
18//! it by id and raw payload, and a [`MessageDescriptor`] gives those bytes named fields, so
19//! a vendor's dialect is usable at runtime without being compiled in.
20
21use crate::error::Result;
22use crate::frame::{Frame, Header, MAX_PAYLOAD};
23
24#[macro_use]
25mod macros;
26
27mod common;
28mod enums;
29mod schema;
30
31pub use common::*;
32pub use enums::*;
33pub use schema::*;
34
35/// A typed MAVLink message: its identity on the wire and how it serializes.
36///
37/// Implemented for every message this crate types, via the `message!` declaration macro.
38pub trait Message: Sized {
39    /// The message id on the wire.
40    const ID: u32;
41    /// The message name, such as `"HEARTBEAT"`, as used to derive [`CRC_EXTRA`](Self::CRC_EXTRA).
42    const NAME: &'static str;
43    /// The `CRC_EXTRA` seed folded into the checksum of a frame carrying this message.
44    const CRC_EXTRA: u8;
45    /// The full length, in bytes, of this message's base fields on the wire.
46    const WIRE_LEN: usize;
47    /// The base fields in wire order as `(type, name, array_len)`, the input from which
48    /// [`CRC_EXTRA`](Self::CRC_EXTRA) is derived and against which it is verified.
49    const BASE_FIELDS: &'static [(&'static str, &'static str, u8)];
50
51    /// The message's shape as data: the runtime counterpart of this type's fields, for a
52    /// caller reading and writing by name rather than through the struct.
53    const DESCRIPTOR: &'static MessageDescriptor<'static>;
54
55    /// Serializes the message into `out`, returning the number of bytes written.
56    ///
57    /// # Arguments
58    ///
59    /// * `out` - the destination buffer, which must be at least [`WIRE_LEN`](Self::WIRE_LEN)
60    ///   bytes; a [`MAX_PAYLOAD`]-byte buffer always suffices.
61    ///
62    /// # Returns
63    ///
64    /// The number of bytes written, which is [`WIRE_LEN`](Self::WIRE_LEN).
65    fn encode(&self, out: &mut [u8]) -> usize;
66
67    /// Deserializes the message from a payload.
68    ///
69    /// A short payload is zero-extended, as MAVLink 2 truncation requires, and a payload
70    /// longer than [`WIRE_LEN`](Self::WIRE_LEN) (one carrying extension fields) has its
71    /// trailing bytes ignored.
72    ///
73    /// # Arguments
74    ///
75    /// * `payload` - the frame payload to read.
76    ///
77    /// # Returns
78    ///
79    /// The decoded message.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`MavlinkError::BadPayload`](crate::MavlinkError::BadPayload) if the
84    /// payload cannot form the message.
85    fn decode(payload: &[u8]) -> Result<Self>;
86}
87
88/// Builds a v2 frame carrying a typed message.
89///
90/// # Arguments
91///
92/// * `header` - the addressing fields to stamp on the frame.
93/// * `message` - the message to send.
94///
95/// # Returns
96///
97/// The frame ready to send.
98///
99/// # Errors
100///
101/// Returns [`MavlinkError::PayloadTooLong`](crate::MavlinkError::PayloadTooLong) if the
102/// message does not fit a frame.
103pub fn encode_message<M: Message>(header: Header, message: &M) -> Result<Frame> {
104    let mut payload = [0u8; MAX_PAYLOAD];
105    let len = message.encode(&mut payload);
106    Frame::encode_v2(header, M::ID, &payload[..len], M::CRC_EXTRA)
107}
108
109/// A message this crate does not type, carried by id and raw payload.
110///
111/// This is the escape hatch for a message id outside the typed set or from another
112/// dialect: supply its id, payload, and `CRC_EXTRA` and it frames and checks like any
113/// other, the way Modbus carries a function code it does not name.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct RawMessage<'a> {
116    /// The message id.
117    pub msgid: u32,
118    /// The `CRC_EXTRA` seed for the message id.
119    pub crc_extra: u8,
120    /// The raw payload bytes.
121    pub payload: &'a [u8],
122}
123
124impl<'a> RawMessage<'a> {
125    /// Builds a v2 frame carrying this raw message.
126    ///
127    /// # Arguments
128    ///
129    /// * `header` - the addressing fields to stamp on the frame.
130    ///
131    /// # Returns
132    ///
133    /// The frame ready to send.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`MavlinkError::PayloadTooLong`](crate::MavlinkError::PayloadTooLong) if the
138    /// payload does not fit a frame.
139    pub fn to_frame(&self, header: Header) -> Result<Frame> {
140        Frame::encode_v2(header, self.msgid, self.payload, self.crc_extra)
141    }
142}
143
144/// Returns the `CRC_EXTRA` for a common-dialect message id, if known.
145///
146/// A [`Parser`](crate::Parser) or [`Frame::parse_with`] uses this to validate the
147/// checksum of a frame off the wire, so traffic from a real autopilot is checked even for
148/// messages this crate does not type.
149///
150/// # Arguments
151///
152/// * `msgid` - the message id to look up.
153///
154/// # Returns
155///
156/// The `CRC_EXTRA` seed, or [`None`] if the id is not in the table.
157pub fn crc_extra(msgid: u32) -> Option<u8> {
158    COMMON_CRC_EXTRA
159        .iter()
160        .find(|(id, _)| *id == msgid)
161        .map(|(_, crc)| *crc)
162}
163
164// The official common-dialect CRC_EXTRA seeds for the message ids this crate handles.
165// Each is the value the reference dialect publishes; the typed messages above re-derive
166// the same value from their field definitions in the test below.
167const COMMON_CRC_EXTRA: &[(u32, u8)] = &[
168    (0, 50),
169    (1, 124),
170    (2, 137),
171    (4, 237),
172    (11, 89),
173    (20, 214),
174    (21, 159),
175    (22, 220),
176    (23, 168),
177    (24, 24),
178    (30, 39),
179    (31, 246),
180    (32, 185),
181    (33, 104),
182    (36, 222),
183    (40, 230),
184    (42, 28),
185    (43, 132),
186    (44, 221),
187    (45, 232),
188    (47, 153),
189    (51, 196),
190    (65, 118),
191    (69, 243),
192    (73, 38),
193    (74, 20),
194    (75, 158),
195    (76, 152),
196    (77, 143),
197    (84, 143),
198    (86, 5),
199    (147, 154),
200    (148, 178),
201    (242, 104),
202    (245, 130),
203    (253, 83),
204];
205
206// Trims a single trailing underscore from a Rust field identifier to recover the wire
207// field name, so a field that collides with a keyword (such as `type_`) carries the name
208// the dialect uses (`type`) in its CRC_EXTRA derivation.
209pub(crate) const fn xml_name(name: &str) -> &str {
210    let bytes = name.as_bytes();
211    let len = bytes.len();
212    if len > 0 && bytes[len - 1] == b'_' {
213        let (head, _) = bytes.split_at(len - 1);
214        match core::str::from_utf8(head) {
215            Ok(trimmed) => trimmed,
216            Err(_) => name,
217        }
218    } else {
219        name
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn the_registry_resolves_known_ids_and_rejects_others() {
229        assert_eq!(crc_extra(0), Some(50));
230        assert_eq!(crc_extra(76), Some(152));
231        assert_eq!(crc_extra(9999), None);
232    }
233
234    #[test]
235    fn xml_name_trims_a_keyword_field() {
236        assert_eq!(xml_name("type_"), "type");
237        assert_eq!(xml_name("custom_mode"), "custom_mode");
238        assert_eq!(xml_name(""), "");
239    }
240}