python-snap7 3.1.2__py3-none-any.whl → 3.2.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.0.dist-info/METADATA +103 -0
- python_snap7-3.2.0.dist-info/RECORD +34 -0
- {python_snap7-3.1.2.dist-info → python_snap7-3.2.0.dist-info}/top_level.txt +1 -0
- s7/__init__.py +52 -0
- snap7/__init__.py +15 -4
- snap7/async_client.py +159 -19
- snap7/cli.py +2 -3
- snap7/client.py +244 -43
- snap7/client_base.py +27 -24
- snap7/connection.py +21 -10
- snap7/datatypes.py +24 -3
- snap7/error.py +6 -0
- snap7/logo.py +48 -6
- snap7/ppi.py +421 -0
- snap7/rate_limiter.py +110 -0
- snap7/s7protocol.py +200 -16
- snap7/server/__init__.py +242 -90
- snap7/szl.py +106 -9
- snap7/tags.py +6 -6
- snap7/type.py +14 -1
- snap7/util/getters.py +21 -4
- snap7/util/setters.py +4 -1
- python_snap7-3.1.2.dist-info/METADATA +0 -131
- python_snap7-3.1.2.dist-info/RECORD +0 -32
- snap7/util/symbols.py +0 -547
- {python_snap7-3.1.2.dist-info → python_snap7-3.2.0.dist-info}/WHEEL +0 -0
- {python_snap7-3.1.2.dist-info → python_snap7-3.2.0.dist-info}/entry_points.txt +0 -0
- {python_snap7-3.1.2.dist-info → python_snap7-3.2.0.dist-info}/licenses/LICENSE +0 -0
snap7/util/symbols.py
DELETED
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Symbolic addressing for S7 PLC data blocks.
|
|
3
|
-
|
|
4
|
-
Provides a SymbolTable class that maps human-readable tag names to PLC
|
|
5
|
-
addresses (db_number, byte_offset, data_type), enabling read/write operations
|
|
6
|
-
by tag name instead of raw addresses.
|
|
7
|
-
|
|
8
|
-
Example::
|
|
9
|
-
|
|
10
|
-
from snap7.util.symbols import SymbolTable
|
|
11
|
-
|
|
12
|
-
symbols = SymbolTable.from_csv("tags.csv")
|
|
13
|
-
value = symbols.read(client, "Motor1.Speed")
|
|
14
|
-
symbols.write(client, "Motor1.Speed", 1500.0)
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import csv
|
|
18
|
-
import io
|
|
19
|
-
import json
|
|
20
|
-
import re
|
|
21
|
-
from dataclasses import dataclass
|
|
22
|
-
from logging import getLogger
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from typing import Any, Dict, Union
|
|
25
|
-
|
|
26
|
-
from snap7.type import ValueType
|
|
27
|
-
from snap7.util import (
|
|
28
|
-
get_bool,
|
|
29
|
-
get_byte,
|
|
30
|
-
get_char,
|
|
31
|
-
get_dint,
|
|
32
|
-
get_dword,
|
|
33
|
-
get_dt,
|
|
34
|
-
get_dtl,
|
|
35
|
-
get_int,
|
|
36
|
-
get_lreal,
|
|
37
|
-
get_lint,
|
|
38
|
-
get_ulint,
|
|
39
|
-
get_ltime,
|
|
40
|
-
get_ltod,
|
|
41
|
-
get_ldt,
|
|
42
|
-
get_real,
|
|
43
|
-
get_sint,
|
|
44
|
-
get_string,
|
|
45
|
-
get_tod,
|
|
46
|
-
get_udint,
|
|
47
|
-
get_uint,
|
|
48
|
-
get_usint,
|
|
49
|
-
get_wchar,
|
|
50
|
-
get_word,
|
|
51
|
-
get_wstring,
|
|
52
|
-
get_date,
|
|
53
|
-
get_time,
|
|
54
|
-
get_lword,
|
|
55
|
-
set_bool,
|
|
56
|
-
set_byte,
|
|
57
|
-
set_char,
|
|
58
|
-
set_date,
|
|
59
|
-
set_dint,
|
|
60
|
-
set_dword,
|
|
61
|
-
set_dt,
|
|
62
|
-
set_dtl,
|
|
63
|
-
set_int,
|
|
64
|
-
set_lint,
|
|
65
|
-
set_ulint,
|
|
66
|
-
set_ltime,
|
|
67
|
-
set_ltod,
|
|
68
|
-
set_ldt,
|
|
69
|
-
set_lreal,
|
|
70
|
-
set_real,
|
|
71
|
-
set_sint,
|
|
72
|
-
set_string,
|
|
73
|
-
set_tod,
|
|
74
|
-
set_udint,
|
|
75
|
-
set_uint,
|
|
76
|
-
set_usint,
|
|
77
|
-
set_wchar,
|
|
78
|
-
set_word,
|
|
79
|
-
set_wstring,
|
|
80
|
-
set_time,
|
|
81
|
-
set_lword,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
logger = getLogger(__name__)
|
|
85
|
-
|
|
86
|
-
# ---------------------------------------------------------------------------
|
|
87
|
-
# Module-level getter/setter dispatch maps (built once, not per call)
|
|
88
|
-
# ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
_GETTER_MAP: Dict[str, Any] = {
|
|
91
|
-
"BYTE": get_byte,
|
|
92
|
-
"SINT": get_sint,
|
|
93
|
-
"USINT": get_usint,
|
|
94
|
-
"CHAR": get_char,
|
|
95
|
-
"INT": get_int,
|
|
96
|
-
"UINT": get_uint,
|
|
97
|
-
# NOTE: get_word is annotated as returning bytearray but actually returns
|
|
98
|
-
# int at runtime (struct.unpack(">H", ...) -> int). It behaves correctly.
|
|
99
|
-
"WORD": get_word,
|
|
100
|
-
"DATE": get_date,
|
|
101
|
-
"DINT": get_dint,
|
|
102
|
-
"UDINT": get_udint,
|
|
103
|
-
"DWORD": get_dword,
|
|
104
|
-
"REAL": get_real,
|
|
105
|
-
"TIME": get_time,
|
|
106
|
-
"TOD": get_tod,
|
|
107
|
-
"TIME_OF_DAY": get_tod,
|
|
108
|
-
"DATE_AND_TIME": get_dt,
|
|
109
|
-
"DT": get_dt,
|
|
110
|
-
"LREAL": get_lreal,
|
|
111
|
-
"LWORD": get_lword,
|
|
112
|
-
"WCHAR": get_wchar,
|
|
113
|
-
"DTL": get_dtl,
|
|
114
|
-
"LINT": get_lint,
|
|
115
|
-
"ULINT": get_ulint,
|
|
116
|
-
"LTIME": get_ltime,
|
|
117
|
-
"LTOD": get_ltod,
|
|
118
|
-
"LDT": get_ldt,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
# Setters that cast value to int before calling
|
|
122
|
-
_INT_SETTER_MAP: Dict[str, Any] = {
|
|
123
|
-
"BYTE": set_byte,
|
|
124
|
-
"SINT": set_sint,
|
|
125
|
-
"USINT": set_usint,
|
|
126
|
-
"INT": set_int,
|
|
127
|
-
"UINT": set_uint,
|
|
128
|
-
"WORD": set_word,
|
|
129
|
-
"DINT": set_dint,
|
|
130
|
-
"UDINT": set_udint,
|
|
131
|
-
"DWORD": set_dword,
|
|
132
|
-
"LINT": set_lint,
|
|
133
|
-
"ULINT": set_ulint,
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
# Setters that pass value through without casting
|
|
137
|
-
_SIMPLE_SETTER_MAP: Dict[str, Any] = {
|
|
138
|
-
"REAL": set_real,
|
|
139
|
-
"LREAL": set_lreal,
|
|
140
|
-
"CHAR": set_char,
|
|
141
|
-
"WCHAR": set_wchar,
|
|
142
|
-
"TIME": set_time,
|
|
143
|
-
"DATE": set_date,
|
|
144
|
-
"TOD": set_tod,
|
|
145
|
-
"TIME_OF_DAY": set_tod,
|
|
146
|
-
"DATE_AND_TIME": set_dt,
|
|
147
|
-
"DT": set_dt,
|
|
148
|
-
"DTL": set_dtl,
|
|
149
|
-
"LWORD": set_lword,
|
|
150
|
-
"LTIME": set_ltime,
|
|
151
|
-
"LTOD": set_ltod,
|
|
152
|
-
"LDT": set_ldt,
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
# Mapping from S7 type name to the number of bytes needed to read
|
|
156
|
-
_TYPE_SIZE: Dict[str, int] = {
|
|
157
|
-
"BOOL": 1,
|
|
158
|
-
"BYTE": 1,
|
|
159
|
-
"SINT": 1,
|
|
160
|
-
"USINT": 1,
|
|
161
|
-
"CHAR": 1,
|
|
162
|
-
"INT": 2,
|
|
163
|
-
"UINT": 2,
|
|
164
|
-
"WORD": 2,
|
|
165
|
-
"DATE": 2,
|
|
166
|
-
"DINT": 4,
|
|
167
|
-
"UDINT": 4,
|
|
168
|
-
"DWORD": 4,
|
|
169
|
-
"REAL": 4,
|
|
170
|
-
"TIME": 4,
|
|
171
|
-
"TOD": 4,
|
|
172
|
-
"TIME_OF_DAY": 4,
|
|
173
|
-
"DATE_AND_TIME": 8,
|
|
174
|
-
"DT": 8,
|
|
175
|
-
"LREAL": 8,
|
|
176
|
-
"LWORD": 8,
|
|
177
|
-
"WCHAR": 2,
|
|
178
|
-
"DTL": 12,
|
|
179
|
-
"LINT": 8,
|
|
180
|
-
"ULINT": 8,
|
|
181
|
-
"LTIME": 8,
|
|
182
|
-
"LTOD": 8,
|
|
183
|
-
"LDT": 8,
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
# Regex to extract STRING[n] or WSTRING[n] with size parameter
|
|
187
|
-
_STRING_RE = re.compile(r"^(STRING|WSTRING|FSTRING)\[(\d+)]$", re.IGNORECASE)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def _read_source(source: Union[str, Path]) -> str:
|
|
191
|
-
"""Resolve *source* to text content.
|
|
192
|
-
|
|
193
|
-
If *source* is a :class:`~pathlib.Path` it is always read as a file.
|
|
194
|
-
If it is a string that contains a newline character it is treated as
|
|
195
|
-
inline content (CSV / JSON). Otherwise the string is checked as a
|
|
196
|
-
file path and read if it exists; if not it is returned verbatim.
|
|
197
|
-
"""
|
|
198
|
-
if isinstance(source, Path):
|
|
199
|
-
return source.read_text()
|
|
200
|
-
s = str(source)
|
|
201
|
-
if "\n" in s:
|
|
202
|
-
return s
|
|
203
|
-
path = Path(s)
|
|
204
|
-
if path.exists():
|
|
205
|
-
return path.read_text()
|
|
206
|
-
return s
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@dataclass(frozen=True)
|
|
210
|
-
class TagAddress:
|
|
211
|
-
"""Resolved address for a single PLC tag."""
|
|
212
|
-
|
|
213
|
-
db: int
|
|
214
|
-
offset: int
|
|
215
|
-
bit: int
|
|
216
|
-
type: str
|
|
217
|
-
|
|
218
|
-
@property
|
|
219
|
-
def byte_offset(self) -> int:
|
|
220
|
-
"""Return the byte offset (without bit component)."""
|
|
221
|
-
return self.offset
|
|
222
|
-
|
|
223
|
-
def read_size(self) -> int:
|
|
224
|
-
"""Return the number of bytes that need to be read from the PLC for this tag."""
|
|
225
|
-
upper = self.type.upper()
|
|
226
|
-
match = _STRING_RE.match(upper)
|
|
227
|
-
if match:
|
|
228
|
-
kind = match.group(1)
|
|
229
|
-
length = int(match.group(2))
|
|
230
|
-
if kind == "FSTRING":
|
|
231
|
-
return length
|
|
232
|
-
elif kind == "STRING":
|
|
233
|
-
# S7 STRING: 2-byte header + max_length characters
|
|
234
|
-
return 2 + length
|
|
235
|
-
elif kind == "WSTRING":
|
|
236
|
-
# S7 WSTRING: 4-byte header + max_length * 2 bytes
|
|
237
|
-
return 4 + length * 2
|
|
238
|
-
if upper in _TYPE_SIZE:
|
|
239
|
-
return _TYPE_SIZE[upper]
|
|
240
|
-
raise ValueError(f"Unknown S7 type: {self.type}")
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def _parse_offset(offset_str: str) -> tuple[int, int]:
|
|
244
|
-
"""Parse an offset string like '4' or '4.0' into (byte_offset, bit_index).
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
offset_str: offset value, e.g. '4', '4.0', '12.3'
|
|
248
|
-
|
|
249
|
-
Returns:
|
|
250
|
-
Tuple of (byte_offset, bit_index).
|
|
251
|
-
"""
|
|
252
|
-
if "." in str(offset_str):
|
|
253
|
-
parts = str(offset_str).split(".")
|
|
254
|
-
return int(parts[0]), int(parts[1])
|
|
255
|
-
return int(float(offset_str)), 0
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
class SymbolTable:
|
|
259
|
-
"""Map symbolic tag names to PLC addresses and perform typed reads/writes.
|
|
260
|
-
|
|
261
|
-
Supports construction from:
|
|
262
|
-
- A Python dict mapping tag names to address dicts
|
|
263
|
-
- A CSV file or string (via :meth:`from_csv`)
|
|
264
|
-
- A JSON file or string (via :meth:`from_json`)
|
|
265
|
-
|
|
266
|
-
Tag names support dot-separated nested paths (e.g. ``"Motor1.Speed"``)
|
|
267
|
-
and array indexing (e.g. ``"Motors[3].Speed"``).
|
|
268
|
-
|
|
269
|
-
Example::
|
|
270
|
-
|
|
271
|
-
symbols = SymbolTable({
|
|
272
|
-
"Motor1.Speed": {"db": 1, "offset": 0, "type": "REAL"},
|
|
273
|
-
"Motor1.Running": {"db": 1, "offset": 4, "bit": 0, "type": "BOOL"},
|
|
274
|
-
})
|
|
275
|
-
value = symbols.read(client, "Motor1.Speed")
|
|
276
|
-
symbols.write(client, "Motor1.Speed", 1500.0)
|
|
277
|
-
"""
|
|
278
|
-
|
|
279
|
-
def __init__(self, tags: Dict[str, Dict[str, Any]]) -> None:
|
|
280
|
-
self._tags: Dict[str, TagAddress] = {}
|
|
281
|
-
for name, info in tags.items():
|
|
282
|
-
self._add_tag(name, info)
|
|
283
|
-
|
|
284
|
-
# ------------------------------------------------------------------
|
|
285
|
-
# Construction helpers
|
|
286
|
-
# ------------------------------------------------------------------
|
|
287
|
-
|
|
288
|
-
def _add_tag(self, name: str, info: Dict[str, Any]) -> None:
|
|
289
|
-
db = int(info["db"])
|
|
290
|
-
offset_raw = info.get("offset", 0)
|
|
291
|
-
byte_offset, default_bit = _parse_offset(str(offset_raw))
|
|
292
|
-
bit = int(info.get("bit", default_bit))
|
|
293
|
-
type_ = str(info["type"])
|
|
294
|
-
self._tags[name] = TagAddress(db=db, offset=byte_offset, bit=bit, type=type_)
|
|
295
|
-
|
|
296
|
-
@classmethod
|
|
297
|
-
def from_csv(cls, source: Union[str, Path]) -> "SymbolTable":
|
|
298
|
-
"""Create a SymbolTable from a CSV file or CSV string.
|
|
299
|
-
|
|
300
|
-
The CSV must have columns: ``tag``, ``db``, ``offset``, ``type``.
|
|
301
|
-
An optional ``bit`` column overrides the bit index parsed from the offset.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
source: path to a CSV file, or a CSV-formatted string. Strings
|
|
305
|
-
that contain newlines are always treated as inline CSV content.
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
A new :class:`SymbolTable`.
|
|
309
|
-
"""
|
|
310
|
-
text = _read_source(source)
|
|
311
|
-
|
|
312
|
-
reader = csv.DictReader(io.StringIO(text))
|
|
313
|
-
tags: Dict[str, Dict[str, Any]] = {}
|
|
314
|
-
for row in reader:
|
|
315
|
-
name = row["tag"].strip()
|
|
316
|
-
entry: Dict[str, Any] = {
|
|
317
|
-
"db": row["db"].strip(),
|
|
318
|
-
"offset": row["offset"].strip(),
|
|
319
|
-
"type": row["type"].strip(),
|
|
320
|
-
}
|
|
321
|
-
if "bit" in row and row["bit"] is not None and row["bit"].strip():
|
|
322
|
-
entry["bit"] = row["bit"].strip()
|
|
323
|
-
tags[name] = entry
|
|
324
|
-
return cls(tags)
|
|
325
|
-
|
|
326
|
-
@classmethod
|
|
327
|
-
def from_json(cls, source: Union[str, Path]) -> "SymbolTable":
|
|
328
|
-
"""Create a SymbolTable from a JSON file or JSON string.
|
|
329
|
-
|
|
330
|
-
The JSON should be an object mapping tag names to address objects,
|
|
331
|
-
each with keys ``db``, ``offset``, ``type``, and optionally ``bit``.
|
|
332
|
-
|
|
333
|
-
Args:
|
|
334
|
-
source: path to a JSON file, or a JSON-formatted string. Strings
|
|
335
|
-
that contain newlines are always treated as inline JSON content.
|
|
336
|
-
|
|
337
|
-
Returns:
|
|
338
|
-
A new :class:`SymbolTable`.
|
|
339
|
-
"""
|
|
340
|
-
text = _read_source(source)
|
|
341
|
-
|
|
342
|
-
data: Dict[str, Dict[str, Any]] = json.loads(text)
|
|
343
|
-
return cls(data)
|
|
344
|
-
|
|
345
|
-
# ------------------------------------------------------------------
|
|
346
|
-
# Lookup
|
|
347
|
-
# ------------------------------------------------------------------
|
|
348
|
-
|
|
349
|
-
def resolve(self, tag: str) -> TagAddress:
|
|
350
|
-
"""Resolve a tag name to its :class:`TagAddress`.
|
|
351
|
-
|
|
352
|
-
Args:
|
|
353
|
-
tag: the symbolic name (e.g. ``"Motor1.Speed"``).
|
|
354
|
-
|
|
355
|
-
Returns:
|
|
356
|
-
The resolved address.
|
|
357
|
-
|
|
358
|
-
Raises:
|
|
359
|
-
KeyError: if the tag is not defined in this table.
|
|
360
|
-
"""
|
|
361
|
-
if tag in self._tags:
|
|
362
|
-
return self._tags[tag]
|
|
363
|
-
raise KeyError(f"Unknown tag: {tag!r}")
|
|
364
|
-
|
|
365
|
-
@property
|
|
366
|
-
def tags(self) -> Dict[str, TagAddress]:
|
|
367
|
-
"""Return a copy of the internal tag mapping."""
|
|
368
|
-
return dict(self._tags)
|
|
369
|
-
|
|
370
|
-
def __len__(self) -> int:
|
|
371
|
-
return len(self._tags)
|
|
372
|
-
|
|
373
|
-
def __contains__(self, tag: str) -> bool:
|
|
374
|
-
return tag in self._tags
|
|
375
|
-
|
|
376
|
-
# ------------------------------------------------------------------
|
|
377
|
-
# Read / write
|
|
378
|
-
# ------------------------------------------------------------------
|
|
379
|
-
|
|
380
|
-
def read(self, client: Any, tag: str) -> ValueType:
|
|
381
|
-
"""Read a single tag value from the PLC.
|
|
382
|
-
|
|
383
|
-
Args:
|
|
384
|
-
client: a connected :class:`~snap7.client.Client`.
|
|
385
|
-
tag: symbolic tag name.
|
|
386
|
-
|
|
387
|
-
Returns:
|
|
388
|
-
The value, typed according to the tag's S7 data type.
|
|
389
|
-
"""
|
|
390
|
-
addr = self.resolve(tag)
|
|
391
|
-
size = addr.read_size()
|
|
392
|
-
data = client.db_read(addr.db, addr.byte_offset, size)
|
|
393
|
-
return self._get_value(data, 0, addr)
|
|
394
|
-
|
|
395
|
-
def write(self, client: Any, tag: str, value: Any) -> None:
|
|
396
|
-
"""Write a single tag value to the PLC.
|
|
397
|
-
|
|
398
|
-
Args:
|
|
399
|
-
client: a connected :class:`~snap7.client.Client`.
|
|
400
|
-
tag: symbolic tag name.
|
|
401
|
-
value: the value to write.
|
|
402
|
-
"""
|
|
403
|
-
addr = self.resolve(tag)
|
|
404
|
-
size = addr.read_size()
|
|
405
|
-
|
|
406
|
-
upper = addr.type.upper()
|
|
407
|
-
if upper == "BOOL":
|
|
408
|
-
# For BOOL we need to read-modify-write the byte
|
|
409
|
-
data = client.db_read(addr.db, addr.byte_offset, 1)
|
|
410
|
-
set_bool(data, 0, addr.bit, bool(value))
|
|
411
|
-
client.db_write(addr.db, addr.byte_offset, data)
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
# For non-BOOL types we can write directly
|
|
415
|
-
data = bytearray(size)
|
|
416
|
-
self._set_value(data, 0, addr, value)
|
|
417
|
-
client.db_write(addr.db, addr.byte_offset, data)
|
|
418
|
-
|
|
419
|
-
def read_many(self, client: Any, tags: list[str]) -> Dict[str, ValueType]:
|
|
420
|
-
"""Read multiple tags in batched requests.
|
|
421
|
-
|
|
422
|
-
Groups tags by DB number and uses :meth:`~snap7.client.Client.read_multi_vars`
|
|
423
|
-
to read them in minimal PDU exchanges (via the optimizer when enabled).
|
|
424
|
-
|
|
425
|
-
Args:
|
|
426
|
-
client: a connected :class:`~snap7.client.Client`.
|
|
427
|
-
tags: list of tag names to read.
|
|
428
|
-
|
|
429
|
-
Returns:
|
|
430
|
-
Dictionary mapping tag names to their values.
|
|
431
|
-
"""
|
|
432
|
-
if not tags:
|
|
433
|
-
return {}
|
|
434
|
-
|
|
435
|
-
# Resolve all tags
|
|
436
|
-
resolved: list[tuple[str, TagAddress]] = [(tag, self.resolve(tag)) for tag in tags]
|
|
437
|
-
|
|
438
|
-
# Build multi-read items grouped by what read_multi_vars expects
|
|
439
|
-
items: list[dict[str, Any]] = []
|
|
440
|
-
for _tag, addr in resolved:
|
|
441
|
-
items.append({"area": 0x84, "db_number": addr.db, "start": addr.offset, "size": addr.read_size()})
|
|
442
|
-
|
|
443
|
-
# Batch read via read_multi_vars (uses optimizer if enabled)
|
|
444
|
-
if len(items) == 1:
|
|
445
|
-
# Single tag, just read directly
|
|
446
|
-
return {tags[0]: self.read(client, tags[0])}
|
|
447
|
-
|
|
448
|
-
_code, data_list = client.read_multi_vars(items)
|
|
449
|
-
|
|
450
|
-
# Parse results
|
|
451
|
-
result: Dict[str, ValueType] = {}
|
|
452
|
-
for i, (tag, addr) in enumerate(resolved):
|
|
453
|
-
raw = data_list[i]
|
|
454
|
-
if isinstance(raw, (bytes, bytearray)):
|
|
455
|
-
result[tag] = self._get_value(bytearray(raw), 0, addr)
|
|
456
|
-
else:
|
|
457
|
-
result[tag] = self.read(client, tag)
|
|
458
|
-
|
|
459
|
-
return result
|
|
460
|
-
|
|
461
|
-
# ------------------------------------------------------------------
|
|
462
|
-
# Internal getter / setter dispatch
|
|
463
|
-
# ------------------------------------------------------------------
|
|
464
|
-
|
|
465
|
-
@staticmethod
|
|
466
|
-
def _get_value(data: bytearray, base_offset: int, addr: TagAddress) -> ValueType:
|
|
467
|
-
"""Extract a typed value from a bytearray at the given offset."""
|
|
468
|
-
upper = addr.type.upper()
|
|
469
|
-
offset = base_offset
|
|
470
|
-
|
|
471
|
-
if upper == "BOOL":
|
|
472
|
-
return get_bool(data, offset, addr.bit)
|
|
473
|
-
|
|
474
|
-
match = _STRING_RE.match(upper)
|
|
475
|
-
if match:
|
|
476
|
-
kind = match.group(1)
|
|
477
|
-
length = int(match.group(2))
|
|
478
|
-
if kind == "FSTRING":
|
|
479
|
-
from snap7.util import get_fstring
|
|
480
|
-
|
|
481
|
-
return get_fstring(data, offset, length)
|
|
482
|
-
elif kind == "STRING":
|
|
483
|
-
return get_string(data, offset)
|
|
484
|
-
elif kind == "WSTRING":
|
|
485
|
-
return get_wstring(data, offset)
|
|
486
|
-
|
|
487
|
-
if upper in _GETTER_MAP:
|
|
488
|
-
return _GETTER_MAP[upper](data, offset) # type: ignore[no-any-return]
|
|
489
|
-
|
|
490
|
-
raise ValueError(f"Unsupported S7 type for reading: {addr.type}")
|
|
491
|
-
|
|
492
|
-
@staticmethod
|
|
493
|
-
def _set_value(data: bytearray, base_offset: int, addr: TagAddress, value: Any) -> None:
|
|
494
|
-
"""Write a typed value into a bytearray at the given offset."""
|
|
495
|
-
upper = addr.type.upper()
|
|
496
|
-
offset = base_offset
|
|
497
|
-
|
|
498
|
-
if upper == "BOOL":
|
|
499
|
-
set_bool(data, offset, addr.bit, bool(value))
|
|
500
|
-
return
|
|
501
|
-
|
|
502
|
-
match = _STRING_RE.match(upper)
|
|
503
|
-
if match:
|
|
504
|
-
kind = match.group(1)
|
|
505
|
-
length = int(match.group(2))
|
|
506
|
-
if kind == "FSTRING":
|
|
507
|
-
from snap7.util import set_fstring
|
|
508
|
-
|
|
509
|
-
set_fstring(data, offset, str(value), length)
|
|
510
|
-
return
|
|
511
|
-
elif kind == "STRING":
|
|
512
|
-
set_string(data, offset, str(value), length)
|
|
513
|
-
return
|
|
514
|
-
elif kind == "WSTRING":
|
|
515
|
-
set_wstring(data, offset, str(value), length)
|
|
516
|
-
return
|
|
517
|
-
|
|
518
|
-
if upper in _INT_SETTER_MAP:
|
|
519
|
-
_INT_SETTER_MAP[upper](data, offset, int(value))
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
if upper in _SIMPLE_SETTER_MAP:
|
|
523
|
-
_SIMPLE_SETTER_MAP[upper](data, offset, value)
|
|
524
|
-
return
|
|
525
|
-
|
|
526
|
-
raise ValueError(f"Unsupported S7 type for writing: {addr.type}")
|
|
527
|
-
|
|
528
|
-
# ------------------------------------------------------------------
|
|
529
|
-
# Merge
|
|
530
|
-
# ------------------------------------------------------------------
|
|
531
|
-
|
|
532
|
-
def merge(self, other: "SymbolTable") -> "SymbolTable":
|
|
533
|
-
"""Return a new SymbolTable containing tags from both tables.
|
|
534
|
-
|
|
535
|
-
Args:
|
|
536
|
-
other: another :class:`SymbolTable` to merge with.
|
|
537
|
-
|
|
538
|
-
Returns:
|
|
539
|
-
A new merged :class:`SymbolTable`. Tags from *other* override
|
|
540
|
-
duplicates from *self*.
|
|
541
|
-
"""
|
|
542
|
-
combined: Dict[str, Dict[str, Any]] = {}
|
|
543
|
-
for name, addr in self._tags.items():
|
|
544
|
-
combined[name] = {"db": addr.db, "offset": addr.offset, "bit": addr.bit, "type": addr.type}
|
|
545
|
-
for name, addr in other._tags.items():
|
|
546
|
-
combined[name] = {"db": addr.db, "offset": addr.offset, "bit": addr.bit, "type": addr.type}
|
|
547
|
-
return SymbolTable(combined)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|