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,10 @@
1
+ from .async_client import AsyncClient
2
+ from .s7comm import S7Comm
3
+ from .sync_client import Client
4
+
5
+
6
+ __all__ = [
7
+ "S7Comm",
8
+ "Client",
9
+ "AsyncClient",
10
+ ]
@@ -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,6 @@
1
+ from .async_client import AsyncS7Comm
2
+ from .client import S7Comm
3
+ from .exceptions import PacketLostError, StalePacketError
4
+
5
+
6
+ __all__ = ["S7Comm", "AsyncS7Comm", "PacketLostError", "StalePacketError"]
@@ -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