1use 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#[derive(Debug, Snafu)]
150pub enum LoadError {
151 #[snafu(display("IO error: {source}"))]
153 Io {
154 source: std::io::Error,
156 },
157 #[snafu(display("Toml parse error: {source}"))]
159 TomlParsing {
160 source: toml::de::Error,
162 },
163 #[snafu(display("Multiple definitions for object with index 0x{id:x}"))]
165 DuplicateObjectIds {
166 id: u16,
168 },
169 #[snafu(display("Multiple definitions of sub index {sub} on object 0x{index:x}"))]
171 DuplicateSubObjects {
172 index: u16,
174 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#[derive(Clone, Copy, Debug, Default, Deserialize)]
539#[serde(rename_all = "lowercase")]
540pub enum AutoStartConfig {
541 #[default]
543 Disabled,
544 Enabled,
546 Unsupported,
548}
549
550#[derive(Clone, Debug, Deserialize, PartialEq)]
552pub struct PdoDefaultConfig {
553 pub cob_id: u32,
555 #[serde(default)]
557 pub extended: bool,
558 pub add_node_id: bool,
560 pub enabled: bool,
562 #[serde(default)]
564 pub rtr_disabled: bool,
565 pub mappings: Vec<PdoMapping>,
567 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#[derive(Debug, Deserialize)]
589struct DevicePdoConfigSerializer {
590 #[serde(default = "default_num_rpdo")]
591 pub num_tpdo: u8,
593 #[serde(default = "default_num_tpdo")]
594 pub num_rpdo: u8,
596
597 #[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#[derive(Clone, Debug, Deserialize)]
619#[serde(try_from = "DevicePdoConfigSerializer")]
620pub struct DevicePdoConfig {
621 pub num_tpdo: u8,
623 pub num_rpdo: u8,
625
626 pub tpdo_defaults: HashMap<usize, PdoDefaultConfig>,
628 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#[derive(Deserialize, Debug, Default, Clone, Copy)]
649#[serde(deny_unknown_fields)]
650pub struct IdentityConfig {
651 pub vendor_id: u32,
653 pub product_code: u32,
655 pub revision_number: u32,
657}
658
659#[derive(Clone, Debug, Deserialize)]
661pub struct BootloaderSection {
662 pub name: String,
664 pub size: u32,
666}
667
668#[derive(Clone, Deserialize, Debug, Default)]
670pub struct BootloaderConfig {
671 #[serde(default)]
674 pub application: bool,
675 #[serde(default)]
677 pub sections: Vec<BootloaderSection>,
678}
679
680#[derive(Deserialize, Debug, Clone)]
681#[serde(deny_unknown_fields)]
682pub struct DeviceConfig {
684 pub device_name: String,
686
687 #[serde(default)]
694 pub autostart: AutoStartConfig,
695
696 #[serde(default = "default_true")]
700 pub support_storage: bool,
701
702 #[serde(default)]
704 pub hardware_version: String,
705 #[serde(default)]
707 pub software_version: String,
708
709 #[serde(default)]
711 pub heartbeat_period: u16,
712
713 pub identity: IdentityConfig,
715
716 #[serde(default)]
718 pub pdos: DevicePdoConfig,
719
720 #[serde(default)]
722 pub bootloader: BootloaderConfig,
723
724 #[serde(default)]
726 pub objects: Vec<ObjectDefinition>,
727}
728
729#[derive(Deserialize, Debug, Default, Clone)]
731#[serde(deny_unknown_fields)]
732pub struct SubDefinition {
733 pub sub_index: u8,
735 #[serde(default)]
737 pub parameter_name: String,
738 #[serde(default)]
743 pub field_name: Option<String>,
744 pub data_type: DataType,
746 #[serde(default)]
748 pub access_type: AccessTypeDeser,
749 #[serde(default)]
751 pub default_value: Option<DefaultValue>,
752 #[serde(default)]
754 pub pdo_mapping: PdoMappable,
755 #[serde(default)]
757 pub persist: bool,
758}
759
760#[derive(Deserialize, Debug, Clone)]
762#[serde(untagged)]
763pub enum DefaultValue {
764 Integer(i64),
766 Float(f64),
768 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#[derive(Deserialize, Debug, Clone)]
804#[serde(tag = "object_type", rename_all = "lowercase")]
805pub enum Object {
806 Var(VarDefinition),
808 Array(ArrayDefinition),
810 Record(RecordDefinition),
812}
813
814#[derive(Default, Deserialize, Debug, Clone)]
816#[serde(deny_unknown_fields)]
817pub struct VarDefinition {
818 pub data_type: DataType,
820 pub access_type: AccessTypeDeser,
822 pub default_value: Option<DefaultValue>,
824 #[serde(default)]
826 pub pdo_mapping: PdoMappable,
827 #[serde(default)]
829 pub persist: bool,
830}
831
832#[derive(Default, Deserialize, Debug, Clone)]
834#[serde(deny_unknown_fields)]
835pub struct ArrayDefinition {
836 pub data_type: DataType,
838 pub access_type: AccessTypeDeser,
840 pub array_size: usize,
842 pub default_value: Option<Vec<DefaultValue>>,
844 #[serde(default)]
845 pub pdo_mapping: PdoMappable,
847 #[serde(default)]
848 pub persist: bool,
850}
851
852#[derive(Deserialize, Debug, Clone)]
854#[serde(deny_unknown_fields)]
855pub struct RecordDefinition {
856 #[serde(default)]
858 pub subs: Vec<SubDefinition>,
859}
860
861#[derive(Clone, Copy, Deserialize, Debug)]
865pub struct DomainDefinition {}
866
867#[derive(Deserialize, Debug, Clone)]
869pub struct ObjectDefinition {
870 pub index: u16,
872 #[serde(default)]
874 pub parameter_name: String,
875 #[serde(default)]
876 pub application_callback: bool,
879 #[serde(flatten)]
881 pub object: Object,
882}
883
884impl ObjectDefinition {
885 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 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 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 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#[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#[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 pub fn is_str(&self) -> bool {
1005 matches!(
1006 self,
1007 DataType::VisibleString(_) | DataType::OctetString(_) | DataType::UnicodeString(_)
1008 )
1009 }
1010
1011 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, }
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}