diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3efb3261..2962fc69 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, '3.x'] + python-version: ['3.x'] steps: - uses: actions/checkout@v2 diff --git a/canopen/lss.py b/canopen/lss.py index d77f528f..d375e852 100644 --- a/canopen/lss.py +++ b/canopen/lss.py @@ -248,7 +248,7 @@ def fast_scan(self): :return: True if a slave is found. False if there is no candidate. - list is the LSS identities [vendor_id, product_code, revision_number, seerial_number] + list is the LSS identities [vendor_id, product_code, revision_number, serial_number] :rtype: bool, list """ lss_id = [0] * 4 @@ -265,21 +265,21 @@ def fast_scan(self): if not self.__send_fast_scan_message(lss_id[lss_sub], lss_bit_check, lss_sub, lss_next): lss_id[lss_sub] |= 1< "Network": Backend specific channel for the CAN interface. :param str bustype: Name of the interface. See - `python-can manual `__ + `python-can manual `__ for full list of supported interfaces. :param int bitrate: Bitrate in bit/s. diff --git a/canopen/node/local.py b/canopen/node/local.py index ecce2dff..9e0a80b3 100644 --- a/canopen/node/local.py +++ b/canopen/node/local.py @@ -100,6 +100,12 @@ def set_data( if check_writable and not obj.writable: raise SdoAbortedError(0x06010002) + # Check length matches type (length of od variable is in bits) + if obj.data_type in objectdictionary.NUMBER_TYPES and ( + not 8 * len(data) == len(obj) + ): + raise SdoAbortedError(0x06070010) + # Try callbacks for callback in self._write_callbacks: callback(index=index, subindex=subindex, od=obj, data=data) diff --git a/canopen/node/remote.py b/canopen/node/remote.py index 864ffeb3..5aca17ff 100644 --- a/canopen/node/remote.py +++ b/canopen/node/remote.py @@ -24,7 +24,7 @@ class RemoteNode(BaseNode): Object dictionary as either a path to a file, an ``ObjectDictionary`` or a file like object. :param load_od: - Enable the Object Dictionary to be sent trough SDO's to the remote + Enable the Object Dictionary to be sent through SDO's to the remote node at startup. """ diff --git a/canopen/objectdictionary/eds.py b/canopen/objectdictionary/eds.py index afd94159..872df234 100644 --- a/canopen/objectdictionary/eds.py +++ b/canopen/objectdictionary/eds.py @@ -52,7 +52,7 @@ def import_eds(source, node_id): else: for rate in [10, 20, 50, 125, 250, 500, 800, 1000]: baudPossible = int( - eds.get("DeviceInfo", "Baudrate_%i" % rate, fallback='0'), 0) + eds.get("DeviceInfo", "BaudRate_%i" % rate, fallback='0'), 0) if baudPossible != 0: od.device_information.allowed_baudrates.add(rate*1000) @@ -85,7 +85,7 @@ def import_eds(source, node_id): pass if eds.has_section("DeviceComissioning"): - od.bitrate = int(eds.get("DeviceComissioning", "Baudrate")) * 1000 + od.bitrate = int(eds.get("DeviceComissioning", "BaudRate")) * 1000 od.node_id = int(eds.get("DeviceComissioning", "NodeID"), 0) for section in eds.sections(): @@ -369,10 +369,10 @@ def export_record(var, eds): "EdsVersion": 4.2, } - file_info.setdefault("ModificationDate", defmtime.strftime("%m-%d-%Y")) - file_info.setdefault("ModificationTime", defmtime.strftime("%I:%m%p")) - for k, v in origFileInfo.items(): - file_info.setdefault(k, v) + file_info.setdefault("ModificationDate", defmtime.strftime("%m-%d-%Y")) + file_info.setdefault("ModificationTime", defmtime.strftime("%I:%m%p")) + for k, v in origFileInfo.items(): + file_info.setdefault(k, v) eds.add_section("FileInfo") for k, v in file_info.items(): @@ -407,13 +407,13 @@ def export_record(var, eds): for rate in od.device_information.allowed_baudrates.union( {10e3, 20e3, 50e3, 125e3, 250e3, 500e3, 800e3, 1000e3}): eds.set( - "DeviceInfo", "Baudrate_%i" % (rate/1000), + "DeviceInfo", "BaudRate_%i" % (rate/1000), int(rate in od.device_information.allowed_baudrates)) if device_commisioning and (od.bitrate or od.node_id): eds.add_section("DeviceComissioning") if od.bitrate: - eds.set("DeviceComissioning", "Baudrate", int(od.bitrate / 1000)) + eds.set("DeviceComissioning", "BaudRate", int(od.bitrate / 1000)) if od.node_id: eds.set("DeviceComissioning", "NodeID", int(od.node_id)) diff --git a/canopen/sdo/client.py b/canopen/sdo/client.py index 0ed083e4..7e0f58bf 100644 --- a/canopen/sdo/client.py +++ b/canopen/sdo/client.py @@ -628,6 +628,8 @@ def __init__(self, sdo_client, index, subindex=0, size=None, request_crc_support self._seqno = 0 self._crc = sdo_client.crc_cls() self._last_bytes_sent = 0 + self._current_block = [] + self._retransmitting = False command = REQUEST_BLOCK_DOWNLOAD | INITIATE_BLOCK_TRANSFER if request_crc_support: command |= CRC_SUPPORTED @@ -708,7 +710,10 @@ def send(self, b, end=False): request[1:len(b) + 1] = b self.sdo_client.send_request(request) self.pos += len(b) - if self.crc_supported: + # Add the sent data to the current block buffer + self._current_block.append(b) + # Don't calculate crc if retransmitting + if self.crc_supported and not self._retransmitting: # Calculate CRC self._crc.process(b) if self._seqno >= self._blksize: @@ -731,14 +736,37 @@ def _block_ack(self): raise SdoCommunicationError("Server did not respond with a " "block download response") if ackseq != self._blksize: - self.sdo_client.abort(0x05040003) - raise SdoCommunicationError( - ("%d of %d sequences were received. " - "Retransmission is not supported yet.") % (ackseq, self._blksize)) + # Sequence error, try to retransmit + self._retransmit(ackseq, blksize) + # We should be back in sync + return + # Clear the current block buffer + self._current_block = [] logger.debug("All %d sequences were received successfully", ackseq) logger.debug("Server requested a block size of %d", blksize) self._blksize = blksize self._seqno = 0 + + def _retransmit(self, ackseq, blksize): + """Retransmit the failed block""" + logger.info(("%d of %d sequences were received. " + "Will start retransmission") % (ackseq, self._blksize)) + # Sub blocks betwen ackseq and end of corrupted block need to be resent + # Get the part of the block to resend + block = self._current_block[ackseq:] + # Go back to correct position in stream + self.pos = self.pos - (len(block) * 7) + # Reset the _current_block before starting the retransmission + self._current_block = [] + # Reset _seqno and update blksize + self._seqno = 0 + self._blksize = blksize + # We are retransmitting + self._retransmitting = True + # Resend the block + for b in block: + self.write(b) + self._retransmitting = False def close(self): """Closes the stream.""" diff --git a/examples/eds/e35.eds b/examples/eds/e35.eds index 4978b818..17b24879 100644 --- a/examples/eds/e35.eds +++ b/examples/eds/e35.eds @@ -13,19 +13,19 @@ ModifiedBy=Manufacturer [DeviceInfo] VendorName=Manufacturer -VendorNumber=001 +VendorNumber=101 ProductName=example ProductNumber=25 RevisionNumber=295 OrderCode=25 -Baudrate_10=1 -Baudrate_20=1 -Baudrate_50=1 -Baudrate_125=1 -Baudrate_250=1 -Baudrate_500=1 -Baudrate_800=0 -Baudrate_1000=1 +BaudRate_10=1 +BaudRate_20=1 +BaudRate_50=1 +BaudRate_125=1 +BaudRate_250=1 +BaudRate_500=1 +BaudRate_800=0 +BaudRate_1000=1 SimpleBootUpMaster=0 SimpleBootUpSlave=1 Granularity=8 @@ -243,7 +243,7 @@ HighLimit=0x7F ParameterValue=0x20 [2000sub2] -ParameterName=Baudrate +ParameterName=BaudRate ObjectType=0x7 DataType=0x0005 AccessType=rw @@ -324,7 +324,7 @@ LowLimit=0x1 HighLimit=0x7F [2001sub2] -ParameterName=Baudrate +ParameterName=BaudRate ObjectType=0x7 DataType=0x0005 AccessType=rw diff --git a/test/sample.eds b/test/sample.eds index b01a9ee5..bea6b9c3 100644 --- a/test/sample.eds +++ b/test/sample.eds @@ -32,7 +32,7 @@ LSS_Supported=0 [DeviceComissioning] NodeID=2 NodeName=Some name -Baudrate=500 +BaudRate=500 NetNumber=0 LSS_SerialNumber=0 diff --git a/test/test_local.py b/test/test_local.py index aed3a28c..f4119d44 100644 --- a/test/test_local.py +++ b/test/test_local.py @@ -71,6 +71,16 @@ def test_expedited_download(self): value = self.local_node.sdo[0x2004].raw self.assertEqual(value, 0xfeff) + def test_expedited_download_wrong_datatype(self): + # Try to write 32 bit in integer16 type + with self.assertRaises(canopen.SdoAbortedError) as error: + self.remote_node.sdo.download(0x2001, 0x0, bytes([10, 10, 10, 10])) + self.assertEqual(error.exception.code, 0x06070010) + # Try to write normal 16 bit word, should be ok + self.remote_node.sdo.download(0x2001, 0x0, bytes([10, 10])) + value = self.remote_node.sdo.upload(0x2001, 0x0) + self.assertEqual(value, bytes([10, 10])) + def test_segmented_download(self): self.remote_node.sdo[0x2000].raw = "Another cool device" value = self.local_node.sdo[0x2000].data