From 638591b58cda45e51ae41df6924fd211d008bc17 Mon Sep 17 00:00:00 2001 From: Jarvis Agent Date: Tue, 10 Nov 2026 23:14:06 +0530 Subject: [PATCH 1/2] Fix timeout at the end of SDO block uploads --- canopen/sdo/client.py | 2 +- test/test_sdo.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/canopen/sdo/client.py b/canopen/sdo/client.py index 82ee0c88..7c936863 100644 --- a/canopen/sdo/client.py +++ b/canopen/sdo/client.py @@ -586,7 +586,7 @@ def _end_upload(self): try: response = self.sdo_client.read_response() except SdoCommunicationError: - self.abort(ABORT_TIMED_OUT) + self.sdo_client.abort(ABORT_TIMED_OUT) raise res_command, self._server_crc = struct.unpack_from(" Date: Thu, 12 Nov 2026 04:09:03 +0530 Subject: [PATCH 2/2] Include object addresses in client SDO aborts Retain the initiating index/subindex in each stream and pass it through all existing abort and retry-timeout paths. Encode the address without inferring it from segment data or retaining global transfer state. Keep direct abort() and abort(code) calls valid, document the optional address keywords and subclass override boundary, and cover the wire frames, retry behavior and original block-upload end timeout. AI assisted with investigation, implementation, review and testing. --- canopen/sdo/client.py | 169 +++++++++++++---- doc/sdo.rst | 20 ++ test/test_sdo.py | 2 +- test/test_sdo_abort_address.py | 326 +++++++++++++++++++++++++++++++++ 4 files changed, 485 insertions(+), 32 deletions(-) create mode 100644 test/test_sdo_abort_address.py diff --git a/canopen/sdo/client.py b/canopen/sdo/client.py index 7c936863..5fce24ff 100644 --- a/canopen/sdo/client.py +++ b/canopen/sdo/client.py @@ -83,7 +83,12 @@ def read_response(self): raise SdoAbortedError(abort_code) return response - def request_response(self, sdo_request): + def request_response(self, sdo_request, *, index=0, subindex=0): + """Send a request, identifying its transfer in any timeout abort. + + Stream operations supply the original object address. Low-level callers + can provide it explicitly; omitted values retain the zero-address default. + """ retries_left = self.MAX_RETRIES if not self.responses.empty(): # logger.warning("There were unexpected messages in the queue") @@ -96,15 +101,25 @@ def request_response(self, sdo_request): except SdoCommunicationError as e: retries_left -= 1 if not retries_left: - self.abort(ABORT_TIMED_OUT) + self.abort(ABORT_TIMED_OUT, index=index, subindex=subindex) raise logger.warning(str(e)) - def abort(self, abort_code=ABORT_GENERAL_ERROR): - """Abort current transfer.""" + def abort(self, abort_code=ABORT_GENERAL_ERROR, *, index=0, subindex=0): + """Abort a transfer with its object dictionary address. + + :param int abort_code: + Reason for aborting. Defaults to a general error. + :param int index: + Object dictionary index of the transfer, or zero if unspecified. + :param int subindex: + Object dictionary subindex of the transfer, or zero if unspecified. + + Stream operations supply their original address automatically. Existing + low-level calls without an address retain their zero-address behavior. + """ request = bytearray(8) - request[0] = REQUEST_ABORTED - # TODO: Is it necessary to include index and subindex? + SDO_STRUCT.pack_into(request, 0, REQUEST_ABORTED, index, subindex) struct.pack_into("> 1) & 0x7) if res_command & NO_MORE_DATA: @@ -348,6 +381,8 @@ def __init__(self, sdo_client, index, subindex=0, size=None, force_segment=False :param bool force_segment: Force use of segmented transfer regardless of size. """ + self._index = index + self._subindex = subindex self.sdo_client = sdo_client self.size = size self.pos = 0 @@ -363,10 +398,18 @@ def __init__(self, sdo_client, index, subindex=0, size=None, force_segment=False command |= SIZE_SPECIFIED struct.pack_into("> 2) & 0x7 @@ -641,6 +726,8 @@ def __init__(self, sdo_client, index, subindex=0, size=None, request_crc_support :param bool request_crc_support: If crc calculation should be requested when using block transfer """ + self._index = index + self._subindex = subindex self.sdo_client = sdo_client self.size = size self.pos = 0 @@ -662,15 +749,23 @@ def __init__(self, sdo_client, index, subindex=0, size=None, request_crc_support else: logger.warning("Data size has not been specified") SDO_STRUCT.pack_into(request, 0, command, index, subindex) - response = sdo_client.request_response(request) + response = sdo_client.request_response( + request, + index=self._index, + subindex=self._subindex, + ) res_command, res_index, res_subindex = SDO_STRUCT.unpack_from(response) if res_command & 0xE0 != RESPONSE_BLOCK_DOWNLOAD: - self.sdo_client.abort(ABORT_INVALID_COMMAND_SPECIFIER) + self.sdo_client.abort( + ABORT_INVALID_COMMAND_SPECIFIER, + index=self._index, + subindex=self._subindex, + ) raise SdoCommunicationError( f"Unexpected response 0x{res_command:02X}") # Check that the message is for us if res_index != index or res_subindex != subindex: - self.sdo_client.abort() + self.sdo_client.abort(index=self._index, subindex=self._subindex) raise SdoCommunicationError( f"Node returned a value for {pretty_index(res_index, res_subindex)} instead, " "maybe there is another SDO client communicating " @@ -750,15 +845,23 @@ def _block_ack(self): try: response = self.sdo_client.read_response() except SdoCommunicationError: - self.sdo_client.abort(ABORT_TIMED_OUT) + self.sdo_client.abort(ABORT_TIMED_OUT, index=self._index, subindex=self._subindex) raise res_command, ackseq, blksize = struct.unpack_from("BBB", response) if res_command & 0xE0 != RESPONSE_BLOCK_DOWNLOAD: - self.sdo_client.abort(ABORT_INVALID_COMMAND_SPECIFIER) + self.sdo_client.abort( + ABORT_INVALID_COMMAND_SPECIFIER, + index=self._index, + subindex=self._subindex, + ) raise SdoCommunicationError( f"Unexpected response 0x{res_command:02X}") if res_command & 0x3 != BLOCK_TRANSFER_RESPONSE: - self.sdo_client.abort(ABORT_INVALID_COMMAND_SPECIFIER) + self.sdo_client.abort( + ABORT_INVALID_COMMAND_SPECIFIER, + index=self._index, + subindex=self._subindex, + ) raise SdoCommunicationError("Server did not respond with a " "block download response") if ackseq != self._blksize: @@ -813,7 +916,11 @@ def close(self): # Add CRC struct.pack_into("