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.
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}")