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