diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 4f8760337..d53e359e9 100644 Binary files a/doc/source/_static/examples.tgz and b/doc/source/_static/examples.tgz differ diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index 9f48d8be8..79c23ddec 100644 Binary files a/doc/source/_static/examples.zip and b/doc/source/_static/examples.zip differ diff --git a/doc/source/client.rst b/doc/source/client.rst index ae4a8404e..4343def75 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -166,6 +166,14 @@ The line :mod:`result = await client.read_coils(2, 3, slave=1)` is an example of The last line :mod:`client.close()` closes the connection and render the object inactive. +Retry logic for async clients +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If no response is received to a request (call), it is retried (parameter retries) times, if not successful +an exception response is returned, BUT the connection is not touched. + +If 3 consequitve requests (calls) do not receive a response, the connection is terminated. + Development notes ^^^^^^^^^^^^^^^^^ diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 655f010d4..ee3b18252 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -11,7 +11,7 @@ from pymodbus.exceptions import ConnectionException, ModbusIOException from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType from pymodbus.logging import Log -from pymodbus.pdu import DecodePDU, ModbusPDU +from pymodbus.pdu import DecodePDU, ExceptionResponse, ModbusPDU from pymodbus.transaction import SyncModbusTransactionManager from pymodbus.transport import CommParams from pymodbus.utilities import ModbusTransactionState @@ -50,6 +50,8 @@ def __init__( self.last_frame_end: float | None = 0 self.silent_interval: float = 0 self._lock = asyncio.Lock() + self.accept_no_response_limit = 3 + self.count_no_responses = 0 @property def connected(self) -> bool: @@ -115,11 +117,16 @@ async def async_execute(self, request) -> ModbusPDU: except asyncio.exceptions.TimeoutError: count += 1 if count > self.retries: - self.ctx.connection_lost(asyncio.TimeoutError("Server not responding")) - raise ModbusIOException( - f"ERROR: No response received after {self.retries} retries" - ) - + if self.count_no_responses >= self.accept_no_response_limit: + self.ctx.connection_lost(asyncio.TimeoutError("Server not responding")) + raise ModbusIOException( + f"ERROR: No response received of the last {self.accept_no_response_limit} request, CLOSING CONNECTION." + ) + self.count_no_responses += 1 + Log.error(f"No response received after {self.retries} retries, continue with next request") + return ExceptionResponse(request.function_code) + + self.count_no_responses = 0 return resp def build_response(self, request: ModbusPDU): diff --git a/pymodbus/pdu/__init__.py b/pymodbus/pdu/__init__.py index 2095a153c..60ba42e58 100644 --- a/pymodbus/pdu/__init__.py +++ b/pymodbus/pdu/__init__.py @@ -2,13 +2,10 @@ __all__ = [ "DecodePDU", "ExceptionResponse", + "ExceptionResponse", "ModbusExceptions", "ModbusPDU", ] from pymodbus.pdu.decoders import DecodePDU -from pymodbus.pdu.pdu import ( - ExceptionResponse, - ModbusExceptions, - ModbusPDU, -) +from pymodbus.pdu.pdu import ExceptionResponse, ModbusExceptions, ModbusPDU diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 138f56771..9871b2a85 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -20,8 +20,8 @@ from pymodbus.client.mixin import ModbusClientMixin from pymodbus.datastore import ModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock -from pymodbus.exceptions import ConnectionException, ModbusException, ModbusIOException -from pymodbus.pdu import ModbusPDU +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse, ModbusPDU from pymodbus.transport import CommParams, CommType @@ -436,8 +436,9 @@ async def test_client_execute_broadcast(): transport = MockTransport(base, request) base.ctx.connection_made(transport=transport) - with pytest.raises(ModbusIOException): - assert not await base.async_execute(request) + # with pytest.raises(ModbusIOException): + # assert not await base.async_execute(request) + assert await base.async_execute(request) async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" @@ -477,8 +478,8 @@ async def test_client_protocol_timeout(): transport = MockTransport(base, request, retries=4) base.ctx.connection_made(transport=transport) - with pytest.raises(ModbusIOException): - await base.async_execute(request) + pdu = await base.async_execute(request) + assert isinstance(pdu, ExceptionResponse) assert transport.retries == 1