From 47b472f1b2b2926ce4d67251ff7f0121d8d7c662 Mon Sep 17 00:00:00 2001 From: Jeff McBride Date: Tue, 30 Dec 2025 11:57:42 -0800 Subject: [PATCH 1/2] [node] Default record sub names should be decimal, not hex numbers --- zencan-build/src/codegen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zencan-build/src/codegen.rs b/zencan-build/src/codegen.rs index e01b0c2..67af273 100644 --- a/zencan-build/src/codegen.rs +++ b/zencan-build/src/codegen.rs @@ -49,7 +49,7 @@ fn get_sub_field_name(sub: &SubDefinition) -> Result { } None => { // Unwrap safety: This should always yield a valid identifier - Ok(syn::parse_str(&format!("sub{:x}", sub.sub_index)).unwrap()) + Ok(syn::parse_str(&format!("sub{}", sub.sub_index)).unwrap()) } } } From b58c621f672e3a9138c9d70ed38fea1874f1fe53 Mon Sep 17 00:00:00 2001 From: Jeff McBride Date: Tue, 30 Dec 2025 12:28:25 -0800 Subject: [PATCH 2/2] Add new object data types u64, i64, f64, TimeDifference, TimeOfDay --- Cargo.toml | 1 - integration_tests/Cargo.toml | 1 + .../device_configs/example1.toml | 69 ++++++ integration_tests/tests/node_test.rs | 132 ++++++++++ zencan-build/src/codegen.rs | 85 ++++--- zencan-build/src/lib.rs | 2 +- zencan-client/Cargo.toml | 1 + zencan-client/src/sdo_client.rs | 234 +++++++----------- zencan-common/Cargo.toml | 3 +- zencan-common/src/device_config.rs | 12 + zencan-common/src/lib.rs | 5 +- zencan-common/src/objects.rs | 6 + zencan-common/src/time_types.rs | 177 +++++++++++++ zencan-node/src/object_dict/sub_objects.rs | 99 +++++++- 14 files changed, 643 insertions(+), 184 deletions(-) create mode 100644 zencan-common/src/time_types.rs diff --git a/Cargo.toml b/Cargo.toml index 4ef0cdc..98684a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ embedded-io = { version = "0.6.1" } futures = { version = "0.3.31", default-features = false, features = [ "async-await", ] } -heapless = "0.8.0" log = "0.4.27" serde = { version = "1.0.219", features = ["derive"] } snafu = { version = "0.8.5", default-features = false } diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 99266c3..7a57b71 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -17,6 +17,7 @@ critical-section = { version = "1.2.0", features = ["std"] } embedded-io.workspace = true futures.workspace = true tokio = { version = "1.44.2", features = ["rt", "macros", "time", "sync"] } +rand = "0.9.2" [dev-dependencies] assertables = "9.8.1" diff --git a/integration_tests/device_configs/example1.toml b/integration_tests/device_configs/example1.toml index f629967..e2cef72 100644 --- a/integration_tests/device_configs/example1.toml +++ b/integration_tests/device_configs/example1.toml @@ -148,3 +148,72 @@ array_size = 9 data_type = "uint8" access_type = "rw" pdo_mapping = "both" + +[[objects]] +index = 0x300B +parameter_name = "Time Objects" +object_type = "record" +[[objects.subs]] +sub_index = 1 +data_type = "TimeOfDay" +access_type = "rw" +pdo_mapping = "tpdo" +[[objects.subs]] +sub_index = 2 +data_type = "TimeDifference" +access_type = "rw" + +[[objects]] +index = 0x300C +parameter_name = "All The Numbers" +object_type = "record" +[[objects.subs]] +sub_index = 1 +data_type = "uint8" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 2 +data_type = "uint16" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 3 +data_type = "uint32" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 4 +data_type = "uint64" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 5 +data_type = "int8" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 6 +data_type = "int16" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 7 +data_type = "int32" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 8 +data_type = "int64" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 9 +data_type = "real32" +access_type = "rw" +pdo_mapping = "both" +[[objects.subs]] +sub_index = 10 +data_type = "real64" +access_type = "rw" +pdo_mapping = "both" \ No newline at end of file diff --git a/integration_tests/tests/node_test.rs b/integration_tests/tests/node_test.rs index db0867f..bb1ee9d 100644 --- a/integration_tests/tests/node_test.rs +++ b/integration_tests/tests/node_test.rs @@ -4,8 +4,10 @@ use std::{ }; use integration_tests::{object_dict1, prelude::*}; +use rand::Rng as _; use serial_test::serial; use zencan_client::nmt_master::NmtMaster; +use zencan_common::{TimeDifference, TimeOfDay}; #[serial] #[tokio::test] @@ -453,3 +455,133 @@ async fn test_node_state_callbacks() { }; test_with_background_process(&mut [&mut node], &mut bus, test_task).await; } + +/// Access time fields +#[serial] +#[tokio::test] +async fn test_time_field_access() { + use object_dict1::*; + + let _ = env_logger::try_init(); + + const NODE_ID: u8 = 1; + let mut bus = SimBus::new(); + bus.add_node(&NODE_MBOX); + let callbacks = Callbacks::new(); + let mut node = Node::new( + NodeId::new(NODE_ID).unwrap(), + callbacks, + &NODE_MBOX, + &NODE_STATE, + &OD_TABLE, + ); + let mut client = get_sdo_client(&mut bus, NODE_ID); + + let _logger = BusLogger::new(bus.new_receiver()); + + let test_task = move |_ctx| async move { + let time = TimeOfDay::from_ymd_hms_ms(2015, 8, 23, 10, 20, 5, 500).unwrap(); + client.write_time_of_day(0x300B, 1, time).await.unwrap(); + assert_eq!(time, OBJECT300B.get_sub1()); + let read_time = client.read_time_of_day(0x300B, 1).await.unwrap(); + assert_eq!(time, read_time); + + let delta = TimeDifference::new(20, 50000); + client + .write_time_difference(0x300B, 2, delta) + .await + .unwrap(); + assert_eq!(delta, OBJECT300B.get_sub2()); + let read_delta = client.read_time_difference(0x300B, 2).await.unwrap(); + assert_eq!(delta, read_delta); + }; + test_with_background_process(&mut [&mut node], &mut bus, test_task).await; +} + +/// Access fields for all numeric types +#[serial] +#[tokio::test] +async fn test_numeric_access() { + use object_dict1::*; + + let _ = env_logger::try_init(); + + const NODE_ID: u8 = 1; + let mut bus = SimBus::new(); + bus.add_node(&NODE_MBOX); + let callbacks = Callbacks::new(); + let mut node = Node::new( + NodeId::new(NODE_ID).unwrap(), + callbacks, + &NODE_MBOX, + &NODE_STATE, + &OD_TABLE, + ); + let mut client = get_sdo_client(&mut bus, NODE_ID); + + let _logger = BusLogger::new(bus.new_receiver()); + + let mut rng = rand::rng(); + let test_task = move |_ctx| async move { + let val: u8 = rng.random(); + client.write_u8(0x300C, 1, val).await.unwrap(); + let read_val = client.read_u8(0x300C, 1).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub1()); + assert_eq!(val, read_val); + + let val: u16 = rng.random(); + client.write_u16(0x300C, 2, val).await.unwrap(); + let read_val = client.read_u16(0x300C, 2).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub2()); + assert_eq!(val, read_val); + + let val: u32 = rng.random(); + client.write_u32(0x300C, 3, val).await.unwrap(); + let read_val = client.read_u32(0x300C, 3).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub3()); + assert_eq!(val, read_val); + + let val: u64 = rng.random(); + client.write_u64(0x300C, 4, val).await.unwrap(); + let read_val = client.read_u64(0x300C, 4).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub4()); + assert_eq!(val, read_val); + + let val: i8 = rng.random(); + client.write_i8(0x300C, 5, val).await.unwrap(); + let read_val = client.read_i8(0x300C, 5).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub5()); + assert_eq!(val, read_val); + + let val: i16 = rng.random(); + client.write_i16(0x300C, 6, val).await.unwrap(); + let read_val = client.read_i16(0x300C, 6).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub6()); + assert_eq!(val, read_val); + + let val: i32 = rng.random(); + client.write_i32(0x300C, 7, val).await.unwrap(); + let read_val = client.read_i32(0x300C, 7).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub7()); + assert_eq!(val, read_val); + + let val: i64 = rng.random(); + client.write_i64(0x300C, 8, val).await.unwrap(); + let read_val = client.read_i64(0x300C, 8).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub8()); + assert_eq!(val, read_val); + + let val: f32 = rng.random(); + client.write_f32(0x300C, 9, val).await.unwrap(); + let read_val = client.read_f32(0x300C, 9).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub9()); + assert_eq!(val, read_val); + + let val: f64 = rng.random(); + client.write_f64(0x300C, 10, val).await.unwrap(); + let read_val = client.read_f64(0x300C, 10).await.unwrap(); + assert_eq!(val, OBJECT300C.get_sub10()); + assert_eq!(val, read_val); + }; + test_with_background_process(&mut [&mut node], &mut bus, test_task).await; +} diff --git a/zencan-build/src/codegen.rs b/zencan-build/src/codegen.rs index 67af273..c4402cd 100644 --- a/zencan-build/src/codegen.rs +++ b/zencan-build/src/codegen.rs @@ -61,17 +61,21 @@ fn get_storage_type(data_type: DCDataType) -> (syn::Type, usize) { DCDataType::Int8 => (syn::parse_quote!(ScalarField), 1), DCDataType::Int16 => (syn::parse_quote!(ScalarField), 2), DCDataType::Int32 => (syn::parse_quote!(ScalarField), 4), + DCDataType::Int64 => (syn::parse_quote!(ScalarField), 8), DCDataType::UInt8 => (syn::parse_quote!(ScalarField), 1), DCDataType::UInt16 => (syn::parse_quote!(ScalarField), 2), DCDataType::UInt32 => (syn::parse_quote!(ScalarField), 4), + DCDataType::UInt64 => (syn::parse_quote!(ScalarField), 8), DCDataType::Real32 => (syn::parse_quote!(ScalarField), 4), + DCDataType::Real64 => (syn::parse_quote!(ScalarField), 8), DCDataType::VisibleString(n) | DCDataType::UnicodeString(n) => ( syn::parse_str(&format!("NullTermByteField::<{}>", n)).unwrap(), n, ), DCDataType::OctetString(n) => (syn::parse_str(&format!("ByteField::<{}>", n)).unwrap(), n), + DCDataType::TimeOfDay => (syn::parse_quote!(ScalarField), 6), + DCDataType::TimeDifference => (syn::parse_quote!(ScalarField), 6), DCDataType::Domain => (syn::parse_quote!(CallbackSubObject), 0), - _ => panic!("Unsupported data type {:?}", data_type), } } @@ -81,15 +85,19 @@ fn get_rust_type_and_size(data_type: DCDataType) -> (syn::Type, usize) { DCDataType::Int8 => (syn::parse_quote!(i8), 1), DCDataType::Int16 => (syn::parse_quote!(i16), 2), DCDataType::Int32 => (syn::parse_quote!(i32), 4), + DCDataType::Int64 => (syn::parse_quote!(i64), 8), DCDataType::UInt8 => (syn::parse_quote!(u8), 1), DCDataType::UInt16 => (syn::parse_quote!(u16), 2), DCDataType::UInt32 => (syn::parse_quote!(u32), 4), + DCDataType::UInt64 => (syn::parse_quote!(u64), 8), DCDataType::Real32 => (syn::parse_quote!(f32), 4), + DCDataType::Real64 => (syn::parse_quote!(f64), 8), DCDataType::VisibleString(n) | DCDataType::OctetString(n) | DCDataType::UnicodeString(n) => (syn::parse_str(&format!("[u8; {}]", n)).unwrap(), n), + DCDataType::TimeOfDay => (syn::parse_quote!(TimeOfDay), 6), + DCDataType::TimeDifference => (syn::parse_quote!(TimeDifference), 6), DCDataType::Domain => (syn::parse_quote!(None), 0), - _ => panic!("Unsupported data type {:?}", data_type), } } @@ -122,10 +130,13 @@ fn data_type_to_tokens(dt: DCDataType) -> TokenStream { DCDataType::Int8 => quote!(zencan_node::common::objects::DataType::Int8), DCDataType::Int16 => quote!(zencan_node::common::objects::DataType::Int16), DCDataType::Int32 => quote!(zencan_node::common::objects::DataType::Int32), + DCDataType::Int64 => quote!(zencan_node::common::objects::DataType::Int64), DCDataType::UInt8 => quote!(zencan_node::common::objects::DataType::UInt8), DCDataType::UInt16 => quote!(zencan_node::common::objects::DataType::UInt16), DCDataType::UInt32 => quote!(zencan_node::common::objects::DataType::UInt32), + DCDataType::UInt64 => quote!(zencan_node::common::objects::DataType::UInt64), DCDataType::Real32 => quote!(zencan_node::common::objects::DataType::Real32), + DCDataType::Real64 => quote!(zencan_node::common::objects::DataType::Real64), DCDataType::VisibleString(_) => { quote!(zencan_node::common::objects::DataType::VisibleString) } @@ -141,7 +152,7 @@ fn data_type_to_tokens(dt: DCDataType) -> TokenStream { } } -fn pdo_mapping_to_tokens(p: PdoMappable) -> TokenStream { +fn pdo_mappable_to_tokens(p: PdoMappable) -> TokenStream { match p { PdoMappable::None => quote!(zencan_node::common::objects::PdoMappable::None), PdoMappable::Tpdo => quote!(zencan_node::common::objects::PdoMappable::Tpdo), @@ -229,38 +240,48 @@ fn generate_object_definition(obj: &ObjectDefinition) -> Result DefaultValue { +fn default_default_value(data_type: DCDataType) -> Option { match data_type { DCDataType::Boolean | DCDataType::Int8 | DCDataType::Int16 | DCDataType::Int32 + | DCDataType::Int64 | DCDataType::UInt8 | DCDataType::UInt16 - | DCDataType::UInt32 => DefaultValue::Integer(0), - DCDataType::Real32 => DefaultValue::Float(0.0), + | DCDataType::UInt32 => Some(DefaultValue::Integer(0)), + DCDataType::UInt64 => Some(DefaultValue::Integer(0)), + DCDataType::Real32 | DCDataType::Real64 => Some(DefaultValue::Float(0.0)), DCDataType::VisibleString(_) | DCDataType::UnicodeString(_) - | DCDataType::OctetString(_) => DefaultValue::String("".to_string()), - DCDataType::TimeOfDay => DefaultValue::String("".to_string()), - DCDataType::TimeDifference => DefaultValue::String("".to_string()), - DCDataType::Domain => DefaultValue::String("".to_string()), + | DCDataType::OctetString(_) => Some(DefaultValue::String("".to_string())), + _ => None, } } fn get_default_tokens( - value: &DefaultValue, + value: Option<&DefaultValue>, data_type: DCDataType, ) -> Result { if matches!(data_type, DCDataType::Domain) { return Ok(quote!(CallbackSubObject::new())); } + if value.is_none() { + return Ok(match data_type { + DCDataType::TimeDifference => { + quote!(ScalarField::::new(TimeDifference::ZERO)) + } + DCDataType::TimeOfDay => quote!(ScalarField::::new(TimeOfDay::EPOCH)), + _ => quote!(Default::Default), + }); + } + let value = value.unwrap(); match value { DefaultValue::String(s) => { if !data_type.is_str() { return Err(CompileError::DefaultValueTypeMismatch { message: format!( - "Default value {} is not a string for type {:?}", + "Default string value '{}' is not a string for type {:?}", s, data_type ), }); @@ -274,10 +295,11 @@ fn get_default_tokens( } } DefaultValue::Float(f) => match data_type { - DCDataType::Real32 => Ok(quote!(ScalarField::new(#f))), + DCDataType::Real32 => Ok(quote!(ScalarField::::new(#f as f32))), + DCDataType::Real64 => Ok(quote!(ScalarField::::new(#f))), _ => Err(CompileError::DefaultValueTypeMismatch { message: format!( - "Default value {} is not a valid value for type {:?}", + "Default float value {} is not a valid value for type {:?}", f, data_type ), }), @@ -295,13 +317,16 @@ fn get_default_tokens( DCDataType::Int8 => Ok(quote!(ScalarField::::new(#i as i8))), DCDataType::Int16 => Ok(quote!(ScalarField::::new(#i as i16))), DCDataType::Int32 => Ok(quote!(ScalarField::::new(#i as i32))), + DCDataType::Int64 => Ok(quote!(ScalarField::::new(#i))), DCDataType::UInt8 => Ok(quote!(ScalarField::::new(#i as u8))), DCDataType::UInt16 => Ok(quote!(ScalarField::::new(#i as u16))), DCDataType::UInt32 => Ok(quote!(ScalarField::::new(#i as u32))), + DCDataType::UInt64 => Ok(quote!(ScalarField::::new(#i as u64))), DCDataType::Real32 => Ok(quote!(ScalarField::::new(#i as f32))), + DCDataType::Real64 => Ok(quote!(ScalarField::::new(#i as f64))), _ => Err(CompileError::DefaultValueTypeMismatch { message: format!( - "Default value {} is not a valid value for type {:?}", + "Default integer value {} is not a valid value for type {:?}", i, data_type ), }), @@ -328,14 +353,14 @@ fn get_object_impls( let getter_name = format_ident!("get_{}", field_name); let data_type = data_type_to_tokens(def.data_type); let access_type = access_type_to_tokens(def.access_type.0); - let pdo_mapping = pdo_mapping_to_tokens(def.pdo_mapping); + let pdo_mapping = pdo_mappable_to_tokens(def.pdo_mapping); let persist = def.persist; let default_value = def .default_value .clone() - .unwrap_or(default_default_value(def.data_type)); - let default_value = get_default_tokens(&default_value, def.data_type)?; + .or_else(|| default_default_value(def.data_type)); + let default_value = get_default_tokens(default_value.as_ref(), def.data_type)?; default_init_tokens.extend(quote! { #field_name: #default_value, }); @@ -383,17 +408,17 @@ fn get_object_impls( let array_size = def.array_size; let data_type = data_type_to_tokens(def.data_type); let access_type = access_type_to_tokens(def.access_type.0); - let pdo_mapping = pdo_mapping_to_tokens(def.pdo_mapping); + let pdo_mapping = pdo_mappable_to_tokens(def.pdo_mapping); let persist = def.persist; - let default_value = - def.default_value - .clone() - .unwrap_or(vec![default_default_value(def.data_type); array_size]); - - let default_tokens: Vec<_> = default_value + let default_values = if let Some(defs) = def.default_value.clone() { + defs.into_iter().map(Some).collect() + } else { + vec![default_default_value(def.data_type); array_size] + }; + let default_tokens: Vec<_> = default_values .iter() - .map(|v| get_default_tokens(v, def.data_type)) + .map(|v| get_default_tokens(v.as_ref(), def.data_type)) .collect::, CompileError>>()?; if !matches!(def.data_type, DCDataType::Domain) { @@ -481,14 +506,14 @@ fn get_object_impls( let getter_name = format_ident!("get_{}", field_name); let sub_index = sub.sub_index; let data_type = data_type_to_tokens(sub.data_type); - let pdo_mapping = pdo_mapping_to_tokens(sub.pdo_mapping); + let pdo_mapping = pdo_mappable_to_tokens(sub.pdo_mapping); let persist = sub.persist; let default_value = sub .default_value .clone() - .unwrap_or(default_default_value(sub.data_type)); - let default_tokens = get_default_tokens(&default_value, sub.data_type)?; + .or_else(|| default_default_value(sub.data_type)); + let default_tokens = get_default_tokens(default_value.as_ref(), sub.data_type)?; let access_type = access_type_to_tokens(sub.access_type.0); @@ -794,6 +819,8 @@ pub fn device_config_to_tokens(dev: &DeviceConfig) -> Result { + + paste! { + #[doc = concat!("Read a ", stringify!($type), " sub object from the SDO server\n\n")] + #[doc = concat!("This is an alias for upload_", stringify!($type), " for a more intuitive API")] + pub async fn [](&mut self, index: u16, sub: u8) -> Result<$type> { + self.[](index, sub).await + } + + #[doc = concat!("Read a ", stringify!($type), " sub object from the SDO server")] + pub async fn [](&mut self, index: u16, sub: u8) -> Result<$type> { + let data = self.upload(index, sub).await?; + if data.len() != size_of::<$type>() { + return UnexpectedSizeSnafu.fail(); + } + Ok($type::from_le_bytes(data.try_into().unwrap())) + } + + #[doc = concat!("Write a ", stringify!($type), " sub object on the SDO server\n\n")] + #[doc = concat!("This is an alias for download_", stringify!($type), " for a more intuitive API")] + pub async fn [](&mut self, index: u16, sub: u8, value: $type) -> Result<()> { + self.[](index, sub, value).await + } + + #[doc = concat!("Read a ", stringify!($type), " sub object from the SDO server")] + pub async fn [](&mut self, index: u16, sub: u8, value: $type) -> Result<()> { + let data = value.to_le_bytes(); + self.download(index, sub, &data).await + } + } + }; +} + #[derive(Debug)] /// A client for accessing a node's SDO server /// @@ -520,84 +555,60 @@ impl SdoClient { Ok(rx_data) } - /// Write to a u32 object on the SDO server - pub async fn download_u32(&mut self, index: u16, sub: u8, data: u32) -> Result<()> { - let data = data.to_le_bytes(); - self.download(index, sub, &data).await - } - - /// Alias for `download_u32` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn write_u32(&mut self, index: u16, sub: u8, data: u32) -> Result<()> { - self.download_u32(index, sub, data).await - } + access_methods!(f64); + access_methods!(f32); + access_methods!(u64); + access_methods!(u32); + access_methods!(u16); + access_methods!(u8); + access_methods!(i64); + access_methods!(i32); + access_methods!(i16); + access_methods!(i8); - /// Write to a u16 object on the SDO server - pub async fn download_u16(&mut self, index: u16, sub: u8, data: u16) -> Result<()> { - let data = data.to_le_bytes(); - self.download(index, sub, &data).await - } - - /// Alias for `download_u16` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn write_u16(&mut self, index: u16, sub: u8, data: u16) -> Result<()> { - self.download_u16(index, sub, data).await - } - - /// Write to a u16 object on the SDO server - pub async fn download_u8(&mut self, index: u16, sub: u8, data: u8) -> Result<()> { + /// Write to a TimeOfDay object on the SDO server + pub async fn download_time_of_day( + &mut self, + index: u16, + sub: u8, + data: TimeOfDay, + ) -> Result<()> { let data = data.to_le_bytes(); self.download(index, sub, &data).await } - /// Alias for `download_u8` + /// Write to a TimeOfDay object on the SDO server /// - /// This is a convenience function to allow for a more intuitive API - pub async fn write_u8(&mut self, index: u16, sub: u8, data: u8) -> Result<()> { - self.download_u8(index, sub, data).await - } - - /// Write to an i32 object on the SDO server - pub async fn download_i32(&mut self, index: u16, sub: u8, data: i32) -> Result<()> { + /// Alias for `download_time_of_day`. This is a convenience function to allow for a more intuitive API. + pub async fn write_time_of_day(&mut self, index: u16, sub: u8, data: TimeOfDay) -> Result<()> { let data = data.to_le_bytes(); self.download(index, sub, &data).await } - /// Alias for `download_i32` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn write_i32(&mut self, index: u16, sub: u8, data: i32) -> Result<()> { - self.download_i32(index, sub, data).await - } - - /// Write to an i16 object on the SDO server - pub async fn download_i16(&mut self, index: u16, sub: u8, data: i16) -> Result<()> { + /// Write to a TimeDifference object on the SDO server + pub async fn download_time_difference( + &mut self, + index: u16, + sub: u8, + data: TimeDifference, + ) -> Result<()> { let data = data.to_le_bytes(); self.download(index, sub, &data).await } - /// Alias for `download_i16` + /// Write to a TimeDifference object on the SDO server /// - /// This is a convenience function to allow for a more intuitive API - pub async fn write_i16(&mut self, index: u16, sub: u8, data: i16) -> Result<()> { - self.download_i16(index, sub, data).await - } - - /// Write to an i8 object on the SDO server - pub async fn download_i8(&mut self, index: u16, sub: u8, data: i8) -> Result<()> { + /// Alias for `download_time_difference`. This is a convenience function to allow for a more intuitive API. + pub async fn write_time_difference( + &mut self, + index: u16, + sub: u8, + data: TimeDifference, + ) -> Result<()> { let data = data.to_le_bytes(); self.download(index, sub, &data).await } - /// Alias for `download_i8` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn write_i8(&mut self, index: u16, sub: u8, data: i8) -> Result<()> { - self.download_i8(index, sub, data).await - } - /// Read a string from the SDO server pub async fn upload_utf8(&mut self, index: u16, sub: u8) -> Result { let data = self.upload(index, sub).await?; @@ -608,99 +619,40 @@ impl SdoClient { self.upload_utf8(index, sub).await } - /// Read a sub-object from the SDO server, assuming it is an u8 - pub async fn upload_u8(&mut self, index: u16, sub: u8) -> Result { - let data = self.upload(index, sub).await?; - if data.len() != 1 { - return UnexpectedSizeSnafu.fail(); - } - Ok(data[0]) - } - /// Alias for `upload_u8` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn read_u8(&mut self, index: u16, sub: u8) -> Result { - self.upload_u8(index, sub).await - } - - /// Read a sub-object from the SDO server, assuming it is an u16 - pub async fn upload_u16(&mut self, index: u16, sub: u8) -> Result { - let data = self.upload(index, sub).await?; - if data.len() != 2 { - return UnexpectedSizeSnafu.fail(); - } - Ok(u16::from_le_bytes(data.try_into().unwrap())) - } - - /// Alias for `upload_u16` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn read_u16(&mut self, index: u16, sub: u8) -> Result { - self.upload_u16(index, sub).await - } - - /// Read a sub-object from the SDO server, assuming it is an u32 - pub async fn upload_u32(&mut self, index: u16, sub: u8) -> Result { - let data = self.upload(index, sub).await?; - if data.len() != 4 { - return UnexpectedSizeSnafu.fail(); - } - Ok(u32::from_le_bytes(data.try_into().unwrap())) - } - - /// Alias for `upload_u32` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn read_u32(&mut self, index: u16, sub: u8) -> Result { - self.upload_u32(index, sub).await - } - - /// Read a sub-object from the SDO server, assuming it is an i8 - pub async fn upload_i8(&mut self, index: u16, sub: u8) -> Result { + /// Read a TimeOfDay object from the SDO server + pub async fn upload_time_of_day(&mut self, index: u16, sub: u8) -> Result { let data = self.upload(index, sub).await?; - if data.len() != 1 { - return UnexpectedSizeSnafu.fail(); - } - Ok(i8::from_le_bytes(data.try_into().unwrap())) - } - - /// Alias for `upload_i8` - /// - /// This is a convenience function to allow for a more intuitive API - pub async fn read_i8(&mut self, index: u16, sub: u8) -> Result { - self.upload_i8(index, sub).await - } - - /// Read a sub-object from the SDO server, assuming it is an i16 - pub async fn upload_i16(&mut self, index: u16, sub: u8) -> Result { - let data = self.upload(index, sub).await?; - if data.len() != 2 { - return UnexpectedSizeSnafu.fail(); + if data.len() != TimeOfDay::SIZE { + UnexpectedSizeSnafu.fail() + } else { + Ok(TimeOfDay::from_le_bytes(data.try_into().unwrap())) } - Ok(i16::from_le_bytes(data.try_into().unwrap())) } - /// Alias for `upload_i16` + /// Read a TimeOfDay object from the SDO server /// - /// This is a convenience function to allow for a more intuitive API - pub async fn read_i16(&mut self, index: u16, sub: u8) -> Result { - self.upload_i16(index, sub).await + /// Alias for `upload_time_of_day`. This is a convenience function to allow for a more intuitive + /// API. + pub async fn read_time_of_day(&mut self, index: u16, sub: u8) -> Result { + self.upload_time_of_day(index, sub).await } - /// Read a sub-object from the SDO server, assuming it is an i32 - pub async fn upload_i32(&mut self, index: u16, sub: u8) -> Result { + /// Read a TimeOfDay object from the SDO server + pub async fn upload_time_difference(&mut self, index: u16, sub: u8) -> Result { let data = self.upload(index, sub).await?; - if data.len() != 4 { - return UnexpectedSizeSnafu.fail(); + if data.len() != TimeDifference::SIZE { + UnexpectedSizeSnafu.fail() + } else { + Ok(TimeDifference::from_le_bytes(data.try_into().unwrap())) } - Ok(i32::from_le_bytes(data.try_into().unwrap())) } - /// Alias for `upload_i32` + /// Read a TimeOfDay object from the SDO server /// - /// This is a convenience function to allow for a more intuitive API - pub async fn read_i32(&mut self, index: u16, sub: u8) -> Result { - self.upload_i32(index, sub).await + /// Alias for `upload_time_of_day`. This is a convenience function to allow for a more intuitive + /// API. + pub async fn read_time_difference(&mut self, index: u16, sub: u8) -> Result { + self.upload_time_difference(index, sub).await } /// Read an object as a visible string diff --git a/zencan-common/Cargo.toml b/zencan-common/Cargo.toml index 61c85ed..3d20e64 100644 --- a/zencan-common/Cargo.toml +++ b/zencan-common/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = { version = "0.4.42", default-features = false } critical-section.workspace = true defmt = { workspace = true, optional = true } defmt-or-log = { workspace = true, default-features = false, features = ["at_least_one"] } @@ -27,7 +28,7 @@ assertables = "9.8.0" [features] default = ["socketcan", "std", "log"] -std = ["critical-section/std", "snafu/std", "dep:toml", "dep:regex", "dep:serde"] +std = ["critical-section/std", "snafu/std", "chrono/std", "dep:toml", "dep:regex", "dep:serde"] socketcan = ["dep:socketcan", "dep:tokio", "std"] defmt = ["defmt-or-log/defmt", "dep:defmt"] log = ["defmt-or-log/log"] diff --git a/zencan-common/src/device_config.rs b/zencan-common/src/device_config.rs index 2fa3b0b..02dcbf1 100644 --- a/zencan-common/src/device_config.rs +++ b/zencan-common/src/device_config.rs @@ -946,11 +946,14 @@ pub enum DataType { Int8, Int16, Int32, + Int64, #[default] UInt8, UInt16, UInt32, + UInt64, Real32, + Real64, VisibleString(usize), OctetString(usize), UnicodeString(usize), @@ -975,10 +978,13 @@ impl DataType { DataType::Int8 => 1, DataType::Int16 => 2, DataType::Int32 => 4, + DataType::Int64 => 8, DataType::UInt8 => 1, DataType::UInt16 => 2, DataType::UInt32 => 4, + DataType::UInt64 => 8, DataType::Real32 => 4, + DataType::Real64 => 8, DataType::VisibleString(size) => *size, DataType::OctetString(size) => *size, DataType::UnicodeString(size) => *size, @@ -1007,14 +1013,20 @@ impl<'de> serde::Deserialize<'de> for DataType { Ok(DataType::Int16) } else if s == "int32" { Ok(DataType::Int32) + } else if s == "int64" { + Ok(DataType::Int64) } else if s == "uint8" { Ok(DataType::UInt8) } else if s == "uint16" { Ok(DataType::UInt16) } else if s == "uint32" { Ok(DataType::UInt32) + } else if s == "uint64" { + Ok(DataType::UInt64) } else if s == "real32" { Ok(DataType::Real32) + } else if s == "real64" { + Ok(DataType::Real64) } else if let Some(caps) = re_visiblestring.captures(&s) { let size: usize = caps[1].parse().map_err(|_| { D::Error::custom(format!("Invalid size for VisibleString: {}", &caps[1])) diff --git a/zencan-common/src/lib.rs b/zencan-common/src/lib.rs index f4010d3..4371615 100644 --- a/zencan-common/src/lib.rs +++ b/zencan-common/src/lib.rs @@ -21,6 +21,7 @@ pub mod node_id; pub mod objects; pub mod pdo; pub mod sdo; +mod time_types; pub mod traits; #[cfg(feature = "socketcan")] @@ -30,6 +31,6 @@ mod socketcan; #[cfg_attr(docsrs, doc(cfg(feature = "socketcan")))] pub use socketcan::open_socketcan; -pub use node_id::NodeId; - pub use messages::{CanError, CanId, CanMessage}; +pub use node_id::NodeId; +pub use time_types::{TimeDifference, TimeOfDay}; diff --git a/zencan-common/src/objects.rs b/zencan-common/src/objects.rs index 93f503e..cf8c137 100644 --- a/zencan-common/src/objects.rs +++ b/zencan-common/src/objects.rs @@ -145,6 +145,12 @@ pub enum DataType { /// An arbitrary byte access type for e.g. data streams, or large chunks of /// data. Size is typically not known at build time. Domain = 0xf, + /// A 64-bit floating point value + Real64 = 0x11, + /// A signed 64-bit integer + Int64 = 0x15, + /// An unsigned 64-bit integer + UInt64 = 0x1b, /// A contained for an unrecognized data type value Other(u16), } diff --git a/zencan-common/src/time_types.rs b/zencan-common/src/time_types.rs new file mode 100644 index 0000000..409663b --- /dev/null +++ b/zencan-common/src/time_types.rs @@ -0,0 +1,177 @@ +//! Data types for TimeOfDay and TimeDifference fields + +use chrono::{Datelike, NaiveDate, NaiveTime, TimeDelta, Timelike}; +use core::time::Duration; +use snafu::Snafu; + +const MILLIS_PER_DAY: u64 = 86_400_000; + +#[derive(Clone, Copy, Debug, Snafu)] +pub enum TimeCreateError { + /// The provided time is before the epoch and cannot be represented + PreEpoch, + /// The provided is too far into the future to be represented by TimeOfDay + OutOfRange, + /// The provided date is invalid + /// + /// This likely means that the date you specified does not exist or is outside the range which + /// can be be represented by chrono::NaiveDate + InvalidDate, +} + +/// Represents a time in 48-bits, as stored in TimeOfDay objects +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TimeOfDay(TimeDifference); + +impl TimeOfDay { + /// The size of a TimeOfDay object in bytes as stored in the object dict + pub const SIZE: usize = 6; + + /// A zero-inintialized TimeOfDay corresponding to the TimeOfDay epoch of 1984-01-01 + pub const EPOCH: TimeOfDay = TimeOfDay(TimeDifference { ms: 0, days: 0 }); + // TimeOfDay objects are encoded with reference to 1984-01-01 + const CHRONO_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1984, 1, 1).unwrap(); + + /// Create a new TimeOfDay + /// + /// # Arguments + /// - `days`: The number of days since January 1, 1984 + /// - `ms`: The number of milliseconds after midnight + pub fn new(days: u16, ms: u32) -> Self { + Self(TimeDifference::new(days, ms)) + } + + /// Create a TimeOfDay corresponding to the provided date and time + pub fn from_ymd_hms_ms( + year: u32, + month: u32, + day: u32, + hour: u32, + min: u32, + sec: u32, + milli: u32, + ) -> Result { + let chrono_date = NaiveDate::from_ymd_opt(year as i32, month, day) + .ok_or(InvalidDateSnafu.build())? + .and_hms_milli_opt(hour, min, sec, milli) + .ok_or(InvalidDateSnafu.build())?; + + let delta = chrono_date - const { Self::CHRONO_EPOCH.and_hms_opt(0, 0, 0).unwrap() }; + let days = delta.num_days(); + let ms = (delta - TimeDelta::days(days)).num_milliseconds(); + if days < 0 { + PreEpochSnafu.fail() + } else if days > u16::MAX as i64 { + OutOfRangeSnafu.fail() + } else { + Ok(Self::new(days as u16, ms as u32)) + } + } + + /// Create a TimeOfDay from little endian bytes + pub fn from_le_bytes(bytes: [u8; 6]) -> Self { + Self(TimeDifference::from_le_bytes(bytes)) + } + + /// Get the little endian byte representation of the time of day + pub fn to_le_bytes(&self) -> [u8; 6] { + self.0.to_le_bytes() + } + + /// Get the date represented + /// + /// Returns (year, month, day) + pub fn date_ymd(&self) -> (u32, u32, u32) { + let date = Self::CHRONO_EPOCH + self.0.as_chrono_delta(); + (date.year() as u32, date.month(), date.day()) + } + + /// Get the date as number of days since 1984-01-01 + pub fn days(&self) -> u16 { + self.0.days + } + + /// Get the time of day as (hour, min, sec, millis) + pub fn time_hmsm(&self) -> (u32, u32, u32, u32) { + let sec = self.0.ms / 1000; + let nanos = (self.0.ms % 1000) * 1000; + let t = NaiveTime::from_num_seconds_from_midnight_opt(sec, nanos).unwrap(); + (t.hour(), t.minute(), t.second(), t.nanosecond() / 1000) + } + + /// Get the time of day as the number of milliseconds since midnight + pub fn time_millis(&self) -> u32 { + self.0.ms + } + + /// Get the time of of day as a [`Duration`] since midnight + pub fn time_duration(&self) -> Duration { + Duration::from_millis(self.time_millis() as u64) + } + + /// Get the total number of milliseconds since 1984-01-01 + pub fn total_millis(&self) -> u64 { + self.0.total_millis() + } + + /// Get the time represented as a SystemTime + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn as_system_time(&self) -> std::time::SystemTime { + use std::time::SystemTime; + + // System time is relative to the UNIX_EPOCH of 1970-01-01 + const UNIX_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + let epoch_delta_millis = (Self::CHRONO_EPOCH - UNIX_EPOCH).num_milliseconds() as u64; + SystemTime::UNIX_EPOCH + self.0.as_duration() + Duration::from_millis(epoch_delta_millis) + } +} + +/// Represents a duration of time in 48-bits, as stored in TimeDifference objects +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TimeDifference { + ms: u32, + days: u16, +} + +impl TimeDifference { + /// The size of a TimeDifference object in bytes as stored in the object dict + pub const SIZE: usize = 6; + + /// A zero time difference + pub const ZERO: TimeDifference = TimeDifference { ms: 0, days: 0 }; + + /// Create a new time difference from the raw u32 value + pub const fn new(days: u16, ms: u32) -> Self { + Self { ms, days } + } + + /// Create a TimeOfDay from little endian bytes + pub fn from_le_bytes(bytes: [u8; 6]) -> Self { + let ms = u32::from_le_bytes(bytes[0..4].try_into().unwrap()); + let days = u16::from_le_bytes(bytes[4..6].try_into().unwrap()); + Self::new(days, ms) + } + + /// Return the little endian byte representation of the TimeDifference + pub fn to_le_bytes(&self) -> [u8; 6] { + let mut bytes = [0; 6]; + bytes[0..4].copy_from_slice(&self.ms.to_le_bytes()); + bytes[4..6].copy_from_slice(&self.days.to_le_bytes()); + bytes + } + + /// Get the time duration as milliseconds + pub fn total_millis(&self) -> u64 { + self.days as u64 * MILLIS_PER_DAY + self.ms as u64 + } + + /// Convert to a [`core::time::Duration`] + pub fn as_duration(&self) -> Duration { + Duration::from_millis(self.days as u64 * MILLIS_PER_DAY + self.ms as u64) + } + + pub(crate) fn as_chrono_delta(&self) -> chrono::TimeDelta { + chrono::TimeDelta::milliseconds(self.days as i64 * MILLIS_PER_DAY as i64 + self.ms as i64) + } +} diff --git a/zencan-node/src/object_dict/sub_objects.rs b/zencan-node/src/object_dict/sub_objects.rs index 6318a4f..5d6aa32 100644 --- a/zencan-node/src/object_dict/sub_objects.rs +++ b/zencan-node/src/object_dict/sub_objects.rs @@ -2,7 +2,7 @@ use core::cell::UnsafeCell; -use zencan_common::{sdo::AbortCode, AtomicCell}; +use zencan_common::{sdo::AbortCode, AtomicCell, TimeDifference, TimeOfDay}; /// Allow transparent byte level access to a sub object pub trait SubObjectAccess: Sync + Send { @@ -129,7 +129,7 @@ impl Default for ScalarField { } macro_rules! impl_scalar_field { - ($rust_type: ty, $data_type: ty) => { + ($rust_type: ty) => { impl ScalarField<$rust_type> { /// Create a new ScalarField with the given value pub const fn new(value: $rust_type) -> Self { @@ -169,13 +169,16 @@ macro_rules! impl_scalar_field { }; } -impl_scalar_field!(u8, DataType::UInt8); -impl_scalar_field!(u16, DataType::UInt16); -impl_scalar_field!(u32, DataType::UInt32); -impl_scalar_field!(i8, DataType::Int8); -impl_scalar_field!(i16, DataType::Int16); -impl_scalar_field!(i32, DataType::Int32); -impl_scalar_field!(f32, DataType::Float); +impl_scalar_field!(u8); +impl_scalar_field!(u16); +impl_scalar_field!(u32); +impl_scalar_field!(u64); +impl_scalar_field!(i8); +impl_scalar_field!(i16); +impl_scalar_field!(i32); +impl_scalar_field!(i64); +impl_scalar_field!(f32); +impl_scalar_field!(f64); // bool doesn't support from_le_bytes so it needs a special implementation impl SubObjectAccess for ScalarField { @@ -202,6 +205,84 @@ impl SubObjectAccess for ScalarField { } } +impl SubObjectAccess for ScalarField { + fn read(&self, offset: usize, buf: &mut [u8]) -> Result { + let value = self.value.load(); + let bytes = value.to_le_bytes(); + if offset < bytes.len() { + let read_len = buf.len().min(bytes.len() - offset); + buf[0..read_len].copy_from_slice(&bytes[offset..offset + read_len]); + Ok(read_len) + } else { + Ok(0) + } + } + + fn read_size(&self) -> usize { + 6 + } + + fn write(&self, data: &[u8]) -> Result<(), AbortCode> { + let value = TimeDifference::from_le_bytes(data.try_into().map_err(|_| { + if data.len() < 6 { + AbortCode::DataTypeMismatchLengthLow + } else { + AbortCode::DataTypeMismatchLengthHigh + } + })?); + self.value.store(value); + Ok(()) + } +} + +impl ScalarField { + /// Create a new ScalarField with the given value + pub const fn new(value: TimeDifference) -> Self { + Self { + value: AtomicCell::new(value), + } + } +} + +impl SubObjectAccess for ScalarField { + fn read(&self, offset: usize, buf: &mut [u8]) -> Result { + let value = self.value.load(); + let bytes = value.to_le_bytes(); + if offset < bytes.len() { + let read_len = buf.len().min(bytes.len() - offset); + buf[0..read_len].copy_from_slice(&bytes[offset..offset + read_len]); + Ok(read_len) + } else { + Ok(0) + } + } + + fn read_size(&self) -> usize { + 6 + } + + fn write(&self, data: &[u8]) -> Result<(), AbortCode> { + let value = TimeOfDay::from_le_bytes(data.try_into().map_err(|_| { + if data.len() < 6 { + AbortCode::DataTypeMismatchLengthLow + } else { + AbortCode::DataTypeMismatchLengthHigh + } + })?); + self.value.store(value); + Ok(()) + } +} + +impl ScalarField { + /// Create a new ScalarField with the given value + pub const fn new(value: TimeOfDay) -> Self { + Self { + value: AtomicCell::new(value), + } + } +} + /// A sub object which contains a fixed-size byte array /// /// This is the data storage backing for all string types