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::lss::{self, LssAddress, LssSlave};
33use crate::nmt::{self, NmtState, NmtStateMachine};
34use crate::object_dictionary::ObjectDictionary;
35use crate::pdo::{self, PdoMapping, TransmissionType};
36use crate::sdo::{self, SdoServer};
37use crate::types::NodeId;
38use crate::{Error, Result};
39
40/// The maximum number of transmit (or receive) PDOs a [`Node`] holds — the four
41/// of the predefined connection set.
42pub const MAX_PDOS: usize = 4;
43
44/// The maximum objects mapped into one PDO: a full eight-byte frame of
45/// one-byte objects.
46pub const MAX_PDO_MAPPING: usize = 8;
47
48/// A frame to transmit: an 11-bit COB-ID and up to eight data bytes.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct TxFrame {
51    /// The COB-ID to transmit on.
52    pub cob_id: u16,
53    data: [u8; 8],
54    len: u8,
55}
56
57impl TxFrame {
58    fn new(cob_id: u16, bytes: &[u8]) -> Self {
59        let len = bytes.len().min(8);
60        let mut data = [0u8; 8];
61        data[..len].copy_from_slice(&bytes[..len]);
62        Self {
63            cob_id,
64            data,
65            len: len as u8,
66        }
67    }
68
69    /// The frame's data bytes (its DLC-trimmed payload).
70    pub fn data(&self) -> &[u8] {
71        &self.data[..self.len as usize]
72    }
73}
74
75/// A configured receive PDO: the COB-ID it listens on and its object mapping.
76#[derive(Debug)]
77struct RpdoSlot {
78    cob_id: u16,
79    mapping: PdoMapping<MAX_PDO_MAPPING>,
80}
81
82/// A configured transmit PDO: its COB-ID, object mapping, and trigger type.
83#[derive(Debug)]
84struct TpdoSlot {
85    cob_id: u16,
86    mapping: PdoMapping<MAX_PDO_MAPPING>,
87    transmission: TransmissionType,
88}
89
90/// A CANopen device node: an object dictionary served over SDO, with NMT state,
91/// heartbeat/boot-up production, and PDO exchange.
92#[derive(Debug)]
93pub struct Node<const N: usize> {
94    node_id: NodeId,
95    od: ObjectDictionary<N>,
96    sdo: SdoServer,
97    nmt: NmtStateMachine,
98    rpdos: Vec<RpdoSlot, MAX_PDOS>,
99    tpdos: Vec<TpdoSlot, MAX_PDOS>,
100    lss: Option<LssSlave>,
101    guard_toggle: bool,
102}
103
104impl<const N: usize> Node<N> {
105    /// Create a node with `node_id` serving `od`. It starts in
106    /// [`NmtState::Initialising`]; call [`Node::boot`] to go operational-ready.
107    pub fn new(node_id: NodeId, od: ObjectDictionary<N>) -> Self {
108        Self {
109            node_id,
110            od,
111            sdo: SdoServer::new(node_id),
112            nmt: NmtStateMachine::new(),
113            rpdos: Vec::new(),
114            tpdos: Vec::new(),
115            lss: None,
116            guard_toggle: false,
117        }
118    }
119
120    /// Enable LSS with this node's 128-bit identity ([`LssAddress`], object
121    /// `0x1018`). The node then answers LSS master requests on `0x7E5`, letting
122    /// a master (re)assign its node-id over the bus.
123    ///
124    /// A node awaiting an LSS-assigned id should be left in
125    /// [`NmtState::Initialising`] (do not call [`Node::boot`]) so it serves only
126    /// LSS; after the id is assigned, call [`Node::apply_lss_node_id`] then boot.
127    pub fn enable_lss(&mut self, address: LssAddress) {
128        self.lss = Some(LssSlave::new(address, self.node_id.raw()));
129    }
130
131    /// Change the node-id, rebuilding the SDO server for the new COB-IDs. Call
132    /// on the reset that follows an LSS reconfiguration.
133    pub fn set_node_id(&mut self, node_id: NodeId) {
134        self.node_id = node_id;
135        self.sdo = SdoServer::new(node_id);
136    }
137
138    /// Adopt a node-id assigned over LSS: if the LSS slave holds a valid pending
139    /// id, apply it (rebuilding the SDO server) and return it. Call this on the
140    /// node's reset after an LSS configuration.
141    pub fn apply_lss_node_id(&mut self) -> Option<NodeId> {
142        let pending = self.lss.as_ref()?.pending_node_id();
143        let node_id = NodeId::new(pending).ok()?;
144        self.set_node_id(node_id);
145        if let Some(lss) = &mut self.lss {
146            lss.adopt_pending();
147        }
148        Some(node_id)
149    }
150
151    /// The LSS slave, if LSS is enabled (e.g. to read its pending node-id).
152    pub fn lss(&self) -> Option<&LssSlave> {
153        self.lss.as_ref()
154    }
155
156    /// Configure a receive PDO: when a frame arrives on `cob_id` (while
157    /// operational), its bytes are unpacked into the mapped objects.
158    ///
159    /// Returns [`Error::MappingFull`] once [`MAX_PDOS`] receive PDOs are set.
160    pub fn add_rpdo(&mut self, cob_id: u16, mapping: PdoMapping<MAX_PDO_MAPPING>) -> Result<()> {
161        self.rpdos
162            .push(RpdoSlot { cob_id, mapping })
163            .map_err(|_| Error::MappingFull)
164    }
165
166    /// Configure a transmit PDO: [`Node::sync_tpdos`] packs and emits it on SYNC
167    /// (for synchronous types) and [`Node::tpdo`] emits it on demand.
168    ///
169    /// Returns [`Error::MappingFull`] once [`MAX_PDOS`] transmit PDOs are set.
170    pub fn add_tpdo(
171        &mut self,
172        cob_id: u16,
173        mapping: PdoMapping<MAX_PDO_MAPPING>,
174        transmission: TransmissionType,
175    ) -> Result<()> {
176        self.tpdos
177            .push(TpdoSlot {
178                cob_id,
179                mapping,
180                transmission,
181            })
182            .map_err(|_| Error::MappingFull)
183    }
184
185    /// (Re)build the PDO configuration from the PDO parameter objects in the
186    /// object dictionary — communication parameters at `0x1400`+ (RPDO) /
187    /// `0x1800`+ (TPDO) and mapping parameters at `0x1600`+ / `0x1A00`+ (CiA 301
188    /// §7.5.2). Call this after a master configures PDOs by writing those
189    /// objects over SDO, so the node picks up the new layout.
190    ///
191    /// Any programmatic PDO configuration is replaced. PDOs whose communication
192    /// COB-ID has the "invalid" bit set are skipped, as are entries that are
193    /// absent or malformed.
194    pub fn configure_pdos_from_od(&mut self) {
195        self.rpdos.clear();
196        self.tpdos.clear();
197        for n in 0..MAX_PDOS as u16 {
198            if let Some(slot) = build_rpdo(&self.od, n) {
199                let _ = self.rpdos.push(slot);
200            }
201            if let Some(slot) = build_tpdo(&self.od, n) {
202                let _ = self.tpdos.push(slot);
203            }
204        }
205    }
206
207    /// Take the address of the object a master most recently wrote over SDO,
208    /// clearing it. Poll after [`Node::on_frame`] to react to configuration
209    /// writes — for example, re-read the PDO layout when a PDO parameter object
210    /// (`0x1400`–`0x1BFF`) changes:
211    ///
212    /// ```
213    /// # use canopen_rs::{node::Node, NodeId, ObjectDictionary};
214    /// # let mut node = Node::new(NodeId::new(1).unwrap(), ObjectDictionary::<8>::new());
215    /// # let (cob_id, data) = (0u16, [0u8; 8]);
216    /// node.on_frame(cob_id, &data);
217    /// if let Some(addr) = node.take_written_object() {
218    ///     if (0x1400..=0x1BFF).contains(&addr.index) {
219    ///         node.configure_pdos_from_od();
220    ///     }
221    /// }
222    /// ```
223    pub fn take_written_object(&mut self) -> Option<crate::object_dictionary::Address> {
224        self.sdo.take_write()
225    }
226
227    /// This node's id.
228    pub fn node_id(&self) -> NodeId {
229        self.node_id
230    }
231
232    /// The current NMT state.
233    pub fn state(&self) -> NmtState {
234        self.nmt.state()
235    }
236
237    /// Borrow the object dictionary (e.g. to publish process data).
238    pub fn od(&self) -> &ObjectDictionary<N> {
239        &self.od
240    }
241
242    /// Mutably borrow the object dictionary.
243    pub fn od_mut(&mut self) -> &mut ObjectDictionary<N> {
244        &mut self.od
245    }
246
247    /// Finish initialisation: enter pre-operational and return the boot-up
248    /// frame to transmit (`0x700 + node`, data `0x00`).
249    pub fn boot(&mut self) -> TxFrame {
250        self.nmt.boot();
251        self.guard_toggle = false;
252        TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &nmt::BOOTUP_FRAME)
253    }
254
255    /// The node-guarding response frame (`0x700 + node`), reporting the current
256    /// state with an alternating toggle bit. Call this when the master polls the
257    /// node with an RTR frame on that COB-ID (the legacy error-control
258    /// alternative to [`Node::heartbeat`]).
259    pub fn node_guard_response(&mut self) -> TxFrame {
260        let byte = nmt::encode_node_guard(self.nmt.state(), self.guard_toggle);
261        self.guard_toggle = !self.guard_toggle;
262        TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &[byte])
263    }
264
265    /// The heartbeat frame for the current state. Transmit it on your heartbeat
266    /// timer (the producer heartbeat time lives in object `0x1017`).
267    pub fn heartbeat(&self) -> TxFrame {
268        TxFrame::new(
269            nmt::heartbeat_cob_id(self.node_id),
270            &nmt::encode_heartbeat(self.nmt.state()),
271        )
272    }
273
274    /// Process an incoming CAN frame, returning a response to transmit, if any.
275    ///
276    /// Handles NMT node-control (`0x000`), SDO requests (`0x600 + node`), LSS
277    /// master requests (`0x7E5`, when enabled), and received PDOs. SDO is served
278    /// only in pre-operational and operational states, and PDOs only in
279    /// operational, per CiA 301; LSS is served regardless of NMT state. Frames
280    /// for other COB-IDs are ignored.
281    pub fn on_frame(&mut self, cob_id: u16, data: &[u8]) -> Option<TxFrame> {
282        if cob_id == nmt::NMT_COMMAND_COB_ID {
283            self.on_nmt(data);
284            None
285        } else if cob_id == lss::LSS_MASTER_COB_ID {
286            self.on_lss(data)
287        } else if cob_id == self.sdo.request_cob_id() {
288            self.on_sdo(data)
289        } else {
290            self.on_rpdo(cob_id, data);
291            None
292        }
293    }
294
295    fn on_lss(&mut self, data: &[u8]) -> Option<TxFrame> {
296        let lss = self.lss.as_mut()?;
297        if data.len() > 8 {
298            return None;
299        }
300        let mut frame: lss::LssFrame = [0u8; 8];
301        frame[..data.len()].copy_from_slice(data);
302        lss.handle(&frame)
303            .map(|resp| TxFrame::new(lss::LSS_SLAVE_COB_ID, &resp))
304    }
305
306    /// The synchronous transmit PDOs to send in response to a SYNC.
307    ///
308    /// Packs every configured TPDO with a synchronous transmission type from the
309    /// current object dictionary. Empty unless the node is operational — PDOs
310    /// are exchanged only in that state (CiA 301 §7.3.5).
311    pub fn sync_tpdos(&self) -> Vec<TxFrame, MAX_PDOS> {
312        let mut frames = Vec::new();
313        if self.nmt.state() != NmtState::Operational {
314            return frames;
315        }
316        for slot in &self.tpdos {
317            if is_synchronous(slot.transmission) {
318                if let Some(frame) = self.build_tpdo(slot) {
319                    // Capacity matches self.tpdos, so this never overflows.
320                    let _ = frames.push(frame);
321                }
322            }
323        }
324        frames
325    }
326
327    /// Emit transmit PDO `index` on demand (an event-driven transmission), or
328    /// `None` if there is no such PDO or the node is not operational.
329    pub fn tpdo(&self, index: usize) -> Option<TxFrame> {
330        if self.nmt.state() != NmtState::Operational {
331            return None;
332        }
333        self.build_tpdo(self.tpdos.get(index)?)
334    }
335
336    fn build_tpdo(&self, slot: &TpdoSlot) -> Option<TxFrame> {
337        if slot.mapping.is_empty() {
338            return None;
339        }
340        let mut buf = [0u8; 8];
341        let len = pdo::pack(&slot.mapping, &self.od, &mut buf).ok()?;
342        Some(TxFrame::new(slot.cob_id, &buf[..len]))
343    }
344
345    fn on_rpdo(&mut self, cob_id: u16, data: &[u8]) {
346        // PDOs are exchanged only in the operational state.
347        if self.nmt.state() != NmtState::Operational {
348            return;
349        }
350        if let Some(i) = self.rpdos.iter().position(|r| r.cob_id == cob_id) {
351            // Disjoint field borrows: `rpdos` (shared) and `od` (mutable).
352            let _ = pdo::unpack(&self.rpdos[i].mapping, &mut self.od, data);
353        }
354    }
355
356    fn on_nmt(&mut self, data: &[u8]) {
357        // An NMT node-control frame is [command specifier, target node].
358        if data.len() < 2 {
359            return;
360        }
361        if let Ok((command, target)) = nmt::decode_command(&[data[0], data[1]]) {
362            if target == NodeId::BROADCAST || target == self.node_id {
363                self.nmt.apply(command);
364            }
365        }
366    }
367
368    fn on_sdo(&mut self, data: &[u8]) -> Option<TxFrame> {
369        // SDO is inactive outside pre-operational / operational (CiA 301 §7.3).
370        if !matches!(
371            self.nmt.state(),
372            NmtState::PreOperational | NmtState::Operational
373        ) {
374            return None;
375        }
376        let mut payload: sdo::SdoPayload = [0u8; 8];
377        if data.len() > payload.len() {
378            return None;
379        }
380        payload[..data.len()].copy_from_slice(data);
381        let response = self.sdo.handle(&mut self.od, &payload)?;
382        Some(TxFrame::new(self.sdo.response_cob_id(), &response))
383    }
384}
385
386/// Whether a transmission type is SYNC-triggered (as opposed to event-driven).
387fn is_synchronous(transmission: TransmissionType) -> bool {
388    matches!(
389        transmission,
390        TransmissionType::SynchronousAcyclic | TransmissionType::SynchronousCyclic(_)
391    )
392}
393
394// --- Reading PDO parameter objects out of the object dictionary -------------
395
396fn od_u32<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u32> {
397    match od
398        .read(crate::object_dictionary::Address::new(index, sub))
399        .ok()?
400    {
401        crate::datatypes::Value::Unsigned32(v) => Some(v),
402        _ => None,
403    }
404}
405
406fn od_u8<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u8> {
407    match od
408        .read(crate::object_dictionary::Address::new(index, sub))
409        .ok()?
410    {
411        crate::datatypes::Value::Unsigned8(v) => Some(v),
412        _ => None,
413    }
414}
415
416/// Read a mapping-parameter record (`sub 0` = count, `sub 1..=count` = the
417/// `0xIIII_SSLL` mapping entries) into a [`PdoMapping`].
418fn read_pdo_mapping<const N: usize>(
419    od: &ObjectDictionary<N>,
420    map_index: u16,
421) -> Option<PdoMapping<MAX_PDO_MAPPING>> {
422    let count = od_u8(od, map_index, 0)?;
423    let mut mapping = PdoMapping::new();
424    for sub in 1..=count {
425        mapping
426            .push(pdo::MappingEntry::from_u32(od_u32(od, map_index, sub)?))
427            .ok()?;
428    }
429    Some(mapping)
430}
431
432fn build_rpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<RpdoSlot> {
433    let cob_id = od_u32(od, 0x1400 + n, 1)?;
434    if !pdo::pdo_is_valid(cob_id) {
435        return None;
436    }
437    Some(RpdoSlot {
438        cob_id: pdo::pdo_can_id(cob_id),
439        mapping: read_pdo_mapping(od, 0x1600 + n)?,
440    })
441}
442
443fn build_tpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<TpdoSlot> {
444    let cob_id = od_u32(od, 0x1800 + n, 1)?;
445    if !pdo::pdo_is_valid(cob_id) {
446        return None;
447    }
448    // Transmission type (comm sub 2); default to event-driven if absent/invalid.
449    let transmission = od_u8(od, 0x1800 + n, 2)
450        .and_then(|b| TransmissionType::from_byte(b).ok())
451        .unwrap_or(TransmissionType::EventDrivenProfile);
452    Some(TpdoSlot {
453        cob_id: pdo::pdo_can_id(cob_id),
454        mapping: read_pdo_mapping(od, 0x1A00 + n)?,
455        transmission,
456    })
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use crate::object_dictionary::{Address, Entry};
463    use crate::pdo::MappingEntry;
464    use crate::sdo::{encode_download_expedited, encode_upload_request};
465    use crate::{DataType, NmtCommand, Value};
466
467    fn start<const N: usize>(n: &mut Node<N>) {
468        n.on_frame(
469            nmt::NMT_COMMAND_COB_ID,
470            &[NmtCommand::StartRemoteNode as u8, 0x10],
471        );
472    }
473
474    fn od() -> ObjectDictionary<8> {
475        let mut od = ObjectDictionary::new();
476        od.insert(
477            Address::new(0x1000, 0),
478            Entry::constant(Value::Unsigned32(0x192)),
479        )
480        .unwrap();
481        od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
482            .unwrap();
483        od
484    }
485
486    fn node() -> Node<8> {
487        Node::new(NodeId::new(0x10).unwrap(), od())
488    }
489
490    #[test]
491    fn boots_from_init_to_preop_and_announces() {
492        let mut n = node();
493        assert_eq!(n.state(), NmtState::Initialising);
494        let boot = n.boot();
495        assert_eq!(n.state(), NmtState::PreOperational);
496        assert_eq!(boot.cob_id, 0x710); // 0x700 + node
497        assert_eq!(boot.data(), &[0x00]);
498    }
499
500    #[test]
501    fn heartbeat_reflects_state() {
502        let mut n = node();
503        n.boot();
504        assert_eq!(n.heartbeat().data(), &[0x7F]); // pre-operational
505        n.on_frame(
506            nmt::NMT_COMMAND_COB_ID,
507            &[NmtCommand::StartRemoteNode as u8, 0x10],
508        );
509        assert_eq!(n.state(), NmtState::Operational);
510        assert_eq!(n.heartbeat().data(), &[0x05]); // operational
511    }
512
513    #[test]
514    fn node_guard_response_toggles() {
515        let mut n = node();
516        n.boot();
517        start(&mut n); // operational (0x05)
518        let first = n.node_guard_response();
519        assert_eq!(first.cob_id, 0x710); // 0x700 + node
520        assert_eq!(first.data(), &[0x05]); // toggle clear
521        assert_eq!(n.node_guard_response().data(), &[0x85]); // toggle set
522        assert_eq!(n.node_guard_response().data(), &[0x05]); // toggles back
523    }
524
525    #[test]
526    fn serves_sdo_read_when_preoperational() {
527        let mut n = node();
528        n.boot();
529        let req = encode_upload_request(Address::new(0x1000, 0));
530        let resp = n.on_frame(0x610, &req).expect("SDO response");
531        assert_eq!(resp.cob_id, 0x590); // 0x580 + node
532        let (_, value) = crate::sdo::decode_upload_expedited_response(
533            resp.data().try_into().unwrap(),
534            DataType::Unsigned32,
535        )
536        .unwrap();
537        assert_eq!(value, Value::Unsigned32(0x192));
538    }
539
540    #[test]
541    fn ignores_sdo_before_boot() {
542        let mut n = node(); // still Initialising
543        let req = encode_upload_request(Address::new(0x1000, 0));
544        assert!(n.on_frame(0x610, &req).is_none());
545    }
546
547    #[test]
548    fn ignores_sdo_when_stopped() {
549        let mut n = node();
550        n.boot();
551        n.on_frame(
552            nmt::NMT_COMMAND_COB_ID,
553            &[NmtCommand::StopRemoteNode as u8, 0x10],
554        );
555        assert_eq!(n.state(), NmtState::Stopped);
556        let req = encode_upload_request(Address::new(0x1000, 0));
557        assert!(n.on_frame(0x610, &req).is_none());
558    }
559
560    #[test]
561    fn nmt_command_for_other_node_is_ignored() {
562        let mut n = node();
563        n.boot();
564        // Start addressed to node 0x20, not us.
565        n.on_frame(
566            nmt::NMT_COMMAND_COB_ID,
567            &[NmtCommand::StartRemoteNode as u8, 0x20],
568        );
569        assert_eq!(n.state(), NmtState::PreOperational); // unchanged
570    }
571
572    #[test]
573    fn broadcast_nmt_applies() {
574        let mut n = node();
575        n.boot();
576        n.on_frame(
577            nmt::NMT_COMMAND_COB_ID,
578            &[NmtCommand::StartRemoteNode as u8, 0x00],
579        );
580        assert_eq!(n.state(), NmtState::Operational);
581    }
582
583    #[test]
584    fn serves_sdo_write_and_updates_od() {
585        let mut n = node();
586        n.boot();
587        let req =
588            encode_download_expedited(Address::new(0x1017, 0), &Value::Unsigned16(1234)).unwrap();
589        assert!(n.on_frame(0x610, &req).is_some());
590        assert_eq!(
591            n.od().read(Address::new(0x1017, 0)).unwrap(),
592            Value::Unsigned16(1234)
593        );
594    }
595
596    #[test]
597    fn ignores_unrelated_cob_id() {
598        let mut n = node();
599        n.boot();
600        assert!(n.on_frame(0x123, &[0; 8]).is_none());
601    }
602
603    // --- PDO ---------------------------------------------------------------
604    fn pdo_od() -> ObjectDictionary<8> {
605        let mut od = ObjectDictionary::new();
606        // TPDO source objects (readable) and an RPDO target (writable).
607        od.insert(
608            Address::new(0x6000, 1),
609            Entry::rw(Value::Unsigned16(0xBEEF)),
610        )
611        .unwrap();
612        od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
613            .unwrap();
614        od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
615            .unwrap();
616        od
617    }
618
619    fn mapping(entries: &[(u16, u8, u8)]) -> PdoMapping<MAX_PDO_MAPPING> {
620        let mut m = PdoMapping::new();
621        for &(index, sub, bits) in entries {
622            m.push(MappingEntry::new(index, sub, bits)).unwrap();
623        }
624        m
625    }
626
627    #[test]
628    fn tpdo_transmits_only_when_operational() {
629        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
630        n.add_tpdo(
631            0x18A,
632            mapping(&[(0x6000, 1, 16), (0x6000, 2, 8)]),
633            TransmissionType::SynchronousAcyclic,
634        )
635        .unwrap();
636        n.boot();
637
638        // Pre-operational: no PDO traffic.
639        assert!(n.sync_tpdos().is_empty());
640
641        start(&mut n);
642        let frames = n.sync_tpdos();
643        assert_eq!(frames.len(), 1);
644        assert_eq!(frames[0].cob_id, 0x18A);
645        // U16 0xBEEF little-endian then U8 0x42.
646        assert_eq!(frames[0].data(), &[0xEF, 0xBE, 0x42]);
647    }
648
649    #[test]
650    fn event_tpdo_by_index() {
651        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
652        // Event-driven type is not emitted by sync_tpdos, only by tpdo().
653        n.add_tpdo(
654            0x18A,
655            mapping(&[(0x6000, 2, 8)]),
656            TransmissionType::EventDrivenProfile,
657        )
658        .unwrap();
659        n.boot();
660        start(&mut n);
661        assert!(n.sync_tpdos().is_empty());
662        assert_eq!(n.tpdo(0).unwrap().data(), &[0x42]);
663        assert!(n.tpdo(1).is_none());
664    }
665
666    #[test]
667    fn rpdo_applies_only_when_operational() {
668        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
669        n.add_rpdo(0x20A, mapping(&[(0x6200, 1, 16)])).unwrap();
670        n.boot();
671
672        // Pre-operational: the RPDO is ignored.
673        assert!(n.on_frame(0x20A, &[0x34, 0x12]).is_none());
674        assert_eq!(
675            n.od().read(Address::new(0x6200, 1)).unwrap(),
676            Value::Unsigned16(0)
677        );
678
679        // Operational: the frame is unpacked into the object dictionary.
680        start(&mut n);
681        n.on_frame(0x20A, &[0x34, 0x12]);
682        assert_eq!(
683            n.od().read(Address::new(0x6200, 1)).unwrap(),
684            Value::Unsigned16(0x1234)
685        );
686    }
687
688    #[test]
689    fn pdo_capacity_is_enforced() {
690        let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
691        for _ in 0..MAX_PDOS {
692            n.add_tpdo(
693                0x18A,
694                mapping(&[(0x6000, 2, 8)]),
695                TransmissionType::SynchronousAcyclic,
696            )
697            .unwrap();
698        }
699        assert_eq!(
700            n.add_tpdo(
701                0x18A,
702                mapping(&[(0x6000, 2, 8)]),
703                TransmissionType::SynchronousAcyclic
704            ),
705            Err(Error::MappingFull)
706        );
707    }
708
709    // --- LSS ---------------------------------------------------------------
710    use crate::lss::{self, encode_configure_node_id, encode_switch_global, LssAddress, LssState};
711
712    fn lss_address() -> LssAddress {
713        LssAddress {
714            vendor_id: 0x1F,
715            product_code: 0x2A,
716            revision_number: 1,
717            serial_number: 0x99,
718        }
719    }
720
721    #[test]
722    fn routes_lss_frames_when_enabled() {
723        let mut n = node();
724        n.enable_lss(lss_address());
725        // Switch into configuration via LSS (COB-ID 0x7E5), no response.
726        assert!(n
727            .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
728            .is_none());
729        assert_eq!(n.lss().unwrap().state(), LssState::Configuration);
730    }
731
732    #[test]
733    fn lss_frames_ignored_when_disabled() {
734        let mut n = node(); // LSS not enabled
735        assert!(n
736            .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
737            .is_none());
738        assert!(n.lss().is_none());
739    }
740
741    #[test]
742    fn lss_assigns_node_id_and_moves_sdo_cob_id() {
743        // A node that comes up unconfigured: leave it in Initialising and serve
744        // only LSS until a master assigns an id.
745        let mut n = Node::new(NodeId::new(1).unwrap(), od());
746        n.enable_lss(lss_address());
747        assert_eq!(n.node_id(), NodeId::new(1).unwrap());
748
749        // Master: switch to configuration, then assign node-id 0x20.
750        n.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true));
751        let resp = n
752            .on_frame(lss::LSS_MASTER_COB_ID, &encode_configure_node_id(0x20))
753            .expect("configure response");
754        assert_eq!(resp.cob_id, lss::LSS_SLAVE_COB_ID);
755        assert_eq!(&resp.data()[..2], &[0x11, 0x00]); // configure success
756
757        // On the node's reset, adopt the assigned id — SDO COB-ID moves.
758        assert_eq!(n.apply_lss_node_id(), Some(NodeId::new(0x20).unwrap()));
759        assert_eq!(n.node_id(), NodeId::new(0x20).unwrap());
760
761        n.boot();
762        let req = encode_upload_request(Address::new(0x1000, 0));
763        assert!(n.on_frame(0x601, &req).is_none()); // old COB-ID no longer served
764        assert!(n.on_frame(0x620, &req).is_some()); // new COB-ID (0x600 + 0x20)
765    }
766
767    // --- PDO configuration from the object dictionary ----------------------
768    #[test]
769    fn configures_pdos_from_od() {
770        let mut od = ObjectDictionary::<16>::new();
771        // RPDO1: comm 0x1400 (COB-ID 0x210, valid), mapping 0x1600 -> 0x6200/1.
772        od.insert(Address::new(0x1400, 1), Entry::rw(Value::Unsigned32(0x210)))
773            .unwrap();
774        od.insert(Address::new(0x1600, 0), Entry::rw(Value::Unsigned8(1)))
775            .unwrap();
776        od.insert(
777            Address::new(0x1600, 1),
778            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6200, 1, 16).to_u32())),
779        )
780        .unwrap();
781        // TPDO1: comm 0x1800 (COB-ID 0x190, valid, sync-cyclic), mapping -> 0x6000/1.
782        od.insert(Address::new(0x1800, 1), Entry::rw(Value::Unsigned32(0x190)))
783            .unwrap();
784        od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
785            .unwrap(); // transmission type 1
786        od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
787            .unwrap();
788        od.insert(
789            Address::new(0x1A00, 1),
790            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
791        )
792        .unwrap();
793        // The mapped process-data objects.
794        od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
795            .unwrap();
796        od.insert(
797            Address::new(0x6000, 1),
798            Entry::rw(Value::Unsigned16(0xBEEF)),
799        )
800        .unwrap();
801
802        let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
803        n.configure_pdos_from_od();
804        n.boot();
805        start(&mut n);
806
807        // The RPDO now applies to the OD…
808        n.on_frame(0x210, &[0x34, 0x12]);
809        assert_eq!(
810            n.od().read(Address::new(0x6200, 1)).unwrap(),
811            Value::Unsigned16(0x1234)
812        );
813        // …and the (synchronous) TPDO is emitted on SYNC.
814        let tpdos = n.sync_tpdos();
815        assert_eq!(tpdos.len(), 1);
816        assert_eq!(tpdos[0].cob_id, 0x190);
817        assert_eq!(tpdos[0].data(), &[0xEF, 0xBE]);
818    }
819
820    #[test]
821    fn skips_pdo_with_invalid_cob_id() {
822        let mut od = ObjectDictionary::<8>::new();
823        // TPDO1 with the COB-ID validity bit (31) set — disabled.
824        od.insert(
825            Address::new(0x1800, 1),
826            Entry::rw(Value::Unsigned32(0x8000_0190)),
827        )
828        .unwrap();
829        od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
830            .unwrap();
831        od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
832            .unwrap();
833        od.insert(
834            Address::new(0x1A00, 1),
835            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
836        )
837        .unwrap();
838        od.insert(
839            Address::new(0x6000, 1),
840            Entry::rw(Value::Unsigned16(0xBEEF)),
841        )
842        .unwrap();
843
844        let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
845        n.configure_pdos_from_od();
846        n.boot();
847        start(&mut n);
848        assert!(n.sync_tpdos().is_empty()); // the invalid TPDO was not configured
849    }
850
851    #[test]
852    fn reacts_to_a_pdo_parameter_write() {
853        // TPDO1 params present but initially disabled (COB-ID invalid bit set).
854        let mut od = ObjectDictionary::<16>::new();
855        od.insert(
856            Address::new(0x1800, 1),
857            Entry::rw(Value::Unsigned32(0x8000_0190)),
858        )
859        .unwrap();
860        od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
861            .unwrap();
862        od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
863            .unwrap();
864        od.insert(
865            Address::new(0x1A00, 1),
866            Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
867        )
868        .unwrap();
869        od.insert(
870            Address::new(0x6000, 1),
871            Entry::rw(Value::Unsigned16(0xBEEF)),
872        )
873        .unwrap();
874
875        let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
876        n.configure_pdos_from_od();
877        n.boot();
878        start(&mut n);
879        assert!(n.sync_tpdos().is_empty());
880
881        // A master enables the TPDO by writing a valid COB-ID to 0x1800/1.
882        let req =
883            encode_download_expedited(Address::new(0x1800, 1), &Value::Unsigned32(0x190)).unwrap();
884        n.on_frame(0x610, &req);
885
886        // The node notices the PDO-parameter write and reconfigures.
887        let written = n.take_written_object().expect("a write was recorded");
888        assert_eq!(written, Address::new(0x1800, 1));
889        assert_eq!(n.take_written_object(), None); // reported once
890        if (0x1400..=0x1BFF).contains(&written.index) {
891            n.configure_pdos_from_od();
892        }
893
894        // The TPDO is now active.
895        let tpdos = n.sync_tpdos();
896        assert_eq!(tpdos.len(), 1);
897        assert_eq!(tpdos[0].cob_id, 0x190);
898    }
899}