diff --git a/snap7/client.py b/snap7/client.py index 7cc5ee32..c6b25956 100644 --- a/snap7/client.py +++ b/snap7/client.py @@ -1292,8 +1292,13 @@ def _read_multi_vars_optimized(self, dict_items: List[dict[str, Any]]) -> Tuple[ def _execute_packets_sequential(self, packet_requests: list[Tuple[int, bytes, ReadPacket]]) -> None: """Execute multi-block packets one at a time.""" - for _, request, packet in packet_requests: - response = self._send_receive(request) + for _, _request, packet in packet_requests: + block_specs = [(blk.area, blk.db_number, blk.start_offset, blk.byte_length) for blk in packet.blocks] + + def build_request(specs: list[tuple[int, int, int, int]] = block_specs) -> bytes: + return self.protocol.build_multi_read_request(specs) + + response = self._send_receive_with_reconnect(build_request) block_data_list = self.protocol.extract_multi_read_data(response, len(packet.blocks)) for blk, buf in zip(packet.blocks, block_data_list): blk.buffer = buf @@ -1302,9 +1307,20 @@ def _execute_packets_parallel(self, packet_requests: list[Tuple[int, bytes, Read """Execute multi-block packets using parallel dispatch. Sends up to *max_parallel* PDUs back-to-back before reading - responses, reducing round-trip overhead. + responses, reducing round-trip overhead. Falls back to + sequential reconnect-aware execution on connection loss. """ - # Process in chunks of max_parallel + try: + self._execute_packets_parallel_inner(packet_requests) + except (S7ConnectionError, OSError) as e: + if not self._auto_reconnect: + raise + logger.warning(f"Connection lost during parallel read: {e}") + self._do_reconnect() + self._execute_packets_sequential(packet_requests) + + def _execute_packets_parallel_inner(self, packet_requests: list[Tuple[int, bytes, ReadPacket]]) -> None: + """Inner parallel dispatch without reconnect handling.""" for chunk_start in range(0, len(packet_requests), self.max_parallel): chunk = packet_requests[chunk_start : chunk_start + self.max_parallel] requests = [(pkt_idx, pdu) for pkt_idx, pdu, _ in chunk] diff --git a/tests/test_reconnect.py b/tests/test_reconnect.py index f36293e0..c61f3f08 100644 --- a/tests/test_reconnect.py +++ b/tests/test_reconnect.py @@ -10,7 +10,8 @@ from snap7.client import Client from snap7.error import S7ConnectionError from snap7.server import Server -from snap7.type import SrvArea +from snap7.tags import Tag +from snap7.type import Area, SrvArea logging.basicConfig(level=logging.WARNING) @@ -152,6 +153,26 @@ def test_reconnect_on_write_failure(self) -> None: finally: client.disconnect() + def test_reconnect_on_multi_block_read(self) -> None: + """Client reconnects when connection drops during multi-block sequential read.""" + client = Client(auto_reconnect=True, max_retries=3, retry_delay=0.1) + client.connect(ip, rack, slot, tcpport) + + try: + tags = [Tag(Area.DB, db_number, i * 2, "INT") for i in range(20)] + data = client.read_tags(tags) + assert len(data) == 20 + + if client.connection and client.connection.socket: + client.connection.socket.close() + client.connected = False + + data = client.read_tags(tags) + assert len(data) == 20 + assert client.connected is True + finally: + client.disconnect() + def test_no_reconnect_when_disabled(self) -> None: """Without auto_reconnect, connection errors propagate immediately.""" client = Client(auto_reconnect=False)