1use std::collections::HashMap;
124
125use crate::objects::{AccessType, ObjectCode};
126use serde::{de::Error, Deserialize};
127
128use snafu::ResultExt as _;
129use snafu::Snafu;
130
131#[derive(Debug, Snafu)]
133pub enum LoadError {
134 #[snafu(display("IO error: {source}"))]
136 Io {
137 source: std::io::Error,
139 },
140 #[snafu(display("Toml parse error: {source}"))]
142 TomlParsing {
143 source: toml::de::Error,
145 },
146 #[snafu(display("Multiple definitions for object with index 0x{id:x}"))]
148 DuplicateObjectIds {
149 id: u16,
151 },
152 #[snafu(display("Multiple definitions of sub index {sub} on object 0x{index:x}"))]
154 DuplicateSubObjects {
155 index: u16,
157 sub: u8,
159 },
160}
161
162fn mandatory_objects(config: &DeviceConfig) -> Vec<ObjectDefinition> {
163 vec![
164 ObjectDefinition {
165 index: 0x1000,
166 parameter_name: "Device Type".to_string(),
167 application_callback: false,
168 object: Object::Var(VarDefinition {
169 data_type: DataType::UInt32,
170 access_type: AccessType::Const.into(),
171 default_value: Some(DefaultValue::Integer(0x00000000)),
172 pdo_mapping: PdoMapping::None,
173 ..Default::default()
174 }),
175 },
176 ObjectDefinition {
177 index: 0x1001,
178 parameter_name: "Error Register".to_string(),
179 application_callback: false,
180 object: Object::Var(VarDefinition {
181 data_type: DataType::UInt8,
182 access_type: AccessType::Ro.into(),
183 default_value: Some(DefaultValue::Integer(0x00000000)),
184 pdo_mapping: PdoMapping::None,
185 ..Default::default()
186 }),
187 },
188 ObjectDefinition {
189 index: 0x1008,
190 parameter_name: "Manufacturer Device Name".to_string(),
191 application_callback: false,
192 object: Object::Var(VarDefinition {
193 data_type: DataType::VisibleString(config.device_name.len()),
194 access_type: AccessType::Const.into(),
195 default_value: Some(DefaultValue::String(config.device_name.clone())),
196 pdo_mapping: PdoMapping::None,
197 ..Default::default()
198 }),
199 },
200 ObjectDefinition {
201 index: 0x1009,
202 parameter_name: "Manufacturer Hardware Version".to_string(),
203 application_callback: false,
204 object: Object::Var(VarDefinition {
205 data_type: DataType::VisibleString(config.hardware_version.len()),
206 access_type: AccessType::Const.into(),
207 default_value: Some(DefaultValue::String(config.hardware_version.clone())),
208 pdo_mapping: PdoMapping::None,
209 ..Default::default()
210 }),
211 },
212 ObjectDefinition {
213 index: 0x100A,
214 parameter_name: "Manufacturer Software Version".to_string(),
215 application_callback: false,
216 object: Object::Var(VarDefinition {
217 data_type: DataType::VisibleString(config.software_version.len()),
218 access_type: AccessType::Const.into(),
219 default_value: Some(DefaultValue::String(config.software_version.clone())),
220 pdo_mapping: PdoMapping::None,
221 ..Default::default()
222 }),
223 },
224 ObjectDefinition {
225 index: 0x1017,
226 parameter_name: "Heartbeat Producer Time (ms)".to_string(),
227 application_callback: false,
228 object: Object::Var(VarDefinition {
229 data_type: DataType::UInt16,
230 access_type: AccessType::Const.into(),
231 default_value: Some(DefaultValue::Integer(config.heartbeat_period as i64)),
232 pdo_mapping: PdoMapping::None,
233 persist: false,
234 }),
235 },
236 ObjectDefinition {
237 index: 0x1018,
238 parameter_name: "Identity".to_string(),
239 application_callback: false,
240 object: Object::Record(RecordDefinition {
241 subs: vec![
242 SubDefinition {
243 sub_index: 1,
244 parameter_name: "Vendor ID".to_string(),
245 field_name: Some("vendor_id".into()),
246 data_type: DataType::UInt32,
247 access_type: AccessType::Const.into(),
248 default_value: Some(DefaultValue::Integer(
249 config.identity.vendor_id as i64,
250 )),
251 pdo_mapping: PdoMapping::None,
252 ..Default::default()
253 },
254 SubDefinition {
255 sub_index: 2,
256 parameter_name: "Product Code".to_string(),
257 field_name: Some("product_code".into()),
258 data_type: DataType::UInt32,
259 access_type: AccessType::Const.into(),
260 default_value: Some(DefaultValue::Integer(
261 config.identity.product_code as i64,
262 )),
263 pdo_mapping: PdoMapping::None,
264 ..Default::default()
265 },
266 SubDefinition {
267 sub_index: 3,
268 parameter_name: "Revision Number".to_string(),
269 field_name: Some("revision".into()),
270 data_type: DataType::UInt32,
271 access_type: AccessType::Const.into(),
272 default_value: Some(DefaultValue::Integer(
273 config.identity.revision_number as i64,
274 )),
275 pdo_mapping: PdoMapping::None,
276 ..Default::default()
277 },
278 SubDefinition {
279 sub_index: 4,
280 parameter_name: "Serial Number".to_string(),
281 field_name: Some("serial".into()),
282 data_type: DataType::UInt32,
283 access_type: AccessType::Const.into(),
284 default_value: Some(DefaultValue::Integer(0)),
285 pdo_mapping: PdoMapping::None,
286 ..Default::default()
287 },
288 ],
289 }),
290 },
291 ObjectDefinition {
292 index: 0x5000,
293 parameter_name: "Auto Start".to_string(),
294 application_callback: false,
295 object: Object::Var(VarDefinition {
296 data_type: DataType::UInt8,
297 access_type: AccessType::Rw.into(),
298 default_value: None,
299 pdo_mapping: PdoMapping::None,
300 persist: true,
301 }),
302 },
303 ]
304}
305
306fn pdo_objects(num_rpdo: usize, num_tpdo: usize) -> Vec<ObjectDefinition> {
307 let mut objects = Vec::new();
308
309 fn add_objects(objects: &mut Vec<ObjectDefinition>, i: usize, tx: bool) {
310 let pdo_type = if tx { "TPDO" } else { "RPDO" };
311 let comm_index = if tx { 0x1800 } else { 0x1400 };
312 let mapping_index = if tx { 0x1A00 } else { 0x1600 };
313
314 objects.push(ObjectDefinition {
315 index: comm_index + i as u16,
316 parameter_name: format!("{}{} Communication Parameter", pdo_type, i),
317 application_callback: true,
318 object: Object::Record(RecordDefinition {
319 subs: vec![
320 SubDefinition {
321 sub_index: 1,
322 parameter_name: format!("COB-ID for {}{}", pdo_type, i),
323 field_name: None,
324 data_type: DataType::UInt32,
325 access_type: AccessType::Rw.into(),
326 default_value: None,
327 pdo_mapping: PdoMapping::None,
328 persist: true,
329 },
330 SubDefinition {
331 sub_index: 2,
332 parameter_name: format!("Transmission type for {}{}", pdo_type, i),
333 field_name: None,
334 data_type: DataType::UInt8,
335 access_type: AccessType::Rw.into(),
336 default_value: None,
337 pdo_mapping: PdoMapping::None,
338 persist: true,
339 },
340 ],
341 }),
342 });
343
344 let mut mapping_subs = vec![SubDefinition {
345 sub_index: 0,
346 parameter_name: "Valid Mappings".to_string(),
347 field_name: None,
348 data_type: DataType::UInt8,
349 access_type: AccessType::Rw.into(),
350 default_value: Some(DefaultValue::Integer(0)),
351 pdo_mapping: PdoMapping::None,
352 persist: true,
353 }];
354 for sub in 1..65 {
355 mapping_subs.push(SubDefinition {
356 sub_index: sub,
357 parameter_name: format!("{}{} Mapping App Object {}", pdo_type, i, sub),
358 field_name: None,
359 data_type: DataType::UInt32,
360 access_type: AccessType::Rw.into(),
361 default_value: None,
362 pdo_mapping: PdoMapping::None,
363 persist: true,
364 });
365 }
366
367 objects.push(ObjectDefinition {
368 index: mapping_index + i as u16,
369 parameter_name: format!("{}{} Mapping Parameters", pdo_type, i),
370 application_callback: true,
371 object: Object::Record(RecordDefinition { subs: mapping_subs }),
372 });
373 }
374 for i in 0..num_rpdo {
375 add_objects(&mut objects, i, false);
376 }
377 for i in 0..num_tpdo {
378 add_objects(&mut objects, i, true);
379 }
380 objects
381}
382
383fn bootloader_objects(cfg: &BootloaderConfig) -> Vec<ObjectDefinition> {
384 let mut objects = Vec::new();
385
386 if cfg.sections.is_empty() {
387 return objects;
388 }
389 objects.push(ObjectDefinition {
390 index: 0x5500,
391 parameter_name: "Bootloader Info".into(),
392 application_callback: false,
393 object: Object::Record(RecordDefinition {
394 subs: vec![
395 SubDefinition {
396 sub_index: 1,
397 parameter_name: "Bootloader Config".into(),
398 field_name: Some("config".into()),
399 data_type: DataType::UInt32,
400 access_type: AccessType::Ro.into(),
401 default_value: Some(0.into()),
402 pdo_mapping: PdoMapping::None,
403 persist: false,
404 },
405 SubDefinition {
406 sub_index: 2,
407 parameter_name: "Number of Section".into(),
408 field_name: Some("num_sections".into()),
409 data_type: DataType::UInt8,
410 access_type: AccessType::Ro.into(),
411 default_value: Some(cfg.sections.len().into()),
412 pdo_mapping: PdoMapping::None,
413 persist: false,
414 },
415 SubDefinition {
416 sub_index: 3,
417 parameter_name: "Reset to Bootloader Command".into(),
418 field_name: None,
419 data_type: DataType::UInt32,
420 access_type: AccessType::Wo.into(),
421 default_value: None,
422 pdo_mapping: PdoMapping::None,
423 persist: false,
424 },
425 ],
426 }),
427 });
428
429 for (i, section) in cfg.sections.iter().enumerate() {
430 objects.push(ObjectDefinition {
431 index: 0x5510 + i as u16,
432 parameter_name: format!("Bootloader Section {i}"),
433 application_callback: true,
434 object: Object::Record(RecordDefinition {
435 subs: vec![
436 SubDefinition {
437 sub_index: 1,
438 parameter_name: "Mode bits".into(),
439 data_type: DataType::UInt8,
440 access_type: AccessType::Const.into(),
441 ..Default::default()
442 },
443 SubDefinition {
444 sub_index: 2,
445 parameter_name: "Section Name".into(),
446 data_type: DataType::VisibleString(0),
447 access_type: AccessType::Const.into(),
448 default_value: Some(section.name.as_str().into()),
449 ..Default::default()
450 },
451 SubDefinition {
452 sub_index: 3,
453 parameter_name: "Section Size".into(),
454 data_type: DataType::UInt32,
455 access_type: AccessType::Const.into(),
456 default_value: Some((section.size as i64).into()),
457 ..Default::default()
458 },
459 SubDefinition {
460 sub_index: 4,
461 parameter_name: "Erase Command".into(),
462 data_type: DataType::UInt8,
463 access_type: AccessType::Wo.into(),
464 ..Default::default()
465 },
466 SubDefinition {
467 sub_index: 5,
468 parameter_name: "Data".into(),
469 data_type: DataType::Domain,
470 access_type: AccessType::Rw.into(),
471 ..Default::default()
472 },
473 ],
474 }),
475 });
476 }
477
478 objects
479}
480
481fn object_storage_objects(dev: &DeviceConfig) -> Vec<ObjectDefinition> {
482 if dev.support_storage {
483 vec![ObjectDefinition {
484 index: 0x1010,
485 parameter_name: "Object Save Command".to_string(),
486 application_callback: false,
487 object: Object::Array(ArrayDefinition {
488 data_type: DataType::UInt32,
489 access_type: AccessType::Rw.into(),
490 array_size: 1,
491 persist: false,
492 ..Default::default()
493 }),
494 }]
495 } else {
496 vec![]
497 }
498}
499
500fn default_num_rpdo() -> u8 {
501 4
502}
503fn default_num_tpdo() -> u8 {
504 4
505}
506fn default_true() -> bool {
507 true
508}
509
510#[derive(Deserialize, Debug, Clone, Copy)]
512pub struct PdoConfig {
513 #[serde(default = "default_num_rpdo")]
514 pub num_tpdo: u8,
516 #[serde(default = "default_num_tpdo")]
517 pub num_rpdo: u8,
519}
520
521impl Default for PdoConfig {
522 fn default() -> Self {
523 Self {
524 num_tpdo: default_num_tpdo(),
525 num_rpdo: default_num_rpdo(),
526 }
527 }
528}
529
530#[derive(Deserialize, Debug, Default, Clone, Copy)]
536#[serde(deny_unknown_fields)]
537pub struct IdentityConfig {
538 pub vendor_id: u32,
540 pub product_code: u32,
542 pub revision_number: u32,
544}
545
546#[derive(Deserialize, Debug, Default, Clone, Copy)]
548#[serde(rename_all = "lowercase")]
549pub enum PdoMapping {
550 #[default]
552 None,
553 Tpdo,
555 Rpdo,
557 Both,
559}
560
561impl PdoMapping {
562 pub fn supports_tpdo(&self) -> bool {
564 matches!(self, PdoMapping::Tpdo | PdoMapping::Both)
565 }
566
567 pub fn supports_rpdo(&self) -> bool {
569 matches!(self, PdoMapping::Rpdo | PdoMapping::Both)
570 }
571}
572
573#[derive(Clone, Debug, Deserialize)]
575pub struct BootloaderSection {
576 pub name: String,
578 pub size: u32,
580}
581
582#[derive(Clone, Deserialize, Debug, Default)]
584pub struct BootloaderConfig {
585 #[serde(default)]
588 pub application: bool,
589 #[serde(default)]
591 pub sections: Vec<BootloaderSection>,
592}
593
594#[derive(Deserialize, Debug, Clone)]
595#[serde(deny_unknown_fields)]
596pub struct DeviceConfig {
598 pub device_name: String,
600
601 #[serde(default = "default_true")]
605 pub support_storage: bool,
606
607 #[serde(default)]
609 pub hardware_version: String,
610 #[serde(default)]
612 pub software_version: String,
613
614 #[serde(default)]
616 pub heartbeat_period: u16,
617
618 pub identity: IdentityConfig,
620
621 #[serde(default)]
623 pub pdos: PdoConfig,
624
625 #[serde(default)]
627 pub bootloader: BootloaderConfig,
628
629 #[serde(default)]
631 pub objects: Vec<ObjectDefinition>,
632}
633
634#[derive(Deserialize, Debug, Default, Clone)]
636#[serde(deny_unknown_fields)]
637pub struct SubDefinition {
638 pub sub_index: u8,
640 #[serde(default)]
642 pub parameter_name: String,
643 #[serde(default)]
648 pub field_name: Option<String>,
649 pub data_type: DataType,
651 #[serde(default)]
653 pub access_type: AccessTypeDeser,
654 #[serde(default)]
656 pub default_value: Option<DefaultValue>,
657 #[serde(default)]
659 pub pdo_mapping: PdoMapping,
660 #[serde(default)]
662 pub persist: bool,
663}
664
665#[derive(Deserialize, Debug, Clone)]
667#[serde(untagged)]
668pub enum DefaultValue {
669 Integer(i64),
671 Float(f64),
673 String(String),
675}
676
677impl From<i64> for DefaultValue {
678 fn from(value: i64) -> Self {
679 Self::Integer(value)
680 }
681}
682
683impl From<i32> for DefaultValue {
684 fn from(value: i32) -> Self {
685 Self::Integer(value as i64)
686 }
687}
688
689impl From<usize> for DefaultValue {
690 fn from(value: usize) -> Self {
691 Self::Integer(value as i64)
692 }
693}
694
695impl From<f64> for DefaultValue {
696 fn from(value: f64) -> Self {
697 Self::Float(value)
698 }
699}
700
701impl From<&str> for DefaultValue {
702 fn from(value: &str) -> Self {
703 Self::String(value.to_string())
704 }
705}
706
707#[derive(Deserialize, Debug, Clone)]
709#[serde(tag = "object_type", rename_all = "lowercase")]
710pub enum Object {
711 Var(VarDefinition),
713 Array(ArrayDefinition),
715 Record(RecordDefinition),
717}
718
719#[derive(Default, Deserialize, Debug, Clone)]
721#[serde(deny_unknown_fields)]
722pub struct VarDefinition {
723 pub data_type: DataType,
725 pub access_type: AccessTypeDeser,
727 pub default_value: Option<DefaultValue>,
729 #[serde(default)]
731 pub pdo_mapping: PdoMapping,
732 #[serde(default)]
734 pub persist: bool,
735}
736
737#[derive(Default, Deserialize, Debug, Clone)]
739#[serde(deny_unknown_fields)]
740pub struct ArrayDefinition {
741 pub data_type: DataType,
743 pub access_type: AccessTypeDeser,
745 pub array_size: usize,
747 pub default_value: Option<Vec<DefaultValue>>,
749 #[serde(default)]
750 pub pdo_mapping: PdoMapping,
752 #[serde(default)]
753 pub persist: bool,
755}
756
757#[derive(Deserialize, Debug, Clone)]
759#[serde(deny_unknown_fields)]
760pub struct RecordDefinition {
761 #[serde(default)]
763 pub subs: Vec<SubDefinition>,
764}
765
766#[derive(Clone, Copy, Deserialize, Debug)]
770pub struct DomainDefinition {}
771
772#[derive(Deserialize, Debug, Clone)]
774pub struct ObjectDefinition {
775 pub index: u16,
777 #[serde(default)]
779 pub parameter_name: String,
780 #[serde(default)]
781 pub application_callback: bool,
784 #[serde(flatten)]
786 pub object: Object,
787}
788
789impl ObjectDefinition {
790 pub fn object_code(&self) -> ObjectCode {
792 match self.object {
793 Object::Var(_) => ObjectCode::Var,
794 Object::Array(_) => ObjectCode::Array,
795 Object::Record(_) => ObjectCode::Record,
796 }
797 }
798}
799
800impl DeviceConfig {
801 pub fn load(config_path: impl AsRef<std::path::Path>) -> Result<Self, LoadError> {
803 let config_str = std::fs::read_to_string(&config_path).context(IoSnafu)?;
804 Self::load_from_str(&config_str)
805 }
806
807 pub fn load_from_str(config_str: &str) -> Result<Self, LoadError> {
809 let mut config: DeviceConfig = toml::from_str(config_str).context(TomlParsingSnafu)?;
810
811 config.objects.extend(mandatory_objects(&config));
813 config
814 .objects
815 .extend(bootloader_objects(&config.bootloader));
816 config.objects.extend(pdo_objects(
817 config.pdos.num_rpdo as usize,
818 config.pdos.num_tpdo as usize,
819 ));
820 config.objects.extend(object_storage_objects(&config));
821
822 Self::validate_unique_indices(&config.objects)?;
823
824 Ok(config)
825 }
826
827 fn validate_unique_indices(objects: &[ObjectDefinition]) -> Result<(), LoadError> {
828 let mut found_indices = HashMap::new();
829 for obj in objects {
830 if found_indices.contains_key(&obj.index) {
831 return DuplicateObjectIdsSnafu { id: obj.index }.fail();
832 }
833 found_indices.insert(&obj.index, ());
834
835 if let Object::Record(record) = &obj.object {
836 let mut found_subs = HashMap::new();
837 for sub in &record.subs {
838 if found_subs.contains_key(&sub.sub_index) {
839 return DuplicateSubObjectsSnafu {
840 index: obj.index,
841 sub: sub.sub_index,
842 }
843 .fail();
844 }
845 found_subs.insert(&sub.sub_index, ());
846 }
847 }
848 }
849
850 Ok(())
851 }
852}
853
854#[derive(Clone, Copy, Debug, Default)]
856pub struct AccessTypeDeser(pub AccessType);
857impl<'de> serde::Deserialize<'de> for AccessTypeDeser {
858 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
859 where
860 D: serde::Deserializer<'de>,
861 {
862 let s = String::deserialize(deserializer)?;
863 match s.to_lowercase().as_str() {
864 "ro" => Ok(AccessTypeDeser(AccessType::Ro)),
865 "rw" => Ok(AccessTypeDeser(AccessType::Rw)),
866 "wo" => Ok(AccessTypeDeser(AccessType::Wo)),
867 "const" => Ok(AccessTypeDeser(AccessType::Const)),
868 _ => Err(D::Error::custom(format!(
869 "Invalid access type: {} (allowed: 'ro', 'rw', 'wo', or 'const')",
870 s
871 ))),
872 }
873 }
874}
875impl From<AccessType> for AccessTypeDeser {
876 fn from(access_type: AccessType) -> Self {
877 AccessTypeDeser(access_type)
878 }
879}
880
881#[derive(Clone, Copy, Debug, Default)]
885#[allow(missing_docs)]
886pub enum DataType {
887 Boolean,
888 Int8,
889 Int16,
890 Int32,
891 #[default]
892 UInt8,
893 UInt16,
894 UInt32,
895 Real32,
896 VisibleString(usize),
897 OctetString(usize),
898 UnicodeString(usize),
899 TimeOfDay,
900 TimeDifference,
901 Domain,
902}
903
904impl DataType {
905 pub fn is_str(&self) -> bool {
907 matches!(
908 self,
909 DataType::VisibleString(_) | DataType::OctetString(_) | DataType::UnicodeString(_)
910 )
911 }
912
913 pub fn size(&self) -> usize {
915 match self {
916 DataType::Boolean => 1,
917 DataType::Int8 => 1,
918 DataType::Int16 => 2,
919 DataType::Int32 => 4,
920 DataType::UInt8 => 1,
921 DataType::UInt16 => 2,
922 DataType::UInt32 => 4,
923 DataType::Real32 => 4,
924 DataType::VisibleString(size) => *size,
925 DataType::OctetString(size) => *size,
926 DataType::UnicodeString(size) => *size,
927 DataType::TimeOfDay => 4,
928 DataType::TimeDifference => 4,
929 DataType::Domain => 0, }
931 }
932}
933
934impl<'de> serde::Deserialize<'de> for DataType {
935 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
936 where
937 D: serde::Deserializer<'de>,
938 {
939 let re_visiblestring = regex::Regex::new(r"^visiblestring\((\d+)\)$").unwrap();
940 let re_octetstring = regex::Regex::new(r"^octetstring\((\d+)\)$").unwrap();
941 let re_unicodestring = regex::Regex::new(r"^unicodestring\((\d+)\)$").unwrap();
942
943 let s = String::deserialize(deserializer)?.to_lowercase();
944 if s == "boolean" {
945 Ok(DataType::Boolean)
946 } else if s == "int8" {
947 Ok(DataType::Int8)
948 } else if s == "int16" {
949 Ok(DataType::Int16)
950 } else if s == "int32" {
951 Ok(DataType::Int32)
952 } else if s == "uint8" {
953 Ok(DataType::UInt8)
954 } else if s == "uint16" {
955 Ok(DataType::UInt16)
956 } else if s == "uint32" {
957 Ok(DataType::UInt32)
958 } else if s == "real32" {
959 Ok(DataType::Real32)
960 } else if let Some(caps) = re_visiblestring.captures(&s) {
961 let size: usize = caps[1].parse().map_err(|_| {
962 D::Error::custom(format!("Invalid size for VisibleString: {}", &caps[1]))
963 })?;
964 Ok(DataType::VisibleString(size))
965 } else if let Some(caps) = re_octetstring.captures(&s) {
966 let size: usize = caps[1].parse().map_err(|_| {
967 D::Error::custom(format!("Invalid size for OctetString: {}", &caps[1]))
968 })?;
969 Ok(DataType::OctetString(size))
970 } else if let Some(caps) = re_unicodestring.captures(&s) {
971 let size: usize = caps[1].parse().map_err(|_| {
972 D::Error::custom(format!("Invalid size for UnicodeString: {}", &caps[1]))
973 })?;
974 Ok(DataType::UnicodeString(size))
975 } else if s == "timeofday" {
976 Ok(DataType::TimeOfDay)
977 } else if s == "timedifference" {
978 Ok(DataType::TimeDifference)
979 } else if s == "domain" {
980 Ok(DataType::Domain)
981 } else {
982 Err(D::Error::custom(format!("Invalid data type: {}", s)))
983 }
984 }
985}
986
987#[cfg(test)]
988mod tests {
989 use crate::device_config::{DeviceConfig, LoadError};
990 use assertables::assert_contains;
991 #[test]
992 fn test_duplicate_objects_errors() {
993 const TOML: &str = r#"
994 device_name = "test"
995 [identity]
996 vendor_id = 0
997 product_code = 1
998 revision_number = 2
999
1000 [[objects]]
1001 index = 0x2000
1002 parameter_name = "Test1"
1003 object_type = "var"
1004 data_type = "int16"
1005 access_type = "rw"
1006
1007 [[objects]]
1008 index = 0x2000
1009 parameter_name = "Duplicate"
1010 object_type = "record"
1011 "#;
1012
1013 let result = DeviceConfig::load_from_str(TOML);
1014
1015 assert!(result.is_err());
1016 let err = result.unwrap_err();
1017 assert!(matches!(err, LoadError::DuplicateObjectIds { id: 0x2000 }));
1018 assert_contains!(
1019 "Multiple definitions for object with index 0x2000",
1020 err.to_string().as_str()
1021 );
1022 }
1023
1024 #[test]
1025 fn test_duplicate_sub_object_errors() {
1026 const TOML: &str = r#"
1027 device_name = "test"
1028 [identity]
1029 vendor_id = 0
1030 product_code = 1
1031 revision_number = 2
1032
1033
1034 [[objects]]
1035 index = 0x2000
1036 parameter_name = "Duplicate"
1037 object_type = "record"
1038 [[objects.subs]]
1039 sub_index = 1
1040 parameter_name = "Test1"
1041 data_type = "int16"
1042 access_type = "rw"
1043 [[objects.subs]]
1044 sub_index = 1
1045 parameter_name = "RepeatedTest1"
1046 data_type = "int16"
1047 access_type = "rw"
1048 "#;
1049
1050 let result = DeviceConfig::load_from_str(TOML);
1051
1052 assert!(result.is_err());
1053 let err = result.unwrap_err();
1054 assert!(matches!(
1055 err,
1056 LoadError::DuplicateSubObjects {
1057 index: 0x2000,
1058 sub: 1
1059 }
1060 ));
1061 assert_contains!(
1062 "Multiple definitions of sub index 1 on object 0x2000",
1063 err.to_string().as_str()
1064 );
1065 }
1066}