Skip to content

Commit 9b67640

Browse files
authored
Test and correct receiving more than one packet (#1965)
1 parent 2a6433a commit 9b67640

File tree

4 files changed

+92
-16
lines changed

4 files changed

+92
-16
lines changed

pymodbus/client/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,13 @@ async def async_execute(self, request=None):
157157

158158
count = 0
159159
while count <= self.retries:
160+
req = self.build_response(request.transaction_id)
160161
if not count or not self.no_resend_on_retry:
161162
self.transport_send(packet)
162163
if self.broadcast_enable and not request.slave_id:
163164
resp = b"Broadcast write sent - no response expected"
164165
break
165166
try:
166-
req = self.build_response(request.transaction_id)
167167
resp = await asyncio.wait_for(
168168
req, timeout=self.comm_params.timeout_connect
169169
)

pymodbus/framer/socket_framer.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def advanceFrame(self):
7777
it or determined that it contains an error. It also has to reset the
7878
current frame header handle
7979
"""
80-
length = self._hsize + self._header["len"]
80+
length = self._hsize + self._header["len"] -1
8181
self._buffer = self._buffer[length:]
8282
self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0}
8383

@@ -129,15 +129,16 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs
129129
The processed and decoded messages are pushed to the callback
130130
function to process and send.
131131
"""
132-
if not self.checkFrame():
133-
Log.debug("Frame check failed, ignoring!!")
134-
return
135-
if not self._validate_slave_id(slave, single):
136-
header_txt = self._header["uid"]
137-
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
138-
self.resetFrame()
139-
return
140-
self._process(callback, tid)
132+
while True:
133+
if not self.checkFrame():
134+
Log.debug("Frame check failed, ignoring!!")
135+
return
136+
if not self._validate_slave_id(slave, single):
137+
header_txt = self._header["uid"]
138+
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
139+
self.resetFrame()
140+
return
141+
self._process(callback, tid)
141142

142143
def _process(self, callback, tid, error=False):
143144
"""Process incoming packets irrespective error condition."""

pymodbus/transport/stub.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
11
"""ModbusProtocol network stub."""
22
from __future__ import annotations
33

4-
from pymodbus.transport.transport import ModbusProtocol
4+
from typing import Callable
5+
6+
from pymodbus.transport.transport import CommParams, ModbusProtocol
57

68

79
class ModbusProtocolStub(ModbusProtocol):
810
"""Protocol layer including transport."""
911

12+
def __init__(
13+
self,
14+
params: CommParams,
15+
is_server: bool,
16+
handler: Callable[[bytes], bytes] | None = None,
17+
) -> None:
18+
"""Initialize a stub instance."""
19+
self.stub_handle_data = handler if handler else self.dummy_handler
20+
super().__init__(params, is_server)
21+
22+
1023
async def start_run(self):
1124
"""Call need functions to start server/client."""
1225
if self.is_server:
1326
return await self.transport_listen()
1427
return await self.transport_connect()
1528

29+
1630
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
1731
"""Handle received data."""
1832
if (response := self.stub_handle_data(data)):
1933
self.transport_send(response)
2034
return len(data)
2135

36+
def callback_new_connection(self) -> ModbusProtocol:
37+
"""Call when listener receive new connection request."""
38+
new_stub = ModbusProtocolStub(self.comm_params, False)
39+
new_stub.stub_handle_data = self.stub_handle_data
40+
return new_stub
41+
2242
# ---------------- #
2343
# external methods #
2444
# ---------------- #
25-
def stub_handle_data(self, data: bytes) -> bytes | None:
45+
def dummy_handler(self, data: bytes) -> bytes | None:
2646
"""Handle received data."""
27-
if len(data) > 5:
28-
return data
29-
return None
47+
return data

test/test_network.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
"""Test transport."""
2+
from __future__ import annotations
3+
4+
import asyncio
25

36
import pytest
47

@@ -24,5 +27,59 @@ async def test_stub(self, use_port, use_cls):
2427
stub = ModbusProtocolStub(use_cls, True)
2528
assert await stub.start_run()
2629
assert await client.connect()
30+
test_data = b"Data got echoed."
31+
client.transport.write(test_data)
32+
client.transport_close()
33+
stub.transport_close()
34+
35+
async def test_double_packet(self, use_port, use_cls):
36+
"""Test double packet on network."""
37+
old_data = b''
38+
client = AsyncModbusTcpClient(NULLMODEM_HOST, port=use_port, retries=0)
39+
40+
def local_handle_data(data: bytes) -> bytes | None:
41+
"""Handle server side for this test case."""
42+
nonlocal old_data
43+
44+
addr = int(data[9])
45+
response = data[0:5] + b'\x05\x00\x03\x02\x00' + (addr*10).to_bytes(1, 'big')
46+
47+
# 1, 4, 7 return correct data
48+
# 2, 5 return NO data
49+
# 3 return 2 + 3
50+
# 6 return 6 + 6 (wrong order
51+
# 8 return 7 + half 8
52+
# 9 return second half 8 + 9
53+
if addr in {2, 5}:
54+
old_data = response
55+
response = None
56+
elif addr == 3:
57+
response = old_data + response
58+
old_data = b''
59+
elif addr == 6:
60+
response = response + old_data
61+
old_data = b''
62+
elif addr == 8:
63+
x = response
64+
response = response[:7]
65+
old_data = x[7:]
66+
elif addr == 9:
67+
response = old_data + response
68+
old_data = b''
69+
return response
70+
71+
async def local_call(addr: int) -> bool:
72+
"""Call read_holding_register and control."""
73+
nonlocal client
74+
reply = await client.read_holding_registers(address=addr, count=1)
75+
assert not reply.isError(), f"addr {addr} isError"
76+
assert reply.registers[0] == addr * 10, f"addr {addr} value"
77+
78+
stub = ModbusProtocolStub(use_cls, True, handler=local_handle_data)
79+
stub.stub_handle_data = local_handle_data
80+
await stub.start_run()
81+
82+
assert await client.connect()
83+
await asyncio.gather(*[local_call(x) for x in range(1, 10)])
2784
client.transport_close()
2885
stub.transport_close()

0 commit comments

Comments
 (0)