Skip to main content

canopen_rs/
datatypes.rs

1//! CANopen basic data types and their little-endian value encoding.
2//!
3//! CANopen is little-endian on the wire (CiA 301 ยง7.1). This module models the
4//! numeric basic data types plus the variable-length ones โ€” `VISIBLE_STRING`,
5//! `OCTET_STRING`, and `DOMAIN`, carried by a bounded [`ByteString`] โ€” and
6//! converts between typed [`Value`]s and their byte encoding. Variable-length
7//! values (over four bytes) travel by segmented SDO transfer.
8//!
9//! ```
10//! use canopen_rs::{DataType, Value};
11//!
12//! let mut buf = [0u8; 8];
13//! let n = Value::Unsigned32(0xDEAD_BEEF).encode_le(&mut buf).unwrap();
14//! assert_eq!(&buf[..n], &[0xEF, 0xBE, 0xAD, 0xDE]); // little-endian
15//!
16//! let value = Value::decode_le(DataType::Unsigned32, &buf[..n]).unwrap();
17//! assert_eq!(value, Value::Unsigned32(0xDEAD_BEEF));
18//! ```
19
20use crate::{Error, Result};
21
22/// The maximum length, in bytes, of a variable-length value (`VISIBLE_STRING`,
23/// `OCTET_STRING`, `DOMAIN`).
24///
25/// Chosen to keep [`Value`] `Copy` and reasonably small while covering the
26/// typical string objects (device name `0x1008`, versions `0x1009` / `0x100A`).
27/// It also bounds the SDO segmented-transfer buffers, so it is the largest
28/// object value the sans-I/O SDO server and client will transfer.
29pub const MAX_STRING_LEN: usize = 32;
30
31/// A bounded, `Copy` byte string backing the variable-length CANopen types
32/// (`VISIBLE_STRING`, `OCTET_STRING`, `DOMAIN`): up to [`MAX_STRING_LEN`] bytes.
33#[derive(Clone, Copy)]
34pub struct ByteString {
35    data: [u8; MAX_STRING_LEN],
36    len: u8,
37}
38
39impl ByteString {
40    /// An empty byte string.
41    pub const fn new() -> Self {
42        Self {
43            data: [0; MAX_STRING_LEN],
44            len: 0,
45        }
46    }
47
48    /// Build from a byte slice, returning [`Error::BadLength`] if it is longer
49    /// than [`MAX_STRING_LEN`].
50    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
51        if bytes.len() > MAX_STRING_LEN {
52            return Err(Error::BadLength);
53        }
54        let mut data = [0u8; MAX_STRING_LEN];
55        data[..bytes.len()].copy_from_slice(bytes);
56        Ok(Self {
57            data,
58            len: bytes.len() as u8,
59        })
60    }
61
62    /// Build from a string slice (a `VISIBLE_STRING`), returning
63    /// [`Error::BadLength`] if it is longer than [`MAX_STRING_LEN`].
64    // Inherent (fallible, bounded) constructor, not the `FromStr` trait.
65    #[allow(clippy::should_implement_trait)]
66    pub fn from_str(s: &str) -> Result<Self> {
67        Self::from_bytes(s.as_bytes())
68    }
69
70    /// The bytes.
71    pub fn as_bytes(&self) -> &[u8] {
72        &self.data[..self.len as usize]
73    }
74
75    /// Interpret the bytes as UTF-8 text, if valid.
76    pub fn as_str(&self) -> Option<&str> {
77        core::str::from_utf8(self.as_bytes()).ok()
78    }
79
80    /// The length in bytes.
81    pub const fn len(&self) -> usize {
82        self.len as usize
83    }
84
85    /// Whether the string is empty.
86    pub const fn is_empty(&self) -> bool {
87        self.len == 0
88    }
89}
90
91impl Default for ByteString {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97// Equality and Debug compare/show only the meaningful bytes, ignoring the
98// unused tail of the fixed buffer.
99impl PartialEq for ByteString {
100    fn eq(&self, other: &Self) -> bool {
101        self.as_bytes() == other.as_bytes()
102    }
103}
104
105impl Eq for ByteString {}
106
107impl core::fmt::Debug for ByteString {
108    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109        match self.as_str() {
110            Some(s) => write!(f, "ByteString({s:?})"),
111            None => write!(f, "ByteString({:?})", self.as_bytes()),
112        }
113    }
114}
115
116/// A CANopen basic data type.
117///
118/// The discriminant is the CiA 301 data type index (the object index under
119/// which the type is described, e.g. `UNSIGNED32` is `0x0007`).
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121#[non_exhaustive]
122pub enum DataType {
123    /// `BOOLEAN` (0x0001), 1 byte.
124    Boolean = 0x01,
125    /// `INTEGER8` (0x0002), 1 byte.
126    Integer8 = 0x02,
127    /// `INTEGER16` (0x0003), 2 bytes.
128    Integer16 = 0x03,
129    /// `INTEGER32` (0x0004), 4 bytes.
130    Integer32 = 0x04,
131    /// `UNSIGNED8` (0x0005), 1 byte.
132    Unsigned8 = 0x05,
133    /// `UNSIGNED16` (0x0006), 2 bytes.
134    Unsigned16 = 0x06,
135    /// `UNSIGNED32` (0x0007), 4 bytes.
136    Unsigned32 = 0x07,
137    /// `REAL32` (0x0008), 4 bytes.
138    Real32 = 0x08,
139    /// `REAL64` (0x0011), 8 bytes.
140    Real64 = 0x11,
141    /// `INTEGER64` (0x0015), 8 bytes.
142    Integer64 = 0x15,
143    /// `UNSIGNED64` (0x001B), 8 bytes.
144    Unsigned64 = 0x1B,
145    /// `VISIBLE_STRING` (0x0009), variable length (ASCII/UTF-8 text).
146    VisibleString = 0x09,
147    /// `OCTET_STRING` (0x000A), variable length (arbitrary bytes).
148    OctetString = 0x0A,
149    /// `DOMAIN` (0x000F), variable length (arbitrary bytes).
150    Domain = 0x0F,
151}
152
153impl DataType {
154    /// The CiA 301 data type index (e.g. `0x0007` for `UNSIGNED32`).
155    pub const fn index(self) -> u16 {
156        self as u16
157    }
158
159    /// The basic data type for a CiA 301 data type index, or `None` for an
160    /// index this crate does not model.
161    pub const fn from_index(index: u16) -> Option<Self> {
162        Some(match index {
163            0x01 => DataType::Boolean,
164            0x02 => DataType::Integer8,
165            0x03 => DataType::Integer16,
166            0x04 => DataType::Integer32,
167            0x05 => DataType::Unsigned8,
168            0x06 => DataType::Unsigned16,
169            0x07 => DataType::Unsigned32,
170            0x08 => DataType::Real32,
171            0x09 => DataType::VisibleString,
172            0x0A => DataType::OctetString,
173            0x0F => DataType::Domain,
174            0x11 => DataType::Real64,
175            0x15 => DataType::Integer64,
176            0x1B => DataType::Unsigned64,
177            _ => return None,
178        })
179    }
180
181    /// The fixed encoded size of a value of this type in bytes, or `None` for a
182    /// variable-length type (`VISIBLE_STRING`, `OCTET_STRING`, `DOMAIN`).
183    pub const fn fixed_size(self) -> Option<usize> {
184        Some(match self {
185            DataType::Boolean | DataType::Integer8 | DataType::Unsigned8 => 1,
186            DataType::Integer16 | DataType::Unsigned16 => 2,
187            DataType::Integer32 | DataType::Unsigned32 | DataType::Real32 => 4,
188            DataType::Integer64 | DataType::Unsigned64 | DataType::Real64 => 8,
189            DataType::VisibleString | DataType::OctetString | DataType::Domain => return None,
190        })
191    }
192
193    /// Whether this is a variable-length type.
194    pub const fn is_variable(self) -> bool {
195        self.fixed_size().is_none()
196    }
197
198    /// The fixed encoded size of a value of this type in bytes, or `0` for a
199    /// variable-length type (whose size depends on the value โ€” see
200    /// [`fixed_size`](DataType::fixed_size)).
201    pub const fn size(self) -> usize {
202        match self.fixed_size() {
203            Some(n) => n,
204            None => 0,
205        }
206    }
207}
208
209/// A typed CANopen value.
210#[derive(Debug, Clone, Copy, PartialEq)]
211#[non_exhaustive]
212pub enum Value {
213    /// A `BOOLEAN`.
214    Boolean(bool),
215    /// An `INTEGER8`.
216    Integer8(i8),
217    /// An `INTEGER16`.
218    Integer16(i16),
219    /// An `INTEGER32`.
220    Integer32(i32),
221    /// An `UNSIGNED8`.
222    Unsigned8(u8),
223    /// An `UNSIGNED16`.
224    Unsigned16(u16),
225    /// An `UNSIGNED32`.
226    Unsigned32(u32),
227    /// A `REAL32`.
228    Real32(f32),
229    /// A `REAL64`.
230    Real64(f64),
231    /// An `INTEGER64`.
232    Integer64(i64),
233    /// An `UNSIGNED64`.
234    Unsigned64(u64),
235    /// A `VISIBLE_STRING` (ASCII/UTF-8 text).
236    VisibleString(ByteString),
237    /// An `OCTET_STRING` (arbitrary bytes).
238    OctetString(ByteString),
239    /// A `DOMAIN` (arbitrary bytes).
240    Domain(ByteString),
241}
242
243impl Value {
244    /// The data type of this value.
245    pub const fn data_type(&self) -> DataType {
246        match self {
247            Value::Boolean(_) => DataType::Boolean,
248            Value::Integer8(_) => DataType::Integer8,
249            Value::Integer16(_) => DataType::Integer16,
250            Value::Integer32(_) => DataType::Integer32,
251            Value::Unsigned8(_) => DataType::Unsigned8,
252            Value::Unsigned16(_) => DataType::Unsigned16,
253            Value::Unsigned32(_) => DataType::Unsigned32,
254            Value::Real32(_) => DataType::Real32,
255            Value::Real64(_) => DataType::Real64,
256            Value::Integer64(_) => DataType::Integer64,
257            Value::Unsigned64(_) => DataType::Unsigned64,
258            Value::VisibleString(_) => DataType::VisibleString,
259            Value::OctetString(_) => DataType::OctetString,
260            Value::Domain(_) => DataType::Domain,
261        }
262    }
263
264    /// The encoded size of this value, in bytes. For a variable-length value
265    /// this is its current content length.
266    pub const fn size(&self) -> usize {
267        match self {
268            Value::VisibleString(s) | Value::OctetString(s) | Value::Domain(s) => s.len(),
269            _ => self.data_type().size(),
270        }
271    }
272
273    /// Encode this value little-endian into `buf`, returning the number of
274    /// bytes written.
275    ///
276    /// Returns [`Error::BadLength`] if `buf` is smaller than [`Value::size`].
277    pub fn encode_le(&self, buf: &mut [u8]) -> Result<usize> {
278        let n = self.size();
279        if buf.len() < n {
280            return Err(Error::BadLength);
281        }
282        match self {
283            Value::Boolean(v) => buf[0] = *v as u8,
284            Value::Integer8(v) => buf[0] = *v as u8,
285            Value::Unsigned8(v) => buf[0] = *v,
286            Value::Integer16(v) => buf[..2].copy_from_slice(&v.to_le_bytes()),
287            Value::Unsigned16(v) => buf[..2].copy_from_slice(&v.to_le_bytes()),
288            Value::Integer32(v) => buf[..4].copy_from_slice(&v.to_le_bytes()),
289            Value::Unsigned32(v) => buf[..4].copy_from_slice(&v.to_le_bytes()),
290            Value::Real32(v) => buf[..4].copy_from_slice(&v.to_le_bytes()),
291            Value::Integer64(v) => buf[..8].copy_from_slice(&v.to_le_bytes()),
292            Value::Unsigned64(v) => buf[..8].copy_from_slice(&v.to_le_bytes()),
293            Value::Real64(v) => buf[..8].copy_from_slice(&v.to_le_bytes()),
294            Value::VisibleString(s) | Value::OctetString(s) | Value::Domain(s) => {
295                buf[..n].copy_from_slice(s.as_bytes())
296            }
297        }
298        Ok(n)
299    }
300
301    /// Decode a little-endian value of `data_type` from `bytes`.
302    ///
303    /// For a fixed-size type `bytes` must be exactly [`DataType::fixed_size`]
304    /// long; for a variable-length type it may be any length up to
305    /// [`MAX_STRING_LEN`]. Otherwise returns [`Error::BadLength`].
306    pub fn decode_le(data_type: DataType, bytes: &[u8]) -> Result<Value> {
307        match data_type.fixed_size() {
308            Some(fixed) if bytes.len() != fixed => return Err(Error::BadLength),
309            None if bytes.len() > MAX_STRING_LEN => return Err(Error::BadLength),
310            _ => {}
311        }
312        let v = match data_type {
313            DataType::Boolean => Value::Boolean(bytes[0] != 0),
314            DataType::Integer8 => Value::Integer8(bytes[0] as i8),
315            DataType::Unsigned8 => Value::Unsigned8(bytes[0]),
316            DataType::Integer16 => Value::Integer16(i16::from_le_bytes([bytes[0], bytes[1]])),
317            DataType::Unsigned16 => Value::Unsigned16(u16::from_le_bytes([bytes[0], bytes[1]])),
318            DataType::Integer32 => {
319                Value::Integer32(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
320            }
321            DataType::Unsigned32 => {
322                Value::Unsigned32(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
323            }
324            DataType::Real32 => {
325                Value::Real32(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
326            }
327            DataType::Integer64 => Value::Integer64(i64::from_le_bytes([
328                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
329            ])),
330            DataType::Unsigned64 => Value::Unsigned64(u64::from_le_bytes([
331                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
332            ])),
333            DataType::Real64 => Value::Real64(f64::from_le_bytes([
334                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
335            ])),
336            DataType::VisibleString => Value::VisibleString(ByteString::from_bytes(bytes)?),
337            DataType::OctetString => Value::OctetString(ByteString::from_bytes(bytes)?),
338            DataType::Domain => Value::Domain(ByteString::from_bytes(bytes)?),
339        };
340        Ok(v)
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn sizes_match_spec() {
350        assert_eq!(DataType::Boolean.size(), 1);
351        assert_eq!(DataType::Unsigned16.size(), 2);
352        assert_eq!(DataType::Unsigned32.size(), 4);
353        assert_eq!(DataType::Real64.size(), 8);
354    }
355
356    #[test]
357    fn data_type_index_roundtrips() {
358        for dt in [
359            DataType::Boolean,
360            DataType::Unsigned32,
361            DataType::Real64,
362            DataType::Integer64,
363            DataType::Unsigned64,
364            DataType::VisibleString,
365            DataType::OctetString,
366            DataType::Domain,
367        ] {
368            assert_eq!(DataType::from_index(dt.index()), Some(dt));
369        }
370        assert_eq!(DataType::Unsigned32.index(), 0x0007);
371        assert_eq!(DataType::VisibleString.index(), 0x0009);
372        // A genuinely unmodelled type (e.g. TIME_OF_DAY 0x0C) is rejected.
373        assert_eq!(DataType::from_index(0x0C), None);
374    }
375
376    #[test]
377    fn variable_length_types_have_no_fixed_size() {
378        for dt in [
379            DataType::VisibleString,
380            DataType::OctetString,
381            DataType::Domain,
382        ] {
383            assert!(dt.is_variable());
384            assert_eq!(dt.fixed_size(), None);
385            assert_eq!(dt.size(), 0);
386        }
387        assert!(!DataType::Unsigned32.is_variable());
388        assert_eq!(DataType::Unsigned32.fixed_size(), Some(4));
389    }
390
391    #[test]
392    fn string_values_round_trip() {
393        let mut buf = [0u8; MAX_STRING_LEN];
394        for v in [
395            Value::VisibleString(ByteString::from_str("canopen-rs").unwrap()),
396            Value::OctetString(ByteString::from_bytes(&[0, 1, 2, 0xFF]).unwrap()),
397            Value::Domain(ByteString::from_bytes(&[]).unwrap()), // empty
398        ] {
399            let n = v.encode_le(&mut buf).unwrap();
400            assert_eq!(n, v.size());
401            let back = Value::decode_le(v.data_type(), &buf[..n]).unwrap();
402            assert_eq!(v, back);
403        }
404    }
405
406    #[test]
407    fn bytestring_rejects_oversized_input() {
408        let too_long = [0u8; MAX_STRING_LEN + 1];
409        assert_eq!(ByteString::from_bytes(&too_long), Err(Error::BadLength));
410        assert_eq!(
411            Value::decode_le(DataType::VisibleString, &too_long),
412            Err(Error::BadLength)
413        );
414    }
415
416    #[test]
417    fn bytestring_equality_ignores_unused_tail() {
418        let a = ByteString::from_str("hi").unwrap();
419        let b = ByteString::from_bytes(b"hi").unwrap();
420        assert_eq!(a, b);
421        assert_eq!(a.as_str(), Some("hi"));
422        assert_eq!(a.len(), 2);
423    }
424
425    #[test]
426    fn u32_is_little_endian() {
427        let mut buf = [0u8; 8];
428        let n = Value::Unsigned32(0x1234_5678).encode_le(&mut buf).unwrap();
429        assert_eq!(n, 4);
430        assert_eq!(&buf[..4], &[0x78, 0x56, 0x34, 0x12]);
431    }
432
433    #[test]
434    fn decode_roundtrips() {
435        let cases = [
436            Value::Boolean(true),
437            Value::Integer8(-5),
438            Value::Unsigned16(0xBEEF),
439            Value::Integer32(-123456),
440            Value::Unsigned32(0xDEAD_BEEF),
441            Value::Unsigned64(0x0102_0304_0506_0708),
442        ];
443        let mut buf = [0u8; 8];
444        for v in cases {
445            let n = v.encode_le(&mut buf).unwrap();
446            let back = Value::decode_le(v.data_type(), &buf[..n]).unwrap();
447            assert_eq!(v, back);
448        }
449    }
450
451    #[test]
452    fn encode_rejects_short_buffer() {
453        let mut buf = [0u8; 2];
454        assert_eq!(
455            Value::Unsigned32(0).encode_le(&mut buf),
456            Err(Error::BadLength)
457        );
458    }
459
460    #[test]
461    fn decode_rejects_wrong_length() {
462        assert_eq!(
463            Value::decode_le(DataType::Unsigned32, &[0, 0]),
464            Err(Error::BadLength)
465        );
466    }
467}