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
4//! the numeric basic data types and converts between typed [`Value`]s and
5//! their byte encoding. Variable-length types (strings, `DOMAIN`) arrive with
6//! segmented/block transfer and are added later.
7//!
8//! ```
9//! use canopen_rs::{DataType, Value};
10//!
11//! let mut buf = [0u8; 8];
12//! let n = Value::Unsigned32(0xDEAD_BEEF).encode_le(&mut buf).unwrap();
13//! assert_eq!(&buf[..n], &[0xEF, 0xBE, 0xAD, 0xDE]); // little-endian
14//!
15//! let value = Value::decode_le(DataType::Unsigned32, &buf[..n]).unwrap();
16//! assert_eq!(value, Value::Unsigned32(0xDEAD_BEEF));
17//! ```
18
19use crate::{Error, Result};
20
21/// A CANopen basic data type.
22///
23/// The discriminant is the CiA 301 data type index (the object index under
24/// which the type is described, e.g. `UNSIGNED32` is `0x0007`).
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum DataType {
28    /// `BOOLEAN` (0x0001), 1 byte.
29    Boolean = 0x01,
30    /// `INTEGER8` (0x0002), 1 byte.
31    Integer8 = 0x02,
32    /// `INTEGER16` (0x0003), 2 bytes.
33    Integer16 = 0x03,
34    /// `INTEGER32` (0x0004), 4 bytes.
35    Integer32 = 0x04,
36    /// `UNSIGNED8` (0x0005), 1 byte.
37    Unsigned8 = 0x05,
38    /// `UNSIGNED16` (0x0006), 2 bytes.
39    Unsigned16 = 0x06,
40    /// `UNSIGNED32` (0x0007), 4 bytes.
41    Unsigned32 = 0x07,
42    /// `REAL32` (0x0008), 4 bytes.
43    Real32 = 0x08,
44    /// `REAL64` (0x0011), 8 bytes.
45    Real64 = 0x11,
46    /// `INTEGER64` (0x0015), 8 bytes.
47    Integer64 = 0x15,
48    /// `UNSIGNED64` (0x001B), 8 bytes.
49    Unsigned64 = 0x1B,
50}
51
52impl DataType {
53    /// The CiA 301 data type index (e.g. `0x0007` for `UNSIGNED32`).
54    pub const fn index(self) -> u16 {
55        self as u16
56    }
57
58    /// The basic data type for a CiA 301 data type index, or `None` for an
59    /// index this crate does not yet model (e.g. the string and `DOMAIN`
60    /// types).
61    pub const fn from_index(index: u16) -> Option<Self> {
62        Some(match index {
63            0x01 => DataType::Boolean,
64            0x02 => DataType::Integer8,
65            0x03 => DataType::Integer16,
66            0x04 => DataType::Integer32,
67            0x05 => DataType::Unsigned8,
68            0x06 => DataType::Unsigned16,
69            0x07 => DataType::Unsigned32,
70            0x08 => DataType::Real32,
71            0x11 => DataType::Real64,
72            0x15 => DataType::Integer64,
73            0x1B => DataType::Unsigned64,
74            _ => return None,
75        })
76    }
77
78    /// The encoded size of a value of this type, in bytes.
79    pub const fn size(self) -> usize {
80        match self {
81            DataType::Boolean | DataType::Integer8 | DataType::Unsigned8 => 1,
82            DataType::Integer16 | DataType::Unsigned16 => 2,
83            DataType::Integer32 | DataType::Unsigned32 | DataType::Real32 => 4,
84            DataType::Integer64 | DataType::Unsigned64 | DataType::Real64 => 8,
85        }
86    }
87}
88
89/// A typed CANopen value.
90#[derive(Debug, Clone, Copy, PartialEq)]
91#[non_exhaustive]
92pub enum Value {
93    /// A `BOOLEAN`.
94    Boolean(bool),
95    /// An `INTEGER8`.
96    Integer8(i8),
97    /// An `INTEGER16`.
98    Integer16(i16),
99    /// An `INTEGER32`.
100    Integer32(i32),
101    /// An `UNSIGNED8`.
102    Unsigned8(u8),
103    /// An `UNSIGNED16`.
104    Unsigned16(u16),
105    /// An `UNSIGNED32`.
106    Unsigned32(u32),
107    /// A `REAL32`.
108    Real32(f32),
109    /// A `REAL64`.
110    Real64(f64),
111    /// An `INTEGER64`.
112    Integer64(i64),
113    /// An `UNSIGNED64`.
114    Unsigned64(u64),
115}
116
117impl Value {
118    /// The data type of this value.
119    pub const fn data_type(&self) -> DataType {
120        match self {
121            Value::Boolean(_) => DataType::Boolean,
122            Value::Integer8(_) => DataType::Integer8,
123            Value::Integer16(_) => DataType::Integer16,
124            Value::Integer32(_) => DataType::Integer32,
125            Value::Unsigned8(_) => DataType::Unsigned8,
126            Value::Unsigned16(_) => DataType::Unsigned16,
127            Value::Unsigned32(_) => DataType::Unsigned32,
128            Value::Real32(_) => DataType::Real32,
129            Value::Real64(_) => DataType::Real64,
130            Value::Integer64(_) => DataType::Integer64,
131            Value::Unsigned64(_) => DataType::Unsigned64,
132        }
133    }
134
135    /// The encoded size of this value, in bytes.
136    pub const fn size(&self) -> usize {
137        self.data_type().size()
138    }
139
140    /// Encode this value little-endian into `buf`, returning the number of
141    /// bytes written.
142    ///
143    /// Returns [`Error::BadLength`] if `buf` is smaller than [`Value::size`].
144    pub fn encode_le(&self, buf: &mut [u8]) -> Result<usize> {
145        let n = self.size();
146        if buf.len() < n {
147            return Err(Error::BadLength);
148        }
149        match self {
150            Value::Boolean(v) => buf[0] = *v as u8,
151            Value::Integer8(v) => buf[0] = *v as u8,
152            Value::Unsigned8(v) => buf[0] = *v,
153            Value::Integer16(v) => buf[..2].copy_from_slice(&v.to_le_bytes()),
154            Value::Unsigned16(v) => buf[..2].copy_from_slice(&v.to_le_bytes()),
155            Value::Integer32(v) => buf[..4].copy_from_slice(&v.to_le_bytes()),
156            Value::Unsigned32(v) => buf[..4].copy_from_slice(&v.to_le_bytes()),
157            Value::Real32(v) => buf[..4].copy_from_slice(&v.to_le_bytes()),
158            Value::Integer64(v) => buf[..8].copy_from_slice(&v.to_le_bytes()),
159            Value::Unsigned64(v) => buf[..8].copy_from_slice(&v.to_le_bytes()),
160            Value::Real64(v) => buf[..8].copy_from_slice(&v.to_le_bytes()),
161        }
162        Ok(n)
163    }
164
165    /// Decode a little-endian value of `data_type` from `bytes`.
166    ///
167    /// `bytes` must be exactly [`DataType::size`] long, else
168    /// [`Error::BadLength`].
169    pub fn decode_le(data_type: DataType, bytes: &[u8]) -> Result<Value> {
170        if bytes.len() != data_type.size() {
171            return Err(Error::BadLength);
172        }
173        let v = match data_type {
174            DataType::Boolean => Value::Boolean(bytes[0] != 0),
175            DataType::Integer8 => Value::Integer8(bytes[0] as i8),
176            DataType::Unsigned8 => Value::Unsigned8(bytes[0]),
177            DataType::Integer16 => Value::Integer16(i16::from_le_bytes([bytes[0], bytes[1]])),
178            DataType::Unsigned16 => Value::Unsigned16(u16::from_le_bytes([bytes[0], bytes[1]])),
179            DataType::Integer32 => {
180                Value::Integer32(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
181            }
182            DataType::Unsigned32 => {
183                Value::Unsigned32(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
184            }
185            DataType::Real32 => {
186                Value::Real32(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
187            }
188            DataType::Integer64 => Value::Integer64(i64::from_le_bytes([
189                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
190            ])),
191            DataType::Unsigned64 => Value::Unsigned64(u64::from_le_bytes([
192                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
193            ])),
194            DataType::Real64 => Value::Real64(f64::from_le_bytes([
195                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
196            ])),
197        };
198        Ok(v)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn sizes_match_spec() {
208        assert_eq!(DataType::Boolean.size(), 1);
209        assert_eq!(DataType::Unsigned16.size(), 2);
210        assert_eq!(DataType::Unsigned32.size(), 4);
211        assert_eq!(DataType::Real64.size(), 8);
212    }
213
214    #[test]
215    fn data_type_index_roundtrips() {
216        for dt in [
217            DataType::Boolean,
218            DataType::Unsigned32,
219            DataType::Real64,
220            DataType::Integer64,
221            DataType::Unsigned64,
222        ] {
223            assert_eq!(DataType::from_index(dt.index()), Some(dt));
224        }
225        assert_eq!(DataType::Unsigned32.index(), 0x0007);
226        // Unmodelled types (e.g. VISIBLE_STRING 0x09) are rejected.
227        assert_eq!(DataType::from_index(0x09), None);
228    }
229
230    #[test]
231    fn u32_is_little_endian() {
232        let mut buf = [0u8; 8];
233        let n = Value::Unsigned32(0x1234_5678).encode_le(&mut buf).unwrap();
234        assert_eq!(n, 4);
235        assert_eq!(&buf[..4], &[0x78, 0x56, 0x34, 0x12]);
236    }
237
238    #[test]
239    fn decode_roundtrips() {
240        let cases = [
241            Value::Boolean(true),
242            Value::Integer8(-5),
243            Value::Unsigned16(0xBEEF),
244            Value::Integer32(-123456),
245            Value::Unsigned32(0xDEAD_BEEF),
246            Value::Unsigned64(0x0102_0304_0506_0708),
247        ];
248        let mut buf = [0u8; 8];
249        for v in cases {
250            let n = v.encode_le(&mut buf).unwrap();
251            let back = Value::decode_le(v.data_type(), &buf[..n]).unwrap();
252            assert_eq!(v, back);
253        }
254    }
255
256    #[test]
257    fn encode_rejects_short_buffer() {
258        let mut buf = [0u8; 2];
259        assert_eq!(
260            Value::Unsigned32(0).encode_le(&mut buf),
261            Err(Error::BadLength)
262        );
263    }
264
265    #[test]
266    fn decode_rejects_wrong_length() {
267        assert_eq!(
268            Value::decode_le(DataType::Unsigned32, &[0, 0]),
269            Err(Error::BadLength)
270        );
271    }
272}