Skip to content

Server sdo block upload (based on v2.3.0) #559

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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions canopen/sdo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@
if seqno == self._ackseq + 1:
self._ackseq = seqno
else:
logger.debug('Wrong seqno')

Check warning on line 533 in canopen/sdo/client.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/client.py#L533

Added line #L533 was not covered by tests
# Wrong sequence number
response = self._retransmit()
res_command, = struct.unpack_from("B", response)
Expand Down
33 changes: 26 additions & 7 deletions canopen/sdo/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

# Command, index, subindex
SDO_STRUCT = struct.Struct("<BHB")
SDO_BLOCKINIT_STRUCT = "<BHBI" # Command + seqno, index, subindex, size
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In contrast to the others, this is not a struct.Struct object, why?

And it seems to be unused.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honnest: I wrote this code some years ago, made it work for our application, and stumbled across it missing in my new PC (unittests on code not running anymore).
So I made a PR with that old code fitted into release 2.3.0. So I can only guess what was in my mind then.

The reason for putting it in a contained class is that the block transfer has some running states to keep (as the command byte does not adhere to normal SDO transfers for expedited or segmented). I did some more sniffer and test code back then and this is a result of all the combined work (it works with the CanOpenNode C firmware on several microprocessor brands).

And: yes we would test it, but I don't know in what timeframe that would be, as this piece of code does the job for us. But getting block transfers into the main branch would be beneficial if I or any of my colleagues would again forget to install our canopen lib and install the public code base.

SDO_BLOCKACK_STRUCT = struct.Struct("<BBB") # c + ackseq + new blocksize
SDO_BLOCKEND_STRUCT = struct.Struct("<BH") # c + CRC
SDO_ABORT_STRUCT = struct.Struct("<BHBI") # c +i + si + Abort code

# Command[5-7]
REQUEST_SEGMENT_DOWNLOAD = 0 << 5
REQUEST_DOWNLOAD = 1 << 5
REQUEST_UPLOAD = 2 << 5
Expand All @@ -19,15 +24,29 @@
RESPONSE_BLOCK_DOWNLOAD = 5 << 5
RESPONSE_BLOCK_UPLOAD = 6 << 5

# Block transfer sub-commands, Command[0-1]
SUB_COMMAND_MASK = 0x3
INITIATE_BLOCK_TRANSFER = 0
END_BLOCK_TRANSFER = 1
BLOCK_TRANSFER_RESPONSE = 2
START_BLOCK_UPLOAD = 3

EXPEDITED = 0x2
SIZE_SPECIFIED = 0x1
BLOCK_SIZE_SPECIFIED = 0x2
CRC_SUPPORTED = 0x4
NO_MORE_DATA = 0x1
NO_MORE_BLOCKS = 0x80
TOGGLE_BIT = 0x10
EXPEDITED = 0x2 # Expedited and segmented
SIZE_SPECIFIED = 0x1 # All transfers
BLOCK_SIZE_SPECIFIED = 0x2 # Block transfer: size specified in message
CRC_SUPPORTED = 0x4 # client/server CRC capable
NO_MORE_DATA = 0x1 # Segmented: last segment
NO_MORE_BLOCKS = 0x80 # Block transfer: last segment
TOGGLE_BIT = 0x10 # segmented toggle mask

# Block states
BLOCK_STATE_NONE = -1
BLOCK_STATE_INIT = 0 # state when entering
BLOCK_STATE_UPLOAD = 0x10 # delimiter, used for block type check
BLOCK_STATE_UP_INIT_RESP = 0x11 # state when entering, response during upload
BLOCK_STATE_UP_DATA = 0x12 # Upload Data transfer state
BLOCK_STATE_UP_END = 0x13 # End of Upload block transfers
BLOCK_STATE_DOWNLOAD = 0x20 # delimiter, used for block type check
BLOCK_STATE_DL_DATA = 0x24 # Download Data transfer state
BLOCK_STATE_DL_END = 0x25 # End of Download block transfers

18 changes: 15 additions & 3 deletions canopen/sdo/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

from typing import Union

class SdoError(Exception):
pass
Expand Down Expand Up @@ -44,9 +44,15 @@
0x08000024: "No data available",
}

def __init__(self, code: int):
def __init__(self, code: Union[int, str]):
#: Abort code
self.code = code
if isinstance(code, str):
try:
self.code = self.from_string(code)
except ValueError as e:
raise ValueError(f"Unknown SDO abort description: {code}") from e

Check warning on line 53 in canopen/sdo/exceptions.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/exceptions.py#L50-L53

Added lines #L50 - L53 were not covered by tests
else:
self.code = code

def __str__(self):
text = f"Code 0x{self.code:08X}"
Expand All @@ -58,6 +64,12 @@
"""Compare two exception objects based on SDO abort code."""
return self.code == other.code

def from_string(self, text):
code = list(SdoAbortedError.CODES.keys())[

Check warning on line 68 in canopen/sdo/exceptions.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/exceptions.py#L68

Added line #L68 was not covered by tests
list(SdoAbortedError.CODES.values()).index(text)
]
return code

Check warning on line 71 in canopen/sdo/exceptions.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/exceptions.py#L71

Added line #L71 was not covered by tests


class SdoCommunicationError(SdoError):
"""No or unexpected response from slave."""
221 changes: 214 additions & 7 deletions canopen/sdo/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
logger = logging.getLogger(__name__)


class SdoBlockException(SdoAbortedError):
""" Dedicated SDO Block exception. """

class SdoServer(SdoBase):
"""Creates an SDO server."""

Expand All @@ -27,8 +30,14 @@
self._index = None
self._subindex = None
self.last_received_error = 0x00000000
self.sdo_block = None

def on_request(self, can_id, data, timestamp):
logger.debug('on_request')
if self.sdo_block and self.sdo_block.state != BLOCK_STATE_NONE:
self.process_block(data)
return

Check warning on line 39 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L38-L39

Added lines #L38 - L39 were not covered by tests

command, = struct.unpack_from("B", data, 0)
ccs = command & 0xE0

Expand Down Expand Up @@ -57,6 +66,82 @@
self.abort()
logger.exception(exc)

def process_block(self, request):
"""
Process a block request, using a state mechanisme from SdoBlock class
to handle the different states of the block transfer.

:param request:
CAN message containing EMCY or SDO request.
"""

logger.debug('process_block')
command, _, _, code = SDO_ABORT_STRUCT.unpack_from(request)

Check warning on line 79 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L78-L79

Added lines #L78 - L79 were not covered by tests
if command == 0x80:
# Abort received
logger.error('Abort: 0x%08X' % code)
self.sdo_block = None
return

Check warning on line 84 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L82-L84

Added lines #L82 - L84 were not covered by tests

if BLOCK_STATE_UPLOAD < self.sdo_block.state < BLOCK_STATE_DOWNLOAD:
logger.debug('BLOCK_STATE_UPLOAD')
command, _, _= SDO_STRUCT.unpack_from(request)

Check warning on line 88 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L87-L88

Added lines #L87 - L88 were not covered by tests
# in upload state
if self.sdo_block.state == BLOCK_STATE_UP_INIT_RESP:
logger.debug('BLOCK_STATE_UP_INIT_RESP')

Check warning on line 91 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L91

Added line #L91 was not covered by tests
#init response was sent, client required to send new request
if (command & REQUEST_BLOCK_UPLOAD) != REQUEST_BLOCK_UPLOAD:
raise SdoBlockException("Unknown SDO command specified")

Check warning on line 94 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L94

Added line #L94 was not covered by tests
if (command & START_BLOCK_UPLOAD) != START_BLOCK_UPLOAD:
raise SdoBlockException("Unknown SDO command specified")

Check warning on line 96 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L96

Added line #L96 was not covered by tests

# now start blasting data to client from server
self.sdo_block.update_state(BLOCK_STATE_UP_DATA)

Check warning on line 99 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L99

Added line #L99 was not covered by tests

blocks = self.sdo_block.get_upload_blocks()

Check warning on line 101 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L101

Added line #L101 was not covered by tests
for block in blocks:
self.send_response(block)

Check warning on line 103 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L103

Added line #L103 was not covered by tests

elif self.sdo_block.state == BLOCK_STATE_UP_DATA:
logger.debug('BLOCK_STATE_UP_DATA')
command, ackseq, newblk = SDO_BLOCKACK_STRUCT.unpack_from(request)

Check warning on line 107 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L106-L107

Added lines #L106 - L107 were not covered by tests
if (command & REQUEST_BLOCK_UPLOAD) != REQUEST_BLOCK_UPLOAD:
raise SdoBlockException("Unknown SDO command specified")

Check warning on line 109 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L109

Added line #L109 was not covered by tests
elif (command & BLOCK_TRANSFER_RESPONSE) != BLOCK_TRANSFER_RESPONSE:
raise SdoBlockException("Unknown SDO command specified")

Check warning on line 111 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L111

Added line #L111 was not covered by tests
elif (ackseq != self.sdo_block.last_seqno):
self.sdo_block.data_uploaded = self.sdo_block.data_succesfull_upload

Check warning on line 113 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L113

Added line #L113 was not covered by tests

if self.sdo_block.size == self.sdo_block.data_uploaded:
logger.debug('BLOCK_STATE_UP_DATA last data')
self.sdo_block.update_state(BLOCK_STATE_UP_END)
response = bytearray(8)
command = RESPONSE_BLOCK_UPLOAD
command |= END_BLOCK_TRANSFER
n = self.sdo_block.last_bytes << 2
command |= n
logger.debug('Last no byte: %d, CRC: x%04X',

Check warning on line 123 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L116-L123

Added lines #L116 - L123 were not covered by tests
self.sdo_block.last_bytes,
self.sdo_block.crc_value)
SDO_BLOCKEND_STRUCT.pack_into(response, 0, command,

Check warning on line 126 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L126

Added line #L126 was not covered by tests
self.sdo_block.crc_value)
self.send_response(response)

Check warning on line 128 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L128

Added line #L128 was not covered by tests
else:
blocks = self.sdo_block.get_upload_blocks()

Check warning on line 130 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L130

Added line #L130 was not covered by tests
for block in blocks:
self.send_response(block)

Check warning on line 132 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L132

Added line #L132 was not covered by tests

elif self.sdo_block.state == BLOCK_STATE_UP_END:
self.sdo_block = None

Check warning on line 135 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L135

Added line #L135 was not covered by tests

elif BLOCK_STATE_DOWNLOAD < self.sdo_block.state:
# in download state
logger.debug('BLOCK_STATE_DOWNLOAD')

Check warning on line 139 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L139

Added line #L139 was not covered by tests
else:
# in neither
raise SdoBlockException("Data can not be transferred or stored to the application "

Check warning on line 142 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L142

Added line #L142 was not covered by tests
"because of the present device state")

def init_upload(self, request):
_, index, subindex = SDO_STRUCT.unpack_from(request)
self._index = index
Expand All @@ -76,10 +161,10 @@
struct.pack_into("<L", response, 4, size)
self._buffer = bytearray(data)
self._toggle = 0

SDO_STRUCT.pack_into(response, 0, res_command, index, subindex)
self.send_response(response)


def segmented_upload(self, command):
if command & TOGGLE_BIT != self._toggle:
# Toggle bit mismatch
Expand All @@ -106,12 +191,33 @@
response[1:1 + size] = data
self.send_response(response)

def block_upload(self, data):
# We currently don't support BLOCK UPLOAD
# according to CIA301 the server is allowed
# to switch to regular upload
logger.info("Received block upload, switch to regular SDO upload")
self.init_upload(data)
def block_upload(self, request):
"""
Process an initial block upload request.
Create a CAN response message and update the state of the SDO block.

:param request:
CAN message containing SDO request.
"""
logging.debug('Enter server block upload')
self.sdo_block = SdoBlock(self._node, request)

Check warning on line 203 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L202-L203

Added lines #L202 - L203 were not covered by tests

res_command = RESPONSE_BLOCK_UPLOAD
res_command |= BLOCK_SIZE_SPECIFIED
res_command |= self.sdo_block.crc
res_command |= INITIATE_BLOCK_TRANSFER
logging.debug('CMD: %02X', res_command)
response = bytearray(8)

Check warning on line 210 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L205-L210

Added lines #L205 - L210 were not covered by tests

struct.pack_into(SDO_STRUCT.format+'I', # add size

Check warning on line 212 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L212

Added line #L212 was not covered by tests
response, 0,
res_command,
self.sdo_block.index,
self.sdo_block.subindex,
self.sdo_block.size)
logging.debug('response %s', response)
self.sdo_block.update_state(BLOCK_STATE_UP_INIT_RESP)
self.send_response(response)

Check warning on line 220 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L218-L220

Added lines #L218 - L220 were not covered by tests

def request_aborted(self, data):
_, index, subindex, code = struct.unpack_from("<BHBL", data)
Expand Down Expand Up @@ -217,3 +323,104 @@
When node responds with an error.
"""
return self._node.set_data(index, subindex, data)

class SdoBlock():
"""
SdoBlock class to handle block transfer. It keeps track of the
current state and the prepares data to be transferred.
"""
state = BLOCK_STATE_NONE
crc = False
data_uploaded = 0
data_succesfull_upload = 0
last_bytes = 0
crc_value = 0
last_seqno = 0

def __init__(self, node, request, docrc=False):
"""
:param node:
Node object owning the server
:param request:
CAN message containing SDO request.
:param docrc:
If True, CRC is calculated and checked.
"""
command, index, subindex = SDO_STRUCT.unpack_from(request)

Check warning on line 349 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L349

Added line #L349 was not covered by tests
# only do crc if crccheck lib is available _and_ if requested
_req_crc = (command & CRC_SUPPORTED) == CRC_SUPPORTED

Check warning on line 351 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L351

Added line #L351 was not covered by tests

if (command & SUB_COMMAND_MASK) == INITIATE_BLOCK_TRANSFER:
self.state = BLOCK_STATE_INIT

Check warning on line 354 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L354

Added line #L354 was not covered by tests
else:
raise SdoBlockException("Unknown SDO command specified")

Check warning on line 356 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L356

Added line #L356 was not covered by tests

# TODO: CRC of data if requested
self.crc = CRC_SUPPORTED if (docrc & _req_crc) else 0
self._node = node
self.index = index
self.subindex = subindex
self.req_blocksize = request[4]
self.seqno = 0

Check warning on line 364 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L359-L364

Added lines #L359 - L364 were not covered by tests
if not 1 <= self.req_blocksize <= 127:
raise SdoBlockException("Invalid block size")

Check warning on line 366 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L366

Added line #L366 was not covered by tests

self.data = self._node.get_data(index,

Check warning on line 368 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L368

Added line #L368 was not covered by tests
subindex,
check_readable=True)
self.size = len(self.data)

Check warning on line 371 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L371

Added line #L371 was not covered by tests

def update_state(self, new_state):
"""
Update the state of the SDO block transfer. The state is
updated only if the new state is higher than the current
state. Otherwise an exception is raised.
"""
logging.debug('update_state %X -> %X', self.state, new_state)

Check warning on line 379 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L379

Added line #L379 was not covered by tests
if new_state >= self.state:
self.state = new_state

Check warning on line 381 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L381

Added line #L381 was not covered by tests
else:
raise SdoBlockException("Data can not be transferred or stored to the application "

Check warning on line 383 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L383

Added line #L383 was not covered by tests
"because of the present device state")

def get_upload_blocks(self):
"""
Get the blocks of data to be sent to the client. The blocks are
created in a messages list of bytearrays.
"""

msgs = []

Check warning on line 392 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L392

Added line #L392 was not covered by tests

# seq no 1 - 127, not 0 -..
for seqno in range(1,self.req_blocksize+1):
logger.debug('SEQNO %d', seqno)
response = bytearray(8)
command = 0

Check warning on line 398 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L396-L398

Added lines #L396 - L398 were not covered by tests
if self.size <= (self.data_uploaded + 7):
# no more segments after this
command |= NO_MORE_BLOCKS

Check warning on line 401 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L401

Added line #L401 was not covered by tests

command |= seqno
response[0] = command

Check warning on line 404 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L403-L404

Added lines #L403 - L404 were not covered by tests
for i in range(7):
databyte = self.get_data_byte()

Check warning on line 406 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L406

Added line #L406 was not covered by tests
if databyte != None:
response[i+1] = databyte

Check warning on line 408 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L408

Added line #L408 was not covered by tests
else:
self.last_bytes = 7 - i
break
msgs.append(response)
self.last_seqno = seqno

Check warning on line 413 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L410-L413

Added lines #L410 - L413 were not covered by tests

if self.size == self.data_uploaded:
break
logger.debug(msgs)
return msgs

Check warning on line 418 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L416-L418

Added lines #L416 - L418 were not covered by tests

def get_data_byte(self):
"""Get the next byte of data to be sent to the client."""
if self.data_uploaded < self.size:
self.data_uploaded += 1
return self.data[self.data_uploaded-1]
return None

Check warning on line 425 in canopen/sdo/server.py

View check run for this annotation

Codecov / codecov/patch

canopen/sdo/server.py#L423-L425

Added lines #L423 - L425 were not covered by tests

15 changes: 8 additions & 7 deletions test/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ def test_expedited_upload(self):
vendor_id = self.remote_node.sdo[0x1400][1].raw
self.assertEqual(vendor_id, 0x99)

def test_block_upload_switch_to_expedite_upload(self):
with self.assertRaises(canopen.SdoCommunicationError) as context:
with self.remote_node.sdo[0x1008].open('r', block_transfer=True) as fp:
pass
# We get this since the sdo client don't support the switch
# from block upload to expedite upload
self.assertEqual("Unexpected response 0x41", str(context.exception))
# Remove this test, as Block upload is now supported:
# def test_block_upload_switch_to_expedite_upload(self):
# with self.assertRaises(canopen.SdoCommunicationError) as context:
# with self.remote_node.sdo[0x1008].open('r', block_transfer=True) as fp:
# pass
# # We get this since the sdo client don't support the switch
# # from block upload to expedite upload
# self.assertEqual("Unexpected response 0x41", str(context.exception))

def test_block_download_not_supported(self):
data = b"TEST DEVICE"
Expand Down
Loading