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        if value.size() != width {
261            return Err(Error::TypeMismatch);
262        }
263        value.encode_le(&mut buf[offset..offset + width])?;
264        offset += width;
265    }
266    Ok(offset)
267}
268
269/// Unpack an RPDO: decode each mapped field from `data` and write it into `od`,
270/// in mapping order. The data type of each field is taken from the object
271/// currently stored in `od`.
272///
273/// Errors: [`Error::BadLength`] if `data` is shorter than
274/// [`PdoMapping::total_bytes`]; [`Error::UnsupportedTransfer`] for a sub-byte
275/// mapping entry; [`Error::ObjectNotFound`] for an unmapped target;
276/// [`Error::TypeMismatch`] if a target object's width disagrees with its
277/// mapping bit length; plus any error from writing the object (e.g.
278/// [`Error::ReadOnly`]).
279pub fn unpack<const N: usize, const K: usize>(
280    mapping: &PdoMapping<N>,
281    od: &mut ObjectDictionary<K>,
282    data: &[u8],
283) -> Result<()> {
284    let total = mapping.total_bytes();
285    if data.len() < total {
286        return Err(Error::BadLength);
287    }
288    let mut offset = 0;
289    for entry in mapping.entries() {
290        let width = byte_width(entry.bit_length)?;
291        let data_type = od
292            .entry(entry.address)
293            .ok_or(Error::ObjectNotFound)?
294            .value
295            .data_type();
296        if data_type.size() != width {
297            return Err(Error::TypeMismatch);
298        }
299        let value = Value::decode_le(data_type, &data[offset..offset + width])?;
300        od.write(entry.address, value)?;
301        offset += width;
302    }
303    Ok(())
304}
305
306/// The whole-byte width of a mapping bit length, rejecting sub-byte entries.
307const fn byte_width(bit_length: u8) -> Result<usize> {
308    if bit_length == 0 || bit_length % 8 != 0 {
309        return Err(Error::UnsupportedTransfer);
310    }
311    Ok((bit_length / 8) as usize)
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::datatypes::Value;
318    use crate::object_dictionary::Entry;
319
320    // --- Default COB-IDs ---------------------------------------------------
321    #[test]
322    fn default_cob_ids_follow_connection_set() {
323        let node = NodeId::new(4).unwrap();
324        assert_eq!(default_cob_id(PdoKind::Transmit, 1, node), Some(0x184));
325        assert_eq!(default_cob_id(PdoKind::Receive, 1, node), Some(0x204));
326        assert_eq!(default_cob_id(PdoKind::Transmit, 4, node), Some(0x484));
327        assert_eq!(default_cob_id(PdoKind::Receive, 4, node), Some(0x504));
328        assert_eq!(default_cob_id(PdoKind::Transmit, 5, node), None);
329    }
330
331    #[test]
332    fn comm_param_cob_id_validity() {
333        // TPDO1 of node 4, valid: 0x00000184. Invalid sets bit 31.
334        assert!(pdo_is_valid(0x0000_0184));
335        assert!(!pdo_is_valid(0x8000_0184));
336        assert_eq!(pdo_can_id(0x8000_0184), 0x184);
337    }
338
339    // --- Transmission type -------------------------------------------------
340    #[test]
341    fn transmission_types_roundtrip() {
342        for byte in [0u8, 1, 240, 252, 253, 254, 255] {
343            let t = TransmissionType::from_byte(byte).unwrap();
344            assert_eq!(t.to_byte(), byte);
345        }
346        assert_eq!(
347            TransmissionType::from_byte(1).unwrap(),
348            TransmissionType::SynchronousCyclic(1)
349        );
350        assert_eq!(
351            TransmissionType::from_byte(245),
352            Err(Error::UnsupportedTransfer)
353        );
354    }
355
356    // --- Mapping entry codec ----------------------------------------------
357    // Known-good mapping value: object 0x6000 sub 0x01, 8 bits → 0x60000108.
358    #[test]
359    fn mapping_entry_matches_known_value() {
360        let e = MappingEntry::new(0x6000, 0x01, 8);
361        assert_eq!(e.to_u32(), 0x6000_0108);
362        assert_eq!(MappingEntry::from_u32(0x6000_0108), e);
363    }
364
365    #[test]
366    fn mapping_rejects_over_eight_bytes() {
367        let mut m: PdoMapping<8> = PdoMapping::new();
368        // 4 × U16 = 64 bits, exactly full.
369        for i in 0..4 {
370            m.push(MappingEntry::new(0x2000 + i, 0, 16)).unwrap();
371        }
372        assert_eq!(m.total_bytes(), 8);
373        // One more bit over the limit is rejected.
374        assert_eq!(
375            m.push(MappingEntry::new(0x2100, 0, 8)),
376            Err(Error::PdoTooLong)
377        );
378    }
379
380    // --- Pack / unpack -----------------------------------------------------
381    // A TPDO mapping two objects: U16 at 0x6000/1 then U8 at 0x6000/2.
382    fn sample_od() -> ObjectDictionary<8> {
383        let mut od = ObjectDictionary::new();
384        od.insert(
385            Address::new(0x6000, 1),
386            Entry::rw(Value::Unsigned16(0xBEEF)),
387        )
388        .unwrap();
389        od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
390            .unwrap();
391        od
392    }
393
394    fn sample_mapping() -> PdoMapping<8> {
395        let mut m = PdoMapping::new();
396        m.push(MappingEntry::new(0x6000, 1, 16)).unwrap();
397        m.push(MappingEntry::new(0x6000, 2, 8)).unwrap();
398        m
399    }
400
401    #[test]
402    fn pack_lays_out_little_endian_in_order() {
403        let od = sample_od();
404        let mapping = sample_mapping();
405        let mut buf = [0u8; 8];
406        let n = pack(&mapping, &od, &mut buf).unwrap();
407        // U16 0xBEEF little-endian (EF BE) then U8 0x42.
408        assert_eq!(n, 3);
409        assert_eq!(&buf[..n], &[0xEF, 0xBE, 0x42]);
410    }
411
412    #[test]
413    fn unpack_writes_fields_back_into_od() {
414        let mut od = sample_od();
415        let mapping = sample_mapping();
416        // New values: U16 0x1234, U8 0x99.
417        unpack(&mapping, &mut od, &[0x34, 0x12, 0x99]).unwrap();
418        assert_eq!(
419            od.read(Address::new(0x6000, 1)).unwrap(),
420            Value::Unsigned16(0x1234)
421        );
422        assert_eq!(
423            od.read(Address::new(0x6000, 2)).unwrap(),
424            Value::Unsigned8(0x99)
425        );
426    }
427
428    #[test]
429    fn pack_unpack_roundtrip() {
430        let od = sample_od();
431        let mapping = sample_mapping();
432        let mut buf = [0u8; 8];
433        let n = pack(&mapping, &od, &mut buf).unwrap();
434
435        let mut dest = ObjectDictionary::<8>::new();
436        dest.insert(Address::new(0x6000, 1), Entry::rw(Value::Unsigned16(0)))
437            .unwrap();
438        dest.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0)))
439            .unwrap();
440        unpack(&mapping, &mut dest, &buf[..n]).unwrap();
441
442        assert_eq!(
443            dest.read(Address::new(0x6000, 1)).unwrap(),
444            Value::Unsigned16(0xBEEF)
445        );
446        assert_eq!(
447            dest.read(Address::new(0x6000, 2)).unwrap(),
448            Value::Unsigned8(0x42)
449        );
450    }
451
452    #[test]
453    fn pack_rejects_short_buffer() {
454        let od = sample_od();
455        let mapping = sample_mapping();
456        let mut buf = [0u8; 2];
457        assert_eq!(pack(&mapping, &od, &mut buf), Err(Error::BadLength));
458    }
459
460    #[test]
461    fn unpack_into_read_only_object_errors() {
462        let mut od = ObjectDictionary::<4>::new();
463        od.insert(Address::new(0x6000, 1), Entry::ro(Value::Unsigned16(0)))
464            .unwrap();
465        let mut m: PdoMapping<4> = PdoMapping::new();
466        m.push(MappingEntry::new(0x6000, 1, 16)).unwrap();
467        assert_eq!(unpack(&m, &mut od, &[0, 0]), Err(Error::ReadOnly));
468    }
469
470    #[test]
471    fn mapping_width_mismatch_errors() {
472        // Map 0x6000/2 (a U8) as 16 bits — inconsistent with the stored type.
473        let od = sample_od();
474        let mut m: PdoMapping<4> = PdoMapping::new();
475        m.push(MappingEntry::new(0x6000, 2, 16)).unwrap();
476        let mut buf = [0u8; 8];
477        assert_eq!(pack(&m, &od, &mut buf), Err(Error::TypeMismatch));
478    }
479}