pwn 0.5.515 → 0.5.517
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/Gemfile +1 -1
- data/README.md +3 -3
- data/lib/pwn/sdr/decoder/rds.rb +123 -0
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/frequency_allocation.rb +81 -161
- data/lib/pwn/sdr/gqrx.rb +184 -228
- data/lib/pwn/sdr.rb +13 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/sdr/decoder/rds_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +8 -4
- metadata +5 -3
data/lib/pwn/sdr/gqrx.rb
CHANGED
|
@@ -7,29 +7,6 @@ module PWN
|
|
|
7
7
|
module SDR
|
|
8
8
|
# This plugin interacts with the remote control interface of GQRX.
|
|
9
9
|
module GQRX
|
|
10
|
-
# Monkey patches for frequency handling
|
|
11
|
-
String.class_eval do
|
|
12
|
-
def cast_to_raw_hz
|
|
13
|
-
gsub('.', '').to_i
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
Integer.class_eval do
|
|
18
|
-
# Should always return format of X.XXX.XXX.XXX
|
|
19
|
-
# So 002_450_000_000 becomes 2.450.000.000
|
|
20
|
-
# So 2_450_000_000 becomes 2.450.000.000
|
|
21
|
-
# So 960_000_000 becomes 960.000.000
|
|
22
|
-
# 1000 should be 1.000
|
|
23
|
-
def cast_to_pretty_hz
|
|
24
|
-
str_hz = to_s
|
|
25
|
-
# Nuke leading zeros
|
|
26
|
-
# E.g., 002450000000 -> 2450000000
|
|
27
|
-
str_hz = str_hz.sub(/^0+/, '') unless str_hz == '0'
|
|
28
|
-
# Insert dots every 3 digits from the right
|
|
29
|
-
str_hz.reverse.scan(/.{1,3}/).join('.').reverse
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
10
|
# Supported Method Parameters::
|
|
34
11
|
# gqrx_resp = PWN::SDR::GQRX.gqrx_cmd(
|
|
35
12
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
@@ -154,17 +131,15 @@ module PWN
|
|
|
154
131
|
raise "ERROR!!! Command: #{cmd} Expected Resp: #{resp_ok}, Got: #{response_str}" if resp_ok && response_str != resp_ok
|
|
155
132
|
|
|
156
133
|
# Reformat positive integer frequency responses (e.g., from 'f')
|
|
157
|
-
response_str = response_str
|
|
134
|
+
response_str = PWN::SDR.hz_to_s(response_str) if response_str.match?(/^\d+$/) && response_str.to_i.positive?
|
|
158
135
|
|
|
159
136
|
response_str
|
|
160
137
|
rescue RuntimeError => e
|
|
161
|
-
|
|
162
|
-
puts 'WARNING: Intermediate Gain is not supported by the radio backend.' if e.message.include?('Command: L IF_GAIN')
|
|
163
|
-
puts 'WARNING: Baseband Gain is not supported by the radio backend.' if e.message.include?('Command: L BB_GAIN')
|
|
138
|
+
response_str = 'Function not supported by this radio backend.' if e.message.include?('RF_GAIN') || e.message.include?('IF_GAIN') || e.message.include?('BB_GAIN')
|
|
164
139
|
|
|
165
|
-
raise e unless e.message.include?('
|
|
166
|
-
e.message.include?('
|
|
167
|
-
e.message.include?('
|
|
140
|
+
raise e unless e.message.include?('RF_GAIN') ||
|
|
141
|
+
e.message.include?('IF_GAIN') ||
|
|
142
|
+
e.message.include?('BB_GAIN')
|
|
168
143
|
rescue StandardError => e
|
|
169
144
|
raise e
|
|
170
145
|
end
|
|
@@ -172,36 +147,49 @@ module PWN
|
|
|
172
147
|
# Supported Method Parameters::
|
|
173
148
|
# strength_db = PWN::SDR::GQRX.measure_signal_strength(
|
|
174
149
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
175
|
-
#
|
|
150
|
+
# freq: 'required - Frequency to measure signal strength',
|
|
151
|
+
# strength_lock: 'optional - Strength lock in dBFS to determine signal edges (defaults to -70.0)',
|
|
152
|
+
# phase: 'optional - Phase of measurement for logging purposes (defaults to :find_candidates)'
|
|
176
153
|
# )
|
|
177
154
|
private_class_method def self.measure_signal_strength(opts = {})
|
|
178
155
|
gqrx_sock = opts[:gqrx_sock]
|
|
156
|
+
freq = PWN::SDR.hz_to_i(opts[:freq])
|
|
157
|
+
freq = PWN::SDR.hz_to_s(freq)
|
|
158
|
+
|
|
179
159
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
160
|
+
phase = opts[:phase] ||= :find_candidates
|
|
180
161
|
|
|
181
162
|
attempts = 0
|
|
182
163
|
strength_db = -99.9
|
|
183
164
|
prev_strength_db = -99.9
|
|
165
|
+
distance_between_unique_samples = 0.0
|
|
166
|
+
samples = []
|
|
167
|
+
unique_samples = []
|
|
168
|
+
strength_measured = false
|
|
184
169
|
loop do
|
|
185
170
|
attempts += 1
|
|
186
171
|
strength_db = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
187
172
|
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
173
|
+
# Fast approach while still maintaining decent accuracy
|
|
174
|
+
samples.push(strength_db)
|
|
175
|
+
unique_samples = samples.uniq
|
|
176
|
+
if unique_samples.length > 1
|
|
177
|
+
prev_strength_db = unique_samples[-2]
|
|
178
|
+
distance_between_unique_samples = (strength_db - prev_strength_db).abs
|
|
179
|
+
strength_measured = true if distance_between_unique_samples.positive? && strength_lock > strength_db
|
|
180
|
+
end
|
|
181
|
+
strength_measured = true if distance_between_unique_samples.positive? && distance_between_unique_samples < 5
|
|
196
182
|
|
|
197
|
-
break if
|
|
183
|
+
break if strength_measured
|
|
198
184
|
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
# Sleep a tiny bit to allow strength_db values to fluctuate
|
|
186
|
+
sleep 0.0001
|
|
201
187
|
end
|
|
202
|
-
|
|
188
|
+
# Uncomment for debugging strength measurement attempts
|
|
189
|
+
# which translates to speed and accuracy refinement
|
|
190
|
+
puts "\tStrength Measurement Attempts: #{attempts} | Freq: #{freq} | Phase: #{phase} | Unique Samples: #{unique_samples} | dbFS Distance Unique Samples: #{distance_between_unique_samples}"
|
|
203
191
|
|
|
204
|
-
strength_db
|
|
192
|
+
strength_db.round(1)
|
|
205
193
|
rescue StandardError => e
|
|
206
194
|
raise e
|
|
207
195
|
end
|
|
@@ -213,7 +201,7 @@ module PWN
|
|
|
213
201
|
# )
|
|
214
202
|
private_class_method def self.tune_to(opts = {})
|
|
215
203
|
gqrx_sock = opts[:gqrx_sock]
|
|
216
|
-
hz = opts[:hz]
|
|
204
|
+
hz = PWN::SDR.hz_to_i(opts[:hz])
|
|
217
205
|
|
|
218
206
|
current_freq = 0
|
|
219
207
|
attempts = 0
|
|
@@ -230,7 +218,7 @@ module PWN
|
|
|
230
218
|
cmd: 'f'
|
|
231
219
|
)
|
|
232
220
|
|
|
233
|
-
break if current_freq
|
|
221
|
+
break if PWN::SDR.hz_to_i(current_freq) == hz
|
|
234
222
|
end
|
|
235
223
|
# puts "Tuned to #{current_freq} in #{attempts} attempt(s)."
|
|
236
224
|
rescue StandardError => e
|
|
@@ -253,21 +241,22 @@ module PWN
|
|
|
253
241
|
right_candidate_signals = []
|
|
254
242
|
candidate_signals = []
|
|
255
243
|
|
|
256
|
-
# left_candidate_signals.clear
|
|
257
244
|
original_hz = hz
|
|
258
245
|
strength_db = 99.9
|
|
259
|
-
puts '
|
|
246
|
+
puts '*** Edge Detection: Locating Beginning of Signal...'
|
|
260
247
|
while strength_db >= strength_lock
|
|
261
248
|
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
262
249
|
strength_db = measure_signal_strength(
|
|
263
250
|
gqrx_sock: gqrx_sock,
|
|
264
|
-
|
|
251
|
+
freq: hz,
|
|
252
|
+
strength_lock: strength_lock,
|
|
253
|
+
phase: :edge_left
|
|
265
254
|
)
|
|
266
255
|
candidate = {
|
|
267
|
-
hz: hz
|
|
268
|
-
freq: hz
|
|
256
|
+
hz: PWN::SDR.hz_to_i(hz),
|
|
257
|
+
freq: PWN::SDR.hz_to_s(hz),
|
|
269
258
|
strength: strength_db,
|
|
270
|
-
|
|
259
|
+
edge: :left
|
|
271
260
|
}
|
|
272
261
|
left_candidate_signals.push(candidate)
|
|
273
262
|
hz -= step_hz
|
|
@@ -277,22 +266,23 @@ module PWN
|
|
|
277
266
|
|
|
278
267
|
# Now scan forwards to find the end of the signal
|
|
279
268
|
# The end of the signal is where the strength drops below strength_lock
|
|
280
|
-
# right_candidate_signals.clear
|
|
281
269
|
hz = original_hz
|
|
282
270
|
|
|
283
271
|
strength_db = 99.9
|
|
284
|
-
puts '
|
|
272
|
+
puts '*** Edge Detection: Locating End of Signal...'
|
|
285
273
|
while strength_db >= strength_lock
|
|
286
274
|
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
287
275
|
strength_db = measure_signal_strength(
|
|
288
276
|
gqrx_sock: gqrx_sock,
|
|
289
|
-
|
|
277
|
+
freq: hz,
|
|
278
|
+
strength_lock: strength_lock,
|
|
279
|
+
phase: :edge_right
|
|
290
280
|
)
|
|
291
281
|
candidate = {
|
|
292
|
-
hz: hz
|
|
293
|
-
freq: hz
|
|
282
|
+
hz: PWN::SDR.hz_to_i(hz),
|
|
283
|
+
freq: PWN::SDR.hz_to_s(hz),
|
|
294
284
|
strength: strength_db,
|
|
295
|
-
|
|
285
|
+
edge: :right
|
|
296
286
|
}
|
|
297
287
|
right_candidate_signals.push(candidate)
|
|
298
288
|
hz += step_hz
|
|
@@ -319,12 +309,13 @@ module PWN
|
|
|
319
309
|
timestamp_start = opts[:timestamp_start]
|
|
320
310
|
scan_log = opts[:scan_log]
|
|
321
311
|
|
|
322
|
-
signals = signals_arr.sort_by { |s| s[:freq]
|
|
312
|
+
signals = signals_arr.sort_by { |s| PWN::SDR.hz_to_i(s[:freq]) }
|
|
323
313
|
# Unique signals by frequency
|
|
324
|
-
signals.uniq! { |s| s[:freq]
|
|
314
|
+
signals.uniq! { |s| PWN::SDR.hz_to_i(s[:freq]) }
|
|
325
315
|
|
|
326
316
|
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
327
|
-
duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
|
|
317
|
+
duration_secs = Time.parse(timestamp_end).to_f - Time.parse(timestamp_start).to_f
|
|
318
|
+
|
|
328
319
|
# Convert duration seconds to hours minutes seconds
|
|
329
320
|
hours = (duration_secs / 3600).to_i
|
|
330
321
|
minutes = ((duration_secs % 3600) / 60).to_i
|
|
@@ -344,55 +335,117 @@ module PWN
|
|
|
344
335
|
JSON.pretty_generate(scan_resp)
|
|
345
336
|
)
|
|
346
337
|
|
|
338
|
+
# Append a new line at end of file to avoid readline
|
|
339
|
+
# issues requiring tput reset in terminal
|
|
340
|
+
File.write(scan_log, "\n", mode: 'a')
|
|
341
|
+
|
|
347
342
|
scan_resp
|
|
348
343
|
rescue StandardError => e
|
|
349
344
|
raise e
|
|
350
345
|
end
|
|
351
346
|
|
|
352
347
|
# Supported Method Parameters::
|
|
353
|
-
#
|
|
354
|
-
# gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
348
|
+
# best_peak = PWN::SDR::GQRX.find_best_peak(
|
|
349
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
350
|
+
# candidate_signals: 'required - Array of candidate signals from edge_detection',
|
|
351
|
+
# step_hz: 'required - Frequency step in Hz for peak finding',
|
|
352
|
+
# strength_lock: 'required - Strength lock in dBFS to determine signal edges'
|
|
355
353
|
# )
|
|
356
|
-
|
|
357
|
-
private_class_method def self.decode_rds(opts = {})
|
|
354
|
+
private_class_method def self.find_best_peak(opts = {})
|
|
358
355
|
gqrx_sock = opts[:gqrx_sock]
|
|
356
|
+
candidate_signals = opts[:candidate_signals]
|
|
357
|
+
step_hz = opts[:step_hz]
|
|
358
|
+
strength_lock = opts[:strength_lock]
|
|
359
359
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
360
|
+
beg_of_signal_hz = PWN::SDR.hz_to_i(candidate_signals.first[:hz])
|
|
361
|
+
end_of_signal_hz = PWN::SDR.hz_to_i(candidate_signals.last[:hz])
|
|
362
|
+
|
|
363
|
+
puts "*** Analyzing Best Peak in Frequency Range: #{PWN::SDR.hz_to_s(beg_of_signal_hz)} Hz - #{PWN::SDR.hz_to_s(end_of_signal_hz)} Hz"
|
|
364
|
+
# puts JSON.pretty_generate(candidate_signals)
|
|
365
|
+
|
|
366
|
+
samples = []
|
|
367
|
+
prev_best_sample = {}
|
|
368
|
+
consecutive_best = 0
|
|
369
|
+
direction_up = true
|
|
370
|
+
|
|
371
|
+
pass_count = 0
|
|
372
|
+
infinite_loop_safeguard = false
|
|
373
|
+
while true
|
|
374
|
+
pass_count += 1
|
|
375
|
+
|
|
376
|
+
# Safeguard against infinite loop
|
|
377
|
+
# infinite_loop_safeguard = true if pass_count >= 100
|
|
378
|
+
infinite_loop_safeguard = true if pass_count >= 10
|
|
379
|
+
puts 'WARNING: Infinite loop safeguard triggered in find_best_peak!' if infinite_loop_safeguard
|
|
380
|
+
break if infinite_loop_safeguard
|
|
381
|
+
|
|
382
|
+
direction_up = !direction_up
|
|
383
|
+
start_hz_direction = direction_up ? beg_of_signal_hz : end_of_signal_hz
|
|
384
|
+
end_hz_direction = direction_up ? end_of_signal_hz : beg_of_signal_hz
|
|
385
|
+
step_hz_direction = direction_up ? step_hz : -step_hz
|
|
386
|
+
|
|
387
|
+
start_hz_direction.step(by: step_hz_direction, to: end_hz_direction) do |hz|
|
|
388
|
+
print '>' if direction_up
|
|
389
|
+
print '<' unless direction_up
|
|
390
|
+
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
391
|
+
strength_db = measure_signal_strength(
|
|
392
|
+
gqrx_sock: gqrx_sock,
|
|
393
|
+
freq: hz,
|
|
394
|
+
strength_lock: strength_lock,
|
|
395
|
+
phase: :find_best_peak
|
|
396
|
+
)
|
|
397
|
+
samples.push({ hz: hz, strength_db: strength_db })
|
|
398
|
+
end
|
|
366
399
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
400
|
+
# Compute fresh averaged_samples from all cumulative samples
|
|
401
|
+
averaged_samples = []
|
|
402
|
+
samples.group_by { |s| s[:hz] }.each do |hz, grouped_samples|
|
|
403
|
+
avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size)
|
|
404
|
+
averaged_samples.push({ hz: hz, strength_db: avg_strength })
|
|
405
|
+
end
|
|
372
406
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
407
|
+
# Sort by hz for trimming
|
|
408
|
+
averaged_samples.sort_by! { |s| s[:hz] }
|
|
409
|
+
|
|
410
|
+
# Find current best for trimming threshold
|
|
411
|
+
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
412
|
+
max_strength = best_sample[:strength_db].round(1)
|
|
413
|
+
|
|
414
|
+
# trim_db_threshold should bet average difference between
|
|
415
|
+
# samples near peak, floor to nearest 0.1 dB
|
|
416
|
+
trim_db_threshold = samples.map { |s| (s[:strength_db] - max_strength).abs }.sum / samples.size
|
|
417
|
+
trim_db_threshold = (trim_db_threshold * 10).floor / 10.0
|
|
418
|
+
puts "\nPass #{pass_count}: Calculated trim_db_threshold: #{trim_db_threshold} dB"
|
|
419
|
+
# Adaptive trim: Remove weak ends (implements the comment about trimming weakest ends)
|
|
420
|
+
averaged_samples.shift while !averaged_samples.empty? && averaged_samples.first[:strength_db] < max_strength - trim_db_threshold
|
|
421
|
+
averaged_samples.pop while !averaged_samples.empty? && averaged_samples.last[:strength_db] < max_strength - trim_db_threshold
|
|
422
|
+
|
|
423
|
+
# Update range for next pass if trimmed
|
|
424
|
+
unless averaged_samples.empty?
|
|
425
|
+
beg_of_signal_hz = averaged_samples.first[:hz]
|
|
426
|
+
end_of_signal_hz = averaged_samples.last[:hz]
|
|
427
|
+
end
|
|
383
428
|
|
|
384
|
-
#
|
|
385
|
-
|
|
386
|
-
# on the current frequency (e.g. false+)
|
|
387
|
-
break if $stdin.ready? && $stdin.read_nonblock(1) == skip_rds
|
|
429
|
+
# Recalculate best_sample after trim
|
|
430
|
+
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
388
431
|
|
|
389
|
-
|
|
432
|
+
# Check for improvement
|
|
433
|
+
if best_sample[:hz] == prev_best_sample[:hz]
|
|
434
|
+
consecutive_best += 1
|
|
435
|
+
else
|
|
436
|
+
consecutive_best = 0
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Dup to avoid reference issues
|
|
440
|
+
prev_best_sample = best_sample.dup
|
|
441
|
+
|
|
442
|
+
puts "Pass #{pass_count}: Best #{PWN::SDR.hz_to_s(best_sample[:hz])} => #{best_sample[:strength_db]} dBFS, consecutive best count: #{consecutive_best}"
|
|
390
443
|
|
|
391
|
-
|
|
392
|
-
|
|
444
|
+
# Break if we have a stable best sample or only one sample remains
|
|
445
|
+
break if consecutive_best.positive? || averaged_samples.length == 1
|
|
393
446
|
end
|
|
394
|
-
|
|
395
|
-
|
|
447
|
+
|
|
448
|
+
best_sample
|
|
396
449
|
rescue StandardError => e
|
|
397
450
|
raise e
|
|
398
451
|
end
|
|
@@ -417,7 +470,7 @@ module PWN
|
|
|
417
470
|
# freq: 'required - Frequency to set',
|
|
418
471
|
# demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
419
472
|
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
420
|
-
# bandwidth: 'optional - Bandwidth (defaults to
|
|
473
|
+
# bandwidth: 'optional - Bandwidth (defaults to "200.000")',
|
|
421
474
|
# squelch: 'optional - Squelch level to set (Defaults to current value)',
|
|
422
475
|
# decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
423
476
|
# record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
@@ -447,7 +500,7 @@ module PWN
|
|
|
447
500
|
|
|
448
501
|
rds = opts[:rds] ||= false
|
|
449
502
|
|
|
450
|
-
bandwidth = opts[:bandwidth] ||=
|
|
503
|
+
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
451
504
|
squelch = opts[:squelch]
|
|
452
505
|
decoder = opts[:decoder]
|
|
453
506
|
record_dir = opts[:record_dir] ||= '/tmp'
|
|
@@ -465,7 +518,7 @@ module PWN
|
|
|
465
518
|
)
|
|
466
519
|
|
|
467
520
|
mode_str = demodulator_mode.to_s.upcase
|
|
468
|
-
passband_hz = bandwidth
|
|
521
|
+
passband_hz = PWN::SDR.hz_to_i(bandwidth)
|
|
469
522
|
gqrx_cmd(
|
|
470
523
|
gqrx_sock: gqrx_sock,
|
|
471
524
|
cmd: "M #{mode_str} #{passband_hz}",
|
|
@@ -475,15 +528,17 @@ module PWN
|
|
|
475
528
|
|
|
476
529
|
tune_to(gqrx_sock: gqrx_sock, hz: freq)
|
|
477
530
|
strength_db = measure_signal_strength(
|
|
478
|
-
gqrx_sock: gqrx_sock
|
|
531
|
+
gqrx_sock: gqrx_sock,
|
|
532
|
+
freq: freq,
|
|
533
|
+
phase: :init_freq
|
|
479
534
|
)
|
|
480
535
|
|
|
481
536
|
freq_obj = {
|
|
482
|
-
|
|
537
|
+
freq: freq,
|
|
483
538
|
demodulator_mode: demodulator_mode,
|
|
539
|
+
bandwidth: bandwidth,
|
|
484
540
|
rds: rds,
|
|
485
|
-
strength_db: strength_db
|
|
486
|
-
freq: freq
|
|
541
|
+
strength_db: strength_db
|
|
487
542
|
}
|
|
488
543
|
|
|
489
544
|
unless suppress_details
|
|
@@ -518,7 +573,7 @@ module PWN
|
|
|
518
573
|
)
|
|
519
574
|
|
|
520
575
|
rds_resp = nil
|
|
521
|
-
rds_resp =
|
|
576
|
+
rds_resp = PWN::SDR::Decoder.rds(gqrx_sock: gqrx_sock) if rds
|
|
522
577
|
|
|
523
578
|
freq_obj[:audio_gain_db] = audio_gain_db
|
|
524
579
|
freq_obj[:demod_mode_n_passband] = demod_n_passband
|
|
@@ -538,6 +593,8 @@ module PWN
|
|
|
538
593
|
case decoder
|
|
539
594
|
when :gsm
|
|
540
595
|
decoder_module = PWN::SDR::Decoder::GSM
|
|
596
|
+
when :rds
|
|
597
|
+
decoder_module = PWN::SDR::Decoder::RDS
|
|
541
598
|
else
|
|
542
599
|
raise "ERROR: Unknown decoder key: #{decoder}. Supported: :gsm"
|
|
543
600
|
end
|
|
@@ -592,7 +649,7 @@ module PWN
|
|
|
592
649
|
# target_freq: 'required - Target frequency of scan range',
|
|
593
650
|
# demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
594
651
|
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
595
|
-
# bandwidth: 'optional - Bandwidth in Hz (Defaults to
|
|
652
|
+
# bandwidth: 'optional - Bandwidth in Hz (Defaults to "200.000")',
|
|
596
653
|
# precision: 'optional - Frequency step precision (number of digits; defaults to 1)',
|
|
597
654
|
# strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
|
|
598
655
|
# squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
|
|
@@ -611,23 +668,24 @@ module PWN
|
|
|
611
668
|
gqrx_sock = opts[:gqrx_sock]
|
|
612
669
|
|
|
613
670
|
start_freq = opts[:start_freq]
|
|
614
|
-
hz_start = start_freq
|
|
671
|
+
hz_start = PWN::SDR.hz_to_i(start_freq)
|
|
615
672
|
|
|
616
673
|
target_freq = opts[:target_freq]
|
|
617
|
-
hz_target = target_freq
|
|
674
|
+
hz_target = PWN::SDR.hz_to_i(target_freq)
|
|
618
675
|
|
|
619
676
|
demodulator_mode = opts[:demodulator_mode]
|
|
620
677
|
rds = opts[:rds] ||= false
|
|
621
678
|
|
|
622
|
-
bandwidth = opts[:bandwidth] ||=
|
|
679
|
+
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
680
|
+
|
|
623
681
|
precision = opts[:precision] ||= 1
|
|
624
682
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
625
683
|
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
626
|
-
scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{hz_start
|
|
684
|
+
scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{PWN::SDR.hz_to_s(hz_start)}-#{PWN::SDR.hz_to_s(hz_target)}_#{log_timestamp}.json"
|
|
627
685
|
location = opts[:location] ||= 'United States'
|
|
628
686
|
|
|
629
687
|
step_hz = 10**(precision - 1)
|
|
630
|
-
|
|
688
|
+
step_hz_direction = hz_start > hz_target ? -step_hz : step_hz
|
|
631
689
|
|
|
632
690
|
# Set squelch once for the scan
|
|
633
691
|
change_squelch_resp = gqrx_cmd(
|
|
@@ -649,7 +707,7 @@ module PWN
|
|
|
649
707
|
|
|
650
708
|
# Set demodulator mode & passband once for the scan
|
|
651
709
|
mode_str = demodulator_mode.to_s.upcase
|
|
652
|
-
passband_hz = bandwidth
|
|
710
|
+
passband_hz = PWN::SDR.hz_to_i(bandwidth)
|
|
653
711
|
gqrx_cmd(
|
|
654
712
|
gqrx_sock: gqrx_sock,
|
|
655
713
|
cmd: "M #{mode_str} #{passband_hz}",
|
|
@@ -701,103 +759,10 @@ module PWN
|
|
|
701
759
|
|
|
702
760
|
candidate_signals = []
|
|
703
761
|
|
|
704
|
-
# Adaptive peak finder – trims weakest ends after each pass
|
|
705
|
-
# Converges quickly to the true center of the bell curve
|
|
706
|
-
find_best_peak = lambda do |opts = {}|
|
|
707
|
-
beg_of_signal_hz = opts[:beg_of_signal_hz].to_s.cast_to_raw_hz
|
|
708
|
-
end_of_signal_hz = opts[:end_of_signal_hz].to_s.cast_to_raw_hz
|
|
709
|
-
|
|
710
|
-
samples = []
|
|
711
|
-
prev_best_sample = {}
|
|
712
|
-
consecutive_best = 0
|
|
713
|
-
direction_up = true
|
|
714
|
-
|
|
715
|
-
pass_count = 0
|
|
716
|
-
infinite_loop_safeguard = false
|
|
717
|
-
while true
|
|
718
|
-
pass_count += 1
|
|
719
|
-
|
|
720
|
-
# Safeguard against infinite loop
|
|
721
|
-
infinite_loop_safeguard = true if pass_count >= 100
|
|
722
|
-
puts 'WARNING: Infinite loop safeguard triggered in find_best_peak!' if infinite_loop_safeguard
|
|
723
|
-
break if infinite_loop_safeguard
|
|
724
|
-
|
|
725
|
-
direction_up = !direction_up
|
|
726
|
-
start_hz_direction = direction_up ? beg_of_signal_hz : end_of_signal_hz
|
|
727
|
-
end_hz_direction = direction_up ? end_of_signal_hz : beg_of_signal_hz
|
|
728
|
-
step_hz_direction = direction_up ? step_hz : -step_hz
|
|
729
|
-
|
|
730
|
-
start_hz_direction.step(by: step_hz_direction, to: end_hz_direction) do |hz|
|
|
731
|
-
print '>' if direction_up
|
|
732
|
-
print '<' unless direction_up
|
|
733
|
-
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
734
|
-
strength_db = measure_signal_strength(
|
|
735
|
-
gqrx_sock: gqrx_sock,
|
|
736
|
-
strength_lock: strength_lock
|
|
737
|
-
)
|
|
738
|
-
samples.push({ hz: hz, strength_db: strength_db })
|
|
739
|
-
|
|
740
|
-
# current_hz = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f').to_s.cast_to_raw_hz
|
|
741
|
-
# puts "Sampled Frequency: #{current_hz.to_i.cast_to_pretty_hz} => Strength: #{strength_db} dBFS"
|
|
742
|
-
end
|
|
743
|
-
|
|
744
|
-
# Compute fresh averaged_samples from all cumulative samples
|
|
745
|
-
averaged_samples = []
|
|
746
|
-
samples.group_by { |s| s[:hz] }.each do |hz, grouped_samples|
|
|
747
|
-
avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size)
|
|
748
|
-
# avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size).round(2)
|
|
749
|
-
# avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size).round(1)
|
|
750
|
-
averaged_samples.push({ hz: hz, strength_db: avg_strength })
|
|
751
|
-
end
|
|
752
|
-
|
|
753
|
-
# Sort by hz for trimming
|
|
754
|
-
averaged_samples.sort_by! { |s| s[:hz] }
|
|
755
|
-
|
|
756
|
-
# Find current best for trimming threshold
|
|
757
|
-
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
758
|
-
max_strength = best_sample[:strength_db]
|
|
759
|
-
|
|
760
|
-
# trim_db_threshold should bet average difference between
|
|
761
|
-
# samples near peak, floor to nearest 0.1 dB
|
|
762
|
-
trim_db_threshold = samples.map { |s| (s[:strength_db] - max_strength).abs }.sum / samples.size
|
|
763
|
-
trim_db_threshold = (trim_db_threshold * 10).floor / 10.0
|
|
764
|
-
puts "\nPass #{pass_count}: Calculated trim_db_threshold: #{trim_db_threshold} dB"
|
|
765
|
-
# Adaptive trim: Remove weak ends (implements the comment about trimming weakest ends)
|
|
766
|
-
averaged_samples.shift while !averaged_samples.empty? && averaged_samples.first[:strength_db] < max_strength - trim_db_threshold
|
|
767
|
-
averaged_samples.pop while !averaged_samples.empty? && averaged_samples.last[:strength_db] < max_strength - trim_db_threshold
|
|
768
|
-
|
|
769
|
-
# Update range for next pass if trimmed
|
|
770
|
-
unless averaged_samples.empty?
|
|
771
|
-
beg_of_signal_hz = averaged_samples.first[:hz]
|
|
772
|
-
end_of_signal_hz = averaged_samples.last[:hz]
|
|
773
|
-
end
|
|
774
|
-
|
|
775
|
-
# Recalculate best_sample after trim
|
|
776
|
-
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
777
|
-
|
|
778
|
-
# Check for improvement
|
|
779
|
-
if best_sample[:hz] == prev_best_sample[:hz]
|
|
780
|
-
consecutive_best += 1
|
|
781
|
-
else
|
|
782
|
-
consecutive_best = 0
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
# Dup to avoid reference issues
|
|
786
|
-
prev_best_sample = best_sample.dup
|
|
787
|
-
|
|
788
|
-
puts "Pass #{pass_count}: Best #{best_sample[:hz].to_i.cast_to_pretty_hz} => #{best_sample[:strength_db]} dBFS, consecutive best count: #{consecutive_best}"
|
|
789
|
-
|
|
790
|
-
# Break if no improvement in 3 consecutive passes or theres only one sample left
|
|
791
|
-
break if consecutive_best.positive? || averaged_samples.size == 1
|
|
792
|
-
end
|
|
793
|
-
|
|
794
|
-
best_sample
|
|
795
|
-
end
|
|
796
|
-
|
|
797
762
|
# Begin scanning range
|
|
798
763
|
puts "\n"
|
|
799
764
|
puts '-' * 86
|
|
800
|
-
puts "INFO: Scanning from #{hz_start
|
|
765
|
+
puts "INFO: Scanning from #{PWN::SDR.hz_to_s(hz_start)} to #{PWN::SDR.hz_to_s(hz_target)} in steps of #{PWN::SDR.hz_to_s(step_hz_direction.abs)} Hz."
|
|
801
766
|
puts "If scans are slow and/or you're experiencing false positives/negatives,"
|
|
802
767
|
puts 'consider adjusting the following:'
|
|
803
768
|
puts "1. The SDR's sample rate in GQRX"
|
|
@@ -812,11 +777,14 @@ module PWN
|
|
|
812
777
|
|
|
813
778
|
signals_arr = []
|
|
814
779
|
hz = hz_start
|
|
815
|
-
|
|
780
|
+
|
|
781
|
+
while step_hz_direction.positive? ? hz <= hz_target : hz >= hz_target
|
|
816
782
|
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
817
783
|
strength_db = measure_signal_strength(
|
|
818
784
|
gqrx_sock: gqrx_sock,
|
|
819
|
-
|
|
785
|
+
freq: hz,
|
|
786
|
+
strength_lock: strength_lock,
|
|
787
|
+
phase: :find_candidates
|
|
820
788
|
)
|
|
821
789
|
|
|
822
790
|
if strength_db >= strength_lock
|
|
@@ -829,28 +797,16 @@ module PWN
|
|
|
829
797
|
strength_lock: strength_lock
|
|
830
798
|
)
|
|
831
799
|
elsif candidate_signals.length.positive?
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
puts JSON.pretty_generate(candidate_signals)
|
|
838
|
-
|
|
839
|
-
prev_freq = prev_freq_obj[:freq].to_s.cast_to_raw_hz
|
|
840
|
-
distance_from_prev_detected_freq_hz = (beg_of_signal_hz - prev_freq).abs
|
|
841
|
-
half_bandwidth = (bandwidth / 2).to_i
|
|
842
|
-
|
|
843
|
-
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"
|
|
844
|
-
|
|
845
|
-
puts 'Finding Best Peak...'
|
|
846
|
-
best_peak = find_best_peak.call(
|
|
847
|
-
beg_of_signal_hz: beg_of_signal_hz,
|
|
848
|
-
end_of_signal_hz: end_of_signal_hz
|
|
800
|
+
best_peak = find_best_peak(
|
|
801
|
+
gqrx_sock: gqrx_sock,
|
|
802
|
+
candidate_signals: candidate_signals,
|
|
803
|
+
step_hz: step_hz,
|
|
804
|
+
strength_lock: strength_lock
|
|
849
805
|
)
|
|
850
806
|
|
|
851
807
|
if best_peak[:hz] && best_peak[:strength_db] > strength_lock
|
|
852
808
|
puts "\n**** Detected Signal ****"
|
|
853
|
-
best_freq = best_peak[:hz]
|
|
809
|
+
best_freq = PWN::SDR.hz_to_s(best_peak[:hz])
|
|
854
810
|
best_strength_db = best_peak[:strength_db]
|
|
855
811
|
prev_freq_obj = init_freq(
|
|
856
812
|
gqrx_sock: gqrx_sock,
|
|
@@ -879,12 +835,12 @@ module PWN
|
|
|
879
835
|
timestamp_start: timestamp_start,
|
|
880
836
|
scan_log: scan_log
|
|
881
837
|
)
|
|
882
|
-
hz =
|
|
838
|
+
hz = candidate_signals.last[:hz]
|
|
883
839
|
# gets
|
|
884
840
|
end
|
|
885
841
|
candidate_signals.clear
|
|
886
842
|
end
|
|
887
|
-
hz +=
|
|
843
|
+
hz += step_hz_direction
|
|
888
844
|
end
|
|
889
845
|
|
|
890
846
|
log_signals(
|
|
@@ -997,7 +953,7 @@ module PWN
|
|
|
997
953
|
freq: 'required - Frequency to set',
|
|
998
954
|
demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
999
955
|
rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
1000
|
-
bandwidth: 'optional - Bandwidth (defaults to
|
|
956
|
+
bandwidth: 'optional - Bandwidth (defaults to \"200.000\")',
|
|
1001
957
|
decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
1002
958
|
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
1003
959
|
suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
|
|
@@ -1009,7 +965,7 @@ module PWN
|
|
|
1009
965
|
start_freq: 'required - Starting frequency',
|
|
1010
966
|
target_freq: 'required - Target frequency',
|
|
1011
967
|
demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
1012
|
-
bandwidth: 'optional - Bandwidth in Hz (Defaults to
|
|
968
|
+
bandwidth: 'optional - Bandwidth in Hz (Defaults to \"200.000\")',
|
|
1013
969
|
precision: 'optional - Precision (Defaults to 1)',
|
|
1014
970
|
strength_lock: 'optional - Strength lock (defaults to -70.0)',
|
|
1015
971
|
squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
|