zencan_common/
device_config.rs

1//! Device config file
2//!
3//! A DeviceConfig is created from a TOML file, and provides build-time configuration for a zencan
4//! node. The device config specifies all of the objects in the object dictionary of the node,
5//! including custom ones defined for the specific application.
6//!
7//! # An example TOML file
8//!
9//! ```toml
10//! device_name = "can-io"
11//! software_version = "v0.0.1"
12//! hardware_version = "rev1"
13//! heartbeat_period = 1000
14//!
15//! # Define 3 out of 4 device unique identifiers. These define the application/device, the fourth is
16//! # the serial number, which must be provided at run-time by the application.
17//! [identity]
18//! vendor_id = 0xCAFE
19//! product_code = 1032
20//! revision_number = 1
21//!
22//! # Defines the number of PDOs the device will support
23//! [pdos]
24//! num_rpdo = 4
25//! num_tpdo = 4
26//!
27//! # User's can create custom objects to hold application specific data
28//! [[objects]]
29//! index = 0x2000
30//! parameter_name = "Raw Analog Input"
31//! object_type = "array"
32//! data_type = "uint16"
33//! access_type = "ro"
34//! array_size = 4
35//! default_value = [0, 0, 0, 0]
36//! pdo_mapping = "tpdo"
37//! ```
38//!
39//! # Object Namespaces
40//!
41//! Application specific objects should be defined in the range 0x2000-0x4fff. Many objects will be
42//! created by default in addition to the ones defined by the user.
43//!
44//! # Standard Objects
45//!
46//! ## 0x1008 - Device Name
47//!
48//! A VAR object containing a string with a human readable device name. This value is set by
49//! [DeviceConfig::device_name].
50//!
51//! ## 0x1009 - Hardware Version
52//!
53//! A VAR object containing a string with a human readable hardware version. This value is set by
54//! [DeviceConfig::hardware_version].
55//!
56//! ## 0x100A - Software Version
57//!
58//! A VAR object containing a string with a human readable software version. This value is set by
59//! [DeviceConfig::software_version]
60//!
61//! ## 0x1010 - Object Save Command
62//!
63//! An array object used to command the node to store its current object values.
64//!
65//! Array size: 1 Data type: u32
66//!
67//! When read, sub-object 1 will return a 1 if a storage callback has been provided by the
68//! application, indicating that saving is supported.
69//!
70//! To trigger a save, write a u32 with the [magic value](crate::constants::values::SAVE_CMD).
71//!
72//! ## 0x1017 - Heartbeat Producer Time
73//!
74//! A VAR object of type U16.
75//!
76//! This object stores the period at which the heartbeat is sent by the device, in milliseconds. It
77//! is set by [DeviceConfig::heartbeat_period].
78//!
79//! ## 0x1018 - Identity
80//!
81//! A record object which stores the 128-bit unique identifier for the node.
82//!
83//! | Sub Object | Type | Description |
84//! | ---------- | ---- | ----------- |
85//! | 0          | u8   | Max sub index - always 4 |
86//! | 1          | u32  | Vendor ID    |
87//! | 2          | u32  | Product Code |
88//! | 3          | u32  | Revision |
89//! | 4          | u32  | Serial |
90//!
91//! ## 0x1400 to 0x1400 + N - RPDO Communications Parameter
92//!
93//! One object for each RPDO supported by the node. This configures how the PDO is received.
94//!
95//! ## 0x1600 to 0x1600 + N - RPDO Mapping Parameters
96//!
97//! One object for each RPDO supported by the node. This configures which sub objects the data in
98//! the PDO message maps to.
99//!
100//! Sub Object 0 contains the number of valid mappings. Sub objects 1 through 9 specify a list of
101//! sub objects to map to.
102//!
103//! ## 0x1800 to 0x1800 + N - TPDO Communications Parameter
104//!
105//! One object for each TPDO supported by the node. This configures how the PDO is transmitted.
106//!
107//! ## 0x1A00 to 0x1A00 + N - TPDO Mapping Parameters
108//!
109//! One object for each TPDO supported by the node. This configures which sub objects the data in
110//! the PDO message maps to.
111//!
112//! Sub Object 0 contains the number of valid mappings. Sub objects 1 through 9 specify a list of
113//! sub objects to map to.
114//!
115//! # Zencan Extensions
116//!
117//! ## 0x5000 - Auto Start
118//!
119//! Setting this to a non-zero value causes the node to immediately move into the Operational state
120//! after power-on, without receiving an NMT command to do so. Note that, if the device is later put
121//! into PreOperational via an NMT command, it will not auto-transition to Operational.
122//!
123use std::collections::HashMap;
124
125use crate::objects::{AccessType, ObjectCode};
126use serde::{de::Error, Deserialize};
127
128use snafu::ResultExt as _;
129use snafu::Snafu;
130
131/// Error returned when loading a device config fails
132#[derive(Debug, Snafu)]
133pub enum LoadError {
134    /// An IO error occured while reading the file
135    #[snafu(display("IO error: {source}"))]
136    Io {
137        /// The underlying IO error
138        source: std::io::Error,
139    },
140    /// An error occured in the TOML parser
141    #[snafu(display("Toml parse error: {source}"))]
142    TomlParsing {
143        /// The toml error which led to this error
144        source: toml::de::Error,
145    },
146    /// Multiple objects defined with same index
147    #[snafu(display("Multiple definitions for object with index 0x{id:x}"))]
148    DuplicateObjectIds {
149        /// index which was defined multiple times
150        id: u16,
151    },
152    /// Duplicate sub objects defined on a record
153    #[snafu(display("Multiple definitions of sub index {sub} on object 0x{index:x}"))]
154    DuplicateSubObjects {
155        /// Index of the record object containing duplicate subs
156        index: u16,
157        /// Duplicated sub index
158        sub: u8,
159    },
160}
161
162fn mandatory_objects(config: &DeviceConfig) -> Vec<ObjectDefinition> {
163    vec![
164        ObjectDefinition {
165            index: 0x1000,
166            parameter_name: "Device Type".to_string(),
167            application_callback: false,
168            object: Object::Var(VarDefinition {
169                data_type: DataType::UInt32,
170                access_type: AccessType::Const.into(),
171                default_value: Some(DefaultValue::Integer(0x00000000)),
172                pdo_mapping: PdoMapping::None,
173                ..Default::default()
174            }),
175        },
176        ObjectDefinition {
177            index: 0x1001,
178            parameter_name: "Error Register".to_string(),
179            application_callback: false,
180            object: Object::Var(VarDefinition {
181                data_type: DataType::UInt8,
182                access_type: AccessType::Ro.into(),
183                default_value: Some(DefaultValue::Integer(0x00000000)),
184                pdo_mapping: PdoMapping::None,
185                ..Default::default()
186            }),
187        },
188        ObjectDefinition {
189            index: 0x1008,
190            parameter_name: "Manufacturer Device Name".to_string(),
191            application_callback: false,
192            object: Object::Var(VarDefinition {
193                data_type: DataType::VisibleString(config.device_name.len()),
194                access_type: AccessType::Const.into(),
195                default_value: Some(DefaultValue::String(config.device_name.clone())),
196                pdo_mapping: PdoMapping::None,
197                ..Default::default()
198            }),
199        },
200        ObjectDefinition {
201            index: 0x1009,
202            parameter_name: "Manufacturer Hardware Version".to_string(),
203            application_callback: false,
204            object: Object::Var(VarDefinition {
205                data_type: DataType::VisibleString(config.hardware_version.len()),
206                access_type: AccessType::Const.into(),
207                default_value: Some(DefaultValue::String(config.hardware_version.clone())),
208                pdo_mapping: PdoMapping::None,
209                ..Default::default()
210            }),
211        },
212        ObjectDefinition {
213            index: 0x100A,
214            parameter_name: "Manufacturer Software Version".to_string(),
215            application_callback: false,
216            object: Object::Var(VarDefinition {
217                data_type: DataType::VisibleString(config.software_version.len()),
218                access_type: AccessType::Const.into(),
219                default_value: Some(DefaultValue::String(config.software_version.clone())),
220                pdo_mapping: PdoMapping::None,
221                ..Default::default()
222            }),
223        },
224        ObjectDefinition {
225            index: 0x1017,
226            parameter_name: "Heartbeat Producer Time (ms)".to_string(),
227            application_callback: false,
228            object: Object::Var(VarDefinition {
229                data_type: DataType::UInt16,
230                access_type: AccessType::Const.into(),
231                default_value: Some(DefaultValue::Integer(config.heartbeat_period as i64)),
232                pdo_mapping: PdoMapping::None,
233                persist: false,
234            }),
235        },
236        ObjectDefinition {
237            index: 0x1018,
238            parameter_name: "Identity".to_string(),
239            application_callback: false,
240            object: Object::Record(RecordDefinition {
241                subs: vec![
242                    SubDefinition {
243                        sub_index: 1,
244                        parameter_name: "Vendor ID".to_string(),
245                        field_name: Some("vendor_id".into()),
246                        data_type: DataType::UInt32,
247                        access_type: AccessType::Const.into(),
248                        default_value: Some(DefaultValue::Integer(
249                            config.identity.vendor_id as i64,
250                        )),
251                        pdo_mapping: PdoMapping::None,
252                        ..Default::default()
253                    },
254                    SubDefinition {
255                        sub_index: 2,
256                        parameter_name: "Product Code".to_string(),
257                        field_name: Some("product_code".into()),
258                        data_type: DataType::UInt32,
259                        access_type: AccessType::Const.into(),
260                        default_value: Some(DefaultValue::Integer(
261                            config.identity.product_code as i64,
262                        )),
263                        pdo_mapping: PdoMapping::None,
264                        ..Default::default()
265                    },
266                    SubDefinition {
267                        sub_index: 3,
268                        parameter_name: "Revision Number".to_string(),
269                        field_name: Some("revision".into()),
270                        data_type: DataType::UInt32,
271                        access_type: AccessType::Const.into(),
272                        default_value: Some(DefaultValue::Integer(
273                            config.identity.revision_number as i64,
274                        )),
275                        pdo_mapping: PdoMapping::None,
276                        ..Default::default()
277                    },
278                    SubDefinition {
279                        sub_index: 4,
280                        parameter_name: "Serial Number".to_string(),
281                        field_name: Some("serial".into()),
282                        data_type: DataType::UInt32,
283                        access_type: AccessType::Const.into(),
284                        default_value: Some(DefaultValue::Integer(0)),
285                        pdo_mapping: PdoMapping::None,
286                        ..Default::default()
287                    },
288                ],
289            }),
290        },
291        ObjectDefinition {
292            index: 0x5000,
293            parameter_name: "Auto Start".to_string(),
294            application_callback: false,
295            object: Object::Var(VarDefinition {
296                data_type: DataType::UInt8,
297                access_type: AccessType::Rw.into(),
298                default_value: None,
299                pdo_mapping: PdoMapping::None,
300                persist: true,
301            }),
302        },
303    ]
304}
305
306fn pdo_objects(num_rpdo: usize, num_tpdo: usize) -> Vec<ObjectDefinition> {
307    let mut objects = Vec::new();
308
309    fn add_objects(objects: &mut Vec<ObjectDefinition>, i: usize, tx: bool) {
310        let pdo_type = if tx { "TPDO" } else { "RPDO" };
311        let comm_index = if tx { 0x1800 } else { 0x1400 };
312        let mapping_index = if tx { 0x1A00 } else { 0x1600 };
313
314        objects.push(ObjectDefinition {
315            index: comm_index + i as u16,
316            parameter_name: format!("{}{} Communication Parameter", pdo_type, i),
317            application_callback: true,
318            object: Object::Record(RecordDefinition {
319                subs: vec![
320                    SubDefinition {
321                        sub_index: 1,
322                        parameter_name: format!("COB-ID for {}{}", pdo_type, i),
323                        field_name: None,
324                        data_type: DataType::UInt32,
325                        access_type: AccessType::Rw.into(),
326                        default_value: None,
327                        pdo_mapping: PdoMapping::None,
328                        persist: true,
329                    },
330                    SubDefinition {
331                        sub_index: 2,
332                        parameter_name: format!("Transmission type for {}{}", pdo_type, i),
333                        field_name: None,
334                        data_type: DataType::UInt8,
335                        access_type: AccessType::Rw.into(),
336                        default_value: None,
337                        pdo_mapping: PdoMapping::None,
338                        persist: true,
339                    },
340                ],
341            }),
342        });
343
344        let mut mapping_subs = vec![SubDefinition {
345            sub_index: 0,
346            parameter_name: "Valid Mappings".to_string(),
347            field_name: None,
348            data_type: DataType::UInt8,
349            access_type: AccessType::Rw.into(),
350            default_value: Some(DefaultValue::Integer(0)),
351            pdo_mapping: PdoMapping::None,
352            persist: true,
353        }];
354        for sub in 1..65 {
355            mapping_subs.push(SubDefinition {
356                sub_index: sub,
357                parameter_name: format!("{}{} Mapping App Object {}", pdo_type, i, sub),
358                field_name: None,
359                data_type: DataType::UInt32,
360                access_type: AccessType::Rw.into(),
361                default_value: None,
362                pdo_mapping: PdoMapping::None,
363                persist: true,
364            });
365        }
366
367        objects.push(ObjectDefinition {
368            index: mapping_index + i as u16,
369            parameter_name: format!("{}{} Mapping Parameters", pdo_type, i),
370            application_callback: true,
371            object: Object::Record(RecordDefinition { subs: mapping_subs }),
372        });
373    }
374    for i in 0..num_rpdo {
375        add_objects(&mut objects, i, false);
376    }
377    for i in 0..num_tpdo {
378        add_objects(&mut objects, i, true);
379    }
380    objects
381}
382
383fn bootloader_objects(cfg: &BootloaderConfig) -> Vec<ObjectDefinition> {
384    let mut objects = Vec::new();
385
386    if cfg.sections.is_empty() {
387        return objects;
388    }
389    objects.push(ObjectDefinition {
390        index: 0x5500,
391        parameter_name: "Bootloader Info".into(),
392        application_callback: false,
393        object: Object::Record(RecordDefinition {
394            subs: vec![
395                SubDefinition {
396                    sub_index: 1,
397                    parameter_name: "Bootloader Config".into(),
398                    field_name: Some("config".into()),
399                    data_type: DataType::UInt32,
400                    access_type: AccessType::Ro.into(),
401                    default_value: Some(0.into()),
402                    pdo_mapping: PdoMapping::None,
403                    persist: false,
404                },
405                SubDefinition {
406                    sub_index: 2,
407                    parameter_name: "Number of Section".into(),
408                    field_name: Some("num_sections".into()),
409                    data_type: DataType::UInt8,
410                    access_type: AccessType::Ro.into(),
411                    default_value: Some(cfg.sections.len().into()),
412                    pdo_mapping: PdoMapping::None,
413                    persist: false,
414                },
415                SubDefinition {
416                    sub_index: 3,
417                    parameter_name: "Reset to Bootloader Command".into(),
418                    field_name: None,
419                    data_type: DataType::UInt32,
420                    access_type: AccessType::Wo.into(),
421                    default_value: None,
422                    pdo_mapping: PdoMapping::None,
423                    persist: false,
424                },
425            ],
426        }),
427    });
428
429    for (i, section) in cfg.sections.iter().enumerate() {
430        objects.push(ObjectDefinition {
431            index: 0x5510 + i as u16,
432            parameter_name: format!("Bootloader Section {i}"),
433            application_callback: true,
434            object: Object::Record(RecordDefinition {
435                subs: vec![
436                    SubDefinition {
437                        sub_index: 1,
438                        parameter_name: "Mode bits".into(),
439                        data_type: DataType::UInt8,
440                        access_type: AccessType::Const.into(),
441                        ..Default::default()
442                    },
443                    SubDefinition {
444                        sub_index: 2,
445                        parameter_name: "Section Name".into(),
446                        data_type: DataType::VisibleString(0),
447                        access_type: AccessType::Const.into(),
448                        default_value: Some(section.name.as_str().into()),
449                        ..Default::default()
450                    },
451                    SubDefinition {
452                        sub_index: 3,
453                        parameter_name: "Section Size".into(),
454                        data_type: DataType::UInt32,
455                        access_type: AccessType::Const.into(),
456                        default_value: Some((section.size as i64).into()),
457                        ..Default::default()
458                    },
459                    SubDefinition {
460                        sub_index: 4,
461                        parameter_name: "Erase Command".into(),
462                        data_type: DataType::UInt8,
463                        access_type: AccessType::Wo.into(),
464                        ..Default::default()
465                    },
466                    SubDefinition {
467                        sub_index: 5,
468                        parameter_name: "Data".into(),
469                        data_type: DataType::Domain,
470                        access_type: AccessType::Rw.into(),
471                        ..Default::default()
472                    },
473                ],
474            }),
475        });
476    }
477
478    objects
479}
480
481fn object_storage_objects(dev: &DeviceConfig) -> Vec<ObjectDefinition> {
482    if dev.support_storage {
483        vec![ObjectDefinition {
484            index: 0x1010,
485            parameter_name: "Object Save Command".to_string(),
486            application_callback: false,
487            object: Object::Array(ArrayDefinition {
488                data_type: DataType::UInt32,
489                access_type: AccessType::Rw.into(),
490                array_size: 1,
491                persist: false,
492                ..Default::default()
493            }),
494        }]
495    } else {
496        vec![]
497    }
498}
499
500fn default_num_rpdo() -> u8 {
501    4
502}
503fn default_num_tpdo() -> u8 {
504    4
505}
506fn default_true() -> bool {
507    true
508}
509
510/// Configuration options for PDOs
511#[derive(Deserialize, Debug, Clone, Copy)]
512pub struct PdoConfig {
513    #[serde(default = "default_num_rpdo")]
514    /// The number of TX PDO slots available in the device. Defaults to 4.
515    pub num_tpdo: u8,
516    #[serde(default = "default_num_tpdo")]
517    /// The number of RX PDO slots available in the device. Defaults to 4.
518    pub num_rpdo: u8,
519}
520
521impl Default for PdoConfig {
522    fn default() -> Self {
523        Self {
524            num_tpdo: default_num_tpdo(),
525            num_rpdo: default_num_rpdo(),
526        }
527    }
528}
529
530/// The device identity is a unique 128-bit number used for addressing the device on the bus
531///
532/// The configures the three hardcoded components of the identity. The serial number component of
533/// the identity must be set by the application to be unique, e.g. based on a value programmed into
534/// non-volatile memory or from a UID register on the MCU.
535#[derive(Deserialize, Debug, Default, Clone, Copy)]
536#[serde(deny_unknown_fields)]
537pub struct IdentityConfig {
538    /// The 32-bit vendor ID for this device
539    pub vendor_id: u32,
540    /// The 32-bit product code for this device
541    pub product_code: u32,
542    /// The 32-bit revision number for this device
543    pub revision_number: u32,
544}
545
546/// Enum indicating what PDO mappings a sub object supports
547#[derive(Deserialize, Debug, Default, Clone, Copy)]
548#[serde(rename_all = "lowercase")]
549pub enum PdoMapping {
550    /// Cannot be mapped to any PDOs
551    #[default]
552    None,
553    /// Can be mapped only to TPDOs
554    Tpdo,
555    /// Can be mapped only to RPDOs
556    Rpdo,
557    /// Can be mapped to any PDO
558    Both,
559}
560
561impl PdoMapping {
562    /// Can be mapped to a TPDO
563    pub fn supports_tpdo(&self) -> bool {
564        matches!(self, PdoMapping::Tpdo | PdoMapping::Both)
565    }
566
567    /// Can be mapped to an RPDO
568    pub fn supports_rpdo(&self) -> bool {
569        matches!(self, PdoMapping::Rpdo | PdoMapping::Both)
570    }
571}
572
573/// Configuration object to define a programmable bootloader section
574#[derive(Clone, Debug, Deserialize)]
575pub struct BootloaderSection {
576    /// Name of the section
577    pub name: String,
578    /// Size of the section
579    pub size: u32,
580}
581
582/// Configuration of bootloader parameters
583#[derive(Clone, Deserialize, Debug, Default)]
584pub struct BootloaderConfig {
585    /// If true, this node is an application which supports resetting to a bootloader, rather than a
586    /// bootloader implementation
587    #[serde(default)]
588    pub application: bool,
589    /// List of programmable sections
590    #[serde(default)]
591    pub sections: Vec<BootloaderSection>,
592}
593
594#[derive(Deserialize, Debug, Clone)]
595#[serde(deny_unknown_fields)]
596/// Private struct for seserializing device config files
597pub struct DeviceConfig {
598    /// The name describing the type of device (e.g. a model)
599    pub device_name: String,
600
601    /// Enables object storage commands (object 0x1010)
602    ///
603    /// Default: true
604    #[serde(default = "default_true")]
605    pub support_storage: bool,
606
607    /// A version describing the hardware
608    #[serde(default)]
609    pub hardware_version: String,
610    /// A version describing the software
611    #[serde(default)]
612    pub software_version: String,
613
614    /// The period at which to transmit heartbeat messages in milliseconds
615    #[serde(default)]
616    pub heartbeat_period: u16,
617
618    /// Configures the identity object on the device
619    pub identity: IdentityConfig,
620
621    /// Configure PDO settings
622    #[serde(default)]
623    pub pdos: PdoConfig,
624
625    /// Configure bootloader options
626    #[serde(default)]
627    pub bootloader: BootloaderConfig,
628
629    /// A list of application specific objects to define on the device
630    #[serde(default)]
631    pub objects: Vec<ObjectDefinition>,
632}
633
634/// Defines a sub-object in a record
635#[derive(Deserialize, Debug, Default, Clone)]
636#[serde(deny_unknown_fields)]
637pub struct SubDefinition {
638    /// Sub index for the sub-object being defined
639    pub sub_index: u8,
640    /// A human readable name for the value stored in this sub-object
641    #[serde(default)]
642    pub parameter_name: String,
643    /// Used to name the struct field associated with this sub object
644    ///
645    /// This is only applicable to record objects. If no name is provided, the default field name
646    /// will be `sub[index]`, where index is the uppercase hex representation of the sub index
647    #[serde(default)]
648    pub field_name: Option<String>,
649    /// The data type of the sub object
650    pub data_type: DataType,
651    /// Access permissions for the sub object
652    #[serde(default)]
653    pub access_type: AccessTypeDeser,
654    /// The default value for the sub object
655    #[serde(default)]
656    pub default_value: Option<DefaultValue>,
657    /// Indicates whether this sub object can be mapped to PDOs
658    #[serde(default)]
659    pub pdo_mapping: PdoMapping,
660    /// Indicates if this sub object should be saved when the save command is sent
661    #[serde(default)]
662    pub persist: bool,
663}
664
665/// An enum to represent object default values
666#[derive(Deserialize, Debug, Clone)]
667#[serde(untagged)]
668pub enum DefaultValue {
669    /// A default value for integer fields
670    Integer(i64),
671    /// A default value for float fields
672    Float(f64),
673    /// A default value for string fields
674    String(String),
675}
676
677impl From<i64> for DefaultValue {
678    fn from(value: i64) -> Self {
679        Self::Integer(value)
680    }
681}
682
683impl From<i32> for DefaultValue {
684    fn from(value: i32) -> Self {
685        Self::Integer(value as i64)
686    }
687}
688
689impl From<usize> for DefaultValue {
690    fn from(value: usize) -> Self {
691        Self::Integer(value as i64)
692    }
693}
694
695impl From<f64> for DefaultValue {
696    fn from(value: f64) -> Self {
697        Self::Float(value)
698    }
699}
700
701impl From<&str> for DefaultValue {
702    fn from(value: &str) -> Self {
703        Self::String(value.to_string())
704    }
705}
706
707/// An enum representing the different types of objects which can be defined in a device config
708#[derive(Deserialize, Debug, Clone)]
709#[serde(tag = "object_type", rename_all = "lowercase")]
710pub enum Object {
711    /// A var object is just a single value
712    Var(VarDefinition),
713    /// An array object is an array of values, all with the same type
714    Array(ArrayDefinition),
715    /// A record is a collection of sub objects all with different types
716    Record(RecordDefinition),
717}
718
719/// Descriptor for a var object
720#[derive(Default, Deserialize, Debug, Clone)]
721#[serde(deny_unknown_fields)]
722pub struct VarDefinition {
723    /// Indicates the type of data stored in the object
724    pub data_type: DataType,
725    /// Indicates how this object can be accessed
726    pub access_type: AccessTypeDeser,
727    /// The default value for this object
728    pub default_value: Option<DefaultValue>,
729    /// Determines which if type of PDO this object can me mapped to
730    #[serde(default)]
731    pub pdo_mapping: PdoMapping,
732    /// Indicates that this object should be saved
733    #[serde(default)]
734    pub persist: bool,
735}
736
737/// Descriptor for an array object
738#[derive(Default, Deserialize, Debug, Clone)]
739#[serde(deny_unknown_fields)]
740pub struct ArrayDefinition {
741    /// The datatype of array fields
742    pub data_type: DataType,
743    /// Access type for all array fields
744    pub access_type: AccessTypeDeser,
745    /// The number of elements in the array
746    pub array_size: usize,
747    /// Default values for all array fields
748    pub default_value: Option<Vec<DefaultValue>>,
749    #[serde(default)]
750    /// Whether fields in this array can be mapped to PDOs
751    pub pdo_mapping: PdoMapping,
752    #[serde(default)]
753    /// Whether this array should be saved to flash on command
754    pub persist: bool,
755}
756
757/// Descriptor for a record object
758#[derive(Deserialize, Debug, Clone)]
759#[serde(deny_unknown_fields)]
760pub struct RecordDefinition {
761    /// The sub object definitions for this record object
762    #[serde(default)]
763    pub subs: Vec<SubDefinition>,
764}
765
766/// Descriptor for a domain object
767///
768/// Not yet implemented
769#[derive(Clone, Copy, Deserialize, Debug)]
770pub struct DomainDefinition {}
771
772/// Descriptor for an object in the object dictionary
773#[derive(Deserialize, Debug, Clone)]
774pub struct ObjectDefinition {
775    /// The index of the object
776    pub index: u16,
777    /// A human readable name to describe the contents of the object
778    #[serde(default)]
779    pub parameter_name: String,
780    #[serde(default)]
781    /// If true, this object is implemented by an application callback, and no storage will be
782    /// allocated for it in the object dictionary.
783    pub application_callback: bool,
784    /// The descriptor for the object
785    #[serde(flatten)]
786    pub object: Object,
787}
788
789impl ObjectDefinition {
790    /// Get the object code specifying the type of this object
791    pub fn object_code(&self) -> ObjectCode {
792        match self.object {
793            Object::Var(_) => ObjectCode::Var,
794            Object::Array(_) => ObjectCode::Array,
795            Object::Record(_) => ObjectCode::Record,
796        }
797    }
798}
799
800impl DeviceConfig {
801    /// Try to read a device config from a file
802    pub fn load(config_path: impl AsRef<std::path::Path>) -> Result<Self, LoadError> {
803        let config_str = std::fs::read_to_string(&config_path).context(IoSnafu)?;
804        Self::load_from_str(&config_str)
805    }
806
807    /// Try to read a config from a &str
808    pub fn load_from_str(config_str: &str) -> Result<Self, LoadError> {
809        let mut config: DeviceConfig = toml::from_str(config_str).context(TomlParsingSnafu)?;
810
811        // Add mandatory objects to the config
812        config.objects.extend(mandatory_objects(&config));
813        config
814            .objects
815            .extend(bootloader_objects(&config.bootloader));
816        config.objects.extend(pdo_objects(
817            config.pdos.num_rpdo as usize,
818            config.pdos.num_tpdo as usize,
819        ));
820        config.objects.extend(object_storage_objects(&config));
821
822        Self::validate_unique_indices(&config.objects)?;
823
824        Ok(config)
825    }
826
827    fn validate_unique_indices(objects: &[ObjectDefinition]) -> Result<(), LoadError> {
828        let mut found_indices = HashMap::new();
829        for obj in objects {
830            if found_indices.contains_key(&obj.index) {
831                return DuplicateObjectIdsSnafu { id: obj.index }.fail();
832            }
833            found_indices.insert(&obj.index, ());
834
835            if let Object::Record(record) = &obj.object {
836                let mut found_subs = HashMap::new();
837                for sub in &record.subs {
838                    if found_subs.contains_key(&sub.sub_index) {
839                        return DuplicateSubObjectsSnafu {
840                            index: obj.index,
841                            sub: sub.sub_index,
842                        }
843                        .fail();
844                    }
845                    found_subs.insert(&sub.sub_index, ());
846                }
847            }
848        }
849
850        Ok(())
851    }
852}
853
854/// A newtype on AccessType to implement serialization
855#[derive(Clone, Copy, Debug, Default)]
856pub struct AccessTypeDeser(pub AccessType);
857impl<'de> serde::Deserialize<'de> for AccessTypeDeser {
858    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
859    where
860        D: serde::Deserializer<'de>,
861    {
862        let s = String::deserialize(deserializer)?;
863        match s.to_lowercase().as_str() {
864            "ro" => Ok(AccessTypeDeser(AccessType::Ro)),
865            "rw" => Ok(AccessTypeDeser(AccessType::Rw)),
866            "wo" => Ok(AccessTypeDeser(AccessType::Wo)),
867            "const" => Ok(AccessTypeDeser(AccessType::Const)),
868            _ => Err(D::Error::custom(format!(
869                "Invalid access type: {} (allowed: 'ro', 'rw', 'wo', or 'const')",
870                s
871            ))),
872        }
873    }
874}
875impl From<AccessType> for AccessTypeDeser {
876    fn from(access_type: AccessType) -> Self {
877        AccessTypeDeser(access_type)
878    }
879}
880
881/// A type to represent data_type fields in a device config
882///
883/// This is similar, but slightly different from the DataType defined in `zencan_common`
884#[derive(Clone, Copy, Debug, Default)]
885#[allow(missing_docs)]
886pub enum DataType {
887    Boolean,
888    Int8,
889    Int16,
890    Int32,
891    #[default]
892    UInt8,
893    UInt16,
894    UInt32,
895    Real32,
896    VisibleString(usize),
897    OctetString(usize),
898    UnicodeString(usize),
899    TimeOfDay,
900    TimeDifference,
901    Domain,
902}
903
904impl DataType {
905    /// Returns true if the type is one of the stringy types
906    pub fn is_str(&self) -> bool {
907        matches!(
908            self,
909            DataType::VisibleString(_) | DataType::OctetString(_) | DataType::UnicodeString(_)
910        )
911    }
912
913    /// Get the storage size of the data type
914    pub fn size(&self) -> usize {
915        match self {
916            DataType::Boolean => 1,
917            DataType::Int8 => 1,
918            DataType::Int16 => 2,
919            DataType::Int32 => 4,
920            DataType::UInt8 => 1,
921            DataType::UInt16 => 2,
922            DataType::UInt32 => 4,
923            DataType::Real32 => 4,
924            DataType::VisibleString(size) => *size,
925            DataType::OctetString(size) => *size,
926            DataType::UnicodeString(size) => *size,
927            DataType::TimeOfDay => 4,
928            DataType::TimeDifference => 4,
929            DataType::Domain => 0, // Domain size is variable
930        }
931    }
932}
933
934impl<'de> serde::Deserialize<'de> for DataType {
935    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
936    where
937        D: serde::Deserializer<'de>,
938    {
939        let re_visiblestring = regex::Regex::new(r"^visiblestring\((\d+)\)$").unwrap();
940        let re_octetstring = regex::Regex::new(r"^octetstring\((\d+)\)$").unwrap();
941        let re_unicodestring = regex::Regex::new(r"^unicodestring\((\d+)\)$").unwrap();
942
943        let s = String::deserialize(deserializer)?.to_lowercase();
944        if s == "boolean" {
945            Ok(DataType::Boolean)
946        } else if s == "int8" {
947            Ok(DataType::Int8)
948        } else if s == "int16" {
949            Ok(DataType::Int16)
950        } else if s == "int32" {
951            Ok(DataType::Int32)
952        } else if s == "uint8" {
953            Ok(DataType::UInt8)
954        } else if s == "uint16" {
955            Ok(DataType::UInt16)
956        } else if s == "uint32" {
957            Ok(DataType::UInt32)
958        } else if s == "real32" {
959            Ok(DataType::Real32)
960        } else if let Some(caps) = re_visiblestring.captures(&s) {
961            let size: usize = caps[1].parse().map_err(|_| {
962                D::Error::custom(format!("Invalid size for VisibleString: {}", &caps[1]))
963            })?;
964            Ok(DataType::VisibleString(size))
965        } else if let Some(caps) = re_octetstring.captures(&s) {
966            let size: usize = caps[1].parse().map_err(|_| {
967                D::Error::custom(format!("Invalid size for OctetString: {}", &caps[1]))
968            })?;
969            Ok(DataType::OctetString(size))
970        } else if let Some(caps) = re_unicodestring.captures(&s) {
971            let size: usize = caps[1].parse().map_err(|_| {
972                D::Error::custom(format!("Invalid size for UnicodeString: {}", &caps[1]))
973            })?;
974            Ok(DataType::UnicodeString(size))
975        } else if s == "timeofday" {
976            Ok(DataType::TimeOfDay)
977        } else if s == "timedifference" {
978            Ok(DataType::TimeDifference)
979        } else if s == "domain" {
980            Ok(DataType::Domain)
981        } else {
982            Err(D::Error::custom(format!("Invalid data type: {}", s)))
983        }
984    }
985}
986
987#[cfg(test)]
988mod tests {
989    use crate::device_config::{DeviceConfig, LoadError};
990    use assertables::assert_contains;
991    #[test]
992    fn test_duplicate_objects_errors() {
993        const TOML: &str = r#"
994            device_name = "test"
995            [identity]
996            vendor_id = 0
997            product_code = 1
998            revision_number = 2
999
1000            [[objects]]
1001            index = 0x2000
1002            parameter_name = "Test1"
1003            object_type = "var"
1004            data_type = "int16"
1005            access_type = "rw"
1006
1007            [[objects]]
1008            index = 0x2000
1009            parameter_name = "Duplicate"
1010            object_type = "record"
1011        "#;
1012
1013        let result = DeviceConfig::load_from_str(TOML);
1014
1015        assert!(result.is_err());
1016        let err = result.unwrap_err();
1017        assert!(matches!(err, LoadError::DuplicateObjectIds { id: 0x2000 }));
1018        assert_contains!(
1019            "Multiple definitions for object with index 0x2000",
1020            err.to_string().as_str()
1021        );
1022    }
1023
1024    #[test]
1025    fn test_duplicate_sub_object_errors() {
1026        const TOML: &str = r#"
1027            device_name = "test"
1028            [identity]
1029            vendor_id = 0
1030            product_code = 1
1031            revision_number = 2
1032
1033
1034            [[objects]]
1035            index = 0x2000
1036            parameter_name = "Duplicate"
1037            object_type = "record"
1038            [[objects.subs]]
1039            sub_index = 1
1040            parameter_name = "Test1"
1041            data_type = "int16"
1042            access_type = "rw"
1043            [[objects.subs]]
1044            sub_index = 1
1045            parameter_name = "RepeatedTest1"
1046            data_type = "int16"
1047            access_type = "rw"
1048        "#;
1049
1050        let result = DeviceConfig::load_from_str(TOML);
1051
1052        assert!(result.is_err());
1053        let err = result.unwrap_err();
1054        assert!(matches!(
1055            err,
1056            LoadError::DuplicateSubObjects {
1057                index: 0x2000,
1058                sub: 1
1059            }
1060        ));
1061        assert_contains!(
1062            "Multiple definitions of sub index 1 on object 0x2000",
1063            err.to_string().as_str()
1064        );
1065    }
1066}