From ebb3d275bf348fd10f03e20171059c0a911fd8b2 Mon Sep 17 00:00:00 2001 From: Jeff McBride Date: Fri, 2 Jan 2026 06:49:54 -0800 Subject: [PATCH] PDO update restricted based on NMT state --- integration_tests/tests/nmt_test.rs | 2 +- zencan-build/src/codegen.rs | 6 +- zencan-client/src/bus_manager/bus_manager.rs | 3 +- zencan-client/src/nmt_master.rs | 3 +- zencan-common/src/lib.rs | 1 + zencan-common/src/messages.rs | 51 +----- zencan-common/src/nmt.rs | 51 ++++++ zencan-node/src/lib.rs | 2 +- zencan-node/src/node.rs | 59 ++++--- zencan-node/src/node_mbox.rs | 4 +- zencan-node/src/node_state.rs | 65 ++++---- zencan-node/src/pdo.rs | 159 ++++++++++++++----- 12 files changed, 245 insertions(+), 161 deletions(-) create mode 100644 zencan-common/src/nmt.rs diff --git a/integration_tests/tests/nmt_test.rs b/integration_tests/tests/nmt_test.rs index d19998c..2e1e43d 100644 --- a/integration_tests/tests/nmt_test.rs +++ b/integration_tests/tests/nmt_test.rs @@ -1,5 +1,5 @@ use zencan_client::nmt_master::NmtMaster; -use zencan_common::{messages::NmtState, NodeId}; +use zencan_common::{nmt::NmtState, NodeId}; use zencan_node::{Callbacks, Node}; use integration_tests::prelude::*; diff --git a/zencan-build/src/codegen.rs b/zencan-build/src/codegen.rs index c4402cd..d2d7414 100644 --- a/zencan-build/src/codegen.rs +++ b/zencan-build/src/codegen.rs @@ -21,7 +21,7 @@ fn pdo_init_tokens(cfg: Option<&PdoDefaultConfig>) -> TokenStream { let mappings: Vec = mappings.iter().map(|m| m.to_object_value()).collect(); quote! { - Pdo::new_with_defaults(&OD_TABLE, &PdoDefaults::new( + Pdo::new_with_defaults(&OD_TABLE, &NODE_STATE, &PdoDefaults::new( #cob_id, #extended, #add_node_id, @@ -32,7 +32,7 @@ fn pdo_init_tokens(cfg: Option<&PdoDefaultConfig>) -> TokenStream { )) } } else { - quote! { Pdo::new_with_defaults(&OD_TABLE, &PdoDefaults::DEFAULT) } + quote! { Pdo::new_with_defaults(&OD_TABLE, &NODE_STATE, &PdoDefaults::DEFAULT) } } } @@ -829,7 +829,7 @@ pub fn device_config_to_tokens(dev: &DeviceConfig) -> Result for CanMessage { } } -/// Possible NMT states for a node -#[derive(Copy, Clone, Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[repr(u8)] -pub enum NmtState { - /// Bootup - /// - /// A node never remains in this state, as all nodes should transition automatically into PreOperational - Bootup = 0, - /// Node has been stopped - Stopped = 4, - /// Normal operational state - Operational = 5, - /// Node is awaiting command to enter operation - PreOperational = 127, -} - -impl core::fmt::Display for NmtState { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - NmtState::Bootup => write!(f, "Bootup"), - NmtState::Stopped => write!(f, "Stopped"), - NmtState::Operational => write!(f, "Operational"), - NmtState::PreOperational => write!(f, "PreOperational"), - } - } -} - -#[derive(Clone, Copy, Debug)] -/// An error for [`NmtState::try_from()`] -pub struct InvalidNmtStateError(u8); - -impl TryFrom for NmtState { - type Error = InvalidNmtStateError; - - /// Attempt to convert a u8 to an NmtState enum - /// - /// Fails with BadNmtStateError if value is not a valid state - fn try_from(value: u8) -> Result { - use NmtState::*; - match value { - x if x == Bootup as u8 => Ok(Bootup), - x if x == Stopped as u8 => Ok(Stopped), - x if x == Operational as u8 => Ok(Operational), - x if x == PreOperational as u8 => Ok(PreOperational), - _ => Err(InvalidNmtStateError(value)), - } - } -} - /// A Heartbeat message #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/zencan-common/src/nmt.rs b/zencan-common/src/nmt.rs new file mode 100644 index 0000000..2a12bbf --- /dev/null +++ b/zencan-common/src/nmt.rs @@ -0,0 +1,51 @@ +//! Definitions for the NMT protocol + +/// Possible NMT states for a node +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum NmtState { + /// Bootup + /// + /// A node never remains in this state, as all nodes should transition automatically into PreOperational + Bootup = 0, + /// Node has been stopped + Stopped = 4, + /// Normal operational state + Operational = 5, + /// Node is awaiting command to enter operation + PreOperational = 127, +} + +impl core::fmt::Display for NmtState { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + NmtState::Bootup => write!(f, "Bootup"), + NmtState::Stopped => write!(f, "Stopped"), + NmtState::Operational => write!(f, "Operational"), + NmtState::PreOperational => write!(f, "PreOperational"), + } + } +} + +#[derive(Clone, Copy, Debug)] +/// An error for [`NmtState::try_from()`] +pub struct InvalidNmtStateError(pub u8); + +impl TryFrom for NmtState { + type Error = InvalidNmtStateError; + + /// Attempt to convert a u8 to an NmtState enum + /// + /// Fails with BadNmtStateError if value is not a valid state + fn try_from(value: u8) -> Result { + use NmtState::*; + match value { + x if x == Bootup as u8 => Ok(Bootup), + x if x == Stopped as u8 => Ok(Stopped), + x if x == Operational as u8 => Ok(Operational), + x if x == PreOperational as u8 => Ok(PreOperational), + _ => Err(InvalidNmtStateError(value)), + } + } +} diff --git a/zencan-node/src/lib.rs b/zencan-node/src/lib.rs index 264ca6f..dffe3fc 100644 --- a/zencan-node/src/lib.rs +++ b/zencan-node/src/lib.rs @@ -220,7 +220,7 @@ pub use bootloader::{BootloaderInfo, BootloaderSection, BootloaderSectionCallbac pub use common::open_socketcan; pub use node::{Callbacks, Node}; pub use node_mbox::NodeMbox; -pub use node_state::{NodeState, NodeStateAccess}; +pub use node_state::NodeState; pub use persist::{restore_stored_comm_objects, restore_stored_objects}; pub use sdo_server::SDO_BUFFER_SIZE; diff --git a/zencan-node/src/node.rs b/zencan-node/src/node.rs index 64e9a04..72cb26e 100644 --- a/zencan-node/src/node.rs +++ b/zencan-node/src/node.rs @@ -6,18 +6,19 @@ use core::{convert::Infallible, sync::atomic::Ordering}; use zencan_common::{ constants::object_ids, lss::LssIdentity, - messages::{ - CanId, CanMessage, Heartbeat, NmtCommandSpecifier, NmtState, ZencanMessage, LSS_RESP_ID, - }, + messages::{CanId, CanMessage, Heartbeat, NmtCommandSpecifier, ZencanMessage, LSS_RESP_ID}, + nmt::NmtState, NodeId, }; +use crate::sdo_server::SdoServer; use crate::{ lss_slave::{LssConfig, LssSlave}, node_mbox::NodeMbox, + node_state::NmtStateAccess as _, object_dict::{find_object, ODEntry}, + NodeState, }; -use crate::{node_state::NodeStateAccess, sdo_server::SdoServer}; use defmt_or_log::{debug, info}; @@ -124,13 +125,12 @@ fn read_autostart(od: &[ODEntry]) -> Option { #[allow(missing_debug_implementations)] pub struct Node<'a> { node_id: NodeId, - nmt_state: NmtState, sdo_server: SdoServer<'a>, lss_slave: LssSlave, message_count: u32, - od: &'a [ODEntry<'a>], - mbox: &'a NodeMbox, - state: &'a dyn NodeStateAccess, + od: &'static [ODEntry<'static>], + mbox: &'static NodeMbox, + state: &'static NodeState<'static>, reassigned_node_id: Option, next_heartbeat_time_us: u64, heartbeat_period_ms: u16, @@ -152,9 +152,9 @@ impl<'a> Node<'a> { pub fn new( node_id: NodeId, callbacks: Callbacks<'a>, - mbox: &'a NodeMbox, - state: &'a dyn NodeStateAccess, - od: &'a [ODEntry<'a>], + mbox: &'static NodeMbox, + state: &'static NodeState<'static>, + od: &'static [ODEntry<'static>], ) -> Self { let message_count = 0; let sdo_server = SdoServer::new(); @@ -163,7 +163,6 @@ impl<'a> Node<'a> { node_id, store_supported: false, }); - let nmt_state = NmtState::Bootup; let reassigned_node_id = None; // Storage command is supported if the application provides a callback @@ -183,7 +182,6 @@ impl<'a> Node<'a> { let mut node = Self { node_id, callbacks, - nmt_state, sdo_server, lss_slave, message_count, @@ -235,10 +233,10 @@ impl<'a> Node<'a> { let mut update_flag = false; if let Some(new_node_id) = self.reassigned_node_id.take() { self.node_id = new_node_id; - self.nmt_state = NmtState::Bootup; + self.state.set_nmt_state(NmtState::Bootup); } - if self.nmt_state == NmtState::Bootup { + if self.nmt_state() == NmtState::Bootup { // Set state before calling boot_up, so the heartbeat state is correct self.enter_preoperational(); self.boot_up(); @@ -322,7 +320,7 @@ impl<'a> Node<'a> { } } - if self.nmt_state == NmtState::Operational { + if self.nmt_state() == NmtState::Operational { // check if a sync has been received let sync = self.mbox.read_sync_flag(); @@ -332,7 +330,7 @@ impl<'a> Node<'a> { // possible when it has nothing to do, so it can be called frequently with little cost. let global_trigger = self.state.object_flag_sync().toggle(); - for pdo in self.state.get_tpdos() { + for pdo in self.state.tpdos() { if !(pdo.valid()) { continue; } @@ -348,11 +346,11 @@ impl<'a> Node<'a> { } } - for pdo in self.state.get_tpdos() { + for pdo in self.state.tpdos() { pdo.clear_events(); } - for rpdo in self.state.get_rpdos() { + for rpdo in self.state.rpdos() { if !rpdo.valid() { continue; } @@ -371,7 +369,7 @@ impl<'a> Node<'a> { } fn handle_nmt_command(&mut self, cmd: NmtCommandSpecifier) { - let prev_state = self.nmt_state; + let prev_state = self.nmt_state(); match cmd { NmtCommandSpecifier::Start => self.enter_operational(), @@ -383,7 +381,8 @@ impl<'a> Node<'a> { debug!( "NMT state changed from {:?} to {:?}", - prev_state, self.nmt_state + prev_state, + self.nmt_state() ); } @@ -394,7 +393,7 @@ impl<'a> Node<'a> { /// Get the current NMT state of the node pub fn nmt_state(&self) -> NmtState { - self.nmt_state + self.state.nmt_state() } /// Get the number of received messages @@ -419,21 +418,21 @@ impl<'a> Node<'a> { } fn enter_operational(&mut self) { - self.nmt_state = NmtState::Operational; + self.state.set_nmt_state(NmtState::Operational); if let Some(cb) = &mut self.callbacks.enter_operational { (*cb)(self.od); } } fn enter_stopped(&mut self) { - self.nmt_state = NmtState::Stopped; + self.state.set_nmt_state(NmtState::Stopped); if let Some(cb) = &mut self.callbacks.enter_stopped { (*cb)(self.od); } } fn enter_preoperational(&mut self) { - self.nmt_state = NmtState::PreOperational; + self.state.set_nmt_state(NmtState::PreOperational); if let Some(cb) = &mut self.callbacks.enter_preoperational { (*cb)(self.od); } @@ -441,24 +440,24 @@ impl<'a> Node<'a> { fn reset_app(&mut self) { // TODO: All objects should get reset to their defaults, but that isn't yet supported - for pdo in self.state.get_rpdos().iter().chain(self.state.get_tpdos()) { + for pdo in self.state.rpdos().iter().chain(self.state.tpdos()) { pdo.init_defaults(self.node_id); } if let Some(reset_app_cb) = &mut self.callbacks.reset_app { (*reset_app_cb)(self.od); } - self.nmt_state = NmtState::Bootup; + self.state.set_nmt_state(NmtState::Bootup); } fn reset_comm(&mut self) { - for pdo in self.state.get_rpdos().iter().chain(self.state.get_tpdos()) { + for pdo in self.state.rpdos().iter().chain(self.state.tpdos()) { pdo.init_defaults(self.node_id); } if let Some(reset_comms_cb) = &mut self.callbacks.reset_comms { (*reset_comms_cb)(self.od); } - self.nmt_state = NmtState::Bootup; + self.state.set_nmt_state(NmtState::Bootup); } fn boot_up(&mut self) { @@ -482,7 +481,7 @@ impl<'a> Node<'a> { let heartbeat = Heartbeat { node: node_id.raw(), toggle: false, - state: self.nmt_state, + state: self.nmt_state(), }; self.send_message(heartbeat.into()); self.next_heartbeat_time_us += (self.heartbeat_period_ms as u64) * 1000; diff --git a/zencan-node/src/node_mbox.rs b/zencan-node/src/node_mbox.rs index be266d4..f0362b8 100644 --- a/zencan-node/src/node_mbox.rs +++ b/zencan-node/src/node_mbox.rs @@ -32,8 +32,8 @@ impl CanMessageQueue for PriorityQueue { /// Incoming messages should be passed to [NodeMbox::store_message]. #[allow(missing_debug_implementations)] pub struct NodeMbox { - rx_pdos: &'static [Pdo], - tx_pdos: &'static [Pdo], + rx_pdos: &'static [Pdo<'static>], + tx_pdos: &'static [Pdo<'static>], /// ID used for transmitting SDO server responses sdo_tx_cob_id: AtomicCell>, /// ID used for receiving SDO server requests diff --git a/zencan-node/src/node_state.rs b/zencan-node/src/node_state.rs index 50877cf..787c344 100644 --- a/zencan-node/src/node_state.rs +++ b/zencan-node/src/node_state.rs @@ -1,19 +1,20 @@ //! Implements node state struct +use zencan_common::nmt::NmtState; +use zencan_common::AtomicCell; + use crate::object_dict::ObjectFlagSync; use crate::pdo::Pdo; use crate::storage::StorageContext; -/// A trait by which NodeState is accessed -pub trait NodeStateAccess: Sync + Send { - /// Get the receive PDO objects - fn get_rpdos(&self) -> &[Pdo]; - /// Get the transmit PDO objects - fn get_tpdos(&self) -> &[Pdo]; - /// Get the PDO flag sync object - fn object_flag_sync(&self) -> &ObjectFlagSync; - /// Get the storage context object - fn storage_context(&self) -> &StorageContext; +pub trait NmtStateAccess: Send + Sync { + fn nmt_state(&self) -> NmtState; +} + +impl NmtStateAccess for AtomicCell { + fn nmt_state(&self) -> NmtState { + self.load() + } } /// The NodeState provides config-dependent storage to the [`Node`](crate::Node) object @@ -24,19 +25,27 @@ pub trait NodeStateAccess: Sync + Send { #[allow(missing_debug_implementations)] pub struct NodeState<'a> { /// Pdo control objects for receive PDOs - rpdos: &'a [Pdo], + rpdos: &'a [Pdo<'a>], /// Pdo control objects for transmit PDOs - tpdos: &'a [Pdo], + tpdos: &'a [Pdo<'a>], /// A global flag used by all objects to synchronize their event flag A/B swapping object_flag_sync: ObjectFlagSync, /// State shared between the [`StorageCommandObject`](crate::storage::StorageCommandObject) and /// [`Node`](crate::node::Node) for indicating when a store objects command has been recieved. storage_context: StorageContext, + /// Global storage for the NMT state + nmt_state: AtomicCell, +} + +impl NmtStateAccess for NodeState<'_> { + fn nmt_state(&self) -> NmtState { + self.nmt_state.load() + } } impl<'a> NodeState<'a> { /// Create a new NodeState object - pub const fn new(rpdos: &'a [Pdo], tpdos: &'a [Pdo]) -> Self { + pub const fn new(rpdos: &'a [Pdo<'a>], tpdos: &'a [Pdo<'a>]) -> Self { let object_flag_sync = ObjectFlagSync::new(); let storage_context = StorageContext::new(); @@ -45,46 +54,36 @@ impl<'a> NodeState<'a> { tpdos, object_flag_sync, storage_context, + nmt_state: AtomicCell::new(NmtState::Bootup), } } /// Access the RPDOs as a const function - pub const fn rpdos(&self) -> &'a [Pdo] { + pub const fn rpdos(&self) -> &'a [Pdo<'a>] { self.rpdos } /// Access the TPDOs as a const function - pub const fn tpdos(&self) -> &'a [Pdo] { + pub const fn tpdos(&self) -> &'a [Pdo<'a>] { self.tpdos } /// Access the pdo_sync as a const function /// /// This is required so that it can be shared with the objects in generated code - pub const fn object_flag_sync(&'static self) -> &'static ObjectFlagSync { + pub const fn object_flag_sync(&'a self) -> &'a ObjectFlagSync { &self.object_flag_sync } /// Access the storage_context as a const function - pub const fn storage_context(&'static self) -> &'static StorageContext { + pub const fn storage_context(&'a self) -> &'a StorageContext { &self.storage_context } -} -impl NodeStateAccess for NodeState<'_> { - fn get_rpdos(&self) -> &[Pdo] { - self.rpdos - } - - fn get_tpdos(&self) -> &[Pdo] { - self.tpdos - } - - fn object_flag_sync(&self) -> &ObjectFlagSync { - &self.object_flag_sync - } - - fn storage_context(&self) -> &StorageContext { - &self.storage_context + /// Set the NMT state + /// + /// This method is intended only for the `Node` object to update the global node nmt state + pub(crate) fn set_nmt_state(&self, nmt_state: NmtState) { + self.nmt_state.store(nmt_state); } } diff --git a/zencan-node/src/pdo.rs b/zencan-node/src/pdo.rs index 95b1e75..54726b8 100644 --- a/zencan-node/src/pdo.rs +++ b/zencan-node/src/pdo.rs @@ -40,10 +40,14 @@ //! ] //! ``` -use crate::object_dict::{ - find_object_entry, ConstField, ODEntry, ObjectAccess, ProvidesSubObjects, SubObjectAccess, +use crate::{ + node_state::NmtStateAccess, + object_dict::{ + find_object_entry, ConstField, ODEntry, ObjectAccess, ProvidesSubObjects, SubObjectAccess, + }, }; use zencan_common::{ + nmt::NmtState, objects::{AccessType, DataType, ObjectCode, PdoMappable, SubInfo}, pdo::PdoMapping, sdo::AbortCode, @@ -58,9 +62,9 @@ const N_MAPPING_PARAMS: usize = 8; #[derive(Clone, Copy)] /// Data structure for storing a PDO object mapping -struct MappingEntry { +struct MappingEntry<'a> { /// A reference to the object which is mapped - pub object: &'static ODEntry<'static>, + pub object: &'a ODEntry<'a>, /// The index of the sub object mapped pub sub: u8, /// The length of the mapping in bytes @@ -70,28 +74,28 @@ struct MappingEntry { #[allow(missing_debug_implementations)] /// Initialization values for a PDO #[derive(Copy, Clone)] -pub struct PdoDefaults { +pub struct PdoDefaults<'a> { cob_id: u32, flags: u8, transmission_type: u8, - mappings: &'static [u32], + mappings: &'a [u32], } -impl Default for PdoDefaults { +impl Default for PdoDefaults<'_> { fn default() -> Self { Self::DEFAULT } } #[allow(missing_docs)] -impl PdoDefaults { +impl<'a> PdoDefaults<'a> { const ADD_NODE_ID_FLAG: usize = 0; const VALID_FLAG: usize = 1; const RTR_DISABLED_FLAG: usize = 2; const IS_EXTENDED_FLAG: usize = 3; /// The PDO defaults used when no other defaults are configured - pub const DEFAULT: PdoDefaults = Self { + pub const DEFAULT: PdoDefaults<'a> = Self { cob_id: 0, flags: 0, transmission_type: 0, @@ -163,11 +167,13 @@ impl PdoDefaults { /// Represents a single PDO state #[allow(missing_debug_implementations)] -pub struct Pdo { +pub struct Pdo<'a> { /// The object dictionary /// /// PDOs have to access other objects and use this to do so - od: &'static [ODEntry<'static>], + od: &'a [ODEntry<'a>], + /// Accessor for the node NMT state + nmt_state: &'a dyn NmtStateAccess, /// Configured Node ID for the system node_id: AtomicCell, /// The COB-ID used to send or receive this PDO @@ -194,14 +200,14 @@ pub struct Pdo { /// The mapping parameters /// /// These specify which objects are - mapping_params: [AtomicCell>; N_MAPPING_PARAMS], + mapping_params: [AtomicCell>>; N_MAPPING_PARAMS], /// System default values for this PDO - defaults: Option<&'static PdoDefaults>, + defaults: Option<&'a PdoDefaults<'a>>, } -impl Pdo { +impl<'a> Pdo<'a> { /// Create a new PDO object - pub const fn new(od: &'static [ODEntry<'static>]) -> Self { + pub const fn new(od: &'a [ODEntry<'a>], nmt_state: &'a dyn NmtStateAccess) -> Self { let cob_id = AtomicCell::new(None); let node_id = AtomicCell::new(NodeId::Unconfigured); let valid = AtomicCell::new(false); @@ -214,6 +220,7 @@ impl Pdo { let defaults = None; Self { od, + nmt_state, node_id, cob_id, valid, @@ -230,9 +237,10 @@ impl Pdo { /// Create a new PDO object with provided defaults pub const fn new_with_defaults( od: &'static [ODEntry<'static>], + nmt_state: &'static dyn NmtStateAccess, defaults: &'static PdoDefaults, ) -> Self { - let mut pdo = Pdo::new(od); + let mut pdo = Pdo::new(od, nmt_state); pdo.defaults = Some(defaults); pdo } @@ -315,6 +323,10 @@ impl Pdo { false } + fn nmt_state(&self) -> NmtState { + self.nmt_state.nmt_state() + } + pub(crate) fn clear_events(&self) { for i in 0..self.mapping_params.len() { let param = self.mapping_params[i].load(); @@ -391,7 +403,7 @@ impl Pdo { /// /// This function may fail if the mapped object doesn't exist, or if it is /// too short. - fn try_create_mapping_entry(&self, mapping: PdoMapping) -> Result { + fn try_create_mapping_entry(&self, mapping: PdoMapping) -> Result, AbortCode> { let PdoMapping { index, sub, @@ -415,7 +427,7 @@ impl Pdo { } /// Initialize the PDO configuration with its default value - pub fn init_defaults(&self, node_id: NodeId) { + pub fn init_defaults(&'a self, node_id: NodeId) { if self.defaults.is_none() { return; } @@ -440,12 +452,12 @@ impl Pdo { } } -struct PdoCobSubObject { - pdo: &'static Pdo, +struct PdoCobSubObject<'a> { + pdo: &'a Pdo<'a>, } -impl PdoCobSubObject { - pub const fn new(pdo: &'static Pdo) -> Self { +impl<'a> PdoCobSubObject<'a> { + pub const fn new(pdo: &'a Pdo<'a>) -> Self { Self { pdo } } @@ -457,7 +469,7 @@ impl PdoCobSubObject { } } -impl SubObjectAccess for PdoCobSubObject { +impl SubObjectAccess for PdoCobSubObject<'_> { fn read(&self, offset: usize, buf: &mut [u8]) -> Result { let cob_id = self.pdo.cob_id(); let mut value = cob_id.raw(); @@ -486,6 +498,10 @@ impl SubObjectAccess for PdoCobSubObject { } fn write(&self, data: &[u8]) -> Result<(), AbortCode> { + // Changing PDO config is only allowed during PreOperational state + if self.pdo.nmt_state() != NmtState::PreOperational { + return Err(AbortCode::GeneralError); + } if data.len() < 4 { Err(AbortCode::DataTypeMismatchLengthLow) } else if data.len() > 4 { @@ -509,17 +525,17 @@ impl SubObjectAccess for PdoCobSubObject { } } -struct PdoTransmissionTypeSubObject { - pdo: &'static Pdo, +struct PdoTransmissionTypeSubObject<'a> { + pdo: &'a Pdo<'a>, } -impl PdoTransmissionTypeSubObject { - pub const fn new(pdo: &'static Pdo) -> Self { +impl<'a> PdoTransmissionTypeSubObject<'a> { + pub const fn new(pdo: &'a Pdo<'a>) -> Self { Self { pdo } } } -impl SubObjectAccess for PdoTransmissionTypeSubObject { +impl SubObjectAccess for PdoTransmissionTypeSubObject<'_> { fn read(&self, offset: usize, buf: &mut [u8]) -> Result { if offset > 1 { return Ok(0); @@ -533,6 +549,10 @@ impl SubObjectAccess for PdoTransmissionTypeSubObject { } fn write(&self, data: &[u8]) -> Result<(), AbortCode> { + // Changing of PDO config only allowed during preoperational state + if self.pdo.nmt_state() != NmtState::PreOperational { + return Err(AbortCode::GeneralError); + } if data.is_empty() { Err(AbortCode::DataTypeMismatchLengthLow) } else { @@ -544,14 +564,14 @@ impl SubObjectAccess for PdoTransmissionTypeSubObject { /// Implements a PDO communications config object for both RPDOs and TPDOs #[allow(missing_debug_implementations)] -pub struct PdoCommObject { - cob: PdoCobSubObject, - transmission_type: PdoTransmissionTypeSubObject, +pub struct PdoCommObject<'a> { + cob: PdoCobSubObject<'a>, + transmission_type: PdoTransmissionTypeSubObject<'a>, } -impl PdoCommObject { +impl<'a> PdoCommObject<'a> { /// Create a new PdoCommObject - pub const fn new(pdo: &'static Pdo) -> Self { + pub const fn new(pdo: &'a Pdo<'a>) -> Self { let cob = PdoCobSubObject::new(pdo); let transmission_type = PdoTransmissionTypeSubObject::new(pdo); Self { @@ -561,7 +581,7 @@ impl PdoCommObject { } } -impl ProvidesSubObjects for PdoCommObject { +impl ProvidesSubObjects for PdoCommObject<'_> { fn get_sub_object(&self, sub: u8) -> Option<(SubInfo, &dyn SubObjectAccess)> { match sub { 0 => Some(( @@ -589,18 +609,18 @@ impl ProvidesSubObjects for PdoCommObject { /// Implements a PDO mapping config object for both TPDOs and RPDOs #[allow(missing_debug_implementations)] -pub struct PdoMappingObject { - pdo: &'static Pdo, +pub struct PdoMappingObject<'a> { + pdo: &'a Pdo<'a>, } -impl PdoMappingObject { +impl<'a> PdoMappingObject<'a> { /// Create a new PdoMappingObject - pub const fn new(pdo: &'static Pdo) -> Self { + pub const fn new(pdo: &'a Pdo<'a>) -> Self { Self { pdo } } } -impl ObjectAccess for PdoMappingObject { +impl ObjectAccess for PdoMappingObject<'_> { fn read(&self, sub: u8, offset: usize, buf: &mut [u8]) -> Result { if sub == 0 { if offset < 1 && !buf.is_empty() { @@ -637,6 +657,10 @@ impl ObjectAccess for PdoMappingObject { } fn write(&self, sub: u8, data: &[u8]) -> Result<(), AbortCode> { + // Changing of PDO mapping is only allowed in PreOperational state + if self.pdo.nmt_state() != NmtState::PreOperational { + return Err(AbortCode::GeneralError); + } if sub == 0 { self.pdo.valid_maps.store(data[0]); Ok(()) @@ -682,3 +706,60 @@ impl ObjectAccess for PdoMappingObject { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::object_dict::ScalarField; + + #[derive(Default)] + struct TestObject { + value: ScalarField, + } + + impl ProvidesSubObjects for TestObject { + fn get_sub_object(&self, sub: u8) -> Option<(SubInfo, &dyn SubObjectAccess)> { + match sub { + 0 => Some((SubInfo::new_u32(), &self.value)), + _ => None, + } + } + + fn object_code(&self) -> ObjectCode { + ObjectCode::Var + } + } + + #[test] + /// Assert that attempts to update PDO comms or mapping parameters fail when in operational mode + pub fn test_changes_denied_while_operational() { + let object1000 = TestObject::default(); + let od = &[ODEntry { + index: 0x1000, + data: &object1000, + }]; + let nmt_state = AtomicCell::new(NmtState::PreOperational); + + let pdo = Pdo::new(od, &nmt_state); + + let comm_obj = PdoCommObject::new(&pdo); + let mapping_obj = PdoMappingObject::new(&pdo); + + // Setup initially + mapping_obj + .write(1, &((0x1000 << 16) | 32 as u32).to_le_bytes()) + .unwrap(); + mapping_obj.write(0, &[1]).unwrap(); + comm_obj.write(1, &(1u32 << 31).to_le_bytes()).unwrap(); + + nmt_state.store(NmtState::Operational); + + // Changing now should error + let result = mapping_obj.write(1, &0u32.to_le_bytes()); + assert_eq!(Err(AbortCode::GeneralError), result); + let result = comm_obj.write(1, &0u32.to_le_bytes()); + assert_eq!(Err(AbortCode::GeneralError), result); + let result = comm_obj.write(2, &0u32.to_le_bytes()); + assert_eq!(Err(AbortCode::GeneralError), result); + } +}