From 6a682fb59206e996550e5985b48adfb816e52dd9 Mon Sep 17 00:00:00 2001 From: Jeff McBride Date: Thu, 8 Jan 2026 09:41:00 -0800 Subject: [PATCH 1/2] Configurable autostart default in device config --- .../device_configs/example1.toml | 1 + .../example2_bootloader_app.toml | 2 + .../device_configs/example3_bootloader.toml | 2 + integration_tests/tests/codegen_tests.rs | 15 +++ zencan-common/src/device_config.rs | 47 +++++++++- zencan-common/src/lss.rs | 2 +- zencan-node/src/node.rs | 93 ++++++++++++++++++- 7 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 integration_tests/tests/codegen_tests.rs diff --git a/integration_tests/device_configs/example1.toml b/integration_tests/device_configs/example1.toml index e2cef72..e5c7720 100644 --- a/integration_tests/device_configs/example1.toml +++ b/integration_tests/device_configs/example1.toml @@ -1,6 +1,7 @@ device_name = "Example 1" hardware_version = "v1.2.3" software_version = "v2.1.0" +autostart = "disabled" [identity] vendor_id = 1234 diff --git a/integration_tests/device_configs/example2_bootloader_app.toml b/integration_tests/device_configs/example2_bootloader_app.toml index 1cc8b5e..59ca2e7 100644 --- a/integration_tests/device_configs/example2_bootloader_app.toml +++ b/integration_tests/device_configs/example2_bootloader_app.toml @@ -1,6 +1,8 @@ # A simple system with a bootloader device_name = "Bootload Example" +autostart = "enabled" + [pdos] num_rpdo = 4 num_tpdo = 4 diff --git a/integration_tests/device_configs/example3_bootloader.toml b/integration_tests/device_configs/example3_bootloader.toml index ae9be4f..dd5a616 100644 --- a/integration_tests/device_configs/example3_bootloader.toml +++ b/integration_tests/device_configs/example3_bootloader.toml @@ -1,6 +1,8 @@ # A simple config for a bootloader device_name = "Bootload Example" +autostart = "unsupported" + [pdos] num_rpdo = 4 num_tpdo = 4 diff --git a/integration_tests/tests/codegen_tests.rs b/integration_tests/tests/codegen_tests.rs new file mode 100644 index 0000000..12fd7c8 --- /dev/null +++ b/integration_tests/tests/codegen_tests.rs @@ -0,0 +1,15 @@ +//! Tests that only validate the generated code +//! + +use integration_tests::{object_dict1, object_dict2, object_dict3}; +use zencan_node::object_dict::find_object; + +#[test] +fn test_autostart_defaults() { + // Example 1 should default to disabled + assert_eq!(0, object_dict1::OBJECT5000.get_value()); + // Example 2 should default to enabled + assert_eq!(1, object_dict2::OBJECT5000.get_value()); + // Example 3 should have no object 5000 + assert!(find_object(&object_dict3::OD_TABLE, 0x5000).is_none()) +} diff --git a/zencan-common/src/device_config.rs b/zencan-common/src/device_config.rs index 02dcbf1..46f8bee 100644 --- a/zencan-common/src/device_config.rs +++ b/zencan-common/src/device_config.rs @@ -10,8 +10,13 @@ //! device_name = "can-io" //! software_version = "v0.0.1" //! hardware_version = "rev1" +//! +//! # How frequently to send heartbeat messages (ms) //! heartbeat_period = 1000 //! +//! # Sets the default value of the Auto-start object +//! autostart = "enabled" +//! //! # Define 3 out of 4 device unique identifiers. These define the application/device, the fourth is //! # the serial number, which must be provided at run-time by the application. //! [identity] @@ -172,7 +177,7 @@ pub enum LoadError { } fn mandatory_objects(config: &DeviceConfig) -> Vec { - vec![ + let mut objects = vec![ ObjectDefinition { index: 0x1000, parameter_name: "Device Type".to_string(), @@ -300,19 +305,29 @@ fn mandatory_objects(config: &DeviceConfig) -> Vec { ], }), }, - ObjectDefinition { + ]; + + let (create_autostart, default) = match config.autostart { + AutoStartConfig::Disabled => (true, 0), + AutoStartConfig::Enabled => (true, 1), + AutoStartConfig::Unsupported => (false, 0), + }; + if create_autostart { + objects.push(ObjectDefinition { index: 0x5000, parameter_name: "Auto Start".to_string(), application_callback: false, object: Object::Var(VarDefinition { data_type: DataType::UInt8, access_type: AccessType::Rw.into(), - default_value: None, + default_value: Some(DefaultValue::Integer(default)), pdo_mapping: PdoMappable::None, persist: true, }), - }, - ] + }); + } + + objects } fn pdo_objects(num_rpdo: usize, num_tpdo: usize) -> Vec { @@ -519,6 +534,19 @@ fn default_true() -> bool { true } +/// Options for Autostart config +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AutoStartConfig { + /// Autostart is supported, but defaults to off + #[default] + Disabled, + /// Autostart defaults to enabled + Enabled, + /// Autostart is not supported -- no 0x5000 object will be created + Unsupported, +} + /// Represents the configuration parameters for a single PDO #[derive(Clone, Debug, Deserialize, PartialEq)] pub struct PdoDefaultConfig { @@ -656,6 +684,15 @@ pub struct DeviceConfig { /// The name describing the type of device (e.g. a model) pub device_name: String, + /// Configures support for the AutoStart (0x5000) object and its default value + /// + /// Allowed values: + /// - 'unsupported': No autostart object is created + /// - 'disabled': An autostart object is created, and it defaults to disabled + /// - 'enabled': An autostart object is created, and it defaults to enabled + #[serde(default)] + pub autostart: AutoStartConfig, + /// Enables object storage commands (object 0x1010) /// /// Default: true diff --git a/zencan-common/src/lss.rs b/zencan-common/src/lss.rs index 5ae591d..4db71f1 100644 --- a/zencan-common/src/lss.rs +++ b/zencan-common/src/lss.rs @@ -600,7 +600,7 @@ impl LssState { /// number must be set by the application to a unique value. This can be done, e.g., using a UID /// register on the MCU, or by loading a previously programmed value from flash. It is important /// that each device on the bus have a unique identity. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct LssIdentity { /// A number indicating the vendor of the device pub vendor_id: u32, diff --git a/zencan-node/src/node.rs b/zencan-node/src/node.rs index 72cb26e..31a70a5 100644 --- a/zencan-node/src/node.rs +++ b/zencan-node/src/node.rs @@ -159,7 +159,7 @@ impl<'a> Node<'a> { let message_count = 0; let sdo_server = SdoServer::new(); let lss_slave = LssSlave::new(LssConfig { - identity: read_identity(od).unwrap(), + identity: read_identity(od).unwrap_or_default(), node_id, store_supported: false, }); @@ -463,7 +463,7 @@ impl<'a> Node<'a> { fn boot_up(&mut self) { // Reset the LSS slave with the new ID self.lss_slave.update_config(LssConfig { - identity: read_identity(self.od).unwrap(), + identity: read_identity(self.od).unwrap_or_default(), node_id: self.node_id, store_supported: self.callbacks.store_node_config.is_some(), }); @@ -488,3 +488,92 @@ impl<'a> Node<'a> { } } } + +#[cfg(test)] +mod tests { + use zencan_common::{ + nmt::NmtState, + objects::{ObjectCode, SubInfo}, + CanMessage, NodeId, + }; + + use crate::{ + object_dict::{ODEntry, ProvidesSubObjects, ScalarField, SubObjectAccess}, + priority_queue::PriorityQueue, + Callbacks, Node, NodeMbox, NodeState, + }; + + struct AutoStartObject { + value: ScalarField, + } + + impl AutoStartObject { + pub fn new(value: u8) -> Self { + Self { + value: ScalarField::::new(value), + } + } + } + impl ProvidesSubObjects for AutoStartObject { + fn get_sub_object(&self, sub: u8) -> Option<(SubInfo, &dyn SubObjectAccess)> { + match sub { + 0 => Some((SubInfo::new_u8(), &self.value)), + _ => None, + } + } + + fn object_code(&self) -> ObjectCode { + ObjectCode::Var + } + } + + #[test] + fn test_node_autostart_enabled() { + let object5000 = Box::leak(Box::new(AutoStartObject::new(1))); + let od_table = Box::leak(Box::new([ODEntry { + index: 0x5000, + data: object5000, + }])); + + let tx_queue = Box::leak(Box::new(PriorityQueue::<4, CanMessage>::new())); + let sdo_buffer = Box::leak(Box::new([0u8; 100])); + let mbox = Box::leak(Box::new(NodeMbox::new(&[], &[], tx_queue, sdo_buffer))); + let state = Box::leak(Box::new(NodeState::new(&[], &[]))); + + let mut node = Node::new( + NodeId::new(1).unwrap(), + Callbacks::default(), + mbox, + state, + od_table, + ); + + node.process(0); + assert_eq!(NmtState::Operational, node.nmt_state()); + } + + #[test] + fn test_node_autostart_disabled() { + let object5000 = Box::leak(Box::new(AutoStartObject::new(0))); + let od_table = Box::leak(Box::new([ODEntry { + index: 0x5000, + data: object5000, + }])); + + let tx_queue = Box::leak(Box::new(PriorityQueue::<4, CanMessage>::new())); + let sdo_buffer = Box::leak(Box::new([0u8; 100])); + let mbox = Box::leak(Box::new(NodeMbox::new(&[], &[], tx_queue, sdo_buffer))); + let state = Box::leak(Box::new(NodeState::new(&[], &[]))); + + let mut node = Node::new( + NodeId::new(1).unwrap(), + Callbacks::default(), + mbox, + state, + od_table, + ); + + node.process(0); + assert_eq!(NmtState::PreOperational, node.nmt_state()); + } +} From 9afdf8962390f36d4c1e5ecab5f4c03ae05fb9fa Mon Sep 17 00:00:00 2001 From: Jeff McBride Date: Thu, 8 Jan 2026 10:05:09 -0800 Subject: [PATCH 2/2] [node] Update changelog --- zencan-node/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zencan-node/CHANGELOG.md b/zencan-node/CHANGELOG.md index a6cee70..7772d9f 100644 --- a/zencan-node/CHANGELOG.md +++ b/zencan-node/CHANGELOG.md @@ -6,7 +6,8 @@ Human-friendly documentation of releases and what's changed in them for the zenc ### Added -- Support for TimeOfDay, TimeDifference, f64, u64, i64 object data types. +- Support for TimeOfDay, TimeDifference, f64, u64, i64 object data types (#42). +- Device config `autostart` field for configuring 0x5000 object default value (#47). ### Fixed