xdd: Implement option to load object dictionary from XDD file.#614
xdd: Implement option to load object dictionary from XDD file.#614zaporozhets wants to merge 1 commit intocanopen-python:masterfrom
Conversation
ea733fe to
466b1b2
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Hi @acolomb, could you please take a look? |
|
Thank you very much for this contribution. Happy to see some progress on the XDD front. I started taking a look and overall I think it's going in a good direction. Need to take some more time to put down my thoughts in detail, but some high-level pointers first:
|
acolomb
left a comment
There was a problem hiding this comment.
Just a partial review again, but I hope you get the general idea... More to come when I get another look with a fresher brain.
|
hi @zaporozhets, thanks for the work on this PR! Are you still planning to continue with it? I have a project where I could really use xdd/xdc parsing right now, so I'd be glad to pick up where you left off and contribute to this effort if you don't have the bandwidth. |
|
@acolomb, thank you for the review, and @tomasbarton-stemcell for your interest in this feature. I was extremely busy last year and didn’t have the chance to address all the comments. I’m currently on sick leave until Monday, so I finally have some time to catch up. Please give me a few days to refresh my memory and go through all the suggestions. I’ll keep you posted. I’ll definitely appreciate a thorough review here, as Python isn’t really my main area of expertise. BR, |
47656be to
95c8be2
Compare
1b06485 to
4b7fc1d
Compare
Signed-off-by: Taras Zaporozhets <[email protected]>
4b7fc1d to
7dd46de
Compare
|
Thank you for your patience. I’ve addressed (hopefully all) the comments and refactored several sections to simplify the parsing logic. 1.Added full type annotations and verified the code with @acolomb, @tomasbarton-stemcell could you please review? |
There was a problem hiding this comment.
Hi, thanks for the update! I tested this on a few xdc and xdd files I have in my project (all generated by the editor that comes with the canopennode C library) and all that I tried worked fine. I added some comments, but I am not really a Python programmer so 🤷🏻♂️
As a side not, could the import_od be relaxed to load xdc files as well? I am not exactly sure what the difference is, but with the files we have I can load them just fine and talk to the devices.
| _add_device_information(od, root) | ||
| _add_object_list(od, root) | ||
| _add_dummy_objects(od, root) |
There was a problem hiding this comment.
if xdd is None (which it can be according to the type signature) then this will throw here because root won't be defined.
thisone root.find('.//{*}DeviceCommissioning') as well
| 'USINT': datatypes.UNSIGNED8, | ||
| 'UINT': datatypes.UNSIGNED16, | ||
| 'UDINT': datatypes.UNSIGNED32, | ||
| 'ULINT': datatypes.UNSIGNED32, |
There was a problem hiding this comment.
is this right here? LINT above is defined to be 64b, but ULINT here is 32b
| return int(value, 0) | ||
|
|
||
|
|
||
| def _convert_variable( |
There was a problem hiding this comment.
unused. Didn't check other files (shouldn't be as this is _), but there is no reference to it in this file.
| device_identity = root.find('.//{*}DeviceIdentity') | ||
| if device_identity is None: | ||
| raise ValueError("Missing 'DeviceIdentity' section in XDD file") | ||
| if device_identity is not None: |
There was a problem hiding this comment.
redundant None check (statement above guarantees this is not None).
There is the same problem below: if general_features is not None:
| ("granularity", "granularity", autoint, None), | ||
| ("nrOfRxPDO", "nr_of_RXPDO", autoint, "0"), | ||
| ("nrOfTxPDO", "nr_of_TXPDO", autoint, "0"), | ||
| ("bootUpSlave", "simple_boot_up_slave", bool, 0), |
There was a problem hiding this comment.
is bool going to work here? I am not sure what the xml parser gives you, but if it is a string "true" or "false" they will both get converted to True using the bool function.
I was just going by the example xdd you have in test: <CANopenGeneralFeatures granularity="8" nrOfRxPDO="4" nrOfTxPDO="4" bootUpSlave="true" />
| value = value.replace(" ", "").upper() | ||
| if '$NODEID' in value: | ||
| if node_id is None: | ||
| logger.warn( |
There was a problem hiding this comment.
logger.warn is deprecated: https://docs.python.org/3/library/logging.html#logging.Logger.warning
| var.description = par_tree.get('description', '') | ||
|
|
||
| # Extract data type | ||
| data_types = { |
There was a problem hiding this comment.
Could probably be a file level constant
| device_commissioning_elem = root.find('.//{*}DeviceCommissioning') | ||
| if device_commissioning_elem is not None: | ||
| node_id_attr = device_commissioning_elem.get("nodeID") |
There was a problem hiding this comment.
One more thing I noticed with my xdc file from CANopenNode editor:
<deviceCommissioning NodeID="2" nodeName="scale" actualBaudRate="0" networkNumber="0" networkName="" CANopenManager="false" />
The casing is different, it is lower case deviceCommissioning and then upper case NodeID. I am not sure which is is correct according to the standard.
bizfsc
left a comment
There was a problem hiding this comment.
Automated review based on static analysis of the PR diff. Several correctness bugs need fixing before merge; design issues are suggestions.
|
|
||
| logger = logging.getLogger(__name__) | ||
| autoint = functools.partial(int, base=0) | ||
| hex = functools.partial(int, base=16) |
There was a problem hiding this comment.
Bug: hex shadows the builtins.hex function.
hex is a Python builtin. Redefining it here makes it unavailable for any downstream code in this module and is confusing.
Suggestion: rename to parse_hex or _from_hex.
parse_hex = functools.partial(int, base=16)| ) -> ObjectDictionary: | ||
| od = ObjectDictionary() | ||
|
|
||
| if xdd is not None: |
There was a problem hiding this comment.
Bug: NameError when xdd=None.
If xdd is None, root is never assigned, but it is used immediately at line 30 (root.find('.//{*}DeviceCommissioning')) and again at lines 42-44.
Either raise early or guard all usages:
if xdd is None:
raise ValueError("source must not be None")
root = etree.parse(xdd).getroot()| device_identity = root.find('.//{*}DeviceIdentity') | ||
| if device_identity is None: | ||
| raise ValueError("Missing 'DeviceIdentity' section in XDD file") | ||
| if device_identity is not None: |
There was a problem hiding this comment.
Dead branch: if device_identity is not None: is always True here.
The raise ValueError on the line above guarantees device_identity is not None at this point. The if guard is misleading — it implies the block could be skipped. Just dedent the body:
if device_identity is None:
raise ValueError("Missing 'DeviceIdentity' section in XDD file")
identity_fields = [...]| general_features = root.find('.//{*}CANopenGeneralFeatures') | ||
| if general_features is None: | ||
| raise ValueError("Missing 'CANopenGeneralFeatures' element") | ||
| if general_features is not None: |
There was a problem hiding this comment.
Same dead-branch issue as with device_identity.
After raise ValueError("Missing 'CANopenGeneralFeatures' element"), the if general_features is not None: guard is always True. Dedent the body.
| ("granularity", "granularity", autoint, None), | ||
| ("nrOfRxPDO", "nr_of_RXPDO", autoint, "0"), | ||
| ("nrOfTxPDO", "nr_of_TXPDO", autoint, "0"), | ||
| ("bootUpSlave", "simple_boot_up_slave", bool, 0), |
There was a problem hiding this comment.
Bug: bool("false") == True in Python.
The XDD attribute value is the string "true" or "false". Passing it to bool(...) will always return True for any non-empty string, including "false".
Fix:
("bootUpSlave", "simple_boot_up_slave", lambda s: str(s).lower() == "true", "false"),| if entry is None: | ||
| raise ValueError( | ||
| "Missing 'entry' attribute for dummy object") | ||
| p = entry.split('=') |
There was a problem hiding this comment.
IndexError if entry contains no = character.
entry.split('=') returns a list with one element when = is absent; p[1] then raises IndexError.
Safer:
key, _, raw_value = entry.partition('=')
if not _:
logger.warning("Unexpected dummyUsage entry format: %r", entry)
continue
value = int(raw_value, 10)| } | ||
|
|
||
| for k, v in data_types.items(): | ||
| if par_tree.find(f'{{*}}{k}') is not None: |
There was a problem hiding this comment.
Last-wins data-type lookup — missing break.
The loop iterates over all keys even after a match is found. If a <parameter> element happened to contain two type tags (malformed XDD), the last one in the dict wins silently.
Add break after setting var.data_type:
for k, v in data_types.items():
if par_tree.find(f'{{*}}{k}') is not None:
var.data_type = v
break| value = value.replace(" ", "").upper() | ||
| if '$NODEID' in value: | ||
| if node_id is None: | ||
| logger.warn( |
There was a problem hiding this comment.
logger.warn() is deprecated since Python 3.2.
Use logger.warning(...) instead.
| return int(value, 0) | ||
|
|
||
|
|
||
| def _convert_variable( |
There was a problem hiding this comment.
_convert_variable is defined but never called (dead code).
It handles OCTET_STRING, DOMAIN, VISIBLE_STRING, FLOAT_TYPES etc., but none of the callers use it — _build_variable and _set_parameters_from_xdd_canopen_object only call _convert_integer. This means default values for non-integer types (e.g. REAL32, VISIBLE_STRING) are silently ignored.
Either wire up _convert_variable for non-integer defaults, or remove it.
| ], | ||
| "int32": [ | ||
| {"hex_str": "7FFFFFFF", "bit_length": 32, "expected": 2147483647}, | ||
| {"hex_str": "80000000", "bit_length": 32, "expected": -2147483648}, |
There was a problem hiding this comment.
Test missing: bool("false") bug.
No test checks that bootUpSlave="false" (string) is parsed as False. This is the easiest way to catch the bool(str) bug in CI.
def test_boot_up_slave_false(self):
# sample.xdd has bootUpSlave="true"
self.assertTrue(self.od.device_information.simple_boot_up_slave)And a complementary file with bootUpSlave="false" should assert False.
Hi All,
This is a WIP merge request that adds support for XDD import to CANopen. I'm still in the process of testing and making improvements, but I’d appreciate any feedback you might have in the meantime.
Feel free to share any questions or suggestions.
Best regards,
Taras