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}