canopen_rs/object_dictionary.rs
1//! The CANopen object dictionary (OD) model.
2//!
3//! Every CANopen device exposes its data as a dictionary of objects addressed
4//! by a 16-bit `index` and an 8-bit `subindex` (CiA 301 §7.4). Communication
5//! objects live in `0x1000..=0x1FFF`, manufacturer-specific objects in
6//! `0x2000..=0x5FFF`, and standardised device-profile objects in
7//! `0x6000..=0x9FFF`.
8//!
9//! [`ObjectDictionary`] is backed by a [`heapless::LinearMap`] with a
10//! compile-time capacity, so the same type serves both `no_std` embedded
11//! nodes and host tooling with no allocator.
12//!
13//! ```
14//! use canopen_rs::{Address, Entry, ObjectDictionary, Value};
15//!
16//! let mut od = ObjectDictionary::<8>::new();
17//! // 0x1017 producer heartbeat time — a read/write UNSIGNED16.
18//! od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000))).unwrap();
19//!
20//! assert_eq!(od.read(Address::new(0x1017, 0)).unwrap(), Value::Unsigned16(1000));
21//! od.write(Address::new(0x1017, 0), Value::Unsigned16(500)).unwrap();
22//! assert_eq!(od.read(Address::new(0x1017, 0)).unwrap(), Value::Unsigned16(500));
23//!
24//! // Access rights and data types are enforced.
25//! od.insert(Address::new(0x1000, 0), Entry::constant(Value::Unsigned32(0x192))).unwrap();
26//! assert!(od.write(Address::new(0x1000, 0), Value::Unsigned32(1)).is_err()); // read-only
27//! ```
28
29use heapless::LinearMap;
30
31use crate::datatypes::Value;
32use crate::{Error, Result};
33
34/// The address of an object dictionary entry: a 16-bit index and 8-bit
35/// subindex.
36///
37/// Subindex `0` conventionally holds the "number of entries" for `RECORD` and
38/// `ARRAY` objects; simple `VAR` objects use subindex `0` for the value.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub struct Address {
41 /// 16-bit object index.
42 pub index: u16,
43 /// 8-bit subindex.
44 pub subindex: u8,
45}
46
47impl Address {
48 /// Construct an address from an index and subindex.
49 pub const fn new(index: u16, subindex: u8) -> Self {
50 Self { index, subindex }
51 }
52}
53
54/// Access rights for an object dictionary entry.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum AccessType {
57 /// Read-only.
58 Ro,
59 /// Write-only.
60 Wo,
61 /// Read/write.
62 Rw,
63 /// Read-only constant (value fixed at design time).
64 Const,
65}
66
67impl AccessType {
68 /// Whether an SDO/PDO read is permitted.
69 pub const fn is_readable(self) -> bool {
70 matches!(self, AccessType::Ro | AccessType::Rw | AccessType::Const)
71 }
72
73 /// Whether an SDO/PDO write is permitted.
74 pub const fn is_writable(self) -> bool {
75 matches!(self, AccessType::Wo | AccessType::Rw)
76 }
77}
78
79/// A single object dictionary entry: a typed value and its access rights.
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub struct Entry {
82 /// The current value (its variant fixes the object's data type).
83 pub value: Value,
84 /// The entry's access rights.
85 pub access: AccessType,
86}
87
88impl Entry {
89 /// A read/write entry.
90 pub const fn rw(value: Value) -> Self {
91 Self {
92 value,
93 access: AccessType::Rw,
94 }
95 }
96
97 /// A read-only entry.
98 pub const fn ro(value: Value) -> Self {
99 Self {
100 value,
101 access: AccessType::Ro,
102 }
103 }
104
105 /// A read-only constant entry.
106 pub const fn constant(value: Value) -> Self {
107 Self {
108 value,
109 access: AccessType::Const,
110 }
111 }
112}
113
114/// An object dictionary holding up to `N` entries, addressed by
115/// `(index, subindex)`.
116#[derive(Debug)]
117pub struct ObjectDictionary<const N: usize> {
118 entries: LinearMap<Address, Entry, N>,
119}
120
121impl<const N: usize> Default for ObjectDictionary<N> {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl<const N: usize> ObjectDictionary<N> {
128 /// Create an empty object dictionary.
129 pub const fn new() -> Self {
130 Self {
131 entries: LinearMap::new(),
132 }
133 }
134
135 /// Insert or replace the entry at `addr`.
136 ///
137 /// Returns [`Error::DictionaryFull`] when capacity `N` is exhausted.
138 pub fn insert(&mut self, addr: Address, entry: Entry) -> Result<()> {
139 self.entries
140 .insert(addr, entry)
141 .map(|_| ())
142 .map_err(|_| Error::DictionaryFull)
143 }
144
145 /// Borrow the raw entry at `addr`, ignoring access rights.
146 pub fn entry(&self, addr: Address) -> Option<&Entry> {
147 self.entries.get(&addr)
148 }
149
150 /// Read the value at `addr`, honouring access rights.
151 ///
152 /// Returns [`Error::ObjectNotFound`] if absent, or [`Error::WriteOnly`] if
153 /// the object is not readable.
154 pub fn read(&self, addr: Address) -> Result<Value> {
155 let entry = self.entries.get(&addr).ok_or(Error::ObjectNotFound)?;
156 if !entry.access.is_readable() {
157 return Err(Error::WriteOnly);
158 }
159 Ok(entry.value)
160 }
161
162 /// Write `value` at `addr`, honouring access rights and the object's data
163 /// type.
164 ///
165 /// Returns [`Error::ObjectNotFound`] if absent, [`Error::ReadOnly`] if not
166 /// writable, or [`Error::TypeMismatch`] if `value`'s type differs from the
167 /// stored object's type.
168 pub fn write(&mut self, addr: Address, value: Value) -> Result<()> {
169 let entry = self.entries.get_mut(&addr).ok_or(Error::ObjectNotFound)?;
170 if !entry.access.is_writable() {
171 return Err(Error::ReadOnly);
172 }
173 if entry.value.data_type() != value.data_type() {
174 return Err(Error::TypeMismatch);
175 }
176 entry.value = value;
177 Ok(())
178 }
179
180 /// Set the value at `addr` from the **device side**, ignoring master access
181 /// rights — those govern SDO access, not the application, so this can update
182 /// a master-read-only object (e.g. publish process data into a TPDO source,
183 /// or the node's error register `0x1001`). The object must exist and the
184 /// value's type must match.
185 ///
186 /// Returns [`Error::ObjectNotFound`] if absent or [`Error::TypeMismatch`] on
187 /// a type mismatch.
188 pub fn set(&mut self, addr: Address, value: Value) -> Result<()> {
189 let entry = self.entries.get_mut(&addr).ok_or(Error::ObjectNotFound)?;
190 if entry.value.data_type() != value.data_type() {
191 return Err(Error::TypeMismatch);
192 }
193 entry.value = value;
194 Ok(())
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::datatypes::Value;
202
203 fn od() -> ObjectDictionary<8> {
204 let mut od = ObjectDictionary::new();
205 // 0x1000 Device type — read-only constant.
206 od.insert(
207 Address::new(0x1000, 0),
208 Entry::constant(Value::Unsigned32(0x0000_0192)),
209 )
210 .unwrap();
211 // 0x1017 Producer heartbeat time — read/write.
212 od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
213 .unwrap();
214 od
215 }
216
217 #[test]
218 fn read_returns_stored_value() {
219 assert_eq!(
220 od().read(Address::new(0x1000, 0)).unwrap(),
221 Value::Unsigned32(0x192)
222 );
223 }
224
225 #[test]
226 fn read_missing_object_errors() {
227 assert_eq!(
228 od().read(Address::new(0x9999, 0)),
229 Err(Error::ObjectNotFound)
230 );
231 }
232
233 #[test]
234 fn write_updates_value() {
235 let mut od = od();
236 od.write(Address::new(0x1017, 0), Value::Unsigned16(500))
237 .unwrap();
238 assert_eq!(
239 od.read(Address::new(0x1017, 0)).unwrap(),
240 Value::Unsigned16(500)
241 );
242 }
243
244 #[test]
245 fn write_read_only_object_errors() {
246 let mut od = od();
247 assert_eq!(
248 od.write(Address::new(0x1000, 0), Value::Unsigned32(1)),
249 Err(Error::ReadOnly)
250 );
251 }
252
253 #[test]
254 fn write_wrong_type_errors() {
255 let mut od = od();
256 assert_eq!(
257 od.write(Address::new(0x1017, 0), Value::Unsigned32(1)),
258 Err(Error::TypeMismatch)
259 );
260 }
261
262 #[test]
263 fn dictionary_full_errors() {
264 let mut od: ObjectDictionary<1> = ObjectDictionary::new();
265 od.insert(Address::new(0x2000, 0), Entry::rw(Value::Unsigned8(1)))
266 .unwrap();
267 assert_eq!(
268 od.insert(Address::new(0x2001, 0), Entry::rw(Value::Unsigned8(2))),
269 Err(Error::DictionaryFull)
270 );
271 }
272}