Skip to main content

canopen_rs/
node.rs

1//! A CANopen device node — object dictionary + SDO server + NMT state in one
2//! frame-driven type.
3//!
4//! [`Node`] bundles the pieces a device needs and, like the rest of the stack,
5//! is **sans-I/O**: hand each incoming CAN frame to [`Node::on_frame`] and
6//! transmit the [`TxFrame`] it returns (if any). It serves SDO requests against
7//! its object dictionary, tracks NMT state from node-control commands, and
8//! produces boot-up and heartbeat frames — the same logic on a host or an MCU.
9//!
10//! ```no_run
11//! # use canopen_rs::{Address, Entry, NodeId, ObjectDictionary, Value};
12//! # use canopen_rs::node::Node;
13//! # fn bus_recv() -> (u16, [u8; 8]) { (0, [0; 8]) }
14//! # fn bus_send(_cob: u16, _data: &[u8]) {}
15//! let mut od = ObjectDictionary::<16>::new();
16//! od.insert(Address::new(0x1000, 0), Entry::constant(Value::Unsigned32(0x0004_0192))).unwrap();
17//! let mut node = Node::new(NodeId::new(0x10).unwrap(), od);
18//!
19//! let boot = node.boot();            // enter pre-operational, announce boot-up
20//! bus_send(boot.cob_id, boot.data());
21//!
22//! loop {
23//!     let (cob_id, data) = bus_recv();
24//!     if let Some(tx) = node.on_frame(cob_id, &data) {
25//!         bus_send(tx.cob_id, tx.data());
26//!     }
27//! }
28//! ```
29
30use heapless::Vec;
31
32use crate::datatypes::Value;
33use crate::emcy::{self, EmergencyMessage, ErrorRegister};
34use crate::lss::{self, LssAddress, LssSlave};
35use crate::nmt::{self, NmtState, NmtStateMachine};
36use crate::object_dictionary::{Address, ObjectDictionary};
37use crate::pdo::{self, PdoMapping, TransmissionType};
38use crate::sdo::{self, SdoServer};
39use crate::sync::{self, SyncCounter};
40use crate::types::NodeId;
41use crate::{Error, Result};
42
43/// The maximum number of transmit (or receive) PDOs a [`Node`] holds — the four
44/// of the predefined connection set.
45pub const MAX_PDOS: usize = 4;
46
47/// The maximum objects mapped into one PDO: a full eight-byte frame of
48/// one-byte objects.
49pub const MAX_PDO_MAPPING: usize = 8;
50
51/// How many past emergencies a [`Node`] keeps in its pre-defined error field
52/// (object `0x1003`), most-recent-first. Older entries are dropped once full.
53pub const MAX_ERROR_HISTORY: usize = 8;
54
55/// A frame to transmit: an 11-bit COB-ID and up to eight data bytes.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct TxFrame {
58    /// The COB-ID to transmit on.
59    pub cob_id: u16,
60    data: [u8; 8],
61    len: u8,
62}
63
64impl TxFrame {
65    fn new(cob_id: u16, bytes: &[u8]) -> Self {
66        let len = bytes.len().min(8);
67        let mut data = [0u8; 8];
68        data[..len].copy_from_slice(&bytes[..len]);
69        Self {
70            cob_id,
71            data,
72            len: len as u8,
73        }
74    }
75
76    /// The frame's data bytes (its DLC-trimmed payload).
77    pub fn data(&self) -> &[u8] {
78        &self.data[..self.len as usize]
79    }
80}
81
82/// A configured receive PDO: the COB-ID it listens on and its object mapping.
83#[derive(Debug)]
84struct RpdoSlot {
85    cob_id: u16,
86    mapping: PdoMapping<MAX_PDO_MAPPING>,
87}
88
89/// A configured transmit PDO: its COB-ID, object mapping, and trigger type.
90#[derive(Debug)]
91struct TpdoSlot {
92    cob_id: u16,
93    mapping: PdoMapping<MAX_PDO_MAPPING>,
94    transmission: TransmissionType,
95}
96
97/// A CANopen device node: an object dictionary served over SDO, with NMT state,
98/// heartbeat/boot-up production, and PDO exchange.
99#[derive(Debug)]
100pub struct Node<const N: usize> {
101    node_id: NodeId,
102    od: ObjectDictionary<N>,
103    sdo: SdoServer,
104    nmt: NmtStateMachine,
105    rpdos: Vec<RpdoSlot, MAX_PDOS>,
106    tpdos: Vec<TpdoSlot, MAX_PDOS>,
107    lss: Option<LssSlave>,
108    guard_toggle: bool,
109    error_register: u8,
110    error_history: Vec<u32, MAX_ERROR_HISTORY>,
111    sync_producer: Option<SyncCounter>,
112}
113
114impl<const N: usize> Node<N> {
115    /// Create a node with `node_id` serving `od`. It starts in
116    /// [`NmtState::Initialising`]; call [`Node::boot`] to go operational-ready.
117    pub fn new(node_id: NodeId, od: ObjectDictionary<N>) -> Self {
118        Self {
119            node_id,
120            od,
121            sdo: SdoServer::new(node_id),
122            nmt: NmtStateMachine::new(),
123            rpdos: Vec::new(),
124            tpdos: Vec::new(),
125            lss: None,
126            guard_toggle: false,
127            error_register: 0,
128            error_history: Vec::new(),
129            sync_producer: None,
130        }
131    }
132
133    /// Enable LSS with this node's 128-bit identity ([`LssAddress`], object
134    /// `0x1018`). The node then answers LSS master requests on `0x7E5`, letting
135    /// a master (re)assign its node-id over the bus.
136    ///
137    /// A node awaiting an LSS-assigned id should be left in
138    /// [`NmtState::Initialising`] (do not call [`Node::boot`]) so it serves only
139    /// LSS; after the id is assigned, call [`Node::apply_lss_node_id`] then boot.
140    pub fn enable_lss(&mut self, address: LssAddress) {
141        self.lss = Some(LssSlave::new(address, self.node_id.raw()));
142    }
143
144    /// Enable LSS on a node that comes up **unconfigured** — it has no node-id
145    /// yet, so it answers *only* LSS (including [`FastscanMaster`] discovery,
146    /// when the master doesn't know its address) and serves no SDO, NMT, or PDO
147    /// traffic until a master assigns an id. Construct the node with any
148    /// placeholder id, call this, and leave it in [`NmtState::Initialising`]
149    /// (do not [`boot`](Node::boot)); once the id is assigned over LSS, call
150    /// [`Node::apply_lss_node_id`] on the node's reset, then boot.
151    ///
152    /// [`FastscanMaster`]: crate::lss::FastscanMaster
153    pub fn enable_lss_unconfigured(&mut self, address: LssAddress) {
154        self.lss = Some(LssSlave::new(address, lss::UNCONFIGURED_NODE_ID));
155    }
156
157    /// Whether LSS is enabled and the node is still unconfigured (no node-id),
158    /// so it should serve only LSS.
159    fn lss_unconfigured(&self) -> bool {
160        matches!(&self.lss, Some(l) if l.node_id() == lss::UNCONFIGURED_NODE_ID)
161    }
162
163    /// Change the node-id, rebuilding the SDO server for the new COB-IDs. Call
164    /// on the reset that follows an LSS reconfiguration.
165    pub fn set_node_id(&mut self, node_id: NodeId) {
166        self.node_id = node_id;
167        self.sdo = SdoServer::new(node_id);
168    }
169
170    /// Adopt a node-id assigned over LSS: if the LSS slave holds a valid pending
171    /// id, apply it (rebuilding the SDO server) and return it. Call this on the
172    /// node's reset after an LSS configuration.
173    pub fn apply_lss_node_id(&mut self) -> Option<NodeId> {
174        let pending = self.lss.as_ref()?.pending_node_id();
175        let node_id = NodeId::new(pending).ok()?;
176        self.set_node_id(node_id);
177        if let Some(lss) = &mut self.lss {
178            lss.adopt_pending();
179        }
180        Some(node_id)
181    }
182
183    /// The LSS slave, if LSS is enabled (e.g. to read its pending node-id).
184    pub fn lss(&self) -> Option<&LssSlave> {
185        self.lss.as_ref()
186    }
187
188    /// Configure a receive PDO: when a frame arrives on `cob_id` (while
189    /// operational), its bytes are unpacked into the mapped objects.
190    ///
191    /// Returns [`Error::MappingFull`] once [`MAX_PDOS`] receive PDOs are set.
192    pub fn add_rpdo(&mut self, cob_id: u16, mapping: PdoMapping<MAX_PDO_MAPPING>) -> Result<()> {
193        self.rpdos
194            .push(RpdoSlot { cob_id, mapping })
195            .map_err(|_| Error::MappingFull)
196    }
197
198    /// Configure a transmit PDO: [`Node::sync_tpdos`] packs and emits it on SYNC
199    /// (for synchronous types) and [`Node::tpdo`] emits it on demand.
200    ///
201    /// Returns [`Error::MappingFull`] once [`MAX_PDOS`] transmit PDOs are set.
202    pub fn add_tpdo(
203        &mut self,
204        cob_id: u16,
205        mapping: PdoMapping<MAX_PDO_MAPPING>,
206        transmission: TransmissionType,
207    ) -> Result<()> {
208        self.tpdos
209            .push(TpdoSlot {
210                cob_id,
211                mapping,
212                transmission,
213            })
214            .map_err(|_| Error::MappingFull)
215    }
216
217    /// (Re)build the PDO configuration from the PDO parameter objects in the
218    /// object dictionary — communication parameters at `0x1400`+ (RPDO) /
219    /// `0x1800`+ (TPDO) and mapping parameters at `0x1600`+ / `0x1A00`+ (CiA 301
220    /// §7.5.2). Call this after a master configures PDOs by writing those
221    /// objects over SDO, so the node picks up the new layout.
222    ///
223    /// Any programmatic PDO configuration is replaced. PDOs whose communication
224    /// COB-ID has the "invalid" bit set are skipped, as are entries that are
225    /// absent or malformed.
226    pub fn configure_pdos_from_od(&mut self) {
227        self.rpdos.clear();
228        self.tpdos.clear();
229        for n in 0..MAX_PDOS as u16 {
230            if let Some(slot) = build_rpdo(&self.od, n) {
231                let _ = self.rpdos.push(slot);
232            }
233            if let Some(slot) = build_tpdo(&self.od, n) {
234                let _ = self.tpdos.push(slot);
235            }
236        }
237    }
238
239    /// Take the address of the object a master most recently wrote over SDO,
240    /// clearing it. Poll after [`Node::on_frame`] to react to configuration
241    /// writes — for example, re-read the PDO layout when a PDO parameter object
242    /// (`0x1400`–`0x1BFF`) changes:
243    ///
244    /// ```
245    /// # use canopen_rs::{node::Node, NodeId, ObjectDictionary};
246    /// # let mut node = Node::new(NodeId::new(1).unwrap(), ObjectDictionary::<8>::new());
247    /// # let (cob_id, data) = (0u16, [0u8; 8]);
248    /// node.on_frame(cob_id, &data);
249    /// if let Some(addr) = node.take_written_object() {
250    ///     if (0x1400..=0x1BFF).contains(&addr.index) {
251    ///         node.configure_pdos_from_od();
252    ///     }
253    /// }
254    /// ```
255    pub fn take_written_object(&mut self) -> Option<crate::object_dictionary::Address> {
256        self.sdo.take_write()
257    }
258
259    /// This node's id.
260    pub fn node_id(&self) -> NodeId {
261        self.node_id
262    }
263
264    /// The current NMT state.
265    pub fn state(&self) -> NmtState {
266        self.nmt.state()
267    }
268
269    /// Borrow the object dictionary (e.g. to publish process data).
270    pub fn od(&self) -> &ObjectDictionary<N> {
271        &self.od
272    }
273
274    /// Mutably borrow the object dictionary.
275    pub fn od_mut(&mut self) -> &mut ObjectDictionary<N> {
276        &mut self.od
277    }
278
279    /// Finish initialisation: enter pre-operational and return the boot-up
280    /// frame to transmit (`0x700 + node`, data `0x00`).
281    pub fn boot(&mut self) -> TxFrame {
282        self.nmt.boot();
283        self.guard_toggle = false;
284        TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &nmt::BOOTUP_FRAME)
285    }
286
287    /// The node-guarding response frame (`0x700 + node`), reporting the current
288    /// state with an alternating toggle bit. Call this when the master polls the
289    /// node with an RTR frame on that COB-ID (the legacy error-control
290    /// alternative to [`Node::heartbeat`]).
291    pub fn node_guard_response(&mut self) -> TxFrame {
292        let byte = nmt::encode_node_guard(self.nmt.state(), self.guard_toggle);
293        self.guard_toggle = !self.guard_toggle;
294        TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &[byte])
295    }
296
297    /// The heartbeat frame for the current state. Transmit it on your heartbeat
298    /// timer (the producer heartbeat time lives in object `0x1017`).
299    pub fn heartbeat(&self) -> TxFrame {
300        TxFrame::new(
301            nmt::heartbeat_cob_id(self.node_id),
302            &nmt::encode_heartbeat(self.nmt.state()),
303        )
304    }
305
306    /// The current error register (object `0x1001`): the bitfield of active
307    /// error classes.
308    pub fn error_register(&self) -> ErrorRegister {
309        ErrorRegister(self.error_register)
310    }
311
312    /// The pre-defined error field (object `0x1003`): recorded emergency codes,
313    /// most-recent-first (up to [`MAX_ERROR_HISTORY`]). The low 16 bits of each
314    /// entry are the emergency error code.
315    pub fn error_history(&self) -> &[u32] {
316        &self.error_history
317    }
318
319    /// Raise an emergency: set `register` (and the generic bit) in the error
320    /// register, record `code` at the front of the pre-defined error field,
321    /// mirror both into the object dictionary (`0x1001` / `0x1003`) when those
322    /// objects exist, and return the EMCY frame to transmit on `0x080 + node`.
323    ///
324    /// `info` carries five manufacturer-specific bytes in the frame. The
325    /// returned frame must still be transmitted by the caller (sans-I/O).
326    pub fn raise_emergency(
327        &mut self,
328        code: u16,
329        register: ErrorRegister,
330        info: [u8; 5],
331    ) -> TxFrame {
332        // Any active error sets the generic bit (CiA 301 §7.5.2.2).
333        self.error_register |= register.0 | ErrorRegister::GENERIC;
334
335        // Record most-recent-first, dropping the oldest once full.
336        if self.error_history.is_full() {
337            self.error_history.pop();
338        }
339        let len = self.error_history.len();
340        let _ = self.error_history.push(0); // room guaranteed by the pop above
341        for i in (0..len).rev() {
342            self.error_history[i + 1] = self.error_history[i];
343        }
344        self.error_history[0] = u32::from(code);
345
346        self.mirror_error_objects();
347        let msg = EmergencyMessage::new(code, ErrorRegister(self.error_register), info);
348        TxFrame::new(emcy::emcy_cob_id(self.node_id), &msg.encode())
349    }
350
351    /// Clear all errors: reset the error register and pre-defined error field,
352    /// mirror the cleared state into the object dictionary, and return an
353    /// *error-reset* EMCY frame (code `0x0000`) to transmit.
354    pub fn clear_errors(&mut self) -> TxFrame {
355        self.error_register = 0;
356        self.error_history.clear();
357        self.mirror_error_objects();
358        TxFrame::new(
359            emcy::emcy_cob_id(self.node_id),
360            &EmergencyMessage::error_reset().encode(),
361        )
362    }
363
364    /// Mirror the error register and pre-defined error field into the object
365    /// dictionary so a master can read them over SDO. Best effort: absent (or
366    /// wrongly-typed) objects are simply skipped, so a node that does not model
367    /// `0x1001` / `0x1003` still works.
368    fn mirror_error_objects(&mut self) {
369        let _ = self.od.set(
370            Address::new(0x1001, 0),
371            Value::Unsigned8(self.error_register),
372        );
373        let count = self.error_history.len() as u8;
374        let _ = self
375            .od
376            .set(Address::new(0x1003, 0), Value::Unsigned8(count));
377        for (i, &entry) in self.error_history.iter().enumerate() {
378            let sub = (i + 1) as u8;
379            let _ = self
380                .od
381                .set(Address::new(0x1003, sub), Value::Unsigned32(entry));
382        }
383    }
384
385    /// Configure this node as the network **SYNC producer**, with the given
386    /// synchronous-counter-overflow value (object `0x1019`): `0` produces
387    /// counter-less (empty) SYNC frames, and `2..=240` a counting SYNC.
388    ///
389    /// Returns [`Error::InvalidSyncCounter`] for the reserved value `1` or any
390    /// value above `240`.
391    pub fn enable_sync_producer(&mut self, counter_overflow: u8) -> Result<()> {
392        self.sync_producer = Some(SyncCounter::new(counter_overflow)?);
393        Ok(())
394    }
395
396    /// Produce the next SYNC frame to broadcast on [`SYNC_COB_ID`](crate::sync::SYNC_COB_ID),
397    /// advancing the counter when one is configured. Returns `None` unless the
398    /// node is a SYNC producer (see [`enable_sync_producer`](Node::enable_sync_producer)).
399    ///
400    /// Call this on your SYNC-period timer. SYNC is a broadcast network service,
401    /// so it is emitted independently of this node's own NMT state; gate the
402    /// call on the network state yourself if you only want SYNC while
403    /// operational.
404    pub fn produce_sync(&mut self) -> Option<TxFrame> {
405        let counter = self.sync_producer.as_mut()?;
406        Some(match counter.advance() {
407            Some(count) => TxFrame::new(sync::SYNC_COB_ID, &sync::encode_counter(count)),
408            None => TxFrame::new(sync::SYNC_COB_ID, &[]),
409        })
410    }
411
412    /// Process an incoming CAN frame, returning a response to transmit, if any.
413    ///
414    /// Handles NMT node-control (`0x000`), SDO requests (`0x600 + node`), LSS
415    /// master requests (`0x7E5`, when enabled), and received PDOs. SDO is served
416    /// only in pre-operational and operational states, and PDOs only in
417    /// operational, per CiA 301; LSS is served regardless of NMT state. Frames
418    /// for other COB-IDs are ignored.
419    ///
420    /// While the node is LSS-unconfigured (see [`Node::enable_lss_unconfigured`])
421    /// it serves *only* LSS — everything else is ignored until a node-id is
422    /// assigned — since its data COB-IDs are not yet meaningful.
423    pub fn on_frame(&mut self, cob_id: u16, data: &[u8]) -> Option<TxFrame> {
424        if cob_id == lss::LSS_MASTER_COB_ID {
425            return self.on_lss(data);
426        }
427        if self.lss_unconfigured() {
428            return None; // no node-id yet: LSS only
429        }
430        if cob_id == nmt::NMT_COMMAND_COB_ID {
431            self.on_nmt(data);
432            None
433        } else if cob_id == self.sdo.request_cob_id() {
434            self.on_sdo(data)
435        } else {
436            self.on_rpdo(cob_id, data);
437            None
438        }
439    }
440
441    fn on_lss(&mut self, data: &[u8]) -> Option<TxFrame> {
442        let lss = self.lss.as_mut()?;
443        if data.len() > 8 {
444            return None;
445        }
446        let mut frame: lss::LssFrame = [0u8; 8];
447        frame[..data.len()].copy_from_slice(data);
448        lss.handle(&frame)
449            .map(|resp| TxFrame::new(lss::LSS_SLAVE_COB_ID, &resp))
450    }
451
452    /// The synchronous transmit PDOs to send in response to a SYNC.
453    ///
454    /// Packs every configured TPDO with a synchronous transmission type from the
455    /// current object dictionary. Empty unless the node is operational — PDOs
456    /// are exchanged only in that state (CiA 301 §7.3.5).
457    pub fn sync_tpdos(&self) -> Vec<TxFrame, MAX_PDOS> {
458        let mut frames = Vec::new();
459        if self.nmt.state() != NmtState::Operational {
460            return frames;
461        }
462        for slot in &self.tpdos {
463            if is_synchronous(slot.transmission) {
464                if let Some(frame) = self.build_tpdo(slot) {
465                    // Capacity matches self.tpdos, so this never overflows.
466                    let _ = frames.push(frame);
467                }
468            }
469        }
470        frames
471    }
472
473    /// Emit transmit PDO `index` on demand (an event-driven transmission), or
474    /// `None` if there is no such PDO or the node is not operational.
475    pub fn tpdo(&self, index: usize) -> Option<TxFrame> {
476        if self.nmt.state() != NmtState::Operational {
477            return None;
478        }
479        self.build_tpdo(self.tpdos.get(index)?)
480    }
481
482    fn build_tpdo(&self, slot: &TpdoSlot) -> Option<TxFrame> {
483        if slot.mapping.is_empty() {
484            return None;
485        }
486        let mut buf = [0u8; 8];
487        let len = pdo::pack(&slot.mapping, &self.od, &mut buf).ok()?;
488        Some(TxFrame::new(slot.cob_id, &buf[..len]))
489    }
490
491    fn on_rpdo(&mut self, cob_id: u16, data: &[u8]) {
492        // PDOs are exchanged only in the operational state.
493        if self.nmt.state() != NmtState::Operational {
494            return;
495        }
496        if let Some(i) = self.rpdos.iter().position(|r| r.cob_id == cob_id) {
497            // Disjoint field borrows: `rpdos` (shared) and `od` (mutable).
498            let _ = pdo::unpack(&self.rpdos[i].mapping, &mut self.od, data);
499        }
500    }
501
502    fn on_nmt(&mut self, data: &[u8]) {
503        // An NMT node-control frame is [command specifier, target node].
504        if data.len() < 2 {
505            return;
506        }
507        if let Ok((command, target)) = nmt::decode_command(&[data[0], data[1]]) {
508            if target == NodeId::BROADCAST || target == self.node_id {
509                self.nmt.apply(command);
510            }
511        }
512    }
513
514    fn on_sdo(&mut self, data: &[u8]) -> Option<TxFrame> {
515        // SDO is inactive outside pre-operational / operational (CiA 301 §7.3).
516        if !matches!(
517            self.nmt.state(),
518            NmtState::PreOperational | NmtState::Operational
519        ) {
520            return None;
521        }
522        let mut payload: sdo::SdoPayload = [0u8; 8];
523        if data.len() > payload.len() {
524            return None;
525        }
526        payload[..data.len()].copy_from_slice(data);
527        let response = self.sdo.handle(&mut self.od, &payload)?;
528        Some(TxFrame::new(self.sdo.response_cob_id(), &response))
529    }
530}
531
532/// Whether a transmission type is SYNC-triggered (as opposed to event-driven).
533fn is_synchronous(transmission: TransmissionType) -> bool {
534    matches!(
535        transmission,
536        TransmissionType::SynchronousAcyclic | TransmissionType::SynchronousCyclic(_)
537    )
538}
539
540// --- Reading PDO parameter objects out of the object dictionary -------------
541
542fn od_u32<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u32> {
543    match od
544        .read(crate::object_dictionary::Address::new(index, sub))
545        .ok()?
546    {
547        crate::datatypes::Value::Unsigned32(v) => Some(v),
548        _ => None,
549    }
550}
551
552fn od_u8<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u8> {
553    match od
554        .read(crate::object_dictionary::Address::new(index, sub))
555        .ok()?
556    {
557        crate::datatypes::Value::Unsigned8(v) => Some(v),
558        _ => None,
559    }
560}
561
562/// Read a mapping-parameter record (`sub 0` = count, `sub 1..=count` = the
563/// `0xIIII_SSLL` mapping entries) into a [`PdoMapping`].
564fn read_pdo_mapping<const N: usize>(
565    od: &ObjectDictionary<N>,
566    map_index: u16,
567) -> Option<PdoMapping<MAX_PDO_MAPPING>> {
568    let count = od_u8(od, map_index, 0)?;
569    let mut mapping = PdoMapping::new();
570    for sub in 1..=count {
571        mapping
572            .push(pdo::MappingEntry::from_u32(od_u32(od, map_index, sub)?))
573            .ok()?;
574    }
575    Some(mapping)
576}
577
578fn build_rpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<RpdoSlot> {
579    let cob_id = od_u32(od, 0x1400 + n, 1)?;
580    if !pdo::pdo_is_valid(cob_id) {
581        return None;
582    }
583    Some(RpdoSlot {
584        cob_id: pdo::pdo_can_id(cob_id),
585        mapping: read_pdo_mapping(od, 0x1600 + n)?,
586    })
587}
588
589fn build_tpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<TpdoSlot> {
590    let cob_id = od_u32(od, 0x1800 + n, 1)?;
591    if !pdo::pdo_is_valid(cob_id) {
592        return None;
593    }
594    // Transmission type (comm sub 2); default to event-driven if absent/invalid.
595    let transmission = od_u8(od, 0x1800 + n, 2)
596        .and_then(|b| TransmissionType::from_byte(b).ok())
597        .unwrap_or(TransmissionType::EventDrivenProfile);
598    Some(TpdoSlot {
599        cob_id: pdo::pdo_can_id(cob_id),
600        mapping: read_pdo_mapping(od, 0x1A00 + n)?,
601        transmission,
602    })
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use crate::object_dictionary::{Address, Entry};
609    use crate::pdo::MappingEntry;
610    use crate::sdo::{encode_download_expedited, encode_upload_request};
611    use crate::{DataType, NmtCommand, Value};
612
613    fn start<const N: usize>(n: &mut Node<N>) {
614        n.on_frame(
615            nmt::NMT_COMMAND_COB_ID,
616            &[NmtCommand::StartRemoteNode as u8, 0x10],
617        );
618    }
619
620    fn od() -> ObjectDictionary<8> {
621        let mut od = ObjectDictionary::new();
622        od.insert(
623            Address::new(0x1000, 0),
624            Entry::constant(Value::Unsigned32(0x192)),
625        )
626        .unwrap();
627        od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
628            .unwrap();
629        od
630    }
631
632    fn node() -> Node<8> {
633        Node::new(NodeId::new(0x10).unwrap(), od())
634    }
635
636    #[test]
637    fn boots_from_init_to_preop_and_announces() {
638        let mut n = node();
639        assert_eq!(n.state(), NmtState::Initialising);
640        let boot = n.boot();
641        assert_eq!(n.state(), NmtState::PreOperational);
642        assert_eq!(boot.cob_id, 0x710); // 0x700 + node
643        assert_eq!(boot.data(), &[0x00]);
644    }
645
646    #[test]
647    fn heartbeat_reflects_state() {
648        let mut n = node();
649        n.boot();
650        assert_eq!(n.heartbeat().data(), &[0x7F]); // pre-operational
651        n.on_frame(
652            nmt::NMT_COMMAND_COB_ID,
653            &[NmtCommand::StartRemoteNode as u8, 0x10],
654        );
655        assert_eq!(n.state(), NmtState::Operational);
656        assert_eq!(n.heartbeat().data(), &[0x05]); // operational
657    }
658
659    #[test]
660    fn node_guard_response_toggles() {
661        let mut n = node();
662        n.boot();
663        start(&mut n); // operational (0x05)
664        let first = n.node_guard_response();
665        assert_eq!(first.cob_id, 0x710); // 0x700 + node
666        assert_eq!(first.data(), &[0x05]); // toggle clear
667        assert_eq!(n.node_guard_response().data(), &[0x85]); // toggle set
668        assert_eq!(n.node_guard_response().data(), &[0x05]); // toggles back
669    }
670
671    #[test]
672    fn serves_sdo_read_when_preoperational() {
673        let mut n = node();
674        n.boot();
675        let req = encode_upload_request(Address::new(0x1000, 0));
676        let resp = n.on_frame(0x610, &req).expect("SDO response");
677        assert_eq!(resp.cob_id, 0x590); // 0x580 + node
678        let (_, value) = crate::sdo::decode_upload_expedited_response(
679            resp.data().try_into().unwrap(),
680            DataType::Unsigned32,
681        )
682        .unwrap();
683        assert_eq!(value, Value::Unsigned32(0x192));
684    }
685
686    #[test]
687    fn ignores_sdo_before_boot() {
688        let mut n = node(); // still Initialising
689        let req = encode_upload_request(Address::new(0x1000, 0));
690        assert!(n.on_frame(0x610, &req).is_none());
691    }
692
693    #[test]
694    fn ignores_sdo_when_stopped() {
695        let mut n = node();
696        n.boot();
697        n.on_frame(
698            nmt::NMT_COMMAND_COB_ID,
699            &[NmtCommand::StopRemoteNode as u8, 0x10],
700        );
701        assert_eq!(n.state(), NmtState::Stopped);
702        let req = encode_upload_request(Address::new(0x1000, 0));
703        assert!(n.on_frame(0x610, &req).is_none());
704    }
705
706    #[test]
707    fn nmt_command_for_other_node_is_ignored() {
708        let mut n = node();
709        n.boot();
710        // Start addressed to node 0x20, not us.
711        n.on_frame(
712            nmt::NMT_COMMAND_COB_ID,
713            &[NmtCommand::StartRemoteNode as u8, 0x20],
714        );
715        assert_eq!(n.state(), NmtState::PreOperational); // unchanged
716    }
717
718    #[test]
719    fn broadcast_nmt_applies() {
720        let mut n = node();
721        n.boot();
722        n.on_frame(
723            nmt::NMT_COMMAND_COB_ID,
724            &[NmtCommand::StartRemoteNode as u8, 0x00],
725        );
726        assert_eq!(n.state(), NmtState::Operational);
727    }
728
729    #[test]
730    fn serves_sdo_write_and_updates_od() {
731        let mut n = node();
732        n.boot();
733        let req =
734            encode_download_expedited(Address::new(0x1017, 0), &Value::Unsigned16(1234)).unwrap();
735        assert!(n.on_frame(0x610, &req).is_some());
736        assert_eq!(
737            n.od().read(Address::new(0x1017, 0)).unwrap(),
738            Value::Unsigned16(1234)
739        );
740    }
741
742    #[test]
743    fn ignores_unrelated_cob_id() {
744        let mut n = node();
745        n.boot();
746        assert!(n.on_frame(0x123, &[0; 8]).is_none());
747    }
748
749    // --- PDO ---------------------------------------------------------------
750    fn pdo_od() -> ObjectDictionary<8> {
751        let mut od = ObjectDictionary::new();
752        // TPDO source objects (readable) and an RPDO target (writable).
753        od.insert(
754            Address::new(0x6000, 1),
755            Entry::rw(Value::Unsigned16(0xBEEF)),
756        )
757        .unwrap();
758        od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
759            .unwrap();
760        od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
761            .unwrap();
762        od
763    }
764
765    fn mapping(entries: &[(u16, u8, u8)]) -> PdoMapping<MAX_PDO_MAPPING> {
766        let mut m = PdoMapping::new();
767        for &(index, sub, bits) in entries {
768            m.push(MappingEntry::new(index, sub, bits)).unwrap();
769        }
770        m
771    }
772
773    #[test]
774    fn tpdo_transmits_only_when_operational() {
775        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
776        n.add_tpdo(
777            0x18A,
778            mapping(&[(0x6000, 1, 16), (0x6000, 2, 8)]),
779            TransmissionType::SynchronousAcyclic,
780        )
781        .unwrap();
782        n.boot();
783
784        // Pre-operational: no PDO traffic.
785        assert!(n.sync_tpdos().is_empty());
786
787        start(&mut n);
788        let frames = n.sync_tpdos();
789        assert_eq!(frames.len(), 1);
790        assert_eq!(frames[0].cob_id, 0x18A);
791        // U16 0xBEEF little-endian then U8 0x42.
792        assert_eq!(frames[0].data(), &[0xEF, 0xBE, 0x42]);
793    }
794
795    #[test]
796    fn event_tpdo_by_index() {
797        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
798        // Event-driven type is not emitted by sync_tpdos, only by tpdo().
799        n.add_tpdo(
800            0x18A,
801            mapping(&[(0x6000, 2, 8)]),
802            TransmissionType::EventDrivenProfile,
803        )
804        .unwrap();
805        n.boot();
806        start(&mut n);
807        assert!(n.sync_tpdos().is_empty());
808        assert_eq!(n.tpdo(0).unwrap().data(), &[0x42]);
809        assert!(n.tpdo(1).is_none());
810    }
811
812    #[test]
813    fn rpdo_applies_only_when_operational() {
814        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
815        n.add_rpdo(0x20A, mapping(&[(0x6200, 1, 16)])).unwrap();
816        n.boot();
817
818        // Pre-operational: the RPDO is ignored.
819        assert!(n.on_frame(0x20A, &[0x34, 0x12]).is_none());
820        assert_eq!(
821            n.od().read(Address::new(0x6200, 1)).unwrap(),
822            Value::Unsigned16(0)
823        );
824
825        // Operational: the frame is unpacked into the object dictionary.
826        start(&mut n);
827        n.on_frame(0x20A, &[0x34, 0x12]);
828        assert_eq!(
829            n.od().read(Address::new(0x6200, 1)).unwrap(),
830            Value::Unsigned16(0x1234)
831        );
832    }
833
834    #[test]
835    fn pdo_capacity_is_enforced() {
836        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
837        for _ in 0..MAX_PDOS {
838            n.add_tpdo(
839                0x18A,
840                mapping(&[(0x6000, 2, 8)]),
841                TransmissionType::SynchronousAcyclic,
842            )
843            .unwrap();
844        }
845        assert_eq!(
846            n.add_tpdo(
847                0x18A,
848                mapping(&[(0x6000, 2, 8)]),
849                TransmissionType::SynchronousAcyclic
850            ),
851            Err(Error::MappingFull)
852        );
853    }
854
855    // --- LSS ---------------------------------------------------------------
856    use crate::lss::{self, encode_configure_node_id, encode_switch_global, LssAddress, LssState};
857
858    fn lss_address() -> LssAddress {
859        LssAddress {
860            vendor_id: 0x1F,
861            product_code: 0x2A,
862            revision_number: 1,
863            serial_number: 0x99,
864        }
865    }
866
867    #[test]
868    fn routes_lss_frames_when_enabled() {
869        let mut n = node();
870        n.enable_lss(lss_address());
871        // Switch into configuration via LSS (COB-ID 0x7E5), no response.
872        assert!(n
873            .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
874            .is_none());
875        assert_eq!(n.lss().unwrap().state(), LssState::Configuration);
876    }
877
878    #[test]
879    fn lss_frames_ignored_when_disabled() {
880        let mut n = node(); // LSS not enabled
881        assert!(n
882            .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
883            .is_none());
884        assert!(n.lss().is_none());
885    }
886
887    #[test]
888    fn lss_assigns_node_id_and_moves_sdo_cob_id() {
889        // A node that comes up unconfigured: leave it in Initialising and serve
890        // only LSS until a master assigns an id.
891        let mut n = Node::new(NodeId::new(1).unwrap(), od());
892        n.enable_lss(lss_address());
893        assert_eq!(n.node_id(), NodeId::new(1).unwrap());
894
895        // Master: switch to configuration, then assign node-id 0x20.
896        n.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true));
897        let resp = n
898            .on_frame(lss::LSS_MASTER_COB_ID, &encode_configure_node_id(0x20))
899            .expect("configure response");
900        assert_eq!(resp.cob_id, lss::LSS_SLAVE_COB_ID);
901        assert_eq!(&resp.data()[..2], &[0x11, 0x00]); // configure success
902
903        // On the node's reset, adopt the assigned id — SDO COB-ID moves.
904        assert_eq!(n.apply_lss_node_id(), Some(NodeId::new(0x20).unwrap()));
905        assert_eq!(n.node_id(), NodeId::new(0x20).unwrap());
906
907        n.boot();
908        let req = encode_upload_request(Address::new(0x1000, 0));
909        assert!(n.on_frame(0x601, &req).is_none()); // old COB-ID no longer served
910        assert!(n.on_frame(0x620, &req).is_some()); // new COB-ID (0x600 + 0x20)
911    }
912
913    #[test]
914    fn unconfigured_node_serves_only_lss() {
915        let mut n = Node::new(NodeId::new(1).unwrap(), od());
916        n.enable_lss_unconfigured(lss_address());
917        n.boot(); // even booted, an unconfigured node serves no data traffic
918
919        // The placeholder id's SDO COB-ID is not served while unconfigured.
920        let req = encode_upload_request(Address::new(0x1000, 0));
921        assert!(n.on_frame(0x601, &req).is_none());
922
923        // ...but LSS still is.
924        n.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true));
925        assert_eq!(n.lss().unwrap().state(), LssState::Configuration);
926    }
927
928    #[test]
929    fn fastscan_discovers_then_configures_node_via_on_frame() {
930        use crate::lss::FastscanMaster;
931        let addr = lss_address();
932        let mut n = Node::new(NodeId::new(1).unwrap(), od());
933        n.enable_lss_unconfigured(addr);
934
935        // The master discovers the unknown address purely through on_frame.
936        let mut master = FastscanMaster::new();
937        let mut steps = 0;
938        while let Some(req) = master.next_request() {
939            let answered = n.on_frame(lss::LSS_MASTER_COB_ID, &req).is_some();
940            master.on_response(answered);
941            steps += 1;
942            assert!(steps < 200, "fastscan did not converge");
943        }
944        assert!(master.found());
945        assert_eq!(master.address(), addr);
946        assert_eq!(n.lss().unwrap().state(), LssState::Configuration);
947
948        // Assign an id; the node then serves SDO on the new COB-ID.
949        n.on_frame(lss::LSS_MASTER_COB_ID, &encode_configure_node_id(0x33));
950        assert_eq!(n.apply_lss_node_id(), Some(NodeId::new(0x33).unwrap()));
951        n.boot();
952        let req = encode_upload_request(Address::new(0x1000, 0));
953        assert!(n.on_frame(0x600 + 0x33, &req).is_some());
954    }
955
956    // --- PDO configuration from the object dictionary ----------------------
957    #[test]
958    fn configures_pdos_from_od() {
959        let mut od = ObjectDictionary::<16>::new();
960        // RPDO1: comm 0x1400 (COB-ID 0x210, valid), mapping 0x1600 -> 0x6200/1.
961        od.insert(Address::new(0x1400, 1), Entry::rw(Value::Unsigned32(0x210)))
962            .unwrap();
963        od.insert(Address::new(0x1600, 0), Entry::rw(Value::Unsigned8(1)))
964            .unwrap();
965        od.insert(
966            Address::new(0x1600, 1),
967            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6200, 1, 16).to_u32())),
968        )
969        .unwrap();
970        // TPDO1: comm 0x1800 (COB-ID 0x190, valid, sync-cyclic), mapping -> 0x6000/1.
971        od.insert(Address::new(0x1800, 1), Entry::rw(Value::Unsigned32(0x190)))
972            .unwrap();
973        od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
974            .unwrap(); // transmission type 1
975        od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
976            .unwrap();
977        od.insert(
978            Address::new(0x1A00, 1),
979            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
980        )
981        .unwrap();
982        // The mapped process-data objects.
983        od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
984            .unwrap();
985        od.insert(
986            Address::new(0x6000, 1),
987            Entry::rw(Value::Unsigned16(0xBEEF)),
988        )
989        .unwrap();
990
991        let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
992        n.configure_pdos_from_od();
993        n.boot();
994        start(&mut n);
995
996        // The RPDO now applies to the OD…
997        n.on_frame(0x210, &[0x34, 0x12]);
998        assert_eq!(
999            n.od().read(Address::new(0x6200, 1)).unwrap(),
1000            Value::Unsigned16(0x1234)
1001        );
1002        // …and the (synchronous) TPDO is emitted on SYNC.
1003        let tpdos = n.sync_tpdos();
1004        assert_eq!(tpdos.len(), 1);
1005        assert_eq!(tpdos[0].cob_id, 0x190);
1006        assert_eq!(tpdos[0].data(), &[0xEF, 0xBE]);
1007    }
1008
1009    #[test]
1010    fn skips_pdo_with_invalid_cob_id() {
1011        let mut od = ObjectDictionary::<8>::new();
1012        // TPDO1 with the COB-ID validity bit (31) set — disabled.
1013        od.insert(
1014            Address::new(0x1800, 1),
1015            Entry::rw(Value::Unsigned32(0x8000_0190)),
1016        )
1017        .unwrap();
1018        od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
1019            .unwrap();
1020        od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
1021            .unwrap();
1022        od.insert(
1023            Address::new(0x1A00, 1),
1024            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
1025        )
1026        .unwrap();
1027        od.insert(
1028            Address::new(0x6000, 1),
1029            Entry::rw(Value::Unsigned16(0xBEEF)),
1030        )
1031        .unwrap();
1032
1033        let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
1034        n.configure_pdos_from_od();
1035        n.boot();
1036        start(&mut n);
1037        assert!(n.sync_tpdos().is_empty()); // the invalid TPDO was not configured
1038    }
1039
1040    #[test]
1041    fn reacts_to_a_pdo_parameter_write() {
1042        // TPDO1 params present but initially disabled (COB-ID invalid bit set).
1043        let mut od = ObjectDictionary::<16>::new();
1044        od.insert(
1045            Address::new(0x1800, 1),
1046            Entry::rw(Value::Unsigned32(0x8000_0190)),
1047        )
1048        .unwrap();
1049        od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
1050            .unwrap();
1051        od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
1052            .unwrap();
1053        od.insert(
1054            Address::new(0x1A00, 1),
1055            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
1056        )
1057        .unwrap();
1058        od.insert(
1059            Address::new(0x6000, 1),
1060            Entry::rw(Value::Unsigned16(0xBEEF)),
1061        )
1062        .unwrap();
1063
1064        let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
1065        n.configure_pdos_from_od();
1066        n.boot();
1067        start(&mut n);
1068        assert!(n.sync_tpdos().is_empty());
1069
1070        // A master enables the TPDO by writing a valid COB-ID to 0x1800/1.
1071        let req =
1072            encode_download_expedited(Address::new(0x1800, 1), &Value::Unsigned32(0x190)).unwrap();
1073        n.on_frame(0x610, &req);
1074
1075        // The node notices the PDO-parameter write and reconfigures.
1076        let written = n.take_written_object().expect("a write was recorded");
1077        assert_eq!(written, Address::new(0x1800, 1));
1078        assert_eq!(n.take_written_object(), None); // reported once
1079        if (0x1400..=0x1BFF).contains(&written.index) {
1080            n.configure_pdos_from_od();
1081        }
1082
1083        // The TPDO is now active.
1084        let tpdos = n.sync_tpdos();
1085        assert_eq!(tpdos.len(), 1);
1086        assert_eq!(tpdos[0].cob_id, 0x190);
1087    }
1088
1089    // --- Emergencies (EMCY + error register / pre-defined error field) ------
1090    fn od_with_error_objects() -> ObjectDictionary<8> {
1091        let mut od = ObjectDictionary::new();
1092        od.insert(Address::new(0x1001, 0), Entry::ro(Value::Unsigned8(0)))
1093            .unwrap();
1094        od.insert(Address::new(0x1003, 0), Entry::ro(Value::Unsigned8(0)))
1095            .unwrap();
1096        od.insert(Address::new(0x1003, 1), Entry::ro(Value::Unsigned32(0)))
1097            .unwrap();
1098        od.insert(Address::new(0x1003, 2), Entry::ro(Value::Unsigned32(0)))
1099            .unwrap();
1100        od
1101    }
1102
1103    #[test]
1104    fn emergency_sets_register_and_emits_frame() {
1105        let mut n = node();
1106        let tx = n.raise_emergency(
1107            emcy::error_code::VOLTAGE,
1108            ErrorRegister(ErrorRegister::VOLTAGE),
1109            [0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
1110        );
1111        // The generic bit is set alongside the class bit.
1112        assert_eq!(
1113            n.error_register(),
1114            ErrorRegister(ErrorRegister::GENERIC | ErrorRegister::VOLTAGE)
1115        );
1116        assert_eq!(tx.cob_id, 0x090); // 0x080 + node 0x10
1117        let msg = EmergencyMessage::decode(tx.data()).unwrap();
1118        assert_eq!(msg.error_code, emcy::error_code::VOLTAGE);
1119        assert_eq!(
1120            msg.error_register.0,
1121            ErrorRegister::GENERIC | ErrorRegister::VOLTAGE
1122        );
1123        assert_eq!(msg.vendor_specific, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]);
1124        assert_eq!(n.error_history(), &[u32::from(emcy::error_code::VOLTAGE)]);
1125    }
1126
1127    #[test]
1128    fn error_history_is_most_recent_first_and_bounded() {
1129        let mut n = node();
1130        let total = MAX_ERROR_HISTORY as u16 + 2;
1131        for i in 0..total {
1132            n.raise_emergency(0x1000 + i, ErrorRegister::NONE, [0; 5]);
1133        }
1134        assert_eq!(n.error_history().len(), MAX_ERROR_HISTORY);
1135        assert_eq!(n.error_history()[0], u32::from(0x1000 + total - 1)); // newest first
1136        let oldest_kept = 0x1000 + total - MAX_ERROR_HISTORY as u16;
1137        assert_eq!(*n.error_history().last().unwrap(), u32::from(oldest_kept));
1138    }
1139
1140    #[test]
1141    fn emergency_mirrors_into_object_dictionary() {
1142        let mut n = Node::new(NodeId::new(0x10).unwrap(), od_with_error_objects());
1143        n.raise_emergency(0x5530, ErrorRegister(ErrorRegister::DEVICE_PROFILE), [0; 5]);
1144
1145        assert_eq!(
1146            n.od().read(Address::new(0x1001, 0)).unwrap(),
1147            Value::Unsigned8(ErrorRegister::GENERIC | ErrorRegister::DEVICE_PROFILE)
1148        );
1149        assert_eq!(
1150            n.od().read(Address::new(0x1003, 0)).unwrap(),
1151            Value::Unsigned8(1)
1152        );
1153        assert_eq!(
1154            n.od().read(Address::new(0x1003, 1)).unwrap(),
1155            Value::Unsigned32(0x5530)
1156        );
1157    }
1158
1159    #[test]
1160    fn clear_errors_resets_register_history_and_od() {
1161        let mut n = Node::new(NodeId::new(0x10).unwrap(), od_with_error_objects());
1162        n.raise_emergency(
1163            0x3210,
1164            ErrorRegister(ErrorRegister::VOLTAGE),
1165            [1, 2, 3, 4, 5],
1166        );
1167
1168        let tx = n.clear_errors();
1169        assert_eq!(n.error_register(), ErrorRegister::NONE);
1170        assert!(n.error_history().is_empty());
1171        let msg = EmergencyMessage::decode(tx.data()).unwrap();
1172        assert!(msg.is_error_reset());
1173        assert_eq!(tx.cob_id, 0x090);
1174        assert_eq!(
1175            n.od().read(Address::new(0x1001, 0)).unwrap(),
1176            Value::Unsigned8(0)
1177        );
1178        assert_eq!(
1179            n.od().read(Address::new(0x1003, 0)).unwrap(),
1180            Value::Unsigned8(0)
1181        );
1182    }
1183
1184    #[test]
1185    fn emergency_without_error_objects_still_works() {
1186        // node()'s OD has no 0x1001/0x1003 — mirroring is best-effort and must
1187        // neither panic nor suppress the frame.
1188        let mut n = node();
1189        let tx = n.raise_emergency(0x8130, ErrorRegister(ErrorRegister::COMMUNICATION), [0; 5]);
1190        assert_eq!(tx.cob_id, 0x090);
1191        assert_eq!(
1192            n.error_register(),
1193            ErrorRegister(ErrorRegister::GENERIC | ErrorRegister::COMMUNICATION)
1194        );
1195    }
1196
1197    // --- SYNC producer -----------------------------------------------------
1198    #[test]
1199    fn sync_production_is_off_by_default() {
1200        let mut n = node();
1201        assert!(n.produce_sync().is_none());
1202    }
1203
1204    #[test]
1205    fn counterless_sync_producer_emits_empty_frames() {
1206        let mut n = node();
1207        n.enable_sync_producer(0).unwrap();
1208        let tx = n.produce_sync().expect("a SYNC frame");
1209        assert_eq!(tx.cob_id, sync::SYNC_COB_ID); // 0x080
1210        assert_eq!(tx.data(), &[] as &[u8]); // counter-less
1211    }
1212
1213    #[test]
1214    fn counting_sync_producer_cycles_and_wraps() {
1215        let mut n = node();
1216        n.enable_sync_producer(3).unwrap();
1217        let counts: [&[u8]; 4] = [&[1], &[2], &[3], &[1]];
1218        for expected in counts {
1219            let tx = n.produce_sync().unwrap();
1220            assert_eq!(tx.cob_id, sync::SYNC_COB_ID);
1221            assert_eq!(tx.data(), expected);
1222        }
1223    }
1224
1225    #[test]
1226    fn enable_sync_producer_rejects_reserved_overflow() {
1227        let mut n = node();
1228        assert_eq!(n.enable_sync_producer(1), Err(Error::InvalidSyncCounter));
1229        assert!(n.produce_sync().is_none()); // still not a producer
1230    }
1231}