Skip to content

Commit

Permalink
Improve retries for sync client. (#2377)
Browse files Browse the repository at this point in the history
  • Loading branch information
janiversen authored Oct 14, 2024
1 parent c382ec9 commit 679d1d0
Show file tree
Hide file tree
Showing 8 changed files with 42 additions and 25 deletions.
5 changes: 1 addition & 4 deletions pymodbus/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/client/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/client/tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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::
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/client/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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::
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/client/udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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::
Expand Down
15 changes: 13 additions & 2 deletions pymodbus/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
]

import struct
import time
from contextlib import suppress
from threading import RLock
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 5 additions & 11 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions test/sub_client/test_client_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 679d1d0

Please sign in to comment.