zencan_client/
node_configuration.rs

1use std::{collections::HashMap, path::Path};
2
3use serde::{de, Deserialize, Deserializer};
4use snafu::{ResultExt, Snafu};
5use zencan_common::CanId;
6
7// Error returned when loading node configuration files
8#[derive(Debug, Snafu)]
9pub enum ConfigError {
10    #[snafu(display("IO error loading {path}: {source:?}"))]
11    Io {
12        path: String,
13        source: std::io::Error,
14    },
15    #[snafu(display("Error parsing TOML: {source}"))]
16    TomlDeserialization { source: toml::de::Error },
17}
18
19/// Represents a store command to write a value to an object
20#[derive(Clone, Debug, PartialEq)]
21pub struct Store {
22    /// Index of the object to be written
23    pub index: u16,
24    /// Sub index to be written
25    pub sub: u8,
26    /// The value to be written to the sub object
27    pub value: StoreValue,
28}
29
30impl Store {
31    /// Get the value as bytes
32    pub fn raw_value(&self) -> Vec<u8> {
33        self.value.raw()
34    }
35}
36
37/// Value to be stored by a [Store] command
38#[derive(Clone, Debug, Deserialize, PartialEq)]
39pub enum StoreValue {
40    U32(u32),
41    U16(u16),
42    U8(u8),
43    I32(i32),
44    I16(i16),
45    I8(i8),
46    F32(f32),
47    String(String),
48}
49
50impl StoreValue {
51    pub fn raw(&self) -> Vec<u8> {
52        match self {
53            StoreValue::U32(v) => v.to_le_bytes().to_vec(),
54            StoreValue::U16(v) => v.to_le_bytes().to_vec(),
55            StoreValue::U8(v) => vec![*v],
56            StoreValue::I32(v) => v.to_le_bytes().to_vec(),
57            StoreValue::I16(v) => v.to_le_bytes().to_vec(),
58            StoreValue::I8(v) => vec![*v as u8],
59            StoreValue::F32(v) => v.to_le_bytes().to_vec(),
60            StoreValue::String(ref s) => s.as_bytes().to_vec(),
61        }
62    }
63}
64
65/// A node configuration
66///
67/// Represents a runtime configuration which can be loaded into a node
68///
69/// It describes the configuration of PDOs, and other arbitrary objects on the node
70#[derive(Debug, Clone)]
71pub struct NodeConfig(NodeConfigSerializer);
72
73impl NodeConfig {
74    /// Read a configuration from a file
75    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<NodeConfig, ConfigError> {
76        let path = path.as_ref();
77        let content = std::fs::read_to_string(path).context(IoSnafu {
78            path: path.to_string_lossy(),
79        })?;
80        Self::load_from_str(&content)
81    }
82
83    /// Read a configuration from a string
84    pub fn load_from_str(s: &str) -> Result<NodeConfig, ConfigError> {
85        let raw_config: NodeConfigSerializer =
86            toml::from_str(s).context(TomlDeserializationSnafu)?;
87
88        Ok(NodeConfig(raw_config))
89    }
90
91    /// Get the transmit PDO configurations
92    pub fn tpdos(&self) -> &HashMap<usize, PdoConfig> {
93        &self.0.tpdo
94    }
95
96    /// Get the receive PDO configurations
97    pub fn rpdos(&self) -> &HashMap<usize, PdoConfig> {
98        &self.0.rpdo
99    }
100
101    /// Get the object configurations
102    ///
103    /// Each store represents a value to be written to a specific sub object during configuration
104    pub fn stores(&self) -> &[Store] {
105        &self.0.store
106    }
107}
108
109#[derive(Clone, Debug, Deserialize)]
110#[serde(deny_unknown_fields)]
111struct NodeConfigSerializer {
112    #[serde(deserialize_with = "deserialize_pdo_map", default)]
113    pub tpdo: HashMap<usize, PdoConfig>,
114    #[serde(deserialize_with = "deserialize_pdo_map", default)]
115    pub rpdo: HashMap<usize, PdoConfig>,
116    #[serde(default, deserialize_with = "deserialize_store")]
117    pub store: Vec<Store>,
118}
119
120/// Represents the configuration parameters for a single PDO
121#[derive(Clone, Debug, Deserialize)]
122#[serde(deny_unknown_fields)]
123struct PdoConfigSerializer {
124    /// The COB ID this PDO will use to send/receive
125    pub cob: u32,
126    #[serde(default)]
127    pub extended: bool,
128    /// The PDO is active
129    pub enabled: bool,
130    /// List of mapping specifying what sub objects are mapped to this PDO
131    pub mappings: Vec<PdoMapping>,
132    /// Specifies when a PDO is sent or latched
133    ///
134    /// - 0: Sent in response to sync, but only after an application specific event (e.g. it may be
135    ///   sent when the value changes, but not when it has not)
136    /// - 1 - 240: Sent in response to every Nth sync
137    /// - 254: Event driven (application to send it whenever it wants)
138    pub transmission_type: u8,
139}
140
141/// Represents the configuration parameters for a single PDO
142#[derive(Clone, Debug, Deserialize)]
143#[serde(try_from = "PdoConfigSerializer")]
144pub struct PdoConfig {
145    /// The COB ID this PDO will use to send/receive
146    pub cob: CanId,
147    /// Indicates if this PDO is enabled
148    pub enabled: bool,
149    /// List of mapping specifying what sub objects are mapped to this PDO
150    pub mappings: Vec<PdoMapping>,
151    /// Specifies when a PDO is sent or latched
152    ///
153    /// - 0: Sent in response to sync, but only after an application specific event (e.g. it may be
154    ///   sent when the value changes, but not when it has not)
155    /// - 1 - 240: Sent in response to every Nth sync
156    /// - 254: Event driven (application to send it whenever it wants)
157    pub transmission_type: u8,
158}
159
160#[derive(Clone, Debug, Snafu)]
161#[snafu(display("{message}"))]
162pub struct PdoConfigParseError {
163    message: String,
164}
165
166impl TryFrom<PdoConfigSerializer> for PdoConfig {
167    type Error = PdoConfigParseError;
168
169    fn try_from(value: PdoConfigSerializer) -> Result<Self, Self::Error> {
170        let cob = if value.extended {
171            CanId::extended(value.cob)
172        } else {
173            if value.cob > 0x7ff {
174                return Err(PdoConfigParseError {
175                    message: format!(
176                        "COB ID 0x{:x} is out of range for standard ID. Set `extended` to true.",
177                        value.cob
178                    ),
179                });
180            }
181            CanId::std(value.cob as u16)
182        };
183
184        Ok(PdoConfig {
185            cob,
186            enabled: value.enabled,
187            mappings: value.mappings,
188            transmission_type: value.transmission_type,
189        })
190    }
191}
192
193/// Represents a PDO mapping
194///
195/// Each mapping specifies one sub-object to be included in the PDO.
196#[derive(Clone, Copy, Debug, Deserialize)]
197#[serde(deny_unknown_fields)]
198pub struct PdoMapping {
199    /// The object index
200    pub index: u16,
201    /// The object sub index
202    pub sub: u8,
203    /// The size of the object to map, in **bits**
204    pub size: u8,
205}
206
207impl PdoMapping {
208    /// Convert a PdoMapping object to the u32 representation stored in the PdoMapping object
209    pub fn to_object_value(&self) -> u32 {
210        ((self.index as u32) << 16) | ((self.sub as u32) << 8) | (self.size as u32)
211    }
212
213    /// Create a PdoMapping object from the raw u32 representation stored in the PdoMapping object
214    pub fn from_object_value(value: u32) -> Self {
215        let index = (value >> 16) as u16;
216        let sub = ((value >> 8) & 0xff) as u8;
217        let size = (value & 0xff) as u8;
218        Self { index, sub, size }
219    }
220}
221
222#[derive(Debug, Clone, Copy, Deserialize)]
223#[serde(rename_all = "lowercase")]
224enum StoreType {
225    U32,
226    U16,
227    U8,
228    I32,
229    I16,
230    I8,
231    F32,
232    String,
233}
234
235#[derive(Debug, Deserialize)]
236#[serde(deny_unknown_fields)]
237struct StoreSerializer {
238    pub index: u16,
239    pub sub: u8,
240    pub value: toml::Value,
241    #[serde(rename = "type")]
242    pub ty: StoreType,
243}
244
245fn deserialize_store<'de, D>(deserializer: D) -> Result<Vec<Store>, D::Error>
246where
247    D: Deserializer<'de>,
248{
249    let raw_store = Vec::<StoreSerializer>::deserialize(deserializer)?;
250
251    let store = raw_store
252        .into_iter()
253        .map(|raw| {
254            let value = match raw.ty {
255                StoreType::U32 => {
256                    let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
257                        de::Unexpected::Str(&raw.value.to_string()),
258                        &"an integer",
259                    ))?;
260                    Ok(StoreValue::U32(value.try_into().map_err(|_| {
261                        de::Error::invalid_value(
262                            de::Unexpected::Signed(value),
263                            &"an integer in range [0..2^32]",
264                        )
265                    })?))
266                }
267                StoreType::U16 => {
268                    let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
269                        de::Unexpected::Str(&raw.value.to_string()),
270                        &"an integer",
271                    ))?;
272                    Ok(StoreValue::U16(value.try_into().map_err(|_| {
273                        de::Error::invalid_value(
274                            de::Unexpected::Signed(value),
275                            &"an integer in range [0..65536]",
276                        )
277                    })?))
278                }
279                StoreType::U8 => {
280                    let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
281                        de::Unexpected::Str(&raw.value.to_string()),
282                        &"an integer",
283                    ))?;
284                    Ok(StoreValue::U8(value.try_into().map_err(|_| {
285                        de::Error::invalid_value(
286                            de::Unexpected::Signed(value),
287                            &"an integer in range [0..256]",
288                        )
289                    })?))
290                }
291                StoreType::I32 => {
292                    let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
293                        de::Unexpected::Str(&raw.value.to_string()),
294                        &"an integer",
295                    ))?;
296                    Ok(StoreValue::I32(value.try_into().map_err(|_| {
297                        de::Error::invalid_value(
298                            de::Unexpected::Signed(value),
299                            &"an integer in range [-2^31..2^31]",
300                        )
301                    })?))
302                }
303                StoreType::I16 => {
304                    let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
305                        de::Unexpected::Str(&raw.value.to_string()),
306                        &"an integer",
307                    ))?;
308                    Ok(StoreValue::I16(value.try_into().map_err(|_| {
309                        de::Error::invalid_value(
310                            de::Unexpected::Signed(value),
311                            &"an integer in range [-32767..32768]",
312                        )
313                    })?))
314                }
315                StoreType::I8 => {
316                    let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
317                        de::Unexpected::Str(&raw.value.to_string()),
318                        &"an integer",
319                    ))?;
320                    Ok(StoreValue::I8(value.try_into().map_err(|_| {
321                        de::Error::invalid_value(
322                            de::Unexpected::Signed(value),
323                            &"an integer in range [-127..128]",
324                        )
325                    })?))
326                }
327                StoreType::F32 => {
328                    let value = raw.value.as_float().ok_or(de::Error::invalid_type(
329                        de::Unexpected::Str(&raw.value.to_string()),
330                        &"a float",
331                    ))?;
332                    Ok(StoreValue::F32(value as f32))
333                }
334                StoreType::String => {
335                    let value = raw.value.as_str().ok_or(de::Error::invalid_type(
336                        de::Unexpected::Str(&raw.value.to_string()),
337                        &"a string",
338                    ))?;
339                    Ok(StoreValue::String(value.to_string()))
340                }
341            }?;
342            Ok(Store {
343                index: raw.index,
344                sub: raw.sub,
345                value,
346            })
347        })
348        .collect::<Result<Vec<_>, _>>()?;
349
350    Ok(store)
351}
352
353fn deserialize_pdo_map<'de, D>(deserializer: D) -> Result<HashMap<usize, PdoConfig>, D::Error>
354where
355    D: Deserializer<'de>,
356{
357    let str_map = HashMap::<String, PdoConfig>::deserialize(deserializer)?;
358    let original_len = str_map.len();
359    let data = {
360        str_map
361            .into_iter()
362            .map(|(str_key, value)| match str_key.parse() {
363                Ok(int_key) => Ok((int_key, value)),
364                Err(_) => Err({
365                    de::Error::invalid_value(
366                        de::Unexpected::Str(&str_key),
367                        &"a non-negative integer",
368                    )
369                }),
370            })
371            .collect::<Result<HashMap<_, _>, _>>()?
372    };
373    // multiple strings could parse to the same int, e.g "0" and "00"
374    if data.len() < original_len {
375        return Err(de::Error::custom("detected duplicate integer key"));
376    }
377    Ok(data)
378}
379
380#[cfg(test)]
381mod test {
382    use super::*;
383    use assertables::assert_contains;
384
385    #[test]
386    fn test_out_of_range_standard_id() {
387        let str = r#"
388        [tpdo.0]
389        enabled = true
390        cob = 0x800
391        transmission_type = 254
392        mappings = [
393            { index=0x1000, sub=1, size=8 },
394        ]
395        "#;
396
397        let result = NodeConfig::load_from_str(str);
398        assert!(result.is_err());
399        let err = result.unwrap_err();
400        assert_contains!(
401            &err.to_string(),
402            "COB ID 0x800 is out of range for standard ID"
403        );
404    }
405
406    #[test]
407    fn test_extended_cob() {
408        let str = r#"
409        [tpdo.0]
410        enabled = true
411        cob = 0x800
412        extended = true
413        transmission_type = 254
414        mappings = [
415            { index=0x1000, sub=1, size=8 },
416        ]
417        "#;
418
419        let result = NodeConfig::load_from_str(str).unwrap();
420        assert_eq!(1, result.tpdos().len());
421        let tpdo = result.tpdos().get(&0).unwrap();
422        assert_eq!(CanId::extended(0x800), tpdo.cob);
423    }
424
425    #[test]
426    fn test_node_config_parse() {
427        let str = r#"
428        [tpdo.0]
429        enabled = true
430        cob = 0x181
431        transmission_type = 254
432        mappings = [
433            { index=0x1000, sub=1, size=8 },
434            { index=0x1000, sub=2, size=16 },
435        ]
436
437        [[store]]
438        type = "u32"
439        value = 12
440        index = 0x1000
441        sub = 0
442        "#;
443
444        let config = match NodeConfig::load_from_str(str) {
445            Ok(config) => config,
446            Err(e) => {
447                println!("{}", e);
448                panic!("Failed to parse config");
449            }
450        };
451
452        println!("{config:?}");
453        assert_eq!(1, config.tpdos().len());
454        assert_eq!(1, config.stores().len());
455    }
456
457    #[test]
458    fn test_out_of_range_integer() {
459        let str = r#"
460        [[store]]
461        type = "u8"
462        value = 256
463        index = 0x1000
464        sub = 0
465        "#;
466
467        let result = NodeConfig::load_from_str(str);
468        assert!(result.is_err());
469        assert!(result
470            .unwrap_err()
471            .to_string()
472            .contains("expected an integer in range [0..256]"));
473    }
474}