Skip to main content

pamoja_mavlink/dialect/schema/
mod.rs

1//! Message shape as data: read and write a message by field name, with no typed struct.
2//!
3//! A [`Message`](crate::dialect::Message) implementation is the fastest way to send a
4//! message this crate types. It is also the only way, which is a problem the moment a
5//! caller needs a message from ArduPilot's dialect, PX4's, or a vendor's private one. This
6//! module removes that limit: a [`MessageDescriptor`] states a message's id, name, and
7//! fields, and [`DynamicMessage`] encodes and decodes against it, so any message from any
8//! dialect is usable once its shape is described.
9//!
10//! Every typed message publishes its own descriptor, so the two layers agree by
11//! construction: [`descriptor`] resolves an id from the common dialect, and a test holds
12//! each descriptor to the same bytes its typed struct produces.
13//!
14//! A dialect describes its fields in declaration order, but MAVLink puts them on the wire
15//! largest first. [`MessageDescriptorBuilder`] applies that reordering and derives the
16//! `CRC_EXTRA` seed from the result, so a caller transcribing a message definition writes
17//! it the way the definition reads.
18
19use super::DESCRIPTORS;
20use crate::crc::crc_extra_of;
21use crate::error::{MavlinkError, Result};
22use crate::frame::{Frame, Header, MAX_PAYLOAD};
23
24#[cfg(feature = "alloc")]
25mod owned;
26
27#[cfg(feature = "alloc")]
28pub use owned::{
29    MessageDescriptorBuilder, OwnedDialect, OwnedFieldDescriptor, OwnedMessageDescriptor,
30};
31
32/// The scalar type of a message field, as a dialect declares it.
33///
34/// The name is the C type MAVLink writes in a message definition, which is also what the
35/// `CRC_EXTRA` derivation folds in, so the spelling is part of the wire contract rather
36/// than a label.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum FieldType {
39    /// `uint8_t`, one unsigned byte.
40    U8,
41    /// `int8_t`, one signed byte.
42    I8,
43    /// `char`, one byte of text; an array of these carries a string.
44    Char,
45    /// `uint16_t`.
46    U16,
47    /// `int16_t`.
48    I16,
49    /// `uint32_t`.
50    U32,
51    /// `int32_t`.
52    I32,
53    /// `uint64_t`.
54    U64,
55    /// `int64_t`.
56    I64,
57    /// `float`, IEEE 754 single precision.
58    F32,
59    /// `double`, IEEE 754 double precision.
60    F64,
61}
62
63impl FieldType {
64    /// Returns the size of one element of this type, in bytes.
65    ///
66    /// # Returns
67    ///
68    /// The element size, from one to eight bytes.
69    pub const fn size(self) -> usize {
70        match self {
71            Self::U8 | Self::I8 | Self::Char => 1,
72            Self::U16 | Self::I16 => 2,
73            Self::U32 | Self::I32 | Self::F32 => 4,
74            Self::U64 | Self::I64 | Self::F64 => 8,
75        }
76    }
77
78    /// Returns the C type name a dialect writes for this type.
79    ///
80    /// # Returns
81    ///
82    /// The name, such as `"uint8_t"` or `"float"`.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use pamoja_mavlink::dialect::FieldType;
88    ///
89    /// assert_eq!(FieldType::F32.wire_name(), "float");
90    /// assert_eq!(FieldType::U64.wire_name(), "uint64_t");
91    /// ```
92    pub const fn wire_name(self) -> &'static str {
93        match self {
94            Self::U8 => "uint8_t",
95            Self::I8 => "int8_t",
96            Self::Char => "char",
97            Self::U16 => "uint16_t",
98            Self::I16 => "int16_t",
99            Self::U32 => "uint32_t",
100            Self::I32 => "int32_t",
101            Self::U64 => "uint64_t",
102            Self::I64 => "int64_t",
103            Self::F32 => "float",
104            Self::F64 => "double",
105        }
106    }
107
108    /// Resolves a C type name from a message definition.
109    ///
110    /// An array declaration carries its length separately, so `"uint8_t[4]"` is written as
111    /// `"uint8_t"` with a length of four rather than parsed here.
112    ///
113    /// # Arguments
114    ///
115    /// * `name` - the C type name, such as `"uint16_t"`.
116    ///
117    /// # Returns
118    ///
119    /// The type, or [`None`] if the name is not one MAVLink defines.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use pamoja_mavlink::dialect::FieldType;
125    ///
126    /// assert_eq!(FieldType::from_wire_name("int32_t"), Some(FieldType::I32));
127    /// assert_eq!(FieldType::from_wire_name("size_t"), None);
128    /// ```
129    pub fn from_wire_name(name: &str) -> Option<Self> {
130        Some(match name {
131            "uint8_t" | "uint8_t_mavlink_version" => Self::U8,
132            "int8_t" => Self::I8,
133            "char" => Self::Char,
134            "uint16_t" => Self::U16,
135            "int16_t" => Self::I16,
136            "uint32_t" => Self::U32,
137            "int32_t" => Self::I32,
138            "uint64_t" => Self::U64,
139            "int64_t" => Self::I64,
140            "float" => Self::F32,
141            "double" => Self::F64,
142            _ => return None,
143        })
144    }
145
146    /// Reports whether values of this type are whole numbers.
147    ///
148    /// # Returns
149    ///
150    /// `true` for every type but `float` and `double`.
151    pub const fn is_integer(self) -> bool {
152        !matches!(self, Self::F32 | Self::F64)
153    }
154
155    /// Reports whether values of this type can be negative.
156    ///
157    /// # Returns
158    ///
159    /// `true` for the signed integer types and the two floating-point types.
160    pub const fn is_signed(self) -> bool {
161        matches!(
162            self,
163            Self::I8 | Self::I16 | Self::I32 | Self::I64 | Self::F32 | Self::F64
164        )
165    }
166}
167
168/// One field of a message: its name, type, and place in the layout.
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub struct FieldDescriptor<'a> {
171    /// The field name as the dialect writes it, such as `"custom_mode"`.
172    pub name: &'a str,
173
174    /// The field's scalar type, which for an array is its element type.
175    pub ty: FieldType,
176
177    /// The element count for an array field, or `0` for a scalar.
178    pub array_len: u8,
179
180    /// Whether this is a MAVLink 2 extension field.
181    ///
182    /// Extension fields sit after the base fields in declaration order, are excluded from
183    /// the `CRC_EXTRA` seed, and may be absent from a frame sent by an older peer, in
184    /// which case they read as zero.
185    pub extension: bool,
186}
187
188impl FieldDescriptor<'_> {
189    /// Returns how many elements the field holds.
190    ///
191    /// # Returns
192    ///
193    /// The array length, or `1` for a scalar.
194    pub const fn elements(&self) -> usize {
195        if self.array_len == 0 {
196            1
197        } else {
198            self.array_len as usize
199        }
200    }
201
202    /// Returns the total size of the field on the wire, in bytes.
203    ///
204    /// # Returns
205    ///
206    /// The element size multiplied by the element count.
207    pub const fn size(&self) -> usize {
208        self.elements() * self.ty.size()
209    }
210}
211
212/// The shape of one message: what a sender fills in and a receiver reads back.
213///
214/// Fields are held in wire order, which for the base fields is largest type first. A
215/// descriptor written by hand must already be in that order;
216/// [`MessageDescriptorBuilder`] does the reordering for a definition written the way a
217/// dialect reads.
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
219pub struct MessageDescriptor<'a> {
220    /// The message id on the wire.
221    pub id: u32,
222
223    /// The message name, such as `"HEARTBEAT"`.
224    pub name: &'a str,
225
226    /// The `CRC_EXTRA` seed folded into the checksum of a frame carrying this message.
227    pub crc_extra: u8,
228
229    /// The fields in wire order: the base fields largest first, then any extensions.
230    pub fields: &'a [FieldDescriptor<'a>],
231}
232
233impl<'a> MessageDescriptor<'a> {
234    /// Returns the full length of the message on the wire, extensions included.
235    ///
236    /// # Returns
237    ///
238    /// The sum of every field's size, in bytes.
239    pub fn wire_len(&self) -> usize {
240        self.fields.iter().map(FieldDescriptor::size).sum()
241    }
242
243    /// Returns the length of the message's base fields, in bytes.
244    ///
245    /// This is what a peer that predates the extensions expects, and the length the
246    /// `CRC_EXTRA` seed describes.
247    ///
248    /// # Returns
249    ///
250    /// The sum of the base fields' sizes.
251    pub fn base_len(&self) -> usize {
252        self.fields
253            .iter()
254            .filter(|field| !field.extension)
255            .map(FieldDescriptor::size)
256            .sum()
257    }
258
259    /// Looks a field up by name.
260    ///
261    /// # Arguments
262    ///
263    /// * `name` - the field name to find.
264    ///
265    /// # Returns
266    ///
267    /// The field, or [`None`] if the message has no field of that name.
268    pub fn field(&self, name: &str) -> Option<&'a FieldDescriptor<'a>> {
269        self.fields.iter().find(|field| field.name == name)
270    }
271
272    /// Returns the byte offset of a field within the payload.
273    ///
274    /// # Arguments
275    ///
276    /// * `name` - the field name to locate.
277    ///
278    /// # Returns
279    ///
280    /// The offset, or [`None`] if the message has no field of that name.
281    pub fn offset_of(&self, name: &str) -> Option<usize> {
282        let mut offset = 0;
283        for field in self.fields {
284            if field.name == name {
285                return Some(offset);
286            }
287            offset += field.size();
288        }
289        None
290    }
291
292    /// Derives the `CRC_EXTRA` seed from this descriptor's base fields.
293    ///
294    /// A descriptor whose [`crc_extra`](Self::crc_extra) differs from this has a field
295    /// wrong: a mistyped field, a wrong array length, or fields out of wire order.
296    ///
297    /// # Returns
298    ///
299    /// The seed the fields imply.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use pamoja_mavlink::dialect::descriptor;
305    ///
306    /// let heartbeat = descriptor(0).expect("HEARTBEAT is in the common dialect");
307    /// assert_eq!(heartbeat.derived_crc_extra(), heartbeat.crc_extra);
308    /// ```
309    pub fn derived_crc_extra(&self) -> u8 {
310        crc_extra_of(
311            self.name,
312            self.fields
313                .iter()
314                .filter(|field| !field.extension)
315                .map(|field| (field.ty.wire_name(), field.name, field.array_len)),
316        )
317    }
318
319    fn locate(&self, name: &str) -> Result<(usize, &'a FieldDescriptor<'a>)> {
320        let mut offset = 0;
321        for field in self.fields {
322            if field.name == name {
323                return Ok((offset, field));
324            }
325            offset += field.size();
326        }
327        Err(MavlinkError::UnknownField)
328    }
329}
330
331// The first double past the signed and unsigned 64-bit ranges. A cast saturates instead of
332// failing, so the bound is checked before the cast rather than after it.
333const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
334const TWO_POW_64: f64 = 18_446_744_073_709_551_616.0;
335
336/// A field's value, whichever of the three kinds its type calls for.
337#[derive(Clone, Copy, Debug, PartialEq)]
338pub enum FieldValue {
339    /// The value of a signed integer field.
340    Int(i64),
341    /// The value of an unsigned integer or `char` field.
342    Uint(u64),
343    /// The value of a `float` or `double` field.
344    Float(f64),
345}
346
347/// A message read and written by field name against a [`MessageDescriptor`].
348///
349/// This is the counterpart to a typed message: the same bytes, reached by name at runtime
350/// rather than through a struct known at compile time. It carries a full-size payload
351/// buffer and no allocation, so it works on a microcontroller as well as a ground station.
352///
353/// # Examples
354///
355/// ```
356/// use pamoja_mavlink::dialect::{
357///     descriptor, mav_state, mav_type, DynamicMessage, Heartbeat, Message,
358/// };
359/// use pamoja_mavlink::Header;
360///
361/// let shape = descriptor(Heartbeat::ID).expect("HEARTBEAT is in the common dialect");
362///
363/// // Fill the message in by name, the way a caller reading a dialect definition would.
364/// let mut heartbeat = DynamicMessage::new(shape)?;
365/// heartbeat.set_uint("type", 0, mav_type::ONBOARD_CONTROLLER.into())?;
366/// heartbeat.set_uint("system_status", 0, mav_state::ACTIVE.into())?;
367/// heartbeat.set_uint("mavlink_version", 0, 3)?;
368///
369/// // It is an ordinary frame, so a typed receiver reads it back unchanged.
370/// let frame = heartbeat.to_frame(Header::new(1, 1, 0))?;
371/// assert_eq!(Heartbeat::decode(frame.payload())?.system_status, mav_state::ACTIVE);
372/// # Ok::<(), pamoja_mavlink::MavlinkError>(())
373/// ```
374#[derive(Clone, Debug)]
375pub struct DynamicMessage<'a> {
376    descriptor: &'a MessageDescriptor<'a>,
377    payload: [u8; MAX_PAYLOAD],
378    len: usize,
379}
380
381impl<'a> DynamicMessage<'a> {
382    /// Creates a message with every field zero.
383    ///
384    /// # Arguments
385    ///
386    /// * `descriptor` - the shape of the message to build.
387    ///
388    /// # Returns
389    ///
390    /// The zeroed message, ready for its fields to be set.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`MavlinkError::PayloadTooLong`] if the descriptor's fields exceed
395    /// [`MAX_PAYLOAD`] bytes, which no message from a valid dialect does.
396    pub fn new(descriptor: &'a MessageDescriptor<'a>) -> Result<Self> {
397        let len = descriptor.wire_len();
398        if len > MAX_PAYLOAD {
399            return Err(MavlinkError::PayloadTooLong);
400        }
401        Ok(Self {
402            descriptor,
403            payload: [0; MAX_PAYLOAD],
404            len,
405        })
406    }
407
408    /// Reads a message out of a frame payload.
409    ///
410    /// A short payload is zero-extended, as MAVLink 2 truncation requires, so a frame from
411    /// a peer that omitted trailing zeros or predates an extension field still decodes.
412    ///
413    /// # Arguments
414    ///
415    /// * `descriptor` - the shape to read the payload as.
416    /// * `payload` - the frame payload.
417    ///
418    /// # Returns
419    ///
420    /// The decoded message.
421    ///
422    /// # Errors
423    ///
424    /// Returns [`MavlinkError::BadPayload`] if the payload is longer than the descriptor
425    /// describes, and [`MavlinkError::PayloadTooLong`] if the descriptor itself does not
426    /// fit a frame.
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// use pamoja_mavlink::dialect::{descriptor, mav_state, mav_type, DynamicMessage};
432    /// use pamoja_mavlink::{Frame, Header};
433    ///
434    /// // A companion computer's HEARTBEAT, built from the dialect's description of it: an
435    /// // onboard controller that is up and running.
436    /// let shape = descriptor(0).expect("HEARTBEAT is in the common dialect");
437    /// let mut beat = DynamicMessage::new(shape)?;
438    /// beat.set_uint("type", 0, mav_type::ONBOARD_CONTROLLER.into())?;
439    /// beat.set_uint("system_status", 0, mav_state::ACTIVE.into())?;
440    /// beat.set_uint("mavlink_version", 0, 3)?;
441    /// let sent = beat.to_frame(Header::new(1, 1, 7))?;
442    ///
443    /// // A receiver checks the frame against the same CRC extra, then reads it field by
444    /// // field.
445    /// let received = Frame::parse(sent.as_bytes(), shape.crc_extra)?;
446    /// let heartbeat = DynamicMessage::decode(shape, received.payload())?;
447    /// assert_eq!(heartbeat.get_uint("type", 0)?, u64::from(mav_type::ONBOARD_CONTROLLER));
448    /// assert_eq!(heartbeat.get_uint("system_status", 0)?, u64::from(mav_state::ACTIVE));
449    /// # Ok::<(), pamoja_mavlink::MavlinkError>(())
450    /// ```
451    pub fn decode(descriptor: &'a MessageDescriptor<'a>, payload: &[u8]) -> Result<Self> {
452        let len = descriptor.wire_len();
453        if len > MAX_PAYLOAD {
454            return Err(MavlinkError::PayloadTooLong);
455        }
456        if payload.len() > len {
457            return Err(MavlinkError::BadPayload);
458        }
459        let mut message = Self {
460            descriptor,
461            payload: [0; MAX_PAYLOAD],
462            len,
463        };
464        message.payload[..payload.len()].copy_from_slice(payload);
465        Ok(message)
466    }
467
468    /// Returns the shape this message is read and written against.
469    ///
470    /// # Returns
471    ///
472    /// The descriptor.
473    pub fn descriptor(&self) -> &'a MessageDescriptor<'a> {
474        self.descriptor
475    }
476
477    /// Returns the message's bytes as they go on the wire.
478    ///
479    /// # Returns
480    ///
481    /// The payload, including any trailing zeros; a frame truncates those itself.
482    pub fn payload(&self) -> &[u8] {
483        &self.payload[..self.len]
484    }
485
486    /// Builds a v2 frame carrying this message.
487    ///
488    /// # Arguments
489    ///
490    /// * `header` - the addressing fields to stamp on the frame.
491    ///
492    /// # Returns
493    ///
494    /// The frame ready to send.
495    ///
496    /// # Errors
497    ///
498    /// Returns [`MavlinkError::PayloadTooLong`] if the message does not fit a frame.
499    pub fn to_frame(&self, header: Header) -> Result<Frame> {
500        Frame::encode_v2(
501            header,
502            self.descriptor.id,
503            self.payload(),
504            self.descriptor.crc_extra,
505        )
506    }
507
508    /// Reads a field as a signed integer.
509    ///
510    /// Any integer field can be read this way, whatever its width or sign.
511    ///
512    /// # Arguments
513    ///
514    /// * `name` - the field name.
515    /// * `index` - the element to read, or `0` for a scalar field.
516    ///
517    /// # Returns
518    ///
519    /// The value.
520    ///
521    /// # Errors
522    ///
523    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
524    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
525    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
526    /// [`MavlinkError::ValueOutOfRange`] for a `uint64_t` value above [`i64::MAX`].
527    pub fn get_int(&self, name: &str, index: usize) -> Result<i64> {
528        let (offset, field) = self.element(name, index)?;
529        if !field.ty.is_integer() {
530            return Err(MavlinkError::FieldTypeMismatch);
531        }
532        Ok(match field.ty {
533            FieldType::I8 => self.read::<1>(offset)[0] as i8 as i64,
534            FieldType::I16 => i16::from_le_bytes(self.read::<2>(offset)) as i64,
535            FieldType::I32 => i32::from_le_bytes(self.read::<4>(offset)) as i64,
536            FieldType::I64 => i64::from_le_bytes(self.read::<8>(offset)),
537            FieldType::U8 | FieldType::Char => self.read::<1>(offset)[0] as i64,
538            FieldType::U16 => u16::from_le_bytes(self.read::<2>(offset)) as i64,
539            FieldType::U32 => u32::from_le_bytes(self.read::<4>(offset)) as i64,
540            FieldType::U64 => i64::try_from(u64::from_le_bytes(self.read::<8>(offset)))
541                .map_err(|_| MavlinkError::ValueOutOfRange)?,
542            FieldType::F32 | FieldType::F64 => unreachable!(),
543        })
544    }
545
546    /// Reads a field as an unsigned integer.
547    ///
548    /// Any integer field can be read this way, whatever its width or sign.
549    ///
550    /// # Arguments
551    ///
552    /// * `name` - the field name.
553    /// * `index` - the element to read, or `0` for a scalar field.
554    ///
555    /// # Returns
556    ///
557    /// The value.
558    ///
559    /// # Errors
560    ///
561    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
562    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
563    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
564    /// [`MavlinkError::ValueOutOfRange`] for a negative value.
565    pub fn get_uint(&self, name: &str, index: usize) -> Result<u64> {
566        let (offset, field) = self.element(name, index)?;
567        if !field.ty.is_integer() {
568            return Err(MavlinkError::FieldTypeMismatch);
569        }
570        if field.ty.is_signed() {
571            return u64::try_from(self.get_int(name, index)?)
572                .map_err(|_| MavlinkError::ValueOutOfRange);
573        }
574        Ok(match field.ty {
575            FieldType::U8 | FieldType::Char => self.read::<1>(offset)[0] as u64,
576            FieldType::U16 => u16::from_le_bytes(self.read::<2>(offset)) as u64,
577            FieldType::U32 => u32::from_le_bytes(self.read::<4>(offset)) as u64,
578            FieldType::U64 => u64::from_le_bytes(self.read::<8>(offset)),
579            _ => unreachable!(),
580        })
581    }
582
583    /// Reads a floating-point field.
584    ///
585    /// # Arguments
586    ///
587    /// * `name` - the field name.
588    /// * `index` - the element to read, or `0` for a scalar field.
589    ///
590    /// # Returns
591    ///
592    /// The value, widened to double precision for a `float` field.
593    ///
594    /// # Errors
595    ///
596    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
597    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
598    /// and [`MavlinkError::FieldTypeMismatch`] for an integer field.
599    pub fn get_float(&self, name: &str, index: usize) -> Result<f64> {
600        let (offset, field) = self.element(name, index)?;
601        Ok(match field.ty {
602            FieldType::F32 => f32::from_le_bytes(self.read::<4>(offset)) as f64,
603            FieldType::F64 => f64::from_le_bytes(self.read::<8>(offset)),
604            _ => return Err(MavlinkError::FieldTypeMismatch),
605        })
606    }
607
608    /// Reads a field as whichever kind of value its type calls for.
609    ///
610    /// # Arguments
611    ///
612    /// * `name` - the field name.
613    /// * `index` - the element to read, or `0` for a scalar field.
614    ///
615    /// # Returns
616    ///
617    /// The value.
618    ///
619    /// # Errors
620    ///
621    /// Returns [`MavlinkError::UnknownField`] if the message has no such field, and
622    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array.
623    pub fn get(&self, name: &str, index: usize) -> Result<FieldValue> {
624        let (_, field) = self.element(name, index)?;
625        Ok(if !field.ty.is_integer() {
626            FieldValue::Float(self.get_float(name, index)?)
627        } else if field.ty.is_signed() {
628            FieldValue::Int(self.get_int(name, index)?)
629        } else {
630            FieldValue::Uint(self.get_uint(name, index)?)
631        })
632    }
633
634    /// Writes a signed integer into a field.
635    ///
636    /// # Arguments
637    ///
638    /// * `name` - the field name.
639    /// * `index` - the element to write, or `0` for a scalar field.
640    /// * `value` - the value to store.
641    ///
642    /// # Errors
643    ///
644    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
645    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
646    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
647    /// [`MavlinkError::ValueOutOfRange`] if the value does not fit the field's type.
648    pub fn set_int(&mut self, name: &str, index: usize, value: i64) -> Result<()> {
649        let (offset, field) = self.element(name, index)?;
650        let ty = field.ty;
651        if !ty.is_integer() {
652            return Err(MavlinkError::FieldTypeMismatch);
653        }
654        let out = MavlinkError::ValueOutOfRange;
655        match ty {
656            FieldType::I8 => {
657                self.write(offset, &i8::try_from(value).map_err(|_| out)?.to_le_bytes())
658            }
659            FieldType::I16 => self.write(
660                offset,
661                &i16::try_from(value).map_err(|_| out)?.to_le_bytes(),
662            ),
663            FieldType::I32 => self.write(
664                offset,
665                &i32::try_from(value).map_err(|_| out)?.to_le_bytes(),
666            ),
667            FieldType::I64 => self.write(offset, &value.to_le_bytes()),
668            _ => return self.set_uint(name, index, u64::try_from(value).map_err(|_| out)?),
669        }
670        Ok(())
671    }
672
673    /// Writes an unsigned integer into a field.
674    ///
675    /// # Arguments
676    ///
677    /// * `name` - the field name.
678    /// * `index` - the element to write, or `0` for a scalar field.
679    /// * `value` - the value to store.
680    ///
681    /// # Errors
682    ///
683    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
684    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
685    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
686    /// [`MavlinkError::ValueOutOfRange`] if the value does not fit the field's type.
687    pub fn set_uint(&mut self, name: &str, index: usize, value: u64) -> Result<()> {
688        let (offset, field) = self.element(name, index)?;
689        let ty = field.ty;
690        if !ty.is_integer() {
691            return Err(MavlinkError::FieldTypeMismatch);
692        }
693        let out = MavlinkError::ValueOutOfRange;
694        match ty {
695            FieldType::U8 | FieldType::Char => {
696                self.write(offset, &u8::try_from(value).map_err(|_| out)?.to_le_bytes())
697            }
698            FieldType::U16 => self.write(
699                offset,
700                &u16::try_from(value).map_err(|_| out)?.to_le_bytes(),
701            ),
702            FieldType::U32 => self.write(
703                offset,
704                &u32::try_from(value).map_err(|_| out)?.to_le_bytes(),
705            ),
706            FieldType::U64 => self.write(offset, &value.to_le_bytes()),
707            _ => return self.set_int(name, index, i64::try_from(value).map_err(|_| out)?),
708        }
709        Ok(())
710    }
711
712    /// Writes a floating-point field.
713    ///
714    /// # Arguments
715    ///
716    /// * `name` - the field name.
717    /// * `index` - the element to write, or `0` for a scalar field.
718    /// * `value` - the value to store, narrowed to single precision for a `float` field.
719    ///
720    /// # Errors
721    ///
722    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
723    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
724    /// and [`MavlinkError::FieldTypeMismatch`] for an integer field.
725    pub fn set_float(&mut self, name: &str, index: usize, value: f64) -> Result<()> {
726        let (offset, field) = self.element(name, index)?;
727        match field.ty {
728            FieldType::F32 => self.write(offset, &(value as f32).to_le_bytes()),
729            FieldType::F64 => self.write(offset, &value.to_le_bytes()),
730            _ => return Err(MavlinkError::FieldTypeMismatch),
731        }
732        Ok(())
733    }
734
735    /// Writes a value into a field, whichever kind it is.
736    ///
737    /// # Arguments
738    ///
739    /// * `name` - the field name.
740    /// * `index` - the element to write, or `0` for a scalar field.
741    /// * `value` - the value to store.
742    ///
743    /// # Errors
744    ///
745    /// Returns the same errors as the typed setter for the value's kind.
746    pub fn set(&mut self, name: &str, index: usize, value: FieldValue) -> Result<()> {
747        match value {
748            FieldValue::Int(value) => self.set_int(name, index, value),
749            FieldValue::Uint(value) => self.set_uint(name, index, value),
750            FieldValue::Float(value) => self.set_float(name, index, value),
751        }
752    }
753
754    /// Reads a field as a double, whatever its type.
755    ///
756    /// This is the reading a host language with one numeric type needs. An integer field
757    /// wider than 53 bits can exceed what a double represents exactly, so read those with
758    /// [`get_int`](Self::get_int) or [`get_uint`](Self::get_uint) where the exact value
759    /// matters.
760    ///
761    /// # Arguments
762    ///
763    /// * `name` - the field name.
764    /// * `index` - the element to read, or `0` for a scalar field.
765    ///
766    /// # Returns
767    ///
768    /// The value as a double.
769    ///
770    /// # Errors
771    ///
772    /// Returns [`MavlinkError::UnknownField`] if the message has no such field, and
773    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array.
774    pub fn get_number(&self, name: &str, index: usize) -> Result<f64> {
775        Ok(match self.get(name, index)? {
776            FieldValue::Int(value) => value as f64,
777            FieldValue::Uint(value) => value as f64,
778            FieldValue::Float(value) => value,
779        })
780    }
781
782    /// Writes a double into a field, converting it to the field's type.
783    ///
784    /// This is the writing a host language with one numeric type needs. A value bound for
785    /// an integer field must be a whole number within that field's range, so a fractional
786    /// or oversized value is refused rather than silently truncated.
787    ///
788    /// # Arguments
789    ///
790    /// * `name` - the field name.
791    /// * `index` - the element to write, or `0` for a scalar field.
792    /// * `value` - the value to store.
793    ///
794    /// # Errors
795    ///
796    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
797    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
798    /// and [`MavlinkError::ValueOutOfRange`] if an integer field is given a value that is
799    /// fractional, infinite, not a number, or outside the range its width holds.
800    pub fn set_number(&mut self, name: &str, index: usize, value: f64) -> Result<()> {
801        let (_, field) = self.element(name, index)?;
802        if !field.ty.is_integer() {
803            return self.set_float(name, index, value);
804        }
805        // Range first, because a cast saturates rather than failing, then the round trip,
806        // which is what rejects a fractional value without needing a floating-point library.
807        if field.ty.is_signed() {
808            if !(-TWO_POW_63..TWO_POW_63).contains(&value) {
809                return Err(MavlinkError::ValueOutOfRange);
810            }
811            let whole = value as i64;
812            if whole as f64 != value {
813                return Err(MavlinkError::ValueOutOfRange);
814            }
815            self.set_int(name, index, whole)
816        } else {
817            if !(0.0..TWO_POW_64).contains(&value) {
818                return Err(MavlinkError::ValueOutOfRange);
819            }
820            let whole = value as u64;
821            if whole as f64 != value {
822                return Err(MavlinkError::ValueOutOfRange);
823            }
824            self.set_uint(name, index, whole)
825        }
826    }
827
828    /// Copies the raw bytes of a byte-wide array field out.
829    ///
830    /// # Arguments
831    ///
832    /// * `name` - the field name.
833    /// * `out` - the destination, which must be at least the field's length.
834    ///
835    /// # Returns
836    ///
837    /// The number of bytes written, which is the field's declared length.
838    ///
839    /// # Errors
840    ///
841    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
842    /// [`MavlinkError::FieldTypeMismatch`] if the field is not an array of `char`,
843    /// `uint8_t`, or `int8_t`, and [`MavlinkError::PayloadTooLong`] if `out` is too small.
844    pub fn get_bytes(&self, name: &str, out: &mut [u8]) -> Result<usize> {
845        let (offset, field) = self.descriptor.locate(name)?;
846        if field.ty.size() != 1 || field.array_len == 0 {
847            return Err(MavlinkError::FieldTypeMismatch);
848        }
849        let len = field.elements();
850        if out.len() < len {
851            return Err(MavlinkError::PayloadTooLong);
852        }
853        out[..len].copy_from_slice(&self.payload[offset..offset + len]);
854        Ok(len)
855    }
856
857    /// Writes the raw bytes of a byte-wide array field, zero-padding the rest.
858    ///
859    /// # Arguments
860    ///
861    /// * `name` - the field name.
862    /// * `bytes` - the bytes to store, at most the field's declared length.
863    ///
864    /// # Errors
865    ///
866    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
867    /// [`MavlinkError::FieldTypeMismatch`] if the field is not an array of `char`,
868    /// `uint8_t`, or `int8_t`, and [`MavlinkError::PayloadTooLong`] if the bytes are
869    /// longer than the field.
870    pub fn set_bytes(&mut self, name: &str, bytes: &[u8]) -> Result<()> {
871        let (offset, field) = self.descriptor.locate(name)?;
872        if field.ty.size() != 1 || field.array_len == 0 {
873            return Err(MavlinkError::FieldTypeMismatch);
874        }
875        let len = field.elements();
876        if bytes.len() > len {
877            return Err(MavlinkError::PayloadTooLong);
878        }
879        self.payload[offset..offset + len].fill(0);
880        self.payload[offset..offset + bytes.len()].copy_from_slice(bytes);
881        Ok(())
882    }
883
884    /// Reads a `char` array as text, stopping at the padding.
885    ///
886    /// MAVLink carries a string in a fixed-length `char` array, padded with zeros when the
887    /// text is shorter and left unterminated when it exactly fills the field.
888    ///
889    /// # Arguments
890    ///
891    /// * `name` - the field name.
892    ///
893    /// # Returns
894    ///
895    /// The text, without its padding.
896    ///
897    /// # Errors
898    ///
899    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
900    /// [`MavlinkError::FieldTypeMismatch`] if the field is not a `char` array, and
901    /// [`MavlinkError::BadPayload`] if the bytes are not valid UTF-8.
902    ///
903    /// # Examples
904    ///
905    /// ```
906    /// use pamoja_mavlink::dialect::{descriptor, DynamicMessage};
907    ///
908    /// let shape = descriptor(253).expect("STATUSTEXT is in the common dialect");
909    /// let mut status = DynamicMessage::new(shape)?;
910    /// status.set_text("text", "preflight checks passed")?;
911    /// assert_eq!(status.text("text")?, "preflight checks passed");
912    /// # Ok::<(), pamoja_mavlink::MavlinkError>(())
913    /// ```
914    pub fn text(&self, name: &str) -> Result<&str> {
915        let (offset, field) = self.descriptor.locate(name)?;
916        if field.ty != FieldType::Char || field.array_len == 0 {
917            return Err(MavlinkError::FieldTypeMismatch);
918        }
919        let bytes = &self.payload[offset..offset + field.elements()];
920        let end = bytes
921            .iter()
922            .position(|byte| *byte == 0)
923            .unwrap_or(bytes.len());
924        core::str::from_utf8(&bytes[..end]).map_err(|_| MavlinkError::BadPayload)
925    }
926
927    /// Writes text into a `char` array, padding the rest with zeros.
928    ///
929    /// # Arguments
930    ///
931    /// * `name` - the field name.
932    /// * `text` - the text to store, at most the field's declared length.
933    ///
934    /// # Errors
935    ///
936    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
937    /// [`MavlinkError::FieldTypeMismatch`] if the field is not a `char` array, and
938    /// [`MavlinkError::PayloadTooLong`] if the text is longer than the field.
939    pub fn set_text(&mut self, name: &str, text: &str) -> Result<()> {
940        let (_, field) = self.descriptor.locate(name)?;
941        if field.ty != FieldType::Char {
942            return Err(MavlinkError::FieldTypeMismatch);
943        }
944        self.set_bytes(name, text.as_bytes())
945    }
946
947    fn element(&self, name: &str, index: usize) -> Result<(usize, &'a FieldDescriptor<'a>)> {
948        let (offset, field) = self.descriptor.locate(name)?;
949        if index >= field.elements() {
950            return Err(MavlinkError::FieldIndexOutOfRange);
951        }
952        Ok((offset + index * field.ty.size(), field))
953    }
954
955    fn read<const N: usize>(&self, offset: usize) -> [u8; N] {
956        let mut bytes = [0; N];
957        bytes.copy_from_slice(&self.payload[offset..offset + N]);
958        bytes
959    }
960
961    fn write(&mut self, offset: usize, bytes: &[u8]) {
962        self.payload[offset..offset + bytes.len()].copy_from_slice(bytes);
963    }
964}
965
966/// Returns the shape of a common-dialect message, if this crate types it.
967///
968/// # Arguments
969///
970/// * `msgid` - the message id to look up.
971///
972/// # Returns
973///
974/// The descriptor, or [`None`] for an id outside the typed set; a caller can describe such
975/// a message itself with [`MessageDescriptorBuilder`].
976///
977/// # Examples
978///
979/// ```
980/// use pamoja_mavlink::dialect::descriptor;
981///
982/// let heartbeat = descriptor(0).expect("HEARTBEAT is in the common dialect");
983/// assert_eq!(heartbeat.name, "HEARTBEAT");
984/// assert_eq!(heartbeat.wire_len(), 9);
985/// assert!(descriptor(50_000).is_none());
986/// ```
987pub fn descriptor(msgid: u32) -> Option<&'static MessageDescriptor<'static>> {
988    DESCRIPTORS.iter().copied().find(|shape| shape.id == msgid)
989}
990
991/// Returns the shape of a common-dialect message by name.
992///
993/// # Arguments
994///
995/// * `name` - the message name, such as `"GLOBAL_POSITION_INT"`.
996///
997/// # Returns
998///
999/// The descriptor, or [`None`] for a name outside the typed set.
1000///
1001/// # Examples
1002///
1003/// ```
1004/// use pamoja_mavlink::dialect::descriptor_by_name;
1005///
1006/// let position = descriptor_by_name("GLOBAL_POSITION_INT").expect("a typed message");
1007/// assert_eq!(position.id, 33);
1008/// ```
1009pub fn descriptor_by_name(name: &str) -> Option<&'static MessageDescriptor<'static>> {
1010    DESCRIPTORS.iter().copied().find(|shape| shape.name == name)
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016    use crate::dialect::{
1017        AutopilotVersion, BatteryStatus, GlobalPositionInt, Heartbeat, Message, MissionAck,
1018        Statustext,
1019    };
1020    use crate::Header;
1021
1022    // Holds each typed message and its descriptor to the same shape. A field mistyped in
1023    // one and not the other changes the bytes on the wire, which this catches at the
1024    // declaration rather than against a live autopilot.
1025    macro_rules! assert_same_shape {
1026        ($( $message:ty ),+ $(,)?) => {
1027            $(
1028                let shape = <$message as Message>::DESCRIPTOR;
1029                assert_eq!(shape.id, <$message as Message>::ID);
1030                assert_eq!(shape.name, <$message as Message>::NAME);
1031                assert_eq!(shape.crc_extra, <$message as Message>::CRC_EXTRA);
1032                assert_eq!(shape.wire_len(), <$message as Message>::WIRE_LEN);
1033                assert_eq!(shape.derived_crc_extra(), <$message as Message>::CRC_EXTRA);
1034
1035                let base: Vec<(&str, &str, u8)> = shape
1036                    .fields
1037                    .iter()
1038                    .filter(|field| !field.extension)
1039                    .map(|field| (field.ty.wire_name(), field.name, field.array_len))
1040                    .collect();
1041                assert_eq!(base.as_slice(), <$message as Message>::BASE_FIELDS);
1042            )+
1043        };
1044    }
1045
1046    #[test]
1047    fn every_typed_message_agrees_with_its_descriptor() {
1048        assert_same_shape!(
1049            crate::dialect::Heartbeat,
1050            crate::dialect::SysStatus,
1051            crate::dialect::SystemTime,
1052            crate::dialect::Ping,
1053            crate::dialect::SetMode,
1054            crate::dialect::ParamRequestRead,
1055            crate::dialect::ParamRequestList,
1056            crate::dialect::ParamValue,
1057            crate::dialect::ParamSet,
1058            crate::dialect::GpsRawInt,
1059            crate::dialect::Attitude,
1060            crate::dialect::AttitudeQuaternion,
1061            crate::dialect::LocalPositionNed,
1062            crate::dialect::GlobalPositionInt,
1063            crate::dialect::ServoOutputRaw,
1064            crate::dialect::MissionRequest,
1065            crate::dialect::MissionCurrent,
1066            crate::dialect::MissionRequestList,
1067            crate::dialect::MissionCount,
1068            crate::dialect::MissionClearAll,
1069            crate::dialect::MissionAck,
1070            crate::dialect::MissionRequestInt,
1071            crate::dialect::RcChannels,
1072            crate::dialect::ManualControl,
1073            crate::dialect::MissionItemInt,
1074            crate::dialect::VfrHud,
1075            crate::dialect::CommandInt,
1076            crate::dialect::CommandLong,
1077            crate::dialect::CommandAck,
1078            crate::dialect::SetPositionTargetLocalNed,
1079            crate::dialect::SetPositionTargetGlobalInt,
1080            crate::dialect::BatteryStatus,
1081            crate::dialect::AutopilotVersion,
1082            crate::dialect::HomePosition,
1083            crate::dialect::ExtendedSysState,
1084            crate::dialect::Statustext,
1085        );
1086    }
1087
1088    #[test]
1089    fn the_registry_covers_exactly_the_typed_messages() {
1090        assert_eq!(DESCRIPTORS.len(), 36);
1091        for shape in DESCRIPTORS {
1092            assert_eq!(crate::dialect::crc_extra(shape.id), Some(shape.crc_extra));
1093            assert_eq!(descriptor(shape.id), Some(*shape));
1094            assert_eq!(descriptor_by_name(shape.name), Some(*shape));
1095        }
1096        assert!(descriptor(50_000).is_none());
1097        assert!(descriptor_by_name("BATTERY_CELLS").is_none());
1098    }
1099
1100    #[test]
1101    fn a_descriptor_writes_the_bytes_its_typed_message_does() -> Result<()> {
1102        let typed = Heartbeat {
1103            custom_mode: 0x0DF0_AD8B,
1104            type_: 2,
1105            autopilot: 3,
1106            base_mode: 81,
1107            system_status: 4,
1108            mavlink_version: 3,
1109        };
1110        let mut expected = [0u8; MAX_PAYLOAD];
1111        let len = typed.encode(&mut expected);
1112
1113        let mut dynamic = DynamicMessage::new(Heartbeat::DESCRIPTOR)?;
1114        dynamic.set_uint("custom_mode", 0, 0x0DF0_AD8B)?;
1115        dynamic.set_uint("type", 0, 2)?;
1116        dynamic.set_uint("autopilot", 0, 3)?;
1117        dynamic.set_uint("base_mode", 0, 81)?;
1118        dynamic.set_uint("system_status", 0, 4)?;
1119        dynamic.set_uint("mavlink_version", 0, 3)?;
1120
1121        assert_eq!(dynamic.payload(), &expected[..len]);
1122        Ok(())
1123    }
1124
1125    #[test]
1126    fn signed_and_floating_fields_round_trip() -> Result<()> {
1127        let mut position = DynamicMessage::new(GlobalPositionInt::DESCRIPTOR)?;
1128        position.set_int("lat", 0, -33_856_780)?;
1129        position.set_int("lon", 0, 151_215_300)?;
1130        position.set_int("vz", 0, -250)?;
1131        position.set_uint("hdg", 0, 18_000)?;
1132
1133        let decoded = GlobalPositionInt::decode(position.payload())?;
1134        assert_eq!(decoded.lat, -33_856_780);
1135        assert_eq!(decoded.vz, -250);
1136        assert_eq!(position.get_int("lon", 0)?, 151_215_300);
1137        assert_eq!(position.get_uint("hdg", 0)?, 18_000);
1138
1139        let mut attitude = DynamicMessage::new(crate::dialect::Attitude::DESCRIPTOR)?;
1140        attitude.set_float("roll", 0, -0.5)?;
1141        assert_eq!(attitude.get_float("roll", 0)?, -0.5);
1142        assert_eq!(attitude.get("roll", 0)?, FieldValue::Float(-0.5));
1143        Ok(())
1144    }
1145
1146    #[test]
1147    fn one_numeric_type_reaches_every_field() -> Result<()> {
1148        let mut position = DynamicMessage::new(GlobalPositionInt::DESCRIPTOR)?;
1149        position.set_number("lat", 0, -33_856_780.0)?;
1150        position.set_number("hdg", 0, 18_000.0)?;
1151        assert_eq!(position.get_number("lat", 0)?, -33_856_780.0);
1152        assert_eq!(position.get_number("hdg", 0)?, 18_000.0);
1153
1154        let mut attitude = DynamicMessage::new(crate::dialect::Attitude::DESCRIPTOR)?;
1155        attitude.set_number("roll", 0, 0.25)?;
1156        assert_eq!(attitude.get_number("roll", 0)?, 0.25);
1157
1158        // An integer field takes only a value it can hold exactly.
1159        for refused in [1.5, f64::NAN, f64::INFINITY, -1.0, 1e30] {
1160            assert_eq!(
1161                position.set_number("hdg", 0, refused).unwrap_err(),
1162                MavlinkError::ValueOutOfRange
1163            );
1164        }
1165        assert_eq!(position.get_number("hdg", 0)?, 18_000.0);
1166        Ok(())
1167    }
1168
1169    #[test]
1170    fn arrays_and_text_are_addressed_by_element() -> Result<()> {
1171        let mut battery = DynamicMessage::new(BatteryStatus::DESCRIPTOR)?;
1172        battery.set_uint("voltages", 0, 4_150)?;
1173        battery.set_uint("voltages", 9, 3_990)?;
1174        assert_eq!(battery.get_uint("voltages", 9)?, 3_990);
1175        assert_eq!(BatteryStatus::decode(battery.payload())?.voltages[9], 3_990);
1176        assert_eq!(
1177            battery.get_uint("voltages", 10),
1178            Err(MavlinkError::FieldIndexOutOfRange)
1179        );
1180
1181        let mut version = DynamicMessage::new(AutopilotVersion::DESCRIPTOR)?;
1182        version.set_bytes("flight_custom_version", &[1, 2, 3])?;
1183        let mut out = [0u8; 8];
1184        assert_eq!(version.get_bytes("flight_custom_version", &mut out)?, 8);
1185        assert_eq!(out, [1, 2, 3, 0, 0, 0, 0, 0]);
1186
1187        let mut status = DynamicMessage::new(Statustext::DESCRIPTOR)?;
1188        status.set_text("text", "battery low")?;
1189        assert_eq!(status.text("text")?, "battery low");
1190        assert_eq!(status.get_uint("severity", 0)?, 0);
1191        Ok(())
1192    }
1193
1194    #[test]
1195    fn extension_fields_sit_after_the_base_fields_and_leave_the_seed_alone() -> Result<()> {
1196        let shape = MissionAck::DESCRIPTOR;
1197        let extension = shape
1198            .field("mission_type")
1199            .expect("MISSION_ACK carries the extension");
1200        assert!(extension.extension);
1201        assert_eq!(shape.offset_of("mission_type"), Some(shape.base_len()));
1202        assert_eq!(shape.derived_crc_extra(), MissionAck::CRC_EXTRA);
1203
1204        // A peer that predates the extension sends only the base fields, and it reads zero.
1205        let base = vec![0u8; shape.base_len()];
1206        let received = DynamicMessage::decode(shape, &base)?;
1207        assert_eq!(received.get_uint("mission_type", 0)?, 0);
1208        Ok(())
1209    }
1210
1211    #[test]
1212    fn a_field_is_rejected_when_it_is_missing_or_misused() -> Result<()> {
1213        let mut heartbeat = DynamicMessage::new(Heartbeat::DESCRIPTOR)?;
1214        assert_eq!(
1215            heartbeat.get_uint("throttle", 0),
1216            Err(MavlinkError::UnknownField)
1217        );
1218        assert_eq!(
1219            heartbeat.set_float("type", 0, 1.0),
1220            Err(MavlinkError::FieldTypeMismatch)
1221        );
1222        assert_eq!(
1223            heartbeat.set_uint("type", 0, 300),
1224            Err(MavlinkError::ValueOutOfRange)
1225        );
1226        assert_eq!(
1227            heartbeat.set_int("type", 0, -1),
1228            Err(MavlinkError::ValueOutOfRange)
1229        );
1230        assert_eq!(heartbeat.text("type"), Err(MavlinkError::FieldTypeMismatch));
1231
1232        let mut attitude = DynamicMessage::new(crate::dialect::Attitude::DESCRIPTOR)?;
1233        assert_eq!(
1234            attitude.set_uint("roll", 0, 1),
1235            Err(MavlinkError::FieldTypeMismatch)
1236        );
1237        assert_eq!(
1238            attitude.get_int("roll", 0),
1239            Err(MavlinkError::FieldTypeMismatch)
1240        );
1241        Ok(())
1242    }
1243
1244    #[test]
1245    fn a_truncated_payload_reads_its_missing_bytes_as_zero() -> Result<()> {
1246        let shape = Heartbeat::DESCRIPTOR;
1247        let frame = DynamicMessage::new(shape)?.to_frame(Header::new(1, 1, 0))?;
1248
1249        // MAVLink 2 trims trailing zeros but never the whole payload, so an all-zero
1250        // HEARTBEAT goes out as a single byte and the receiver restores the rest.
1251        assert_eq!(frame.payload(), &[0]);
1252        let received = DynamicMessage::decode(shape, frame.payload())?;
1253        assert_eq!(received.get_uint("custom_mode", 0)?, 0);
1254        assert_eq!(
1255            DynamicMessage::decode(shape, &[0u8; 10]).unwrap_err(),
1256            MavlinkError::BadPayload
1257        );
1258        Ok(())
1259    }
1260
1261    #[test]
1262    fn a_builder_puts_declared_fields_into_wire_order() -> Result<()> {
1263        // HEARTBEAT as its definition declares it: the 32-bit field sits fourth, and wire
1264        // order pulls it to the front.
1265        let heartbeat = MessageDescriptorBuilder::new(0, "HEARTBEAT")
1266            .field("type", FieldType::U8, 0)
1267            .field("autopilot", FieldType::U8, 0)
1268            .field("base_mode", FieldType::U8, 0)
1269            .field("custom_mode", FieldType::U32, 0)
1270            .field("system_status", FieldType::U8, 0)
1271            .field("mavlink_version", FieldType::U8, 0)
1272            .build()?;
1273
1274        assert_eq!(heartbeat.crc_extra(), Heartbeat::CRC_EXTRA);
1275        heartbeat.with_descriptor(|shape| {
1276            assert_eq!(shape.fields, Heartbeat::DESCRIPTOR.fields);
1277        });
1278
1279        // SYS_STATUS declares an int8 in the middle of its 16-bit fields, so a stable sort
1280        // by size is what moves it to the end and nothing else with it. Its extensions stay
1281        // in the order they are declared, after every base field.
1282        let status = MessageDescriptorBuilder::new(1, "SYS_STATUS")
1283            .field("onboard_control_sensors_present", FieldType::U32, 0)
1284            .field("onboard_control_sensors_enabled", FieldType::U32, 0)
1285            .field("onboard_control_sensors_health", FieldType::U32, 0)
1286            .field("load", FieldType::U16, 0)
1287            .field("voltage_battery", FieldType::U16, 0)
1288            .field("current_battery", FieldType::I16, 0)
1289            .field("battery_remaining", FieldType::I8, 0)
1290            .field("drop_rate_comm", FieldType::U16, 0)
1291            .field("errors_comm", FieldType::U16, 0)
1292            .field("errors_count1", FieldType::U16, 0)
1293            .field("errors_count2", FieldType::U16, 0)
1294            .field("errors_count3", FieldType::U16, 0)
1295            .field("errors_count4", FieldType::U16, 0)
1296            .extension(
1297                "onboard_control_sensors_present_extended",
1298                FieldType::U32,
1299                0,
1300            )
1301            .extension(
1302                "onboard_control_sensors_enabled_extended",
1303                FieldType::U32,
1304                0,
1305            )
1306            .extension("onboard_control_sensors_health_extended", FieldType::U32, 0)
1307            .build()?;
1308
1309        assert_eq!(status.crc_extra(), crate::dialect::SysStatus::CRC_EXTRA);
1310        status.with_descriptor(|shape| {
1311            assert_eq!(shape.fields, crate::dialect::SysStatus::DESCRIPTOR.fields);
1312        });
1313        Ok(())
1314    }
1315
1316    #[test]
1317    fn a_builder_rejects_a_shape_it_cannot_describe() {
1318        let duplicated = MessageDescriptorBuilder::new(1, "TWICE")
1319            .field("value", FieldType::U8, 0)
1320            .field("value", FieldType::U16, 0)
1321            .build();
1322        assert_eq!(duplicated.unwrap_err(), MavlinkError::DuplicateField);
1323
1324        let overlong = MessageDescriptorBuilder::new(1, "HUGE")
1325            .field("payload", FieldType::U32, 64)
1326            .build();
1327        assert_eq!(overlong.unwrap_err(), MavlinkError::PayloadTooLong);
1328    }
1329
1330    #[test]
1331    fn an_owned_dialect_resolves_its_own_ids_and_falls_back_to_the_common_one() -> Result<()> {
1332        let mut dialect = OwnedDialect::new();
1333        assert!(dialect.is_empty());
1334        dialect.insert(
1335            MessageDescriptorBuilder::new(50_000, "BATTERY_CELLS")
1336                .field("cell_mv", FieldType::U16, 6)
1337                .field("pack_id", FieldType::U8, 0)
1338                .build()?,
1339        );
1340        assert_eq!(dialect.len(), 1);
1341
1342        let private = dialect.get(50_000).expect("just inserted");
1343        assert_eq!(dialect.crc_extra(50_000), Some(private.crc_extra()));
1344        assert_eq!(dialect.crc_extra(0), Some(Heartbeat::CRC_EXTRA));
1345        assert_eq!(dialect.crc_extra(49_999), None);
1346        assert!(dialect.by_name("BATTERY_CELLS").is_some());
1347
1348        // Replacing a shape keeps one entry rather than shadowing the first.
1349        dialect.insert(
1350            MessageDescriptorBuilder::new(50_000, "BATTERY_CELLS")
1351                .field("pack_id", FieldType::U8, 0)
1352                .build()?,
1353        );
1354        assert_eq!(dialect.len(), 1);
1355        Ok(())
1356    }
1357
1358    #[test]
1359    fn an_owned_copy_keeps_the_shape_it_was_taken_from() {
1360        let owned = OwnedMessageDescriptor::from_descriptor(Statustext::DESCRIPTOR);
1361        assert_eq!(owned.id(), Statustext::ID);
1362        assert_eq!(owned.crc_extra(), Statustext::CRC_EXTRA);
1363        owned.with_descriptor(|shape| {
1364            assert_eq!(shape.fields, Statustext::DESCRIPTOR.fields);
1365            assert_eq!(shape.wire_len(), Statustext::WIRE_LEN);
1366        });
1367    }
1368}