Skip to content

Commit 131f43e

Browse files
eds: fix LowLimit/HighLimit parsing for all signed integer types
- Replace incomplete _calc_bit_length() (only handled 4 of 8 signed types) with a lookup dict covering all SIGNED_TYPES (INTEGER8/16/24/32/40/48/56/64) - Log a warning instead of silently ignoring invalid limit values - Add EDS test entries for INTEGER24/40/48/56 with hex-encoded negative limits - Extend test_record_with_limits and add test_invalid_limit_logs_warning Note: eds.py and test_eds.py were formatted with black. The formatting changes can be dropped if a pure functional change is preferred.
1 parent c05b49d commit 131f43e

3 files changed

Lines changed: 237 additions & 120 deletions

File tree

canopen/objectdictionary/eds.py

Lines changed: 86 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222

2323
logger = logging.getLogger(__name__)
2424

25+
2526
def import_eds(source, node_id):
26-
eds = RawConfigParser(inline_comment_prefixes=(';',))
27+
eds = RawConfigParser(inline_comment_prefixes=(";",))
2728
eds.optionxform = str
2829
opened_here = False
2930
try:
@@ -42,25 +43,26 @@ def import_eds(source, node_id):
4243

4344
if eds.has_section("FileInfo"):
4445
od.__edsFileInfo = {
45-
opt: eds.get("FileInfo", opt)
46-
for opt in eds.options("FileInfo")
46+
opt: eds.get("FileInfo", opt) for opt in eds.options("FileInfo")
4747
}
4848

4949
if eds.has_section("Comments"):
5050
linecount = int(eds.get("Comments", "Lines"), 0)
51-
od.comments = '\n'.join([
52-
eds.get("Comments", f"Line{line}")
53-
for line in range(1, linecount+1)
54-
])
51+
od.comments = "\n".join(
52+
[eds.get("Comments", f"Line{line}") for line in range(1, linecount + 1)]
53+
)
5554

5655
if not eds.has_section("DeviceInfo"):
57-
logger.warn("eds file does not have a DeviceInfo section. This section is mandatory")
56+
logger.warn(
57+
"eds file does not have a DeviceInfo section. This section is mandatory"
58+
)
5859
else:
5960
for rate in [10, 20, 50, 125, 250, 500, 800, 1000]:
6061
baudPossible = int(
61-
eds.get("DeviceInfo", f"BaudRate_{rate}", fallback='0'), 0)
62+
eds.get("DeviceInfo", f"BaudRate_{rate}", fallback="0"), 0
63+
)
6264
if baudPossible != 0:
63-
od.device_information.allowed_baudrates.add(rate*1000)
65+
od.device_information.allowed_baudrates.add(rate * 1000)
6466

6567
for t, eprop, odprop in [
6668
(str, "VendorName", "vendor_name"),
@@ -80,13 +82,13 @@ def import_eds(source, node_id):
8082
]:
8183
try:
8284
if t in (int, bool):
83-
setattr(od.device_information, odprop,
84-
t(int(eds.get("DeviceInfo", eprop), 0))
85-
)
85+
setattr(
86+
od.device_information,
87+
odprop,
88+
t(int(eds.get("DeviceInfo", eprop), 0)),
89+
)
8690
elif t is str:
87-
setattr(od.device_information, odprop,
88-
eds.get("DeviceInfo", eprop)
89-
)
91+
setattr(od.device_information, odprop, eds.get("DeviceInfo", eprop))
9092
except NoOptionError:
9193
pass
9294

@@ -131,10 +133,11 @@ def import_eds(source, node_id):
131133
if object_type in (objectcodes.VAR, objectcodes.DOMAIN):
132134
var = build_variable(eds, section, node_id, index)
133135
od.add_object(var)
134-
elif object_type == objectcodes.ARRAY and eds.has_option(section, "CompactSubObj"):
136+
elif object_type == objectcodes.ARRAY and eds.has_option(
137+
section, "CompactSubObj"
138+
):
135139
arr = ODArray(name, index)
136-
last_subindex = ODVariable(
137-
"Number of entries", index, 0)
140+
last_subindex = ODVariable("Number of entries", index, 0)
138141
last_subindex.data_type = datatypes.UNSIGNED8
139142
arr.add_member(last_subindex)
140143
arr.add_member(build_variable(eds, section, node_id, index, 1))
@@ -178,7 +181,7 @@ def import_eds(source, node_id):
178181

179182

180183
def import_from_node(node_id: int, network: canopen.network.Network):
181-
""" Download the configuration from the remote node
184+
"""Download the configuration from the remote node
182185
:param int node_id: Identifier of the node
183186
:param network: network object
184187
"""
@@ -192,25 +195,23 @@ def import_from_node(node_id: int, network: canopen.network.Network):
192195
with sdo_client.open(0x1021, 0, "rt") as eds_fp:
193196
od = import_eds(eds_fp, node_id)
194197
except Exception as e:
195-
logger.error("No object dictionary could be loaded for node %d: %s",
196-
node_id, e)
198+
logger.error("No object dictionary could be loaded for node %d: %s", node_id, e)
197199
od = None
198200
finally:
199201
network.unsubscribe(0x580 + node_id)
200202
return od
201203

202204

203-
def _calc_bit_length(data_type):
204-
if data_type == datatypes.INTEGER8:
205-
return 8
206-
elif data_type == datatypes.INTEGER16:
207-
return 16
208-
elif data_type == datatypes.INTEGER32:
209-
return 32
210-
elif data_type == datatypes.INTEGER64:
211-
return 64
212-
else:
213-
raise ValueError(f"Invalid data_type '{data_type}', expecting a signed integer data_type.")
205+
_SIGNED_BIT_LENGTHS = {
206+
datatypes.INTEGER8: 8,
207+
datatypes.INTEGER16: 16,
208+
datatypes.INTEGER24: 24,
209+
datatypes.INTEGER32: 32,
210+
datatypes.INTEGER40: 40,
211+
datatypes.INTEGER48: 48,
212+
datatypes.INTEGER56: 56,
213+
datatypes.INTEGER64: 64,
214+
}
214215

215216

216217
def _signed_int_from_hex(hex_str, bit_length):
@@ -233,8 +234,8 @@ def _convert_variable(node_id, var_type, value):
233234
else:
234235
# COB-ID can contain '$NODEID+' so replace this with node_id before converting
235236
value = value.replace(" ", "").upper()
236-
if '$NODEID' in value and node_id is not None:
237-
return int(re.sub(r'\+?\$NODEID\+?', '', value), 0) + node_id
237+
if "$NODEID" in value and node_id is not None:
238+
return int(re.sub(r"\+?\$NODEID\+?", "", value), 0) + node_id
238239
else:
239240
return int(value, 0)
240241

@@ -276,7 +277,9 @@ def build_variable(eds, section, node_id, index, subindex=0):
276277
try:
277278
var.data_type = int(eds.get(f"{var.data_type:X}sub1", "DefaultValue"), 0)
278279
except NoSectionError:
279-
logger.warning("%s has an unknown or unsupported data type (0x%X)", name, var.data_type)
280+
logger.warning(
281+
"%s has an unknown or unsupported data type (0x%X)", name, var.data_type
282+
)
280283
# Assume DOMAIN to force application to interpret the byte data
281284
var.data_type = datatypes.DOMAIN
282285

@@ -286,32 +289,40 @@ def build_variable(eds, section, node_id, index, subindex=0):
286289
try:
287290
min_string = eds.get(section, "LowLimit")
288291
if var.data_type in datatypes.SIGNED_TYPES:
289-
var.min = _signed_int_from_hex(min_string, _calc_bit_length(var.data_type))
292+
var.min = _signed_int_from_hex(
293+
min_string, _SIGNED_BIT_LENGTHS[var.data_type]
294+
)
290295
else:
291296
var.min = int(min_string, 0)
292297
except ValueError:
293-
pass
298+
logger.warning("Failed to parse LowLimit for %s: %r", name, min_string)
294299
if eds.has_option(section, "HighLimit"):
295300
try:
296301
max_string = eds.get(section, "HighLimit")
297302
if var.data_type in datatypes.SIGNED_TYPES:
298-
var.max = _signed_int_from_hex(max_string, _calc_bit_length(var.data_type))
303+
var.max = _signed_int_from_hex(
304+
max_string, _SIGNED_BIT_LENGTHS[var.data_type]
305+
)
299306
else:
300307
var.max = int(max_string, 0)
301308
except ValueError:
302-
pass
309+
logger.warning("Failed to parse HighLimit for %s: %r", name, max_string)
303310
if eds.has_option(section, "DefaultValue"):
304311
try:
305312
var.default_raw = eds.get(section, "DefaultValue")
306-
if '$NODEID' in var.default_raw:
313+
if "$NODEID" in var.default_raw:
307314
var.relative = True
308-
var.default = _convert_variable(node_id, var.data_type, eds.get(section, "DefaultValue"))
315+
var.default = _convert_variable(
316+
node_id, var.data_type, eds.get(section, "DefaultValue")
317+
)
309318
except ValueError:
310319
pass
311320
if eds.has_option(section, "ParameterValue"):
312321
try:
313322
var.value_raw = eds.get(section, "ParameterValue")
314-
var.value = _convert_variable(node_id, var.data_type, eds.get(section, "ParameterValue"))
323+
var.value = _convert_variable(
324+
node_id, var.data_type, eds.get(section, "ParameterValue")
325+
)
315326
except ValueError:
316327
pass
317328
# Factor, Description and Unit are not standard according to the CANopen specifications, but they are implemented in the python canopen package, so we can at least try to use them
@@ -376,32 +387,36 @@ def export_variable(var, eds):
376387
if var.access_type:
377388
eds.set(section, "AccessType", var.access_type)
378389

379-
if getattr(var, 'default_raw', None) is not None:
390+
if getattr(var, "default_raw", None) is not None:
380391
eds.set(section, "DefaultValue", var.default_raw)
381-
elif getattr(var, 'default', None) is not None:
382-
eds.set(section, "DefaultValue", _revert_variable(
383-
var.data_type, var.default))
392+
elif getattr(var, "default", None) is not None:
393+
eds.set(
394+
section, "DefaultValue", _revert_variable(var.data_type, var.default)
395+
)
384396

385397
if device_commisioning:
386-
if getattr(var, 'value_raw', None) is not None:
398+
if getattr(var, "value_raw", None) is not None:
387399
eds.set(section, "ParameterValue", var.value_raw)
388-
elif getattr(var, 'value', None) is not None:
389-
eds.set(section, "ParameterValue",
390-
_revert_variable(var.data_type, var.value))
400+
elif getattr(var, "value", None) is not None:
401+
eds.set(
402+
section,
403+
"ParameterValue",
404+
_revert_variable(var.data_type, var.value),
405+
)
391406

392407
eds.set(section, "DataType", f"0x{var.data_type:04X}")
393408
eds.set(section, "PDOMapping", hex(var.pdo_mappable))
394409

395-
if getattr(var, 'min', None) is not None:
410+
if getattr(var, "min", None) is not None:
396411
eds.set(section, "LowLimit", var.min)
397-
if getattr(var, 'max', None) is not None:
412+
if getattr(var, "max", None) is not None:
398413
eds.set(section, "HighLimit", var.max)
399414

400-
if getattr(var, 'description', '') != '':
415+
if getattr(var, "description", "") != "":
401416
eds.set(section, "Description", var.description)
402-
if getattr(var, 'factor', 1) != 1:
417+
if getattr(var, "factor", 1) != 1:
403418
eds.set(section, "Factor", var.factor)
404-
if getattr(var, 'unit', '') != '':
419+
if getattr(var, "unit", "") != "":
405420
eds.set(section, "Unit", var.unit)
406421

407422
def export_record(var, eds):
@@ -420,6 +435,7 @@ def export_record(var, eds):
420435
eds.optionxform = str
421436

422437
from datetime import datetime as dt
438+
423439
defmtime = dt.utcnow()
424440

425441
try:
@@ -469,10 +485,13 @@ def export_record(var, eds):
469485

470486
# we are also adding out of spec baudrates here.
471487
for rate in od.device_information.allowed_baudrates.union(
472-
{10e3, 20e3, 50e3, 125e3, 250e3, 500e3, 800e3, 1000e3}):
488+
{10e3, 20e3, 50e3, 125e3, 250e3, 500e3, 800e3, 1000e3}
489+
):
473490
eds.set(
474-
"DeviceInfo", f"BaudRate_{int(rate//1000)}",
475-
int(rate in od.device_information.allowed_baudrates))
491+
"DeviceInfo",
492+
f"BaudRate_{int(rate//1000)}",
493+
int(rate in od.device_information.allowed_baudrates),
494+
)
476495

477496
if device_commisioning and (od.bitrate or od.node_id):
478497
eds.add_section("DeviceComissioning")
@@ -500,11 +519,13 @@ def manufacturer_indices(x):
500519
return 0x2000 <= x < 0x6000
501520

502521
def optional_indices(x):
503-
return all((
504-
x > 0x1001,
505-
not mandatory_indices(x),
506-
not manufacturer_indices(x),
507-
))
522+
return all(
523+
(
524+
x > 0x1001,
525+
not mandatory_indices(x),
526+
not manufacturer_indices(x),
527+
)
528+
)
508529

509530
supported_mantatory_indices = list(filter(mandatory_indices, od))
510531
supported_optional_indices = list(filter(optional_indices, od))
@@ -524,6 +545,7 @@ def add_list(section, list):
524545

525546
if not dest:
526547
import sys
548+
527549
dest = sys.stdout
528550

529551
eds.write(dest, False)

test/sample.eds

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,3 +1017,39 @@ PDOMapping=0x0
10171017
Factor=ERROR
10181018
Description=
10191019
Unit=
1020+
1021+
[3031]
1022+
ParameterName=INTEGER24 value range -1 to 0
1023+
ObjectType=0x7
1024+
DataType=0x10
1025+
AccessType=rw
1026+
HighLimit=0x000000
1027+
LowLimit=0xFFFFFF
1028+
PDOMapping=0
1029+
1030+
[3032]
1031+
ParameterName=INTEGER40 value range -1 to 0
1032+
ObjectType=0x7
1033+
DataType=0x12
1034+
AccessType=rw
1035+
HighLimit=0x0000000000
1036+
LowLimit=0xFFFFFFFFFF
1037+
PDOMapping=0
1038+
1039+
[3033]
1040+
ParameterName=INTEGER48 value range -1 to 0
1041+
ObjectType=0x7
1042+
DataType=0x13
1043+
AccessType=rw
1044+
HighLimit=0x000000000000
1045+
LowLimit=0xFFFFFFFFFFFF
1046+
PDOMapping=0
1047+
1048+
[3034]
1049+
ParameterName=INTEGER56 value range -1 to 0
1050+
ObjectType=0x7
1051+
DataType=0x14
1052+
AccessType=rw
1053+
HighLimit=0x00000000000000
1054+
LowLimit=0xFFFFFFFFFFFFFF
1055+
PDOMapping=0

0 commit comments

Comments
 (0)