python-snap7 3.2.0__py3-none-any.whl → 3.2.1__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.2.0.dist-info → python_snap7-3.2.1.dist-info}/METADATA +5 -1
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/RECORD +12 -12
- snap7/async_client.py +76 -66
- snap7/client.py +82 -89
- snap7/client_base.py +75 -2
- snap7/s7protocol.py +170 -134
- snap7/server/__init__.py +178 -220
- snap7/util/getters.py +5 -5
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/WHEEL +0 -0
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/entry_points.txt +0 -0
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/licenses/LICENSE +0 -0
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/top_level.txt +0 -0
snap7/s7protocol.py
CHANGED
|
@@ -546,22 +546,15 @@ class S7Protocol:
|
|
|
546
546
|
Returns:
|
|
547
547
|
Complete S7 PDU for start upload request
|
|
548
548
|
"""
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
if not 0 <= block_type <= 0xFF:
|
|
550
|
+
raise ValueError("block_type must fit in one byte")
|
|
551
|
+
if not 0 <= block_num <= 99999:
|
|
552
|
+
raise ValueError("block_num must be between 0 and 99999")
|
|
552
553
|
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
S7Function.START_UPLOAD, # Function code
|
|
558
|
-
0x00, # Status
|
|
559
|
-
0x00, # Reserved (error code)
|
|
560
|
-
0x00000000, # Upload ID (0 for start)
|
|
561
|
-
len(block_addr), # Block address length
|
|
562
|
-
)
|
|
563
|
-
+ block_addr
|
|
564
|
-
)
|
|
554
|
+
# TReqFunStartUploadParams: function, six reserved bytes, one-byte
|
|
555
|
+
# upload ID, then the nine-byte block address `_0TNNNNNA`.
|
|
556
|
+
block_addr = b"_0" + bytes((block_type,)) + f"{block_num:05d}".encode("ascii") + b"A"
|
|
557
|
+
param_data = bytes((S7Function.START_UPLOAD,)) + bytes(7) + bytes((len(block_addr),)) + block_addr
|
|
565
558
|
|
|
566
559
|
header = struct.pack(
|
|
567
560
|
">BBHHHH",
|
|
@@ -585,13 +578,9 @@ class S7Protocol:
|
|
|
585
578
|
Returns:
|
|
586
579
|
Complete S7 PDU for upload request
|
|
587
580
|
"""
|
|
588
|
-
|
|
589
|
-
"
|
|
590
|
-
|
|
591
|
-
0x00, # Status
|
|
592
|
-
0x00, # Reserved
|
|
593
|
-
upload_id, # Upload ID
|
|
594
|
-
)
|
|
581
|
+
if not 0 <= upload_id <= 0xFF:
|
|
582
|
+
raise ValueError("upload_id must fit in one byte")
|
|
583
|
+
param_data = bytes((S7Function.UPLOAD,)) + bytes(6) + bytes((upload_id,))
|
|
595
584
|
|
|
596
585
|
header = struct.pack(
|
|
597
586
|
">BBHHHH",
|
|
@@ -615,13 +604,9 @@ class S7Protocol:
|
|
|
615
604
|
Returns:
|
|
616
605
|
Complete S7 PDU for end upload request
|
|
617
606
|
"""
|
|
618
|
-
|
|
619
|
-
"
|
|
620
|
-
|
|
621
|
-
0x00, # Status
|
|
622
|
-
0x00, # Reserved
|
|
623
|
-
upload_id, # Upload ID
|
|
624
|
-
)
|
|
607
|
+
if not 0 <= upload_id <= 0xFF:
|
|
608
|
+
raise ValueError("upload_id must fit in one byte")
|
|
609
|
+
param_data = bytes((S7Function.END_UPLOAD,)) + bytes(6) + bytes((upload_id,))
|
|
625
610
|
|
|
626
611
|
header = struct.pack(
|
|
627
612
|
">BBHHHH",
|
|
@@ -642,24 +627,42 @@ class S7Protocol:
|
|
|
642
627
|
Returns:
|
|
643
628
|
Dictionary with upload_id and block_length
|
|
644
629
|
"""
|
|
645
|
-
result = {"upload_id": 0, "block_length": 0}
|
|
646
|
-
|
|
647
630
|
raw_params = response.get("raw_parameters", b"")
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
631
|
+
if len(raw_params) != 16 or raw_params[0] != S7Function.START_UPLOAD:
|
|
632
|
+
raise S7ProtocolError("Invalid start-upload response parameters")
|
|
633
|
+
try:
|
|
634
|
+
block_length = int(raw_params[11:16].decode("ascii"))
|
|
635
|
+
except (UnicodeDecodeError, ValueError) as exc:
|
|
636
|
+
raise S7ProtocolError("Invalid start-upload block length") from exc
|
|
637
|
+
return {"upload_id": raw_params[7], "block_length": block_length}
|
|
638
|
+
|
|
639
|
+
def parse_upload_fragment(
|
|
640
|
+
self,
|
|
641
|
+
response: Dict[str, Any],
|
|
642
|
+
*,
|
|
643
|
+
first_fragment: bool = False,
|
|
644
|
+
include_block_header: bool = False,
|
|
645
|
+
) -> Tuple[bytes, bool]:
|
|
646
|
+
"""Validate and decode one upload response fragment."""
|
|
647
|
+
raw_params = response.get("raw_parameters", b"")
|
|
648
|
+
if len(raw_params) != 2 or raw_params[0] != S7Function.UPLOAD or raw_params[1] not in (0, 1):
|
|
649
|
+
raise S7ProtocolError("Invalid upload response parameters")
|
|
650
|
+
|
|
651
|
+
raw_data = response.get("raw_data", b"")
|
|
652
|
+
if len(raw_data) < 4:
|
|
653
|
+
raise S7ProtocolError("Upload response data is truncated")
|
|
654
|
+
declared_length, marker = struct.unpack(">HH", raw_data[:4])
|
|
655
|
+
if marker != 0x00FB:
|
|
656
|
+
raise S7ProtocolError("Invalid upload response data marker")
|
|
657
|
+
if declared_length != len(raw_data) - 4:
|
|
658
|
+
raise S7ProtocolError("Upload response data length mismatch")
|
|
659
|
+
payload = raw_data[4:]
|
|
660
|
+
if first_fragment and not include_block_header:
|
|
661
|
+
compact_header_size = 36
|
|
662
|
+
if len(payload) < compact_header_size:
|
|
663
|
+
raise S7ProtocolError("First upload response is missing its compact block header")
|
|
664
|
+
payload = payload[compact_header_size:]
|
|
665
|
+
return payload, raw_params[1] == 0
|
|
663
666
|
|
|
664
667
|
def parse_upload_response(self, response: Dict[str, Any]) -> bytes:
|
|
665
668
|
"""
|
|
@@ -668,13 +671,15 @@ class S7Protocol:
|
|
|
668
671
|
Returns:
|
|
669
672
|
Block data bytes
|
|
670
673
|
"""
|
|
671
|
-
|
|
672
|
-
|
|
674
|
+
payload, _ = self.parse_upload_fragment(response)
|
|
675
|
+
return payload
|
|
673
676
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
677
|
+
def check_end_upload_response(self, response: Dict[str, Any]) -> None:
|
|
678
|
+
"""Require a canonical END_UPLOAD acknowledgement."""
|
|
679
|
+
if response.get("raw_parameters", b"") != bytes((S7Function.END_UPLOAD,)):
|
|
680
|
+
raise S7ProtocolError("Invalid end-upload response")
|
|
681
|
+
if response.get("raw_data", b""):
|
|
682
|
+
raise S7ProtocolError("End-upload response must not contain data")
|
|
678
683
|
|
|
679
684
|
def build_download_request(self, block_type: int, block_num: int, block_data: bytes) -> bytes:
|
|
680
685
|
"""
|
|
@@ -688,26 +693,20 @@ class S7Protocol:
|
|
|
688
693
|
Returns:
|
|
689
694
|
Complete S7 PDU for request download
|
|
690
695
|
"""
|
|
691
|
-
|
|
692
|
-
|
|
696
|
+
if not 0 <= block_type <= 0xFF or not 0 <= block_num <= 99999:
|
|
697
|
+
raise ValueError("Invalid block type or number")
|
|
698
|
+
if len(block_data) > 999999:
|
|
699
|
+
raise ValueError("Block is too large for the download request")
|
|
700
|
+
if len(block_data) >= 36 and block_data[2] == 1 and block_data[4] == 5:
|
|
701
|
+
mc7_size = struct.unpack(">H", block_data[34:36])[0]
|
|
702
|
+
else:
|
|
703
|
+
mc7_size = len(block_data) - 36 if len(block_data) >= 36 else len(block_data)
|
|
704
|
+
if mc7_size > 999999:
|
|
705
|
+
raise ValueError("MC7 size is too large for the download request")
|
|
693
706
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
# Parameters
|
|
698
|
-
param_data = (
|
|
699
|
-
struct.pack(
|
|
700
|
-
">BBBBB",
|
|
701
|
-
S7Function.REQUEST_DOWNLOAD, # Function code
|
|
702
|
-
0x00, # Status
|
|
703
|
-
0x00, # Reserved
|
|
704
|
-
0x00, # Reserved
|
|
705
|
-
len(block_addr), # Block address length
|
|
706
|
-
)
|
|
707
|
-
+ block_addr
|
|
708
|
-
+ struct.pack(">B", len(length_str))
|
|
709
|
-
+ length_str
|
|
710
|
-
)
|
|
707
|
+
block_addr = b"_0" + bytes((block_type,)) + f"{block_num:05d}".encode("ascii") + b"P"
|
|
708
|
+
sizes = b"1" + f"{len(block_data):06d}{mc7_size:06d}".encode("ascii")
|
|
709
|
+
param_data = bytes((S7Function.REQUEST_DOWNLOAD, 0, 1)) + bytes(5) + b"\x09" + block_addr + b"\x0d" + sizes
|
|
711
710
|
|
|
712
711
|
header = struct.pack(
|
|
713
712
|
">BBHHHH",
|
|
@@ -721,6 +720,37 @@ class S7Protocol:
|
|
|
721
720
|
|
|
722
721
|
return header + param_data
|
|
723
722
|
|
|
723
|
+
@staticmethod
|
|
724
|
+
def parse_download_service_request(pdu: bytes, function: int, block_num: int) -> int:
|
|
725
|
+
"""Validate a PLC request for a download slice or completion acknowledgement."""
|
|
726
|
+
if len(pdu) != 28 or pdu[0:2] != bytes((0x32, S7PDUType.REQUEST)):
|
|
727
|
+
raise S7ProtocolError("Invalid PLC download request header")
|
|
728
|
+
sequence, param_len, data_len = struct.unpack(">HHH", pdu[4:10])
|
|
729
|
+
if param_len != 18 or data_len != 0:
|
|
730
|
+
raise S7ProtocolError("Invalid PLC download request lengths")
|
|
731
|
+
params = pdu[10:]
|
|
732
|
+
expected_address = b"_0" + bytes((0x41,)) + f"{block_num:05d}".encode("ascii") + b"P"
|
|
733
|
+
if params[0] != function or params[8] != 9 or params[9:] != expected_address:
|
|
734
|
+
raise S7ProtocolError("Unexpected PLC download request")
|
|
735
|
+
return int(sequence)
|
|
736
|
+
|
|
737
|
+
@staticmethod
|
|
738
|
+
def build_download_fragment_response(sequence: int, is_last: bool, data: bytes) -> bytes:
|
|
739
|
+
"""Reply to a PLC DOWNLOAD_BLOCK request with one bounded fragment."""
|
|
740
|
+
if not 0 <= sequence <= 0xFFFF or len(data) > 0xFFFF:
|
|
741
|
+
raise ValueError("Invalid download response sequence or payload length")
|
|
742
|
+
parameters = bytes((S7Function.DOWNLOAD_BLOCK, 0 if is_last else 1))
|
|
743
|
+
payload = struct.pack(">HH", len(data), 0x00FB) + data
|
|
744
|
+
header = struct.pack(">BBHHHHH", 0x32, S7PDUType.ACK_DATA, 0, sequence, 2, len(payload), 0)
|
|
745
|
+
return header + parameters + payload
|
|
746
|
+
|
|
747
|
+
@staticmethod
|
|
748
|
+
def build_download_ended_response(sequence: int) -> bytes:
|
|
749
|
+
"""Acknowledge the PLC's DOWNLOAD_ENDED request."""
|
|
750
|
+
if not 0 <= sequence <= 0xFFFF:
|
|
751
|
+
raise ValueError("Invalid download response sequence")
|
|
752
|
+
return struct.pack(">BBHHHHH", 0x32, S7PDUType.ACK_DATA, 0, sequence, 1, 0, 0) + bytes((S7Function.DOWNLOAD_ENDED,))
|
|
753
|
+
|
|
724
754
|
def build_delete_block_request(self, block_type: int, block_num: int) -> bytes:
|
|
725
755
|
"""
|
|
726
756
|
Build delete block request.
|
|
@@ -845,16 +875,7 @@ class S7Protocol:
|
|
|
845
875
|
)
|
|
846
876
|
|
|
847
877
|
# Data section: block type (0x30 prefix + type per Snap7 C format)
|
|
848
|
-
data_section = struct.pack(
|
|
849
|
-
">BBHBBBB",
|
|
850
|
-
0x0A, # Return value (request)
|
|
851
|
-
0x00, # Transport size
|
|
852
|
-
0x0004, # Length (4 bytes)
|
|
853
|
-
0x30, # Block type indicator
|
|
854
|
-
block_type, # Block type code
|
|
855
|
-
0x0A, # Trailing bytes per Snap7 C
|
|
856
|
-
0x00,
|
|
857
|
-
)
|
|
878
|
+
data_section = struct.pack(">BBHBB", 0xFF, 0x09, 2, 0x30, block_type)
|
|
858
879
|
|
|
859
880
|
# S7 header for USER_DATA
|
|
860
881
|
header = struct.pack(
|
|
@@ -894,6 +915,8 @@ class S7Protocol:
|
|
|
894
915
|
|
|
895
916
|
if not raw_data:
|
|
896
917
|
return result
|
|
918
|
+
if len(raw_data) % 4 != 0:
|
|
919
|
+
raise S7ProtocolError("Block-count response length is not a multiple of four")
|
|
897
920
|
|
|
898
921
|
# Parse block entries (4 bytes each: 0x30 | type | count_hi | count_lo)
|
|
899
922
|
# Block type codes
|
|
@@ -913,8 +936,9 @@ class S7Protocol:
|
|
|
913
936
|
block_type = raw_data[offset + 1]
|
|
914
937
|
count = struct.unpack(">H", raw_data[offset + 2 : offset + 4])[0]
|
|
915
938
|
|
|
916
|
-
if indicator
|
|
917
|
-
|
|
939
|
+
if indicator != 0x30 or block_type not in type_to_name:
|
|
940
|
+
raise S7ProtocolError("Invalid block-count response entry")
|
|
941
|
+
result[type_to_name[block_type]] = count
|
|
918
942
|
|
|
919
943
|
offset += 4
|
|
920
944
|
|
|
@@ -937,6 +961,8 @@ class S7Protocol:
|
|
|
937
961
|
|
|
938
962
|
if not raw_data:
|
|
939
963
|
return result
|
|
964
|
+
if len(raw_data) % 4 != 0:
|
|
965
|
+
raise S7ProtocolError("Block-list response length is not a multiple of four")
|
|
940
966
|
|
|
941
967
|
# Parse block entries (4 bytes each per TDataFunGetBotItem:
|
|
942
968
|
# BlockNum(2) + Unknown(1) + BlockLang(1))
|
|
@@ -976,15 +1002,7 @@ class S7Protocol:
|
|
|
976
1002
|
# Block number is 5-digit zero-padded ASCII (e.g., 1 -> "00001")
|
|
977
1003
|
block_num_ascii = f"{block_num:05d}".encode("ascii")
|
|
978
1004
|
data_payload = struct.pack(">BB", 0x30, block_type) + b"A" + block_num_ascii
|
|
979
|
-
data_section = (
|
|
980
|
-
struct.pack(
|
|
981
|
-
">BBH",
|
|
982
|
-
0x0A, # Return value (request)
|
|
983
|
-
0x00, # Transport size
|
|
984
|
-
len(data_payload), # Length
|
|
985
|
-
)
|
|
986
|
-
+ data_payload
|
|
987
|
-
)
|
|
1005
|
+
data_section = struct.pack(">BBH", 0xFF, 0x09, len(data_payload)) + data_payload
|
|
988
1006
|
|
|
989
1007
|
# S7 header for USER_DATA
|
|
990
1008
|
header = struct.pack(
|
|
@@ -1030,8 +1048,8 @@ class S7Protocol:
|
|
|
1030
1048
|
data_info = response.get("data", {})
|
|
1031
1049
|
raw_data = data_info.get("data", b"")
|
|
1032
1050
|
|
|
1033
|
-
if len(raw_data)
|
|
1034
|
-
|
|
1051
|
+
if len(raw_data) != 78:
|
|
1052
|
+
raise S7ProtocolError("Block-info response must contain exactly 78 bytes")
|
|
1035
1053
|
|
|
1036
1054
|
# Parse block info structure per TResDataBlockInfo layout
|
|
1037
1055
|
result["block_type"] = raw_data[1]
|
|
@@ -1257,17 +1275,20 @@ class S7Protocol:
|
|
|
1257
1275
|
def to_bcd(value: int) -> int:
|
|
1258
1276
|
return ((value // 10) << 4) | (value % 10)
|
|
1259
1277
|
|
|
1260
|
-
year = dt.year % 100
|
|
1278
|
+
year = dt.year % 100
|
|
1279
|
+
millisecond = dt.microsecond // 1000
|
|
1261
1280
|
bcd_time = struct.pack(
|
|
1262
|
-
">
|
|
1263
|
-
0x00,
|
|
1264
|
-
|
|
1281
|
+
">BBBBBBBBBB",
|
|
1282
|
+
0x00,
|
|
1283
|
+
0x19, # Siemens date/time prefix
|
|
1284
|
+
to_bcd(year),
|
|
1265
1285
|
to_bcd(dt.month), # Month (BCD)
|
|
1266
1286
|
to_bcd(dt.day), # Day (BCD)
|
|
1267
1287
|
to_bcd(dt.hour), # Hour (BCD)
|
|
1268
1288
|
to_bcd(dt.minute), # Minute (BCD)
|
|
1269
1289
|
to_bcd(dt.second), # Second (BCD)
|
|
1270
|
-
(
|
|
1290
|
+
to_bcd(millisecond // 10),
|
|
1291
|
+
((millisecond % 10) << 4) | ((dt.weekday() + 1) & 0x0F),
|
|
1271
1292
|
)
|
|
1272
1293
|
|
|
1273
1294
|
# Parameter section for USER_DATA clock request
|
|
@@ -1284,15 +1305,7 @@ class S7Protocol:
|
|
|
1284
1305
|
)
|
|
1285
1306
|
|
|
1286
1307
|
# Data section with BCD time
|
|
1287
|
-
data_section = (
|
|
1288
|
-
struct.pack(
|
|
1289
|
-
">BBH",
|
|
1290
|
-
0x0A, # Return value (request)
|
|
1291
|
-
0x00, # Transport size
|
|
1292
|
-
len(bcd_time), # Length
|
|
1293
|
-
)
|
|
1294
|
-
+ bcd_time
|
|
1295
|
-
)
|
|
1308
|
+
data_section = struct.pack(">BBH", 0xFF, 0x09, len(bcd_time)) + bcd_time
|
|
1296
1309
|
|
|
1297
1310
|
# S7 header for USER_DATA
|
|
1298
1311
|
header = struct.pack(
|
|
@@ -1322,9 +1335,8 @@ class S7Protocol:
|
|
|
1322
1335
|
data_info = response.get("data", {})
|
|
1323
1336
|
raw_data = data_info.get("data", b"")
|
|
1324
1337
|
|
|
1325
|
-
if len(raw_data)
|
|
1326
|
-
|
|
1327
|
-
return dt_class.now().replace(microsecond=0)
|
|
1338
|
+
if len(raw_data) != 8:
|
|
1339
|
+
raise S7ProtocolError("Clock response must contain exactly eight bytes")
|
|
1328
1340
|
|
|
1329
1341
|
# Parse BCD time
|
|
1330
1342
|
def from_bcd(value: int) -> int:
|
|
@@ -1343,8 +1355,8 @@ class S7Protocol:
|
|
|
1343
1355
|
|
|
1344
1356
|
try:
|
|
1345
1357
|
return dt_class(full_year, month, day, hour, minute, second)
|
|
1346
|
-
except ValueError:
|
|
1347
|
-
|
|
1358
|
+
except ValueError as exc:
|
|
1359
|
+
raise S7ProtocolError("Clock response contains an invalid BCD timestamp") from exc
|
|
1348
1360
|
|
|
1349
1361
|
# ========================================================================
|
|
1350
1362
|
# Session Password PDU Builders (Security / Function Group 5)
|
|
@@ -1354,8 +1366,8 @@ class S7Protocol:
|
|
|
1354
1366
|
def encode_password(password: str) -> bytes:
|
|
1355
1367
|
"""Encode an S7 session password into the 8-byte wire format.
|
|
1356
1368
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1369
|
+
Snap7 pads with spaces, XORs with ``0x55``, and chains each byte
|
|
1370
|
+
from the encoded byte two positions earlier.
|
|
1359
1371
|
|
|
1360
1372
|
Args:
|
|
1361
1373
|
password: Plaintext password (max 8 characters).
|
|
@@ -1363,12 +1375,13 @@ class S7Protocol:
|
|
|
1363
1375
|
Returns:
|
|
1364
1376
|
8-byte encoded password block.
|
|
1365
1377
|
"""
|
|
1366
|
-
|
|
1367
|
-
|
|
1378
|
+
raw = password.encode("ascii")
|
|
1379
|
+
if not 1 <= len(raw) <= 8:
|
|
1380
|
+
raise ValueError("Session password must contain between one and eight ASCII characters")
|
|
1381
|
+
raw = raw.ljust(8, b" ")
|
|
1368
1382
|
encoded = bytearray(8)
|
|
1369
1383
|
for i in range(8):
|
|
1370
|
-
|
|
1371
|
-
encoded[i] = ((xored << 3) | (xored >> 5)) & 0xFF
|
|
1384
|
+
encoded[i] = raw[i] ^ 0x55 ^ (encoded[i - 2] if i >= 2 else 0)
|
|
1372
1385
|
return bytes(encoded)
|
|
1373
1386
|
|
|
1374
1387
|
def build_set_session_password_request(self, encoded_password: bytes) -> bytes:
|
|
@@ -1396,15 +1409,9 @@ class S7Protocol:
|
|
|
1396
1409
|
)
|
|
1397
1410
|
|
|
1398
1411
|
# Data section: encoded password
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
0x0A, # Return value (request)
|
|
1403
|
-
0x00, # Transport size
|
|
1404
|
-
len(encoded_password), # Length
|
|
1405
|
-
)
|
|
1406
|
-
+ encoded_password
|
|
1407
|
-
)
|
|
1412
|
+
if len(encoded_password) != 8:
|
|
1413
|
+
raise ValueError("Encoded session password must contain eight bytes")
|
|
1414
|
+
data_section = struct.pack(">BBH", 0xFF, 0x09, 8) + encoded_password
|
|
1408
1415
|
|
|
1409
1416
|
header = struct.pack(
|
|
1410
1417
|
">BBHHHH",
|
|
@@ -1457,7 +1464,12 @@ class S7Protocol:
|
|
|
1457
1464
|
|
|
1458
1465
|
return header + param_data + data_section
|
|
1459
1466
|
|
|
1460
|
-
def check_userdata_response(
|
|
1467
|
+
def check_userdata_response(
|
|
1468
|
+
self,
|
|
1469
|
+
response: Dict[str, Any],
|
|
1470
|
+
expected_group: int | None = None,
|
|
1471
|
+
expected_subfunction: int | None = None,
|
|
1472
|
+
) -> None:
|
|
1461
1473
|
"""Check a USERDATA response for errors.
|
|
1462
1474
|
|
|
1463
1475
|
Verifies both the parameter-level error code and the data section
|
|
@@ -1475,6 +1487,12 @@ class S7Protocol:
|
|
|
1475
1487
|
if param_error != 0:
|
|
1476
1488
|
error_msg = get_protocol_error_message(param_error)
|
|
1477
1489
|
raise S7ProtocolError(f"USERDATA request failed: {error_msg} (0x{param_error:04x})")
|
|
1490
|
+
if expected_group is not None and params.get("group") != expected_group:
|
|
1491
|
+
raise S7ProtocolError("Unexpected USERDATA response function group")
|
|
1492
|
+
if expected_subfunction is not None and params.get("subfunction") != expected_subfunction:
|
|
1493
|
+
raise S7ProtocolError("Unexpected USERDATA response subfunction")
|
|
1494
|
+
else:
|
|
1495
|
+
raise S7ProtocolError("Missing USERDATA response parameters")
|
|
1478
1496
|
|
|
1479
1497
|
data_info = response.get("data", {})
|
|
1480
1498
|
if isinstance(data_info, dict):
|
|
@@ -1482,6 +1500,8 @@ class S7Protocol:
|
|
|
1482
1500
|
if return_code != 0xFF:
|
|
1483
1501
|
desc = get_return_code_description(return_code)
|
|
1484
1502
|
raise S7ProtocolError(f"USERDATA request failed: {desc} (0x{return_code:02x})")
|
|
1503
|
+
else:
|
|
1504
|
+
raise S7ProtocolError("Missing USERDATA response data")
|
|
1485
1505
|
|
|
1486
1506
|
def build_cpu_state_request(self) -> bytes:
|
|
1487
1507
|
"""
|
|
@@ -1583,6 +1603,7 @@ class S7Protocol:
|
|
|
1583
1603
|
raise S7ProtocolError("Parameter section extends beyond PDU")
|
|
1584
1604
|
|
|
1585
1605
|
param_data = pdu[offset : offset + param_len]
|
|
1606
|
+
response["raw_parameters"] = param_data
|
|
1586
1607
|
response["parameters"] = self._parse_parameters(param_data)
|
|
1587
1608
|
offset += param_len
|
|
1588
1609
|
|
|
@@ -1592,16 +1613,27 @@ class S7Protocol:
|
|
|
1592
1613
|
raise S7ProtocolError("Data section extends beyond PDU")
|
|
1593
1614
|
|
|
1594
1615
|
data_section = pdu[offset : offset + data_len]
|
|
1595
|
-
|
|
1616
|
+
function_code = (response.get("parameters") or {}).get("function_code")
|
|
1617
|
+
if function_code == S7Function.WRITE_AREA:
|
|
1596
1618
|
# WRITE data is an array of acknowledgement codes, not a READ item header.
|
|
1597
1619
|
response["data"] = {"return_code": data_section[0]}
|
|
1620
|
+
elif function_code == S7Function.UPLOAD:
|
|
1621
|
+
response["data"] = {"data": data_section}
|
|
1598
1622
|
else:
|
|
1599
|
-
|
|
1623
|
+
parameters = response.get("parameters") or {}
|
|
1624
|
+
validate_item_length = function_code != S7Function.READ_AREA or parameters.get("item_count") == 1
|
|
1600
1625
|
response["data"] = self._parse_data_section(
|
|
1601
1626
|
data_section,
|
|
1602
|
-
validate_length=
|
|
1627
|
+
validate_length=validate_item_length,
|
|
1603
1628
|
)
|
|
1604
1629
|
response["raw_data"] = data_section
|
|
1630
|
+
offset += data_len
|
|
1631
|
+
|
|
1632
|
+
if offset != len(pdu):
|
|
1633
|
+
raise S7ProtocolError("S7 response contains trailing bytes")
|
|
1634
|
+
|
|
1635
|
+
if pdu_type == S7PDUType.USERDATA:
|
|
1636
|
+
self.check_userdata_response(response)
|
|
1605
1637
|
|
|
1606
1638
|
return response
|
|
1607
1639
|
|
|
@@ -1612,7 +1644,9 @@ class S7Protocol:
|
|
|
1612
1644
|
|
|
1613
1645
|
# Detect USERDATA response parameters:
|
|
1614
1646
|
# byte 0 = 0x00 (reserved), len >= 12, byte 2 = 0x12, byte 4 = 0x12 (method=response)
|
|
1615
|
-
if param_data[0] == 0x00 and len(param_data) >=
|
|
1647
|
+
if param_data[0] == 0x00 and len(param_data) >= 5 and param_data[2] == 0x12 and param_data[4] == 0x12:
|
|
1648
|
+
if len(param_data) != 12:
|
|
1649
|
+
raise S7ProtocolError("USERDATA response parameters must contain exactly 12 bytes")
|
|
1616
1650
|
return self._parse_userdata_response_params(param_data)
|
|
1617
1651
|
|
|
1618
1652
|
function_code = param_data[0]
|
|
@@ -1718,7 +1752,9 @@ class S7Protocol:
|
|
|
1718
1752
|
byte_length = data_length // 8
|
|
1719
1753
|
|
|
1720
1754
|
if validate_length and 4 + byte_length > len(data_section):
|
|
1721
|
-
raise S7ProtocolError("
|
|
1755
|
+
raise S7ProtocolError("Data item is truncated")
|
|
1756
|
+
if validate_length and 4 + byte_length < len(data_section):
|
|
1757
|
+
raise S7ProtocolError("Data item contains trailing bytes")
|
|
1722
1758
|
actual_data = data_section[4 : 4 + byte_length]
|
|
1723
1759
|
|
|
1724
1760
|
return {"return_code": return_code, "transport_size": transport_size, "data_length": data_length, "data": actual_data}
|