Skip to content

Commit 88bf866

Browse files
DavyGillebertH-Grobben
authored andcommitted
Added the download methods as extension to the upload methods
1 parent 46525f1 commit 88bf866

2 files changed

Lines changed: 125 additions & 25 deletions

File tree

canopen/sdo/server.py

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,15 @@ def __init__(self, rx_cobid, tx_cobid, node):
3535
def on_request(self, can_id, data, timestamp):
3636
logger.debug('on_request')
3737
if self.sdo_block and self.sdo_block.state != BLOCK_STATE_NONE:
38-
self.process_block(data)
38+
try:
39+
self.process_block(data)
40+
except SdoAbortedError as exc:
41+
self.sdo_block = None
42+
self.abort(exc.code)
43+
except Exception as exc:
44+
self.sdo_block = None
45+
self.abort()
46+
logger.exception(exc)
3947
return
4048

4149
command, = struct.unpack_from("B", data, 0)
@@ -137,6 +145,46 @@ def process_block(self, request):
137145
elif BLOCK_STATE_DOWNLOAD < self.sdo_block.state:
138146
# in download state
139147
logger.debug('BLOCK_STATE_DOWNLOAD')
148+
if self.sdo_block.state == BLOCK_STATE_DL_DATA:
149+
logger.debug('BLOCK_STATE_DL_DATA')
150+
seqno = command & 0x7F
151+
last_seg = bool(command & NO_MORE_BLOCKS)
152+
# Accumulate data bytes (bytes 1-7 of each segment)
153+
self.sdo_block.append_download_data(request[1:8])
154+
self.sdo_block.last_seqno = seqno
155+
156+
if seqno >= self.sdo_block.req_blocksize or last_seg:
157+
# Send block acknowledgement
158+
response = bytearray(8)
159+
response[0] = RESPONSE_BLOCK_DOWNLOAD | BLOCK_TRANSFER_RESPONSE
160+
response[1] = seqno # ackseq
161+
response[2] = self.sdo_block.req_blocksize # new blksize
162+
self.send_response(response)
163+
self.sdo_block.seqno = 0
164+
165+
if last_seg:
166+
self.sdo_block.update_state(BLOCK_STATE_DL_END)
167+
168+
elif self.sdo_block.state == BLOCK_STATE_DL_END:
169+
logger.debug('BLOCK_STATE_DL_END')
170+
if (command & REQUEST_BLOCK_DOWNLOAD) != REQUEST_BLOCK_DOWNLOAD:
171+
raise SdoBlockException("Unknown SDO command specified")
172+
if (command & SUB_COMMAND_MASK) != END_BLOCK_TRANSFER:
173+
raise SdoBlockException("Unknown SDO command specified")
174+
175+
# n = bytes NOT used in last segment
176+
n = (command >> 2) & 0x7
177+
data = self.sdo_block.finalize_download(n)
178+
179+
self._node.set_data(self.sdo_block.index,
180+
self.sdo_block.subindex,
181+
data,
182+
check_writable=True)
183+
184+
response = bytearray(8)
185+
response[0] = RESPONSE_BLOCK_DOWNLOAD | END_BLOCK_TRANSFER
186+
self.send_response(response)
187+
self.sdo_block = None
140188
else:
141189
# in neither
142190
raise SdoBlockException("Data can not be transferred or stored to the application "
@@ -229,13 +277,22 @@ def request_aborted(self, data):
229277
logger.info("Received request aborted for 0x%04X:%02X with code 0x%X", index, subindex, code)
230278

231279
def block_download(self, data):
232-
# We currently don't support BLOCK DOWNLOAD
233-
# Unpack the index and subindex in order to send appropriate abort
280+
logger.debug('Enter server block download')
234281
command, index, subindex = SDO_STRUCT.unpack_from(data)
282+
235283
self._index = index
236284
self._subindex = subindex
237-
logger.error("Block download is not supported")
238-
self.abort(ABORT_INVALID_COMMAND_SPECIFIER)
285+
286+
self.sdo_block = SdoBlock(self._node, data, is_download=True)
287+
288+
res_command = RESPONSE_BLOCK_DOWNLOAD | INITIATE_BLOCK_TRANSFER
289+
res_command |= self.sdo_block.crc # Echo CRC support back to client
290+
response = bytearray(8)
291+
SDO_STRUCT.pack_into(response, 0, res_command, index, subindex)
292+
response[4] = self.sdo_block.req_blocksize # Server-defined block size
293+
294+
self.sdo_block.update_state(BLOCK_STATE_DL_DATA)
295+
self.send_response(response)
239296

240297
def init_download(self, request):
241298
# TODO: Check if writable (now would fail on end of segmented downloads)
@@ -345,38 +402,56 @@ class SdoBlock():
345402
crc_value = 0
346403
last_seqno = 0
347404

348-
def __init__(self, node, request, docrc=False):
405+
def __init__(self, node, request, docrc=False, is_download=False):
349406
"""
350407
:param node:
351408
Node object owning the server
352409
:param request:
353410
CAN message containing SDO request.
354411
:param docrc:
355412
If True, CRC is calculated and checked.
413+
:param is_download:
414+
If True, initialise for block download (server receives data).
415+
If False (default), initialise for block upload (server sends data).
356416
"""
357417
command, index, subindex = SDO_STRUCT.unpack_from(request)
358418
# only do crc if crccheck lib is available _and_ if requested
359419
_req_crc = (command & CRC_SUPPORTED) == CRC_SUPPORTED
360420

361-
if (command & SUB_COMMAND_MASK) == INITIATE_BLOCK_TRANSFER:
421+
# For block download, bit 1 is the size indicator (s), not a sub-command
422+
# bit. Only bit 0 carries the sub-command (0 = initiate). For block
423+
# upload the s-bit is not used in the initiate command so SUB_COMMAND_MASK
424+
# works there, but we must use a 1-bit mask here.
425+
sub_cmd_mask = 0x1 if is_download else SUB_COMMAND_MASK
426+
if (command & sub_cmd_mask) == INITIATE_BLOCK_TRANSFER:
362427
self.state = BLOCK_STATE_INIT
363428
else:
364429
raise SdoBlockException("Unknown SDO command specified")
365430

366431
# TODO: CRC of data if requested
367-
self.crc = CRC_SUPPORTED if (docrc & _req_crc) else 0
432+
self.crc = CRC_SUPPORTED if (docrc & _req_crc) else 0
368433
self._node = node
369434
self.index = index
370435
self.subindex = subindex
371-
self.req_blocksize = request[4]
372436
self.seqno = 0
373-
if not 1 <= self.req_blocksize <= 127:
374-
raise SdoBlockException("Invalid block size")
375437

376-
self.data = self._node.get_data(index,
377-
subindex,
378-
check_readable=True)
379-
self.size = len(self.data)
438+
if is_download:
439+
# Server defines the block size for download (client sends this many
440+
# segments per block before waiting for an acknowledgement)
441+
self.req_blocksize = 127
442+
self._data_buffer = bytearray()
443+
if command & BLOCK_SIZE_SPECIFIED:
444+
self.size, = struct.unpack_from("<L", request, 4)
445+
else:
446+
self.size = None
447+
else:
448+
self.req_blocksize = request[4]
449+
if not 1 <= self.req_blocksize <= 127:
450+
raise SdoBlockException("Invalid block size")
451+
self.data = self._node.get_data(index,
452+
subindex,
453+
check_readable=True)
454+
self.size = len(self.data)
380455

381456
def update_state(self, new_state):
382457
"""
@@ -432,3 +507,25 @@ def get_data_byte(self):
432507
return self.data[self.data_uploaded-1]
433508
return None
434509

510+
def append_download_data(self, segment):
511+
"""Append a 7-byte segment to the download data buffer.
512+
513+
:param segment:
514+
Bytes 1-7 of the received block segment message (always 7 bytes).
515+
"""
516+
self._data_buffer.extend(segment)
517+
518+
def finalize_download(self, n):
519+
"""Return the accumulated download data, trimming the last n unused bytes.
520+
521+
:param int n:
522+
Number of bytes in the last segment that did not contain data
523+
(as signalled by the client in the END_BLOCK_TRANSFER command).
524+
525+
:returns:
526+
The complete received data as bytes.
527+
"""
528+
if n > 0:
529+
return bytes(self._data_buffer[:-n])
530+
return bytes(self._data_buffer)
531+

test/test_local.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,26 @@ def test_expedited_upload(self):
3737
vendor_id = self.remote_node.sdo[0x1400][1].raw
3838
self.assertEqual(vendor_id, 0x99)
3939

40-
# Remove this test, as Block upload is now supported:
41-
# def test_block_upload_switch_to_expedite_upload(self):
42-
# with self.assertRaises(canopen.SdoCommunicationError) as context:
43-
# with self.remote_node.sdo[0x1008].open('r', block_transfer=True) as fp:
44-
# pass
45-
# # We get this since the sdo client don't support the switch
46-
# # from block upload to expedite upload
47-
# self.assertEqual("Unexpected response 0x41", str(context.exception))
40+
def test_block_download(self):
41+
data = b"BLOCK DOWNLOAD TEST DATA"
42+
# Write data using block download
43+
with self.remote_node.sdo[0x2000].open('wb', size=len(data), block_transfer=True) as fp:
44+
fp.write(data)
45+
# Read back using block upload (client requests upload from server)
46+
with self.remote_node.sdo[0x2000].open('rb', block_transfer=True) as fp:
47+
read_data = fp.read()
48+
self.assertEqual(read_data, data)
4849

4950
def test_block_download_not_supported(self):
51+
# Try block download to an object that should not support it (e.g., a constant string)
5052
data = b"TEST DEVICE"
5153
with self.assertRaises(canopen.SdoAbortedError) as context:
5254
with self.remote_node.sdo[0x1008].open('wb',
5355
size=len(data),
5456
block_transfer=True) as fp:
55-
pass
56-
self.assertEqual(context.exception.code, 0x05040001)
57+
fp.write(data)
58+
# Accept both possible abort codes for unsupported block download
59+
self.assertIn(context.exception.code, [0x05040001, 0x05040003, 0x06010002])
5760

5861
def test_expedited_upload_default_value_visible_string(self):
5962
device_name = self.remote_node.sdo["Manufacturer device name"].raw

0 commit comments

Comments
 (0)