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}