Skip to main content

pamoja_mavlink/dialect/schema/
owned.rs

1//! Message shapes a caller owns, for dialects this crate does not ship.
2//!
3//! A [`MessageDescriptor`] borrows its name and fields, which suits the built-in registry
4//! where every string is static. A caller transcribing a vendor's dialect owns those
5//! strings instead, so [`OwnedMessageDescriptor`] holds them and lends a borrowed view
6//! when one is needed.
7//!
8//! [`MessageDescriptorBuilder`] takes fields in the order a message definition lists them
9//! and puts them in wire order itself, then derives the `CRC_EXTRA` seed from the result.
10
11use alloc::string::String;
12use alloc::vec::Vec;
13
14use super::{FieldDescriptor, FieldType, MessageDescriptor};
15use crate::error::{MavlinkError, Result};
16use crate::frame::MAX_PAYLOAD;
17
18/// One field of an owned message shape.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct OwnedFieldDescriptor {
21    /// The field name as the dialect writes it.
22    pub name: String,
23
24    /// The field's scalar type, which for an array is its element type.
25    pub ty: FieldType,
26
27    /// The element count for an array field, or `0` for a scalar.
28    pub array_len: u8,
29
30    /// Whether this is a MAVLink 2 extension field.
31    pub extension: bool,
32}
33
34/// A message shape a caller owns, for a message from a dialect this crate does not type.
35///
36/// # Examples
37///
38/// ```
39/// use pamoja_mavlink::dialect::{DynamicMessage, FieldType, MessageDescriptorBuilder};
40///
41/// // A private message, written the way its definition reads: declaration order, with
42/// // the builder putting the fields on the wire largest first.
43/// let shape = MessageDescriptorBuilder::new(50_000, "BATTERY_CELLS")
44///     .field("cell_mv", FieldType::U16, 6)
45///     .field("pack_id", FieldType::U8, 0)
46///     .field("uptime_ms", FieldType::U32, 0)
47///     .build()?;
48///
49/// shape.with_descriptor(|shape| -> Result<(), pamoja_mavlink::MavlinkError> {
50///     // The 32-bit field leads, then the array, then the byte.
51///     assert_eq!(shape.offset_of("uptime_ms"), Some(0));
52///     assert_eq!(shape.offset_of("cell_mv"), Some(4));
53///     assert_eq!(shape.wire_len(), 17);
54///
55///     let mut message = DynamicMessage::new(shape)?;
56///     message.set_uint("cell_mv", 3, 4_150)?;
57///     assert_eq!(message.get_uint("cell_mv", 3)?, 4_150);
58///     Ok(())
59/// })?;
60/// # Ok::<(), pamoja_mavlink::MavlinkError>(())
61/// ```
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct OwnedMessageDescriptor {
64    id: u32,
65    name: String,
66    crc_extra: u8,
67    fields: Vec<OwnedFieldDescriptor>,
68}
69
70impl OwnedMessageDescriptor {
71    /// Takes an owned copy of a borrowed shape.
72    ///
73    /// # Arguments
74    ///
75    /// * `descriptor` - the shape to copy, such as one from the built-in registry.
76    ///
77    /// # Returns
78    ///
79    /// The owned shape.
80    pub fn from_descriptor(descriptor: &MessageDescriptor<'_>) -> Self {
81        Self {
82            id: descriptor.id,
83            name: String::from(descriptor.name),
84            crc_extra: descriptor.crc_extra,
85            fields: descriptor
86                .fields
87                .iter()
88                .map(|field| OwnedFieldDescriptor {
89                    name: String::from(field.name),
90                    ty: field.ty,
91                    array_len: field.array_len,
92                    extension: field.extension,
93                })
94                .collect(),
95        }
96    }
97
98    /// Returns the message id.
99    ///
100    /// # Returns
101    ///
102    /// The id on the wire.
103    pub fn id(&self) -> u32 {
104        self.id
105    }
106
107    /// Returns the message name.
108    ///
109    /// # Returns
110    ///
111    /// The name, such as `"BATTERY_CELLS"`.
112    pub fn name(&self) -> &str {
113        &self.name
114    }
115
116    /// Returns the `CRC_EXTRA` seed this shape implies.
117    ///
118    /// # Returns
119    ///
120    /// The seed, which a frame carrying this message folds into its checksum.
121    pub fn crc_extra(&self) -> u8 {
122        self.crc_extra
123    }
124
125    /// Returns the fields in wire order.
126    ///
127    /// # Returns
128    ///
129    /// The base fields largest first, then any extensions.
130    pub fn fields(&self) -> &[OwnedFieldDescriptor] {
131        &self.fields
132    }
133
134    /// Lends a borrowed view of this shape for the duration of a call.
135    ///
136    /// A [`MessageDescriptor`] borrows a slice of fields that does not exist until it is
137    /// assembled, so the view is built for the call rather than stored, which keeps this
138    /// type free of self-references.
139    ///
140    /// # Arguments
141    ///
142    /// * `query` - what to do with the borrowed shape.
143    ///
144    /// # Returns
145    ///
146    /// Whatever `query` returns.
147    pub fn with_descriptor<R>(&self, query: impl FnOnce(&MessageDescriptor<'_>) -> R) -> R {
148        let fields: Vec<FieldDescriptor<'_>> = self
149            .fields
150            .iter()
151            .map(|field| FieldDescriptor {
152                name: &field.name,
153                ty: field.ty,
154                array_len: field.array_len,
155                extension: field.extension,
156            })
157            .collect();
158        query(&MessageDescriptor {
159            id: self.id,
160            name: &self.name,
161            crc_extra: self.crc_extra,
162            fields: &fields,
163        })
164    }
165}
166
167/// Builds a message shape from a definition written the way a dialect reads.
168///
169/// Fields are given in declaration order. MAVLink puts the base fields on the wire largest
170/// type first, keeping equal-sized fields in the order declared, and leaves extension
171/// fields at the end untouched; [`build`](Self::build) applies that and derives the
172/// `CRC_EXTRA` seed from the result, so a transcription error surfaces as a checksum a
173/// peer rejects rather than as silently misread fields.
174#[derive(Clone, Debug, Default)]
175pub struct MessageDescriptorBuilder {
176    id: u32,
177    name: String,
178    fields: Vec<OwnedFieldDescriptor>,
179}
180
181impl MessageDescriptorBuilder {
182    /// Starts a shape for a message id and name.
183    ///
184    /// # Arguments
185    ///
186    /// * `id` - the message id on the wire.
187    /// * `name` - the message name, which the `CRC_EXTRA` derivation folds in, so it must
188    ///   match the dialect exactly.
189    ///
190    /// # Returns
191    ///
192    /// The builder, with no fields yet.
193    pub fn new(id: u32, name: impl Into<String>) -> Self {
194        Self {
195            id,
196            name: name.into(),
197            fields: Vec::new(),
198        }
199    }
200
201    /// Adds a base field, in the order the definition declares it.
202    ///
203    /// # Arguments
204    ///
205    /// * `name` - the field name.
206    /// * `ty` - the field's scalar type, or an array's element type.
207    /// * `array_len` - the element count for an array, or `0` for a scalar.
208    ///
209    /// # Returns
210    ///
211    /// The builder.
212    pub fn field(mut self, name: impl Into<String>, ty: FieldType, array_len: u8) -> Self {
213        self.fields.push(OwnedFieldDescriptor {
214            name: name.into(),
215            ty,
216            array_len,
217            extension: false,
218        });
219        self
220    }
221
222    /// Adds a MAVLink 2 extension field, in the order the definition declares it.
223    ///
224    /// Extensions keep their declared order, are excluded from the `CRC_EXTRA` seed, and
225    /// read as zero from a frame sent by a peer that predates them.
226    ///
227    /// # Arguments
228    ///
229    /// * `name` - the field name.
230    /// * `ty` - the field's scalar type, or an array's element type.
231    /// * `array_len` - the element count for an array, or `0` for a scalar.
232    ///
233    /// # Returns
234    ///
235    /// The builder.
236    pub fn extension(mut self, name: impl Into<String>, ty: FieldType, array_len: u8) -> Self {
237        self.fields.push(OwnedFieldDescriptor {
238            name: name.into(),
239            ty,
240            array_len,
241            extension: true,
242        });
243        self
244    }
245
246    /// Puts the fields in wire order and derives the seed.
247    ///
248    /// # Returns
249    ///
250    /// The finished shape.
251    ///
252    /// # Errors
253    ///
254    /// Returns [`MavlinkError::DuplicateField`] if two fields share a name, and
255    /// [`MavlinkError::PayloadTooLong`] if the fields do not fit a MAVLink payload.
256    pub fn build(self) -> Result<OwnedMessageDescriptor> {
257        let Self {
258            id,
259            name,
260            mut fields,
261        } = self;
262
263        for (index, field) in fields.iter().enumerate() {
264            if fields[index + 1..]
265                .iter()
266                .any(|other| other.name == field.name)
267            {
268                return Err(MavlinkError::DuplicateField);
269            }
270        }
271
272        let total: usize = fields
273            .iter()
274            .map(|field| {
275                let elements = if field.array_len == 0 {
276                    1
277                } else {
278                    field.array_len as usize
279                };
280                elements * field.ty.size()
281            })
282            .sum();
283        if total > MAX_PAYLOAD {
284            return Err(MavlinkError::PayloadTooLong);
285        }
286
287        // MAVLink orders the base fields by type size, largest first, and a stable sort
288        // keeps equal-sized fields as declared. Extensions are already at the end and are
289        // not reordered, so sorting only the leading base run leaves them where they are.
290        let base = fields
291            .iter()
292            .position(|field| field.extension)
293            .unwrap_or(fields.len());
294        fields[..base].sort_by_key(|field| core::cmp::Reverse(field.ty.size()));
295
296        let crc_extra = crate::crc::crc_extra_of(
297            &name,
298            fields
299                .iter()
300                .filter(|field| !field.extension)
301                .map(|field| (field.ty.wire_name(), field.name.as_str(), field.array_len)),
302        );
303
304        Ok(OwnedMessageDescriptor {
305            id,
306            name,
307            crc_extra,
308            fields,
309        })
310    }
311}
312
313/// A dialect a caller owns: message shapes looked up by id or name.
314///
315/// This is what makes a whole dialect usable rather than one message at a time. It also
316/// resolves the `CRC_EXTRA` a [`Parser`](crate::Parser) needs, so frames from a private
317/// dialect check like any other.
318///
319/// # Examples
320///
321/// ```
322/// use pamoja_mavlink::dialect::{FieldType, MessageDescriptorBuilder, OwnedDialect};
323/// use pamoja_mavlink::{Header, Parser};
324///
325/// let mut dialect = OwnedDialect::new();
326/// dialect.insert(
327///     MessageDescriptorBuilder::new(50_000, "BATTERY_CELLS")
328///         .field("pack_id", FieldType::U8, 0)
329///         .field("uptime_ms", FieldType::U32, 0)
330///         .build()?,
331/// );
332///
333/// let shape = dialect.by_name("BATTERY_CELLS").expect("just inserted");
334/// let frame = shape.with_descriptor(|shape| {
335///     let mut message = pamoja_mavlink::dialect::DynamicMessage::new(shape)?;
336///     message.set_uint("pack_id", 0, 2)?;
337///     message.to_frame(Header::new(9, 1, 0))
338/// })?;
339///
340/// // A parser resolves the seed through the dialect, so the private frame verifies.
341/// let resolve = |id| dialect.crc_extra(id);
342/// let mut parser = Parser::new();
343/// let received = frame
344///     .as_bytes()
345///     .iter()
346///     .filter_map(|byte| parser.push_byte(*byte, &resolve))
347///     .next()
348///     .expect("a whole frame");
349/// assert_eq!(received.message_id(), 50_000);
350/// # Ok::<(), pamoja_mavlink::MavlinkError>(())
351/// ```
352#[derive(Clone, Debug, Default)]
353pub struct OwnedDialect {
354    messages: Vec<OwnedMessageDescriptor>,
355}
356
357impl OwnedDialect {
358    /// Creates a dialect with no messages.
359    ///
360    /// # Returns
361    ///
362    /// The empty dialect.
363    pub fn new() -> Self {
364        Self {
365            messages: Vec::new(),
366        }
367    }
368
369    /// Adds a message shape, replacing any shape already held for its id.
370    ///
371    /// # Arguments
372    ///
373    /// * `descriptor` - the shape to add.
374    pub fn insert(&mut self, descriptor: OwnedMessageDescriptor) {
375        let id = descriptor.id();
376        self.messages.retain(|held| held.id() != id);
377        self.messages.push(descriptor);
378    }
379
380    /// Looks a message shape up by id.
381    ///
382    /// # Arguments
383    ///
384    /// * `msgid` - the message id.
385    ///
386    /// # Returns
387    ///
388    /// The shape, or [`None`] if this dialect does not describe that id.
389    pub fn get(&self, msgid: u32) -> Option<&OwnedMessageDescriptor> {
390        self.messages.iter().find(|held| held.id() == msgid)
391    }
392
393    /// Looks a message shape up by name.
394    ///
395    /// # Arguments
396    ///
397    /// * `name` - the message name.
398    ///
399    /// # Returns
400    ///
401    /// The shape, or [`None`] if this dialect does not describe that name.
402    pub fn by_name(&self, name: &str) -> Option<&OwnedMessageDescriptor> {
403        self.messages.iter().find(|held| held.name() == name)
404    }
405
406    /// Returns the `CRC_EXTRA` for a message id, falling back to the common dialect.
407    ///
408    /// # Arguments
409    ///
410    /// * `msgid` - the message id.
411    ///
412    /// # Returns
413    ///
414    /// The seed, or [`None`] if neither this dialect nor the common one knows the id.
415    pub fn crc_extra(&self, msgid: u32) -> Option<u8> {
416        self.get(msgid)
417            .map(OwnedMessageDescriptor::crc_extra)
418            .or_else(|| crate::dialect::crc_extra(msgid))
419    }
420
421    /// Returns how many message shapes this dialect holds.
422    ///
423    /// # Returns
424    ///
425    /// The count.
426    pub fn len(&self) -> usize {
427        self.messages.len()
428    }
429
430    /// Reports whether this dialect holds no message shapes.
431    ///
432    /// # Returns
433    ///
434    /// `true` if it is empty.
435    pub fn is_empty(&self) -> bool {
436        self.messages.is_empty()
437    }
438}