zencan_common/
node_configuration.rs

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