Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Disregard OD variable description in SdoClient.upload() #592

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 10 commits into
base: master
Choose a base branch
from
13 changes: 12 additions & 1 deletion canopen/sdo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,18 @@ def __init__(self, sdo_node: SdoBase, od: objectdictionary.ODVariable):
variable.Variable.__init__(self, od)

def get_data(self) -> bytes:
return self.sdo_node.upload(self.od.index, self.od.subindex)
data = self.sdo_node.upload(self.od.index, self.od.subindex)
response_size = len(data)

# If size is available through variable in OD, then use the smaller of the two sizes.
# Some devices send U32/I32 even if variable is smaller in OD
if self.od.fixed_size:
# Get the size in bytes for this variable
var_size = len(self.od) // 8
if response_size is None or var_size < response_size:
# Truncate the data to specified size
data = data[:var_size]
return data

def set_data(self, data: bytes):
force_segment = self.od.data_type == objectdictionary.DOMAIN
Expand Down
17 changes: 6 additions & 11 deletions canopen/sdo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ def abort(self, abort_code=0x08000000):
def upload(self, index: int, subindex: int) -> bytes:
"""May be called to make a read operation without an Object Dictionary.

No validation against the Object Dictionary is performed, even if an object description
would be available. The length of the returned data depends only on the transferred
amount, possibly truncated to the size indicated by the server.

:param index:
Index of object to read.
:param subindex:
Expand All @@ -121,17 +125,8 @@ def upload(self, index: int, subindex: int) -> bytes:
response_size = fp.size
data = fp.read()

# If size is available through variable in OD, then use the smaller of the two sizes.
# Some devices send U32/I32 even if variable is smaller in OD
var = self.od.get_variable(index, subindex)
if var is not None:
# Found a matching variable in OD
if var.fixed_size:
# Get the size in bytes for this variable
var_size = len(var) // 8
if response_size is None or var_size < response_size:
# Truncate the data to specified size
data = data[0:var_size]
if response_size and response_size < len(data):
data = data[:response_size]
Comment on lines +128 to +129
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a little conflicted on what takes precedence: The actual amount of data transferred in the SDO, or the data size indicated by the size fields. Does the 301 standard mention this at all?

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't found any solution to this dilemma in the standard. My interpretation is that specifying the size in advance is the definitive answer. I doubt it will ever make a difference, as that would be a pretty awful server implementation.

In the end, what counts is the receiver's (client's) understanding of the data. And for that, the OD is needed anyway. If someone needs the full data in addition to the indicated size (to check for differences), they can just use the .open() API directly, without this mid-level convenience API.

Copy link
Collaborator

@sveinse sveinse Jun 22, 2025

Choose a reason for hiding this comment

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

When making protocol one should hope for the best and plan for the worst and not just expect happy path. So I believe its just to figure out how to resolve when a remote server replies with odd responses.

There are datatypes which is variable, such as string (which happens to be used in the test for this PR), so the OD cannot resolve them all.

Many times I'd wished that the protocol test suite from CiA would be public so we could see how they resolve it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, one almost sound reason I could see for sending extra data would be string termination. For convenience, the size might indicate the string length, but the data could be extended to include a NUL terminator byte. Which kind of makes sense in a C-based implementation.

Anyway, since there is already an alternative API in this library (file stream), let's wait and see if anybody comes back complaining about this change. I doubt many people are even using this API directly.

return data

def download(
Expand Down
27 changes: 23 additions & 4 deletions test/test_sdo.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,16 @@ def test_expedited_upload(self):

# UNSIGNED8 without padded data part (see issue #5)
self.data = [
(TX, b'\x40\x00\x14\x02\x00\x00\x00\x00'),
(RX, b'\x4f\x00\x14\x02\xfe')
(TX, b'\x40\x00\x14\x02\x00\x00\x00\x00'), # upload initiate 0x1400:02
(RX, b'\x4f\x00\x14\x02\xfe'), # expedited, size=1
]
trans_type = self.network[2].sdo[0x1400]['Transmission type RPDO 1'].raw
self.assertEqual(trans_type, 254)

# Same with padding to a full SDO frame
self.data = [
(TX, b'\x40\x00\x14\x02\x00\x00\x00\x00'), # upload initiate 0x1400:02
(RX, b'\x42\x00\x14\x02\xfe\x00\x00\x00'), # expedited, no size indicated
]
trans_type = self.network[2].sdo[0x1400]['Transmission type RPDO 1'].raw
self.assertEqual(trans_type, 254)
Expand All @@ -102,9 +110,9 @@ def test_size_not_specified(self):
(TX, b'\x40\x00\x14\x02\x00\x00\x00\x00'),
(RX, b'\x42\x00\x14\x02\xfe\x00\x00\x00')
]
# Make sure the size of the data is 1 byte
# This method used to truncate to 1 byte, but returns raw content now
data = self.network[2].sdo.upload(0x1400, 2)
self.assertEqual(data, b'\xfe')
self.assertEqual(data, b'\xfe\x00\x00\x00')
self.assertTrue(self.message_sent)

def test_expedited_download(self):
Expand All @@ -131,6 +139,17 @@ def test_segmented_upload(self):
device_name = self.network[2].sdo[0x1008].raw
self.assertEqual(device_name, "Tiny Node - Mega Domains !")

def test_segmented_upload_too_much_data(self):
# Server sends 5 bytes, but indicated size 4
self.data = [
(TX, b'\x40\x08\x10\x00\x00\x00\x00\x00'), # upload initiate, 0x1008:00
(RX, b'\x41\x08\x10\x00\x04\x00\x00\x00'), # segmented, size indicated, 4 bytes
(TX, b'\x60\x00\x00\x00\x00\x00\x00\x00'), # upload segment
(RX, b'\x05\x54\x69\x6E\x79\x20\x00\x00'), # segment complete, 5 bytes
]
device_name = self.network[2].sdo[0x1008].raw
self.assertEqual(device_name, "Tiny")

def test_segmented_download(self):
self.data = [
(TX, b'\x21\x00\x20\x00\x0d\x00\x00\x00'),
Expand Down
Loading