Skip to content

Commit 5dff918

Browse files
bizfscFedericoSpada
andcommitted
eds: Add first-class support for CiA 306 ObjFlags and Denotation
ObjFlags (UNSIGNED32 bitfield) and Denotation (DCF-only string) are standard CiA 306 fields that are now parsed and exported explicitly. Changes: * ODVariable, ODRecord and ODArray gain an obj_flags: int = 0 attribute. * ODVariable gains a denotation: str = '' attribute. * _get_obj_flags() helper reads and validates the ObjFlags integer value. * ObjFlags is parsed for VAR/DOMAIN (via build_variable), ARRAY and RECORD. * ObjFlags is exported whenever non-zero. * Denotation is read in build_variable() and written only in DCF mode (device_commisioning=True), matching the CiA 306 specification. * Adds test object 0x3060 (ObjFlags=0x1) to sample.eds and four new tests covering read, round-trip and DCF export for both fields. Co-authored-by: FedericoSpada <FedericoSpada@users.noreply.github.com>
1 parent 4e789fe commit 5dff918

4 files changed

Lines changed: 78 additions & 0 deletions

File tree

canopen/objectdictionary/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ def __init__(self, name: str, index: int):
207207
self.name = name
208208
#: Storage location of index
209209
self.storage_location = None
210+
#: CiA 306 ObjFlags bitfield
211+
self.obj_flags: int = 0
210212
self.subindices = {}
211213
self.names = {}
212214

@@ -266,6 +268,8 @@ def __init__(self, name: str, index: int):
266268
self.name = name
267269
#: Storage location of index
268270
self.storage_location = None
271+
#: CiA 306 ObjFlags bitfield
272+
self.obj_flags: int = 0
269273
self.subindices = {}
270274
self.names = {}
271275

@@ -372,6 +376,10 @@ def __init__(self, name: str, index: int, subindex: int = 0):
372376
self.bit_definitions: dict[str, list[int]] = {}
373377
#: Storage location of index
374378
self.storage_location = None
379+
#: CiA 306 ObjFlags bitfield
380+
self.obj_flags: int = 0
381+
#: CiA 306 Denotation string (DCF only)
382+
self.denotation: str = ""
375383
#: Can this variable be mapped to a PDO
376384
self.pdo_mappable = False
377385

canopen/objectdictionary/eds.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,17 @@ def import_eds(source, node_id):
139139
arr.add_member(last_subindex)
140140
arr.add_member(build_variable(eds, section, node_id, index, 1))
141141
arr.storage_location = storage_location
142+
arr.obj_flags = _get_obj_flags(eds, section)
142143
od.add_object(arr)
143144
elif object_type == objectcodes.ARRAY:
144145
arr = ODArray(name, index)
145146
arr.storage_location = storage_location
147+
arr.obj_flags = _get_obj_flags(eds, section)
146148
od.add_object(arr)
147149
elif object_type == objectcodes.RECORD:
148150
record = ODRecord(name, index)
149151
record.storage_location = storage_location
152+
record.obj_flags = _get_obj_flags(eds, section)
150153
od.add_object(record)
151154

152155
continue
@@ -252,6 +255,15 @@ def _revert_variable(var_type, value):
252255
return f"0x{value:02X}"
253256

254257

258+
def _get_obj_flags(eds, section):
259+
if eds.has_option(section, "ObjFlags"):
260+
try:
261+
return int(eds.get(section, "ObjFlags"), 0)
262+
except ValueError:
263+
pass
264+
return 0
265+
266+
255267
def build_variable(eds, section, node_id, index, subindex=0):
256268
"""Creates a object dictionary entry.
257269
:param eds: String stream of the eds file
@@ -330,6 +342,9 @@ def build_variable(eds, section, node_id, index, subindex=0):
330342
var.unit = eds.get(section, "Unit")
331343
except ValueError:
332344
pass
345+
var.obj_flags = _get_obj_flags(eds, section)
346+
if eds.has_option(section, "Denotation"):
347+
var.denotation = eds.get(section, "Denotation")
333348
return var
334349

335350

@@ -404,12 +419,19 @@ def export_variable(var, eds):
404419
if getattr(var, 'unit', '') != '':
405420
eds.set(section, "Unit", var.unit)
406421

422+
if getattr(var, 'obj_flags', 0) != 0:
423+
eds.set(section, "ObjFlags", f"0x{var.obj_flags:X}")
424+
if device_commisioning and getattr(var, 'denotation', '') != '':
425+
eds.set(section, "Denotation", var.denotation)
426+
407427
def export_record(var, eds):
408428
section = f"{var.index:04X}"
409429
export_common(var, eds, section)
410430
eds.set(section, "SubNumber", f"0x{len(var.subindices):X}")
411431
ot = objectcodes.RECORD if isinstance(var, ODRecord) else objectcodes.ARRAY
412432
eds.set(section, "ObjectType", f"0x{ot:X}")
433+
if getattr(var, 'obj_flags', 0) != 0:
434+
eds.set(section, "ObjFlags", f"0x{var.obj_flags:X}")
413435
for i in var:
414436
export_variable(var[i], eds)
415437

test/sample.eds

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,3 +1017,11 @@ PDOMapping=0x0
10171017
Factor=ERROR
10181018
Description=
10191019
Unit=
1020+
1021+
[3060]
1022+
ParameterName=Object with ObjFlags
1023+
ObjectType=0x7
1024+
DataType=0x0007
1025+
AccessType=rw
1026+
PDOMapping=0
1027+
ObjFlags=0x1

test/test_eds.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,46 @@ def verify_od(self, source, doctype):
359359

360360
self.assertEqual(self.od.comments, exported_od.comments)
361361

362+
def test_reading_obj_flags(self):
363+
var = self.od[0x3060]
364+
self.assertIsInstance(var, canopen.objectdictionary.ODVariable)
365+
self.assertEqual(var.obj_flags, 0x1)
366+
367+
def test_reading_obj_flags_default(self):
368+
"""Standard objects without ObjFlags must have obj_flags == 0."""
369+
var = self.od[0x1017] # Producer heartbeat time — no ObjFlags in sample.eds
370+
self.assertEqual(var.obj_flags, 0)
371+
372+
def test_roundtrip_obj_flags(self):
373+
import io
374+
with io.StringIO() as dest:
375+
canopen.export_od(self.od, dest, 'eds')
376+
dest.name = 'mock.eds'
377+
dest.seek(0)
378+
od2 = canopen.import_od(dest)
379+
self.assertEqual(od2[0x3060].obj_flags, 0x1)
380+
self.assertEqual(od2[0x1017].obj_flags, 0)
381+
382+
def test_denotation_roundtrip_dcf(self):
383+
import io
384+
self.od[0x3060].denotation = 'FlaggedObject'
385+
with io.StringIO() as dest:
386+
canopen.export_od(self.od, dest, 'dcf')
387+
dest.name = 'mock.dcf'
388+
dest.seek(0)
389+
od2 = canopen.import_od(dest)
390+
self.assertEqual(od2[0x3060].denotation, 'FlaggedObject')
391+
392+
def test_denotation_not_exported_in_eds_mode(self):
393+
import io
394+
self.od[0x3060].denotation = 'ShouldNotAppear'
395+
with io.StringIO() as dest:
396+
canopen.export_od(self.od, dest, 'eds')
397+
dest.name = 'mock.eds'
398+
dest.seek(0)
399+
od2 = canopen.import_od(dest)
400+
self.assertEqual(od2[0x3060].denotation, '')
401+
362402

363403
if __name__ == "__main__":
364404
unittest.main()

0 commit comments

Comments
 (0)