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.
- python_s7comm/__init__.py +10 -0
- python_s7comm/async_client.py +79 -0
- python_s7comm/py.typed +0 -0
- python_s7comm/s7comm/__init__.py +6 -0
- python_s7comm/s7comm/async_client.py +192 -0
- python_s7comm/s7comm/base.py +54 -0
- python_s7comm/s7comm/client.py +185 -0
- python_s7comm/s7comm/enums.py +180 -0
- python_s7comm/s7comm/error_messages.py +210 -0
- python_s7comm/s7comm/exceptions.py +14 -0
- python_s7comm/s7comm/packets/__init__.py +28 -0
- python_s7comm/s7comm/packets/data_item.py +28 -0
- python_s7comm/s7comm/packets/error.py +22 -0
- python_s7comm/s7comm/packets/exceptions.py +14 -0
- python_s7comm/s7comm/packets/headers.py +96 -0
- python_s7comm/s7comm/packets/packet.py +39 -0
- python_s7comm/s7comm/packets/parser.py +62 -0
- python_s7comm/s7comm/packets/plc_command.py +53 -0
- python_s7comm/s7comm/packets/rw_variable.py +424 -0
- python_s7comm/s7comm/packets/setup_communication.py +75 -0
- python_s7comm/s7comm/packets/szl.py +97 -0
- python_s7comm/s7comm/packets/user_data.py +213 -0
- python_s7comm/s7comm/packets/variable_address.py +113 -0
- python_s7comm/s7comm/szl.py +97 -0
- python_s7comm/s7comm/transport/__init__.py +5 -0
- python_s7comm/s7comm/transport/cotp/__init__.py +18 -0
- python_s7comm/s7comm/transport/cotp/connection.py +91 -0
- python_s7comm/s7comm/transport/cotp/cotp.py +215 -0
- python_s7comm/s7comm/transport/cotp/data.py +89 -0
- python_s7comm/s7comm/transport/cotp/enums.py +41 -0
- python_s7comm/s7comm/transport/cotp/error.py +36 -0
- python_s7comm/s7comm/transport/cotp/parameters.py +58 -0
- python_s7comm/s7comm/transport/cotp/tpkt.py +44 -0
- python_s7comm/s7comm/transport/transport.py +15 -0
- python_s7comm/sync_client.py +87 -0
- python_s7comm-0.1.0.dist-info/METADATA +251 -0
- python_s7comm-0.1.0.dist-info/RECORD +40 -0
- python_s7comm-0.1.0.dist-info/WHEEL +5 -0
- python_s7comm-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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()
|