diff --git a/canopen/objectdictionary/__init__.py b/canopen/objectdictionary/__init__.py index c8de5aa4..99502c13 100644 --- a/canopen/objectdictionary/__init__.py +++ b/canopen/objectdictionary/__init__.py @@ -208,11 +208,16 @@ def __init__(self, name: str, index: int): self.name = name #: Storage location of index self.storage_location: Optional[str] = None + #: CiA 306 ObjFlags bitfield + self.obj_flags: int = 0 + #: CiA 306 Denotation string (DCF only) + self.denotation: str = "" self.subindices: dict[int, ODVariable] = {} self.names: dict[str, ODVariable] = {} def __repr__(self) -> str: - return f"<{type(self).__qualname__} {self.name!r} at {pretty_index(self.index)}>" + flags = f" flags=0x{self.obj_flags:X}" if self.obj_flags else "" + return f"<{type(self).__qualname__} {self.name!r} at {pretty_index(self.index)}{flags}>" def __getitem__(self, subindex: Union[int, str]) -> ODVariable: item = self.names.get(subindex) or self.subindices.get(subindex) @@ -269,11 +274,16 @@ def __init__(self, name: str, index: int): self.name = name #: Storage location of index self.storage_location: Optional[str] = None + #: CiA 306 ObjFlags bitfield + self.obj_flags: int = 0 + #: CiA 306 Denotation string (DCF only) + self.denotation: str = "" self.subindices: dict[int, ODVariable] = {} self.names: dict[str, ODVariable] = {} def __repr__(self) -> str: - return f"<{type(self).__qualname__} {self.name!r} at {pretty_index(self.index)}>" + flags = f" flags=0x{self.obj_flags:X}" if self.obj_flags else "" + return f"<{type(self).__qualname__} {self.name!r} at {pretty_index(self.index)}{flags}>" def __getitem__(self, subindex: Union[int, str]) -> ODVariable: var = self.names.get(subindex) or self.subindices.get(subindex) @@ -379,12 +389,17 @@ def __init__(self, name: str, index: int, subindex: int = 0): self.bit_definitions: dict[str, list[int]] = {} #: Storage location of index self.storage_location: Optional[str] = None + #: CiA 306 ObjFlags bitfield + self.obj_flags: int = 0 + #: CiA 306 Denotation string (DCF only) + self.denotation: str = "" #: Can this variable be mapped to a PDO self.pdo_mappable = False def __repr__(self) -> str: subindex = self.subindex if isinstance(self.parent, (ODRecord, ODArray)) else None - return f"<{type(self).__qualname__} {self.qualname!r} at {pretty_index(self.index, subindex)}>" + flags = f" flags=0x{self.obj_flags:X}" if self.obj_flags else "" + return f"<{type(self).__qualname__} {self.qualname!r} at {pretty_index(self.index, subindex)}{flags}>" @property def qualname(self) -> str: diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index d47a3019..0d308013 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -139,14 +139,20 @@ def import_eds(source, node_id): arr.add_member(last_subindex) arr.add_member(build_variable(eds, section, node_id, object_type, index, 1)) arr.storage_location = storage_location + arr.obj_flags = _get_obj_flags(eds, section) + arr.denotation = eds.get(section, "Denotation") if eds.has_option(section, "Denotation") else "" od.add_object(arr) elif object_type == objectcodes.ARRAY: arr = ODArray(name, index) arr.storage_location = storage_location + arr.obj_flags = _get_obj_flags(eds, section) + arr.denotation = eds.get(section, "Denotation") if eds.has_option(section, "Denotation") else "" od.add_object(arr) elif object_type == objectcodes.RECORD: record = ODRecord(name, index) record.storage_location = storage_location + record.obj_flags = _get_obj_flags(eds, section) + record.denotation = eds.get(section, "Denotation") if eds.has_option(section, "Denotation") else "" od.add_object(record) continue @@ -258,6 +264,15 @@ def _revert_variable(var_type, value): return f"0x{value:02X}" +def _get_obj_flags(eds, section): + if eds.has_option(section, "ObjFlags"): + try: + return int(eds.get(section, "ObjFlags"), 0) + except ValueError: + pass + return 0 + + def build_variable( eds: RawConfigParser, section: str, @@ -350,6 +365,9 @@ def build_variable( var.unit = eds.get(section, "Unit") except ValueError: pass + var.obj_flags = _get_obj_flags(eds, section) + if eds.has_option(section, "Denotation"): + var.denotation = eds.get(section, "Denotation") return var @@ -425,12 +443,21 @@ def export_variable(var, eds): if getattr(var, 'unit', '') != '': eds.set(section, "Unit", var.unit) + if getattr(var, 'obj_flags', 0) != 0: + eds.set(section, "ObjFlags", f"0x{var.obj_flags:X}") + if device_commisioning and getattr(var, 'denotation', '') != '': + eds.set(section, "Denotation", var.denotation) + def export_record(var, eds): section = f"{var.index:04X}" export_common(var, eds, section) eds.set(section, "SubNumber", f"0x{len(var.subindices):X}") ot = objectcodes.RECORD if isinstance(var, ODRecord) else objectcodes.ARRAY eds.set(section, "ObjectType", f"0x{ot:X}") + if getattr(var, 'obj_flags', 0) != 0: + eds.set(section, "ObjFlags", f"0x{var.obj_flags:X}") + if device_commisioning and getattr(var, 'denotation', '') != '': + eds.set(section, "Denotation", var.denotation) for i in var: export_variable(var[i], eds) diff --git a/test/sample.eds b/test/sample.eds index ad00a12e..ccf94f71 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -1025,6 +1025,14 @@ DataType=0x0007 AccessType=rw PDOMapping=0 +[3060] +ParameterName=Object with ObjFlags +ObjectType=0x7 +DataType=0x0007 +AccessType=rw +PDOMapping=0 +ObjFlags=0x1 + [3064] ParameterName=Record with DOMAIN sub-object SubNumber=0x2 @@ -1044,3 +1052,24 @@ ObjectType=0x2 DataType=0x0007 AccessType=rw PDOMapping=0 + +[3065] +ParameterName=Record with ObjFlags +ObjectType=0x9 +ObjFlags=0x3 +SubNumber=0x2 + +[3065sub0] +ParameterName=Highest sub-index supported +ObjectType=0x7 +DataType=0x0005 +AccessType=ro +DefaultValue=0x01 +PDOMapping=0 + +[3065sub1] +ParameterName=Value +ObjectType=0x7 +DataType=0x0007 +AccessType=rw +PDOMapping=0 diff --git a/test/test_eds.py b/test/test_eds.py index 7a19ffeb..80a826e9 100644 --- a/test/test_eds.py +++ b/test/test_eds.py @@ -396,6 +396,88 @@ def verify_od(self, source, doctype): self.assertEqual(self.od.comments, exported_od.comments) + def test_reading_obj_flags(self): + var = self.od[0x3060] + self.assertIsInstance(var, canopen.objectdictionary.ODVariable) + self.assertEqual(var.obj_flags, 0x1) + + def test_reading_obj_flags_default(self): + """Standard objects without ObjFlags must have obj_flags == 0.""" + var = self.od[0x1017] # Producer heartbeat time — no ObjFlags in sample.eds + self.assertEqual(var.obj_flags, 0) + + def test_reading_obj_flags_record(self): + record = self.od[0x3065] + self.assertIsInstance(record, canopen.objectdictionary.ODRecord) + self.assertEqual(record.obj_flags, 0x3) + + def test_roundtrip_obj_flags(self): + 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[0x3060].obj_flags, 0x1) + self.assertEqual(od2[0x1017].obj_flags, 0) + + def test_roundtrip_obj_flags_record(self): + 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[0x3065].obj_flags, 0x3) + + def test_invalid_obj_flags_returns_zero(self): + import configparser + from canopen.objectdictionary.eds import _get_obj_flags + eds = configparser.RawConfigParser() + eds.optionxform = str + eds.add_section("3060") + eds.set("3060", "ObjFlags", "not_a_number") + self.assertEqual(_get_obj_flags(eds, "3060"), 0) + + def test_denotation_roundtrip_dcf(self): + import io + self.od[0x3060].denotation = 'FlaggedObject' + with io.StringIO() as dest: + canopen.export_od(self.od, dest, 'dcf') + dest.name = 'mock.dcf' + dest.seek(0) + od2 = canopen.import_od(dest) + self.assertEqual(od2[0x3060].denotation, 'FlaggedObject') + + def test_denotation_not_exported_in_eds_mode(self): + import io + self.od[0x3060].denotation = 'ShouldNotAppear' + 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[0x3060].denotation, '') + + def test_obj_flags_in_repr(self): + var = self.od[0x3060] + self.assertIn("flags=0x1", repr(var)) + record = self.od[0x3065] + self.assertIn("flags=0x3", repr(record)) + # zero flags must not clutter repr + self.assertNotIn("flags", repr(self.od[0x1017])) + + def test_denotation_record_roundtrip_dcf(self): + """Denotation on ODRecord/ODArray is preserved in DCF round-trip.""" + import io + self.od[0x3065].denotation = 'RecordLabel' + with io.StringIO() as dest: + canopen.export_od(self.od, dest, 'dcf') + dest.name = 'mock.dcf' + dest.seek(0) + od2 = canopen.import_od(dest) + self.assertEqual(od2[0x3065].denotation, 'RecordLabel') + if __name__ == "__main__": unittest.main()