From 5824743b027c5972af9a7561ca5dd3bd57374897 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Mon, 25 Aug 2025 10:40:35 +0200 Subject: [PATCH 1/8] Add wrapper for char arrays to access str conveniently This adds a wrapper around char arrays that allows to access them either as slices or as strings. When accessing the string representation, it will automatically convert the char array to a string slice up to the first null byte. --- mavlink-bindgen/src/parser.rs | 48 ++++-- ...apshots__deprecated.xml@deprecated.rs.snap | 5 +- ...snapshots__heartbeat.xml@heartbeat.rs.snap | 4 +- ...scription.xml@no_field_description.rs.snap | 5 +- ...apshots__parameters.xml@parameters.rs.snap | 62 +++----- mavlink-core/src/lib.rs | 1 + mavlink-core/src/types.rs | 140 ++++++++++++++++++ mavlink/tests/serde_test.rs | 4 +- mavlink/tests/v2_encode_decode_tests.rs | 2 +- 9 files changed, 211 insertions(+), 60 deletions(-) create mode 100644 mavlink-core/src/types.rs diff --git a/mavlink-bindgen/src/parser.rs b/mavlink-bindgen/src/parser.rs index 6a5c4c77e5c..72e6614d9e1 100644 --- a/mavlink-bindgen/src/parser.rs +++ b/mavlink-bindgen/src/parser.rs @@ -215,7 +215,7 @@ impl MavProfile { #[allow(unused_imports)] use bitflags::bitflags; - use mavlink_core::{MavlinkVersion, Message, MessageData, bytes::Bytes, bytes_mut::BytesMut}; + use mavlink_core::{MavlinkVersion, Message, MessageData, bytes::Bytes, bytes_mut::BytesMut, types::CharArray}; #[cfg(feature = "serde")] use serde::{Serialize, Deserialize}; @@ -1113,6 +1113,7 @@ pub enum MavType { Char, Float, Double, + CharArray(usize), Array(Box, usize), } @@ -1133,16 +1134,18 @@ impl MavType { "float" => Some(Float), "Double" => Some(Double), "double" => Some(Double), - _ => { - if s.ends_with(']') { - let start = s.find('[')?; - let size = s[start + 1..(s.len() - 1)].parse::().ok()?; - let mtype = Self::parse_type(&s[0..start])?; - Some(Array(Box::new(mtype), size)) - } else { - None - } + _ if s.starts_with("char[") => { + let start = s.find('[')?; + let size = s[start + 1..(s.len() - 1)].parse::().ok()?; + Some(CharArray(size)) } + _ if s.ends_with(']') => { + let start = s.find('[')?; + let size = s[start + 1..(s.len() - 1)].parse::().ok()?; + let mtype = Self::parse_type(&s[0..start])?; + Some(Array(Box::new(mtype), size)) + } + _ => None, } } @@ -1162,6 +1165,15 @@ impl MavType { Int64 => quote! {#val = #buf.get_i64_le();}, Float => quote! {#val = #buf.get_f32_le();}, Double => quote! {#val = #buf.get_f64_le();}, + CharArray(size) => { + quote! { + let mut tmp = [0_u8; #size]; + for v in &mut tmp { + *v = #buf.get_u8(); + } + #val = CharArray::new(tmp); + } + } Array(t, _) => { let r = t.rust_reader("e!(let val), buf); quote! { @@ -1190,6 +1202,14 @@ impl MavType { UInt64 => quote! {#buf.put_u64_le(#val);}, Int64 => quote! {#buf.put_i64_le(#val);}, Double => quote! {#buf.put_f64_le(#val);}, + CharArray(_) => { + let w = Char.rust_writer("e!(*val), buf); + quote! { + for val in &#val { + #w + } + } + } Array(t, _size) => { let w = t.rust_writer("e!(*val), buf); quote! { @@ -1209,6 +1229,7 @@ impl MavType { UInt16 | Int16 => 2, UInt32 | Int32 | Float => 4, UInt64 | Int64 | Double => 8, + CharArray(size) => *size, Array(t, size) => t.len() * size, } } @@ -1217,7 +1238,7 @@ impl MavType { fn order_len(&self) -> usize { use self::MavType::*; match self { - UInt8MavlinkVersion | UInt8 | Int8 | Char => 1, + UInt8MavlinkVersion | UInt8 | Int8 | Char | CharArray(_) => 1, UInt16 | Int16 => 2, UInt32 | Int32 | Float => 4, UInt64 | Int64 | Double => 8, @@ -1241,6 +1262,7 @@ impl MavType { UInt64 => "uint64_t".into(), Int64 => "int64_t".into(), Double => "double".into(), + CharArray(_) => "char".into(), Array(t, _) => t.primitive_type(), } } @@ -1261,6 +1283,7 @@ impl MavType { UInt64 => "u64".into(), Int64 => "i64".into(), Double => "f64".into(), + CharArray(size) => format!("CharArray<{}>", size), Array(t, size) => format!("[{};{}]", t.rust_type(), size), } } @@ -1286,6 +1309,7 @@ impl MavType { UInt64 => quote!(0_u64), Int64 => quote!(0_i64), Double => quote!(0.0_f64), + CharArray(size) => quote!(CharArray::new([0_u8; #size])), Array(ty, size) => { let default_value = ty.emit_default_value(dialect_has_version); quote!([#default_value; #size]) @@ -1866,7 +1890,7 @@ pub fn extra_crc(msg: &MavMessage) -> u8 { crc.digest(field.name.as_bytes()); } crc.digest(b" "); - if let MavType::Array(_, size) = field.mavtype { + if let MavType::Array(_, size) | MavType::CharArray(size) = field.mavtype { crc.digest(&[size as u8]); } } diff --git a/mavlink-bindgen/tests/snapshots/e2e_snapshots__deprecated.xml@deprecated.rs.snap b/mavlink-bindgen/tests/snapshots/e2e_snapshots__deprecated.xml@deprecated.rs.snap index c9f36282671..b621c8bb474 100644 --- a/mavlink-bindgen/tests/snapshots/e2e_snapshots__deprecated.xml@deprecated.rs.snap +++ b/mavlink-bindgen/tests/snapshots/e2e_snapshots__deprecated.xml@deprecated.rs.snap @@ -1,6 +1,5 @@ --- source: mavlink-bindgen/tests/e2e_snapshots.rs -assertion_line: 26 expression: contents --- #![doc = "MAVLink deprecated dialect."] @@ -11,7 +10,9 @@ expression: contents use arbitrary::Arbitrary; #[allow(unused_imports)] use bitflags::bitflags; -use mavlink_core::{bytes::Bytes, bytes_mut::BytesMut, MavlinkVersion, Message, MessageData}; +use mavlink_core::{ + bytes::Bytes, bytes_mut::BytesMut, types::CharArray, MavlinkVersion, Message, MessageData, +}; #[allow(unused_imports)] use num_derive::FromPrimitive; #[allow(unused_imports)] diff --git a/mavlink-bindgen/tests/snapshots/e2e_snapshots__heartbeat.xml@heartbeat.rs.snap b/mavlink-bindgen/tests/snapshots/e2e_snapshots__heartbeat.xml@heartbeat.rs.snap index 406cefbf3c1..c6d87ed30c6 100644 --- a/mavlink-bindgen/tests/snapshots/e2e_snapshots__heartbeat.xml@heartbeat.rs.snap +++ b/mavlink-bindgen/tests/snapshots/e2e_snapshots__heartbeat.xml@heartbeat.rs.snap @@ -11,7 +11,9 @@ expression: contents use arbitrary::Arbitrary; #[allow(unused_imports)] use bitflags::bitflags; -use mavlink_core::{bytes::Bytes, bytes_mut::BytesMut, MavlinkVersion, Message, MessageData}; +use mavlink_core::{ + bytes::Bytes, bytes_mut::BytesMut, types::CharArray, MavlinkVersion, Message, MessageData, +}; #[allow(unused_imports)] use num_derive::FromPrimitive; #[allow(unused_imports)] diff --git a/mavlink-bindgen/tests/snapshots/e2e_snapshots__no_field_description.xml@no_field_description.rs.snap b/mavlink-bindgen/tests/snapshots/e2e_snapshots__no_field_description.xml@no_field_description.rs.snap index e3aae837bf2..3acd19d3f81 100644 --- a/mavlink-bindgen/tests/snapshots/e2e_snapshots__no_field_description.xml@no_field_description.rs.snap +++ b/mavlink-bindgen/tests/snapshots/e2e_snapshots__no_field_description.xml@no_field_description.rs.snap @@ -1,6 +1,5 @@ --- source: mavlink-bindgen/tests/e2e_snapshots.rs -assertion_line: 26 expression: contents --- #![doc = "MAVLink no_field_description dialect."] @@ -11,7 +10,9 @@ expression: contents use arbitrary::Arbitrary; #[allow(unused_imports)] use bitflags::bitflags; -use mavlink_core::{bytes::Bytes, bytes_mut::BytesMut, MavlinkVersion, Message, MessageData}; +use mavlink_core::{ + bytes::Bytes, bytes_mut::BytesMut, types::CharArray, MavlinkVersion, Message, MessageData, +}; #[allow(unused_imports)] use num_derive::FromPrimitive; #[allow(unused_imports)] diff --git a/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap b/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap index fa6237bc73b..79b2acfe27a 100644 --- a/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap +++ b/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap @@ -1,6 +1,5 @@ --- source: mavlink-bindgen/tests/e2e_snapshots.rs -assertion_line: 26 expression: contents --- #![doc = "MAVLink parameters dialect."] @@ -11,7 +10,9 @@ expression: contents use arbitrary::Arbitrary; #[allow(unused_imports)] use bitflags::bitflags; -use mavlink_core::{bytes::Bytes, bytes_mut::BytesMut, MavlinkVersion, Message, MessageData}; +use mavlink_core::{ + bytes::Bytes, bytes_mut::BytesMut, types::CharArray, MavlinkVersion, Message, MessageData, +}; #[allow(unused_imports)] use num_derive::FromPrimitive; #[allow(unused_imports)] @@ -156,15 +157,7 @@ pub struct PARAM_REQUEST_READ_DATA { #[doc = "Component ID"] pub target_component: u8, #[doc = "Onboard parameter id, terminated by NULL if the length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte if the length is exactly 16 chars - applications have to provide 16+1 bytes storage if the ID is stored as string"] - #[cfg_attr( - feature = "serde", - serde( - serialize_with = "crate::nulstr::serialize::<_, 16>", - deserialize_with = "crate::nulstr::deserialize::<_, 16>" - ) - )] - #[cfg_attr(feature = "ts", ts(type = "string"))] - pub param_id: [u8; 16], + pub param_id: CharArray<16>, } impl PARAM_REQUEST_READ_DATA { pub const ENCODED_LEN: usize = 20usize; @@ -172,7 +165,7 @@ impl PARAM_REQUEST_READ_DATA { param_index: 0_i16, target_system: 0_u8, target_component: 0_u8, - param_id: [0_u8; 16usize], + param_id: CharArray::new([0_u8; 16usize]), }; #[cfg(feature = "arbitrary")] pub fn random(rng: &mut R) -> Self { @@ -210,10 +203,11 @@ impl MessageData for PARAM_REQUEST_READ_DATA { __struct.param_index = buf.get_i16_le(); __struct.target_system = buf.get_u8(); __struct.target_component = buf.get_u8(); - for v in &mut __struct.param_id { - let val = buf.get_u8(); - *v = val; + let mut tmp = [0_u8; 16usize]; + for v in &mut tmp { + *v = buf.get_u8(); } + __struct.param_id = CharArray::new(tmp); Ok(__struct) } fn ser(&self, version: MavlinkVersion, bytes: &mut [u8]) -> usize { @@ -257,15 +251,7 @@ pub struct PARAM_SET_DATA { #[doc = "Component ID"] pub target_component: u8, #[doc = "Onboard parameter id, terminated by NULL if the length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte if the length is exactly 16 chars - applications have to provide 16+1 bytes storage if the ID is stored as string"] - #[cfg_attr( - feature = "serde", - serde( - serialize_with = "crate::nulstr::serialize::<_, 16>", - deserialize_with = "crate::nulstr::deserialize::<_, 16>" - ) - )] - #[cfg_attr(feature = "ts", ts(type = "string"))] - pub param_id: [u8; 16], + pub param_id: CharArray<16>, #[doc = "Onboard parameter type."] pub param_type: MavParamType, } @@ -275,7 +261,7 @@ impl PARAM_SET_DATA { param_value: 0.0_f32, target_system: 0_u8, target_component: 0_u8, - param_id: [0_u8; 16usize], + param_id: CharArray::new([0_u8; 16usize]), param_type: MavParamType::DEFAULT, }; #[cfg(feature = "arbitrary")] @@ -314,10 +300,11 @@ impl MessageData for PARAM_SET_DATA { __struct.param_value = buf.get_f32_le(); __struct.target_system = buf.get_u8(); __struct.target_component = buf.get_u8(); - for v in &mut __struct.param_id { - let val = buf.get_u8(); - *v = val; + let mut tmp = [0_u8; 16usize]; + for v in &mut tmp { + *v = buf.get_u8(); } + __struct.param_id = CharArray::new(tmp); let tmp = buf.get_u8(); __struct.param_type = FromPrimitive::from_u8(tmp).ok_or(::mavlink_core::error::ParserError::InvalidEnum { @@ -368,15 +355,7 @@ pub struct PARAM_VALUE_DATA { #[doc = "Index of this onboard parameter"] pub param_index: u16, #[doc = "Onboard parameter id, terminated by NULL if the length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte if the length is exactly 16 chars - applications have to provide 16+1 bytes storage if the ID is stored as string"] - #[cfg_attr( - feature = "serde", - serde( - serialize_with = "crate::nulstr::serialize::<_, 16>", - deserialize_with = "crate::nulstr::deserialize::<_, 16>" - ) - )] - #[cfg_attr(feature = "ts", ts(type = "string"))] - pub param_id: [u8; 16], + pub param_id: CharArray<16>, #[doc = "Onboard parameter type."] pub param_type: MavParamType, } @@ -386,7 +365,7 @@ impl PARAM_VALUE_DATA { param_value: 0.0_f32, param_count: 0_u16, param_index: 0_u16, - param_id: [0_u8; 16usize], + param_id: CharArray::new([0_u8; 16usize]), param_type: MavParamType::DEFAULT, }; #[cfg(feature = "arbitrary")] @@ -425,10 +404,11 @@ impl MessageData for PARAM_VALUE_DATA { __struct.param_value = buf.get_f32_le(); __struct.param_count = buf.get_u16_le(); __struct.param_index = buf.get_u16_le(); - for v in &mut __struct.param_id { - let val = buf.get_u8(); - *v = val; + let mut tmp = [0_u8; 16usize]; + for v in &mut tmp { + *v = buf.get_u8(); } + __struct.param_id = CharArray::new(tmp); let tmp = buf.get_u8(); __struct.param_type = FromPrimitive::from_u8(tmp).ok_or(::mavlink_core::error::ParserError::InvalidEnum { diff --git a/mavlink-core/src/lib.rs b/mavlink-core/src/lib.rs index 92b857e2be6..cb6a6842361 100644 --- a/mavlink-core/src/lib.rs +++ b/mavlink-core/src/lib.rs @@ -113,6 +113,7 @@ pub mod bytes_mut; #[cfg(feature = "std")] mod connection; pub mod error; +pub mod types; #[cfg(feature = "std")] pub use self::connection::{connect, Connectable, MavConnection}; diff --git a/mavlink-core/src/types.rs b/mavlink-core/src/types.rs new file mode 100644 index 00000000000..972a9c6ab34 --- /dev/null +++ b/mavlink-core/src/types.rs @@ -0,0 +1,140 @@ +use core::ops::{Deref, DerefMut}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Unstructured}; + +/// Abstraction around a byte array that represents a string. +/// +/// MAVLink encodes strings as C char arrays and the handling is field dependent. +/// This abstration allows to choose if one wants to handle the field as +/// a raw byte array or if one wants the convenience of a str that stops at the first null byte. +/// +/// # Example +/// ``` +/// use mavlink_core::types::CharArray; +/// +/// let data = [0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x00, 0x57, 0x4f, 0x52, 0x4c, 0x44, 0x00, 0x00, 0x00]; +/// let ca = CharArray::new(data); +/// assert_eq!(ca.to_str(), "HELLO"); +/// ``` +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct CharArray { + #[cfg_attr(feature = "serde", serde(with = "serde_arrays"))] + data: [u8; N], + + #[cfg_attr(feature = "serde", serde(skip))] + str_len: usize, +} + +impl CharArray { + pub const fn new(data: [u8; N]) -> Self { + // Note: The generated code uses this in const contexts, so this is a const fn + // and so we can't use iterators or other fancy stuff unfortunately. + let mut first_null = N; + let mut i = 0; + loop { + if i >= N { + break; + } + if data[i] == 0 { + first_null = i; + break; + } + i += 1; + } + Self { + data, + str_len: first_null, + } + } + + /// Get the string representation of the char array. + /// Returns the string stopping at the first null byte and if the string is not valid utf8 + /// the returned string will be empty. + pub fn to_str(&self) -> &str { + std::str::from_utf8(&self.data[..self.str_len]).unwrap_or("") + } +} + +impl Deref for CharArray { + type Target = [u8; N]; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for CharArray { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl<'a, const N: usize> IntoIterator for &'a CharArray { + type Item = &'a u8; + type IntoIter = core::slice::Iter<'a, u8>; + + fn into_iter(self) -> Self::IntoIter { + self.data.iter() + } +} + +impl From<[u8; N]> for CharArray { + fn from(data: [u8; N]) -> Self { + Self::new(data) + } +} + +impl From> for [u8; N] { + fn from(value: CharArray) -> Self { + value.data + } +} + +#[cfg(feature = "serde")] +impl<'de, const N: usize> Deserialize<'de> for CharArray { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let data: [u8; N] = serde_arrays::deserialize(deserializer)?; + Ok(Self::new(data)) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a, const N: usize> Arbitrary<'a> for CharArray { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let mut data = [0u8; N]; + u.fill_buffer(&mut data)?; + Ok(CharArray::new(data)) + } +} + +#[cfg(test)] +mod tests { + use super::CharArray; + + #[test] + fn char_array_to_str_handles_no_nulls() { + let data = *b"HELLOWORLD"; + let ca = CharArray::new(data); + assert_eq!(ca.len(), 10); + assert_eq!(ca.to_str(), "HELLOWORLD"); + } + + #[test] + fn char_array_to_str_trims_after_first_null() { + let mut data = [0u8; 10]; + data[..3].copy_from_slice(b"abc"); + // data[3..] are zeros + let ca = CharArray::new(data); + assert_eq!(ca.len(), 10); + assert_eq!(ca.to_str(), "abc"); + } +} diff --git a/mavlink/tests/serde_test.rs b/mavlink/tests/serde_test.rs index 3a4a9f9b7fc..2104e4456b4 100644 --- a/mavlink/tests/serde_test.rs +++ b/mavlink/tests/serde_test.rs @@ -101,6 +101,8 @@ mod serde_test { use core::{f32, f64}; use std::u64; + use mavlink_core::types::CharArray; + use mavlink::test::{MavMessage, TEST_TYPES_DATA}; let test_message = MavMessage::TEST_TYPES(TEST_TYPES_DATA { u64: 0, @@ -125,7 +127,7 @@ mod serde_test { f: f32::EPSILON, f_array: [f32::NEG_INFINITY, 0.0, f32::MIN], c: b'R', - s: *b"rustmavlin", // 10 chars + s: CharArray::new(*b"rustmavlin"), // 10 chars }); assert_tokens( &test_message, diff --git a/mavlink/tests/v2_encode_decode_tests.rs b/mavlink/tests/v2_encode_decode_tests.rs index 10c50bf75f6..8a8508a9e00 100644 --- a/mavlink/tests/v2_encode_decode_tests.rs +++ b/mavlink/tests/v2_encode_decode_tests.rs @@ -208,7 +208,7 @@ mod test_v2_encode_decode { ), }; - let param_id = String::from_utf8(param_value.param_id[..11].to_vec()).unwrap(); + let param_id = param_value.param_id.to_str(); assert_eq!(param_id, "_HASH_CHECK"); assert_eq!( param_value.param_type, From 9688ee65364cb00c4caad95eb4415417d2505117 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 28 Aug 2025 01:52:04 +0200 Subject: [PATCH 2/8] Implement RustDefault for CharArray --- mavlink-core/src/types.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mavlink-core/src/types.rs b/mavlink-core/src/types.rs index 972a9c6ab34..e5df26fece2 100644 --- a/mavlink-core/src/types.rs +++ b/mavlink-core/src/types.rs @@ -20,7 +20,7 @@ use arbitrary::{Arbitrary, Unstructured}; /// let ca = CharArray::new(data); /// assert_eq!(ca.to_str(), "HELLO"); /// ``` -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize))] #[cfg_attr(feature = "serde", serde(transparent))] pub struct CharArray { @@ -57,7 +57,7 @@ impl CharArray { /// Returns the string stopping at the first null byte and if the string is not valid utf8 /// the returned string will be empty. pub fn to_str(&self) -> &str { - std::str::from_utf8(&self.data[..self.str_len]).unwrap_or("") + core::str::from_utf8(&self.data[..self.str_len]).unwrap_or("") } } @@ -96,6 +96,13 @@ impl From> for [u8; N] { } } +impl crate::utils::RustDefault for CharArray { + #[inline(always)] + fn rust_default() -> Self { + Self::new([0u8; N]) + } +} + #[cfg(feature = "serde")] impl<'de, const N: usize> Deserialize<'de> for CharArray { fn deserialize(deserializer: D) -> Result From 544f7f6d365a94a7f0a4bb491c1c3a66457af8ce Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Wed, 8 Oct 2025 19:32:05 +0200 Subject: [PATCH 3/8] Fix review comments --- mavlink-bindgen/src/parser.rs | 22 +++++----------------- mavlink-core/src/types.rs | 10 +++++----- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/mavlink-bindgen/src/parser.rs b/mavlink-bindgen/src/parser.rs index 72e6614d9e1..19b483ff4a0 100644 --- a/mavlink-bindgen/src/parser.rs +++ b/mavlink-bindgen/src/parser.rs @@ -710,23 +710,11 @@ impl MavMessage { quote!() }; - let serde_with_attr = if let MavType::Array(_, size) = field.mavtype { - if field.mavtype.primitive_type() == "char" { - let format_serialize = format!("crate::nulstr::serialize::<_, {}>", size); - let format_deserialize = format!("crate::nulstr::deserialize::<_, {}>", size); - quote!( - #[cfg_attr(feature = "serde", serde( - serialize_with = #format_serialize, - deserialize_with = #format_deserialize - ))] - #[cfg_attr(feature = "ts", ts(type = "string"))] - ) - } else { - quote!( - #[cfg_attr(feature = "serde", serde(with = "serde_arrays"))] - #[cfg_attr(feature = "ts", ts(type = "Array"))] - ) - } + let serde_with_attr = if matches!(field.mavtype, MavType::Array(_, _) | MavType::CharArray(_)) { + quote!( + #[cfg_attr(feature = "serde", serde(with = "serde_arrays"))] + #[cfg_attr(feature = "ts", ts(type = "Array"))] + ) } else { quote!() }; diff --git a/mavlink-core/src/types.rs b/mavlink-core/src/types.rs index e5df26fece2..c371d3e8c84 100644 --- a/mavlink-core/src/types.rs +++ b/mavlink-core/src/types.rs @@ -20,9 +20,9 @@ use arbitrary::{Arbitrary, Unstructured}; /// let ca = CharArray::new(data); /// assert_eq!(ca.to_str(), "HELLO"); /// ``` -#[derive(Debug, PartialEq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize))] #[cfg_attr(feature = "serde", serde(transparent))] +#[derive(Debug, PartialEq, Clone, Copy)] pub struct CharArray { #[cfg_attr(feature = "serde", serde(with = "serde_arrays"))] data: [u8; N], @@ -56,8 +56,8 @@ impl CharArray { /// Get the string representation of the char array. /// Returns the string stopping at the first null byte and if the string is not valid utf8 /// the returned string will be empty. - pub fn to_str(&self) -> &str { - core::str::from_utf8(&self.data[..self.str_len]).unwrap_or("") + pub fn to_str(&self) -> Result<&str, core::str::Utf8Error> { + core::str::from_utf8(&self.data[..self.str_len]) } } @@ -132,7 +132,7 @@ mod tests { let data = *b"HELLOWORLD"; let ca = CharArray::new(data); assert_eq!(ca.len(), 10); - assert_eq!(ca.to_str(), "HELLOWORLD"); + assert_eq!(ca.to_str().unwrap(), "HELLOWORLD"); } #[test] @@ -142,6 +142,6 @@ mod tests { // data[3..] are zeros let ca = CharArray::new(data); assert_eq!(ca.len(), 10); - assert_eq!(ca.to_str(), "abc"); + assert_eq!(ca.to_str().unwrap(), "abc"); } } From a6aecfc651eb12ae0026351edd89e402bc3e498c Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Wed, 8 Oct 2025 20:33:35 +0200 Subject: [PATCH 4/8] Fix tests --- mavlink-bindgen/src/parser.rs | 9 +++++++-- mavlink-core/src/types.rs | 2 +- mavlink/tests/serde_test.rs | 2 +- mavlink/tests/v2_encode_decode_tests.rs | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mavlink-bindgen/src/parser.rs b/mavlink-bindgen/src/parser.rs index 19b483ff4a0..70a626ba0e8 100644 --- a/mavlink-bindgen/src/parser.rs +++ b/mavlink-bindgen/src/parser.rs @@ -710,12 +710,17 @@ impl MavMessage { quote!() }; - let serde_with_attr = if matches!(field.mavtype, MavType::Array(_, _) | MavType::CharArray(_)) { + let serde_with_attr = if matches!(field.mavtype, MavType::Array(_, _)) { quote!( #[cfg_attr(feature = "serde", serde(with = "serde_arrays"))] #[cfg_attr(feature = "ts", ts(type = "Array"))] ) - } else { + } else if matches!(field.mavtype, MavType::CharArray(_)) { + quote!( + #[cfg_attr(feature = "ts", ts(type = "string"))] + ) + } + else { quote!() }; diff --git a/mavlink-core/src/types.rs b/mavlink-core/src/types.rs index c371d3e8c84..1fd7d68cff2 100644 --- a/mavlink-core/src/types.rs +++ b/mavlink-core/src/types.rs @@ -18,7 +18,7 @@ use arbitrary::{Arbitrary, Unstructured}; /// /// let data = [0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x00, 0x57, 0x4f, 0x52, 0x4c, 0x44, 0x00, 0x00, 0x00]; /// let ca = CharArray::new(data); -/// assert_eq!(ca.to_str(), "HELLO"); +/// assert_eq!(ca.to_str().unwrap(), "HELLO"); /// ``` #[cfg_attr(feature = "serde", derive(Serialize))] #[cfg_attr(feature = "serde", serde(transparent))] diff --git a/mavlink/tests/serde_test.rs b/mavlink/tests/serde_test.rs index 2104e4456b4..6a598a34aa4 100644 --- a/mavlink/tests/serde_test.rs +++ b/mavlink/tests/serde_test.rs @@ -263,7 +263,7 @@ mod serde_test_json { for (i, c) in src.iter().enumerate() { buf[i] = *c; } - buf + buf.into() }, target_system: 0, target_component: 0, diff --git a/mavlink/tests/v2_encode_decode_tests.rs b/mavlink/tests/v2_encode_decode_tests.rs index 8a8508a9e00..1ae16876467 100644 --- a/mavlink/tests/v2_encode_decode_tests.rs +++ b/mavlink/tests/v2_encode_decode_tests.rs @@ -208,7 +208,7 @@ mod test_v2_encode_decode { ), }; - let param_id = param_value.param_id.to_str(); + let param_id = param_value.param_id.to_str().unwrap(); assert_eq!(param_id, "_HASH_CHECK"); assert_eq!( param_value.param_type, From a2d8dee03a2fc50b48355db3554602ccb7481a58 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Wed, 8 Oct 2025 20:35:18 +0200 Subject: [PATCH 5/8] Fix formatting --- mavlink-bindgen/src/parser.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mavlink-bindgen/src/parser.rs b/mavlink-bindgen/src/parser.rs index 70a626ba0e8..8815b8912d5 100644 --- a/mavlink-bindgen/src/parser.rs +++ b/mavlink-bindgen/src/parser.rs @@ -719,8 +719,7 @@ impl MavMessage { quote!( #[cfg_attr(feature = "ts", ts(type = "string"))] ) - } - else { + } else { quote!() }; From 1476ed0c742428ce24f9c85ae82ca1e3c59bcb87 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 9 Oct 2025 16:15:39 +0200 Subject: [PATCH 6/8] Fix ts annotation for CharArray --- mavlink-bindgen/src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mavlink-bindgen/src/parser.rs b/mavlink-bindgen/src/parser.rs index 8815b8912d5..7d42a016258 100644 --- a/mavlink-bindgen/src/parser.rs +++ b/mavlink-bindgen/src/parser.rs @@ -717,7 +717,7 @@ impl MavMessage { ) } else if matches!(field.mavtype, MavType::CharArray(_)) { quote!( - #[cfg_attr(feature = "ts", ts(type = "string"))] + #[cfg_attr(feature = "ts", ts(type = "Array"))] ) } else { quote!() From 81be9b846b71de910963085f9b547aceeb930edb Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sat, 11 Oct 2025 15:24:51 +0200 Subject: [PATCH 7/8] Fix merge conflicts and change serialization --- mavlink-bindgen/src/parser.rs | 2 +- .../e2e_snapshots__parameters.xml@parameters.rs.snap | 3 +++ mavlink-core/src/types.rs | 9 +++++++-- mavlink/tests/serde_test.rs | 6 +++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/mavlink-bindgen/src/parser.rs b/mavlink-bindgen/src/parser.rs index 7d42a016258..8815b8912d5 100644 --- a/mavlink-bindgen/src/parser.rs +++ b/mavlink-bindgen/src/parser.rs @@ -717,7 +717,7 @@ impl MavMessage { ) } else if matches!(field.mavtype, MavType::CharArray(_)) { quote!( - #[cfg_attr(feature = "ts", ts(type = "Array"))] + #[cfg_attr(feature = "ts", ts(type = "string"))] ) } else { quote!() diff --git a/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap b/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap index 79b2acfe27a..012329b232d 100644 --- a/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap +++ b/mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@parameters.rs.snap @@ -157,6 +157,7 @@ pub struct PARAM_REQUEST_READ_DATA { #[doc = "Component ID"] pub target_component: u8, #[doc = "Onboard parameter id, terminated by NULL if the length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte if the length is exactly 16 chars - applications have to provide 16+1 bytes storage if the ID is stored as string"] + #[cfg_attr(feature = "ts", ts(type = "string"))] pub param_id: CharArray<16>, } impl PARAM_REQUEST_READ_DATA { @@ -251,6 +252,7 @@ pub struct PARAM_SET_DATA { #[doc = "Component ID"] pub target_component: u8, #[doc = "Onboard parameter id, terminated by NULL if the length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte if the length is exactly 16 chars - applications have to provide 16+1 bytes storage if the ID is stored as string"] + #[cfg_attr(feature = "ts", ts(type = "string"))] pub param_id: CharArray<16>, #[doc = "Onboard parameter type."] pub param_type: MavParamType, @@ -355,6 +357,7 @@ pub struct PARAM_VALUE_DATA { #[doc = "Index of this onboard parameter"] pub param_index: u16, #[doc = "Onboard parameter id, terminated by NULL if the length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte if the length is exactly 16 chars - applications have to provide 16+1 bytes storage if the ID is stored as string"] + #[cfg_attr(feature = "ts", ts(type = "string"))] pub param_id: CharArray<16>, #[doc = "Onboard parameter type."] pub param_type: MavParamType, diff --git a/mavlink-core/src/types.rs b/mavlink-core/src/types.rs index 1fd7d68cff2..9623b5905f3 100644 --- a/mavlink-core/src/types.rs +++ b/mavlink-core/src/types.rs @@ -1,5 +1,7 @@ use core::ops::{Deref, DerefMut}; +#[cfg(feature = "serde")] +use crate::utils::nulstr; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -24,7 +26,10 @@ use arbitrary::{Arbitrary, Unstructured}; #[cfg_attr(feature = "serde", serde(transparent))] #[derive(Debug, PartialEq, Clone, Copy)] pub struct CharArray { - #[cfg_attr(feature = "serde", serde(with = "serde_arrays"))] + #[cfg_attr( + feature = "serde", + serde(serialize_with = "nulstr::serialize::<_, N>",) + )] data: [u8; N], #[cfg_attr(feature = "serde", serde(skip))] @@ -109,7 +114,7 @@ impl<'de, const N: usize> Deserialize<'de> for CharArray { where D: serde::Deserializer<'de>, { - let data: [u8; N] = serde_arrays::deserialize(deserializer)?; + let data: [u8; N] = nulstr::deserialize(deserializer)?; Ok(Self::new(data)) } } diff --git a/mavlink/tests/serde_test.rs b/mavlink/tests/serde_test.rs index 6a598a34aa4..81ab72af52e 100644 --- a/mavlink/tests/serde_test.rs +++ b/mavlink/tests/serde_test.rs @@ -284,6 +284,8 @@ mod serde_test_json { #[test] fn test_serde_input() { + use std::ops::Deref; + let heartbeat_json = json!({ "type": "HEARTBEAT", "custom_mode": 0, @@ -329,9 +331,7 @@ mod serde_test_json { assert_eq!(data.target_component, 0); // Check that param_id string is correctly deserialized - let param_id_str = std::str::from_utf8(&data.param_id) - .unwrap() - .trim_end_matches('\0'); + let param_id_str = data.param_id.to_str().unwrap(); assert_eq!(param_id_str, "TEST_PARAM"); } _ => panic!("Expected PARAM_REQUEST_READ message"), From 34efc392bf772556b8b474f997759b61508cfa1c Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sat, 11 Oct 2025 22:27:15 +0200 Subject: [PATCH 8/8] Avoid unnecessary find --- mavlink-bindgen/src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mavlink-bindgen/src/parser.rs b/mavlink-bindgen/src/parser.rs index 8815b8912d5..3b64b0b257c 100644 --- a/mavlink-bindgen/src/parser.rs +++ b/mavlink-bindgen/src/parser.rs @@ -1127,7 +1127,7 @@ impl MavType { "Double" => Some(Double), "double" => Some(Double), _ if s.starts_with("char[") => { - let start = s.find('[')?; + let start = 4; let size = s[start + 1..(s.len() - 1)].parse::().ok()?; Some(CharArray(size)) }