zencan_node/
node.rs

1//! Implements the core Node object
2//!
3
4use core::{convert::Infallible, sync::atomic::Ordering};
5
6use zencan_common::{
7    constants::object_ids,
8    lss::LssIdentity,
9    messages::{CanId, CanMessage, Heartbeat, NmtCommandSpecifier, ZencanMessage, LSS_RESP_ID},
10    nmt::NmtState,
11    NodeId,
12};
13
14use crate::sdo_server::SdoServer;
15use crate::{
16    lss_slave::{LssConfig, LssSlave},
17    node_mbox::NodeMbox,
18    node_state::NmtStateAccess as _,
19    object_dict::{find_object, ODEntry},
20    NodeState,
21};
22
23use defmt_or_log::{debug, info};
24
25pub type StoreNodeConfigFn<'a> = dyn FnMut(NodeId) + 'a;
26pub type StoreObjectsFn<'a> = dyn Fn(&mut dyn embedded_io::Read<Error = Infallible>, usize) + 'a;
27pub type StateChangeFn<'a> = dyn FnMut(&'a [ODEntry<'a>]) + 'a;
28
29/// Collection of callbacks events which Node object can call.
30///
31/// Most are optional, and may be implemented by the application or not.
32#[allow(missing_debug_implementations)]
33#[derive(Default)]
34pub struct Callbacks<'a> {
35    /// Store node config to flash
36    ///
37    /// An application should implement this callback in order to support storing a configured node
38    /// ID persistently. It is triggered when the LSS StoreConfiguration command is received. The
39    /// passed NodeId should be stored, and used when creating the [`Node`] object on the next boot.
40    pub store_node_config: Option<&'a mut StoreNodeConfigFn<'a>>,
41
42    /// Store object data to persistent flash
43    ///
44    /// The bytes read from the provided reader (arg 1) should be stored. The total number of bytes
45    /// in the stream is given in the second arg.
46    pub store_objects: Option<&'a mut StoreObjectsFn<'a>>,
47
48    /// The RESET_APP NMT state has been entered
49    ///
50    /// If the application supported storing persistent object values, it should restore them now
51    /// using the [`restore_stored_objects`](crate::restore_stored_objects) method. The application
52    /// should also do whatever is appropraite to reset its state to it's reset condition.
53    pub reset_app: Option<&'a mut StateChangeFn<'a>>,
54
55    /// The RESET_COMMS NMT state has been entered
56    ///
57    /// During RESET COMMS, communications objects (i.e. 0x1000-0x1fff) are reset to their boot up
58    /// values. Application which store persistent object values should restore ONLY THE COMM
59    /// OBJECTS now, using the [`restore_stored_comm_objects`](crate::restore_stored_comm_objects)
60    /// function.
61    ///
62    /// This event will only be triggered by an NMT RESET_COMMS command -- when a RESET_APP event
63    /// occurs, only the reset_app callback is called.
64    pub reset_comms: Option<&'a mut StateChangeFn<'a>>,
65
66    /// The node is entering OPERATIONAL state
67    pub enter_operational: Option<&'a mut StateChangeFn<'a>>,
68
69    /// The node is entering the STOPPED state
70    pub enter_stopped: Option<&'a mut StateChangeFn<'a>>,
71
72    /// The node is entering the PRE-OPERATIONAL state
73    pub enter_preoperational: Option<&'a mut StateChangeFn<'a>>,
74}
75
76impl<'a> Callbacks<'a> {
77    /// Create a new Callbacks struct with the provided send_message callback
78    pub const fn new() -> Self {
79        Self {
80            store_node_config: None,
81            store_objects: None,
82            reset_app: None,
83            reset_comms: None,
84            enter_operational: None,
85            enter_stopped: None,
86            enter_preoperational: None,
87        }
88    }
89}
90
91fn read_identity(od: &[ODEntry]) -> Option<LssIdentity> {
92    let obj = find_object(od, object_ids::IDENTITY)?;
93    let vendor_id = obj.read_u32(1).ok()?;
94    let product_code = obj.read_u32(2).ok()?;
95    let revision = obj.read_u32(3).ok()?;
96    let serial = obj.read_u32(4).ok()?;
97    Some(LssIdentity {
98        vendor_id,
99        product_code,
100        revision,
101        serial,
102    })
103}
104
105fn read_heartbeat_period(od: &[ODEntry]) -> Option<u16> {
106    let obj = find_object(od, object_ids::HEARTBEAT_PRODUCER_TIME)?;
107    obj.read_u16(0).ok()
108}
109
110fn read_autostart(od: &[ODEntry]) -> Option<bool> {
111    let obj = find_object(od, object_ids::AUTO_START)?;
112    Some(obj.read_u8(0).unwrap() != 0)
113}
114
115/// The main object representing a node
116///
117/// # Operation
118///
119/// The node is run by polling the [`Node::process`] method in your application. It is safe to call
120/// this method as frequently as you like. There is no hard minimum for call frequency, but calling
121/// your node's responses to messages will be delayed until process is called, and this will slow
122/// down communication to your node. It is recommended to register a callback using
123/// [`NodeMbox::set_process_notify_callback`], and use this callback to trigger an immediate call to
124/// process, e.g. by waking a task or signaling the processing thread.
125#[allow(missing_debug_implementations)]
126pub struct Node<'a> {
127    node_id: NodeId,
128    sdo_server: SdoServer<'a>,
129    lss_slave: LssSlave,
130    message_count: u32,
131    od: &'static [ODEntry<'static>],
132    mbox: &'static NodeMbox,
133    state: &'static NodeState<'static>,
134    reassigned_node_id: Option<NodeId>,
135    next_heartbeat_time_us: u64,
136    heartbeat_period_ms: u16,
137    auto_start: bool,
138    last_process_time_us: u64,
139    callbacks: Callbacks<'a>,
140    transmit_flag: bool,
141}
142
143impl<'a> Node<'a> {
144    /// Create a new [`Node`]
145    ///
146    /// # Arguments
147    ///
148    /// * `node_id` - Initial node ID assignment
149    /// * `mbox` - The `NODE_MBOX` object created by `zencan-build`
150    /// * `state` - The `NODE_STATE` state object created by `zencan-build`
151    /// * `od` - The `OD_TABLE` object containing the object dictionary created by `zencan-build`
152    pub fn new(
153        node_id: NodeId,
154        callbacks: Callbacks<'a>,
155        mbox: &'static NodeMbox,
156        state: &'static NodeState<'static>,
157        od: &'static [ODEntry<'static>],
158    ) -> Self {
159        let message_count = 0;
160        let sdo_server = SdoServer::new();
161        let lss_slave = LssSlave::new(LssConfig {
162            identity: read_identity(od).unwrap_or_default(),
163            node_id,
164            store_supported: false,
165        });
166        let reassigned_node_id = None;
167
168        // Storage command is supported if the application provides a callback
169        if callbacks.store_objects.is_some() {
170            state
171                .storage_context()
172                .store_supported
173                .store(true, Ordering::Relaxed);
174        }
175
176        let heartbeat_period_ms = read_heartbeat_period(od).unwrap_or(0);
177        let next_heartbeat_time_us = 0;
178        let auto_start = read_autostart(od).unwrap_or(false);
179        let last_process_time_us = 0;
180        let transmit_flag = false;
181
182        let mut node = Self {
183            node_id,
184            callbacks,
185            sdo_server,
186            lss_slave,
187            message_count,
188            od,
189            mbox,
190            state,
191            reassigned_node_id,
192            next_heartbeat_time_us,
193            heartbeat_period_ms,
194            auto_start,
195            last_process_time_us,
196            transmit_flag,
197        };
198
199        node.reset_app();
200        node
201    }
202
203    /// Manually set the node ID. Changing the node id will cause an NMT comm reset to occur,
204    /// resetting communication parameter defaults and triggering a bootup heartbeat message if the
205    /// ID is valid. Setting the node ID to 255 will put the node into unconfigured mode.
206    pub fn set_node_id(&mut self, node_id: NodeId) {
207        self.reassigned_node_id = Some(node_id);
208    }
209
210    /// Run periodic processing
211    ///
212    /// This should be called periodically by the application so that the node can update it's
213    /// state, send periodic messages, process received messages, etc.
214    ///
215    /// It is sufficient to call this based on a timer, but the [NodeMbox] object also provides a
216    /// notification callback, which can be used by an application to accelerate the call to process
217    /// when an action is required.
218    ///
219    /// # Arguments
220    /// - `now_us`: A monotonic time in microseconds. This is used for measuring time and triggering
221    ///   time-based actions such as heartbeat transmission or SDO timeout
222    ///
223    /// # Returns
224    ///
225    /// A boolean indicating if objects were updated. This will be true when an SDO download has
226    /// been completed, or when one or more RPDOs have been received.
227    pub fn process(&mut self, now_us: u64) -> bool {
228        let elapsed = (now_us - self.last_process_time_us) as u32;
229        self.last_process_time_us = now_us;
230
231        self.transmit_flag = false;
232
233        let mut update_flag = false;
234        if let Some(new_node_id) = self.reassigned_node_id.take() {
235            self.node_id = new_node_id;
236            self.state.set_nmt_state(NmtState::Bootup);
237        }
238
239        if self.nmt_state() == NmtState::Bootup {
240            // Set state before calling boot_up, so the heartbeat state is correct
241            self.enter_preoperational();
242            self.boot_up();
243        }
244
245        // If auto start is set on boot, and we already have an ID, we make the first transition to
246        // Operational automatically
247        if self.auto_start && self.node_id.is_configured() {
248            // Clear flag so that we will not automatically enter operational again until reboot
249            self.auto_start = false;
250            self.enter_operational();
251        }
252
253        // Process SDO server
254        let (message_sent, updated_index) =
255            self.sdo_server
256                .process(self.mbox.sdo_comms(), elapsed, self.od);
257
258        self.transmit_flag |= message_sent;
259        if updated_index.is_some() {
260            update_flag = true;
261        }
262
263        // Read and clear the store command flag
264        if self
265            .state
266            .storage_context()
267            .store_flag
268            .swap(false, Ordering::Relaxed)
269        {
270            // If the flag is set, and the user has provided a callback, call it
271            if let Some(cb) = &mut self.callbacks.store_objects {
272                crate::persist::serialize(self.od, *cb);
273            }
274        }
275
276        // Process NMT
277        if let Some(msg) = self.mbox.read_nmt_mbox() {
278            if let Ok(ZencanMessage::NmtCommand(cmd)) = msg.try_into() {
279                self.message_count += 1;
280                // We cannot respond to NMT commands if we do not have a valid node ID
281
282                if let NodeId::Configured(node_id) = self.node_id {
283                    if cmd.node == 0 || cmd.node == node_id.raw() {
284                        debug!("Received NMT command: {:?}", cmd.cs);
285                        self.handle_nmt_command(cmd.cs);
286                    }
287                }
288            }
289        }
290
291        if let Ok(Some(resp)) = self.lss_slave.process(self.mbox.lss_receiver()) {
292            self.send_message(resp.to_can_message(LSS_RESP_ID));
293
294            if let Some(event) = self.lss_slave.pending_event() {
295                info!("LSS Slave Event: {:?}", event);
296                match event {
297                    crate::lss_slave::LssEvent::StoreConfiguration => {
298                        if let Some(cb) = &mut self.callbacks.store_node_config {
299                            (cb)(self.node_id)
300                        }
301                    }
302                    crate::lss_slave::LssEvent::ActivateBitTiming {
303                        table: _,
304                        index: _,
305                        delay: _,
306                    } => (),
307                    crate::lss_slave::LssEvent::ConfigureNodeId { node_id } => {
308                        self.set_node_id(node_id)
309                    }
310                }
311            }
312        }
313
314        if self.heartbeat_period_ms != 0 && now_us >= self.next_heartbeat_time_us {
315            self.send_heartbeat();
316            // Perform catchup if we are behind, e.g. if we have not send a heartbeat in a long
317            // time because we have not been configured
318            if self.next_heartbeat_time_us < now_us {
319                self.next_heartbeat_time_us = now_us;
320            }
321        }
322
323        if self.nmt_state() == NmtState::Operational {
324            // check if a sync has been received
325            let sync = self.mbox.read_sync_flag();
326
327            // Swap the active TPDO flag set. Returns true if any object flags were set since last
328            // toggle. Tracking the global trigger is a performance boost, at least in the frequent
329            // case when no events have been triggered. The goal is for `process` to be as fast as
330            // possible when it has nothing to do, so it can be called frequently with little cost.
331            let global_trigger = self.state.object_flag_sync().toggle();
332
333            for pdo in self.state.tpdos() {
334                if !(pdo.valid()) {
335                    continue;
336                }
337                let transmission_type = pdo.transmission_type();
338                if transmission_type >= 254 {
339                    if global_trigger && pdo.read_events() {
340                        pdo.send_pdo();
341                        self.transmit_flag = true;
342                    }
343                } else if sync && pdo.sync_update() {
344                    pdo.send_pdo();
345                    self.transmit_flag = true;
346                }
347            }
348
349            for pdo in self.state.tpdos() {
350                pdo.clear_events();
351            }
352
353            for rpdo in self.state.rpdos() {
354                if !rpdo.valid() {
355                    continue;
356                }
357                if let Some(new_data) = rpdo.buffered_value.take() {
358                    rpdo.store_pdo_data(&new_data);
359                    update_flag = true;
360                }
361            }
362        }
363
364        if self.transmit_flag {
365            self.mbox.transmit_notify();
366        }
367
368        update_flag
369    }
370
371    fn handle_nmt_command(&mut self, cmd: NmtCommandSpecifier) {
372        let prev_state = self.nmt_state();
373
374        match cmd {
375            NmtCommandSpecifier::Start => self.enter_operational(),
376            NmtCommandSpecifier::Stop => self.enter_stopped(),
377            NmtCommandSpecifier::EnterPreOp => self.enter_preoperational(),
378            NmtCommandSpecifier::ResetApp => self.reset_app(),
379            NmtCommandSpecifier::ResetComm => self.reset_comm(),
380        }
381
382        debug!(
383            "NMT state changed from {:?} to {:?}",
384            prev_state,
385            self.nmt_state()
386        );
387    }
388
389    /// Get the current Node ID
390    pub fn node_id(&self) -> u8 {
391        self.node_id.into()
392    }
393
394    /// Get the current NMT state of the node
395    pub fn nmt_state(&self) -> NmtState {
396        self.state.nmt_state()
397    }
398
399    /// Get the number of received messages
400    pub fn rx_message_count(&self) -> u32 {
401        self.message_count
402    }
403
404    fn sdo_tx_cob_id(&self) -> CanId {
405        let node_id: u8 = self.node_id.into();
406        CanId::Std(0x580 + node_id as u16)
407    }
408
409    fn sdo_rx_cob_id(&self) -> CanId {
410        let node_id: u8 = self.node_id.into();
411        CanId::Std(0x600 + node_id as u16)
412    }
413
414    fn send_message(&mut self, msg: CanMessage) {
415        self.transmit_flag = true;
416        // TODO: return  the error, and then handle it everywhere
417        self.mbox.queue_transmit_message(msg).ok();
418    }
419
420    fn enter_operational(&mut self) {
421        self.state.set_nmt_state(NmtState::Operational);
422        if let Some(cb) = &mut self.callbacks.enter_operational {
423            (*cb)(self.od);
424        }
425    }
426
427    fn enter_stopped(&mut self) {
428        self.state.set_nmt_state(NmtState::Stopped);
429        if let Some(cb) = &mut self.callbacks.enter_stopped {
430            (*cb)(self.od);
431        }
432    }
433
434    fn enter_preoperational(&mut self) {
435        self.state.set_nmt_state(NmtState::PreOperational);
436        if let Some(cb) = &mut self.callbacks.enter_preoperational {
437            (*cb)(self.od);
438        }
439    }
440
441    fn reset_app(&mut self) {
442        // TODO: All objects should get reset to their defaults, but that isn't yet supported
443        for pdo in self.state.rpdos().iter().chain(self.state.tpdos()) {
444            pdo.init_defaults(self.node_id);
445        }
446
447        if let Some(reset_app_cb) = &mut self.callbacks.reset_app {
448            (*reset_app_cb)(self.od);
449        }
450        self.state.set_nmt_state(NmtState::Bootup);
451    }
452
453    fn reset_comm(&mut self) {
454        for pdo in self.state.rpdos().iter().chain(self.state.tpdos()) {
455            pdo.init_defaults(self.node_id);
456        }
457        if let Some(reset_comms_cb) = &mut self.callbacks.reset_comms {
458            (*reset_comms_cb)(self.od);
459        }
460        self.state.set_nmt_state(NmtState::Bootup);
461    }
462
463    fn boot_up(&mut self) {
464        // Reset the LSS slave with the new ID
465        self.lss_slave.update_config(LssConfig {
466            identity: read_identity(self.od).unwrap_or_default(),
467            node_id: self.node_id,
468            store_supported: self.callbacks.store_node_config.is_some(),
469        });
470
471        if let NodeId::Configured(node_id) = self.node_id {
472            info!("Booting node with ID {}", node_id.raw());
473            self.mbox.set_sdo_rx_cob_id(Some(self.sdo_rx_cob_id()));
474            self.mbox.set_sdo_tx_cob_id(Some(self.sdo_tx_cob_id()));
475            self.send_heartbeat();
476        }
477    }
478
479    fn send_heartbeat(&mut self) {
480        if let NodeId::Configured(node_id) = self.node_id {
481            let heartbeat = Heartbeat {
482                node: node_id.raw(),
483                toggle: false,
484                state: self.nmt_state(),
485            };
486            self.send_message(heartbeat.into());
487            self.next_heartbeat_time_us += (self.heartbeat_period_ms as u64) * 1000;
488        }
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use zencan_common::{
495        nmt::NmtState,
496        objects::{ObjectCode, SubInfo},
497        CanMessage, NodeId,
498    };
499
500    use crate::{
501        object_dict::{ODEntry, ProvidesSubObjects, ScalarField, SubObjectAccess},
502        priority_queue::PriorityQueue,
503        Callbacks, Node, NodeMbox, NodeState,
504    };
505
506    struct AutoStartObject {
507        value: ScalarField<u8>,
508    }
509
510    impl AutoStartObject {
511        pub fn new(value: u8) -> Self {
512            Self {
513                value: ScalarField::<u8>::new(value),
514            }
515        }
516    }
517    impl ProvidesSubObjects for AutoStartObject {
518        fn get_sub_object(&self, sub: u8) -> Option<(SubInfo, &dyn SubObjectAccess)> {
519            match sub {
520                0 => Some((SubInfo::new_u8(), &self.value)),
521                _ => None,
522            }
523        }
524
525        fn object_code(&self) -> ObjectCode {
526            ObjectCode::Var
527        }
528    }
529
530    #[test]
531    fn test_node_autostart_enabled() {
532        let object5000 = Box::leak(Box::new(AutoStartObject::new(1)));
533        let od_table = Box::leak(Box::new([ODEntry {
534            index: 0x5000,
535            data: object5000,
536        }]));
537
538        let tx_queue = Box::leak(Box::new(PriorityQueue::<4, CanMessage>::new()));
539        let sdo_buffer = Box::leak(Box::new([0u8; 100]));
540        let mbox = Box::leak(Box::new(NodeMbox::new(&[], &[], tx_queue, sdo_buffer)));
541        let state = Box::leak(Box::new(NodeState::new(&[], &[])));
542
543        let mut node = Node::new(
544            NodeId::new(1).unwrap(),
545            Callbacks::default(),
546            mbox,
547            state,
548            od_table,
549        );
550
551        node.process(0);
552        assert_eq!(NmtState::Operational, node.nmt_state());
553    }
554
555    #[test]
556    fn test_node_autostart_disabled() {
557        let object5000 = Box::leak(Box::new(AutoStartObject::new(0)));
558        let od_table = Box::leak(Box::new([ODEntry {
559            index: 0x5000,
560            data: object5000,
561        }]));
562
563        let tx_queue = Box::leak(Box::new(PriorityQueue::<4, CanMessage>::new()));
564        let sdo_buffer = Box::leak(Box::new([0u8; 100]));
565        let mbox = Box::leak(Box::new(NodeMbox::new(&[], &[], tx_queue, sdo_buffer)));
566        let state = Box::leak(Box::new(NodeState::new(&[], &[])));
567
568        let mut node = Node::new(
569            NodeId::new(1).unwrap(),
570            Callbacks::default(),
571            mbox,
572            state,
573            od_table,
574        );
575
576        node.process(0);
577        assert_eq!(NmtState::PreOperational, node.nmt_state());
578    }
579}