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::{descriptor, DynamicMessage, Heartbeat, Message};
357/// use pamoja_mavlink::Header;
358///
359/// let shape = descriptor(Heartbeat::ID).expect("HEARTBEAT is in the common dialect");
360///
361/// // Fill the message in by name, the way a caller reading a dialect definition would.
362/// let mut heartbeat = DynamicMessage::new(shape)?;
363/// heartbeat.set_uint("type", 0, 18)?; // MAV_TYPE_ONBOARD_CONTROLLER
364/// heartbeat.set_uint("system_status", 0, 4)?; // MAV_STATE_ACTIVE
365/// heartbeat.set_uint("mavlink_version", 0, 3)?;
366///
367/// // It is an ordinary frame, so a typed receiver reads it back unchanged.
368/// let frame = heartbeat.to_frame(Header::new(1, 1, 0))?;
369/// assert_eq!(Heartbeat::decode(frame.payload())?.system_status, 4);
370/// # Ok::<(), pamoja_mavlink::MavlinkError>(())
371/// ```
372#[derive(Clone, Debug)]
373pub struct DynamicMessage<'a> {
374    descriptor: &'a MessageDescriptor<'a>,
375    payload: [u8; MAX_PAYLOAD],
376    len: usize,
377}
378
379impl<'a> DynamicMessage<'a> {
380    /// Creates a message with every field zero.
381    ///
382    /// # Arguments
383    ///
384    /// * `descriptor` - the shape of the message to build.
385    ///
386    /// # Returns
387    ///
388    /// The zeroed message, ready for its fields to be set.
389    ///
390    /// # Errors
391    ///
392    /// Returns [`MavlinkError::PayloadTooLong`] if the descriptor's fields exceed
393    /// [`MAX_PAYLOAD`] bytes, which no message from a valid dialect does.
394    pub fn new(descriptor: &'a MessageDescriptor<'a>) -> Result<Self> {
395        let len = descriptor.wire_len();
396        if len > MAX_PAYLOAD {
397            return Err(MavlinkError::PayloadTooLong);
398        }
399        Ok(Self {
400            descriptor,
401            payload: [0; MAX_PAYLOAD],
402            len,
403        })
404    }
405
406    /// Reads a message out of a frame payload.
407    ///
408    /// A short payload is zero-extended, as MAVLink 2 truncation requires, so a frame from
409    /// a peer that omitted trailing zeros or predates an extension field still decodes.
410    ///
411    /// # Arguments
412    ///
413    /// * `descriptor` - the shape to read the payload as.
414    /// * `payload` - the frame payload.
415    ///
416    /// # Returns
417    ///
418    /// The decoded message.
419    ///
420    /// # Errors
421    ///
422    /// Returns [`MavlinkError::BadPayload`] if the payload is longer than the descriptor
423    /// describes, and [`MavlinkError::PayloadTooLong`] if the descriptor itself does not
424    /// fit a frame.
425    ///
426    /// # Examples
427    ///
428    /// ```
429    /// use pamoja_mavlink::dialect::{descriptor, DynamicMessage};
430    /// use pamoja_mavlink::Frame;
431    ///
432    /// let shape = descriptor(0).expect("HEARTBEAT is in the common dialect");
433    /// let received = Frame::parse(
434    ///     &[0xfd, 0x09, 0, 0, 7, 1, 1, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 4, 3, 0x75, 0x3a],
435    ///     shape.crc_extra,
436    /// )?;
437    ///
438    /// let heartbeat = DynamicMessage::decode(shape, received.payload())?;
439    /// assert_eq!(heartbeat.get_uint("type", 0)?, 18);
440    /// # Ok::<(), pamoja_mavlink::MavlinkError>(())
441    /// ```
442    pub fn decode(descriptor: &'a MessageDescriptor<'a>, payload: &[u8]) -> Result<Self> {
443        let len = descriptor.wire_len();
444        if len > MAX_PAYLOAD {
445            return Err(MavlinkError::PayloadTooLong);
446        }
447        if payload.len() > len {
448            return Err(MavlinkError::BadPayload);
449        }
450        let mut message = Self {
451            descriptor,
452            payload: [0; MAX_PAYLOAD],
453            len,
454        };
455        message.payload[..payload.len()].copy_from_slice(payload);
456        Ok(message)
457    }
458
459    /// Returns the shape this message is read and written against.
460    ///
461    /// # Returns
462    ///
463    /// The descriptor.
464    pub fn descriptor(&self) -> &'a MessageDescriptor<'a> {
465        self.descriptor
466    }
467
468    /// Returns the message's bytes as they go on the wire.
469    ///
470    /// # Returns
471    ///
472    /// The payload, including any trailing zeros; a frame truncates those itself.
473    pub fn payload(&self) -> &[u8] {
474        &self.payload[..self.len]
475    }
476
477    /// Builds a v2 frame carrying this message.
478    ///
479    /// # Arguments
480    ///
481    /// * `header` - the addressing fields to stamp on the frame.
482    ///
483    /// # Returns
484    ///
485    /// The frame ready to send.
486    ///
487    /// # Errors
488    ///
489    /// Returns [`MavlinkError::PayloadTooLong`] if the message does not fit a frame.
490    pub fn to_frame(&self, header: Header) -> Result<Frame> {
491        Frame::encode_v2(
492            header,
493            self.descriptor.id,
494            self.payload(),
495            self.descriptor.crc_extra,
496        )
497    }
498
499    /// Reads a field as a signed integer.
500    ///
501    /// Any integer field can be read this way, whatever its width or sign.
502    ///
503    /// # Arguments
504    ///
505    /// * `name` - the field name.
506    /// * `index` - the element to read, or `0` for a scalar field.
507    ///
508    /// # Returns
509    ///
510    /// The value.
511    ///
512    /// # Errors
513    ///
514    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
515    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
516    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
517    /// [`MavlinkError::ValueOutOfRange`] for a `uint64_t` value above [`i64::MAX`].
518    pub fn get_int(&self, name: &str, index: usize) -> Result<i64> {
519        let (offset, field) = self.element(name, index)?;
520        if !field.ty.is_integer() {
521            return Err(MavlinkError::FieldTypeMismatch);
522        }
523        Ok(match field.ty {
524            FieldType::I8 => self.read::<1>(offset)[0] as i8 as i64,
525            FieldType::I16 => i16::from_le_bytes(self.read::<2>(offset)) as i64,
526            FieldType::I32 => i32::from_le_bytes(self.read::<4>(offset)) as i64,
527            FieldType::I64 => i64::from_le_bytes(self.read::<8>(offset)),
528            FieldType::U8 | FieldType::Char => self.read::<1>(offset)[0] as i64,
529            FieldType::U16 => u16::from_le_bytes(self.read::<2>(offset)) as i64,
530            FieldType::U32 => u32::from_le_bytes(self.read::<4>(offset)) as i64,
531            FieldType::U64 => i64::try_from(u64::from_le_bytes(self.read::<8>(offset)))
532                .map_err(|_| MavlinkError::ValueOutOfRange)?,
533            FieldType::F32 | FieldType::F64 => unreachable!(),
534        })
535    }
536
537    /// Reads a field as an unsigned integer.
538    ///
539    /// Any integer field can be read this way, whatever its width or sign.
540    ///
541    /// # Arguments
542    ///
543    /// * `name` - the field name.
544    /// * `index` - the element to read, or `0` for a scalar field.
545    ///
546    /// # Returns
547    ///
548    /// The value.
549    ///
550    /// # Errors
551    ///
552    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
553    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
554    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
555    /// [`MavlinkError::ValueOutOfRange`] for a negative value.
556    pub fn get_uint(&self, name: &str, index: usize) -> Result<u64> {
557        let (offset, field) = self.element(name, index)?;
558        if !field.ty.is_integer() {
559            return Err(MavlinkError::FieldTypeMismatch);
560        }
561        if field.ty.is_signed() {
562            return u64::try_from(self.get_int(name, index)?)
563                .map_err(|_| MavlinkError::ValueOutOfRange);
564        }
565        Ok(match field.ty {
566            FieldType::U8 | FieldType::Char => self.read::<1>(offset)[0] as u64,
567            FieldType::U16 => u16::from_le_bytes(self.read::<2>(offset)) as u64,
568            FieldType::U32 => u32::from_le_bytes(self.read::<4>(offset)) as u64,
569            FieldType::U64 => u64::from_le_bytes(self.read::<8>(offset)),
570            _ => unreachable!(),
571        })
572    }
573
574    /// Reads a floating-point field.
575    ///
576    /// # Arguments
577    ///
578    /// * `name` - the field name.
579    /// * `index` - the element to read, or `0` for a scalar field.
580    ///
581    /// # Returns
582    ///
583    /// The value, widened to double precision for a `float` field.
584    ///
585    /// # Errors
586    ///
587    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
588    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
589    /// and [`MavlinkError::FieldTypeMismatch`] for an integer field.
590    pub fn get_float(&self, name: &str, index: usize) -> Result<f64> {
591        let (offset, field) = self.element(name, index)?;
592        Ok(match field.ty {
593            FieldType::F32 => f32::from_le_bytes(self.read::<4>(offset)) as f64,
594            FieldType::F64 => f64::from_le_bytes(self.read::<8>(offset)),
595            _ => return Err(MavlinkError::FieldTypeMismatch),
596        })
597    }
598
599    /// Reads a field as whichever kind of value its type calls for.
600    ///
601    /// # Arguments
602    ///
603    /// * `name` - the field name.
604    /// * `index` - the element to read, or `0` for a scalar field.
605    ///
606    /// # Returns
607    ///
608    /// The value.
609    ///
610    /// # Errors
611    ///
612    /// Returns [`MavlinkError::UnknownField`] if the message has no such field, and
613    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array.
614    pub fn get(&self, name: &str, index: usize) -> Result<FieldValue> {
615        let (_, field) = self.element(name, index)?;
616        Ok(if !field.ty.is_integer() {
617            FieldValue::Float(self.get_float(name, index)?)
618        } else if field.ty.is_signed() {
619            FieldValue::Int(self.get_int(name, index)?)
620        } else {
621            FieldValue::Uint(self.get_uint(name, index)?)
622        })
623    }
624
625    /// Writes a signed integer into a field.
626    ///
627    /// # Arguments
628    ///
629    /// * `name` - the field name.
630    /// * `index` - the element to write, or `0` for a scalar field.
631    /// * `value` - the value to store.
632    ///
633    /// # Errors
634    ///
635    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
636    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
637    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
638    /// [`MavlinkError::ValueOutOfRange`] if the value does not fit the field's type.
639    pub fn set_int(&mut self, name: &str, index: usize, value: i64) -> Result<()> {
640        let (offset, field) = self.element(name, index)?;
641        let ty = field.ty;
642        if !ty.is_integer() {
643            return Err(MavlinkError::FieldTypeMismatch);
644        }
645        let out = MavlinkError::ValueOutOfRange;
646        match ty {
647            FieldType::I8 => {
648                self.write(offset, &i8::try_from(value).map_err(|_| out)?.to_le_bytes())
649            }
650            FieldType::I16 => self.write(
651                offset,
652                &i16::try_from(value).map_err(|_| out)?.to_le_bytes(),
653            ),
654            FieldType::I32 => self.write(
655                offset,
656                &i32::try_from(value).map_err(|_| out)?.to_le_bytes(),
657            ),
658            FieldType::I64 => self.write(offset, &value.to_le_bytes()),
659            _ => return self.set_uint(name, index, u64::try_from(value).map_err(|_| out)?),
660        }
661        Ok(())
662    }
663
664    /// Writes an unsigned integer into a field.
665    ///
666    /// # Arguments
667    ///
668    /// * `name` - the field name.
669    /// * `index` - the element to write, or `0` for a scalar field.
670    /// * `value` - the value to store.
671    ///
672    /// # Errors
673    ///
674    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
675    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
676    /// [`MavlinkError::FieldTypeMismatch`] for a floating-point field, and
677    /// [`MavlinkError::ValueOutOfRange`] if the value does not fit the field's type.
678    pub fn set_uint(&mut self, name: &str, index: usize, value: u64) -> Result<()> {
679        let (offset, field) = self.element(name, index)?;
680        let ty = field.ty;
681        if !ty.is_integer() {
682            return Err(MavlinkError::FieldTypeMismatch);
683        }
684        let out = MavlinkError::ValueOutOfRange;
685        match ty {
686            FieldType::U8 | FieldType::Char => {
687                self.write(offset, &u8::try_from(value).map_err(|_| out)?.to_le_bytes())
688            }
689            FieldType::U16 => self.write(
690                offset,
691                &u16::try_from(value).map_err(|_| out)?.to_le_bytes(),
692            ),
693            FieldType::U32 => self.write(
694                offset,
695                &u32::try_from(value).map_err(|_| out)?.to_le_bytes(),
696            ),
697            FieldType::U64 => self.write(offset, &value.to_le_bytes()),
698            _ => return self.set_int(name, index, i64::try_from(value).map_err(|_| out)?),
699        }
700        Ok(())
701    }
702
703    /// Writes a floating-point field.
704    ///
705    /// # Arguments
706    ///
707    /// * `name` - the field name.
708    /// * `index` - the element to write, or `0` for a scalar field.
709    /// * `value` - the value to store, narrowed to single precision for a `float` field.
710    ///
711    /// # Errors
712    ///
713    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
714    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
715    /// and [`MavlinkError::FieldTypeMismatch`] for an integer field.
716    pub fn set_float(&mut self, name: &str, index: usize, value: f64) -> Result<()> {
717        let (offset, field) = self.element(name, index)?;
718        match field.ty {
719            FieldType::F32 => self.write(offset, &(value as f32).to_le_bytes()),
720            FieldType::F64 => self.write(offset, &value.to_le_bytes()),
721            _ => return Err(MavlinkError::FieldTypeMismatch),
722        }
723        Ok(())
724    }
725
726    /// Writes a value into a field, whichever kind it is.
727    ///
728    /// # Arguments
729    ///
730    /// * `name` - the field name.
731    /// * `index` - the element to write, or `0` for a scalar field.
732    /// * `value` - the value to store.
733    ///
734    /// # Errors
735    ///
736    /// Returns the same errors as the typed setter for the value's kind.
737    pub fn set(&mut self, name: &str, index: usize, value: FieldValue) -> Result<()> {
738        match value {
739            FieldValue::Int(value) => self.set_int(name, index, value),
740            FieldValue::Uint(value) => self.set_uint(name, index, value),
741            FieldValue::Float(value) => self.set_float(name, index, value),
742        }
743    }
744
745    /// Reads a field as a double, whatever its type.
746    ///
747    /// This is the reading a host language with one numeric type needs. An integer field
748    /// wider than 53 bits can exceed what a double represents exactly, so read those with
749    /// [`get_int`](Self::get_int) or [`get_uint`](Self::get_uint) where the exact value
750    /// matters.
751    ///
752    /// # Arguments
753    ///
754    /// * `name` - the field name.
755    /// * `index` - the element to read, or `0` for a scalar field.
756    ///
757    /// # Returns
758    ///
759    /// The value as a double.
760    ///
761    /// # Errors
762    ///
763    /// Returns [`MavlinkError::UnknownField`] if the message has no such field, and
764    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array.
765    pub fn get_number(&self, name: &str, index: usize) -> Result<f64> {
766        Ok(match self.get(name, index)? {
767            FieldValue::Int(value) => value as f64,
768            FieldValue::Uint(value) => value as f64,
769            FieldValue::Float(value) => value,
770        })
771    }
772
773    /// Writes a double into a field, converting it to the field's type.
774    ///
775    /// This is the writing a host language with one numeric type needs. A value bound for
776    /// an integer field must be a whole number within that field's range, so a fractional
777    /// or oversized value is refused rather than silently truncated.
778    ///
779    /// # Arguments
780    ///
781    /// * `name` - the field name.
782    /// * `index` - the element to write, or `0` for a scalar field.
783    /// * `value` - the value to store.
784    ///
785    /// # Errors
786    ///
787    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
788    /// [`MavlinkError::FieldIndexOutOfRange`] if the element is past the end of an array,
789    /// and [`MavlinkError::ValueOutOfRange`] if an integer field is given a value that is
790    /// fractional, infinite, not a number, or outside the range its width holds.
791    pub fn set_number(&mut self, name: &str, index: usize, value: f64) -> Result<()> {
792        let (_, field) = self.element(name, index)?;
793        if !field.ty.is_integer() {
794            return self.set_float(name, index, value);
795        }
796        // Range first, because a cast saturates rather than failing, then the round trip,
797        // which is what rejects a fractional value without needing a floating-point library.
798        if field.ty.is_signed() {
799            if !(-TWO_POW_63..TWO_POW_63).contains(&value) {
800                return Err(MavlinkError::ValueOutOfRange);
801            }
802            let whole = value as i64;
803            if whole as f64 != value {
804                return Err(MavlinkError::ValueOutOfRange);
805            }
806            self.set_int(name, index, whole)
807        } else {
808            if !(0.0..TWO_POW_64).contains(&value) {
809                return Err(MavlinkError::ValueOutOfRange);
810            }
811            let whole = value as u64;
812            if whole as f64 != value {
813                return Err(MavlinkError::ValueOutOfRange);
814            }
815            self.set_uint(name, index, whole)
816        }
817    }
818
819    /// Copies the raw bytes of a byte-wide array field out.
820    ///
821    /// # Arguments
822    ///
823    /// * `name` - the field name.
824    /// * `out` - the destination, which must be at least the field's length.
825    ///
826    /// # Returns
827    ///
828    /// The number of bytes written, which is the field's declared length.
829    ///
830    /// # Errors
831    ///
832    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
833    /// [`MavlinkError::FieldTypeMismatch`] if the field is not an array of `char`,
834    /// `uint8_t`, or `int8_t`, and [`MavlinkError::PayloadTooLong`] if `out` is too small.
835    pub fn get_bytes(&self, name: &str, out: &mut [u8]) -> Result<usize> {
836        let (offset, field) = self.descriptor.locate(name)?;
837        if field.ty.size() != 1 || field.array_len == 0 {
838            return Err(MavlinkError::FieldTypeMismatch);
839        }
840        let len = field.elements();
841        if out.len() < len {
842            return Err(MavlinkError::PayloadTooLong);
843        }
844        out[..len].copy_from_slice(&self.payload[offset..offset + len]);
845        Ok(len)
846    }
847
848    /// Writes the raw bytes of a byte-wide array field, zero-padding the rest.
849    ///
850    /// # Arguments
851    ///
852    /// * `name` - the field name.
853    /// * `bytes` - the bytes to store, at most the field's declared length.
854    ///
855    /// # Errors
856    ///
857    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
858    /// [`MavlinkError::FieldTypeMismatch`] if the field is not an array of `char`,
859    /// `uint8_t`, or `int8_t`, and [`MavlinkError::PayloadTooLong`] if the bytes are
860    /// longer than the field.
861    pub fn set_bytes(&mut self, name: &str, bytes: &[u8]) -> Result<()> {
862        let (offset, field) = self.descriptor.locate(name)?;
863        if field.ty.size() != 1 || field.array_len == 0 {
864            return Err(MavlinkError::FieldTypeMismatch);
865        }
866        let len = field.elements();
867        if bytes.len() > len {
868            return Err(MavlinkError::PayloadTooLong);
869        }
870        self.payload[offset..offset + len].fill(0);
871        self.payload[offset..offset + bytes.len()].copy_from_slice(bytes);
872        Ok(())
873    }
874
875    /// Reads a `char` array as text, stopping at the padding.
876    ///
877    /// MAVLink carries a string in a fixed-length `char` array, padded with zeros when the
878    /// text is shorter and left unterminated when it exactly fills the field.
879    ///
880    /// # Arguments
881    ///
882    /// * `name` - the field name.
883    ///
884    /// # Returns
885    ///
886    /// The text, without its padding.
887    ///
888    /// # Errors
889    ///
890    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
891    /// [`MavlinkError::FieldTypeMismatch`] if the field is not a `char` array, and
892    /// [`MavlinkError::BadPayload`] if the bytes are not valid UTF-8.
893    ///
894    /// # Examples
895    ///
896    /// ```
897    /// use pamoja_mavlink::dialect::{descriptor, DynamicMessage};
898    ///
899    /// let shape = descriptor(253).expect("STATUSTEXT is in the common dialect");
900    /// let mut status = DynamicMessage::new(shape)?;
901    /// status.set_text("text", "preflight checks passed")?;
902    /// assert_eq!(status.text("text")?, "preflight checks passed");
903    /// # Ok::<(), pamoja_mavlink::MavlinkError>(())
904    /// ```
905    pub fn text(&self, name: &str) -> Result<&str> {
906        let (offset, field) = self.descriptor.locate(name)?;
907        if field.ty != FieldType::Char || field.array_len == 0 {
908            return Err(MavlinkError::FieldTypeMismatch);
909        }
910        let bytes = &self.payload[offset..offset + field.elements()];
911        let end = bytes
912            .iter()
913            .position(|byte| *byte == 0)
914            .unwrap_or(bytes.len());
915        core::str::from_utf8(&bytes[..end]).map_err(|_| MavlinkError::BadPayload)
916    }
917
918    /// Writes text into a `char` array, padding the rest with zeros.
919    ///
920    /// # Arguments
921    ///
922    /// * `name` - the field name.
923    /// * `text` - the text to store, at most the field's declared length.
924    ///
925    /// # Errors
926    ///
927    /// Returns [`MavlinkError::UnknownField`] if the message has no such field,
928    /// [`MavlinkError::FieldTypeMismatch`] if the field is not a `char` array, and
929    /// [`MavlinkError::PayloadTooLong`] if the text is longer than the field.
930    pub fn set_text(&mut self, name: &str, text: &str) -> Result<()> {
931        let (_, field) = self.descriptor.locate(name)?;
932        if field.ty != FieldType::Char {
933            return Err(MavlinkError::FieldTypeMismatch);
934        }
935        self.set_bytes(name, text.as_bytes())
936    }
937
938    fn element(&self, name: &str, index: usize) -> Result<(usize, &'a FieldDescriptor<'a>)> {
939        let (offset, field) = self.descriptor.locate(name)?;
940        if index >= field.elements() {
941            return Err(MavlinkError::FieldIndexOutOfRange);
942        }
943        Ok((offset + index * field.ty.size(), field))
944    }
945
946    fn read<const N: usize>(&self, offset: usize) -> [u8; N] {
947        let mut bytes = [0; N];
948        bytes.copy_from_slice(&self.payload[offset..offset + N]);
949        bytes
950    }
951
952    fn write(&mut self, offset: usize, bytes: &[u8]) {
953        self.payload[offset..offset + bytes.len()].copy_from_slice(bytes);
954    }
955}
956
957/// Returns the shape of a common-dialect message, if this crate types it.
958///
959/// # Arguments
960///
961/// * `msgid` - the message id to look up.
962///
963/// # Returns
964///
965/// The descriptor, or [`None`] for an id outside the typed set; a caller can describe such
966/// a message itself with [`MessageDescriptorBuilder`].
967///
968/// # Examples
969///
970/// ```
971/// use pamoja_mavlink::dialect::descriptor;
972///
973/// let heartbeat = descriptor(0).expect("HEARTBEAT is in the common dialect");
974/// assert_eq!(heartbeat.name, "HEARTBEAT");
975/// assert_eq!(heartbeat.wire_len(), 9);
976/// assert!(descriptor(50_000).is_none());
977/// ```
978pub fn descriptor(msgid: u32) -> Option<&'static MessageDescriptor<'static>> {
979    DESCRIPTORS.iter().copied().find(|shape| shape.id == msgid)
980}
981
982/// Returns the shape of a common-dialect message by name.
983///
984/// # Arguments
985///
986/// * `name` - the message name, such as `"GLOBAL_POSITION_INT"`.
987///
988/// # Returns
989///
990/// The descriptor, or [`None`] for a name outside the typed set.
991///
992/// # Examples
993///
994/// ```
995/// use pamoja_mavlink::dialect::descriptor_by_name;
996///
997/// let position = descriptor_by_name("GLOBAL_POSITION_INT").expect("a typed message");
998/// assert_eq!(position.id, 33);
999/// ```
1000pub fn descriptor_by_name(name: &str) -> Option<&'static MessageDescriptor<'static>> {
1001    DESCRIPTORS.iter().copied().find(|shape| shape.name == name)
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006    use super::*;
1007    use crate::dialect::{
1008        AutopilotVersion, BatteryStatus, GlobalPositionInt, Heartbeat, Message, MissionAck,
1009        Statustext,
1010    };
1011    use crate::Header;
1012
1013    // Holds each typed message and its descriptor to the same shape. A field mistyped in
1014    // one and not the other changes the bytes on the wire, which this catches at the
1015    // declaration rather than against a live autopilot.
1016    macro_rules! assert_same_shape {
1017        ($( $message:ty ),+ $(,)?) => {
1018            $(
1019                let shape = <$message as Message>::DESCRIPTOR;
1020                assert_eq!(shape.id, <$message as Message>::ID);
1021                assert_eq!(shape.name, <$message as Message>::NAME);
1022                assert_eq!(shape.crc_extra, <$message as Message>::CRC_EXTRA);
1023                assert_eq!(shape.wire_len(), <$message as Message>::WIRE_LEN);
1024                assert_eq!(shape.derived_crc_extra(), <$message as Message>::CRC_EXTRA);
1025
1026                let base: Vec<(&str, &str, u8)> = shape
1027                    .fields
1028                    .iter()
1029                    .filter(|field| !field.extension)
1030                    .map(|field| (field.ty.wire_name(), field.name, field.array_len))
1031                    .collect();
1032                assert_eq!(base.as_slice(), <$message as Message>::BASE_FIELDS);
1033            )+
1034        };
1035    }
1036
1037    #[test]
1038    fn every_typed_message_agrees_with_its_descriptor() {
1039        assert_same_shape!(
1040            crate::dialect::Heartbeat,
1041            crate::dialect::SysStatus,
1042            crate::dialect::SystemTime,
1043            crate::dialect::Ping,
1044            crate::dialect::SetMode,
1045            crate::dialect::ParamRequestRead,
1046            crate::dialect::ParamRequestList,
1047            crate::dialect::ParamValue,
1048            crate::dialect::ParamSet,
1049            crate::dialect::GpsRawInt,
1050            crate::dialect::Attitude,
1051            crate::dialect::AttitudeQuaternion,
1052            crate::dialect::LocalPositionNed,
1053            crate::dialect::GlobalPositionInt,
1054            crate::dialect::ServoOutputRaw,
1055            crate::dialect::MissionRequest,
1056            crate::dialect::MissionCurrent,
1057            crate::dialect::MissionRequestList,
1058            crate::dialect::MissionCount,
1059            crate::dialect::MissionClearAll,
1060            crate::dialect::MissionAck,
1061            crate::dialect::MissionRequestInt,
1062            crate::dialect::RcChannels,
1063            crate::dialect::ManualControl,
1064            crate::dialect::MissionItemInt,
1065            crate::dialect::VfrHud,
1066            crate::dialect::CommandInt,
1067            crate::dialect::CommandLong,
1068            crate::dialect::CommandAck,
1069            crate::dialect::SetPositionTargetLocalNed,
1070            crate::dialect::SetPositionTargetGlobalInt,
1071            crate::dialect::BatteryStatus,
1072            crate::dialect::AutopilotVersion,
1073            crate::dialect::HomePosition,
1074            crate::dialect::ExtendedSysState,
1075            crate::dialect::Statustext,
1076        );
1077    }
1078
1079    #[test]
1080    fn the_registry_covers_exactly_the_typed_messages() {
1081        assert_eq!(DESCRIPTORS.len(), 36);
1082        for shape in DESCRIPTORS {
1083            assert_eq!(crate::dialect::crc_extra(shape.id), Some(shape.crc_extra));
1084            assert_eq!(descriptor(shape.id), Some(*shape));
1085            assert_eq!(descriptor_by_name(shape.name), Some(*shape));
1086        }
1087        assert!(descriptor(50_000).is_none());
1088        assert!(descriptor_by_name("BATTERY_CELLS").is_none());
1089    }
1090
1091    #[test]
1092    fn a_descriptor_writes_the_bytes_its_typed_message_does() -> Result<()> {
1093        let typed = Heartbeat {
1094            custom_mode: 0x0DF0_AD8B,
1095            type_: 2,
1096            autopilot: 3,
1097            base_mode: 81,
1098            system_status: 4,
1099            mavlink_version: 3,
1100        };
1101        let mut expected = [0u8; MAX_PAYLOAD];
1102        let len = typed.encode(&mut expected);
1103
1104        let mut dynamic = DynamicMessage::new(Heartbeat::DESCRIPTOR)?;
1105        dynamic.set_uint("custom_mode", 0, 0x0DF0_AD8B)?;
1106        dynamic.set_uint("type", 0, 2)?;
1107        dynamic.set_uint("autopilot", 0, 3)?;
1108        dynamic.set_uint("base_mode", 0, 81)?;
1109        dynamic.set_uint("system_status", 0, 4)?;
1110        dynamic.set_uint("mavlink_version", 0, 3)?;
1111
1112        assert_eq!(dynamic.payload(), &expected[..len]);
1113        Ok(())
1114    }
1115
1116    #[test]
1117    fn signed_and_floating_fields_round_trip() -> Result<()> {
1118        let mut position = DynamicMessage::new(GlobalPositionInt::DESCRIPTOR)?;
1119        position.set_int("lat", 0, -33_856_780)?;
1120        position.set_int("lon", 0, 151_215_300)?;
1121        position.set_int("vz", 0, -250)?;
1122        position.set_uint("hdg", 0, 18_000)?;
1123
1124        let decoded = GlobalPositionInt::decode(position.payload())?;
1125        assert_eq!(decoded.lat, -33_856_780);
1126        assert_eq!(decoded.vz, -250);
1127        assert_eq!(position.get_int("lon", 0)?, 151_215_300);
1128        assert_eq!(position.get_uint("hdg", 0)?, 18_000);
1129
1130        let mut attitude = DynamicMessage::new(crate::dialect::Attitude::DESCRIPTOR)?;
1131        attitude.set_float("roll", 0, -0.5)?;
1132        assert_eq!(attitude.get_float("roll", 0)?, -0.5);
1133        assert_eq!(attitude.get("roll", 0)?, FieldValue::Float(-0.5));
1134        Ok(())
1135    }
1136
1137    #[test]
1138    fn one_numeric_type_reaches_every_field() -> Result<()> {
1139        let mut position = DynamicMessage::new(GlobalPositionInt::DESCRIPTOR)?;
1140        position.set_number("lat", 0, -33_856_780.0)?;
1141        position.set_number("hdg", 0, 18_000.0)?;
1142        assert_eq!(position.get_number("lat", 0)?, -33_856_780.0);
1143        assert_eq!(position.get_number("hdg", 0)?, 18_000.0);
1144
1145        let mut attitude = DynamicMessage::new(crate::dialect::Attitude::DESCRIPTOR)?;
1146        attitude.set_number("roll", 0, 0.25)?;
1147        assert_eq!(attitude.get_number("roll", 0)?, 0.25);
1148
1149        // An integer field takes only a value it can hold exactly.
1150        for refused in [1.5, f64::NAN, f64::INFINITY, -1.0, 1e30] {
1151            assert_eq!(
1152                position.set_number("hdg", 0, refused).unwrap_err(),
1153                MavlinkError::ValueOutOfRange
1154            );
1155        }
1156        assert_eq!(position.get_number("hdg", 0)?, 18_000.0);
1157        Ok(())
1158    }
1159
1160    #[test]
1161    fn arrays_and_text_are_addressed_by_element() -> Result<()> {
1162        let mut battery = DynamicMessage::new(BatteryStatus::DESCRIPTOR)?;
1163        battery.set_uint("voltages", 0, 4_150)?;
1164        battery.set_uint("voltages", 9, 3_990)?;
1165        assert_eq!(battery.get_uint("voltages", 9)?, 3_990);
1166        assert_eq!(BatteryStatus::decode(battery.payload())?.voltages[9], 3_990);
1167        assert_eq!(
1168            battery.get_uint("voltages", 10),
1169            Err(MavlinkError::FieldIndexOutOfRange)
1170        );
1171
1172        let mut version = DynamicMessage::new(AutopilotVersion::DESCRIPTOR)?;
1173        version.set_bytes("flight_custom_version", &[1, 2, 3])?;
1174        let mut out = [0u8; 8];
1175        assert_eq!(version.get_bytes("flight_custom_version", &mut out)?, 8);
1176        assert_eq!(out, [1, 2, 3, 0, 0, 0, 0, 0]);
1177
1178        let mut status = DynamicMessage::new(Statustext::DESCRIPTOR)?;
1179        status.set_text("text", "battery low")?;
1180        assert_eq!(status.text("text")?, "battery low");
1181        assert_eq!(status.get_uint("severity", 0)?, 0);
1182        Ok(())
1183    }
1184
1185    #[test]
1186    fn extension_fields_sit_after_the_base_fields_and_leave_the_seed_alone() -> Result<()> {
1187        let shape = MissionAck::DESCRIPTOR;
1188        let extension = shape
1189            .field("mission_type")
1190            .expect("MISSION_ACK carries the extension");
1191        assert!(extension.extension);
1192        assert_eq!(shape.offset_of("mission_type"), Some(shape.base_len()));
1193        assert_eq!(shape.derived_crc_extra(), MissionAck::CRC_EXTRA);
1194
1195        // A peer that predates the extension sends only the base fields, and it reads zero.
1196        let base = vec![0u8; shape.base_len()];
1197        let received = DynamicMessage::decode(shape, &base)?;
1198        assert_eq!(received.get_uint("mission_type", 0)?, 0);
1199        Ok(())
1200    }
1201
1202    #[test]
1203    fn a_field_is_rejected_when_it_is_missing_or_misused() -> Result<()> {
1204        let mut heartbeat = DynamicMessage::new(Heartbeat::DESCRIPTOR)?;
1205        assert_eq!(
1206            heartbeat.get_uint("throttle", 0),
1207            Err(MavlinkError::UnknownField)
1208        );
1209        assert_eq!(
1210            heartbeat.set_float("type", 0, 1.0),
1211            Err(MavlinkError::FieldTypeMismatch)
1212        );
1213        assert_eq!(
1214            heartbeat.set_uint("type", 0, 300),
1215            Err(MavlinkError::ValueOutOfRange)
1216        );
1217        assert_eq!(
1218            heartbeat.set_int("type", 0, -1),
1219            Err(MavlinkError::ValueOutOfRange)
1220        );
1221        assert_eq!(heartbeat.text("type"), Err(MavlinkError::FieldTypeMismatch));
1222
1223        let mut attitude = DynamicMessage::new(crate::dialect::Attitude::DESCRIPTOR)?;
1224        assert_eq!(
1225            attitude.set_uint("roll", 0, 1),
1226            Err(MavlinkError::FieldTypeMismatch)
1227        );
1228        assert_eq!(
1229            attitude.get_int("roll", 0),
1230            Err(MavlinkError::FieldTypeMismatch)
1231        );
1232        Ok(())
1233    }
1234
1235    #[test]
1236    fn a_truncated_payload_reads_its_missing_bytes_as_zero() -> Result<()> {
1237        let shape = Heartbeat::DESCRIPTOR;
1238        let frame = DynamicMessage::new(shape)?.to_frame(Header::new(1, 1, 0))?;
1239
1240        // MAVLink 2 trims trailing zeros but never the whole payload, so an all-zero
1241        // HEARTBEAT goes out as a single byte and the receiver restores the rest.
1242        assert_eq!(frame.payload(), &[0]);
1243        let received = DynamicMessage::decode(shape, frame.payload())?;
1244        assert_eq!(received.get_uint("custom_mode", 0)?, 0);
1245        assert_eq!(
1246            DynamicMessage::decode(shape, &[0u8; 10]).unwrap_err(),
1247            MavlinkError::BadPayload
1248        );
1249        Ok(())
1250    }
1251
1252    #[test]
1253    fn a_builder_puts_declared_fields_into_wire_order() -> Result<()> {
1254        // HEARTBEAT as its definition declares it: the 32-bit field sits fourth, and wire
1255        // order pulls it to the front.
1256        let heartbeat = MessageDescriptorBuilder::new(0, "HEARTBEAT")
1257            .field("type", FieldType::U8, 0)
1258            .field("autopilot", FieldType::U8, 0)
1259            .field("base_mode", FieldType::U8, 0)
1260            .field("custom_mode", FieldType::U32, 0)
1261            .field("system_status", FieldType::U8, 0)
1262            .field("mavlink_version", FieldType::U8, 0)
1263            .build()?;
1264
1265        assert_eq!(heartbeat.crc_extra(), Heartbeat::CRC_EXTRA);
1266        heartbeat.with_descriptor(|shape| {
1267            assert_eq!(shape.fields, Heartbeat::DESCRIPTOR.fields);
1268        });
1269
1270        // SYS_STATUS declares an int8 in the middle of its 16-bit fields, so a stable sort
1271        // by size is what moves it to the end and nothing else with it.
1272        let status = MessageDescriptorBuilder::new(1, "SYS_STATUS")
1273            .field("onboard_control_sensors_present", FieldType::U32, 0)
1274            .field("onboard_control_sensors_enabled", FieldType::U32, 0)
1275            .field("onboard_control_sensors_health", FieldType::U32, 0)
1276            .field("load", FieldType::U16, 0)
1277            .field("voltage_battery", FieldType::U16, 0)
1278            .field("current_battery", FieldType::I16, 0)
1279            .field("battery_remaining", FieldType::I8, 0)
1280            .field("drop_rate_comm", FieldType::U16, 0)
1281            .field("errors_comm", FieldType::U16, 0)
1282            .field("errors_count1", FieldType::U16, 0)
1283            .field("errors_count2", FieldType::U16, 0)
1284            .field("errors_count3", FieldType::U16, 0)
1285            .field("errors_count4", FieldType::U16, 0)
1286            .build()?;
1287
1288        assert_eq!(status.crc_extra(), crate::dialect::SysStatus::CRC_EXTRA);
1289        status.with_descriptor(|shape| {
1290            assert_eq!(shape.fields, crate::dialect::SysStatus::DESCRIPTOR.fields);
1291        });
1292        Ok(())
1293    }
1294
1295    #[test]
1296    fn a_builder_rejects_a_shape_it_cannot_describe() {
1297        let duplicated = MessageDescriptorBuilder::new(1, "TWICE")
1298            .field("value", FieldType::U8, 0)
1299            .field("value", FieldType::U16, 0)
1300            .build();
1301        assert_eq!(duplicated.unwrap_err(), MavlinkError::DuplicateField);
1302
1303        let overlong = MessageDescriptorBuilder::new(1, "HUGE")
1304            .field("payload", FieldType::U32, 64)
1305            .build();
1306        assert_eq!(overlong.unwrap_err(), MavlinkError::PayloadTooLong);
1307    }
1308
1309    #[test]
1310    fn an_owned_dialect_resolves_its_own_ids_and_falls_back_to_the_common_one() -> Result<()> {
1311        let mut dialect = OwnedDialect::new();
1312        assert!(dialect.is_empty());
1313        dialect.insert(
1314            MessageDescriptorBuilder::new(50_000, "BATTERY_CELLS")
1315                .field("cell_mv", FieldType::U16, 6)
1316                .field("pack_id", FieldType::U8, 0)
1317                .build()?,
1318        );
1319        assert_eq!(dialect.len(), 1);
1320
1321        let private = dialect.get(50_000).expect("just inserted");
1322        assert_eq!(dialect.crc_extra(50_000), Some(private.crc_extra()));
1323        assert_eq!(dialect.crc_extra(0), Some(Heartbeat::CRC_EXTRA));
1324        assert_eq!(dialect.crc_extra(49_999), None);
1325        assert!(dialect.by_name("BATTERY_CELLS").is_some());
1326
1327        // Replacing a shape keeps one entry rather than shadowing the first.
1328        dialect.insert(
1329            MessageDescriptorBuilder::new(50_000, "BATTERY_CELLS")
1330                .field("pack_id", FieldType::U8, 0)
1331                .build()?,
1332        );
1333        assert_eq!(dialect.len(), 1);
1334        Ok(())
1335    }
1336
1337    #[test]
1338    fn an_owned_copy_keeps_the_shape_it_was_taken_from() {
1339        let owned = OwnedMessageDescriptor::from_descriptor(Statustext::DESCRIPTOR);
1340        assert_eq!(owned.id(), Statustext::ID);
1341        assert_eq!(owned.crc_extra(), Statustext::CRC_EXTRA);
1342        owned.with_descriptor(|shape| {
1343            assert_eq!(shape.fields, Statustext::DESCRIPTOR.fields);
1344            assert_eq!(shape.wire_len(), Statustext::WIRE_LEN);
1345        });
1346    }
1347}