Skip to main content

canopen_rs/
pdo.rs

1//! Process Data Object (PDO) — real-time process data (CiA 301 §7.2.2).
2//!
3//! PDOs carry mapped process data with no protocol overhead: up to eight bytes
4//! of application data per frame, laid out by the PDO *mapping parameters*.
5//! Transmit PDOs (TPDO) publish data *from* a node's object dictionary;
6//! receive PDOs (RPDO) consume data *into* it.
7//!
8//! A PDO is described by two object-dictionary records:
9//!
10//! * a **communication parameter** (`0x1400`+ RPDO, `0x1800`+ TPDO) — the
11//!   COB-ID (with a validity bit) and the transmission type, and
12//! * a **mapping parameter** (`0x1600`+ RPDO, `0x1A00`+ TPDO) — the ordered
13//!   list of `(index, subindex, bit length)` triples packed into the frame.
14//!
15//! This module models the mapping ([`MappingEntry`], [`PdoMapping`]), packs a
16//! mapping's objects out of an [`ObjectDictionary`] into a frame ([`pack`]),
17//! and unpacks a received frame back into it ([`unpack`]). It also parses the
18//! transmission type and the default (predefined connection set) COB-IDs.
19//!
20//! **Scope:** mappings are byte-aligned — every mapped object's bit length is a
21//! whole number of bytes, which covers the overwhelming majority of real
22//! devices. Sub-byte bit packing (e.g. several `BOOLEAN`s in one byte) is a
23//! later addition.
24//!
25//! ```
26//! use canopen_rs::pdo::{pack, MappingEntry, PdoMapping};
27//! use canopen_rs::{Address, Entry, ObjectDictionary, Value};
28//!
29//! let mut od = ObjectDictionary::<4>::new();
30//! od.insert(Address::new(0x6000, 1), Entry::rw(Value::Unsigned16(0xBEEF))).unwrap();
31//! od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42))).unwrap();
32//!
33//! // Map both objects into one PDO, then pack them into a frame.
34//! let mut mapping = PdoMapping::<8>::new();
35//! mapping.push(MappingEntry::new(0x6000, 1, 16)).unwrap();
36//! mapping.push(MappingEntry::new(0x6000, 2, 8)).unwrap();
37//!
38//! let mut buf = [0u8; 8];
39//! let len = pack(&mapping, &od, &mut buf).unwrap();
40//! assert_eq!(&buf[..len], &[0xEF, 0xBE, 0x42]); // U16 little-endian, then U8
41//! ```
42
43use heapless::Vec;
44
45use crate::datatypes::Value;
46use crate::object_dictionary::{Address, ObjectDictionary};
47use crate::types::NodeId;
48use crate::{Error, Result};
49
50/// Whether a PDO transmits from, or receives into, the node.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum PdoKind {
53    /// Transmit PDO: the node publishes data from its object dictionary.
54    Transmit,
55    /// Receive PDO: the node consumes data into its object dictionary.
56    Receive,
57}
58
59/// Bit 31 of a communication-parameter COB-ID: set means the PDO is *invalid*
60/// (unused). CiA 301 §7.5.2.35.
61pub const PDO_COB_ID_INVALID: u32 = 0x8000_0000;
62
63/// Whether a communication-parameter COB-ID marks the PDO as valid (in use).
64pub const fn pdo_is_valid(comm_cob_id: u32) -> bool {
65    comm_cob_id & PDO_COB_ID_INVALID == 0
66}
67
68/// The 11-bit CAN identifier carried in a communication-parameter COB-ID.
69pub const fn pdo_can_id(comm_cob_id: u32) -> u16 {
70    (comm_cob_id & 0x7FF) as u16
71}
72
73/// The default COB-ID of PDO `number` (`1..=4`) for `node`, from the
74/// predefined connection set (CiA 301 §7.3.5): TPDOs at `0x180/0x280/0x380/
75/// 0x480 + node`, RPDOs at `0x200/0x300/0x400/0x500 + node`.
76///
77/// Returns `None` for a PDO number outside `1..=4`.
78pub const fn default_cob_id(kind: PdoKind, number: u8, node: NodeId) -> Option<u16> {
79    if number < 1 || number > 4 {
80        return None;
81    }
82    let base = match kind {
83        // TPDO1 = 0x180, TPDO2 = 0x280, … = 0x80 + number * 0x100.
84        PdoKind::Transmit => 0x80 + (number as u16) * 0x100,
85        // RPDO1 = 0x200, RPDO2 = 0x300, … = 0x100 + number * 0x100.
86        PdoKind::Receive => 0x100 + (number as u16) * 0x100,
87    };
88    Some(base + node.raw() as u16)
89}
90
91/// The transmission type of a PDO (CiA 301 §7.5.2.36), byte 2 of the
92/// communication parameter.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum TransmissionType {
96    /// `0`: synchronous, acyclic — transmitted after the next SYNC when the
97    /// mapped data has changed.
98    SynchronousAcyclic,
99    /// `1..=240`: synchronous, cyclic — transmitted every *n*-th SYNC.
100    SynchronousCyclic(u8),
101    /// `252`: synchronous, RTR-only.
102    SynchronousRtrOnly,
103    /// `253`: event-driven, RTR-only.
104    EventDrivenRtrOnly,
105    /// `254`: event-driven, manufacturer-specific event.
106    EventDrivenManufacturer,
107    /// `255`: event-driven, device-profile / application event.
108    EventDrivenProfile,
109}
110
111impl TransmissionType {
112    /// Decode a transmission-type byte.
113    ///
114    /// Returns [`Error::UnsupportedTransfer`] for the reserved range
115    /// `241..=251`.
116    pub const fn from_byte(byte: u8) -> Result<Self> {
117        Ok(match byte {
118            0 => TransmissionType::SynchronousAcyclic,
119            1..=240 => TransmissionType::SynchronousCyclic(byte),
120            252 => TransmissionType::SynchronousRtrOnly,
121            253 => TransmissionType::EventDrivenRtrOnly,
122            254 => TransmissionType::EventDrivenManufacturer,
123            255 => TransmissionType::EventDrivenProfile,
124            _ => return Err(Error::UnsupportedTransfer),
125        })
126    }
127
128    /// The transmission-type byte for this variant.
129    pub const fn to_byte(self) -> u8 {
130        match self {
131            TransmissionType::SynchronousAcyclic => 0,
132            TransmissionType::SynchronousCyclic(n) => n,
133            TransmissionType::SynchronousRtrOnly => 252,
134            TransmissionType::EventDrivenRtrOnly => 253,
135            TransmissionType::EventDrivenManufacturer => 254,
136            TransmissionType::EventDrivenProfile => 255,
137        }
138    }
139}
140
141/// One entry of a PDO mapping: the object it references and its width in bits.
142///
143/// On the wire (and in the mapping-parameter record) this is a `u32`
144/// `0xIIII_SSLL` — index `IIII`, subindex `SS`, bit length `LL`.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct MappingEntry {
147    /// The mapped object's address.
148    pub address: Address,
149    /// The number of bits contributed to the PDO frame.
150    pub bit_length: u8,
151}
152
153impl MappingEntry {
154    /// Construct a mapping entry.
155    pub const fn new(index: u16, subindex: u8, bit_length: u8) -> Self {
156        Self {
157            address: Address::new(index, subindex),
158            bit_length,
159        }
160    }
161
162    /// The `0xIIII_SSLL` mapping value stored in the OD mapping record.
163    pub const fn to_u32(self) -> u32 {
164        ((self.address.index as u32) << 16)
165            | ((self.address.subindex as u32) << 8)
166            | self.bit_length as u32
167    }
168
169    /// Parse a `0xIIII_SSLL` mapping value.
170    pub const fn from_u32(raw: u32) -> Self {
171        Self {
172            address: Address::new((raw >> 16) as u16, (raw >> 8) as u8),
173            bit_length: raw as u8,
174        }
175    }
176}
177
178/// An ordered PDO mapping of up to `N` entries.
179///
180/// The mapped objects are packed into the frame in insertion order, and the
181/// total width may not exceed eight bytes (64 bits).
182#[derive(Debug)]
183pub struct PdoMapping<const N: usize> {
184    entries: Vec<MappingEntry, N>,
185}
186
187impl<const N: usize> Default for PdoMapping<N> {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193impl<const N: usize> PdoMapping<N> {
194    /// An empty mapping.
195    pub const fn new() -> Self {
196        Self {
197            entries: Vec::new(),
198        }
199    }
200
201    /// Append a mapping entry.
202    ///
203    /// Returns [`Error::PdoTooLong`] if it would push the total past 64 bits,
204    /// or [`Error::MappingFull`] if the fixed capacity `N` is exhausted.
205    pub fn push(&mut self, entry: MappingEntry) -> Result<()> {
206        if self.total_bits() + entry.bit_length as u32 > 64 {
207            return Err(Error::PdoTooLong);
208        }
209        self.entries.push(entry).map_err(|_| Error::MappingFull)
210    }
211
212    /// The mapping entries, in packing order.
213    pub fn entries(&self) -> &[MappingEntry] {
214        self.entries.as_slice()
215    }
216
217    /// The number of mapped entries (the value of mapping subindex `0`).
218    pub fn len(&self) -> usize {
219        self.entries.len()
220    }
221
222    /// Whether the mapping is empty (PDO disabled).
223    pub fn is_empty(&self) -> bool {
224        self.entries.is_empty()
225    }
226
227    /// The total width of the mapped data, in bits.
228    pub fn total_bits(&self) -> u32 {
229        self.entries.iter().map(|e| e.bit_length as u32).sum()
230    }
231
232    /// The total width of the mapped data, in whole bytes (the CAN frame DLC).
233    pub fn total_bytes(&self) -> usize {
234        (self.total_bits() as usize).div_ceil(8)
235    }
236}
237
238/// Pack a TPDO: read each mapped object out of `od` and lay it into `buf`
239/// little-endian, in mapping order. Returns the number of bytes written (the
240/// frame's data length).
241///
242/// Errors: [`Error::BadLength`] if `buf` is shorter than
243/// [`PdoMapping::total_bytes`]; [`Error::UnsupportedTransfer`] for a sub-byte
244/// mapping entry; [`Error::TypeMismatch`] if a mapped object's stored width
245/// disagrees with its mapping bit length; plus any error from reading the
246/// object (e.g. [`Error::ObjectNotFound`], [`Error::WriteOnly`]).
247pub fn pack<const N: usize, const K: usize>(
248    mapping: &PdoMapping<N>,
249    od: &ObjectDictionary<K>,
250    buf: &mut [u8],
251) -> Result<usize> {
252    let total = mapping.total_bytes();
253    if buf.len() < total {
254        return Err(Error::BadLength);
255    }
256    let mut offset = 0;
257    for entry in mapping.entries() {
258        let width = byte_width(entry.bit_length)?;
259        let value = od.read(entry.address)?;
260        // Variable-length types (strings/DOMAIN) cannot be mapped into a PDO.
261        if value.data_type().is_variable() || value.size() != width {
262            return Err(Error::TypeMismatch);
263        }
264        value.encode_le(&mut buf[offset..offset + width])?;
265        offset += width;
266    }
267    Ok(offset)
268}
269
270/// Unpack an RPDO: decode each mapped field from `data` and write it into `od`,
271/// in mapping order. The data type of each field is taken from the object
272/// currently stored in `od`.
273///
274/// Errors: [`Error::BadLength`] if `data` is shorter than
275/// [`PdoMapping::total_bytes`]; [`Error::UnsupportedTransfer`] for a sub-byte
276/// mapping entry; [`Error::ObjectNotFound`] for an unmapped target;
277/// [`Error::TypeMismatch`] if a target object's width disagrees with its
278/// mapping bit length; plus any error from writing the object (e.g.
279/// [`Error::ReadOnly`]).
280pub fn unpack<const N: usize, const K: usize>(
281    mapping: &PdoMapping<N>,
282    od: &mut ObjectDictionary<K>,
283    data: &[u8],
284) -> Result<()> {
285    let total = mapping.total_bytes();
286    if data.len() < total {
287        return Err(Error::BadLength);
288    }
289    let mut offset = 0;
290    for entry in mapping.entries() {
291        let width = byte_width(entry.bit_length)?;
292        let data_type = od
293            .entry(entry.address)
294            .ok_or(Error::ObjectNotFound)?
295            .value
296            .data_type();
297        if data_type.size() != width {
298            return Err(Error::TypeMismatch);
299        }
300        let value = Value::decode_le(data_type, &data[offset..offset + width])?;
301        od.write(entry.address, value)?;
302        offset += width;
303    }
304    Ok(())
305}
306
307/// The whole-byte width of a mapping bit length, rejecting sub-byte entries.
308const fn byte_width(bit_length: u8) -> Result<usize> {
309    if bit_length == 0 || bit_length % 8 != 0 {
310        return Err(Error::UnsupportedTransfer);
311    }
312    Ok((bit_length / 8) as usize)
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::datatypes::Value;
319    use crate::object_dictionary::Entry;
320
321    // --- Default COB-IDs ---------------------------------------------------
322    #[test]
323    fn default_cob_ids_follow_connection_set() {
324        let node = NodeId::new(4).unwrap();
325        assert_eq!(default_cob_id(PdoKind::Transmit, 1, node), Some(0x184));
326        assert_eq!(default_cob_id(PdoKind::Receive, 1, node), Some(0x204));
327        assert_eq!(default_cob_id(PdoKind::Transmit, 4, node), Some(0x484));
328        assert_eq!(default_cob_id(PdoKind::Receive, 4, node), Some(0x504));
329        assert_eq!(default_cob_id(PdoKind::Transmit, 5, node), None);
330    }
331
332    #[test]
333    fn comm_param_cob_id_validity() {
334        // TPDO1 of node 4, valid: 0x00000184. Invalid sets bit 31.
335        assert!(pdo_is_valid(0x0000_0184));
336        assert!(!pdo_is_valid(0x8000_0184));
337        assert_eq!(pdo_can_id(0x8000_0184), 0x184);
338    }
339
340    // --- Transmission type -------------------------------------------------
341    #[test]
342    fn transmission_types_roundtrip() {
343        for byte in [0u8, 1, 240, 252, 253, 254, 255] {
344            let t = TransmissionType::from_byte(byte).unwrap();
345            assert_eq!(t.to_byte(), byte);
346        }
347        assert_eq!(
348            TransmissionType::from_byte(1).unwrap(),
349            TransmissionType::SynchronousCyclic(1)
350        );
351        assert_eq!(
352            TransmissionType::from_byte(245),
353            Err(Error::UnsupportedTransfer)
354        );
355    }
356
357    // --- Mapping entry codec ----------------------------------------------
358    // Known-good mapping value: object 0x6000 sub 0x01, 8 bits → 0x60000108.
359    #[test]
360    fn mapping_entry_matches_known_value() {
361        let e = MappingEntry::new(0x6000, 0x01, 8);
362        assert_eq!(e.to_u32(), 0x6000_0108);
363        assert_eq!(MappingEntry::from_u32(0x6000_0108), e);
364    }
365
366    #[test]
367    fn mapping_rejects_over_eight_bytes() {
368        let mut m: PdoMapping<8> = PdoMapping::new();
369        // 4 × U16 = 64 bits, exactly full.
370        for i in 0..4 {
371            m.push(MappingEntry::new(0x2000 + i, 0, 16)).unwrap();
372        }
373        assert_eq!(m.total_bytes(), 8);
374        // One more bit over the limit is rejected.
375        assert_eq!(
376            m.push(MappingEntry::new(0x2100, 0, 8)),
377            Err(Error::PdoTooLong)
378        );
379    }
380
381    // --- Pack / unpack -----------------------------------------------------
382    // A TPDO mapping two objects: U16 at 0x6000/1 then U8 at 0x6000/2.
383    fn sample_od() -> ObjectDictionary<8> {
384        let mut od = ObjectDictionary::new();
385        od.insert(
386            Address::new(0x6000, 1),
387            Entry::rw(Value::Unsigned16(0xBEEF)),
388        )
389        .unwrap();
390        od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
391            .unwrap();
392        od
393    }
394
395    fn sample_mapping() -> PdoMapping<8> {
396        let mut m = PdoMapping::new();
397        m.push(MappingEntry::new(0x6000, 1, 16)).unwrap();
398        m.push(MappingEntry::new(0x6000, 2, 8)).unwrap();
399        m
400    }
401
402    #[test]
403    fn pack_lays_out_little_endian_in_order() {
404        let od = sample_od();
405        let mapping = sample_mapping();
406        let mut buf = [0u8; 8];
407        let n = pack(&mapping, &od, &mut buf).unwrap();
408        // U16 0xBEEF little-endian (EF BE) then U8 0x42.
409        assert_eq!(n, 3);
410        assert_eq!(&buf[..n], &[0xEF, 0xBE, 0x42]);
411    }
412
413    #[test]
414    fn unpack_writes_fields_back_into_od() {
415        let mut od = sample_od();
416        let mapping = sample_mapping();
417        // New values: U16 0x1234, U8 0x99.
418        unpack(&mapping, &mut od, &[0x34, 0x12, 0x99]).unwrap();
419        assert_eq!(
420            od.read(Address::new(0x6000, 1)).unwrap(),
421            Value::Unsigned16(0x1234)
422        );
423        assert_eq!(
424            od.read(Address::new(0x6000, 2)).unwrap(),
425            Value::Unsigned8(0x99)
426        );
427    }
428
429    #[test]
430    fn pack_unpack_roundtrip() {
431        let od = sample_od();
432        let mapping = sample_mapping();
433        let mut buf = [0u8; 8];
434        let n = pack(&mapping, &od, &mut buf).unwrap();
435
436        let mut dest = ObjectDictionary::<8>::new();
437        dest.insert(Address::new(0x6000, 1), Entry::rw(Value::Unsigned16(0)))
438            .unwrap();
439        dest.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0)))
440            .unwrap();
441        unpack(&mapping, &mut dest, &buf[..n]).unwrap();
442
443        assert_eq!(
444            dest.read(Address::new(0x6000, 1)).unwrap(),
445            Value::Unsigned16(0xBEEF)
446        );
447        assert_eq!(
448            dest.read(Address::new(0x6000, 2)).unwrap(),
449            Value::Unsigned8(0x42)
450        );
451    }
452
453    #[test]
454    fn pack_rejects_short_buffer() {
455        let od = sample_od();
456        let mapping = sample_mapping();
457        let mut buf = [0u8; 2];
458        assert_eq!(pack(&mapping, &od, &mut buf), Err(Error::BadLength));
459    }
460
461    #[test]
462    fn unpack_into_read_only_object_errors() {
463        let mut od = ObjectDictionary::<4>::new();
464        od.insert(Address::new(0x6000, 1), Entry::ro(Value::Unsigned16(0)))
465            .unwrap();
466        let mut m: PdoMapping<4> = PdoMapping::new();
467        m.push(MappingEntry::new(0x6000, 1, 16)).unwrap();
468        assert_eq!(unpack(&m, &mut od, &[0, 0]), Err(Error::ReadOnly));
469    }
470
471    #[test]
472    fn mapping_width_mismatch_errors() {
473        // Map 0x6000/2 (a U8) as 16 bits — inconsistent with the stored type.
474        let od = sample_od();
475        let mut m: PdoMapping<4> = PdoMapping::new();
476        m.push(MappingEntry::new(0x6000, 2, 16)).unwrap();
477        let mut buf = [0u8; 8];
478        assert_eq!(pack(&m, &od, &mut buf), Err(Error::TypeMismatch));
479    }
480}