pwn 0.5.512 → 0.5.513
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile +1 -1
- data/README.md +3 -3
- data/bin/pwn_gqrx_scanner +13 -8
- data/lib/pwn/plugins/sock.rb +11 -1
- data/lib/pwn/sdr/frequency_allocation.rb +162 -162
- data/lib/pwn/sdr/gqrx.rb +407 -206
- data/lib/pwn/version.rb +1 -1
- metadata +3 -3
data/lib/pwn/sdr/gqrx.rb
CHANGED
|
@@ -9,7 +9,7 @@ module PWN
|
|
|
9
9
|
module GQRX
|
|
10
10
|
# Monkey patches for frequency handling
|
|
11
11
|
String.class_eval do
|
|
12
|
-
def
|
|
12
|
+
def cast_to_raw_hz
|
|
13
13
|
gsub('.', '').to_i
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -20,67 +20,16 @@ module PWN
|
|
|
20
20
|
# So 2_450_000_000 becomes 2.450.000.000
|
|
21
21
|
# So 960_000_000 becomes 960.000.000
|
|
22
22
|
# 1000 should be 1.000
|
|
23
|
-
def
|
|
23
|
+
def cast_to_pretty_hz
|
|
24
24
|
str_hz = to_s
|
|
25
25
|
# Nuke leading zeros
|
|
26
26
|
# E.g., 002450000000 -> 2450000000
|
|
27
|
-
str_hz = str_hz.sub(/^0+/, '')
|
|
27
|
+
str_hz = str_hz.sub(/^0+/, '') unless str_hz == '0'
|
|
28
28
|
# Insert dots every 3 digits from the right
|
|
29
29
|
str_hz.reverse.scan(/.{1,3}/).join('.').reverse
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# Supported Method Parameters::
|
|
34
|
-
# scan_resp = PWN::SDR::GQRX.log_signals(
|
|
35
|
-
# signals_arr: 'required - Array of detected signals',
|
|
36
|
-
# timestamp_start: 'required - Scan start timestamp',
|
|
37
|
-
# scan_log: 'required - Path to save detected signals log'
|
|
38
|
-
# )
|
|
39
|
-
private_class_method def self.log_signals(opts = {})
|
|
40
|
-
signals_arr = opts[:signals_arr]
|
|
41
|
-
timestamp_start = opts[:timestamp_start]
|
|
42
|
-
scan_log = opts[:scan_log]
|
|
43
|
-
|
|
44
|
-
signals = signals_arr.sort_by { |s| s[:freq].to_s.raw_hz }
|
|
45
|
-
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
46
|
-
duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
|
|
47
|
-
# Convert duration seconds to hours minutes seconds
|
|
48
|
-
hours = (duration_secs / 3600).to_i
|
|
49
|
-
minutes = ((duration_secs % 3600) / 60).to_i
|
|
50
|
-
seconds = (duration_secs % 60).to_i
|
|
51
|
-
duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
|
|
52
|
-
|
|
53
|
-
scan_resp = {
|
|
54
|
-
signals: signals,
|
|
55
|
-
timestamp_start: timestamp_start,
|
|
56
|
-
timestamp_end: timestamp_end,
|
|
57
|
-
duration: duration
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
File.write(
|
|
61
|
-
scan_log,
|
|
62
|
-
JSON.pretty_generate(scan_resp)
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
scan_resp
|
|
66
|
-
rescue StandardError => e
|
|
67
|
-
raise e
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Supported Method Parameters::
|
|
71
|
-
# gqrx_sock = PWN::SDR::GQRX.connect(
|
|
72
|
-
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
73
|
-
# port: 'optional - GQRX target port (defaults to 7356)'
|
|
74
|
-
# )
|
|
75
|
-
public_class_method def self.connect(opts = {})
|
|
76
|
-
target = opts[:target] ||= '127.0.0.1'
|
|
77
|
-
port = opts[:port] ||= 7356
|
|
78
|
-
|
|
79
|
-
PWN::Plugins::Sock.connect(target: target, port: port)
|
|
80
|
-
rescue StandardError => e
|
|
81
|
-
raise e
|
|
82
|
-
end
|
|
83
|
-
|
|
84
33
|
# Supported Method Parameters::
|
|
85
34
|
# gqrx_resp = PWN::SDR::GQRX.gqrx_cmd(
|
|
86
35
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
@@ -88,17 +37,22 @@ module PWN
|
|
|
88
37
|
# resp_ok: 'optional - Expected response from GQRX to indicate success'
|
|
89
38
|
# )
|
|
90
39
|
|
|
91
|
-
|
|
40
|
+
private_class_method def self.gqrx_cmd(opts = {})
|
|
92
41
|
gqrx_sock = opts[:gqrx_sock]
|
|
93
42
|
cmd = opts[:cmd]
|
|
94
43
|
resp_ok = opts[:resp_ok]
|
|
95
44
|
|
|
96
45
|
# Most Recent GQRX Command Set:
|
|
97
46
|
# https://raw.githubusercontent.com/gqrx-sdr/gqrx/master/resources/remote-control.txt
|
|
47
|
+
# Remote control protocol.
|
|
48
|
+
#
|
|
98
49
|
# Supported commands:
|
|
99
|
-
# f
|
|
100
|
-
#
|
|
101
|
-
#
|
|
50
|
+
# f
|
|
51
|
+
# Get frequency [Hz]
|
|
52
|
+
# F <frequency>
|
|
53
|
+
# Set frequency [Hz]
|
|
54
|
+
# m
|
|
55
|
+
# Get demodulator mode and passband
|
|
102
56
|
# M <mode> [passband]
|
|
103
57
|
# Set demodulator mode and passband [Hz]
|
|
104
58
|
# Passing a '?' as the first argument instead of 'mode' will return
|
|
@@ -121,18 +75,30 @@ module PWN
|
|
|
121
75
|
# Set the value of the gain setting with the name <gain_name> to <value>
|
|
122
76
|
# p RDS_PI
|
|
123
77
|
# Get the RDS PI code (in hexadecimal). Returns 0000 if not applicable.
|
|
78
|
+
# p RDS_PS_NAME
|
|
79
|
+
# Get the RDS Program Service (PS) name
|
|
80
|
+
# p RDS_RADIOTEXT
|
|
81
|
+
# Get the RDS RadioText message
|
|
124
82
|
# u RECORD
|
|
125
83
|
# Get status of audio recorder
|
|
126
84
|
# U RECORD <status>
|
|
127
85
|
# Set status of audio recorder to <status>
|
|
86
|
+
# u IQRECORD
|
|
87
|
+
# Get status of IQ recorder
|
|
88
|
+
# U IQRECORD <status>
|
|
89
|
+
# Set status of IQ recorder to <status>
|
|
128
90
|
# u DSP
|
|
129
91
|
# Get DSP (SDR receiver) status
|
|
130
92
|
# U DSP <status>
|
|
131
93
|
# Set DSP (SDR receiver) status to <status>
|
|
132
94
|
# u RDS
|
|
133
|
-
# Get RDS decoder
|
|
95
|
+
# Get RDS decoder status. Only functions in WFM mode.
|
|
134
96
|
# U RDS <status>
|
|
135
97
|
# Set RDS decoder to <status>. Only functions in WFM mode.
|
|
98
|
+
# u MUTE
|
|
99
|
+
# Get audio mute status
|
|
100
|
+
# U MUTE <status>
|
|
101
|
+
# Set audio mute to <status>
|
|
136
102
|
# q|Q
|
|
137
103
|
# Close connection
|
|
138
104
|
# AOS
|
|
@@ -159,6 +125,7 @@ module PWN
|
|
|
159
125
|
# _
|
|
160
126
|
# Get version
|
|
161
127
|
#
|
|
128
|
+
#
|
|
162
129
|
# Reply:
|
|
163
130
|
# RPRT 0
|
|
164
131
|
# Command successful
|
|
@@ -191,7 +158,7 @@ module PWN
|
|
|
191
158
|
raise "ERROR!!! Command: #{cmd} Expected Resp: #{resp_ok}, Got: #{response_str}" if resp_ok && response_str != resp_ok
|
|
192
159
|
|
|
193
160
|
# Reformat positive integer frequency responses (e.g., from 'f')
|
|
194
|
-
response_str = response_str.to_i.
|
|
161
|
+
response_str = response_str.to_i.cast_to_pretty_hz if response_str.match?(/^\d+$/) && response_str.to_i.positive?
|
|
195
162
|
|
|
196
163
|
response_str
|
|
197
164
|
rescue RuntimeError => e
|
|
@@ -206,11 +173,216 @@ module PWN
|
|
|
206
173
|
raise e
|
|
207
174
|
end
|
|
208
175
|
|
|
176
|
+
# Supported Method Parameters::
|
|
177
|
+
# strength_db = PWN::SDR::GQRX.measure_signal_strength(
|
|
178
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
179
|
+
# )
|
|
180
|
+
private_class_method def self.measure_signal_strength(opts = {})
|
|
181
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
182
|
+
|
|
183
|
+
strength_db = -99.9
|
|
184
|
+
prev_strength_db = strength_db
|
|
185
|
+
# While strength_db is rising, keep measuring
|
|
186
|
+
loop do
|
|
187
|
+
strength_db = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
188
|
+
break if strength_db <= prev_strength_db
|
|
189
|
+
|
|
190
|
+
prev_strength_db = strength_db
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
strength_db
|
|
194
|
+
rescue StandardError => e
|
|
195
|
+
raise e
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Supported Method Parameters::
|
|
199
|
+
# candidate_signals = PWN::SDR::GQRX.edge_detection(
|
|
200
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
201
|
+
# hz: 'required - Frequency to start edge detection from',
|
|
202
|
+
# step_hz: 'required - Frequency step in Hz for edge detection',
|
|
203
|
+
# strength_lock: 'required - Strength lock in dBFS to determine signal edges'
|
|
204
|
+
# )
|
|
205
|
+
private_class_method def self.edge_detection(opts = {})
|
|
206
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
207
|
+
hz = opts[:hz]
|
|
208
|
+
step_hz = opts[:step_hz]
|
|
209
|
+
strength_lock = opts[:strength_lock]
|
|
210
|
+
left_candidate_signals = []
|
|
211
|
+
right_candidate_signals = []
|
|
212
|
+
candidate_signals = []
|
|
213
|
+
|
|
214
|
+
# left_candidate_signals.clear
|
|
215
|
+
original_hz = hz
|
|
216
|
+
strength_db = 99.9
|
|
217
|
+
puts 'Finding Beginning Edge of Signal...'
|
|
218
|
+
while strength_db >= strength_lock
|
|
219
|
+
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
220
|
+
current_freq = 0
|
|
221
|
+
while current_freq.to_s.cast_to_raw_hz != hz.to_s.cast_to_raw_hz
|
|
222
|
+
current_freq = gqrx_cmd(
|
|
223
|
+
gqrx_sock: gqrx_sock,
|
|
224
|
+
cmd: 'f'
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
228
|
+
candidate = {
|
|
229
|
+
hz: hz.to_s.cast_to_raw_hz,
|
|
230
|
+
freq: hz.to_i.cast_to_pretty_hz,
|
|
231
|
+
strength: strength_db,
|
|
232
|
+
side: :left
|
|
233
|
+
}
|
|
234
|
+
left_candidate_signals.push(candidate)
|
|
235
|
+
hz -= step_hz
|
|
236
|
+
end
|
|
237
|
+
left_candidate_signals.uniq! { |s| s[:hz] }
|
|
238
|
+
left_candidate_signals.sort_by! { |s| s[:hz] }
|
|
239
|
+
|
|
240
|
+
# Now scan forwards to find the end of the signal
|
|
241
|
+
# The end of the signal is where the strength drops below strength_lock
|
|
242
|
+
# right_candidate_signals.clear
|
|
243
|
+
hz = original_hz
|
|
244
|
+
|
|
245
|
+
strength_db = 99.9
|
|
246
|
+
puts 'Finding Ending Edge of Signal...'
|
|
247
|
+
while strength_db >= strength_lock
|
|
248
|
+
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
249
|
+
current_freq = 0
|
|
250
|
+
while current_freq.to_s.cast_to_raw_hz != hz.to_s.cast_to_raw_hz
|
|
251
|
+
current_freq = gqrx_cmd(
|
|
252
|
+
gqrx_sock: gqrx_sock,
|
|
253
|
+
cmd: 'f'
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
257
|
+
candidate = {
|
|
258
|
+
hz: hz.to_s.cast_to_raw_hz,
|
|
259
|
+
freq: hz.to_i.cast_to_pretty_hz,
|
|
260
|
+
strength: strength_db,
|
|
261
|
+
side: :right
|
|
262
|
+
}
|
|
263
|
+
right_candidate_signals.push(candidate)
|
|
264
|
+
hz += step_hz
|
|
265
|
+
end
|
|
266
|
+
# Update candidate signals to remove duplicates and sort by hz
|
|
267
|
+
right_candidate_signals.uniq! { |s| s[:hz] }
|
|
268
|
+
right_candidate_signals.sort_by! { |s| s[:hz] }
|
|
269
|
+
|
|
270
|
+
candidate_signals = left_candidate_signals + right_candidate_signals
|
|
271
|
+
candidate_signals.uniq! { |s| s[:hz] }
|
|
272
|
+
candidate_signals.sort_by! { |s| s[:hz] }
|
|
273
|
+
rescue StandardError => e
|
|
274
|
+
raise e
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Supported Method Parameters::
|
|
278
|
+
# scan_resp = PWN::SDR::GQRX.log_signals(
|
|
279
|
+
# signals_arr: 'required - Array of detected signals',
|
|
280
|
+
# timestamp_start: 'required - Scan start timestamp',
|
|
281
|
+
# scan_log: 'required - Path to save detected signals log'
|
|
282
|
+
# )
|
|
283
|
+
private_class_method def self.log_signals(opts = {})
|
|
284
|
+
signals_arr = opts[:signals_arr]
|
|
285
|
+
timestamp_start = opts[:timestamp_start]
|
|
286
|
+
scan_log = opts[:scan_log]
|
|
287
|
+
|
|
288
|
+
signals = signals_arr.sort_by { |s| s[:freq].to_s.cast_to_raw_hz }
|
|
289
|
+
# Unique signals by frequency
|
|
290
|
+
signals.uniq! { |s| s[:hz] }
|
|
291
|
+
|
|
292
|
+
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
293
|
+
duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
|
|
294
|
+
# Convert duration seconds to hours minutes seconds
|
|
295
|
+
hours = (duration_secs / 3600).to_i
|
|
296
|
+
minutes = ((duration_secs % 3600) / 60).to_i
|
|
297
|
+
seconds = (duration_secs % 60).to_i
|
|
298
|
+
duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
|
|
299
|
+
|
|
300
|
+
scan_resp = {
|
|
301
|
+
signals: signals,
|
|
302
|
+
total: signals.length,
|
|
303
|
+
timestamp_start: timestamp_start,
|
|
304
|
+
timestamp_end: timestamp_end,
|
|
305
|
+
duration: duration
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
File.write(
|
|
309
|
+
scan_log,
|
|
310
|
+
JSON.pretty_generate(scan_resp)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
scan_resp
|
|
314
|
+
rescue StandardError => e
|
|
315
|
+
raise e
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Supported Method Parameters::
|
|
319
|
+
# rds_resp = PWN::SDR::GQRX.decode_rds(
|
|
320
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
321
|
+
# )
|
|
322
|
+
|
|
323
|
+
private_class_method def self.decode_rds(opts = {})
|
|
324
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
325
|
+
|
|
326
|
+
# We toggle RDS off and on to reset the decoder
|
|
327
|
+
rds_resp = gqrx_cmd(
|
|
328
|
+
gqrx_sock: gqrx_sock,
|
|
329
|
+
cmd: 'U RDS 0',
|
|
330
|
+
resp_ok: 'RPRT 0'
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
rds_resp = gqrx_cmd(
|
|
334
|
+
gqrx_sock: gqrx_sock,
|
|
335
|
+
cmd: 'U RDS 1',
|
|
336
|
+
resp_ok: 'RPRT 0'
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
rds_resp = {}
|
|
340
|
+
attempts = 0
|
|
341
|
+
max_attempts = 90
|
|
342
|
+
skip_rds = "\n"
|
|
343
|
+
print 'INFO: Decoding FM radio RDS data (Press ENTER to skip)...'
|
|
344
|
+
max_attempts.times do
|
|
345
|
+
attempts += 1
|
|
346
|
+
rds_resp[:rds_pi] = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'p RDS_PI')
|
|
347
|
+
rds_resp[:rds_ps_name] = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'p RDS_PS_NAME')
|
|
348
|
+
rds_resp[:rds_radiotext] = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'p RDS_RADIOTEXT')
|
|
349
|
+
|
|
350
|
+
# Break if ENTER key pressed
|
|
351
|
+
# This is useful if no RDS data is available
|
|
352
|
+
# on the current frequency (e.g. false+)
|
|
353
|
+
break if $stdin.ready? && $stdin.read_nonblock(1) == skip_rds
|
|
354
|
+
|
|
355
|
+
break if rds_resp[:rds_pi] != '0000' && !rds_resp[:rds_ps_name].empty? && !rds_resp[:rds_radiotext].empty?
|
|
356
|
+
|
|
357
|
+
print '.'
|
|
358
|
+
sleep 0.1
|
|
359
|
+
end
|
|
360
|
+
puts 'complete.'
|
|
361
|
+
rds_resp
|
|
362
|
+
rescue StandardError => e
|
|
363
|
+
raise e
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Supported Method Parameters::
|
|
367
|
+
# gqrx_sock = PWN::SDR::GQRX.connect(
|
|
368
|
+
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
369
|
+
# port: 'optional - GQRX target port (defaults to 7356)'
|
|
370
|
+
# )
|
|
371
|
+
public_class_method def self.connect(opts = {})
|
|
372
|
+
target = opts[:target] ||= '127.0.0.1'
|
|
373
|
+
port = opts[:port] ||= 7356
|
|
374
|
+
|
|
375
|
+
PWN::Plugins::Sock.connect(target: target, port: port)
|
|
376
|
+
rescue StandardError => e
|
|
377
|
+
raise e
|
|
378
|
+
end
|
|
379
|
+
|
|
209
380
|
# Supported Method Parameters::
|
|
210
381
|
# freq_obj = PWN::SDR::GQRX.init_freq(
|
|
211
382
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
212
383
|
# freq: 'required - Frequency to set',
|
|
213
384
|
# demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
385
|
+
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
214
386
|
# bandwidth: 'optional - Bandwidth (defaults to 200_000)',
|
|
215
387
|
# squelch: 'optional - Squelch level to set (Defaults to current value)',
|
|
216
388
|
# decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
@@ -239,6 +411,8 @@ module PWN
|
|
|
239
411
|
demodulator_mode = opts[:demodulator_mode] ||= :WFM
|
|
240
412
|
raise "ERROR: Invalid demodulator_mode '#{demodulator_mode}'. Valid modes: #{valid_demodulator_modes.join(', ')}" unless valid_demodulator_modes.include?(demodulator_mode.to_sym)
|
|
241
413
|
|
|
414
|
+
rds = opts[:rds] ||= false
|
|
415
|
+
|
|
242
416
|
bandwidth = opts[:bandwidth] ||= 200_000
|
|
243
417
|
squelch = opts[:squelch]
|
|
244
418
|
decoder = opts[:decoder]
|
|
@@ -248,24 +422,16 @@ module PWN
|
|
|
248
422
|
|
|
249
423
|
raise "ERROR: record_dir '#{record_dir}' does not exist. Please create it or provide a valid path." if decoder && !Dir.exist?(record_dir)
|
|
250
424
|
|
|
251
|
-
|
|
425
|
+
unless keep_alive
|
|
426
|
+
squelch = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'l SQL').to_f if squelch.nil?
|
|
252
427
|
change_squelch_resp = gqrx_cmd(
|
|
253
428
|
gqrx_sock: gqrx_sock,
|
|
254
429
|
cmd: "L SQL #{squelch}",
|
|
255
430
|
resp_ok: 'RPRT 0'
|
|
256
431
|
)
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
change_freq_resp = gqrx_cmd(
|
|
260
|
-
gqrx_sock: gqrx_sock,
|
|
261
|
-
cmd: "F #{freq.to_s.raw_hz}",
|
|
262
|
-
resp_ok: 'RPRT 0'
|
|
263
|
-
)
|
|
264
432
|
|
|
265
|
-
# Set demod mode and bandwidth (always, using defaults if not provided)
|
|
266
|
-
unless keep_alive
|
|
267
433
|
mode_str = demodulator_mode.to_s.upcase
|
|
268
|
-
passband_hz = bandwidth.to_s.
|
|
434
|
+
passband_hz = bandwidth.to_s.cast_to_raw_hz
|
|
269
435
|
gqrx_cmd(
|
|
270
436
|
gqrx_sock: gqrx_sock,
|
|
271
437
|
cmd: "M #{mode_str} #{passband_hz}",
|
|
@@ -273,38 +439,41 @@ module PWN
|
|
|
273
439
|
)
|
|
274
440
|
end
|
|
275
441
|
|
|
276
|
-
|
|
277
|
-
demod_n_passband = gqrx_cmd(
|
|
442
|
+
change_freq_resp = gqrx_cmd(
|
|
278
443
|
gqrx_sock: gqrx_sock,
|
|
279
|
-
cmd:
|
|
444
|
+
cmd: "F #{freq.to_s.cast_to_raw_hz}",
|
|
445
|
+
resp_ok: 'RPRT 0'
|
|
280
446
|
)
|
|
281
447
|
|
|
282
|
-
|
|
283
|
-
current_freq
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
448
|
+
current_freq = 0
|
|
449
|
+
while current_freq.to_s.cast_to_raw_hz != freq.to_s.cast_to_raw_hz
|
|
450
|
+
current_freq = gqrx_cmd(
|
|
451
|
+
gqrx_sock: gqrx_sock,
|
|
452
|
+
cmd: 'f'
|
|
453
|
+
)
|
|
454
|
+
end
|
|
287
455
|
|
|
288
456
|
freq_obj = {
|
|
457
|
+
bandwidth: bandwidth,
|
|
289
458
|
demodulator_mode: demodulator_mode,
|
|
290
|
-
|
|
291
|
-
freq:
|
|
292
|
-
bandwidth: bandwidth
|
|
459
|
+
rds: rds,
|
|
460
|
+
freq: freq
|
|
293
461
|
}
|
|
294
462
|
|
|
295
463
|
unless suppress_details
|
|
464
|
+
demod_n_passband = gqrx_cmd(
|
|
465
|
+
gqrx_sock: gqrx_sock,
|
|
466
|
+
cmd: 'm'
|
|
467
|
+
)
|
|
468
|
+
|
|
296
469
|
audio_gain_db = gqrx_cmd(
|
|
297
470
|
gqrx_sock: gqrx_sock,
|
|
298
471
|
cmd: 'l AF'
|
|
299
472
|
).to_f
|
|
300
473
|
|
|
301
|
-
|
|
302
|
-
gqrx_sock: gqrx_sock,
|
|
303
|
-
cmd: 'l STRENGTH'
|
|
304
|
-
).to_f
|
|
305
|
-
strength_db = strength_db_float.round(1)
|
|
474
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
306
475
|
|
|
307
|
-
|
|
476
|
+
squelch = gqrx_cmd(
|
|
308
477
|
gqrx_sock: gqrx_sock,
|
|
309
478
|
cmd: 'l SQL'
|
|
310
479
|
).to_f
|
|
@@ -324,12 +493,17 @@ module PWN
|
|
|
324
493
|
cmd: 'l BB_GAIN'
|
|
325
494
|
)
|
|
326
495
|
|
|
496
|
+
rds_resp = nil
|
|
497
|
+
rds_resp = decode_rds(gqrx_sock: gqrx_sock) if rds
|
|
498
|
+
|
|
327
499
|
freq_obj[:audio_gain_db] = audio_gain_db
|
|
328
|
-
freq_obj[:
|
|
329
|
-
freq_obj[:rf_gain] = rf_gain
|
|
330
|
-
freq_obj[:if_gain] = if_gain
|
|
500
|
+
freq_obj[:demod_mode_n_passband] = demod_n_passband
|
|
331
501
|
freq_obj[:bb_gain] = bb_gain
|
|
502
|
+
freq_obj[:if_gain] = if_gain
|
|
503
|
+
freq_obj[:rf_gain] = rf_gain
|
|
504
|
+
freq_obj[:squelch] = squelch
|
|
332
505
|
freq_obj[:strength_db] = strength_db
|
|
506
|
+
freq_obj[:rds] = rds_resp
|
|
333
507
|
end
|
|
334
508
|
|
|
335
509
|
# Start recording and decoding if decoder provided
|
|
@@ -365,6 +539,7 @@ module PWN
|
|
|
365
539
|
decoder_thread = decoder_module.start(freq_obj: freq_obj)
|
|
366
540
|
freq_obj.delete(:gqrx_sock)
|
|
367
541
|
|
|
542
|
+
freq_obj[:freq] = current_freq
|
|
368
543
|
freq_obj[:decoder] = decoder
|
|
369
544
|
freq_obj[:decoder_module] = decoder_module
|
|
370
545
|
freq_obj[:decoder_thread] = decoder_thread
|
|
@@ -393,10 +568,9 @@ module PWN
|
|
|
393
568
|
# start_freq: 'required - Start frequency of scan range',
|
|
394
569
|
# target_freq: 'required - Target frequency of scan range',
|
|
395
570
|
# demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
571
|
+
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
396
572
|
# bandwidth: 'optional - Bandwidth in Hz (Defaults to 200_000)',
|
|
397
|
-
# overlap_protection: 'optional - Boolean to enable/disable bandwidth overlap protection (defaults to false)',
|
|
398
573
|
# precision: 'optional - Frequency step precision (number of digits; defaults to 1)',
|
|
399
|
-
# lock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',
|
|
400
574
|
# strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
|
|
401
575
|
# squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
|
|
402
576
|
# audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
|
|
@@ -414,19 +588,19 @@ module PWN
|
|
|
414
588
|
gqrx_sock = opts[:gqrx_sock]
|
|
415
589
|
|
|
416
590
|
start_freq = opts[:start_freq]
|
|
417
|
-
hz_start = start_freq.to_s.
|
|
591
|
+
hz_start = start_freq.to_s.cast_to_raw_hz
|
|
418
592
|
|
|
419
593
|
target_freq = opts[:target_freq]
|
|
420
|
-
hz_target = target_freq.to_s.
|
|
594
|
+
hz_target = target_freq.to_s.cast_to_raw_hz
|
|
421
595
|
|
|
422
596
|
demodulator_mode = opts[:demodulator_mode]
|
|
597
|
+
rds = opts[:rds] ||= false
|
|
598
|
+
|
|
423
599
|
bandwidth = opts[:bandwidth] ||= 200_000
|
|
424
|
-
overlap_protection = opts[:overlap_protection] || false
|
|
425
600
|
precision = opts[:precision] ||= 1
|
|
426
|
-
lock_freq_duration = opts[:lock_freq_duration] ||= 0.04
|
|
427
601
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
428
602
|
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
429
|
-
scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{hz_start.
|
|
603
|
+
scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{hz_start.to_i.cast_to_pretty_hz}-#{hz_target.to_i.cast_to_pretty_hz}_#{log_timestamp}.json"
|
|
430
604
|
location = opts[:location] ||= 'United States'
|
|
431
605
|
|
|
432
606
|
step_hz = 10**(precision - 1)
|
|
@@ -439,9 +613,20 @@ module PWN
|
|
|
439
613
|
resp_ok: 'RPRT 0'
|
|
440
614
|
)
|
|
441
615
|
|
|
616
|
+
# We always disable RDS decoding at during the scan
|
|
617
|
+
# to prevent unnecessary processing overhead.
|
|
618
|
+
# We return the rds boolean in the scan_resp object
|
|
619
|
+
# so it will be picked up and used appropriately
|
|
620
|
+
# when calling analyze_scan or analyze_log methods.
|
|
621
|
+
rds_resp = gqrx_cmd(
|
|
622
|
+
gqrx_sock: gqrx_sock,
|
|
623
|
+
cmd: 'U RDS 0',
|
|
624
|
+
resp_ok: 'RPRT 0'
|
|
625
|
+
)
|
|
626
|
+
|
|
442
627
|
# Set demodulator mode & passband once for the scan
|
|
443
628
|
mode_str = demodulator_mode.to_s.upcase
|
|
444
|
-
passband_hz = bandwidth.to_s.
|
|
629
|
+
passband_hz = bandwidth.to_s.cast_to_raw_hz
|
|
445
630
|
gqrx_cmd(
|
|
446
631
|
gqrx_sock: gqrx_sock,
|
|
447
632
|
cmd: "M #{mode_str} #{passband_hz}",
|
|
@@ -450,7 +635,7 @@ module PWN
|
|
|
450
635
|
|
|
451
636
|
audio_gain_db = opts[:audio_gain_db] ||= 6.0
|
|
452
637
|
audio_gain_db = audio_gain_db.to_f
|
|
453
|
-
audio_gain_db_resp =
|
|
638
|
+
audio_gain_db_resp = gqrx_cmd(
|
|
454
639
|
gqrx_sock: gqrx_sock,
|
|
455
640
|
cmd: "L AF #{audio_gain_db}",
|
|
456
641
|
resp_ok: 'RPRT 0'
|
|
@@ -458,7 +643,7 @@ module PWN
|
|
|
458
643
|
|
|
459
644
|
rf_gain = opts[:rf_gain] ||= 0.0
|
|
460
645
|
rf_gain = rf_gain.to_f
|
|
461
|
-
rf_gain_resp =
|
|
646
|
+
rf_gain_resp = gqrx_cmd(
|
|
462
647
|
gqrx_sock: gqrx_sock,
|
|
463
648
|
cmd: "L RF_GAIN #{rf_gain}",
|
|
464
649
|
resp_ok: 'RPRT 0'
|
|
@@ -466,7 +651,7 @@ module PWN
|
|
|
466
651
|
|
|
467
652
|
intermediate_gain = opts[:intermediate_gain] ||= 32.0
|
|
468
653
|
intermediate_gain = intermediate_gain.to_f
|
|
469
|
-
intermediate_resp =
|
|
654
|
+
intermediate_resp = gqrx_cmd(
|
|
470
655
|
gqrx_sock: gqrx_sock,
|
|
471
656
|
cmd: "L IF_GAIN #{intermediate_gain}",
|
|
472
657
|
resp_ok: 'RPRT 0'
|
|
@@ -474,35 +659,33 @@ module PWN
|
|
|
474
659
|
|
|
475
660
|
baseband_gain = opts[:baseband_gain] ||= 10.0
|
|
476
661
|
baseband_gain = baseband_gain.to_f
|
|
477
|
-
baseband_resp =
|
|
662
|
+
baseband_resp = gqrx_cmd(
|
|
478
663
|
gqrx_sock: gqrx_sock,
|
|
479
664
|
cmd: "L BB_GAIN #{baseband_gain}",
|
|
480
665
|
resp_ok: 'RPRT 0'
|
|
481
666
|
)
|
|
482
667
|
|
|
483
|
-
prev_freq_obj =
|
|
668
|
+
prev_freq_obj = init_freq(
|
|
669
|
+
gqrx_sock: gqrx_sock,
|
|
670
|
+
freq: hz_start,
|
|
671
|
+
demodulator_mode: demodulator_mode,
|
|
672
|
+
rds: rds,
|
|
673
|
+
bandwidth: bandwidth,
|
|
674
|
+
squelch: squelch,
|
|
675
|
+
suppress_details: true,
|
|
676
|
+
keep_alive: true
|
|
677
|
+
)
|
|
484
678
|
|
|
485
|
-
in_signal = false
|
|
486
679
|
candidate_signals = []
|
|
487
|
-
strength_history = []
|
|
488
680
|
|
|
489
681
|
# Adaptive peak finder – trims weakest ends after each pass
|
|
490
682
|
# Converges quickly to the true center of the bell curve
|
|
491
683
|
find_best_peak = lambda do |opts = {}|
|
|
492
|
-
beg_of_signal_hz = opts[:beg_of_signal_hz].to_s.
|
|
493
|
-
|
|
494
|
-
end_of_signal_hz = top_of_signal_hz + step_hz
|
|
495
|
-
|
|
496
|
-
# current_hz = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f').to_s.raw_hz
|
|
497
|
-
# puts "Current Frequency: #{current_hz.pretty_hz}"
|
|
498
|
-
puts "Signal Began: #{beg_of_signal_hz.pretty_hz}"
|
|
499
|
-
puts "Signal Appeared to Peak at: #{top_of_signal_hz.pretty_hz}"
|
|
500
|
-
puts "Calculated Signal End: #{end_of_signal_hz.pretty_hz}"
|
|
501
|
-
# steps_between_beg_n_end = ((end_of_signal_hz - beg_of_signal_hz) / step_hz).abs
|
|
502
|
-
# puts steps_between_beg_n_end.inspect
|
|
684
|
+
beg_of_signal_hz = opts[:beg_of_signal_hz].to_s.cast_to_raw_hz
|
|
685
|
+
end_of_signal_hz = opts[:end_of_signal_hz].to_s.cast_to_raw_hz
|
|
503
686
|
|
|
504
687
|
samples = []
|
|
505
|
-
prev_best_sample =
|
|
688
|
+
prev_best_sample = {}
|
|
506
689
|
consecutive_best = 0
|
|
507
690
|
direction_up = true
|
|
508
691
|
|
|
@@ -525,19 +708,19 @@ module PWN
|
|
|
525
708
|
print '>' if direction_up
|
|
526
709
|
print '<' unless direction_up
|
|
527
710
|
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
528
|
-
|
|
529
|
-
strength_db_float = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
530
|
-
strength_db = strength_db_float.round(1)
|
|
711
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
531
712
|
samples.push({ hz: hz, strength_db: strength_db })
|
|
532
713
|
|
|
533
|
-
# current_hz = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f').to_s.
|
|
534
|
-
# puts "Sampled Frequency: #{current_hz.
|
|
714
|
+
# current_hz = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f').to_s.cast_to_raw_hz
|
|
715
|
+
# puts "Sampled Frequency: #{current_hz.to_i.cast_to_pretty_hz} => Strength: #{strength_db} dBFS"
|
|
535
716
|
end
|
|
536
717
|
|
|
537
718
|
# Compute fresh averaged_samples from all cumulative samples
|
|
538
719
|
averaged_samples = []
|
|
539
720
|
samples.group_by { |s| s[:hz] }.each do |hz, grouped_samples|
|
|
540
|
-
avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size)
|
|
721
|
+
avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size)
|
|
722
|
+
# avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size).round(2)
|
|
723
|
+
# avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size).round(1)
|
|
541
724
|
averaged_samples.push({ hz: hz, strength_db: avg_strength })
|
|
542
725
|
end
|
|
543
726
|
|
|
@@ -567,7 +750,7 @@ module PWN
|
|
|
567
750
|
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
568
751
|
|
|
569
752
|
# Check for improvement
|
|
570
|
-
if best_sample == prev_best_sample
|
|
753
|
+
if best_sample[:hz] == prev_best_sample[:hz]
|
|
571
754
|
consecutive_best += 1
|
|
572
755
|
else
|
|
573
756
|
consecutive_best = 0
|
|
@@ -576,7 +759,7 @@ module PWN
|
|
|
576
759
|
# Dup to avoid reference issues
|
|
577
760
|
prev_best_sample = best_sample.dup
|
|
578
761
|
|
|
579
|
-
puts "Pass #{pass_count}: Best #{best_sample[:hz].
|
|
762
|
+
puts "Pass #{pass_count}: Best #{best_sample[:hz].to_i.cast_to_pretty_hz} => #{best_sample[:strength_db]} dBFS, consecutive best count: #{consecutive_best}"
|
|
580
763
|
|
|
581
764
|
# Break if no improvement in 3 consecutive passes or theres only one sample left
|
|
582
765
|
break if consecutive_best.positive? || averaged_samples.size == 1
|
|
@@ -586,82 +769,101 @@ module PWN
|
|
|
586
769
|
end
|
|
587
770
|
|
|
588
771
|
# Begin scanning range
|
|
589
|
-
puts "
|
|
772
|
+
puts "\n"
|
|
773
|
+
puts '-' * 86
|
|
774
|
+
puts "INFO: Scanning from #{hz_start.to_i.cast_to_pretty_hz} to #{hz_target.to_i.cast_to_pretty_hz} in steps of #{step.abs.to_i.cast_to_pretty_hz} Hz."
|
|
775
|
+
puts "If scans are slow and/or you're experiencing false positives/negatives,"
|
|
776
|
+
puts 'consider adjusting the following:'
|
|
777
|
+
puts "1. The SDR's sample rate in GQRX"
|
|
778
|
+
puts "\s\s- Click on `Configure I/O devices`."
|
|
779
|
+
puts "\s\s- A lower `Input rate` value seems counter-intuitive but works well (e.g. ADALM PLUTO ~ 1000000)."
|
|
780
|
+
puts '2. Adjust the :strength_lock parameter.'
|
|
781
|
+
puts '3. Adjust the :precision parameter.'
|
|
782
|
+
puts '4. Disable AI introspection in PWN::Env'
|
|
783
|
+
puts 'Happy scanning!'
|
|
784
|
+
puts '-' * 86
|
|
785
|
+
puts "\n\n\n"
|
|
590
786
|
|
|
591
787
|
signals_arr = []
|
|
592
|
-
|
|
593
|
-
|
|
788
|
+
hz = hz_start
|
|
789
|
+
while hz <= hz_target
|
|
594
790
|
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
791
|
+
current_freq = 0
|
|
792
|
+
while current_freq.to_s.cast_to_raw_hz != hz.to_s.cast_to_raw_hz
|
|
793
|
+
current_freq = gqrx_cmd(
|
|
794
|
+
gqrx_sock: gqrx_sock,
|
|
795
|
+
cmd: 'f'
|
|
796
|
+
)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
800
|
+
|
|
801
|
+
if strength_db >= strength_lock
|
|
802
|
+
puts '-' * 86
|
|
803
|
+
# Find left and right edges of the signal
|
|
804
|
+
candidate_signals = edge_detection(
|
|
805
|
+
gqrx_sock: gqrx_sock,
|
|
806
|
+
hz: hz,
|
|
807
|
+
step_hz: step_hz,
|
|
808
|
+
strength_lock: strength_lock
|
|
809
|
+
)
|
|
810
|
+
elsif candidate_signals.length.positive?
|
|
811
|
+
beg_of_signal_hz = candidate_signals.first[:hz]
|
|
812
|
+
top_of_signal_hz_idx = (candidate_signals.length - 1) / 2
|
|
813
|
+
top_of_signal_hz = candidate_signals[top_of_signal_hz_idx][:hz]
|
|
814
|
+
end_of_signal_hz = candidate_signals.last[:hz]
|
|
815
|
+
puts 'Candidate Signal(s) Detected:'
|
|
816
|
+
puts JSON.pretty_generate(candidate_signals)
|
|
817
|
+
|
|
818
|
+
prev_freq = prev_freq_obj[:freq].to_s.cast_to_raw_hz
|
|
819
|
+
distance_from_prev_detected_freq_hz = (beg_of_signal_hz - prev_freq).abs
|
|
820
|
+
half_bandwidth = (bandwidth / 2).to_i
|
|
821
|
+
|
|
822
|
+
puts "Key Frequencies: Begin: #{beg_of_signal_hz.to_i.cast_to_pretty_hz} Hz | Estimated Top: #{top_of_signal_hz.to_i.cast_to_pretty_hz} Hz | End: #{end_of_signal_hz.to_i.cast_to_pretty_hz} Hz"
|
|
823
|
+
|
|
824
|
+
puts 'Finding Best Peak...'
|
|
825
|
+
best_peak = find_best_peak.call(
|
|
826
|
+
beg_of_signal_hz: beg_of_signal_hz,
|
|
827
|
+
end_of_signal_hz: end_of_signal_hz
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
if best_peak[:hz] && best_peak[:strength_db] > strength_lock
|
|
831
|
+
puts "\n**** Detected Signal ****"
|
|
832
|
+
best_freq = best_peak[:hz].to_i.cast_to_pretty_hz
|
|
833
|
+
best_strength_db = best_peak[:strength_db]
|
|
834
|
+
prev_freq_obj = init_freq(
|
|
835
|
+
gqrx_sock: gqrx_sock,
|
|
836
|
+
freq: best_freq,
|
|
837
|
+
rds: rds,
|
|
838
|
+
suppress_details: true,
|
|
839
|
+
keep_alive: true
|
|
840
|
+
)
|
|
841
|
+
prev_freq_obj[:strength_lock] = strength_lock
|
|
842
|
+
prev_freq_obj[:strength_db] = best_strength_db
|
|
843
|
+
|
|
844
|
+
system_role_content = "Analyze signal data captured by a software-defined-radio using GQRX at the following location: #{location}. Respond with just FCC information about the transmission if available. If the frequency is unlicensed or not found in FCC records, state that clearly. Be clear and concise in your analysis."
|
|
845
|
+
ai_analysis = PWN::AI::Introspection.reflect_on(
|
|
846
|
+
request: prev_freq_obj.to_json,
|
|
847
|
+
system_role_content: system_role_content,
|
|
848
|
+
suppress_pii_warning: true
|
|
628
849
|
)
|
|
629
850
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
system_role_content = "Analyze signal data captured by a software-defined-radio using GQRX at the following location: #{location}. Respond with just FCC information about the transmission if available. If the frequency is unlicensed or not found in FCC records, state that clearly. Be clear and concise in your analysis."
|
|
644
|
-
ai_analysis = PWN::AI::Introspection.reflect_on(
|
|
645
|
-
request: prev_freq_obj.to_json,
|
|
646
|
-
system_role_content: system_role_content,
|
|
647
|
-
suppress_pii_warning: true
|
|
648
|
-
)
|
|
649
|
-
prev_freq_obj[:ai_analysis] = ai_analysis unless ai_analysis.nil?
|
|
650
|
-
puts "\n**** Detected Signal ****"
|
|
651
|
-
puts JSON.pretty_generate(prev_freq_obj)
|
|
652
|
-
signals_arr.push(prev_freq_obj)
|
|
653
|
-
log_signals(
|
|
654
|
-
signals_arr: signals_arr,
|
|
655
|
-
timestamp_start: timestamp_start,
|
|
656
|
-
scan_log: scan_log
|
|
657
|
-
)
|
|
658
|
-
end
|
|
659
|
-
candidate_signals.clear
|
|
660
|
-
sleep lock_freq_duration
|
|
851
|
+
prev_freq_obj[:ai_analysis] = ai_analysis unless ai_analysis.nil?
|
|
852
|
+
puts JSON.pretty_generate(prev_freq_obj)
|
|
853
|
+
puts '-' * 86
|
|
854
|
+
puts "\n\n\n"
|
|
855
|
+
signals_arr.push(prev_freq_obj)
|
|
856
|
+
log_signals(
|
|
857
|
+
signals_arr: signals_arr,
|
|
858
|
+
timestamp_start: timestamp_start,
|
|
859
|
+
scan_log: scan_log
|
|
860
|
+
)
|
|
861
|
+
hz = end_of_signal_hz
|
|
862
|
+
# gets
|
|
661
863
|
end
|
|
662
|
-
|
|
663
|
-
strength_history = []
|
|
864
|
+
candidate_signals.clear
|
|
664
865
|
end
|
|
866
|
+
hz += step_hz
|
|
665
867
|
end
|
|
666
868
|
|
|
667
869
|
log_signals(
|
|
@@ -669,6 +871,8 @@ module PWN
|
|
|
669
871
|
timestamp_start: timestamp_start,
|
|
670
872
|
scan_log: scan_log
|
|
671
873
|
)
|
|
874
|
+
rescue Interrupt
|
|
875
|
+
puts "\nCTRL+C detected - goodbye."
|
|
672
876
|
rescue StandardError => e
|
|
673
877
|
raise e
|
|
674
878
|
ensure
|
|
@@ -693,9 +897,11 @@ module PWN
|
|
|
693
897
|
)
|
|
694
898
|
|
|
695
899
|
scan_resp[:signals].each do |signal|
|
|
696
|
-
|
|
900
|
+
signal[:gqrx_sock] = gqrx_sock
|
|
901
|
+
# This is required to keep connection alive during analysis
|
|
902
|
+
signal[:keep_alive] = true
|
|
903
|
+
freq_obj = init_freq(signal)
|
|
697
904
|
freq_obj = signal.merge(freq_obj)
|
|
698
|
-
freq_obj = init_freq(freq_obj)
|
|
699
905
|
# Redact gqrx_sock from output
|
|
700
906
|
freq_obj.delete(:gqrx_sock)
|
|
701
907
|
puts JSON.pretty_generate(freq_obj)
|
|
@@ -703,6 +909,8 @@ module PWN
|
|
|
703
909
|
gets
|
|
704
910
|
puts "\n" * 3
|
|
705
911
|
end
|
|
912
|
+
rescue Interrupt
|
|
913
|
+
puts "\nCTRL+C detected - goodbye."
|
|
706
914
|
rescue StandardError => e
|
|
707
915
|
raise e
|
|
708
916
|
ensure
|
|
@@ -741,7 +949,7 @@ module PWN
|
|
|
741
949
|
public_class_method def self.disconnect(opts = {})
|
|
742
950
|
gqrx_sock = opts[:gqrx_sock]
|
|
743
951
|
|
|
744
|
-
PWN::Plugins::Sock.disconnect(sock_obj: gqrx_sock)
|
|
952
|
+
PWN::Plugins::Sock.disconnect(sock_obj: gqrx_sock) unless gqrx_sock.closed?
|
|
745
953
|
rescue StandardError => e
|
|
746
954
|
raise e
|
|
747
955
|
end
|
|
@@ -763,16 +971,11 @@ module PWN
|
|
|
763
971
|
port: 'optional - GQRX target port (defaults to 7356)'
|
|
764
972
|
)
|
|
765
973
|
|
|
766
|
-
gqrx_resp = #{self}.gqrx_cmd(
|
|
767
|
-
gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
768
|
-
cmd: 'required - GQRX command to execute',
|
|
769
|
-
resp_ok: 'optional - Expected response from GQRX to indicate success'
|
|
770
|
-
)
|
|
771
|
-
|
|
772
974
|
freq_obj = #{self}.init_freq(
|
|
773
975
|
gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
774
976
|
freq: 'required - Frequency to set',
|
|
775
977
|
demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
978
|
+
rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
776
979
|
bandwidth: 'optional - Bandwidth (defaults to 200_000)',
|
|
777
980
|
decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
778
981
|
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
@@ -786,9 +989,7 @@ module PWN
|
|
|
786
989
|
target_freq: 'required - Target frequency',
|
|
787
990
|
demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
788
991
|
bandwidth: 'optional - Bandwidth in Hz (Defaults to 200_000)',
|
|
789
|
-
overlap_protection: 'optional - Boolean to enable/disable bandwidth overlap protection (defaults to false)',
|
|
790
992
|
precision: 'optional - Precision (Defaults to 1)',
|
|
791
|
-
lock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',
|
|
792
993
|
strength_lock: 'optional - Strength lock (defaults to -70.0)',
|
|
793
994
|
squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
|
|
794
995
|
audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
|