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,79 @@
|
|
|
1
|
+
from .s7comm import AsyncS7Comm, enums
|
|
2
|
+
from .s7comm.packets.variable_address import VariableAddress
|
|
3
|
+
from .s7comm.szl import (
|
|
4
|
+
CPUStateDataTree,
|
|
5
|
+
ModuleIdentificationDataTree,
|
|
6
|
+
ModuleIdentificationIndex,
|
|
7
|
+
SZLResponseData,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncClient:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
tpdu_size: int = 1024, # cotp packet length
|
|
15
|
+
pdu_length: int = 480, # s7comm packet length
|
|
16
|
+
source_tsap: int = 0x0100,
|
|
17
|
+
dest_tsap: int = 0x0101,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.s7comm = AsyncS7Comm(
|
|
20
|
+
tpdu_size=tpdu_size,
|
|
21
|
+
pdu_length=pdu_length,
|
|
22
|
+
source_tsap=source_tsap,
|
|
23
|
+
dest_tsap=dest_tsap,
|
|
24
|
+
)
|
|
25
|
+
self.is_connected = False
|
|
26
|
+
|
|
27
|
+
async def connect(self, address: str, rack: int, slot: int, port: int = 102) -> None:
|
|
28
|
+
await self.s7comm.connect(address=address, rack=rack, slot=slot, port=port)
|
|
29
|
+
self.is_connected = True
|
|
30
|
+
|
|
31
|
+
async def disconnect(self) -> None:
|
|
32
|
+
await self.close()
|
|
33
|
+
self.is_connected = False
|
|
34
|
+
|
|
35
|
+
def get_pdu_length(self) -> int:
|
|
36
|
+
return self.s7comm.pdu_length
|
|
37
|
+
|
|
38
|
+
async def close(self) -> None:
|
|
39
|
+
await self.s7comm.close()
|
|
40
|
+
|
|
41
|
+
async def get_cpu_state(self) -> enums.CPUStatus:
|
|
42
|
+
response = await self.s7comm.read_szl(szl_id=0x0424, szl_index=0x0000)
|
|
43
|
+
response_data = SZLResponseData.parse(response.data.data)
|
|
44
|
+
cpu_state = CPUStateDataTree.parse(response_data.szl_data_tree_list[0])
|
|
45
|
+
return cpu_state.requested_mode
|
|
46
|
+
|
|
47
|
+
async def read_area(self, address: str) -> bytes:
|
|
48
|
+
return await self.s7comm.read_area(VariableAddress.from_string(address))
|
|
49
|
+
|
|
50
|
+
async def write_area(self, address: str, data: bytes) -> None:
|
|
51
|
+
await self.s7comm.write_area(address=VariableAddress.from_string(address), data=data)
|
|
52
|
+
|
|
53
|
+
async def get_order_code(self) -> str | None:
|
|
54
|
+
response = await self.s7comm.read_szl(szl_id=0x0011, szl_index=0x0000)
|
|
55
|
+
szl_data = SZLResponseData.parse(response.data.data)
|
|
56
|
+
for date_tree in szl_data.szl_data_tree_list:
|
|
57
|
+
module_identification = ModuleIdentificationDataTree.parse(date_tree)
|
|
58
|
+
if module_identification.index == ModuleIdentificationIndex.MODULE_IDENTIFICATION:
|
|
59
|
+
return module_identification.order_number
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
async def read_szl(self, szl_id: int, szl_index: int = 0x0000) -> SZLResponseData:
|
|
63
|
+
response_data = await self.s7comm.read_szl(szl_id=szl_id, szl_index=szl_index)
|
|
64
|
+
return SZLResponseData.parse(response_data.data.data)
|
|
65
|
+
|
|
66
|
+
async def read_szl_list(self) -> SZLResponseData:
|
|
67
|
+
response_data = await self.s7comm.read_szl(szl_id=0x0000, szl_index=0x0000)
|
|
68
|
+
return SZLResponseData.parse(response_data.data.data)
|
|
69
|
+
|
|
70
|
+
async def read_multi_vars(self, items: list[str]) -> list[bytes]:
|
|
71
|
+
vars_ = [VariableAddress.from_string(item) for item in items]
|
|
72
|
+
response = await self.s7comm.read_multi_vars(items=vars_)
|
|
73
|
+
return response.values()
|
|
74
|
+
|
|
75
|
+
async def write_multi_vars(self, items: list[tuple[str, bytes]]) -> None:
|
|
76
|
+
vars_ = [(VariableAddress.from_string(address), data) for address, data in items]
|
|
77
|
+
response = await self.s7comm.write_multi_vars(items=vars_)
|
|
78
|
+
response.check_result()
|
|
79
|
+
return None
|
python_s7comm/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import struct
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
from .base import BaseS7Comm
|
|
7
|
+
from .enums import HeaderError, ItemReturnCode, MessageType, SubfunctionCode, UserdataFunction, UserdataLastPDU
|
|
8
|
+
from .error_messages import ERROR_MESSAGES
|
|
9
|
+
from .exceptions import PacketLostError, StalePacketError
|
|
10
|
+
from .packets import (
|
|
11
|
+
RequestPLCStop,
|
|
12
|
+
S7AckDataHeader,
|
|
13
|
+
S7Packet,
|
|
14
|
+
SetupCommunicationRequest,
|
|
15
|
+
UserDataContinuationRequest,
|
|
16
|
+
UserDataRequest,
|
|
17
|
+
UserDataResponse,
|
|
18
|
+
VariableAddress,
|
|
19
|
+
VariableReadRequest,
|
|
20
|
+
VariableWriteRequest,
|
|
21
|
+
)
|
|
22
|
+
from .packets.exceptions import ReadVariableException
|
|
23
|
+
from .packets.parser import S7PacketParser
|
|
24
|
+
from .packets.rw_variable import ReadVariableResponse, WriteVariableResponse
|
|
25
|
+
from .transport import AsyncCOTP, AsyncTransport
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncS7Comm(BaseS7Comm):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
tpdu_size: int = 1024,
|
|
35
|
+
pdu_length: int = 480,
|
|
36
|
+
source_tsap: int = 0x0100,
|
|
37
|
+
dest_tsap: int = 0x0101,
|
|
38
|
+
transport: AsyncTransport | None = None,
|
|
39
|
+
):
|
|
40
|
+
super().__init__(pdu_length=pdu_length)
|
|
41
|
+
self.transport: AsyncTransport
|
|
42
|
+
if transport is None:
|
|
43
|
+
self.transport = AsyncCOTP(tpdu_size=tpdu_size, source_tsap=source_tsap, dest_tsap=dest_tsap)
|
|
44
|
+
else:
|
|
45
|
+
self.transport = transport
|
|
46
|
+
self.lock = asyncio.Lock()
|
|
47
|
+
|
|
48
|
+
async def connect(
|
|
49
|
+
self,
|
|
50
|
+
address: str,
|
|
51
|
+
rack: int,
|
|
52
|
+
slot: int,
|
|
53
|
+
port: int = 102,
|
|
54
|
+
max_amq_caller_ack: int = 0x0001,
|
|
55
|
+
max_amq_callee_ack: int = 0x0001,
|
|
56
|
+
pdu_length: int = 0x01E0,
|
|
57
|
+
) -> None:
|
|
58
|
+
await self.transport.connect(address=address, rack=rack, slot=slot, port=port)
|
|
59
|
+
|
|
60
|
+
request = self._create_communication_request(
|
|
61
|
+
max_amq_caller_ack=max_amq_caller_ack,
|
|
62
|
+
max_amq_callee_ack=max_amq_callee_ack,
|
|
63
|
+
pdu_length=pdu_length,
|
|
64
|
+
)
|
|
65
|
+
response = await self.send(request=request)
|
|
66
|
+
if not isinstance(response, SetupCommunicationRequest):
|
|
67
|
+
raise ValueError("Invalid packet")
|
|
68
|
+
self.pdu_length = response.parameter.pdu_length
|
|
69
|
+
|
|
70
|
+
async def send(self, request: S7Packet) -> S7Packet:
|
|
71
|
+
async with self.lock:
|
|
72
|
+
await self._send_raw(request)
|
|
73
|
+
response = await self._receive()
|
|
74
|
+
|
|
75
|
+
# Validate PDU reference, retry receive if stale packet
|
|
76
|
+
while True:
|
|
77
|
+
if response.header is None:
|
|
78
|
+
raise ValueError("Response header is None")
|
|
79
|
+
try:
|
|
80
|
+
self._validate_pdu_reference(response=response)
|
|
81
|
+
break
|
|
82
|
+
except StalePacketError:
|
|
83
|
+
logger.warning("Dropping stale packet")
|
|
84
|
+
response = await self._receive() # retry receive
|
|
85
|
+
continue
|
|
86
|
+
except PacketLostError:
|
|
87
|
+
raise # unrecoverable
|
|
88
|
+
|
|
89
|
+
if response.header.message_type == MessageType.Userdata and isinstance(response, UserDataResponse):
|
|
90
|
+
accumulated_data = response.data.data
|
|
91
|
+
while response.parameter.last_data_unit == UserdataLastPDU.NO:
|
|
92
|
+
continuation_request = UserDataContinuationRequest.from_response(response)
|
|
93
|
+
await self._send_raw(continuation_request)
|
|
94
|
+
next_response = await self._receive()
|
|
95
|
+
if not isinstance(next_response, UserDataResponse):
|
|
96
|
+
raise ValueError("Expected UserDataResponse for continuation")
|
|
97
|
+
accumulated_data += next_response.data.data
|
|
98
|
+
response = next_response
|
|
99
|
+
response.data.data = accumulated_data
|
|
100
|
+
# Check Error
|
|
101
|
+
if isinstance(response.header, S7AckDataHeader) and response.header.error_code != HeaderError.NO_ERROR:
|
|
102
|
+
error_message = ERROR_MESSAGES.get(0x81 << 8 | 0x04, "Unknown Error")
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Error class {response.header.error_class}, "
|
|
105
|
+
f"error code: f{response.header.error_code}, "
|
|
106
|
+
f"error message: {error_message}"
|
|
107
|
+
)
|
|
108
|
+
return response
|
|
109
|
+
|
|
110
|
+
async def _send_raw(self, request: S7Packet) -> None:
|
|
111
|
+
packet = self._create_packet(request=request, pdu_reference=self.pdu_reference)
|
|
112
|
+
await self.transport.send(payload=packet)
|
|
113
|
+
|
|
114
|
+
async def _receive(self) -> S7Packet:
|
|
115
|
+
"""Receive and parse S7 packet from transport."""
|
|
116
|
+
return S7PacketParser.parse(await self.transport.receive())
|
|
117
|
+
|
|
118
|
+
async def close(self) -> None:
|
|
119
|
+
await self.transport.close()
|
|
120
|
+
|
|
121
|
+
async def read_area(self, address: VariableAddress) -> bytes:
|
|
122
|
+
result = bytes()
|
|
123
|
+
main_request = VariableReadRequest.create(items=[address])
|
|
124
|
+
|
|
125
|
+
async for request in main_request.async_request_generator(pdu_length=self.pdu_length):
|
|
126
|
+
response = await self.send(request=request)
|
|
127
|
+
if isinstance(response, ReadVariableResponse):
|
|
128
|
+
for response_item in response.data:
|
|
129
|
+
if response_item.return_code != ItemReturnCode.SUCCESS:
|
|
130
|
+
raise ReadVariableException(
|
|
131
|
+
f"DataItem return code: {response_item.return_code}", response=response
|
|
132
|
+
)
|
|
133
|
+
result += response_item.data
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
async def write_area(self, address: VariableAddress, data: bytes) -> WriteVariableResponse:
|
|
137
|
+
"""
|
|
138
|
+
Writes data to PLC memory area with automatic splitting into multiple requests
|
|
139
|
+
if data exceeds PDU size.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
address: Memory area address (e.g., "M0 INT 1")
|
|
143
|
+
data: Data to write
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
WriteVariableResponse with operation result
|
|
147
|
+
"""
|
|
148
|
+
main_request = VariableWriteRequest.create(items=[(address, data)])
|
|
149
|
+
for request in main_request.request_generator(pdu_length=self.pdu_length):
|
|
150
|
+
response = await self.send(request=request)
|
|
151
|
+
if not isinstance(response, WriteVariableResponse):
|
|
152
|
+
raise ValueError("Invalid response class")
|
|
153
|
+
response.check_result()
|
|
154
|
+
# TODO: combine all separate requests into one common initial request and return
|
|
155
|
+
return cast(WriteVariableResponse, response)
|
|
156
|
+
|
|
157
|
+
async def read_multi_vars(self, items: list[VariableAddress]) -> ReadVariableResponse:
|
|
158
|
+
if len(items) > self.MAX_VARS:
|
|
159
|
+
raise ValueError("Too many items")
|
|
160
|
+
|
|
161
|
+
request = VariableReadRequest.create(items=items)
|
|
162
|
+
response = await self.send(request=request)
|
|
163
|
+
if not isinstance(response, ReadVariableResponse):
|
|
164
|
+
raise ValueError("Invalid response class")
|
|
165
|
+
return response
|
|
166
|
+
|
|
167
|
+
async def write_multi_vars(self, items: list[tuple[VariableAddress, bytes]]) -> WriteVariableResponse:
|
|
168
|
+
if len(items) > self.MAX_VARS:
|
|
169
|
+
raise ValueError("Too many items")
|
|
170
|
+
|
|
171
|
+
request = VariableWriteRequest.create(items=items)
|
|
172
|
+
response = await self.send(request=request)
|
|
173
|
+
if not isinstance(response, WriteVariableResponse):
|
|
174
|
+
raise ValueError("Invalid response class")
|
|
175
|
+
return response
|
|
176
|
+
|
|
177
|
+
async def read_szl(self, szl_id: int, szl_index: int) -> UserDataResponse:
|
|
178
|
+
data = struct.pack("!HH", szl_id, szl_index)
|
|
179
|
+
request = UserDataRequest.create(
|
|
180
|
+
function_group=UserdataFunction.CPU_FUNCTION,
|
|
181
|
+
subfunction=SubfunctionCode.READ_SZL,
|
|
182
|
+
length=len(data),
|
|
183
|
+
data=data,
|
|
184
|
+
)
|
|
185
|
+
response = await self.send(request=request)
|
|
186
|
+
if not isinstance(response, UserDataResponse):
|
|
187
|
+
raise ValueError("Invalid response class")
|
|
188
|
+
return response
|
|
189
|
+
|
|
190
|
+
async def plc_stop(self) -> S7Packet:
|
|
191
|
+
response = await self.send(request=RequestPLCStop())
|
|
192
|
+
return response
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from .exceptions import PacketLostError, StalePacketError
|
|
2
|
+
from .packets import (
|
|
3
|
+
S7Header,
|
|
4
|
+
S7Packet,
|
|
5
|
+
SetupCommunicationParameter,
|
|
6
|
+
SetupCommunicationRequest,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseS7Comm:
|
|
11
|
+
MAX_VARS = 20 # Max vars that can be transferred with MultiRead/MultiWrite
|
|
12
|
+
|
|
13
|
+
def __init__(self, pdu_length: int = 480) -> None:
|
|
14
|
+
self._pdu_reference = -1
|
|
15
|
+
self.pdu_length = pdu_length
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def pdu_reference(self) -> int:
|
|
19
|
+
self._pdu_reference = (self._pdu_reference + 1) & 0xFFFF
|
|
20
|
+
return self._pdu_reference
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _create_communication_request(
|
|
24
|
+
max_amq_caller_ack: int = 0x0001,
|
|
25
|
+
max_amq_callee_ack: int = 0x0001,
|
|
26
|
+
pdu_length: int = 0x01E0,
|
|
27
|
+
) -> SetupCommunicationRequest:
|
|
28
|
+
parameter = SetupCommunicationParameter(
|
|
29
|
+
max_amq_caller_ack=max_amq_caller_ack,
|
|
30
|
+
max_amq_callee_ack=max_amq_callee_ack,
|
|
31
|
+
pdu_length=pdu_length,
|
|
32
|
+
)
|
|
33
|
+
return SetupCommunicationRequest(parameter=parameter)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _create_packet(request: S7Packet, pdu_reference: int) -> bytes:
|
|
37
|
+
parameter = request.serialize_parameter()
|
|
38
|
+
data = request.serialize_data()
|
|
39
|
+
header = S7Header(
|
|
40
|
+
pdu_reference=pdu_reference,
|
|
41
|
+
message_type=request.MESSAGE_TYPE,
|
|
42
|
+
parameter_length=len(parameter),
|
|
43
|
+
data_length=len(data),
|
|
44
|
+
)
|
|
45
|
+
request.header = header
|
|
46
|
+
return bytes(header.serialize() + parameter + data)
|
|
47
|
+
|
|
48
|
+
def _validate_pdu_reference(self, response: S7Packet) -> None:
|
|
49
|
+
"""Raises if PDU reference is invalid."""
|
|
50
|
+
assert response.header is not None
|
|
51
|
+
if response.header.pdu_reference > self._pdu_reference:
|
|
52
|
+
raise PacketLostError(f"Expected {self._pdu_reference}, got {response.header.pdu_reference}")
|
|
53
|
+
elif response.header.pdu_reference < self._pdu_reference:
|
|
54
|
+
raise StalePacketError(f"Stale packet: {response.header.pdu_reference}")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import struct
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from .base import BaseS7Comm
|
|
6
|
+
from .enums import HeaderError, ItemReturnCode, MessageType, SubfunctionCode, UserdataFunction, UserdataLastPDU
|
|
7
|
+
from .error_messages import ERROR_MESSAGES
|
|
8
|
+
from .exceptions import PacketLostError, StalePacketError
|
|
9
|
+
from .packets import (
|
|
10
|
+
RequestPLCStop,
|
|
11
|
+
S7AckDataHeader,
|
|
12
|
+
S7Packet,
|
|
13
|
+
SetupCommunicationRequest,
|
|
14
|
+
UserDataContinuationRequest,
|
|
15
|
+
UserDataRequest,
|
|
16
|
+
UserDataResponse,
|
|
17
|
+
VariableAddress,
|
|
18
|
+
VariableReadRequest,
|
|
19
|
+
VariableWriteRequest,
|
|
20
|
+
)
|
|
21
|
+
from .packets.exceptions import ReadVariableException
|
|
22
|
+
from .packets.parser import S7PacketParser
|
|
23
|
+
from .packets.rw_variable import ReadVariableResponse, WriteVariableResponse
|
|
24
|
+
from .transport import COTP, Transport
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class S7Comm(BaseS7Comm):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
tpdu_size: int = 1024,
|
|
34
|
+
pdu_length: int = 480,
|
|
35
|
+
source_tsap: int = 0x0100,
|
|
36
|
+
dest_tsap: int = 0x0101,
|
|
37
|
+
transport: Transport | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(pdu_length=pdu_length)
|
|
40
|
+
self.transport: Transport
|
|
41
|
+
if transport is None:
|
|
42
|
+
self.transport = COTP(tpdu_size=tpdu_size, source_tsap=source_tsap, dest_tsap=dest_tsap)
|
|
43
|
+
else:
|
|
44
|
+
self.transport = transport
|
|
45
|
+
|
|
46
|
+
def read_multi_vars(self, items: list[VariableAddress]) -> ReadVariableResponse:
|
|
47
|
+
if len(items) > self.MAX_VARS:
|
|
48
|
+
raise ValueError("Too many items")
|
|
49
|
+
|
|
50
|
+
request = VariableReadRequest.create(items=items)
|
|
51
|
+
response = self.send(request=request)
|
|
52
|
+
if not isinstance(response, ReadVariableResponse):
|
|
53
|
+
raise ValueError("Invalid response class")
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
def write_multi_vars(self, items: list[tuple[VariableAddress, bytes]]) -> WriteVariableResponse:
|
|
57
|
+
if len(items) > self.MAX_VARS:
|
|
58
|
+
raise ValueError("Too many items")
|
|
59
|
+
|
|
60
|
+
request = VariableWriteRequest.create(items=items)
|
|
61
|
+
response = self.send(request=request)
|
|
62
|
+
if not isinstance(response, WriteVariableResponse):
|
|
63
|
+
raise ValueError("Invalid response class")
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
def read_area(self, address: VariableAddress) -> bytes:
|
|
67
|
+
result = bytes()
|
|
68
|
+
main_request = VariableReadRequest.create(items=[address])
|
|
69
|
+
|
|
70
|
+
for request in main_request.request_generator(pdu_length=self.pdu_length):
|
|
71
|
+
response = self.send(request=request)
|
|
72
|
+
if isinstance(response, ReadVariableResponse):
|
|
73
|
+
for response_item in response.data:
|
|
74
|
+
if response_item.return_code != ItemReturnCode.SUCCESS:
|
|
75
|
+
raise ReadVariableException(
|
|
76
|
+
f"DataItem return code: {response_item.return_code}", response=response
|
|
77
|
+
)
|
|
78
|
+
result += response_item.data
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
def write_area(self, address: VariableAddress, data: bytes) -> WriteVariableResponse:
|
|
82
|
+
"""
|
|
83
|
+
Writes data to PLC memory area with automatic splitting into multiple requests
|
|
84
|
+
if data exceeds PDU size.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
address: Memory area address (e.g., "M0 INT 1")
|
|
88
|
+
data: Data to write
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
WriteVariableResponse with operation result
|
|
92
|
+
"""
|
|
93
|
+
main_request = VariableWriteRequest.create(items=[(address, data)])
|
|
94
|
+
for request in main_request.request_generator(pdu_length=self.pdu_length):
|
|
95
|
+
response = self.send(request=request)
|
|
96
|
+
if not isinstance(response, WriteVariableResponse):
|
|
97
|
+
raise ValueError("Invalid response class")
|
|
98
|
+
response.check_result()
|
|
99
|
+
# TODO: combine all separate requests into one common initial request and return
|
|
100
|
+
return cast(WriteVariableResponse, response)
|
|
101
|
+
|
|
102
|
+
def connect(
|
|
103
|
+
self,
|
|
104
|
+
address: str,
|
|
105
|
+
rack: int,
|
|
106
|
+
slot: int,
|
|
107
|
+
port: int = 102,
|
|
108
|
+
max_amq_caller_ack: int = 0x0001,
|
|
109
|
+
max_amq_callee_ack: int = 0x0001,
|
|
110
|
+
pdu_length: int = 0x01E0,
|
|
111
|
+
) -> None:
|
|
112
|
+
self.transport.connect(address=address, rack=rack, slot=slot, port=port)
|
|
113
|
+
request = self._create_communication_request(
|
|
114
|
+
max_amq_caller_ack=max_amq_caller_ack,
|
|
115
|
+
max_amq_callee_ack=max_amq_callee_ack,
|
|
116
|
+
pdu_length=pdu_length,
|
|
117
|
+
)
|
|
118
|
+
response = self.send(request=request)
|
|
119
|
+
if not isinstance(response, SetupCommunicationRequest):
|
|
120
|
+
raise ValueError("Invalid packet")
|
|
121
|
+
self.pdu_length = response.parameter.pdu_length
|
|
122
|
+
|
|
123
|
+
def _send_raw(self, request: S7Packet) -> None:
|
|
124
|
+
packet = self._create_packet(request=request, pdu_reference=self.pdu_reference)
|
|
125
|
+
self.transport.send(payload=packet)
|
|
126
|
+
|
|
127
|
+
def send(self, request: S7Packet) -> S7Packet:
|
|
128
|
+
self._send_raw(request)
|
|
129
|
+
response = self._receive()
|
|
130
|
+
|
|
131
|
+
# Validate PDU reference, retry receive if stale packet
|
|
132
|
+
while True:
|
|
133
|
+
if response.header is None:
|
|
134
|
+
raise ValueError("Response header is None")
|
|
135
|
+
try:
|
|
136
|
+
self._validate_pdu_reference(response=response)
|
|
137
|
+
break
|
|
138
|
+
except StalePacketError:
|
|
139
|
+
logger.warning("Dropping stale packet")
|
|
140
|
+
response = self._receive() # retry receive
|
|
141
|
+
continue
|
|
142
|
+
except PacketLostError:
|
|
143
|
+
raise # unrecoverable
|
|
144
|
+
|
|
145
|
+
if response.header.message_type == MessageType.Userdata and isinstance(response, UserDataResponse):
|
|
146
|
+
accumulated_data = response.data.data
|
|
147
|
+
while response.parameter.last_data_unit == UserdataLastPDU.NO:
|
|
148
|
+
continuation_request = UserDataContinuationRequest.from_response(response)
|
|
149
|
+
self._send_raw(continuation_request)
|
|
150
|
+
next_response = self._receive()
|
|
151
|
+
if not isinstance(next_response, UserDataResponse):
|
|
152
|
+
raise ValueError("Expected UserDataResponse for continuation")
|
|
153
|
+
accumulated_data += next_response.data.data
|
|
154
|
+
response = next_response
|
|
155
|
+
response.data.data = accumulated_data
|
|
156
|
+
# Check Error
|
|
157
|
+
if isinstance(response.header, S7AckDataHeader) and response.header.error_code != HeaderError.NO_ERROR:
|
|
158
|
+
error_message = ERROR_MESSAGES.get(0x81 << 8 | 0x04, "Unknown Error")
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Error class {response.header.error_class}, "
|
|
161
|
+
f"error code: f{response.header.error_code}, "
|
|
162
|
+
f"error message: {error_message}"
|
|
163
|
+
)
|
|
164
|
+
return response
|
|
165
|
+
|
|
166
|
+
def _receive(self) -> S7Packet:
|
|
167
|
+
"""Receive and parse S7 packet from transport."""
|
|
168
|
+
return S7PacketParser.parse(self.transport.receive())
|
|
169
|
+
|
|
170
|
+
def close(self) -> None:
|
|
171
|
+
self.transport.close()
|
|
172
|
+
|
|
173
|
+
def read_szl(self, szl_id: int, szl_index: int) -> S7Packet:
|
|
174
|
+
data = struct.pack("!HH", szl_id, szl_index)
|
|
175
|
+
request = UserDataRequest.create(
|
|
176
|
+
function_group=UserdataFunction.CPU_FUNCTION,
|
|
177
|
+
subfunction=SubfunctionCode.READ_SZL,
|
|
178
|
+
data=data,
|
|
179
|
+
)
|
|
180
|
+
response = self.send(request=request)
|
|
181
|
+
return response
|
|
182
|
+
|
|
183
|
+
def plc_stop(self) -> S7Packet:
|
|
184
|
+
response = self.send(request=RequestPLCStop())
|
|
185
|
+
return response
|