Skip to content

Commit 3b59362

Browse files
committed
Test and hard delayed response test. (#2636)
1 parent b134b95 commit 3b59362

File tree

2 files changed

+71
-16
lines changed

2 files changed

+71
-16
lines changed

pymodbus/transaction/transaction.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from threading import RLock
77

88
from pymodbus.exceptions import ConnectionException, ModbusIOException
9-
from pymodbus.framer import FramerBase
9+
from pymodbus.framer import FramerAscii, FramerBase, FramerRTU
1010
from pymodbus.logging import Log
1111
from pymodbus.pdu import ExceptionResponse, ModbusPDU
1212
from pymodbus.transport import CommParams, ModbusProtocol
@@ -49,6 +49,7 @@ def __init__(
4949
self.retries = retries
5050
self.next_tid: int = 0
5151
self.request_dev_id: int = 0
52+
self.request_transaction_id: int = 0
5253
self.trace_packet = trace_packet or self.dummy_trace_packet
5354
self.trace_pdu = trace_pdu or self.dummy_trace_pdu
5455
self.trace_connect = trace_connect or self.dummy_trace_connect
@@ -191,6 +192,7 @@ async def execute(self, no_response_expected: bool, request: ModbusPDU) -> Modbu
191192
def pdu_send(self, pdu: ModbusPDU, addr: tuple | None = None) -> None:
192193
"""Build byte stream and send."""
193194
self.request_dev_id = pdu.dev_id
195+
self.request_transaction_id = pdu.transaction_id
194196
packet = self.framer.buildFrame(self.trace_pdu(True, pdu))
195197
if self.is_sync and self.comm_params.handle_local_echo:
196198
self.sent_buffer = packet
@@ -218,19 +220,20 @@ def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
218220
self.last_addr = addr
219221
if not self.is_server:
220222
if pdu.dev_id != self.request_dev_id:
221-
raise ModbusIOException(
222-
f"ERROR: request ask for id={self.request_dev_id} but got id={pdu.dev_id}, CLOSING CONNECTION."
223-
)
224-
if self.response_future.done():
225-
raise ModbusIOException("received pdu without a corresponding request")
226-
self.response_future.set_result(self.last_pdu
227-
228-
)
223+
Log.warning(f"ERROR: expected id {self.request_dev_id} but got {pdu.dev_id}, IGNORING.")
224+
elif pdu.transaction_id != self.request_transaction_id:
225+
Log.warning(f"ERROR: expected transaction {self.request_transaction_id} but got {pdu.transaction_id}, IGNORING.")
226+
elif self.response_future.done():
227+
Log.warning("ERROR: received pdu without a corresponding request, IGNORING")
228+
else:
229+
self.response_future.set_result(self.last_pdu)
229230
return used_len
230231

231232
def getNextTID(self) -> int:
232233
"""Retrieve the next transaction identifier."""
233-
if self.next_tid >= 65000:
234+
if isinstance(self.framer, (FramerAscii, FramerRTU)):
235+
self.next_tid = 0
236+
elif self.next_tid >= 65000:
234237
self.next_tid = 1
235238
else:
236239
self.next_tid += 1

test/transaction/test_transaction.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def test_transaction_manager_tid(self, use_clc):
4141
"""Test next TID."""
4242
transact = TransactionManager(
4343
use_clc,
44-
FramerRTU(DecodePDU(False)),
44+
FramerSocket(DecodePDU(False)),
4545
5,
4646
False,
4747
None,
@@ -161,8 +161,7 @@ async def test_transaction_data_2(self, use_clc, test):
161161
else:
162162
pdu.dev_id = 0
163163
transact.response_future.set_result((1, pdu))
164-
with pytest.raises(ModbusIOException):
165-
transact.callback_data(packet)
164+
transact.callback_data(packet)
166165

167166
@pytest.mark.parametrize("scenario", range(8))
168167
async def test_transaction_execute(self, use_clc, scenario):
@@ -264,6 +263,7 @@ async def test_client_protocol_execute_outside(self, use_clc, no_resp):
264263
None,
265264
)
266265
transact.send = mock.Mock()
266+
transact.comm_params.timeout_connect = 0.1
267267
request = ReadCoilsRequest(address=117, count=5, dev_id=1)
268268
transact.retries = 0
269269
transact.connection_made(mock.AsyncMock())
@@ -272,13 +272,13 @@ async def test_client_protocol_execute_outside(self, use_clc, no_resp):
272272
await asyncio.sleep(0.2)
273273
data = b"\x00\x00\x12\x34\x00\x06\x01\x01\x01\x02\x00\x04"
274274
transact.data_received(data)
275-
result = await resp
276275
if no_resp:
276+
result = await resp
277277
assert result.isError()
278278
assert isinstance(result, ExceptionResponse)
279279
else:
280-
assert not result.isError()
281-
assert isinstance(result, ReadCoilsResponse)
280+
with pytest.raises(ModbusIOException):
281+
await resp
282282

283283
async def test_transaction_id0(self, use_clc):
284284
"""Test tracers in disconnect."""
@@ -315,6 +315,58 @@ async def test_transaction_id0(self, use_clc):
315315
await asyncio.sleep(0.1)
316316
assert response == await resp
317317

318+
@pytest.mark.parametrize(("framer"), [FramerRTU, FramerSocket])
319+
@pytest.mark.parametrize("scenario", range(2))
320+
async def test_delayed_response(self, use_clc, framer, scenario):
321+
"""Test delayed rtu response combined with retries."""
322+
transact = TransactionManager(
323+
use_clc,
324+
framer(DecodePDU(False)),
325+
5,
326+
False,
327+
None,
328+
None,
329+
None,
330+
)
331+
transact.send = mock.Mock()
332+
request1 = ReadCoilsRequest(address=117, count=5, dev_id=1)
333+
request2 = ReadCoilsRequest(address=118, count=2, dev_id=1)
334+
response1 = ReadCoilsResponse(bits=[True, False, True, True] + [False]*4, dev_id=1)
335+
response2 = ReadCoilsResponse(bits=[True] + [False]*7, dev_id=1)
336+
if framer == FramerRTU:
337+
cb_response1 = b'\x01\x01\x01\r\x90M'
338+
cb_response2 = b'\x01\x01\x01\x01\x90H'
339+
else:
340+
cb_response1 = b'\x00\x01\x00\x00\x00\x04\x01\x01\x01\r'
341+
cb_response2 = b'\x00\x02\x00\x00\x00\x04\x01\x01\x01\x01'
342+
transact.retries = 1
343+
transact.connection_made(mock.AsyncMock())
344+
transact.transport.write = mock.Mock()
345+
transact.comm_params.timeout_connect = 0.1
346+
347+
if scenario == 0: # timeout + double response
348+
resp = asyncio.create_task(transact.execute(False, request1))
349+
await asyncio.sleep(0.15)
350+
transact.callback_data(cb_response1, None)
351+
transact.callback_data(cb_response1, None)
352+
result = await resp
353+
assert result.bits == response1.bits
354+
elif scenario == 1: # timeout + new request + double response
355+
resp = asyncio.create_task(transact.execute(False, request1))
356+
await asyncio.sleep(0.25)
357+
with pytest.raises(ModbusIOException):
358+
await resp
359+
resp = asyncio.create_task(transact.execute(False, request2))
360+
await asyncio.sleep(0.05)
361+
transact.callback_data(cb_response1, None)
362+
transact.callback_data(cb_response2, None)
363+
result = await resp
364+
if framer == FramerRTU:
365+
# Return WRONG response
366+
assert result.bits == response1.bits
367+
else:
368+
# Return CORRECT response
369+
assert result.bits == response2.bits
318370

319371
@pytest.mark.parametrize("use_port", [5098])
320372
class TestSyncTransaction:

0 commit comments

Comments
 (0)