Skip to content

Commit 4ed45dd

Browse files
add back and add more exception handling via warning instead of silent pass
1 parent f65a1fb commit 4ed45dd

2 files changed

Lines changed: 76 additions & 48 deletions

File tree

canopen/objectdictionary/eds.py

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -204,26 +204,28 @@ def import_from_node(node_id: int, network: canopen.network.Network):
204204
return od
205205

206206

207-
_SIGNED_BIT_LENGTHS = {
208-
datatypes.INTEGER8: 8,
209-
datatypes.INTEGER16: 16,
210-
datatypes.INTEGER24: 24,
211-
datatypes.INTEGER32: 32,
212-
datatypes.INTEGER40: 40,
213-
datatypes.INTEGER48: 48,
214-
datatypes.INTEGER56: 56,
215-
datatypes.INTEGER64: 64,
216-
}
207+
def _calc_bit_length(data_type: int) -> int:
208+
if data_type in datatypes.SIGNED_TYPES:
209+
st = ODVariable.STRUCT_TYPES[data_type]
210+
if isinstance(st, datatypes.IntegerN):
211+
return st.width
212+
return st.size * 8
213+
else:
214+
raise ValueError(
215+
f"Invalid data_type '{data_type}', expecting a signed integer data_type."
216+
)
217217

218218

219219
def _signed_int_from_hex(hex_str, bit_length):
220220
number = int(hex_str, 0)
221+
if number < 0 or number >= (1 << bit_length):
222+
raise ValueError(
223+
f"Value {hex_str!r} is out of range for a {bit_length}-bit signed integer"
224+
)
221225
max_value = (1 << (bit_length - 1)) - 1
222-
223226
if number > max_value:
224227
return number - (1 << bit_length)
225-
else:
226-
return number
228+
return number
227229

228230

229231
def _convert_variable(node_id, var_type, value):
@@ -242,6 +244,18 @@ def _convert_variable(node_id, var_type, value):
242244
return int(value, 0)
243245

244246

247+
def _int_to_hex(data_type: int, value: int) -> str:
248+
"""Format an integer as EDS hex string.
249+
250+
Signed types with a negative value are written as two's-complement hex
251+
(e.g. INTEGER8 -1 → 0xFF) so the output is a valid EDS literal.
252+
"""
253+
if data_type in datatypes.SIGNED_TYPES and value < 0:
254+
bit_length = _calc_bit_length(data_type)
255+
return f"0x{value + (1 << bit_length):0{bit_length // 4}X}"
256+
return f"0x{value:02X}"
257+
258+
245259
def _revert_variable(var_type, value):
246260
if value is None:
247261
return None
@@ -252,7 +266,7 @@ def _revert_variable(var_type, value):
252266
elif var_type in datatypes.FLOAT_TYPES:
253267
return value
254268
else:
255-
return f"0x{value:02X}"
269+
return _int_to_hex(var_type, value)
256270

257271

258272
def build_variable(
@@ -302,51 +316,60 @@ def build_variable(
302316
try:
303317
min_string = eds.get(section, "LowLimit")
304318
if var.data_type in datatypes.SIGNED_TYPES:
305-
var.min = _signed_int_from_hex(min_string, _SIGNED_BIT_LENGTHS[var.data_type])
319+
var.min = _signed_int_from_hex(min_string, _calc_bit_length(var.data_type))
306320
else:
307321
var.min = int(min_string, 0)
308322
except ValueError:
309-
pass
323+
logger.warning(
324+
"Invalid LowLimit %r for %s (0x%X), ignoring",
325+
eds.get(section, "LowLimit"), var.name, var.index,
326+
)
310327
if eds.has_option(section, "HighLimit"):
311328
try:
312329
max_string = eds.get(section, "HighLimit")
313330
if var.data_type in datatypes.SIGNED_TYPES:
314-
var.max = _signed_int_from_hex(max_string, _SIGNED_BIT_LENGTHS[var.data_type])
331+
var.max = _signed_int_from_hex(max_string, _calc_bit_length(var.data_type))
315332
else:
316333
var.max = int(max_string, 0)
317334
except ValueError:
318-
pass
335+
logger.warning(
336+
"Invalid HighLimit %r for %s (0x%X), ignoring",
337+
eds.get(section, "HighLimit"), var.name, var.index,
338+
)
319339
if eds.has_option(section, "DefaultValue"):
320340
try:
321341
var.default_raw = eds.get(section, "DefaultValue")
322342
if '$NODEID' in var.default_raw:
323343
var.relative = True
324344
var.default = _convert_variable(node_id, var.data_type, var.default_raw)
325345
except ValueError:
326-
pass
346+
logger.warning(
347+
"Invalid DefaultValue %r for %s (0x%X), ignoring",
348+
var.default_raw, var.name, var.index,
349+
)
327350
if eds.has_option(section, "ParameterValue"):
328351
try:
329352
var.value_raw = eds.get(section, "ParameterValue")
330353
var.value = _convert_variable(node_id, var.data_type, var.value_raw)
331354
except ValueError:
332-
pass
355+
logger.warning(
356+
"Invalid ParameterValue %r for %s (0x%X), ignoring",
357+
var.value_raw, var.name, var.index,
358+
)
333359
# Factor, Description and Unit are not standard according to the CANopen specifications, but
334360
# they are implemented in the python canopen package, so we can at least try to use them
335361
if eds.has_option(section, "Factor"):
336362
try:
337363
var.factor = float(eds.get(section, "Factor"))
338364
except ValueError:
339-
pass
365+
logger.warning(
366+
"Invalid Factor %r for %s (0x%X), ignoring",
367+
eds.get(section, "Factor"), var.name, var.index,
368+
)
340369
if eds.has_option(section, "Description"):
341-
try:
342-
var.description = eds.get(section, "Description")
343-
except ValueError:
344-
pass
370+
var.description = eds.get(section, "Description")
345371
if eds.has_option(section, "Unit"):
346-
try:
347-
var.unit = eds.get(section, "Unit")
348-
except ValueError:
349-
pass
372+
var.unit = eds.get(section, "Unit")
350373
return var
351374

352375

@@ -411,9 +434,9 @@ def export_variable(var, eds):
411434
eds.set(section, "PDOMapping", hex(var.pdo_mappable))
412435

413436
if getattr(var, 'min', None) is not None:
414-
eds.set(section, "LowLimit", var.min)
437+
eds.set(section, "LowLimit", _int_to_hex(var.data_type, var.min))
415438
if getattr(var, 'max', None) is not None:
416-
eds.set(section, "HighLimit", var.max)
439+
eds.set(section, "HighLimit", _int_to_hex(var.data_type, var.max))
417440

418441
if getattr(var, 'description', '') != '':
419442
eds.set(section, "Description", var.description)

test/test_eds.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -136,23 +136,21 @@ def test_record(self):
136136
self.assertFalse(var.is_domain)
137137

138138
def test_record_with_limits(self):
139-
int8 = self.od[0x3020]
140-
self.assertEqual(int8.min, 0)
141-
self.assertEqual(int8.max, 127)
142-
uint8 = self.od[0x3021]
143-
self.assertEqual(uint8.min, 2)
144-
self.assertEqual(uint8.max, 10)
145-
int32 = self.od[0x3030]
146-
self.assertEqual(int32.min, -2147483648)
147-
self.assertEqual(int32.max, -1)
148-
int64 = self.od[0x3040]
149-
self.assertEqual(int64.min, -10)
150-
self.assertEqual(int64.max, +10)
151-
# Verify all remaining SIGNED_TYPES are handled (INTEGER24/40/48/56)
152-
for index in (0x3031, 0x3032, 0x3033, 0x3034):
153-
var = self.od[index]
154-
self.assertEqual(var.min, -1, f"min mismatch at 0x{index:04X}")
155-
self.assertEqual(var.max, 0, f"max mismatch at 0x{index:04X}")
139+
cases = [
140+
(0x3020, 0, 127), # INTEGER8
141+
(0x3021, 2, 10), # UNSIGNED8
142+
(0x3030, -2147483648, -1), # INTEGER32
143+
(0x3031, -1, 0), # INTEGER24
144+
(0x3032, -1, 0), # INTEGER40
145+
(0x3033, -1, 0), # INTEGER48
146+
(0x3034, -1, 0), # INTEGER56
147+
(0x3040, -10, +10), # INTEGER64
148+
]
149+
for index, expected_min, expected_max in cases:
150+
with self.subTest(index=f"0x{index:04X}"):
151+
var = self.od[index]
152+
self.assertEqual(var.min, expected_min)
153+
self.assertEqual(var.max, expected_max)
156154

157155
def test_signed_int_from_hex(self):
158156
for data_type, test_cases in self.test_data.items():
@@ -161,6 +159,13 @@ def test_signed_int_from_hex(self):
161159
result = _signed_int_from_hex('0x' + test_case["hex_str"], test_case["bit_length"])
162160
self.assertEqual(result, test_case["expected"])
163161

162+
def test_signed_int_from_hex_rejects_out_of_range(self):
163+
# Value must fit in bit_length bits (unsigned representation).
164+
with self.assertRaises(ValueError):
165+
_signed_int_from_hex("0xFFFF", 8) # 16-bit value into 8-bit field
166+
with self.assertRaises(ValueError):
167+
_signed_int_from_hex("-1", 8) # negative inputs are never valid
168+
164169
def test_array_compact_subobj(self):
165170
array = self.od[0x1003]
166171
self.assertIsInstance(array, canopen.objectdictionary.ODArray)

0 commit comments

Comments
 (0)