diff --git a/doc/source/conf.py b/doc/source/conf.py index cca2987..76c52d0 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -60,7 +60,7 @@ # built documents. # # The short X.Y version. -version = u'1.9' +version = u'2.0' # The full version, including alpha/beta/rc tags. release = version diff --git a/doc/source/index.rst b/doc/source/index.rst index f9c34f5..94eb43b 100755 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -12,4 +12,28 @@ Python support for IsoTP Transport protocol (ISO-15765) This project is a Python package meant to provide support for IsoTP (ISO-15765) protocol written in Python 3. The code is published under MIT license on GitHub (`pylessard/python-can-isotp `_). -This package contains a Python implementation of the protocol that works in the user space that may or may not be coupled with `python-can `_. It also contains a wrapper for a simplified usage of the `Linux SocketCAN IsoTP kernel module `__ +This package contains a Python implementation of the protocol in pure python that works in the user space that may or may not be coupled with `python-can `_. +It also contains a wrapper for a simplified usage of the `Linux SocketCAN IsoTP kernel module `_ + +.. note:: You are looking at the isotp v2.x documentation. The legacy `v1.x documentation `_ is still online. + +V2.x changes +------------ + +V2.x addressed several flaws that were present in v1.x. The main change is regarding the timing capabilities of the module. V2.x can achieve much better timing performance than +the previous version by performing blocking IO operations. The CanStack object is also able to use the python-can Notifier which behave better performance-wise + +Here is the major API changes to v2.x that might make an application designed with v1.x to break + + - The Transport Layer timing is handled into an internal thread, removing the need for the user to periodically call the ``process()`` function. + - The user provided CAN layer receive function ``rxfn`` is expected be blocking for better performance (using the OS asynchronous read capabilities). Non-blocking ``rxfn`` are possible, but the execution of the transport layer will be throttled by calls to ``sleeps()`` to avoid bloating the CPU usage; possibly degrading overall timing + - Some parameter have been modified. + + 1. ``squash_stmin_requirement`` has been removed and replaced by ``override_receiver_stmin`` + 2. Deprecated ``ll_data_length`` parameter is not supported anymore. Replaced by ``tx_data_length`` + + - The transport layer can perform blocking sends, allowing an UDS layer to better handle its timeouts (P2/P2* vs P6 timeouts) + - Some methods dedicated to internal usage have been prefixed with an underscore (``_``) to indicates that they are internals + - The CanStack object uses a Notifier instead of performing ``bus.recv()`` solving the popular issue of a CanStack depleting the receive queue and starving other modules from their incoming messages + - The ``isotp.socket.recv()`` method does not return ``None`` on timeout anymore. + The API now comply with the Python socket API and will raise the proper exception in case of timeout. diff --git a/doc/source/isotp/examples.rst b/doc/source/isotp/examples.rst index febd541..4285a5c 100755 --- a/doc/source/isotp/examples.rst +++ b/doc/source/isotp/examples.rst @@ -1,99 +1,76 @@ Examples ======== -.. _example_transmit_no_thread_can_stack: +.. _example_transmit_can_stack: Basic transmission with python-can ---------------------------------- .. code-block:: python - import isotp - import logging - import time + import isotp + import logging + import time - from can.interfaces.vector import VectorBus + from can.interfaces.vector import VectorBus - def my_error_handler(error): - logging.warning('IsoTp error happened : %s - %s' % (error.__class__.__name__, str(error))) + def my_error_handler(error): + # Called by a different thread. Make it thread safe. + logging.warning('IsoTp error happened : %s - %s' % (error.__class__.__name__, str(error))) - bus = VectorBus(channel=0, bitrate=500000) - addr = isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=0x123, txid=0x456) + bus = VectorBus(channel=0, bitrate=500000) + addr = isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=0x123, txid=0x456) - stack = isotp.CanStack(bus, address=addr, error_handler=my_error_handler) - stack.send(b'Hello, this is a long payload sent in small chunks') + stack = isotp.CanStack(bus, address=addr, error_handler=my_error_handler) + stack.start() + stack.send(b'Hello, this is a long payload sent in small chunks') - while stack.transmitting(): - stack.process() - time.sleep(stack.sleep_time()) + while stack.transmitting(): + time.sleep(stack.sleep_time()) - print("Payload transmission done.") - - bus.shutdown() + print("Payload transmission done.") + stack.stop() + bus.shutdown() ----- -.. _example_receive_threaded_can_stack: +.. _example_transmit_can_stack_blocking_send: -Threaded reception with python-can +Basic blocking transmission ---------------------------------- - .. code-block:: python - import isotp - import logging - import time - import threading - - from can.interfaces.socketcan import SocketcanBus - - class ThreadedApp: - def __init__(self): - self.exit_requested = False - self.bus = SocketcanBus(channel='vcan0') - addr = isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=0x123, txid=0x456) - self.stack = isotp.CanStack(self.bus, address=addr, error_handler=self.my_error_handler) - - def start(self): - self.exit_requested = False - self.thread = threading.Thread(target = self.thread_task) - self.thread.start() - - def stop(self): - self.exit_requested = True - if self.thread.isAlive(): - self.thread.join() - - def my_error_handler(self, error): - logging.warning('IsoTp error happened : %s - %s' % (error.__class__.__name__, str(error))) - - def thread_task(self): - while self.exit_requested == False: - self.stack.process() # Non-blocking - time.sleep(self.stack.sleep_time()) # Variable sleep time based on state machine state - - def shutdown(self): - self.stop() - self.bus.shutdown() - - if __name__ == '__main__': - app = ThreadedApp() - app.start() - - print('Waiting for payload - maximum 5 sec') - t1 = time.time() - while time.time() - t1 < 5: - if app.stack.available(): - payload = app.stack.recv() - print("Received payload : %s" % (payload)) - break - time.sleep(0.2) - - print("Exiting") - app.shutdown() + import isotp + import logging + import time + + from can.interfaces.vector import VectorBus + + def my_error_handler(error): + # Called by a different thread. Make it thread safe. + logging.warning('IsoTp error happened : %s - %s' % (error.__class__.__name__, str(error))) + + bus = SocketcanBus(channel='vcan0') + addr = isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=0x123, txid=0x456) + + params = { + 'blocking_send' : True + } + + stack = isotp.CanStack(bus, address=addr, error_handler=my_error_handler, params=params) + stack.start() + try: + stack.send(b'Hello, this is a long payload sent in small chunks', timeout=1.0) + except isotp.BlockingSendFailure: + # Catches all failure, including isotp.BlockingSendTimeout + print("Failed to transmit") + print("Payload transmission done.") + stack.stop() + bus.shutdown() ----- + .. _example_addressing: Different type of addresses @@ -129,7 +106,6 @@ Sending with functional addressing (broadcast) stack.send(b'Hello', isotp.TargetAddressType.Functional) # Payload must fit a Single Frame. Functional addressing only works with Single Frames while stack.transmitting(): - stack.process() time.sleep(stack.sleep_time()) bus.shutdown() diff --git a/doc/source/isotp/implementation.rst b/doc/source/isotp/implementation.rst index 645de40..d5adabe 100755 --- a/doc/source/isotp/implementation.rst +++ b/doc/source/isotp/implementation.rst @@ -37,7 +37,8 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: 0** The single-byte Separation Time to include in the flow control message that the layer will send when receiving data. - Refer to ISO-15765-2 for specific values. From 1 to 127, represents milliseconds. From 0xF1 to 0xF9, represents hundreds of microseconds (100us, 200us, ..., 900us). 0 Means no timing requirements + Refer to ISO-15765-2 for specific values. From 1 to 127, represents milliseconds. From 0xF1 to 0xF9, represents hundreds of microseconds (100us, 200us, ..., 900us). + 0 Means no timing requirements .. _param_blocksize: @@ -47,7 +48,8 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: 8** The single-byte Block Size to include in the flow control message that the layer will send when receiving data. - Represents the number of consecutive frames 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) + Represents the number of consecutive frames 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_tx_data_length: @@ -59,9 +61,7 @@ The transport layer ``params`` parameter must be a dictionary with the following The maximum number of bytes that the Link Layer (CAN layer) can transport. In other words, the biggest number of data bytes possible in a single CAN message. Valid values are : 8, 12, 16, 20, 24, 32, 48, 64. - Large frames will be transmitted in small CAN messages of this size except for the last CAN message that will be as small as possible, unless padding is used. - - This parameter was formely named ``ll_data_length`` but has been renamed to explicitly indicate that it affects transmitted messages only. + Large IsoTP frames will be transmitted in small CAN messages of this size except for the last CAN message that will be as small as possible, unless padding is used. .. _param_tx_data_min_length: @@ -72,19 +72,24 @@ The transport layer ``params`` parameter must be a dictionary with the following Sets the minimum length of CAN messages. Message with less data than this value will be padded using ``tx_padding`` byte or ``0xCC`` if ``tx_padding=None``. - When set to ``None``, CAN messages will be as small as possible unless ``tx_data_length=8`` and ``tx_padding != None``; in that case, all CAN messages will be padded up to 8 bytes to be compliant with ISO-15765. + When set to ``None``, CAN messages will be as small as possible unless ``tx_data_length=8`` and ``tx_padding != None``; in that case, all CAN messages will be padded up + to 8 bytes to be compliant with ISO-15765. Valid values are : 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64. -.. _param_squash_stmin_requirement: +.. _param_override_receiver_stmin: -.. attribute:: squash_stmin_requirement - :annotation: (bool) +.. attribute:: override_receiver_stmin + :annotation: (float or None) - **default: False** + **default: None** - Indicates if the layer should override the receiver separation time (stmin) when sending and try sending as fast as possible instead. - This can be useful when the layer is running on an operating system giving low priority to your application; such as Windows that has a thread resolution of 16ms. + Time in seconds to wait between consecutive frames when transmitting. When set, this value will override the receiver ``stmin`` requirement. When ``None``, the receiver + ``stmin`` parameter will be respected. This parameter can be useful to speed up a transmission by setting a value of 0 (send as fast as possible) on a system that has low + execution priority or coarse thread resolution. + + This parameter replace the ``squash_stmin_requirement`` parameter available in v1.x + .. _param_rx_flowcontrol_timeout: @@ -121,7 +126,7 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: 0** - The single-byte Wait Frame Max to include in the flow control message that the layer will send when receiving data. + The single-byte "Wait Frame Max" to include in the flow control message that the layer will send when receiving data. When this limits is reached, reception will stop and trigger a :class:`MaximumWaitFrameReachedError` A value of 0 means that wait frames are not supported and none shall be sent. @@ -146,7 +151,7 @@ The transport layer ``params`` parameter must be a dictionary with the following When set to ``True``, transmitted messages will be CAN FD. CAN 2.0 when ``False``. - Setting this parameter to ``True`` does not change the behaviour of the :class:`TransportLayer` except that outputted message will have their ``is_fd`` property set to ``True``. This parameter is just a convenience to integrate more easily with python-can + Setting this parameter to ``True`` does not change the behavior of the :class:`TransportLayer` except that outputted message will have their ``is_fd`` property set to ``True``. This parameter is just a convenience to integrate more easily with python-can .. _param_bitrate_switch: @@ -156,9 +161,9 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: False** - When set to ``True``, tx message will have a flag ``bitrate_switch`` marked as ``True``, meaning that the underlying layer shall performe a CAN FD bitrate switch after arbitration phase. + When set to ``True``, tx message will have a flag ``bitrate_switch`` marked as ``True``, meaning that the underlying layer shall perform a CAN FD bitrate switch after arbitration phase. - Setting this parameter to ``True`` does not change the behaviour of the :class:`TransportLayer` except that outputted message will have their ``bitrate_switch`` property set to ``True``. This parameter is just a convenience to integrate more easily with python-can + Setting this parameter to ``True`` does not change the behavior of the :class:`TransportLayer` except that outputted message will have their ``bitrate_switch`` property set to ``True``. This parameter is just a convenience to integrate more easily with python-can .. _param_default_target_address_type: @@ -169,7 +174,7 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: Physical (0)** When using the :meth:`TransportLayer.send` method without specifying ``target_address_type``, the value in this field will be used. - The purpose of this parameter is to easily switch the address type if your program is not calling `send` directly; for example, if you use a library + The purpose of this parameter is to easily switch the address type if an application is not calling ``send()`` directly; for example, if you use a library that interact with the :class:`TransportLayer` object (such as a UDS client). Can either be :class:`Physical (0)` or :meth:`Functional (1)` @@ -182,7 +187,7 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: False** - Enable or disable the rate limiter. When disabled, no throttling is done on the output rate. When enabled, extra wait states are added in between CAN message tranmission to meet ``rate_limit_max_bitrate`` + Enable or disable the rate limiter. When disabled, no throttling is done on the output rate. When enabled, extra wait states are added in between CAN message transmission to meet ``rate_limit_max_bitrate`` Refer to :ref:`Rate Limiter Section` for more details @@ -193,7 +198,7 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: 10000000 b/s** - Defines the target bitrate in Bits/seconds that the TranportLayer object should try to respect. This rate limiter only apply to the data of the output messages. + Defines the target bitrate in Bits/seconds that the TransportLayer object should try to respect. This rate limiter only apply to the data of the output messages. Refer to :ref:`Rate Limiter Section` for more details @@ -221,8 +226,28 @@ The transport layer ``params`` parameter must be a dictionary with the following **default: False** When Listen Mode is enabled, the :class:`TransportLayer` will correctly receive and transmit ISO-TP Frame, but will not send Flow Control - message when receiving a frame. This mode of operation is usefull to listen to a transmission between two third-party devices without interferring. + message when receiving a frame. This mode of operation is useful to listen to a transmission between two third-party devices without interfering. + +.. _param_blocking_send: +.. attribute:: blocking_send + :annotation: (bool) + + **default: False** + + When True, the ``send()`` method will block until the transmission is complete or an error occurred (including a timeout). + + .. warning:: This parameter requires the processing of the transport layer to happen in parallel, therefore ``TransportLayer.start()`` must be called prior to ``send()`` or + a manually generated thread must called ``process()`` as fast as possible. + +.. _param_logger_name: + +.. attribute:: logger_name + :annotation: (str) + + **default: "isotp"** + + Sets the name of the logger from the ``logging`` module used to log info and debug information ----- @@ -250,7 +275,7 @@ this situation, it is useful to use the rate limiter to reduces the strain on th In the above scenario, having a bitrate of 80000 bps and a window size of 0.1 sec would make the :class:`isotp.TransportLayer` output a burst of 8000 bits (1000 bytes) every 0.1 seconds. .. warning:: The bitrate defined by :ref:`rate_limit_max_bitrate` represent the bitrate of the CAN payload that goes out of the :class:`isotp.TransportLayer` object only, - the CAN layer overhead is exluded. + the CAN layer overhead is excluded. Knowing the a classical CAN message with 11bits ID and a payload of 64 bits usually have 111 bits, the extra 47 bits of overhead will not be considered by the rate limiter. This means that even if the rate limiter is requested to keep a steady 10kbps, depending on the CAN layer configuration, the effective hardware bitrate measured might be much more significant, from 1 to 1.5x more. @@ -264,25 +289,45 @@ Usage The :class:`isotp.TransportLayer` object has the following methods +.. automethod:: isotp.TransportLayer.start +.. automethod:: isotp.TransportLayer.stop .. automethod:: isotp.TransportLayer.send .. automethod:: isotp.TransportLayer.recv .. automethod:: isotp.TransportLayer.available .. automethod:: isotp.TransportLayer.transmitting .. automethod:: isotp.TransportLayer.set_address +.. automethod:: isotp.TransportLayer.stop_sending +.. automethod:: isotp.TransportLayer.stop_receiving + +.. warning:: ``set_address`` is not thread safe and should be called before ``start()`` is called. + +----- + +Legacy methods (v1.x) +--------------------- + +With isotp v2.x, the processing of the transport layer is done from an internal thread. For backward compatibility, the following methods are still accessible to the +users, but **should not** be called from the user thread if ``start()`` has been called. It is safe to call them if no call to ``start()`` is done. + .. automethod:: isotp.TransportLayer.reset .. automethod:: isotp.TransportLayer.process .. automethod:: isotp.TransportLayer.sleep_time ------ +The unthreaded transport layer object used in the isotp module v1.x is still accessible un the name :class:`isotp.TransportLayerLogic`. +The :class:`isotp.TransportLayer` object is an extension of it that can spawn a thread and calls methods that are were to be called by the user. + +--------- .. _Errors: Errors ------ -When calling ``TransportLayer.process``, no exception should raise. Still, errors are possible and are given to an error handler provided by the user. +When a transmission error happens, for instance the receiving party stop responding or bad messages are received, the error is signaled to the user by calling an error handler. An error handler should be a callable function that expects an Exception as first parameter. +.. warning:: The error handler will be called from the internal thread, therefore, any interaction with the application should use a thread safe mechanism + .. function:: my_error_handler(error) :param error: The error @@ -305,3 +350,18 @@ All errors inherit :class:`isotp.IsoTpError` which itself inhe .. autoclass:: isotp.MissingEscapeSequenceError .. autoclass:: isotp.InvalidCanFdFirstFrameRXDL .. autoclass:: isotp.OverflowError + +---------- + +.. _exceptions: + +Exceptions +---------- + +Some exception can be raised in special cases. These are never sent to the error handler + +.. autoclass:: isotp.BlockingSendFailure +.. autoclass:: isotp.BlockingSendTimeout + +.. note:: ``BlockingSendTimeout`` inherits ``BlockingSendTimeout``. Catching a ``BlockingSendFailure`` will also catch timeouts if no + dedicated catching of ``BlockingSendTimeout`` is done diff --git a/doc/source/isotp/socket.rst b/doc/source/isotp/socket.rst index 9afa5e8..acfb1bd 100755 --- a/doc/source/isotp/socket.rst +++ b/doc/source/isotp/socket.rst @@ -18,7 +18,7 @@ A socket is a standard interface for communication protocols and as anything gen Troubleshooting --------------- - - **My socket module does not include the `CAN_ISOTP` constant** + - **My socket module does not include the ``CAN_ISOTP`` constant** That means that your Python version does not include support for IsoTP protocol. It should be included starting from Python 3.7, under Linux build only. See `Python issue `_ and `Pull request `_ diff --git a/isotp/errors.py b/isotp/errors.py index b325174..468aa15 100755 --- a/isotp/errors.py +++ b/isotp/errors.py @@ -6,10 +6,12 @@ def __init__(self, *args, **kwargs): class BlockingSendFailure(IsoTpError): + """Raised when a blocking send is performed and an error occurred""" pass class BlockingSendTimeout(BlockingSendFailure): + """Raised when a blocking send fails to complete within the given timeout value""" pass diff --git a/isotp/protocol.py b/isotp/protocol.py index d8aef03..e2b15d2 100755 --- a/isotp/protocol.py +++ b/isotp/protocol.py @@ -294,29 +294,30 @@ def inform_byte_sent(self, datalen: int) -> None: class TransportLayerLogic: """ - The IsoTP transport layer raw implementation. When using this class, the user is responsible of handling timings by calling the `process()` function + The IsoTP transport layer raw implementation. When using this class, the user is responsible of handling timings by calling the ``process()`` function as fast as possible, like the legacy V1 library was requiring. For an easier solution with less degrees of freedom, use the :ref:`TransportLayer` :param rxfn: Function to be called by the transport layer to read the CAN layer. Must return a :class:`isotp.CanMessage` or None if no message has been received. - :type rxfn: Callable + :type rxfn: Callable : expected signature: ``my_txfn(msg:isotp.CanMessage) -> None`` :param txfn: Function to be called by the transport layer to send a message on the CAN layer. This function should receive a :class:`isotp.CanMessage` - :type txfn: Callable + :type txfn: Callable : expected signature: ``my_rxfn(timeout:float) -> Optional[isotp.CanMessage]`` - :param address: The address information of CAN messages. Includes the addressing mode, txid/rxid, source/target address and address extension. See :class:`isotp.Address` for more details. - :type address: isotp.Address + :param address: The address information of CAN messages. Includes the addressing mode, txid/rxid, source/target address and address extension. + See :class:`isotp.Address` for more details. + :type address: :class:`isotp.Address` - :param error_handler: A function to be called when an error has been detected. An :class:`isotp.IsoTpError` (inheriting Exception class) will be given as sole parameter. See the :ref:`Error section` - :type error_handler: Callable + :param error_handler: A function to be called when an error has been detected. An :class:`isotp.IsoTpError` (inheriting Exception class) + will be given as sole parameter. See the :ref:`Error section` + :type error_handler: Callable : Expected signature: ``my_error_handler(error:isotp.IsoTpError) -> None`` :param params: List of parameters for the transport layer. See :ref:`the list of parameters :type params: dict :param post_send_callback: An optional callback to be called right after a send request has been enqueued. The main purpose of this parameter is to allow - a facility to stop waiting for a message (if blocked in `rxfn`) and start transmitting right away if possible. - The function must have this signature : `my_func(send_request)` Where `send_request` is an instance of :ref:`SendRequest` + a facility to stop waiting for a message (if blocked in ``rxfn``) and start transmitting right away if possible. + the function must have this signature : ``my_func(send_request)`` Where ``send_request`` is an instance of :ref:`SendRequest` :type post_send_callback: Callable - """ LOGGER_NAME = 'isotp' @@ -325,7 +326,7 @@ class TransportLayerLogic: class Params: stmin: int blocksize: int - squash_stmin_requirement: bool + override_receiver_stmin: Optional[float] rx_flowcontrol_timeout: float rx_consecutive_frame_timeout: float tx_padding: Optional[int] @@ -346,7 +347,7 @@ class Params: def __init__(self): self.stmin = 0 self.blocksize = 8 - self.squash_stmin_requirement = False + self.override_receiver_stmin = None self.rx_flowcontrol_timeout = 1000 self.rx_consecutive_frame_timeout = 1000 self.tx_padding = None @@ -365,8 +366,7 @@ def __init__(self): self.logger_name = TransportLayer.LOGGER_NAME def set(self, key: str, val: Any, validate: bool = True) -> None: - param_alias = { - 'll_data_length': 'tx_data_length' # For backward compatibility + param_alias: Dict[str, str] = { } if key in param_alias: key = param_alias[key] @@ -406,8 +406,13 @@ def validate(self) -> None: if self.blocksize < 0 or self.blocksize > 0xFF: raise ValueError('blocksize must be and integer between 0x00 and 0xFF') - if not isinstance(self.squash_stmin_requirement, bool): - raise ValueError('squash_stmin_requirement must be a boolean value') + if self.override_receiver_stmin is not None: + if not isinstance(self.override_receiver_stmin, (int, float)) or isinstance(self.override_receiver_stmin, bool): + raise ValueError('override_receiver_stmin must be a float') + self.override_receiver_stmin = float(self.override_receiver_stmin) + + if self.override_receiver_stmin < 0 or not math.isfinite(self.override_receiver_stmin): + raise ValueError('Invalid override_receiver_stmin') if not isinstance(self.wftmax, int): raise ValueError('wftmax must be an integer') @@ -512,7 +517,7 @@ def complete(self, success: bool) -> None: @dataclass(slots=True, frozen=True) class ProcessStats: - """Some statistics produced by every `process` called indicating how much has been accomplish during that iteration.""" + """Some statistics produced by every ``process`` called indicating how much has been accomplish during that iteration.""" received: int received_processed: int sent: int @@ -612,31 +617,31 @@ def __init__(self, self.rx_state = self.RxState.IDLE # State of the reception FSM self.tx_state = self.TxState.IDLE # State of the transmission FSM - self.last_rx_state = self.rx_state - self.last_tx_state = self.tx_state + self.last_rx_state = self.rx_state # Used to log changes in states + self.last_tx_state = self.tx_state # Used to log changes in states - self.rx_block_counter = 0 + self.rx_block_counter = 0 # Keeps track of how many block we've received. Used to determine when to send a flow control message self.last_seqnum = 0 # Consecutive frame Sequence number of previous message self.rx_frame_length = 0 # Length of IsoTP frame being received at the moment self.tx_frame_length = 0 # Length of the data that we are sending self.last_flow_control_frame = None # When a FlowControl is received. Put here - self.tx_block_counter = 0 # Keeps track of how many block we've sent + self.tx_block_counter = 0 # Keeps track of how many block we've sent. USed to determine when to wait for a flow control message self.tx_seqnum = 0 # Keeps track of the actual sequence number while sending self.wft_counter = 0 # Keeps track of how many wait frame we've received - self.pending_flow_control_tx = False # Flag indicating that we need to transmit a flow control message. Set by Rx Process, Cleared by Tx Process + self.pending_flow_control_tx = False # Flag indicating that we need to transmit a flow control message. Set by Rx Process, Cleared by Tx Process self._empty_rx_buffer() self._empty_tx_buffer() self.timer_tx_stmin = Timer(timeout=0) self.error_handler = error_handler - self.actual_rxdl = None - self.is_threaded_implementation = False + self.actual_rxdl = None # Length of the CAN messages during a reception. Set by the first frame. All consecutive frames must be identical + # Legacy (v1.x) timing recommendation self.timings = { - (self.RxState.IDLE, self.TxState.IDLE): 0.05, - (self.RxState.IDLE, self.TxState.WAIT_FC): 0.01, + (self.RxState.IDLE, self.TxState.IDLE): 0.02, + (self.RxState.IDLE, self.TxState.WAIT_FC): 0.005, } self.load_params() @@ -655,19 +660,26 @@ def send(self, target_address_type: Optional[Union[isotp.address.TargetAddressType, int]] = None, send_timeout: float = 0): """ - Enqueue an IsoTP frame to be sent over CAN network + Enqueue an IsoTP frame to be sent over CAN network. + When performing a blocking send, this method returns only when the transmission is complete or raise an exception when a failure or a timeout occurs. + See :ref:`blocking_send` :param data: The data to be sent :type data: bytearray - :param target_address_type: Optional parameter that can be Physical (0) for 1-to-1 communication or Functional (1) for 1-to-n. See :class:`isotp.TargetAddressType`. - If not provided, parameter :ref:`default_target_address_type` will be used (default to `Physical`) + :param target_address_type: Optional parameter that can be Physical (0) for 1-to-1 communication or Functional (1) for 1-to-n. + See :class:`isotp.TargetAddressType`. + If not provided, parameter :ref:`default_target_address_type` will be used (default to ``Physical``) :type target_address_type: int + :param send_timeout: Timeout value for blocking send. Unused if :ref:`blocking_send` is ``False`` + :type send_timeout: float + :raises ValueError: Input parameter is not a bytearray or not convertible to bytearray :raises RuntimeError: Transmit queue is full - :raises BlockingSendTimeout: When `:ref:`blocking_send` is set to `True` and the send operation does not complete in the given timeout - :raises BlockingSendFailure: When `:ref:`blocking_send` is set to `True` and the transmission failed for any reason (e.g. unexpected frame or bad timings) + :raises BlockingSendTimeout: When :ref:`blocking_send` is set to ``True`` and the send operation does not complete in the given timeout. + :raises BlockingSendFailure: When :ref:`blocking_send` is set to ``True`` and the transmission failed for any reason (e.g. unexpected frame or bad timings), including a timeout. Note that + :class:`BlockingSendTimeout` inherits :class:`BlockingSendFailure`. """ if target_address_type is None: @@ -967,7 +979,10 @@ def _process_tx(self) -> ProcessTxReport: self.wft_counter = 0 self.timer_rx_fc.stop() assert flow_control_frame.stmin_sec is not None - self.timer_tx_stmin.set_timeout(flow_control_frame.stmin_sec) + if self.params.override_receiver_stmin is not None: + self.timer_tx_stmin.set_timeout(self.params.override_receiver_stmin) + else: + self.timer_tx_stmin.set_timeout(flow_control_frame.stmin_sec) self.remote_blocksize = flow_control_frame.blocksize if self.tx_state == self.TxState.WAIT_FC: @@ -1065,7 +1080,7 @@ def _process_tx(self) -> ProcessTxReport: elif self.tx_state == self.TxState.TRANSMIT_CF: assert self.remote_blocksize is not None - if self.timer_tx_stmin.is_timed_out() or self.params.squash_stmin_requirement: + if self.timer_tx_stmin.is_timed_out(): data_length = self.params.tx_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_arbitration_id() @@ -1345,8 +1360,6 @@ def next_cf_delay(self) -> Optional[float]: if not self.is_tx_transmitting_cf(): return None - if self.params.squash_stmin_requirement: - return 0 if self.timer_tx_stmin.is_timed_out(): return 0 return self.timer_tx_stmin.remaining() @@ -1354,27 +1367,26 @@ def next_cf_delay(self) -> Optional[float]: class TransportLayer(TransportLayerLogic): """ - An IsoTP transport layer implementation that runs in a separate thread. The main public interface are `start`, `stop`, `send`, `recv`. - For backward compatibility, this class will behave similarly as a V1 TransportLayer if start/stop are never called; meaning that `process()` can be called by the user + An IsoTP transport layer implementation that runs in a separate thread. The main public interface are ``start``, ``stop``, ``send``, ``recv``. + For backward compatibility, this class will behave similarly as a V1 TransportLayer if start/stop are never called; meaning that ``process()`` can be called by the user :param rxfn: Function to be called by the transport layer to read the CAN layer. Must return a :class:`isotp.CanMessage` or None if no message has been received. - For optimal performance, this function should perform a blocking read that waits on IO - :type rxfn: Callable : expected signature: `my_txfn(msg:isotp.CanMessage) -> None` + For optimal performance, this function should perform a blocking read that waits on IO + :type rxfn: Callable : expected signature: ``my_txfn(msg:isotp.CanMessage) -> None`` :param txfn: Function to be called by the transport layer to send a message on the CAN layer. This function should receive a :class:`isotp.CanMessage` - :type txfn: Callable : expected signature: `my_rxfn(timeout:float) -> Optional[isotp.CanMessage]` + :type txfn: Callable : expected signature: ``my_rxfn(timeout:float) -> Optional[isotp.CanMessage]`` :param address: The address information of CAN messages. Includes the addressing mode, txid/rxid, source/target address and address extension. See :class:`isotp.Address` for more details. :type address: isotp.Address :param error_handler: A function to be called when an error has been detected. - An :class:`isotp.IsoTpError` (inheriting Exception class) will be given as sole parameter. See the :ref:`Error section` - When started, the error handler will be called from a different thread than the user thread, make sure to consider thread safety if the error handler is more complex than a log. + An :class:`isotp.IsoTpError` (inheriting Exception class) will be given as sole parameter. See the :ref:`Error section` + When started, the error handler will be called from a different thread than the user thread, make sure to consider thread safety if the error handler is more complex than a log. :type error_handler: Callable - :param params: List of parameters for the transport layer. See :ref:`the list of parameters + :param params: List of parameters for the transport layer. See :ref:`the list of parameters` :type params: dict - """ class Events: main_thread_ready: threading.Event diff --git a/isotp/tools.py b/isotp/tools.py index b94f440..92455a9 100644 --- a/isotp/tools.py +++ b/isotp/tools.py @@ -37,7 +37,7 @@ def elapsed_ns(self) -> int: def remaining_ns(self) -> int: if self.is_stopped(): return 0 - return self.timeout - self.elapsed_ns() + return max(0, self.timeout - self.elapsed_ns()) def remaining(self) -> float: return float(self.remaining_ns()) / 1e9 diff --git a/isotp/tpsock/__init__.py b/isotp/tpsock/__init__.py index 9fdabde..e551fea 100755 --- a/isotp/tpsock/__init__.py +++ b/isotp/tpsock/__init__.py @@ -119,7 +119,7 @@ def set_ll_opts(self, ) -> "opts.LinkLayerOpts": """ 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. - Values of `None` will leave the parameter unchanged + Values of ``None`` will leave the parameter unchanged :param mtu: The internal CAN frame structure size. Possible values are defined in :class:`isotp.socket.LinkLayerProtocol` :type mtu: int @@ -151,7 +151,7 @@ def set_opts(self, tx_stmin: Optional[int] = None) -> "opts.GeneralOpts": """ - Sets the general options of the socket. Values of `None` will leave the parameter unchanged + Sets the general options of the socket. Values of ``None`` will leave the parameter unchanged :param optflag: A list of flags modifying the protocol behavior. Refer to :class:`socket.flags` :type optflag: int @@ -193,7 +193,7 @@ def set_opts(self, def set_fc_opts(self, bs: Optional[int] = None, stmin: Optional[int] = None, wftmax: Optional[int] = None) -> "opts.FlowControlOpts": """ - Sets the flow control options of the socket. Values of `None` will leave the parameter unchanged + Sets the flow control options of the socket. Values of ``None`` will leave the parameter unchanged :param bs: The block size sent in the flow control message. Indicates the number of consecutive frame a sender can send before the socket sends a new flow control. A block size of 0 means that no additional flow control message will be sent (block size of infinity) :type bs: int diff --git a/setup.py b/setup.py index 29dbeb9..1de0fa8 100755 --- a/setup.py +++ b/setup.py @@ -10,14 +10,14 @@ setup( name='can-isotp', packages=find_packages(where='.', exclude=['test', 'test.*'], include=['isotp', "isotp.*"]), - version='1.9', + version='2.0', description='Module enabling the IsoTP protocol defined by ISO-15765', long_description=long_description, author='Pier-Yves Lessard', author_email='py.lessard@gmail.com', license='MIT', url='https://github.com/pylessard/python-can-isotp', - download_url='https://github.com/pylessard/python-can-isotp/archive/v1.9.tar.gz', + download_url='https://github.com/pylessard/python-can-isotp/archive/v2.0.tar.gz', keywords=['isotp', 'can', 'iso-15765', '15765', 'iso15765'], python_requires='>=3.7', classifiers=[ diff --git a/test/test_layer_vs_socket.py b/test/test_layer_vs_socket.py index 8527823..620e02c 100755 --- a/test/test_layer_vs_socket.py +++ b/test/test_layer_vs_socket.py @@ -53,7 +53,7 @@ def get_can_stack(self, **isotp_params_override): 'tx_padding': 0, # Will pad all transmitted CAN messages with byte 0x00. None means no padding 'rx_flowcontrol_timeout': 1000, # Triggers a timeout if a flow control is awaited for more than 1000 milliseconds 'rx_consecutive_frame_timeout': 1000, # Triggers a timeout if a consecutive frame is awaited for more than 1000 milliseconds - 'squash_stmin_requirement': False, # When sending, respect the stmin requirement of the receiver. If set to True, go as fast as possible. + 'override_receiver_stmin': None, # When sending, respect the stmin requirement of the receiver. If set to True, go as fast as possible. 'can_fd': False, 'max_frame_size': 4095, 'bitrate_switch': False, diff --git a/test/test_transport_layer_logic.py b/test/test_transport_layer_logic.py index 80b68b2..36068b5 100755 --- a/test/test_transport_layer_logic.py +++ b/test/test_transport_layer_logic.py @@ -5,7 +5,7 @@ Message = isotp.CanMessage -# Check the behaviour of the transport layer. Sequenece of CAN frames, timings, etc. +# Check the behavior of the transport layer. Sequenece of CAN frames, timings, etc. class TestTransportLayerLogicNonBlockingRxfn(TransportLayerBaseTest): TXID = 0x111 RXID = 0x222 @@ -13,7 +13,7 @@ class TestTransportLayerLogicNonBlockingRxfn(TransportLayerBaseTest): STACK_PARAMS = { 'stmin': 1, 'blocksize': 8, - 'squash_stmin_requirement': False, + 'override_receiver_stmin': None, 'rx_flowcontrol_timeout': 1000, 'rx_consecutive_frame_timeout': 1000, 'wftmax': 0, @@ -920,8 +920,8 @@ def perform_multiframe_test_no_stmin(self, payload_size, blocksize): self.stack.process() # Receive the flow control and enqueue another bloc of can message. block_counter = 0 - def test_squash_timing_requirement(self): - self.stack.params.set('squash_stmin_requirement', True) + def test_override_receiver_stmin_0(self): + self.stack.params.set('override_receiver_stmin', 0) payload_size = 4095 stmin = 100 # 100 msec @@ -1154,7 +1154,7 @@ def test_transmit_single_frame_txdl_64_bytes_min_length_32(self): def test_transmit_single_frame_txdl_8_min_length_6(self): self.stack.params.set('tx_data_length', 8) - self.stack.params.set('tx_data_min_length', 6) # This behaviour is not defined by the standard, but we allow it + self.stack.params.set('tx_data_min_length', 6) # This behavior is not defined by the standard, but we allow it self.stack.params.set('tx_padding', 0xAA) payload = self.make_payload(3) self.tx_isotp_frame(payload) @@ -1365,7 +1365,7 @@ def test_params_bad_values(self): params = { 'stmin': 1, 'blocksize': 8, - 'squash_stmin_requirement': False, + 'override_receiver_stmin': 0, 'rx_flowcontrol_timeout': 1000, 'rx_consecutive_frame_timeout': 1000, 'tx_data_length': 8, @@ -1402,9 +1402,18 @@ def test_params_bad_values(self): params['blocksize'] = 8 with self.assertRaises(ValueError): - params['squash_stmin_requirement'] = 'string' + params['override_receiver_stmin'] = 'string' + self.create_layer(params) + with self.assertRaises(ValueError): + params['override_receiver_stmin'] = True + self.create_layer(params) + with self.assertRaises(ValueError): + params['override_receiver_stmin'] = -1 self.create_layer(params) - params['squash_stmin_requirement'] = False + + params['override_receiver_stmin'] = 0.001 + self.create_layer(params) + params['override_receiver_stmin'] = None with self.assertRaises(ValueError): params['rx_flowcontrol_timeout'] = -1 @@ -1568,7 +1577,6 @@ def test_params_bad_values(self): params['blocking_send'] = False - for val in [0, True, None]: with self.assertRaises(ValueError): params['logger_name'] = val @@ -1577,7 +1585,7 @@ def test_params_bad_values(self): params['logger_name'] = 'asd' self.create_layer(params) -# Check the behaviour of the transport layer. Sequenece of CAN frames, timings, etc. +# Check the behavior of the transport layer. Sequenece of CAN frames, timings, etc. class TestTransportLayerLogicBlockingRxfn(TransportLayerBaseTest): @@ -1587,7 +1595,7 @@ class TestTransportLayerLogicBlockingRxfn(TransportLayerBaseTest): STACK_PARAMS = { 'stmin': 1, 'blocksize': 8, - 'squash_stmin_requirement': False, + 'override_receiver_stmin': None, 'rx_flowcontrol_timeout': 1000, 'rx_consecutive_frame_timeout': 1000, 'wftmax': 0, diff --git a/test/test_transport_layer_threading_behavior.py b/test/test_transport_layer_threading_behavior.py index 5f71032..6f98781 100755 --- a/test/test_transport_layer_threading_behavior.py +++ b/test/test_transport_layer_threading_behavior.py @@ -8,7 +8,7 @@ Message = isotp.CanMessage -# Check the behaviour of the transport layer. Sequenece of CAN frames, timings, etc. +# Check the behavior of the transport layer. Sequenece of CAN frames, timings, etc. class TestTransportLayerStackAgainstStack(unittest.TestCase): TXID = 0x120 RXID = 0x121 @@ -16,7 +16,7 @@ class TestTransportLayerStackAgainstStack(unittest.TestCase): STACK_PARAMS = { 'stmin': 10, 'blocksize': 8, - 'squash_stmin_requirement': True, + 'override_receiver_stmin': 0, 'rx_flowcontrol_timeout': 1000, 'rx_consecutive_frame_timeout': 1000, 'wftmax': 0,