Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ repos:
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: '\.bin$'
- id: end-of-file-fixer
exclude: '\.bin$'
- id: check-ast
- id: check-shebang-scripts-are-executable
- id: check-json
Expand Down
28 changes: 26 additions & 2 deletions s7/_s7commplus_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def connect(
tls_cert=tls_cert,
tls_key=tls_key,
tls_ca=tls_ca,
password=password or "",
)

if password is not None and self._connection.tls_active:
Expand Down Expand Up @@ -283,7 +284,10 @@ def explore(self, explore_id: int = 0) -> bytes:
if self._connection is None:
raise RuntimeError("Not connected")

payload = _build_explore_payload(explore_id)
if self._connection._session_key is not None:
payload = _build_explore_payload_v3(explore_id if explore_id else 0x38)
else:
payload = _build_explore_payload(explore_id)
response = self._connection.send_request(FunctionCode.EXPLORE, payload, integrity_tail=5, reassemble=True)
return response

Expand Down Expand Up @@ -387,7 +391,14 @@ def list_datablocks(self) -> list[dict[str, Any]]:
if self._connection is None:
raise RuntimeError("Not connected")

payload = _build_explore_request(Ids.NATIVE_THE_PLC_PROGRAM_RID, [Ids.OBJECT_VARIABLE_TYPE_NAME, Ids.BLOCK_BLOCK_NUMBER])
if self._connection._session_key is not None:
# V1-initial PLCs: explore the DB wildcard address (0x8A11FFFF)
# matching TIA Portal's browse pattern
payload = _build_explore_payload_v3(0x8A11FFFF)
else:
payload = _build_explore_request(
Ids.NATIVE_THE_PLC_PROGRAM_RID, [Ids.OBJECT_VARIABLE_TYPE_NAME, Ids.BLOCK_BLOCK_NUMBER]
)
response = self._connection.send_request(FunctionCode.EXPLORE, payload, integrity_tail=5, reassemble=True)
return _parse_explore_datablocks(response)

Expand Down Expand Up @@ -805,6 +816,19 @@ def _build_explore_payload(explore_id: int = 0) -> bytes:
return bytes(payload)


def _build_explore_payload_v3(explore_id: int, sequence: int = 10) -> bytes:
"""Build a V3-style EXPLORE request payload matching TIA Portal format.

V1-initial PLCs use a 4-byte big-endian InObjectId followed by
fixed parameters, rather than the VLQ-based format.
"""
payload = struct.pack(">I", explore_id)
payload += bytes([0x00, 0x01, 0x00, 0x01, 0x00, 0x00])
payload += bytes([sequence & 0xFF])
payload += bytes([0x00, 0x00, 0x00, 0x00, 0x00])
return payload


def _build_invoke_payload(state: int) -> bytes:
"""Build an INVOKE request payload for SetPlcOperatingState.

Expand Down
15 changes: 3 additions & 12 deletions s7/_s7commplus_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,16 +630,7 @@ def _build_response_header(self, function_code: int, seq_num: int) -> bytes:
def _handle_init_ssl(self, seq_num: int) -> bytes:
"""Handle InitSSL -- respond to SSL initialization (V1 emulation, no real TLS)."""
response = bytearray()
response += struct.pack(
">BHHHHIB",
Opcode.RESPONSE,
0x0000,
FunctionCode.INIT_SSL,
0x0000,
seq_num,
0x00000000,
0x00, # Transport flags
)
response += self._build_response_header(FunctionCode.INIT_SSL, seq_num)
response += encode_uint32_vlq(0) # Return code: success
response += struct.pack(">I", 0)
return bytes(response)
Expand All @@ -650,10 +641,10 @@ def _handle_create_object(self, seq_num: int, request_data: bytes) -> bytes:
session_id = self._next_session_id
self._next_session_id += 1

# Build CreateObject response
# Build CreateObject response — uses a 14-byte header (with SessionId)
# unlike other responses which use the 10-byte _build_response_header.
response = bytearray()

# Response header
response += struct.pack(
">BHHHHIB",
Opcode.RESPONSE,
Expand Down
Loading
Loading