1use std::collections::BTreeMap;
28use std::fmt;
29use std::fs;
30use std::io;
31use std::path::Path;
32
33use canopen_rs::datatypes::{DataType, Value};
34use canopen_rs::object_dictionary::{AccessType, Address, Entry, ObjectDictionary};
35
36#[derive(Debug)]
38pub enum EdsError {
39 Io(io::Error),
41 MissingField {
43 section: String,
45 field: &'static str,
47 },
48 UnknownAccessType(String),
50 InvalidValue {
52 section: String,
54 value: String,
56 },
57 TooManyObjects,
59}
60
61impl fmt::Display for EdsError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 EdsError::Io(e) => write!(f, "reading EDS file: {e}"),
65 EdsError::MissingField { section, field } => {
66 write!(f, "section [{section}] is missing required field `{field}`")
67 }
68 EdsError::UnknownAccessType(s) => write!(f, "unknown AccessType `{s}`"),
69 EdsError::InvalidValue { section, value } => {
70 write!(f, "section [{section}] has an invalid value `{value}`")
71 }
72 EdsError::TooManyObjects => f.write_str("more objects than dictionary capacity"),
73 }
74 }
75}
76
77impl std::error::Error for EdsError {
78 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79 match self {
80 EdsError::Io(e) => Some(e),
81 _ => None,
82 }
83 }
84}
85
86impl From<io::Error> for EdsError {
87 fn from(e: io::Error) -> Self {
88 EdsError::Io(e)
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct ObjectDescription {
95 pub address: Address,
97 pub parameter_name: String,
99 pub data_type: DataType,
101 pub access: AccessType,
103 pub default_value: Value,
105 pub pdo_mappable: bool,
107}
108
109impl ObjectDescription {
110 pub fn entry(&self) -> Entry {
112 Entry {
113 value: self.default_value,
114 access: self.access,
115 }
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct Eds {
122 pub vendor_name: Option<String>,
124 pub product_name: Option<String>,
126 pub node_id: Option<u8>,
129 pub objects: Vec<ObjectDescription>,
131}
132
133impl Eds {
134 pub fn parse(text: &str) -> Result<Eds, EdsError> {
136 let sections = parse_ini(text);
137
138 let device = sections.get("deviceinfo");
139 let vendor_name = device.and_then(|s| s.get("vendorname")).cloned();
140 let product_name = device.and_then(|s| s.get("productname")).cloned();
141
142 let node_id = sections
144 .get("devicecomissioning")
145 .or_else(|| sections.get("devicecommissioning"))
146 .and_then(|s| s.get("nodeid"))
147 .and_then(|v| eval_int_expr(v, 0))
148 .and_then(|n| u8::try_from(n).ok());
149 let node_for_expr = node_id.unwrap_or(0);
150
151 let mut objects = Vec::new();
152 for (name, fields) in §ions {
153 let Some(address) = parse_object_section(name) else {
154 continue;
155 };
156 let Some(dt_raw) = fields.get("datatype") else {
160 continue;
161 };
162 let Some(dt_index) = eval_int_expr(dt_raw, node_for_expr) else {
163 return Err(EdsError::InvalidValue {
164 section: name.clone(),
165 value: dt_raw.clone(),
166 });
167 };
168 let Some(data_type) = u16::try_from(dt_index).ok().and_then(DataType::from_index)
171 else {
172 continue;
173 };
174
175 let access = match fields.get("accesstype") {
176 Some(a) => access_from_str(a)?,
177 None => AccessType::Ro,
178 };
179
180 let raw_value = fields
181 .get("parametervalue")
182 .or_else(|| fields.get("defaultvalue"))
183 .map(String::as_str)
184 .unwrap_or("");
185 let default_value =
186 value_from_str(data_type, raw_value, node_for_expr).ok_or_else(|| {
187 EdsError::InvalidValue {
188 section: name.clone(),
189 value: raw_value.to_string(),
190 }
191 })?;
192
193 objects.push(ObjectDescription {
194 address,
195 parameter_name: fields.get("parametername").cloned().unwrap_or_default(),
196 data_type,
197 access,
198 default_value,
199 pdo_mappable: fields
200 .get("pdomapping")
201 .map(|v| v.trim() != "0")
202 .unwrap_or(false),
203 });
204 }
205
206 objects.sort_by_key(|o| o.address);
207 Ok(Eds {
208 vendor_name,
209 product_name,
210 node_id,
211 objects,
212 })
213 }
214
215 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Eds, EdsError> {
217 Self::parse(&fs::read_to_string(path)?)
218 }
219
220 pub fn get(&self, address: Address) -> Option<&ObjectDescription> {
222 self.objects.iter().find(|o| o.address == address)
223 }
224
225 pub fn object_dictionary<const N: usize>(&self) -> Result<ObjectDictionary<N>, EdsError> {
230 let mut od = ObjectDictionary::new();
231 for obj in &self.objects {
232 od.insert(obj.address, obj.entry())
233 .map_err(|_| EdsError::TooManyObjects)?;
234 }
235 Ok(od)
236 }
237}
238
239fn parse_ini(text: &str) -> BTreeMap<String, BTreeMap<String, String>> {
244 let mut sections = BTreeMap::new();
245 let mut current: Option<String> = None;
246 for line in text.lines() {
247 let line = line.trim();
248 if line.is_empty() || line.starts_with(';') {
249 continue;
250 }
251 if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
252 let name = inner.trim().to_ascii_lowercase();
253 sections.entry(name.clone()).or_insert_with(BTreeMap::new);
254 current = Some(name);
255 } else if let (Some(section), Some((key, value))) = (¤t, line.split_once('=')) {
256 sections
257 .get_mut(section)
258 .expect("current section was inserted")
259 .insert(key.trim().to_ascii_lowercase(), value.trim().to_string());
260 }
261 }
262 sections
263}
264
265fn parse_object_section(name: &str) -> Option<Address> {
268 if let Some((index, subindex)) = name.split_once("sub") {
269 Some(Address::new(
270 u16::from_str_radix(index, 16).ok()?,
271 u8::from_str_radix(subindex, 16).ok()?,
272 ))
273 } else {
274 Some(Address::new(u16::from_str_radix(name, 16).ok()?, 0))
275 }
276}
277
278fn access_from_str(s: &str) -> Result<AccessType, EdsError> {
279 match s.trim().to_ascii_lowercase().as_str() {
280 "ro" => Ok(AccessType::Ro),
281 "wo" => Ok(AccessType::Wo),
282 "rw" | "rwr" | "rww" => Ok(AccessType::Rw),
283 "const" => Ok(AccessType::Const),
284 other => Err(EdsError::UnknownAccessType(other.to_string())),
285 }
286}
287
288fn eval_int_expr(s: &str, node_id: u8) -> Option<i128> {
291 let mut sum: i128 = 0;
292 for term in s.split('+') {
293 let term = term.trim();
294 if term.is_empty() {
295 continue;
296 } else if term.eq_ignore_ascii_case("$nodeid") {
297 sum += node_id as i128;
298 } else if let Some(hex) = term.strip_prefix("0x").or_else(|| term.strip_prefix("0X")) {
299 sum += i128::from_str_radix(hex, 16).ok()?;
300 } else {
301 sum += term.parse::<i128>().ok()?;
302 }
303 }
304 Some(sum)
305}
306
307fn value_from_str(data_type: DataType, s: &str, node_id: u8) -> Option<Value> {
309 let s = s.trim();
310 match data_type {
312 DataType::Real32 => {
313 return Some(Value::Real32(if s.is_empty() {
314 0.0
315 } else {
316 s.parse().ok()?
317 }))
318 }
319 DataType::Real64 => {
320 return Some(Value::Real64(if s.is_empty() {
321 0.0
322 } else {
323 s.parse().ok()?
324 }))
325 }
326 _ => {}
327 }
328 let n = if s.is_empty() {
330 0
331 } else {
332 eval_int_expr(s, node_id)?
333 };
334 Some(match data_type {
335 DataType::Boolean => Value::Boolean(n != 0),
336 DataType::Integer8 => Value::Integer8(n as i8),
337 DataType::Integer16 => Value::Integer16(n as i16),
338 DataType::Integer32 => Value::Integer32(n as i32),
339 DataType::Unsigned8 => Value::Unsigned8(n as u8),
340 DataType::Unsigned16 => Value::Unsigned16(n as u16),
341 DataType::Unsigned32 => Value::Unsigned32(n as u32),
342 DataType::Integer64 => Value::Integer64(n as i64),
343 DataType::Unsigned64 => Value::Unsigned64(n as u64),
344 _ => return None,
346 })
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 const SAMPLE: &str = "\
357[DeviceInfo]
358VendorName=Acme Robotics
359ProductName=Widget 3000
360
361[DeviceComissioning]
362NodeID=0x10
363
364[MandatoryObjects]
365SupportedObjects=2
3661=0x1000
3672=0x1018
368
369[1000]
370ParameterName=Device type
371ObjectType=0x7
372DataType=0x0007
373AccessType=ro
374DefaultValue=0x00040192
375PDOMapping=0
376
377[1008]
378ParameterName=Device name
379ObjectType=0x7
380DataType=0x0009
381AccessType=const
382DefaultValue=Widget
383
384[1017]
385ParameterName=Producer heartbeat time
386ObjectType=0x7
387DataType=0x0006
388AccessType=rw
389DefaultValue=1000
390PDOMapping=1
391
392[1018]
393ParameterName=Identity object
394ObjectType=0x9
395SubNumber=2
396
397[1018sub0]
398ParameterName=Highest sub-index supported
399DataType=0x0005
400AccessType=const
401DefaultValue=1
402
403[1018sub1]
404ParameterName=Vendor-ID
405DataType=0x0007
406AccessType=ro
407DefaultValue=0x000000AB
408
409[1400sub1]
410ParameterName=COB-ID used by RPDO
411DataType=0x0007
412AccessType=rw
413DefaultValue=$NODEID+0x200
414
415[6000]
416ParameterName=Scale factor
417ObjectType=0x7
418DataType=0x0008
419AccessType=rw
420DefaultValue=1.5
421";
422
423 fn sample() -> Eds {
424 Eds::parse(SAMPLE).unwrap()
425 }
426
427 #[test]
428 fn reads_device_metadata_and_node_id() {
429 let eds = sample();
430 assert_eq!(eds.vendor_name.as_deref(), Some("Acme Robotics"));
431 assert_eq!(eds.product_name.as_deref(), Some("Widget 3000"));
432 assert_eq!(eds.node_id, Some(0x10));
433 }
434
435 #[test]
436 fn parses_var_with_hex_default() {
437 let obj = sample().get(Address::new(0x1000, 0)).unwrap().clone();
438 assert_eq!(obj.parameter_name, "Device type");
439 assert_eq!(obj.data_type, DataType::Unsigned32);
440 assert_eq!(obj.access, AccessType::Ro);
441 assert_eq!(obj.default_value, Value::Unsigned32(0x0004_0192));
442 }
443
444 #[test]
445 fn parses_record_subindices() {
446 let eds = sample();
447 assert_eq!(
448 eds.get(Address::new(0x1018, 0)).unwrap().default_value,
449 Value::Unsigned8(1)
450 );
451 assert_eq!(
452 eds.get(Address::new(0x1018, 1)).unwrap().default_value,
453 Value::Unsigned32(0x0000_00AB)
454 );
455 }
456
457 #[test]
458 fn resolves_nodeid_expression() {
459 let obj = sample().get(Address::new(0x1400, 1)).unwrap().clone();
461 assert_eq!(obj.default_value, Value::Unsigned32(0x210));
462 }
463
464 #[test]
465 fn parses_float_and_pdo_flag() {
466 let eds = sample();
467 assert_eq!(
468 eds.get(Address::new(0x6000, 0)).unwrap().default_value,
469 Value::Real32(1.5)
470 );
471 assert!(eds.get(Address::new(0x1017, 0)).unwrap().pdo_mappable);
472 assert!(!eds.get(Address::new(0x1000, 0)).unwrap().pdo_mappable);
473 }
474
475 #[test]
476 fn skips_unmodelled_string_object() {
477 assert!(sample().get(Address::new(0x1008, 0)).is_none());
479 }
480
481 #[test]
482 fn builds_object_dictionary() {
483 let eds = sample();
484 let od = eds.object_dictionary::<16>().unwrap();
485 assert_eq!(
486 od.read(Address::new(0x1000, 0)).unwrap(),
487 Value::Unsigned32(0x0004_0192)
488 );
489 assert_eq!(
490 od.read(Address::new(0x1017, 0)).unwrap(),
491 Value::Unsigned16(1000)
492 );
493 }
494
495 #[test]
496 fn object_dictionary_capacity_is_enforced() {
497 assert!(matches!(
499 sample().object_dictionary::<2>(),
500 Err(EdsError::TooManyObjects)
501 ));
502 }
503
504 #[test]
505 fn unknown_access_type_errors() {
506 let text = "[2000]\nDataType=0x0005\nAccessType=bogus\n";
507 assert!(matches!(
508 Eds::parse(text),
509 Err(EdsError::UnknownAccessType(_))
510 ));
511 }
512}