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