Skip to main content

canopen_rs/
nmt.rs

1//! Network Management (NMT) — the node state machine (CiA 301 §7.3).
2//!
3//! NMT governs a node's lifecycle. A node powers up in *Initialisation*,
4//! emits its boot-up message, and autonomously enters *Pre-operational*.
5//! From there an NMT master moves it between *Operational* and *Stopped* with
6//! node-control commands broadcast on COB-ID `0x000`. Each node reports its
7//! current state to the network through the error-control (heartbeat)
8//! producer on `0x700 + node`.
9//!
10//! ```text
11//!                    ┌──────────────── reset ◄────────────┐
12//!                    ▼                                     │
13//!            ┌───────────────┐  boot   ┌──────────────────┴─┐
14//!            │ Initialising  ├────────►│  Pre-operational   │
15//!            └───────────────┘         └──┬──────────────▲──┘
16//!                              start(0x01)│              │enter-preop(0x80)
17//!                                         ▼              │
18//!                                   ┌─────┴──────────────┴──┐
19//!                       ┌──────────►│     Operational       │
20//!            start(0x01)│           └─────────┬─────────────┘
21//!                       │              stop(0x02)
22//!                 ┌─────┴─────┐               ▼
23//!                 │  Stopped  │◄──────────────┘
24//!                 └───────────┘
25//! ```
26//!
27//! Like the SDO codec, the functions here encode and decode raw CAN *data
28//! fields* only; selecting the COB-ID and moving the frame is the transport's
29//! job. [`NmtStateMachine`] is the node-side state logic, with no I/O.
30//!
31//! ```
32//! use canopen_rs::nmt::encode_command;
33//! use canopen_rs::{NmtCommand, NmtState, NmtStateMachine, NodeId};
34//!
35//! // Master side: command node 5 to start (enter operational).
36//! let frame = encode_command(NmtCommand::StartRemoteNode, NodeId::new(5).unwrap());
37//! assert_eq!(frame, [0x01, 0x05]); // [command specifier, target node]
38//!
39//! // Node side: the guarded state machine.
40//! let mut sm = NmtStateMachine::new();
41//! assert_eq!(sm.state(), NmtState::Initialising);
42//! sm.boot(); // -> pre-operational
43//! assert_eq!(sm.apply(NmtCommand::StartRemoteNode), NmtState::Operational);
44//! assert_eq!(sm.apply(NmtCommand::ResetNode), NmtState::Initialising);
45//! ```
46
47use crate::types::NodeId;
48use crate::{Error, Result};
49
50/// COB-ID of the NMT node-control channel (master → nodes): `0x000`.
51///
52/// This is the highest-priority id on the bus; a single frame can address one
53/// node or, with target `0`, every node at once.
54pub const NMT_COMMAND_COB_ID: u16 = 0x000;
55
56/// COB-ID base for the error-control (heartbeat / boot-up) channel:
57/// `0x700 + node`.
58pub const HEARTBEAT_COB_BASE: u16 = 0x700;
59
60/// The COB-ID of a node's heartbeat / boot-up producer channel.
61pub const fn heartbeat_cob_id(node: NodeId) -> u16 {
62    HEARTBEAT_COB_BASE + node.raw() as u16
63}
64
65/// A node's NMT state.
66///
67/// The discriminant is the value carried in a heartbeat frame (CiA 301
68/// §7.2.8.3). [`NmtState::Initialising`] is reported as `0` — the value of a
69/// boot-up message, sent once as the node leaves initialisation.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum NmtState {
72    /// Initialising (boot-up); reported as `0`.
73    Initialising = 0x00,
74    /// Stopped: only NMT and error-control are active.
75    Stopped = 0x04,
76    /// Operational: all communication objects (incl. PDOs) are active.
77    Operational = 0x05,
78    /// Pre-operational: SDOs active, PDOs inactive.
79    PreOperational = 0x7F,
80}
81
82impl NmtState {
83    /// Decode a heartbeat state byte, ignoring the reserved toggle bit (7).
84    ///
85    /// Returns [`Error::UnknownState`] for an unrecognised value.
86    pub const fn from_wire(byte: u8) -> Result<Self> {
87        Ok(match byte & 0x7F {
88            0x00 => NmtState::Initialising,
89            0x04 => NmtState::Stopped,
90            0x05 => NmtState::Operational,
91            0x7F => NmtState::PreOperational,
92            _ => return Err(Error::UnknownState),
93        })
94    }
95}
96
97/// An NMT node-control command from the master (CiA 301 §7.2.8.2).
98///
99/// The discriminant is the command specifier carried in byte 0 of an
100/// `0x000` frame.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum NmtCommand {
103    /// Start remote node → *Operational*.
104    StartRemoteNode = 0x01,
105    /// Stop remote node → *Stopped*.
106    StopRemoteNode = 0x02,
107    /// Enter pre-operational → *Pre-operational*.
108    EnterPreOperational = 0x80,
109    /// Reset node → re-enter *Initialising* (application + communication).
110    ResetNode = 0x81,
111    /// Reset communication → re-enter *Initialising* (communication only).
112    ResetCommunication = 0x82,
113}
114
115impl NmtCommand {
116    /// Decode a command specifier byte.
117    ///
118    /// Returns [`Error::UnexpectedCommand`] for an unrecognised specifier.
119    pub const fn from_byte(byte: u8) -> Result<Self> {
120        Ok(match byte {
121            0x01 => NmtCommand::StartRemoteNode,
122            0x02 => NmtCommand::StopRemoteNode,
123            0x80 => NmtCommand::EnterPreOperational,
124            0x81 => NmtCommand::ResetNode,
125            0x82 => NmtCommand::ResetCommunication,
126            _ => return Err(Error::UnexpectedCommand),
127        })
128    }
129}
130
131/// The 2-byte data field of an NMT node-control frame.
132pub type NmtCommandFrame = [u8; 2];
133
134/// The 1-byte data field of a heartbeat / boot-up frame.
135pub type HeartbeatFrame = [u8; 1];
136
137/// The boot-up message data field (`0x00`), emitted once when a node leaves
138/// initialisation and enters pre-operational.
139pub const BOOTUP_FRAME: HeartbeatFrame = [0x00];
140
141/// Encode an NMT node-control frame for `target`.
142///
143/// Pass [`NodeId::BROADCAST`] to address every node on the bus at once.
144pub fn encode_command(command: NmtCommand, target: NodeId) -> NmtCommandFrame {
145    [command as u8, target.raw()]
146}
147
148/// Decode an NMT node-control frame into `(command, target)`.
149///
150/// A target byte of `0` decodes to [`NodeId::BROADCAST`]; any other value is a
151/// checked device id (`1..=127`), else [`Error::InvalidNodeId`].
152pub fn decode_command(frame: &NmtCommandFrame) -> Result<(NmtCommand, NodeId)> {
153    let command = NmtCommand::from_byte(frame[0])?;
154    let target = if frame[1] == 0 {
155        NodeId::BROADCAST
156    } else {
157        NodeId::new(frame[1])?
158    };
159    Ok((command, target))
160}
161
162/// Encode a heartbeat frame reporting `state`.
163pub fn encode_heartbeat(state: NmtState) -> HeartbeatFrame {
164    [state as u8]
165}
166
167/// Decode a heartbeat frame into the reported [`NmtState`].
168pub fn decode_heartbeat(frame: &HeartbeatFrame) -> Result<NmtState> {
169    NmtState::from_wire(frame[0])
170}
171
172/// Encode a **node-guarding** response byte: the alternating toggle bit in bit
173/// 7 and the current [`NmtState`] in bits 0–6.
174///
175/// Node guarding (CiA 301 §7.3.1) is the legacy error-control alternative to
176/// heartbeat: the master polls the node with an RTR frame on `0x700 + node` and
177/// the node replies with this byte, toggling bit 7 on each reply so lost or
178/// duplicated responses are detectable.
179pub fn encode_node_guard(state: NmtState, toggle: bool) -> u8 {
180    (if toggle { 0x80 } else { 0 }) | state as u8
181}
182
183/// Decode a node-guarding response byte into `(toggle, state)`.
184pub fn decode_node_guard(byte: u8) -> Result<(bool, NmtState)> {
185    Ok((byte & 0x80 != 0, NmtState::from_wire(byte)?))
186}
187
188/// The node-side NMT state machine (CiA 301 §7.3.2).
189///
190/// Holds only the current state and applies the guarded transitions of the
191/// diagram above. It performs no I/O: feed it decoded [`NmtCommand`]s and read
192/// back the resulting [`NmtState`] to drive heartbeat production and to gate
193/// which services (e.g. PDOs) are active.
194#[derive(Debug)]
195pub struct NmtStateMachine {
196    state: NmtState,
197}
198
199impl Default for NmtStateMachine {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl NmtStateMachine {
206    /// A freshly powered node, in [`NmtState::Initialising`].
207    pub const fn new() -> Self {
208        Self {
209            state: NmtState::Initialising,
210        }
211    }
212
213    /// The current state.
214    pub const fn state(&self) -> NmtState {
215        self.state
216    }
217
218    /// Complete initialisation: the node enters pre-operational (CiA 301
219    /// §7.3.2.1). The caller emits [`BOOTUP_FRAME`] alongside this transition.
220    ///
221    /// A no-op unless currently initialising.
222    pub fn boot(&mut self) -> NmtState {
223        if let NmtState::Initialising = self.state {
224            self.state = NmtState::PreOperational;
225        }
226        self.state
227    }
228
229    /// Apply an NMT master command, returning the resulting state.
230    ///
231    /// Transitions are guarded: a reset returns to *Initialising* from any
232    /// state, while start/stop/enter-pre-operational are ignored during
233    /// initialisation (the node is not yet a network participant) and
234    /// otherwise move between the operational states. Repeating a command that
235    /// matches the current state is a harmless no-op.
236    pub fn apply(&mut self, command: NmtCommand) -> NmtState {
237        self.state = match (self.state, command) {
238            // Reset re-enters initialisation from any state.
239            (_, NmtCommand::ResetNode | NmtCommand::ResetCommunication) => NmtState::Initialising,
240            // During initialisation the node ignores operational commands; it
241            // leaves this state only via `boot`.
242            (NmtState::Initialising, _) => NmtState::Initialising,
243            (_, NmtCommand::StartRemoteNode) => NmtState::Operational,
244            (_, NmtCommand::StopRemoteNode) => NmtState::Stopped,
245            (_, NmtCommand::EnterPreOperational) => NmtState::PreOperational,
246        };
247        self.state
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    // --- COB-IDs -----------------------------------------------------------
256    #[test]
257    fn heartbeat_cob_id_follows_convention() {
258        assert_eq!(heartbeat_cob_id(NodeId::new(0x05).unwrap()), 0x705);
259        assert_eq!(NMT_COMMAND_COB_ID, 0x000);
260    }
261
262    // --- Command codec: known-good frames ----------------------------------
263    // NMT frames are [command specifier, target node]. Target 0 = all nodes.
264    #[test]
265    fn start_broadcast_matches_known_frame() {
266        assert_eq!(
267            encode_command(NmtCommand::StartRemoteNode, NodeId::BROADCAST),
268            [0x01, 0x00]
269        );
270    }
271
272    #[test]
273    fn start_single_node_matches_known_frame() {
274        let f = encode_command(NmtCommand::StartRemoteNode, NodeId::new(5).unwrap());
275        assert_eq!(f, [0x01, 0x05]);
276    }
277
278    #[test]
279    fn stop_single_node_matches_known_frame() {
280        let f = encode_command(NmtCommand::StopRemoteNode, NodeId::new(5).unwrap());
281        assert_eq!(f, [0x02, 0x05]);
282    }
283
284    #[test]
285    fn reset_commands_match_known_frames() {
286        let node = NodeId::new(0x7F).unwrap();
287        assert_eq!(
288            encode_command(NmtCommand::EnterPreOperational, node),
289            [0x80, 0x7F]
290        );
291        assert_eq!(encode_command(NmtCommand::ResetNode, node), [0x81, 0x7F]);
292        assert_eq!(
293            encode_command(NmtCommand::ResetCommunication, node),
294            [0x82, 0x7F]
295        );
296    }
297
298    #[test]
299    fn command_frame_roundtrips() {
300        let (cmd, target) = decode_command(&[0x01, 0x05]).unwrap();
301        assert_eq!(cmd, NmtCommand::StartRemoteNode);
302        assert_eq!(target, NodeId::new(5).unwrap());
303
304        let (cmd, target) = decode_command(&[0x82, 0x00]).unwrap();
305        assert_eq!(cmd, NmtCommand::ResetCommunication);
306        assert_eq!(target, NodeId::BROADCAST);
307    }
308
309    #[test]
310    fn decode_rejects_unknown_command() {
311        assert_eq!(decode_command(&[0x7E, 0x05]), Err(Error::UnexpectedCommand));
312    }
313
314    // --- Heartbeat / boot-up codec: known-good frames ----------------------
315    #[test]
316    fn heartbeat_states_match_known_frames() {
317        assert_eq!(encode_heartbeat(NmtState::Operational), [0x05]);
318        assert_eq!(encode_heartbeat(NmtState::Stopped), [0x04]);
319        assert_eq!(encode_heartbeat(NmtState::PreOperational), [0x7F]);
320        assert_eq!(BOOTUP_FRAME, [0x00]);
321    }
322
323    #[test]
324    fn heartbeat_decode_ignores_toggle_bit() {
325        // Bit 7 is reserved (toggle in node guarding); decoding must mask it.
326        assert_eq!(decode_heartbeat(&[0x05]).unwrap(), NmtState::Operational);
327        assert_eq!(decode_heartbeat(&[0x85]).unwrap(), NmtState::Operational);
328    }
329
330    #[test]
331    fn heartbeat_decode_rejects_unknown_state() {
332        assert_eq!(decode_heartbeat(&[0x42]), Err(Error::UnknownState));
333    }
334
335    // --- Node guarding -----------------------------------------------------
336    #[test]
337    fn node_guard_encodes_toggle_and_state() {
338        // Operational (0x05) with toggle clear, then set.
339        assert_eq!(encode_node_guard(NmtState::Operational, false), 0x05);
340        assert_eq!(encode_node_guard(NmtState::Operational, true), 0x85);
341        assert_eq!(
342            decode_node_guard(0x85).unwrap(),
343            (true, NmtState::Operational)
344        );
345        assert_eq!(
346            decode_node_guard(0x7F).unwrap(),
347            (false, NmtState::PreOperational)
348        );
349        assert_eq!(decode_node_guard(0x42), Err(Error::UnknownState));
350    }
351
352    // --- State machine -----------------------------------------------------
353    #[test]
354    fn boots_from_init_to_preoperational() {
355        let mut sm = NmtStateMachine::new();
356        assert_eq!(sm.state(), NmtState::Initialising);
357        assert_eq!(sm.boot(), NmtState::PreOperational);
358    }
359
360    #[test]
361    fn ignores_operational_commands_while_initialising() {
362        let mut sm = NmtStateMachine::new();
363        assert_eq!(
364            sm.apply(NmtCommand::StartRemoteNode),
365            NmtState::Initialising
366        );
367    }
368
369    #[test]
370    fn full_operational_lifecycle() {
371        let mut sm = NmtStateMachine::new();
372        sm.boot();
373        assert_eq!(sm.apply(NmtCommand::StartRemoteNode), NmtState::Operational);
374        assert_eq!(
375            sm.apply(NmtCommand::EnterPreOperational),
376            NmtState::PreOperational
377        );
378        assert_eq!(sm.apply(NmtCommand::StopRemoteNode), NmtState::Stopped);
379        assert_eq!(sm.apply(NmtCommand::StartRemoteNode), NmtState::Operational);
380    }
381
382    #[test]
383    fn reset_returns_to_initialising_from_any_state() {
384        let mut sm = NmtStateMachine::new();
385        sm.boot();
386        sm.apply(NmtCommand::StartRemoteNode);
387        assert_eq!(sm.apply(NmtCommand::ResetNode), NmtState::Initialising);
388        // And a fresh boot works again.
389        assert_eq!(sm.boot(), NmtState::PreOperational);
390
391        sm.apply(NmtCommand::StartRemoteNode);
392        assert_eq!(
393            sm.apply(NmtCommand::ResetCommunication),
394            NmtState::Initialising
395        );
396    }
397
398    #[test]
399    fn repeated_command_is_idempotent() {
400        let mut sm = NmtStateMachine::new();
401        sm.boot();
402        sm.apply(NmtCommand::StartRemoteNode);
403        assert_eq!(sm.apply(NmtCommand::StartRemoteNode), NmtState::Operational);
404    }
405}