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/datatypes.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
S7 data types and conversion utilities.
|
|
3
|
+
|
|
4
|
+
Handles S7-specific data types, endianness conversion, and address encoding.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import struct
|
|
8
|
+
from enum import IntEnum
|
|
9
|
+
from typing import List, NoReturn, Sequence, Tuple, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _assert_never(value: NoReturn) -> NoReturn:
|
|
13
|
+
"""Exhaustive type check helper (equivalent to typing.assert_never for Python <3.11)."""
|
|
14
|
+
raise AssertionError(f"Unhandled value: {value!r}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_bit_addr(bit_addr: int) -> None:
|
|
18
|
+
"""Validate that a bit address is in the valid range 0-7."""
|
|
19
|
+
if not 0 <= bit_addr <= 7:
|
|
20
|
+
raise ValueError(f"Bit address must be 0-7, got {bit_addr}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class S7Area(IntEnum):
|
|
24
|
+
"""S7 memory area identifiers."""
|
|
25
|
+
|
|
26
|
+
PE = 0x81 # Process Input (Peripheral Input)
|
|
27
|
+
PA = 0x82 # Process Output (Peripheral Output)
|
|
28
|
+
MK = 0x83 # Memory/Merkers (Flags)
|
|
29
|
+
DB = 0x84 # Data Blocks
|
|
30
|
+
CT = 0x1C # Counters
|
|
31
|
+
TM = 0x1D # Timers
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class S7WordLen(IntEnum):
|
|
35
|
+
"""S7 data word length identifiers."""
|
|
36
|
+
|
|
37
|
+
BIT = 0x01 # Single bit
|
|
38
|
+
BYTE = 0x02 # 8-bit byte
|
|
39
|
+
CHAR = 0x03 # 8-bit character
|
|
40
|
+
WORD = 0x04 # 16-bit word
|
|
41
|
+
INT = 0x05 # 16-bit signed integer
|
|
42
|
+
DWORD = 0x06 # 32-bit double word
|
|
43
|
+
DINT = 0x07 # 32-bit signed integer
|
|
44
|
+
REAL = 0x08 # 32-bit IEEE float
|
|
45
|
+
COUNTER = 0x1C # Counter value
|
|
46
|
+
TIMER = 0x1D # Timer value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class S7DataTypes:
|
|
50
|
+
"""S7 data type conversion utilities."""
|
|
51
|
+
|
|
52
|
+
# Word length to byte size mapping
|
|
53
|
+
WORD_LEN_SIZE = {
|
|
54
|
+
S7WordLen.BIT: 1, # Bit operations use 1 byte
|
|
55
|
+
S7WordLen.BYTE: 1, # 1 byte
|
|
56
|
+
S7WordLen.CHAR: 1, # 1 byte
|
|
57
|
+
S7WordLen.WORD: 2, # 2 bytes
|
|
58
|
+
S7WordLen.INT: 2, # 2 bytes
|
|
59
|
+
S7WordLen.DWORD: 4, # 4 bytes
|
|
60
|
+
S7WordLen.DINT: 4, # 4 bytes
|
|
61
|
+
S7WordLen.REAL: 4, # 4 bytes
|
|
62
|
+
S7WordLen.COUNTER: 2, # 2 bytes
|
|
63
|
+
S7WordLen.TIMER: 2, # 2 bytes
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def get_size_bytes(word_len: S7WordLen, count: int = 1) -> int:
|
|
68
|
+
"""Get total size in bytes for given word length and count."""
|
|
69
|
+
return S7DataTypes.WORD_LEN_SIZE[word_len] * count
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def encode_address(area: S7Area, db_number: int, start: int, word_len: S7WordLen, count: int) -> bytes:
|
|
73
|
+
"""
|
|
74
|
+
Encode S7 address into parameter format.
|
|
75
|
+
|
|
76
|
+
Returns 12-byte parameter section for read/write operations.
|
|
77
|
+
"""
|
|
78
|
+
# Parameter format for read/write operations
|
|
79
|
+
# Byte 0: Specification type (0x12 for address specification)
|
|
80
|
+
# Byte 1: Length of following address specification (0x0A = 10 bytes)
|
|
81
|
+
# Byte 2: Syntax ID (0x10 = S7-Any)
|
|
82
|
+
# Byte 3: Transport size (word length)
|
|
83
|
+
# Bytes 4-5: Count (number of items)
|
|
84
|
+
# Bytes 6-7: DB number (for DB area) or 0
|
|
85
|
+
# Bytes 8: Area code
|
|
86
|
+
# Bytes 9-11: Start address (byte.bit format)
|
|
87
|
+
|
|
88
|
+
if start < 0:
|
|
89
|
+
raise ValueError(f"Start address must be non-negative, got {start}")
|
|
90
|
+
|
|
91
|
+
# Convert start address to byte.bit format
|
|
92
|
+
if word_len == S7WordLen.BIT:
|
|
93
|
+
# For bit access: byte address + bit offset
|
|
94
|
+
byte_addr = start // 8
|
|
95
|
+
bit_addr = start % 8
|
|
96
|
+
address = (byte_addr << 3) | bit_addr
|
|
97
|
+
else:
|
|
98
|
+
# For word access: convert to bit address
|
|
99
|
+
address = start * 8
|
|
100
|
+
|
|
101
|
+
address_bytes = struct.pack(">I", address)[1:] # 3-byte address (big-endian)
|
|
102
|
+
|
|
103
|
+
return struct.pack(
|
|
104
|
+
">BBBBHHB3s",
|
|
105
|
+
0x12, # Specification type
|
|
106
|
+
0x0A, # Length of address spec
|
|
107
|
+
0x10, # Syntax ID (S7-Any)
|
|
108
|
+
word_len, # Transport size
|
|
109
|
+
count, # Count
|
|
110
|
+
db_number if area == S7Area.DB else 0, # DB number
|
|
111
|
+
area, # Area code
|
|
112
|
+
address_bytes, # 3-byte address (big-endian)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def decode_s7_data(data: bytes, word_len: S7WordLen, count: int) -> List[Union[bool, int, float]]:
|
|
117
|
+
"""
|
|
118
|
+
Decode S7 data from bytes to Python values.
|
|
119
|
+
|
|
120
|
+
Handles Siemens big-endian byte order.
|
|
121
|
+
"""
|
|
122
|
+
values: List[Union[bool, int, float]] = []
|
|
123
|
+
offset = 0
|
|
124
|
+
|
|
125
|
+
for i in range(count):
|
|
126
|
+
if word_len == S7WordLen.BIT:
|
|
127
|
+
# Extract single bit
|
|
128
|
+
byte_val = data[offset]
|
|
129
|
+
values.append(bool(byte_val))
|
|
130
|
+
offset += 1
|
|
131
|
+
|
|
132
|
+
elif word_len == S7WordLen.BYTE or word_len == S7WordLen.CHAR:
|
|
133
|
+
# 8-bit values
|
|
134
|
+
values.append(data[offset])
|
|
135
|
+
offset += 1
|
|
136
|
+
|
|
137
|
+
elif word_len == S7WordLen.WORD or word_len == S7WordLen.COUNTER or word_len == S7WordLen.TIMER:
|
|
138
|
+
# 16-bit unsigned values (big-endian)
|
|
139
|
+
value = struct.unpack(">H", data[offset : offset + 2])[0]
|
|
140
|
+
values.append(value)
|
|
141
|
+
offset += 2
|
|
142
|
+
|
|
143
|
+
elif word_len == S7WordLen.INT:
|
|
144
|
+
# 16-bit signed values (big-endian)
|
|
145
|
+
value = struct.unpack(">h", data[offset : offset + 2])[0]
|
|
146
|
+
values.append(value)
|
|
147
|
+
offset += 2
|
|
148
|
+
|
|
149
|
+
elif word_len == S7WordLen.DWORD:
|
|
150
|
+
# 32-bit unsigned values (big-endian)
|
|
151
|
+
value = struct.unpack(">I", data[offset : offset + 4])[0]
|
|
152
|
+
values.append(value)
|
|
153
|
+
offset += 4
|
|
154
|
+
|
|
155
|
+
elif word_len == S7WordLen.DINT:
|
|
156
|
+
# 32-bit signed values (big-endian)
|
|
157
|
+
value = struct.unpack(">i", data[offset : offset + 4])[0]
|
|
158
|
+
values.append(value)
|
|
159
|
+
offset += 4
|
|
160
|
+
|
|
161
|
+
elif word_len == S7WordLen.REAL:
|
|
162
|
+
# 32-bit IEEE float (big-endian)
|
|
163
|
+
value = struct.unpack(">f", data[offset : offset + 4])[0]
|
|
164
|
+
values.append(value)
|
|
165
|
+
offset += 4
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
_assert_never(word_len)
|
|
169
|
+
|
|
170
|
+
return values
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def encode_s7_data(values: Sequence[Union[bool, int, float]], word_len: S7WordLen) -> bytes:
|
|
174
|
+
"""
|
|
175
|
+
Encode Python values to S7 data bytes.
|
|
176
|
+
|
|
177
|
+
Handles Siemens big-endian byte order.
|
|
178
|
+
"""
|
|
179
|
+
data = bytearray()
|
|
180
|
+
|
|
181
|
+
for value in values:
|
|
182
|
+
if word_len == S7WordLen.BIT:
|
|
183
|
+
# Single bit to byte
|
|
184
|
+
data.append(0x01 if value else 0x00)
|
|
185
|
+
|
|
186
|
+
elif word_len == S7WordLen.BYTE or word_len == S7WordLen.CHAR:
|
|
187
|
+
# 8-bit values
|
|
188
|
+
data.append(int(value) & 0xFF)
|
|
189
|
+
|
|
190
|
+
elif word_len == S7WordLen.WORD or word_len == S7WordLen.COUNTER or word_len == S7WordLen.TIMER:
|
|
191
|
+
# 16-bit unsigned values (big-endian)
|
|
192
|
+
data.extend(struct.pack(">H", int(value) & 0xFFFF))
|
|
193
|
+
|
|
194
|
+
elif word_len == S7WordLen.INT:
|
|
195
|
+
# 16-bit signed values (big-endian)
|
|
196
|
+
data.extend(struct.pack(">h", int(value)))
|
|
197
|
+
|
|
198
|
+
elif word_len == S7WordLen.DWORD:
|
|
199
|
+
# 32-bit unsigned values (big-endian)
|
|
200
|
+
data.extend(struct.pack(">I", int(value) & 0xFFFFFFFF))
|
|
201
|
+
|
|
202
|
+
elif word_len == S7WordLen.DINT:
|
|
203
|
+
# 32-bit signed values (big-endian)
|
|
204
|
+
data.extend(struct.pack(">i", int(value)))
|
|
205
|
+
|
|
206
|
+
elif word_len == S7WordLen.REAL:
|
|
207
|
+
# 32-bit IEEE float (big-endian)
|
|
208
|
+
data.extend(struct.pack(">f", float(value)))
|
|
209
|
+
|
|
210
|
+
else:
|
|
211
|
+
_assert_never(word_len)
|
|
212
|
+
|
|
213
|
+
return bytes(data)
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def parse_address(address_str: str) -> Tuple[S7Area, int, int]:
|
|
217
|
+
"""
|
|
218
|
+
Parse S7 address string to area, DB number, and offset.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
- "DB1.DBX0.0" -> (DB, 1, 0)
|
|
222
|
+
- "M10.5" -> (MK, 0, 85) # bit 5 of byte 10 = bit 85
|
|
223
|
+
- "IW20" -> (PE, 0, 20)
|
|
224
|
+
"""
|
|
225
|
+
address_str = address_str.upper().strip()
|
|
226
|
+
|
|
227
|
+
# Data Block addresses: DB1.DBX0.0, DB1.DBW10, etc.
|
|
228
|
+
if address_str.startswith("DB"):
|
|
229
|
+
db_part, addr_part = address_str.split(".", 1)
|
|
230
|
+
db_number = int(db_part[2:])
|
|
231
|
+
|
|
232
|
+
if addr_part.startswith("DBX"):
|
|
233
|
+
# Bit address: DBX10.5
|
|
234
|
+
if "." in addr_part:
|
|
235
|
+
byte_addr, bit_addr = addr_part[3:].split(".")
|
|
236
|
+
_validate_bit_addr(int(bit_addr))
|
|
237
|
+
offset = int(byte_addr) * 8 + int(bit_addr)
|
|
238
|
+
else:
|
|
239
|
+
offset = int(addr_part[3:]) * 8
|
|
240
|
+
elif addr_part.startswith("DBB"):
|
|
241
|
+
# Byte address: DBB10
|
|
242
|
+
offset = int(addr_part[3:])
|
|
243
|
+
elif addr_part.startswith("DBW"):
|
|
244
|
+
# Word address: DBW10
|
|
245
|
+
offset = int(addr_part[3:])
|
|
246
|
+
elif addr_part.startswith("DBD"):
|
|
247
|
+
# Double word address: DBD10
|
|
248
|
+
offset = int(addr_part[3:])
|
|
249
|
+
else:
|
|
250
|
+
raise ValueError(f"Invalid DB address format: {address_str}")
|
|
251
|
+
|
|
252
|
+
return S7Area.DB, db_number, offset
|
|
253
|
+
|
|
254
|
+
# Memory/Flag addresses: M10.5, MW20, etc.
|
|
255
|
+
elif address_str.startswith("M"):
|
|
256
|
+
if "." in address_str:
|
|
257
|
+
# Bit address: M10.5
|
|
258
|
+
byte_addr, bit_addr = address_str[1:].split(".")
|
|
259
|
+
_validate_bit_addr(int(bit_addr))
|
|
260
|
+
offset = int(byte_addr) * 8 + int(bit_addr)
|
|
261
|
+
elif address_str.startswith("MW"):
|
|
262
|
+
# Word address: MW20
|
|
263
|
+
offset = int(address_str[2:])
|
|
264
|
+
elif address_str.startswith("MD"):
|
|
265
|
+
# Double word address: MD20
|
|
266
|
+
offset = int(address_str[2:])
|
|
267
|
+
else:
|
|
268
|
+
# Byte address: M10
|
|
269
|
+
offset = int(address_str[1:])
|
|
270
|
+
|
|
271
|
+
return S7Area.MK, 0, offset
|
|
272
|
+
|
|
273
|
+
# Input addresses: I0.0, IW10, etc.
|
|
274
|
+
elif address_str.startswith("I"):
|
|
275
|
+
if "." in address_str:
|
|
276
|
+
# Bit address: I0.0
|
|
277
|
+
byte_addr, bit_addr = address_str[1:].split(".")
|
|
278
|
+
_validate_bit_addr(int(bit_addr))
|
|
279
|
+
offset = int(byte_addr) * 8 + int(bit_addr)
|
|
280
|
+
elif address_str.startswith("IW"):
|
|
281
|
+
# Word address: IW10
|
|
282
|
+
offset = int(address_str[2:])
|
|
283
|
+
elif address_str.startswith("ID"):
|
|
284
|
+
# Double word address: ID10
|
|
285
|
+
offset = int(address_str[2:])
|
|
286
|
+
else:
|
|
287
|
+
# Byte address: I10
|
|
288
|
+
offset = int(address_str[1:])
|
|
289
|
+
|
|
290
|
+
return S7Area.PE, 0, offset
|
|
291
|
+
|
|
292
|
+
# Output addresses: Q0.0, QW10, etc.
|
|
293
|
+
elif address_str.startswith("Q"):
|
|
294
|
+
if "." in address_str:
|
|
295
|
+
# Bit address: Q0.0
|
|
296
|
+
byte_addr, bit_addr = address_str[1:].split(".")
|
|
297
|
+
_validate_bit_addr(int(bit_addr))
|
|
298
|
+
offset = int(byte_addr) * 8 + int(bit_addr)
|
|
299
|
+
elif address_str.startswith("QW"):
|
|
300
|
+
# Word address: QW10
|
|
301
|
+
offset = int(address_str[2:])
|
|
302
|
+
elif address_str.startswith("QD"):
|
|
303
|
+
# Double word address: QD10
|
|
304
|
+
offset = int(address_str[2:])
|
|
305
|
+
else:
|
|
306
|
+
# Byte address: Q10
|
|
307
|
+
offset = int(address_str[1:])
|
|
308
|
+
|
|
309
|
+
return S7Area.PA, 0, offset
|
|
310
|
+
|
|
311
|
+
else:
|
|
312
|
+
raise ValueError(f"Unsupported address format: {address_str}")
|