pamoja_ffi/mavlink_schema.rs
1//! The C ABI for MAVLink message shapes: reading and writing a message by field name.
2//!
3//! [`mavlink`](crate::mavlink) carries any message's bytes, which is enough to move traffic
4//! but leaves the caller hand-packing payloads against a message definition. This is the
5//! layer above: a schema states a message's fields, and a message reads and writes them by
6//! name, so a caller works in `custom_mode` and `lat` rather than byte offsets.
7//!
8//! Two sources of shape are supported and behave identically. Every message the engine
9//! types is published, so [`pamoja_mavlink_schema_for_id`] resolves the common dialect with
10//! nothing to declare. A message from ArduPilot's dialect, PX4's, or a vendor's private one
11//! is described through [`PamojaMavlinkSchemaBuilder`], which puts the fields in wire order
12//! and derives the `CRC_EXTRA` seed, so a caller transcribes a definition as it reads.
13
14use std::ffi::{c_char, CString};
15
16use pamoja_mavlink::dialect::{
17 descriptor, descriptor_by_name, DynamicMessage, FieldType, MessageDescriptor,
18 MessageDescriptorBuilder, OwnedMessageDescriptor, DESCRIPTORS,
19};
20use pamoja_mavlink::{Header, Result as MavlinkResult};
21
22use crate::mavlink::{status_of, PamojaMavlinkDialect, PamojaMavlinkFrame, PamojaMavlinkHeader};
23use crate::{read_bytes, read_str, set_last_error, PamojaStatus};
24
25/// A `uint8_t` field.
26pub const PAMOJA_MAVLINK_FIELD_UINT8: u32 = 1;
27/// An `int8_t` field.
28pub const PAMOJA_MAVLINK_FIELD_INT8: u32 = 2;
29/// A `char` field; an array of these carries text.
30pub const PAMOJA_MAVLINK_FIELD_CHAR: u32 = 3;
31/// A `uint16_t` field.
32pub const PAMOJA_MAVLINK_FIELD_UINT16: u32 = 4;
33/// An `int16_t` field.
34pub const PAMOJA_MAVLINK_FIELD_INT16: u32 = 5;
35/// A `uint32_t` field.
36pub const PAMOJA_MAVLINK_FIELD_UINT32: u32 = 6;
37/// An `int32_t` field.
38pub const PAMOJA_MAVLINK_FIELD_INT32: u32 = 7;
39/// A `uint64_t` field.
40pub const PAMOJA_MAVLINK_FIELD_UINT64: u32 = 8;
41/// An `int64_t` field.
42pub const PAMOJA_MAVLINK_FIELD_INT64: u32 = 9;
43/// A `float` field.
44pub const PAMOJA_MAVLINK_FIELD_FLOAT: u32 = 10;
45/// A `double` field.
46pub const PAMOJA_MAVLINK_FIELD_DOUBLE: u32 = 11;
47
48/// Maps a field type onto its stable code.
49///
50/// The codes are written out rather than taken from a Rust enum's discriminants, so a
51/// value that crosses the boundary means the same thing in every build.
52fn code_of(ty: FieldType) -> u32 {
53 match ty {
54 FieldType::U8 => PAMOJA_MAVLINK_FIELD_UINT8,
55 FieldType::I8 => PAMOJA_MAVLINK_FIELD_INT8,
56 FieldType::Char => PAMOJA_MAVLINK_FIELD_CHAR,
57 FieldType::U16 => PAMOJA_MAVLINK_FIELD_UINT16,
58 FieldType::I16 => PAMOJA_MAVLINK_FIELD_INT16,
59 FieldType::U32 => PAMOJA_MAVLINK_FIELD_UINT32,
60 FieldType::I32 => PAMOJA_MAVLINK_FIELD_INT32,
61 FieldType::U64 => PAMOJA_MAVLINK_FIELD_UINT64,
62 FieldType::I64 => PAMOJA_MAVLINK_FIELD_INT64,
63 FieldType::F32 => PAMOJA_MAVLINK_FIELD_FLOAT,
64 FieldType::F64 => PAMOJA_MAVLINK_FIELD_DOUBLE,
65 }
66}
67
68/// Resolves a field type code, reporting an unknown one as an error.
69fn type_of(code: u32) -> Result<FieldType, PamojaStatus> {
70 Ok(match code {
71 PAMOJA_MAVLINK_FIELD_UINT8 => FieldType::U8,
72 PAMOJA_MAVLINK_FIELD_INT8 => FieldType::I8,
73 PAMOJA_MAVLINK_FIELD_CHAR => FieldType::Char,
74 PAMOJA_MAVLINK_FIELD_UINT16 => FieldType::U16,
75 PAMOJA_MAVLINK_FIELD_INT16 => FieldType::I16,
76 PAMOJA_MAVLINK_FIELD_UINT32 => FieldType::U32,
77 PAMOJA_MAVLINK_FIELD_INT32 => FieldType::I32,
78 PAMOJA_MAVLINK_FIELD_UINT64 => FieldType::U64,
79 PAMOJA_MAVLINK_FIELD_INT64 => FieldType::I64,
80 PAMOJA_MAVLINK_FIELD_FLOAT => FieldType::F32,
81 PAMOJA_MAVLINK_FIELD_DOUBLE => FieldType::F64,
82 _ => {
83 set_last_error(format!("{code} is not a MAVLink field type"));
84 return Err(PamojaStatus::InvalidArgument);
85 }
86 })
87}
88
89/// One field of a message shape, as read back from a schema.
90///
91/// Both names point into the schema they came from and stay valid until it is released, so
92/// a caller reads them in place rather than freeing them.
93#[repr(C)]
94#[derive(Clone, Copy, Debug)]
95pub struct PamojaMavlinkFieldInfo {
96 /// The field name as the dialect writes it, such as `custom_mode`.
97 pub name: *const c_char,
98 /// The field's type name as the dialect writes it, such as `uint32_t`.
99 pub type_name: *const c_char,
100 /// The field's type, one of the `PAMOJA_MAVLINK_FIELD_*` codes.
101 pub field_type: u32,
102 /// The element count for an array field, or `0` for a scalar.
103 pub array_len: u8,
104 /// `1` for a MAVLink 2 extension field, `0` for a base field.
105 pub extension: u8,
106 /// The field's byte offset within the payload.
107 pub offset: usize,
108}
109
110/// The shape of one message: its id, name, seed, and fields.
111///
112/// A schema is what turns bytes into named fields. It comes either from the built-in
113/// registry of typed messages or from a [`PamojaMavlinkSchemaBuilder`], and behaves the
114/// same way whichever it is.
115pub struct PamojaMavlinkSchema {
116 shape: OwnedMessageDescriptor,
117 name: CString,
118 field_names: Vec<CString>,
119}
120
121impl PamojaMavlinkSchema {
122 /// Moves a shape onto the heap, interning its names for the C side to borrow.
123 fn into_handle(shape: OwnedMessageDescriptor) -> Result<*mut Self, PamojaStatus> {
124 let interior = || {
125 set_last_error("a message or field name contains an interior null byte".to_owned());
126 PamojaStatus::InvalidArgument
127 };
128 let name = CString::new(shape.name()).map_err(|_| interior())?;
129 let field_names = shape
130 .fields()
131 .iter()
132 .map(|field| CString::new(field.name.as_str()))
133 .collect::<std::result::Result<Vec<_>, _>>()
134 .map_err(|_| interior())?;
135 Ok(Box::into_raw(Box::new(Self {
136 shape,
137 name,
138 field_names,
139 })))
140 }
141}
142
143/// Returns the shape of a message the engine types, by id.
144///
145/// # Arguments
146///
147/// * `msgid` - the message id to look up.
148/// * `out_schema` - set to a new schema handle on success, which the caller releases with
149/// [`pamoja_mavlink_schema_free`].
150///
151/// # Returns
152///
153/// [`PamojaStatus::Ok`] on success.
154///
155/// # Errors
156///
157/// Returns [`PamojaStatus::InvalidArgument`] if `out_schema` is null, and
158/// [`PamojaStatus::Unsupported`] for an id this build does not type, which is what
159/// [`PamojaMavlinkSchemaBuilder`] is for.
160///
161/// # Safety
162///
163/// `out_schema` must point at writable storage for one pointer.
164#[no_mangle]
165pub unsafe extern "C" fn pamoja_mavlink_schema_for_id(
166 msgid: u32,
167 out_schema: *mut *mut PamojaMavlinkSchema,
168) -> PamojaStatus {
169 if out_schema.is_null() {
170 set_last_error("out_schema must not be null".to_owned());
171 return PamojaStatus::InvalidArgument;
172 }
173 let Some(shape) = descriptor(msgid) else {
174 set_last_error(format!("message {msgid} is not one this build types"));
175 return PamojaStatus::Unsupported;
176 };
177 publish(shape, out_schema)
178}
179
180/// Returns the shape of a message the engine types, by name.
181///
182/// # Arguments
183///
184/// * `name` - the message name, such as `GLOBAL_POSITION_INT`.
185/// * `out_schema` - set to a new schema handle on success, which the caller releases with
186/// [`pamoja_mavlink_schema_free`].
187///
188/// # Returns
189///
190/// [`PamojaStatus::Ok`] on success.
191///
192/// # Errors
193///
194/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or `name` is not
195/// UTF-8, and [`PamojaStatus::Unsupported`] for a name this build does not type.
196///
197/// # Safety
198///
199/// `name` must be a null-terminated C string, and `out_schema` must point at writable
200/// storage for one pointer.
201#[no_mangle]
202pub unsafe extern "C" fn pamoja_mavlink_schema_for_name(
203 name: *const c_char,
204 out_schema: *mut *mut PamojaMavlinkSchema,
205) -> PamojaStatus {
206 if out_schema.is_null() {
207 set_last_error("out_schema must not be null".to_owned());
208 return PamojaStatus::InvalidArgument;
209 }
210 let Some(name) = read_str(name, "name") else {
211 return PamojaStatus::InvalidArgument;
212 };
213 let Some(shape) = descriptor_by_name(name) else {
214 set_last_error(format!("{name} is not a message this build types"));
215 return PamojaStatus::Unsupported;
216 };
217 publish(shape, out_schema)
218}
219
220/// Returns how many messages this build types.
221///
222/// Together with [`pamoja_mavlink_schema_at`] this enumerates the built-in registry, so a
223/// caller can discover what is available rather than guessing ids.
224///
225/// # Returns
226///
227/// The count.
228#[no_mangle]
229pub extern "C" fn pamoja_mavlink_schema_count() -> usize {
230 DESCRIPTORS.len()
231}
232
233/// Returns the shape of the message at a position in the built-in registry.
234///
235/// # Arguments
236///
237/// * `index` - the position, below [`pamoja_mavlink_schema_count`].
238/// * `out_schema` - set to a new schema handle on success, which the caller releases with
239/// [`pamoja_mavlink_schema_free`].
240///
241/// # Returns
242///
243/// [`PamojaStatus::Ok`] on success.
244///
245/// # Errors
246///
247/// Returns [`PamojaStatus::InvalidArgument`] if `out_schema` is null or `index` is past
248/// the end of the registry.
249///
250/// # Safety
251///
252/// `out_schema` must point at writable storage for one pointer.
253#[no_mangle]
254pub unsafe extern "C" fn pamoja_mavlink_schema_at(
255 index: usize,
256 out_schema: *mut *mut PamojaMavlinkSchema,
257) -> PamojaStatus {
258 if out_schema.is_null() {
259 set_last_error("out_schema must not be null".to_owned());
260 return PamojaStatus::InvalidArgument;
261 }
262 let Some(shape) = DESCRIPTORS.get(index) else {
263 set_last_error(format!("{index} is past the end of the registry"));
264 return PamojaStatus::InvalidArgument;
265 };
266 publish(shape, out_schema)
267}
268
269unsafe fn publish(
270 shape: &MessageDescriptor<'static>,
271 out_schema: *mut *mut PamojaMavlinkSchema,
272) -> PamojaStatus {
273 match PamojaMavlinkSchema::into_handle(OwnedMessageDescriptor::from_descriptor(shape)) {
274 Ok(handle) => {
275 *out_schema = handle;
276 PamojaStatus::Ok
277 }
278 Err(status) => status,
279 }
280}
281
282/// Returns the id of the message a schema describes.
283///
284/// # Arguments
285///
286/// * `schema` - the shape to read.
287///
288/// # Returns
289///
290/// The message id, or `0` if `schema` is null.
291///
292/// # Safety
293///
294/// `schema` must be a live schema handle or null.
295#[no_mangle]
296pub unsafe extern "C" fn pamoja_mavlink_schema_id(schema: *const PamojaMavlinkSchema) -> u32 {
297 schema.as_ref().map_or(0, |schema| schema.shape.id())
298}
299
300/// Returns the name of the message a schema describes.
301///
302/// The pointer borrows the schema and stays valid until it is released.
303///
304/// # Arguments
305///
306/// * `schema` - the shape to read.
307///
308/// # Returns
309///
310/// The name, or null if `schema` is null.
311///
312/// # Safety
313///
314/// `schema` must be a live schema handle or null.
315#[no_mangle]
316pub unsafe extern "C" fn pamoja_mavlink_schema_name(
317 schema: *const PamojaMavlinkSchema,
318) -> *const c_char {
319 schema
320 .as_ref()
321 .map_or(std::ptr::null(), |schema| schema.name.as_ptr())
322}
323
324/// Returns the `CRC_EXTRA` seed a schema implies.
325///
326/// # Arguments
327///
328/// * `schema` - the shape to read.
329///
330/// # Returns
331///
332/// The seed, or `0` if `schema` is null.
333///
334/// # Safety
335///
336/// `schema` must be a live schema handle or null.
337#[no_mangle]
338pub unsafe extern "C" fn pamoja_mavlink_schema_crc_extra(schema: *const PamojaMavlinkSchema) -> u8 {
339 schema.as_ref().map_or(0, |schema| schema.shape.crc_extra())
340}
341
342/// Returns the length of the message on the wire, extensions included.
343///
344/// # Arguments
345///
346/// * `schema` - the shape to read.
347///
348/// # Returns
349///
350/// The length in bytes, or `0` if `schema` is null.
351///
352/// # Safety
353///
354/// `schema` must be a live schema handle or null.
355#[no_mangle]
356pub unsafe extern "C" fn pamoja_mavlink_schema_wire_len(
357 schema: *const PamojaMavlinkSchema,
358) -> usize {
359 schema.as_ref().map_or(0, |schema| {
360 schema.shape.with_descriptor(|shape| shape.wire_len())
361 })
362}
363
364/// Returns how many fields a message has.
365///
366/// # Arguments
367///
368/// * `schema` - the shape to read.
369///
370/// # Returns
371///
372/// The field count, or `0` if `schema` is null.
373///
374/// # Safety
375///
376/// `schema` must be a live schema handle or null.
377#[no_mangle]
378pub unsafe extern "C" fn pamoja_mavlink_schema_field_count(
379 schema: *const PamojaMavlinkSchema,
380) -> usize {
381 schema
382 .as_ref()
383 .map_or(0, |schema| schema.shape.fields().len())
384}
385
386/// Describes one field of a message.
387///
388/// # Arguments
389///
390/// * `schema` - the shape to read.
391/// * `index` - the field position, in wire order, below
392/// [`pamoja_mavlink_schema_field_count`].
393/// * `out_field` - set to the field's description on success.
394///
395/// # Returns
396///
397/// [`PamojaStatus::Ok`] on success.
398///
399/// # Errors
400///
401/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or `index` is past
402/// the end of the field list.
403///
404/// # Safety
405///
406/// `schema` must be a live schema handle, and `out_field` must point at writable storage
407/// for one [`PamojaMavlinkFieldInfo`].
408#[no_mangle]
409pub unsafe extern "C" fn pamoja_mavlink_schema_field(
410 schema: *const PamojaMavlinkSchema,
411 index: usize,
412 out_field: *mut PamojaMavlinkFieldInfo,
413) -> PamojaStatus {
414 let Some(schema) = schema.as_ref() else {
415 set_last_error("schema must not be null".to_owned());
416 return PamojaStatus::InvalidArgument;
417 };
418 if out_field.is_null() {
419 set_last_error("out_field must not be null".to_owned());
420 return PamojaStatus::InvalidArgument;
421 }
422 let Some(field) = schema.shape.fields().get(index) else {
423 set_last_error(format!("{index} is past the end of the field list"));
424 return PamojaStatus::InvalidArgument;
425 };
426 let offset = schema
427 .shape
428 .with_descriptor(|shape| shape.offset_of(&field.name))
429 .expect("a field of this shape has an offset in it");
430
431 *out_field = PamojaMavlinkFieldInfo {
432 name: schema.field_names[index].as_ptr(),
433 type_name: TYPE_NAMES[type_index(field.ty)].as_ptr().cast(),
434 field_type: code_of(field.ty),
435 array_len: field.array_len,
436 extension: u8::from(field.extension),
437 offset,
438 };
439 PamojaStatus::Ok
440}
441
442// The type names handed back as borrowed C strings. They are null-terminated here so a
443// caller can read them in place, the way it reads a field name out of a schema.
444const TYPE_NAMES: [&[u8]; 11] = [
445 b"uint8_t\0",
446 b"int8_t\0",
447 b"char\0",
448 b"uint16_t\0",
449 b"int16_t\0",
450 b"uint32_t\0",
451 b"int32_t\0",
452 b"uint64_t\0",
453 b"int64_t\0",
454 b"float\0",
455 b"double\0",
456];
457
458fn type_index(ty: FieldType) -> usize {
459 code_of(ty) as usize - 1
460}
461
462/// Releases a schema.
463///
464/// # Arguments
465///
466/// * `schema` - the handle to release; null is ignored.
467///
468/// # Safety
469///
470/// `schema` must have come from one of the schema constructors and must not be used
471/// afterwards.
472#[no_mangle]
473pub unsafe extern "C" fn pamoja_mavlink_schema_free(schema: *mut PamojaMavlinkSchema) {
474 if !schema.is_null() {
475 drop(Box::from_raw(schema));
476 }
477}
478
479/// Adds a schema's message to a dialect table, so frames carrying it check.
480///
481/// # Arguments
482///
483/// * `dialect` - the table to extend.
484/// * `schema` - the shape whose id and seed to add.
485///
486/// # Returns
487///
488/// [`PamojaStatus::Ok`] on success.
489///
490/// # Errors
491///
492/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
493///
494/// # Safety
495///
496/// `dialect` must be a live dialect handle and `schema` a live schema handle.
497#[no_mangle]
498pub unsafe extern "C" fn pamoja_mavlink_dialect_add_schema(
499 dialect: *mut PamojaMavlinkDialect,
500 schema: *const PamojaMavlinkSchema,
501) -> PamojaStatus {
502 let Some(schema) = schema.as_ref() else {
503 set_last_error("schema must not be null".to_owned());
504 return PamojaStatus::InvalidArgument;
505 };
506 crate::mavlink::pamoja_mavlink_dialect_add(dialect, schema.shape.id(), schema.shape.crc_extra())
507}
508
509/// Describes a message this build does not type, one field at a time.
510///
511/// Fields are added in the order the message definition lists them;
512/// [`pamoja_mavlink_schema_builder_build`] puts them in wire order and derives the
513/// `CRC_EXTRA` seed from the result.
514pub struct PamojaMavlinkSchemaBuilder {
515 builder: Option<MessageDescriptorBuilder>,
516}
517
518/// Starts describing a message.
519///
520/// # Arguments
521///
522/// * `msgid` - the message id on the wire.
523/// * `name` - the message name, which the seed derivation folds in, so it must match the
524/// dialect exactly.
525///
526/// # Returns
527///
528/// A builder the caller releases with [`pamoja_mavlink_schema_builder_free`] or consumes
529/// with [`pamoja_mavlink_schema_builder_build`], or null if `name` is null or not UTF-8.
530///
531/// # Safety
532///
533/// `name` must be a null-terminated C string.
534#[no_mangle]
535pub unsafe extern "C" fn pamoja_mavlink_schema_builder_new(
536 msgid: u32,
537 name: *const c_char,
538) -> *mut PamojaMavlinkSchemaBuilder {
539 let Some(name) = read_str(name, "name") else {
540 return std::ptr::null_mut();
541 };
542 Box::into_raw(Box::new(PamojaMavlinkSchemaBuilder {
543 builder: Some(MessageDescriptorBuilder::new(msgid, name)),
544 }))
545}
546
547/// Adds a base field, in the order the definition declares it.
548///
549/// # Arguments
550///
551/// * `builder` - the description to extend.
552/// * `name` - the field name.
553/// * `field_type` - the field's type, one of the `PAMOJA_MAVLINK_FIELD_*` codes.
554/// * `array_len` - the element count for an array, or `0` for a scalar.
555///
556/// # Returns
557///
558/// [`PamojaStatus::Ok`] on success.
559///
560/// # Errors
561///
562/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, `name` is not UTF-8, or
563/// `field_type` is not a MAVLink field type.
564///
565/// # Safety
566///
567/// `builder` must be a live builder handle and `name` a null-terminated C string.
568#[no_mangle]
569pub unsafe extern "C" fn pamoja_mavlink_schema_builder_field(
570 builder: *mut PamojaMavlinkSchemaBuilder,
571 name: *const c_char,
572 field_type: u32,
573 array_len: u8,
574) -> PamojaStatus {
575 add_field(builder, name, field_type, array_len, false)
576}
577
578/// Adds a MAVLink 2 extension field, in the order the definition declares it.
579///
580/// Extensions keep their declared order, stay out of the `CRC_EXTRA` seed, and read as zero
581/// from a frame sent by a peer that predates them.
582///
583/// # Arguments
584///
585/// * `builder` - the description to extend.
586/// * `name` - the field name.
587/// * `field_type` - the field's type, one of the `PAMOJA_MAVLINK_FIELD_*` codes.
588/// * `array_len` - the element count for an array, or `0` for a scalar.
589///
590/// # Returns
591///
592/// [`PamojaStatus::Ok`] on success.
593///
594/// # Errors
595///
596/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, `name` is not UTF-8, or
597/// `field_type` is not a MAVLink field type.
598///
599/// # Safety
600///
601/// `builder` must be a live builder handle and `name` a null-terminated C string.
602#[no_mangle]
603pub unsafe extern "C" fn pamoja_mavlink_schema_builder_extension(
604 builder: *mut PamojaMavlinkSchemaBuilder,
605 name: *const c_char,
606 field_type: u32,
607 array_len: u8,
608) -> PamojaStatus {
609 add_field(builder, name, field_type, array_len, true)
610}
611
612unsafe fn add_field(
613 builder: *mut PamojaMavlinkSchemaBuilder,
614 name: *const c_char,
615 field_type: u32,
616 array_len: u8,
617 extension: bool,
618) -> PamojaStatus {
619 let Some(handle) = builder.as_mut() else {
620 set_last_error("builder must not be null".to_owned());
621 return PamojaStatus::InvalidArgument;
622 };
623 let Some(name) = read_str(name, "name") else {
624 return PamojaStatus::InvalidArgument;
625 };
626 let ty = match type_of(field_type) {
627 Ok(ty) => ty,
628 Err(status) => return status,
629 };
630 let Some(builder) = handle.builder.take() else {
631 set_last_error("this builder has already been built".to_owned());
632 return PamojaStatus::InvalidArgument;
633 };
634 handle.builder = Some(if extension {
635 builder.extension(name, ty, array_len)
636 } else {
637 builder.field(name, ty, array_len)
638 });
639 PamojaStatus::Ok
640}
641
642/// Puts the declared fields in wire order and finishes the shape.
643///
644/// The builder is consumed and released whether or not the shape is valid, so it must not
645/// be used again.
646///
647/// # Arguments
648///
649/// * `builder` - the description to finish.
650/// * `out_schema` - set to a new schema handle on success, which the caller releases with
651/// [`pamoja_mavlink_schema_free`].
652///
653/// # Returns
654///
655/// [`PamojaStatus::Ok`] on success.
656///
657/// # Errors
658///
659/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, if two fields share a
660/// name, or if the fields do not fit a MAVLink payload.
661///
662/// # Safety
663///
664/// `builder` must be a live builder handle, and `out_schema` must point at writable
665/// storage for one pointer.
666#[no_mangle]
667pub unsafe extern "C" fn pamoja_mavlink_schema_builder_build(
668 builder: *mut PamojaMavlinkSchemaBuilder,
669 out_schema: *mut *mut PamojaMavlinkSchema,
670) -> PamojaStatus {
671 if builder.is_null() {
672 set_last_error("builder must not be null".to_owned());
673 return PamojaStatus::InvalidArgument;
674 }
675 if out_schema.is_null() {
676 set_last_error("out_schema must not be null".to_owned());
677 return PamojaStatus::InvalidArgument;
678 }
679 let handle = Box::from_raw(builder);
680 let Some(builder) = handle.builder else {
681 set_last_error("this builder has already been built".to_owned());
682 return PamojaStatus::InvalidArgument;
683 };
684 let shape = match builder.build() {
685 Ok(shape) => shape,
686 Err(error) => return status_of(error),
687 };
688 match PamojaMavlinkSchema::into_handle(shape) {
689 Ok(schema) => {
690 *out_schema = schema;
691 PamojaStatus::Ok
692 }
693 Err(status) => status,
694 }
695}
696
697/// Releases a builder that was never built.
698///
699/// # Arguments
700///
701/// * `builder` - the handle to release; null is ignored.
702///
703/// # Safety
704///
705/// `builder` must have come from [`pamoja_mavlink_schema_builder_new`], must not already
706/// have been built, and must not be used afterwards.
707#[no_mangle]
708pub unsafe extern "C" fn pamoja_mavlink_schema_builder_free(
709 builder: *mut PamojaMavlinkSchemaBuilder,
710) {
711 if !builder.is_null() {
712 drop(Box::from_raw(builder));
713 }
714}
715
716/// A message being written or read field by field against a schema.
717pub struct PamojaMavlinkMessage {
718 shape: OwnedMessageDescriptor,
719 payload: Vec<u8>,
720}
721
722impl PamojaMavlinkMessage {
723 /// Wraps a message the engine decoded, for another module in this crate to hand back.
724 pub(crate) fn from_typed(shape: &MessageDescriptor<'static>, payload: Vec<u8>) -> *mut Self {
725 Box::into_raw(Box::new(Self {
726 shape: OwnedMessageDescriptor::from_descriptor(shape),
727 payload,
728 }))
729 }
730}
731
732/// Runs a read against a message, writing the result through `out`.
733unsafe fn get<T: Copy>(
734 message: *const PamojaMavlinkMessage,
735 name: *const c_char,
736 out: *mut T,
737 query: impl FnOnce(&DynamicMessage<'_>, &str) -> MavlinkResult<T>,
738) -> PamojaStatus {
739 let Some(message) = message.as_ref() else {
740 set_last_error("message must not be null".to_owned());
741 return PamojaStatus::InvalidArgument;
742 };
743 if out.is_null() {
744 set_last_error("the output pointer must not be null".to_owned());
745 return PamojaStatus::InvalidArgument;
746 }
747 let Some(name) = read_str(name, "field") else {
748 return PamojaStatus::InvalidArgument;
749 };
750 let result = message.shape.with_descriptor(|shape| {
751 let dynamic = DynamicMessage::decode(shape, &message.payload)?;
752 query(&dynamic, name)
753 });
754 match result {
755 Ok(value) => {
756 *out = value;
757 PamojaStatus::Ok
758 }
759 Err(error) => status_of(error),
760 }
761}
762
763/// Runs a write against a message, keeping its bytes unchanged if the write fails.
764unsafe fn set(
765 message: *mut PamojaMavlinkMessage,
766 name: *const c_char,
767 step: impl FnOnce(&mut DynamicMessage<'_>, &str) -> MavlinkResult<()>,
768) -> PamojaStatus {
769 let Some(message) = message.as_mut() else {
770 set_last_error("message must not be null".to_owned());
771 return PamojaStatus::InvalidArgument;
772 };
773 let Some(name) = read_str(name, "field") else {
774 return PamojaStatus::InvalidArgument;
775 };
776 let payload = std::mem::take(&mut message.payload);
777 let result = message.shape.with_descriptor(|shape| {
778 let mut dynamic = DynamicMessage::decode(shape, &payload)?;
779 step(&mut dynamic, name)?;
780 Ok(dynamic.payload().to_vec())
781 });
782 match result {
783 Ok(updated) => {
784 message.payload = updated;
785 PamojaStatus::Ok
786 }
787 Err(error) => {
788 message.payload = payload;
789 status_of(error)
790 }
791 }
792}
793
794/// Creates a message with every field zero.
795///
796/// # Arguments
797///
798/// * `schema` - the shape of the message to build.
799/// * `out_message` - set to a new message handle on success, which the caller releases
800/// with [`pamoja_mavlink_message_free`].
801///
802/// # Returns
803///
804/// [`PamojaStatus::Ok`] on success.
805///
806/// # Errors
807///
808/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or the shape does
809/// not fit a MAVLink payload.
810///
811/// # Safety
812///
813/// `schema` must be a live schema handle, and `out_message` must point at writable storage
814/// for one pointer.
815#[no_mangle]
816pub unsafe extern "C" fn pamoja_mavlink_message_new(
817 schema: *const PamojaMavlinkSchema,
818 out_message: *mut *mut PamojaMavlinkMessage,
819) -> PamojaStatus {
820 let Some(schema) = schema.as_ref() else {
821 set_last_error("schema must not be null".to_owned());
822 return PamojaStatus::InvalidArgument;
823 };
824 if out_message.is_null() {
825 set_last_error("out_message must not be null".to_owned());
826 return PamojaStatus::InvalidArgument;
827 }
828 let built = schema.shape.with_descriptor(|shape| {
829 DynamicMessage::new(shape).map(|message| message.payload().to_vec())
830 });
831 match built {
832 Ok(payload) => {
833 *out_message = Box::into_raw(Box::new(PamojaMavlinkMessage {
834 shape: schema.shape.clone(),
835 payload,
836 }));
837 PamojaStatus::Ok
838 }
839 Err(error) => status_of(error),
840 }
841}
842
843/// Reads a message out of a frame payload.
844///
845/// A payload shorter than the shape is zero-extended, as MAVLink 2 truncation requires, so
846/// a frame from a peer that trimmed trailing zeros or predates an extension field decodes.
847///
848/// # Arguments
849///
850/// * `schema` - the shape to read the payload as.
851/// * `payload` - the frame payload.
852/// * `payload_len` - the payload length in bytes.
853/// * `out_message` - set to a new message handle on success, which the caller releases
854/// with [`pamoja_mavlink_message_free`].
855///
856/// # Returns
857///
858/// [`PamojaStatus::Ok`] on success.
859///
860/// # Errors
861///
862/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, and
863/// [`PamojaStatus::Codec`] if the payload is longer than the shape describes.
864///
865/// # Safety
866///
867/// `schema` must be a live schema handle, `payload` must point at `payload_len` readable
868/// bytes, and `out_message` must point at writable storage for one pointer.
869#[no_mangle]
870pub unsafe extern "C" fn pamoja_mavlink_message_decode(
871 schema: *const PamojaMavlinkSchema,
872 payload: *const u8,
873 payload_len: usize,
874 out_message: *mut *mut PamojaMavlinkMessage,
875) -> PamojaStatus {
876 let Some(schema) = schema.as_ref() else {
877 set_last_error("schema must not be null".to_owned());
878 return PamojaStatus::InvalidArgument;
879 };
880 if out_message.is_null() {
881 set_last_error("out_message must not be null".to_owned());
882 return PamojaStatus::InvalidArgument;
883 }
884 let payload = match read_bytes(payload, payload_len) {
885 Ok(payload) => payload,
886 Err(status) => return status,
887 };
888 let decoded = schema.shape.with_descriptor(|shape| {
889 DynamicMessage::decode(shape, &payload).map(|message| message.payload().to_vec())
890 });
891 match decoded {
892 Ok(payload) => {
893 *out_message = Box::into_raw(Box::new(PamojaMavlinkMessage {
894 shape: schema.shape.clone(),
895 payload,
896 }));
897 PamojaStatus::Ok
898 }
899 Err(error) => status_of(error),
900 }
901}
902
903/// Returns a pointer to a message's payload bytes.
904///
905/// The pointer borrows the message and stays valid until it is written to or released.
906///
907/// # Arguments
908///
909/// * `message` - the message to read.
910/// * `out_len` - set to the payload length in bytes.
911///
912/// # Returns
913///
914/// A pointer to the bytes, or null if either pointer is null.
915///
916/// # Safety
917///
918/// `message` must be a live message handle, and `out_len` must point at writable storage
919/// for one length.
920#[no_mangle]
921pub unsafe extern "C" fn pamoja_mavlink_message_payload(
922 message: *const PamojaMavlinkMessage,
923 out_len: *mut usize,
924) -> *const u8 {
925 let Some(message) = message.as_ref() else {
926 return std::ptr::null();
927 };
928 if out_len.is_null() {
929 return std::ptr::null();
930 }
931 *out_len = message.payload.len();
932 message.payload.as_ptr()
933}
934
935/// Builds a v2 frame carrying a message.
936///
937/// # Arguments
938///
939/// * `message` - the message to send.
940/// * `header` - the addressing fields to stamp on the frame.
941/// * `out_frame` - set to a new frame handle on success, which the caller releases with
942/// `pamoja_mavlink_frame_free`.
943///
944/// # Returns
945///
946/// [`PamojaStatus::Ok`] on success.
947///
948/// # Errors
949///
950/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or the message does
951/// not fit a frame.
952///
953/// # Safety
954///
955/// `message` must be a live message handle, and `out_frame` must point at writable storage
956/// for one pointer.
957#[no_mangle]
958pub unsafe extern "C" fn pamoja_mavlink_message_to_frame(
959 message: *const PamojaMavlinkMessage,
960 header: PamojaMavlinkHeader,
961 out_frame: *mut *mut PamojaMavlinkFrame,
962) -> PamojaStatus {
963 let Some(message) = message.as_ref() else {
964 set_last_error("message must not be null".to_owned());
965 return PamojaStatus::InvalidArgument;
966 };
967 if out_frame.is_null() {
968 set_last_error("out_frame must not be null".to_owned());
969 return PamojaStatus::InvalidArgument;
970 }
971 let built = message.shape.with_descriptor(|shape| {
972 DynamicMessage::decode(shape, &message.payload)?.to_frame(Header::from(header))
973 });
974 match built {
975 Ok(frame) => {
976 *out_frame = PamojaMavlinkFrame::into_handle(frame);
977 PamojaStatus::Ok
978 }
979 Err(error) => status_of(error),
980 }
981}
982
983/// Releases a message.
984///
985/// # Arguments
986///
987/// * `message` - the handle to release; null is ignored.
988///
989/// # Safety
990///
991/// `message` must have come from [`pamoja_mavlink_message_new`] or
992/// [`pamoja_mavlink_message_decode`] and must not be used afterwards.
993#[no_mangle]
994pub unsafe extern "C" fn pamoja_mavlink_message_free(message: *mut PamojaMavlinkMessage) {
995 if !message.is_null() {
996 drop(Box::from_raw(message));
997 }
998}
999
1000/// Reads a field as a signed integer.
1001///
1002/// Any integer field reads this way, whatever its width or sign.
1003///
1004/// # Arguments
1005///
1006/// * `message` - the message to read.
1007/// * `field` - the field name.
1008/// * `index` - the element to read, or `0` for a scalar field.
1009/// * `out_value` - set to the value on success.
1010///
1011/// # Returns
1012///
1013/// [`PamojaStatus::Ok`] on success.
1014///
1015/// # Errors
1016///
1017/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1018/// field, the element is past the end of an array, the field is floating-point, or a
1019/// `uint64_t` value is above the signed range.
1020///
1021/// # Safety
1022///
1023/// `message` must be a live message handle, `field` a null-terminated C string, and
1024/// `out_value` must point at writable storage for one 64-bit integer.
1025#[no_mangle]
1026pub unsafe extern "C" fn pamoja_mavlink_message_get_int(
1027 message: *const PamojaMavlinkMessage,
1028 field: *const c_char,
1029 index: usize,
1030 out_value: *mut i64,
1031) -> PamojaStatus {
1032 get(message, field, out_value, |dynamic, name| {
1033 dynamic.get_int(name, index)
1034 })
1035}
1036
1037/// Reads a field as an unsigned integer.
1038///
1039/// Any integer field reads this way, whatever its width or sign.
1040///
1041/// # Arguments
1042///
1043/// * `message` - the message to read.
1044/// * `field` - the field name.
1045/// * `index` - the element to read, or `0` for a scalar field.
1046/// * `out_value` - set to the value on success.
1047///
1048/// # Returns
1049///
1050/// [`PamojaStatus::Ok`] on success.
1051///
1052/// # Errors
1053///
1054/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1055/// field, the element is past the end of an array, the field is floating-point, or the
1056/// value is negative.
1057///
1058/// # Safety
1059///
1060/// `message` must be a live message handle, `field` a null-terminated C string, and
1061/// `out_value` must point at writable storage for one 64-bit integer.
1062#[no_mangle]
1063pub unsafe extern "C" fn pamoja_mavlink_message_get_uint(
1064 message: *const PamojaMavlinkMessage,
1065 field: *const c_char,
1066 index: usize,
1067 out_value: *mut u64,
1068) -> PamojaStatus {
1069 get(message, field, out_value, |dynamic, name| {
1070 dynamic.get_uint(name, index)
1071 })
1072}
1073
1074/// Reads a floating-point field.
1075///
1076/// # Arguments
1077///
1078/// * `message` - the message to read.
1079/// * `field` - the field name.
1080/// * `index` - the element to read, or `0` for a scalar field.
1081/// * `out_value` - set to the value on success, widened from `float` where needed.
1082///
1083/// # Returns
1084///
1085/// [`PamojaStatus::Ok`] on success.
1086///
1087/// # Errors
1088///
1089/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1090/// field, the element is past the end of an array, or the field is an integer.
1091///
1092/// # Safety
1093///
1094/// `message` must be a live message handle, `field` a null-terminated C string, and
1095/// `out_value` must point at writable storage for one double.
1096#[no_mangle]
1097pub unsafe extern "C" fn pamoja_mavlink_message_get_float(
1098 message: *const PamojaMavlinkMessage,
1099 field: *const c_char,
1100 index: usize,
1101 out_value: *mut f64,
1102) -> PamojaStatus {
1103 get(message, field, out_value, |dynamic, name| {
1104 dynamic.get_float(name, index)
1105 })
1106}
1107
1108/// Writes a signed integer into a field.
1109///
1110/// # Arguments
1111///
1112/// * `message` - the message to write.
1113/// * `field` - the field name.
1114/// * `index` - the element to write, or `0` for a scalar field.
1115/// * `value` - the value to store.
1116///
1117/// # Returns
1118///
1119/// [`PamojaStatus::Ok`] on success.
1120///
1121/// # Errors
1122///
1123/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1124/// field, the element is past the end of an array, the field is floating-point, or the
1125/// value does not fit the field's type.
1126///
1127/// # Safety
1128///
1129/// `message` must be a live message handle and `field` a null-terminated C string.
1130#[no_mangle]
1131pub unsafe extern "C" fn pamoja_mavlink_message_set_int(
1132 message: *mut PamojaMavlinkMessage,
1133 field: *const c_char,
1134 index: usize,
1135 value: i64,
1136) -> PamojaStatus {
1137 set(message, field, |dynamic, name| {
1138 dynamic.set_int(name, index, value)
1139 })
1140}
1141
1142/// Writes an unsigned integer into a field.
1143///
1144/// # Arguments
1145///
1146/// * `message` - the message to write.
1147/// * `field` - the field name.
1148/// * `index` - the element to write, or `0` for a scalar field.
1149/// * `value` - the value to store.
1150///
1151/// # Returns
1152///
1153/// [`PamojaStatus::Ok`] on success.
1154///
1155/// # Errors
1156///
1157/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1158/// field, the element is past the end of an array, the field is floating-point, or the
1159/// value does not fit the field's type.
1160///
1161/// # Safety
1162///
1163/// `message` must be a live message handle and `field` a null-terminated C string.
1164#[no_mangle]
1165pub unsafe extern "C" fn pamoja_mavlink_message_set_uint(
1166 message: *mut PamojaMavlinkMessage,
1167 field: *const c_char,
1168 index: usize,
1169 value: u64,
1170) -> PamojaStatus {
1171 set(message, field, |dynamic, name| {
1172 dynamic.set_uint(name, index, value)
1173 })
1174}
1175
1176/// Writes a floating-point field.
1177///
1178/// # Arguments
1179///
1180/// * `message` - the message to write.
1181/// * `field` - the field name.
1182/// * `index` - the element to write, or `0` for a scalar field.
1183/// * `value` - the value to store, narrowed to `float` where the field is one.
1184///
1185/// # Returns
1186///
1187/// [`PamojaStatus::Ok`] on success.
1188///
1189/// # Errors
1190///
1191/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1192/// field, the element is past the end of an array, or the field is an integer.
1193///
1194/// # Safety
1195///
1196/// `message` must be a live message handle and `field` a null-terminated C string.
1197#[no_mangle]
1198pub unsafe extern "C" fn pamoja_mavlink_message_set_float(
1199 message: *mut PamojaMavlinkMessage,
1200 field: *const c_char,
1201 index: usize,
1202 value: f64,
1203) -> PamojaStatus {
1204 set(message, field, |dynamic, name| {
1205 dynamic.set_float(name, index, value)
1206 })
1207}
1208
1209/// Reads a field as a double, whatever its type.
1210///
1211/// This is the reading a host language with one numeric type needs. An integer field wider
1212/// than 53 bits can exceed what a double holds exactly, so read those with
1213/// [`pamoja_mavlink_message_get_int`] or [`pamoja_mavlink_message_get_uint`] where the
1214/// exact value matters.
1215///
1216/// # Arguments
1217///
1218/// * `message` - the message to read.
1219/// * `field` - the field name.
1220/// * `index` - the element to read, or `0` for a scalar field.
1221/// * `out_value` - set to the value on success.
1222///
1223/// # Returns
1224///
1225/// [`PamojaStatus::Ok`] on success.
1226///
1227/// # Errors
1228///
1229/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1230/// field, or the element is past the end of an array.
1231///
1232/// # Safety
1233///
1234/// `message` must be a live message handle, `field` a null-terminated C string, and
1235/// `out_value` must point at writable storage for one double.
1236#[no_mangle]
1237pub unsafe extern "C" fn pamoja_mavlink_message_get_number(
1238 message: *const PamojaMavlinkMessage,
1239 field: *const c_char,
1240 index: usize,
1241 out_value: *mut f64,
1242) -> PamojaStatus {
1243 get(message, field, out_value, |dynamic, name| {
1244 dynamic.get_number(name, index)
1245 })
1246}
1247
1248/// Writes a double into a field, converting it to the field's type.
1249///
1250/// This is the writing a host language with one numeric type needs. A value bound for an
1251/// integer field must be a whole number within that field's range, so a fractional or
1252/// oversized value is refused rather than silently truncated.
1253///
1254/// # Arguments
1255///
1256/// * `message` - the message to write.
1257/// * `field` - the field name.
1258/// * `index` - the element to write, or `0` for a scalar field.
1259/// * `value` - the value to store.
1260///
1261/// # Returns
1262///
1263/// [`PamojaStatus::Ok`] on success.
1264///
1265/// # Errors
1266///
1267/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1268/// field, the element is past the end of an array, or an integer field is given a value
1269/// that is fractional, infinite, not a number, or outside the range its width holds.
1270///
1271/// # Safety
1272///
1273/// `message` must be a live message handle and `field` a null-terminated C string.
1274#[no_mangle]
1275pub unsafe extern "C" fn pamoja_mavlink_message_set_number(
1276 message: *mut PamojaMavlinkMessage,
1277 field: *const c_char,
1278 index: usize,
1279 value: f64,
1280) -> PamojaStatus {
1281 set(message, field, |dynamic, name| {
1282 dynamic.set_number(name, index, value)
1283 })
1284}
1285
1286/// Copies the raw bytes of a byte-wide array field out.
1287///
1288/// This is how a `char` array carrying text is read: the bytes come back padded with zeros,
1289/// and the caller stops at the first one.
1290///
1291/// # Arguments
1292///
1293/// * `message` - the message to read.
1294/// * `field` - the field name.
1295/// * `out_bytes` - the destination, which must hold at least the field's length.
1296/// * `out_bytes_len` - the space available at `out_bytes`.
1297///
1298/// # Returns
1299///
1300/// [`PamojaStatus::Ok`] on success.
1301///
1302/// # Errors
1303///
1304/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1305/// field, the field is not a byte-wide array, or the destination is too small.
1306///
1307/// # Safety
1308///
1309/// `message` must be a live message handle, `field` a null-terminated C string, and
1310/// `out_bytes` must point at `out_bytes_len` writable bytes.
1311#[no_mangle]
1312pub unsafe extern "C" fn pamoja_mavlink_message_get_bytes(
1313 message: *const PamojaMavlinkMessage,
1314 field: *const c_char,
1315 out_bytes: *mut u8,
1316 out_bytes_len: usize,
1317) -> PamojaStatus {
1318 let Some(message) = message.as_ref() else {
1319 set_last_error("message must not be null".to_owned());
1320 return PamojaStatus::InvalidArgument;
1321 };
1322 if out_bytes.is_null() {
1323 set_last_error("out_bytes must not be null".to_owned());
1324 return PamojaStatus::InvalidArgument;
1325 }
1326 let Some(name) = read_str(field, "field") else {
1327 return PamojaStatus::InvalidArgument;
1328 };
1329 let out = std::slice::from_raw_parts_mut(out_bytes, out_bytes_len);
1330 let read = message.shape.with_descriptor(|shape| {
1331 DynamicMessage::decode(shape, &message.payload)?
1332 .get_bytes(name, out)
1333 .map(|_| ())
1334 });
1335 match read {
1336 Ok(()) => PamojaStatus::Ok,
1337 Err(error) => status_of(error),
1338 }
1339}
1340
1341/// Writes the raw bytes of a byte-wide array field, zero-padding the rest.
1342///
1343/// This is how a `char` array carrying text is written: pass the text's bytes and the field
1344/// is padded to its declared length.
1345///
1346/// # Arguments
1347///
1348/// * `message` - the message to write.
1349/// * `field` - the field name.
1350/// * `bytes` - the bytes to store, at most the field's declared length.
1351/// * `bytes_len` - the number of bytes to store.
1352///
1353/// # Returns
1354///
1355/// [`PamojaStatus::Ok`] on success.
1356///
1357/// # Errors
1358///
1359/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1360/// field, the field is not a byte-wide array, or the bytes are longer than the field.
1361///
1362/// # Safety
1363///
1364/// `message` must be a live message handle, `field` a null-terminated C string, and
1365/// `bytes` must point at `bytes_len` readable bytes.
1366#[no_mangle]
1367pub unsafe extern "C" fn pamoja_mavlink_message_set_bytes(
1368 message: *mut PamojaMavlinkMessage,
1369 field: *const c_char,
1370 bytes: *const u8,
1371 bytes_len: usize,
1372) -> PamojaStatus {
1373 let bytes = match read_bytes(bytes, bytes_len) {
1374 Ok(bytes) => bytes,
1375 Err(status) => return status,
1376 };
1377 set(message, field, |dynamic, name| {
1378 dynamic.set_bytes(name, &bytes)
1379 })
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384 use super::*;
1385 use std::ffi::CString;
1386
1387 unsafe fn schema_for(name: &str) -> *mut PamojaMavlinkSchema {
1388 let mut schema = std::ptr::null_mut();
1389 let wanted = CString::new(name).unwrap();
1390 assert_eq!(
1391 pamoja_mavlink_schema_for_name(wanted.as_ptr(), &mut schema),
1392 PamojaStatus::Ok
1393 );
1394 schema
1395 }
1396
1397 #[test]
1398 fn a_typed_message_is_described_and_filled_in_by_name() {
1399 unsafe {
1400 let schema = schema_for("HEARTBEAT");
1401 assert_eq!(pamoja_mavlink_schema_id(schema), 0);
1402 assert_eq!(pamoja_mavlink_schema_crc_extra(schema), 50);
1403 assert_eq!(pamoja_mavlink_schema_wire_len(schema), 9);
1404 assert_eq!(pamoja_mavlink_schema_field_count(schema), 6);
1405
1406 // The 32-bit field leads, as wire order puts it.
1407 let mut field = std::mem::zeroed::<PamojaMavlinkFieldInfo>();
1408 assert_eq!(
1409 pamoja_mavlink_schema_field(schema, 0, &mut field),
1410 PamojaStatus::Ok
1411 );
1412 assert_eq!(
1413 std::ffi::CStr::from_ptr(field.name).to_str().unwrap(),
1414 "custom_mode"
1415 );
1416 assert_eq!(
1417 std::ffi::CStr::from_ptr(field.type_name).to_str().unwrap(),
1418 "uint32_t"
1419 );
1420 assert_eq!(field.field_type, PAMOJA_MAVLINK_FIELD_UINT32);
1421 assert_eq!(field.offset, 0);
1422 assert_eq!(field.extension, 0);
1423
1424 let mut message = std::ptr::null_mut();
1425 assert_eq!(
1426 pamoja_mavlink_message_new(schema, &mut message),
1427 PamojaStatus::Ok
1428 );
1429 let kind = CString::new("type").unwrap();
1430 assert_eq!(
1431 pamoja_mavlink_message_set_uint(message, kind.as_ptr(), 0, 18),
1432 PamojaStatus::Ok
1433 );
1434 let mut value = 0u64;
1435 assert_eq!(
1436 pamoja_mavlink_message_get_uint(message, kind.as_ptr(), 0, &mut value),
1437 PamojaStatus::Ok
1438 );
1439 assert_eq!(value, 18);
1440
1441 // Out of range for a uint8_t, and the message keeps its old bytes.
1442 assert_eq!(
1443 pamoja_mavlink_message_set_uint(message, kind.as_ptr(), 0, 300),
1444 PamojaStatus::InvalidArgument
1445 );
1446 assert_eq!(
1447 pamoja_mavlink_message_get_uint(message, kind.as_ptr(), 0, &mut value),
1448 PamojaStatus::Ok
1449 );
1450 assert_eq!(value, 18);
1451
1452 let mut len = 0;
1453 let payload = pamoja_mavlink_message_payload(message, &mut len);
1454 assert_eq!(std::slice::from_raw_parts(payload, len)[4], 18);
1455
1456 pamoja_mavlink_message_free(message);
1457 pamoja_mavlink_schema_free(schema);
1458 }
1459 }
1460
1461 #[test]
1462 fn a_private_message_is_described_and_carried_like_any_other() {
1463 unsafe {
1464 let name = CString::new("BATTERY_CELLS").unwrap();
1465 let builder = pamoja_mavlink_schema_builder_new(50_000, name.as_ptr());
1466 assert!(!builder.is_null());
1467
1468 let cells = CString::new("cell_mv").unwrap();
1469 let pack = CString::new("pack_id").unwrap();
1470 let uptime = CString::new("uptime_ms").unwrap();
1471 assert_eq!(
1472 pamoja_mavlink_schema_builder_field(
1473 builder,
1474 cells.as_ptr(),
1475 PAMOJA_MAVLINK_FIELD_UINT16,
1476 6
1477 ),
1478 PamojaStatus::Ok
1479 );
1480 assert_eq!(
1481 pamoja_mavlink_schema_builder_field(
1482 builder,
1483 pack.as_ptr(),
1484 PAMOJA_MAVLINK_FIELD_UINT8,
1485 0
1486 ),
1487 PamojaStatus::Ok
1488 );
1489 assert_eq!(
1490 pamoja_mavlink_schema_builder_field(
1491 builder,
1492 uptime.as_ptr(),
1493 PAMOJA_MAVLINK_FIELD_UINT32,
1494 0
1495 ),
1496 PamojaStatus::Ok
1497 );
1498
1499 let mut schema = std::ptr::null_mut();
1500 assert_eq!(
1501 pamoja_mavlink_schema_builder_build(builder, &mut schema),
1502 PamojaStatus::Ok
1503 );
1504 assert_eq!(pamoja_mavlink_schema_wire_len(schema), 17);
1505
1506 // Wire order pulls the 32-bit field to the front.
1507 let mut field = std::mem::zeroed::<PamojaMavlinkFieldInfo>();
1508 assert_eq!(
1509 pamoja_mavlink_schema_field(schema, 0, &mut field),
1510 PamojaStatus::Ok
1511 );
1512 assert_eq!(
1513 std::ffi::CStr::from_ptr(field.name).to_str().unwrap(),
1514 "uptime_ms"
1515 );
1516
1517 let mut message = std::ptr::null_mut();
1518 assert_eq!(
1519 pamoja_mavlink_message_new(schema, &mut message),
1520 PamojaStatus::Ok
1521 );
1522 assert_eq!(
1523 pamoja_mavlink_message_set_uint(message, cells.as_ptr(), 3, 4_150),
1524 PamojaStatus::Ok
1525 );
1526 let mut value = 0u64;
1527 assert_eq!(
1528 pamoja_mavlink_message_get_uint(message, cells.as_ptr(), 3, &mut value),
1529 PamojaStatus::Ok
1530 );
1531 assert_eq!(value, 4_150);
1532
1533 pamoja_mavlink_message_free(message);
1534 pamoja_mavlink_schema_free(schema);
1535 }
1536 }
1537
1538 #[test]
1539 fn an_unknown_message_or_field_is_reported_rather_than_guessed() {
1540 unsafe {
1541 let mut schema = std::ptr::null_mut();
1542 assert_eq!(
1543 pamoja_mavlink_schema_for_id(50_000, &mut schema),
1544 PamojaStatus::Unsupported
1545 );
1546
1547 let heartbeat = schema_for("HEARTBEAT");
1548 let mut message = std::ptr::null_mut();
1549 assert_eq!(
1550 pamoja_mavlink_message_new(heartbeat, &mut message),
1551 PamojaStatus::Ok
1552 );
1553 let missing = CString::new("throttle").unwrap();
1554 let mut value = 0u64;
1555 assert_eq!(
1556 pamoja_mavlink_message_get_uint(message, missing.as_ptr(), 0, &mut value),
1557 PamojaStatus::InvalidArgument
1558 );
1559 pamoja_mavlink_message_free(message);
1560 pamoja_mavlink_schema_free(heartbeat);
1561 }
1562 }
1563}