python-snap7 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_snap7-3.0.0.dist-info/METADATA +94 -0
- python_snap7-3.0.0.dist-info/RECORD +22 -0
- python_snap7-3.0.0.dist-info/WHEEL +5 -0
- python_snap7-3.0.0.dist-info/entry_points.txt +2 -0
- python_snap7-3.0.0.dist-info/licenses/LICENSE +20 -0
- python_snap7-3.0.0.dist-info/top_level.txt +1 -0
- snap7/__init__.py +34 -0
- snap7/client.py +1923 -0
- snap7/connection.py +456 -0
- snap7/datatypes.py +312 -0
- snap7/error.py +444 -0
- snap7/logo.py +222 -0
- snap7/partner.py +680 -0
- snap7/py.typed +0 -0
- snap7/s7protocol.py +1509 -0
- snap7/server/__init__.py +2681 -0
- snap7/server/__main__.py +41 -0
- snap7/type.py +391 -0
- snap7/util/__init__.py +108 -0
- snap7/util/db.py +789 -0
- snap7/util/getters.py +766 -0
- snap7/util/setters.py +733 -0
snap7/util/db.py
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains utility functions for working with PLC DB objects.
|
|
3
|
+
There are functions to work with the raw bytearray data snap7 functions return
|
|
4
|
+
In order to work with this data you need to make python able to work with the
|
|
5
|
+
PLC bytearray data.
|
|
6
|
+
|
|
7
|
+
For example code see test_util.py and example.py in the example folder.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
example::
|
|
11
|
+
|
|
12
|
+
spec/DB layout
|
|
13
|
+
|
|
14
|
+
# Byte index Variable name Datatype
|
|
15
|
+
layout=\"\"\"
|
|
16
|
+
4 ID INT
|
|
17
|
+
6 NAME STRING[6]
|
|
18
|
+
|
|
19
|
+
12.0 test_bool1 BOOL
|
|
20
|
+
12.1 test_bool2 BOOL
|
|
21
|
+
12.2 test_bool3 BOOL
|
|
22
|
+
12.3 test_bool4 BOOL
|
|
23
|
+
12.4 test_bool5 BOOL
|
|
24
|
+
12.5 test_bool6 BOOL
|
|
25
|
+
12.6 test_bool7 BOOL
|
|
26
|
+
12.7 test_bool8 BOOL
|
|
27
|
+
13 testReal REAL
|
|
28
|
+
17 testDword DWORD
|
|
29
|
+
\"\"\"
|
|
30
|
+
|
|
31
|
+
client = snap7.client.Client()
|
|
32
|
+
client.connect('192.168.200.24', 0, 3)
|
|
33
|
+
|
|
34
|
+
# this looks confusing but this means uploading from the PLC to YOU
|
|
35
|
+
# so downloading in the PC world :)
|
|
36
|
+
|
|
37
|
+
all_data = client.upload(db_number)
|
|
38
|
+
|
|
39
|
+
simple:
|
|
40
|
+
|
|
41
|
+
from snap7 import DB
|
|
42
|
+
db1 = DB(
|
|
43
|
+
db_number, # the db we use
|
|
44
|
+
all_data, # bytearray from the plc
|
|
45
|
+
layout, # layout specification DB variable data
|
|
46
|
+
# A DB specification is the specification of a
|
|
47
|
+
# DB object in the PLC you can find it using
|
|
48
|
+
# the dataview option on a DB object in PCS7
|
|
49
|
+
|
|
50
|
+
17+2, # size of the specification 17 is start
|
|
51
|
+
# of last value
|
|
52
|
+
# which is a DWORD which is 2 bytes,
|
|
53
|
+
|
|
54
|
+
1, # number of row's / specifications
|
|
55
|
+
|
|
56
|
+
id_field='ID', # field we can use to identify a row.
|
|
57
|
+
# default index is used
|
|
58
|
+
layout_offset=4, # sometimes specification does not start a 0
|
|
59
|
+
# like in our example
|
|
60
|
+
db_offset=0 # At which point in 'all_data' should we start
|
|
61
|
+
# reading. This could be that the specification
|
|
62
|
+
# does not start at 0
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
Now we can use db1 in python as a dict. if 'ID' contains
|
|
66
|
+
the 'test' we can identify the 'test' row in the all_data bytearray
|
|
67
|
+
|
|
68
|
+
To test of you layout matches the data from the plc you can
|
|
69
|
+
just print db1[0] or db['test'] in the example
|
|
70
|
+
|
|
71
|
+
db1['test']['test_bool1'] = 0
|
|
72
|
+
|
|
73
|
+
If we do not specify an id_field this should work to read out the
|
|
74
|
+
same data.
|
|
75
|
+
|
|
76
|
+
db1[0]['test_bool1']
|
|
77
|
+
|
|
78
|
+
to read and write a single Row from the plc. takes like 5ms!
|
|
79
|
+
|
|
80
|
+
db1['test'].write()
|
|
81
|
+
|
|
82
|
+
db1['test'].read(client)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
import re
|
|
88
|
+
from logging import getLogger
|
|
89
|
+
from datetime import datetime, date, timedelta
|
|
90
|
+
from typing import Any, Optional, Union, Iterator, Tuple, Dict, Callable
|
|
91
|
+
|
|
92
|
+
from snap7 import Client
|
|
93
|
+
from snap7.type import Area, ValueType
|
|
94
|
+
|
|
95
|
+
from snap7.util import (
|
|
96
|
+
set_bool,
|
|
97
|
+
set_fstring,
|
|
98
|
+
set_string,
|
|
99
|
+
set_wstring,
|
|
100
|
+
set_real,
|
|
101
|
+
set_dword,
|
|
102
|
+
set_udint,
|
|
103
|
+
set_dint,
|
|
104
|
+
set_uint,
|
|
105
|
+
set_int,
|
|
106
|
+
set_word,
|
|
107
|
+
set_byte,
|
|
108
|
+
set_char,
|
|
109
|
+
set_wchar,
|
|
110
|
+
set_usint,
|
|
111
|
+
set_sint,
|
|
112
|
+
set_time,
|
|
113
|
+
set_lreal,
|
|
114
|
+
set_date,
|
|
115
|
+
set_tod,
|
|
116
|
+
set_dtl,
|
|
117
|
+
set_dt,
|
|
118
|
+
get_bool,
|
|
119
|
+
get_fstring,
|
|
120
|
+
get_string,
|
|
121
|
+
get_wstring,
|
|
122
|
+
get_real,
|
|
123
|
+
get_dword,
|
|
124
|
+
get_udint,
|
|
125
|
+
get_dint,
|
|
126
|
+
get_uint,
|
|
127
|
+
get_int,
|
|
128
|
+
get_word,
|
|
129
|
+
get_byte,
|
|
130
|
+
get_s5time,
|
|
131
|
+
get_dt,
|
|
132
|
+
get_usint,
|
|
133
|
+
get_sint,
|
|
134
|
+
get_time,
|
|
135
|
+
get_date,
|
|
136
|
+
get_tod,
|
|
137
|
+
get_lreal,
|
|
138
|
+
get_char,
|
|
139
|
+
get_wchar,
|
|
140
|
+
get_dtl,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger = getLogger(__name__)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_specification(db_specification: str) -> Dict[str, Any]:
|
|
147
|
+
"""Create a db specification derived from a
|
|
148
|
+
dataview of a db in which the byte layout
|
|
149
|
+
is specified
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
db_specification: string formatted table with the indexes, aliases and types.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Parsed DB specification.
|
|
156
|
+
"""
|
|
157
|
+
parsed_db_specification = {}
|
|
158
|
+
pattern = r"""
|
|
159
|
+
(?P<index>\d+(\.\d+)?)\s+ # Match integer or decimal index
|
|
160
|
+
(?P<var_name>.*?)\s+ # Non-greedy match for variable name
|
|
161
|
+
(?P<_type>\S+)$ # Match type at end of line
|
|
162
|
+
"""
|
|
163
|
+
regex = re.compile(pattern, re.VERBOSE)
|
|
164
|
+
|
|
165
|
+
for line in db_specification.split("\n"):
|
|
166
|
+
if line and not line.lstrip().startswith("#"):
|
|
167
|
+
match = regex.match(line.strip())
|
|
168
|
+
if match:
|
|
169
|
+
index = match.group("index")
|
|
170
|
+
var_name = match.group("var_name")
|
|
171
|
+
_type = match.group("_type")
|
|
172
|
+
var_name = var_name.strip()
|
|
173
|
+
|
|
174
|
+
parsed_db_specification[var_name] = (index, _type)
|
|
175
|
+
|
|
176
|
+
return parsed_db_specification
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def print_row(data: bytearray) -> None:
|
|
180
|
+
"""print a single db row in chr and str"""
|
|
181
|
+
index_line = ""
|
|
182
|
+
pri_line1 = ""
|
|
183
|
+
chr_line2 = ""
|
|
184
|
+
matcher = re.compile("[a-zA-Z0-9 ]")
|
|
185
|
+
|
|
186
|
+
for i, xi in enumerate(data):
|
|
187
|
+
# index
|
|
188
|
+
if not i % 5:
|
|
189
|
+
diff = len(pri_line1) - len(index_line)
|
|
190
|
+
index_line += diff * " "
|
|
191
|
+
index_line += str(i)
|
|
192
|
+
# i = i + (ws - len(i)) * ' ' + ','
|
|
193
|
+
|
|
194
|
+
# byte array line
|
|
195
|
+
str_v = str(xi)
|
|
196
|
+
pri_line1 += str(xi) + ","
|
|
197
|
+
# char line
|
|
198
|
+
c = chr(xi)
|
|
199
|
+
c = c if matcher.match(c) else " "
|
|
200
|
+
# align white space
|
|
201
|
+
w = len(str_v)
|
|
202
|
+
c = c + (w - 1) * " " + ","
|
|
203
|
+
chr_line2 += c
|
|
204
|
+
|
|
205
|
+
logger.info(index_line)
|
|
206
|
+
logger.info(pri_line1)
|
|
207
|
+
logger.info(chr_line2)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class DB:
|
|
211
|
+
"""
|
|
212
|
+
Manage a DB bytearray block given a specification
|
|
213
|
+
of the Layout.
|
|
214
|
+
|
|
215
|
+
It is possible to have many repetitive instances of
|
|
216
|
+
a specification this is called a "row".
|
|
217
|
+
|
|
218
|
+
Probably most usecases there is just one row
|
|
219
|
+
|
|
220
|
+
Note:
|
|
221
|
+
This class has some of the semantics of a dict. In particular, the membership operators
|
|
222
|
+
(``in``, ``not it``), the access operator (``[]``), as well as the :func:`~DB.keys()` and
|
|
223
|
+
:func:`~DB.items()` methods work as usual. Iteration, on the other hand, happens on items
|
|
224
|
+
instead of keys (much like :func:`~DB.items()` method).
|
|
225
|
+
|
|
226
|
+
Attributes:
|
|
227
|
+
bytearray_: buffer data from the PLC.
|
|
228
|
+
specification: layout of the DB Rows.
|
|
229
|
+
row_size: bytes size of a db row.
|
|
230
|
+
layout_offset: at which byte in the row specificaion we
|
|
231
|
+
start reading the data.
|
|
232
|
+
db_offset: at which byte in the db starts reading.
|
|
233
|
+
|
|
234
|
+
Examples:
|
|
235
|
+
>>> db = DB()
|
|
236
|
+
>>> db[0]['test_bool1'] = "test"
|
|
237
|
+
>>> db.write(Client()) # puts data in plc
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
bytearray_: Optional[bytearray] = None # data from plc
|
|
241
|
+
specification: str # layout of db rows
|
|
242
|
+
id_field: Optional[str] = None # ID field of the rows
|
|
243
|
+
row_size: int = 0 # bytes size of a db row
|
|
244
|
+
layout_offset: int = 0 # at which byte in row specification should
|
|
245
|
+
db_offset: int = 0 # at which byte in db should we start reading?
|
|
246
|
+
|
|
247
|
+
# first fields could be status data.
|
|
248
|
+
# and only the last part could be control data
|
|
249
|
+
# now you can be sure you will never overwrite
|
|
250
|
+
# critical parts of db
|
|
251
|
+
|
|
252
|
+
def __init__(
|
|
253
|
+
self,
|
|
254
|
+
db_number: int,
|
|
255
|
+
bytearray_: bytearray,
|
|
256
|
+
specification: str,
|
|
257
|
+
row_size: int,
|
|
258
|
+
size: int,
|
|
259
|
+
id_field: Optional[str] = None,
|
|
260
|
+
db_offset: int = 0,
|
|
261
|
+
layout_offset: int = 0,
|
|
262
|
+
row_offset: int = 0,
|
|
263
|
+
area: Area = Area.DB,
|
|
264
|
+
):
|
|
265
|
+
"""Creates a new instance of the `Row` class.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
db_number: number of the DB to read from. This value should be 0 if area!=Areas.DB.
|
|
269
|
+
bytearray_: initial buffer read from the PLC.
|
|
270
|
+
specification: layout of the PLC memory.
|
|
271
|
+
row_size: bytes size of a db row.
|
|
272
|
+
size: length of the memory area.
|
|
273
|
+
id_field: name to reference the row. Optional.
|
|
274
|
+
db_offset: at which byte in the db starts reading.
|
|
275
|
+
layout_offset: at which byte in the row specification we
|
|
276
|
+
start reading the data.
|
|
277
|
+
row_offset: offset between rows.
|
|
278
|
+
area: which memory area this row is representing.
|
|
279
|
+
"""
|
|
280
|
+
self.db_number = db_number
|
|
281
|
+
self.size = size
|
|
282
|
+
self.row_size = row_size
|
|
283
|
+
self.id_field = id_field
|
|
284
|
+
self.area = area
|
|
285
|
+
|
|
286
|
+
self.db_offset = db_offset
|
|
287
|
+
self.layout_offset = layout_offset
|
|
288
|
+
self.row_offset = row_offset
|
|
289
|
+
|
|
290
|
+
self._bytearray = bytearray_
|
|
291
|
+
self.specification = specification
|
|
292
|
+
# loop over bytearray. make rowObjects
|
|
293
|
+
# store index of id_field to row objects
|
|
294
|
+
self.index: Dict[str, Row] = {}
|
|
295
|
+
self.make_rows()
|
|
296
|
+
|
|
297
|
+
def make_rows(self) -> None:
|
|
298
|
+
"""Make each row for the DB."""
|
|
299
|
+
id_field = self.id_field
|
|
300
|
+
row_size = self.row_size
|
|
301
|
+
specification = self.specification
|
|
302
|
+
layout_offset = self.layout_offset
|
|
303
|
+
row_offset = self.row_offset
|
|
304
|
+
|
|
305
|
+
for i in range(self.size):
|
|
306
|
+
# calculate where row in bytearray starts
|
|
307
|
+
db_offset = i * (row_size + row_offset) + self.db_offset
|
|
308
|
+
# create a row object
|
|
309
|
+
row = Row(
|
|
310
|
+
self,
|
|
311
|
+
specification,
|
|
312
|
+
row_size=row_size,
|
|
313
|
+
db_offset=db_offset,
|
|
314
|
+
layout_offset=layout_offset,
|
|
315
|
+
row_offset=self.row_offset,
|
|
316
|
+
area=self.area,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# store row object
|
|
320
|
+
key = row[id_field] if id_field else i
|
|
321
|
+
if key and key in self.index:
|
|
322
|
+
msg = f"{key} not unique!"
|
|
323
|
+
logger.error(msg)
|
|
324
|
+
self.index[str(key)] = row
|
|
325
|
+
|
|
326
|
+
def __getitem__(self, key: str, default: Optional[None] = None) -> Union[None, "Row"]:
|
|
327
|
+
"""Access a row of the table through its index.
|
|
328
|
+
|
|
329
|
+
Rows (values) are of type :class:`DB_Row`.
|
|
330
|
+
|
|
331
|
+
Notes:
|
|
332
|
+
This method has the same semantics as :class:`dict` access.
|
|
333
|
+
"""
|
|
334
|
+
return self.index.get(key, default)
|
|
335
|
+
|
|
336
|
+
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
|
337
|
+
"""Iterate over the items contained in the table, in the physical order they are contained
|
|
338
|
+
in memory.
|
|
339
|
+
|
|
340
|
+
Notes:
|
|
341
|
+
This method does not have the same semantics as :class:`dict` iteration. Instead, it
|
|
342
|
+
has the same semantics as the :func:`~DB.items` method, yielding ``(index, row)``
|
|
343
|
+
tuples.
|
|
344
|
+
"""
|
|
345
|
+
yield from self.index.items()
|
|
346
|
+
|
|
347
|
+
def __len__(self) -> int:
|
|
348
|
+
"""Return the number of rows contained in the DB.
|
|
349
|
+
|
|
350
|
+
Notes:
|
|
351
|
+
If more than one row has the same index value, it is only counted once.
|
|
352
|
+
"""
|
|
353
|
+
return len(self.index)
|
|
354
|
+
|
|
355
|
+
def __contains__(self, key: str) -> bool:
|
|
356
|
+
"""Return whether the given key is the index of a row in the DB."""
|
|
357
|
+
return key in self.index
|
|
358
|
+
|
|
359
|
+
def keys(self) -> Iterator[str]:
|
|
360
|
+
"""Return a *view object* of the keys that are used as indices for the rows in the
|
|
361
|
+
DB.
|
|
362
|
+
"""
|
|
363
|
+
yield from self.index.keys()
|
|
364
|
+
|
|
365
|
+
def items(self) -> Iterator[Tuple[str, Any]]:
|
|
366
|
+
"""Return a *view object* of the items (``(index, row)`` pairs) that are used as indices
|
|
367
|
+
for the rows in the DB.
|
|
368
|
+
"""
|
|
369
|
+
yield from self.index.items()
|
|
370
|
+
|
|
371
|
+
def export(self) -> Dict[str, Any]:
|
|
372
|
+
"""Export the object to a dict, where each item in the dictionary
|
|
373
|
+
has an index as the key, and the value of the DB row associated with that index
|
|
374
|
+
as a value, represented itself as a :class:`dict` (as returned by :func:`DB_Row.export`).
|
|
375
|
+
|
|
376
|
+
The outer dictionary contains the rows in the physical order they are contained in
|
|
377
|
+
memory.
|
|
378
|
+
|
|
379
|
+
Notes:
|
|
380
|
+
This function effectively returns a snapshot of the DB.
|
|
381
|
+
"""
|
|
382
|
+
ret = {}
|
|
383
|
+
for k, v in self.items():
|
|
384
|
+
ret[k] = v.export()
|
|
385
|
+
return ret
|
|
386
|
+
|
|
387
|
+
def set_data(self, bytearray_: bytearray) -> None:
|
|
388
|
+
"""Set the new buffer data from the PLC to the current instance.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
bytearray_: buffer to save.
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
:obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray`
|
|
395
|
+
"""
|
|
396
|
+
if not isinstance(bytearray_, bytearray):
|
|
397
|
+
raise TypeError(f"Value bytearray_: {bytearray_} is not from type bytearray")
|
|
398
|
+
self._bytearray = bytearray_
|
|
399
|
+
|
|
400
|
+
def read(self, client: Client) -> None:
|
|
401
|
+
"""Reads all the rows from the PLC to the :obj:`bytearray` of this instance.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
client: :obj:`Client` snap7 instance.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
:obj:`ValueError`: if the `row_size` is less than 0.
|
|
408
|
+
"""
|
|
409
|
+
if self.row_size < 0:
|
|
410
|
+
raise ValueError("row_size must be greater equal zero.")
|
|
411
|
+
|
|
412
|
+
total_size = self.size * (self.row_size + self.row_offset)
|
|
413
|
+
if self.area == Area.DB: # note: is it worth using the upload method?
|
|
414
|
+
bytearray_ = client.db_read(self.db_number, self.db_offset, total_size)
|
|
415
|
+
else:
|
|
416
|
+
bytearray_ = client.read_area(self.area, 0, self.db_offset, total_size)
|
|
417
|
+
|
|
418
|
+
# replace data in bytearray
|
|
419
|
+
for i, b in enumerate(bytearray_):
|
|
420
|
+
self._bytearray[i + self.db_offset] = b
|
|
421
|
+
|
|
422
|
+
self.index.clear()
|
|
423
|
+
self.make_rows()
|
|
424
|
+
|
|
425
|
+
def write(self, client: Client) -> None:
|
|
426
|
+
"""Writes all the rows from the :obj:`bytearray` of this instance to the PLC
|
|
427
|
+
|
|
428
|
+
Notes:
|
|
429
|
+
When the row_offset property has been set to something other than None while
|
|
430
|
+
constructing this object, this operation is not guaranteed to be atomic.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
client: :obj:`Client` snap7 instance.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
:obj:`ValueError`: if the `row_size` is less than 0.
|
|
437
|
+
"""
|
|
438
|
+
if self.row_size < 0:
|
|
439
|
+
raise ValueError("row_size must be greater equal zero.")
|
|
440
|
+
|
|
441
|
+
# special case: we have a row offset, so we must write each row individually
|
|
442
|
+
# this is because we don't want to change the data before the offset
|
|
443
|
+
if self.row_offset:
|
|
444
|
+
for _, v in self.index.items():
|
|
445
|
+
v.write(client)
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
total_size = self.size * (self.row_size + self.row_offset)
|
|
449
|
+
data = self._bytearray[self.db_offset : self.db_offset + total_size]
|
|
450
|
+
|
|
451
|
+
if self.area == Area.DB:
|
|
452
|
+
client.db_write(self.db_number, self.db_offset, data)
|
|
453
|
+
else:
|
|
454
|
+
client.write_area(self.area, 0, self.db_offset, data)
|
|
455
|
+
|
|
456
|
+
def get_bytearray(self) -> bytearray:
|
|
457
|
+
return self._bytearray
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class Row:
|
|
461
|
+
"""
|
|
462
|
+
Provide ROW API for DB bytearray
|
|
463
|
+
|
|
464
|
+
Attributes:
|
|
465
|
+
bytearray_: reference to the data of the parent DB.
|
|
466
|
+
_specification: row specification layout.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
bytearray_: bytearray # data of reference to parent DB
|
|
470
|
+
_specification: Dict[str, Any] = {} # row specification
|
|
471
|
+
|
|
472
|
+
def __init__(
|
|
473
|
+
self,
|
|
474
|
+
bytearray_: Union[bytearray, "DB"],
|
|
475
|
+
_specification: str,
|
|
476
|
+
row_size: int = 0,
|
|
477
|
+
db_offset: int = 0,
|
|
478
|
+
layout_offset: int = 0,
|
|
479
|
+
row_offset: Optional[int] = 0,
|
|
480
|
+
area: Area = Area.DB,
|
|
481
|
+
):
|
|
482
|
+
"""Creates a new instance of the `DB_Row` class.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
bytearray_: reference to the data of the parent DB.
|
|
486
|
+
_specification: row specification layout.
|
|
487
|
+
row_size: Amount of bytes of the row.
|
|
488
|
+
db_offset: at which byte in the db starts reading.
|
|
489
|
+
layout_offset: at which byte in the row specification we
|
|
490
|
+
start reading the data.
|
|
491
|
+
row_offset: offset between rows.
|
|
492
|
+
area: which memory area this row is representing.
|
|
493
|
+
|
|
494
|
+
Raises:
|
|
495
|
+
:obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray` or :obj:`DB`.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
self.db_offset = db_offset # start point of row data in db
|
|
499
|
+
self.layout_offset = layout_offset # start point of row data in layout
|
|
500
|
+
self.row_size = row_size # length of the read
|
|
501
|
+
self.row_offset = row_offset # start of writable part of row
|
|
502
|
+
self.area = area
|
|
503
|
+
|
|
504
|
+
if not isinstance(bytearray_, (bytearray, DB)):
|
|
505
|
+
raise TypeError(f"Value bytearray_ {bytearray_} is not from type (bytearray, DB)")
|
|
506
|
+
self._bytearray = bytearray_
|
|
507
|
+
self._specification = parse_specification(_specification)
|
|
508
|
+
|
|
509
|
+
def get_bytearray(self) -> bytearray:
|
|
510
|
+
"""Gets bytearray from self or DB parent
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Buffer data corresponding to the row.
|
|
514
|
+
"""
|
|
515
|
+
if isinstance(self._bytearray, DB):
|
|
516
|
+
return self._bytearray.get_bytearray()
|
|
517
|
+
return self._bytearray
|
|
518
|
+
|
|
519
|
+
def export(self) -> Dict[str, Union[str, int, float, bool, datetime]]:
|
|
520
|
+
"""Export dictionary with values
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
dictionary containing the values of each value of the row.
|
|
524
|
+
"""
|
|
525
|
+
return {key: self[key] for key in self._specification}
|
|
526
|
+
|
|
527
|
+
def __getitem__(self, key: str) -> Any:
|
|
528
|
+
"""
|
|
529
|
+
Get a specific db field
|
|
530
|
+
"""
|
|
531
|
+
index, _type = self._specification[key]
|
|
532
|
+
return self.get_value(index, _type)
|
|
533
|
+
|
|
534
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
535
|
+
index, _type = self._specification[key]
|
|
536
|
+
self.set_value(index, _type, value)
|
|
537
|
+
|
|
538
|
+
def __repr__(self) -> str:
|
|
539
|
+
string = ""
|
|
540
|
+
for var_name, (index, _type) in self._specification.items():
|
|
541
|
+
string = f"{string}\n{var_name:<20} {self.get_value(index, _type)!r:<10}"
|
|
542
|
+
return string
|
|
543
|
+
|
|
544
|
+
def unchanged(self, bytearray_: bytearray) -> bool:
|
|
545
|
+
"""Checks if the bytearray is the same
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
bytearray_: buffer of data to check.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
True if the current `bytearray_` is equal to the new one. Otherwise, this is False.
|
|
552
|
+
"""
|
|
553
|
+
return self.get_bytearray() == bytearray_
|
|
554
|
+
|
|
555
|
+
def get_offset(self, byte_index: Union[str, int]) -> int:
|
|
556
|
+
"""Calculate correct beginning position for a row
|
|
557
|
+
the db_offset = row_size * index
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
byte_index: byte index from where to start reading from.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Amount of bytes to ignore.
|
|
564
|
+
"""
|
|
565
|
+
# add float typ to avoid error because of
|
|
566
|
+
# the variable address with decimal point(like 0.0 or 4.0)
|
|
567
|
+
return int(float(byte_index)) - self.layout_offset + self.db_offset
|
|
568
|
+
|
|
569
|
+
def get_value(self, byte_index: Union[str, int], type_: str) -> ValueType:
|
|
570
|
+
"""Gets the value for a specific type.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
byte_index: byte index from where start reading.
|
|
574
|
+
type_: type of data to read.
|
|
575
|
+
|
|
576
|
+
Raises:
|
|
577
|
+
:obj:`ValueError`: if reading a `string` when checking the length of the string.
|
|
578
|
+
:obj:`ValueError`: if the `type_` is not handled.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
Value read according to the `type_`
|
|
582
|
+
"""
|
|
583
|
+
bytearray_ = self.get_bytearray()
|
|
584
|
+
|
|
585
|
+
# set parsing non case-sensitive
|
|
586
|
+
type_ = type_.upper()
|
|
587
|
+
|
|
588
|
+
if type_ == "BOOL":
|
|
589
|
+
byte_index, bool_index = str(byte_index).split(".")
|
|
590
|
+
return get_bool(bytearray_, self.get_offset(byte_index), int(bool_index))
|
|
591
|
+
|
|
592
|
+
# remove 4 from byte index since
|
|
593
|
+
# first 4 bytes are used by db
|
|
594
|
+
byte_index = self.get_offset(byte_index)
|
|
595
|
+
|
|
596
|
+
if type_.startswith("FSTRING"):
|
|
597
|
+
max_size = re.search(r"\d+", type_)
|
|
598
|
+
if max_size is None:
|
|
599
|
+
raise ValueError("Max size could not be determinate. re.search() returned None")
|
|
600
|
+
return get_fstring(bytearray_, byte_index, int(max_size[0]))
|
|
601
|
+
elif type_.startswith("STRING"):
|
|
602
|
+
max_size = re.search(r"\d+", type_)
|
|
603
|
+
if max_size is None:
|
|
604
|
+
raise ValueError("Max size could not be determinate. re.search() returned None")
|
|
605
|
+
return get_string(bytearray_, byte_index)
|
|
606
|
+
elif type_.startswith("WSTRING"):
|
|
607
|
+
max_size = re.search(r"\d+", type_)
|
|
608
|
+
if max_size is None:
|
|
609
|
+
raise ValueError("Max size could not be determinate. re.search() returned None")
|
|
610
|
+
return get_wstring(bytearray_, byte_index)
|
|
611
|
+
else:
|
|
612
|
+
type_to_func: Dict[str, Callable[[bytearray, int], ValueType]] = {
|
|
613
|
+
"REAL": get_real,
|
|
614
|
+
"DWORD": get_dword,
|
|
615
|
+
"UDINT": get_udint,
|
|
616
|
+
"DINT": get_dint,
|
|
617
|
+
"UINT": get_uint,
|
|
618
|
+
"INT": get_int,
|
|
619
|
+
"WORD": get_word,
|
|
620
|
+
"BYTE": get_byte,
|
|
621
|
+
"S5TIME": get_s5time,
|
|
622
|
+
"DATE_AND_TIME": get_dt,
|
|
623
|
+
"USINT": get_usint,
|
|
624
|
+
"SINT": get_sint,
|
|
625
|
+
"TIME": get_time,
|
|
626
|
+
"DATE": get_date,
|
|
627
|
+
"TIME_OF_DAY": get_tod,
|
|
628
|
+
"LREAL": get_lreal,
|
|
629
|
+
"TOD": get_tod,
|
|
630
|
+
"CHAR": get_char,
|
|
631
|
+
"WCHAR": get_wchar,
|
|
632
|
+
"DTL": get_dtl,
|
|
633
|
+
}
|
|
634
|
+
if type_ in type_to_func:
|
|
635
|
+
return type_to_func[type_](bytearray_, byte_index)
|
|
636
|
+
raise ValueError
|
|
637
|
+
|
|
638
|
+
def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool, str, float]) -> Optional[bytearray]:
|
|
639
|
+
"""Sets the value for a specific type in the specified byte index.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
byte_index: byte index to start writing to.
|
|
643
|
+
type_: type of value to write.
|
|
644
|
+
value: value to write.
|
|
645
|
+
|
|
646
|
+
Raises:
|
|
647
|
+
:obj:`ValueError`: if reading a `string` when checking the length of the string.
|
|
648
|
+
:obj:`ValueError`: if the `type_` is not handled.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
Buffer data with the value written. Optional.
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
bytearray_ = self.get_bytearray()
|
|
655
|
+
|
|
656
|
+
if type_ == "BOOL" and isinstance(value, bool):
|
|
657
|
+
byte_index, bool_index = str(byte_index).split(".")
|
|
658
|
+
return set_bool(bytearray_, self.get_offset(byte_index), int(bool_index), value)
|
|
659
|
+
|
|
660
|
+
byte_index = self.get_offset(byte_index)
|
|
661
|
+
|
|
662
|
+
if type_.startswith("FSTRING") and isinstance(value, str):
|
|
663
|
+
max_size = re.search(r"\d+", type_)
|
|
664
|
+
if max_size is None:
|
|
665
|
+
raise ValueError("Max size could not be determinate. re.search() returned None")
|
|
666
|
+
max_size_grouped = max_size.group(0)
|
|
667
|
+
max_size_int = int(max_size_grouped)
|
|
668
|
+
set_fstring(bytearray_, byte_index, value, max_size_int)
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
if type_.startswith("STRING") and isinstance(value, str):
|
|
672
|
+
max_size = re.search(r"\d+", type_)
|
|
673
|
+
if max_size is None:
|
|
674
|
+
raise ValueError("Max size could not be determinate. re.search() returned None")
|
|
675
|
+
max_size_grouped = max_size.group(0)
|
|
676
|
+
max_size_int = int(max_size_grouped)
|
|
677
|
+
set_string(bytearray_, byte_index, value, max_size_int)
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
if type_.startswith("WSTRING") and isinstance(value, str):
|
|
681
|
+
max_size = re.search(r"\d+", type_)
|
|
682
|
+
if max_size is None:
|
|
683
|
+
raise ValueError("Max size could not be determinate. re.search() returned None")
|
|
684
|
+
max_size_int = int(max_size.group(0))
|
|
685
|
+
set_wstring(bytearray_, byte_index, value, max_size_int)
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
if type_ == "REAL":
|
|
689
|
+
return set_real(bytearray_, byte_index, value)
|
|
690
|
+
|
|
691
|
+
if type_ == "LREAL" and isinstance(value, float):
|
|
692
|
+
return set_lreal(bytearray_, byte_index, value)
|
|
693
|
+
|
|
694
|
+
if type_ == "CHAR" and isinstance(value, str):
|
|
695
|
+
return set_char(bytearray_, byte_index, value)
|
|
696
|
+
|
|
697
|
+
if type_ == "WCHAR" and isinstance(value, str):
|
|
698
|
+
return set_wchar(bytearray_, byte_index, value)
|
|
699
|
+
|
|
700
|
+
if isinstance(value, int):
|
|
701
|
+
type_to_func = {
|
|
702
|
+
"DWORD": set_dword,
|
|
703
|
+
"UDINT": set_udint,
|
|
704
|
+
"DINT": set_dint,
|
|
705
|
+
"UINT": set_uint,
|
|
706
|
+
"INT": set_int,
|
|
707
|
+
"WORD": set_word,
|
|
708
|
+
"BYTE": set_byte,
|
|
709
|
+
"USINT": set_usint,
|
|
710
|
+
"SINT": set_sint,
|
|
711
|
+
}
|
|
712
|
+
if type_ in type_to_func:
|
|
713
|
+
return type_to_func[type_](bytearray_, byte_index, value)
|
|
714
|
+
|
|
715
|
+
if type_ == "TIME" and isinstance(value, str):
|
|
716
|
+
return set_time(bytearray_, byte_index, value)
|
|
717
|
+
|
|
718
|
+
if type_ == "DATE" and isinstance(value, date):
|
|
719
|
+
return set_date(bytearray_, byte_index, value)
|
|
720
|
+
|
|
721
|
+
if type_ in ("TIME_OF_DAY", "TOD") and isinstance(value, timedelta):
|
|
722
|
+
return set_tod(bytearray_, byte_index, value)
|
|
723
|
+
|
|
724
|
+
if type_ == "DTL" and isinstance(value, datetime):
|
|
725
|
+
return set_dtl(bytearray_, byte_index, value)
|
|
726
|
+
|
|
727
|
+
if type_ == "DATE_AND_TIME" and isinstance(value, datetime):
|
|
728
|
+
return set_dt(bytearray_, byte_index, value)
|
|
729
|
+
|
|
730
|
+
raise ValueError
|
|
731
|
+
|
|
732
|
+
def write(self, client: Client) -> None:
|
|
733
|
+
"""Write current data to db in plc
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
client: :obj:`Client` snap7 instance.
|
|
737
|
+
|
|
738
|
+
Raises:
|
|
739
|
+
:obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class.
|
|
740
|
+
:obj:`ValueError`: if the `row_size` is less than 0.
|
|
741
|
+
"""
|
|
742
|
+
if not isinstance(self._bytearray, DB):
|
|
743
|
+
raise TypeError(f"Value self._bytearray: {self._bytearray} is not from type DB.")
|
|
744
|
+
if self.row_size < 0:
|
|
745
|
+
raise ValueError("row_size must be greater equal zero.")
|
|
746
|
+
|
|
747
|
+
db_nr = self._bytearray.db_number
|
|
748
|
+
offset = self.db_offset
|
|
749
|
+
data = self.get_bytearray()[offset : offset + self.row_size]
|
|
750
|
+
db_offset = self.db_offset
|
|
751
|
+
|
|
752
|
+
# indicate start of write only area of row!
|
|
753
|
+
if self.row_offset:
|
|
754
|
+
data = data[self.row_offset :]
|
|
755
|
+
db_offset += self.row_offset
|
|
756
|
+
|
|
757
|
+
if self.area == Area.DB:
|
|
758
|
+
client.db_write(db_nr, db_offset, data)
|
|
759
|
+
else:
|
|
760
|
+
client.write_area(self.area, 0, db_offset, data)
|
|
761
|
+
|
|
762
|
+
def read(self, client: Client) -> None:
|
|
763
|
+
"""Read current data of db row from plc.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
client: :obj:`Client` snap7 instance.
|
|
767
|
+
|
|
768
|
+
Raises:
|
|
769
|
+
:obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class.
|
|
770
|
+
:obj:`ValueError`: if the `row_size` is less than 0.
|
|
771
|
+
"""
|
|
772
|
+
if not isinstance(self._bytearray, DB):
|
|
773
|
+
raise TypeError(f"Value self._bytearray:{self._bytearray} is not from type DB.")
|
|
774
|
+
if self.row_size < 0:
|
|
775
|
+
raise ValueError("row_size must be greater equal zero.")
|
|
776
|
+
db_nr = self._bytearray.db_number
|
|
777
|
+
if self.area == Area.DB:
|
|
778
|
+
bytearray_ = client.db_read(db_nr, self.db_offset, self.row_size)
|
|
779
|
+
else:
|
|
780
|
+
bytearray_ = client.read_area(self.area, 0, self.db_offset, self.row_size)
|
|
781
|
+
|
|
782
|
+
data = self.get_bytearray()
|
|
783
|
+
# replace data in bytearray
|
|
784
|
+
for i, b in enumerate(bytearray_):
|
|
785
|
+
data[i + self.db_offset] = b
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
# backwards compatible alias
|
|
789
|
+
DB_Row = Row
|