From 1096eb0f98e921f311329c1c1935a50aa470098a Mon Sep 17 00:00:00 2001 From: Pier-Yves Lessard Date: Tue, 1 Jan 2019 19:21:03 -0800 Subject: [PATCH] Support variable Link Layer mtu via parameter ll_data_length --- doc/source/isotp/implementation.rst | 10 +++ doc/source/isotp/socket.rst | 4 +- isotp/protocol.py | 40 ++++++---- isotp/tpsock/__init__.py | 4 +- test/test_helper_classes.py | 36 ++++++++- test/test_transport_layer.py | 117 +++++++++++++++++++++++++++- 6 files changed, 185 insertions(+), 26 deletions(-) diff --git a/doc/source/isotp/implementation.rst b/doc/source/isotp/implementation.rst index 03bb837..3fafce1 100755 --- a/doc/source/isotp/implementation.rst +++ b/doc/source/isotp/implementation.rst @@ -43,6 +43,16 @@ The transport layer ``params`` parameter must be a dictionary with the following The single-byte Block Size to include in the flow control message that the layer will send when receiving data. Represents to number of consecutive frame that a sender should send before expecting the layer to send a flow control message. 0 Means infinitely large block size (implying no flow control message) +.. _param_ll_data_length: + +.. attribute:: ll_data_length + :annotation: (int) + + **default: 8** + + The number of bytes that the Link Layer (CAN layer) can transport. For CAN 2.0, this value should stay 8, for other type of link layer (such as CAN FD) any integer greater or equal to 4 can be provided. + This parameter will affect the size of the CAN messages. + .. _param_squash_stmin_requirement: .. attribute:: squash_stmin_requirement diff --git a/doc/source/isotp/socket.rst b/doc/source/isotp/socket.rst index 82777e2..916f4c5 100755 --- a/doc/source/isotp/socket.rst +++ b/doc/source/isotp/socket.rst @@ -122,7 +122,7 @@ To configure a socket, few methods are available Sets the link layer options. Default values are set to work with CAN 2.0. Link layer may be configure to work in CAN FD. - :param mtu: The internal CAN frame structure size. Possible values are defined in :class:`isotp.socket.mtu` + :param mtu: The internal CAN frame structure size. Possible values are defined in :class:`isotp.socket.LinkLayerProtocol` :type mtu: int :param tx_dl: The CAN message payload length. For CAN 2.0, this value should be 8. For CAN FD, possible values are 8,12,16,20,24,32,48,64 @@ -138,7 +138,7 @@ To configure a socket, few methods are available :undoc-members: :member-order: bysource -.. autoclass:: isotp.socket.mtu +.. autoclass:: isotp.socket.LinkLayerProtocol :members: :undoc-members: :member-order: bysource diff --git a/isotp/protocol.py b/isotp/protocol.py index 556df16..a2e6ed5 100755 --- a/isotp/protocol.py +++ b/isotp/protocol.py @@ -51,7 +51,7 @@ class FlowStatus: Wait = 1 Overflow = 2 - def __init__(self, msg = None, start_of_data=0): + def __init__(self, msg = None, start_of_data=0, datalen=8): self.type = None self.length = None @@ -78,7 +78,7 @@ def __init__(self, msg = None, start_of_data=0): if len(msg.data) < self.length + 1: raise ValueError('Single Frame length is bigger than CAN frame length') - if self.length == 0 or self.length > 7-start_of_data: + if self.length == 0 or self.length > datalen-1-start_of_data: raise ValueError("Received Single Frame with invalid length of %d" % self.length) self.data = msg.data[1+start_of_data:][:self.length] @@ -87,15 +87,15 @@ def __init__(self, msg = None, start_of_data=0): raise ValueError('First frame must be at least %d bytes long' % (2+start_of_data)) self.length = ((int(msg.data[start_of_data]) & 0xF) << 8) | int(msg.data[start_of_data+1]) - if len(msg.data) < 8: + if len(msg.data) < datalen: if len(msg.data) < self.length + 2 + start_of_data: raise ValueError('First frame specifies a length that is inconsistent with underlying CAN message DLC') - self.data = msg.data[2+start_of_data:][:min(self.length, 6-start_of_data)] + self.data = msg.data[2+start_of_data:][:min(self.length, datalen-2-start_of_data)] elif self.type == self.Type.CONSECUTIVE_FRAME: self.seqnum = int(msg.data[start_of_data]) & 0xF - self.data = msg.data[start_of_data+1:] + self.data = msg.data[start_of_data+1:datalen] elif self.type == self.Type.FLOW_CONTROL: if len(msg.data) < 3+start_of_data: @@ -160,7 +160,7 @@ class TransportLayer: LOGGER_NAME = 'isotp' class Params: - __slots__ = 'stmin', 'blocksize', 'squash_stmin_requirement', 'rx_flowcontrol_timeout', 'rx_consecutive_frame_timeout', 'tx_padding', 'wftmax' + __slots__ = 'stmin', 'blocksize', 'squash_stmin_requirement', 'rx_flowcontrol_timeout', 'rx_consecutive_frame_timeout', 'tx_padding', 'wftmax', 'll_data_length' def __init__(self): self.stmin = 0 @@ -170,6 +170,7 @@ def __init__(self): self.rx_consecutive_frame_timeout = 1000 self.tx_padding = None self.wftmax = 0 + self.ll_data_length = 8 def set(self, key, val): setattr(self, key, val) @@ -215,7 +216,13 @@ def validate(self): raise ValueError('wftmax must be an integer') if self.wftmax < 0 : - raise ValueError('wftmax must be and integerequal or greater than 0') + raise ValueError('wftmax must be and integer equal or greater than 0') + + if not isinstance(self.ll_data_length, int): + raise ValueError('ll_data_length must be an integer') + + if self.ll_data_length < 4: + raise ValueError('ll_data_length must be at least 4 bytes') class Timer: def __init__(self, timeout): @@ -376,7 +383,7 @@ def process_rx(self, msg): # Decoding of message into PDU try: - pdu = PDU(msg, self.address.rx_prefix_size) + pdu = PDU(msg, start_of_data=self.address.rx_prefix_size, datalen=self.params.ll_data_length) except Exception as e: self.trigger_error(isotp.errors.InvalidCanDataError("Received invalid CAN frame. %s" % (str(e)))) self.stop_receiving() @@ -511,17 +518,17 @@ def process_tx(self): else: self.tx_buffer = bytearray(popped_object['data']) - - if len(self.tx_buffer) <= 7-len(self.address.tx_payload_prefix): # Single frame + if len(self.tx_buffer) <= self.params.ll_data_length-1-len(self.address.tx_payload_prefix): # Single frame msg_data = self.address.tx_payload_prefix + bytearray([0x0 | len(self.tx_buffer)]) + self.tx_buffer arbitration_id = self.address.get_tx_arbitraton_id(popped_object['target_address_type']) output_msg = self.make_tx_msg(arbitration_id, msg_data) else: # Multi frame + data_length = self.params.ll_data_length-2-len(self.address.tx_payload_prefix) self.tx_frame_length = len(self.tx_buffer) - msg_data = self.address.tx_payload_prefix + bytearray([0x10|((self.tx_frame_length >> 8) & 0xF), self.tx_frame_length&0xFF]) + self.tx_buffer[:6-len(self.address.tx_payload_prefix)] + msg_data = self.address.tx_payload_prefix + bytearray([0x10|((self.tx_frame_length >> 8) & 0xF), self.tx_frame_length&0xFF]) + self.tx_buffer[:data_length] arbitration_id = self.address.get_tx_arbitraton_id() output_msg = self.make_tx_msg(arbitration_id, msg_data) - self.tx_buffer = self.tx_buffer[6-len(self.address.tx_payload_prefix):] + self.tx_buffer = self.tx_buffer[data_length:] self.tx_state = self.TxState.WAIT_FC self.tx_seqnum = 1 self.start_rx_fc_timer() @@ -531,10 +538,11 @@ def process_tx(self): elif self.tx_state == self.TxState.TRANSMIT_CF: if self.timer_tx_stmin.is_timed_out() or self.params.squash_stmin_requirement: - msg_data = self.address.tx_payload_prefix + bytearray([0x20 | self.tx_seqnum]) + self.tx_buffer[:7-len(self.address.tx_payload_prefix)] + data_length = self.params.ll_data_length-1-len(self.address.tx_payload_prefix) + msg_data = self.address.tx_payload_prefix + bytearray([0x20 | self.tx_seqnum]) + self.tx_buffer[:data_length] arbitration_id = self.address.get_tx_arbitraton_id() output_msg = self.make_tx_msg(arbitration_id, msg_data) - self.tx_buffer = self.tx_buffer[7-len(self.address.tx_payload_prefix):] + self.tx_buffer = self.tx_buffer[data_length:] self.tx_seqnum = (self.tx_seqnum + 1 ) & 0xF self.timer_tx_stmin.start() self.tx_block_counter+=1 @@ -562,8 +570,8 @@ def set_address(self, address): self.logger.warning('Used rxid overlaps the range of ID reserved by ISO-15765 (0x7F4-0x7F6 and 0x7FA-0x7FB)') def pad_message_data(self, msg_data): - if len(msg_data) < 8 and self.params.tx_padding is not None: - msg_data.extend(bytearray([self.params.tx_padding & 0xFF] * (8-len(msg_data)))) + if len(msg_data) < self.params.ll_data_length and self.params.tx_padding is not None: + msg_data.extend(bytearray([self.params.tx_padding & 0xFF] * (self.params.ll_data_length-len(msg_data)))) def empty_rx_buffer(self): self.rx_buffer = bytearray() diff --git a/isotp/tpsock/__init__.py b/isotp/tpsock/__init__.py index 671106d..233b5db 100755 --- a/isotp/tpsock/__init__.py +++ b/isotp/tpsock/__init__.py @@ -43,7 +43,7 @@ class flags: RX_EXT_ADDR = 0x200 """ When sets, a different extended address can be used for reception than for transmission.""" -class mtu: +class LinkLayerProtocol: CAN = 16 """ Internal structure size of a CAN 2.0 frame""" @@ -62,7 +62,7 @@ class socket: # We want that syntax isotp.socket.flags and isotp.socket.mtu # This is a workaround for sphinx autodoc that fails to load docstring for nested-class members flags = flags - mtu = mtu + LinkLayerProtocol = LinkLayerProtocol def __init__(self, timeout=0.1): check_support() diff --git a/test/test_helper_classes.py b/test/test_helper_classes.py index e6e44e2..5358ec4 100755 --- a/test/test_helper_classes.py +++ b/test/test_helper_classes.py @@ -26,8 +26,8 @@ def test_timer(self): # Here we check that we decode properly ecah type of frame class TestPDUDecoding(unittest.TestCase): - def make_pdu(self, data): - return isotp.protocol.PDU(Message(data=bytearray(data))) + def make_pdu(self, data, start_of_data=0, datalen=8): + return isotp.protocol.PDU(Message(data=bytearray(data)),start_of_data=start_of_data, datalen=datalen) def test_decode_single_frame(self): with self.assertRaises(ValueError): @@ -69,6 +69,15 @@ def test_decode_single_frame(self): with self.assertRaises(ValueError): self.make_pdu([0x08, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]) + with self.assertRaises(ValueError): + self.make_pdu([0x05, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], datalen=5) + + with self.assertRaises(ValueError): + self.make_pdu([0x01, 0x07, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77], start_of_data = 1, datalen=8) + + with self.assertRaises(ValueError): + self.make_pdu([0x01, 0x06, 0x22, 0x33, 0x44, 0x55, 0x66], start_of_data = 1, datalen=7) + def test_decode_first_frame(self): with self.assertRaises(ValueError): # Empty payload @@ -104,6 +113,12 @@ def test_decode_first_frame(self): with self.assertRaises(ValueError): # Missing data byte frame = self.make_pdu([0x10, 0x0A, 0x11, 0x22, 0x33, 0x44, 0x55]) + with self.assertRaises(ValueError): # Missing data byte + frame = self.make_pdu([0x01, 0x10, 0x0A, 0x11, 0x22, 0x33, 0x44], start_of_data=1) + + with self.assertRaises(ValueError): # Missing data byte + frame = self.make_pdu([0x01, 0x10, 0x0A, 0x11, 0x22, 0x33, 0x44, 0x55], start_of_data=1, datalen=12) + frame = self.make_pdu([0x10, 0x0A, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66]) self.assertEqual(frame.type, isotp.protocol.PDU.Type.FIRST_FRAME) self.assertEqual(frame.data, bytearray([0x11, 0x22, 0x33, 0x44, 0x55, 0x66])) @@ -114,6 +129,11 @@ def test_decode_first_frame(self): self.assertEqual(frame.data, bytearray([0x11, 0x22, 0x33, 0x44, 0x55, 0x66])) self.assertEqual(frame.length, 0xABC) + frame = self.make_pdu([0x00, 0x00, 0x1A, 0xBC, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99], start_of_data=2, datalen=12) + self.assertEqual(frame.type, isotp.protocol.PDU.Type.FIRST_FRAME) + self.assertEqual(frame.data, bytearray([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88])) + self.assertEqual(frame.length, 0xABC) + def test_decode_consecutive_frame(self): with self.assertRaises(ValueError): # Empty payload @@ -134,6 +154,11 @@ def test_decode_consecutive_frame(self): self.assertEqual(frame.data, bytearray([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77])) self.assertEqual(frame.seqnum, 0xA) + frame = self.make_pdu([0x00, 0x2A, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77], start_of_data=1, datalen=6) + self.assertEqual(frame.type, isotp.protocol.PDU.Type.CONSECUTIVE_FRAME) + self.assertEqual(frame.data, bytearray([0x11, 0x22, 0x33, 0x44])) + self.assertEqual(frame.seqnum, 0xA) + def test_decode_flow_control(self): with self.assertRaises(ValueError): # Empty payload @@ -166,6 +191,13 @@ def test_decode_flow_control(self): self.assertEqual(frame.stmin, 0) self.assertEqual(frame.stmin_sec, 0) + frame = self.make_pdu([0xFF, 0xFF, 0x32, 0x01, 0x01], start_of_data=2) + self.assertEqual(frame.type, isotp.protocol.PDU.Type.FLOW_CONTROL) + self.assertEqual(frame.flow_status, isotp.protocol.PDU.FlowStatus.Overflow) + self.assertEqual(frame.blocksize, 1) + self.assertEqual(frame.stmin, 1) + self.assertEqual(frame.stmin_sec, 1/1000) + for i in range(3, 0xF): # Reserved Flow status with self.assertRaises(ValueError): frame = self.make_pdu([0x30 + i, 0x00, 0x00]) diff --git a/test/test_transport_layer.py b/test/test_transport_layer.py index 93e09f1..7ef070d 100755 --- a/test/test_transport_layer.py +++ b/test/test_transport_layer.py @@ -144,17 +144,17 @@ def test_long_multiframe_2_flow_control(self): self.stack.process() self.assert_sent_flow_control(stmin=5, blocksize=3) self.assertIsNone(self.rx_isotp_frame()) - self.simulate_rx(data = [0x21] + payload[6:14]) + self.simulate_rx(data = [0x21] + payload[6:13]) self.stack.process() self.assertIsNone(self.rx_isotp_frame()) - self.simulate_rx(data = [0x22] + payload[14:21]) + self.simulate_rx(data = [0x22] + payload[13:20]) self.stack.process() self.assertIsNone(self.rx_isotp_frame()) - self.simulate_rx(data = [0x23] + payload[21:28]) + self.simulate_rx(data = [0x23] + payload[20:27]) self.stack.process() self.assert_sent_flow_control(stmin=5, blocksize=3) self.assertIsNone(self.rx_isotp_frame()) - self.simulate_rx(data = [0x24] + payload[28:30]) + self.simulate_rx(data = [0x24] + payload[27:30]) self.stack.process() data = self.rx_isotp_frame() self.assertEqual(data, bytearray(payload)) @@ -293,6 +293,43 @@ def receive_invalid_can_message(self): self.assert_error_triggered(isotp.InvalidCanDataError) self.clear_errors() + def test_receive_data_length_12_bytes(self): + self.stack.params.set('ll_data_length', 12) + payload_size = 30 + payload = self.make_payload(payload_size) + self.simulate_rx(data = [0x10, payload_size] + payload[0:10]) + self.stack.process() + self.simulate_rx(data = [0x21] + payload[10:21]) + self.simulate_rx(data = [0x22] + payload[21:30]) + self.stack.process() + data = self.rx_isotp_frame() + self.assertEqual(data, bytearray(payload)) + + def test_receive_data_length_5_bytes(self): + self.stack.params.set('ll_data_length', 5) + payload_size = 15 + payload = self.make_payload(payload_size) + self.simulate_rx(data = [0x10, payload_size] + payload[0:3]) + self.stack.process() + self.simulate_rx(data = [0x21] + payload[3:7]) + self.simulate_rx(data = [0x22] + payload[7:11]) + self.simulate_rx(data = [0x23] + payload[11:15]) + self.stack.process() + data = self.rx_isotp_frame() + self.assertEqual(data, bytearray(payload)) + + def test_receive_data_length_12_but_set_8_bytes(self): + self.stack.params.set('ll_data_length', 8) + payload_size = 30 + payload = self.make_payload(payload_size) + self.simulate_rx(data = [0x10, payload_size] + payload[0:10]) + self.stack.process() + self.simulate_rx(data = [0x21] + payload[10:21]) + self.simulate_rx(data = [0x22] + payload[21:30]) + self.stack.process() + data = self.rx_isotp_frame() + self.assertNotEqual(data, bytearray(payload)) + # ================ Transmission ==================== @@ -334,6 +371,21 @@ def test_padding_single_frame(self): self.assertEqual(msg.dlc, 8) self.assertEqual(msg.data, bytearray([0x0 | i] + payload + [padding_byte] * (7-i))) + def test_padding_single_frame_dl_12_bytes(self): + padding_byte = 0xAA + self.stack.params.set('tx_padding', padding_byte) + self.stack.params.set('ll_data_length', 12) + + for i in range(1,11): + payload = self.make_payload(i,i) + self.assertIsNone(self.get_tx_can_msg()) + self.tx_isotp_frame(payload) + self.stack.process() + msg = self.get_tx_can_msg() + self.assertEqual(msg.arbitration_id, self.TXID) + self.assertEqual(msg.dlc, 12) + self.assertEqual(msg.data, bytearray([0x0 | i] + payload + [padding_byte] * (11-i))) + def test_send_multiple_single_frame_one_process(self): payloads = dict() for i in range(1,8): @@ -398,6 +450,32 @@ def test_padding_multi_frame(self): self.stack.process() self.assertIsNone(self.get_tx_can_msg()) + def test_padding_multi_frame_dl_12_bytes(self): + padding_byte = 0x55 + self.stack.params.set('tx_padding', padding_byte) + self.stack.params.set('ll_data_length', 12) + payload = self.make_payload(15) + self.assertIsNone(self.get_tx_can_msg()) + self.tx_isotp_frame(payload) + self.stack.process() + msg = self.get_tx_can_msg() + self.assertEqual(msg.arbitration_id, self.TXID) + self.assertEqual(msg.dlc, 12) + self.assertEqual(msg.data, bytearray([0x10, 15] + payload[:10])) + self.assertIsNone(self.get_tx_can_msg()) + self.stack.process() + self.assertIsNone(self.get_tx_can_msg()) + self.simulate_rx_flowcontrol(flow_status=0, stmin=0, blocksize=8) + self.stack.process() + msg = self.get_tx_can_msg() + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, self.TXID) + self.assertEqual(msg.dlc, 12) + self.assertEqual(msg.data, bytearray([0x21] + payload[10:15] + [padding_byte]*6)) + self.assertIsNone(self.get_tx_can_msg()) + self.stack.process() + self.assertIsNone(self.get_tx_can_msg()) + def test_send_2_small_multiframe(self): payload1 = self.make_payload(10) payload2 = self.make_payload(10,1) @@ -718,6 +796,37 @@ def test_send_blocksize_zero(self): if n > 4095: break + def test_transmit_data_length_12_bytes(self): + self.stack.params.set('ll_data_length', 12) + payload = self.make_payload(30) + self.tx_isotp_frame(payload) + self.stack.process() + msg = self.get_tx_can_msg() + self.assertEqual(msg.data, bytearray([0x10, 30] + payload[:10])) + self.simulate_rx_flowcontrol(flow_status=0, stmin=0, blocksize=0) + self.stack.process() + msg = self.get_tx_can_msg() + self.assertEqual(msg.data, bytearray([0x21] + payload[10:21])) + msg = self.get_tx_can_msg() + self.assertEqual(msg.data, bytearray([0x22] + payload[21:30])) + + def test_transmit_data_length_5_bytes(self): + self.stack.params.set('ll_data_length', 5) + payload = self.make_payload(15) + self.tx_isotp_frame(payload) + self.stack.process() + msg = self.get_tx_can_msg() + self.assertEqual(msg.data, bytearray([0x10, 15] + payload[:3])) + self.simulate_rx_flowcontrol(flow_status=0, stmin=0, blocksize=0) + self.stack.process() + msg = self.get_tx_can_msg() + self.assertEqual(msg.data, bytearray([0x21] + payload[3:7])) + msg = self.get_tx_can_msg() + self.assertEqual(msg.data, bytearray([0x22] + payload[7:11])) + msg = self.get_tx_can_msg() + self.assertEqual(msg.data, bytearray([0x23] + payload[11:15])) + + # =============== Parameters =========== def create_layer(self, params): address = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=self.TXID, rxid=self.RXID)