Skip to main content

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