zencan_node/
node.rs

1//! Implements the core Node object
2//!
3
4use zencan_common::{
5    constants::object_ids,
6    lss::LssIdentity,
7    messages::{
8        CanId, CanMessage, Heartbeat, NmtCommandSpecifier, NmtState, ZencanMessage, LSS_RESP_ID,
9    },
10    NodeId,
11};
12
13use crate::{
14    lss_slave::{LssConfig, LssSlave},
15    node_mbox::NodeMbox,
16    object_dict::{find_object, ODEntry},
17    storage::StoreObjectsCallback,
18};
19use crate::{node_state::NodeStateAccess, sdo_server::SdoServer};
20
21use defmt_or_log::{debug, info};
22
23type StoreNodeConfigCallback = dyn Fn(&NodeId) + Sync;
24
25#[derive(Default)]
26struct Callbacks {
27    store_node_config: Option<&'static StoreNodeConfigCallback>,
28}
29
30fn read_identity(od: &[ODEntry]) -> Option<LssIdentity> {
31    let obj = find_object(od, object_ids::IDENTITY)?;
32    let vendor_id = obj.read_u32(1).ok()?;
33    let product_code = obj.read_u32(2).ok()?;
34    let revision = obj.read_u32(3).ok()?;
35    let serial = obj.read_u32(4).ok()?;
36    Some(LssIdentity {
37        vendor_id,
38        product_code,
39        revision,
40        serial,
41    })
42}
43
44fn read_heartbeat_period(od: &[ODEntry]) -> Option<u16> {
45    let obj = find_object(od, object_ids::HEARTBEAT_PRODUCER_TIME)?;
46    obj.read_u16(0).ok()
47}
48
49fn read_autostart(od: &[ODEntry]) -> Option<bool> {
50    let obj = find_object(od, object_ids::AUTO_START)?;
51    Some(obj.read_u8(0).unwrap() != 0)
52}
53
54/// The main object representing a node
55///
56/// # Operation
57///
58/// The node is run by polling the [`Node::process`] method in your application. It is safe to call
59/// this method as frequently as you like. There is no hard minimum for call frequency, but calling
60/// your node's responses to messages will be delayed until process is called, and this will slow
61/// down communication to your node. It is recommended to register a callback using
62/// [`NodeMbox::set_process_notify_callback`], and use this callback to trigger an immediate call to
63/// process, e.g. by waking a task or signaling the processing thread.
64#[allow(missing_debug_implementations)]
65pub struct Node {
66    node_id: NodeId,
67    nmt_state: NmtState,
68    sdo_server: SdoServer,
69    lss_slave: LssSlave,
70    message_count: u32,
71    od: &'static [ODEntry<'static>],
72    mbox: &'static NodeMbox,
73    state: &'static dyn NodeStateAccess,
74    reassigned_node_id: Option<NodeId>,
75    callbacks: Callbacks,
76    next_heartbeat_time_us: u64,
77    heartbeat_period_ms: u16,
78    heartbeat_toggle: bool,
79    auto_start: bool,
80    last_process_time_us: u64,
81}
82
83impl Node {
84    /// Create a new [`Node`]
85    ///
86    /// # Arguments
87    ///
88    /// * `node_id` - Initial node ID assignment
89    /// * `mbox` - The `NODE_MBOX` object created by `zencan-build`
90    /// * `state` - The `NODE_STATE` state object created by `zencan-build`
91    /// * `od` - The `OD_TABLE` object containing the object dictionary created by `zencan-build`
92    pub fn new(
93        node_id: NodeId,
94        mbox: &'static NodeMbox,
95        state: &'static dyn NodeStateAccess,
96        od: &'static [ODEntry<'static>],
97    ) -> Self {
98        let message_count = 0;
99        let sdo_server = SdoServer::new();
100        let lss_slave = LssSlave::new(LssConfig {
101            identity: read_identity(od).unwrap(),
102            node_id,
103            store_supported: false,
104        });
105        let nmt_state = NmtState::Bootup;
106        let reassigned_node_id = None;
107
108        let heartbeat_period_ms = read_heartbeat_period(od).expect("Heartbeat object must exist");
109        let next_heartbeat_time_us = 0;
110        let heartbeat_toggle = false;
111        let auto_start = read_autostart(od).expect("auto start object must exist");
112        let last_process_time_us = 0;
113        Self {
114            node_id,
115            nmt_state,
116            sdo_server,
117            lss_slave,
118            message_count,
119            od,
120            mbox,
121            state,
122            reassigned_node_id,
123            next_heartbeat_time_us,
124            heartbeat_period_ms,
125            heartbeat_toggle,
126            auto_start,
127            callbacks: Callbacks::default(),
128            last_process_time_us,
129        }
130    }
131
132    /// Manually set the node ID. Changing the node id will cause an NMT comm reset to occur,
133    /// resetting communication parameter defaults and triggering a bootup heartbeat message if the
134    /// ID is valid. Setting the node ID to 255 will put the node into unconfigured mode.
135    pub fn set_node_id(&mut self, node_id: NodeId) {
136        self.reassigned_node_id = Some(node_id);
137    }
138
139    /// Register a callback to store node configuration data persistently
140    pub fn register_store_node_config(&mut self, cb: &'static StoreNodeConfigCallback) {
141        self.callbacks.store_node_config = Some(cb);
142    }
143
144    /// Register a callback to store object data persistently
145    pub fn register_store_objects(&mut self, cb: &'static StoreObjectsCallback) {
146        self.state.storage_context().store_callback.store(Some(cb));
147    }
148
149    /// Run periodic processing
150    ///
151    /// This should be called periodically by the application so that the node can update it's
152    /// state, send periodic messages, process received messages, etc.
153    ///
154    /// It is sufficient to call this based on a timer, but the [NodeMbox] object also provides a
155    /// notification callback, which can be used by an application to accelerate the call to process
156    /// when an action is required
157    ///
158    /// # Arguments
159    /// - `now_us`: A monotonic time in microseconds. This is used for measuring time and triggering
160    ///   time-based actions such as heartbeat transmission or SDO timeout
161    /// - `send_cb`: A callback function for transmitting can messages
162    ///
163    /// # Returns
164    ///
165    /// A boolean indicating if objects were updated. This will be true when an SDO download has
166    /// been completed, or when one or more RPDOs have been received.
167    pub fn process(&mut self, now_us: u64, send_cb: &mut dyn FnMut(CanMessage)) -> bool {
168        let elapsed = (now_us - self.last_process_time_us) as u32;
169        self.last_process_time_us = now_us;
170
171        let mut update_flag = false;
172        if let Some(new_node_id) = self.reassigned_node_id.take() {
173            self.node_id = new_node_id;
174            self.nmt_state = NmtState::Bootup;
175        }
176
177        if self.nmt_state == NmtState::Bootup {
178            // Set state before calling boot_up, so the heartbeat state is correct
179            self.nmt_state = NmtState::PreOperational;
180            self.boot_up(send_cb);
181        }
182
183        // If auto start is set on boot, and we already have an ID, we make the first transition to
184        // Operational automatically
185        if self.auto_start && self.node_id.is_configured() {
186            self.auto_start = false;
187            self.nmt_state = NmtState::Operational;
188        }
189
190        // Process SDO server
191        let (resp, updated_index) =
192            self.sdo_server
193                .process(self.mbox.sdo_receiver(), elapsed, self.od);
194        if let Some(resp) = resp {
195            send_cb(resp.to_can_message(self.sdo_tx_cob_id()));
196        }
197        if updated_index.is_some() {
198            update_flag = true;
199        }
200
201        // Process NMT
202        if let Some(msg) = self.mbox.read_nmt_mbox() {
203            if let Ok(ZencanMessage::NmtCommand(cmd)) = msg.try_into() {
204                self.message_count += 1;
205                // We cannot respond to NMT commands if we do not have a valid node ID
206
207                if let NodeId::Configured(node_id) = self.node_id {
208                    if cmd.node == 0 || cmd.node == node_id.raw() {
209                        debug!("Received NMT command: {:?}", cmd.cs);
210                        self.handle_nmt_command(cmd.cs);
211                    }
212                }
213            }
214        }
215
216        if let Ok(Some(resp)) = self.lss_slave.process(self.mbox.lss_receiver()) {
217            send_cb(resp.to_can_message(LSS_RESP_ID));
218
219            if let Some(event) = self.lss_slave.pending_event() {
220                info!("LSS Slave Event: {:?}", event);
221                match event {
222                    crate::lss_slave::LssEvent::StoreConfiguration => {
223                        if let Some(cb) = self.callbacks.store_node_config {
224                            (cb)(&self.node_id)
225                        }
226                    }
227                    crate::lss_slave::LssEvent::ActivateBitTiming {
228                        table: _,
229                        index: _,
230                        delay: _,
231                    } => (),
232                    crate::lss_slave::LssEvent::ConfigureNodeId { node_id } => {
233                        self.set_node_id(node_id)
234                    }
235                }
236            }
237        }
238
239        if self.heartbeat_period_ms != 0 && now_us >= self.next_heartbeat_time_us {
240            self.send_heartbeat(send_cb);
241            // Perform catchup if we are behind, e.g. if we have not send a heartbeat in a long
242            // time because we have not been configured
243            if self.next_heartbeat_time_us < now_us {
244                self.next_heartbeat_time_us = now_us;
245            }
246        }
247
248        if self.nmt_state == NmtState::Operational {
249            // check if a sync has been received
250            let sync = self.mbox.read_sync_flag();
251
252            // Swap the active TPDO flag set. Returns true if any object flags were set since last
253            // toggle. Tracking the global trigger is a performance boost, at least in the frequent
254            // case when no events have been triggered. The goal is for `process` to be as fast as
255            // possible when it has nothing to do, so it can be called frequently with little cost.
256            let global_trigger = self.state.get_pdo_sync().toggle();
257
258            for pdo in self.state.get_tpdos() {
259                if !(pdo.valid()) {
260                    continue;
261                }
262                let transmission_type = pdo.transmission_type();
263                if transmission_type >= 254 {
264                    if global_trigger && pdo.read_events() {
265                        let mut data = [0u8; 8];
266                        pdo.read_pdo_data(&mut data);
267                        let msg = CanMessage::new(pdo.cob_id(), &data);
268                        send_cb(msg);
269                    }
270                } else if sync && pdo.sync_update() {
271                    let mut data = [0u8; 8];
272                    pdo.read_pdo_data(&mut data);
273                    let msg = CanMessage::new(pdo.cob_id(), &data);
274                    send_cb(msg);
275                }
276            }
277
278            for pdo in self.state.get_tpdos() {
279                pdo.clear_events();
280            }
281
282            for rpdo in self.state.get_rpdos() {
283                if !rpdo.valid() {
284                    continue;
285                }
286                if let Some(new_data) = rpdo.buffered_value.take() {
287                    rpdo.store_pdo_data(&new_data);
288                    update_flag = true;
289                }
290            }
291        }
292
293        update_flag
294    }
295
296    fn handle_nmt_command(&mut self, cmd: NmtCommandSpecifier) {
297        let prev_state = self.nmt_state;
298
299        match cmd {
300            NmtCommandSpecifier::Start => self.nmt_state = NmtState::Operational,
301            NmtCommandSpecifier::Stop => self.nmt_state = NmtState::Stopped,
302            NmtCommandSpecifier::EnterPreOp => self.nmt_state = NmtState::PreOperational,
303            NmtCommandSpecifier::ResetApp => {
304                // if let Some(cb) = self.app_reset_callback.as_mut() {
305                //     cb();
306                // }
307                self.nmt_state = NmtState::Bootup;
308            }
309            NmtCommandSpecifier::ResetComm => self.nmt_state = NmtState::Bootup,
310        }
311
312        debug!(
313            "NMT state changed from {:?} to {:?}",
314            prev_state, self.nmt_state
315        );
316    }
317
318    /// Get the current Node ID
319    pub fn node_id(&self) -> u8 {
320        self.node_id.into()
321    }
322
323    /// Get the current NMT state of the node
324    pub fn nmt_state(&self) -> NmtState {
325        self.nmt_state
326    }
327
328    /// Get the number of received messages
329    pub fn rx_message_count(&self) -> u32 {
330        self.message_count
331    }
332
333    fn sdo_tx_cob_id(&self) -> CanId {
334        let node_id: u8 = self.node_id.into();
335        CanId::Std(0x580 + node_id as u16)
336    }
337
338    fn sdo_rx_cob_id(&self) -> CanId {
339        let node_id: u8 = self.node_id.into();
340        CanId::Std(0x600 + node_id as u16)
341    }
342
343    fn boot_up(&mut self, sender: &mut dyn FnMut(CanMessage)) {
344        // Reset the LSS slave with the new ID
345        self.lss_slave.update_config(LssConfig {
346            identity: read_identity(self.od).unwrap(),
347            node_id: self.node_id,
348            store_supported: self.callbacks.store_node_config.is_some(),
349        });
350
351        if let NodeId::Configured(node_id) = self.node_id {
352            info!("Booting node with ID {}", node_id.raw());
353            self.mbox.set_sdo_cob_id(Some(self.sdo_rx_cob_id()));
354            self.send_heartbeat(sender);
355        }
356    }
357
358    fn send_heartbeat(&mut self, sender: &mut dyn FnMut(CanMessage)) {
359        if let NodeId::Configured(node_id) = self.node_id {
360            let heartbeat = Heartbeat {
361                node: node_id.raw(),
362                toggle: self.heartbeat_toggle,
363                state: self.nmt_state,
364            };
365            self.heartbeat_toggle = !self.heartbeat_toggle;
366            sender(heartbeat.into());
367            self.next_heartbeat_time_us += (self.heartbeat_period_ms as u64) * 1000;
368        }
369    }
370}