Skip to main content

zencan_common/
node_configuration.rs

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