From 570547587246afe4cedadb15c3a31ee1158676d5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 30 Aug 2026 10:31:43 +0200 Subject: [PATCH 1/2] Do not close serial port when no response. --- pymodbus/transaction/transaction.py | 5 +++-- test/transaction/test_transaction.py | 24 +++++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/pymodbus/transaction/transaction.py b/pymodbus/transaction/transaction.py index 9d58a9857..429bcb915 100644 --- a/pymodbus/transaction/transaction.py +++ b/pymodbus/transaction/transaction.py @@ -11,7 +11,7 @@ from ..framer import FramerAscii, FramerBase, FramerRTU from ..logging import Log from ..pdu import ModbusPDU -from ..transport import CommParams, ModbusProtocol +from ..transport import CommParams, CommType, ModbusProtocol class TransactionManager(ModbusProtocol): @@ -169,7 +169,8 @@ def sync_execute(self, no_response_expected: bool, request: ModbusPDU) -> Modbus except asyncio.exceptions.TimeoutError: count_retries += 1 if self.count_until_disconnect < 0: - self.connection_lost(asyncio.TimeoutError("Server not responding")) + if self.comm_params.comm_type != CommType.SERIAL: + self.connection_lost(asyncio.TimeoutError("Server not responding")) raise self._io_exception_from_request( "ERROR: No response received of the last requests (default: retries+3), CLOSING CONNECTION.", request, diff --git a/test/transaction/test_transaction.py b/test/transaction/test_transaction.py index 66b9ed4d4..8ae7e302c 100755 --- a/test/transaction/test_transaction.py +++ b/test/transaction/test_transaction.py @@ -12,6 +12,7 @@ from pymodbus.pdu import DecodePDU, ExceptionResponse from pymodbus.pdu.bit_message import ReadCoilsRequest, ReadCoilsResponse from pymodbus.transaction import TransactionManager +from pymodbus.transport import CommType @pytest.mark.parametrize("use_port", [5098]) @@ -186,8 +187,8 @@ async def test_transaction_data_2(self, use_clc, test): transact.response_future.set_result((1, pdu)) transact.callback_data(packet) - @pytest.mark.parametrize("scenario", range(10)) - async def test_transaction_execute(self, use_clc, scenario): + @pytest.mark.parametrize("scenario", range(11)) + async def test_transaction_execute(self, use_clc, scenario): # noqa: C901 """Test tracers in disconnect.""" transact = TransactionManager( use_clc, @@ -236,12 +237,21 @@ async def test_transaction_execute(self, use_clc, scenario): await transact.execute(False, request) assert exc_info.value.fcode == request.function_code assert exc_info.value.transaction_id == request.transaction_id - elif scenario == 5: # wait receive,timeout, no_responses pass + elif scenario == 5: # wait receive,timeout, disconnect + transact.comm_params.timeout_connect = 0.1 + transact.comm_params.comm_type = CommType.SERIAL + transact.count_until_disconnect = -1 + transact.connection_lost = mock.Mock() # type: ignore[method-assign] + with pytest.raises(ModbusIOException) as exc_info: + await transact.execute(False, request) + assert exc_info.value.fcode == request.function_code + assert exc_info.value.transaction_id == request.transaction_id + elif scenario == 6: # wait receive,timeout, no_responses pass transact.comm_params.timeout_connect = 0.1 transact.connection_lost = mock.Mock() # type: ignore[method-assign] with pytest.raises(ModbusIOException): await transact.execute(False, request) - elif scenario == 6: # wait receive, cancel + elif scenario == 7: # wait receive, cancel transact.comm_params.timeout_connect = 0.2 resp = asyncio.create_task(transact.execute(False, request)) await asyncio.sleep(0.1) @@ -250,14 +260,14 @@ async def test_transaction_execute(self, use_clc, scenario): with pytest.raises(asyncio.CancelledError): await resp assert resp.cancelled() - elif scenario == 7: # response + elif scenario == 8: # response transact.comm_params.timeout_connect = 0.2 resp = asyncio.create_task(transact.execute(False, request)) await asyncio.sleep(0.1) transact.response_future.set_result(response) await asyncio.sleep(0.1) assert response == await resp - elif scenario == 8: # response wrong dev_id + elif scenario == 9: # response wrong dev_id transact.comm_params.timeout_connect = 0.2 resp = asyncio.create_task(transact.execute(False, request)) await asyncio.sleep(0.1) @@ -270,7 +280,7 @@ async def test_transaction_execute(self, use_clc, scenario): assert exc_info.value.fcode == request.function_code assert exc_info.value.dev_id == request.dev_id assert exc_info.value.transaction_id == request.transaction_id - else: # if scenario == 9: # response wrong tid + else: # if scenario == 10: # response wrong tid transact.comm_params.timeout_connect = 0.2 resp = asyncio.create_task(transact.execute(False, request)) await asyncio.sleep(0.1) From 125d4ae03a3304b49f65be1957365221134c1d74 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 30 Aug 2026 10:54:39 +0200 Subject: [PATCH 2/2] correction. --- pymodbus/transaction/transaction.py | 3 ++- test/transaction/test_transaction.py | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/pymodbus/transaction/transaction.py b/pymodbus/transaction/transaction.py index 429bcb915..188be10c7 100644 --- a/pymodbus/transaction/transaction.py +++ b/pymodbus/transaction/transaction.py @@ -226,7 +226,8 @@ async def execute( except asyncio.exceptions.TimeoutError: count_retries += 1 if self.count_until_disconnect < 0: - self.connection_lost(asyncio.TimeoutError("Server not responding")) + if self.comm_params.comm_type != CommType.SERIAL: + self.connection_lost(asyncio.TimeoutError("Server not responding")) raise self._io_exception_from_request( "ERROR: No response received of the last requests (default: retries+3), CLOSING CONNECTION.", request, diff --git a/test/transaction/test_transaction.py b/test/transaction/test_transaction.py index 8ae7e302c..6972ca0e5 100755 --- a/test/transaction/test_transaction.py +++ b/test/transaction/test_transaction.py @@ -431,8 +431,8 @@ def test_sync_transaction_instance(self, use_clc): sync_client=client, ) - @pytest.mark.parametrize("scenario", range(10)) - async def test_sync_transaction_execute(self, use_clc, scenario): + @pytest.mark.parametrize("scenario", range(11)) + async def test_sync_transaction_execute(self, use_clc, scenario): # noqa: C901 """Test tracers in disconnect.""" client = self.dummy_client(use_clc) transact = TransactionManager( @@ -483,11 +483,19 @@ async def test_sync_transaction_execute(self, use_clc, scenario): transact.sync_execute(False, request) assert exc_info.value.fcode == request.function_code assert exc_info.value.transaction_id == request.transaction_id - elif scenario == 5: # wait receive,timeout, no_responses pass + elif scenario == 5: # wait receive,timeout, disconnect + transact.comm_params.timeout_connect = 0.1 + transact.comm_params.comm_type = CommType.SERIAL + transact.count_until_disconnect = -1 + with pytest.raises(ModbusIOException) as exc_info: + transact.sync_execute(False, request) + assert exc_info.value.fcode == request.function_code + assert exc_info.value.transaction_id == request.transaction_id + elif scenario == 6: # wait receive,timeout, no_responses pass transact.comm_params.timeout_connect = 0.1 with pytest.raises(ModbusIOException): transact.sync_execute(False, request) - elif scenario == 6: # response + elif scenario == 7: # response transact.transport = 1 # type: ignore[assignment] resp_bytes = transact.framer.buildFrame(response) transact.sync_client.recv = mock.Mock(return_value=resp_bytes) @@ -495,7 +503,7 @@ async def test_sync_transaction_execute(self, use_clc, scenario): transact.comm_params.timeout_connect = 0.2 resp = transact.sync_execute(False, request) assert response.bits == resp.bits - elif scenario == 7: # response wrong dev_id + elif scenario == 8: # response wrong dev_id transact.transport = 1 # type: ignore[assignment] pdu = copy.deepcopy(response) pdu.dev_id = 17 @@ -507,7 +515,7 @@ async def test_sync_transaction_execute(self, use_clc, scenario): assert exc_info.value.fcode == request.function_code assert exc_info.value.dev_id == request.dev_id assert exc_info.value.transaction_id == request.transaction_id - elif scenario == 8: # response wrong tid + elif scenario == 9: # response wrong tid transact.transport = 1 # type: ignore[assignment] pdu = copy.deepcopy(response) pdu.transaction_id = 17 @@ -519,7 +527,7 @@ async def test_sync_transaction_execute(self, use_clc, scenario): assert exc_info.value.fcode == request.function_code assert exc_info.value.dev_id == request.dev_id assert exc_info.value.transaction_id == request.transaction_id - else: # if scenario == 9 # pdu_send from client + else: # if scenario == 10 # pdu_send from client transact.transport = 1 # type: ignore[assignment] transact.is_server = True resp_bytes = transact.framer.buildFrame(response)