Skip to content

Commit e96829a

Browse files
gijzelaerrclaude
andcommitted
Fix USERDATA PDU header size (10 bytes, not 12)
USERDATA PDUs use a 10-byte header without error_class/error_code, while ACK/ACK_DATA use 12-byte headers. This was causing "Data section extends beyond PDU" errors when parsing USERDATA responses from real PLCs. Changes: - s7protocol.py: parse_response() now detects PDU type and uses correct header size (10 bytes for USERDATA, 12 bytes for ACK/ACK_DATA) - server/__init__.py: Build USERDATA responses with 10-byte header - client.py: Check for errors in data section return_code for USERDATA responses (errors are in data section, not header) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 20e6416 commit e96829a

File tree

3 files changed

+33
-17
lines changed

3 files changed

+33
-17
lines changed

snap7/client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1297,10 +1297,16 @@ def read_szl(self, ssl_id: int, index: int = 0) -> S7SZL:
12971297
response_data = conn.receive_data()
12981298
response = self.protocol.parse_response(response_data)
12991299

1300-
# Check for errors
1300+
# Check for errors in header (for ACK/ACK_DATA)
13011301
if response.get("error_code", 0) != 0:
13021302
raise RuntimeError(f"Read SZL failed with error: {response['error_code']}")
13031303

1304+
# Check for errors in data section (for USERDATA - return_code != 0xFF means error)
1305+
data_info = response.get("data", {})
1306+
return_code = data_info.get("return_code", 0xFF) if isinstance(data_info, dict) else 0xFF
1307+
if return_code != 0xFF:
1308+
raise RuntimeError(f"Read SZL failed with return code: {return_code:#02x}")
1309+
13041310
# Parse SZL response
13051311
szl_result = self.protocol.parse_read_szl_response(response)
13061312

snap7/s7protocol.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,12 +1162,28 @@ def parse_response(self, pdu: bytes) -> Dict[str, Any]:
11621162
Returns:
11631163
Parsed response data
11641164
"""
1165-
if len(pdu) < 12:
1165+
if len(pdu) < 10:
11661166
raise S7ProtocolError("PDU too short for S7 response header")
11671167

1168-
# Parse S7 response header (includes error class and error code)
1169-
header = struct.unpack(">BBHHHHBB", pdu[:12])
1170-
protocol_id, pdu_type, reserved, sequence, param_len, data_len, error_class, error_code = header
1168+
# First peek at PDU type to determine header size
1169+
pdu_type = pdu[1]
1170+
1171+
if pdu_type == S7PDUType.USERDATA:
1172+
# USERDATA PDUs have a 10-byte header (no error_class/error_code in header)
1173+
if len(pdu) < 10:
1174+
raise S7ProtocolError("PDU too short for USERDATA header")
1175+
header = struct.unpack(">BBHHHH", pdu[:10])
1176+
protocol_id, pdu_type, reserved, sequence, param_len, data_len = header
1177+
error_class = 0
1178+
error_code = 0
1179+
offset = 10
1180+
else:
1181+
# ACK/ACK_DATA PDUs have a 12-byte header (with error_class/error_code)
1182+
if len(pdu) < 12:
1183+
raise S7ProtocolError("PDU too short for ACK/ACK_DATA header")
1184+
header = struct.unpack(">BBHHHHBB", pdu[:12])
1185+
protocol_id, pdu_type, reserved, sequence, param_len, data_len, error_class, error_code = header
1186+
offset = 12
11711187

11721188
if protocol_id != 0x32:
11731189
raise S7ProtocolError(f"Invalid protocol ID: {protocol_id:#02x}")
@@ -1185,8 +1201,6 @@ def parse_response(self, pdu: bytes) -> Dict[str, Any]:
11851201
"error_code": (error_class << 8) | error_code,
11861202
}
11871203

1188-
offset = 12
1189-
11901204
# Parse parameters if present
11911205
if param_len > 0:
11921206
if offset + param_len > len(pdu):

snap7/server/__init__.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,20 +1980,18 @@ def _build_userdata_error_response(self, request: Dict[str, Any], error_code: in
19801980
0x00, # Reserved
19811981
)
19821982

1983-
# Data section: return code only
1984-
data_section = struct.pack(">BB", (error_code >> 8) & 0xFF, error_code & 0xFF)
1983+
# Data section: return code only (error code in transport format)
1984+
data_section = struct.pack(">BBH", (error_code >> 8) & 0xFF, 0x00, 0)
19851985

1986-
# Build S7 header with error bytes
1986+
# Build S7 header for USERDATA (10 bytes, no error_class/error_code in header)
19871987
header = struct.pack(
1988-
">BBHHHHBB",
1988+
">BBHHHH",
19891989
0x32, # Protocol ID
19901990
S7PDUType.USERDATA, # PDU type
19911991
0x0000, # Reserved
19921992
request.get("sequence", 0), # Sequence
19931993
len(param_data), # Parameter length
19941994
len(data_section), # Data length
1995-
(error_code >> 8) & 0xFF, # Error class
1996-
error_code & 0xFF, # Error code
19971995
)
19981996

19991997
return header + param_data + data_section
@@ -2031,17 +2029,15 @@ def _build_userdata_success_response(self, request: Dict[str, Any], userdata_par
20312029
# Data section: return code (0xFF = success) + data
20322030
data_section = struct.pack(">BBH", 0xFF, 0x09, len(data)) + data
20332031

2034-
# Build S7 header
2032+
# Build S7 header for USERDATA (10 bytes, no error_class/error_code in header)
20352033
header = struct.pack(
2036-
">BBHHHHBB",
2034+
">BBHHHH",
20372035
0x32, # Protocol ID
20382036
S7PDUType.USERDATA, # PDU type
20392037
0x0000, # Reserved
20402038
request.get("sequence", 0), # Sequence
20412039
len(param_data), # Parameter length
20422040
len(data_section), # Data length
2043-
0x00, # Error class (success)
2044-
0x00, # Error code (success)
20452041
)
20462042

20472043
return header + param_data + data_section

0 commit comments

Comments
 (0)