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)]
552pub struct PdoDefaultConfig {
553    /// The COB ID this PDO will use to send/receive
554    pub cob_id: u32,
555    /// The COB ID is an extended 29-bit ID
556    #[serde(default)]
557    pub extended: bool,
558    /// The node ID should be added to `cob_id`` at runtime
559    pub add_node_id: bool,
560    /// Indicates if this PDO is enabled
561    pub enabled: bool,
562    /// If set, this PDO will not respond to requests
563    #[serde(default)]
564    pub rtr_disabled: bool,
565    /// List of mapping specifying what sub objects are mapped to this PDO
566    pub mappings: Vec<PdoMapping>,
567    /// Specifies when a PDO is sent or latched
568    ///
569    /// - 0: Sent in response to sync, but only after an application specific event (e.g. it may be
570    ///   sent when the value changes, but not when it has not)
571    /// - 1 - 240: Sent in response to every Nth sync
572    /// - 254: Event driven (application to send it whenever it wants)
573    pub transmission_type: u8,
574}
575
576#[derive(Clone, Debug, Default, Deserialize)]
577pub(crate) struct PdoDefaultConfigMapSerializer(
578    #[serde(deserialize_with = "deserialize_pdo_map", default)] pub HashMap<usize, PdoDefaultConfig>,
579);
580
581impl From<PdoDefaultConfigMapSerializer> for HashMap<usize, PdoDefaultConfig> {
582    fn from(value: PdoDefaultConfigMapSerializer) -> Self {
583        value.0
584    }
585}
586
587/// Private struct for deserializing [pdos] section of device config TOML
588#[derive(Debug, Deserialize)]
589struct DevicePdoConfigSerializer {
590    #[serde(default = "default_num_rpdo")]
591    /// The number of TX PDO slots available in the device. Defaults to 4.
592    pub num_tpdo: u8,
593    #[serde(default = "default_num_tpdo")]
594    /// The number of RX PDO slots available in the device. Defaults to 4.
595    pub num_rpdo: u8,
596
597    /// Map of default configurations for individual TPDOs
598    #[serde(default)]
599    pub tpdo: PdoDefaultConfigMapSerializer,
600    #[serde(default)]
601    pub rpdo: PdoDefaultConfigMapSerializer,
602}
603
604impl From<DevicePdoConfigSerializer> for DevicePdoConfig {
605    fn from(value: DevicePdoConfigSerializer) -> Self {
606        Self {
607            num_tpdo: value.num_tpdo,
608            num_rpdo: value.num_rpdo,
609            tpdo_defaults: value.tpdo.0,
610            rpdo_defaults: value.rpdo.0,
611        }
612    }
613}
614
615/// Device PDO configuration options
616///
617/// This controls how many TPDO/RPDO slots are created, and how they are configured by default
618#[derive(Clone, Debug, Deserialize)]
619#[serde(try_from = "DevicePdoConfigSerializer")]
620pub struct DevicePdoConfig {
621    /// The number of TX PDO slots available in the device. Defaults to 4.
622    pub num_tpdo: u8,
623    /// The number of RX PDO slots available in the device. Defaults to 4.
624    pub num_rpdo: u8,
625
626    /// Map of default configurations for individual TPDOs
627    pub tpdo_defaults: HashMap<usize, PdoDefaultConfig>,
628    /// Map of default configurations for individual RPDOs
629    pub rpdo_defaults: HashMap<usize, PdoDefaultConfig>,
630}
631
632impl Default for DevicePdoConfig {
633    fn default() -> Self {
634        Self {
635            num_tpdo: default_num_tpdo(),
636            num_rpdo: default_num_rpdo(),
637            tpdo_defaults: HashMap::new(),
638            rpdo_defaults: HashMap::new(),
639        }
640    }
641}
642
643/// The device identity is a unique 128-bit number used for addressing the device on the bus
644///
645/// The configures the three hardcoded components of the identity. The serial number component of
646/// the identity must be set by the application to be unique, e.g. based on a value programmed into
647/// non-volatile memory or from a UID register on the MCU.
648#[derive(Deserialize, Debug, Default, Clone, Copy)]
649#[serde(deny_unknown_fields)]
650pub struct IdentityConfig {
651    /// The 32-bit vendor ID for this device
652    pub vendor_id: u32,
653    /// The 32-bit product code for this device
654    pub product_code: u32,
655    /// The 32-bit revision number for this device
656    pub revision_number: u32,
657}
658
659/// Configuration object to define a programmable bootloader section
660#[derive(Clone, Debug, Deserialize)]
661pub struct BootloaderSection {
662    /// Name of the section
663    pub name: String,
664    /// Size of the section
665    pub size: u32,
666}
667
668/// Configuration of bootloader parameters
669#[derive(Clone, Deserialize, Debug, Default)]
670pub struct BootloaderConfig {
671    /// If true, this node is an application which supports resetting to a bootloader, rather than a
672    /// bootloader implementation
673    #[serde(default)]
674    pub application: bool,
675    /// List of programmable sections
676    #[serde(default)]
677    pub sections: Vec<BootloaderSection>,
678}
679
680#[derive(Deserialize, Debug, Clone)]
681#[serde(deny_unknown_fields)]
682/// Private struct for seserializing device config files
683pub struct DeviceConfig {
684    /// The name describing the type of device (e.g. a model)
685    pub device_name: String,
686
687    /// Configures support for the AutoStart (0x5000) object and its default value
688    ///
689    /// Allowed values:
690    /// - 'unsupported': No autostart object is created
691    /// - 'disabled': An autostart object is created, and it defaults to disabled
692    /// - 'enabled': An autostart object is created, and it defaults to enabled
693    #[serde(default)]
694    pub autostart: AutoStartConfig,
695
696    /// Enables object storage commands (object 0x1010)
697    ///
698    /// Default: true
699    #[serde(default = "default_true")]
700    pub support_storage: bool,
701
702    /// A version describing the hardware
703    #[serde(default)]
704    pub hardware_version: String,
705    /// A version describing the software
706    #[serde(default)]
707    pub software_version: String,
708
709    /// The period at which to transmit heartbeat messages in milliseconds
710    #[serde(default)]
711    pub heartbeat_period: u16,
712
713    /// Configures the identity object on the device
714    pub identity: IdentityConfig,
715
716    /// Configure PDO settings
717    #[serde(default)]
718    pub pdos: DevicePdoConfig,
719
720    /// Configure bootloader options
721    #[serde(default)]
722    pub bootloader: BootloaderConfig,
723
724    /// A list of application specific objects to define on the device
725    #[serde(default)]
726    pub objects: Vec<ObjectDefinition>,
727}
728
729/// Defines a sub-object in a record
730#[derive(Deserialize, Debug, Default, Clone)]
731#[serde(deny_unknown_fields)]
732pub struct SubDefinition {
733    /// Sub index for the sub-object being defined
734    pub sub_index: u8,
735    /// A human readable name for the value stored in this sub-object
736    #[serde(default)]
737    pub parameter_name: String,
738    /// Used to name the struct field associated with this sub object
739    ///
740    /// This is only applicable to record objects. If no name is provided, the default field name
741    /// will be `sub[index]`, where index is the uppercase hex representation of the sub index
742    #[serde(default)]
743    pub field_name: Option<String>,
744    /// The data type of the sub object
745    pub data_type: DataType,
746    /// Access permissions for the sub object
747    #[serde(default)]
748    pub access_type: AccessTypeDeser,
749    /// The default value for the sub object
750    #[serde(default)]
751    pub default_value: Option<DefaultValue>,
752    /// Indicates whether this sub object can be mapped to PDOs
753    #[serde(default)]
754    pub pdo_mapping: PdoMappable,
755    /// Indicates if this sub object should be saved when the save command is sent
756    #[serde(default)]
757    pub persist: bool,
758}
759
760/// An enum to represent object default values
761#[derive(Deserialize, Debug, Clone)]
762#[serde(untagged)]
763pub enum DefaultValue {
764    /// A default value for integer fields
765    Integer(i64),
766    /// A default value for float fields
767    Float(f64),
768    /// A default value for string fields
769    String(String),
770}
771
772impl From<i64> for DefaultValue {
773    fn from(value: i64) -> Self {
774        Self::Integer(value)
775    }
776}
777
778impl From<i32> for DefaultValue {
779    fn from(value: i32) -> Self {
780        Self::Integer(value as i64)
781    }
782}
783
784impl From<usize> for DefaultValue {
785    fn from(value: usize) -> Self {
786        Self::Integer(value as i64)
787    }
788}
789
790impl From<f64> for DefaultValue {
791    fn from(value: f64) -> Self {
792        Self::Float(value)
793    }
794}
795
796impl From<&str> for DefaultValue {
797    fn from(value: &str) -> Self {
798        Self::String(value.to_string())
799    }
800}
801
802/// An enum representing the different types of objects which can be defined in a device config
803#[derive(Deserialize, Debug, Clone)]
804#[serde(tag = "object_type", rename_all = "lowercase")]
805pub enum Object {
806    /// A var object is just a single value
807    Var(VarDefinition),
808    /// An array object is an array of values, all with the same type
809    Array(ArrayDefinition),
810    /// A record is a collection of sub objects all with different types
811    Record(RecordDefinition),
812}
813
814/// Descriptor for a var object
815#[derive(Default, Deserialize, Debug, Clone)]
816#[serde(deny_unknown_fields)]
817pub struct VarDefinition {
818    /// Indicates the type of data stored in the object
819    pub data_type: DataType,
820    /// Indicates how this object can be accessed
821    pub access_type: AccessTypeDeser,
822    /// The default value for this object
823    pub default_value: Option<DefaultValue>,
824    /// Determines which if type of PDO this object can me mapped to
825    #[serde(default)]
826    pub pdo_mapping: PdoMappable,
827    /// Indicates that this object should be saved
828    #[serde(default)]
829    pub persist: bool,
830}
831
832/// Descriptor for an array object
833#[derive(Default, Deserialize, Debug, Clone)]
834#[serde(deny_unknown_fields)]
835pub struct ArrayDefinition {
836    /// The datatype of array fields
837    pub data_type: DataType,
838    /// Access type for all array fields
839    pub access_type: AccessTypeDeser,
840    /// The number of elements in the array
841    pub array_size: usize,
842    /// Default values for all array fields
843    pub default_value: Option<Vec<DefaultValue>>,
844    #[serde(default)]
845    /// Whether fields in this array can be mapped to PDOs
846    pub pdo_mapping: PdoMappable,
847    #[serde(default)]
848    /// Whether this array should be saved to flash on command
849    pub persist: bool,
850}
851
852/// Descriptor for a record object
853#[derive(Deserialize, Debug, Clone)]
854#[serde(deny_unknown_fields)]
855pub struct RecordDefinition {
856    /// The sub object definitions for this record object
857    #[serde(default)]
858    pub subs: Vec<SubDefinition>,
859}
860
861/// Descriptor for a domain object
862///
863/// Not yet implemented
864#[derive(Clone, Copy, Deserialize, Debug)]
865pub struct DomainDefinition {}
866
867/// Descriptor for an object in the object dictionary
868#[derive(Deserialize, Debug, Clone)]
869pub struct ObjectDefinition {
870    /// The index of the object
871    pub index: u16,
872    /// A human readable name to describe the contents of the object
873    #[serde(default)]
874    pub parameter_name: String,
875    #[serde(default)]
876    /// If true, this object is implemented by an application callback, and no storage will be
877    /// allocated for it in the object dictionary.
878    pub application_callback: bool,
879    /// The descriptor for the object
880    #[serde(flatten)]
881    pub object: Object,
882}
883
884impl ObjectDefinition {
885    /// Get the object code specifying the type of this object
886    pub fn object_code(&self) -> ObjectCode {
887        match self.object {
888            Object::Var(_) => ObjectCode::Var,
889            Object::Array(_) => ObjectCode::Array,
890            Object::Record(_) => ObjectCode::Record,
891        }
892    }
893}
894
895impl DeviceConfig {
896    /// Try to read a device config from a file
897    pub fn load(config_path: impl AsRef<std::path::Path>) -> Result<Self, LoadError> {
898        let config_str = std::fs::read_to_string(&config_path).context(IoSnafu)?;
899        Self::load_from_str(&config_str)
900    }
901
902    /// Try to read a config from a &str
903    pub fn load_from_str(config_str: &str) -> Result<Self, LoadError> {
904        let mut config: DeviceConfig = toml::from_str(config_str).context(TomlParsingSnafu)?;
905
906        // Add mandatory objects to the config
907        config.objects.extend(mandatory_objects(&config));
908        config
909            .objects
910            .extend(bootloader_objects(&config.bootloader));
911        config.objects.extend(pdo_objects(
912            config.pdos.num_rpdo as usize,
913            config.pdos.num_tpdo as usize,
914        ));
915        config.objects.extend(object_storage_objects(&config));
916
917        Self::validate_unique_indices(&config.objects)?;
918
919        Ok(config)
920    }
921
922    fn validate_unique_indices(objects: &[ObjectDefinition]) -> Result<(), LoadError> {
923        let mut found_indices = HashMap::new();
924        for obj in objects {
925            if found_indices.contains_key(&obj.index) {
926                return DuplicateObjectIdsSnafu { id: obj.index }.fail();
927            }
928            found_indices.insert(&obj.index, ());
929
930            if let Object::Record(record) = &obj.object {
931                let mut found_subs = HashMap::new();
932                for sub in &record.subs {
933                    if found_subs.contains_key(&sub.sub_index) {
934                        return DuplicateSubObjectsSnafu {
935                            index: obj.index,
936                            sub: sub.sub_index,
937                        }
938                        .fail();
939                    }
940                    found_subs.insert(&sub.sub_index, ());
941                }
942            }
943        }
944
945        Ok(())
946    }
947}
948
949/// A newtype on AccessType to implement serialization
950#[derive(Clone, Copy, Debug, Default)]
951pub struct AccessTypeDeser(pub AccessType);
952impl<'de> serde::Deserialize<'de> for AccessTypeDeser {
953    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
954    where
955        D: serde::Deserializer<'de>,
956    {
957        let s = String::deserialize(deserializer)?;
958        match s.to_lowercase().as_str() {
959            "ro" => Ok(AccessTypeDeser(AccessType::Ro)),
960            "rw" => Ok(AccessTypeDeser(AccessType::Rw)),
961            "wo" => Ok(AccessTypeDeser(AccessType::Wo)),
962            "const" => Ok(AccessTypeDeser(AccessType::Const)),
963            _ => Err(D::Error::custom(format!(
964                "Invalid access type: {} (allowed: 'ro', 'rw', 'wo', or 'const')",
965                s
966            ))),
967        }
968    }
969}
970impl From<AccessType> for AccessTypeDeser {
971    fn from(access_type: AccessType) -> Self {
972        AccessTypeDeser(access_type)
973    }
974}
975
976/// A type to represent data_type fields in a device config
977///
978/// This is similar, but slightly different from the DataType defined in `zencan_common`
979#[derive(Clone, Copy, Debug, Default)]
980#[allow(missing_docs)]
981pub enum DataType {
982    Boolean,
983    Int8,
984    Int16,
985    Int32,
986    Int64,
987    #[default]
988    UInt8,
989    UInt16,
990    UInt32,
991    UInt64,
992    Real32,
993    Real64,
994    VisibleString(usize),
995    OctetString(usize),
996    UnicodeString(usize),
997    TimeOfDay,
998    TimeDifference,
999    Domain,
1000}
1001
1002impl DataType {
1003    /// Returns true if the type is one of the stringy types
1004    pub fn is_str(&self) -> bool {
1005        matches!(
1006            self,
1007            DataType::VisibleString(_) | DataType::OctetString(_) | DataType::UnicodeString(_)
1008        )
1009    }
1010
1011    /// Get the storage size of the data type
1012    pub fn size(&self) -> usize {
1013        match self {
1014            DataType::Boolean => 1,
1015            DataType::Int8 => 1,
1016            DataType::Int16 => 2,
1017            DataType::Int32 => 4,
1018            DataType::Int64 => 8,
1019            DataType::UInt8 => 1,
1020            DataType::UInt16 => 2,
1021            DataType::UInt32 => 4,
1022            DataType::UInt64 => 8,
1023            DataType::Real32 => 4,
1024            DataType::Real64 => 8,
1025            DataType::VisibleString(size) => *size,
1026            DataType::OctetString(size) => *size,
1027            DataType::UnicodeString(size) => *size,
1028            DataType::TimeOfDay => 4,
1029            DataType::TimeDifference => 4,
1030            DataType::Domain => 0, // Domain size is variable
1031        }
1032    }
1033}
1034
1035impl<'de> serde::Deserialize<'de> for DataType {
1036    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1037    where
1038        D: serde::Deserializer<'de>,
1039    {
1040        let re_visiblestring = regex::Regex::new(r"^visiblestring\((\d+)\)$").unwrap();
1041        let re_octetstring = regex::Regex::new(r"^octetstring\((\d+)\)$").unwrap();
1042        let re_unicodestring = regex::Regex::new(r"^unicodestring\((\d+)\)$").unwrap();
1043
1044        let s = String::deserialize(deserializer)?.to_lowercase();
1045        if s == "boolean" {
1046            Ok(DataType::Boolean)
1047        } else if s == "int8" {
1048            Ok(DataType::Int8)
1049        } else if s == "int16" {
1050            Ok(DataType::Int16)
1051        } else if s == "int32" {
1052            Ok(DataType::Int32)
1053        } else if s == "int64" {
1054            Ok(DataType::Int64)
1055        } else if s == "uint8" {
1056            Ok(DataType::UInt8)
1057        } else if s == "uint16" {
1058            Ok(DataType::UInt16)
1059        } else if s == "uint32" {
1060            Ok(DataType::UInt32)
1061        } else if s == "uint64" {
1062            Ok(DataType::UInt64)
1063        } else if s == "real32" {
1064            Ok(DataType::Real32)
1065        } else if s == "real64" {
1066            Ok(DataType::Real64)
1067        } else if let Some(caps) = re_visiblestring.captures(&s) {
1068            let size: usize = caps[1].parse().map_err(|_| {
1069                D::Error::custom(format!("Invalid size for VisibleString: {}", &caps[1]))
1070            })?;
1071            Ok(DataType::VisibleString(size))
1072        } else if let Some(caps) = re_octetstring.captures(&s) {
1073            let size: usize = caps[1].parse().map_err(|_| {
1074                D::Error::custom(format!("Invalid size for OctetString: {}", &caps[1]))
1075            })?;
1076            Ok(DataType::OctetString(size))
1077        } else if let Some(caps) = re_unicodestring.captures(&s) {
1078            let size: usize = caps[1].parse().map_err(|_| {
1079                D::Error::custom(format!("Invalid size for UnicodeString: {}", &caps[1]))
1080            })?;
1081            Ok(DataType::UnicodeString(size))
1082        } else if s == "timeofday" {
1083            Ok(DataType::TimeOfDay)
1084        } else if s == "timedifference" {
1085            Ok(DataType::TimeDifference)
1086        } else if s == "domain" {
1087            Ok(DataType::Domain)
1088        } else {
1089            Err(D::Error::custom(format!("Invalid data type: {}", s)))
1090        }
1091    }
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096    use crate::device_config::{DeviceConfig, LoadError};
1097    use assertables::assert_contains;
1098    #[test]
1099    fn test_duplicate_objects_errors() {
1100        const TOML: &str = r#"
1101            device_name = "test"
1102            [identity]
1103            vendor_id = 0
1104            product_code = 1
1105            revision_number = 2
1106
1107            [[objects]]
1108            index = 0x2000
1109            parameter_name = "Test1"
1110            object_type = "var"
1111            data_type = "int16"
1112            access_type = "rw"
1113
1114            [[objects]]
1115            index = 0x2000
1116            parameter_name = "Duplicate"
1117            object_type = "record"
1118        "#;
1119
1120        let result = DeviceConfig::load_from_str(TOML);
1121
1122        assert!(result.is_err());
1123        let err = result.unwrap_err();
1124        assert!(matches!(err, LoadError::DuplicateObjectIds { id: 0x2000 }));
1125        assert_contains!(
1126            "Multiple definitions for object with index 0x2000",
1127            err.to_string().as_str()
1128        );
1129    }
1130
1131    #[test]
1132    fn test_duplicate_sub_object_errors() {
1133        const TOML: &str = r#"
1134            device_name = "test"
1135            [identity]
1136            vendor_id = 0
1137            product_code = 1
1138            revision_number = 2
1139
1140
1141            [[objects]]
1142            index = 0x2000
1143            parameter_name = "Duplicate"
1144            object_type = "record"
1145            [[objects.subs]]
1146            sub_index = 1
1147            parameter_name = "Test1"
1148            data_type = "int16"
1149            access_type = "rw"
1150            [[objects.subs]]
1151            sub_index = 1
1152            parameter_name = "RepeatedTest1"
1153            data_type = "int16"
1154            access_type = "rw"
1155        "#;
1156
1157        let result = DeviceConfig::load_from_str(TOML);
1158
1159        assert!(result.is_err());
1160        let err = result.unwrap_err();
1161        assert!(matches!(
1162            err,
1163            LoadError::DuplicateSubObjects {
1164                index: 0x2000,
1165                sub: 1
1166            }
1167        ));
1168        assert_contains!(
1169            "Multiple definitions of sub index 1 on object 0x2000",
1170            err.to_string().as_str()
1171        );
1172    }
1173}