Skip to content

Commit 90dbe07

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 90dbe07

4 files changed

Lines changed: 122 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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,3 +1017,32 @@ 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
1028+
1029+
[3063]
1030+
ParameterName=Record with ObjFlags
1031+
ObjectType=0x9
1032+
ObjFlags=0x3
1033+
SubNumber=0x2
1034+
1035+
[3063sub0]
1036+
ParameterName=Highest sub-index supported
1037+
ObjectType=0x7
1038+
DataType=0x0005
1039+
AccessType=ro
1040+
DefaultValue=0x01
1041+
PDOMapping=0
1042+
1043+
[3063sub1]
1044+
ParameterName=Value
1045+
ObjectType=0x7
1046+
DataType=0x0007
1047+
AccessType=rw
1048+
PDOMapping=0

test/test_eds.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,69 @@ 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_reading_obj_flags_record(self):
373+
record = self.od[0x3063]
374+
self.assertIsInstance(record, canopen.objectdictionary.ODRecord)
375+
self.assertEqual(record.obj_flags, 0x3)
376+
377+
def test_roundtrip_obj_flags(self):
378+
import io
379+
with io.StringIO() as dest:
380+
canopen.export_od(self.od, dest, 'eds')
381+
dest.name = 'mock.eds'
382+
dest.seek(0)
383+
od2 = canopen.import_od(dest)
384+
self.assertEqual(od2[0x3060].obj_flags, 0x1)
385+
self.assertEqual(od2[0x1017].obj_flags, 0)
386+
387+
def test_roundtrip_obj_flags_record(self):
388+
import io
389+
with io.StringIO() as dest:
390+
canopen.export_od(self.od, dest, 'eds')
391+
dest.name = 'mock.eds'
392+
dest.seek(0)
393+
od2 = canopen.import_od(dest)
394+
self.assertEqual(od2[0x3063].obj_flags, 0x3)
395+
396+
def test_invalid_obj_flags_returns_zero(self):
397+
import configparser
398+
eds = configparser.RawConfigParser()
399+
eds.optionxform = str
400+
eds.add_section("3060")
401+
eds.set("3060", "ObjFlags", "not_a_number")
402+
from canopen.objectdictionary.eds import _get_obj_flags
403+
self.assertEqual(_get_obj_flags(eds, "3060"), 0)
404+
405+
def test_denotation_roundtrip_dcf(self):
406+
import io
407+
self.od[0x3060].denotation = 'FlaggedObject'
408+
with io.StringIO() as dest:
409+
canopen.export_od(self.od, dest, 'dcf')
410+
dest.name = 'mock.dcf'
411+
dest.seek(0)
412+
od2 = canopen.import_od(dest)
413+
self.assertEqual(od2[0x3060].denotation, 'FlaggedObject')
414+
415+
def test_denotation_not_exported_in_eds_mode(self):
416+
import io
417+
self.od[0x3060].denotation = 'ShouldNotAppear'
418+
with io.StringIO() as dest:
419+
canopen.export_od(self.od, dest, 'eds')
420+
dest.name = 'mock.eds'
421+
dest.seek(0)
422+
od2 = canopen.import_od(dest)
423+
self.assertEqual(od2[0x3060].denotation, '')
424+
362425

363426
if __name__ == "__main__":
364427
unittest.main()

0 commit comments

Comments
 (0)