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}