python-snap7 3.1.0__tar.gz → 3.1.2__tar.gz
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.1.0/python_snap7.egg-info → python_snap7-3.1.2}/PKG-INFO +2 -2
- {python_snap7-3.1.0 → python_snap7-3.1.2}/pyproject.toml +2 -2
- {python_snap7-3.1.0 → python_snap7-3.1.2/python_snap7.egg-info}/PKG-INFO +2 -2
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/async_client.py +16 -4
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/client.py +43 -18
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/client_base.py +8 -2
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/connection.py +11 -3
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/server/__init__.py +187 -62
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/util/getters.py +16 -8
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/util/setters.py +24 -35
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_async_client.py +51 -2
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_client.py +12 -1
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_reconnect.py +22 -1
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_server.py +110 -3
- {python_snap7-3.1.0 → python_snap7-3.1.2}/LICENSE +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/MANIFEST.in +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/README.rst +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/python_snap7.egg-info/SOURCES.txt +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/python_snap7.egg-info/dependency_links.txt +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/python_snap7.egg-info/entry_points.txt +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/python_snap7.egg-info/requires.txt +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/python_snap7.egg-info/top_level.txt +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/setup.cfg +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/__init__.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/cli.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/datatypes.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/demo.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/discovery.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/error.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/log.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/logo.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/optimizer.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/partner.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/py.typed +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/s7protocol.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/server/__main__.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/szl.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/tags.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/type.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/util/__init__.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/util/db.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/snap7/util/symbols.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_api_surface.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_behavioral_compatibility.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_cli.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_client_e2e.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_datatypes.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_demo.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_discovery.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_finalize.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_hypothesis.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_logging.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_logo_client.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_mainloop.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_multipacket.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_optimizer.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_partner.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_property.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_routing.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_s7protocol.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_stress.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_tags.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_typed_access.py +0 -0
- {python_snap7-3.1.0 → python_snap7-3.1.2}/tests/test_util.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-snap7
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.2
|
|
4
4
|
Summary: Pure Python S7 communication library for Siemens PLCs
|
|
5
5
|
Author-email: Gijs Molenaar <gijsmolenaar@gmail.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/gijzelaerr/python-snap7
|
|
8
8
|
Project-URL: Documentation, https://python-snap7.readthedocs.io/en/latest/
|
|
9
9
|
Keywords: snap7,s7,siemens,plc
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-snap7"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.2"
|
|
8
8
|
description = "Pure Python S7 communication library for Siemens PLCs"
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
authors = [
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
24
|
"Programming Language :: Python :: 3.14",
|
|
25
25
|
]
|
|
26
|
-
license =
|
|
26
|
+
license = "MIT"
|
|
27
27
|
requires-python = ">=3.10"
|
|
28
28
|
keywords = ["snap7", "s7", "siemens", "plc"]
|
|
29
29
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-snap7
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.2
|
|
4
4
|
Summary: Pure Python S7 communication library for Siemens PLCs
|
|
5
5
|
Author-email: Gijs Molenaar <gijsmolenaar@gmail.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/gijzelaerr/python-snap7
|
|
8
8
|
Project-URL: Documentation, https://python-snap7.readthedocs.io/en/latest/
|
|
9
9
|
Keywords: snap7,s7,siemens,plc
|
|
@@ -239,9 +239,17 @@ class AsyncISOTCPConnection:
|
|
|
239
239
|
param_data = data[offset + 2 : offset + 2 + param_len]
|
|
240
240
|
if param_code == self.COTP_PARAM_PDU_SIZE:
|
|
241
241
|
if param_len == 1:
|
|
242
|
-
|
|
242
|
+
exponent = param_data[0]
|
|
243
|
+
if 7 <= exponent <= 13:
|
|
244
|
+
self.pdu_size = 1 << exponent
|
|
245
|
+
else:
|
|
246
|
+
logger.warning(f"Invalid COTP PDU size exponent {exponent}, using default")
|
|
243
247
|
elif param_len == 2:
|
|
244
|
-
|
|
248
|
+
raw = struct.unpack(">H", param_data)[0]
|
|
249
|
+
if 128 <= raw <= 8192:
|
|
250
|
+
self.pdu_size = raw
|
|
251
|
+
else:
|
|
252
|
+
logger.warning(f"Invalid COTP PDU size {raw}, using default")
|
|
245
253
|
logger.debug(f"Negotiated PDU size: {self.pdu_size}")
|
|
246
254
|
offset += 2 + param_len
|
|
247
255
|
|
|
@@ -389,7 +397,7 @@ class AsyncClient(ClientMixin):
|
|
|
389
397
|
self.slot = slot
|
|
390
398
|
self._params[Parameter.RemotePort] = tcp_port
|
|
391
399
|
|
|
392
|
-
self.remote_tsap =
|
|
400
|
+
self.remote_tsap = (self.connection_type << 8) | (rack << 5) | slot
|
|
393
401
|
|
|
394
402
|
try:
|
|
395
403
|
start_time = time.time()
|
|
@@ -1159,7 +1167,11 @@ class AsyncClient(ClientMixin):
|
|
|
1159
1167
|
if response.get("parameters"):
|
|
1160
1168
|
params = response["parameters"]
|
|
1161
1169
|
if "pdu_length" in params:
|
|
1162
|
-
|
|
1170
|
+
negotiated = params["pdu_length"]
|
|
1171
|
+
if negotiated < 64:
|
|
1172
|
+
logger.warning(f"Server negotiated implausible PDU length {negotiated}, using minimum 240")
|
|
1173
|
+
negotiated = 240
|
|
1174
|
+
self.pdu_length = negotiated
|
|
1163
1175
|
self._params[Parameter.PDURequest] = self.pdu_length
|
|
1164
1176
|
logger.info(f"Negotiated PDU length: {self.pdu_length}")
|
|
1165
1177
|
|
|
@@ -52,7 +52,7 @@ _VALID_AREA_VALUES: frozenset[int] = frozenset(a.value for a in Area)
|
|
|
52
52
|
logger = logging.getLogger(__name__)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def _decode_tag(tag: Tag, data: bytearray) -> Any:
|
|
55
|
+
def _decode_tag(tag: Tag, data: bytearray, encoding: str = "latin-1") -> Any:
|
|
56
56
|
"""Decode a Tag's raw bytes into a typed Python value."""
|
|
57
57
|
upper = tag.datatype.upper()
|
|
58
58
|
# Variable-length string types
|
|
@@ -60,9 +60,9 @@ def _decode_tag(tag: Tag, data: bytearray) -> Any:
|
|
|
60
60
|
if match:
|
|
61
61
|
kind, length = match.group(1), int(match.group(2))
|
|
62
62
|
if kind == "FSTRING":
|
|
63
|
-
return util.get_fstring(data, 0, length)
|
|
63
|
+
return util.get_fstring(data, 0, length, encoding=encoding)
|
|
64
64
|
if kind == "STRING":
|
|
65
|
-
return util.get_string(data, 0)
|
|
65
|
+
return util.get_string(data, 0, encoding=encoding)
|
|
66
66
|
if kind == "WSTRING":
|
|
67
67
|
return util.get_wstring(data, 0)
|
|
68
68
|
|
|
@@ -129,17 +129,17 @@ def _decode_scalar(datatype: str, data: bytearray, bit: int) -> Any:
|
|
|
129
129
|
raise ValueError(f"Unsupported tag datatype: {datatype}")
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
def _encode_tag(tag: Tag, buf: bytearray, value: Any) -> None:
|
|
132
|
+
def _encode_tag(tag: Tag, buf: bytearray, value: Any, encoding: str = "latin-1") -> None:
|
|
133
133
|
"""Encode a typed Python value into a Tag's byte buffer."""
|
|
134
134
|
upper = tag.datatype.upper()
|
|
135
135
|
match = _STRING_RE.match(upper)
|
|
136
136
|
if match:
|
|
137
137
|
kind, length = match.group(1), int(match.group(2))
|
|
138
138
|
if kind == "FSTRING":
|
|
139
|
-
util.set_fstring(buf, 0, value, length)
|
|
139
|
+
util.set_fstring(buf, 0, value, length, encoding=encoding)
|
|
140
140
|
return
|
|
141
141
|
if kind == "STRING":
|
|
142
|
-
util.set_string(buf, 0, value, length)
|
|
142
|
+
util.set_string(buf, 0, value, length, encoding=encoding)
|
|
143
143
|
return
|
|
144
144
|
if kind == "WSTRING":
|
|
145
145
|
util.set_wstring(buf, 0, value, length)
|
|
@@ -656,7 +656,7 @@ class Client(ClientMixin):
|
|
|
656
656
|
self._params[Parameter.RemotePort] = port
|
|
657
657
|
|
|
658
658
|
# Remote TSAP targets the gateway rack/slot
|
|
659
|
-
self.remote_tsap =
|
|
659
|
+
self.remote_tsap = (self.connection_type << 8) | (router_rack << 5) | router_slot
|
|
660
660
|
|
|
661
661
|
try:
|
|
662
662
|
start_time = time.time()
|
|
@@ -780,7 +780,7 @@ class Client(ClientMixin):
|
|
|
780
780
|
struct.pack_into(fmt, data, i * item_size, v)
|
|
781
781
|
return self.db_write(db_number, start, data)
|
|
782
782
|
|
|
783
|
-
def read_tag(self, tag: "Union[Tag, str]") -> Any:
|
|
783
|
+
def read_tag(self, tag: "Union[Tag, str]", encoding: str = "latin-1") -> Any:
|
|
784
784
|
"""Read a typed value by :class:`~snap7.tags.Tag` or address string.
|
|
785
785
|
|
|
786
786
|
Accepts a :class:`~snap7.tags.Tag` or a PLC4X-style address string
|
|
@@ -788,6 +788,8 @@ class Client(ClientMixin):
|
|
|
788
788
|
|
|
789
789
|
Args:
|
|
790
790
|
tag: A :class:`~snap7.tags.Tag` instance or a parseable address string.
|
|
791
|
+
encoding: Character encoding for STRING/FSTRING values (default ``"latin-1"``).
|
|
792
|
+
Use ``"gbk"`` for Chinese PLCs or ``"utf-8"`` for UTF-8 encoded strings.
|
|
791
793
|
|
|
792
794
|
Returns:
|
|
793
795
|
The typed value (bool/int/float/datetime/str depending on type).
|
|
@@ -798,6 +800,7 @@ class Client(ClientMixin):
|
|
|
798
800
|
client.read_tag("DB1.DBD4:REAL") # float
|
|
799
801
|
client.read_tag("DB1:20:STRING[30]") # variable-length string
|
|
800
802
|
client.read_tag(Tag(Area.DB, 1, 0, "REAL")) # from Tag instance
|
|
803
|
+
client.read_tag("DB1:34:STRING[10]", encoding="gbk") # Chinese string
|
|
801
804
|
"""
|
|
802
805
|
resolved = Tag.from_string(tag) if isinstance(tag, str) else tag
|
|
803
806
|
if resolved.is_symbolic:
|
|
@@ -805,14 +808,15 @@ class Client(ClientMixin):
|
|
|
805
808
|
"Symbolic (LID-based) tag access requires S7CommPlus. Use s7.Client instead of snap7.Client."
|
|
806
809
|
)
|
|
807
810
|
data = self.read_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, resolved.size)
|
|
808
|
-
return _decode_tag(resolved, bytearray(data))
|
|
811
|
+
return _decode_tag(resolved, bytearray(data), encoding=encoding)
|
|
809
812
|
|
|
810
|
-
def write_tag(self, tag: "Union[Tag, str]", value: Any) -> int:
|
|
813
|
+
def write_tag(self, tag: "Union[Tag, str]", value: Any, encoding: str = "latin-1") -> int:
|
|
811
814
|
"""Write a typed value by :class:`~snap7.tags.Tag` or address string.
|
|
812
815
|
|
|
813
816
|
Args:
|
|
814
817
|
tag: A :class:`~snap7.tags.Tag` instance or a parseable address string.
|
|
815
818
|
value: The value to write (type must match the tag's datatype).
|
|
819
|
+
encoding: Character encoding for STRING/FSTRING values (default ``"latin-1"``).
|
|
816
820
|
|
|
817
821
|
Returns:
|
|
818
822
|
0 on success.
|
|
@@ -828,10 +832,10 @@ class Client(ClientMixin):
|
|
|
828
832
|
if resolved.datatype.upper() == "BOOL":
|
|
829
833
|
current = self.read_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, 1)
|
|
830
834
|
buf[0] = current[0]
|
|
831
|
-
_encode_tag(resolved, buf, value)
|
|
835
|
+
_encode_tag(resolved, buf, value, encoding=encoding)
|
|
832
836
|
return self.write_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, buf)
|
|
833
837
|
|
|
834
|
-
def read_tags(self, tags: "list[Union[Tag, str]]") -> list[Any]:
|
|
838
|
+
def read_tags(self, tags: "list[Union[Tag, str]]", encoding: str = "latin-1") -> list[Any]:
|
|
835
839
|
"""Read multiple tags in a single optimized request.
|
|
836
840
|
|
|
837
841
|
Uses the multi-variable read optimizer when available to batch
|
|
@@ -839,6 +843,7 @@ class Client(ClientMixin):
|
|
|
839
843
|
|
|
840
844
|
Args:
|
|
841
845
|
tags: List of :class:`~snap7.tags.Tag` instances or address strings.
|
|
846
|
+
encoding: Character encoding for STRING/FSTRING values (default ``"latin-1"``).
|
|
842
847
|
|
|
843
848
|
Returns:
|
|
844
849
|
List of decoded values in the same order as input.
|
|
@@ -846,7 +851,7 @@ class Client(ClientMixin):
|
|
|
846
851
|
resolved = [Tag.from_string(t) if isinstance(t, str) else t for t in tags]
|
|
847
852
|
items = [{"area": Area(t.area), "db_number": t.db_number, "start": t.byte_offset, "size": t.size} for t in resolved]
|
|
848
853
|
_code, data_list = self.read_multi_vars(items)
|
|
849
|
-
return [_decode_tag(t, bytearray(d)) for t, d in zip(resolved, data_list)]
|
|
854
|
+
return [_decode_tag(t, bytearray(d), encoding=encoding) for t, d in zip(resolved, data_list)]
|
|
850
855
|
|
|
851
856
|
def db_read(self, db_number: int, start: int, size: int) -> bytearray:
|
|
852
857
|
"""
|
|
@@ -1289,8 +1294,13 @@ class Client(ClientMixin):
|
|
|
1289
1294
|
|
|
1290
1295
|
def _execute_packets_sequential(self, packet_requests: list[Tuple[int, bytes, ReadPacket]]) -> None:
|
|
1291
1296
|
"""Execute multi-block packets one at a time."""
|
|
1292
|
-
for _,
|
|
1293
|
-
|
|
1297
|
+
for _, _request, packet in packet_requests:
|
|
1298
|
+
block_specs = [(blk.area, blk.db_number, blk.start_offset, blk.byte_length) for blk in packet.blocks]
|
|
1299
|
+
|
|
1300
|
+
def build_request(specs: list[tuple[int, int, int, int]] = block_specs) -> bytes:
|
|
1301
|
+
return self.protocol.build_multi_read_request(specs)
|
|
1302
|
+
|
|
1303
|
+
response = self._send_receive_with_reconnect(build_request)
|
|
1294
1304
|
block_data_list = self.protocol.extract_multi_read_data(response, len(packet.blocks))
|
|
1295
1305
|
for blk, buf in zip(packet.blocks, block_data_list):
|
|
1296
1306
|
blk.buffer = buf
|
|
@@ -1299,9 +1309,20 @@ class Client(ClientMixin):
|
|
|
1299
1309
|
"""Execute multi-block packets using parallel dispatch.
|
|
1300
1310
|
|
|
1301
1311
|
Sends up to *max_parallel* PDUs back-to-back before reading
|
|
1302
|
-
responses, reducing round-trip overhead.
|
|
1312
|
+
responses, reducing round-trip overhead. Falls back to
|
|
1313
|
+
sequential reconnect-aware execution on connection loss.
|
|
1303
1314
|
"""
|
|
1304
|
-
|
|
1315
|
+
try:
|
|
1316
|
+
self._execute_packets_parallel_inner(packet_requests)
|
|
1317
|
+
except (S7ConnectionError, OSError) as e:
|
|
1318
|
+
if not self._auto_reconnect:
|
|
1319
|
+
raise
|
|
1320
|
+
logger.warning(f"Connection lost during parallel read: {e}")
|
|
1321
|
+
self._do_reconnect()
|
|
1322
|
+
self._execute_packets_sequential(packet_requests)
|
|
1323
|
+
|
|
1324
|
+
def _execute_packets_parallel_inner(self, packet_requests: list[Tuple[int, bytes, ReadPacket]]) -> None:
|
|
1325
|
+
"""Inner parallel dispatch without reconnect handling."""
|
|
1305
1326
|
for chunk_start in range(0, len(packet_requests), self.max_parallel):
|
|
1306
1327
|
chunk = packet_requests[chunk_start : chunk_start + self.max_parallel]
|
|
1307
1328
|
requests = [(pkt_idx, pdu) for pkt_idx, pdu, _ in chunk]
|
|
@@ -2641,7 +2662,11 @@ class Client(ClientMixin):
|
|
|
2641
2662
|
if response.get("parameters"):
|
|
2642
2663
|
params = response["parameters"]
|
|
2643
2664
|
if "pdu_length" in params:
|
|
2644
|
-
|
|
2665
|
+
negotiated = params["pdu_length"]
|
|
2666
|
+
if negotiated < 64:
|
|
2667
|
+
logger.warning(f"Server negotiated implausible PDU length {negotiated}, using minimum 240")
|
|
2668
|
+
negotiated = 240
|
|
2669
|
+
self.pdu_length = negotiated
|
|
2645
2670
|
self._params[Parameter.PDURequest] = self.pdu_length
|
|
2646
2671
|
logger.info(f"Negotiated PDU length: {self.pdu_length}")
|
|
2647
2672
|
|
|
@@ -224,16 +224,22 @@ class ClientMixin:
|
|
|
224
224
|
|
|
225
225
|
Calculated as PDU length minus overhead:
|
|
226
226
|
12 bytes S7 header + 2 bytes param + 4 bytes data header = 18 bytes.
|
|
227
|
+
|
|
228
|
+
Returns at least 1 to prevent infinite loops in chunked reads
|
|
229
|
+
when the server negotiates an implausibly small PDU.
|
|
227
230
|
"""
|
|
228
|
-
return self.pdu_length - 18
|
|
231
|
+
return max(1, self.pdu_length - 18)
|
|
229
232
|
|
|
230
233
|
def _max_write_size(self) -> int:
|
|
231
234
|
"""Maximum payload bytes for a single write request.
|
|
232
235
|
|
|
233
236
|
Calculated as PDU length minus overhead:
|
|
234
237
|
12 bytes S7 header + 14 bytes param + 4 bytes data header + 5 bytes padding = 35 bytes.
|
|
238
|
+
|
|
239
|
+
Returns at least 1 to prevent infinite loops in chunked writes
|
|
240
|
+
when the server negotiates an implausibly small PDU.
|
|
235
241
|
"""
|
|
236
|
-
return self.pdu_length - 35
|
|
242
|
+
return max(1, self.pdu_length - 35)
|
|
237
243
|
|
|
238
244
|
def _map_area(self, area: Area) -> S7Area:
|
|
239
245
|
"""Map library area enum to native S7 area."""
|
|
@@ -371,11 +371,19 @@ class ISOTCPConnection:
|
|
|
371
371
|
if param_code == self.COTP_PARAM_PDU_SIZE:
|
|
372
372
|
# PDU Size parameter
|
|
373
373
|
if param_len == 1:
|
|
374
|
-
# ISO 8073 code: size = 2^code
|
|
375
|
-
|
|
374
|
+
# ISO 8073 code: size = 2^code, valid range 7-13
|
|
375
|
+
exponent = param_data[0]
|
|
376
|
+
if 7 <= exponent <= 13:
|
|
377
|
+
self.pdu_size = 1 << exponent
|
|
378
|
+
else:
|
|
379
|
+
logger.warning(f"Invalid COTP PDU size exponent {exponent}, using default")
|
|
376
380
|
elif param_len == 2:
|
|
377
381
|
# Raw 2-byte value
|
|
378
|
-
|
|
382
|
+
raw = struct.unpack(">H", param_data)[0]
|
|
383
|
+
if 128 <= raw <= 8192:
|
|
384
|
+
self.pdu_size = raw
|
|
385
|
+
else:
|
|
386
|
+
logger.warning(f"Invalid COTP PDU size {raw}, using default")
|
|
379
387
|
logger.debug(f"Negotiated PDU size: {self.pdu_size}")
|
|
380
388
|
else:
|
|
381
389
|
logger.debug(f"Unsupported COTP parameter: code={param_code:#04x}, length={param_len}")
|