python-snap7 3.2.1__py3-none-any.whl → 3.3.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.2.1.dist-info → python_snap7-3.3.0.dist-info}/METADATA +24 -2
- {python_snap7-3.2.1.dist-info → python_snap7-3.3.0.dist-info}/RECORD +12 -12
- {python_snap7-3.2.1.dist-info → python_snap7-3.3.0.dist-info}/WHEEL +1 -1
- snap7/async_client.py +327 -5
- snap7/client.py +17 -6
- snap7/s7protocol.py +87 -103
- snap7/server/__init__.py +38 -29
- snap7/szl.py +1 -1
- snap7/util/getters.py +14 -5
- {python_snap7-3.2.1.dist-info → python_snap7-3.3.0.dist-info}/entry_points.txt +0 -0
- {python_snap7-3.2.1.dist-info → python_snap7-3.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_snap7-3.2.1.dist-info → python_snap7-3.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-snap7
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Pure Python S7 communication library for Siemens PLCs
|
|
5
5
|
Author-email: Gijs Molenaar <gijsmolenaar@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,7 +24,7 @@ License-File: LICENSE
|
|
|
24
24
|
Provides-Extra: test
|
|
25
25
|
Requires-Dist: pytest; extra == "test"
|
|
26
26
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
27
|
-
Requires-Dist: pytest-bdd<
|
|
27
|
+
Requires-Dist: pytest-bdd<10,>=8.1; extra == "test"
|
|
28
28
|
Requires-Dist: pytest-cov; extra == "test"
|
|
29
29
|
Requires-Dist: pytest-html; extra == "test"
|
|
30
30
|
Requires-Dist: hypothesis; extra == "test"
|
|
@@ -75,6 +75,26 @@ using the classic S7 protocol. It supports S7-300 and S7-400 controllers, as
|
|
|
75
75
|
well as S7-1200 and S7-1500 controllers with PUT/GET access enabled. It
|
|
76
76
|
supports Python 3.10+ on Windows, Linux, and macOS without native dependencies.
|
|
77
77
|
|
|
78
|
+
S7-1200 and S7-1500
|
|
79
|
+
===================
|
|
80
|
+
|
|
81
|
+
On S7-1200 and S7-1500 controllers, python-snap7 only works when PUT/GET access
|
|
82
|
+
is enabled in TIA Portal, and it cannot access optimized data blocks. PUT/GET
|
|
83
|
+
is unauthenticated and disabled by default, so leaving it disabled is the more
|
|
84
|
+
secure configuration.
|
|
85
|
+
|
|
86
|
+
If PUT/GET is disabled, or you need optimized data blocks or symbolic access,
|
|
87
|
+
use `s7commplus <https://github.com/gijzelaerr/s7commplus>`_ instead::
|
|
88
|
+
|
|
89
|
+
pip install s7commplus
|
|
90
|
+
|
|
91
|
+
s7commplus is a separate, younger project that implements S7CommPlus, the
|
|
92
|
+
protocol these controllers speak natively. Check the
|
|
93
|
+
`firmware versions validated so far <https://s7commplus.readthedocs.io/en/latest/connections.html>`_
|
|
94
|
+
before relying on it, and see the
|
|
95
|
+
`PLC support matrix <https://python-snap7.readthedocs.io/en/latest/plc-support.html>`_
|
|
96
|
+
for what python-snap7 supports on each controller family.
|
|
97
|
+
|
|
78
98
|
Installation
|
|
79
99
|
============
|
|
80
100
|
|
|
@@ -105,3 +125,5 @@ Read the full documentation at
|
|
|
105
125
|
|
|
106
126
|
The safe volunteer hardware-test procedure is in
|
|
107
127
|
`Real-PLC acceptance testing <doc/real-plc-testing.rst>`_.
|
|
128
|
+
The CPUs and firmware versions it has passed on are listed under
|
|
129
|
+
`Tested and Verified Hardware <doc/plc-support.rst>`_.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
python_snap7-3.
|
|
1
|
+
python_snap7-3.3.0.dist-info/licenses/LICENSE,sha256=PIihE_yP-8gc-ff9ikvQQPOa0biOCZ6Bc-6h3G3xRUQ,1097
|
|
2
2
|
s7/__init__.py,sha256=q6E4rA1BDx36PPkJS55xv7XCxwlaQeEK6HNotFjLeYo,1096
|
|
3
3
|
snap7/__init__.py,sha256=2NAAE1oaSSd7DqPl0dx7jNXtsM2PRBWRm9X-bkmgC6U,1275
|
|
4
|
-
snap7/async_client.py,sha256=
|
|
4
|
+
snap7/async_client.py,sha256=SxIf2W-mimITD6P0cL3J66vpEr1SeCHne645lwkmbPU,66346
|
|
5
5
|
snap7/cli.py,sha256=L1B7NMAemDcl93JUSqJV3Ls_CN2ghlG75nTMD08JkMs,13787
|
|
6
|
-
snap7/client.py,sha256=
|
|
6
|
+
snap7/client.py,sha256=WYRZiQ6qwyHMl-SUd_ckhcPmIOzaZ9RiIOKe2v26MXg,108330
|
|
7
7
|
snap7/client_base.py,sha256=tNZs_MA8FGit8pg9axNoljH1kFToDI1HIzXC8WQLYHs,11324
|
|
8
8
|
snap7/connection.py,sha256=Qjis-ICK6E74_YfoJuy2_lgDqfSYs4ULux1FShJVvtw,18834
|
|
9
9
|
snap7/datatypes.py,sha256=Gqu1XDuKkQI76n4TYrFrhm0RlPz4XNA5GKA-GJJXJDU,12042
|
|
@@ -17,18 +17,18 @@ snap7/partner.py,sha256=HxaTYfojsf5Z170R4cAYFzoMSwqCOF42qPCkOaxbUkU,37916
|
|
|
17
17
|
snap7/ppi.py,sha256=p0fVelfUQOT3QH5uuWOjpDxA5IyT3gx5FzCMaYYEuWQ,16070
|
|
18
18
|
snap7/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
snap7/rate_limiter.py,sha256=dnXCOJLWmszo8oskbFM0om0gPC4LHHCOr-Nt22tImeU,4151
|
|
20
|
-
snap7/s7protocol.py,sha256=
|
|
21
|
-
snap7/szl.py,sha256=
|
|
20
|
+
snap7/s7protocol.py,sha256=gPfU4BL3S4iicKgDrqksSTjPuRnVmHmrrMDzcLSBiDY,70946
|
|
21
|
+
snap7/szl.py,sha256=952lFvuTTE4nBlG2Lvn_3SaS_hazkM0ZfboIPgTwFM8,7999
|
|
22
22
|
snap7/tags.py,sha256=TmbNRHNWZVe3Yl_1L5WXE3Qb0lxIEW-veoCRGt0giZQ,26987
|
|
23
23
|
snap7/type.py,sha256=EiXqU0ayYYxlM-5fUcPL7psgL1EGNNqAa7JXH4QlAnU,9752
|
|
24
|
-
snap7/server/__init__.py,sha256=
|
|
24
|
+
snap7/server/__init__.py,sha256=BsLUWAn0X3kVJ6NNZvJ1iLXfw12ksjlO2cRfCWMDQAU,113218
|
|
25
25
|
snap7/server/__main__.py,sha256=OdOgyMmFThjSe2jSgvlbkquGYnZcBW6wubs2ldhNSOE,1206
|
|
26
26
|
snap7/util/__init__.py,sha256=d6n2oy7tNYke1ia0TUyWz_ni3l0XN1qvilM5sW0T-Es,1874
|
|
27
27
|
snap7/util/db.py,sha256=geesYS2oyOuTADOJa2me1p1rqJVzPuLTCfye6JIeav0,27120
|
|
28
|
-
snap7/util/getters.py,sha256=
|
|
28
|
+
snap7/util/getters.py,sha256=UjojRLazJECrngor_8BI38R8VMEMdXusC95zqNEErJg,25767
|
|
29
29
|
snap7/util/setters.py,sha256=BpFqVxxUbhqarV6Gzt4ZuxycHljtDYsONCr6e9G33Ms,27226
|
|
30
|
-
python_snap7-3.
|
|
31
|
-
python_snap7-3.
|
|
32
|
-
python_snap7-3.
|
|
33
|
-
python_snap7-3.
|
|
34
|
-
python_snap7-3.
|
|
30
|
+
python_snap7-3.3.0.dist-info/METADATA,sha256=_fe4bkkd7bKdlTxAUa6CbNOKpeuIYTytvvifNBSVY3g,4669
|
|
31
|
+
python_snap7-3.3.0.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
|
|
32
|
+
python_snap7-3.3.0.dist-info/entry_points.txt,sha256=IPZyYr1lKyavXXpAc4xO505afTGwTY8GmNFuy5lsSiM,75
|
|
33
|
+
python_snap7-3.3.0.dist-info/top_level.txt,sha256=PTgDoDG45QNL2XIE9RM7S_ZCdWQjtMMuXNyrI2UdnbU,9
|
|
34
|
+
python_snap7-3.3.0.dist-info/RECORD,,
|
snap7/async_client.py
CHANGED
|
@@ -10,9 +10,9 @@ import asyncio
|
|
|
10
10
|
import logging
|
|
11
11
|
import struct
|
|
12
12
|
import time
|
|
13
|
-
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
14
|
from contextvars import ContextVar
|
|
15
|
-
from typing import List, Any, Optional, Tuple, Type
|
|
15
|
+
from typing import List, Any, Optional, Tuple, Type, Union
|
|
16
16
|
from types import TracebackType
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
|
|
@@ -21,6 +21,8 @@ from .s7protocol import S7Protocol, S7UserDataGroup, S7UserDataSubfunction, get_
|
|
|
21
21
|
from .datatypes import S7DataTypes, S7WordLen
|
|
22
22
|
from .error import S7Error, S7ConnectionError, S7ProtocolError, S7TimeoutError
|
|
23
23
|
from .client_base import ClientMixin, _instrumented
|
|
24
|
+
from .tags import Tag
|
|
25
|
+
from .client import Client, _decode_tag, _encode_tag
|
|
24
26
|
from .szl import parse_cp_info_szl, parse_cpu_info_szl, parse_order_code_szl, parse_protection_szl
|
|
25
27
|
from .client import _parse_force_szl
|
|
26
28
|
from .rate_limiter import RateLimitAlgorithm, RateLimitBehavior, RequestRateLimiter
|
|
@@ -59,6 +61,8 @@ class AsyncISOTCPConnection:
|
|
|
59
61
|
COTP_PARAM_PDU_SIZE = 0xC0
|
|
60
62
|
COTP_PARAM_CALLING_TSAP = 0xC1
|
|
61
63
|
COTP_PARAM_CALLED_TSAP = 0xC2
|
|
64
|
+
COTP_PARAM_SUBNET_ID = 0xC6
|
|
65
|
+
COTP_PARAM_ROUTING_TSAP = 0xC7
|
|
62
66
|
|
|
63
67
|
def __init__(
|
|
64
68
|
self,
|
|
@@ -83,6 +87,17 @@ class AsyncISOTCPConnection:
|
|
|
83
87
|
self._reader: Optional[asyncio.StreamReader] = None
|
|
84
88
|
self._writer: Optional[asyncio.StreamWriter] = None
|
|
85
89
|
|
|
90
|
+
# Routing parameters (set via set_routing)
|
|
91
|
+
self._routing: bool = False
|
|
92
|
+
self._subnet_id: int = 0
|
|
93
|
+
self._routing_tsap: int = 0
|
|
94
|
+
|
|
95
|
+
def set_routing(self, subnet_id: int, dest_rack: int, dest_slot: int) -> None:
|
|
96
|
+
"""Configure S7 routing parameters for multi-subnet access (experimental)."""
|
|
97
|
+
self._routing = True
|
|
98
|
+
self._subnet_id = subnet_id & 0xFFFF
|
|
99
|
+
self._routing_tsap = 0x0100 | (dest_rack << 5) | dest_slot
|
|
100
|
+
|
|
86
101
|
async def connect(self, timeout: float = 5.0) -> None:
|
|
87
102
|
"""Establish ISO on TCP connection."""
|
|
88
103
|
self.timeout = timeout
|
|
@@ -204,6 +219,9 @@ class AsyncISOTCPConnection:
|
|
|
204
219
|
called_tsap = struct.pack(">BBH", self.COTP_PARAM_CALLED_TSAP, 2, self.remote_tsap)
|
|
205
220
|
pdu_size_param = struct.pack(">BBB", self.COTP_PARAM_PDU_SIZE, 1, self.tpdu_size)
|
|
206
221
|
parameters = calling_tsap + called_tsap + pdu_size_param
|
|
222
|
+
if self._routing:
|
|
223
|
+
parameters += struct.pack(">BBH", self.COTP_PARAM_SUBNET_ID, 2, self._subnet_id)
|
|
224
|
+
parameters += struct.pack(">BBH", self.COTP_PARAM_ROUTING_TSAP, 2, self._routing_tsap)
|
|
207
225
|
total_length = 6 + len(parameters)
|
|
208
226
|
cr_pdu = struct.pack(">B", total_length) + base_pdu[1:] + parameters
|
|
209
227
|
|
|
@@ -457,6 +475,78 @@ class AsyncClient(ClientMixin):
|
|
|
457
475
|
|
|
458
476
|
return self
|
|
459
477
|
|
|
478
|
+
@property
|
|
479
|
+
def is_alive(self) -> bool:
|
|
480
|
+
"""Whether the client is connected.
|
|
481
|
+
|
|
482
|
+
Unlike the synchronous client there is no background heartbeat, so this
|
|
483
|
+
reflects the connection state seen by the last operation.
|
|
484
|
+
"""
|
|
485
|
+
return self.connected and self.connection is not None and self.connection.connected
|
|
486
|
+
|
|
487
|
+
async def connect_routed(
|
|
488
|
+
self,
|
|
489
|
+
host: str,
|
|
490
|
+
router_rack: int,
|
|
491
|
+
router_slot: int,
|
|
492
|
+
subnet: int,
|
|
493
|
+
dest_rack: int,
|
|
494
|
+
dest_slot: int,
|
|
495
|
+
port: int = 102,
|
|
496
|
+
timeout: float = 5.0,
|
|
497
|
+
) -> "AsyncClient":
|
|
498
|
+
"""Connect to an S7 PLC via a routing gateway on another subnet.
|
|
499
|
+
|
|
500
|
+
.. warning:: This method is experimental and may change in future versions.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
host: IP address of the routing gateway PLC
|
|
504
|
+
router_rack: Rack number of the gateway PLC
|
|
505
|
+
router_slot: Slot number of the gateway PLC
|
|
506
|
+
subnet: Subnet ID of the target network (0x0000-0xFFFF)
|
|
507
|
+
dest_rack: Rack number of the destination PLC
|
|
508
|
+
dest_slot: Slot number of the destination PLC
|
|
509
|
+
port: TCP port (default 102)
|
|
510
|
+
timeout: Connection timeout in seconds
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Self for method chaining
|
|
514
|
+
"""
|
|
515
|
+
self.host = host
|
|
516
|
+
self.port = port
|
|
517
|
+
self.rack = router_rack
|
|
518
|
+
self.slot = router_slot
|
|
519
|
+
self._params[Parameter.RemotePort] = port
|
|
520
|
+
|
|
521
|
+
# Remote TSAP targets the gateway rack/slot
|
|
522
|
+
self.remote_tsap = (self.connection_type << 8) | (router_rack << 5) | router_slot
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
start_time = time.time()
|
|
526
|
+
|
|
527
|
+
self.connection = AsyncISOTCPConnection(
|
|
528
|
+
host=host, port=port, local_tsap=self.local_tsap, remote_tsap=self.remote_tsap
|
|
529
|
+
)
|
|
530
|
+
self.connection.set_routing(subnet, dest_rack, dest_slot)
|
|
531
|
+
await self.connection.connect(timeout=timeout)
|
|
532
|
+
|
|
533
|
+
await self._setup_communication()
|
|
534
|
+
|
|
535
|
+
self.connected = True
|
|
536
|
+
self._exec_time = int((time.time() - start_time) * 1000)
|
|
537
|
+
logger.info(
|
|
538
|
+
f"Connected (routed) to {host}:{port} via rack {router_rack} slot {router_slot}, "
|
|
539
|
+
f"subnet {subnet:#06x} -> rack {dest_rack} slot {dest_slot}"
|
|
540
|
+
)
|
|
541
|
+
except Exception as e:
|
|
542
|
+
await self.disconnect()
|
|
543
|
+
if isinstance(e, S7Error):
|
|
544
|
+
raise
|
|
545
|
+
else:
|
|
546
|
+
raise S7ConnectionError(f"Routed connection failed: {e}")
|
|
547
|
+
|
|
548
|
+
return self
|
|
549
|
+
|
|
460
550
|
async def disconnect(self) -> int:
|
|
461
551
|
"""Disconnect from S7 PLC.
|
|
462
552
|
|
|
@@ -508,6 +598,224 @@ class AsyncClient(ClientMixin):
|
|
|
508
598
|
await self.write_area(Area.DB, db_number, start, data)
|
|
509
599
|
return 0
|
|
510
600
|
|
|
601
|
+
# ---------------------------------------------------------------
|
|
602
|
+
# Typed DB access and tag helpers (parity with the sync Client)
|
|
603
|
+
# ---------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
async def db_read_array(self, db_number: int, start: int, count: int, fmt: str = ">f") -> list[Any]:
|
|
606
|
+
"""Read *count* consecutive values of struct format *fmt* from a DB."""
|
|
607
|
+
item_size = struct.calcsize(fmt)
|
|
608
|
+
data = await self.db_read(db_number, start, item_size * count)
|
|
609
|
+
return [struct.unpack_from(fmt, data, i * item_size)[0] for i in range(count)]
|
|
610
|
+
|
|
611
|
+
async def db_write_array(self, db_number: int, start: int, values: list[Any], fmt: str = ">f") -> int:
|
|
612
|
+
"""Write *values* packed with struct format *fmt* to a DB. Returns 0 on success."""
|
|
613
|
+
item_size = struct.calcsize(fmt)
|
|
614
|
+
data = bytearray(item_size * len(values))
|
|
615
|
+
for i, v in enumerate(values):
|
|
616
|
+
struct.pack_into(fmt, data, i * item_size, v)
|
|
617
|
+
return await self.db_write(db_number, start, data)
|
|
618
|
+
|
|
619
|
+
async def db_read_bool(self, db_number: int, byte_offset: int, bit_offset: int) -> bool:
|
|
620
|
+
"""Read a single bit from a DB."""
|
|
621
|
+
from .util import get_bool
|
|
622
|
+
|
|
623
|
+
data = await self.db_read(db_number, byte_offset, 1)
|
|
624
|
+
return get_bool(data, 0, bit_offset)
|
|
625
|
+
|
|
626
|
+
async def db_write_bool(self, db_number: int, byte_offset: int, bit_offset: int, value: bool) -> None:
|
|
627
|
+
"""Write a single bit to a DB, preserving the other bits in the byte."""
|
|
628
|
+
from .util import set_bool
|
|
629
|
+
|
|
630
|
+
data = await self.db_read(db_number, byte_offset, 1)
|
|
631
|
+
set_bool(data, 0, bit_offset, value)
|
|
632
|
+
await self.db_write(db_number, byte_offset, data)
|
|
633
|
+
|
|
634
|
+
async def db_read_byte(self, db_number: int, offset: int) -> int:
|
|
635
|
+
"""Read a BYTE (8-bit unsigned) from a DB."""
|
|
636
|
+
data = await self.db_read(db_number, offset, 1)
|
|
637
|
+
return data[0]
|
|
638
|
+
|
|
639
|
+
async def db_write_byte(self, db_number: int, offset: int, value: int) -> None:
|
|
640
|
+
"""Write a BYTE (8-bit unsigned) to a DB."""
|
|
641
|
+
from .util import set_byte
|
|
642
|
+
|
|
643
|
+
data = bytearray(1)
|
|
644
|
+
set_byte(data, 0, value)
|
|
645
|
+
await self.db_write(db_number, offset, data)
|
|
646
|
+
|
|
647
|
+
async def db_read_int(self, db_number: int, offset: int) -> int:
|
|
648
|
+
"""Read an INT (16-bit signed) from a DB."""
|
|
649
|
+
from .util import get_int
|
|
650
|
+
|
|
651
|
+
return get_int(await self.db_read(db_number, offset, 2), 0)
|
|
652
|
+
|
|
653
|
+
async def db_write_int(self, db_number: int, offset: int, value: int) -> None:
|
|
654
|
+
"""Write an INT (16-bit signed) to a DB."""
|
|
655
|
+
from .util import set_int
|
|
656
|
+
|
|
657
|
+
data = bytearray(2)
|
|
658
|
+
set_int(data, 0, value)
|
|
659
|
+
await self.db_write(db_number, offset, data)
|
|
660
|
+
|
|
661
|
+
async def db_read_uint(self, db_number: int, offset: int) -> int:
|
|
662
|
+
"""Read a UINT (16-bit unsigned) from a DB."""
|
|
663
|
+
from .util import get_uint
|
|
664
|
+
|
|
665
|
+
return get_uint(await self.db_read(db_number, offset, 2), 0)
|
|
666
|
+
|
|
667
|
+
async def db_write_uint(self, db_number: int, offset: int, value: int) -> None:
|
|
668
|
+
"""Write a UINT (16-bit unsigned) to a DB."""
|
|
669
|
+
from .util import set_uint
|
|
670
|
+
|
|
671
|
+
data = bytearray(2)
|
|
672
|
+
set_uint(data, 0, value)
|
|
673
|
+
await self.db_write(db_number, offset, data)
|
|
674
|
+
|
|
675
|
+
async def db_read_word(self, db_number: int, offset: int) -> int:
|
|
676
|
+
"""Read a WORD (16-bit unsigned) from a DB."""
|
|
677
|
+
data = await self.db_read(db_number, offset, 2)
|
|
678
|
+
return (data[0] << 8) | data[1]
|
|
679
|
+
|
|
680
|
+
async def db_write_word(self, db_number: int, offset: int, value: int) -> None:
|
|
681
|
+
"""Write a WORD (16-bit unsigned) to a DB."""
|
|
682
|
+
from .util import set_word
|
|
683
|
+
|
|
684
|
+
data = bytearray(2)
|
|
685
|
+
set_word(data, 0, value)
|
|
686
|
+
await self.db_write(db_number, offset, data)
|
|
687
|
+
|
|
688
|
+
async def db_read_dint(self, db_number: int, offset: int) -> int:
|
|
689
|
+
"""Read a DINT (32-bit signed) from a DB."""
|
|
690
|
+
from .util import get_dint
|
|
691
|
+
|
|
692
|
+
return get_dint(await self.db_read(db_number, offset, 4), 0)
|
|
693
|
+
|
|
694
|
+
async def db_write_dint(self, db_number: int, offset: int, value: int) -> None:
|
|
695
|
+
"""Write a DINT (32-bit signed) to a DB."""
|
|
696
|
+
from .util import set_dint
|
|
697
|
+
|
|
698
|
+
data = bytearray(4)
|
|
699
|
+
set_dint(data, 0, value)
|
|
700
|
+
await self.db_write(db_number, offset, data)
|
|
701
|
+
|
|
702
|
+
async def db_read_udint(self, db_number: int, offset: int) -> int:
|
|
703
|
+
"""Read a UDINT (32-bit unsigned) from a DB."""
|
|
704
|
+
from .util import get_udint
|
|
705
|
+
|
|
706
|
+
return get_udint(await self.db_read(db_number, offset, 4), 0)
|
|
707
|
+
|
|
708
|
+
async def db_write_udint(self, db_number: int, offset: int, value: int) -> None:
|
|
709
|
+
"""Write a UDINT (32-bit unsigned) to a DB."""
|
|
710
|
+
from .util import set_udint
|
|
711
|
+
|
|
712
|
+
data = bytearray(4)
|
|
713
|
+
set_udint(data, 0, value)
|
|
714
|
+
await self.db_write(db_number, offset, data)
|
|
715
|
+
|
|
716
|
+
async def db_read_dword(self, db_number: int, offset: int) -> int:
|
|
717
|
+
"""Read a DWORD (32-bit unsigned) from a DB."""
|
|
718
|
+
from .util import get_dword
|
|
719
|
+
|
|
720
|
+
return get_dword(await self.db_read(db_number, offset, 4), 0)
|
|
721
|
+
|
|
722
|
+
async def db_write_dword(self, db_number: int, offset: int, value: int) -> None:
|
|
723
|
+
"""Write a DWORD (32-bit unsigned) to a DB."""
|
|
724
|
+
from .util import set_dword
|
|
725
|
+
|
|
726
|
+
data = bytearray(4)
|
|
727
|
+
set_dword(data, 0, value)
|
|
728
|
+
await self.db_write(db_number, offset, data)
|
|
729
|
+
|
|
730
|
+
async def db_read_real(self, db_number: int, offset: int) -> float:
|
|
731
|
+
"""Read a REAL (32-bit float) from a DB."""
|
|
732
|
+
from .util import get_real
|
|
733
|
+
|
|
734
|
+
return get_real(await self.db_read(db_number, offset, 4), 0)
|
|
735
|
+
|
|
736
|
+
async def db_write_real(self, db_number: int, offset: int, value: float) -> None:
|
|
737
|
+
"""Write a REAL (32-bit float) to a DB."""
|
|
738
|
+
from .util import set_real
|
|
739
|
+
|
|
740
|
+
data = bytearray(4)
|
|
741
|
+
set_real(data, 0, value)
|
|
742
|
+
await self.db_write(db_number, offset, data)
|
|
743
|
+
|
|
744
|
+
async def db_read_lreal(self, db_number: int, offset: int) -> float:
|
|
745
|
+
"""Read a LREAL (64-bit float) from a DB."""
|
|
746
|
+
from .util import get_lreal
|
|
747
|
+
|
|
748
|
+
return get_lreal(await self.db_read(db_number, offset, 8), 0)
|
|
749
|
+
|
|
750
|
+
async def db_write_lreal(self, db_number: int, offset: int, value: float) -> None:
|
|
751
|
+
"""Write a LREAL (64-bit float) to a DB."""
|
|
752
|
+
from .util import set_lreal
|
|
753
|
+
|
|
754
|
+
data = bytearray(8)
|
|
755
|
+
set_lreal(data, 0, value)
|
|
756
|
+
await self.db_write(db_number, offset, data)
|
|
757
|
+
|
|
758
|
+
async def db_read_string(self, db_number: int, offset: int) -> str:
|
|
759
|
+
"""Read an S7 STRING from a DB (header first to learn the max length)."""
|
|
760
|
+
from .util import get_string
|
|
761
|
+
|
|
762
|
+
header = await self.db_read(db_number, offset, 2)
|
|
763
|
+
data = await self.db_read(db_number, offset, 2 + header[0])
|
|
764
|
+
return get_string(data, 0)
|
|
765
|
+
|
|
766
|
+
async def db_write_string(self, db_number: int, offset: int, value: str, max_length: int = 254) -> None:
|
|
767
|
+
"""Write an S7 STRING with the given maximum length to a DB."""
|
|
768
|
+
from .util import set_string
|
|
769
|
+
|
|
770
|
+
data = bytearray(2 + max_length)
|
|
771
|
+
set_string(data, 0, value, max_length)
|
|
772
|
+
await self.db_write(db_number, offset, data)
|
|
773
|
+
|
|
774
|
+
async def db_read_wstring(self, db_number: int, offset: int) -> str:
|
|
775
|
+
"""Read an S7 WSTRING from a DB (header first to learn the max length)."""
|
|
776
|
+
from .util import get_wstring
|
|
777
|
+
|
|
778
|
+
header = await self.db_read(db_number, offset, 4)
|
|
779
|
+
max_len = (header[0] << 8) | header[1]
|
|
780
|
+
data = await self.db_read(db_number, offset, 4 + max_len * 2)
|
|
781
|
+
return get_wstring(data, 0)
|
|
782
|
+
|
|
783
|
+
async def db_write_wstring(self, db_number: int, offset: int, value: str, max_length: int = 254) -> None:
|
|
784
|
+
"""Write an S7 WSTRING with the given maximum length (in characters) to a DB."""
|
|
785
|
+
from .util import set_wstring
|
|
786
|
+
|
|
787
|
+
data = bytearray(4 + max_length * 2)
|
|
788
|
+
set_wstring(data, 0, value, max_length)
|
|
789
|
+
await self.db_write(db_number, offset, data)
|
|
790
|
+
|
|
791
|
+
async def read_tag(self, tag: "Union[Tag, str]", encoding: str = "latin-1") -> Any:
|
|
792
|
+
"""Read a typed value by :class:`~snap7.tags.Tag` or PLC4X-style address string."""
|
|
793
|
+
resolved = Tag.from_string(tag) if isinstance(tag, str) else tag
|
|
794
|
+
if resolved.is_symbolic:
|
|
795
|
+
raise NotImplementedError("Symbolic (LID-based) tag access is not supported by the classic S7 client.")
|
|
796
|
+
data = await self.read_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, resolved.size)
|
|
797
|
+
return _decode_tag(resolved, bytearray(data), encoding=encoding)
|
|
798
|
+
|
|
799
|
+
async def write_tag(self, tag: "Union[Tag, str]", value: Any, encoding: str = "latin-1") -> int:
|
|
800
|
+
"""Write a typed value by :class:`~snap7.tags.Tag` or address string. Returns 0 on success."""
|
|
801
|
+
resolved = Tag.from_string(tag) if isinstance(tag, str) else tag
|
|
802
|
+
if resolved.is_symbolic:
|
|
803
|
+
raise NotImplementedError("Symbolic (LID-based) tag access is not supported by the classic S7 client.")
|
|
804
|
+
buf = bytearray(resolved.size)
|
|
805
|
+
if resolved.datatype.upper() == "BOOL":
|
|
806
|
+
# Preserve the other bits of the byte
|
|
807
|
+
current = await self.read_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, 1)
|
|
808
|
+
buf[0] = current[0]
|
|
809
|
+
_encode_tag(resolved, buf, value, encoding=encoding)
|
|
810
|
+
return await self.write_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, buf)
|
|
811
|
+
|
|
812
|
+
async def read_tags(self, tags: "Sequence[Union[Tag, str]]", encoding: str = "latin-1") -> list[Any]:
|
|
813
|
+
"""Read multiple tags in a single optimized request, in input order."""
|
|
814
|
+
resolved = [Tag.from_string(t) if isinstance(t, str) else t for t in tags]
|
|
815
|
+
items = [{"area": Area(t.area), "db_number": t.db_number, "start": t.byte_offset, "size": t.size} for t in resolved]
|
|
816
|
+
_code, data_list = await self.read_multi_vars(items)
|
|
817
|
+
return [_decode_tag(t, bytearray(d), encoding=encoding) for t, d in zip(resolved, data_list)]
|
|
818
|
+
|
|
511
819
|
async def db_get(self, db_number: int, size: int = 0) -> bytearray:
|
|
512
820
|
"""Get entire DB.
|
|
513
821
|
|
|
@@ -1010,7 +1318,9 @@ class AsyncClient(ClientMixin):
|
|
|
1010
1318
|
|
|
1011
1319
|
request = self.protocol.build_set_clock_request(dt)
|
|
1012
1320
|
response = await self._send_receive(request)
|
|
1013
|
-
self.protocol.check_userdata_response(
|
|
1321
|
+
self.protocol.check_userdata_response(
|
|
1322
|
+
response, S7UserDataGroup.TIME, S7UserDataSubfunction.SET_CLOCK, accept_null_ack=True
|
|
1323
|
+
)
|
|
1014
1324
|
logger.info(f"Set PLC datetime to {dt}")
|
|
1015
1325
|
return 0
|
|
1016
1326
|
|
|
@@ -1090,6 +1400,14 @@ class AsyncClient(ClientMixin):
|
|
|
1090
1400
|
|
|
1091
1401
|
return szl
|
|
1092
1402
|
|
|
1403
|
+
async def read_diagnostic_buffer(self) -> list[dict[str, Any]]:
|
|
1404
|
+
"""Read the PLC diagnostic buffer (experimental), newest first.
|
|
1405
|
+
|
|
1406
|
+
Each entry is a dict with keys ``event_id``, ``timestamp`` and ``info``.
|
|
1407
|
+
"""
|
|
1408
|
+
szl = await self.read_szl(0x00A0, 0)
|
|
1409
|
+
return Client._parse_diagnostic_buffer(bytes(szl.Data[: szl.Header.LengthDR]))
|
|
1410
|
+
|
|
1093
1411
|
async def read_szl_list(self) -> bytes:
|
|
1094
1412
|
"""Read list of available SZL IDs."""
|
|
1095
1413
|
if not self.get_connected():
|
|
@@ -1198,7 +1516,9 @@ class AsyncClient(ClientMixin):
|
|
|
1198
1516
|
encoded = self.protocol.encode_password(password)
|
|
1199
1517
|
request = self.protocol.build_set_session_password_request(encoded)
|
|
1200
1518
|
response = await self._send_receive(request)
|
|
1201
|
-
self.protocol.check_userdata_response(
|
|
1519
|
+
self.protocol.check_userdata_response(
|
|
1520
|
+
response, S7UserDataGroup.SECURITY, S7UserDataSubfunction.SET_SESSION_PASSWORD, accept_null_ack=True
|
|
1521
|
+
)
|
|
1202
1522
|
logger.info("Session password set successfully")
|
|
1203
1523
|
return 0
|
|
1204
1524
|
|
|
@@ -1219,7 +1539,9 @@ class AsyncClient(ClientMixin):
|
|
|
1219
1539
|
|
|
1220
1540
|
request = self.protocol.build_clear_session_password_request()
|
|
1221
1541
|
response = await self._send_receive(request)
|
|
1222
|
-
self.protocol.check_userdata_response(
|
|
1542
|
+
self.protocol.check_userdata_response(
|
|
1543
|
+
response, S7UserDataGroup.SECURITY, S7UserDataSubfunction.CLEAR_SESSION_PASSWORD, accept_null_ack=True
|
|
1544
|
+
)
|
|
1223
1545
|
logger.info("Session password cleared successfully")
|
|
1224
1546
|
return 0
|
|
1225
1547
|
|
snap7/client.py
CHANGED
|
@@ -1892,7 +1892,9 @@ class Client(ClientMixin):
|
|
|
1892
1892
|
# Build and send set clock request
|
|
1893
1893
|
request = self.protocol.build_set_clock_request(dt)
|
|
1894
1894
|
response = self._send_receive(request)
|
|
1895
|
-
self.protocol.check_userdata_response(
|
|
1895
|
+
self.protocol.check_userdata_response(
|
|
1896
|
+
response, S7UserDataGroup.TIME, S7UserDataSubfunction.SET_CLOCK, accept_null_ack=True
|
|
1897
|
+
)
|
|
1896
1898
|
|
|
1897
1899
|
logger.info(f"Set PLC datetime to {dt}")
|
|
1898
1900
|
return 0
|
|
@@ -1915,7 +1917,7 @@ class Client(ClientMixin):
|
|
|
1915
1917
|
"""
|
|
1916
1918
|
Compress PLC memory.
|
|
1917
1919
|
|
|
1918
|
-
Sends real S7 PLC_CONTROL protocol with PI service "
|
|
1920
|
+
Sends real S7 PLC_CONTROL protocol with PI service "_GARB".
|
|
1919
1921
|
|
|
1920
1922
|
Args:
|
|
1921
1923
|
timeout: Timeout in milliseconds (used for receive timeout)
|
|
@@ -1938,7 +1940,7 @@ class Client(ClientMixin):
|
|
|
1938
1940
|
"""
|
|
1939
1941
|
Copy RAM to ROM.
|
|
1940
1942
|
|
|
1941
|
-
Sends real S7 PLC_CONTROL protocol with PI service "
|
|
1943
|
+
Sends real S7 PLC_CONTROL protocol with PI service "_MODU" and file system "EP".
|
|
1942
1944
|
|
|
1943
1945
|
Args:
|
|
1944
1946
|
timeout: Timeout in milliseconds (used for receive timeout)
|
|
@@ -2093,7 +2095,9 @@ class Client(ClientMixin):
|
|
|
2093
2095
|
return self.protocol.build_set_session_password_request(encoded)
|
|
2094
2096
|
|
|
2095
2097
|
response = self._send_receive_with_reconnect(build_request)
|
|
2096
|
-
self.protocol.check_userdata_response(
|
|
2098
|
+
self.protocol.check_userdata_response(
|
|
2099
|
+
response, S7UserDataGroup.SECURITY, S7UserDataSubfunction.SET_SESSION_PASSWORD, accept_null_ack=True
|
|
2100
|
+
)
|
|
2097
2101
|
logger.info("Session password set successfully")
|
|
2098
2102
|
return 0
|
|
2099
2103
|
|
|
@@ -2116,7 +2120,9 @@ class Client(ClientMixin):
|
|
|
2116
2120
|
return self.protocol.build_clear_session_password_request()
|
|
2117
2121
|
|
|
2118
2122
|
response = self._send_receive_with_reconnect(build_request)
|
|
2119
|
-
self.protocol.check_userdata_response(
|
|
2123
|
+
self.protocol.check_userdata_response(
|
|
2124
|
+
response, S7UserDataGroup.SECURITY, S7UserDataSubfunction.CLEAR_SESSION_PASSWORD, accept_null_ack=True
|
|
2125
|
+
)
|
|
2120
2126
|
logger.info("Session password cleared successfully")
|
|
2121
2127
|
return 0
|
|
2122
2128
|
|
|
@@ -2233,6 +2239,11 @@ class Client(ClientMixin):
|
|
|
2233
2239
|
szl = self.read_szl(0x00A0, 0)
|
|
2234
2240
|
raw = bytes(szl.Data[: szl.Header.LengthDR])
|
|
2235
2241
|
|
|
2242
|
+
return self._parse_diagnostic_buffer(raw)
|
|
2243
|
+
|
|
2244
|
+
@classmethod
|
|
2245
|
+
def _parse_diagnostic_buffer(cls, raw: bytes) -> list[dict[str, Any]]:
|
|
2246
|
+
"""Decode raw SZL 0x00A0 data into diagnostic entries."""
|
|
2236
2247
|
entries: list[dict[str, Any]] = []
|
|
2237
2248
|
# Each diagnostic entry is 20 bytes
|
|
2238
2249
|
entry_size = 20
|
|
@@ -2243,7 +2254,7 @@ class Client(ClientMixin):
|
|
|
2243
2254
|
# BCD-encoded timestamp at offset 2..9
|
|
2244
2255
|
ts_bytes = raw[offset + 2 : offset + 10]
|
|
2245
2256
|
try:
|
|
2246
|
-
ts =
|
|
2257
|
+
ts = cls._parse_bcd_timestamp(ts_bytes)
|
|
2247
2258
|
except Exception:
|
|
2248
2259
|
ts = None
|
|
2249
2260
|
|
snap7/s7protocol.py
CHANGED
|
@@ -114,6 +114,10 @@ _STOP_PARAMS = bytes.fromhex("29000000000009") + b"P_PROGRAM"
|
|
|
114
114
|
_HOT_START_PARAMS = bytes.fromhex("28000000000000fd000009") + b"P_PROGRAM"
|
|
115
115
|
# Cold start: same as hot start but with block "C " before PI service
|
|
116
116
|
_COLD_START_PARAMS = bytes.fromhex("28000000000000fd0002432009") + b"P_PROGRAM"
|
|
117
|
+
# Compress: function(0x28) + 7 reserved + block count(0) + PI length(5) + "_GARB" (as native Snap7 TReqFunCompress)
|
|
118
|
+
_COMPRESS_PARAMS = bytes.fromhex("28000000000000fd000005") + b"_GARB"
|
|
119
|
+
# Copy RAM to ROM: like compress but with file system "EP" before PI service "_MODU" (native TReqFunCopyRamToRom)
|
|
120
|
+
_COPY_RAM_TO_ROM_PARAMS = bytes.fromhex("28000000000000fd0002455005") + b"_MODU"
|
|
117
121
|
|
|
118
122
|
|
|
119
123
|
class S7Protocol:
|
|
@@ -402,7 +406,7 @@ class S7Protocol:
|
|
|
402
406
|
Build PLC control request using the S7 PI service PDU format.
|
|
403
407
|
|
|
404
408
|
Args:
|
|
405
|
-
operation: Control operation ('stop', 'hot_start', 'cold_start')
|
|
409
|
+
operation: Control operation ('stop', 'hot_start', 'cold_start', 'compress', 'copy_ram_to_rom')
|
|
406
410
|
|
|
407
411
|
Returns:
|
|
408
412
|
Complete S7 PDU for PLC control
|
|
@@ -411,6 +415,8 @@ class S7Protocol:
|
|
|
411
415
|
"stop": _STOP_PARAMS,
|
|
412
416
|
"hot_start": _HOT_START_PARAMS,
|
|
413
417
|
"cold_start": _COLD_START_PARAMS,
|
|
418
|
+
"compress": _COMPRESS_PARAMS,
|
|
419
|
+
"copy_ram_to_rom": _COPY_RAM_TO_ROM_PARAMS,
|
|
414
420
|
}
|
|
415
421
|
|
|
416
422
|
if operation not in params:
|
|
@@ -449,87 +455,23 @@ class S7Protocol:
|
|
|
449
455
|
"""
|
|
450
456
|
Build PLC control request for memory compression.
|
|
451
457
|
|
|
452
|
-
Uses PI service "
|
|
458
|
+
Uses PI service "_GARB" (compress memory), as native Snap7 does.
|
|
453
459
|
|
|
454
460
|
Returns:
|
|
455
461
|
Complete S7 PDU for compress request
|
|
456
462
|
"""
|
|
457
|
-
|
|
458
|
-
pi_service = b"_MSZL"
|
|
459
|
-
|
|
460
|
-
# Parameter section: function code + PI service
|
|
461
|
-
# Format: func(1) + unknown(7) + pi_len(1) + pi_service
|
|
462
|
-
param_data = (
|
|
463
|
-
struct.pack(
|
|
464
|
-
">BBBBBBBBB",
|
|
465
|
-
S7Function.PLC_CONTROL, # 0x28
|
|
466
|
-
0x00, # Reserved
|
|
467
|
-
0x00, # Reserved
|
|
468
|
-
0x00, # Reserved
|
|
469
|
-
0x00, # Reserved
|
|
470
|
-
0x00, # Reserved
|
|
471
|
-
0x00, # Reserved
|
|
472
|
-
0x00, # Reserved
|
|
473
|
-
len(pi_service), # PI service length
|
|
474
|
-
)
|
|
475
|
-
+ pi_service
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
header = struct.pack(
|
|
479
|
-
">BBHHHH",
|
|
480
|
-
0x32, # Protocol ID
|
|
481
|
-
S7PDUType.REQUEST, # PDU type
|
|
482
|
-
0x0000, # Reserved
|
|
483
|
-
self._next_sequence(), # Sequence
|
|
484
|
-
len(param_data), # Parameter length
|
|
485
|
-
0x0000, # Data length
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
return header + param_data
|
|
463
|
+
return self.build_plc_control_request("compress")
|
|
489
464
|
|
|
490
465
|
def build_copy_ram_to_rom_request(self) -> bytes:
|
|
491
466
|
"""
|
|
492
467
|
Build PLC control request for copying RAM to ROM.
|
|
493
468
|
|
|
494
|
-
Uses PI service "
|
|
469
|
+
Uses PI service "_MODU" with the "EP" file system, as native Snap7 does.
|
|
495
470
|
|
|
496
471
|
Returns:
|
|
497
472
|
Complete S7 PDU for copy RAM to ROM request
|
|
498
473
|
"""
|
|
499
|
-
|
|
500
|
-
# Uses EP parameter for target file system
|
|
501
|
-
pi_service = b"_MSZL"
|
|
502
|
-
file_id = b"P" # P = passive file system (ROM)
|
|
503
|
-
|
|
504
|
-
# Parameter section with file system identifier
|
|
505
|
-
param_data = (
|
|
506
|
-
struct.pack(
|
|
507
|
-
">BBBBBBBBB",
|
|
508
|
-
S7Function.PLC_CONTROL, # 0x28
|
|
509
|
-
0x00, # Reserved
|
|
510
|
-
0x00, # Reserved
|
|
511
|
-
0x00, # Reserved
|
|
512
|
-
0x00, # Reserved
|
|
513
|
-
0x00, # Reserved
|
|
514
|
-
0x00, # Reserved
|
|
515
|
-
len(file_id), # File ID length
|
|
516
|
-
len(pi_service), # PI service length
|
|
517
|
-
)
|
|
518
|
-
+ file_id
|
|
519
|
-
+ pi_service
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
header = struct.pack(
|
|
523
|
-
">BBHHHH",
|
|
524
|
-
0x32, # Protocol ID
|
|
525
|
-
S7PDUType.REQUEST, # PDU type
|
|
526
|
-
0x0000, # Reserved
|
|
527
|
-
self._next_sequence(), # Sequence
|
|
528
|
-
len(param_data), # Parameter length
|
|
529
|
-
0x0000, # Data length
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
return header + param_data
|
|
474
|
+
return self.build_plc_control_request("copy_ram_to_rom")
|
|
533
475
|
|
|
534
476
|
# ========================================================================
|
|
535
477
|
# Block Transfer PDU Builders (Upload/Download)
|
|
@@ -913,10 +855,9 @@ class S7Protocol:
|
|
|
913
855
|
data_info = response.get("data", {})
|
|
914
856
|
raw_data = data_info.get("data", b"")
|
|
915
857
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
raise S7ProtocolError("Block-count response length is not a multiple of four")
|
|
858
|
+
# Native Snap7 requires exactly seven four-byte entries (28 bytes)
|
|
859
|
+
if len(raw_data) != 28:
|
|
860
|
+
raise S7ProtocolError("Block-count response must contain exactly seven entries (28 bytes)")
|
|
920
861
|
|
|
921
862
|
# Parse block entries (4 bytes each: 0x30 | type | count_hi | count_lo)
|
|
922
863
|
# Block type codes
|
|
@@ -930,18 +871,19 @@ class S7Protocol:
|
|
|
930
871
|
0x46: "SFBCount", # System Function Block
|
|
931
872
|
}
|
|
932
873
|
|
|
933
|
-
|
|
934
|
-
|
|
874
|
+
seen: set[int] = set()
|
|
875
|
+
for offset in range(0, 28, 4):
|
|
935
876
|
indicator = raw_data[offset]
|
|
936
877
|
block_type = raw_data[offset + 1]
|
|
937
878
|
count = struct.unpack(">H", raw_data[offset + 2 : offset + 4])[0]
|
|
938
879
|
|
|
939
880
|
if indicator != 0x30 or block_type not in type_to_name:
|
|
940
881
|
raise S7ProtocolError("Invalid block-count response entry")
|
|
882
|
+
if block_type in seen:
|
|
883
|
+
raise S7ProtocolError("Duplicate block type in block-count response")
|
|
884
|
+
seen.add(block_type)
|
|
941
885
|
result[type_to_name[block_type]] = count
|
|
942
886
|
|
|
943
|
-
offset += 4
|
|
944
|
-
|
|
945
887
|
return result
|
|
946
888
|
|
|
947
889
|
def parse_list_blocks_of_type_response(self, response: Dict[str, Any]) -> List[int]:
|
|
@@ -998,10 +940,10 @@ class S7Protocol:
|
|
|
998
940
|
0x00, # DataRef (0x00 for initial request)
|
|
999
941
|
)
|
|
1000
942
|
|
|
1001
|
-
# Data section: [0x30, type,
|
|
943
|
+
# Data section: [0x30, type, ASCII_num(5), 'A'] per Snap7 C TReqDataBlockInfo
|
|
1002
944
|
# Block number is 5-digit zero-padded ASCII (e.g., 1 -> "00001")
|
|
1003
945
|
block_num_ascii = f"{block_num:05d}".encode("ascii")
|
|
1004
|
-
data_payload = struct.pack(">BB", 0x30, block_type) + b"A"
|
|
946
|
+
data_payload = struct.pack(">BB", 0x30, block_type) + block_num_ascii + b"A"
|
|
1005
947
|
data_section = struct.pack(">BBH", 0xFF, 0x09, len(data_payload)) + data_payload
|
|
1006
948
|
|
|
1007
949
|
# S7 header for USER_DATA
|
|
@@ -1117,8 +1059,8 @@ class S7Protocol:
|
|
|
1117
1059
|
# Data section: SZL ID and Index
|
|
1118
1060
|
data_section = struct.pack(
|
|
1119
1061
|
">BBHHH",
|
|
1120
|
-
|
|
1121
|
-
|
|
1062
|
+
0xFF, # Return value (0xFF, as engineering tools and native Snap7 send for requests carrying data)
|
|
1063
|
+
0x09, # Transport size (octet string)
|
|
1122
1064
|
0x0004, # Length (4 bytes for ID + Index)
|
|
1123
1065
|
szl_id, # SZL ID
|
|
1124
1066
|
szl_index, # SZL Index
|
|
@@ -1149,18 +1091,23 @@ class S7Protocol:
|
|
|
1149
1091
|
Returns:
|
|
1150
1092
|
Complete S7 PDU for follow-up request
|
|
1151
1093
|
"""
|
|
1152
|
-
# Parameter section
|
|
1094
|
+
# Parameter section as sent by engineering tools for continuation requests: 12 bytes, method 0x12,
|
|
1095
|
+
# DataRef = sequence_number, followed by last-data-unit and error code fields set to zero
|
|
1153
1096
|
type_group = 0x40 | (group & 0x0F) # Type 4 (request) | group
|
|
1154
1097
|
param_data = struct.pack(
|
|
1155
|
-
">
|
|
1098
|
+
">BBBBBBBBBBBB",
|
|
1156
1099
|
0x00, # Reserved
|
|
1157
1100
|
0x01, # Parameter count
|
|
1158
1101
|
0x12, # Type/length header
|
|
1159
|
-
|
|
1160
|
-
|
|
1102
|
+
0x08, # Length of following data
|
|
1103
|
+
0x12, # Method (0x12)
|
|
1161
1104
|
type_group, # Type | Group
|
|
1162
1105
|
subfunction, # Subfunction
|
|
1163
1106
|
sequence_number, # DataRef from previous response
|
|
1107
|
+
0x00, # Last data unit
|
|
1108
|
+
0x00, # Reserved
|
|
1109
|
+
0x00, # Error code high
|
|
1110
|
+
0x00, # Error code low
|
|
1164
1111
|
)
|
|
1165
1112
|
|
|
1166
1113
|
# Minimal data section for follow-up
|
|
@@ -1268,6 +1215,9 @@ class S7Protocol:
|
|
|
1268
1215
|
|
|
1269
1216
|
Returns:
|
|
1270
1217
|
Complete S7 PDU for set clock request
|
|
1218
|
+
|
|
1219
|
+
Raises:
|
|
1220
|
+
ValueError: If the year is outside 1990-2089, which the two-digit S7 year cannot represent.
|
|
1271
1221
|
"""
|
|
1272
1222
|
|
|
1273
1223
|
# Convert datetime to BCD format
|
|
@@ -1275,6 +1225,9 @@ class S7Protocol:
|
|
|
1275
1225
|
def to_bcd(value: int) -> int:
|
|
1276
1226
|
return ((value // 10) << 4) | (value % 10)
|
|
1277
1227
|
|
|
1228
|
+
# The two-digit S7 year maps 90-99 to 1990-1999 and 00-89 to 2000-2089
|
|
1229
|
+
if not 1990 <= dt.year <= 2089:
|
|
1230
|
+
raise ValueError(f"S7 clock supports years 1990-2089, got {dt.year}")
|
|
1278
1231
|
year = dt.year % 100
|
|
1279
1232
|
millisecond = dt.microsecond // 1000
|
|
1280
1233
|
bcd_time = struct.pack(
|
|
@@ -1288,7 +1241,7 @@ class S7Protocol:
|
|
|
1288
1241
|
to_bcd(dt.minute), # Minute (BCD)
|
|
1289
1242
|
to_bcd(dt.second), # Second (BCD)
|
|
1290
1243
|
to_bcd(millisecond // 10),
|
|
1291
|
-
((millisecond % 10) << 4) | (
|
|
1244
|
+
((millisecond % 10) << 4) | (dt.isoweekday() % 7 + 1), # S7 weekday: 1=Sunday .. 7=Saturday
|
|
1292
1245
|
)
|
|
1293
1246
|
|
|
1294
1247
|
# Parameter section for USER_DATA clock request
|
|
@@ -1335,26 +1288,39 @@ class S7Protocol:
|
|
|
1335
1288
|
data_info = response.get("data", {})
|
|
1336
1289
|
raw_data = data_info.get("data", b"")
|
|
1337
1290
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1291
|
+
# The S7 clock data is ten bytes: reserved, century (0x19/0x20), then year, month, day,
|
|
1292
|
+
# hour, minute, second and milliseconds/weekday. The older eight-byte form (no century byte,
|
|
1293
|
+
# no milliseconds) is still accepted.
|
|
1294
|
+
if len(raw_data) == 10:
|
|
1295
|
+
time_bytes = raw_data[2:]
|
|
1296
|
+
elif len(raw_data) == 8:
|
|
1297
|
+
time_bytes = raw_data[1:]
|
|
1298
|
+
else:
|
|
1299
|
+
raise S7ProtocolError("Clock response must contain ten bytes (or eight in the legacy form)")
|
|
1340
1300
|
|
|
1341
|
-
# Parse BCD time
|
|
1342
1301
|
def from_bcd(value: int) -> int:
|
|
1302
|
+
if value >> 4 > 9 or value & 0x0F > 9:
|
|
1303
|
+
raise S7ProtocolError("Clock response contains an invalid BCD timestamp")
|
|
1343
1304
|
return ((value >> 4) * 10) + (value & 0x0F)
|
|
1344
1305
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1306
|
+
year = from_bcd(time_bytes[0])
|
|
1307
|
+
month = from_bcd(time_bytes[1])
|
|
1308
|
+
day = from_bcd(time_bytes[2])
|
|
1309
|
+
hour = from_bcd(time_bytes[3])
|
|
1310
|
+
minute = from_bcd(time_bytes[4])
|
|
1311
|
+
second = from_bcd(time_bytes[5])
|
|
1312
|
+
microsecond = 0
|
|
1313
|
+
if len(raw_data) == 10:
|
|
1314
|
+
# Byte 8: milliseconds hundreds/tens (BCD); high nibble of byte 9: milliseconds ones
|
|
1315
|
+
microsecond = (from_bcd(time_bytes[6]) * 10 + (time_bytes[7] >> 4)) * 1000
|
|
1316
|
+
if microsecond > 999000:
|
|
1317
|
+
raise S7ProtocolError("Clock response contains an invalid BCD timestamp")
|
|
1318
|
+
|
|
1319
|
+
# The two-digit S7 year maps 90-99 to 1990-1999 and 00-89 to 2000-2089
|
|
1354
1320
|
full_year = 2000 + year if year < 90 else 1900 + year
|
|
1355
1321
|
|
|
1356
1322
|
try:
|
|
1357
|
-
return dt_class(full_year, month, day, hour, minute, second)
|
|
1323
|
+
return dt_class(full_year, month, day, hour, minute, second, microsecond)
|
|
1358
1324
|
except ValueError as exc:
|
|
1359
1325
|
raise S7ProtocolError("Clock response contains an invalid BCD timestamp") from exc
|
|
1360
1326
|
|
|
@@ -1469,6 +1435,8 @@ class S7Protocol:
|
|
|
1469
1435
|
response: Dict[str, Any],
|
|
1470
1436
|
expected_group: int | None = None,
|
|
1471
1437
|
expected_subfunction: int | None = None,
|
|
1438
|
+
*,
|
|
1439
|
+
accept_null_ack: bool = False,
|
|
1472
1440
|
) -> None:
|
|
1473
1441
|
"""Check a USERDATA response for errors.
|
|
1474
1442
|
|
|
@@ -1477,6 +1445,11 @@ class S7Protocol:
|
|
|
1477
1445
|
|
|
1478
1446
|
Args:
|
|
1479
1447
|
response: Parsed S7 response from :meth:`parse_response`.
|
|
1448
|
+
expected_group: Expected function group, if it should be verified.
|
|
1449
|
+
expected_subfunction: Expected subfunction, if it should be verified.
|
|
1450
|
+
accept_null_ack: For services that return no data (set clock, session passwords). Real PLCs
|
|
1451
|
+
acknowledge these with return code 0x0A, transport size 0 and length 0 next to a zero
|
|
1452
|
+
parameter error code, which is then a success.
|
|
1480
1453
|
|
|
1481
1454
|
Raises:
|
|
1482
1455
|
~snap7.error.S7ProtocolError: If the response indicates an error.
|
|
@@ -1487,6 +1460,8 @@ class S7Protocol:
|
|
|
1487
1460
|
if param_error != 0:
|
|
1488
1461
|
error_msg = get_protocol_error_message(param_error)
|
|
1489
1462
|
raise S7ProtocolError(f"USERDATA request failed: {error_msg} (0x{param_error:04x})")
|
|
1463
|
+
if params.get("last_data_unit", 0) not in (0x00, 0x01):
|
|
1464
|
+
raise S7ProtocolError(f"Invalid USERDATA last data unit value 0x{params['last_data_unit']:02x}")
|
|
1490
1465
|
if expected_group is not None and params.get("group") != expected_group:
|
|
1491
1466
|
raise S7ProtocolError("Unexpected USERDATA response function group")
|
|
1492
1467
|
if expected_subfunction is not None and params.get("subfunction") != expected_subfunction:
|
|
@@ -1497,7 +1472,13 @@ class S7Protocol:
|
|
|
1497
1472
|
data_info = response.get("data", {})
|
|
1498
1473
|
if isinstance(data_info, dict):
|
|
1499
1474
|
return_code = data_info.get("return_code", 0xFF)
|
|
1500
|
-
|
|
1475
|
+
null_ack = (
|
|
1476
|
+
accept_null_ack
|
|
1477
|
+
and return_code == 0x0A
|
|
1478
|
+
and data_info.get("transport_size", 0) == 0
|
|
1479
|
+
and data_info.get("data_length", 0) == 0
|
|
1480
|
+
)
|
|
1481
|
+
if return_code != 0xFF and not null_ack:
|
|
1501
1482
|
desc = get_return_code_description(return_code)
|
|
1502
1483
|
raise S7ProtocolError(f"USERDATA request failed: {desc} (0x{return_code:02x})")
|
|
1503
1484
|
else:
|
|
@@ -1580,8 +1561,9 @@ class S7Protocol:
|
|
|
1580
1561
|
if pdu_type not in (S7PDUType.ACK, S7PDUType.ACK_DATA, S7PDUType.USERDATA):
|
|
1581
1562
|
raise S7ProtocolError(f"Expected response PDU, got {pdu_type}")
|
|
1582
1563
|
|
|
1564
|
+
# Like native Snap7, treat the whole 16-bit header error word as the failure indicator
|
|
1583
1565
|
combined_error = (error_class << 8) | error_code
|
|
1584
|
-
if
|
|
1566
|
+
if combined_error != 0:
|
|
1585
1567
|
error_msg = get_protocol_error_message(combined_error)
|
|
1586
1568
|
raise S7ProtocolError(
|
|
1587
1569
|
f"S7 protocol error (class={error_class:#04x}, code={error_code:#04x}): {error_msg}",
|
|
@@ -1633,7 +1615,9 @@ class S7Protocol:
|
|
|
1633
1615
|
raise S7ProtocolError("S7 response contains trailing bytes")
|
|
1634
1616
|
|
|
1635
1617
|
if pdu_type == S7PDUType.USERDATA:
|
|
1636
|
-
|
|
1618
|
+
# The null acknowledgement is only a success for no-data services; callers that expect data
|
|
1619
|
+
# run check_userdata_response again without accept_null_ack.
|
|
1620
|
+
self.check_userdata_response(response, accept_null_ack=True)
|
|
1637
1621
|
|
|
1638
1622
|
return response
|
|
1639
1623
|
|
|
@@ -1707,7 +1691,7 @@ class S7Protocol:
|
|
|
1707
1691
|
[6] Subfunction
|
|
1708
1692
|
[7] Sequence number (used as DataRef in follow-up)
|
|
1709
1693
|
[8] Data unit reference
|
|
1710
|
-
[9] Last data unit (0x00 = last,
|
|
1694
|
+
[9] Last data unit (0x00 = last, 0x01 = more)
|
|
1711
1695
|
[10-11] Error code
|
|
1712
1696
|
"""
|
|
1713
1697
|
type_group = param_data[5]
|
snap7/server/__init__.py
CHANGED
|
@@ -1129,14 +1129,10 @@ class Server:
|
|
|
1129
1129
|
pi_service = params.get("pi_service", b"")
|
|
1130
1130
|
|
|
1131
1131
|
# Check for PI service operations
|
|
1132
|
-
if pi_service == b"
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
logger.info(f"Copy RAM to ROM requested from {client_address}")
|
|
1137
|
-
else:
|
|
1138
|
-
# Compress memory
|
|
1139
|
-
logger.info(f"Compress memory requested from {client_address}")
|
|
1132
|
+
if pi_service == b"_MODU":
|
|
1133
|
+
logger.info(f"Copy RAM to ROM requested from {client_address}")
|
|
1134
|
+
elif pi_service == b"_GARB":
|
|
1135
|
+
logger.info(f"Compress memory requested from {client_address}")
|
|
1140
1136
|
elif len(params) >= 2:
|
|
1141
1137
|
# Has restart type parameter - start operation
|
|
1142
1138
|
restart_type = params.get("restart_type", 1)
|
|
@@ -1411,9 +1407,10 @@ class Server:
|
|
|
1411
1407
|
if b"P_PROGRAM" in param_data:
|
|
1412
1408
|
restart_type = 2 if b"C " in param_data else 1
|
|
1413
1409
|
return {"function_code": function_code, "restart_type": restart_type, "pi_service": b"P_PROGRAM"}
|
|
1414
|
-
elif b"
|
|
1415
|
-
|
|
1416
|
-
|
|
1410
|
+
elif b"_MODU" in param_data:
|
|
1411
|
+
return {"function_code": function_code, "pi_service": b"_MODU", "file_id": b"EP"}
|
|
1412
|
+
elif b"_GARB" in param_data:
|
|
1413
|
+
return {"function_code": function_code, "pi_service": b"_GARB"}
|
|
1417
1414
|
elif b"_DELE" in param_data:
|
|
1418
1415
|
return {"function_code": function_code, "pi_service": b"_DELE"}
|
|
1419
1416
|
if len(param_data) >= 2 and param_data[1] in (1, 2):
|
|
@@ -1867,15 +1864,17 @@ class Server:
|
|
|
1867
1864
|
"""
|
|
1868
1865
|
Handle get clock request - returns current server time.
|
|
1869
1866
|
|
|
1870
|
-
Returns time in BCD format (
|
|
1867
|
+
Returns time in BCD format (10 bytes, as native Snap7 ``TResDataGetTime``):
|
|
1871
1868
|
- Byte 0: Reserved (0x00)
|
|
1872
|
-
- Byte 1:
|
|
1873
|
-
- Byte 2:
|
|
1874
|
-
- Byte 3:
|
|
1875
|
-
- Byte 4:
|
|
1876
|
-
- Byte 5:
|
|
1877
|
-
- Byte 6:
|
|
1878
|
-
- Byte 7:
|
|
1869
|
+
- Byte 1: Century (0x19 for years 1990-1999, 0x20 for 2000-2089)
|
|
1870
|
+
- Byte 2: Year (BCD, 0-99)
|
|
1871
|
+
- Byte 3: Month (BCD, 1-12)
|
|
1872
|
+
- Byte 4: Day (BCD, 1-31)
|
|
1873
|
+
- Byte 5: Hour (BCD, 0-23)
|
|
1874
|
+
- Byte 6: Minute (BCD, 0-59)
|
|
1875
|
+
- Byte 7: Second (BCD, 0-59)
|
|
1876
|
+
- Byte 8: Milliseconds hundreds/tens (BCD)
|
|
1877
|
+
- Byte 9: Milliseconds ones (high nibble) and day of week (low nibble, 1=Sunday .. 7=Saturday)
|
|
1879
1878
|
"""
|
|
1880
1879
|
from datetime import datetime
|
|
1881
1880
|
|
|
@@ -1885,16 +1884,19 @@ class Server:
|
|
|
1885
1884
|
return ((value // 10) << 4) | (value % 10)
|
|
1886
1885
|
|
|
1887
1886
|
year = now.year % 100
|
|
1887
|
+
millisecond = now.microsecond // 1000
|
|
1888
1888
|
bcd_time = struct.pack(
|
|
1889
|
-
">
|
|
1889
|
+
">BBBBBBBBBB",
|
|
1890
1890
|
0x00, # Reserved
|
|
1891
|
+
0x19 if now.year < 2000 else 0x20, # Century
|
|
1891
1892
|
to_bcd(year), # Year (BCD)
|
|
1892
1893
|
to_bcd(now.month), # Month (BCD)
|
|
1893
1894
|
to_bcd(now.day), # Day (BCD)
|
|
1894
1895
|
to_bcd(now.hour), # Hour (BCD)
|
|
1895
1896
|
to_bcd(now.minute), # Minute (BCD)
|
|
1896
1897
|
to_bcd(now.second), # Second (BCD)
|
|
1897
|
-
(
|
|
1898
|
+
to_bcd(millisecond // 10), # Milliseconds hundreds/tens (BCD)
|
|
1899
|
+
((millisecond % 10) << 4) | (now.isoweekday() % 7 + 1), # Milliseconds ones, day of week (1=Sunday)
|
|
1898
1900
|
)
|
|
1899
1901
|
|
|
1900
1902
|
logger.debug(f"Get clock from {client_address}: returning {now}")
|
|
@@ -1940,8 +1942,8 @@ class Server:
|
|
|
1940
1942
|
|
|
1941
1943
|
logger.info(f"Set clock from {client_address}: {timestamp}")
|
|
1942
1944
|
|
|
1943
|
-
# Return success (
|
|
1944
|
-
return self._build_userdata_success_response(request, userdata_params, b"")
|
|
1945
|
+
# Return success (null acknowledgement, as real PLCs send for set clock)
|
|
1946
|
+
return self._build_userdata_success_response(request, userdata_params, b"", null_ack=True)
|
|
1945
1947
|
|
|
1946
1948
|
def _handle_security(
|
|
1947
1949
|
self, request: Dict[str, Any], userdata_params: Dict[str, Any], client_address: Tuple[str, int]
|
|
@@ -1953,7 +1955,7 @@ class Server:
|
|
|
1953
1955
|
"""
|
|
1954
1956
|
logger.debug(f"Security request from {client_address} (returning success)")
|
|
1955
1957
|
# Return success - emulator doesn't require password
|
|
1956
|
-
return self._build_userdata_success_response(request, userdata_params, b"")
|
|
1958
|
+
return self._build_userdata_success_response(request, userdata_params, b"", null_ack=True)
|
|
1957
1959
|
|
|
1958
1960
|
def _handle_list_all_blocks(
|
|
1959
1961
|
self, request: Dict[str, Any], userdata_params: Dict[str, Any], client_address: Tuple[str, int]
|
|
@@ -2098,13 +2100,13 @@ class Server:
|
|
|
2098
2100
|
# Block type code constants
|
|
2099
2101
|
block_db = 0x41 # Data Block
|
|
2100
2102
|
|
|
2101
|
-
# Parse request: handle
|
|
2103
|
+
# Parse request: handle the Snap7 format [0x30, type, ASCII_num(5), 'A']
|
|
2102
2104
|
# and old format [type, num(2), 0x41]
|
|
2103
2105
|
if len(raw_data) >= 8 and raw_data[0] == 0x30:
|
|
2104
|
-
#
|
|
2106
|
+
# Snap7 format: 0x30 + type + 5-digit ASCII number + 'A'
|
|
2105
2107
|
requested_type = raw_data[1]
|
|
2106
2108
|
try:
|
|
2107
|
-
block_number = int(raw_data[
|
|
2109
|
+
block_number = int(raw_data[2:7].decode("ascii"))
|
|
2108
2110
|
except (ValueError, UnicodeDecodeError):
|
|
2109
2111
|
block_number = 1
|
|
2110
2112
|
elif len(raw_data) >= 3:
|
|
@@ -2198,7 +2200,9 @@ class Server:
|
|
|
2198
2200
|
|
|
2199
2201
|
return header + param_data + data_section
|
|
2200
2202
|
|
|
2201
|
-
def _build_userdata_success_response(
|
|
2203
|
+
def _build_userdata_success_response(
|
|
2204
|
+
self, request: Dict[str, Any], userdata_params: Dict[str, Any], data: bytes, null_ack: bool = False
|
|
2205
|
+
) -> bytes:
|
|
2202
2206
|
"""
|
|
2203
2207
|
Build USER_DATA success response PDU.
|
|
2204
2208
|
|
|
@@ -2206,6 +2210,8 @@ class Server:
|
|
|
2206
2210
|
request: Original request
|
|
2207
2211
|
userdata_params: Parsed USER_DATA parameters
|
|
2208
2212
|
data: Response data
|
|
2213
|
+
null_ack: Acknowledge with the no-data form a real PLC uses for services that return nothing
|
|
2214
|
+
(return code 0x0A, transport size 0, length 0) instead of 0xFF/0x09
|
|
2209
2215
|
|
|
2210
2216
|
Returns:
|
|
2211
2217
|
Success response PDU
|
|
@@ -2232,7 +2238,10 @@ class Server:
|
|
|
2232
2238
|
)
|
|
2233
2239
|
|
|
2234
2240
|
# Data section: return code (0xFF = success) + data
|
|
2235
|
-
|
|
2241
|
+
if null_ack:
|
|
2242
|
+
data_section = struct.pack(">BBH", 0x0A, 0x00, 0)
|
|
2243
|
+
else:
|
|
2244
|
+
data_section = struct.pack(">BBH", 0xFF, 0x09, len(data)) + data
|
|
2236
2245
|
|
|
2237
2246
|
# Build S7 header for USERDATA (10 bytes, no error_class/error_code in header)
|
|
2238
2247
|
header = struct.pack(
|
snap7/szl.py
CHANGED
|
@@ -104,7 +104,7 @@ def _parse_order_code_structured(data: bytes, record_len: int, ndr: int, order_c
|
|
|
104
104
|
|
|
105
105
|
if record_id == 0x0001 and record_len >= 22:
|
|
106
106
|
found_structured = True
|
|
107
|
-
order_code.OrderCode = rec[2:22].rstrip(b"\x00")
|
|
107
|
+
order_code.OrderCode = rec[2:22].rstrip(b"\x00 ")
|
|
108
108
|
if record_len >= 26 and not has_0x0007 and (rec[23] != 0 or rec[24] != 0):
|
|
109
109
|
order_code.V1, order_code.V2, order_code.V3 = rec[23], rec[24], rec[25]
|
|
110
110
|
|
snap7/util/getters.py
CHANGED
|
@@ -218,6 +218,10 @@ def get_string(bytearray_: Buffer, byte_index: int, encoding: str = "latin-1") -
|
|
|
218
218
|
"String contains {str_length} chars, but max. {max_string_size} chars are expected or is "
|
|
219
219
|
"larger than 254. Bytearray doesn't seem to be a valid string."
|
|
220
220
|
)
|
|
221
|
+
if byte_index + 2 + str_length > len(bytearray_):
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"String contains {str_length} chars, but only {len(bytearray_) - byte_index - 2} bytes are in the buffer"
|
|
224
|
+
)
|
|
221
225
|
raw = bytes(bytearray_[byte_index + 2 : byte_index + 2 + str_length])
|
|
222
226
|
return raw.decode(encoding)
|
|
223
227
|
|
|
@@ -781,17 +785,22 @@ def get_wstring(bytearray_: Buffer, byte_index: int) -> str:
|
|
|
781
785
|
|
|
782
786
|
max_wstring_size = bytearray_[byte_index : byte_index + 2]
|
|
783
787
|
packed = struct.pack("2B", *max_wstring_size)
|
|
784
|
-
|
|
788
|
+
max_wstring_chars = struct.unpack(">H", packed)[0]
|
|
785
789
|
|
|
786
790
|
wstr_length_raw = bytearray_[byte_index + 2 : byte_index + 4]
|
|
787
|
-
|
|
791
|
+
wstr_chars = struct.unpack(">H", struct.pack("2B", *wstr_length_raw))[0]
|
|
788
792
|
|
|
789
|
-
if
|
|
793
|
+
if wstr_chars > max_wstring_chars or max_wstring_chars > 16382:
|
|
790
794
|
logger.error("The wstring is too big for the size encountered in specification")
|
|
791
795
|
logger.error("WRONG SIZED STRING ENCOUNTERED")
|
|
792
796
|
raise TypeError(
|
|
793
|
-
f"WString contains {
|
|
797
|
+
f"WString contains {wstr_chars} chars, but max {max_wstring_chars} chars are "
|
|
794
798
|
f"expected or is larger than 16382. Bytearray doesn't seem to be a valid string."
|
|
795
799
|
)
|
|
796
800
|
|
|
797
|
-
|
|
801
|
+
wstr_bytes = wstr_chars * 2
|
|
802
|
+
if wstring_start + wstr_bytes > len(bytearray_):
|
|
803
|
+
raise ValueError(
|
|
804
|
+
f"WString contains {wstr_chars} chars, but only {len(bytearray_) - wstring_start} bytes are in the buffer"
|
|
805
|
+
)
|
|
806
|
+
return bytes(bytearray_[wstring_start : wstring_start + wstr_bytes]).decode("utf-16-be")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|