From 96adc990a7acce6632d4c5dd1ec52c9ad1d6c867 Mon Sep 17 00:00:00 2001 From: Raffael Rott Date: Fri, 13 Feb 2026 00:27:02 +0100 Subject: [PATCH 1/3] Added the rust liquidCAN protoc l implementation. For development details look at the ECUEmulator Repo(48081e45) --- .gitignore | 16 + Rust-Implementation/LiquidCan.rs | 0 liquidcan_rust/Cargo.toml | 12 + .../liquidcan_rust_macros/Cargo.toml | 9 + .../liquidcan_rust_macros_derive/Cargo.toml | 11 + .../liquidcan_rust_macros_derive/src/lib.rs | 51 +++ .../liquidcan_rust_macros/src/lib.rs | 4 + .../liquidcan_rust_macros/src/padded_enum.rs | 351 ++++++++++++++++++ liquidcan_rust/src/can_message.rs | 101 +++++ liquidcan_rust/src/lib.rs | 8 + liquidcan_rust/src/message_conversion.rs | 216 +++++++++++ liquidcan_rust/src/payloads.rs | 141 +++++++ liquidcan_rust/src/raw_can_message.rs | 31 ++ 13 files changed, 951 insertions(+) delete mode 100644 Rust-Implementation/LiquidCan.rs create mode 100644 liquidcan_rust/Cargo.toml create mode 100644 liquidcan_rust/liquidcan_rust_macros/Cargo.toml create mode 100644 liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/Cargo.toml create mode 100644 liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/src/lib.rs create mode 100644 liquidcan_rust/liquidcan_rust_macros/src/lib.rs create mode 100644 liquidcan_rust/liquidcan_rust_macros/src/padded_enum.rs create mode 100644 liquidcan_rust/src/can_message.rs create mode 100644 liquidcan_rust/src/lib.rs create mode 100644 liquidcan_rust/src/message_conversion.rs create mode 100644 liquidcan_rust/src/payloads.rs create mode 100644 liquidcan_rust/src/raw_can_message.rs diff --git a/.gitignore b/.gitignore index 4a36f24..3b28bab 100644 --- a/.gitignore +++ b/.gitignore @@ -327,3 +327,19 @@ TSWLatexianTemp* # option is specified. Footnotes are the stored in a file with suffix Notes.bib. # Uncomment the next line to have this generated file ignored. #*Notes.bib + +# Rust.gitignore +# Generated by Cargo +# will have compiled files and executables +**/debug/ +**/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +**/Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/Rust-Implementation/LiquidCan.rs b/Rust-Implementation/LiquidCan.rs deleted file mode 100644 index e69de29..0000000 diff --git a/liquidcan_rust/Cargo.toml b/liquidcan_rust/Cargo.toml new file mode 100644 index 0000000..0069574 --- /dev/null +++ b/liquidcan_rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "liquidcan_rust" +version = "0.1.0" +edition = "2024" + +[dependencies] +modular-bitfield = "0.13.0" +static_assertions = "1.1.0" +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" } diff --git a/liquidcan_rust/liquidcan_rust_macros/Cargo.toml b/liquidcan_rust/liquidcan_rust_macros/Cargo.toml new file mode 100644 index 0000000..2bb0040 --- /dev/null +++ b/liquidcan_rust/liquidcan_rust_macros/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "liquidcan_rust_macros" +version = "0.1.0" +edition = "2024" + +[dependencies] +paste = "1.0.15" +zerocopy = "0.8.28" +zerocopy-derive = "0.8.28" 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 new file mode 100644 index 0000000..4c76c4b --- /dev/null +++ b/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "liquidcan_rust_macros_derive" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = "2.0.110" +quote = "1.0.42" \ 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 new file mode 100644 index 0000000..016e3b0 --- /dev/null +++ b/liquidcan_rust/liquidcan_rust_macros/liquidcan_rust_macros_derive/src/lib.rs @@ -0,0 +1,51 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::Attribute; + +#[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(); + + // Build the trait implementation. + impl_enum_discriminate_derive(&ast) +} + +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; + } + Ok(()) + }) + .unwrap() + } + } + 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"); + } + 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 + } + } + } + }; + generated.into() +} diff --git a/liquidcan_rust/liquidcan_rust_macros/src/lib.rs b/liquidcan_rust/liquidcan_rust_macros/src/lib.rs new file mode 100644 index 0000000..c019794 --- /dev/null +++ b/liquidcan_rust/liquidcan_rust_macros/src/lib.rs @@ -0,0 +1,4 @@ +#[doc(hidden)] +pub use paste::paste; + +mod padded_enum; \ No newline at end of file diff --git a/liquidcan_rust/liquidcan_rust_macros/src/padded_enum.rs b/liquidcan_rust/liquidcan_rust_macros/src/padded_enum.rs new file mode 100644 index 0000000..cc90720 --- /dev/null +++ b/liquidcan_rust/liquidcan_rust_macros/src/padded_enum.rs @@ -0,0 +1,351 @@ +/// 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<'a>(&self, buf: &'a mut [u8; $size]) -> &'a [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/src/can_message.rs b/liquidcan_rust/src/can_message.rs new file mode 100644 index 0000000..318b635 --- /dev/null +++ b/liquidcan_rust/src/can_message.rs @@ -0,0 +1,101 @@ +use crate::payloads; +use liquidcan_rust_macros::padded_enum; +use liquidcan_rust_macros_derive::EnumDiscriminate; + +padded_enum! { + +(size = 64) + +#[derive(Debug, EnumDiscriminate, 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 + } = 10, + #[pad(0)] + WarningStatus { + payload: payloads::StatusPayload + } = 11, + #[pad(0)] + ErrorStatus { + 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 + } = 40, + #[pad(59)] + HeartbeatRes { + 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(61)] + ParameterSetLockConfirmation { + payload: payloads::ParameterSetLockPayload, + } = 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(61)] + FieldIDLookupRes { + payload: payloads::FieldIDLookupResPayload, + } = 63, +} + +} diff --git a/liquidcan_rust/src/lib.rs b/liquidcan_rust/src/lib.rs new file mode 100644 index 0000000..66b6774 --- /dev/null +++ b/liquidcan_rust/src/lib.rs @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..0234358 --- /dev/null +++ b/liquidcan_rust/src/message_conversion.rs @@ -0,0 +1,216 @@ +use crate::can_message::{CanMessage, CanMessagePadded}; +use crate::CanMessageFrame; +use zerocopy::{IntoBytes, TryFromBytes, FromZeros}; + +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) + } +} + +impl From for CanMessageFrame { + 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 + } +} + +#[cfg(test)] +mod tests { + use crate::can_message::CanMessage; + use crate::payloads; + use crate::CanMessageFrame; + + fn test_round_trip(msg: CanMessage) { + let can_data: CanMessageFrame = msg.clone().into(); + let msg_back: CanMessage = can_data + .try_into() + .expect("Failed to convert back to Command"); + assert_eq!(msg, msg_back); + } + + #[test] + fn test_node_info_req() { + let msg = CanMessage::NodeInfoReq; + test_round_trip(msg); + } + + #[test] + fn test_node_info_announcement() { + let payload = payloads::NodeInfoResPayload { + tel_count: 7, + par_count: 5, + firmware_hash: 1234, + liquid_hash: 5678, + device_name: [0xAA; 53], + }; + let msg = CanMessage::NodeInfoAnnouncement { payload }; + test_round_trip(msg); + } + + #[test] + fn test_info_status() { + let payload = payloads::StatusPayload { msg: [0xBB; 63] }; + let msg = CanMessage::InfoStatus { payload }; + test_round_trip(msg); + } + + #[test] + fn test_warning_status() { + let payload = payloads::StatusPayload { msg: [0xCC; 63] }; + let msg = CanMessage::WarningStatus { payload }; + test_round_trip(msg); + } + + #[test] + fn test_error_status() { + let payload = payloads::StatusPayload { msg: [0xDD; 63] }; + let msg = CanMessage::ErrorStatus { payload }; + test_round_trip(msg); + } + + #[test] + fn test_telemetry_value_registration() { + let payload = payloads::FieldRegistrationPayload { + field_id: 5, + field_type: payloads::CanDataType::UInt16, + field_name: [0xEE; 61], + }; + let msg = CanMessage::TelemetryValueRegistration { payload }; + test_round_trip(msg); + } + + #[test] + fn test_parameter_registration() { + let payload = payloads::FieldRegistrationPayload { + field_id: 7, + field_type: payloads::CanDataType::Boolean, + field_name: [0xFF; 61], + }; + let msg = CanMessage::ParameterRegistration { payload }; + test_round_trip(msg); + } + + #[test] + fn test_telemetry_group_definition() { + let payload = payloads::TelemetryGroupDefinitionPayload { + group_id: 3, + field_ids: [0xFA; 62], + }; + let msg = CanMessage::TelemetryGroupDefinition { payload }; + test_round_trip(msg); + } + + #[test] + fn test_telemetry_group_update() { + let payload = payloads::TelemetryGroupUpdatePayload { + group_id: 4, + values: [0xFB; 62], + }; + let msg = CanMessage::TelemetryGroupUpdate { payload }; + test_round_trip(msg); + } + + #[test] + fn test_heartbeat_req() { + let payload = payloads::HeartbeatPayload { counter: 17 }; + let msg = CanMessage::HeartbeatReq { payload }; + test_round_trip(msg); + } + + #[test] + fn test_heartbeat_res() { + let payload = payloads::HeartbeatPayload { counter: 18 }; + let msg = CanMessage::HeartbeatRes { payload }; + test_round_trip(msg); + } + + #[test] + fn test_parameter_set_req() { + let payload = payloads::ParameterSetReqPayload { + parameter_id: 10, + value: [0xAA; 61], + }; + let msg = CanMessage::ParameterSetReq { payload }; + test_round_trip(msg); + } + + #[test] + fn test_parameter_set_confirmation() { + let payload = payloads::ParameterSetConfirmationPayload { + parameter_id: 11, + status: payloads::ParameterSetStatus::Success, + value: [0xBB; 61], + }; + let msg = CanMessage::ParameterSetConfirmation { payload }; + test_round_trip(msg); + } + + #[test] + fn test_parameter_set_lock_req() { + let payload = payloads::ParameterSetLockPayload { + parameter_id: 12, + parameter_lock: payloads::ParameterLockStatus::Locked, + }; + let msg = CanMessage::ParameterSetLockReq { payload }; + test_round_trip(msg); + } + + #[test] + fn test_parameter_set_lock_confirmation() { + let payload = payloads::ParameterSetLockPayload { + parameter_id: 13, + parameter_lock: payloads::ParameterLockStatus::Unlocked, + }; + let msg = CanMessage::ParameterSetLockConfirmation { payload }; + test_round_trip(msg); + } + + #[test] + fn test_field_get_req() { + let payload = payloads::FieldGetReqPayload { field_id: 20 }; + let msg = CanMessage::FieldGetReq { payload }; + test_round_trip(msg); + } + + #[test] + fn test_field_get_res() { + let payload = payloads::FieldGetResPayload { + field_id: 21, + value: [0xCC; 62], + }; + let msg = CanMessage::FieldGetRes { payload }; + test_round_trip(msg); + } + + #[test] + fn test_field_id_lookup_req() { + let payload = payloads::FieldIDLookupReqPayload { + field_name: [0xDD; 61], + }; + let msg = CanMessage::FieldIDLookupReq { payload }; + test_round_trip(msg); + } + + #[test] + fn test_field_id_lookup_res() { + let payload = payloads::FieldIDLookupResPayload { + field_id: 22, + field_type: payloads::CanDataType::Float32, + }; + let msg = CanMessage::FieldIDLookupRes { payload }; + test_round_trip(msg); + } +} diff --git a/liquidcan_rust/src/payloads.rs b/liquidcan_rust/src/payloads.rs new file mode 100644 index 0000000..554f166 --- /dev/null +++ b/liquidcan_rust/src/payloads.rs @@ -0,0 +1,141 @@ +use modular_bitfield::{private::static_assertions, Specifier}; +use zerocopy_derive::{FromBytes, 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, +} + +#[derive(Specifier, Debug, Copy, Clone, PartialEq, Eq, Immutable, TryFromBytes, IntoBytes)] +#[repr(u8)] +pub enum ParameterSetStatus { + Success = 0, // Parameter was successfully set + InvalidParameterID = 1, // The parameter ID does not exist + ParameterLocked = 2, // The parameter is locked and cannot be modified + NodeToNodeModification = 3, // The parameter was modified by another node +} + +#[derive(Specifier, Debug, Copy, Clone, PartialEq, Eq, Immutable, TryFromBytes, IntoBytes)] +#[repr(u8)] +pub enum ParameterLockStatus { + Unlocked = 0, + Locked = 1, +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +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 +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct StatusPayload { + pub msg: [u8; 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)] +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 +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct TelemetryGroupDefinitionPayload { + pub group_id: u8, // Unique identifier for this group + pub field_ids: [u8; 62], // Array of field IDs in this group +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct TelemetryGroupUpdatePayload { + pub group_id: u8, // Group identifier + pub values: [u8; 62], // Packed values of all telemetry values in the group +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct HeartbeatPayload { + pub counter: u32, // Incrementing counter value +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct ParameterSetReqPayload { + pub parameter_id: u8, // Parameter identifier + pub value: [u8; 61], // 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)] +pub struct ParameterSetConfirmationPayload { + pub parameter_id: u8, // Parameter identifier + pub status: ParameterSetStatus, // Status code + pub value: [u8; 61], // Confirmed value after set operation +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct FieldGetReqPayload { + pub field_id: u8, // Field identifier +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct FieldGetResPayload { + pub field_id: u8, // Field identifier + pub value: [u8; 62], // Field value +} + +#[derive(Debug, Clone, FromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct FieldIDLookupReqPayload { + pub field_name: [u8; 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)] +pub struct FieldIDLookupResPayload { + pub field_id: u8, // Field ID + pub field_type: CanDataType, // Field Datatype +} + +// Important: only derives TryFromBytes because bool doesn't derive FromBytes +#[derive(Debug, Clone, TryFromBytes, IntoBytes, Immutable, PartialEq)] +#[repr(C, packed)] +pub struct ParameterSetLockPayload { + pub parameter_id: u8, // Parameter identifier to lock + pub parameter_lock: ParameterLockStatus, // Lock status (0=unlocked, 1=locked) +} + +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::(), 2); +static_assertions::const_assert_eq!(size_of::(), 2); diff --git a/liquidcan_rust/src/raw_can_message.rs b/liquidcan_rust/src/raw_can_message.rs new file mode 100644 index 0000000..93e8463 --- /dev/null +++ b/liquidcan_rust/src/raw_can_message.rs @@ -0,0 +1,31 @@ +use modular_bitfield::prelude::B5; +use modular_bitfield::private::static_assertions; +use modular_bitfield::{bitfield, Specifier}; +use std::mem::size_of; +use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(Specifier, Debug, PartialEq, Eq)] +pub enum CanMessagePriority { + Low = 0, + High = 1, +} + +#[bitfield] +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u16)] +pub struct CanMessageId { + pub receiver_id: B5, + pub sender_id: B5, + pub priority: CanMessagePriority, + #[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); From c332fbe274d9dc266d653e1c639edb7da647ea71 Mon Sep 17 00:00:00 2001 From: Raffael Rott Date: Fri, 13 Feb 2026 00:33:34 +0100 Subject: [PATCH 2/3] added rust github action --- .github/workflows/rust.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..734636e --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,24 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + working-directory: liquidcan_rust + run: cargo build --verbose + - name: Run tests + working-directory: liquidcan_rust + run: cargo test --verbose From baf0693eca3786c07b282999d9302fee6dade219 Mon Sep 17 00:00:00 2001 From: Raffael Rott Date: Fri, 13 Feb 2026 00:35:32 +0100 Subject: [PATCH 3/3] fixed compilation bug --- liquidcan_rust/Cargo.toml | 1 + liquidcan_rust/src/message_conversion.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/liquidcan_rust/Cargo.toml b/liquidcan_rust/Cargo.toml index 0069574..ac8d7d2 100644 --- a/liquidcan_rust/Cargo.toml +++ b/liquidcan_rust/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0.100" modular-bitfield = "0.13.0" static_assertions = "1.1.0" zerocopy = "0.8.27" diff --git a/liquidcan_rust/src/message_conversion.rs b/liquidcan_rust/src/message_conversion.rs index 0234358..b65cf42 100644 --- a/liquidcan_rust/src/message_conversion.rs +++ b/liquidcan_rust/src/message_conversion.rs @@ -1,5 +1,6 @@ use crate::can_message::{CanMessage, CanMessagePadded}; use crate::CanMessageFrame; +use anyhow::anyhow; use zerocopy::{IntoBytes, TryFromBytes, FromZeros}; impl TryFrom for CanMessage {