From f1c01e45ce4227aa9360096ad7fa9708d7802a7e Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Mon, 4 May 2026 22:44:40 +0200 Subject: [PATCH 1/2] Implemented the management for DOMAIN Objects Changed to save whether a Variable has ObjectType==DOMAIN while importing an EDS, so that it is possible to export it unchanged. --- canopen/objectdictionary/__init__.py | 2 ++ canopen/objectdictionary/eds.py | 17 +++++++++---- test/sample.eds | 27 ++++++++++++++++++++ test/test_eds.py | 37 ++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/canopen/objectdictionary/__init__.py b/canopen/objectdictionary/__init__.py index fa694c56..a6251681 100644 --- a/canopen/objectdictionary/__init__.py +++ b/canopen/objectdictionary/__init__.py @@ -364,6 +364,8 @@ def __init__(self, name: str, index: int, subindex: int = 0): self.data_type: Optional[int] = None #: Access type, should be "rw", "ro", "wo", or "const" self.access_type: str = "rw" + #: The variable represents a DOMAIN ObjectType + self.is_domain: bool = False #: Description of variable self.description: str = "" #: Dictionary of value descriptions diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index 312874d5..cb40d333 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -129,7 +129,7 @@ def import_eds(source, node_id): storage_location = None if object_type in (objectcodes.VAR, objectcodes.DOMAIN): - var = build_variable(eds, section, node_id, index) + var = build_variable(eds, section, node_id, index, is_domain=object_type==objectcodes.DOMAIN) od.add_object(var) elif object_type == objectcodes.ARRAY and eds.has_option(section, "CompactSubObj"): arr = ODArray(name, index) @@ -158,7 +158,11 @@ def import_eds(source, node_id): subindex = int(match.group(2), 16) entry = od[index] if isinstance(entry, (ODRecord, ODArray)): - var = build_variable(eds, section, node_id, index, subindex) + try: + object_type = int(eds.get(section, "ObjectType"), 0) + except NoOptionError: + object_type = objectcodes.VAR + var = build_variable(eds, section, node_id, index, subindex, is_domain=object_type==objectcodes.DOMAIN) entry.add_member(var) # Match [index]Name @@ -252,13 +256,14 @@ def _revert_variable(var_type, value): return f"0x{value:02X}" -def build_variable(eds, section, node_id, index, subindex=0): +def build_variable(eds, section, node_id, index, subindex=0, is_domain=False): """Creates a object dictionary entry. :param eds: String stream of the eds file :param section: :param node_id: Node ID :param index: Index of the CANOpen object - :param subindex: Subindex of the CANOpen object (if presente, else 0) + :param subindex: Subindex of the CANOpen object (if present, else 0) + :param is_domain: variable represents a DOMAIN ObjectType (if present, else False) """ name = eds.get(section, "ParameterName") var = ODVariable(name, index, subindex) @@ -268,6 +273,7 @@ def build_variable(eds, section, node_id, index, subindex=0): var.storage_location = None var.data_type = int(eds.get(section, "DataType"), 0) var.access_type = eds.get(section, "AccessType").lower() + var.is_domain = is_domain if var.data_type > 0x1B: # The object dictionary editor from CANFestival creates an optional object if min max values are used # This optional object is then placed in the eds under the section [A0] (start point, iterates for more) @@ -370,7 +376,8 @@ def export_variable(var, eds): section = f"{var.index:04X}sub{var.subindex:X}" export_common(var, eds, section) - eds.set(section, "ObjectType", f"0x{objectcodes.VAR:X}") + object_type = objectcodes.DOMAIN if var.is_domain else objectcodes.VAR + eds.set(section, "ObjectType", f"0x{object_type:X}") if var.data_type: eds.set(section, "DataType", f"0x{var.data_type:04X}") if var.access_type: diff --git a/test/sample.eds b/test/sample.eds index 1afe9965..ad00a12e 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -1017,3 +1017,30 @@ PDOMapping=0x0 Factor=ERROR Description= Unit= + +[3063] +ParameterName=DOMAIN object +ObjectType=0x2 +DataType=0x0007 +AccessType=rw +PDOMapping=0 + +[3064] +ParameterName=Record with DOMAIN sub-object +SubNumber=0x2 +ObjectType=0x9 + +[3064sub0] +ParameterName=Highest subindex +ObjectType=0x7 +DataType=0x0005 +AccessType=ro +DefaultValue=0x01 +PDOMapping=0 + +[3064sub1] +ParameterName=DOMAIN sub-object +ObjectType=0x2 +DataType=0x0007 +AccessType=rw +PDOMapping=0 diff --git a/test/test_eds.py b/test/test_eds.py index 68f5ad3c..ce65e536 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -111,6 +111,7 @@ def test_variable(self): self.assertEqual(var.name, 'Producer heartbeat time') self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED16) self.assertEqual(var.access_type, 'rw') + self.assertEqual(var.is_domain, False) self.assertEqual(var.default, 0) self.assertFalse(var.relative) @@ -132,6 +133,7 @@ def test_record(self): self.assertEqual(var.subindex, 1) self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) self.assertEqual(var.access_type, 'ro') + self.assertEqual(var.is_domain, False) def test_record_with_limits(self): int8 = self.od[0x3020] @@ -166,6 +168,7 @@ def test_array_compact_subobj(self): self.assertEqual(var.subindex, 5) self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) self.assertEqual(var.access_type, 'ro') + self.assertEqual(var.is_domain, False) def test_explicit_name_subobj(self): name = self.od[0x3004].name @@ -197,6 +200,7 @@ def test_dummy_variable(self): self.assertEqual(var.name, 'Dummy0003') self.assertEqual(var.data_type, canopen.objectdictionary.INTEGER16) self.assertEqual(var.access_type, 'const') + self.assertEqual(var.is_domain, False) self.assertEqual(len(var), 16) def test_dummy_variable_undefined(self): @@ -213,6 +217,39 @@ def test_reading_factor(self): self.assertEqual(var2.factor, 1) self.assertEqual(var2.unit, '') + def test_read_domain_object(self): + var = self.od[0x3063] + self.assertIsInstance(var, canopen.objectdictionary.ODVariable) + self.assertEqual(var.index, 0x3063) + self.assertEqual(var.subindex, 0) + self.assertEqual(var.name, 'DOMAIN object') + self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) + self.assertEqual(var.access_type, 'rw') + self.assertEqual(var.is_domain, True) + + def test_read_domain_subobject(self): + record = self.od[0x3064] + var = record[1] + self.assertIsInstance(var, canopen.objectdictionary.ODVariable) + self.assertEqual(var.index, 0x3064) + self.assertEqual(var.subindex, 1) + self.assertEqual(var.name, 'DOMAIN sub-object') + self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) + self.assertEqual(var.access_type, 'rw') + self.assertEqual(var.is_domain, True) + + def test_roundtrip_domain_objects(self): + # ObjectType==DOMAIN survive an EDS export/import round-trip + import io + with io.StringIO() as dest: + canopen.export_od(self.od, dest, 'eds') + dest.name = 'mock.eds' + dest.seek(0) + od2 = canopen.import_od(dest) + self.assertEqual(od2['Producer heartbeat time'].is_domain, False) + self.assertEqual(od2['Identity object']['Vendor-ID'].is_domain, False) + self.assertEqual(od2[0x3063].is_domain, True) + self.assertEqual(od2[0x3064][1].is_domain, True) def test_comments(self): From aaa495e70f23206aafcadd38b73643de6dbafff8 Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Wed, 6 May 2026 21:50:14 +0200 Subject: [PATCH 2/2] Small changes requested by the maintainers --- canopen/objectdictionary/eds.py | 4 ++-- test/test_eds.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index cb40d333..19c4b02f 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -129,7 +129,7 @@ def import_eds(source, node_id): storage_location = None if object_type in (objectcodes.VAR, objectcodes.DOMAIN): - var = build_variable(eds, section, node_id, index, is_domain=object_type==objectcodes.DOMAIN) + var = build_variable(eds, section, node_id, index, is_domain=object_type == objectcodes.DOMAIN) od.add_object(var) elif object_type == objectcodes.ARRAY and eds.has_option(section, "CompactSubObj"): arr = ODArray(name, index) @@ -162,7 +162,7 @@ def import_eds(source, node_id): object_type = int(eds.get(section, "ObjectType"), 0) except NoOptionError: object_type = objectcodes.VAR - var = build_variable(eds, section, node_id, index, subindex, is_domain=object_type==objectcodes.DOMAIN) + var = build_variable(eds, section, node_id, index, subindex, is_domain=object_type == objectcodes.DOMAIN) entry.add_member(var) # Match [index]Name diff --git a/test/test_eds.py b/test/test_eds.py index ce65e536..7a19ffeb 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -111,7 +111,7 @@ def test_variable(self): self.assertEqual(var.name, 'Producer heartbeat time') self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED16) self.assertEqual(var.access_type, 'rw') - self.assertEqual(var.is_domain, False) + self.assertFalse(var.is_domain) self.assertEqual(var.default, 0) self.assertFalse(var.relative) @@ -133,7 +133,7 @@ def test_record(self): self.assertEqual(var.subindex, 1) self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) self.assertEqual(var.access_type, 'ro') - self.assertEqual(var.is_domain, False) + self.assertFalse(var.is_domain) def test_record_with_limits(self): int8 = self.od[0x3020] @@ -168,7 +168,7 @@ def test_array_compact_subobj(self): self.assertEqual(var.subindex, 5) self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) self.assertEqual(var.access_type, 'ro') - self.assertEqual(var.is_domain, False) + self.assertFalse(var.is_domain) def test_explicit_name_subobj(self): name = self.od[0x3004].name @@ -200,7 +200,7 @@ def test_dummy_variable(self): self.assertEqual(var.name, 'Dummy0003') self.assertEqual(var.data_type, canopen.objectdictionary.INTEGER16) self.assertEqual(var.access_type, 'const') - self.assertEqual(var.is_domain, False) + self.assertFalse(var.is_domain) self.assertEqual(len(var), 16) def test_dummy_variable_undefined(self): @@ -225,7 +225,7 @@ def test_read_domain_object(self): self.assertEqual(var.name, 'DOMAIN object') self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) self.assertEqual(var.access_type, 'rw') - self.assertEqual(var.is_domain, True) + self.assertTrue(var.is_domain) def test_read_domain_subobject(self): record = self.od[0x3064] @@ -236,7 +236,7 @@ def test_read_domain_subobject(self): self.assertEqual(var.name, 'DOMAIN sub-object') self.assertEqual(var.data_type, canopen.objectdictionary.UNSIGNED32) self.assertEqual(var.access_type, 'rw') - self.assertEqual(var.is_domain, True) + self.assertTrue(var.is_domain) def test_roundtrip_domain_objects(self): # ObjectType==DOMAIN survive an EDS export/import round-trip @@ -246,10 +246,10 @@ def test_roundtrip_domain_objects(self): dest.name = 'mock.eds' dest.seek(0) od2 = canopen.import_od(dest) - self.assertEqual(od2['Producer heartbeat time'].is_domain, False) - self.assertEqual(od2['Identity object']['Vendor-ID'].is_domain, False) - self.assertEqual(od2[0x3063].is_domain, True) - self.assertEqual(od2[0x3064][1].is_domain, True) + self.assertFalse(od2['Producer heartbeat time'].is_domain) + self.assertFalse(od2['Identity object']['Vendor-ID'].is_domain) + self.assertTrue(od2[0x3063].is_domain) + self.assertTrue(od2[0x3064][1].is_domain) def test_comments(self):