python-snap7 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_snap7-3.0.0.dist-info/METADATA +94 -0
- python_snap7-3.0.0.dist-info/RECORD +22 -0
- python_snap7-3.0.0.dist-info/WHEEL +5 -0
- python_snap7-3.0.0.dist-info/entry_points.txt +2 -0
- python_snap7-3.0.0.dist-info/licenses/LICENSE +20 -0
- python_snap7-3.0.0.dist-info/top_level.txt +1 -0
- snap7/__init__.py +34 -0
- snap7/client.py +1923 -0
- snap7/connection.py +456 -0
- snap7/datatypes.py +312 -0
- snap7/error.py +444 -0
- snap7/logo.py +222 -0
- snap7/partner.py +680 -0
- snap7/py.typed +0 -0
- snap7/s7protocol.py +1509 -0
- snap7/server/__init__.py +2681 -0
- snap7/server/__main__.py +41 -0
- snap7/type.py +391 -0
- snap7/util/__init__.py +108 -0
- snap7/util/db.py +789 -0
- snap7/util/getters.py +766 -0
- snap7/util/setters.py +733 -0
snap7/partner.py
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pure Python S7 partner implementation.
|
|
3
|
+
|
|
4
|
+
S7 peer-to-peer communication for bidirectional data exchange.
|
|
5
|
+
Unlike client-server where client requests and server responds,
|
|
6
|
+
partners have equal rights and can send data asynchronously.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import socket
|
|
10
|
+
import struct
|
|
11
|
+
import logging
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Optional, Tuple, Callable, Type
|
|
14
|
+
from queue import Queue, Empty
|
|
15
|
+
from typing import Any
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from types import TracebackType
|
|
18
|
+
from ctypes import c_int32, c_uint32
|
|
19
|
+
|
|
20
|
+
from .connection import ISOTCPConnection
|
|
21
|
+
from .error import S7Error, S7ConnectionError
|
|
22
|
+
from .type import Parameter
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PartnerStatus:
|
|
28
|
+
"""Partner status constants."""
|
|
29
|
+
|
|
30
|
+
STOPPED = 0
|
|
31
|
+
RUNNING = 1
|
|
32
|
+
CONNECTED = 2
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Partner:
|
|
36
|
+
"""
|
|
37
|
+
Pure Python S7 partner implementation.
|
|
38
|
+
|
|
39
|
+
Implements peer-to-peer S7 communication where both partners can
|
|
40
|
+
send and receive data asynchronously. Supports both active (initiates
|
|
41
|
+
connection) and passive (waits for connection) modes.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
>>> import snap7
|
|
45
|
+
>>> partner = snap7.Partner(active=True)
|
|
46
|
+
>>> partner.start_to("0.0.0.0", "192.168.1.10", 0x0100, 0x0102)
|
|
47
|
+
>>> partner.set_send_data(b"Hello")
|
|
48
|
+
>>> partner.b_send()
|
|
49
|
+
>>> partner.stop()
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, active: bool = False, **kwargs: object) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Initialize S7 partner.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
active: If True, this partner initiates the connection.
|
|
58
|
+
If False, this partner waits for incoming connections.
|
|
59
|
+
**kwargs: Ignored. Kept for backwards compatibility.
|
|
60
|
+
"""
|
|
61
|
+
self.active = active
|
|
62
|
+
self.connected = False
|
|
63
|
+
self.running = False
|
|
64
|
+
|
|
65
|
+
# Connection parameters
|
|
66
|
+
self.local_ip = "0.0.0.0"
|
|
67
|
+
self.remote_ip = ""
|
|
68
|
+
self.local_tsap = 0x0100
|
|
69
|
+
self.remote_tsap = 0x0102
|
|
70
|
+
self.port = 1102 # Non-privileged port (was 102)
|
|
71
|
+
self.local_port = 0 # Let OS choose
|
|
72
|
+
self.remote_port = 1102 # Non-privileged port (was 102)
|
|
73
|
+
|
|
74
|
+
# Socket and connection
|
|
75
|
+
self._socket: Optional[socket.socket] = None
|
|
76
|
+
self._server_socket: Optional[socket.socket] = None # For passive mode
|
|
77
|
+
self._connection: Optional[ISOTCPConnection] = None
|
|
78
|
+
|
|
79
|
+
# Statistics
|
|
80
|
+
self.bytes_sent = 0
|
|
81
|
+
self.bytes_recv = 0
|
|
82
|
+
self.send_errors = 0
|
|
83
|
+
self.recv_errors = 0
|
|
84
|
+
|
|
85
|
+
# Timing
|
|
86
|
+
self.last_send_time = 0
|
|
87
|
+
self.last_recv_time = 0
|
|
88
|
+
|
|
89
|
+
# Callbacks
|
|
90
|
+
self._recv_callback: Optional[Callable[[bytes], None]] = None
|
|
91
|
+
self._send_callback_fn: Optional[Callable[[int], None]] = None
|
|
92
|
+
|
|
93
|
+
# Async operation support
|
|
94
|
+
self._async_send_queue: Queue[Any] = Queue()
|
|
95
|
+
self._async_recv_queue: Queue[Any] = Queue()
|
|
96
|
+
self._async_thread: Optional[threading.Thread] = None
|
|
97
|
+
self._stop_event = threading.Event()
|
|
98
|
+
|
|
99
|
+
# Last error
|
|
100
|
+
self.last_error = 0
|
|
101
|
+
|
|
102
|
+
# Buffer for send/recv operations
|
|
103
|
+
self._send_data: Optional[bytes] = None
|
|
104
|
+
self._recv_data: Optional[bytes] = None
|
|
105
|
+
self._async_send_in_progress = False
|
|
106
|
+
self._async_send_result = 0
|
|
107
|
+
|
|
108
|
+
logger.info(f"S7 Partner initialized (active={active}, pure Python implementation)")
|
|
109
|
+
|
|
110
|
+
def create(self, active: bool = False) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Creates a Partner.
|
|
113
|
+
|
|
114
|
+
Note: For pure Python implementation, the partner is created in __init__.
|
|
115
|
+
This method exists for API compatibility.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
active: If True, this partner initiates connections
|
|
119
|
+
"""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def destroy(self) -> int:
|
|
123
|
+
"""
|
|
124
|
+
Destroy the Partner.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
0 on success
|
|
128
|
+
"""
|
|
129
|
+
self.stop()
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
def start(self) -> int:
|
|
133
|
+
"""
|
|
134
|
+
Start the partner with default parameters.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
0 on success
|
|
138
|
+
"""
|
|
139
|
+
return self.start_to(self.local_ip, self.remote_ip, self.local_tsap, self.remote_tsap)
|
|
140
|
+
|
|
141
|
+
def start_to(self, local_ip: str, remote_ip: str, local_tsap: int, remote_tsap: int) -> int:
|
|
142
|
+
"""
|
|
143
|
+
Start the partner with specific connection parameters.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
local_ip: Local IP address to bind to
|
|
147
|
+
remote_ip: Remote partner IP address (for active mode)
|
|
148
|
+
local_tsap: Local TSAP
|
|
149
|
+
remote_tsap: Remote TSAP
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
0 on success
|
|
153
|
+
"""
|
|
154
|
+
self.local_ip = local_ip
|
|
155
|
+
self.remote_ip = remote_ip
|
|
156
|
+
self.local_tsap = local_tsap
|
|
157
|
+
self.remote_tsap = remote_tsap
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if self.active:
|
|
161
|
+
# Active mode: initiate connection to remote partner
|
|
162
|
+
self._connect_to_remote()
|
|
163
|
+
else:
|
|
164
|
+
# Passive mode: start listening for incoming connections
|
|
165
|
+
self._start_listening()
|
|
166
|
+
|
|
167
|
+
self.running = True
|
|
168
|
+
|
|
169
|
+
# Start async processing thread
|
|
170
|
+
self._stop_event.clear()
|
|
171
|
+
self._async_thread = threading.Thread(target=self._async_processor, daemon=True)
|
|
172
|
+
self._async_thread.start()
|
|
173
|
+
|
|
174
|
+
logger.info(f"Partner started ({'active' if self.active else 'passive'} mode)")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
self.last_error = -1
|
|
179
|
+
logger.error(f"Partner start failed: {e}")
|
|
180
|
+
raise S7ConnectionError(f"Partner start failed: {e}")
|
|
181
|
+
|
|
182
|
+
def stop(self) -> int:
|
|
183
|
+
"""
|
|
184
|
+
Stop the partner and disconnect.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
0 on success
|
|
188
|
+
"""
|
|
189
|
+
self._stop_event.set()
|
|
190
|
+
|
|
191
|
+
if self._async_thread and self._async_thread.is_alive():
|
|
192
|
+
self._async_thread.join(timeout=2.0)
|
|
193
|
+
|
|
194
|
+
if self._connection:
|
|
195
|
+
self._connection.disconnect()
|
|
196
|
+
self._connection = None
|
|
197
|
+
|
|
198
|
+
if self._server_socket:
|
|
199
|
+
try:
|
|
200
|
+
self._server_socket.close()
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
self._server_socket = None
|
|
204
|
+
|
|
205
|
+
if self._socket:
|
|
206
|
+
try:
|
|
207
|
+
self._socket.close()
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
self._socket = None
|
|
211
|
+
|
|
212
|
+
self.connected = False
|
|
213
|
+
self.running = False
|
|
214
|
+
|
|
215
|
+
logger.info("Partner stopped")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
def b_send(self) -> int:
|
|
219
|
+
"""
|
|
220
|
+
Send data synchronously (blocking).
|
|
221
|
+
|
|
222
|
+
Note: Call set_send_data() first to set the data to send.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
0 on success
|
|
226
|
+
"""
|
|
227
|
+
if self._send_data is None:
|
|
228
|
+
return -1
|
|
229
|
+
|
|
230
|
+
if not self.connected or self._connection is None:
|
|
231
|
+
self.send_errors += 1
|
|
232
|
+
raise S7ConnectionError("Not connected")
|
|
233
|
+
|
|
234
|
+
start_time = datetime.now()
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
# Build partner data PDU
|
|
238
|
+
pdu = self._build_partner_data_pdu(self._send_data)
|
|
239
|
+
|
|
240
|
+
# Send via ISO connection
|
|
241
|
+
self._connection.send_data(pdu)
|
|
242
|
+
|
|
243
|
+
# Wait for acknowledgment
|
|
244
|
+
ack_data = self._connection.receive_data()
|
|
245
|
+
self._parse_partner_ack(ack_data)
|
|
246
|
+
|
|
247
|
+
self.bytes_sent += len(self._send_data)
|
|
248
|
+
self.last_send_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
249
|
+
|
|
250
|
+
logger.debug(f"Sent {len(self._send_data)} bytes synchronously")
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.send_errors += 1
|
|
255
|
+
self.last_error = -1
|
|
256
|
+
logger.error(f"Synchronous send failed: {e}")
|
|
257
|
+
raise S7ConnectionError(f"Send failed: {e}")
|
|
258
|
+
|
|
259
|
+
def b_recv(self) -> int:
|
|
260
|
+
"""
|
|
261
|
+
Receive data synchronously (blocking).
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
0 on success
|
|
265
|
+
"""
|
|
266
|
+
if not self.connected or self._connection is None:
|
|
267
|
+
self.recv_errors += 1
|
|
268
|
+
self._recv_data = None
|
|
269
|
+
return -1
|
|
270
|
+
|
|
271
|
+
start_time = datetime.now()
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
# Receive partner data
|
|
275
|
+
data = self._connection.receive_data()
|
|
276
|
+
received = self._parse_partner_data_pdu(data)
|
|
277
|
+
|
|
278
|
+
# Send acknowledgment
|
|
279
|
+
ack = self._build_partner_ack()
|
|
280
|
+
self._connection.send_data(ack)
|
|
281
|
+
|
|
282
|
+
self.bytes_recv += len(received)
|
|
283
|
+
self.last_recv_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
284
|
+
self._recv_data = received
|
|
285
|
+
|
|
286
|
+
# Call receive callback if set
|
|
287
|
+
if self._recv_callback:
|
|
288
|
+
self._recv_callback(received)
|
|
289
|
+
|
|
290
|
+
logger.debug(f"Received {len(received)} bytes synchronously")
|
|
291
|
+
return 0
|
|
292
|
+
|
|
293
|
+
except socket.timeout:
|
|
294
|
+
self._recv_data = None
|
|
295
|
+
return 1 # Timeout
|
|
296
|
+
except Exception as e:
|
|
297
|
+
self.recv_errors += 1
|
|
298
|
+
self.last_error = -1
|
|
299
|
+
self._recv_data = None
|
|
300
|
+
logger.error(f"Synchronous receive failed: {e}")
|
|
301
|
+
return -1
|
|
302
|
+
|
|
303
|
+
def as_b_send(self) -> int:
|
|
304
|
+
"""
|
|
305
|
+
Send data asynchronously (non-blocking).
|
|
306
|
+
|
|
307
|
+
Note: Call set_send_data() first to set the data to send.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
0 on success (send initiated)
|
|
311
|
+
"""
|
|
312
|
+
if self._send_data is None:
|
|
313
|
+
return -1
|
|
314
|
+
|
|
315
|
+
if not self.connected:
|
|
316
|
+
self.send_errors += 1
|
|
317
|
+
return -1
|
|
318
|
+
|
|
319
|
+
self._async_send_in_progress = True
|
|
320
|
+
self._async_send_result = 1 # In progress
|
|
321
|
+
|
|
322
|
+
# Queue the send operation
|
|
323
|
+
self._async_send_queue.put(self._send_data)
|
|
324
|
+
|
|
325
|
+
logger.debug(f"Async send initiated for {len(self._send_data)} bytes")
|
|
326
|
+
return 0
|
|
327
|
+
|
|
328
|
+
def check_as_b_send_completion(self) -> Tuple[str, c_int32]:
|
|
329
|
+
"""
|
|
330
|
+
Check if async send completed.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Tuple of (status_string, operation_result)
|
|
334
|
+
"""
|
|
335
|
+
if self._async_send_in_progress:
|
|
336
|
+
return "job in progress", c_int32(0)
|
|
337
|
+
|
|
338
|
+
return_values = {
|
|
339
|
+
0: "job complete",
|
|
340
|
+
1: "job in progress",
|
|
341
|
+
-2: "invalid handled supplied",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
result = self._async_send_result
|
|
345
|
+
return return_values.get(0, "unknown"), c_int32(result)
|
|
346
|
+
|
|
347
|
+
def wait_as_b_send_completion(self, timeout: int = 0) -> int:
|
|
348
|
+
"""
|
|
349
|
+
Wait for async send to complete.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
timeout: Timeout in milliseconds (0 for infinite)
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
0 on success, non-zero on error/timeout
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
RuntimeError: If no async operation is in progress
|
|
359
|
+
"""
|
|
360
|
+
if not self._async_send_in_progress:
|
|
361
|
+
raise RuntimeError("No async send operation in progress")
|
|
362
|
+
|
|
363
|
+
# Wait for completion
|
|
364
|
+
wait_time = timeout / 1000.0 if timeout > 0 else None
|
|
365
|
+
start = datetime.now()
|
|
366
|
+
|
|
367
|
+
while self._async_send_in_progress:
|
|
368
|
+
if wait_time is not None:
|
|
369
|
+
elapsed = (datetime.now() - start).total_seconds()
|
|
370
|
+
if elapsed >= wait_time:
|
|
371
|
+
return -1 # Timeout
|
|
372
|
+
threading.Event().wait(0.01) # Small sleep
|
|
373
|
+
|
|
374
|
+
return self._async_send_result
|
|
375
|
+
|
|
376
|
+
def check_as_b_recv_completion(self) -> int:
|
|
377
|
+
"""
|
|
378
|
+
Check if async receive completed.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
0 if data available, 1 if in progress
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
self._recv_data = self._async_recv_queue.get_nowait()
|
|
385
|
+
return 0 # Data available
|
|
386
|
+
except Empty:
|
|
387
|
+
return 1 # No data yet
|
|
388
|
+
|
|
389
|
+
def get_status(self) -> c_int32:
|
|
390
|
+
"""
|
|
391
|
+
Get partner status.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Status code (0=stopped, 1=running, 2=connected)
|
|
395
|
+
"""
|
|
396
|
+
if self.connected:
|
|
397
|
+
return c_int32(PartnerStatus.CONNECTED)
|
|
398
|
+
elif self.running:
|
|
399
|
+
return c_int32(PartnerStatus.RUNNING)
|
|
400
|
+
else:
|
|
401
|
+
return c_int32(PartnerStatus.STOPPED)
|
|
402
|
+
|
|
403
|
+
def get_stats(self) -> Tuple[c_uint32, c_uint32, c_uint32, c_uint32]:
|
|
404
|
+
"""
|
|
405
|
+
Get partner statistics.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Tuple of (bytes_sent, bytes_recv, send_errors, recv_errors)
|
|
409
|
+
"""
|
|
410
|
+
return (c_uint32(self.bytes_sent), c_uint32(self.bytes_recv), c_uint32(self.send_errors), c_uint32(self.recv_errors))
|
|
411
|
+
|
|
412
|
+
def get_times(self) -> Tuple[c_int32, c_int32]:
|
|
413
|
+
"""
|
|
414
|
+
Get last operation times.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Tuple of (last_send_time_ms, last_recv_time_ms)
|
|
418
|
+
"""
|
|
419
|
+
return c_int32(self.last_send_time), c_int32(self.last_recv_time)
|
|
420
|
+
|
|
421
|
+
def get_last_error(self) -> c_int32:
|
|
422
|
+
"""
|
|
423
|
+
Get last error code.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Last error code
|
|
427
|
+
"""
|
|
428
|
+
return c_int32(self.last_error)
|
|
429
|
+
|
|
430
|
+
def get_param(self, parameter: Parameter) -> int:
|
|
431
|
+
"""
|
|
432
|
+
Get partner parameter.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
parameter: Parameter to read
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Parameter value
|
|
439
|
+
"""
|
|
440
|
+
param_values = {
|
|
441
|
+
Parameter.LocalPort: self.local_port,
|
|
442
|
+
Parameter.RemotePort: self.remote_port,
|
|
443
|
+
Parameter.PingTimeout: 750,
|
|
444
|
+
Parameter.SendTimeout: 10,
|
|
445
|
+
Parameter.RecvTimeout: 3000,
|
|
446
|
+
Parameter.SrcRef: 256,
|
|
447
|
+
Parameter.DstRef: 0,
|
|
448
|
+
Parameter.PDURequest: 480,
|
|
449
|
+
Parameter.WorkInterval: 100,
|
|
450
|
+
Parameter.BSendTimeout: 3000,
|
|
451
|
+
Parameter.BRecvTimeout: 3000,
|
|
452
|
+
Parameter.RecoveryTime: 500,
|
|
453
|
+
Parameter.KeepAliveTime: 5000,
|
|
454
|
+
}
|
|
455
|
+
value = param_values.get(parameter)
|
|
456
|
+
if value is None:
|
|
457
|
+
raise RuntimeError(f"Parameter {parameter} not supported")
|
|
458
|
+
logger.debug(f"Getting parameter {parameter} = {value}")
|
|
459
|
+
return value
|
|
460
|
+
|
|
461
|
+
def set_param(self, parameter: Parameter, value: int) -> int:
|
|
462
|
+
"""
|
|
463
|
+
Set partner parameter.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
parameter: Parameter to set
|
|
467
|
+
value: Value to set
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
0 on success
|
|
471
|
+
"""
|
|
472
|
+
# Some parameters cannot be set
|
|
473
|
+
if parameter == Parameter.RemotePort:
|
|
474
|
+
raise RuntimeError(f"Cannot set parameter {parameter}")
|
|
475
|
+
|
|
476
|
+
if parameter == Parameter.LocalPort:
|
|
477
|
+
self.local_port = value
|
|
478
|
+
logger.debug(f"Setting parameter {parameter} to {value}")
|
|
479
|
+
return 0
|
|
480
|
+
|
|
481
|
+
def set_recv_callback(self) -> int:
|
|
482
|
+
"""
|
|
483
|
+
Sets the user callback for incoming data.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
0 on success
|
|
487
|
+
"""
|
|
488
|
+
logger.debug("set_recv_callback called")
|
|
489
|
+
return 0
|
|
490
|
+
|
|
491
|
+
def set_send_callback(self) -> int:
|
|
492
|
+
"""
|
|
493
|
+
Sets the user callback for completed async sends.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
0 on success
|
|
497
|
+
"""
|
|
498
|
+
logger.debug("set_send_callback called")
|
|
499
|
+
return 0
|
|
500
|
+
|
|
501
|
+
def set_send_data(self, data: bytes) -> None:
|
|
502
|
+
"""
|
|
503
|
+
Set data to be sent by b_send() or as_b_send().
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
data: Data to send
|
|
507
|
+
"""
|
|
508
|
+
self._send_data = data
|
|
509
|
+
|
|
510
|
+
def get_recv_data(self) -> Optional[bytes]:
|
|
511
|
+
"""
|
|
512
|
+
Get data received by b_recv().
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Received data or None
|
|
516
|
+
"""
|
|
517
|
+
return self._recv_data
|
|
518
|
+
|
|
519
|
+
def _connect_to_remote(self) -> None:
|
|
520
|
+
"""Connect to remote partner (active mode)."""
|
|
521
|
+
if not self.remote_ip:
|
|
522
|
+
raise S7ConnectionError("Remote IP not specified for active partner")
|
|
523
|
+
|
|
524
|
+
self._connection = ISOTCPConnection(
|
|
525
|
+
host=self.remote_ip, port=self.port, local_tsap=self.local_tsap, remote_tsap=self.remote_tsap
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
self._connection.connect()
|
|
529
|
+
self._socket = self._connection.socket
|
|
530
|
+
self.connected = True
|
|
531
|
+
|
|
532
|
+
logger.info(f"Connected to remote partner at {self.remote_ip}:{self.port}")
|
|
533
|
+
|
|
534
|
+
def _start_listening(self) -> None:
|
|
535
|
+
"""Start listening for incoming connections (passive mode)."""
|
|
536
|
+
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
537
|
+
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
538
|
+
# Try to use SO_REUSEPORT if available (Linux, macOS) for faster port reuse
|
|
539
|
+
if hasattr(socket, "SO_REUSEPORT"):
|
|
540
|
+
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
541
|
+
self._server_socket.bind((self.local_ip, self.port))
|
|
542
|
+
self._server_socket.listen(1)
|
|
543
|
+
self._server_socket.settimeout(1.0) # Allow periodic check
|
|
544
|
+
|
|
545
|
+
logger.info(f"Partner listening on {self.local_ip}:{self.port}")
|
|
546
|
+
|
|
547
|
+
# Start accept thread
|
|
548
|
+
accept_thread = threading.Thread(target=self._accept_connection, daemon=True)
|
|
549
|
+
accept_thread.start()
|
|
550
|
+
|
|
551
|
+
def _accept_connection(self) -> None:
|
|
552
|
+
"""Accept incoming connection in passive mode."""
|
|
553
|
+
if self._server_socket is None:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
while self.running and not self._stop_event.is_set():
|
|
557
|
+
try:
|
|
558
|
+
client_sock, addr = self._server_socket.accept()
|
|
559
|
+
|
|
560
|
+
# Create connection object
|
|
561
|
+
self._socket = client_sock
|
|
562
|
+
self._connection = ISOTCPConnection(
|
|
563
|
+
host=addr[0], port=addr[1], local_tsap=self.local_tsap, remote_tsap=self.remote_tsap
|
|
564
|
+
)
|
|
565
|
+
self._connection.socket = client_sock
|
|
566
|
+
self._connection.connected = True
|
|
567
|
+
self.connected = True
|
|
568
|
+
|
|
569
|
+
logger.info(f"Partner connection accepted from {addr}")
|
|
570
|
+
break
|
|
571
|
+
|
|
572
|
+
except socket.timeout:
|
|
573
|
+
continue
|
|
574
|
+
except Exception as e:
|
|
575
|
+
if self.running:
|
|
576
|
+
logger.error(f"Accept failed: {e}")
|
|
577
|
+
break
|
|
578
|
+
|
|
579
|
+
def _async_processor(self) -> None:
|
|
580
|
+
"""Background thread for processing async operations."""
|
|
581
|
+
while not self._stop_event.is_set():
|
|
582
|
+
# Process async sends
|
|
583
|
+
try:
|
|
584
|
+
data = self._async_send_queue.get(timeout=0.1)
|
|
585
|
+
|
|
586
|
+
try:
|
|
587
|
+
# Temporarily set send data and call b_send
|
|
588
|
+
old_data = self._send_data
|
|
589
|
+
self._send_data = data
|
|
590
|
+
result = self.b_send()
|
|
591
|
+
self._send_data = old_data
|
|
592
|
+
self._async_send_result = result
|
|
593
|
+
|
|
594
|
+
if self._send_callback_fn:
|
|
595
|
+
self._send_callback_fn(result)
|
|
596
|
+
|
|
597
|
+
except Exception as e:
|
|
598
|
+
self._async_send_result = -1
|
|
599
|
+
logger.error(f"Async send failed: {e}")
|
|
600
|
+
finally:
|
|
601
|
+
self._async_send_in_progress = False
|
|
602
|
+
|
|
603
|
+
except Empty:
|
|
604
|
+
pass
|
|
605
|
+
except Exception:
|
|
606
|
+
break
|
|
607
|
+
|
|
608
|
+
def _build_partner_data_pdu(self, data: bytes) -> bytes:
|
|
609
|
+
"""
|
|
610
|
+
Build partner data PDU.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
data: Data to send
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
PDU bytes
|
|
617
|
+
"""
|
|
618
|
+
# S7 partner data PDU format:
|
|
619
|
+
# Header + Data
|
|
620
|
+
header = struct.pack(
|
|
621
|
+
">BBHH",
|
|
622
|
+
0x32, # Protocol ID (S7)
|
|
623
|
+
0x07, # Partner PDU type
|
|
624
|
+
len(data), # Data length high
|
|
625
|
+
0x0000, # Reserved
|
|
626
|
+
)
|
|
627
|
+
return header + data
|
|
628
|
+
|
|
629
|
+
def _parse_partner_data_pdu(self, pdu: bytes) -> bytes:
|
|
630
|
+
"""
|
|
631
|
+
Parse partner data PDU.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
pdu: PDU bytes
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
Extracted data
|
|
638
|
+
"""
|
|
639
|
+
if len(pdu) < 6:
|
|
640
|
+
raise S7Error("Invalid partner PDU: too short")
|
|
641
|
+
|
|
642
|
+
# Skip header
|
|
643
|
+
return pdu[6:]
|
|
644
|
+
|
|
645
|
+
def _build_partner_ack(self) -> bytes:
|
|
646
|
+
"""Build partner acknowledgment PDU."""
|
|
647
|
+
return struct.pack(
|
|
648
|
+
">BBHH",
|
|
649
|
+
0x32, # Protocol ID
|
|
650
|
+
0x08, # ACK type
|
|
651
|
+
0x0000, # Reserved
|
|
652
|
+
0x0000, # Status OK
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
def _parse_partner_ack(self, pdu: bytes) -> None:
|
|
656
|
+
"""Parse partner acknowledgment PDU."""
|
|
657
|
+
if len(pdu) < 6:
|
|
658
|
+
raise S7Error("Invalid partner ACK: too short")
|
|
659
|
+
|
|
660
|
+
protocol_id, pdu_type = struct.unpack(">BB", pdu[:2])
|
|
661
|
+
|
|
662
|
+
if pdu_type != 0x08:
|
|
663
|
+
raise S7Error(f"Expected partner ACK, got {pdu_type:#02x}")
|
|
664
|
+
|
|
665
|
+
def __enter__(self) -> "Partner":
|
|
666
|
+
"""Context manager entry."""
|
|
667
|
+
return self
|
|
668
|
+
|
|
669
|
+
def __exit__(
|
|
670
|
+
self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
|
|
671
|
+
) -> None:
|
|
672
|
+
"""Context manager exit."""
|
|
673
|
+
self.destroy()
|
|
674
|
+
|
|
675
|
+
def __del__(self) -> None:
|
|
676
|
+
"""Destructor."""
|
|
677
|
+
try:
|
|
678
|
+
self.stop()
|
|
679
|
+
except Exception:
|
|
680
|
+
pass
|
snap7/py.typed
ADDED
|
File without changes
|