From bf972c1a9e14ac442aae19764e9b50528ad935ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frieder=20Sch=C3=BCler?= Date: Thu, 7 May 2026 10:21:16 +0200 Subject: [PATCH 1/2] eds: fix LowLimit/HighLimit parsing for all signed integer types - Replace _calc_bit_length() (only handled INTEGER8/16/32/64) with a lookup dict _SIGNED_BIT_LENGTHS covering all 8 SIGNED_TYPES (INTEGER8/16/24/32/40/48/56/64) - Log a warning instead of silently ignoring invalid limit values (except ValueError: pass) - Add EDS test entries in sample.eds for INTEGER24/40/48/56 with hex-encoded negative limits - Extend test_record_with_limits and add test_invalid_limit_logs_warning Closes part of #352. --- canopen/objectdictionary/eds.py | 31 +++++++++++++--------------- test/sample.eds | 36 +++++++++++++++++++++++++++++++++ test/test_eds.py | 22 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index d47a3019..3231d4f6 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -204,19 +204,16 @@ def import_from_node(node_id: int, network: canopen.network.Network): return od -def _calc_bit_length(data_type): - if data_type == datatypes.INTEGER8: - return 8 - elif data_type == datatypes.INTEGER16: - return 16 - elif data_type == datatypes.INTEGER32: - return 32 - elif data_type == datatypes.INTEGER64: - return 64 - else: - raise ValueError( - f"Invalid data_type '{data_type}', expecting a signed integer data_type." - ) +_SIGNED_BIT_LENGTHS = { + datatypes.INTEGER8: 8, + datatypes.INTEGER16: 16, + datatypes.INTEGER24: 24, + datatypes.INTEGER32: 32, + datatypes.INTEGER40: 40, + datatypes.INTEGER48: 48, + datatypes.INTEGER56: 56, + datatypes.INTEGER64: 64, +} def _signed_int_from_hex(hex_str, bit_length): @@ -305,20 +302,20 @@ def build_variable( try: min_string = eds.get(section, "LowLimit") if var.data_type in datatypes.SIGNED_TYPES: - var.min = _signed_int_from_hex(min_string, _calc_bit_length(var.data_type)) + var.min = _signed_int_from_hex(min_string, _SIGNED_BIT_LENGTHS[var.data_type]) else: var.min = int(min_string, 0) except ValueError: - pass + logger.warning("Failed to parse LowLimit for %s: %r", name, min_string) if eds.has_option(section, "HighLimit"): try: max_string = eds.get(section, "HighLimit") if var.data_type in datatypes.SIGNED_TYPES: - var.max = _signed_int_from_hex(max_string, _calc_bit_length(var.data_type)) + var.max = _signed_int_from_hex(max_string, _SIGNED_BIT_LENGTHS[var.data_type]) else: var.max = int(max_string, 0) except ValueError: - pass + logger.warning("Failed to parse HighLimit for %s: %r", name, max_string) if eds.has_option(section, "DefaultValue"): try: var.default_raw = eds.get(section, "DefaultValue") diff --git a/test/sample.eds b/test/sample.eds index ad00a12e..49c95ce7 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -976,6 +976,42 @@ HighLimit=0xFFFFFFFF LowLimit=0x80000000 PDOMapping=0 +[3031] +ParameterName=INTEGER24 value range -1 to 0 +ObjectType=0x7 +DataType=0x10 +AccessType=rw +HighLimit=0x000000 +LowLimit=0xFFFFFF +PDOMapping=0 + +[3032] +ParameterName=INTEGER40 value range -1 to 0 +ObjectType=0x7 +DataType=0x12 +AccessType=rw +HighLimit=0x0000000000 +LowLimit=0xFFFFFFFFFF +PDOMapping=0 + +[3033] +ParameterName=INTEGER48 value range -1 to 0 +ObjectType=0x7 +DataType=0x13 +AccessType=rw +HighLimit=0x000000000000 +LowLimit=0xFFFFFFFFFFFF +PDOMapping=0 + +[3034] +ParameterName=INTEGER56 value range -1 to 0 +ObjectType=0x7 +DataType=0x14 +AccessType=rw +HighLimit=0x00000000000000 +LowLimit=0xFFFFFFFFFFFFFF +PDOMapping=0 + [3040] ParameterName=INTEGER64 value range -10 to +10 ObjectType=0x7 diff --git a/test/test_eds.py b/test/test_eds.py index 7a19ffeb..9250197a 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -148,6 +148,28 @@ def test_record_with_limits(self): int64 = self.od[0x3040] self.assertEqual(int64.min, -10) self.assertEqual(int64.max, +10) + # Verify all remaining SIGNED_TYPES are handled (INTEGER24/40/48/56) + for index in (0x3031, 0x3032, 0x3033, 0x3034): + var = self.od[index] + self.assertEqual(var.min, -1, f"min mismatch at 0x{index:04X}") + self.assertEqual(var.max, 0, f"max mismatch at 0x{index:04X}") + + def test_invalid_limit_logs_warning(self): + import io + import logging + + with open(SAMPLE_EDS) as f: + content = f.read() + invalid_eds = content.replace( + "LowLimit=0x02\nPDOMapping=0\n\n[3030]", + "LowLimit=INVALID\nPDOMapping=0\n\n[3030]", + ) + with io.StringIO(invalid_eds) as buf: + buf.name = "mock.eds" + with self.assertLogs("canopen.objectdictionary.eds", level=logging.WARNING) as cm: + od = canopen.import_od(buf) + self.assertIsNone(od[0x3021].min) + self.assertTrue(any("LowLimit" in msg for msg in cm.output)) def test_signed_int_from_hex(self): for data_type, test_cases in self.test_data.items(): From f65a1fb148b398b7d7ab181746eef9fccdac7245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frieder=20Sch=C3=BCler?= Date: Thu, 7 May 2026 21:47:59 +0200 Subject: [PATCH 2/2] eds: revert LowLimit/HighLimit parse error to silent pass, remove warning test --- canopen/objectdictionary/eds.py | 4 ++-- test/test_eds.py | 17 ----------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index 3231d4f6..1967b22e 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -306,7 +306,7 @@ def build_variable( else: var.min = int(min_string, 0) except ValueError: - logger.warning("Failed to parse LowLimit for %s: %r", name, min_string) + pass if eds.has_option(section, "HighLimit"): try: max_string = eds.get(section, "HighLimit") @@ -315,7 +315,7 @@ def build_variable( else: var.max = int(max_string, 0) except ValueError: - logger.warning("Failed to parse HighLimit for %s: %r", name, max_string) + pass if eds.has_option(section, "DefaultValue"): try: var.default_raw = eds.get(section, "DefaultValue") diff --git a/test/test_eds.py b/test/test_eds.py index 9250197a..ab7cf613 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -154,23 +154,6 @@ def test_record_with_limits(self): self.assertEqual(var.min, -1, f"min mismatch at 0x{index:04X}") self.assertEqual(var.max, 0, f"max mismatch at 0x{index:04X}") - def test_invalid_limit_logs_warning(self): - import io - import logging - - with open(SAMPLE_EDS) as f: - content = f.read() - invalid_eds = content.replace( - "LowLimit=0x02\nPDOMapping=0\n\n[3030]", - "LowLimit=INVALID\nPDOMapping=0\n\n[3030]", - ) - with io.StringIO(invalid_eds) as buf: - buf.name = "mock.eds" - with self.assertLogs("canopen.objectdictionary.eds", level=logging.WARNING) as cm: - od = canopen.import_od(buf) - self.assertIsNone(od[0x3021].min) - self.assertTrue(any("LowLimit" in msg for msg in cm.output)) - def test_signed_int_from_hex(self): for data_type, test_cases in self.test_data.items(): for test_case in test_cases: