From 6b4b2b35e013084bf2ecac8a9977acb7975c3554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 22 Jun 2025 23:46:23 +0200 Subject: [PATCH 1/4] Define constants for all standardized SDO abort codes. --- canopen/sdo/constants.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/canopen/sdo/constants.py b/canopen/sdo/constants.py index e8a07359..7182d251 100644 --- a/canopen/sdo/constants.py +++ b/canopen/sdo/constants.py @@ -31,3 +31,36 @@ NO_MORE_DATA = 0x1 NO_MORE_BLOCKS = 0x80 TOGGLE_BIT = 0x10 + +# Abort codes +ABORT_TOGGLE_NOT_ALTERNATED = 0x0503_0000 +ABORT_TIMED_OUT = 0x0504_0000 +ABORT_INVALID_COMMAND_SPECIFIER = 0x0504_0001 +ABORT_INVALID_BLOCK_SIZE = 0x0504_0002 +ABORT_INVALID_SEQUENCE_NUMBER = 0x0504_0003 +ABORT_CRC_ERROR = 0x0504_0004 +ABORT_OUT_OF_MEMORY = 0x0504_0005 +ABORT_UNSUPPORTED_ACCESS = 0x0601_0000 +ABORT_READ_WRITEONLY = 0x0601_0001 +ABORT_WRITE_READONLY = 0x0601_0002 +ABORT_NOT_IN_OD = 0x0602_0000 +ABORT_PDO_CANNOT_MAP = 0x0604_0041 +ABORT_PDO_LENGTH_EXCEEDED = 0x0604_0042 +ABORT_PARAMETER_INCOMPATIBLE = 0x0604_0043 +ABORT_INTERNAL_INCOMPATIBILITY = 0x0604_0047 +ABORT_HARDWARE_ERROR = 0x0606_0000 +ABORT_LENGTH_NOT_MATCHED = 0x0607_0010 +ABORT_LENGTH_TOO_HIGH = 0x0607_0012 +ABORT_LENGTH_TOO_LOW = 0x0607_0013 +ABORT_NO_SUBINDEX = 0x0609_0011 +ABORT_INVALID_VALUE = 0x0609_0030 # download only +ABORT_VALUE_TOO_HIGH = 0x0609_0031 # download only +ABORT_VALUE_TOO_LOW = 0x0609_0032 # download only +ABORT_MAXIMUM_LESS_THAN_MINIMUM = 0x0609_0036 +ABORT_NO_SDO_CONNECTION = 0x060A_0023 +ABORT_GENERAL_ERROR = 0x0800_0000 +ABORT_STORE_APPLICATION = 0x0800_0020 +ABORT_APPLICATION_LOCAL_CONTROL = 0x0800_0021 +ABORT_APPLICATION_DEVICE_STATE = 0x0800_0022 +ABORT_OD_GENERATION = 0x0800_0023 +ABORT_NO_DATA_AVAILABLE = 0x0800_0024 From 60ebf26ff2268d11b13c7d529be342ffba76761d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 22 Jun 2025 23:52:28 +0200 Subject: [PATCH 2/4] Type annotations for constants (Final). --- canopen/sdo/constants.py | 64 +++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/canopen/sdo/constants.py b/canopen/sdo/constants.py index 7182d251..e19c79a6 100644 --- a/canopen/sdo/constants.py +++ b/canopen/sdo/constants.py @@ -1,4 +1,6 @@ import struct +from typing import Final + # Command, index, subindex SDO_STRUCT = struct.Struct(" Date: Mon, 23 Jun 2025 00:04:58 +0200 Subject: [PATCH 3/4] Use constants in SDO client and server. --- canopen/sdo/client.py | 20 ++++++++++---------- canopen/sdo/server.py | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/canopen/sdo/client.py b/canopen/sdo/client.py index 76fa6fbc..576c0593 100644 --- a/canopen/sdo/client.py +++ b/canopen/sdo/client.py @@ -89,11 +89,11 @@ def request_response(self, sdo_request): except SdoCommunicationError as e: retries_left -= 1 if not retries_left: - self.abort(0x5040000) + self.abort(ABORT_TIMED_OUT) raise logger.warning(str(e)) - def abort(self, abort_code=0x08000000): + def abort(self, abort_code=ABORT_GENERAL_ERROR): """Abort current transfer.""" request = bytearray(8) request[0] = REQUEST_ABORTED @@ -487,7 +487,7 @@ def __init__(self, sdo_client, index, subindex=0, request_crc_support=True): res_command, res_index, res_subindex = SDO_STRUCT.unpack_from(response) if res_command & 0xE0 != RESPONSE_BLOCK_UPLOAD: self._error = True - self.sdo_client.abort(0x05040001) + self.sdo_client.abort(ABORT_INVALID_COMMAND_SPECIFIER) raise SdoCommunicationError(f"Unexpected response 0x{res_command:02X}") # Check that the message is for us if res_index != index or res_subindex != subindex: @@ -544,7 +544,7 @@ def read(self, size=-1): if self._done: if self._server_crc != self._crc.final(): self._error = True - self.sdo_client.abort(0x05040004) + self.sdo_client.abort(ABORT_CRC_ERROR) raise SdoCommunicationError("CRC is not OK") logger.info("CRC is OK") self.pos += len(data) @@ -564,7 +564,7 @@ def _retransmit(self): self._ackseq = seqno return response self._error = True - self.sdo_client.abort(0x05040000) + self.sdo_client.abort(ABORT_TIMED_OUT) raise SdoCommunicationError("Some data were lost and could not be retransmitted") def _ack_block(self): @@ -580,11 +580,11 @@ def _end_upload(self): res_command, self._server_crc = struct.unpack_from("> 2) & 0x7 @@ -654,7 +654,7 @@ def __init__(self, sdo_client, index, subindex=0, size=None, request_crc_support response = sdo_client.request_response(request) res_command, res_index, res_subindex = SDO_STRUCT.unpack_from(response) if res_command & 0xE0 != RESPONSE_BLOCK_DOWNLOAD: - self.sdo_client.abort(0x05040001) + self.sdo_client.abort(ABORT_INVALID_COMMAND_SPECIFIER) raise SdoCommunicationError( f"Unexpected response 0x{res_command:02X}") # Check that the message is for us @@ -739,11 +739,11 @@ def _block_ack(self): response = self.sdo_client.read_response() res_command, ackseq, blksize = struct.unpack_from("BBB", response) if res_command & 0xE0 != RESPONSE_BLOCK_DOWNLOAD: - self.sdo_client.abort(0x05040001) + self.sdo_client.abort(ABORT_INVALID_COMMAND_SPECIFIER) raise SdoCommunicationError( f"Unexpected response 0x{res_command:02X}") if res_command & 0x3 != BLOCK_TRANSFER_RESPONSE: - self.sdo_client.abort(0x05040001) + self.sdo_client.abort(ABORT_INVALID_COMMAND_SPECIFIER) raise SdoCommunicationError("Server did not respond with a " "block download response") if ackseq != self._blksize: diff --git a/canopen/sdo/server.py b/canopen/sdo/server.py index 8d693633..c26dc998 100644 --- a/canopen/sdo/server.py +++ b/canopen/sdo/server.py @@ -48,11 +48,11 @@ def on_request(self, can_id, data, timestamp): elif ccs == REQUEST_ABORTED: self.request_aborted(data) else: - self.abort(0x05040001) + self.abort(ABORT_INVALID_COMMAND_SPECIFIER) except SdoAbortedError as exc: self.abort(exc.code) except KeyError as exc: - self.abort(0x06020000) + self.abort(ABORT_NOT_IN_OD) except Exception as exc: self.abort() logger.exception(exc) @@ -68,7 +68,7 @@ def init_upload(self, request): size = len(data) if size == 0: logger.info("No content to upload for 0x%04X:%02X", index, subindex) - self.abort(0x0800_0024) + self.abort(ABORT_NO_DATA_AVAILABLE) return elif size <= 4: logger.info("Expedited upload for 0x%04X:%02X", index, subindex) @@ -87,7 +87,7 @@ def init_upload(self, request): def segmented_upload(self, command): if command & TOGGLE_BIT != self._toggle: # Toggle bit mismatch - raise SdoAbortedError(0x05030000) + raise SdoAbortedError(ABORT_TOGGLE_NOT_ALTERNATED) data = self._buffer[:7] size = len(data) @@ -129,7 +129,7 @@ def block_download(self, data): self._index = index self._subindex = subindex logger.error("Block download is not supported") - self.abort(0x05040001) + self.abort(ABORT_INVALID_COMMAND_SPECIFIER) def init_download(self, request): # TODO: Check if writable (now would fail on end of segmented downloads) @@ -160,7 +160,7 @@ def init_download(self, request): def segmented_download(self, command, request): if command & TOGGLE_BIT != self._toggle: # Toggle bit mismatch - raise SdoAbortedError(0x05030000) + raise SdoAbortedError(ABORT_TOGGLE_NOT_ALTERNATED) last_byte = 8 - ((command >> 1) & 0x7) self._buffer.extend(request[1:last_byte]) @@ -183,7 +183,7 @@ def segmented_download(self, command, request): def send_response(self, response): self.network.send_message(self.tx_cobid, response) - def abort(self, abort_code=0x08000000): + def abort(self, abort_code=ABORT_GENERAL_ERROR): """Abort current transfer.""" data = struct.pack(" Date: Mon, 23 Jun 2025 00:07:03 +0200 Subject: [PATCH 4/4] Keep literals in tests, but comment and format. --- test/test_local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_local.py b/test/test_local.py index 31404bd6..6ab94645 100644 --- a/test/test_local.py +++ b/test/test_local.py @@ -116,11 +116,11 @@ def test_nmt_state_initializing_to_preoper(self): self.assertEqual(state, 'PRE-OPERATIONAL') def test_receive_abort_request(self): - self.remote_node.sdo.abort(0x05040003) + self.remote_node.sdo.abort(0x0504_0003) # Invalid sequence number # Line below is just so that we are sure the client have received the abort # before we do the check time.sleep(0.1) - self.assertEqual(self.local_node.sdo.last_received_error, 0x05040003) + self.assertEqual(self.local_node.sdo.last_received_error, 0x0504_0003) def test_start_remote_node(self): self.remote_node.nmt.state = 'OPERATIONAL'