Skip to main content

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}