pydnp3-stepfunc 1.6.0.3__cp39-cp39-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/handler.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Callback handler wrappers for DNP3 read/command/event callbacks.
|
|
3
|
+
|
|
4
|
+
The C FFI uses function pointers in structs for async callbacks.
|
|
5
|
+
This module wraps them with cffi @ffi.callback decorators and
|
|
6
|
+
collects data into Python objects.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
from typing import List, Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
from ._ffi import ffi, lib
|
|
13
|
+
from .types import (
|
|
14
|
+
BinaryInput,
|
|
15
|
+
DoubleBitBinaryInput,
|
|
16
|
+
BinaryOutputStatus,
|
|
17
|
+
Counter,
|
|
18
|
+
FrozenCounter,
|
|
19
|
+
AnalogInput,
|
|
20
|
+
AnalogOutputStatus,
|
|
21
|
+
IIN,
|
|
22
|
+
ReadType,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReadHandler:
|
|
27
|
+
"""
|
|
28
|
+
Collects data received from DNP3 read operations.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
handler = ReadHandler()
|
|
32
|
+
# Pass handler.as_ffi() to master_channel_read or add_association
|
|
33
|
+
# After read completes, access handler.binary_inputs, handler.analog_inputs, etc.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.binary_inputs: List[BinaryInput] = []
|
|
38
|
+
self.double_bit_binary_inputs: List[DoubleBitBinaryInput] = []
|
|
39
|
+
self.binary_output_statuses: List[BinaryOutputStatus] = []
|
|
40
|
+
self.counters: List[Counter] = []
|
|
41
|
+
self.frozen_counters: List[FrozenCounter] = []
|
|
42
|
+
self.analog_inputs: List[AnalogInput] = []
|
|
43
|
+
self.analog_output_statuses: List[AnalogOutputStatus] = []
|
|
44
|
+
self.octet_strings: List[Dict[str, Any]] = []
|
|
45
|
+
self.string_attrs: List[Dict[str, Any]] = []
|
|
46
|
+
self.iin: Optional[IIN] = None
|
|
47
|
+
self._lock = threading.Lock()
|
|
48
|
+
|
|
49
|
+
# Create persistent C callbacks - these must be kept alive
|
|
50
|
+
# as long as the handler is in use
|
|
51
|
+
|
|
52
|
+
@ffi.callback("void(dnp3_read_type_t, dnp3_response_header_t, void*)")
|
|
53
|
+
def _begin_fragment(read_type, header, ctx):
|
|
54
|
+
with self._lock:
|
|
55
|
+
self.iin = IIN.from_ffi(header.iin)
|
|
56
|
+
|
|
57
|
+
@ffi.callback("void(dnp3_read_type_t, dnp3_response_header_t, void*)")
|
|
58
|
+
def _end_fragment(read_type, header, ctx):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_binary_input_iterator_t*, void*)")
|
|
62
|
+
def _handle_binary_input(info, it, ctx):
|
|
63
|
+
with self._lock:
|
|
64
|
+
while True:
|
|
65
|
+
val = lib.dnp3_binary_input_iterator_next(it)
|
|
66
|
+
if val == ffi.NULL:
|
|
67
|
+
break
|
|
68
|
+
self.binary_inputs.append(BinaryInput.from_ffi(val))
|
|
69
|
+
|
|
70
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_double_bit_binary_input_iterator_t*, void*)")
|
|
71
|
+
def _handle_double_bit(info, it, ctx):
|
|
72
|
+
with self._lock:
|
|
73
|
+
while True:
|
|
74
|
+
val = lib.dnp3_double_bit_binary_input_iterator_next(it)
|
|
75
|
+
if val == ffi.NULL:
|
|
76
|
+
break
|
|
77
|
+
self.double_bit_binary_inputs.append(DoubleBitBinaryInput.from_ffi(val))
|
|
78
|
+
|
|
79
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_binary_output_status_iterator_t*, void*)")
|
|
80
|
+
def _handle_bos(info, it, ctx):
|
|
81
|
+
with self._lock:
|
|
82
|
+
while True:
|
|
83
|
+
val = lib.dnp3_binary_output_status_iterator_next(it)
|
|
84
|
+
if val == ffi.NULL:
|
|
85
|
+
break
|
|
86
|
+
self.binary_output_statuses.append(BinaryOutputStatus.from_ffi(val))
|
|
87
|
+
|
|
88
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_counter_iterator_t*, void*)")
|
|
89
|
+
def _handle_counter(info, it, ctx):
|
|
90
|
+
with self._lock:
|
|
91
|
+
while True:
|
|
92
|
+
val = lib.dnp3_counter_iterator_next(it)
|
|
93
|
+
if val == ffi.NULL:
|
|
94
|
+
break
|
|
95
|
+
self.counters.append(Counter.from_ffi(val))
|
|
96
|
+
|
|
97
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_frozen_counter_iterator_t*, void*)")
|
|
98
|
+
def _handle_frozen_counter(info, it, ctx):
|
|
99
|
+
with self._lock:
|
|
100
|
+
while True:
|
|
101
|
+
val = lib.dnp3_frozen_counter_iterator_next(it)
|
|
102
|
+
if val == ffi.NULL:
|
|
103
|
+
break
|
|
104
|
+
self.frozen_counters.append(FrozenCounter.from_ffi(val))
|
|
105
|
+
|
|
106
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_analog_input_iterator_t*, void*)")
|
|
107
|
+
def _handle_analog_input(info, it, ctx):
|
|
108
|
+
with self._lock:
|
|
109
|
+
while True:
|
|
110
|
+
val = lib.dnp3_analog_input_iterator_next(it)
|
|
111
|
+
if val == ffi.NULL:
|
|
112
|
+
break
|
|
113
|
+
self.analog_inputs.append(AnalogInput.from_ffi(val))
|
|
114
|
+
|
|
115
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_analog_output_status_iterator_t*, void*)")
|
|
116
|
+
def _handle_aos(info, it, ctx):
|
|
117
|
+
with self._lock:
|
|
118
|
+
while True:
|
|
119
|
+
val = lib.dnp3_analog_output_status_iterator_next(it)
|
|
120
|
+
if val == ffi.NULL:
|
|
121
|
+
break
|
|
122
|
+
self.analog_output_statuses.append(AnalogOutputStatus.from_ffi(val))
|
|
123
|
+
|
|
124
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_octet_string_iterator_t*, void*)")
|
|
125
|
+
def _handle_octet_string(info, it, ctx):
|
|
126
|
+
with self._lock:
|
|
127
|
+
while True:
|
|
128
|
+
val = lib.dnp3_octet_string_iterator_next(it)
|
|
129
|
+
if val == ffi.NULL:
|
|
130
|
+
break
|
|
131
|
+
data = []
|
|
132
|
+
while True:
|
|
133
|
+
byte_ptr = lib.dnp3_byte_iterator_next(val.value)
|
|
134
|
+
if byte_ptr == ffi.NULL:
|
|
135
|
+
break
|
|
136
|
+
data.append(byte_ptr[0])
|
|
137
|
+
self.octet_strings.append({"index": val.index, "value": bytes(data)})
|
|
138
|
+
|
|
139
|
+
@ffi.callback("void(dnp3_header_info_t, dnp3_string_attr_t, uint8_t, uint8_t, const char*, void*)")
|
|
140
|
+
def _handle_string_attr(info, attr, set_id, variation, value, ctx):
|
|
141
|
+
with self._lock:
|
|
142
|
+
self.string_attrs.append({
|
|
143
|
+
"attr": int(attr),
|
|
144
|
+
"set": set_id,
|
|
145
|
+
"variation": variation,
|
|
146
|
+
"value": ffi.string(value).decode() if value != ffi.NULL else "",
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
# Store references to prevent GC
|
|
150
|
+
self._begin_fragment = _begin_fragment
|
|
151
|
+
self._end_fragment = _end_fragment
|
|
152
|
+
self._handle_binary_input = _handle_binary_input
|
|
153
|
+
self._handle_double_bit = _handle_double_bit
|
|
154
|
+
self._handle_bos = _handle_bos
|
|
155
|
+
self._handle_counter = _handle_counter
|
|
156
|
+
self._handle_frozen_counter = _handle_frozen_counter
|
|
157
|
+
self._handle_analog_input = _handle_analog_input
|
|
158
|
+
self._handle_aos = _handle_aos
|
|
159
|
+
self._handle_octet_string = _handle_octet_string
|
|
160
|
+
self._handle_string_attr = _handle_string_attr
|
|
161
|
+
|
|
162
|
+
def clear(self):
|
|
163
|
+
"""Reset all collected data."""
|
|
164
|
+
with self._lock:
|
|
165
|
+
self.binary_inputs.clear()
|
|
166
|
+
self.double_bit_binary_inputs.clear()
|
|
167
|
+
self.binary_output_statuses.clear()
|
|
168
|
+
self.counters.clear()
|
|
169
|
+
self.frozen_counters.clear()
|
|
170
|
+
self.analog_inputs.clear()
|
|
171
|
+
self.analog_output_statuses.clear()
|
|
172
|
+
self.octet_strings.clear()
|
|
173
|
+
self.string_attrs.clear()
|
|
174
|
+
self.iin = None
|
|
175
|
+
|
|
176
|
+
def as_ffi(self):
|
|
177
|
+
"""Return a dnp3_read_handler_t struct for use with the C FFI."""
|
|
178
|
+
return ffi.new("dnp3_read_handler_t*", {
|
|
179
|
+
"begin_fragment": self._begin_fragment,
|
|
180
|
+
"end_fragment": self._end_fragment,
|
|
181
|
+
"handle_binary_input": self._handle_binary_input,
|
|
182
|
+
"handle_double_bit_binary_input": self._handle_double_bit,
|
|
183
|
+
"handle_binary_output_status": self._handle_bos,
|
|
184
|
+
"handle_counter": self._handle_counter,
|
|
185
|
+
"handle_frozen_counter": self._handle_frozen_counter,
|
|
186
|
+
"handle_analog_input": self._handle_analog_input,
|
|
187
|
+
"handle_analog_output_status": self._handle_aos,
|
|
188
|
+
"handle_octet_string": self._handle_octet_string,
|
|
189
|
+
"handle_string_attr": self._handle_string_attr,
|
|
190
|
+
"on_destroy": ffi.NULL,
|
|
191
|
+
"ctx": ffi.NULL,
|
|
192
|
+
})[0]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ClientStateListener:
|
|
196
|
+
"""Tracks master channel connection state changes."""
|
|
197
|
+
|
|
198
|
+
def __init__(self):
|
|
199
|
+
self.state = None
|
|
200
|
+
self._event = threading.Event()
|
|
201
|
+
self._lock = threading.Lock()
|
|
202
|
+
|
|
203
|
+
@ffi.callback("void(dnp3_client_state_t, void*)")
|
|
204
|
+
def _on_change(state, ctx):
|
|
205
|
+
with self._lock:
|
|
206
|
+
self.state = int(state)
|
|
207
|
+
self._event.set()
|
|
208
|
+
|
|
209
|
+
self._on_change = _on_change
|
|
210
|
+
|
|
211
|
+
def wait_for_state(self, expected_state, timeout=10.0) -> bool:
|
|
212
|
+
"""Wait for a specific state, returns True if reached."""
|
|
213
|
+
deadline = threading.Event()
|
|
214
|
+
while True:
|
|
215
|
+
with self._lock:
|
|
216
|
+
if self.state == expected_state:
|
|
217
|
+
return True
|
|
218
|
+
if not self._event.wait(timeout):
|
|
219
|
+
return False
|
|
220
|
+
self._event.clear()
|
|
221
|
+
|
|
222
|
+
def as_ffi(self):
|
|
223
|
+
return ffi.new("dnp3_client_state_listener_t*", {
|
|
224
|
+
"on_change": self._on_change,
|
|
225
|
+
"on_destroy": ffi.NULL,
|
|
226
|
+
"ctx": ffi.NULL,
|
|
227
|
+
})[0]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class PortStateListener:
|
|
231
|
+
"""Tracks serial port state changes."""
|
|
232
|
+
|
|
233
|
+
def __init__(self):
|
|
234
|
+
self.state = None
|
|
235
|
+
|
|
236
|
+
@ffi.callback("void(dnp3_port_state_t, void*)")
|
|
237
|
+
def _on_change(state, ctx):
|
|
238
|
+
self.state = int(state)
|
|
239
|
+
|
|
240
|
+
self._on_change = _on_change
|
|
241
|
+
|
|
242
|
+
def as_ffi(self):
|
|
243
|
+
return ffi.new("dnp3_port_state_listener_t*", {
|
|
244
|
+
"on_change": self._on_change,
|
|
245
|
+
"on_destroy": ffi.NULL,
|
|
246
|
+
"ctx": ffi.NULL,
|
|
247
|
+
})[0]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class ConnectionStateListener:
|
|
251
|
+
"""Tracks outstation server connection state changes."""
|
|
252
|
+
|
|
253
|
+
def __init__(self):
|
|
254
|
+
self.state = None
|
|
255
|
+
|
|
256
|
+
@ffi.callback("void(dnp3_connection_state_t, void*)")
|
|
257
|
+
def _on_change(state, ctx):
|
|
258
|
+
self.state = int(state)
|
|
259
|
+
|
|
260
|
+
self._on_change = _on_change
|
|
261
|
+
|
|
262
|
+
def as_ffi(self):
|
|
263
|
+
return ffi.new("dnp3_connection_state_listener_t*", {
|
|
264
|
+
"on_change": self._on_change,
|
|
265
|
+
"on_destroy": ffi.NULL,
|
|
266
|
+
"ctx": ffi.NULL,
|
|
267
|
+
})[0]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class AssociationHandler:
|
|
271
|
+
"""Provides time synchronization for master-outstation association."""
|
|
272
|
+
|
|
273
|
+
def __init__(self):
|
|
274
|
+
@ffi.callback("dnp3_utc_timestamp_t(void*)")
|
|
275
|
+
def _get_current_time(ctx):
|
|
276
|
+
import time
|
|
277
|
+
return ffi.new("dnp3_utc_timestamp_t*", {
|
|
278
|
+
"value": int(time.time() * 1000),
|
|
279
|
+
"is_valid": True,
|
|
280
|
+
})[0]
|
|
281
|
+
|
|
282
|
+
self._get_current_time = _get_current_time
|
|
283
|
+
|
|
284
|
+
def as_ffi(self):
|
|
285
|
+
return ffi.new("dnp3_association_handler_t*", {
|
|
286
|
+
"get_current_time": self._get_current_time,
|
|
287
|
+
"on_destroy": ffi.NULL,
|
|
288
|
+
"ctx": ffi.NULL,
|
|
289
|
+
})[0]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class AssociationInformation:
|
|
293
|
+
"""Receives task status notifications."""
|
|
294
|
+
|
|
295
|
+
def __init__(self):
|
|
296
|
+
self.last_task_type = None
|
|
297
|
+
self.last_error = None
|
|
298
|
+
|
|
299
|
+
@ffi.callback("void(dnp3_task_type_t, dnp3_function_code_t, uint8_t, void*)")
|
|
300
|
+
def _task_start(task_type, fc, seq, ctx):
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
@ffi.callback("void(dnp3_task_type_t, dnp3_function_code_t, uint8_t, void*)")
|
|
304
|
+
def _task_success(task_type, fc, seq, ctx):
|
|
305
|
+
self.last_task_type = int(task_type)
|
|
306
|
+
|
|
307
|
+
@ffi.callback("void(dnp3_task_type_t, dnp3_task_error_t, void*)")
|
|
308
|
+
def _task_fail(task_type, error, ctx):
|
|
309
|
+
self.last_task_type = int(task_type)
|
|
310
|
+
self.last_error = int(error)
|
|
311
|
+
|
|
312
|
+
@ffi.callback("void(bool, uint8_t, void*)")
|
|
313
|
+
def _unsolicited_response(is_duplicate, seq, ctx):
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
self._task_start = _task_start
|
|
317
|
+
self._task_success = _task_success
|
|
318
|
+
self._task_fail = _task_fail
|
|
319
|
+
self._unsolicited_response = _unsolicited_response
|
|
320
|
+
|
|
321
|
+
def as_ffi(self):
|
|
322
|
+
return ffi.new("dnp3_association_information_t*", {
|
|
323
|
+
"task_start": self._task_start,
|
|
324
|
+
"task_success": self._task_success,
|
|
325
|
+
"task_fail": self._task_fail,
|
|
326
|
+
"unsolicited_response": self._unsolicited_response,
|
|
327
|
+
"on_destroy": ffi.NULL,
|
|
328
|
+
"ctx": ffi.NULL,
|
|
329
|
+
})[0]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class TaskCallback:
|
|
333
|
+
"""Generic callback for async operations (read, command, restart, etc.)."""
|
|
334
|
+
|
|
335
|
+
def __init__(self):
|
|
336
|
+
self.success = False
|
|
337
|
+
self.result = None
|
|
338
|
+
self.error = None
|
|
339
|
+
self._event = threading.Event()
|
|
340
|
+
|
|
341
|
+
def wait(self, timeout=30.0) -> bool:
|
|
342
|
+
"""Wait for the callback to fire. Returns True if it completed."""
|
|
343
|
+
return self._event.wait(timeout)
|
|
344
|
+
|
|
345
|
+
def _signal_success(self, result=None):
|
|
346
|
+
self.success = True
|
|
347
|
+
self.result = result
|
|
348
|
+
self._event.set()
|
|
349
|
+
|
|
350
|
+
def _signal_failure(self, error):
|
|
351
|
+
self.success = False
|
|
352
|
+
self.error = error
|
|
353
|
+
self._event.set()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class ReadTaskCallback(TaskCallback):
|
|
357
|
+
"""Callback for read operations."""
|
|
358
|
+
|
|
359
|
+
def __init__(self):
|
|
360
|
+
super().__init__()
|
|
361
|
+
|
|
362
|
+
@ffi.callback("void(dnp3_nothing_t, void*)")
|
|
363
|
+
def _on_complete(nothing, ctx):
|
|
364
|
+
self._signal_success()
|
|
365
|
+
|
|
366
|
+
@ffi.callback("void(dnp3_read_error_t, void*)")
|
|
367
|
+
def _on_failure(error, ctx):
|
|
368
|
+
self._signal_failure(int(error))
|
|
369
|
+
|
|
370
|
+
self._on_complete = _on_complete
|
|
371
|
+
self._on_failure = _on_failure
|
|
372
|
+
|
|
373
|
+
def as_ffi(self):
|
|
374
|
+
return ffi.new("dnp3_read_task_callback_t*", {
|
|
375
|
+
"on_complete": self._on_complete,
|
|
376
|
+
"on_failure": self._on_failure,
|
|
377
|
+
"on_destroy": ffi.NULL,
|
|
378
|
+
"ctx": ffi.NULL,
|
|
379
|
+
})[0]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class CommandTaskCallback(TaskCallback):
|
|
383
|
+
"""Callback for command (operate/select) operations."""
|
|
384
|
+
|
|
385
|
+
def __init__(self):
|
|
386
|
+
super().__init__()
|
|
387
|
+
|
|
388
|
+
@ffi.callback("void(dnp3_nothing_t, void*)")
|
|
389
|
+
def _on_complete(nothing, ctx):
|
|
390
|
+
self._signal_success()
|
|
391
|
+
|
|
392
|
+
@ffi.callback("void(dnp3_command_error_t, void*)")
|
|
393
|
+
def _on_failure(error, ctx):
|
|
394
|
+
self._signal_failure(int(error))
|
|
395
|
+
|
|
396
|
+
self._on_complete = _on_complete
|
|
397
|
+
self._on_failure = _on_failure
|
|
398
|
+
|
|
399
|
+
def as_ffi(self):
|
|
400
|
+
return ffi.new("dnp3_command_task_callback_t*", {
|
|
401
|
+
"on_complete": self._on_complete,
|
|
402
|
+
"on_failure": self._on_failure,
|
|
403
|
+
"on_destroy": ffi.NULL,
|
|
404
|
+
"ctx": ffi.NULL,
|
|
405
|
+
})[0]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class TimeSyncTaskCallback(TaskCallback):
|
|
409
|
+
"""Callback for time sync operations."""
|
|
410
|
+
|
|
411
|
+
def __init__(self):
|
|
412
|
+
super().__init__()
|
|
413
|
+
|
|
414
|
+
@ffi.callback("void(dnp3_nothing_t, void*)")
|
|
415
|
+
def _on_complete(nothing, ctx):
|
|
416
|
+
self._signal_success()
|
|
417
|
+
|
|
418
|
+
@ffi.callback("void(dnp3_time_sync_error_t, void*)")
|
|
419
|
+
def _on_failure(error, ctx):
|
|
420
|
+
self._signal_failure(int(error))
|
|
421
|
+
|
|
422
|
+
self._on_complete = _on_complete
|
|
423
|
+
self._on_failure = _on_failure
|
|
424
|
+
|
|
425
|
+
def as_ffi(self):
|
|
426
|
+
return ffi.new("dnp3_time_sync_task_callback_t*", {
|
|
427
|
+
"on_complete": self._on_complete,
|
|
428
|
+
"on_failure": self._on_failure,
|
|
429
|
+
"on_destroy": ffi.NULL,
|
|
430
|
+
"ctx": ffi.NULL,
|
|
431
|
+
})[0]
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class RestartTaskCallback(TaskCallback):
|
|
435
|
+
"""Callback for restart operations — returns delay in ms."""
|
|
436
|
+
|
|
437
|
+
def __init__(self):
|
|
438
|
+
super().__init__()
|
|
439
|
+
|
|
440
|
+
@ffi.callback("void(uint64_t, void*)")
|
|
441
|
+
def _on_complete(delay, ctx):
|
|
442
|
+
self._signal_success(delay)
|
|
443
|
+
|
|
444
|
+
@ffi.callback("void(dnp3_restart_error_t, void*)")
|
|
445
|
+
def _on_failure(error, ctx):
|
|
446
|
+
self._signal_failure(int(error))
|
|
447
|
+
|
|
448
|
+
self._on_complete = _on_complete
|
|
449
|
+
self._on_failure = _on_failure
|
|
450
|
+
|
|
451
|
+
def as_ffi(self):
|
|
452
|
+
return ffi.new("dnp3_restart_task_callback_t*", {
|
|
453
|
+
"on_complete": self._on_complete,
|
|
454
|
+
"on_failure": self._on_failure,
|
|
455
|
+
"on_destroy": ffi.NULL,
|
|
456
|
+
"ctx": ffi.NULL,
|
|
457
|
+
})[0]
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class EmptyResponseCallback(TaskCallback):
|
|
461
|
+
"""Callback for operations that expect empty responses."""
|
|
462
|
+
|
|
463
|
+
def __init__(self):
|
|
464
|
+
super().__init__()
|
|
465
|
+
|
|
466
|
+
@ffi.callback("void(dnp3_nothing_t, void*)")
|
|
467
|
+
def _on_complete(nothing, ctx):
|
|
468
|
+
self._signal_success()
|
|
469
|
+
|
|
470
|
+
@ffi.callback("void(dnp3_empty_response_error_t, void*)")
|
|
471
|
+
def _on_failure(error, ctx):
|
|
472
|
+
self._signal_failure(int(error))
|
|
473
|
+
|
|
474
|
+
self._on_complete = _on_complete
|
|
475
|
+
self._on_failure = _on_failure
|
|
476
|
+
|
|
477
|
+
def as_ffi(self):
|
|
478
|
+
return ffi.new("dnp3_empty_response_callback_t*", {
|
|
479
|
+
"on_complete": self._on_complete,
|
|
480
|
+
"on_failure": self._on_failure,
|
|
481
|
+
"on_destroy": ffi.NULL,
|
|
482
|
+
"ctx": ffi.NULL,
|
|
483
|
+
})[0]
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class LinkStatusCallback(TaskCallback):
|
|
487
|
+
"""Callback for link status check."""
|
|
488
|
+
|
|
489
|
+
def __init__(self):
|
|
490
|
+
super().__init__()
|
|
491
|
+
|
|
492
|
+
@ffi.callback("void(dnp3_nothing_t, void*)")
|
|
493
|
+
def _on_complete(nothing, ctx):
|
|
494
|
+
self._signal_success()
|
|
495
|
+
|
|
496
|
+
@ffi.callback("void(dnp3_link_status_error_t, void*)")
|
|
497
|
+
def _on_failure(error, ctx):
|
|
498
|
+
self._signal_failure(int(error))
|
|
499
|
+
|
|
500
|
+
self._on_complete = _on_complete
|
|
501
|
+
self._on_failure = _on_failure
|
|
502
|
+
|
|
503
|
+
def as_ffi(self):
|
|
504
|
+
return ffi.new("dnp3_link_status_callback_t*", {
|
|
505
|
+
"on_complete": self._on_complete,
|
|
506
|
+
"on_failure": self._on_failure,
|
|
507
|
+
"on_destroy": ffi.NULL,
|
|
508
|
+
"ctx": ffi.NULL,
|
|
509
|
+
})[0]
|
dnp3/logging.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DNP3 protocol logging configuration.
|
|
3
|
+
|
|
4
|
+
Wraps the stepfunc/dnp3 tracing/logging subsystem.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from typing import Optional, Callable
|
|
9
|
+
|
|
10
|
+
from ._ffi import ffi, lib, check_error
|
|
11
|
+
|
|
12
|
+
_logging_configured = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LogLevel(IntEnum):
|
|
16
|
+
ERROR = 0
|
|
17
|
+
WARN = 1
|
|
18
|
+
INFO = 2
|
|
19
|
+
DEBUG = 3
|
|
20
|
+
TRACE = 4
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LogOutputFormat(IntEnum):
|
|
24
|
+
TEXT = 0
|
|
25
|
+
JSON = 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TimeFormat(IntEnum):
|
|
29
|
+
NONE = 0
|
|
30
|
+
RFC_3339 = 1
|
|
31
|
+
SYSTEM = 2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def configure_logging(
|
|
35
|
+
level: LogLevel = LogLevel.WARN,
|
|
36
|
+
output_format: LogOutputFormat = LogOutputFormat.TEXT,
|
|
37
|
+
print_level: bool = True,
|
|
38
|
+
print_module_info: bool = False,
|
|
39
|
+
time_format: TimeFormat = TimeFormat.SYSTEM,
|
|
40
|
+
callback: Optional[Callable[[LogLevel, str], None]] = None,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Configure the DNP3 library logging.
|
|
44
|
+
|
|
45
|
+
Can only be called once. Subsequent calls will be ignored.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
level: Minimum log level
|
|
49
|
+
output_format: Text or JSON
|
|
50
|
+
print_level: Include log level in output
|
|
51
|
+
print_module_info: Include module info
|
|
52
|
+
time_format: Timestamp format
|
|
53
|
+
callback: Optional callback for log messages. If None, logs to stdout.
|
|
54
|
+
"""
|
|
55
|
+
global _logging_configured
|
|
56
|
+
if _logging_configured:
|
|
57
|
+
return
|
|
58
|
+
_logging_configured = True
|
|
59
|
+
|
|
60
|
+
config = ffi.new("dnp3_logging_config_t*")
|
|
61
|
+
config.level = level.value
|
|
62
|
+
config.output_format = output_format.value
|
|
63
|
+
config.print_level = print_level
|
|
64
|
+
config.print_module_info = print_module_info
|
|
65
|
+
config.time_format = time_format.value
|
|
66
|
+
|
|
67
|
+
if callback is not None:
|
|
68
|
+
@ffi.callback("void(dnp3_log_level_t, const char*, void*)")
|
|
69
|
+
def _on_message(lvl, msg, ctx):
|
|
70
|
+
callback(LogLevel(lvl), ffi.string(msg).decode())
|
|
71
|
+
|
|
72
|
+
# Must keep reference alive
|
|
73
|
+
configure_logging._callback_ref = _on_message
|
|
74
|
+
|
|
75
|
+
logger = ffi.new("dnp3_logger_t*", {
|
|
76
|
+
"on_message": _on_message,
|
|
77
|
+
"on_destroy": ffi.NULL,
|
|
78
|
+
"ctx": ffi.NULL,
|
|
79
|
+
})[0]
|
|
80
|
+
else:
|
|
81
|
+
@ffi.callback("void(dnp3_log_level_t, const char*, void*)")
|
|
82
|
+
def _on_message(lvl, msg, ctx):
|
|
83
|
+
print(ffi.string(msg).decode(), end="")
|
|
84
|
+
|
|
85
|
+
configure_logging._callback_ref = _on_message
|
|
86
|
+
|
|
87
|
+
logger = ffi.new("dnp3_logger_t*", {
|
|
88
|
+
"on_message": _on_message,
|
|
89
|
+
"on_destroy": ffi.NULL,
|
|
90
|
+
"ctx": ffi.NULL,
|
|
91
|
+
})[0]
|
|
92
|
+
|
|
93
|
+
lib.dnp3_configure_logging(config[0], logger)
|