python-s7comm 0.1.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.
Files changed (40) hide show
  1. python_s7comm/__init__.py +10 -0
  2. python_s7comm/async_client.py +79 -0
  3. python_s7comm/py.typed +0 -0
  4. python_s7comm/s7comm/__init__.py +6 -0
  5. python_s7comm/s7comm/async_client.py +192 -0
  6. python_s7comm/s7comm/base.py +54 -0
  7. python_s7comm/s7comm/client.py +185 -0
  8. python_s7comm/s7comm/enums.py +180 -0
  9. python_s7comm/s7comm/error_messages.py +210 -0
  10. python_s7comm/s7comm/exceptions.py +14 -0
  11. python_s7comm/s7comm/packets/__init__.py +28 -0
  12. python_s7comm/s7comm/packets/data_item.py +28 -0
  13. python_s7comm/s7comm/packets/error.py +22 -0
  14. python_s7comm/s7comm/packets/exceptions.py +14 -0
  15. python_s7comm/s7comm/packets/headers.py +96 -0
  16. python_s7comm/s7comm/packets/packet.py +39 -0
  17. python_s7comm/s7comm/packets/parser.py +62 -0
  18. python_s7comm/s7comm/packets/plc_command.py +53 -0
  19. python_s7comm/s7comm/packets/rw_variable.py +424 -0
  20. python_s7comm/s7comm/packets/setup_communication.py +75 -0
  21. python_s7comm/s7comm/packets/szl.py +97 -0
  22. python_s7comm/s7comm/packets/user_data.py +213 -0
  23. python_s7comm/s7comm/packets/variable_address.py +113 -0
  24. python_s7comm/s7comm/szl.py +97 -0
  25. python_s7comm/s7comm/transport/__init__.py +5 -0
  26. python_s7comm/s7comm/transport/cotp/__init__.py +18 -0
  27. python_s7comm/s7comm/transport/cotp/connection.py +91 -0
  28. python_s7comm/s7comm/transport/cotp/cotp.py +215 -0
  29. python_s7comm/s7comm/transport/cotp/data.py +89 -0
  30. python_s7comm/s7comm/transport/cotp/enums.py +41 -0
  31. python_s7comm/s7comm/transport/cotp/error.py +36 -0
  32. python_s7comm/s7comm/transport/cotp/parameters.py +58 -0
  33. python_s7comm/s7comm/transport/cotp/tpkt.py +44 -0
  34. python_s7comm/s7comm/transport/transport.py +15 -0
  35. python_s7comm/sync_client.py +87 -0
  36. python_s7comm-0.1.0.dist-info/METADATA +251 -0
  37. python_s7comm-0.1.0.dist-info/RECORD +40 -0
  38. python_s7comm-0.1.0.dist-info/WHEEL +5 -0
  39. python_s7comm-0.1.0.dist-info/licenses/LICENSE +21 -0
  40. python_s7comm-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,62 @@
1
+ import struct
2
+
3
+ from ..enums import FunctionCode, HeaderError, MessageType
4
+ from .error import S7Error
5
+ from .headers import S7AckDataHeader, S7Header
6
+ from .packet import S7Packet
7
+ from .rw_variable import (
8
+ ReadVariableResponse,
9
+ VariableReadRequest,
10
+ VariableWriteRequest,
11
+ WriteVariableResponse,
12
+ )
13
+ from .setup_communication import SetupCommunicationRequest
14
+ from .user_data import UserDataResponse
15
+
16
+
17
+ response_code_to_packet: dict[FunctionCode, type[S7Packet]] = {
18
+ FunctionCode.SetupCommunication: SetupCommunicationRequest,
19
+ FunctionCode.ReadVariable: ReadVariableResponse,
20
+ FunctionCode.WriteVariable: WriteVariableResponse,
21
+ }
22
+
23
+ request_code_to_packet: dict[FunctionCode, type[S7Packet]] = {
24
+ FunctionCode.SetupCommunication: SetupCommunicationRequest,
25
+ FunctionCode.ReadVariable: VariableReadRequest,
26
+ FunctionCode.WriteVariable: VariableWriteRequest,
27
+ }
28
+
29
+
30
+ class S7PacketParser:
31
+ @staticmethod
32
+ def parse(packet: bytes) -> S7Packet:
33
+ header: S7Header
34
+ response: S7Packet
35
+ protocol_id = packet[0]
36
+ if protocol_id != S7Header.PROTOCOL_ID:
37
+ raise ValueError(f"Invalid protocol id. received: {protocol_id}, expected: {S7Header.PROTOCOL_ID}")
38
+ message_type = MessageType(packet[1])
39
+
40
+ if message_type in (MessageType.Response, MessageType.Ack):
41
+ header = S7AckDataHeader.parse(packet)
42
+ error_class = HeaderError(header.error_class)
43
+ if error_class != HeaderError.NO_ERROR:
44
+ return S7Error(header=header)
45
+
46
+ function_code = struct.unpack_from("!B", packet, header.LENGTH)[0]
47
+
48
+ # parse parameter
49
+ parameter = packet[header.LENGTH :]
50
+ response = response_code_to_packet[function_code].parse(parameter)
51
+ elif message_type == MessageType.JobRequest:
52
+ header = S7Header.parse(packet)
53
+ function_code = struct.unpack_from("!B", packet, header.LENGTH)[0]
54
+ parameter = packet[header.LENGTH :]
55
+ response = request_code_to_packet[function_code].parse(parameter)
56
+ elif message_type == MessageType.Userdata:
57
+ header = S7Header.parse(packet)
58
+ response = UserDataResponse.parse(packet[header.LENGTH :])
59
+ else:
60
+ raise ValueError(f"Invalid message type: {message_type}")
61
+ response.header = header
62
+ return response
@@ -0,0 +1,53 @@
1
+ import struct
2
+
3
+ from ..enums import FunctionCode, MessageType
4
+ from .packet import S7Packet
5
+
6
+
7
+ class RequestPLCStop(S7Packet):
8
+ """
9
+ message_type: enums.MessageType = enums.MessageType.JobRequest
10
+ FUNCTION_CODE: bytes = struct.pack("!B", enums.FunctionCode.PLCStop)
11
+ UNKNOWN_5B: bytes = b"\x00\x00\x00\x00\x00"
12
+ LENGTH = b"\x09"
13
+ SERVICE_NAME = b"P_PROGRAM"
14
+ """
15
+
16
+ MESSAGE_TYPE = MessageType.JobRequest
17
+
18
+ def __init__(self) -> None:
19
+ self.header = None
20
+ self.parameter: bytes = b"\x29\x00\x00\x00\x00\x00\x09\x50\x5f\x50\x52\x4f\x47\x52\x41\x4d"
21
+ self.data = None
22
+
23
+ def serialize_parameter(self) -> bytes:
24
+ return self.parameter
25
+
26
+ def serialize_data(self) -> bytes:
27
+ return b""
28
+
29
+ @classmethod
30
+ def parse(cls, packet: bytes) -> "RequestPLCStop":
31
+ raise NotImplementedError("RequestPLCStop is created from constructor, not parsed from bytes")
32
+
33
+
34
+ class PIServiceRequest:
35
+ UNKNOWN_BYTES = b"\x00\x00\x00\x00\x00\x00\xfd"
36
+
37
+ def __init__(self, function_code: FunctionCode, parameter_block: bytes, pi_service: str) -> None:
38
+ self.function_code = function_code
39
+ self.parameter_block = parameter_block
40
+ self.pi_service = pi_service
41
+
42
+ def serialize(self) -> bytes:
43
+ string_length = len(self.pi_service)
44
+ parameter_block_length = len(self.parameter_block)
45
+ function_code = struct.pack("!B", self.function_code)
46
+ params = struct.pack(
47
+ f"!HB{parameter_block_length}BB{string_length}s",
48
+ parameter_block_length,
49
+ self.parameter_block,
50
+ string_length,
51
+ self.pi_service.encode(),
52
+ )
53
+ return function_code + self.UNKNOWN_BYTES + params
@@ -0,0 +1,424 @@
1
+ import struct
2
+ from dataclasses import dataclass
3
+ from typing import AsyncGenerator, ClassVar, Generator
4
+
5
+ from ..enums import (
6
+ Area,
7
+ DataSizeByte,
8
+ DataTransportSize,
9
+ DataType,
10
+ DataTypeTransportSize,
11
+ FunctionCode,
12
+ ItemReturnCode,
13
+ MessageType,
14
+ ParameterTransportSize,
15
+ )
16
+ from .data_item import DataItem
17
+ from .exceptions import ReadVariableException, WriteVariableException
18
+ from .headers import S7AckDataHeader, S7Header
19
+ from .packet import S7Packet
20
+ from .variable_address import VariableAddress
21
+
22
+
23
+ class RequestParameterItem:
24
+ """_summary_
25
+
26
+ ITEM_HEAD:
27
+ variable specification: 0x12
28
+ Length of following address specification: 0x0a
29
+ Syntax Id: S7ANY (0x10)
30
+
31
+ Raises:
32
+ ValueError: _description_
33
+ ValueError: _description_
34
+ ValueError: _description_
35
+
36
+ Returns:
37
+ _type_: _description_
38
+ """
39
+
40
+ ITEM_HEAD: ClassVar[bytes] = b"\x12\x0a\x10"
41
+ LENGTH = 12
42
+
43
+ def __init__(self, address: VariableAddress, transport_size: ParameterTransportSize | None = None):
44
+ # word_size is required by the request_generator to calculate maximum elements per packet
45
+ if transport_size is None:
46
+ self.transport_size = ParameterTransportSize[address.data_type.value]
47
+ else:
48
+ self.transport_size = transport_size
49
+ self.word_size = DataSizeByte[address.data_type.value]
50
+ self.length: int = address.amount
51
+ self.address = address
52
+
53
+ def serialize(self) -> bytes:
54
+ if self.address.area != Area.DB:
55
+ self.address.db_number = 0
56
+ packet = self.ITEM_HEAD + struct.pack(
57
+ "!BHHB",
58
+ self.transport_size,
59
+ self.length,
60
+ self.address.db_number,
61
+ self.address.area,
62
+ )
63
+ if self.transport_size in [ParameterTransportSize.COUNTER, ParameterTransportSize.TIMER]:
64
+ address = self.address.start
65
+ else:
66
+ address = (self.address.start << 3) + self.address.start_bit
67
+ return bytes(packet + address.to_bytes(length=3, byteorder="big"))
68
+
69
+ def validate(self) -> None:
70
+ if self.address.area != Area.DB:
71
+ self.address.db_number = 0
72
+
73
+ if self.transport_size != ParameterTransportSize.BIT and self.address.start_bit != 0:
74
+ raise ValueError(f"{self.transport_size.name} Invalid start_bit: {self.address.start_bit}. should be 0")
75
+
76
+ if self.address.db_number < 0 or self.address.db_number > 65535 or self.address.start < 0 or self.length < 1:
77
+ raise ValueError("Invalid parameters")
78
+
79
+ if self.transport_size == ParameterTransportSize.BIT and self.length > 1:
80
+ raise ValueError("Invalid transport size")
81
+
82
+ @classmethod
83
+ def parse(cls, packet: bytes) -> "RequestParameterItem":
84
+ _, transport_size, amount, db_number, area, address = struct.unpack_from("!3sBHHB3s", packet)
85
+ transport_size = ParameterTransportSize(transport_size)
86
+ start_bit = 0
87
+ start_address = int.from_bytes(address, byteorder="big")
88
+ if transport_size in [ParameterTransportSize.COUNTER, ParameterTransportSize.TIMER]:
89
+ start = start_address
90
+ else:
91
+ start = start_address >> 3
92
+ start_bit = start_address & 0x000007
93
+ data_type = DataType[transport_size.name]
94
+ variable_address = VariableAddress(
95
+ area=area,
96
+ db_number=db_number,
97
+ start=start,
98
+ data_type=data_type,
99
+ start_bit=start_bit,
100
+ amount=amount,
101
+ )
102
+ return cls(address=variable_address, transport_size=transport_size)
103
+
104
+
105
+ class VariableRequestParameter:
106
+ LENGTH = 2
107
+
108
+ def __init__(self, function_code: FunctionCode, items: list[RequestParameterItem]):
109
+ self.function_code = function_code
110
+ self.items: list[RequestParameterItem] = items
111
+
112
+ def serialize(self) -> bytes:
113
+ parameter = struct.pack("!BB", self.function_code, len(self.items))
114
+ for item in self.items:
115
+ parameter += item.serialize()
116
+ self.length = len(parameter)
117
+ return parameter
118
+
119
+
120
+ class VariableReadRequest(S7Packet):
121
+ MESSAGE_TYPE = MessageType.JobRequest
122
+ FUNCTION_CODE = FunctionCode.ReadVariable
123
+
124
+ def __init__(self, parameter: VariableRequestParameter) -> None:
125
+ self.header = None
126
+ self.parameter = parameter
127
+ self.data = None
128
+
129
+ @classmethod
130
+ def create(cls, items: list[VariableAddress]) -> "VariableReadRequest":
131
+ parameter_items = cls._create_parameter_item_list(items=items)
132
+ parameter = VariableRequestParameter(function_code=cls.FUNCTION_CODE, items=parameter_items)
133
+ return cls(parameter=parameter)
134
+
135
+ @staticmethod
136
+ def _create_parameter_item_list(items: list[VariableAddress]) -> list[RequestParameterItem]:
137
+ parameter_items = []
138
+ for item in items:
139
+ parameter_item = RequestParameterItem(address=item)
140
+ parameter_item.validate()
141
+ parameter_items.append(parameter_item)
142
+ return parameter_items
143
+
144
+ def serialize(self) -> bytes:
145
+ return self.parameter.serialize()
146
+
147
+ def request_generator(self, pdu_length: int) -> Generator["VariableReadRequest", None, None]:
148
+ """Generate multiple read requests that fit within the PDU length limit.
149
+
150
+ Splits a large read request into smaller chunks based on the maximum
151
+ payload size allowed by the negotiated PDU length.
152
+
153
+ Args:
154
+ pdu_length: Maximum PDU size negotiated during connection setup.
155
+
156
+ Yields:
157
+ VariableReadRequest: Individual requests sized to fit within pdu_length.
158
+ """
159
+ fixed_packet_part = S7AckDataHeader.LENGTH + VariableRequestParameter.LENGTH + DataItem.HEADER_LENGTH
160
+ max_payload_length = pdu_length - fixed_packet_part
161
+ parameter_item = self.parameter.items[0]
162
+ max_elements = int(max_payload_length / parameter_item.word_size)
163
+ total_elements = parameter_item.address.amount
164
+ offset = parameter_item.address.start
165
+
166
+ while total_elements > 0:
167
+ number_of_elements = max_elements if total_elements > max_elements else total_elements
168
+ parameter_item.address.start = offset
169
+ parameter_item.length = number_of_elements
170
+
171
+ request = VariableReadRequest(
172
+ parameter=VariableRequestParameter(items=[parameter_item], function_code=self.FUNCTION_CODE)
173
+ )
174
+ yield request
175
+ total_elements -= number_of_elements
176
+ offset += number_of_elements * parameter_item.word_size
177
+
178
+ async def async_request_generator(self, pdu_length: int) -> AsyncGenerator["VariableReadRequest", None]:
179
+ for item in self.request_generator(pdu_length):
180
+ yield item
181
+
182
+ @classmethod
183
+ def parse(cls, packet: bytes) -> "VariableReadRequest":
184
+ function_code, item_count = struct.unpack_from("!BB", packet)
185
+ if function_code != cls.FUNCTION_CODE.value:
186
+ raise ValueError(f"Invalid function code, got: {function_code}, expected: {cls.FUNCTION_CODE.value}")
187
+ offset = 2
188
+ items = []
189
+ for _ in range(item_count):
190
+ item = RequestParameterItem.parse(packet[offset : offset + RequestParameterItem.LENGTH])
191
+ items.append(item)
192
+ offset += RequestParameterItem.LENGTH
193
+ parameter = VariableRequestParameter(function_code=function_code, items=items)
194
+ return cls(parameter=parameter)
195
+
196
+ def serialize_parameter(self) -> bytes:
197
+ return self.parameter.serialize()
198
+
199
+ def serialize_data(self) -> bytes:
200
+ return b""
201
+
202
+
203
+ class VariableWriteRequest(S7Packet):
204
+ MESSAGE_TYPE = MessageType.JobRequest
205
+ FUNCTION_CODE = FunctionCode.WriteVariable
206
+
207
+ def __init__(self, parameter: VariableRequestParameter, data: list[DataItem]) -> None:
208
+ self.header = None
209
+ self.parameter = parameter
210
+ self.data = data
211
+
212
+ @classmethod
213
+ def create(cls, items: list[tuple[VariableAddress, bytes]]) -> "VariableWriteRequest":
214
+ parameter_items = []
215
+ data_items = []
216
+ for item, data in items:
217
+ parameter_item = RequestParameterItem(address=item)
218
+ parameter_item.validate()
219
+ parameter_items.append(parameter_item)
220
+ data_item = cls._create_data_item(parameter_item=parameter_item, data=data)
221
+ data_items.append(data_item)
222
+ parameter = VariableRequestParameter(function_code=cls.FUNCTION_CODE, items=parameter_items)
223
+ return cls(parameter=parameter, data=data_items)
224
+
225
+ @classmethod
226
+ def _create_data_item(cls, parameter_item: RequestParameterItem, data: bytes) -> DataItem:
227
+ data_transport_size = DataTypeTransportSize[parameter_item.transport_size.name].value
228
+ amount = parameter_item.address.amount
229
+ data_length = parameter_item.word_size * amount
230
+ if parameter_item.transport_size not in [
231
+ ParameterTransportSize.COUNTER,
232
+ ParameterTransportSize.TIMER,
233
+ ParameterTransportSize.REAL,
234
+ ParameterTransportSize.CHAR,
235
+ ParameterTransportSize.BIT,
236
+ ]:
237
+ data_length *= 8
238
+ return DataItem(transport_size=data_transport_size, data_length=data_length, data=data)
239
+
240
+ def request_generator(self, pdu_length: int) -> Generator["VariableWriteRequest", None, None]:
241
+ """Generate multiple write requests that fit within the PDU length limit.
242
+
243
+ Splits a large write request into smaller chunks based on the maximum
244
+ payload size allowed by the negotiated PDU length.
245
+
246
+ Args:
247
+ pdu_length: Maximum PDU size negotiated during connection setup.
248
+
249
+ Yields:
250
+ VariableWriteRequest: Individual requests sized to fit within pdu_length.
251
+ """
252
+ max_payload_length = (
253
+ pdu_length
254
+ - S7Header.LENGTH
255
+ - VariableRequestParameter.LENGTH
256
+ - RequestParameterItem.LENGTH
257
+ - DataItem.HEADER_LENGTH
258
+ )
259
+ parameter_item: RequestParameterItem = self.parameter.items[0]
260
+
261
+ max_elements = max_payload_length // parameter_item.word_size
262
+ total_elements = parameter_item.address.amount
263
+ offset = parameter_item.address.start
264
+
265
+ data_item = self.data[0]
266
+ size_unit = parameter_item.word_size.value
267
+
268
+ if parameter_item.transport_size not in [
269
+ ParameterTransportSize.COUNTER,
270
+ ParameterTransportSize.TIMER,
271
+ ParameterTransportSize.REAL,
272
+ ParameterTransportSize.CHAR,
273
+ ParameterTransportSize.BIT,
274
+ ]:
275
+ size_unit *= 8
276
+ data_offset = 0
277
+
278
+ while total_elements > 0:
279
+ number_of_elements = max_elements if total_elements > max_elements else total_elements
280
+ parameter_item.address.start = offset
281
+ parameter_item.length = number_of_elements
282
+ data_length = number_of_elements * size_unit
283
+
284
+ data = data_item.data[data_offset : data_offset + data_length]
285
+
286
+ request = VariableWriteRequest(
287
+ parameter=VariableRequestParameter(items=[parameter_item], function_code=self.FUNCTION_CODE),
288
+ data=[DataItem(transport_size=data_item.transport_size, data_length=data_length, data=data)],
289
+ )
290
+ yield request
291
+ total_elements -= number_of_elements
292
+ offset += number_of_elements * parameter_item.word_size
293
+ data_offset += number_of_elements * size_unit
294
+
295
+ async def async_request_generator(self, pdu_length: int) -> AsyncGenerator["VariableWriteRequest", None]:
296
+ for item in self.request_generator(pdu_length):
297
+ yield item
298
+
299
+ @classmethod
300
+ def parse(cls, packet: bytes) -> "VariableWriteRequest":
301
+ function_code, item_count = struct.unpack_from("!BB", packet)
302
+ if function_code != cls.FUNCTION_CODE.value:
303
+ raise ValueError(f"Invalid function code, got: {function_code}, expected: {cls.FUNCTION_CODE.value}")
304
+ offset = 2
305
+ parameter_items = []
306
+ data_items = []
307
+ for _ in range(item_count):
308
+ item = RequestParameterItem.parse(packet[offset : offset + RequestParameterItem.LENGTH])
309
+ parameter_items.append(item)
310
+ offset += RequestParameterItem.LENGTH
311
+ for _ in range(item_count):
312
+ data_item = DataItem.parse(packet[offset:])
313
+ offset += data_item.HEADER_LENGTH + data_item.data_length
314
+ data_items.append(data_item)
315
+ parameter = VariableRequestParameter(function_code=function_code, items=parameter_items)
316
+ return cls(parameter=parameter, data=data_items)
317
+
318
+ def serialize_parameter(self) -> bytes:
319
+ return self.parameter.serialize()
320
+
321
+ def serialize_data(self) -> bytes:
322
+ return b"".join(data_item.serialize() for data_item in self.data)
323
+
324
+
325
+ @dataclass
326
+ class VariableParameter:
327
+ function_code: FunctionCode
328
+ item_count: int
329
+
330
+ def serialize(self) -> bytes:
331
+ return struct.pack("!BB", self.function_code, self.item_count)
332
+
333
+
334
+ class ReadVariableResponse(S7Packet):
335
+ MESSAGE_TYPE = MessageType.Response
336
+
337
+ def __init__(self, parameter: VariableParameter, data: list[DataItem]) -> None:
338
+ self.header = None
339
+ self.parameter = parameter
340
+ self.data = data
341
+
342
+ @classmethod
343
+ def parse(cls, packet: bytes) -> "ReadVariableResponse":
344
+ function_code, item_count = struct.unpack_from("!BB", packet)
345
+ offset = 2
346
+ items = []
347
+ for _ in range(item_count):
348
+ return_code, transport_size, data_length = struct.unpack_from("!BBH", packet, offset=offset)
349
+ offset = offset + 4
350
+ data_size = data_length
351
+ if transport_size not in [
352
+ DataTransportSize.OCTET,
353
+ DataTransportSize.REAL,
354
+ DataTransportSize.BIT,
355
+ ]:
356
+ data_size = int(data_length / 8)
357
+
358
+ data = packet[offset : offset + data_size]
359
+ item = DataItem(
360
+ return_code=ItemReturnCode(return_code),
361
+ transport_size=DataTransportSize(transport_size),
362
+ data_length=data_size,
363
+ data=data,
364
+ )
365
+ items.append(item)
366
+ offset = offset + data_size
367
+ parameter = VariableParameter(function_code=function_code, item_count=item_count)
368
+ return cls(parameter=parameter, data=items)
369
+
370
+ def values(self) -> list[bytes]:
371
+ """Extract and return data values from the response.
372
+
373
+ Returns:
374
+ list[bytes]: List of raw data bytes from each response item.
375
+
376
+ Raises:
377
+ ReadVariableException: If any item in the response has a non-success return code.
378
+ """
379
+ result = []
380
+ for item in self.data:
381
+ if item.return_code != ItemReturnCode.SUCCESS:
382
+ raise ReadVariableException(f"ReadVariableResponseItem return code: {item.return_code}", response=self)
383
+ result.append(item.data)
384
+ return result
385
+
386
+ def serialize_parameter(self) -> bytes:
387
+ return self.parameter.serialize()
388
+
389
+ def serialize_data(self) -> bytes:
390
+ return b"".join(item.serialize() for item in self.data)
391
+
392
+
393
+ class WriteVariableResponse(S7Packet):
394
+ MESSAGE_TYPE = MessageType.Response
395
+
396
+ def __init__(self, parameter: VariableParameter, data: list[ItemReturnCode]) -> None:
397
+ self.header = None
398
+ self.parameter = parameter
399
+ self.data: list[ItemReturnCode] = data
400
+
401
+ @classmethod
402
+ def parse(cls, packet: bytes) -> "WriteVariableResponse":
403
+ function_code, item_count = struct.unpack_from("!BB", packet)
404
+ offset = 2
405
+ parameter = VariableParameter(function_code=function_code, item_count=item_count)
406
+ data: list[ItemReturnCode] = [ItemReturnCode(item) for item in packet[offset:item_count]]
407
+ return cls(parameter=parameter, data=data)
408
+
409
+ def serialize_parameter(self) -> bytes:
410
+ return self.parameter.serialize()
411
+
412
+ def serialize_data(self) -> bytes:
413
+ return struct.pack(f"!{len(self.data)}B", *self.data)
414
+
415
+ def check_result(self) -> None:
416
+ """Check the result of each write operation.
417
+
418
+ Raises:
419
+ WriteVariableException: If any item in the response has a non-success return code.
420
+ """
421
+ for item in self.data:
422
+ if item != ItemReturnCode.SUCCESS:
423
+ raise WriteVariableException(f"WriteVariableResponseItem return code: {item}", response=self)
424
+ return None
@@ -0,0 +1,75 @@
1
+ import struct
2
+
3
+ from ..enums import MessageType
4
+ from .packet import S7Packet
5
+
6
+
7
+ class SetupCommunicationParameter:
8
+ PACKET_STRUCT = struct.Struct("!BBHHH")
9
+ FUNCTION_CODE = 0xF0
10
+
11
+ def __init__(self, max_amq_caller_ack: int = 0x0001, max_amq_callee_ack: int = 0x0001, pdu_length: int = 0x01E0):
12
+ self.function_code: int = self.FUNCTION_CODE
13
+ self.reserved: int = 0x00
14
+ self.max_amq_caller_ack: int = max_amq_caller_ack
15
+ self.max_amq_callee_ack: int = max_amq_callee_ack
16
+ self.pdu_length: int = pdu_length
17
+
18
+ @classmethod
19
+ def parse(cls, packet: bytes) -> "SetupCommunicationParameter":
20
+ function_code, _, max_amq_caller_ack, max_amq_callee_ack, pdu_length = cls.PACKET_STRUCT.unpack_from(packet, 0)
21
+ if function_code != cls.FUNCTION_CODE:
22
+ raise ValueError(f"Invalid function code. receive: {function_code}, expected: {cls.FUNCTION_CODE}")
23
+ return cls(max_amq_caller_ack=max_amq_caller_ack, max_amq_callee_ack=max_amq_callee_ack, pdu_length=pdu_length)
24
+
25
+ def serialize(self) -> bytes:
26
+ return self.PACKET_STRUCT.pack(
27
+ self.FUNCTION_CODE,
28
+ self.reserved,
29
+ self.max_amq_caller_ack,
30
+ self.max_amq_callee_ack,
31
+ self.pdu_length,
32
+ )
33
+
34
+
35
+ class SetupCommunicationRequest(S7Packet):
36
+ MESSAGE_TYPE = MessageType.JobRequest
37
+
38
+ def __init__(self, parameter: SetupCommunicationParameter):
39
+ self.header = None
40
+ self.parameter = parameter
41
+ self.data = None
42
+
43
+ def serialize_parameter(self) -> bytes:
44
+ return self.parameter.serialize()
45
+
46
+ def serialize_data(self) -> bytes:
47
+ return b""
48
+
49
+ @classmethod
50
+ def parse(cls, packet: bytes) -> "SetupCommunicationRequest":
51
+ return cls(parameter=SetupCommunicationParameter.parse(packet=packet))
52
+
53
+
54
+ class SetupCommunicationResponse(S7Packet):
55
+ MESSAGE_TYPE = MessageType.Response
56
+
57
+ def __init__(self, parameter: SetupCommunicationParameter) -> None:
58
+ self.parameter = parameter
59
+
60
+ @classmethod
61
+ def create(
62
+ cls,
63
+ max_amq_caller_ack: int = 0x0001,
64
+ max_amq_callee_ack: int = 0x0001,
65
+ pdu_length: int = 0x00F0,
66
+ ) -> "SetupCommunicationResponse":
67
+ parameter = SetupCommunicationParameter(
68
+ max_amq_caller_ack=max_amq_caller_ack,
69
+ max_amq_callee_ack=max_amq_callee_ack,
70
+ pdu_length=pdu_length,
71
+ )
72
+ return cls(parameter=parameter)
73
+
74
+ def serialize(self) -> bytes:
75
+ return self.parameter.serialize()