python-snap7 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_snap7-3.0.0.dist-info/METADATA +94 -0
- python_snap7-3.0.0.dist-info/RECORD +22 -0
- python_snap7-3.0.0.dist-info/WHEEL +5 -0
- python_snap7-3.0.0.dist-info/entry_points.txt +2 -0
- python_snap7-3.0.0.dist-info/licenses/LICENSE +20 -0
- python_snap7-3.0.0.dist-info/top_level.txt +1 -0
- snap7/__init__.py +34 -0
- snap7/client.py +1923 -0
- snap7/connection.py +456 -0
- snap7/datatypes.py +312 -0
- snap7/error.py +444 -0
- snap7/logo.py +222 -0
- snap7/partner.py +680 -0
- snap7/py.typed +0 -0
- snap7/s7protocol.py +1509 -0
- snap7/server/__init__.py +2681 -0
- snap7/server/__main__.py +41 -0
- snap7/type.py +391 -0
- snap7/util/__init__.py +108 -0
- snap7/util/db.py +789 -0
- snap7/util/getters.py +766 -0
- snap7/util/setters.py +733 -0
snap7/s7protocol.py
ADDED
|
@@ -0,0 +1,1509 @@
|
|
|
1
|
+
"""
|
|
2
|
+
S7 protocol implementation.
|
|
3
|
+
|
|
4
|
+
Handles S7 PDU encoding/decoding and protocol operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import struct
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import List, Dict, Any
|
|
11
|
+
from enum import IntEnum
|
|
12
|
+
|
|
13
|
+
from .datatypes import S7Area, S7WordLen, S7DataTypes
|
|
14
|
+
from .error import S7ProtocolError, S7StalePacketError, S7PacketLostError, get_protocol_error_message
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class S7Function(IntEnum):
|
|
20
|
+
"""S7 protocol function codes."""
|
|
21
|
+
|
|
22
|
+
READ_AREA = 0x04
|
|
23
|
+
WRITE_AREA = 0x05
|
|
24
|
+
REQUEST_DOWNLOAD = 0x1A
|
|
25
|
+
DOWNLOAD_BLOCK = 0x1B
|
|
26
|
+
DOWNLOAD_ENDED = 0x1C
|
|
27
|
+
START_UPLOAD = 0x1D
|
|
28
|
+
UPLOAD = 0x1E
|
|
29
|
+
END_UPLOAD = 0x1F
|
|
30
|
+
PLC_CONTROL = 0x28
|
|
31
|
+
PLC_STOP = 0x29
|
|
32
|
+
SETUP_COMMUNICATION = 0xF0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class S7PDUType(IntEnum):
|
|
36
|
+
"""S7 PDU type codes."""
|
|
37
|
+
|
|
38
|
+
REQUEST = 0x01
|
|
39
|
+
ACK = 0x02 # Acknowledge without data (e.g., write responses)
|
|
40
|
+
ACK_DATA = 0x03 # Acknowledge with data (e.g., read responses)
|
|
41
|
+
USERDATA = 0x07
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class S7UserDataGroup(IntEnum):
|
|
45
|
+
"""S7 USER_DATA type groups (from s7_types.h)."""
|
|
46
|
+
|
|
47
|
+
PROGRAMMER = 0x01 # grProgrammer
|
|
48
|
+
CYCLIC_DATA = 0x02 # grCyclicData
|
|
49
|
+
BLOCK_INFO = 0x03 # grBlocksInfo
|
|
50
|
+
SZL = 0x04 # grSZL
|
|
51
|
+
SECURITY = 0x05 # grPassword
|
|
52
|
+
TIME = 0x07 # grClock
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class S7UserDataSubfunction(IntEnum):
|
|
56
|
+
"""S7 USER_DATA subfunctions."""
|
|
57
|
+
|
|
58
|
+
# Block info subfunctions
|
|
59
|
+
LIST_ALL = 0x01 # SFun_ListAll
|
|
60
|
+
LIST_BLOCKS_OF_TYPE = 0x02 # SFun_ListBoT
|
|
61
|
+
BLOCK_INFO = 0x03 # SFun_BlkInfo
|
|
62
|
+
|
|
63
|
+
# SZL subfunctions
|
|
64
|
+
READ_SZL = 0x01 # SFun_ReadSZL
|
|
65
|
+
SYSTEM_STATE = 0x02 # System state request
|
|
66
|
+
|
|
67
|
+
# Clock subfunctions
|
|
68
|
+
GET_CLOCK = 0x01
|
|
69
|
+
SET_CLOCK = 0x02
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# S7 data section return codes with human-readable descriptions
|
|
73
|
+
S7_RETURN_CODES: Dict[int, str] = {
|
|
74
|
+
0x00: "Reserved",
|
|
75
|
+
0x01: "Hardware error",
|
|
76
|
+
0x03: "Accessing the object not allowed",
|
|
77
|
+
0x05: "Invalid address",
|
|
78
|
+
0x06: "Data type not supported",
|
|
79
|
+
0x07: "Data type inconsistent",
|
|
80
|
+
0x0A: "Object does not exist",
|
|
81
|
+
0x10: "Invalid block type number",
|
|
82
|
+
0x11: "Block not found in storage medium",
|
|
83
|
+
0x12: "Block already exists",
|
|
84
|
+
0x13: "Block is protected",
|
|
85
|
+
0x14: "Block download without proper block first",
|
|
86
|
+
0x19: "Block download sequence error",
|
|
87
|
+
0x1A: "Insufficient working memory",
|
|
88
|
+
0x1B: "Insufficient load memory",
|
|
89
|
+
0x1C: "Not enough work retentive data (instance DBs)",
|
|
90
|
+
0x1D: "Interface error",
|
|
91
|
+
0x1E: "Delete block refused",
|
|
92
|
+
0x20: "Invalid parameter",
|
|
93
|
+
0x21: "PG resource error (max connections reached)",
|
|
94
|
+
0xFF: "Success",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_return_code_description(return_code: int) -> str:
|
|
99
|
+
"""Get human-readable description for S7 return code."""
|
|
100
|
+
if return_code in S7_RETURN_CODES:
|
|
101
|
+
return S7_RETURN_CODES[return_code]
|
|
102
|
+
return "Unknown error"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class S7Protocol:
|
|
106
|
+
"""
|
|
107
|
+
S7 protocol implementation.
|
|
108
|
+
|
|
109
|
+
Handles encoding and decoding of S7 PDUs for communication with Siemens PLCs.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self) -> None:
|
|
113
|
+
self.sequence = 0 # Message sequence counter
|
|
114
|
+
|
|
115
|
+
def _next_sequence(self) -> int:
|
|
116
|
+
"""Get next sequence number for S7 PDU."""
|
|
117
|
+
self.sequence = (self.sequence + 1) & 0xFFFF
|
|
118
|
+
return self.sequence
|
|
119
|
+
|
|
120
|
+
def validate_pdu_reference(self, response_sequence: int) -> None:
|
|
121
|
+
"""Validate the PDU reference number from a response.
|
|
122
|
+
|
|
123
|
+
Compares the response sequence number against the expected (current) sequence.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
response_sequence: Sequence number from the response PDU.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
S7StalePacketError: If response is older than expected (stale).
|
|
130
|
+
S7PacketLostError: If response is ahead of expected (packet loss).
|
|
131
|
+
"""
|
|
132
|
+
if response_sequence < self.sequence:
|
|
133
|
+
raise S7StalePacketError(f"Stale packet: expected sequence {self.sequence}, got {response_sequence}")
|
|
134
|
+
elif response_sequence > self.sequence:
|
|
135
|
+
raise S7PacketLostError(f"Packet lost: expected sequence {self.sequence}, got {response_sequence}")
|
|
136
|
+
|
|
137
|
+
def build_read_request(self, area: S7Area, db_number: int, start: int, word_len: S7WordLen, count: int) -> bytes:
|
|
138
|
+
"""
|
|
139
|
+
Build S7 read request PDU.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
area: Memory area to read from
|
|
143
|
+
db_number: DB number (for DB area)
|
|
144
|
+
start: Start address/offset
|
|
145
|
+
word_len: Data word length
|
|
146
|
+
count: Number of items to read
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Complete S7 PDU
|
|
150
|
+
"""
|
|
151
|
+
# S7 Header (12 bytes)
|
|
152
|
+
header = struct.pack(
|
|
153
|
+
">BBHHHH",
|
|
154
|
+
0x32, # Protocol ID
|
|
155
|
+
S7PDUType.REQUEST, # PDU type
|
|
156
|
+
0x0000, # Reserved
|
|
157
|
+
self._next_sequence(), # Sequence
|
|
158
|
+
0x000E, # Parameter length (14 bytes)
|
|
159
|
+
0x0000, # Data length (no data for read)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Parameter section (14 bytes)
|
|
163
|
+
parameters = struct.pack(
|
|
164
|
+
">BBB",
|
|
165
|
+
S7Function.READ_AREA, # Function code
|
|
166
|
+
0x01, # Item count
|
|
167
|
+
0x12, # Variable specification
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Add address specification
|
|
171
|
+
address_spec = S7DataTypes.encode_address(area, db_number, start, word_len, count)
|
|
172
|
+
parameters += address_spec[1:] # Skip first byte (already included as 0x12)
|
|
173
|
+
|
|
174
|
+
return header + parameters
|
|
175
|
+
|
|
176
|
+
def build_write_request(self, area: S7Area, db_number: int, start: int, word_len: S7WordLen, data: bytes) -> bytes:
|
|
177
|
+
"""
|
|
178
|
+
Build S7 write request PDU.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
area: Memory area to write to
|
|
182
|
+
db_number: DB number (for DB area)
|
|
183
|
+
start: Start address/offset
|
|
184
|
+
word_len: Data word length
|
|
185
|
+
data: Data to write
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Complete S7 PDU
|
|
189
|
+
"""
|
|
190
|
+
# Calculate count from data length
|
|
191
|
+
item_size = S7DataTypes.get_size_bytes(word_len, 1)
|
|
192
|
+
count = len(data) // item_size
|
|
193
|
+
|
|
194
|
+
# Parameter length: function + item count + address spec
|
|
195
|
+
param_len = 3 + 11 # 14 bytes total
|
|
196
|
+
|
|
197
|
+
# Data length: transport size + data
|
|
198
|
+
data_len = 4 + len(data) # Transport size (4 bytes) + actual data
|
|
199
|
+
|
|
200
|
+
# S7 Header
|
|
201
|
+
header = struct.pack(
|
|
202
|
+
">BBHHHH",
|
|
203
|
+
0x32, # Protocol ID
|
|
204
|
+
S7PDUType.REQUEST, # PDU type
|
|
205
|
+
0x0000, # Reserved
|
|
206
|
+
self._next_sequence(), # Sequence
|
|
207
|
+
param_len, # Parameter length
|
|
208
|
+
data_len, # Data length
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Parameter section
|
|
212
|
+
parameters = struct.pack(
|
|
213
|
+
">BBB",
|
|
214
|
+
S7Function.WRITE_AREA, # Function code
|
|
215
|
+
0x01, # Item count
|
|
216
|
+
0x12, # Variable specification
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Add address specification
|
|
220
|
+
address_spec = S7DataTypes.encode_address(area, db_number, start, word_len, count)
|
|
221
|
+
parameters += address_spec[1:] # Skip first byte
|
|
222
|
+
|
|
223
|
+
# Map word_len to data section transport size
|
|
224
|
+
# Data section uses different transport size codes than address specification:
|
|
225
|
+
# - 0x03 = BIT
|
|
226
|
+
# - 0x04 = BYTE/WORD/DWORD (byte-oriented data)
|
|
227
|
+
# - 0x05 = INT
|
|
228
|
+
# - 0x06 = DINT
|
|
229
|
+
# - 0x07 = REAL
|
|
230
|
+
# - 0x09 = OCTET STRING
|
|
231
|
+
transport_size_map = {
|
|
232
|
+
S7WordLen.BIT: 0x03,
|
|
233
|
+
S7WordLen.BYTE: 0x04,
|
|
234
|
+
S7WordLen.CHAR: 0x04,
|
|
235
|
+
S7WordLen.WORD: 0x04,
|
|
236
|
+
S7WordLen.INT: 0x05,
|
|
237
|
+
S7WordLen.DWORD: 0x04,
|
|
238
|
+
S7WordLen.DINT: 0x06,
|
|
239
|
+
S7WordLen.REAL: 0x07,
|
|
240
|
+
S7WordLen.COUNTER: 0x04,
|
|
241
|
+
S7WordLen.TIMER: 0x04,
|
|
242
|
+
}
|
|
243
|
+
transport_size = transport_size_map.get(word_len, 0x04)
|
|
244
|
+
|
|
245
|
+
# Data section
|
|
246
|
+
data_section = (
|
|
247
|
+
struct.pack(
|
|
248
|
+
">BBH",
|
|
249
|
+
0x00, # Reserved/Error
|
|
250
|
+
transport_size, # Transport size (proper S7 data section format)
|
|
251
|
+
len(data) * 8, # Bit length (data length in bits)
|
|
252
|
+
)
|
|
253
|
+
+ data
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return header + parameters + data_section
|
|
257
|
+
|
|
258
|
+
def build_setup_communication_request(self, max_amq_caller: int = 1, max_amq_callee: int = 1, pdu_length: int = 480) -> bytes:
|
|
259
|
+
"""
|
|
260
|
+
Build S7 setup communication request.
|
|
261
|
+
|
|
262
|
+
This negotiates communication parameters with the PLC.
|
|
263
|
+
"""
|
|
264
|
+
header = struct.pack(
|
|
265
|
+
">BBHHHH",
|
|
266
|
+
0x32, # Protocol ID
|
|
267
|
+
S7PDUType.REQUEST, # PDU type
|
|
268
|
+
0x0000, # Reserved
|
|
269
|
+
self._next_sequence(), # Sequence
|
|
270
|
+
0x0008, # Parameter length (8 bytes)
|
|
271
|
+
0x0000, # Data length
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
parameters = struct.pack(
|
|
275
|
+
">BBHHH",
|
|
276
|
+
S7Function.SETUP_COMMUNICATION, # Function code
|
|
277
|
+
0x00, # Reserved
|
|
278
|
+
max_amq_caller, # Max AMQ caller
|
|
279
|
+
max_amq_callee, # Max AMQ callee
|
|
280
|
+
pdu_length, # PDU length
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return header + parameters
|
|
284
|
+
|
|
285
|
+
def build_plc_control_request(self, operation: str) -> bytes:
|
|
286
|
+
"""
|
|
287
|
+
Build PLC control request.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
operation: Control operation ('stop', 'hot_start', 'cold_start')
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Complete S7 PDU for PLC control
|
|
294
|
+
"""
|
|
295
|
+
# Map operations to S7 control codes
|
|
296
|
+
control_codes = {
|
|
297
|
+
"stop": 0x29, # PLC_STOP
|
|
298
|
+
"hot_start": 0x28, # PLC_CONTROL (warm restart)
|
|
299
|
+
"cold_start": 0x28, # PLC_CONTROL (cold restart)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if operation not in control_codes:
|
|
303
|
+
raise ValueError(f"Unknown PLC control operation: {operation}")
|
|
304
|
+
|
|
305
|
+
function_code = control_codes[operation]
|
|
306
|
+
|
|
307
|
+
# Build control-specific parameters
|
|
308
|
+
if operation == "stop":
|
|
309
|
+
# Simple stop command
|
|
310
|
+
param_data = struct.pack(">B", function_code)
|
|
311
|
+
else:
|
|
312
|
+
# Start commands with restart type
|
|
313
|
+
restart_type = 1 if operation == "hot_start" else 2 # 1=warm, 2=cold
|
|
314
|
+
param_data = struct.pack(">BB", function_code, restart_type)
|
|
315
|
+
|
|
316
|
+
header = struct.pack(
|
|
317
|
+
">BBHHHH",
|
|
318
|
+
0x32, # Protocol ID
|
|
319
|
+
S7PDUType.REQUEST, # PDU type
|
|
320
|
+
0x0000, # Reserved
|
|
321
|
+
self._next_sequence(), # Sequence
|
|
322
|
+
len(param_data), # Parameter length
|
|
323
|
+
0x0000, # Data length
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return header + param_data
|
|
327
|
+
|
|
328
|
+
def check_control_response(self, response: Dict[str, Any]) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Check PLC control response for errors.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
response: Parsed S7 response
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
S7ProtocolError: If control operation failed
|
|
337
|
+
"""
|
|
338
|
+
# For now, just check that we got a response
|
|
339
|
+
# In a full implementation, we would check specific error codes
|
|
340
|
+
if response.get("error_code", 0) != 0:
|
|
341
|
+
raise S7ProtocolError(f"PLC control failed with error: {response['error_code']}")
|
|
342
|
+
|
|
343
|
+
def build_compress_request(self) -> bytes:
|
|
344
|
+
"""
|
|
345
|
+
Build PLC control request for memory compression.
|
|
346
|
+
|
|
347
|
+
Uses PI service "_MSZL" (compress memory).
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Complete S7 PDU for compress request
|
|
351
|
+
"""
|
|
352
|
+
# PI service command for compress
|
|
353
|
+
pi_service = b"_MSZL"
|
|
354
|
+
|
|
355
|
+
# Parameter section: function code + PI service
|
|
356
|
+
# Format: func(1) + unknown(7) + pi_len(1) + pi_service
|
|
357
|
+
param_data = (
|
|
358
|
+
struct.pack(
|
|
359
|
+
">BBBBBBBBB",
|
|
360
|
+
S7Function.PLC_CONTROL, # 0x28
|
|
361
|
+
0x00, # Reserved
|
|
362
|
+
0x00, # Reserved
|
|
363
|
+
0x00, # Reserved
|
|
364
|
+
0x00, # Reserved
|
|
365
|
+
0x00, # Reserved
|
|
366
|
+
0x00, # Reserved
|
|
367
|
+
0x00, # Reserved
|
|
368
|
+
len(pi_service), # PI service length
|
|
369
|
+
)
|
|
370
|
+
+ pi_service
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
header = struct.pack(
|
|
374
|
+
">BBHHHH",
|
|
375
|
+
0x32, # Protocol ID
|
|
376
|
+
S7PDUType.REQUEST, # PDU type
|
|
377
|
+
0x0000, # Reserved
|
|
378
|
+
self._next_sequence(), # Sequence
|
|
379
|
+
len(param_data), # Parameter length
|
|
380
|
+
0x0000, # Data length
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return header + param_data
|
|
384
|
+
|
|
385
|
+
def build_copy_ram_to_rom_request(self) -> bytes:
|
|
386
|
+
"""
|
|
387
|
+
Build PLC control request for copying RAM to ROM.
|
|
388
|
+
|
|
389
|
+
Uses PI service "_MSZL" with file system parameters.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Complete S7 PDU for copy RAM to ROM request
|
|
393
|
+
"""
|
|
394
|
+
# PI service command for copy RAM to ROM
|
|
395
|
+
# Uses EP parameter for target file system
|
|
396
|
+
pi_service = b"_MSZL"
|
|
397
|
+
file_id = b"P" # P = passive file system (ROM)
|
|
398
|
+
|
|
399
|
+
# Parameter section with file system identifier
|
|
400
|
+
param_data = (
|
|
401
|
+
struct.pack(
|
|
402
|
+
">BBBBBBBBB",
|
|
403
|
+
S7Function.PLC_CONTROL, # 0x28
|
|
404
|
+
0x00, # Reserved
|
|
405
|
+
0x00, # Reserved
|
|
406
|
+
0x00, # Reserved
|
|
407
|
+
0x00, # Reserved
|
|
408
|
+
0x00, # Reserved
|
|
409
|
+
0x00, # Reserved
|
|
410
|
+
len(file_id), # File ID length
|
|
411
|
+
len(pi_service), # PI service length
|
|
412
|
+
)
|
|
413
|
+
+ file_id
|
|
414
|
+
+ pi_service
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
header = struct.pack(
|
|
418
|
+
">BBHHHH",
|
|
419
|
+
0x32, # Protocol ID
|
|
420
|
+
S7PDUType.REQUEST, # PDU type
|
|
421
|
+
0x0000, # Reserved
|
|
422
|
+
self._next_sequence(), # Sequence
|
|
423
|
+
len(param_data), # Parameter length
|
|
424
|
+
0x0000, # Data length
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return header + param_data
|
|
428
|
+
|
|
429
|
+
# ========================================================================
|
|
430
|
+
# Block Transfer PDU Builders (Upload/Download)
|
|
431
|
+
# ========================================================================
|
|
432
|
+
|
|
433
|
+
def build_start_upload_request(self, block_type: int, block_num: int) -> bytes:
|
|
434
|
+
"""
|
|
435
|
+
Build start upload request.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
block_type: Block type code (0x38=OB, 0x41=DB, 0x42=SDB, 0x43=FC, 0x44=SFC, 0x45=FB, 0x46=SFB)
|
|
439
|
+
block_num: Block number
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Complete S7 PDU for start upload request
|
|
443
|
+
"""
|
|
444
|
+
# Block address string: e.g., "0A00001P" for DB1
|
|
445
|
+
# Format: block_type (2 hex) + block_num (5 digits) + file_system (1 char)
|
|
446
|
+
block_addr = f"{block_type:02X}{block_num:05d}A".encode("ascii")
|
|
447
|
+
|
|
448
|
+
# Parameters: function + status + reserved + upload_id + block_addr_len + block_addr
|
|
449
|
+
param_data = (
|
|
450
|
+
struct.pack(
|
|
451
|
+
">BBBIB",
|
|
452
|
+
S7Function.START_UPLOAD, # Function code
|
|
453
|
+
0x00, # Status
|
|
454
|
+
0x00, # Reserved (error code)
|
|
455
|
+
0x00000000, # Upload ID (0 for start)
|
|
456
|
+
len(block_addr), # Block address length
|
|
457
|
+
)
|
|
458
|
+
+ block_addr
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
header = struct.pack(
|
|
462
|
+
">BBHHHH",
|
|
463
|
+
0x32, # Protocol ID
|
|
464
|
+
S7PDUType.REQUEST, # PDU type
|
|
465
|
+
0x0000, # Reserved
|
|
466
|
+
self._next_sequence(), # Sequence
|
|
467
|
+
len(param_data), # Parameter length
|
|
468
|
+
0x0000, # Data length
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
return header + param_data
|
|
472
|
+
|
|
473
|
+
def build_upload_request(self, upload_id: int) -> bytes:
|
|
474
|
+
"""
|
|
475
|
+
Build upload request to get block data.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
upload_id: Upload ID from start upload response
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Complete S7 PDU for upload request
|
|
482
|
+
"""
|
|
483
|
+
param_data = struct.pack(
|
|
484
|
+
">BBBI",
|
|
485
|
+
S7Function.UPLOAD, # Function code
|
|
486
|
+
0x00, # Status
|
|
487
|
+
0x00, # Reserved
|
|
488
|
+
upload_id, # Upload ID
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
header = struct.pack(
|
|
492
|
+
">BBHHHH",
|
|
493
|
+
0x32, # Protocol ID
|
|
494
|
+
S7PDUType.REQUEST, # PDU type
|
|
495
|
+
0x0000, # Reserved
|
|
496
|
+
self._next_sequence(), # Sequence
|
|
497
|
+
len(param_data), # Parameter length
|
|
498
|
+
0x0000, # Data length
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
return header + param_data
|
|
502
|
+
|
|
503
|
+
def build_end_upload_request(self, upload_id: int) -> bytes:
|
|
504
|
+
"""
|
|
505
|
+
Build end upload request.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
upload_id: Upload ID from start upload response
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Complete S7 PDU for end upload request
|
|
512
|
+
"""
|
|
513
|
+
param_data = struct.pack(
|
|
514
|
+
">BBBI",
|
|
515
|
+
S7Function.END_UPLOAD, # Function code
|
|
516
|
+
0x00, # Status
|
|
517
|
+
0x00, # Reserved
|
|
518
|
+
upload_id, # Upload ID
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
header = struct.pack(
|
|
522
|
+
">BBHHHH",
|
|
523
|
+
0x32, # Protocol ID
|
|
524
|
+
S7PDUType.REQUEST, # PDU type
|
|
525
|
+
0x0000, # Reserved
|
|
526
|
+
self._next_sequence(), # Sequence
|
|
527
|
+
len(param_data), # Parameter length
|
|
528
|
+
0x0000, # Data length
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return header + param_data
|
|
532
|
+
|
|
533
|
+
def parse_start_upload_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
|
534
|
+
"""
|
|
535
|
+
Parse start upload response.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Dictionary with upload_id and block_length
|
|
539
|
+
"""
|
|
540
|
+
result = {"upload_id": 0, "block_length": 0}
|
|
541
|
+
|
|
542
|
+
raw_params = response.get("raw_parameters", b"")
|
|
543
|
+
|
|
544
|
+
if len(raw_params) >= 8:
|
|
545
|
+
# Parse: function + status + reserved + upload_id
|
|
546
|
+
result["upload_id"] = struct.unpack(">I", raw_params[4:8])[0]
|
|
547
|
+
if len(raw_params) > 8:
|
|
548
|
+
# Block length string follows
|
|
549
|
+
len_field = raw_params[8]
|
|
550
|
+
if len(raw_params) > 9 + len_field:
|
|
551
|
+
length_str = raw_params[9 : 9 + len_field]
|
|
552
|
+
try:
|
|
553
|
+
result["block_length"] = int(length_str)
|
|
554
|
+
except ValueError:
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
def parse_upload_response(self, response: Dict[str, Any]) -> bytes:
|
|
560
|
+
"""
|
|
561
|
+
Parse upload response and extract block data.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Block data bytes
|
|
565
|
+
"""
|
|
566
|
+
data_info = response.get("data", {})
|
|
567
|
+
raw_data: bytes = data_info.get("data", b"")
|
|
568
|
+
|
|
569
|
+
# Skip the data header if present (length + unknown bytes)
|
|
570
|
+
if len(raw_data) > 2:
|
|
571
|
+
return raw_data
|
|
572
|
+
return b""
|
|
573
|
+
|
|
574
|
+
def build_download_request(self, block_type: int, block_num: int, block_data: bytes) -> bytes:
|
|
575
|
+
"""
|
|
576
|
+
Build request download request.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
block_type: Block type code
|
|
580
|
+
block_num: Block number
|
|
581
|
+
block_data: Block data to download
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Complete S7 PDU for request download
|
|
585
|
+
"""
|
|
586
|
+
# Block address string
|
|
587
|
+
block_addr = f"{block_type:02X}{block_num:05d}P".encode("ascii")
|
|
588
|
+
|
|
589
|
+
# Block length as string
|
|
590
|
+
length_str = f"{len(block_data):06d}".encode("ascii")
|
|
591
|
+
|
|
592
|
+
# Parameters
|
|
593
|
+
param_data = (
|
|
594
|
+
struct.pack(
|
|
595
|
+
">BBBBB",
|
|
596
|
+
S7Function.REQUEST_DOWNLOAD, # Function code
|
|
597
|
+
0x00, # Status
|
|
598
|
+
0x00, # Reserved
|
|
599
|
+
0x00, # Reserved
|
|
600
|
+
len(block_addr), # Block address length
|
|
601
|
+
)
|
|
602
|
+
+ block_addr
|
|
603
|
+
+ struct.pack(">B", len(length_str))
|
|
604
|
+
+ length_str
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
header = struct.pack(
|
|
608
|
+
">BBHHHH",
|
|
609
|
+
0x32, # Protocol ID
|
|
610
|
+
S7PDUType.REQUEST, # PDU type
|
|
611
|
+
0x0000, # Reserved
|
|
612
|
+
self._next_sequence(), # Sequence
|
|
613
|
+
len(param_data), # Parameter length
|
|
614
|
+
0x0000, # Data length
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
return header + param_data
|
|
618
|
+
|
|
619
|
+
def build_delete_block_request(self, block_type: int, block_num: int) -> bytes:
|
|
620
|
+
"""
|
|
621
|
+
Build delete block request.
|
|
622
|
+
|
|
623
|
+
Uses PLC_CONTROL with PI service "_DELE" for block deletion.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
block_type: Block type code
|
|
627
|
+
block_num: Block number
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
Complete S7 PDU for delete block request
|
|
631
|
+
"""
|
|
632
|
+
# PI service for delete
|
|
633
|
+
pi_service = b"_DELE"
|
|
634
|
+
|
|
635
|
+
# Block specification: type + number + filesystem
|
|
636
|
+
block_spec = f"{block_type:02X}{block_num:05d}P".encode("ascii")
|
|
637
|
+
|
|
638
|
+
# Parameter section
|
|
639
|
+
param_data = (
|
|
640
|
+
struct.pack(
|
|
641
|
+
">BBBBBBBBB",
|
|
642
|
+
S7Function.PLC_CONTROL, # 0x28
|
|
643
|
+
0x00, # Reserved
|
|
644
|
+
0x00, # Reserved
|
|
645
|
+
0x00, # Reserved
|
|
646
|
+
0x00, # Reserved
|
|
647
|
+
0x00, # Reserved
|
|
648
|
+
len(block_spec), # Block spec length
|
|
649
|
+
len(pi_service), # PI service length
|
|
650
|
+
0x00, # Reserved
|
|
651
|
+
)
|
|
652
|
+
+ block_spec
|
|
653
|
+
+ pi_service
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
header = struct.pack(
|
|
657
|
+
">BBHHHH",
|
|
658
|
+
0x32, # Protocol ID
|
|
659
|
+
S7PDUType.REQUEST, # PDU type
|
|
660
|
+
0x0000, # Reserved
|
|
661
|
+
self._next_sequence(), # Sequence
|
|
662
|
+
len(param_data), # Parameter length
|
|
663
|
+
0x0000, # Data length
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
return header + param_data
|
|
667
|
+
|
|
668
|
+
# ========================================================================
|
|
669
|
+
# USER_DATA PDU Builders (Chunk 3 of protocol implementation)
|
|
670
|
+
# ========================================================================
|
|
671
|
+
|
|
672
|
+
def build_list_blocks_request(self) -> bytes:
|
|
673
|
+
"""
|
|
674
|
+
Build USER_DATA request for listing all blocks.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Complete S7 PDU for list blocks request
|
|
678
|
+
"""
|
|
679
|
+
# USER_DATA PDU format:
|
|
680
|
+
# - S7 header (10 bytes)
|
|
681
|
+
# - Parameter section (8 bytes for USER_DATA)
|
|
682
|
+
# - Data section (4 bytes for list blocks)
|
|
683
|
+
|
|
684
|
+
# Parameter section for USER_DATA request
|
|
685
|
+
# Format: header + method + type|group + subfunction + seq
|
|
686
|
+
param_data = struct.pack(
|
|
687
|
+
">BBBBBBBB",
|
|
688
|
+
0x00, # Reserved
|
|
689
|
+
0x01, # Parameter count
|
|
690
|
+
0x12, # Type/length header
|
|
691
|
+
0x04, # Length of following data
|
|
692
|
+
0x11, # Method (0x11 = request)
|
|
693
|
+
0x43, # Type (4=request) | Group (3=grBlocksInfo)
|
|
694
|
+
S7UserDataSubfunction.LIST_ALL, # Subfunction (0x01 = list all)
|
|
695
|
+
0x00, # DataRef (0x00 for initial request)
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Data section: return code placeholder
|
|
699
|
+
data_section = struct.pack(
|
|
700
|
+
">BBH",
|
|
701
|
+
0x0A, # Return value (request)
|
|
702
|
+
0x00, # Transport size
|
|
703
|
+
0x0000, # Length (0 for request)
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# S7 header for USER_DATA
|
|
707
|
+
header = struct.pack(
|
|
708
|
+
">BBHHHH",
|
|
709
|
+
0x32, # Protocol ID
|
|
710
|
+
S7PDUType.USERDATA, # PDU type (0x07)
|
|
711
|
+
0x0000, # Reserved
|
|
712
|
+
self._next_sequence(), # Sequence
|
|
713
|
+
len(param_data), # Parameter length
|
|
714
|
+
len(data_section), # Data length
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
return header + param_data + data_section
|
|
718
|
+
|
|
719
|
+
def build_list_blocks_of_type_request(self, block_type: int) -> bytes:
|
|
720
|
+
"""
|
|
721
|
+
Build USER_DATA request for listing blocks of a specific type.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
block_type: Block type code (e.g., 0x41 for DB)
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
Complete S7 PDU for list blocks of type request
|
|
728
|
+
"""
|
|
729
|
+
# Parameter section for USER_DATA request
|
|
730
|
+
param_data = struct.pack(
|
|
731
|
+
">BBBBBBBB",
|
|
732
|
+
0x00, # Reserved
|
|
733
|
+
0x01, # Parameter count
|
|
734
|
+
0x12, # Type/length header
|
|
735
|
+
0x04, # Length of following data
|
|
736
|
+
0x11, # Method (0x11 = request)
|
|
737
|
+
0x43, # Type (4=request) | Group (3=grBlocksInfo)
|
|
738
|
+
S7UserDataSubfunction.LIST_BLOCKS_OF_TYPE, # Subfunction (0x02)
|
|
739
|
+
0x00, # DataRef (0x00 for initial request)
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# Data section: block type (0x30 prefix + type per Snap7 C format)
|
|
743
|
+
data_section = struct.pack(
|
|
744
|
+
">BBHBBBB",
|
|
745
|
+
0x0A, # Return value (request)
|
|
746
|
+
0x00, # Transport size
|
|
747
|
+
0x0004, # Length (4 bytes)
|
|
748
|
+
0x30, # Block type indicator
|
|
749
|
+
block_type, # Block type code
|
|
750
|
+
0x0A, # Trailing bytes per Snap7 C
|
|
751
|
+
0x00,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# S7 header for USER_DATA
|
|
755
|
+
header = struct.pack(
|
|
756
|
+
">BBHHHH",
|
|
757
|
+
0x32, # Protocol ID
|
|
758
|
+
S7PDUType.USERDATA, # PDU type (0x07)
|
|
759
|
+
0x0000, # Reserved
|
|
760
|
+
self._next_sequence(), # Sequence
|
|
761
|
+
len(param_data), # Parameter length
|
|
762
|
+
len(data_section), # Data length
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
return header + param_data + data_section
|
|
766
|
+
|
|
767
|
+
def parse_list_blocks_response(self, response: Dict[str, Any]) -> Dict[str, int]:
|
|
768
|
+
"""
|
|
769
|
+
Parse list blocks response and extract block counts.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
response: Parsed S7 response
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
Dictionary mapping block type names to counts
|
|
776
|
+
"""
|
|
777
|
+
result = {
|
|
778
|
+
"OBCount": 0,
|
|
779
|
+
"FBCount": 0,
|
|
780
|
+
"FCCount": 0,
|
|
781
|
+
"SFBCount": 0,
|
|
782
|
+
"SFCCount": 0,
|
|
783
|
+
"DBCount": 0,
|
|
784
|
+
"SDBCount": 0,
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
data_info = response.get("data", {})
|
|
788
|
+
raw_data = data_info.get("data", b"")
|
|
789
|
+
|
|
790
|
+
if not raw_data:
|
|
791
|
+
return result
|
|
792
|
+
|
|
793
|
+
# Parse block entries (4 bytes each: 0x30 | type | count_hi | count_lo)
|
|
794
|
+
# Block type codes
|
|
795
|
+
type_to_name = {
|
|
796
|
+
0x38: "OBCount", # Organization Block
|
|
797
|
+
0x41: "DBCount", # Data Block
|
|
798
|
+
0x42: "SDBCount", # System Data Block
|
|
799
|
+
0x43: "FCCount", # Function
|
|
800
|
+
0x44: "SFCCount", # System Function
|
|
801
|
+
0x45: "FBCount", # Function Block
|
|
802
|
+
0x46: "SFBCount", # System Function Block
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
offset = 0
|
|
806
|
+
while offset + 4 <= len(raw_data):
|
|
807
|
+
indicator = raw_data[offset]
|
|
808
|
+
block_type = raw_data[offset + 1]
|
|
809
|
+
count = struct.unpack(">H", raw_data[offset + 2 : offset + 4])[0]
|
|
810
|
+
|
|
811
|
+
if indicator == 0x30 and block_type in type_to_name:
|
|
812
|
+
result[type_to_name[block_type]] = count
|
|
813
|
+
|
|
814
|
+
offset += 4
|
|
815
|
+
|
|
816
|
+
return result
|
|
817
|
+
|
|
818
|
+
def parse_list_blocks_of_type_response(self, response: Dict[str, Any]) -> List[int]:
|
|
819
|
+
"""
|
|
820
|
+
Parse list blocks of type response and extract block numbers.
|
|
821
|
+
|
|
822
|
+
Args:
|
|
823
|
+
response: Parsed S7 response
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
List of block numbers
|
|
827
|
+
"""
|
|
828
|
+
result: List[int] = []
|
|
829
|
+
|
|
830
|
+
data_info = response.get("data", {})
|
|
831
|
+
raw_data = data_info.get("data", b"")
|
|
832
|
+
|
|
833
|
+
if not raw_data:
|
|
834
|
+
return result
|
|
835
|
+
|
|
836
|
+
# Parse block entries (4 bytes each per TDataFunGetBotItem:
|
|
837
|
+
# BlockNum(2) + Unknown(1) + BlockLang(1))
|
|
838
|
+
offset = 0
|
|
839
|
+
while offset + 4 <= len(raw_data):
|
|
840
|
+
block_num = struct.unpack(">H", raw_data[offset : offset + 2])[0]
|
|
841
|
+
result.append(block_num)
|
|
842
|
+
offset += 4
|
|
843
|
+
|
|
844
|
+
return result
|
|
845
|
+
|
|
846
|
+
def build_get_block_info_request(self, block_type: int, block_num: int) -> bytes:
|
|
847
|
+
"""
|
|
848
|
+
Build USER_DATA request for getting block information.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
block_type: Block type code (0x38=OB, 0x41=DB, 0x42=SDB, 0x43=FC, 0x44=SFC, 0x45=FB, 0x46=SFB)
|
|
852
|
+
block_num: Block number
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
Complete S7 PDU for get block info request
|
|
856
|
+
"""
|
|
857
|
+
# Parameter section for USER_DATA block info request
|
|
858
|
+
param_data = struct.pack(
|
|
859
|
+
">BBBBBBBB",
|
|
860
|
+
0x00, # Reserved
|
|
861
|
+
0x01, # Parameter count
|
|
862
|
+
0x12, # Type/length header
|
|
863
|
+
0x04, # Length of following data
|
|
864
|
+
0x11, # Method (0x11 = request)
|
|
865
|
+
0x43, # Type (4=request) | Group (3=grBlocksInfo)
|
|
866
|
+
S7UserDataSubfunction.BLOCK_INFO, # Subfunction (0x03)
|
|
867
|
+
0x00, # DataRef (0x00 for initial request)
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# Data section: [0x30, type, 'A', ASCII_num(5)] per Snap7 C format
|
|
871
|
+
# Block number is 5-digit zero-padded ASCII (e.g., 1 -> "00001")
|
|
872
|
+
block_num_ascii = f"{block_num:05d}".encode("ascii")
|
|
873
|
+
data_payload = struct.pack(">BB", 0x30, block_type) + b"A" + block_num_ascii
|
|
874
|
+
data_section = (
|
|
875
|
+
struct.pack(
|
|
876
|
+
">BBH",
|
|
877
|
+
0x0A, # Return value (request)
|
|
878
|
+
0x00, # Transport size
|
|
879
|
+
len(data_payload), # Length
|
|
880
|
+
)
|
|
881
|
+
+ data_payload
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
# S7 header for USER_DATA
|
|
885
|
+
header = struct.pack(
|
|
886
|
+
">BBHHHH",
|
|
887
|
+
0x32, # Protocol ID
|
|
888
|
+
S7PDUType.USERDATA, # PDU type (0x07)
|
|
889
|
+
0x0000, # Reserved
|
|
890
|
+
self._next_sequence(), # Sequence
|
|
891
|
+
len(param_data), # Parameter length
|
|
892
|
+
len(data_section), # Data length
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
return header + param_data + data_section
|
|
896
|
+
|
|
897
|
+
def parse_get_block_info_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
|
898
|
+
"""
|
|
899
|
+
Parse get block info response.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
response: Parsed S7 response
|
|
903
|
+
|
|
904
|
+
Returns:
|
|
905
|
+
Dictionary with block info fields
|
|
906
|
+
"""
|
|
907
|
+
result: Dict[str, Any] = {
|
|
908
|
+
"block_type": 0,
|
|
909
|
+
"block_number": 0,
|
|
910
|
+
"block_lang": 0,
|
|
911
|
+
"block_flags": 0,
|
|
912
|
+
"mc7_size": 0,
|
|
913
|
+
"load_size": 0,
|
|
914
|
+
"local_data": 0,
|
|
915
|
+
"sbb_length": 0,
|
|
916
|
+
"checksum": 0,
|
|
917
|
+
"version": 0,
|
|
918
|
+
"code_date": b"",
|
|
919
|
+
"intf_date": b"",
|
|
920
|
+
"author": b"",
|
|
921
|
+
"family": b"",
|
|
922
|
+
"header": b"",
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
data_info = response.get("data", {})
|
|
926
|
+
raw_data = data_info.get("data", b"")
|
|
927
|
+
|
|
928
|
+
if len(raw_data) < 78:
|
|
929
|
+
return result
|
|
930
|
+
|
|
931
|
+
# Parse block info structure per TResDataBlockInfo layout
|
|
932
|
+
result["block_type"] = raw_data[1]
|
|
933
|
+
result["block_flags"] = raw_data[9]
|
|
934
|
+
result["block_lang"] = raw_data[10]
|
|
935
|
+
result["block_number"] = struct.unpack(">H", raw_data[12:14])[0]
|
|
936
|
+
result["load_size"] = struct.unpack(">I", raw_data[14:18])[0]
|
|
937
|
+
result["sbb_length"] = struct.unpack(">H", raw_data[34:36])[0]
|
|
938
|
+
result["local_data"] = struct.unpack(">H", raw_data[38:40])[0]
|
|
939
|
+
result["mc7_size"] = struct.unpack(">H", raw_data[40:42])[0]
|
|
940
|
+
result["version"] = raw_data[66]
|
|
941
|
+
result["checksum"] = struct.unpack(">H", raw_data[68:70])[0]
|
|
942
|
+
|
|
943
|
+
# Dates (6 bytes each: ms(2) + days(2) + reserved(2))
|
|
944
|
+
result["code_date"] = raw_data[22:28]
|
|
945
|
+
result["intf_date"] = raw_data[28:34]
|
|
946
|
+
|
|
947
|
+
# Strings (8 bytes each)
|
|
948
|
+
result["author"] = raw_data[42:50]
|
|
949
|
+
result["family"] = raw_data[50:58]
|
|
950
|
+
result["header"] = raw_data[58:66]
|
|
951
|
+
|
|
952
|
+
return result
|
|
953
|
+
|
|
954
|
+
def build_read_szl_request(self, szl_id: int, szl_index: int) -> bytes:
|
|
955
|
+
"""
|
|
956
|
+
Build USER_DATA request for reading SZL (System Status List).
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
szl_id: SZL identifier
|
|
960
|
+
szl_index: SZL index
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
Complete S7 PDU for read SZL request
|
|
964
|
+
"""
|
|
965
|
+
# Parameter section for USER_DATA SZL request
|
|
966
|
+
param_data = struct.pack(
|
|
967
|
+
">BBBBBBBB",
|
|
968
|
+
0x00, # Reserved
|
|
969
|
+
0x01, # Parameter count
|
|
970
|
+
0x12, # Type/length header
|
|
971
|
+
0x04, # Length of following data
|
|
972
|
+
0x11, # Method (0x11 = request)
|
|
973
|
+
0x44, # Type (4=request) | Group (4=grSZL)
|
|
974
|
+
S7UserDataSubfunction.READ_SZL, # Subfunction (0x01)
|
|
975
|
+
0x00, # DataRef (0x00 for initial request)
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
# Data section: SZL ID and Index
|
|
979
|
+
data_section = struct.pack(
|
|
980
|
+
">BBHHH",
|
|
981
|
+
0x0A, # Return value (request)
|
|
982
|
+
0x00, # Transport size
|
|
983
|
+
0x0004, # Length (4 bytes for ID + Index)
|
|
984
|
+
szl_id, # SZL ID
|
|
985
|
+
szl_index, # SZL Index
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
# S7 header for USER_DATA
|
|
989
|
+
header = struct.pack(
|
|
990
|
+
">BBHHHH",
|
|
991
|
+
0x32, # Protocol ID
|
|
992
|
+
S7PDUType.USERDATA, # PDU type (0x07)
|
|
993
|
+
0x0000, # Reserved
|
|
994
|
+
self._next_sequence(), # Sequence
|
|
995
|
+
len(param_data), # Parameter length
|
|
996
|
+
len(data_section), # Data length
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
return header + param_data + data_section
|
|
1000
|
+
|
|
1001
|
+
def build_userdata_followup_request(self, group: int, subfunction: int, sequence_number: int) -> bytes:
|
|
1002
|
+
"""
|
|
1003
|
+
Build USERDATA follow-up request for multi-packet responses.
|
|
1004
|
+
|
|
1005
|
+
Args:
|
|
1006
|
+
group: USERDATA group (e.g., 4 for SZL, 3 for block info)
|
|
1007
|
+
subfunction: Subfunction code
|
|
1008
|
+
sequence_number: Sequence number from the previous response
|
|
1009
|
+
|
|
1010
|
+
Returns:
|
|
1011
|
+
Complete S7 PDU for follow-up request
|
|
1012
|
+
"""
|
|
1013
|
+
# Parameter section: same as initial but with DataRef = sequence_number
|
|
1014
|
+
type_group = 0x40 | (group & 0x0F) # Type 4 (request) | group
|
|
1015
|
+
param_data = struct.pack(
|
|
1016
|
+
">BBBBBBBB",
|
|
1017
|
+
0x00, # Reserved
|
|
1018
|
+
0x01, # Parameter count
|
|
1019
|
+
0x12, # Type/length header
|
|
1020
|
+
0x04, # Length of following data
|
|
1021
|
+
0x11, # Method (0x11 = request)
|
|
1022
|
+
type_group, # Type | Group
|
|
1023
|
+
subfunction, # Subfunction
|
|
1024
|
+
sequence_number, # DataRef from previous response
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
# Minimal data section for follow-up
|
|
1028
|
+
data_section = struct.pack(
|
|
1029
|
+
">BBH",
|
|
1030
|
+
0x0A, # Return value (request)
|
|
1031
|
+
0x00, # Transport size
|
|
1032
|
+
0x0000, # Length (0 bytes)
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
header = struct.pack(
|
|
1036
|
+
">BBHHHH",
|
|
1037
|
+
0x32, # Protocol ID
|
|
1038
|
+
S7PDUType.USERDATA, # PDU type (0x07)
|
|
1039
|
+
0x0000, # Reserved
|
|
1040
|
+
self._next_sequence(), # Sequence
|
|
1041
|
+
len(param_data), # Parameter length
|
|
1042
|
+
len(data_section), # Data length
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
return header + param_data + data_section
|
|
1046
|
+
|
|
1047
|
+
def parse_read_szl_response(self, response: Dict[str, Any], first_fragment: bool = True) -> Dict[str, Any]:
|
|
1048
|
+
"""
|
|
1049
|
+
Parse read SZL response.
|
|
1050
|
+
|
|
1051
|
+
Args:
|
|
1052
|
+
response: Parsed S7 response
|
|
1053
|
+
first_fragment: If True (default), parse SZL header (ID+Index).
|
|
1054
|
+
If False, treat all data as raw payload (follow-up fragments).
|
|
1055
|
+
|
|
1056
|
+
Returns:
|
|
1057
|
+
Dictionary with SZL ID, Index, and data
|
|
1058
|
+
"""
|
|
1059
|
+
result: Dict[str, Any] = {
|
|
1060
|
+
"szl_id": 0,
|
|
1061
|
+
"szl_index": 0,
|
|
1062
|
+
"data": b"",
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
data_info = response.get("data", {})
|
|
1066
|
+
raw_data = data_info.get("data", b"")
|
|
1067
|
+
|
|
1068
|
+
if first_fragment:
|
|
1069
|
+
if len(raw_data) < 4:
|
|
1070
|
+
return result
|
|
1071
|
+
|
|
1072
|
+
# Parse SZL header: ID (2) + Index (2)
|
|
1073
|
+
result["szl_id"] = struct.unpack(">H", raw_data[0:2])[0]
|
|
1074
|
+
result["szl_index"] = struct.unpack(">H", raw_data[2:4])[0]
|
|
1075
|
+
result["data"] = raw_data[4:]
|
|
1076
|
+
else:
|
|
1077
|
+
# Follow-up fragments don't include SZL header
|
|
1078
|
+
result["data"] = raw_data
|
|
1079
|
+
|
|
1080
|
+
return result
|
|
1081
|
+
|
|
1082
|
+
def build_get_clock_request(self) -> bytes:
|
|
1083
|
+
"""
|
|
1084
|
+
Build USER_DATA request for reading PLC clock.
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
Complete S7 PDU for get clock request
|
|
1088
|
+
"""
|
|
1089
|
+
# Parameter section for USER_DATA clock request
|
|
1090
|
+
param_data = struct.pack(
|
|
1091
|
+
">BBBBBBBB",
|
|
1092
|
+
0x00, # Reserved
|
|
1093
|
+
0x01, # Parameter count
|
|
1094
|
+
0x12, # Type/length header
|
|
1095
|
+
0x04, # Length of following data
|
|
1096
|
+
0x11, # Method (0x11 = request)
|
|
1097
|
+
0x47, # Type (4=request) | Group (7=grClock)
|
|
1098
|
+
S7UserDataSubfunction.GET_CLOCK, # Subfunction (0x01)
|
|
1099
|
+
0x00, # DataRef (0x00 for initial request)
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
# Data section: empty for get clock
|
|
1103
|
+
data_section = struct.pack(
|
|
1104
|
+
">BBH",
|
|
1105
|
+
0x0A, # Return value (request)
|
|
1106
|
+
0x00, # Transport size
|
|
1107
|
+
0x0000, # Length (0 bytes)
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
# S7 header for USER_DATA
|
|
1111
|
+
header = struct.pack(
|
|
1112
|
+
">BBHHHH",
|
|
1113
|
+
0x32, # Protocol ID
|
|
1114
|
+
S7PDUType.USERDATA, # PDU type (0x07)
|
|
1115
|
+
0x0000, # Reserved
|
|
1116
|
+
self._next_sequence(), # Sequence
|
|
1117
|
+
len(param_data), # Parameter length
|
|
1118
|
+
len(data_section), # Data length
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
return header + param_data + data_section
|
|
1122
|
+
|
|
1123
|
+
def build_set_clock_request(self, dt: "datetime") -> bytes:
|
|
1124
|
+
"""
|
|
1125
|
+
Build USER_DATA request for setting PLC clock.
|
|
1126
|
+
|
|
1127
|
+
Args:
|
|
1128
|
+
dt: Datetime to set
|
|
1129
|
+
|
|
1130
|
+
Returns:
|
|
1131
|
+
Complete S7 PDU for set clock request
|
|
1132
|
+
"""
|
|
1133
|
+
|
|
1134
|
+
# Convert datetime to BCD format
|
|
1135
|
+
# BCD encoding: each decimal digit is stored in a nibble
|
|
1136
|
+
def to_bcd(value: int) -> int:
|
|
1137
|
+
return ((value // 10) << 4) | (value % 10)
|
|
1138
|
+
|
|
1139
|
+
year = dt.year % 100 # Only last 2 digits
|
|
1140
|
+
bcd_time = struct.pack(
|
|
1141
|
+
">BBBBBBBB",
|
|
1142
|
+
0x00, # Reserved
|
|
1143
|
+
to_bcd(year), # Year (BCD)
|
|
1144
|
+
to_bcd(dt.month), # Month (BCD)
|
|
1145
|
+
to_bcd(dt.day), # Day (BCD)
|
|
1146
|
+
to_bcd(dt.hour), # Hour (BCD)
|
|
1147
|
+
to_bcd(dt.minute), # Minute (BCD)
|
|
1148
|
+
to_bcd(dt.second), # Second (BCD)
|
|
1149
|
+
(dt.weekday() + 1) & 0x0F, # Day of week (1=Monday)
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
# Parameter section for USER_DATA clock request
|
|
1153
|
+
param_data = struct.pack(
|
|
1154
|
+
">BBBBBBBB",
|
|
1155
|
+
0x00, # Reserved
|
|
1156
|
+
0x01, # Parameter count
|
|
1157
|
+
0x12, # Type/length header
|
|
1158
|
+
0x04, # Length of following data
|
|
1159
|
+
0x11, # Method (0x11 = request)
|
|
1160
|
+
0x47, # Type (4=request) | Group (7=grClock)
|
|
1161
|
+
S7UserDataSubfunction.SET_CLOCK, # Subfunction (0x02)
|
|
1162
|
+
0x00, # DataRef (0x00 for initial request)
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
# Data section with BCD time
|
|
1166
|
+
data_section = (
|
|
1167
|
+
struct.pack(
|
|
1168
|
+
">BBH",
|
|
1169
|
+
0x0A, # Return value (request)
|
|
1170
|
+
0x00, # Transport size
|
|
1171
|
+
len(bcd_time), # Length
|
|
1172
|
+
)
|
|
1173
|
+
+ bcd_time
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
# S7 header for USER_DATA
|
|
1177
|
+
header = struct.pack(
|
|
1178
|
+
">BBHHHH",
|
|
1179
|
+
0x32, # Protocol ID
|
|
1180
|
+
S7PDUType.USERDATA, # PDU type (0x07)
|
|
1181
|
+
0x0000, # Reserved
|
|
1182
|
+
self._next_sequence(), # Sequence
|
|
1183
|
+
len(param_data), # Parameter length
|
|
1184
|
+
len(data_section), # Data length
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
return header + param_data + data_section
|
|
1188
|
+
|
|
1189
|
+
def parse_get_clock_response(self, response: Dict[str, Any]) -> "datetime":
|
|
1190
|
+
"""
|
|
1191
|
+
Parse get clock response.
|
|
1192
|
+
|
|
1193
|
+
Args:
|
|
1194
|
+
response: Parsed S7 response
|
|
1195
|
+
|
|
1196
|
+
Returns:
|
|
1197
|
+
Datetime from PLC
|
|
1198
|
+
"""
|
|
1199
|
+
from datetime import datetime as dt_class
|
|
1200
|
+
|
|
1201
|
+
data_info = response.get("data", {})
|
|
1202
|
+
raw_data = data_info.get("data", b"")
|
|
1203
|
+
|
|
1204
|
+
if len(raw_data) < 8:
|
|
1205
|
+
# Return current time if no valid data
|
|
1206
|
+
return dt_class.now().replace(microsecond=0)
|
|
1207
|
+
|
|
1208
|
+
# Parse BCD time
|
|
1209
|
+
def from_bcd(value: int) -> int:
|
|
1210
|
+
return ((value >> 4) * 10) + (value & 0x0F)
|
|
1211
|
+
|
|
1212
|
+
# Skip first byte (reserved)
|
|
1213
|
+
year = from_bcd(raw_data[1])
|
|
1214
|
+
month = from_bcd(raw_data[2])
|
|
1215
|
+
day = from_bcd(raw_data[3])
|
|
1216
|
+
hour = from_bcd(raw_data[4])
|
|
1217
|
+
minute = from_bcd(raw_data[5])
|
|
1218
|
+
second = from_bcd(raw_data[6])
|
|
1219
|
+
|
|
1220
|
+
# Determine century (assume 2000s for years 0-99)
|
|
1221
|
+
full_year = 2000 + year if year < 90 else 1900 + year
|
|
1222
|
+
|
|
1223
|
+
try:
|
|
1224
|
+
return dt_class(full_year, month, day, hour, minute, second)
|
|
1225
|
+
except ValueError:
|
|
1226
|
+
return dt_class.now().replace(microsecond=0)
|
|
1227
|
+
|
|
1228
|
+
def build_cpu_state_request(self) -> bytes:
|
|
1229
|
+
"""
|
|
1230
|
+
Build CPU state request.
|
|
1231
|
+
|
|
1232
|
+
Returns:
|
|
1233
|
+
Complete S7 PDU for CPU state query
|
|
1234
|
+
"""
|
|
1235
|
+
# Simple CPU state request - in real S7 this would be a userdata function
|
|
1236
|
+
header = struct.pack(
|
|
1237
|
+
">BBHHHH",
|
|
1238
|
+
0x32, # Protocol ID
|
|
1239
|
+
S7PDUType.REQUEST, # PDU type
|
|
1240
|
+
0x0000, # Reserved
|
|
1241
|
+
self._next_sequence(), # Sequence
|
|
1242
|
+
0x0001, # Parameter length
|
|
1243
|
+
0x0000, # Data length
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
# Use a custom function code for CPU state
|
|
1247
|
+
parameters = struct.pack(">B", 0x04) # Use READ_AREA function for simplicity
|
|
1248
|
+
|
|
1249
|
+
return header + parameters
|
|
1250
|
+
|
|
1251
|
+
def extract_cpu_state(self, response: Dict[str, Any]) -> str:
|
|
1252
|
+
"""
|
|
1253
|
+
Extract CPU state from response.
|
|
1254
|
+
|
|
1255
|
+
Args:
|
|
1256
|
+
response: Parsed S7 response
|
|
1257
|
+
|
|
1258
|
+
Returns:
|
|
1259
|
+
CPU state string in S7CpuStatus format (e.g., 'S7CpuStatusRun')
|
|
1260
|
+
"""
|
|
1261
|
+
# Map internal states to S7 status format for API compatibility with master branch
|
|
1262
|
+
# The cpu_statuses dict in type.py uses: {0: "S7CpuStatusUnknown", 4: "S7CpuStatusStop", 8: "S7CpuStatusRun"}
|
|
1263
|
+
return "S7CpuStatusRun" # Default state for pure Python server
|
|
1264
|
+
|
|
1265
|
+
def parse_response(self, pdu: bytes) -> Dict[str, Any]:
|
|
1266
|
+
"""
|
|
1267
|
+
Parse S7 response PDU.
|
|
1268
|
+
|
|
1269
|
+
Args:
|
|
1270
|
+
pdu: Complete S7 PDU
|
|
1271
|
+
|
|
1272
|
+
Returns:
|
|
1273
|
+
Parsed response data
|
|
1274
|
+
"""
|
|
1275
|
+
if len(pdu) < 10:
|
|
1276
|
+
raise S7ProtocolError("PDU too short for S7 response header")
|
|
1277
|
+
|
|
1278
|
+
# First peek at PDU type to determine header size
|
|
1279
|
+
pdu_type = pdu[1]
|
|
1280
|
+
|
|
1281
|
+
if pdu_type == S7PDUType.USERDATA:
|
|
1282
|
+
# USERDATA PDUs have a 10-byte header (no error_class/error_code in header)
|
|
1283
|
+
if len(pdu) < 10:
|
|
1284
|
+
raise S7ProtocolError("PDU too short for USERDATA header")
|
|
1285
|
+
header = struct.unpack(">BBHHHH", pdu[:10])
|
|
1286
|
+
protocol_id, pdu_type, reserved, sequence, param_len, data_len = header
|
|
1287
|
+
error_class = 0
|
|
1288
|
+
error_code = 0
|
|
1289
|
+
offset = 10
|
|
1290
|
+
else:
|
|
1291
|
+
# ACK/ACK_DATA PDUs have a 12-byte header (with error_class/error_code)
|
|
1292
|
+
if len(pdu) < 12:
|
|
1293
|
+
raise S7ProtocolError("PDU too short for ACK/ACK_DATA header")
|
|
1294
|
+
header = struct.unpack(">BBHHHHBB", pdu[:12])
|
|
1295
|
+
protocol_id, pdu_type, reserved, sequence, param_len, data_len, error_class, error_code = header
|
|
1296
|
+
offset = 12
|
|
1297
|
+
|
|
1298
|
+
if protocol_id != 0x32:
|
|
1299
|
+
raise S7ProtocolError(f"Invalid protocol ID: {protocol_id:#02x}")
|
|
1300
|
+
|
|
1301
|
+
# Accept ACK (write responses), ACK_DATA (read responses), and USERDATA response types
|
|
1302
|
+
if pdu_type not in (S7PDUType.ACK, S7PDUType.ACK_DATA, S7PDUType.USERDATA):
|
|
1303
|
+
raise S7ProtocolError(f"Expected response PDU, got {pdu_type}")
|
|
1304
|
+
|
|
1305
|
+
combined_error = (error_class << 8) | error_code
|
|
1306
|
+
if error_class != 0:
|
|
1307
|
+
error_msg = get_protocol_error_message(combined_error)
|
|
1308
|
+
raise S7ProtocolError(
|
|
1309
|
+
f"S7 protocol error (class={error_class:#04x}, code={error_code:#04x}): {error_msg}",
|
|
1310
|
+
error_code=combined_error,
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
response = {
|
|
1314
|
+
"sequence": sequence,
|
|
1315
|
+
"param_length": param_len,
|
|
1316
|
+
"data_length": data_len,
|
|
1317
|
+
"parameters": None,
|
|
1318
|
+
"data": None,
|
|
1319
|
+
"error_code": combined_error,
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
# Parse parameters if present
|
|
1323
|
+
if param_len > 0:
|
|
1324
|
+
if offset + param_len > len(pdu):
|
|
1325
|
+
raise S7ProtocolError("Parameter section extends beyond PDU")
|
|
1326
|
+
|
|
1327
|
+
param_data = pdu[offset : offset + param_len]
|
|
1328
|
+
response["parameters"] = self._parse_parameters(param_data)
|
|
1329
|
+
offset += param_len
|
|
1330
|
+
|
|
1331
|
+
# Parse data if present
|
|
1332
|
+
if data_len > 0:
|
|
1333
|
+
if offset + data_len > len(pdu):
|
|
1334
|
+
raise S7ProtocolError("Data section extends beyond PDU")
|
|
1335
|
+
|
|
1336
|
+
data_section = pdu[offset : offset + data_len]
|
|
1337
|
+
response["data"] = self._parse_data_section(data_section)
|
|
1338
|
+
|
|
1339
|
+
return response
|
|
1340
|
+
|
|
1341
|
+
def _parse_parameters(self, param_data: bytes) -> Dict[str, Any]:
|
|
1342
|
+
"""Parse S7 parameter section."""
|
|
1343
|
+
if len(param_data) < 1:
|
|
1344
|
+
return {}
|
|
1345
|
+
|
|
1346
|
+
# Detect USERDATA response parameters:
|
|
1347
|
+
# byte 0 = 0x00 (reserved), len >= 12, byte 2 = 0x12, byte 4 = 0x12 (method=response)
|
|
1348
|
+
if param_data[0] == 0x00 and len(param_data) >= 12 and param_data[2] == 0x12 and param_data[4] == 0x12:
|
|
1349
|
+
return self._parse_userdata_response_params(param_data)
|
|
1350
|
+
|
|
1351
|
+
function_code = param_data[0]
|
|
1352
|
+
|
|
1353
|
+
if function_code == S7Function.READ_AREA:
|
|
1354
|
+
return self._parse_read_response_params(param_data)
|
|
1355
|
+
elif function_code == S7Function.WRITE_AREA:
|
|
1356
|
+
return self._parse_write_response_params(param_data)
|
|
1357
|
+
elif function_code == S7Function.SETUP_COMMUNICATION:
|
|
1358
|
+
return self._parse_setup_comm_response_params(param_data)
|
|
1359
|
+
else:
|
|
1360
|
+
return {"function_code": function_code}
|
|
1361
|
+
|
|
1362
|
+
def _parse_read_response_params(self, param_data: bytes) -> Dict[str, Any]:
|
|
1363
|
+
"""Parse read area response parameters."""
|
|
1364
|
+
if len(param_data) < 2:
|
|
1365
|
+
raise S7ProtocolError("Read response parameters too short")
|
|
1366
|
+
|
|
1367
|
+
function_code = param_data[0]
|
|
1368
|
+
item_count = param_data[1]
|
|
1369
|
+
|
|
1370
|
+
return {"function_code": function_code, "item_count": item_count}
|
|
1371
|
+
|
|
1372
|
+
def _parse_write_response_params(self, param_data: bytes) -> Dict[str, Any]:
|
|
1373
|
+
"""Parse write area response parameters."""
|
|
1374
|
+
if len(param_data) < 2:
|
|
1375
|
+
raise S7ProtocolError("Write response parameters too short")
|
|
1376
|
+
|
|
1377
|
+
function_code = param_data[0]
|
|
1378
|
+
item_count = param_data[1]
|
|
1379
|
+
|
|
1380
|
+
return {"function_code": function_code, "item_count": item_count}
|
|
1381
|
+
|
|
1382
|
+
def _parse_setup_comm_response_params(self, param_data: bytes) -> Dict[str, Any]:
|
|
1383
|
+
"""Parse setup communication response parameters."""
|
|
1384
|
+
if len(param_data) < 8:
|
|
1385
|
+
raise S7ProtocolError("Setup communication response parameters too short")
|
|
1386
|
+
|
|
1387
|
+
function_code, reserved, max_amq_caller, max_amq_callee, pdu_length = struct.unpack(">BBHHH", param_data[:8])
|
|
1388
|
+
|
|
1389
|
+
return {
|
|
1390
|
+
"function_code": function_code,
|
|
1391
|
+
"max_amq_caller": max_amq_caller,
|
|
1392
|
+
"max_amq_callee": max_amq_callee,
|
|
1393
|
+
"pdu_length": pdu_length,
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
def _parse_userdata_response_params(self, param_data: bytes) -> Dict[str, Any]:
|
|
1397
|
+
"""Parse USERDATA response parameter section (12 bytes).
|
|
1398
|
+
|
|
1399
|
+
Layout:
|
|
1400
|
+
[0] Reserved (0x00)
|
|
1401
|
+
[1] Parameter count (0x01)
|
|
1402
|
+
[2] Type header (0x12)
|
|
1403
|
+
[3] Length of following data (0x08 for response)
|
|
1404
|
+
[4] Method (0x12 = response)
|
|
1405
|
+
[5] Type (high nibble) | Group (low nibble)
|
|
1406
|
+
[6] Subfunction
|
|
1407
|
+
[7] Sequence number (used as DataRef in follow-up)
|
|
1408
|
+
[8] Data unit reference
|
|
1409
|
+
[9] Last data unit (0x00 = last, non-zero = more)
|
|
1410
|
+
[10-11] Error code
|
|
1411
|
+
"""
|
|
1412
|
+
type_group = param_data[5]
|
|
1413
|
+
group = type_group & 0x0F
|
|
1414
|
+
subfunction = param_data[6]
|
|
1415
|
+
sequence_number = param_data[7]
|
|
1416
|
+
last_data_unit = param_data[9]
|
|
1417
|
+
error_code = struct.unpack(">H", param_data[10:12])[0]
|
|
1418
|
+
|
|
1419
|
+
if error_code != 0:
|
|
1420
|
+
error_msg = get_protocol_error_message(error_code)
|
|
1421
|
+
logger.warning(f"USERDATA response error {error_code:#06x}: {error_msg}")
|
|
1422
|
+
|
|
1423
|
+
return {
|
|
1424
|
+
"group": group,
|
|
1425
|
+
"subfunction": subfunction,
|
|
1426
|
+
"sequence_number": sequence_number,
|
|
1427
|
+
"last_data_unit": last_data_unit,
|
|
1428
|
+
"error_code": error_code,
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
def _parse_data_section(self, data_section: bytes) -> Dict[str, Any]:
|
|
1432
|
+
"""Parse S7 data section."""
|
|
1433
|
+
if len(data_section) == 1:
|
|
1434
|
+
# Simple return code (for write responses)
|
|
1435
|
+
return {"return_code": data_section[0], "transport_size": 0, "data_length": 0, "data": b""}
|
|
1436
|
+
elif len(data_section) >= 4:
|
|
1437
|
+
# Full data header
|
|
1438
|
+
return_code = data_section[0]
|
|
1439
|
+
transport_size = data_section[1]
|
|
1440
|
+
data_length = struct.unpack(">H", data_section[2:4])[0]
|
|
1441
|
+
|
|
1442
|
+
# Extract actual data - length interpretation depends on transport_size
|
|
1443
|
+
# Transport size 0x09 (octet string): byte length (USERDATA responses)
|
|
1444
|
+
# Transport size 0x00: byte length (USERDATA requests)
|
|
1445
|
+
# Transport size 0x04 (byte): bit length (READ_AREA responses)
|
|
1446
|
+
if transport_size in (0x00, 0x09):
|
|
1447
|
+
# USERDATA uses byte length directly
|
|
1448
|
+
actual_data = data_section[4 : 4 + data_length]
|
|
1449
|
+
else:
|
|
1450
|
+
# READ_AREA responses use bit length
|
|
1451
|
+
actual_data = data_section[4 : 4 + (data_length // 8)]
|
|
1452
|
+
|
|
1453
|
+
return {"return_code": return_code, "transport_size": transport_size, "data_length": data_length, "data": actual_data}
|
|
1454
|
+
else:
|
|
1455
|
+
return {"raw_data": data_section}
|
|
1456
|
+
|
|
1457
|
+
def extract_read_data(self, response: Dict[str, Any], word_len: S7WordLen, count: int) -> List[Any]:
|
|
1458
|
+
"""
|
|
1459
|
+
Extract and decode data from read response.
|
|
1460
|
+
|
|
1461
|
+
Args:
|
|
1462
|
+
response: Parsed S7 response
|
|
1463
|
+
word_len: Expected data word length
|
|
1464
|
+
count: Expected number of items
|
|
1465
|
+
|
|
1466
|
+
Returns:
|
|
1467
|
+
List of decoded values
|
|
1468
|
+
"""
|
|
1469
|
+
if not response.get("data"):
|
|
1470
|
+
raise S7ProtocolError("No data in response")
|
|
1471
|
+
|
|
1472
|
+
data_info = response["data"]
|
|
1473
|
+
return_code = data_info.get("return_code", 0)
|
|
1474
|
+
|
|
1475
|
+
if return_code != 0xFF: # 0xFF = Success
|
|
1476
|
+
desc = get_return_code_description(return_code)
|
|
1477
|
+
raise S7ProtocolError(f"Read operation failed: {desc} (0x{return_code:02x})")
|
|
1478
|
+
|
|
1479
|
+
raw_data = data_info.get("data", b"")
|
|
1480
|
+
|
|
1481
|
+
# Return raw bytes directly - caller handles type conversion
|
|
1482
|
+
return list(raw_data)
|
|
1483
|
+
|
|
1484
|
+
def check_write_response(self, response: Dict[str, Any]) -> None:
|
|
1485
|
+
"""
|
|
1486
|
+
Check write operation response for errors.
|
|
1487
|
+
|
|
1488
|
+
Args:
|
|
1489
|
+
response: Parsed S7 response
|
|
1490
|
+
|
|
1491
|
+
Raises:
|
|
1492
|
+
S7ProtocolError: If write operation failed
|
|
1493
|
+
"""
|
|
1494
|
+
# First check for errors in the response header
|
|
1495
|
+
# S7-1200/1500 returns error codes in the header for write failures
|
|
1496
|
+
header_error = response.get("error_code", 0)
|
|
1497
|
+
if header_error != 0:
|
|
1498
|
+
error_msg = f"Write operation failed with S7 error code: {header_error:#06x}"
|
|
1499
|
+
raise S7ProtocolError(error_msg)
|
|
1500
|
+
|
|
1501
|
+
# For successful writes, check the data section return code if present
|
|
1502
|
+
if response.get("data"):
|
|
1503
|
+
data_info = response["data"]
|
|
1504
|
+
return_code = data_info.get("return_code", 0xFF) # Default to success
|
|
1505
|
+
|
|
1506
|
+
if return_code != 0xFF: # 0xFF = Success
|
|
1507
|
+
desc = get_return_code_description(return_code)
|
|
1508
|
+
raise S7ProtocolError(f"Write operation failed: {desc} (0x{return_code:02x})")
|
|
1509
|
+
# If no data and no header error, the write was successful (ACK without data)
|