diff --git a/LiquidCAN.pdf b/LiquidCAN.pdf index f273590..a93e574 100644 Binary files a/LiquidCAN.pdf and b/LiquidCAN.pdf differ diff --git a/LiquidCAN.tex b/LiquidCAN.tex index b48dfba..5cdfc83 100644 --- a/LiquidCAN.tex +++ b/LiquidCAN.tex @@ -1,4 +1,3 @@ -% /home/raffael/Documents/Nextcloud/Hobbies/SpaceTeam/LiquidCan/documentation.tex \documentclass[11pt,a4paper]{article} \usepackage[margin=20mm,a4paper]{geometry} \usepackage{lmodern} @@ -90,7 +89,7 @@ \vspace{1cm} -{\large Version 1.3} +{\large Version 1.4} \vspace{0.5cm} @@ -116,6 +115,7 @@ \section*{Version History} 1.1 & 2026-01-25 & Fixed total sizes of structs. & Fabian Weichselbaum \\ 1.2 & 2026-02-09 & Fixed inconsistant field value lengths \& typos & Raffael Rott\\ 1.3 & 2026-02-28 & Added a status field to field access responses& Raffael Rott\\ +1.4 & 2026-03-05 & Variable-length frame serialization; updated payload sizes & Michael Debertol\\ % Make sure to change the version on the first page \bottomrule \end{longtable} diff --git a/liquidcan_rust/Cargo.toml b/liquidcan_rust/Cargo.toml index ac8d7d2..de340ef 100644 --- a/liquidcan_rust/Cargo.toml +++ b/liquidcan_rust/Cargo.toml @@ -11,3 +11,4 @@ zerocopy = "0.8.27" zerocopy-derive = "0.8.27" liquidcan_rust_macros = { path = "liquidcan_rust_macros" } liquidcan_rust_macros_derive = { path = "liquidcan_rust_macros/liquidcan_rust_macros_derive" } +socketcan = "3.5.0" diff --git a/liquidcan_rust/liquidcan_rust_macros/Cargo.toml b/liquidcan_rust/liquidcan_rust_macros/Cargo.toml index 2bb0040..c5c748e 100644 --- a/liquidcan_rust/liquidcan_rust_macros/Cargo.toml +++ b/liquidcan_rust/liquidcan_rust_macros/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -paste = "1.0.15" -zerocopy = "0.8.28" -zerocopy-derive = "0.8.28" +liquidcan_rust_macros_derive = { path = "./liquidcan_rust_macros_derive", version = "0.1.0" } +thiserror = "2.0" +zerocopy = { version = "0.8", features = ["derive"] } \ No newline at end of file diff --git a/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/Cargo.toml b/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/Cargo.toml index 4c76c4b..7193636 100644 --- a/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/Cargo.toml +++ b/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/Cargo.toml @@ -8,4 +8,5 @@ proc-macro = true [dependencies] syn = "2.0.110" -quote = "1.0.42" \ No newline at end of file +quote = "1.0.42" +proc-macro2 = "1.0" \ No newline at end of file diff --git a/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/src/lib.rs b/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/src/lib.rs index 016e3b0..27a7c25 100644 --- a/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/src/lib.rs +++ b/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/src/lib.rs @@ -1,51 +1,310 @@ use proc_macro::TokenStream; -use quote::quote; -use syn::Attribute; +use quote::{format_ident, quote}; +use syn::{ + Data, DeriveInput, Fields, Type, Variant, parse_macro_input, punctuated::Punctuated, + token::Comma, +}; -#[proc_macro_derive(EnumDiscriminate)] -pub fn enum_discriminate_derive(input: TokenStream) -> TokenStream { - // Construct a representation of Rust code as a syntax tree - // that we can manipulate. - let ast = syn::parse(input).unwrap(); +#[proc_macro_derive(ByteCodec)] +pub fn derive_byte_codec(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let type_name = &input.ident; - // Build the trait implementation. - impl_enum_discriminate_derive(&ast) + let expanded = match &input.data { + Data::Enum(data_enum) => { + let variants = &data_enum.variants; + let max_size_impl = build_enum_max_serialized_size(variants); + let serialize_impl = build_serialize(type_name, variants); + let deserialize_impl = build_deserialize(type_name, variants); + + quote! { + impl liquidcan_rust_macros::byte_codec::ByteCodec for #type_name { + #max_size_impl + #serialize_impl + #deserialize_impl + } + } + } + Data::Struct(data_struct) => { + let max_size_impl = build_struct_max_serialized_size(&data_struct.fields); + let serialize_impl = build_struct_serialize(type_name, &data_struct.fields); + let deserialize_impl = build_struct_deserialize(type_name, &data_struct.fields); + + quote! { + impl liquidcan_rust_macros::byte_codec::ByteCodec for #type_name { + #max_size_impl + #serialize_impl + #deserialize_impl + } + } + } + Data::Union(_) => panic!("ByteCodec cannot be derived for unions"), + }; + + TokenStream::from(expanded) } -fn has_repr_u8(attrs: &[Attribute]) -> bool { - let mut is_u8 = false; - for attr in attrs { - if attr.path().is_ident("repr") { - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("u8") { - is_u8 = true; +fn build_serialize( + enum_name: &syn::Ident, + variants: &Punctuated, +) -> proc_macro2::TokenStream { + let match_arms = variants.iter().map(|variant| { + let variant_name = &variant.ident; + let (_, variant_discriminant) = variant + .discriminant + .as_ref() + .expect("Must explicitly specify discriminant"); + + match &variant.fields { + Fields::Named(fields) => { + let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); + + quote! { + #enum_name::#variant_name { #(#field_names),* } => { + out.push(#variant_discriminant); + #( #field_names.serialize(out); )* + } + } + } + Fields::Unnamed(fields) => { + // Generate dummy identifiers for the tuple fields (e.g., f0, f1, f2) + let field_idents: Vec<_> = (0..fields.unnamed.len()) + .map(|i| format_ident!("f{}", i)) + .collect(); + + quote! { + #enum_name::#variant_name( #(#field_idents),* ) => { + out.push(#variant_discriminant); + #( #field_idents.serialize(out); )* + } + } + } + Fields::Unit => { + quote! { + #enum_name::#variant_name => { + out.push(#variant_discriminant); + } } - Ok(()) - }) - .unwrap() + } } + }); + + let expanded = quote! { fn serialize(&self, out: &mut Vec) { + let out_len_before = out.len(); + match self { + #(#match_arms)* + } + assert!(out.len() - out_len_before <= Self::MAX_SERIALIZED_SIZE, "Serialized data exceeds MAX_SERIALIZED_SIZE"); + }}; + + return expanded; +} + +fn max_size_for_type(ty: &Type) -> proc_macro2::TokenStream { + quote! { + <#ty as liquidcan_rust_macros::byte_codec::ByteCodec>::MAX_SERIALIZED_SIZE } - is_u8 } -fn impl_enum_discriminate_derive(ast: &syn::DeriveInput) -> TokenStream { - let name = &ast.ident; - if !has_repr_u8(&ast.attrs) { - panic!("EnumDiscriminate can only be derived for enums which have the u8 repr"); +fn sum_max_sizes(types: Vec<&Type>) -> proc_macro2::TokenStream { + let field_sizes: Vec<_> = types.into_iter().map(max_size_for_type).collect(); + quote! { + 0usize #( + #field_sizes )* } - let generated = quote! { - impl #name { - pub const fn discriminant(&self) -> u8 { - // SAFETY: Because we require the enum to be marked as `repr(u8)`, its layout is a `repr(C)` `union` - // between `repr(C)` structs, each of which has the `u8` discriminant as its first - // field, so we can read the discriminant without offsetting the pointer. - unsafe { - let ptr = self as *const Self; - let discriminant_ptr = ptr.cast::(); - *discriminant_ptr +} + +fn build_struct_max_serialized_size(fields: &Fields) -> proc_macro2::TokenStream { + let payload_size = match fields { + Fields::Named(named) => { + let types: Vec<_> = named.named.iter().map(|f| &f.ty).collect(); + sum_max_sizes(types) + } + Fields::Unnamed(unnamed) => { + let types: Vec<_> = unnamed.unnamed.iter().map(|f| &f.ty).collect(); + sum_max_sizes(types) + } + Fields::Unit => quote! { 0usize }, + }; + + quote! { + const MAX_SERIALIZED_SIZE: usize = #payload_size; + } +} + +fn build_enum_max_serialized_size( + variants: &Punctuated, +) -> proc_macro2::TokenStream { + let variant_sizes: Vec<_> = variants + .iter() + .map(|variant| { + let payload_size = match &variant.fields { + Fields::Named(named) => { + let types: Vec<_> = named.named.iter().map(|f| &f.ty).collect(); + sum_max_sizes(types) + } + Fields::Unnamed(unnamed) => { + let types: Vec<_> = unnamed.unnamed.iter().map(|f| &f.ty).collect(); + sum_max_sizes(types) + } + Fields::Unit => quote! { 0usize }, + }; + + quote! { + 1usize + (#payload_size) + } + }) + .collect(); + + let mut max_expr = variant_sizes + .first() + .cloned() + .expect("ByteCodec cannot be derived for enums without variants"); + + for size_expr in variant_sizes.iter().skip(1) { + max_expr = quote! { + { + let a = #max_expr; + let b = #size_expr; + if a > b { a } else { b } + } + }; + } + + quote! { + const MAX_SERIALIZED_SIZE: usize = #max_expr; + } +} + +fn build_deserialize( + enum_name: &syn::Ident, + variants: &Punctuated, +) -> proc_macro2::TokenStream { + let match_arms = variants.iter().map(|variant| { + let variant_name = &variant.ident; + let (_, variant_discriminant) = variant + .discriminant + .as_ref() + .expect("Must explicitly specify discriminant"); + + match &variant.fields { + Fields::Named(fields) => { + let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); + + quote! { + #variant_discriminant => { + #( + let (#field_names, input) = liquidcan_rust_macros::byte_codec::ByteCodec::deserialize(input)?; + )* + Ok((#enum_name::#variant_name { #( #field_names ),* }, input)) + } + } + } + Fields::Unnamed(fields) => { + let field_idents: Vec<_> = (0..fields.unnamed.len()) + .map(|i| format_ident!("f{}", i)) + .collect(); + + quote! { + #variant_discriminant => { + #( + let (#field_idents, input) = liquidcan_rust_macros::byte_codec::ByteCodec::deserialize(input)?; + )* + Ok((#enum_name::#variant_name( #( #field_idents ),* ), input)) + } + } + } + Fields::Unit => { + quote! { + #variant_discriminant => Ok((#enum_name::#variant_name, input)) } } } - }; - generated.into() + }); + + let expanded = quote! { fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), liquidcan_rust_macros::byte_codec::DeserializationError> { + let (discriminant, input) = input + .split_first() + .ok_or(liquidcan_rust_macros::byte_codec::DeserializationError::NotEnoughData)?; + + match discriminant { + #(#match_arms,)* + _ => Err(liquidcan_rust_macros::byte_codec::DeserializationError::InvalidDiscriminant(*discriminant)), + } + }}; + + return expanded; +} + +fn build_struct_serialize(type_name: &syn::Ident, fields: &Fields) -> proc_macro2::TokenStream { + match fields { + Fields::Named(fields) => { + let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); + quote! { + fn serialize(&self, out: &mut Vec) { + let out_len_before = out.len(); + let #type_name { #( #field_names ),* } = self; + #( #field_names.serialize(out); )* + assert!(out.len() - out_len_before <= Self::MAX_SERIALIZED_SIZE, "Serialized data exceeds MAX_SERIALIZED_SIZE"); + } + } + } + Fields::Unnamed(fields) => { + let field_idents: Vec<_> = (0..fields.unnamed.len()) + .map(|i| format_ident!("f{}", i)) + .collect(); + + quote! { + fn serialize(&self, out: &mut Vec) { + let out_len_before = out.len(); + let #type_name( #( #field_idents ),* ) = self; + #( #field_idents.serialize(out); )* + assert!(out.len() - out_len_before <= Self::MAX_SERIALIZED_SIZE, "Serialized data exceeds MAX_SERIALIZED_SIZE"); + } + } + } + Fields::Unit => { + quote! { + fn serialize(&self, _out: &mut Vec) { + } + } + } + } +} + +fn build_struct_deserialize(type_name: &syn::Ident, fields: &Fields) -> proc_macro2::TokenStream { + match fields { + Fields::Named(fields) => { + let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); + + quote! { + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), liquidcan_rust_macros::byte_codec::DeserializationError> { + #( + let (#field_names, input) = liquidcan_rust_macros::byte_codec::ByteCodec::deserialize(input)?; + )* + Ok((#type_name { #( #field_names ),* }, input)) + } + } + } + Fields::Unnamed(fields) => { + let field_idents: Vec<_> = (0..fields.unnamed.len()) + .map(|i| format_ident!("f{}", i)) + .collect(); + + quote! { + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), liquidcan_rust_macros::byte_codec::DeserializationError> { + #( + let (#field_idents, input) = liquidcan_rust_macros::byte_codec::ByteCodec::deserialize(input)?; + )* + Ok((#type_name( #( #field_idents ),* ), input)) + } + } + } + Fields::Unit => { + quote! { + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), liquidcan_rust_macros::byte_codec::DeserializationError> { + Ok((#type_name, input)) + } + } + } + } } diff --git a/liquidcan_rust/liquidcan_rust_macros/src/byte_codec.rs b/liquidcan_rust/liquidcan_rust_macros/src/byte_codec.rs new file mode 100644 index 0000000..273c9ea --- /dev/null +++ b/liquidcan_rust/liquidcan_rust_macros/src/byte_codec.rs @@ -0,0 +1,72 @@ +pub use liquidcan_rust_macros_derive::ByteCodec; +use thiserror::Error; +use zerocopy::{Immutable, IntoBytes, TryFromBytes}; + +/// A trait for types that can be serialized to and deserialized from bytes. +/// +/// Serialization is variable-length: types only emit the bytes they actually +/// need, which may be fewer than [`MAX_SERIALIZED_SIZE`](Self::MAX_SERIALIZED_SIZE). +/// +/// # Variable-length field placement +/// +/// Some types (e.g. `CanDataValue`, `PackedCanDataValues`, `NonNullCanBytes`) +/// consume **all remaining input** during deserialization because they have no +/// in-band length delimiter. Such types **must be the last field** in any +/// struct that derives `ByteCodec`; placing them before other fields will +/// cause deserialization to consume bytes that belong to subsequent fields, +/// preventing a clean serialize-deserialize round-trip. +/// +/// Types with an in-band delimiter (e.g. `CanString`, which is +/// null-terminated) may safely appear at any position. +pub trait ByteCodec { + /// The maximum number of bytes that [`serialize`](Self::serialize) will ever write. + /// The actual serialized size may be smaller. + const MAX_SERIALIZED_SIZE: usize; + + /// Serializes `self` into the provided output buffer. + /// + /// The caller is responsible for ensuring that the buffer has enough capacity to hold the serialized data. + /// Implementations of this method must not write more than `MAX_SERIALIZED_SIZE` bytes to the output buffer. + fn serialize(&self, out: &mut Vec); + + /// Deserializes an instance of `Self` from the provided input bytes. + /// + /// Returns a tuple containing the deserialized instance and a slice of the remaining input bytes after the deserialized data. + /// Thus, implementations must know how many bytes they consume from the input, in case the input contains more data than needed to deserialize an instance of `Self`. + /// The caller is responsible for ensuring that the input bytes contain enough data to deserialize an instance of `Self`. + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), DeserializationError> + where + Self: Sized; +} + +impl ByteCodec for T +where + T: TryFromBytes + IntoBytes + Immutable + Sized, +{ + const MAX_SERIALIZED_SIZE: usize = core::mem::size_of::(); + + fn serialize(&self, out: &mut Vec) { + out.extend_from_slice(self.as_bytes()); + } + + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), DeserializationError> { + let size = Self::MAX_SERIALIZED_SIZE; + if input.len() < size { + return Err(DeserializationError::NotEnoughData); + } + + T::try_read_from_prefix(input) + .map_err(|err| DeserializationError::InvalidData(format!("{err:?}"))) + } +} + +#[non_exhaustive] +#[derive(Debug, Error, PartialEq, Eq)] +pub enum DeserializationError { + #[error("invalid enum discriminant: {0}")] + InvalidDiscriminant(u8), + #[error("not enough input data")] + NotEnoughData, + #[error("input bytes are not a valid value for target type")] + InvalidData(String), +} diff --git a/liquidcan_rust/liquidcan_rust_macros/src/lib.rs b/liquidcan_rust/liquidcan_rust_macros/src/lib.rs index b2e5b99..f7deb87 100644 --- a/liquidcan_rust/liquidcan_rust_macros/src/lib.rs +++ b/liquidcan_rust/liquidcan_rust_macros/src/lib.rs @@ -1,4 +1 @@ -#[doc(hidden)] -pub use paste::paste; - -mod padded_enum; +pub mod byte_codec; diff --git a/liquidcan_rust/liquidcan_rust_macros/src/padded_enum.rs b/liquidcan_rust/liquidcan_rust_macros/src/padded_enum.rs deleted file mode 100644 index 21a6c2c..0000000 --- a/liquidcan_rust/liquidcan_rust_macros/src/padded_enum.rs +++ /dev/null @@ -1,350 +0,0 @@ -/// Generates a padded, `zerocopy`-safe version of an enum. -/// -/// # Arguments -/// * `(size = N)` - A compile-time assertion that the generated enum is exactly N bytes. -/// * `#[pad(N)]` - An attribute placed on each variant to add N bytes of explicit zero-padding. -/// -/// # Example -/// ```rust -/// use zerocopy::{IntoBytes, TryFromBytes, Immutable, KnownLayout}; -/// use liquidcan_rust_macros::padded_enum; -/// -/// padded_enum! { -/// (size = 5) // Mandatory size check. All enum variants must take 5 bytes. -/// #[derive(Debug, Clone, Copy, PartialEq)] -/// pub enum Command { -/// // Variant 1: Tag(1) + u32(4) + Pad(0) = 5 bytes -/// #[pad(0)] -/// Move{val: u32}, -/// -/// // Variant 2: Tag(1) + Pad(4) = 5 bytes -/// #[pad(4)] -/// Stop -/// } -/// } -/// -/// // Usage -/// let cmd = Command::Move { val: 42 }; -/// let mut buffer = [0u8; 5]; -/// let bytes = cmd.to_bytes(&mut buffer); -/// let cmd = Command::from_bytes(bytes).unwrap(); -/// ``` -#[macro_export] -macro_rules! padded_enum { - ( - (size = $size:expr) - $(#[$meta:meta])* - $vis:vis enum $Original:ident { - $( - #[pad($pad:expr)] - $Variant:ident $( { $( $field_name:ident : $field_type:ty ),* $(,)?} )? $( = $disc:expr )? - ),* $(,)? - } - ) => { - // --------------------------------------------------------- - // 1. The Original Ergonomic Enum - // --------------------------------------------------------- - $(#[$meta])* - $vis enum $Original { - $( - $Variant $( { $($field_name: $field_type),* } )? $( = $disc )?, - )* - } - - // We use the `paste` crate to concatenate names (Original + Padded) - $crate::paste! { - - // --------------------------------------------------------- - // 2. Internal Packed Structs - // --------------------------------------------------------- - // These wrap the data to force alignment to 1, preventing - // the compiler from inserting uninitialized padding bytes - // between the Enum Tag and the Variant Data. - $( - #[repr(C, packed)] - #[derive( - zerocopy_derive::IntoBytes, - zerocopy_derive::TryFromBytes, - zerocopy_derive::Immutable, - zerocopy_derive::KnownLayout - )] - #[allow(non_camel_case_types)] - $vis struct [<$Original Padded _ $Variant _Body>] { - $($( pub $field_name: $field_type, )*)? - pub _pad: [u8; $pad], - } - )* - - // --------------------------------------------------------- - // 3. The Padded (Wire-Format) Enum - // --------------------------------------------------------- - #[repr(u8)] - #[derive( - zerocopy_derive::IntoBytes, - zerocopy_derive::TryFromBytes, - zerocopy_derive::Immutable, - zerocopy_derive::KnownLayout - )] - $vis enum [<$Original Padded>] { - $( - $Variant( [<$Original Padded _ $Variant _Body>] ) $( = $disc )?, - )* - } - - // --------------------------------------------------------- - // 4. Direct conversions between Original and bytes - // --------------------------------------------------------- - impl $Original { - /// Serializes the enum to a vector of bytes, omitting the padding. - #[allow(unused)] - pub fn to_bytes(self, buf: & mut [u8; $size]) -> &[u8] { - match self { - $( - $Original::$Variant $( { $($field_name),* } )? => { - // Construct the padded body struct - let padded_body = [<$Original Padded _ $Variant _Body>] { - $($( $field_name: $field_name.clone(), )*)? - _pad: [0u8; $pad], - }; - let padded_enum = [<$Original Padded>]::$Variant(padded_body); - let bytes = ::zerocopy::IntoBytes::as_bytes(&padded_enum); - buf.copy_from_slice(bytes); - &buf[0..(bytes.len() - $pad)] - } - )* - } - } - - /// Deserializes from a byte slice, padding with zeros if necessary. - #[allow(unused)] - pub fn from_bytes(bytes: &[u8]) -> Result { - let mut buf = [0u8; $size]; - let len = ::std::cmp::min(bytes.len(), $size); - buf[0..len].copy_from_slice(&bytes[0..len]); - - let padded = <[<$Original Padded>] as ::zerocopy::TryFromBytes>::try_read_from_bytes(&buf) - .map_err(|e| ::std::format!("{:?}", e))?; - Ok(padded.into()) - } - } - - // --------------------------------------------------------- - // 5. Conversion: Original -> Padded - // --------------------------------------------------------- - impl From<$Original> for [<$Original Padded>] { - fn from(orig: $Original) -> Self { - match orig { - $( - $Original::$Variant $( { $($field_name),* } )? => { - [<$Original Padded>]::$Variant( - [<$Original Padded _ $Variant _Body>] { - $($( $field_name, )*)? - _pad: [0u8; $pad] - } - ) - } - )* - } - } - } - - // --------------------------------------------------------- - // 6. Conversion: Padded -> Original - // --------------------------------------------------------- - impl From<[<$Original Padded>]> for $Original { - fn from(padded: [<$Original Padded>]) -> Self { - match padded { - $( - #[allow(unused_variables)] - [<$Original Padded>]::$Variant(body) => { - $Original::$Variant $( { $( $field_name: body.$field_name ),* } )? - } - )* - } - } - } - - // --------------------------------------------------------- - // 7. Size Check (Type Mismatch Trick) - // --------------------------------------------------------- - // If the size doesn't match, this triggers a compiler error: - // "Expected array of size X, found array of size Y" - const _: [(); $size] = [(); std::mem::size_of::<[<$Original Padded>]>()]; - } - }; -} - -// --------------------------------------------------------- -// Unit Tests -// --------------------------------------------------------- -#[cfg(test)] -mod tests { - use zerocopy::{IntoBytes, TryFromBytes}; - - // Define a test enum using the macro - padded_enum! { - (size = 5) // Tag(1) + MaxPayload(4) - - #[derive(Debug, Clone, Copy, PartialEq)] - #[repr(u8)] - pub enum MyProto { - // 1 + 4 + 0 = 5 - #[pad(0)] - Move{dist: u32,}, - - // 1 + 1 + 3 = 5 - #[pad(3)] - Jump{height: u8}, - - // 1 + 0 + 4 = 5 - #[pad(4)] - Stop - } - } - - #[test] - fn test_layout_size() { - assert_eq!(std::mem::size_of::(), 5); - } - - #[test] - fn test_move_variant() { - let original = MyProto::Move { dist: 0xAABBCCDD }; - let padded: MyProtoPadded = original.into(); - - // Check bytes: Tag (0) + u32 (DD CC BB AA) in little endian - let bytes = padded.as_bytes(); - // Note: Tag values depend on declaration order. Move=0 - assert_eq!(bytes[0], 0); - assert_eq!(&bytes[1..5], &[0xDD, 0xCC, 0xBB, 0xAA]); - - let padded_back: MyProtoPadded = MyProtoPadded::try_read_from_bytes(bytes).unwrap(); - // Round trip - let back: MyProto = padded_back.into(); - assert_eq!(original, back); - } - - #[test] - fn test_jump_variant_padding() { - let original = MyProto::Jump { height: 0xFF }; - let padded: MyProtoPadded = original.into(); - - let bytes = padded.as_bytes(); - // Tag=1, Data=FF, Pad=00 00 00 - assert_eq!(bytes[0], 1); - assert_eq!(bytes[1], 0xFF); - assert_eq!(bytes[2], 0x00); // Pad must be zero - assert_eq!(bytes[3], 0x00); - assert_eq!(bytes[4], 0x00); - - // Round trip - let back: MyProto = padded.into(); - assert_eq!(original, back); - } - - #[test] - fn test_stop_variant_padding() { - let original = MyProto::Stop; - let padded: MyProtoPadded = original.into(); - - let bytes = padded.as_bytes(); - // Tag=2, Pad=00 00 00 00 - assert_eq!(bytes[0], 2); - assert_eq!(bytes[1..5], [0, 0, 0, 0]); - - // Round trip - let back: MyProto = padded.into(); - assert_eq!(original, back); - } - - // Test enum with discriminants set via constants - const CMD_PING: u8 = 10; - const CMD_PONG: u8 = 20; - const CMD_DATA: u8 = 30; - - padded_enum! { - (size = 5) - - #[derive(Debug, Clone, Copy, PartialEq)] - #[repr(u8)] - pub enum ConstDiscriminant { - #[pad(4)] - Ping = CMD_PING, - - #[pad(4)] - Pong = CMD_PONG, - - #[pad(0)] - Data { value: u32 } = CMD_DATA, - } - } - - #[test] - fn test_constant_discriminants() { - // Verify discriminants are set correctly via constants - let ping = ConstDiscriminant::Ping; - let padded: ConstDiscriminantPadded = ping.into(); - let bytes = padded.as_bytes(); - assert_eq!(bytes[0], CMD_PING); - - let pong = ConstDiscriminant::Pong; - let padded: ConstDiscriminantPadded = pong.into(); - let bytes = padded.as_bytes(); - assert_eq!(bytes[0], CMD_PONG); - - let data = ConstDiscriminant::Data { value: 0x12345678 }; - let padded: ConstDiscriminantPadded = data.into(); - let bytes = padded.as_bytes(); - assert_eq!(bytes[0], CMD_DATA); - assert_eq!(&bytes[1..5], &[0x78, 0x56, 0x34, 0x12]); // little endian - } - - #[test] - fn test_constant_discriminants_round_trip() { - let original = ConstDiscriminant::Data { value: 0xDEADBEEF }; - let padded: ConstDiscriminantPadded = original.into(); - let bytes = padded.as_bytes(); - - let padded_back = ConstDiscriminantPadded::try_read_from_bytes(bytes).unwrap(); - let back: ConstDiscriminant = padded_back.into(); - assert_eq!(original, back); - } - - #[test] - fn test_to_from_bytes_for_original() { - let mut buffer = [0u8; 5]; - let original = MyProto::Move { dist: 0xA1B2C3D4 }; - let bytes = original.to_bytes(&mut buffer); - // Tag(0) + dist(4) = 5. padding is 0. - assert_eq!(bytes.len(), 5); - assert_eq!(bytes[0], 0); - assert_eq!(&bytes[1..5], &[0xD4, 0xC3, 0xB2, 0xA1]); - - let recovered = MyProto::from_bytes(bytes).unwrap(); - assert_eq!(original, recovered); - - // Test padding logic - let jump = MyProto::Jump { height: 0x77 }; - let bytes_jump = jump.to_bytes(&mut buffer); - // Tag(1) + height(1) + pad(3) = 5. - // Stripped bytes = 5 - 3 = 2. - assert_eq!(bytes_jump.len(), 2); - assert_eq!(bytes_jump, vec![1, 0x77]); - - let recovered_jump = MyProto::from_bytes(bytes_jump).unwrap(); - assert_eq!(jump, recovered_jump); - - // Test undersized input - let incomplete = vec![1, 0x77]; // implicit padding - let recovered = MyProto::from_bytes(&incomplete).unwrap(); - assert_eq!(jump, recovered); - - // Test empty input (should be padded with 0 -> Tag=0 -> Move{val:0}) - let empty: Vec = vec![]; - let recovered_empty = MyProto::from_bytes(&empty).unwrap(); - match recovered_empty { - MyProto::Move { dist } => assert_eq!(dist, 0), - _ => panic!("Expected Move(0)"), - } - } -} diff --git a/liquidcan_rust/liquidcan_rust_macros/tests/test_byte_codec.rs b/liquidcan_rust/liquidcan_rust_macros/tests/test_byte_codec.rs new file mode 100644 index 0000000..3312509 --- /dev/null +++ b/liquidcan_rust/liquidcan_rust_macros/tests/test_byte_codec.rs @@ -0,0 +1,201 @@ +use liquidcan_rust_macros::byte_codec::{ByteCodec, DeserializationError}; +use zerocopy::{Immutable, IntoBytes, TryFromBytes}; + +#[repr(C)] +#[derive(TryFromBytes, IntoBytes, Immutable, Clone, Copy, Debug, PartialEq, Eq)] +struct CanFramePayload { + arbitration_id: u16, + data: u16, +} + +#[repr(u8)] +#[derive(ByteCodec, Debug, PartialEq, Eq)] +enum CanMessage { + Heartbeat = 0x01, + Frame(CanFramePayload) = 0x02, + Command { cmd: u16, value: u16 } = 0x03, + Empty = 0x04, +} + +#[repr(u8)] +#[derive(TryFromBytes, IntoBytes, Immutable, Clone, Copy, Debug, PartialEq, Eq)] +enum CheckedByte { + A = 0x11, + B = 0x22, +} + +#[derive(ByteCodec, Debug, PartialEq, Eq)] +struct NamedPacket { + port: u16, + payload: CanFramePayload, +} + +#[derive(ByteCodec, Debug, PartialEq, Eq)] +struct TuplePacket(u16, CanFramePayload); + +#[derive(ByteCodec, Debug, PartialEq, Eq)] +struct Marker; + +#[test] +fn serializes_to_variable_lengths() { + let mut heartbeat = Vec::new(); + CanMessage::Heartbeat.serialize(&mut heartbeat); + + let mut frame = Vec::new(); + CanMessage::Frame(CanFramePayload { + arbitration_id: 0x1234, + data: 0xABCD, + }) + .serialize(&mut frame); + + let mut command = Vec::new(); + CanMessage::Command { + cmd: 0x0102, + value: 0x0304, + } + .serialize(&mut command); + + assert_eq!(heartbeat.len(), 1); + assert_eq!(frame.len(), 1 + core::mem::size_of::()); + assert_eq!(command.len(), 1 + core::mem::size_of::() * 2); +} + +#[test] +fn roundtrips_all_variant_shapes() { + let original = [ + CanMessage::Heartbeat, + CanMessage::Frame(CanFramePayload { + arbitration_id: 0x0001, + data: 0xCAFE, + }), + CanMessage::Command { + cmd: 0xBEEF, + value: 0x2222, + }, + CanMessage::Empty, + ]; + + for value in original { + let mut bytes = Vec::new(); + value.serialize(&mut bytes); + + let (decoded, rest) = CanMessage::deserialize(&bytes).expect("deserialize should succeed"); + assert_eq!(decoded, value); + assert!(rest.is_empty()); + } +} + +#[test] +fn fails_for_invalid_discriminant() { + let err = CanMessage::deserialize(&[0xFF]).expect_err("expected invalid discriminant"); + assert!(matches!( + err, + DeserializationError::InvalidDiscriminant(0xFF) + )); +} + +#[test] +fn fails_on_empty_input() { + let err = CanMessage::deserialize(&[]).expect_err("expected not enough data"); + assert!(matches!(err, DeserializationError::NotEnoughData)); +} + +#[test] +fn fails_when_variant_payload_is_truncated() { + let err = CanMessage::deserialize(&[0x03, 0xAA]).expect_err("expected not enough data"); + assert!(matches!(err, DeserializationError::NotEnoughData)); +} + +#[test] +fn zerocopy_base_case_roundtrip() { + let payload = CanFramePayload { + arbitration_id: 0x1357, + data: 0x2468, + }; + + let mut bytes = Vec::new(); + payload.serialize(&mut bytes); + assert_eq!(bytes.len(), core::mem::size_of::()); + + let (decoded, rest) = CanFramePayload::deserialize(&bytes).expect("deserialize should succeed"); + assert_eq!(decoded, payload); + assert!(rest.is_empty()); +} + +#[test] +fn zerocopy_base_case_reports_invalid_data() { + let err = CheckedByte::deserialize(&[0xFF]).expect_err("expected invalid data"); + assert!(matches!(err, DeserializationError::InvalidData(_))); +} + +#[test] +fn zerocopy_checked_enum_roundtrip() { + let mut bytes = Vec::new(); + CheckedByte::A.serialize(&mut bytes); + + let (decoded, rest) = CheckedByte::deserialize(&bytes).expect("deserialize should succeed"); + assert_eq!(decoded, CheckedByte::A); + assert!(rest.is_empty()); + + let mut bytes_b = Vec::new(); + CheckedByte::B.serialize(&mut bytes_b); + let (decoded_b, rest_b) = + CheckedByte::deserialize(&bytes_b).expect("deserialize should succeed"); + assert_eq!(decoded_b, CheckedByte::B); + assert!(rest_b.is_empty()); +} + +#[test] +fn struct_named_roundtrip() { + let packet = NamedPacket { + port: 0x55AA, + payload: CanFramePayload { + arbitration_id: 0x1234, + data: 0x5678, + }, + }; + + let mut bytes = Vec::new(); + packet.serialize(&mut bytes); + + let (decoded, rest) = NamedPacket::deserialize(&bytes).expect("deserialize should succeed"); + assert_eq!(decoded, packet); + assert!(rest.is_empty()); +} + +#[test] +fn struct_tuple_roundtrip() { + let packet = TuplePacket( + 0x0A0B, + CanFramePayload { + arbitration_id: 0x0C0D, + data: 0x0E0F, + }, + ); + + let mut bytes = Vec::new(); + packet.serialize(&mut bytes); + + let (decoded, rest) = TuplePacket::deserialize(&bytes).expect("deserialize should succeed"); + assert_eq!(decoded, packet); + assert!(rest.is_empty()); +} + +#[test] +fn struct_unit_roundtrip_preserves_remaining_bytes() { + let marker = Marker; + let mut bytes = Vec::new(); + marker.serialize(&mut bytes); + assert!(bytes.is_empty()); + + let input = [0xAA, 0xBB]; + let (decoded, rest) = Marker::deserialize(&input).expect("deserialize should succeed"); + assert_eq!(decoded, Marker); + assert_eq!(rest, &input); +} + +#[test] +fn struct_deserialize_truncated_field_fails() { + let err = NamedPacket::deserialize(&[0x01]).expect_err("expected not enough data"); + assert!(matches!(err, DeserializationError::NotEnoughData)); +} diff --git a/liquidcan_rust/src/can_data.rs b/liquidcan_rust/src/can_data.rs new file mode 100644 index 0000000..d11a2bb --- /dev/null +++ b/liquidcan_rust/src/can_data.rs @@ -0,0 +1,405 @@ +use liquidcan_rust_macros::byte_codec::{ByteCodec, DeserializationError}; +use modular_bitfield::Specifier; +use zerocopy_derive::{Immutable, IntoBytes, TryFromBytes}; + +#[derive(Specifier, Debug, Copy, Clone, PartialEq, Eq, Immutable, TryFromBytes, IntoBytes)] +#[repr(u8)] +pub enum CanDataType { + Float32 = 0, + Int32 = 1, + Int16 = 2, + Int8 = 3, + UInt32 = 4, + UInt16 = 5, + UInt8 = 6, + Boolean = 7, +} + +impl CanDataType { + pub fn get_size(&self) -> usize { + match self { + CanDataType::Float32 => 4, + CanDataType::Int32 => 4, + CanDataType::Int16 => 2, + CanDataType::Int8 => 1, + CanDataType::UInt32 => 4, + CanDataType::UInt16 => 2, + CanDataType::UInt8 => 1, + CanDataType::Boolean => 1, + } + } +} + +#[repr(u8)] +#[derive(Debug, Clone, PartialEq)] +pub enum CanDataValue { + Float32(f32) = 0, + Int32(i32) = 1, + Int16(i16) = 2, + Int8(i8) = 3, + UInt32(u32) = 4, + UInt16(u16) = 5, + UInt8(u8) = 6, + Boolean(bool) = 7, + Raw(Vec) = u8::MAX, +} + +impl CanDataValue { + pub fn convert_from_slice( + data: &[u8], + data_type: CanDataType, + ) -> Result { + if data.len() < data_type.get_size() { + return Err(DeserializationError::InvalidData(format!( + "Data length {} does not match expected length {} for type {:?}", + data.len(), + data_type.get_size(), + data_type + ))); + } + + let data = &data[..data_type.get_size()]; + + match data_type { + CanDataType::Float32 => { + let mut arr = [0u8; 4]; + arr.copy_from_slice(data); + Ok(CanDataValue::Float32(f32::from_le_bytes(arr))) + } + CanDataType::Int32 => { + let mut arr = [0u8; 4]; + arr.copy_from_slice(data); + Ok(CanDataValue::Int32(i32::from_le_bytes(arr))) + } + CanDataType::Int16 => { + let mut arr = [0u8; 2]; + arr.copy_from_slice(data); + Ok(CanDataValue::Int16(i16::from_le_bytes(arr))) + } + CanDataType::Int8 => Ok(CanDataValue::Int8(data[0] as i8)), + CanDataType::UInt32 => { + let mut arr = [0u8; 4]; + arr.copy_from_slice(data); + Ok(CanDataValue::UInt32(u32::from_le_bytes(arr))) + } + CanDataType::UInt16 => { + let mut arr = [0u8; 2]; + arr.copy_from_slice(data); + Ok(CanDataValue::UInt16(u16::from_le_bytes(arr))) + } + CanDataType::UInt8 => Ok(CanDataValue::UInt8(data[0])), + CanDataType::Boolean => Ok(CanDataValue::Boolean(data[0] != 0)), + } + } + + /// Convert a Raw CanDataValue into a strongly-typed CanDataValue based on the provided CanDataType. + /// + /// Since the data type is not known at the time of deserialization, we initially deserialize into a Raw variant containing the raw bytes. + pub fn convert_from_raw( + &self, + data_type: CanDataType, + ) -> Result { + let Self::Raw(raw_data) = self else { + return Err(DeserializationError::InvalidData( + "CanDataValue is not a Raw variant".to_string(), + )); + }; + + CanDataValue::convert_from_slice(raw_data, data_type) + } +} + +/// # Variable-length serialization +/// +/// `CanDataValue` serializes to 1–4 bytes depending on the variant. +/// During deserialization the concrete type is not known, so `deserialize` +/// consumes **all remaining input** into a [`Raw`](CanDataValue::Raw) variant. +/// Use [`convert_from_raw`](CanDataValue::convert_from_raw) to reinterpret +/// the raw bytes once the expected type is known. +/// +/// **Because deserialization is greedy, `CanDataValue` must be the last field +/// in any `ByteCodec` struct.** Placing it before other fields will prevent +/// a clean round-trip. +impl ByteCodec for CanDataValue { + const MAX_SERIALIZED_SIZE: usize = 4; + + fn serialize(&self, out: &mut Vec) { + // don't include the tag in the serialized data, as message type must be known at deserialization time + match self { + CanDataValue::Float32(v) => { + out.extend_from_slice(&v.to_le_bytes()); + } + CanDataValue::Int32(v) => { + out.extend_from_slice(&v.to_le_bytes()); + } + CanDataValue::Int16(v) => { + out.extend_from_slice(&v.to_le_bytes()); + } + CanDataValue::Int8(v) => { + out.push(*v as u8); + } + CanDataValue::UInt32(v) => { + out.extend_from_slice(&v.to_le_bytes()); + } + CanDataValue::UInt16(v) => { + out.extend_from_slice(&v.to_le_bytes()); + } + CanDataValue::UInt8(v) => { + out.push(*v); + } + CanDataValue::Boolean(v) => { + out.push(*v as u8); + } + CanDataValue::Raw(data) => { + out.extend_from_slice(data); + } + } + } + + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), DeserializationError> { + if input.is_empty() { + return Err(DeserializationError::InvalidData("Empty input".to_string())); + } + + // Deserialization of CanDataValue requires external knowledge of the expected data type, + // so we can't determine the variant from the input data alone. + // Return the raw bytes and let the caller interpret them based on the expected data type. + Ok((CanDataValue::Raw(input.to_vec()), &[])) + } +} + +/// Custom string type for CAN messages, with a maximum buffer of N bytes, +/// null terminated (i.e. at most N-1 non-null bytes), ascii-only. +/// +/// # Variable-length serialization +/// +/// Only the characters up to and including the null terminator are serialized, +/// so the wire size is `len + 1` rather than `N`. +/// Because the null byte acts as an unambiguous delimiter, `CanString` may +/// safely appear at any position in a `ByteCodec` struct (not just the end). +#[derive(Debug, Clone)] +pub struct CanString { + data: [u8; N], +} + +impl TryFrom<[u8; N]> for CanString { + type Error = String; + + fn try_from(data: [u8; N]) -> Result { + if !data.contains(&0) { + return Err("CanString must be null-terminated.".to_string()); + } + if !data.iter().all(|&b| b.is_ascii()) { + return Err("CanString must contain only ASCII characters.".to_string()); + } + Ok(CanString { data }) + } +} + +impl TryFrom<&str> for CanString { + type Error = String; + + fn try_from(s: &str) -> Result { + let bytes = s.as_bytes(); + if bytes.len() >= N { + return Err(format!("String too long for CanString<{}>", N)); + } + + let mut data = [0u8; N]; + data[..bytes.len()].copy_from_slice(bytes); + + data.try_into() + } +} + +impl PartialEq for CanString { + fn eq(&self, other: &Self) -> bool { + let self_len = self.data.iter().position(|&b| b == 0).unwrap(); + let other_len = other.data.iter().position(|&b| b == 0).unwrap(); + self_len == other_len && self.data[..self_len] == other.data[..other_len] + } +} + +impl ByteCodec for CanString { + const MAX_SERIALIZED_SIZE: usize = N; + + fn serialize(&self, out: &mut Vec) { + // Write bytes up to the null terminator + let length = self + .data + .iter() + .position(|&b| b == 0) + .expect("CanString must be null-terminated."); + out.extend_from_slice(&self.data[..length]); + + out.push(0); // Null terminator + } + + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), DeserializationError> { + if let Some(pos) = input.iter().position(|&b| b == 0) { + if pos < N { + let mut data = [0u8; N]; + data[..pos].copy_from_slice(&input[..pos]); + + let s: CanString = data + .try_into() + .map_err(|e: String| DeserializationError::InvalidData(e))?; + Ok((s, &input[pos + 1..])) + } else { + Err(DeserializationError::InvalidData(format!( + "CanString exceeds maximum length of {}", + N + ))) + } + } else { + Err(DeserializationError::InvalidData( + "CanString is not null-terminated".to_string(), + )) + } + } +} + +/// Represents a packed set of CAN data values. +/// The raw byte form must fit into N bytes. +/// +/// # Variable-length serialization +/// +/// Only the bytes that were actually packed are serialized (i.e. the wire +/// size equals the sum of the sizes of the contained values, not `N`). +/// Deserialization consumes up to `N` bytes from the remaining input, so +/// **this type must be the last field** in any `ByteCodec` struct to +/// round-trip correctly. +#[derive(Clone, Debug, PartialEq)] +pub struct PackedCanDataValues { + data: Vec, +} + +impl PackedCanDataValues { + /// Unpack the raw byte data into a vector of CanDataValue based on the provided data types. + /// The caller must ensure that the order and types of the data match what was originally packed. + pub fn unpack( + &self, + data_types: &[CanDataType], + ) -> Result, DeserializationError> { + let mut values = Vec::new(); + let mut offset = 0; + + for &data_type in data_types { + let size = data_type.get_size(); + if offset + size > self.data.len() { + return Err(DeserializationError::InvalidData(format!( + "Not enough data to unpack CanDataValue of type {:?}", + data_type + ))); + } + + let slice = &self.data[offset..offset + size]; + let value = CanDataValue::convert_from_slice(slice, data_type)?; + values.push(value); + offset += size; + } + + Ok(values) + } +} + +impl TryFrom<&[CanDataValue]> for PackedCanDataValues { + type Error = String; + + fn try_from(values: &[CanDataValue]) -> Result { + let mut data = Vec::new(); + for value in values { + value.serialize(&mut data); + } + if data.len() > N { + return Err(format!( + "Packed data length {} exceeds maximum of {}", + data.len(), + N + )); + } + Ok(PackedCanDataValues { data }) + } +} + +impl ByteCodec for PackedCanDataValues { + const MAX_SERIALIZED_SIZE: usize = N; + + fn serialize(&self, out: &mut Vec) { + out.extend_from_slice(&self.data); + } + + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), DeserializationError> { + let len = input.len().min(N); + let data = input[..len].to_vec(); + Ok((PackedCanDataValues { data }, &input[len..])) + } +} + +/// Up to N bytes that do not include a null byte. +/// +/// # Variable-length serialization +/// +/// Only the meaningful (non-zero) prefix is serialized. +/// Deserialization reads until a null byte or end of input (up to `N` bytes), +/// so **this type must be the last field** in any `ByteCodec` struct to +/// round-trip correctly. +#[derive(Debug, Clone)] +pub struct NonNullCanBytes { + data: [u8; N], +} + +impl TryFrom<&[u8]> for NonNullCanBytes { + type Error = String; + + fn try_from(value: &[u8]) -> Result { + if value.len() > N { + return Err(format!( + "Input data length {} exceeds maximum of {}", + value.len(), + N + )); + } + if value.contains(&0) { + return Err("Input data contains null byte".to_string()); + } + let mut data = [0u8; N]; + data[..value.len()].copy_from_slice(value); + Ok(NonNullCanBytes { data }) + } +} + +impl<'a, const N: usize> From<&'a NonNullCanBytes> for &'a [u8] { + fn from(value: &'a NonNullCanBytes) -> Self { + let len = value.data.iter().position(|&b| b == 0).unwrap_or(N); + &value.data[..len] + } +} + +impl ByteCodec for NonNullCanBytes { + const MAX_SERIALIZED_SIZE: usize = N; + + fn serialize(&self, out: &mut Vec) { + let len = self.data.iter().position(|&b| b == 0).unwrap_or(N); + out.extend_from_slice(&self.data[..len]); + } + + fn deserialize(input: &[u8]) -> Result<(Self, &[u8]), DeserializationError> { + let len = input + .iter() + .position(|&b| b == 0) + .unwrap_or(input.len()) + .min(N); + let mut data = [0u8; N]; + data[..len].copy_from_slice(&input[..len]); + Ok((NonNullCanBytes { data }, &input[len..])) + } +} + +impl PartialEq for NonNullCanBytes { + fn eq(&self, other: &Self) -> bool { + let self_len = self.data.iter().position(|&b| b == 0).unwrap_or(N); + let other_len = other.data.iter().position(|&b| b == 0).unwrap_or(N); + self_len == other_len && self.data[..self_len] == other.data[..other_len] + } +} diff --git a/liquidcan_rust/src/can_message.rs b/liquidcan_rust/src/can_message.rs index 09c8d63..d755376 100644 --- a/liquidcan_rust/src/can_message.rs +++ b/liquidcan_rust/src/can_message.rs @@ -1,101 +1,78 @@ -use crate::payloads; -use liquidcan_rust_macros::padded_enum; -use liquidcan_rust_macros_derive::EnumDiscriminate; - -padded_enum! { +use liquidcan_rust_macros::byte_codec::ByteCodec; -(size = 64) +use crate::payloads; -#[derive(Debug, EnumDiscriminate, PartialEq, Clone)] +#[derive(Debug, ByteCodec, PartialEq, Clone)] #[repr(u8)] pub enum CanMessage { // Node Discovery and Information - #[pad(63)] NodeInfoReq = 0, // NO payload - #[pad(0)] NodeInfoAnnouncement { payload: payloads::NodeInfoResPayload, } = 1, // Status Messages - #[pad(0)] InfoStatus { - payload: payloads::StatusPayload + payload: payloads::StatusPayload, } = 10, - #[pad(0)] WarningStatus { - payload: payloads::StatusPayload + payload: payloads::StatusPayload, } = 11, - #[pad(0)] ErrorStatus { - payload: payloads::StatusPayload + payload: payloads::StatusPayload, } = 12, // Field Registration - #[pad(0)] TelemetryValueRegistration { payload: payloads::FieldRegistrationPayload, } = 20, - #[pad(0)] ParameterRegistration { payload: payloads::FieldRegistrationPayload, } = 21, // Telemetry Group Management - #[pad(0)] TelemetryGroupDefinition { payload: payloads::TelemetryGroupDefinitionPayload, } = 30, - #[pad(0)] TelemetryGroupUpdate { payload: payloads::TelemetryGroupUpdatePayload, } = 31, // Heartbeat - #[pad(59)] HeartbeatReq { - payload: payloads::HeartbeatPayload + payload: payloads::HeartbeatPayload, } = 40, - #[pad(59)] HeartbeatRes { - payload: payloads::HeartbeatPayload + payload: payloads::HeartbeatPayload, } = 41, // Parameter Management - #[pad(1)] ParameterSetReq { payload: payloads::ParameterSetReqPayload, } = 50, - #[pad(0)] ParameterSetConfirmation { payload: payloads::ParameterSetConfirmationPayload, } = 51, - #[pad(61)] ParameterSetLockReq { payload: payloads::ParameterSetLockPayload, } = 52, - #[pad(60)] ParameterSetLockConfirmation { payload: payloads::ParameterSetLockConfirmationPayload, } = 53, // Field Access - #[pad(62)] FieldGetReq { payload: payloads::FieldGetReqPayload, } = 60, - #[pad(0)] FieldGetRes { payload: payloads::FieldGetResPayload, } = 61, - #[pad(2)] FieldIDLookupReq { payload: payloads::FieldIDLookupReqPayload, } = 62, - #[pad(60)] FieldIDLookupRes { payload: payloads::FieldIDLookupResPayload, } = 63, } -} +static_assertions::const_assert_eq!(CanMessage::MAX_SERIALIZED_SIZE, 64); diff --git a/liquidcan_rust/src/lib.rs b/liquidcan_rust/src/lib.rs index 66b6774..026e243 100644 --- a/liquidcan_rust/src/lib.rs +++ b/liquidcan_rust/src/lib.rs @@ -1,8 +1,8 @@ +mod can_data; pub mod can_message; pub mod message_conversion; pub mod payloads; pub mod raw_can_message; pub use can_message::CanMessage; -pub use raw_can_message::CanMessageFrame; pub use raw_can_message::CanMessageId; diff --git a/liquidcan_rust/src/message_conversion.rs b/liquidcan_rust/src/message_conversion.rs index faf4e84..5b0d4f7 100644 --- a/liquidcan_rust/src/message_conversion.rs +++ b/liquidcan_rust/src/message_conversion.rs @@ -1,49 +1,65 @@ -use crate::CanMessageFrame; -use crate::can_message::{CanMessage, CanMessagePadded}; -use anyhow::anyhow; -use zerocopy::{FromZeros, IntoBytes, TryFromBytes}; +use crate::can_message::CanMessage; +use liquidcan_rust_macros::byte_codec::ByteCodec; +use socketcan::EmbeddedFrame; -impl TryFrom for CanMessage { +impl TryFrom for CanMessage { type Error = anyhow::Error; - fn try_from(frame: CanMessageFrame) -> Result { - let frame_data = frame.as_bytes(); - let padded_msg = CanMessagePadded::try_read_from_bytes(frame_data) - .map_err(|e| anyhow!("Failed to convert message: {}", e))?; - let msg: CanMessage = padded_msg.into(); - Ok(msg) + fn try_from(frame: socketcan::CanFdFrame) -> Result { + let frame_data = frame.data(); + let (message, _) = CanMessage::deserialize(frame_data)?; + Ok(message) } } -impl From for CanMessageFrame { +impl From for socketcan::CanFdFrame { fn from(msg: CanMessage) -> Self { - let mut msg_frame = CanMessageFrame::new_zeroed(); - let discriminant = msg.discriminant(); - let padded_msg: CanMessagePadded = msg.into(); - // The first byte is the discriminant, which is set separately. - let bytes: &[u8] = &padded_msg.as_bytes()[1..]; - msg_frame.data[..bytes.len()].copy_from_slice(bytes); - msg_frame.message_type = discriminant; - msg_frame + let mut buf = Vec::with_capacity(64); + msg.serialize(&mut buf); + + // ID needs to be set at a later point + let id = socketcan::StandardId::ZERO; + + socketcan::CanFdFrame::new(id, &buf).unwrap() } } #[cfg(test)] mod tests { - use crate::CanMessageFrame; use crate::can_message::CanMessage; use crate::payloads; use crate::payloads::FieldStatus; - use zerocopy::FromZeros; + use socketcan::EmbeddedFrame; fn test_round_trip(msg: CanMessage) { - let can_data: CanMessageFrame = msg.clone().into(); + let can_data: socketcan::CanFdFrame = msg.clone().into(); let msg_back: CanMessage = can_data .try_into() .expect("Failed to convert back to Command"); assert_eq!(msg, msg_back); } + fn test_round_trip_lossy(msg: CanMessage) { + let can_data: socketcan::CanFdFrame = msg.into(); + let msg_back: CanMessage = can_data + .try_into() + .expect("Failed to convert back to Command"); + + // For payloads where type metadata is absent, decode is intentionally lossy. + // Assert canonical wire round-tripping instead of strict AST equality. + let can_data_back: socketcan::CanFdFrame = msg_back.clone().into(); + assert_eq!( + can_data.data(), + can_data_back.data(), + "encoded bytes must be stable after one decode/encode cycle" + ); + + let msg_back_again: CanMessage = can_data_back + .try_into() + .expect("Failed to convert canonical bytes back to Command"); + assert_eq!(msg_back, msg_back_again); + } + #[test] fn test_node_info_req() { let msg = CanMessage::NodeInfoReq; @@ -57,7 +73,7 @@ mod tests { par_count: 5, firmware_hash: 1234, liquid_hash: 5678, - device_name: [0xAA; 53], + device_name: "Test".try_into().unwrap(), }; let msg = CanMessage::NodeInfoAnnouncement { payload }; test_round_trip(msg); @@ -65,21 +81,27 @@ mod tests { #[test] fn test_info_status() { - let payload = payloads::StatusPayload { msg: [0xBB; 63] }; + let payload = payloads::StatusPayload { + msg: "Info status message".try_into().unwrap(), + }; let msg = CanMessage::InfoStatus { payload }; test_round_trip(msg); } #[test] fn test_warning_status() { - let payload = payloads::StatusPayload { msg: [0xCC; 63] }; + let payload = payloads::StatusPayload { + msg: "Warning status message".try_into().unwrap(), + }; let msg = CanMessage::WarningStatus { payload }; test_round_trip(msg); } #[test] fn test_error_status() { - let payload = payloads::StatusPayload { msg: [0xDD; 63] }; + let payload = payloads::StatusPayload { + msg: "Error status message".try_into().unwrap(), + }; let msg = CanMessage::ErrorStatus { payload }; test_round_trip(msg); } @@ -89,7 +111,7 @@ mod tests { let payload = payloads::FieldRegistrationPayload { field_id: 5, field_type: payloads::CanDataType::UInt16, - field_name: [0xEE; 61], + field_name: "Telemetry Value Field".try_into().unwrap(), }; let msg = CanMessage::TelemetryValueRegistration { payload }; test_round_trip(msg); @@ -100,7 +122,7 @@ mod tests { let payload = payloads::FieldRegistrationPayload { field_id: 7, field_type: payloads::CanDataType::Boolean, - field_name: [0xFF; 61], + field_name: "Parameter Field".try_into().unwrap(), }; let msg = CanMessage::ParameterRegistration { payload }; test_round_trip(msg); @@ -110,7 +132,7 @@ mod tests { fn test_telemetry_group_definition() { let payload = payloads::TelemetryGroupDefinitionPayload { group_id: 3, - field_ids: [0xFA; 62], + field_ids: [0xFA; 62].as_slice().try_into().unwrap(), }; let msg = CanMessage::TelemetryGroupDefinition { payload }; test_round_trip(msg); @@ -118,12 +140,17 @@ mod tests { #[test] fn test_telemetry_group_update() { + let data_values = [ + payloads::CanDataValue::Int32(42), + payloads::CanDataValue::Float32(12.34), + payloads::CanDataValue::Boolean(true), + ]; let payload = payloads::TelemetryGroupUpdatePayload { group_id: 4, - values: [0xFB; 62], + values: data_values.as_slice().try_into().unwrap(), }; let msg = CanMessage::TelemetryGroupUpdate { payload }; - test_round_trip(msg); + test_round_trip_lossy(msg); } #[test] @@ -144,10 +171,10 @@ mod tests { fn test_parameter_set_req() { let payload = payloads::ParameterSetReqPayload { parameter_id: 10, - value: [0xAA; 61], + value: payloads::CanDataValue::Int32(67), }; let msg = CanMessage::ParameterSetReq { payload }; - test_round_trip(msg); + test_round_trip_lossy(msg); } #[test] @@ -155,10 +182,10 @@ mod tests { let payload = payloads::ParameterSetConfirmationPayload { parameter_id: 11, status: payloads::ParameterSetStatus::Success, - value: [0xBB; 61], + value: payloads::CanDataValue::Float32(42.0), }; let msg = CanMessage::ParameterSetConfirmation { payload }; - test_round_trip(msg); + test_round_trip_lossy(msg); } #[test] @@ -194,16 +221,16 @@ mod tests { let payload = payloads::FieldGetResPayload { field_id: 21, field_status: FieldStatus::Ok, - value: [0xCC; 61], + value: payloads::CanDataValue::Boolean(true), }; let msg = CanMessage::FieldGetRes { payload }; - test_round_trip(msg); + test_round_trip_lossy(msg); } #[test] fn test_field_id_lookup_req() { let payload = payloads::FieldIDLookupReqPayload { - field_name: [0xDD; 61], + field_name: "Test Field Name".try_into().unwrap(), }; let msg = CanMessage::FieldIDLookupReq { payload }; test_round_trip(msg); @@ -213,8 +240,8 @@ mod tests { fn test_field_id_lookup_res() { let payload = payloads::FieldIDLookupResPayload { field_id: 22, - field_status: FieldStatus::Ok, field_type: payloads::CanDataType::Float32, + field_status: FieldStatus::Ok, }; let msg = CanMessage::FieldIDLookupRes { payload }; test_round_trip(msg); @@ -223,14 +250,13 @@ mod tests { #[test] fn test_invalid_message_type() { // Create a frame with an invalid message type (255 is not defined) - let mut frame = CanMessageFrame::new_zeroed(); - frame.message_type = 255; + let frame = socketcan::CanFdFrame::new(socketcan::StandardId::ZERO, &[255]).unwrap(); let result: Result = frame.try_into(); assert!(result.is_err(), "Expected error for invalid message type"); let err_msg = result.unwrap_err().to_string(); assert!( - err_msg.contains("Failed to convert message"), + err_msg.contains("invalid enum discriminant"), "Error message should mention conversion failure: {}", err_msg ); @@ -239,11 +265,16 @@ mod tests { #[test] fn test_invalid_can_data_type() { // Create a FieldRegistration with invalid CanDataType (255 is out of range) - let mut frame = CanMessageFrame::new_zeroed(); - frame.message_type = 20; // TelemetryValueRegistration - frame.data[0] = 5; // field_id - frame.data[1] = 255; // Invalid CanDataType - // Rest is field_name + let frame = socketcan::CanFdFrame::new( + socketcan::StandardId::ZERO, + &[ + 20, // TelemetryValueRegistration + 5, // field_id + 255, // Invalid CanDataType + // Rest is field_name + ], + ) + .unwrap(); let result: Result = frame.try_into(); assert!(result.is_err(), "Expected error for invalid CanDataType"); @@ -252,10 +283,15 @@ mod tests { #[test] fn test_invalid_parameter_set_status() { // Create a ParameterSetConfirmation with invalid status - let mut frame = CanMessageFrame::new_zeroed(); - frame.message_type = 51; // ParameterSetConfirmation - frame.data[0] = 10; // parameter_id - frame.data[1] = 255; // Invalid ParameterSetStatus + let frame = socketcan::CanFdFrame::new( + socketcan::StandardId::ZERO, + &[ + 51, // ParameterSetConfirmation + 10, // parameter_id + 255, // Invalid ParameterSetStatus + ], + ) + .unwrap(); // Rest is value let result: Result = frame.try_into(); @@ -268,10 +304,15 @@ mod tests { #[test] fn test_invalid_parameter_lock_status() { // Create a ParameterSetLockReq with invalid lock status - let mut frame = CanMessageFrame::new_zeroed(); - frame.message_type = 52; // ParameterSetLockReq - frame.data[0] = 12; // parameter_id - frame.data[1] = 255; // Invalid ParameterLockStatus + let frame = socketcan::CanFdFrame::new( + socketcan::StandardId::ZERO, + &[ + 52, // ParameterSetLockReq + 12, // parameter_id + 255, // Invalid ParameterLockStatus + ], + ) + .unwrap(); let result: Result = frame.try_into(); assert!( diff --git a/liquidcan_rust/src/payloads.rs b/liquidcan_rust/src/payloads.rs index 38a29f7..42da856 100644 --- a/liquidcan_rust/src/payloads.rs +++ b/liquidcan_rust/src/payloads.rs @@ -1,18 +1,10 @@ -use modular_bitfield::{Specifier, private::static_assertions}; -use zerocopy_derive::{FromBytes, Immutable, IntoBytes, TryFromBytes}; +use liquidcan_rust_macros::byte_codec::ByteCodec; +use modular_bitfield::Specifier; +use zerocopy_derive::{Immutable, IntoBytes, TryFromBytes}; -#[derive(Specifier, Debug, Copy, Clone, PartialEq, Eq, Immutable, TryFromBytes, IntoBytes)] -#[repr(u8)] -pub enum CanDataType { - Float32 = 0, - Int32 = 1, - Int16 = 2, - Int8 = 3, - UInt32 = 4, - UInt16 = 5, - UInt8 = 6, - Boolean = 7, -} +pub use crate::can_data::{ + CanDataType, CanDataValue, CanString, NonNullCanBytes, PackedCanDataValues, +}; #[derive(Specifier, Debug, Copy, Clone, PartialEq, Eq, Immutable, TryFromBytes, IntoBytes)] #[repr(u8)] @@ -30,65 +22,56 @@ pub enum ParameterLockStatus { Locked = 1, } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct NodeInfoResPayload { - pub tel_count: u8, // Number of telemetryValues on this node - pub par_count: u8, // Number of parameters on this node - pub firmware_hash: u32, // Hash of the firmware version - pub liquid_hash: u32, // Hash of the LiquidCan protocol version - pub device_name: [u8; 53], // Human-readable device name + pub tel_count: u8, // Number of telemetryValues on this node + pub par_count: u8, // Number of parameters on this node + pub firmware_hash: u32, // Hash of the firmware version + pub liquid_hash: u32, // Hash of the LiquidCan protocol version + pub device_name: CanString<53>, // Human-readable device name } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct StatusPayload { - pub msg: [u8; 63], // Status message text + pub msg: CanString<63>, // Status message text } -// Important: only derives TryFromBytes because enum CanDataType doesn't cover all possible enum variants for u8 -#[derive(Debug, Clone, TryFromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct FieldRegistrationPayload { - pub field_id: u8, // Unique identifier for this field - pub field_type: CanDataType, // Data type - pub field_name: [u8; 61], // Human-readable field name + pub field_id: u8, // Unique identifier for this field + pub field_type: CanDataType, // Data type + pub field_name: CanString<61>, // Human-readable field name } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct TelemetryGroupDefinitionPayload { - pub group_id: u8, // Unique identifier for this group - pub field_ids: [u8; 62], // Array of field IDs in this group + pub group_id: u8, // Unique identifier for this group + pub field_ids: NonNullCanBytes<62>, // Array of field IDs in this group } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct TelemetryGroupUpdatePayload { - pub group_id: u8, // Group identifier - pub values: [u8; 62], // Packed values of all telemetry values in the group + pub group_id: u8, // Group identifier + pub values: PackedCanDataValues<62>, // Packed values of all telemetry values in the group } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct HeartbeatPayload { pub counter: u32, // Incrementing counter value } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct ParameterSetReqPayload { - pub parameter_id: u8, // Parameter identifier - pub value: [u8; 61], // New value (type depends on parameter) + pub parameter_id: u8, // Parameter identifier + pub value: CanDataValue, // New value (type depends on parameter) } // Important: only derives TryFromBytes because enum ParameterSetStatus doesn't cover all possible enum variants for u8 -#[derive(Debug, Clone, TryFromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct ParameterSetConfirmationPayload { pub parameter_id: u8, // Parameter identifier pub status: ParameterSetStatus, // Status code - pub value: [u8; 61], // Confirmed value after set operation + pub value: CanDataValue, // Confirmed value after set operation } #[derive(Specifier, Debug, Copy, Clone, PartialEq, Eq, Immutable, TryFromBytes, IntoBytes)] #[repr(u8)] @@ -97,38 +80,33 @@ pub enum FieldStatus { NotFound = 1, } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct FieldGetReqPayload { pub field_id: u8, // Field identifier } -#[derive(Debug, Clone, TryFromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct FieldGetResPayload { pub field_id: u8, // Field identifier pub field_status: FieldStatus, // Status of the get operation - pub value: [u8; 61], // Field value + pub value: CanDataValue, // Field value } -#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct FieldIDLookupReqPayload { - pub field_name: [u8; 61], // Field name + pub field_name: CanString<61>, // Field name } // Important: only derives TryFromBytes because enum CanDataType doesn't cover all possible enum variants for u8 -#[derive(Debug, Clone, TryFromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct FieldIDLookupResPayload { pub field_id: u8, // Field ID - pub field_status: FieldStatus, // Status of the lookup operation pub field_type: CanDataType, // Field Datatype + pub field_status: FieldStatus, // Status of the lookup operation } // Important: only derives TryFromBytes because bool doesn't derive FromBytes -#[derive(Debug, Clone, TryFromBytes, IntoBytes, Immutable, PartialEq)] -#[repr(C, packed)] +#[derive(Debug, Clone, ByteCodec, PartialEq)] pub struct ParameterSetLockPayload { pub parameter_id: u8, // Parameter identifier to lock pub parameter_lock: ParameterLockStatus, // Lock status (0=unlocked, 1=locked) @@ -142,21 +120,20 @@ pub struct ParameterSetLockConfirmationPayload { pub field_status: FieldStatus, // Status of the parameter } -static_assertions::const_assert_eq!(size_of::(), 63); -static_assertions::const_assert_eq!(size_of::(), 63); -static_assertions::const_assert_eq!(size_of::(), 63); -static_assertions::const_assert_eq!(size_of::(), 63); -static_assertions::const_assert_eq!(size_of::(), 63); -static_assertions::const_assert_eq!(size_of::(), 4); -static_assertions::const_assert_eq!(size_of::(), 62); -static_assertions::const_assert_eq!(size_of::(), 63); -static_assertions::const_assert_eq!(size_of::(), 1); -static_assertions::const_assert_eq!(size_of::(), 63); -static_assertions::const_assert_eq!(size_of::(), 61); -static_assertions::const_assert_eq!(size_of::(), 3); -static_assertions::const_assert_eq!(size_of::(), 3); -static_assertions::const_assert_eq!(size_of::(), 2); -static_assertions::const_assert_eq!(size_of::(), 1); -static_assertions::const_assert_eq!(size_of::(), 1); -static_assertions::const_assert_eq!(size_of::(), 1); -static_assertions::const_assert_eq!(size_of::(), 1); +static_assertions::const_assert_eq!(NodeInfoResPayload::MAX_SERIALIZED_SIZE, 63); +static_assertions::const_assert_eq!(StatusPayload::MAX_SERIALIZED_SIZE, 63); +static_assertions::const_assert_eq!(FieldRegistrationPayload::MAX_SERIALIZED_SIZE, 63); +static_assertions::const_assert_eq!(TelemetryGroupDefinitionPayload::MAX_SERIALIZED_SIZE, 63); +static_assertions::const_assert_eq!(TelemetryGroupUpdatePayload::MAX_SERIALIZED_SIZE, 63); +static_assertions::const_assert_eq!(HeartbeatPayload::MAX_SERIALIZED_SIZE, 4); +static_assertions::const_assert_eq!(ParameterSetReqPayload::MAX_SERIALIZED_SIZE, 5); +static_assertions::const_assert_eq!(ParameterSetConfirmationPayload::MAX_SERIALIZED_SIZE, 6); +static_assertions::const_assert_eq!(FieldGetReqPayload::MAX_SERIALIZED_SIZE, 1); +static_assertions::const_assert_eq!(FieldGetResPayload::MAX_SERIALIZED_SIZE, 6); +static_assertions::const_assert_eq!(FieldIDLookupReqPayload::MAX_SERIALIZED_SIZE, 61); +static_assertions::const_assert_eq!(FieldIDLookupResPayload::MAX_SERIALIZED_SIZE, 3); +static_assertions::const_assert_eq!(ParameterSetLockPayload::MAX_SERIALIZED_SIZE, 2); +static_assertions::const_assert_eq!(FieldStatus::MAX_SERIALIZED_SIZE, 1); +static_assertions::const_assert_eq!(CanDataType::MAX_SERIALIZED_SIZE, 1); +static_assertions::const_assert_eq!(ParameterSetStatus::MAX_SERIALIZED_SIZE, 1); +static_assertions::const_assert_eq!(ParameterLockStatus::MAX_SERIALIZED_SIZE, 1); diff --git a/liquidcan_rust/src/raw_can_message.rs b/liquidcan_rust/src/raw_can_message.rs index f183066..9eb15e3 100644 --- a/liquidcan_rust/src/raw_can_message.rs +++ b/liquidcan_rust/src/raw_can_message.rs @@ -1,8 +1,5 @@ use modular_bitfield::prelude::B5; -use modular_bitfield::private::static_assertions; use modular_bitfield::{Specifier, bitfield}; -use std::mem::size_of; -use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout}; #[derive(Specifier, Debug, PartialEq, Eq)] pub enum CanMessagePriority { @@ -20,12 +17,3 @@ pub struct CanMessageId { #[skip] __: B5, } - -#[derive(Debug, IntoBytes, FromBytes, Immutable, KnownLayout)] -#[repr(C, packed)] -pub struct CanMessageFrame { - pub message_type: u8, - pub data: [u8; 63], -} - -static_assertions::const_assert_eq!(size_of::(), 64); diff --git a/sections/03_frame_layout.tex b/sections/03_frame_layout.tex index 4ed87b7..dbd2bbb 100644 --- a/sections/03_frame_layout.tex +++ b/sections/03_frame_layout.tex @@ -1,7 +1,8 @@ \section{Common Frame Layout}\label{sec:frame-layout} \paragraph{} -The CAN data field consists of 64 bytes. -The first byte of each message contains the message type. +The CAN data field is variable-length, up to a maximum of 64 bytes. +The first byte of each message contains the message type. +The remaining bytes carry the payload, whose length depends on the message type. This simple format allows the protocol to be extended in the future by adding more message types. See Section \ref{sec:message-types} for a detailed description of message types and their numeric values. @@ -11,7 +12,14 @@ \section{Common Frame Layout}\label{sec:frame-layout} Field & Bytes & Description \\ \midrule message\_type & 1 & Type of message (see Message Types) \\ -data & 63 & Payload data \\ +data & 0--63 & Payload data (length depends on message type) \\ \bottomrule \end{tabular} \end{center} + +\textbf{Note:} Each message type defines its own payload size. Message types that +contain only fixed-size fields have a constant wire size, while those that end +with a variable-length field (e.g.\ null-terminated strings or packed data +arrays) may use fewer bytes than the maximum. Should the payload not fit exactly into +one of the CAN FD data field sizes (1-8, 12, 16, 20, 24, 32, 48, or 64 bytes), +it should be padded with zeros to the next valid size. \ No newline at end of file diff --git a/sections/08_message_types.tex b/sections/08_message_types.tex index 2e7373d..87fc3ac 100644 --- a/sections/08_message_types.tex +++ b/sections/08_message_types.tex @@ -30,7 +30,7 @@ \section{Message Types}\label{sec:message-types} 50 & \texttt{parameter\_set\_req}\label{msg:parameter-set-req} & ParameterSetReq (\ref{struct:ParameterSetReq}) & Server/Node $\rightarrow$ Node & Request to set a parameter value \\ 51 & \texttt{parameter\_set\_confirmation}\label{msg:parameter-set-confirmation} & ParameterSetConfirmation (\ref{struct:ParameterSetConfirmation}) & Node $\rightarrow$ Server/Node & Response with confirmed parameter value \\ 52 & \texttt{parameter\_set\_lock\_req}\label{msg:parameter-set-lock-req} & ParameterSetLock (\ref{struct:ParameterSetLock}) & Server/Node $\rightarrow$ Node & Request to lock a parameter \\ -53 & \texttt{parameter\_set\_lock\_confirmation}\label{msg:parameter-set-lock-res} & ParameterSetLock (\ref{struct:ParameterSetLock}) & Node $\rightarrow$ Server & Response confirming parameter lock \\ +53 & \texttt{parameter\_set\_lock\_confirmation}\label{msg:parameter-set-lock-res} & ParameterSetLockConfirmation (\ref{struct:ParameterSetLockConfirmation}) & Node $\rightarrow$ Server & Response confirming parameter lock \\ \midrule \multicolumn{5}{l}{\textit{Field Access}} \\ 60 & \texttt{field\_get\_req}\label{msg:field-get-req} & FieldGetReq (\ref{struct:FieldGetReq}) & Server/Node $\rightarrow$ Node & Request field value \\ diff --git a/sections/09_data_structures.tex b/sections/09_data_structures.tex index c0cc8f3..9334e6a 100644 --- a/sections/09_data_structures.tex +++ b/sections/09_data_structures.tex @@ -59,9 +59,9 @@ \subsection{NodeInfoRes}\label{struct:NodeInfoRes} par\_cnt & 1 & Number of parameters on this node \\ firmware\_hash & 4 & Hash of the firmware version \\ liquid\_hash & 4 & Hash of the LiquidCan protocol version \\ -device\_name & 53 & Human-readable device name \\ +device\_name & $\leq$53 & Human-readable device name (null-terminated) \\ \midrule -\textbf{Total} & \textbf{63} & \\ +\textbf{Total} & \textbf{11--63} & \\ \bottomrule \end{tabular} \end{center} @@ -86,7 +86,7 @@ \subsection{Status}\label{struct:Status} \toprule Field & Bytes & Description \\ \midrule -msg & 63 & Status message text \\ +msg & $\leq$63 & Status message text (null-terminated) \\ \bottomrule \end{tabular} \end{center} @@ -110,9 +110,9 @@ \subsection{FieldRegistration}\label{struct:FieldRegistration} \midrule field\_id & 1 & Unique identifier for this field \\ field\_type & 1 & Data type (DataType enum) \\ -field\_name & 61 & Human-readable field name \\ +field\_name & $\leq$61 & Human-readable field name (null-terminated) \\ \midrule -\textbf{Total} & \textbf{63} & \\ +\textbf{Total} & \textbf{3--63} & \\ \bottomrule \end{tabular} \end{center} @@ -134,9 +134,9 @@ \subsection{TelemetryGroupDefinition}\label{struct:TelemetryGroupDefinition} Field & Bytes & Description \\ \midrule group\_id & 1 & Unique identifier for this group \\ -field\_ids & 62 & Array of field IDs in this group \\ +field\_ids & $\leq$62 & Array of field IDs in this group (terminated by end of frame) \\ \midrule -\textbf{Total} & \textbf{63} & \\ +\textbf{Total} & \textbf{2--63} & \\ \bottomrule \end{tabular} \end{center} @@ -157,9 +157,9 @@ \subsection{TelemetryGroupUpdate}\label{struct:TelemetryGroupUpdate} Field & Bytes & Description \\ \midrule group\_id & 1 & Group identifier \\ -values & 62 & Packed values of all telemetry values in the group \\ +values & $\leq$62 & Packed values of all telemetry values in the group \\ \midrule -\textbf{Total} & \textbf{63} & \\ +\textbf{Total} & \textbf{2--63} & \\ \bottomrule \end{tabular} \end{center} @@ -205,9 +205,9 @@ \subsection{ParameterSetReq}\label{struct:ParameterSetReq} Field & Bytes & Description \\ \midrule parameter\_id & 1 & Parameter identifier \\ -value & 61 & New value (type depends on parameter) \\ +value & 1--4 & New value (size depends on parameter datatype) \\ \midrule -\textbf{Total} & \textbf{62} & \\ +\textbf{Total} & \textbf{2--5} & \\ \bottomrule \end{tabular} \end{center} @@ -215,7 +215,7 @@ \subsection{ParameterSetReq}\label{struct:ParameterSetReq} \begin{lstlisting}[caption={ParameterSetReq struct}] typedef struct __attribute__((packed)) { uint8_t parameter_id; - uint8_t value[61]; + uint8_t value[]; // 1-4 bytes, flexible array member } parameter_set_req_t; \end{lstlisting} @@ -257,9 +257,9 @@ \subsection{ParameterSetConfirmation}\label{struct:ParameterSetConfirmation} \midrule parameter\_id & 1 & Parameter identifier \\ status & 1 & Status code (ParameterSetStatus enum) \\ -value & 61 & Confirmed value after set operation \\ +value & 1--4 & Confirmed value after set operation (size depends on datatype) \\ \midrule -\textbf{Total} & \textbf{63} & \\ +\textbf{Total} & \textbf{3--6} & \\ \bottomrule \end{tabular} \end{center} @@ -271,7 +271,7 @@ \subsection{ParameterSetConfirmation}\label{struct:ParameterSetConfirmation} typedef struct __attribute__((packed)) { uint8_t parameter_id; uint8_t status; - uint8_t value[61]; + uint8_t value[]; // 1-4 bytes, flexible array member } parameter_set_confirmation_t; \end{lstlisting} @@ -305,9 +305,9 @@ \subsection{FieldGetRes}\label{struct:FieldGetRes} \midrule field\_id & 1 & Field identifier \\ field\_status & 1 & Field existence status (FieldStatus enum) \\ -value & 61 & Field value \\ +value & 1--4 & Field value (size depends on field datatype) \\ \midrule -\textbf{Total} & \textbf{63} & \\ +\textbf{Total} & \textbf{3--6} & \\ \bottomrule \end{tabular} \end{center} @@ -320,7 +320,7 @@ \subsection{FieldGetRes}\label{struct:FieldGetRes} typedef struct __attribute__((packed)) { uint8_t field_id; uint8_t field_status; - uint8_t value[61]; + uint8_t value[]; // 1-4 bytes, flexible array member } field_get_res_t; \end{lstlisting} @@ -333,9 +333,9 @@ \subsection{FieldIDLookupReq}\label{struct:FieldIDLookupReq} \toprule Field & Bytes & Description \\ \midrule -field\_name & 61 & Field Name \\ +field\_name & $\leq$61 & Field Name (null-terminated) \\ \midrule -\textbf{Total} & \textbf{61} & \\ +\textbf{Total} & \textbf{2--61} & \\ \bottomrule \end{tabular} \end{center}