Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions canopen/objectdictionary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = ""
Comment thread
bizfsc marked this conversation as resolved.
#: 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:
Expand Down
27 changes: 27 additions & 0 deletions canopen/objectdictionary/eds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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)

Expand Down
29 changes: 29 additions & 0 deletions test/sample.eds
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
82 changes: 82 additions & 0 deletions test/test_eds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading