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}