CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/client/iec104/iec104.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
#
8
# this module sends IEC104 commands
9
#
10
11
include Msf::Exploit::Remote::Tcp
12
13
def initialize(info = {})
14
super(update_info(info,
15
'Name' => 'IEC104 Client Utility',
16
'Description' => %q(
17
This module allows sending 104 commands.
18
),
19
'Author' =>
20
[
21
'Michael John <mjohn.info[at]gmail.com>'
22
],
23
'License' => MSF_LICENSE,
24
'Actions' =>
25
[
26
['SEND_COMMAND', 'Description' => 'Send command to device']
27
],
28
'DefaultAction' => 'SEND_COMMAND'))
29
30
register_options(
31
[
32
Opt::RPORT(2404),
33
OptInt.new('ORIGINATOR_ADDRESS', [true, "Originator Address", 0]),
34
OptInt.new('ASDU_ADDRESS', [true, "Common Address of ASDU", 1]),
35
OptInt.new('COMMAND_ADDRESS', [true, "Command Address / IOA Address", 0]),
36
OptInt.new('COMMAND_TYPE', [true, "Command Type", 100]),
37
OptInt.new('COMMAND_VALUE', [true, "Command Value", 20])
38
]
39
)
40
end
41
42
# sends the frame data over tcp connection and returns received string
43
# using sock.get is causing quite some delay, but script needs to process responses from 104 server
44
def send_frame(data)
45
begin
46
sock.put(data)
47
sock.get(-1, sock.def_read_timeout)
48
rescue StandardError => e
49
print_error("Error:" + e.message)
50
end
51
end
52
53
# ACPI formats:
54
# TESTFR_CON = '\x83\x00\x00\x00'
55
# TESTFR_ACT = '\x43\x00\x00\x00'
56
# STOPDT_CON = '\x23\x00\x00\x00'
57
# STOPDT_ACT = '\x13\x00\x00\x00'
58
# STARTDT_CON = '\x0b\x00\x00\x00'
59
# STARTDT_ACT = '\x07\x00\x00\x00'
60
61
# creates and STARTDT Activation frame -> answer should be a STARTDT confirmation
62
def startcon
63
apci_data = "\x68"
64
apci_data << "\x04"
65
apci_data << "\x07"
66
apci_data << "\x00"
67
apci_data << "\x00"
68
apci_data << "\x00"
69
apci_data
70
end
71
72
# creates and STOPDT Activation frame -> answer should be a STOPDT confirmation
73
def stopcon
74
apci_data = "\x68"
75
apci_data << "\x04"
76
apci_data << "\x13"
77
apci_data << "\x00"
78
apci_data << "\x00"
79
apci_data << "\x00"
80
apci_data
81
end
82
83
# creates the acpi header of a 104 message
84
def make_apci(asdu_data)
85
apci_data = "\x68"
86
apci_data << [asdu_data.size + 4].pack("c") # size byte
87
apci_data << String([$tx].pack('v'))
88
apci_data << String([$rx].pack('v'))
89
$rx = $rx + 2
90
$tx = $tx + 2
91
apci_data << asdu_data
92
apci_data
93
end
94
95
# parses the header of a 104 message
96
def parse_headers(response_data)
97
if !response_data[0].eql?("\x04") && !response_data[1].eql?("\x01")
98
$rx = + (response_data[2].unpack('H*').first + response_data[1].unpack('H*').first).to_i(16)
99
print_good(" TX: " + response_data[4].unpack('H*').first + response_data[3].unpack('H*').first + \
100
" RX: " + response_data[2].unpack('H*').first + response_data[1].unpack('H*').first)
101
end
102
if response_data[7].eql?("\x07")
103
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activation Confirmation)")
104
elsif response_data[7].eql?("\x0a")
105
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Termination Activation)")
106
elsif response_data[7].eql?("\x14")
107
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Inrogen)")
108
elsif response_data[7].eql?("\x0b")
109
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Feedback by distant command / Retrem)")
110
elsif response_data[7].eql?("\x03")
111
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Spontaneous)")
112
elsif response_data[7].eql?("\x04")
113
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Initialized)")
114
elsif response_data[7].eql?("\x05")
115
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Interrogation)")
116
elsif response_data[7].eql?("\x06")
117
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activiation)")
118
119
# 104 error messages
120
elsif response_data[7].eql?("\x2c")
121
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Type Identification Unknown)")
122
elsif response_data[7].eql?("\x2d")
123
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Cause Unknown)")
124
elsif response_data[7].eql?("\x2e")
125
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (ASDU Address Unknown)")
126
elsif response_data[7].eql?("\x2f")
127
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (IOA Address Unknown)")
128
elsif response_data[7].eql?("\x6e")
129
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Unknown Comm Address ASDU)")
130
end
131
end
132
133
##############################################################################################################
134
# following functions parse different 104 ASDU messages and prints it content, not all messages of the standard are currently implemented
135
##############################################################################################################
136
def parse_m_sp_na_1(response_data)
137
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000 # this bit determines the object addressing structure
138
response_data = response_data[11..-1] # cut out acpi data
139
if sq_bit.eql?(0b10000000)
140
ioa = response_data[0..3] # extract ioa value
141
response_data = response_data[3..-1] # cut ioa from message
142
i = 0
143
while response_data.length >= 1
144
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
145
" SIQ: 0x" + response_data[0].unpack('H*').first)
146
response_data = response_data[1..-1]
147
i += 1
148
end
149
else
150
while response_data.length >= 4
151
ioa = response_data[0..3] # extract ioa
152
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
153
" SIQ: 0x" + response_data[3].unpack('H*').first)
154
response_data = response_data[4..-1]
155
end
156
end
157
end
158
159
def parse_m_me_nb_1(response_data)
160
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
161
response_data = response_data[11..-1] # cut out acpi data
162
if sq_bit.eql?(0b10000000)
163
ioa = response_data[0..3]
164
response_data = response_data[3..-1]
165
i = 0
166
while response_data.length >= 3
167
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
168
" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)
169
response_data = response_data[3..-1]
170
i += 1
171
end
172
else
173
while response_data.length >= 6
174
ioa = response_data[0..5]
175
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
176
" Value: 0x" + response_data[3..4].unpack('H*').first + " QDS: 0x" + + response_data[5].unpack('H*').first)
177
response_data = response_data[6..-1]
178
end
179
end
180
end
181
182
def parse_c_sc_na_1(response_data)
183
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
184
response_data = response_data[11..-1] # cut out acpi data
185
if sq_bit.eql?(0b10000000)
186
ioa = response_data[0..3]
187
response_data = response_data[3..-1]
188
i = 0
189
while response_data.length >= 1
190
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
191
" DIQ: 0x" + response_data[0].unpack('H*').first)
192
response_data = response_data[1..-1]
193
i += 1
194
end
195
else
196
while response_data.length >= 4
197
ioa = response_data[0..3]
198
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
199
" DIQ: 0x" + response_data[3].unpack('H*').first)
200
response_data = response_data[4..-1]
201
end
202
end
203
end
204
205
def parse_m_dp_na_1(response_data)
206
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
207
response_data = response_data[11..-1] # cut out acpi data
208
if sq_bit.eql?(0b10000000)
209
ioa = response_data[0..3]
210
response_data = response_data[3..-1]
211
i = 0
212
while response_data.length >= 1
213
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
214
" SIQ: 0x" + response_data[0].unpack('H*').first)
215
response_data = response_data[1..-1]
216
i += 1
217
end
218
else
219
while response_data.length >= 4
220
ioa = response_data[0..3]
221
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
222
" SIQ: 0x" + response_data[3].unpack('H*').first)
223
response_data = response_data[4..-1]
224
end
225
end
226
end
227
228
def parse_m_st_na_1(response_data)
229
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
230
response_data = response_data[11..-1] # cut out acpi data
231
if sq_bit.eql?(0b10000000)
232
ioa = response_data[0..3]
233
response_data = response_data[3..-1]
234
i = 0
235
while response_data.length >= 2
236
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
237
" VTI: 0x" + response_data[0].unpack('H*').first + " QDS: 0x" + response_data[1].unpack('H*').first)
238
response_data = response_data[2..-1]
239
i += 1
240
end
241
else
242
while response_data.length >= 5
243
ioa = response_data[0..4]
244
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
245
" VTI: 0x" + response_data[3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
246
response_data = response_data[5..-1]
247
end
248
end
249
end
250
251
def parse_m_dp_tb_1(response_data)
252
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
253
response_data = response_data[11..-1] # cut out acpi data
254
if sq_bit.eql?(0b10000000)
255
ioa = response_data[0..3]
256
response_data = response_data[3..-1]
257
i = 0
258
while response_data.length >= 8
259
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
260
" DIQ: 0x" + response_data[0].unpack('H*').first)
261
print_cp56time2a(response_data[1..7])
262
response_data = response_data[8..-1]
263
i += 1
264
end
265
else
266
while response_data.length >= 11
267
ioa = response_data[0..10]
268
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
269
" DIQ: 0x" + response_data[3].unpack('H*').first)
270
print_cp56time2a(response_data[4..10])
271
response_data = response_data[11..-1]
272
end
273
end
274
end
275
276
def parse_m_sp_tb_1(response_data)
277
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
278
response_data = response_data[11..-1] # cut out acpi data
279
if sq_bit.eql?(0b10000000)
280
ioa = response_data[0..3]
281
response_data = response_data[3..-1]
282
i = 0
283
while response_data.length >= 8
284
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
285
" SIQ: 0x" + response_data[0].unpack('H*').first)
286
print_cp56time2a(response_data[1..7])
287
response_data = response_data[8..-1]
288
i += 1
289
end
290
else
291
while response_data.length >= 11
292
ioa = response_data[0..10]
293
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
294
" SIQ: 0x" + response_data[3].unpack('H*').first)
295
print_cp56time2a(response_data[4..10])
296
response_data = response_data[11..-1]
297
end
298
end
299
end
300
301
def parse_c_dc_na_1(response_data)
302
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
303
response_data = response_data[11..-1] # cut out acpi data
304
if sq_bit.eql?(0b10000000)
305
ioa = response_data[0..3]
306
response_data = response_data[3..-1]
307
i = 0
308
while response_data.length >= 1
309
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
310
" DCO: 0x" + response_data[0].unpack('H*').first)
311
response_data = response_data[1..-1]
312
i += 1
313
end
314
else
315
while response_data.length >= 4
316
ioa = response_data[0..3]
317
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
318
" DCO: 0x" + response_data[3].unpack('H*').first)
319
response_data = response_data[4..-1]
320
end
321
end
322
end
323
324
def parse_m_me_na_1(response_data)
325
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
326
response_data = response_data[11..-1] # cut out acpi data
327
if sq_bit.eql?(0b10000000)
328
ioa = response_data[0..3]
329
response_data = response_data[3..-1]
330
i = 0
331
while response_data.length >= 3
332
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
333
" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)
334
response_data = response_data[3..-1]
335
i += 1
336
end
337
else
338
while response_data.length >= 6
339
ioa = response_data[0..3]
340
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
341
" Value: 0x" + ioa[3..4].unpack('H*').first + " QDS: 0x" + response_data[5].unpack('H*').first)
342
response_data = response_data[6..-1]
343
end
344
end
345
end
346
347
def parse_m_me_nc_1(response_data)
348
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
349
response_data = response_data[11..-1] # cut out acpi data
350
if sq_bit.eql?(0b10000000)
351
ioa = response_data[0..3]
352
response_data = response_data[3..-1]
353
i = 0
354
while response_data.length >= 5
355
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
356
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
357
response_data = response_data[5..-1]
358
i += 1
359
end
360
else
361
while response_data.length >= 8
362
ioa = response_data[0..3]
363
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
364
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
365
response_data = response_data[8..-1]
366
end
367
end
368
end
369
370
def parse_m_it_na_1(response_data)
371
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
372
response_data = response_data[11..-1] # cut out acpi data
373
if sq_bit.eql?(0b10000000)
374
response_data = response_data[11..-1]
375
ioa = response_data[0..3]
376
i = 0
377
while response_data.length >= 5
378
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
379
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
380
response_data = response_data[5..-1]
381
i += 1
382
end
383
else
384
while response_data.length >= 8
385
ioa = response_data[0..3]
386
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
387
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
388
response_data = response_data[8..-1]
389
end
390
end
391
end
392
393
def parse_m_bo_na_1(response_data)
394
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
395
response_data = response_data[11..-1] # cut out acpi data
396
if sq_bit.eql?(0b10000000)
397
ioa = response_data[0..3]
398
response_data = response_data[3..-1]
399
i = 0
400
while response_data.length >= 5
401
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
402
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
403
response_data = response_data[5..-1]
404
i += 1
405
end
406
else
407
while response_data.length >= 8
408
ioa = response_data[0..3]
409
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
410
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
411
response_data = response_data[8..-1]
412
end
413
end
414
end
415
416
# function to parses time format used in IEC 104
417
# function ported to ruby from: https://github.com/Ebolon/iec104
418
def print_cp56time2a(buf)
419
us = ((Integer(buf[1].unpack('c').first) & 0xFF) << 8) | (Integer(buf[0].unpack('c').first) & 0xFF)
420
second = Integer(us) / 1000
421
us = us % 1000
422
minute = Integer(buf[2].unpack('c').first) & 0x3F
423
hour = Integer(buf[3].unpack('c').first) & 0x1F
424
day = Integer(buf[4].unpack('c').first) & 0x1F
425
month = (Integer(buf[5].unpack('c').first) & 0x0F) - 1
426
year = (Integer(buf[6].unpack('c').first) & 0x7F) + 2000
427
print_good(" Timestamp: " + String(year) + "-" + String(format("%02d", month)) + "-" + String(format("%02d", day)) + " " + \
428
String(format("%02d", hour)) + ":" + String(format("%02d", minute)) + ":" + String(format("%02d", second)) + "." + String(us))
429
end
430
431
##############################################################################################################
432
# END of individual parse functions section
433
##############################################################################################################
434
435
# parses the 104 response string of a message
436
def parse_response(response)
437
response_elements = response.split("\x68")
438
response_elements.shift
439
response_elements.each do |response_element|
440
if response_element[5].eql?("\x64")
441
print_good(" Parsing response: Interrogation command (C_IC_NA_1)")
442
parse_headers(response_element)
443
elsif response_element[5].eql?("\x01")
444
print_good(" Parsing response: Single point information (M_SP_NA_1)")
445
parse_headers(response_element)
446
parse_m_sp_na_1(response_element)
447
elsif response_element[5].eql?("\x0b")
448
print_good(" Parsing response: Measured value, scaled value (M_ME_NB_1)")
449
parse_headers(response_element)
450
parse_m_me_nb_1(response_element)
451
elsif response_element[5].eql?("\x2d")
452
print_good(" Parsing response: Single command (C_SC_NA_1)")
453
parse_headers(response_element)
454
parse_c_sc_na_1(response_element)
455
elsif response_element[5].eql?("\x03")
456
print_good(" Parsing response: Double point information (M_DP_NA_1)")
457
parse_headers(response_element)
458
parse_m_dp_na_1(response_element)
459
elsif response_element[5].eql?("\x05")
460
print_good(" Parsing response: Step position information (M_ST_NA_1)")
461
parse_headers(response_element)
462
parse_m_st_na_1(response_element)
463
elsif response_element[5].eql?("\x1f")
464
print_good(" Parsing response: Double point information with time (M_DP_TB_1)")
465
parse_headers(response_element)
466
parse_m_dp_tb_1(response_element)
467
elsif response_element[5].eql?("\x2e")
468
print_good(" Parsing response: Double command (C_DC_NA_1)")
469
parse_headers(response_element)
470
parse_c_dc_na_1(response_element)
471
elsif response_element[5].eql?("\x1e")
472
print_good(" Parsing response: Single point information with time (M_SP_TB_1)")
473
parse_headers(response_element)
474
parse_m_sp_tb_1(response_element)
475
elsif response_element[5].eql?("\x09")
476
print_good(" Parsing response: Measured value, normalized value (M_ME_NA_1)")
477
parse_headers(response_element)
478
parse_m_me_na_1(response_element)
479
elsif response_element[5].eql?("\x0d")
480
print_good(" Parsing response: Measured value, short floating point value (M_ME_NC_1)")
481
parse_headers(response_element)
482
parse_m_me_nc_1(response_element)
483
elsif response_element[5].eql?("\x0f")
484
print_good(" Parsing response: Integrated total without time tag (M_IT_NA_1)")
485
parse_headers(response_element)
486
parse_m_it_na_1(response_element)
487
elsif response_element[5].eql?("\x07")
488
print_good(" Parsing response: Bitstring of 32 bits without time tag. (M_BO_NA_1)")
489
parse_headers(response_element)
490
parse_m_bo_na_1(response_element)
491
492
elsif response_element[5].eql?("\x46")
493
print_good("Received end of initialisation confirmation")
494
parse_headers(response_element)
495
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x01") && response_element[2].eql?("\x00")
496
print_good("Received S-Frame")
497
parse_headers(response_element)
498
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x0b") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
499
print_good("Received STARTDT_ACT")
500
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x23") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
501
print_good("Received STOPDT_ACT")
502
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x43") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
503
print_good("Received TESTFR_ACT")
504
else
505
print_status("Received unknown message")
506
parse_headers(response_element)
507
print_status(response_element.unpack('H*').first)
508
end
509
# Uncomment for print received data
510
# print_good("DEBUG: " + response_element.unpack('H*').first)
511
end
512
end
513
514
# sends 104 command with configure datastore options
515
# default values are for a general interrogation command
516
# for example a switching command would be:
517
# COMMAND_TYPE => 46 // double command without time
518
# COMMAND_ADDRESS => 100 // any IOA address that should be switched
519
# COMMAND_VALUE => 6 // switching off with short pulse
520
# use value 5 to switch on with short pulse
521
#
522
# Structure of 104 message:
523
# 1byte command type
524
# 1byte num ix -> 1 (one item send)
525
# 1byte cause of transmission -> 6 (activation)
526
# 1byte originator address
527
# 2byte common adsu address
528
# 3byte command address
529
# 1byte command value
530
def func_send_command
531
print_status("Sending 104 command")
532
533
asdu = [datastore['COMMAND_TYPE']].pack("c") # type of command
534
asdu << "\x01" # num ix -> only one item is send
535
asdu << "\x06" # cause of transmission = activation, 6
536
asdu << [datastore['ORIGINATOR_ADDRESS']].pack("c") # sets originator address of client
537
asdu << String([Integer(datastore['ASDU_ADDRESS'])].pack('v')) # sets the common address of ADSU
538
asdu << String([Integer(datastore['COMMAND_ADDRESS'])].pack('V'))[0..2] # sets the IOA address, todo: check command address fits in the 3byte address field
539
asdu << [datastore['COMMAND_VALUE']].pack("c") # sets the value of the command
540
541
# Uncomment for debugging
542
# print_status("Sending: " + make_apci(asdu).unpack('H*').first)
543
response = send_frame(make_apci(asdu))
544
545
if response.nil?
546
print_error("No answer")
547
else
548
parse_response(response)
549
end
550
print_status("operation ended")
551
end
552
553
def run
554
$rx = 0
555
$tx = 0
556
begin
557
connect
558
rescue StandardError => e
559
print_error("Error:" + e.message)
560
return
561
end
562
563
# send STARTDT_CON to activate connection
564
response = send_frame(startcon)
565
if response.nil?
566
print_error("Could not connect to 104 service")
567
return
568
else
569
parse_response(response)
570
end
571
572
# send the 104 command
573
case action.name
574
when "SEND_COMMAND"
575
func_send_command
576
else
577
print_error("Invalid ACTION")
578
end
579
580
# send STOPDT_CON to terminate connection
581
response = send_frame(stopcon)
582
if response.nil?
583
print_error("Terminating Connection")
584
return
585
else
586
print_status("Terminating Connection")
587
parse_response(response)
588
end
589
590
begin
591
disconnect
592
rescue StandardError => e
593
print_error("Error:" + e.message)
594
end
595
end
596
end
597
598