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
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::datatypes::Value;
185
186 fn od() -> ObjectDictionary<8> {
187 let mut od = ObjectDictionary::new();
188 // 0x1000 Device type โ read-only constant.
189 od.insert(
190 Address::new(0x1000, 0),
191 Entry::constant(Value::Unsigned32(0x0000_0192)),
192 )
193 .unwrap();
194 // 0x1017 Producer heartbeat time โ read/write.
195 od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
196 .unwrap();
197 od
198 }
199
200 #[test]
201 fn read_returns_stored_value() {
202 assert_eq!(
203 od().read(Address::new(0x1000, 0)).unwrap(),
204 Value::Unsigned32(0x192)
205 );
206 }
207
208 #[test]
209 fn read_missing_object_errors() {
210 assert_eq!(
211 od().read(Address::new(0x9999, 0)),
212 Err(Error::ObjectNotFound)
213 );
214 }
215
216 #[test]
217 fn write_updates_value() {
218 let mut od = od();
219 od.write(Address::new(0x1017, 0), Value::Unsigned16(500))
220 .unwrap();
221 assert_eq!(
222 od.read(Address::new(0x1017, 0)).unwrap(),
223 Value::Unsigned16(500)
224 );
225 }
226
227 #[test]
228 fn write_read_only_object_errors() {
229 let mut od = od();
230 assert_eq!(
231 od.write(Address::new(0x1000, 0), Value::Unsigned32(1)),
232 Err(Error::ReadOnly)
233 );
234 }
235
236 #[test]
237 fn write_wrong_type_errors() {
238 let mut od = od();
239 assert_eq!(
240 od.write(Address::new(0x1017, 0), Value::Unsigned32(1)),
241 Err(Error::TypeMismatch)
242 );
243 }
244
245 #[test]
246 fn dictionary_full_errors() {
247 let mut od: ObjectDictionary<1> = ObjectDictionary::new();
248 od.insert(Address::new(0x2000, 0), Entry::rw(Value::Unsigned8(1)))
249 .unwrap();
250 assert_eq!(
251 od.insert(Address::new(0x2001, 0), Entry::rw(Value::Unsigned8(2))),
252 Err(Error::DictionaryFull)
253 );
254 }
255}