pydnp3-stepfunc 1.6.0.3__cp310-cp310-manylinux1_x86_64.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.
- dnp3/__init__.py +138 -0
- dnp3/_build_ffi.py +166 -0
- dnp3/_ffi.py +64 -0
- dnp3/channel.py +240 -0
- dnp3/handler.py +509 -0
- dnp3/logging.py +93 -0
- dnp3/master.py +539 -0
- dnp3/outstation.py +400 -0
- dnp3/runtime.py +57 -0
- dnp3/types.py +517 -0
- dnp3/vendor/dnp3.h +8253 -0
- dnp3/vendor/libdnp3_ffi.so +0 -0
- pydnp3_stepfunc-1.6.0.3.dist-info/METADATA +238 -0
- pydnp3_stepfunc-1.6.0.3.dist-info/RECORD +17 -0
- pydnp3_stepfunc-1.6.0.3.dist-info/WHEEL +5 -0
- pydnp3_stepfunc-1.6.0.3.dist-info/licenses/LICENSE +21 -0
- pydnp3_stepfunc-1.6.0.3.dist-info/top_level.txt +1 -0
dnp3/outstation.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Outstation (server) implementation for testing purposes.
|
|
3
|
+
|
|
4
|
+
This allows creating a DNP3 outstation that can be used as a test target.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from ._ffi import ffi, lib, check_error
|
|
10
|
+
from .types import (
|
|
11
|
+
LinkErrorMode,
|
|
12
|
+
DecodeLevel,
|
|
13
|
+
)
|
|
14
|
+
from .handler import ConnectionStateListener
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventBufferConfig:
|
|
18
|
+
"""Configuration for event buffer sizes."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
binary: int = 10,
|
|
23
|
+
double_bit_binary: int = 10,
|
|
24
|
+
binary_output_status: int = 10,
|
|
25
|
+
counter: int = 5,
|
|
26
|
+
frozen_counter: int = 5,
|
|
27
|
+
analog: int = 5,
|
|
28
|
+
analog_output_status: int = 5,
|
|
29
|
+
octet_string: int = 3,
|
|
30
|
+
):
|
|
31
|
+
self.binary = binary
|
|
32
|
+
self.double_bit_binary = double_bit_binary
|
|
33
|
+
self.binary_output_status = binary_output_status
|
|
34
|
+
self.counter = counter
|
|
35
|
+
self.frozen_counter = frozen_counter
|
|
36
|
+
self.analog = analog
|
|
37
|
+
self.analog_output_status = analog_output_status
|
|
38
|
+
self.octet_string = octet_string
|
|
39
|
+
|
|
40
|
+
def to_ffi(self):
|
|
41
|
+
return ffi.new("dnp3_event_buffer_config_t*", {
|
|
42
|
+
"max_binary": self.binary,
|
|
43
|
+
"max_double_bit_binary": self.double_bit_binary,
|
|
44
|
+
"max_binary_output_status": self.binary_output_status,
|
|
45
|
+
"max_counter": self.counter,
|
|
46
|
+
"max_frozen_counter": self.frozen_counter,
|
|
47
|
+
"max_analog": self.analog,
|
|
48
|
+
"max_analog_output_status": self.analog_output_status,
|
|
49
|
+
"max_octet_string": self.octet_string,
|
|
50
|
+
})[0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OutstationConfig:
|
|
54
|
+
"""Configuration for an outstation."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
outstation_address: int = 1024,
|
|
59
|
+
master_address: int = 1,
|
|
60
|
+
event_buffer: Optional[EventBufferConfig] = None,
|
|
61
|
+
decode_level: Optional[DecodeLevel] = None,
|
|
62
|
+
):
|
|
63
|
+
self.outstation_address = outstation_address
|
|
64
|
+
self.master_address = master_address
|
|
65
|
+
self.event_buffer = event_buffer or EventBufferConfig()
|
|
66
|
+
self.decode_level = decode_level or DecodeLevel()
|
|
67
|
+
|
|
68
|
+
def to_ffi(self):
|
|
69
|
+
config = ffi.new("dnp3_outstation_config_t*")
|
|
70
|
+
config.outstation_address = self.outstation_address
|
|
71
|
+
config.master_address = self.master_address
|
|
72
|
+
config.event_buffer_config = self.event_buffer.to_ffi()
|
|
73
|
+
config.solicited_buffer_size = 2048
|
|
74
|
+
config.unsolicited_buffer_size = 2048
|
|
75
|
+
config.rx_buffer_size = 2048
|
|
76
|
+
config.decode_level = ffi.new("dnp3_decode_level_t*", self.decode_level.to_ffi())[0]
|
|
77
|
+
config.confirm_timeout = 5000
|
|
78
|
+
config.select_timeout = 5000
|
|
79
|
+
# Features — matches dnp3_outstation_features_init() defaults
|
|
80
|
+
config.features.self_address = False
|
|
81
|
+
config.features.broadcast = True
|
|
82
|
+
config.features.unsolicited = True
|
|
83
|
+
config.features.respond_to_any_master = False
|
|
84
|
+
config.max_unsolicited_retries = 4294967295 # max uint32
|
|
85
|
+
config.unsolicited_retry_delay = 5000
|
|
86
|
+
config.keep_alive_timeout = 60000 # ms
|
|
87
|
+
config.max_read_request_headers = 64
|
|
88
|
+
config.max_controls_per_request = 65535
|
|
89
|
+
# Class zero config — matches dnp3_class_zero_config_init() defaults
|
|
90
|
+
config.class_zero.binary = True
|
|
91
|
+
config.class_zero.double_bit_binary = True
|
|
92
|
+
config.class_zero.binary_output_status = True
|
|
93
|
+
config.class_zero.counter = True
|
|
94
|
+
config.class_zero.frozen_counter = True
|
|
95
|
+
config.class_zero.analog = True
|
|
96
|
+
config.class_zero.analog_output_status = True
|
|
97
|
+
config.class_zero.octet_string = False
|
|
98
|
+
return config[0]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class OutstationApplication:
|
|
102
|
+
"""Default outstation application callbacks."""
|
|
103
|
+
|
|
104
|
+
def __init__(self):
|
|
105
|
+
@ffi.callback("uint16_t(void*)")
|
|
106
|
+
def _get_processing_delay_ms(ctx):
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
@ffi.callback("dnp3_write_time_result_t(uint64_t, void*)")
|
|
110
|
+
def _write_absolute_time(time, ctx):
|
|
111
|
+
return lib.DNP3_WRITE_TIME_RESULT_OK
|
|
112
|
+
|
|
113
|
+
@ffi.callback("dnp3_application_iin_t(void*)")
|
|
114
|
+
def _get_application_iin(ctx):
|
|
115
|
+
return ffi.new("dnp3_application_iin_t*", {
|
|
116
|
+
"need_time": False,
|
|
117
|
+
"local_control": False,
|
|
118
|
+
"device_trouble": False,
|
|
119
|
+
"config_corrupt": False,
|
|
120
|
+
})[0]
|
|
121
|
+
|
|
122
|
+
@ffi.callback("dnp3_restart_delay_t(void*)")
|
|
123
|
+
def _cold_restart(ctx):
|
|
124
|
+
return ffi.new("dnp3_restart_delay_t*", {
|
|
125
|
+
"restart_type": lib.DNP3_RESTART_DELAY_TYPE_SECONDS,
|
|
126
|
+
"value": 60,
|
|
127
|
+
})[0]
|
|
128
|
+
|
|
129
|
+
@ffi.callback("dnp3_restart_delay_t(void*)")
|
|
130
|
+
def _warm_restart(ctx):
|
|
131
|
+
return ffi.new("dnp3_restart_delay_t*", {
|
|
132
|
+
"restart_type": lib.DNP3_RESTART_DELAY_TYPE_NOT_SUPPORTED,
|
|
133
|
+
"value": 0,
|
|
134
|
+
})[0]
|
|
135
|
+
|
|
136
|
+
@ffi.callback("dnp3_freeze_result_t(dnp3_freeze_type_t, dnp3_database_handle_t*, void*)")
|
|
137
|
+
def _freeze_counters_all(freeze_type, db, ctx):
|
|
138
|
+
return lib.DNP3_FREEZE_RESULT_NOT_SUPPORTED
|
|
139
|
+
|
|
140
|
+
@ffi.callback("dnp3_freeze_result_t(uint16_t, uint16_t, dnp3_freeze_type_t, dnp3_database_handle_t*, void*)")
|
|
141
|
+
def _freeze_counters_range(start, stop, freeze_type, db, ctx):
|
|
142
|
+
return lib.DNP3_FREEZE_RESULT_NOT_SUPPORTED
|
|
143
|
+
|
|
144
|
+
@ffi.callback("bool(uint8_t, uint8_t, dnp3_string_attr_t, const char*, void*)")
|
|
145
|
+
def _write_string_attr(set_id, var, attr, value, ctx):
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
self._get_processing_delay_ms = _get_processing_delay_ms
|
|
149
|
+
self._write_absolute_time = _write_absolute_time
|
|
150
|
+
self._get_application_iin = _get_application_iin
|
|
151
|
+
self._cold_restart = _cold_restart
|
|
152
|
+
self._warm_restart = _warm_restart
|
|
153
|
+
self._freeze_counters_all = _freeze_counters_all
|
|
154
|
+
self._freeze_counters_range = _freeze_counters_range
|
|
155
|
+
self._write_string_attr = _write_string_attr
|
|
156
|
+
|
|
157
|
+
def as_ffi(self):
|
|
158
|
+
return ffi.new("dnp3_outstation_application_t*", {
|
|
159
|
+
"get_processing_delay_ms": self._get_processing_delay_ms,
|
|
160
|
+
"write_absolute_time": self._write_absolute_time,
|
|
161
|
+
"get_application_iin": self._get_application_iin,
|
|
162
|
+
"cold_restart": self._cold_restart,
|
|
163
|
+
"warm_restart": self._warm_restart,
|
|
164
|
+
"freeze_counters_all": self._freeze_counters_all,
|
|
165
|
+
"freeze_counters_range": self._freeze_counters_range,
|
|
166
|
+
"write_string_attr": self._write_string_attr,
|
|
167
|
+
"on_destroy": ffi.NULL,
|
|
168
|
+
"ctx": ffi.NULL,
|
|
169
|
+
})[0]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class OutstationInformation:
|
|
173
|
+
"""Default outstation information callbacks (no-op)."""
|
|
174
|
+
|
|
175
|
+
def __init__(self):
|
|
176
|
+
@ffi.callback("void(dnp3_request_header_t, void*)")
|
|
177
|
+
def _noop_header(h, ctx):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
@ffi.callback("void(dnp3_function_code_t, dnp3_broadcast_action_t, void*)")
|
|
181
|
+
def _broadcast_received(fc, action, ctx):
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
@ffi.callback("void(uint8_t, void*)")
|
|
185
|
+
def _noop_u8(v, ctx):
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
@ffi.callback("void(uint8_t, uint8_t, void*)")
|
|
189
|
+
def _noop_u8_u8(a, b, ctx):
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
@ffi.callback("void(bool, uint8_t, void*)")
|
|
193
|
+
def _noop_bool_u8(a, b, ctx):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
@ffi.callback("void(uint8_t, bool, void*)")
|
|
197
|
+
def _noop_u8_bool(a, b, ctx):
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
@ffi.callback("void(void*)")
|
|
201
|
+
def _noop(ctx):
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
self._noop_header = _noop_header
|
|
205
|
+
self._broadcast_received = _broadcast_received
|
|
206
|
+
self._noop_u8 = _noop_u8
|
|
207
|
+
self._noop_u8_u8 = _noop_u8_u8
|
|
208
|
+
self._noop_bool_u8 = _noop_bool_u8
|
|
209
|
+
self._noop_u8_bool = _noop_u8_bool
|
|
210
|
+
self._noop = _noop
|
|
211
|
+
|
|
212
|
+
def as_ffi(self):
|
|
213
|
+
return ffi.new("dnp3_outstation_information_t*", {
|
|
214
|
+
"process_request_from_idle": self._noop_header,
|
|
215
|
+
"broadcast_received": self._broadcast_received,
|
|
216
|
+
"enter_solicited_confirm_wait": self._noop_u8,
|
|
217
|
+
"solicited_confirm_timeout": self._noop_u8,
|
|
218
|
+
"solicited_confirm_received": self._noop_u8,
|
|
219
|
+
"solicited_confirm_wait_new_request": self._noop,
|
|
220
|
+
"wrong_solicited_confirm_seq": self._noop_u8_u8,
|
|
221
|
+
"unexpected_confirm": self._noop_bool_u8,
|
|
222
|
+
"enter_unsolicited_confirm_wait": self._noop_u8,
|
|
223
|
+
"unsolicited_confirm_timeout": self._noop_u8_bool,
|
|
224
|
+
"unsolicited_confirmed": self._noop_u8,
|
|
225
|
+
"clear_restart_iin": self._noop,
|
|
226
|
+
"on_destroy": ffi.NULL,
|
|
227
|
+
"ctx": ffi.NULL,
|
|
228
|
+
})[0]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ControlHandler:
|
|
232
|
+
"""Default control handler (accepts all controls)."""
|
|
233
|
+
|
|
234
|
+
def __init__(self):
|
|
235
|
+
@ffi.callback("void(void*)")
|
|
236
|
+
def _begin_fragment(ctx):
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
@ffi.callback("void(dnp3_database_handle_t*, void*)")
|
|
240
|
+
def _end_fragment(db, ctx):
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
@ffi.callback("dnp3_command_status_t(dnp3_group12_var1_t, uint16_t, dnp3_database_handle_t*, void*)")
|
|
244
|
+
def _select_g12v1(control, index, db, ctx):
|
|
245
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
246
|
+
|
|
247
|
+
@ffi.callback("dnp3_command_status_t(dnp3_group12_var1_t, uint16_t, dnp3_operate_type_t, dnp3_database_handle_t*, void*)")
|
|
248
|
+
def _operate_g12v1(control, index, op_type, db, ctx):
|
|
249
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
250
|
+
|
|
251
|
+
@ffi.callback("dnp3_command_status_t(int32_t, uint16_t, dnp3_database_handle_t*, void*)")
|
|
252
|
+
def _select_g41v1(value, index, db, ctx):
|
|
253
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
254
|
+
|
|
255
|
+
@ffi.callback("dnp3_command_status_t(int32_t, uint16_t, dnp3_operate_type_t, dnp3_database_handle_t*, void*)")
|
|
256
|
+
def _operate_g41v1(value, index, op_type, db, ctx):
|
|
257
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
258
|
+
|
|
259
|
+
@ffi.callback("dnp3_command_status_t(int16_t, uint16_t, dnp3_database_handle_t*, void*)")
|
|
260
|
+
def _select_g41v2(value, index, db, ctx):
|
|
261
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
262
|
+
|
|
263
|
+
@ffi.callback("dnp3_command_status_t(int16_t, uint16_t, dnp3_operate_type_t, dnp3_database_handle_t*, void*)")
|
|
264
|
+
def _operate_g41v2(value, index, op_type, db, ctx):
|
|
265
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
266
|
+
|
|
267
|
+
@ffi.callback("dnp3_command_status_t(float, uint16_t, dnp3_database_handle_t*, void*)")
|
|
268
|
+
def _select_g41v3(value, index, db, ctx):
|
|
269
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
270
|
+
|
|
271
|
+
@ffi.callback("dnp3_command_status_t(float, uint16_t, dnp3_operate_type_t, dnp3_database_handle_t*, void*)")
|
|
272
|
+
def _operate_g41v3(value, index, op_type, db, ctx):
|
|
273
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
274
|
+
|
|
275
|
+
@ffi.callback("dnp3_command_status_t(double, uint16_t, dnp3_database_handle_t*, void*)")
|
|
276
|
+
def _select_g41v4(value, index, db, ctx):
|
|
277
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
278
|
+
|
|
279
|
+
@ffi.callback("dnp3_command_status_t(double, uint16_t, dnp3_operate_type_t, dnp3_database_handle_t*, void*)")
|
|
280
|
+
def _operate_g41v4(value, index, op_type, db, ctx):
|
|
281
|
+
return lib.DNP3_COMMAND_STATUS_SUCCESS
|
|
282
|
+
|
|
283
|
+
self._begin_fragment = _begin_fragment
|
|
284
|
+
self._end_fragment = _end_fragment
|
|
285
|
+
self._select_g12v1 = _select_g12v1
|
|
286
|
+
self._operate_g12v1 = _operate_g12v1
|
|
287
|
+
self._select_g41v1 = _select_g41v1
|
|
288
|
+
self._operate_g41v1 = _operate_g41v1
|
|
289
|
+
self._select_g41v2 = _select_g41v2
|
|
290
|
+
self._operate_g41v2 = _operate_g41v2
|
|
291
|
+
self._select_g41v3 = _select_g41v3
|
|
292
|
+
self._operate_g41v3 = _operate_g41v3
|
|
293
|
+
self._select_g41v4 = _select_g41v4
|
|
294
|
+
self._operate_g41v4 = _operate_g41v4
|
|
295
|
+
|
|
296
|
+
def as_ffi(self):
|
|
297
|
+
return ffi.new("dnp3_control_handler_t*", {
|
|
298
|
+
"begin_fragment": self._begin_fragment,
|
|
299
|
+
"end_fragment": self._end_fragment,
|
|
300
|
+
"select_g12v1": self._select_g12v1,
|
|
301
|
+
"operate_g12v1": self._operate_g12v1,
|
|
302
|
+
"select_g41v1": self._select_g41v1,
|
|
303
|
+
"operate_g41v1": self._operate_g41v1,
|
|
304
|
+
"select_g41v2": self._select_g41v2,
|
|
305
|
+
"operate_g41v2": self._operate_g41v2,
|
|
306
|
+
"select_g41v3": self._select_g41v3,
|
|
307
|
+
"operate_g41v3": self._operate_g41v3,
|
|
308
|
+
"select_g41v4": self._select_g41v4,
|
|
309
|
+
"operate_g41v4": self._operate_g41v4,
|
|
310
|
+
"on_destroy": ffi.NULL,
|
|
311
|
+
"ctx": ffi.NULL,
|
|
312
|
+
})[0]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class OutstationServer:
|
|
316
|
+
"""
|
|
317
|
+
TCP outstation server for testing purposes.
|
|
318
|
+
|
|
319
|
+
Creates a server that accepts master connections.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
def __init__(
|
|
323
|
+
self,
|
|
324
|
+
runtime,
|
|
325
|
+
address: str = "127.0.0.1:20000",
|
|
326
|
+
link_error_mode: LinkErrorMode = LinkErrorMode.CLOSE,
|
|
327
|
+
):
|
|
328
|
+
self._runtime = runtime
|
|
329
|
+
self._outstations = []
|
|
330
|
+
self._destroyed = False
|
|
331
|
+
|
|
332
|
+
server_ptr = ffi.new("dnp3_outstation_server_t**")
|
|
333
|
+
err = lib.dnp3_outstation_server_create_tcp_server(
|
|
334
|
+
runtime._ptr,
|
|
335
|
+
link_error_mode.value,
|
|
336
|
+
address.encode(),
|
|
337
|
+
server_ptr,
|
|
338
|
+
)
|
|
339
|
+
check_error(err)
|
|
340
|
+
self._ptr = server_ptr[0]
|
|
341
|
+
|
|
342
|
+
def add_outstation(
|
|
343
|
+
self,
|
|
344
|
+
config: Optional[OutstationConfig] = None,
|
|
345
|
+
application: Optional[OutstationApplication] = None,
|
|
346
|
+
information: Optional[OutstationInformation] = None,
|
|
347
|
+
control_handler: Optional[ControlHandler] = None,
|
|
348
|
+
listener: Optional[ConnectionStateListener] = None,
|
|
349
|
+
):
|
|
350
|
+
"""Add an outstation to the server."""
|
|
351
|
+
config = config or OutstationConfig()
|
|
352
|
+
application = application or OutstationApplication()
|
|
353
|
+
information = information or OutstationInformation()
|
|
354
|
+
control_handler = control_handler or ControlHandler()
|
|
355
|
+
listener = listener or ConnectionStateListener()
|
|
356
|
+
|
|
357
|
+
# Keep references
|
|
358
|
+
self._app = application
|
|
359
|
+
self._info = information
|
|
360
|
+
self._ctrl = control_handler
|
|
361
|
+
self._listener = listener
|
|
362
|
+
|
|
363
|
+
address_filter = lib.dnp3_address_filter_any()
|
|
364
|
+
outstation_ptr = ffi.new("dnp3_outstation_t**")
|
|
365
|
+
|
|
366
|
+
err = lib.dnp3_outstation_server_add_outstation(
|
|
367
|
+
self._ptr,
|
|
368
|
+
config.to_ffi(),
|
|
369
|
+
application.as_ffi(),
|
|
370
|
+
information.as_ffi(),
|
|
371
|
+
control_handler.as_ffi(),
|
|
372
|
+
listener.as_ffi(),
|
|
373
|
+
address_filter,
|
|
374
|
+
outstation_ptr,
|
|
375
|
+
)
|
|
376
|
+
lib.dnp3_address_filter_destroy(address_filter)
|
|
377
|
+
check_error(err)
|
|
378
|
+
|
|
379
|
+
self._outstations.append(outstation_ptr[0])
|
|
380
|
+
return outstation_ptr[0]
|
|
381
|
+
|
|
382
|
+
def bind(self):
|
|
383
|
+
"""Start accepting connections."""
|
|
384
|
+
check_error(lib.dnp3_outstation_server_bind(self._ptr))
|
|
385
|
+
|
|
386
|
+
def destroy(self):
|
|
387
|
+
"""Clean up all outstations and the server."""
|
|
388
|
+
if self._destroyed:
|
|
389
|
+
return
|
|
390
|
+
self._destroyed = True
|
|
391
|
+
for os in self._outstations:
|
|
392
|
+
lib.dnp3_outstation_destroy(os)
|
|
393
|
+
self._outstations.clear()
|
|
394
|
+
lib.dnp3_outstation_server_destroy(self._ptr)
|
|
395
|
+
|
|
396
|
+
def __del__(self):
|
|
397
|
+
try:
|
|
398
|
+
self.destroy()
|
|
399
|
+
except Exception:
|
|
400
|
+
pass
|
dnp3/runtime.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DNP3 runtime management.
|
|
3
|
+
|
|
4
|
+
The runtime manages the Tokio async runtime used by the Rust library.
|
|
5
|
+
It must be created before any channels and destroyed after all channels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._ffi import ffi, lib, check_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Runtime:
|
|
12
|
+
"""
|
|
13
|
+
Manages the underlying Tokio async runtime.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
runtime = Runtime(num_threads=4)
|
|
17
|
+
# ... create channels ...
|
|
18
|
+
runtime.destroy()
|
|
19
|
+
|
|
20
|
+
Or as a context manager:
|
|
21
|
+
with Runtime() as runtime:
|
|
22
|
+
channel = create_tcp_channel(runtime, ...)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, num_threads: int = 0):
|
|
26
|
+
"""
|
|
27
|
+
Create a new runtime.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
num_threads: Number of worker threads. 0 = auto-detect from CPU count.
|
|
31
|
+
"""
|
|
32
|
+
rt_ptr = ffi.new("dnp3_runtime_t**")
|
|
33
|
+
config = ffi.new("dnp3_runtime_config_t*", {"num_core_threads": num_threads})
|
|
34
|
+
err = lib.dnp3_runtime_create(config[0], rt_ptr)
|
|
35
|
+
check_error(err)
|
|
36
|
+
self._ptr = rt_ptr[0]
|
|
37
|
+
self._destroyed = False
|
|
38
|
+
|
|
39
|
+
def set_shutdown_timeout(self, timeout_secs: int):
|
|
40
|
+
"""Set maximum time to wait during shutdown."""
|
|
41
|
+
lib.dnp3_runtime_set_shutdown_timeout(self._ptr, timeout_secs)
|
|
42
|
+
|
|
43
|
+
def destroy(self):
|
|
44
|
+
"""Destroy the runtime. Blocks until all tasks complete."""
|
|
45
|
+
if not self._destroyed and self._ptr is not None:
|
|
46
|
+
lib.dnp3_runtime_destroy(self._ptr)
|
|
47
|
+
self._destroyed = True
|
|
48
|
+
|
|
49
|
+
def __enter__(self):
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
53
|
+
self.destroy()
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def __del__(self):
|
|
57
|
+
self.destroy()
|