pwn 0.5.512 → 0.5.514
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 +409 -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,218 @@ 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
|
+
print '$'
|
|
189
|
+
break if strength_db <= prev_strength_db
|
|
190
|
+
|
|
191
|
+
prev_strength_db = strength_db
|
|
192
|
+
sleep 0.0001
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
strength_db
|
|
196
|
+
rescue StandardError => e
|
|
197
|
+
raise e
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Supported Method Parameters::
|
|
201
|
+
# candidate_signals = PWN::SDR::GQRX.edge_detection(
|
|
202
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
203
|
+
# hz: 'required - Frequency to start edge detection from',
|
|
204
|
+
# step_hz: 'required - Frequency step in Hz for edge detection',
|
|
205
|
+
# strength_lock: 'required - Strength lock in dBFS to determine signal edges'
|
|
206
|
+
# )
|
|
207
|
+
private_class_method def self.edge_detection(opts = {})
|
|
208
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
209
|
+
hz = opts[:hz]
|
|
210
|
+
step_hz = opts[:step_hz]
|
|
211
|
+
strength_lock = opts[:strength_lock]
|
|
212
|
+
left_candidate_signals = []
|
|
213
|
+
right_candidate_signals = []
|
|
214
|
+
candidate_signals = []
|
|
215
|
+
|
|
216
|
+
# left_candidate_signals.clear
|
|
217
|
+
original_hz = hz
|
|
218
|
+
strength_db = 99.9
|
|
219
|
+
puts 'Finding Beginning Edge of Signal...'
|
|
220
|
+
while strength_db >= strength_lock
|
|
221
|
+
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
222
|
+
current_freq = 0
|
|
223
|
+
while current_freq.to_s.cast_to_raw_hz != hz.to_s.cast_to_raw_hz
|
|
224
|
+
current_freq = gqrx_cmd(
|
|
225
|
+
gqrx_sock: gqrx_sock,
|
|
226
|
+
cmd: 'f'
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
230
|
+
candidate = {
|
|
231
|
+
hz: hz.to_s.cast_to_raw_hz,
|
|
232
|
+
freq: hz.to_i.cast_to_pretty_hz,
|
|
233
|
+
strength: strength_db,
|
|
234
|
+
side: :left
|
|
235
|
+
}
|
|
236
|
+
left_candidate_signals.push(candidate)
|
|
237
|
+
hz -= step_hz
|
|
238
|
+
end
|
|
239
|
+
left_candidate_signals.uniq! { |s| s[:hz] }
|
|
240
|
+
left_candidate_signals.sort_by! { |s| s[:hz] }
|
|
241
|
+
|
|
242
|
+
# Now scan forwards to find the end of the signal
|
|
243
|
+
# The end of the signal is where the strength drops below strength_lock
|
|
244
|
+
# right_candidate_signals.clear
|
|
245
|
+
hz = original_hz
|
|
246
|
+
|
|
247
|
+
strength_db = 99.9
|
|
248
|
+
puts 'Finding Ending Edge of Signal...'
|
|
249
|
+
while strength_db >= strength_lock
|
|
250
|
+
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
251
|
+
current_freq = 0
|
|
252
|
+
while current_freq.to_s.cast_to_raw_hz != hz.to_s.cast_to_raw_hz
|
|
253
|
+
current_freq = gqrx_cmd(
|
|
254
|
+
gqrx_sock: gqrx_sock,
|
|
255
|
+
cmd: 'f'
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
259
|
+
candidate = {
|
|
260
|
+
hz: hz.to_s.cast_to_raw_hz,
|
|
261
|
+
freq: hz.to_i.cast_to_pretty_hz,
|
|
262
|
+
strength: strength_db,
|
|
263
|
+
side: :right
|
|
264
|
+
}
|
|
265
|
+
right_candidate_signals.push(candidate)
|
|
266
|
+
hz += step_hz
|
|
267
|
+
end
|
|
268
|
+
# Update candidate signals to remove duplicates and sort by hz
|
|
269
|
+
right_candidate_signals.uniq! { |s| s[:hz] }
|
|
270
|
+
right_candidate_signals.sort_by! { |s| s[:hz] }
|
|
271
|
+
|
|
272
|
+
candidate_signals = left_candidate_signals + right_candidate_signals
|
|
273
|
+
candidate_signals.uniq! { |s| s[:hz] }
|
|
274
|
+
candidate_signals.sort_by! { |s| s[:hz] }
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
raise e
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Supported Method Parameters::
|
|
280
|
+
# scan_resp = PWN::SDR::GQRX.log_signals(
|
|
281
|
+
# signals_arr: 'required - Array of detected signals',
|
|
282
|
+
# timestamp_start: 'required - Scan start timestamp',
|
|
283
|
+
# scan_log: 'required - Path to save detected signals log'
|
|
284
|
+
# )
|
|
285
|
+
private_class_method def self.log_signals(opts = {})
|
|
286
|
+
signals_arr = opts[:signals_arr]
|
|
287
|
+
timestamp_start = opts[:timestamp_start]
|
|
288
|
+
scan_log = opts[:scan_log]
|
|
289
|
+
|
|
290
|
+
signals = signals_arr.sort_by { |s| s[:freq].to_s.cast_to_raw_hz }
|
|
291
|
+
# Unique signals by frequency
|
|
292
|
+
signals.uniq! { |s| s[:freq].to_s.cast_to_raw_hz }
|
|
293
|
+
|
|
294
|
+
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
295
|
+
duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
|
|
296
|
+
# Convert duration seconds to hours minutes seconds
|
|
297
|
+
hours = (duration_secs / 3600).to_i
|
|
298
|
+
minutes = ((duration_secs % 3600) / 60).to_i
|
|
299
|
+
seconds = (duration_secs % 60).to_i
|
|
300
|
+
duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
|
|
301
|
+
|
|
302
|
+
scan_resp = {
|
|
303
|
+
signals: signals,
|
|
304
|
+
total: signals.length,
|
|
305
|
+
timestamp_start: timestamp_start,
|
|
306
|
+
timestamp_end: timestamp_end,
|
|
307
|
+
duration: duration
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
File.write(
|
|
311
|
+
scan_log,
|
|
312
|
+
JSON.pretty_generate(scan_resp)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
scan_resp
|
|
316
|
+
rescue StandardError => e
|
|
317
|
+
raise e
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Supported Method Parameters::
|
|
321
|
+
# rds_resp = PWN::SDR::GQRX.decode_rds(
|
|
322
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
323
|
+
# )
|
|
324
|
+
|
|
325
|
+
private_class_method def self.decode_rds(opts = {})
|
|
326
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
327
|
+
|
|
328
|
+
# We toggle RDS off and on to reset the decoder
|
|
329
|
+
rds_resp = gqrx_cmd(
|
|
330
|
+
gqrx_sock: gqrx_sock,
|
|
331
|
+
cmd: 'U RDS 0',
|
|
332
|
+
resp_ok: 'RPRT 0'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
rds_resp = gqrx_cmd(
|
|
336
|
+
gqrx_sock: gqrx_sock,
|
|
337
|
+
cmd: 'U RDS 1',
|
|
338
|
+
resp_ok: 'RPRT 0'
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
rds_resp = {}
|
|
342
|
+
attempts = 0
|
|
343
|
+
max_attempts = 90
|
|
344
|
+
skip_rds = "\n"
|
|
345
|
+
print 'INFO: Decoding FM radio RDS data (Press ENTER to skip)...'
|
|
346
|
+
max_attempts.times do
|
|
347
|
+
attempts += 1
|
|
348
|
+
rds_resp[:rds_pi] = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'p RDS_PI')
|
|
349
|
+
rds_resp[:rds_ps_name] = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'p RDS_PS_NAME')
|
|
350
|
+
rds_resp[:rds_radiotext] = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'p RDS_RADIOTEXT')
|
|
351
|
+
|
|
352
|
+
# Break if ENTER key pressed
|
|
353
|
+
# This is useful if no RDS data is available
|
|
354
|
+
# on the current frequency (e.g. false+)
|
|
355
|
+
break if $stdin.ready? && $stdin.read_nonblock(1) == skip_rds
|
|
356
|
+
|
|
357
|
+
break if rds_resp[:rds_pi] != '0000' && !rds_resp[:rds_ps_name].empty? && !rds_resp[:rds_radiotext].empty?
|
|
358
|
+
|
|
359
|
+
print '.'
|
|
360
|
+
sleep 0.1
|
|
361
|
+
end
|
|
362
|
+
puts 'complete.'
|
|
363
|
+
rds_resp
|
|
364
|
+
rescue StandardError => e
|
|
365
|
+
raise e
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Supported Method Parameters::
|
|
369
|
+
# gqrx_sock = PWN::SDR::GQRX.connect(
|
|
370
|
+
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
371
|
+
# port: 'optional - GQRX target port (defaults to 7356)'
|
|
372
|
+
# )
|
|
373
|
+
public_class_method def self.connect(opts = {})
|
|
374
|
+
target = opts[:target] ||= '127.0.0.1'
|
|
375
|
+
port = opts[:port] ||= 7356
|
|
376
|
+
|
|
377
|
+
PWN::Plugins::Sock.connect(target: target, port: port)
|
|
378
|
+
rescue StandardError => e
|
|
379
|
+
raise e
|
|
380
|
+
end
|
|
381
|
+
|
|
209
382
|
# Supported Method Parameters::
|
|
210
383
|
# freq_obj = PWN::SDR::GQRX.init_freq(
|
|
211
384
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
212
385
|
# freq: 'required - Frequency to set',
|
|
213
386
|
# demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
387
|
+
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
214
388
|
# bandwidth: 'optional - Bandwidth (defaults to 200_000)',
|
|
215
389
|
# squelch: 'optional - Squelch level to set (Defaults to current value)',
|
|
216
390
|
# decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
@@ -239,6 +413,8 @@ module PWN
|
|
|
239
413
|
demodulator_mode = opts[:demodulator_mode] ||= :WFM
|
|
240
414
|
raise "ERROR: Invalid demodulator_mode '#{demodulator_mode}'. Valid modes: #{valid_demodulator_modes.join(', ')}" unless valid_demodulator_modes.include?(demodulator_mode.to_sym)
|
|
241
415
|
|
|
416
|
+
rds = opts[:rds] ||= false
|
|
417
|
+
|
|
242
418
|
bandwidth = opts[:bandwidth] ||= 200_000
|
|
243
419
|
squelch = opts[:squelch]
|
|
244
420
|
decoder = opts[:decoder]
|
|
@@ -248,24 +424,16 @@ module PWN
|
|
|
248
424
|
|
|
249
425
|
raise "ERROR: record_dir '#{record_dir}' does not exist. Please create it or provide a valid path." if decoder && !Dir.exist?(record_dir)
|
|
250
426
|
|
|
251
|
-
|
|
427
|
+
unless keep_alive
|
|
428
|
+
squelch = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'l SQL').to_f if squelch.nil?
|
|
252
429
|
change_squelch_resp = gqrx_cmd(
|
|
253
430
|
gqrx_sock: gqrx_sock,
|
|
254
431
|
cmd: "L SQL #{squelch}",
|
|
255
432
|
resp_ok: 'RPRT 0'
|
|
256
433
|
)
|
|
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
434
|
|
|
265
|
-
# Set demod mode and bandwidth (always, using defaults if not provided)
|
|
266
|
-
unless keep_alive
|
|
267
435
|
mode_str = demodulator_mode.to_s.upcase
|
|
268
|
-
passband_hz = bandwidth.to_s.
|
|
436
|
+
passband_hz = bandwidth.to_s.cast_to_raw_hz
|
|
269
437
|
gqrx_cmd(
|
|
270
438
|
gqrx_sock: gqrx_sock,
|
|
271
439
|
cmd: "M #{mode_str} #{passband_hz}",
|
|
@@ -273,38 +441,41 @@ module PWN
|
|
|
273
441
|
)
|
|
274
442
|
end
|
|
275
443
|
|
|
276
|
-
|
|
277
|
-
demod_n_passband = gqrx_cmd(
|
|
444
|
+
change_freq_resp = gqrx_cmd(
|
|
278
445
|
gqrx_sock: gqrx_sock,
|
|
279
|
-
cmd:
|
|
446
|
+
cmd: "F #{freq.to_s.cast_to_raw_hz}",
|
|
447
|
+
resp_ok: 'RPRT 0'
|
|
280
448
|
)
|
|
281
449
|
|
|
282
|
-
|
|
283
|
-
current_freq
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
450
|
+
current_freq = 0
|
|
451
|
+
while current_freq.to_s.cast_to_raw_hz != freq.to_s.cast_to_raw_hz
|
|
452
|
+
current_freq = gqrx_cmd(
|
|
453
|
+
gqrx_sock: gqrx_sock,
|
|
454
|
+
cmd: 'f'
|
|
455
|
+
)
|
|
456
|
+
end
|
|
287
457
|
|
|
288
458
|
freq_obj = {
|
|
459
|
+
bandwidth: bandwidth,
|
|
289
460
|
demodulator_mode: demodulator_mode,
|
|
290
|
-
|
|
291
|
-
freq:
|
|
292
|
-
bandwidth: bandwidth
|
|
461
|
+
rds: rds,
|
|
462
|
+
freq: freq
|
|
293
463
|
}
|
|
294
464
|
|
|
295
465
|
unless suppress_details
|
|
466
|
+
demod_n_passband = gqrx_cmd(
|
|
467
|
+
gqrx_sock: gqrx_sock,
|
|
468
|
+
cmd: 'm'
|
|
469
|
+
)
|
|
470
|
+
|
|
296
471
|
audio_gain_db = gqrx_cmd(
|
|
297
472
|
gqrx_sock: gqrx_sock,
|
|
298
473
|
cmd: 'l AF'
|
|
299
474
|
).to_f
|
|
300
475
|
|
|
301
|
-
|
|
302
|
-
gqrx_sock: gqrx_sock,
|
|
303
|
-
cmd: 'l STRENGTH'
|
|
304
|
-
).to_f
|
|
305
|
-
strength_db = strength_db_float.round(1)
|
|
476
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
306
477
|
|
|
307
|
-
|
|
478
|
+
squelch = gqrx_cmd(
|
|
308
479
|
gqrx_sock: gqrx_sock,
|
|
309
480
|
cmd: 'l SQL'
|
|
310
481
|
).to_f
|
|
@@ -324,12 +495,17 @@ module PWN
|
|
|
324
495
|
cmd: 'l BB_GAIN'
|
|
325
496
|
)
|
|
326
497
|
|
|
498
|
+
rds_resp = nil
|
|
499
|
+
rds_resp = decode_rds(gqrx_sock: gqrx_sock) if rds
|
|
500
|
+
|
|
327
501
|
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
|
|
502
|
+
freq_obj[:demod_mode_n_passband] = demod_n_passband
|
|
331
503
|
freq_obj[:bb_gain] = bb_gain
|
|
504
|
+
freq_obj[:if_gain] = if_gain
|
|
505
|
+
freq_obj[:rf_gain] = rf_gain
|
|
506
|
+
freq_obj[:squelch] = squelch
|
|
332
507
|
freq_obj[:strength_db] = strength_db
|
|
508
|
+
freq_obj[:rds] = rds_resp
|
|
333
509
|
end
|
|
334
510
|
|
|
335
511
|
# Start recording and decoding if decoder provided
|
|
@@ -365,6 +541,7 @@ module PWN
|
|
|
365
541
|
decoder_thread = decoder_module.start(freq_obj: freq_obj)
|
|
366
542
|
freq_obj.delete(:gqrx_sock)
|
|
367
543
|
|
|
544
|
+
freq_obj[:freq] = current_freq
|
|
368
545
|
freq_obj[:decoder] = decoder
|
|
369
546
|
freq_obj[:decoder_module] = decoder_module
|
|
370
547
|
freq_obj[:decoder_thread] = decoder_thread
|
|
@@ -393,10 +570,9 @@ module PWN
|
|
|
393
570
|
# start_freq: 'required - Start frequency of scan range',
|
|
394
571
|
# target_freq: 'required - Target frequency of scan range',
|
|
395
572
|
# demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
573
|
+
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
396
574
|
# bandwidth: 'optional - Bandwidth in Hz (Defaults to 200_000)',
|
|
397
|
-
# overlap_protection: 'optional - Boolean to enable/disable bandwidth overlap protection (defaults to false)',
|
|
398
575
|
# 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
576
|
# strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
|
|
401
577
|
# squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
|
|
402
578
|
# audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
|
|
@@ -414,19 +590,19 @@ module PWN
|
|
|
414
590
|
gqrx_sock = opts[:gqrx_sock]
|
|
415
591
|
|
|
416
592
|
start_freq = opts[:start_freq]
|
|
417
|
-
hz_start = start_freq.to_s.
|
|
593
|
+
hz_start = start_freq.to_s.cast_to_raw_hz
|
|
418
594
|
|
|
419
595
|
target_freq = opts[:target_freq]
|
|
420
|
-
hz_target = target_freq.to_s.
|
|
596
|
+
hz_target = target_freq.to_s.cast_to_raw_hz
|
|
421
597
|
|
|
422
598
|
demodulator_mode = opts[:demodulator_mode]
|
|
599
|
+
rds = opts[:rds] ||= false
|
|
600
|
+
|
|
423
601
|
bandwidth = opts[:bandwidth] ||= 200_000
|
|
424
|
-
overlap_protection = opts[:overlap_protection] || false
|
|
425
602
|
precision = opts[:precision] ||= 1
|
|
426
|
-
lock_freq_duration = opts[:lock_freq_duration] ||= 0.04
|
|
427
603
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
428
604
|
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
429
|
-
scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{hz_start.
|
|
605
|
+
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
606
|
location = opts[:location] ||= 'United States'
|
|
431
607
|
|
|
432
608
|
step_hz = 10**(precision - 1)
|
|
@@ -439,9 +615,20 @@ module PWN
|
|
|
439
615
|
resp_ok: 'RPRT 0'
|
|
440
616
|
)
|
|
441
617
|
|
|
618
|
+
# We always disable RDS decoding at during the scan
|
|
619
|
+
# to prevent unnecessary processing overhead.
|
|
620
|
+
# We return the rds boolean in the scan_resp object
|
|
621
|
+
# so it will be picked up and used appropriately
|
|
622
|
+
# when calling analyze_scan or analyze_log methods.
|
|
623
|
+
rds_resp = gqrx_cmd(
|
|
624
|
+
gqrx_sock: gqrx_sock,
|
|
625
|
+
cmd: 'U RDS 0',
|
|
626
|
+
resp_ok: 'RPRT 0'
|
|
627
|
+
)
|
|
628
|
+
|
|
442
629
|
# Set demodulator mode & passband once for the scan
|
|
443
630
|
mode_str = demodulator_mode.to_s.upcase
|
|
444
|
-
passband_hz = bandwidth.to_s.
|
|
631
|
+
passband_hz = bandwidth.to_s.cast_to_raw_hz
|
|
445
632
|
gqrx_cmd(
|
|
446
633
|
gqrx_sock: gqrx_sock,
|
|
447
634
|
cmd: "M #{mode_str} #{passband_hz}",
|
|
@@ -450,7 +637,7 @@ module PWN
|
|
|
450
637
|
|
|
451
638
|
audio_gain_db = opts[:audio_gain_db] ||= 6.0
|
|
452
639
|
audio_gain_db = audio_gain_db.to_f
|
|
453
|
-
audio_gain_db_resp =
|
|
640
|
+
audio_gain_db_resp = gqrx_cmd(
|
|
454
641
|
gqrx_sock: gqrx_sock,
|
|
455
642
|
cmd: "L AF #{audio_gain_db}",
|
|
456
643
|
resp_ok: 'RPRT 0'
|
|
@@ -458,7 +645,7 @@ module PWN
|
|
|
458
645
|
|
|
459
646
|
rf_gain = opts[:rf_gain] ||= 0.0
|
|
460
647
|
rf_gain = rf_gain.to_f
|
|
461
|
-
rf_gain_resp =
|
|
648
|
+
rf_gain_resp = gqrx_cmd(
|
|
462
649
|
gqrx_sock: gqrx_sock,
|
|
463
650
|
cmd: "L RF_GAIN #{rf_gain}",
|
|
464
651
|
resp_ok: 'RPRT 0'
|
|
@@ -466,7 +653,7 @@ module PWN
|
|
|
466
653
|
|
|
467
654
|
intermediate_gain = opts[:intermediate_gain] ||= 32.0
|
|
468
655
|
intermediate_gain = intermediate_gain.to_f
|
|
469
|
-
intermediate_resp =
|
|
656
|
+
intermediate_resp = gqrx_cmd(
|
|
470
657
|
gqrx_sock: gqrx_sock,
|
|
471
658
|
cmd: "L IF_GAIN #{intermediate_gain}",
|
|
472
659
|
resp_ok: 'RPRT 0'
|
|
@@ -474,35 +661,33 @@ module PWN
|
|
|
474
661
|
|
|
475
662
|
baseband_gain = opts[:baseband_gain] ||= 10.0
|
|
476
663
|
baseband_gain = baseband_gain.to_f
|
|
477
|
-
baseband_resp =
|
|
664
|
+
baseband_resp = gqrx_cmd(
|
|
478
665
|
gqrx_sock: gqrx_sock,
|
|
479
666
|
cmd: "L BB_GAIN #{baseband_gain}",
|
|
480
667
|
resp_ok: 'RPRT 0'
|
|
481
668
|
)
|
|
482
669
|
|
|
483
|
-
prev_freq_obj =
|
|
670
|
+
prev_freq_obj = init_freq(
|
|
671
|
+
gqrx_sock: gqrx_sock,
|
|
672
|
+
freq: hz_start,
|
|
673
|
+
demodulator_mode: demodulator_mode,
|
|
674
|
+
rds: rds,
|
|
675
|
+
bandwidth: bandwidth,
|
|
676
|
+
squelch: squelch,
|
|
677
|
+
suppress_details: true,
|
|
678
|
+
keep_alive: true
|
|
679
|
+
)
|
|
484
680
|
|
|
485
|
-
in_signal = false
|
|
486
681
|
candidate_signals = []
|
|
487
|
-
strength_history = []
|
|
488
682
|
|
|
489
683
|
# Adaptive peak finder – trims weakest ends after each pass
|
|
490
684
|
# Converges quickly to the true center of the bell curve
|
|
491
685
|
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
|
|
686
|
+
beg_of_signal_hz = opts[:beg_of_signal_hz].to_s.cast_to_raw_hz
|
|
687
|
+
end_of_signal_hz = opts[:end_of_signal_hz].to_s.cast_to_raw_hz
|
|
503
688
|
|
|
504
689
|
samples = []
|
|
505
|
-
prev_best_sample =
|
|
690
|
+
prev_best_sample = {}
|
|
506
691
|
consecutive_best = 0
|
|
507
692
|
direction_up = true
|
|
508
693
|
|
|
@@ -525,19 +710,19 @@ module PWN
|
|
|
525
710
|
print '>' if direction_up
|
|
526
711
|
print '<' unless direction_up
|
|
527
712
|
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)
|
|
713
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
531
714
|
samples.push({ hz: hz, strength_db: strength_db })
|
|
532
715
|
|
|
533
|
-
# current_hz = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f').to_s.
|
|
534
|
-
# puts "Sampled Frequency: #{current_hz.
|
|
716
|
+
# current_hz = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f').to_s.cast_to_raw_hz
|
|
717
|
+
# puts "Sampled Frequency: #{current_hz.to_i.cast_to_pretty_hz} => Strength: #{strength_db} dBFS"
|
|
535
718
|
end
|
|
536
719
|
|
|
537
720
|
# Compute fresh averaged_samples from all cumulative samples
|
|
538
721
|
averaged_samples = []
|
|
539
722
|
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)
|
|
723
|
+
avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size)
|
|
724
|
+
# avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size).round(2)
|
|
725
|
+
# avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size).round(1)
|
|
541
726
|
averaged_samples.push({ hz: hz, strength_db: avg_strength })
|
|
542
727
|
end
|
|
543
728
|
|
|
@@ -567,7 +752,7 @@ module PWN
|
|
|
567
752
|
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
568
753
|
|
|
569
754
|
# Check for improvement
|
|
570
|
-
if best_sample == prev_best_sample
|
|
755
|
+
if best_sample[:hz] == prev_best_sample[:hz]
|
|
571
756
|
consecutive_best += 1
|
|
572
757
|
else
|
|
573
758
|
consecutive_best = 0
|
|
@@ -576,7 +761,7 @@ module PWN
|
|
|
576
761
|
# Dup to avoid reference issues
|
|
577
762
|
prev_best_sample = best_sample.dup
|
|
578
763
|
|
|
579
|
-
puts "Pass #{pass_count}: Best #{best_sample[:hz].
|
|
764
|
+
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
765
|
|
|
581
766
|
# Break if no improvement in 3 consecutive passes or theres only one sample left
|
|
582
767
|
break if consecutive_best.positive? || averaged_samples.size == 1
|
|
@@ -586,82 +771,101 @@ module PWN
|
|
|
586
771
|
end
|
|
587
772
|
|
|
588
773
|
# Begin scanning range
|
|
589
|
-
puts "
|
|
774
|
+
puts "\n"
|
|
775
|
+
puts '-' * 86
|
|
776
|
+
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."
|
|
777
|
+
puts "If scans are slow and/or you're experiencing false positives/negatives,"
|
|
778
|
+
puts 'consider adjusting the following:'
|
|
779
|
+
puts "1. The SDR's sample rate in GQRX"
|
|
780
|
+
puts "\s\s- Click on `Configure I/O devices`."
|
|
781
|
+
puts "\s\s- A lower `Input rate` value seems counter-intuitive but works well (e.g. ADALM PLUTO ~ 1000000)."
|
|
782
|
+
puts '2. Adjust the :strength_lock parameter.'
|
|
783
|
+
puts '3. Adjust the :precision parameter.'
|
|
784
|
+
puts '4. Disable AI introspection in PWN::Env'
|
|
785
|
+
puts 'Happy scanning!'
|
|
786
|
+
puts '-' * 86
|
|
787
|
+
puts "\n\n\n"
|
|
590
788
|
|
|
591
789
|
signals_arr = []
|
|
592
|
-
|
|
593
|
-
|
|
790
|
+
hz = hz_start
|
|
791
|
+
while hz <= hz_target
|
|
594
792
|
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
|
-
|
|
793
|
+
current_freq = 0
|
|
794
|
+
while current_freq.to_s.cast_to_raw_hz != hz.to_s.cast_to_raw_hz
|
|
795
|
+
current_freq = gqrx_cmd(
|
|
796
|
+
gqrx_sock: gqrx_sock,
|
|
797
|
+
cmd: 'f'
|
|
798
|
+
)
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
802
|
+
|
|
803
|
+
if strength_db >= strength_lock
|
|
804
|
+
puts '-' * 86
|
|
805
|
+
# Find left and right edges of the signal
|
|
806
|
+
candidate_signals = edge_detection(
|
|
807
|
+
gqrx_sock: gqrx_sock,
|
|
808
|
+
hz: hz,
|
|
809
|
+
step_hz: step_hz,
|
|
810
|
+
strength_lock: strength_lock
|
|
811
|
+
)
|
|
812
|
+
elsif candidate_signals.length.positive?
|
|
813
|
+
beg_of_signal_hz = candidate_signals.first[:hz]
|
|
814
|
+
top_of_signal_hz_idx = (candidate_signals.length - 1) / 2
|
|
815
|
+
top_of_signal_hz = candidate_signals[top_of_signal_hz_idx][:hz]
|
|
816
|
+
end_of_signal_hz = candidate_signals.last[:hz]
|
|
817
|
+
puts 'Candidate Signal(s) Detected:'
|
|
818
|
+
puts JSON.pretty_generate(candidate_signals)
|
|
819
|
+
|
|
820
|
+
prev_freq = prev_freq_obj[:freq].to_s.cast_to_raw_hz
|
|
821
|
+
distance_from_prev_detected_freq_hz = (beg_of_signal_hz - prev_freq).abs
|
|
822
|
+
half_bandwidth = (bandwidth / 2).to_i
|
|
823
|
+
|
|
824
|
+
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"
|
|
825
|
+
|
|
826
|
+
puts 'Finding Best Peak...'
|
|
827
|
+
best_peak = find_best_peak.call(
|
|
828
|
+
beg_of_signal_hz: beg_of_signal_hz,
|
|
829
|
+
end_of_signal_hz: end_of_signal_hz
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
if best_peak[:hz] && best_peak[:strength_db] > strength_lock
|
|
833
|
+
puts "\n**** Detected Signal ****"
|
|
834
|
+
best_freq = best_peak[:hz].to_i.cast_to_pretty_hz
|
|
835
|
+
best_strength_db = best_peak[:strength_db]
|
|
836
|
+
prev_freq_obj = init_freq(
|
|
837
|
+
gqrx_sock: gqrx_sock,
|
|
838
|
+
freq: best_freq,
|
|
839
|
+
rds: rds,
|
|
840
|
+
suppress_details: true,
|
|
841
|
+
keep_alive: true
|
|
842
|
+
)
|
|
843
|
+
prev_freq_obj[:strength_lock] = strength_lock
|
|
844
|
+
prev_freq_obj[:strength_db] = best_strength_db
|
|
845
|
+
|
|
846
|
+
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."
|
|
847
|
+
ai_analysis = PWN::AI::Introspection.reflect_on(
|
|
848
|
+
request: prev_freq_obj.to_json,
|
|
849
|
+
system_role_content: system_role_content,
|
|
850
|
+
suppress_pii_warning: true
|
|
628
851
|
)
|
|
629
852
|
|
|
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
|
|
853
|
+
prev_freq_obj[:ai_analysis] = ai_analysis unless ai_analysis.nil?
|
|
854
|
+
puts JSON.pretty_generate(prev_freq_obj)
|
|
855
|
+
puts '-' * 86
|
|
856
|
+
puts "\n\n\n"
|
|
857
|
+
signals_arr.push(prev_freq_obj)
|
|
858
|
+
log_signals(
|
|
859
|
+
signals_arr: signals_arr,
|
|
860
|
+
timestamp_start: timestamp_start,
|
|
861
|
+
scan_log: scan_log
|
|
862
|
+
)
|
|
863
|
+
hz = end_of_signal_hz
|
|
864
|
+
# gets
|
|
661
865
|
end
|
|
662
|
-
|
|
663
|
-
strength_history = []
|
|
866
|
+
candidate_signals.clear
|
|
664
867
|
end
|
|
868
|
+
hz += step_hz
|
|
665
869
|
end
|
|
666
870
|
|
|
667
871
|
log_signals(
|
|
@@ -669,6 +873,8 @@ module PWN
|
|
|
669
873
|
timestamp_start: timestamp_start,
|
|
670
874
|
scan_log: scan_log
|
|
671
875
|
)
|
|
876
|
+
rescue Interrupt
|
|
877
|
+
puts "\nCTRL+C detected - goodbye."
|
|
672
878
|
rescue StandardError => e
|
|
673
879
|
raise e
|
|
674
880
|
ensure
|
|
@@ -693,9 +899,11 @@ module PWN
|
|
|
693
899
|
)
|
|
694
900
|
|
|
695
901
|
scan_resp[:signals].each do |signal|
|
|
696
|
-
|
|
902
|
+
signal[:gqrx_sock] = gqrx_sock
|
|
903
|
+
# This is required to keep connection alive during analysis
|
|
904
|
+
signal[:keep_alive] = true
|
|
905
|
+
freq_obj = init_freq(signal)
|
|
697
906
|
freq_obj = signal.merge(freq_obj)
|
|
698
|
-
freq_obj = init_freq(freq_obj)
|
|
699
907
|
# Redact gqrx_sock from output
|
|
700
908
|
freq_obj.delete(:gqrx_sock)
|
|
701
909
|
puts JSON.pretty_generate(freq_obj)
|
|
@@ -703,6 +911,8 @@ module PWN
|
|
|
703
911
|
gets
|
|
704
912
|
puts "\n" * 3
|
|
705
913
|
end
|
|
914
|
+
rescue Interrupt
|
|
915
|
+
puts "\nCTRL+C detected - goodbye."
|
|
706
916
|
rescue StandardError => e
|
|
707
917
|
raise e
|
|
708
918
|
ensure
|
|
@@ -741,7 +951,7 @@ module PWN
|
|
|
741
951
|
public_class_method def self.disconnect(opts = {})
|
|
742
952
|
gqrx_sock = opts[:gqrx_sock]
|
|
743
953
|
|
|
744
|
-
PWN::Plugins::Sock.disconnect(sock_obj: gqrx_sock)
|
|
954
|
+
PWN::Plugins::Sock.disconnect(sock_obj: gqrx_sock) unless gqrx_sock.closed?
|
|
745
955
|
rescue StandardError => e
|
|
746
956
|
raise e
|
|
747
957
|
end
|
|
@@ -763,16 +973,11 @@ module PWN
|
|
|
763
973
|
port: 'optional - GQRX target port (defaults to 7356)'
|
|
764
974
|
)
|
|
765
975
|
|
|
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
976
|
freq_obj = #{self}.init_freq(
|
|
773
977
|
gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
774
978
|
freq: 'required - Frequency to set',
|
|
775
979
|
demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
980
|
+
rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
776
981
|
bandwidth: 'optional - Bandwidth (defaults to 200_000)',
|
|
777
982
|
decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
778
983
|
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
@@ -786,9 +991,7 @@ module PWN
|
|
|
786
991
|
target_freq: 'required - Target frequency',
|
|
787
992
|
demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
788
993
|
bandwidth: 'optional - Bandwidth in Hz (Defaults to 200_000)',
|
|
789
|
-
overlap_protection: 'optional - Boolean to enable/disable bandwidth overlap protection (defaults to false)',
|
|
790
994
|
precision: 'optional - Precision (Defaults to 1)',
|
|
791
|
-
lock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',
|
|
792
995
|
strength_lock: 'optional - Strength lock (defaults to -70.0)',
|
|
793
996
|
squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
|
|
794
997
|
audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
|