-
Notifications
You must be signed in to change notification settings - Fork 358
eds: Fix signed integer limit parsing and export for all SIGNED_TYPES #658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
bf972c1
f65a1fb
4ed45dd
4319d4a
2382b96
28c7d26
3d1d2d4
aa5257a
cb00c3b
417ace5
2b900a9
e6d1074
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -205,14 +205,11 @@ def import_from_node(node_id: int, network: canopen.network.Network): | |||||||
|
|
||||||||
|
|
||||||||
| def _calc_bit_length(data_type: int) -> int: | ||||||||
| if data_type == datatypes.INTEGER8: | ||||||||
| return 8 | ||||||||
| elif data_type == datatypes.INTEGER16: | ||||||||
| return 16 | ||||||||
| elif data_type == datatypes.INTEGER32: | ||||||||
| return 32 | ||||||||
| elif data_type == datatypes.INTEGER64: | ||||||||
| return 64 | ||||||||
| if data_type in datatypes.INTEGER_TYPES: | ||||||||
| st = ODVariable.STRUCT_TYPES[data_type] | ||||||||
| if isinstance(st, datatypes.IntegerN): | ||||||||
| return st.width | ||||||||
| return st.size * 8 | ||||||||
| else: | ||||||||
| raise ValueError( | ||||||||
| f"Invalid data_type 0x{data_type:04X}, expecting an integer data_type." | ||||||||
|
|
@@ -221,13 +218,27 @@ def _calc_bit_length(data_type: int) -> int: | |||||||
|
|
||||||||
| def _signed_int_from_hex(hex_str, bit_length): | ||||||||
| number = int(hex_str, 0) | ||||||||
| max_value = (1 << (bit_length - 1)) - 1 | ||||||||
| min_signed = -(1 << (bit_length - 1)) | ||||||||
| max_signed = (1 << (bit_length - 1)) - 1 | ||||||||
| max_unsigned = (1 << bit_length) - 1 | ||||||||
|
|
||||||||
| if number > max_value: | ||||||||
| return number - (1 << bit_length) | ||||||||
| else: | ||||||||
| if number < min_signed: | ||||||||
| raise ValueError( | ||||||||
| f"Value {hex_str!r} is out of range for a {bit_length}-bit signed integer" | ||||||||
| ) | ||||||||
| if number < 0: | ||||||||
| # Negative literal (e.g. LowLimit=-32768 or -0x8000) | ||||||||
| return number | ||||||||
|
|
||||||||
| if number > max_unsigned: | ||||||||
| raise ValueError( | ||||||||
| f"Value {hex_str!r} is out of range for a {bit_length}-bit signed integer" | ||||||||
| ) | ||||||||
| if number > max_signed: | ||||||||
| # Unsigned hex literal, two's-complement (e.g. LowLimit=0xFFFF → -1 for INTEGER16) | ||||||||
| return number - (1 << bit_length) | ||||||||
| return number | ||||||||
|
|
||||||||
|
|
||||||||
| def _convert_variable(node_id: int, var_type: int, value: Any) -> Any: | ||||||||
| if var_type in (datatypes.OCTET_STRING, datatypes.DOMAIN): | ||||||||
|
|
@@ -248,14 +259,14 @@ def _convert_variable(node_id: int, var_type: int, value: Any) -> Any: | |||||||
| def _revert_variable(var_type: int, value: Any) -> Any: | ||||||||
| if value is None: | ||||||||
| return None | ||||||||
| if var_type in (datatypes.OCTET_STRING, datatypes.DOMAIN): | ||||||||
| if var_type in (datatypes.OCTET_STRING, datatypes.DOMAIN) and isinstance(value, bytes): | ||||||||
| return bytes.hex(value) | ||||||||
| elif var_type in (datatypes.VISIBLE_STRING, datatypes.UNICODE_STRING): | ||||||||
| return value | ||||||||
| elif var_type in datatypes.FLOAT_TYPES: | ||||||||
| return value | ||||||||
| else: | ||||||||
| return f"0x{value:02X}" | ||||||||
| return str(value) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
|
||||||||
|
|
||||||||
| def build_variable( | ||||||||
|
|
@@ -309,7 +320,10 @@ def build_variable( | |||||||
| else: | ||||||||
| var.min = int(min_string, 0) | ||||||||
| except ValueError: | ||||||||
| pass | ||||||||
| logger.warning( | ||||||||
| "Invalid LowLimit %r for %s (0x%X), ignoring", | ||||||||
| eds.get(section, "LowLimit"), var.name, var.index, | ||||||||
|
Comment on lines
+324
to
+325
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| ) | ||||||||
| if eds.has_option(section, "HighLimit"): | ||||||||
| try: | ||||||||
| max_string = eds.get(section, "HighLimit") | ||||||||
|
|
@@ -318,38 +332,44 @@ def build_variable( | |||||||
| else: | ||||||||
| var.max = int(max_string, 0) | ||||||||
| except ValueError: | ||||||||
| pass | ||||||||
| logger.warning( | ||||||||
| "Invalid HighLimit %r for %s (0x%X), ignoring", | ||||||||
| eds.get(section, "HighLimit"), var.name, var.index, | ||||||||
|
Comment on lines
+336
to
+337
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| ) | ||||||||
| if eds.has_option(section, "DefaultValue"): | ||||||||
| try: | ||||||||
| var.default_raw = eds.get(section, "DefaultValue") | ||||||||
| if '$NODEID' in var.default_raw: | ||||||||
| var.relative = True | ||||||||
| var.default = _convert_variable(node_id, var.data_type, var.default_raw) | ||||||||
| except ValueError: | ||||||||
| pass | ||||||||
| logger.warning( | ||||||||
| "Invalid DefaultValue %r for %s (0x%X), ignoring", | ||||||||
| eds.get(section, "DefaultValue"), var.name, var.index, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| ) | ||||||||
| if eds.has_option(section, "ParameterValue"): | ||||||||
| try: | ||||||||
| var.value_raw = eds.get(section, "ParameterValue") | ||||||||
| var.value = _convert_variable(node_id, var.data_type, var.value_raw) | ||||||||
| except ValueError: | ||||||||
| pass | ||||||||
| logger.warning( | ||||||||
| "Invalid ParameterValue %r for %s (0x%X), ignoring", | ||||||||
| eds.get(section, "ParameterValue"), var.name, var.index, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| ) | ||||||||
| # Factor, Description and Unit are not standard according to the CANopen specifications, but | ||||||||
| # they are implemented in the python canopen package, so we can at least try to use them | ||||||||
| if eds.has_option(section, "Factor"): | ||||||||
| try: | ||||||||
| var.factor = float(eds.get(section, "Factor")) | ||||||||
| except ValueError: | ||||||||
| pass | ||||||||
| logger.warning( | ||||||||
| "Invalid Factor %r for %s (0x%X), ignoring", | ||||||||
| eds.get(section, "Factor"), var.name, var.index, | ||||||||
| ) | ||||||||
| if eds.has_option(section, "Description"): | ||||||||
| try: | ||||||||
| var.description = eds.get(section, "Description") | ||||||||
| except ValueError: | ||||||||
| pass | ||||||||
| var.description = eds.get(section, "Description") | ||||||||
| if eds.has_option(section, "Unit"): | ||||||||
| try: | ||||||||
| var.unit = eds.get(section, "Unit") | ||||||||
| except ValueError: | ||||||||
| pass | ||||||||
| var.unit = eds.get(section, "Unit") | ||||||||
| return var | ||||||||
|
|
||||||||
|
|
||||||||
|
|
@@ -414,9 +434,9 @@ def export_variable(var, eds): | |||||||
| eds.set(section, "PDOMapping", hex(var.pdo_mappable)) | ||||||||
|
|
||||||||
| if getattr(var, 'min', None) is not None: | ||||||||
| eds.set(section, "LowLimit", var.min) | ||||||||
| eds.set(section, "LowLimit", _revert_variable(var.data_type, var.min)) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| if getattr(var, 'max', None) is not None: | ||||||||
| eds.set(section, "HighLimit", var.max) | ||||||||
| eds.set(section, "HighLimit", _revert_variable(var.data_type, var.max)) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
|
||||||||
| if getattr(var, 'description', '') != '': | ||||||||
| eds.set(section, "Description", var.description) | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was really confused why this is necessary to avoid a test failure now. Here's my analysis:
DataType=0x40andLowLimit=0x3.build_variable()sees it's an unknown data type without a correspondingDefaultValuein a section40sub1.data_typeis forced to DOMAIN.var.minis correctly parsed from theLowLimitto anint(3).export_variablenow tries to pass theintto_revert_variable().bytes, so the string conversion viabytes.hex()fails with the integer argument.Why exactly do we need to call
_revert_variable()at all on the limit values? We didn't do that before.This function is terribly named by the way, I cannot even imagine whether it should be applied to the limits (which are always numeric). But as it mirrors
_convert_variable(), one should assume that it requiresbytesfor a DOMAIN variable unconditionally. Simply skipping to astr()conversion seems wrong.