From 679d1d07b717803aaeee5a8d15bba7db04df16db Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 14 Oct 2024 15:20:13 +0200 Subject: [PATCH] Improve retries for sync client. (#2377) --- pymodbus/client/base.py | 5 +---- pymodbus/client/serial.py | 4 ++-- pymodbus/client/tcp.py | 4 ++-- pymodbus/client/tls.py | 4 ++-- pymodbus/client/udp.py | 4 ++-- pymodbus/transaction.py | 15 +++++++++++++-- test/conftest.py | 16 +++++----------- test/sub_client/test_client_sync.py | 15 +++++++++++++++ 8 files changed, 42 insertions(+), 25 deletions(-) diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 8ac6950ce..ce36e24b8 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -108,9 +108,6 @@ async def async_execute(self, request) -> ModbusResponse: async with self._lock: req = self.build_response(request) self.ctx.send(packet) - if not request.slave_id: - resp = None - break try: resp = await asyncio.wait_for( req, timeout=self.ctx.comm_params.timeout_connect @@ -124,7 +121,7 @@ async def async_execute(self, request) -> ModbusResponse: f"ERROR: No response received after {self.retries} retries" ) - return resp # type: ignore[return-value] + return resp def build_response(self, request: ModbusRequest): """Return a deferred response for the current request. diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index fe74fb99f..a94502624 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -37,7 +37,7 @@ class AsyncModbusSerialClient(ModbusBaseClient): :param name: Set communication name, used in logging :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -121,7 +121,7 @@ class ModbusSerialClient(ModbusBaseSyncClient): :param name: Set communication name, used in logging :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. Note that unlike the async client, the sync client does not perform diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index 360a80e0f..0983f2c19 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -28,7 +28,7 @@ class AsyncModbusTcpClient(ModbusBaseClient): :param source_address: source address of client :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -99,7 +99,7 @@ class ModbusTcpClient(ModbusBaseSyncClient): :param source_address: source address of client :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. .. tip:: diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 55a224fb1..0b4d6a44c 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -27,7 +27,7 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): :param source_address: Source address of client :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -122,7 +122,7 @@ class ModbusTlsClient(ModbusTcpClient): :param source_address: Source address of client :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. .. tip:: diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index d3c68612c..86c05895f 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -30,7 +30,7 @@ class AsyncModbusUdpClient(ModbusBaseClient): :param source_address: source address of client, :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -101,7 +101,7 @@ class ModbusUdpClient(ModbusBaseSyncClient): :param source_address: source address of client, :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. .. tip:: diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 317d10318..76fc5f7e6 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -8,6 +8,7 @@ ] import struct +import time from contextlib import suppress from threading import RLock from typing import TYPE_CHECKING @@ -387,11 +388,21 @@ def _recv(self, expected_response_length, full) -> bytes: # noqa: C901 total = expected_response_length + min_size else: total = expected_response_length + retries = 0 + missing_len = expected_response_length + result = read_min + while missing_len and retries < self.retries: + if retries: + time.sleep(0.1) + data = self.client.recv(expected_response_length) + result += data + missing_len -= len(data) + retries += 1 else: read_min = b"" total = expected_response_length - result = self.client.recv(expected_response_length) - result = read_min + result + result = self.client.recv(expected_response_length) + result = read_min + result actual = len(result) if total is not None and actual != total: msg_start = "Incomplete message" if actual else "No response" diff --git a/test/conftest.py b/test/conftest.py index 7becad60d..834c09c95 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -287,17 +287,7 @@ def recv(self, size): """Receive.""" if not self.packets or not size: return b"" - # if not self.buffer: - # self.buffer = self.packets.popleft() - # if size >= len(self.buffer): - # retval = self.buffer - # self.buffer = None - # else: - # retval = self.buffer[0:size] - # self.buffer = self.buffer[size] - self.buffer = self.packets.popleft() - retval = self.buffer - self.buffer = None + retval = self.packets.popleft() self.in_waiting -= len(retval) return retval @@ -309,6 +299,10 @@ def recvfrom(self, size): """Receive from.""" return [self.recv(size)] + def write(self, msg): + """Write.""" + return self.send(msg) + def send(self, msg): """Send.""" if not self.copy_send: diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index bac978476..529c18787 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -422,6 +422,21 @@ def test_serial_client_recv(self): client.socket.timeout = 0 assert client.recv(0) == b"" + def test_serial_client_recv_split(self): + """Test the serial client receive method.""" + client = ModbusSerialClient("/dev/null") + with pytest.raises(ConnectionException): + client.recv(1024) + client.socket = mockSocket(copy_send=False) + client.socket.mock_prepare_receive(b'') + client.socket.mock_prepare_receive(b'\x11\x03\x06\xAE') + client.socket.mock_prepare_receive(b'\x41\x56\x52\x43\x40\x49') + client.socket.mock_prepare_receive(b'\xAD') + reply_ok = client.read_input_registers(0x820, 3, slave=17) + assert not reply_ok.isError() + client.close() + + def test_serial_client_repr(self): """Test serial client.""" client = ModbusSerialClient("/dev/null")