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