-
Notifications
You must be signed in to change notification settings - Fork 356
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
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 |
|---|---|---|
|
|
@@ -204,15 +204,12 @@ def import_from_node(node_id: int, network: canopen.network.Network): | |
| return od | ||
|
|
||
|
|
||
| def _calc_bit_length(data_type): | ||
| 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 | ||
| def _calc_bit_length(data_type: int) -> int: | ||
| if data_type in datatypes.SIGNED_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 '{data_type}', expecting a signed integer data_type." | ||
|
|
@@ -221,13 +218,27 @@ def _calc_bit_length(data_type): | |
|
|
||
| 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, var_type, value): | ||
| if var_type in (datatypes.OCTET_STRING, datatypes.DOMAIN): | ||
|
|
@@ -245,6 +256,18 @@ def _convert_variable(node_id, var_type, value): | |
| return int(value, 0) | ||
|
|
||
|
|
||
| def _int_to_hex(data_type: int, value: int) -> str: | ||
| """Format an integer as EDS hex string. | ||
|
|
||
| Signed types with a negative value are written as two's-complement hex | ||
| (e.g. INTEGER8 -1 → 0xFF) so the output is a valid EDS literal. | ||
|
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. According to CiA 306:
I don't see any need to restrict ourselves to writing hex or two's complement at all. We may still accept it, though, as the data type rules are very clear on the possible interpretation. But the spec doesn't mention this at all. It's also completely unclear how to handle floating point numbers in EDS files. I guess if we can parse them in Python, we can accept them. But I wouldn't assume any particular conversion to apply for a literal in hex notation: It's fine to write >>> float(int("-0xFFFF", 16)) # this works fine, but not directly
-65535.0Just because CiA 301 defines a wire format for REAL types, doesn't mean we should try parsing hexadecimal byte sequences as IEEE floats. When exporting, the best approach I can see is to simply use Python's float to string conversion. This function gets in the way though, prohibiting us from writing anything but integer limits at all. Why not simply use |
||
| """ | ||
| if data_type in datatypes.SIGNED_TYPES and value < 0: | ||
| bit_length = _calc_bit_length(data_type) | ||
| return f"0x{value + (1 << bit_length):0{bit_length // 4}X}" | ||
| return f"0x{value:02X}" | ||
|
|
||
|
|
||
| def _revert_variable(var_type, value): | ||
| if value is None: | ||
| return None | ||
|
|
@@ -255,7 +278,7 @@ def _revert_variable(var_type, value): | |
| elif var_type in datatypes.FLOAT_TYPES: | ||
| return value | ||
| else: | ||
| return f"0x{value:02X}" | ||
| return _int_to_hex(var_type, value) | ||
|
|
||
|
|
||
| def build_variable( | ||
|
|
@@ -309,7 +332,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, | ||
| ) | ||
| if eds.has_option(section, "HighLimit"): | ||
| try: | ||
| max_string = eds.get(section, "HighLimit") | ||
|
|
@@ -318,38 +344,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, | ||
| ) | ||
| 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, | ||
| ) | ||
| 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, | ||
| ) | ||
| # 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 +446,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", _int_to_hex(var.data_type, var.min)) | ||
| if getattr(var, 'max', None) is not None: | ||
| eds.set(section, "HighLimit", var.max) | ||
| eds.set(section, "HighLimit", _int_to_hex(var.data_type, var.max)) | ||
|
|
||
| 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.
Works equally well for unsigned integers (and could be used for floats if needed):