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