Skip to main content

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