dnp3py 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.
- dnp3/__init__.py +3 -0
- dnp3/application/__init__.py +97 -0
- dnp3/application/builder.py +531 -0
- dnp3/application/fragment.py +189 -0
- dnp3/application/header.py +330 -0
- dnp3/application/parser.py +326 -0
- dnp3/application/qualifiers.py +384 -0
- dnp3/core/__init__.py +76 -0
- dnp3/core/crc.py +77 -0
- dnp3/core/enums.py +175 -0
- dnp3/core/exceptions.py +41 -0
- dnp3/core/flags.py +137 -0
- dnp3/core/timestamp.py +86 -0
- dnp3/core/types.py +31 -0
- dnp3/database/__init__.py +57 -0
- dnp3/database/database.py +640 -0
- dnp3/database/event_buffer.py +459 -0
- dnp3/database/point.py +383 -0
- dnp3/datalink/__init__.py +54 -0
- dnp3/datalink/builder.py +337 -0
- dnp3/datalink/control.py +82 -0
- dnp3/datalink/frame.py +197 -0
- dnp3/datalink/parser.py +240 -0
- dnp3/master/__init__.py +51 -0
- dnp3/master/commands.py +503 -0
- dnp3/master/config.py +88 -0
- dnp3/master/handler.py +310 -0
- dnp3/master/master.py +671 -0
- dnp3/master/polling.py +327 -0
- dnp3/master/state.py +286 -0
- dnp3/objects/__init__.py +138 -0
- dnp3/objects/analog_input.py +625 -0
- dnp3/objects/base.py +159 -0
- dnp3/objects/binary_input.py +212 -0
- dnp3/objects/binary_output.py +346 -0
- dnp3/objects/class_data.py +112 -0
- dnp3/objects/counter.py +580 -0
- dnp3/objects/registry.py +124 -0
- dnp3/objects/time.py +204 -0
- dnp3/outstation/__init__.py +31 -0
- dnp3/outstation/config.py +93 -0
- dnp3/outstation/handler.py +316 -0
- dnp3/outstation/outstation.py +1115 -0
- dnp3/outstation/state.py +333 -0
- dnp3/py.typed +0 -0
- dnp3/transport/__init__.py +35 -0
- dnp3/transport/reassembler.py +189 -0
- dnp3/transport/segment.py +216 -0
- dnp3/transport/segmenter.py +123 -0
- dnp3/transport_io/__init__.py +56 -0
- dnp3/transport_io/channel.py +307 -0
- dnp3/transport_io/simulator.py +443 -0
- dnp3/transport_io/tcp_client.py +337 -0
- dnp3/transport_io/tcp_server.py +469 -0
- dnp3py-0.1.0.dist-info/METADATA +187 -0
- dnp3py-0.1.0.dist-info/RECORD +58 -0
- dnp3py-0.1.0.dist-info/WHEEL +4 -0
- dnp3py-0.1.0.dist-info/licenses/LICENSE +21 -0
dnp3/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Application Layer implementation per IEEE 1815-2012 Clause 4."""
|
|
2
|
+
|
|
3
|
+
from dnp3.application.builder import (
|
|
4
|
+
build_all_objects_request,
|
|
5
|
+
build_class_poll,
|
|
6
|
+
build_cold_restart_request,
|
|
7
|
+
build_confirm_request,
|
|
8
|
+
build_count_request,
|
|
9
|
+
build_delay_measure_request,
|
|
10
|
+
build_direct_operate_request,
|
|
11
|
+
build_disable_unsolicited_request,
|
|
12
|
+
build_enable_unsolicited_request,
|
|
13
|
+
build_integrity_poll,
|
|
14
|
+
build_null_response,
|
|
15
|
+
build_operate_request,
|
|
16
|
+
build_range_request,
|
|
17
|
+
build_read_request,
|
|
18
|
+
build_response,
|
|
19
|
+
build_select_request,
|
|
20
|
+
build_unsolicited_response,
|
|
21
|
+
build_warm_restart_request,
|
|
22
|
+
build_write_request,
|
|
23
|
+
)
|
|
24
|
+
from dnp3.application.fragment import (
|
|
25
|
+
DEFAULT_MAX_FRAGMENT_SIZE,
|
|
26
|
+
MAX_FRAGMENT_SIZE,
|
|
27
|
+
MIN_FRAGMENT_SIZE,
|
|
28
|
+
ObjectBlock,
|
|
29
|
+
RequestFragment,
|
|
30
|
+
ResponseFragment,
|
|
31
|
+
)
|
|
32
|
+
from dnp3.application.header import (
|
|
33
|
+
MAX_APP_SEQUENCE,
|
|
34
|
+
REQUEST_HEADER_SIZE,
|
|
35
|
+
RESPONSE_HEADER_SIZE,
|
|
36
|
+
ApplicationControl,
|
|
37
|
+
RequestHeader,
|
|
38
|
+
ResponseHeader,
|
|
39
|
+
)
|
|
40
|
+
from dnp3.application.parser import (
|
|
41
|
+
ParseError,
|
|
42
|
+
is_request,
|
|
43
|
+
is_response,
|
|
44
|
+
parse_request,
|
|
45
|
+
parse_response,
|
|
46
|
+
)
|
|
47
|
+
from dnp3.application.qualifiers import (
|
|
48
|
+
CountRange,
|
|
49
|
+
ObjectHeader,
|
|
50
|
+
PrefixCode,
|
|
51
|
+
RangeCode,
|
|
52
|
+
StartStopRange,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"DEFAULT_MAX_FRAGMENT_SIZE",
|
|
57
|
+
"MAX_APP_SEQUENCE",
|
|
58
|
+
"MAX_FRAGMENT_SIZE",
|
|
59
|
+
"MIN_FRAGMENT_SIZE",
|
|
60
|
+
"REQUEST_HEADER_SIZE",
|
|
61
|
+
"RESPONSE_HEADER_SIZE",
|
|
62
|
+
"ApplicationControl",
|
|
63
|
+
"CountRange",
|
|
64
|
+
"ObjectBlock",
|
|
65
|
+
"ObjectHeader",
|
|
66
|
+
"ParseError",
|
|
67
|
+
"PrefixCode",
|
|
68
|
+
"RangeCode",
|
|
69
|
+
"RequestFragment",
|
|
70
|
+
"RequestHeader",
|
|
71
|
+
"ResponseFragment",
|
|
72
|
+
"ResponseHeader",
|
|
73
|
+
"StartStopRange",
|
|
74
|
+
"build_all_objects_request",
|
|
75
|
+
"build_class_poll",
|
|
76
|
+
"build_cold_restart_request",
|
|
77
|
+
"build_confirm_request",
|
|
78
|
+
"build_count_request",
|
|
79
|
+
"build_delay_measure_request",
|
|
80
|
+
"build_direct_operate_request",
|
|
81
|
+
"build_disable_unsolicited_request",
|
|
82
|
+
"build_enable_unsolicited_request",
|
|
83
|
+
"build_integrity_poll",
|
|
84
|
+
"build_null_response",
|
|
85
|
+
"build_operate_request",
|
|
86
|
+
"build_range_request",
|
|
87
|
+
"build_read_request",
|
|
88
|
+
"build_response",
|
|
89
|
+
"build_select_request",
|
|
90
|
+
"build_unsolicited_response",
|
|
91
|
+
"build_warm_restart_request",
|
|
92
|
+
"build_write_request",
|
|
93
|
+
"is_request",
|
|
94
|
+
"is_response",
|
|
95
|
+
"parse_request",
|
|
96
|
+
"parse_response",
|
|
97
|
+
]
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""Application layer builder for constructing requests and responses.
|
|
2
|
+
|
|
3
|
+
Factory functions for building common DNP3 application layer messages.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dnp3.application.fragment import ObjectBlock, RequestFragment, ResponseFragment
|
|
7
|
+
from dnp3.application.header import RequestHeader, ResponseHeader
|
|
8
|
+
from dnp3.application.qualifiers import (
|
|
9
|
+
CountRange,
|
|
10
|
+
ObjectHeader,
|
|
11
|
+
PrefixCode,
|
|
12
|
+
RangeCode,
|
|
13
|
+
StartStopRange,
|
|
14
|
+
)
|
|
15
|
+
from dnp3.core.enums import FunctionCode
|
|
16
|
+
from dnp3.core.flags import IIN
|
|
17
|
+
|
|
18
|
+
# Size limits for range specifier selection
|
|
19
|
+
MAX_UINT8 = 255
|
|
20
|
+
MAX_UINT16 = 65535
|
|
21
|
+
|
|
22
|
+
# Class data group/variations (Group 60)
|
|
23
|
+
CLASS_0_GV = (60, 1)
|
|
24
|
+
CLASS_1_GV = (60, 2)
|
|
25
|
+
CLASS_2_GV = (60, 3)
|
|
26
|
+
CLASS_3_GV = (60, 4)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_read_request(
|
|
30
|
+
objects: tuple[ObjectBlock, ...],
|
|
31
|
+
seq: int = 0,
|
|
32
|
+
fir: bool = True,
|
|
33
|
+
fin: bool = True,
|
|
34
|
+
) -> RequestFragment:
|
|
35
|
+
"""Build a READ request.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
objects: Object blocks to read.
|
|
39
|
+
seq: Sequence number.
|
|
40
|
+
fir: First fragment flag.
|
|
41
|
+
fin: Final fragment flag.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
RequestFragment for READ.
|
|
45
|
+
"""
|
|
46
|
+
header = RequestHeader.build(function=FunctionCode.READ, seq=seq, fir=fir, fin=fin)
|
|
47
|
+
return RequestFragment(header=header, objects=objects)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_write_request(
|
|
51
|
+
objects: tuple[ObjectBlock, ...],
|
|
52
|
+
seq: int = 0,
|
|
53
|
+
fir: bool = True,
|
|
54
|
+
fin: bool = True,
|
|
55
|
+
) -> RequestFragment:
|
|
56
|
+
"""Build a WRITE request.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
objects: Object blocks to write.
|
|
60
|
+
seq: Sequence number.
|
|
61
|
+
fir: First fragment flag.
|
|
62
|
+
fin: Final fragment flag.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
RequestFragment for WRITE.
|
|
66
|
+
"""
|
|
67
|
+
header = RequestHeader.build(function=FunctionCode.WRITE, seq=seq, fir=fir, fin=fin)
|
|
68
|
+
return RequestFragment(header=header, objects=objects)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_integrity_poll(seq: int = 0) -> RequestFragment:
|
|
72
|
+
"""Build an integrity poll (READ class 0, 1, 2, 3).
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
seq: Sequence number.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
RequestFragment for integrity poll.
|
|
79
|
+
"""
|
|
80
|
+
blocks = []
|
|
81
|
+
for group, variation in [CLASS_1_GV, CLASS_2_GV, CLASS_3_GV, CLASS_0_GV]:
|
|
82
|
+
header = ObjectHeader.build(
|
|
83
|
+
group=group,
|
|
84
|
+
variation=variation,
|
|
85
|
+
prefix=PrefixCode.NONE,
|
|
86
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
87
|
+
)
|
|
88
|
+
blocks.append(ObjectBlock(header=header))
|
|
89
|
+
|
|
90
|
+
return build_read_request(objects=tuple(blocks), seq=seq)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_class_poll(
|
|
94
|
+
class_1: bool = True,
|
|
95
|
+
class_2: bool = True,
|
|
96
|
+
class_3: bool = True,
|
|
97
|
+
seq: int = 0,
|
|
98
|
+
) -> RequestFragment:
|
|
99
|
+
"""Build an event class poll.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
class_1: Include Class 1 events.
|
|
103
|
+
class_2: Include Class 2 events.
|
|
104
|
+
class_3: Include Class 3 events.
|
|
105
|
+
seq: Sequence number.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
RequestFragment for event poll.
|
|
109
|
+
"""
|
|
110
|
+
blocks = []
|
|
111
|
+
if class_1:
|
|
112
|
+
header = ObjectHeader.build(
|
|
113
|
+
group=CLASS_1_GV[0],
|
|
114
|
+
variation=CLASS_1_GV[1],
|
|
115
|
+
prefix=PrefixCode.NONE,
|
|
116
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
117
|
+
)
|
|
118
|
+
blocks.append(ObjectBlock(header=header))
|
|
119
|
+
if class_2:
|
|
120
|
+
header = ObjectHeader.build(
|
|
121
|
+
group=CLASS_2_GV[0],
|
|
122
|
+
variation=CLASS_2_GV[1],
|
|
123
|
+
prefix=PrefixCode.NONE,
|
|
124
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
125
|
+
)
|
|
126
|
+
blocks.append(ObjectBlock(header=header))
|
|
127
|
+
if class_3:
|
|
128
|
+
header = ObjectHeader.build(
|
|
129
|
+
group=CLASS_3_GV[0],
|
|
130
|
+
variation=CLASS_3_GV[1],
|
|
131
|
+
prefix=PrefixCode.NONE,
|
|
132
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
133
|
+
)
|
|
134
|
+
blocks.append(ObjectBlock(header=header))
|
|
135
|
+
|
|
136
|
+
return build_read_request(objects=tuple(blocks), seq=seq)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def build_all_objects_request(
|
|
140
|
+
function: FunctionCode,
|
|
141
|
+
group: int,
|
|
142
|
+
variation: int,
|
|
143
|
+
seq: int = 0,
|
|
144
|
+
) -> RequestFragment:
|
|
145
|
+
"""Build a request for all objects of a type.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
function: Function code (e.g., READ, WRITE).
|
|
149
|
+
group: Object group number.
|
|
150
|
+
variation: Object variation number.
|
|
151
|
+
seq: Sequence number.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
RequestFragment.
|
|
155
|
+
"""
|
|
156
|
+
obj_header = ObjectHeader.build(
|
|
157
|
+
group=group,
|
|
158
|
+
variation=variation,
|
|
159
|
+
prefix=PrefixCode.NONE,
|
|
160
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
161
|
+
)
|
|
162
|
+
block = ObjectBlock(header=obj_header)
|
|
163
|
+
header = RequestHeader.build(function=function, seq=seq)
|
|
164
|
+
return RequestFragment(header=header, objects=(block,))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_range_request(
|
|
168
|
+
function: FunctionCode,
|
|
169
|
+
group: int,
|
|
170
|
+
variation: int,
|
|
171
|
+
start: int,
|
|
172
|
+
stop: int,
|
|
173
|
+
seq: int = 0,
|
|
174
|
+
) -> RequestFragment:
|
|
175
|
+
"""Build a request for a range of objects.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
function: Function code (e.g., READ, WRITE).
|
|
179
|
+
group: Object group number.
|
|
180
|
+
variation: Object variation number.
|
|
181
|
+
start: Start index.
|
|
182
|
+
stop: Stop index (inclusive).
|
|
183
|
+
seq: Sequence number.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
RequestFragment.
|
|
187
|
+
"""
|
|
188
|
+
# Choose appropriate range code based on values
|
|
189
|
+
if start <= MAX_UINT8 and stop <= MAX_UINT8:
|
|
190
|
+
range_code = RangeCode.UINT8_START_STOP
|
|
191
|
+
range_data = StartStopRange(start=start, stop=stop).to_bytes_1()
|
|
192
|
+
elif start <= MAX_UINT16 and stop <= MAX_UINT16:
|
|
193
|
+
range_code = RangeCode.UINT16_START_STOP
|
|
194
|
+
range_data = StartStopRange(start=start, stop=stop).to_bytes_2()
|
|
195
|
+
else:
|
|
196
|
+
range_code = RangeCode.UINT32_START_STOP
|
|
197
|
+
range_data = StartStopRange(start=start, stop=stop).to_bytes_4()
|
|
198
|
+
|
|
199
|
+
obj_header = ObjectHeader.build(
|
|
200
|
+
group=group,
|
|
201
|
+
variation=variation,
|
|
202
|
+
prefix=PrefixCode.NONE,
|
|
203
|
+
range_code=range_code,
|
|
204
|
+
)
|
|
205
|
+
block = ObjectBlock(header=obj_header, data=range_data)
|
|
206
|
+
header = RequestHeader.build(function=function, seq=seq)
|
|
207
|
+
return RequestFragment(header=header, objects=(block,))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_count_request(
|
|
211
|
+
function: FunctionCode,
|
|
212
|
+
group: int,
|
|
213
|
+
variation: int,
|
|
214
|
+
count: int,
|
|
215
|
+
seq: int = 0,
|
|
216
|
+
) -> RequestFragment:
|
|
217
|
+
"""Build a request for a count of objects.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
function: Function code (e.g., READ).
|
|
221
|
+
group: Object group number.
|
|
222
|
+
variation: Object variation number.
|
|
223
|
+
count: Number of objects.
|
|
224
|
+
seq: Sequence number.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
RequestFragment.
|
|
228
|
+
"""
|
|
229
|
+
# Choose appropriate range code based on count
|
|
230
|
+
if count <= MAX_UINT8:
|
|
231
|
+
range_code = RangeCode.UINT8_COUNT
|
|
232
|
+
range_data = CountRange(count=count).to_bytes_1()
|
|
233
|
+
elif count <= MAX_UINT16:
|
|
234
|
+
range_code = RangeCode.UINT16_COUNT
|
|
235
|
+
range_data = CountRange(count=count).to_bytes_2()
|
|
236
|
+
else:
|
|
237
|
+
range_code = RangeCode.UINT32_COUNT
|
|
238
|
+
range_data = CountRange(count=count).to_bytes_4()
|
|
239
|
+
|
|
240
|
+
obj_header = ObjectHeader.build(
|
|
241
|
+
group=group,
|
|
242
|
+
variation=variation,
|
|
243
|
+
prefix=PrefixCode.NONE,
|
|
244
|
+
range_code=range_code,
|
|
245
|
+
)
|
|
246
|
+
block = ObjectBlock(header=obj_header, data=range_data)
|
|
247
|
+
header = RequestHeader.build(function=function, seq=seq)
|
|
248
|
+
return RequestFragment(header=header, objects=(block,))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def build_null_response(
|
|
252
|
+
iin: IIN | None = None,
|
|
253
|
+
seq: int = 0,
|
|
254
|
+
fir: bool = True,
|
|
255
|
+
fin: bool = True,
|
|
256
|
+
) -> ResponseFragment:
|
|
257
|
+
"""Build a null (empty) response.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
iin: Internal indications.
|
|
261
|
+
seq: Sequence number.
|
|
262
|
+
fir: First fragment flag.
|
|
263
|
+
fin: Final fragment flag.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
ResponseFragment with no objects.
|
|
267
|
+
"""
|
|
268
|
+
header = ResponseHeader.build(
|
|
269
|
+
function=FunctionCode.RESPONSE,
|
|
270
|
+
iin=iin,
|
|
271
|
+
seq=seq,
|
|
272
|
+
fir=fir,
|
|
273
|
+
fin=fin,
|
|
274
|
+
)
|
|
275
|
+
return ResponseFragment(header=header)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def build_response(
|
|
279
|
+
objects: tuple[ObjectBlock, ...],
|
|
280
|
+
iin: IIN | None = None,
|
|
281
|
+
seq: int = 0,
|
|
282
|
+
fir: bool = True,
|
|
283
|
+
fin: bool = True,
|
|
284
|
+
) -> ResponseFragment:
|
|
285
|
+
"""Build a response with objects.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
objects: Object blocks to include.
|
|
289
|
+
iin: Internal indications.
|
|
290
|
+
seq: Sequence number.
|
|
291
|
+
fir: First fragment flag.
|
|
292
|
+
fin: Final fragment flag.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
ResponseFragment.
|
|
296
|
+
"""
|
|
297
|
+
header = ResponseHeader.build(
|
|
298
|
+
function=FunctionCode.RESPONSE,
|
|
299
|
+
iin=iin,
|
|
300
|
+
seq=seq,
|
|
301
|
+
fir=fir,
|
|
302
|
+
fin=fin,
|
|
303
|
+
)
|
|
304
|
+
return ResponseFragment(header=header, objects=objects)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def build_unsolicited_response(
|
|
308
|
+
objects: tuple[ObjectBlock, ...],
|
|
309
|
+
iin: IIN | None = None,
|
|
310
|
+
seq: int = 0,
|
|
311
|
+
fir: bool = True,
|
|
312
|
+
fin: bool = True,
|
|
313
|
+
) -> ResponseFragment:
|
|
314
|
+
"""Build an unsolicited response.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
objects: Object blocks to include.
|
|
318
|
+
iin: Internal indications.
|
|
319
|
+
seq: Sequence number.
|
|
320
|
+
fir: First fragment flag.
|
|
321
|
+
fin: Final fragment flag.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
ResponseFragment with UNS flag set.
|
|
325
|
+
"""
|
|
326
|
+
header = ResponseHeader.build(
|
|
327
|
+
function=FunctionCode.UNSOLICITED_RESPONSE,
|
|
328
|
+
iin=iin,
|
|
329
|
+
seq=seq,
|
|
330
|
+
fir=fir,
|
|
331
|
+
fin=fin,
|
|
332
|
+
uns=True,
|
|
333
|
+
)
|
|
334
|
+
return ResponseFragment(header=header, objects=objects)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def build_confirm_request(seq: int) -> RequestFragment:
|
|
338
|
+
"""Build a CONFIRM request.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
seq: Sequence number to confirm.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
RequestFragment for CONFIRM.
|
|
345
|
+
"""
|
|
346
|
+
header = RequestHeader.build(function=FunctionCode.CONFIRM, seq=seq)
|
|
347
|
+
return RequestFragment(header=header)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def build_direct_operate_request(
|
|
351
|
+
objects: tuple[ObjectBlock, ...],
|
|
352
|
+
seq: int = 0,
|
|
353
|
+
) -> RequestFragment:
|
|
354
|
+
"""Build a DIRECT_OPERATE request.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
objects: Control objects to operate.
|
|
358
|
+
seq: Sequence number.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
RequestFragment for DIRECT_OPERATE.
|
|
362
|
+
"""
|
|
363
|
+
header = RequestHeader.build(function=FunctionCode.DIRECT_OPERATE, seq=seq)
|
|
364
|
+
return RequestFragment(header=header, objects=objects)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def build_select_request(
|
|
368
|
+
objects: tuple[ObjectBlock, ...],
|
|
369
|
+
seq: int = 0,
|
|
370
|
+
) -> RequestFragment:
|
|
371
|
+
"""Build a SELECT request (first step of SBO).
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
objects: Control objects to select.
|
|
375
|
+
seq: Sequence number.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
RequestFragment for SELECT.
|
|
379
|
+
"""
|
|
380
|
+
header = RequestHeader.build(function=FunctionCode.SELECT, seq=seq)
|
|
381
|
+
return RequestFragment(header=header, objects=objects)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def build_operate_request(
|
|
385
|
+
objects: tuple[ObjectBlock, ...],
|
|
386
|
+
seq: int = 0,
|
|
387
|
+
) -> RequestFragment:
|
|
388
|
+
"""Build an OPERATE request (second step of SBO).
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
objects: Control objects to operate.
|
|
392
|
+
seq: Sequence number.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
RequestFragment for OPERATE.
|
|
396
|
+
"""
|
|
397
|
+
header = RequestHeader.build(function=FunctionCode.OPERATE, seq=seq)
|
|
398
|
+
return RequestFragment(header=header, objects=objects)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def build_delay_measure_request(seq: int = 0) -> RequestFragment:
|
|
402
|
+
"""Build a DELAY_MEASURE request for time sync.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
seq: Sequence number.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
RequestFragment for DELAY_MEASURE.
|
|
409
|
+
"""
|
|
410
|
+
header = RequestHeader.build(function=FunctionCode.DELAY_MEASURE, seq=seq)
|
|
411
|
+
return RequestFragment(header=header)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def build_cold_restart_request(seq: int = 0) -> RequestFragment:
|
|
415
|
+
"""Build a COLD_RESTART request.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
seq: Sequence number.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
RequestFragment for COLD_RESTART.
|
|
422
|
+
"""
|
|
423
|
+
header = RequestHeader.build(function=FunctionCode.COLD_RESTART, seq=seq)
|
|
424
|
+
return RequestFragment(header=header)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def build_warm_restart_request(seq: int = 0) -> RequestFragment:
|
|
428
|
+
"""Build a WARM_RESTART request.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
seq: Sequence number.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
RequestFragment for WARM_RESTART.
|
|
435
|
+
"""
|
|
436
|
+
header = RequestHeader.build(function=FunctionCode.WARM_RESTART, seq=seq)
|
|
437
|
+
return RequestFragment(header=header)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def build_enable_unsolicited_request(
|
|
441
|
+
class_1: bool = True,
|
|
442
|
+
class_2: bool = True,
|
|
443
|
+
class_3: bool = True,
|
|
444
|
+
seq: int = 0,
|
|
445
|
+
) -> RequestFragment:
|
|
446
|
+
"""Build an ENABLE_UNSOLICITED request.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
class_1: Enable Class 1 unsolicited.
|
|
450
|
+
class_2: Enable Class 2 unsolicited.
|
|
451
|
+
class_3: Enable Class 3 unsolicited.
|
|
452
|
+
seq: Sequence number.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
RequestFragment for ENABLE_UNSOLICITED.
|
|
456
|
+
"""
|
|
457
|
+
blocks = []
|
|
458
|
+
if class_1:
|
|
459
|
+
header = ObjectHeader.build(
|
|
460
|
+
group=CLASS_1_GV[0],
|
|
461
|
+
variation=CLASS_1_GV[1],
|
|
462
|
+
prefix=PrefixCode.NONE,
|
|
463
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
464
|
+
)
|
|
465
|
+
blocks.append(ObjectBlock(header=header))
|
|
466
|
+
if class_2:
|
|
467
|
+
header = ObjectHeader.build(
|
|
468
|
+
group=CLASS_2_GV[0],
|
|
469
|
+
variation=CLASS_2_GV[1],
|
|
470
|
+
prefix=PrefixCode.NONE,
|
|
471
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
472
|
+
)
|
|
473
|
+
blocks.append(ObjectBlock(header=header))
|
|
474
|
+
if class_3:
|
|
475
|
+
header = ObjectHeader.build(
|
|
476
|
+
group=CLASS_3_GV[0],
|
|
477
|
+
variation=CLASS_3_GV[1],
|
|
478
|
+
prefix=PrefixCode.NONE,
|
|
479
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
480
|
+
)
|
|
481
|
+
blocks.append(ObjectBlock(header=header))
|
|
482
|
+
|
|
483
|
+
req_header = RequestHeader.build(function=FunctionCode.ENABLE_UNSOLICITED, seq=seq)
|
|
484
|
+
return RequestFragment(header=req_header, objects=tuple(blocks))
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def build_disable_unsolicited_request(
|
|
488
|
+
class_1: bool = True,
|
|
489
|
+
class_2: bool = True,
|
|
490
|
+
class_3: bool = True,
|
|
491
|
+
seq: int = 0,
|
|
492
|
+
) -> RequestFragment:
|
|
493
|
+
"""Build a DISABLE_UNSOLICITED request.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
class_1: Disable Class 1 unsolicited.
|
|
497
|
+
class_2: Disable Class 2 unsolicited.
|
|
498
|
+
class_3: Disable Class 3 unsolicited.
|
|
499
|
+
seq: Sequence number.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
RequestFragment for DISABLE_UNSOLICITED.
|
|
503
|
+
"""
|
|
504
|
+
blocks = []
|
|
505
|
+
if class_1:
|
|
506
|
+
header = ObjectHeader.build(
|
|
507
|
+
group=CLASS_1_GV[0],
|
|
508
|
+
variation=CLASS_1_GV[1],
|
|
509
|
+
prefix=PrefixCode.NONE,
|
|
510
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
511
|
+
)
|
|
512
|
+
blocks.append(ObjectBlock(header=header))
|
|
513
|
+
if class_2:
|
|
514
|
+
header = ObjectHeader.build(
|
|
515
|
+
group=CLASS_2_GV[0],
|
|
516
|
+
variation=CLASS_2_GV[1],
|
|
517
|
+
prefix=PrefixCode.NONE,
|
|
518
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
519
|
+
)
|
|
520
|
+
blocks.append(ObjectBlock(header=header))
|
|
521
|
+
if class_3:
|
|
522
|
+
header = ObjectHeader.build(
|
|
523
|
+
group=CLASS_3_GV[0],
|
|
524
|
+
variation=CLASS_3_GV[1],
|
|
525
|
+
prefix=PrefixCode.NONE,
|
|
526
|
+
range_code=RangeCode.ALL_OBJECTS,
|
|
527
|
+
)
|
|
528
|
+
blocks.append(ObjectBlock(header=header))
|
|
529
|
+
|
|
530
|
+
req_header = RequestHeader.build(function=FunctionCode.DISABLE_UNSOLICITED, seq=seq)
|
|
531
|
+
return RequestFragment(header=req_header, objects=tuple(blocks))
|