Skip to content

Commit

Permalink
Support variable Link Layer mtu via parameter ll_data_length
Browse files Browse the repository at this point in the history
  • Loading branch information
pylessard committed Jan 2, 2019
1 parent 936e520 commit 1096eb0
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 26 deletions.
10 changes: 10 additions & 0 deletions doc/source/isotp/implementation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions doc/source/isotp/socket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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<isotp.socket.mtu>`
:param mtu: The internal CAN frame structure size. Possible values are defined in :class:`isotp.socket.LinkLayerProtocol<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
Expand All @@ -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
40 changes: 24 additions & 16 deletions isotp/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions isotp/tpsock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand All @@ -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()
Expand Down
36 changes: 34 additions & 2 deletions test/test_helper_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]))
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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])
Expand Down
Loading

0 comments on commit 1096eb0

Please sign in to comment.