From 7882cc53efd720cf3cdc4500223721c1e20867e6 Mon Sep 17 00:00:00 2001 From: Brunno Vanelli Date: Thu, 9 Jul 2026 21:56:54 +0200 Subject: [PATCH 1/3] fix: Fix IntegrityId and response parsing for Browse. - Shorten the expected response from 14 to 10 bytes since the response is not expected to carry the SessionId - Add the IntegrityId as being optional for the Browse symbolic read. I believe this has to be a incremental number, otherwise the PLC drops the connection. --- s7/_s7commplus_async_client.py | 13 +++++++------ s7/_s7commplus_client.py | 10 ++++++++-- s7/connection.py | 17 +++++++++-------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/s7/_s7commplus_async_client.py b/s7/_s7commplus_async_client.py index 19eaaab6..0fa3c796 100644 --- a/s7/_s7commplus_async_client.py +++ b/s7/_s7commplus_async_client.py @@ -488,7 +488,8 @@ async def read_symbolic(self, access_area: int, lids: list[int], symbol_crc: int .. warning:: This method is **experimental** and may change. """ - payload = _build_symbolic_read_payload(access_area, lids, symbol_crc) + # TODO: Send the correct integrity id once available + payload = _build_symbolic_read_payload(access_area, lids, symbol_crc, integrity_id=0) response = await self._send_request(FunctionCode.GET_MULTI_VARIABLES, payload) results = _parse_read_response(response) if not results or results[0] is None: @@ -815,12 +816,12 @@ async def _create_session(self) -> None: version, data_length, consumed = decode_header(response_data) response = response_data[consumed : consumed + data_length] - if len(response) < 14: + if len(response) < 10: raise RuntimeError("CreateObject response too short") - # Parse response body: ReturnValue(VLQ) + ObjectIdCount + ObjectIds(VLQ). - # The usable session id is ObjectIds[0] (NOT the header SessionId field). - body = response[14:] + # Response header is 10 bytes (opcode+reserved+func+reserved+seq+transport). + # Responses do NOT carry a SessionId field (unlike requests which are 14 bytes). + body = response[10:] object_ids, obj_end = parse_create_object_session_id(body) if object_ids: self._session_id = object_ids[0] @@ -828,7 +829,7 @@ async def _create_session(self) -> None: self._session_id = struct.unpack_from(">I", response, 9)[0] self._protocol_version = version - self._server_session_version = parse_server_session_version(response[14 + obj_end :]) + self._server_session_version = parse_server_session_version(response[10 + obj_end :]) if self._server_session_version is not None: logger.info(f"ServerSessionVersion captured: {len(self._server_session_version)} bytes") else: diff --git a/s7/_s7commplus_client.py b/s7/_s7commplus_client.py index 5bd99779..95a47d31 100644 --- a/s7/_s7commplus_client.py +++ b/s7/_s7commplus_client.py @@ -730,7 +730,12 @@ def _build_area_write_payload(area_rid: int, start: int, data: bytes) -> bytes: return bytes(payload) -def _build_symbolic_read_payload(access_area: int, lids: list[int], symbol_crc: int = 0) -> bytes: +def _build_symbolic_read_payload( + access_area: int, + lids: list[int], + symbol_crc: int = 0, + integrity_id: int = 1, +) -> bytes: """Build a GetMultiVariables payload for symbolic (LID-based) access. Used for optimized block access on S7-1200/1500 where byte offsets @@ -758,7 +763,8 @@ def _build_symbolic_read_payload(access_area: int, lids: list[int], symbol_crc: payload += encode_uint32_vlq(field_count) payload += addr_bytes payload += encode_object_qualifier() - payload += encode_uint32_vlq(1) + if integrity_id is not None: + payload += encode_uint32_vlq(integrity_id) payload += struct.pack(">I", 0) return bytes(payload) diff --git a/s7/connection.py b/s7/connection.py index 4df3af64..9fba4f4d 100644 --- a/s7/connection.py +++ b/s7/connection.py @@ -660,7 +660,7 @@ def _init_ssl(self) -> None: version, data_length, consumed = decode_header(response_frame) response = response_frame[consumed:] - if len(response) < 14: + if len(response) < 10: from snap7.error import S7ConnectionError raise S7ConnectionError("InitSSL response too short") @@ -751,14 +751,15 @@ def _create_session(self) -> None: logger.debug(f"CreateObject response: version=V{version}, data_length={data_length}") logger.debug(f"CreateObject response body ({len(response)} bytes): {response.hex(' ')}") - if len(response) < 14: + if len(response) < 10: from snap7.error import S7ConnectionError raise S7ConnectionError("CreateObject response too short") - # Parse response body: ReturnValue(UInt64 VLQ) + ObjectIdCount + ObjectIds(VLQ). - # The usable session id is ObjectIds[0] (NOT the header SessionId field). - body = response[14:] + # Response header is 10 bytes (opcode+reserved+func+reserved+seq+transport). + # Responses do NOT carry a SessionId field (unlike requests which are 14 bytes). + # The session ID comes from the payload body, not the header. + body = response[10:] object_ids, obj_end = parse_create_object_session_id(body) if object_ids: self._session_id = object_ids[0] @@ -770,16 +771,16 @@ def _create_session(self) -> None: resp_opcode = response[0] resp_func = struct.unpack_from(">H", response, 3)[0] resp_seq = struct.unpack_from(">H", response, 7)[0] - resp_transport = response[13] + resp_transport = response[9] logger.debug( f"CreateObject response header: opcode=0x{resp_opcode:02X} function=0x{resp_func:04X} " f"seq={resp_seq} session=0x{self._session_id:08X} transport=0x{resp_transport:02X}" ) - logger.debug(f"CreateObject response payload: {response[14:].hex(' ')}") + logger.debug(f"CreateObject response payload: {response[10:].hex(' ')}") logger.debug(f"Session created: id=0x{self._session_id:08X} ({self._session_id}), version=V{version}") # Parse response payload to extract ServerSessionVersion - self._server_session_version = parse_server_session_version(response[14 + obj_end :]) + self._server_session_version = parse_server_session_version(response[10 + obj_end :]) if self._server_session_version is not None: logger.info(f"ServerSessionVersion captured: {len(self._server_session_version)} bytes") else: From 6eee90003259b2361d80163d865ddbe03f08bd6d Mon Sep 17 00:00:00 2001 From: Brunno Vanelli Date: Thu, 9 Jul 2026 22:25:29 +0200 Subject: [PATCH 2/3] fix: Make code a bit more like the original function. --- s7/_s7commplus_async_client.py | 2 +- s7/_s7commplus_client.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/s7/_s7commplus_async_client.py b/s7/_s7commplus_async_client.py index 0fa3c796..c183e450 100644 --- a/s7/_s7commplus_async_client.py +++ b/s7/_s7commplus_async_client.py @@ -489,7 +489,7 @@ async def read_symbolic(self, access_area: int, lids: list[int], symbol_crc: int .. warning:: This method is **experimental** and may change. """ # TODO: Send the correct integrity id once available - payload = _build_symbolic_read_payload(access_area, lids, symbol_crc, integrity_id=0) + payload = _build_symbolic_read_payload(access_area, lids, symbol_crc, False, self._integrity_id_read) response = await self._send_request(FunctionCode.GET_MULTI_VARIABLES, payload) results = _parse_read_response(response) if not results or results[0] is None: diff --git a/s7/_s7commplus_client.py b/s7/_s7commplus_client.py index 95a47d31..b752d395 100644 --- a/s7/_s7commplus_client.py +++ b/s7/_s7commplus_client.py @@ -734,6 +734,7 @@ def _build_symbolic_read_payload( access_area: int, lids: list[int], symbol_crc: int = 0, + with_integrity: bool = True, integrity_id: int = 1, ) -> bytes: """Build a GetMultiVariables payload for symbolic (LID-based) access. @@ -763,7 +764,7 @@ def _build_symbolic_read_payload( payload += encode_uint32_vlq(field_count) payload += addr_bytes payload += encode_object_qualifier() - if integrity_id is not None: + if with_integrity: payload += encode_uint32_vlq(integrity_id) payload += struct.pack(">I", 0) return bytes(payload) From a5a750825772a4e58166ce503fdc1c3bb867f0a4 Mon Sep 17 00:00:00 2001 From: Brunno Vanelli Date: Fri, 10 Jul 2026 13:52:55 +0200 Subject: [PATCH 3/3] fix: Fix browsing for the sync code. --- s7/_s7commplus_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/s7/_s7commplus_client.py b/s7/_s7commplus_client.py index b752d395..c44ef8e2 100644 --- a/s7/_s7commplus_client.py +++ b/s7/_s7commplus_client.py @@ -238,7 +238,8 @@ def read_symbolic(self, access_area: int, lids: list[int], symbol_crc: int = 0) if self._connection is None: raise RuntimeError("Not connected") - payload = _build_symbolic_read_payload(access_area, lids, symbol_crc) + # TODO: Send the correct integrity id once available + payload = _build_symbolic_read_payload(access_area, lids, symbol_crc, False) response = self._connection.send_request(FunctionCode.GET_MULTI_VARIABLES, payload) results = _parse_read_response(response) if not results or results[0] is None: