Skip to main content

canopen_rs/
standard.rs

1//! The mandatory CiA 301 communication-profile objects.
2//!
3//! Every conformant CANopen device exposes a common baseline of objects in the
4//! communication profile area (`0x1000`–`0x1FFF`): the device type, error
5//! register, heartbeat time, and identity. Hand-inserting them into an
6//! [`ObjectDictionary`] for every node is tedious and easy to get wrong, so
7//! [`StandardObjects`] builds that baseline in one call.
8//!
9//! It ties the recent pieces together: the identity (`0x1018`) is the same
10//! [`LssAddress`] an LSS master matches (see [`crate::lss`]), and the error
11//! register (`0x1001`) / pre-defined error field (`0x1003`) are exactly the
12//! objects [`Node::raise_emergency`](crate::node::Node::raise_emergency) mirrors
13//! into, so a node built this way is ready for both LSS and EMCY out of the box.
14//!
15//! ```
16//! use canopen_rs::{LssAddress, Node, NodeId, ObjectDictionary};
17//! use canopen_rs::standard::StandardObjects;
18//!
19//! let identity = LssAddress { vendor_id: 0x0000_012E, product_code: 0x4001, revision_number: 1, serial_number: 0xC0FFEE };
20//! let mut od = ObjectDictionary::<16>::new();
21//! StandardObjects::new(0x0004_0192, identity)
22//!     .with_heartbeat(1000)
23//!     .with_error_history(4)
24//!     .insert_into(&mut od)
25//!     .unwrap();
26//!
27//! let mut node = Node::new(NodeId::new(0x10).unwrap(), od);
28//! // 0x1018 is populated, so the same identity enables LSS/Fastscan…
29//! node.enable_lss(identity);
30//! ```
31
32use crate::datatypes::{ByteString, Value, MAX_STRING_LEN};
33use crate::lss::LssAddress;
34use crate::object_dictionary::{Address, Entry, ObjectDictionary};
35use crate::Result;
36
37/// Builder for the mandatory CiA 301 communication-profile objects.
38///
39/// The objects it inserts are all node-id-independent, so it can populate a
40/// dictionary before a node-id is known (e.g. an LSS-unconfigured node). COB-ID
41/// objects that depend on the node-id (`0x1014` EMCY, `0x1005` SYNC) are left to
42/// the application, which knows the assigned id.
43#[derive(Debug, Clone, Copy)]
44pub struct StandardObjects {
45    /// Object `0x1000` — device type (profile and type information).
46    pub device_type: u32,
47    /// Object `0x1018` — the identity record (vendor / product / revision /
48    /// serial), the same value an LSS master matches.
49    pub identity: LssAddress,
50    /// Object `0x1017` — producer heartbeat time, in milliseconds (`0` disables).
51    pub producer_heartbeat_ms: u16,
52    /// Number of `0x1003` pre-defined-error-field sub-entries to pre-allocate
53    /// (`0` omits the object entirely).
54    pub error_history_len: u8,
55    /// Object `0x1008` — the manufacturer device name (a `VISIBLE_STRING`), or
56    /// `None` to omit it.
57    pub device_name: Option<ByteString>,
58}
59
60impl StandardObjects {
61    /// A baseline with the given device type and identity, no heartbeat, and no
62    /// pre-defined error field. Refine with [`with_heartbeat`](Self::with_heartbeat)
63    /// and [`with_error_history`](Self::with_error_history).
64    pub const fn new(device_type: u32, identity: LssAddress) -> Self {
65        Self {
66            device_type,
67            identity,
68            producer_heartbeat_ms: 0,
69            error_history_len: 0,
70            device_name: None,
71        }
72    }
73
74    /// Set the producer heartbeat time (`0x1017`), in milliseconds.
75    pub fn with_heartbeat(mut self, ms: u16) -> Self {
76        self.producer_heartbeat_ms = ms;
77        self
78    }
79
80    /// Pre-allocate `entries` sub-entries of the pre-defined error field
81    /// (`0x1003`) so [`Node::raise_emergency`](crate::node::Node::raise_emergency)
82    /// can mirror the error history into the dictionary.
83    pub fn with_error_history(mut self, entries: u8) -> Self {
84        self.error_history_len = entries;
85        self
86    }
87
88    /// Set the device name (`0x1008`, a `VISIBLE_STRING`), truncated to
89    /// [`MAX_STRING_LEN`] bytes.
90    pub fn with_device_name(mut self, name: &str) -> Self {
91        let end = name.len().min(MAX_STRING_LEN);
92        // `end` is a byte length ≤ MAX_STRING_LEN, so from_bytes cannot fail.
93        self.device_name = ByteString::from_bytes(&name.as_bytes()[..end]).ok();
94        self
95    }
96
97    /// Insert the objects into `od`.
98    ///
99    /// Returns [`Error::DictionaryFull`](crate::Error::DictionaryFull) if the
100    /// dictionary lacks capacity — it needs room for the 9 base objects, the
101    /// optional device name, and the error-history entries plus their count
102    /// sub-index. Existing objects at these addresses are overwritten.
103    pub fn insert_into<const N: usize>(&self, od: &mut ObjectDictionary<N>) -> Result<()> {
104        // 0x1000 device type, 0x1001 error register, 0x1017 heartbeat time.
105        od.insert(
106            Address::new(0x1000, 0),
107            Entry::constant(Value::Unsigned32(self.device_type)),
108        )?;
109        od.insert(Address::new(0x1001, 0), Entry::ro(Value::Unsigned8(0)))?;
110        od.insert(
111            Address::new(0x1017, 0),
112            Entry::rw(Value::Unsigned16(self.producer_heartbeat_ms)),
113        )?;
114
115        // 0x1018 identity record: sub 0 = highest sub-index (4), then the four
116        // 32-bit identity values.
117        od.insert(
118            Address::new(0x1018, 0),
119            Entry::constant(Value::Unsigned8(4)),
120        )?;
121        od.insert(
122            Address::new(0x1018, 1),
123            Entry::constant(Value::Unsigned32(self.identity.vendor_id)),
124        )?;
125        od.insert(
126            Address::new(0x1018, 2),
127            Entry::constant(Value::Unsigned32(self.identity.product_code)),
128        )?;
129        od.insert(
130            Address::new(0x1018, 3),
131            Entry::constant(Value::Unsigned32(self.identity.revision_number)),
132        )?;
133        od.insert(
134            Address::new(0x1018, 4),
135            Entry::constant(Value::Unsigned32(self.identity.serial_number)),
136        )?;
137
138        // 0x1008 manufacturer device name (optional).
139        if let Some(name) = self.device_name {
140            od.insert(
141                Address::new(0x1008, 0),
142                Entry::constant(Value::VisibleString(name)),
143            )?;
144        }
145
146        // 0x1003 pre-defined error field: sub 0 is the writable count (a master
147        // writes 0 to clear), subs 1.. hold the recorded error codes.
148        if self.error_history_len > 0 {
149            od.insert(Address::new(0x1003, 0), Entry::rw(Value::Unsigned8(0)))?;
150            for sub in 1..=self.error_history_len {
151                od.insert(Address::new(0x1003, sub), Entry::ro(Value::Unsigned32(0)))?;
152            }
153        }
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::Error;
162
163    fn identity() -> LssAddress {
164        LssAddress {
165            vendor_id: 0x0000_012E,
166            product_code: 0x4001_0002,
167            revision_number: 0x0000_0003,
168            serial_number: 0x00C0_FFEE,
169        }
170    }
171
172    #[test]
173    fn inserts_the_mandatory_baseline() {
174        let mut od = ObjectDictionary::<16>::new();
175        StandardObjects::new(0x0004_0192, identity())
176            .with_heartbeat(1000)
177            .insert_into(&mut od)
178            .unwrap();
179
180        assert_eq!(
181            od.read(Address::new(0x1000, 0)).unwrap(),
182            Value::Unsigned32(0x0004_0192)
183        );
184        assert_eq!(
185            od.read(Address::new(0x1001, 0)).unwrap(),
186            Value::Unsigned8(0)
187        );
188        assert_eq!(
189            od.read(Address::new(0x1017, 0)).unwrap(),
190            Value::Unsigned16(1000)
191        );
192    }
193
194    #[test]
195    fn identity_record_matches_the_lss_address() {
196        let mut od = ObjectDictionary::<16>::new();
197        let id = identity();
198        StandardObjects::new(0, id).insert_into(&mut od).unwrap();
199
200        assert_eq!(
201            od.read(Address::new(0x1018, 0)).unwrap(),
202            Value::Unsigned8(4) // highest sub-index
203        );
204        assert_eq!(
205            od.read(Address::new(0x1018, 1)).unwrap(),
206            Value::Unsigned32(id.vendor_id)
207        );
208        assert_eq!(
209            od.read(Address::new(0x1018, 2)).unwrap(),
210            Value::Unsigned32(id.product_code)
211        );
212        assert_eq!(
213            od.read(Address::new(0x1018, 3)).unwrap(),
214            Value::Unsigned32(id.revision_number)
215        );
216        assert_eq!(
217            od.read(Address::new(0x1018, 4)).unwrap(),
218            Value::Unsigned32(id.serial_number)
219        );
220    }
221
222    #[test]
223    fn error_history_is_optional_and_sized() {
224        // None by default.
225        let mut od = ObjectDictionary::<16>::new();
226        StandardObjects::new(0, identity())
227            .insert_into(&mut od)
228            .unwrap();
229        assert!(od.entry(Address::new(0x1003, 0)).is_none());
230
231        // Requested: count sub-index plus one entry each.
232        let mut od = ObjectDictionary::<16>::new();
233        StandardObjects::new(0, identity())
234            .with_error_history(3)
235            .insert_into(&mut od)
236            .unwrap();
237        assert_eq!(
238            od.read(Address::new(0x1003, 0)).unwrap(),
239            Value::Unsigned8(0)
240        );
241        assert!(od.entry(Address::new(0x1003, 3)).is_some());
242        assert!(od.entry(Address::new(0x1003, 4)).is_none());
243    }
244
245    #[test]
246    fn node_built_from_standard_objects_mirrors_emergency() {
247        use crate::emcy::{error_code, ErrorRegister};
248        use crate::node::Node;
249        use crate::types::NodeId;
250
251        let mut od = ObjectDictionary::<16>::new();
252        StandardObjects::new(0x0004_0192, identity())
253            .with_heartbeat(1000)
254            .with_error_history(4)
255            .insert_into(&mut od)
256            .unwrap();
257        let mut node = Node::new(NodeId::new(0x10).unwrap(), od);
258
259        node.raise_emergency(
260            error_code::VOLTAGE,
261            ErrorRegister(ErrorRegister::VOLTAGE),
262            [0; 5],
263        );
264        // The builder's 0x1001 / 0x1003 objects received the mirror, so a master
265        // can read the register and the recorded code over SDO.
266        assert_eq!(
267            node.od().read(Address::new(0x1001, 0)).unwrap(),
268            Value::Unsigned8(ErrorRegister::GENERIC | ErrorRegister::VOLTAGE)
269        );
270        assert_eq!(
271            node.od().read(Address::new(0x1003, 0)).unwrap(),
272            Value::Unsigned8(1)
273        );
274        assert_eq!(
275            node.od().read(Address::new(0x1003, 1)).unwrap(),
276            Value::Unsigned32(u32::from(error_code::VOLTAGE))
277        );
278    }
279
280    #[test]
281    fn device_name_is_optional_visible_string() {
282        // Omitted by default.
283        let mut od = ObjectDictionary::<16>::new();
284        StandardObjects::new(0, identity())
285            .insert_into(&mut od)
286            .unwrap();
287        assert!(od.entry(Address::new(0x1008, 0)).is_none());
288
289        // Present when set, as a VISIBLE_STRING.
290        let mut od = ObjectDictionary::<16>::new();
291        StandardObjects::new(0, identity())
292            .with_device_name("canopen-rs node")
293            .insert_into(&mut od)
294            .unwrap();
295        match od.read(Address::new(0x1008, 0)).unwrap() {
296            Value::VisibleString(bs) => assert_eq!(bs.as_str(), Some("canopen-rs node")),
297            other => panic!("expected a VISIBLE_STRING, got {other:?}"),
298        }
299    }
300
301    #[test]
302    fn reports_dictionary_full() {
303        // The baseline needs 9 objects; a smaller dictionary overflows.
304        let mut od = ObjectDictionary::<4>::new();
305        assert_eq!(
306            StandardObjects::new(0, identity()).insert_into(&mut od),
307            Err(Error::DictionaryFull)
308        );
309    }
310}