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