pwn 0.5.514 → 0.5.516
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/frequency_allocation.rb +81 -81
- data/lib/pwn/sdr/gqrx.rb +238 -191
- data/lib/pwn/version.rb +1 -1
- data/third_party/pwn_rdoc.jsonl +8 -4
- metadata +3 -3
data/lib/pwn/sdr/gqrx.rb
CHANGED
|
@@ -16,10 +16,10 @@ module PWN
|
|
|
16
16
|
|
|
17
17
|
Integer.class_eval do
|
|
18
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
|
|
19
|
+
# So 002_450_000_000 becomes 2.450.000.000 = 2.45 GHz
|
|
20
|
+
# So 2_450_000_000 becomes 2.450.000.000 = 2.45 GHz
|
|
21
|
+
# So 960_000_000 becomes 960.000.000 = 960 MHz
|
|
22
|
+
# 1000 should be 1.000 = 1 kHz
|
|
23
23
|
def cast_to_pretty_hz
|
|
24
24
|
str_hz = to_s
|
|
25
25
|
# Nuke leading zeros
|
|
@@ -141,11 +141,7 @@ module PWN
|
|
|
141
141
|
response.push(gqrx_sock.readline.chomp)
|
|
142
142
|
# Drain any additional lines quickly
|
|
143
143
|
loop do
|
|
144
|
-
|
|
145
|
-
# We're trading speed for accuracy here.
|
|
146
|
-
# break if gqrx_sock.wait_readable(0.0625).nil? && cmd == 'l STRENGTH'
|
|
147
|
-
break if gqrx_sock.wait_readable(0.04).nil? && cmd == 'l STRENGTH'
|
|
148
|
-
break if gqrx_sock.wait_readable(0.001).nil? && cmd != 'l STRENGTH'
|
|
144
|
+
break if gqrx_sock.wait_readable(0.0001).nil?
|
|
149
145
|
|
|
150
146
|
response.push(gqrx_sock.readline.chomp)
|
|
151
147
|
end
|
|
@@ -162,37 +158,92 @@ module PWN
|
|
|
162
158
|
|
|
163
159
|
response_str
|
|
164
160
|
rescue RuntimeError => e
|
|
165
|
-
|
|
166
|
-
puts 'WARNING: Intermediate Gain is not supported by the radio backend.' if e.message.include?('Command: L IF_GAIN')
|
|
167
|
-
puts 'WARNING: Baseband Gain is not supported by the radio backend.' if e.message.include?('Command: L BB_GAIN')
|
|
161
|
+
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')
|
|
168
162
|
|
|
169
|
-
raise e unless e.message.include?('
|
|
170
|
-
e.message.include?('
|
|
171
|
-
e.message.include?('
|
|
163
|
+
raise e unless e.message.include?('RF_GAIN') ||
|
|
164
|
+
e.message.include?('IF_GAIN') ||
|
|
165
|
+
e.message.include?('BB_GAIN')
|
|
172
166
|
rescue StandardError => e
|
|
173
167
|
raise e
|
|
174
168
|
end
|
|
175
169
|
|
|
176
170
|
# Supported Method Parameters::
|
|
177
171
|
# strength_db = PWN::SDR::GQRX.measure_signal_strength(
|
|
178
|
-
# gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
172
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
173
|
+
# freq: 'required - Frequency to measure signal strength',
|
|
174
|
+
# strength_lock: 'optional - Strength lock in dBFS to determine signal edges (defaults to -70.0)',
|
|
175
|
+
# phase: 'optional - Phase of measurement for logging purposes (defaults to :find_candidates)'
|
|
179
176
|
# )
|
|
180
177
|
private_class_method def self.measure_signal_strength(opts = {})
|
|
181
178
|
gqrx_sock = opts[:gqrx_sock]
|
|
179
|
+
freq = opts[:freq].to_s.cast_to_raw_hz
|
|
180
|
+
freq = freq.to_i.cast_to_pretty_hz
|
|
181
|
+
|
|
182
|
+
strength_lock = opts[:strength_lock] ||= -70.0
|
|
183
|
+
phase = opts[:phase] ||= :find_candidates
|
|
182
184
|
|
|
185
|
+
attempts = 0
|
|
183
186
|
strength_db = -99.9
|
|
184
|
-
prev_strength_db =
|
|
185
|
-
|
|
187
|
+
prev_strength_db = -99.9
|
|
188
|
+
distance_between_unique_samples = 0.0
|
|
189
|
+
samples = []
|
|
190
|
+
unique_samples = []
|
|
191
|
+
strength_measured = false
|
|
186
192
|
loop do
|
|
193
|
+
attempts += 1
|
|
187
194
|
strength_db = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
188
|
-
print '$'
|
|
189
|
-
break if strength_db <= prev_strength_db
|
|
190
195
|
|
|
191
|
-
|
|
196
|
+
# Fast approach while still maintaining decent accuracy
|
|
197
|
+
samples.push(strength_db)
|
|
198
|
+
unique_samples = samples.uniq
|
|
199
|
+
if unique_samples.length > 1
|
|
200
|
+
prev_strength_db = unique_samples[-2]
|
|
201
|
+
distance_between_unique_samples = (strength_db - prev_strength_db).abs
|
|
202
|
+
strength_measured = true if distance_between_unique_samples.positive? && strength_lock > strength_db
|
|
203
|
+
end
|
|
204
|
+
strength_measured = true if distance_between_unique_samples.positive? && distance_between_unique_samples < 5
|
|
205
|
+
|
|
206
|
+
break if strength_measured
|
|
207
|
+
|
|
208
|
+
# Sleep a tiny bit to allow strength_db values to fluctuate
|
|
192
209
|
sleep 0.0001
|
|
193
210
|
end
|
|
211
|
+
# Uncomment for debugging strength measurement attempts
|
|
212
|
+
# which translates to speed and accuracy refinement
|
|
213
|
+
puts "\tStrength Measurement Attempts: #{attempts} | Freq: #{freq} | Phase: #{phase} | Unique Samples: #{unique_samples} | dbFS Distance Unique Samples: #{distance_between_unique_samples}"
|
|
194
214
|
|
|
195
|
-
strength_db
|
|
215
|
+
strength_db.round(1)
|
|
216
|
+
rescue StandardError => e
|
|
217
|
+
raise e
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Supported Method Parameters::
|
|
221
|
+
# tune_resp = PWN::SDR::GQRX.tune_to(
|
|
222
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
223
|
+
# hz: 'required - Frequency to tune to'
|
|
224
|
+
# )
|
|
225
|
+
private_class_method def self.tune_to(opts = {})
|
|
226
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
227
|
+
hz = opts[:hz].to_s.cast_to_raw_hz
|
|
228
|
+
|
|
229
|
+
current_freq = 0
|
|
230
|
+
attempts = 0
|
|
231
|
+
loop do
|
|
232
|
+
attempts += 1
|
|
233
|
+
gqrx_cmd(
|
|
234
|
+
gqrx_sock: gqrx_sock,
|
|
235
|
+
cmd: "F #{hz}",
|
|
236
|
+
resp_ok: 'RPRT 0'
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
current_freq = gqrx_cmd(
|
|
240
|
+
gqrx_sock: gqrx_sock,
|
|
241
|
+
cmd: 'f'
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
break if current_freq.to_s.cast_to_raw_hz == hz
|
|
245
|
+
end
|
|
246
|
+
# puts "Tuned to #{current_freq} in #{attempts} attempt(s)."
|
|
196
247
|
rescue StandardError => e
|
|
197
248
|
raise e
|
|
198
249
|
end
|
|
@@ -213,25 +264,22 @@ module PWN
|
|
|
213
264
|
right_candidate_signals = []
|
|
214
265
|
candidate_signals = []
|
|
215
266
|
|
|
216
|
-
# left_candidate_signals.clear
|
|
217
267
|
original_hz = hz
|
|
218
268
|
strength_db = 99.9
|
|
219
|
-
puts '
|
|
269
|
+
puts '*** Edge Detection: Locating Beginning of Signal...'
|
|
220
270
|
while strength_db >= strength_lock
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
end
|
|
229
|
-
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
271
|
+
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
272
|
+
strength_db = measure_signal_strength(
|
|
273
|
+
gqrx_sock: gqrx_sock,
|
|
274
|
+
freq: hz,
|
|
275
|
+
strength_lock: strength_lock,
|
|
276
|
+
phase: :edge_left
|
|
277
|
+
)
|
|
230
278
|
candidate = {
|
|
231
279
|
hz: hz.to_s.cast_to_raw_hz,
|
|
232
280
|
freq: hz.to_i.cast_to_pretty_hz,
|
|
233
281
|
strength: strength_db,
|
|
234
|
-
|
|
282
|
+
edge: :left
|
|
235
283
|
}
|
|
236
284
|
left_candidate_signals.push(candidate)
|
|
237
285
|
hz -= step_hz
|
|
@@ -241,26 +289,23 @@ module PWN
|
|
|
241
289
|
|
|
242
290
|
# Now scan forwards to find the end of the signal
|
|
243
291
|
# The end of the signal is where the strength drops below strength_lock
|
|
244
|
-
# right_candidate_signals.clear
|
|
245
292
|
hz = original_hz
|
|
246
293
|
|
|
247
294
|
strength_db = 99.9
|
|
248
|
-
puts '
|
|
295
|
+
puts '*** Edge Detection: Locating End of Signal...'
|
|
249
296
|
while strength_db >= strength_lock
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
end
|
|
258
|
-
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
297
|
+
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
298
|
+
strength_db = measure_signal_strength(
|
|
299
|
+
gqrx_sock: gqrx_sock,
|
|
300
|
+
freq: hz,
|
|
301
|
+
strength_lock: strength_lock,
|
|
302
|
+
phase: :edge_right
|
|
303
|
+
)
|
|
259
304
|
candidate = {
|
|
260
305
|
hz: hz.to_s.cast_to_raw_hz,
|
|
261
306
|
freq: hz.to_i.cast_to_pretty_hz,
|
|
262
307
|
strength: strength_db,
|
|
263
|
-
|
|
308
|
+
edge: :right
|
|
264
309
|
}
|
|
265
310
|
right_candidate_signals.push(candidate)
|
|
266
311
|
hz += step_hz
|
|
@@ -292,7 +337,8 @@ module PWN
|
|
|
292
337
|
signals.uniq! { |s| s[:freq].to_s.cast_to_raw_hz }
|
|
293
338
|
|
|
294
339
|
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
295
|
-
duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
|
|
340
|
+
duration_secs = Time.parse(timestamp_end).to_f - Time.parse(timestamp_start).to_f
|
|
341
|
+
|
|
296
342
|
# Convert duration seconds to hours minutes seconds
|
|
297
343
|
hours = (duration_secs / 3600).to_i
|
|
298
344
|
minutes = ((duration_secs % 3600) / 60).to_i
|
|
@@ -312,6 +358,10 @@ module PWN
|
|
|
312
358
|
JSON.pretty_generate(scan_resp)
|
|
313
359
|
)
|
|
314
360
|
|
|
361
|
+
# Append a new line at end of file to avoid readline
|
|
362
|
+
# issues requiring tput reset in terminal
|
|
363
|
+
File.write(scan_log, "\n", mode: 'a')
|
|
364
|
+
|
|
315
365
|
scan_resp
|
|
316
366
|
rescue StandardError => e
|
|
317
367
|
raise e
|
|
@@ -340,7 +390,7 @@ module PWN
|
|
|
340
390
|
|
|
341
391
|
rds_resp = {}
|
|
342
392
|
attempts = 0
|
|
343
|
-
max_attempts =
|
|
393
|
+
max_attempts = 120
|
|
344
394
|
skip_rds = "\n"
|
|
345
395
|
print 'INFO: Decoding FM radio RDS data (Press ENTER to skip)...'
|
|
346
396
|
max_attempts.times do
|
|
@@ -365,6 +415,115 @@ module PWN
|
|
|
365
415
|
raise e
|
|
366
416
|
end
|
|
367
417
|
|
|
418
|
+
# Supported Method Parameters::
|
|
419
|
+
# best_peak = PWN::SDR::GQRX.find_best_peak(
|
|
420
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
421
|
+
# candidate_signals: 'required - Array of candidate signals from edge_detection',
|
|
422
|
+
# step_hz: 'required - Frequency step in Hz for peak finding',
|
|
423
|
+
# strength_lock: 'required - Strength lock in dBFS to determine signal edges'
|
|
424
|
+
# )
|
|
425
|
+
private_class_method def self.find_best_peak(opts = {})
|
|
426
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
427
|
+
candidate_signals = opts[:candidate_signals]
|
|
428
|
+
step_hz = opts[:step_hz]
|
|
429
|
+
strength_lock = opts[:strength_lock]
|
|
430
|
+
|
|
431
|
+
beg_of_signal_hz = candidate_signals.first[:hz].to_s.cast_to_raw_hz
|
|
432
|
+
end_of_signal_hz = candidate_signals.last[:hz].to_s.cast_to_raw_hz
|
|
433
|
+
|
|
434
|
+
puts "*** Analyzing Best Peak in Frequency Range: #{beg_of_signal_hz.to_i.cast_to_pretty_hz} Hz - #{end_of_signal_hz.to_i.cast_to_pretty_hz} Hz"
|
|
435
|
+
# puts JSON.pretty_generate(candidate_signals)
|
|
436
|
+
|
|
437
|
+
samples = []
|
|
438
|
+
prev_best_sample = {}
|
|
439
|
+
consecutive_best = 0
|
|
440
|
+
direction_up = true
|
|
441
|
+
|
|
442
|
+
pass_count = 0
|
|
443
|
+
infinite_loop_safeguard = false
|
|
444
|
+
while true
|
|
445
|
+
pass_count += 1
|
|
446
|
+
|
|
447
|
+
# Safeguard against infinite loop
|
|
448
|
+
# infinite_loop_safeguard = true if pass_count >= 100
|
|
449
|
+
infinite_loop_safeguard = true if pass_count >= 10
|
|
450
|
+
puts 'WARNING: Infinite loop safeguard triggered in find_best_peak!' if infinite_loop_safeguard
|
|
451
|
+
break if infinite_loop_safeguard
|
|
452
|
+
|
|
453
|
+
direction_up = !direction_up
|
|
454
|
+
start_hz_direction = direction_up ? beg_of_signal_hz : end_of_signal_hz
|
|
455
|
+
end_hz_direction = direction_up ? end_of_signal_hz : beg_of_signal_hz
|
|
456
|
+
step_hz_direction = direction_up ? step_hz : -step_hz
|
|
457
|
+
|
|
458
|
+
start_hz_direction.step(by: step_hz_direction, to: end_hz_direction) do |hz|
|
|
459
|
+
print '>' if direction_up
|
|
460
|
+
print '<' unless direction_up
|
|
461
|
+
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
462
|
+
strength_db = measure_signal_strength(
|
|
463
|
+
gqrx_sock: gqrx_sock,
|
|
464
|
+
freq: hz,
|
|
465
|
+
strength_lock: strength_lock,
|
|
466
|
+
phase: :find_best_peak
|
|
467
|
+
)
|
|
468
|
+
samples.push({ hz: hz, strength_db: strength_db })
|
|
469
|
+
|
|
470
|
+
# current_hz = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f').to_s.cast_to_raw_hz
|
|
471
|
+
# puts "Sampled Frequency: #{current_hz.to_i.cast_to_pretty_hz} => Strength: #{strength_db} dBFS"
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Compute fresh averaged_samples from all cumulative samples
|
|
475
|
+
averaged_samples = []
|
|
476
|
+
samples.group_by { |s| s[:hz] }.each do |hz, grouped_samples|
|
|
477
|
+
avg_strength = (grouped_samples.map { |s| s[:strength_db] }.sum / grouped_samples.size)
|
|
478
|
+
averaged_samples.push({ hz: hz, strength_db: avg_strength })
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Sort by hz for trimming
|
|
482
|
+
averaged_samples.sort_by! { |s| s[:hz] }
|
|
483
|
+
|
|
484
|
+
# Find current best for trimming threshold
|
|
485
|
+
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
486
|
+
max_strength = best_sample[:strength_db].round(1)
|
|
487
|
+
|
|
488
|
+
# trim_db_threshold should bet average difference between
|
|
489
|
+
# samples near peak, floor to nearest 0.1 dB
|
|
490
|
+
trim_db_threshold = samples.map { |s| (s[:strength_db] - max_strength).abs }.sum / samples.size
|
|
491
|
+
trim_db_threshold = (trim_db_threshold * 10).floor / 10.0
|
|
492
|
+
puts "\nPass #{pass_count}: Calculated trim_db_threshold: #{trim_db_threshold} dB"
|
|
493
|
+
# Adaptive trim: Remove weak ends (implements the comment about trimming weakest ends)
|
|
494
|
+
averaged_samples.shift while !averaged_samples.empty? && averaged_samples.first[:strength_db] < max_strength - trim_db_threshold
|
|
495
|
+
averaged_samples.pop while !averaged_samples.empty? && averaged_samples.last[:strength_db] < max_strength - trim_db_threshold
|
|
496
|
+
|
|
497
|
+
# Update range for next pass if trimmed
|
|
498
|
+
unless averaged_samples.empty?
|
|
499
|
+
beg_of_signal_hz = averaged_samples.first[:hz]
|
|
500
|
+
end_of_signal_hz = averaged_samples.last[:hz]
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Recalculate best_sample after trim
|
|
504
|
+
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
505
|
+
|
|
506
|
+
# Check for improvement
|
|
507
|
+
if best_sample[:hz] == prev_best_sample[:hz]
|
|
508
|
+
consecutive_best += 1
|
|
509
|
+
else
|
|
510
|
+
consecutive_best = 0
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Dup to avoid reference issues
|
|
514
|
+
prev_best_sample = best_sample.dup
|
|
515
|
+
|
|
516
|
+
puts "Pass #{pass_count}: Best #{best_sample[:hz].to_i.cast_to_pretty_hz} => #{best_sample[:strength_db]} dBFS, consecutive best count: #{consecutive_best}"
|
|
517
|
+
|
|
518
|
+
# Break if we have a stable best sample or only one sample remains
|
|
519
|
+
break if consecutive_best.positive? || averaged_samples.length == 1
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
best_sample
|
|
523
|
+
rescue StandardError => e
|
|
524
|
+
raise e
|
|
525
|
+
end
|
|
526
|
+
|
|
368
527
|
# Supported Method Parameters::
|
|
369
528
|
# gqrx_sock = PWN::SDR::GQRX.connect(
|
|
370
529
|
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
@@ -385,7 +544,7 @@ module PWN
|
|
|
385
544
|
# freq: 'required - Frequency to set',
|
|
386
545
|
# demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
387
546
|
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
388
|
-
# bandwidth: 'optional - Bandwidth (defaults to
|
|
547
|
+
# bandwidth: 'optional - Bandwidth (defaults to "200.000")',
|
|
389
548
|
# squelch: 'optional - Squelch level to set (Defaults to current value)',
|
|
390
549
|
# decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
391
550
|
# record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
@@ -415,7 +574,7 @@ module PWN
|
|
|
415
574
|
|
|
416
575
|
rds = opts[:rds] ||= false
|
|
417
576
|
|
|
418
|
-
bandwidth = opts[:bandwidth] ||=
|
|
577
|
+
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
419
578
|
squelch = opts[:squelch]
|
|
420
579
|
decoder = opts[:decoder]
|
|
421
580
|
record_dir = opts[:record_dir] ||= '/tmp'
|
|
@@ -441,25 +600,19 @@ module PWN
|
|
|
441
600
|
)
|
|
442
601
|
end
|
|
443
602
|
|
|
444
|
-
|
|
603
|
+
tune_to(gqrx_sock: gqrx_sock, hz: freq)
|
|
604
|
+
strength_db = measure_signal_strength(
|
|
445
605
|
gqrx_sock: gqrx_sock,
|
|
446
|
-
|
|
447
|
-
|
|
606
|
+
freq: freq,
|
|
607
|
+
phase: :init_freq
|
|
448
608
|
)
|
|
449
609
|
|
|
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
|
|
457
|
-
|
|
458
610
|
freq_obj = {
|
|
459
|
-
|
|
611
|
+
freq: freq,
|
|
460
612
|
demodulator_mode: demodulator_mode,
|
|
613
|
+
bandwidth: bandwidth,
|
|
461
614
|
rds: rds,
|
|
462
|
-
|
|
615
|
+
strength_db: strength_db
|
|
463
616
|
}
|
|
464
617
|
|
|
465
618
|
unless suppress_details
|
|
@@ -473,8 +626,6 @@ module PWN
|
|
|
473
626
|
cmd: 'l AF'
|
|
474
627
|
).to_f
|
|
475
628
|
|
|
476
|
-
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
477
|
-
|
|
478
629
|
squelch = gqrx_cmd(
|
|
479
630
|
gqrx_sock: gqrx_sock,
|
|
480
631
|
cmd: 'l SQL'
|
|
@@ -504,7 +655,6 @@ module PWN
|
|
|
504
655
|
freq_obj[:if_gain] = if_gain
|
|
505
656
|
freq_obj[:rf_gain] = rf_gain
|
|
506
657
|
freq_obj[:squelch] = squelch
|
|
507
|
-
freq_obj[:strength_db] = strength_db
|
|
508
658
|
freq_obj[:rds] = rds_resp
|
|
509
659
|
end
|
|
510
660
|
|
|
@@ -571,7 +721,7 @@ module PWN
|
|
|
571
721
|
# target_freq: 'required - Target frequency of scan range',
|
|
572
722
|
# demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
573
723
|
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
574
|
-
# bandwidth: 'optional - Bandwidth in Hz (Defaults to
|
|
724
|
+
# bandwidth: 'optional - Bandwidth in Hz (Defaults to "200.000")',
|
|
575
725
|
# precision: 'optional - Frequency step precision (number of digits; defaults to 1)',
|
|
576
726
|
# strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
|
|
577
727
|
# squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
|
|
@@ -598,7 +748,8 @@ module PWN
|
|
|
598
748
|
demodulator_mode = opts[:demodulator_mode]
|
|
599
749
|
rds = opts[:rds] ||= false
|
|
600
750
|
|
|
601
|
-
bandwidth = opts[:bandwidth] ||=
|
|
751
|
+
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
752
|
+
|
|
602
753
|
precision = opts[:precision] ||= 1
|
|
603
754
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
604
755
|
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
@@ -606,7 +757,7 @@ module PWN
|
|
|
606
757
|
location = opts[:location] ||= 'United States'
|
|
607
758
|
|
|
608
759
|
step_hz = 10**(precision - 1)
|
|
609
|
-
|
|
760
|
+
step_hz_direction = hz_start > hz_target ? -step_hz : step_hz
|
|
610
761
|
|
|
611
762
|
# Set squelch once for the scan
|
|
612
763
|
change_squelch_resp = gqrx_cmd(
|
|
@@ -680,100 +831,10 @@ module PWN
|
|
|
680
831
|
|
|
681
832
|
candidate_signals = []
|
|
682
833
|
|
|
683
|
-
# Adaptive peak finder – trims weakest ends after each pass
|
|
684
|
-
# Converges quickly to the true center of the bell curve
|
|
685
|
-
find_best_peak = lambda do |opts = {}|
|
|
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
|
|
688
|
-
|
|
689
|
-
samples = []
|
|
690
|
-
prev_best_sample = {}
|
|
691
|
-
consecutive_best = 0
|
|
692
|
-
direction_up = true
|
|
693
|
-
|
|
694
|
-
pass_count = 0
|
|
695
|
-
infinite_loop_safeguard = false
|
|
696
|
-
while true
|
|
697
|
-
pass_count += 1
|
|
698
|
-
|
|
699
|
-
# Safeguard against infinite loop
|
|
700
|
-
infinite_loop_safeguard = true if pass_count >= 100
|
|
701
|
-
puts 'WARNING: Infinite loop safeguard triggered in find_best_peak!' if infinite_loop_safeguard
|
|
702
|
-
break if infinite_loop_safeguard
|
|
703
|
-
|
|
704
|
-
direction_up = !direction_up
|
|
705
|
-
start_hz_direction = direction_up ? beg_of_signal_hz : end_of_signal_hz
|
|
706
|
-
end_hz_direction = direction_up ? end_of_signal_hz : beg_of_signal_hz
|
|
707
|
-
step_hz_direction = direction_up ? step_hz : -step_hz
|
|
708
|
-
|
|
709
|
-
start_hz_direction.step(by: step_hz_direction, to: end_hz_direction) do |hz|
|
|
710
|
-
print '>' if direction_up
|
|
711
|
-
print '<' unless direction_up
|
|
712
|
-
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
713
|
-
strength_db = measure_signal_strength(gqrx_sock: gqrx_sock)
|
|
714
|
-
samples.push({ hz: hz, strength_db: strength_db })
|
|
715
|
-
|
|
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"
|
|
718
|
-
end
|
|
719
|
-
|
|
720
|
-
# Compute fresh averaged_samples from all cumulative samples
|
|
721
|
-
averaged_samples = []
|
|
722
|
-
samples.group_by { |s| s[:hz] }.each do |hz, grouped_samples|
|
|
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)
|
|
726
|
-
averaged_samples.push({ hz: hz, strength_db: avg_strength })
|
|
727
|
-
end
|
|
728
|
-
|
|
729
|
-
# Sort by hz for trimming
|
|
730
|
-
averaged_samples.sort_by! { |s| s[:hz] }
|
|
731
|
-
|
|
732
|
-
# Find current best for trimming threshold
|
|
733
|
-
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
734
|
-
max_strength = best_sample[:strength_db]
|
|
735
|
-
|
|
736
|
-
# trim_db_threshold should bet average difference between
|
|
737
|
-
# samples near peak, floor to nearest 0.1 dB
|
|
738
|
-
trim_db_threshold = samples.map { |s| (s[:strength_db] - max_strength).abs }.sum / samples.size
|
|
739
|
-
trim_db_threshold = (trim_db_threshold * 10).floor / 10.0
|
|
740
|
-
puts "\nPass #{pass_count}: Calculated trim_db_threshold: #{trim_db_threshold} dB"
|
|
741
|
-
# Adaptive trim: Remove weak ends (implements the comment about trimming weakest ends)
|
|
742
|
-
averaged_samples.shift while !averaged_samples.empty? && averaged_samples.first[:strength_db] < max_strength - trim_db_threshold
|
|
743
|
-
averaged_samples.pop while !averaged_samples.empty? && averaged_samples.last[:strength_db] < max_strength - trim_db_threshold
|
|
744
|
-
|
|
745
|
-
# Update range for next pass if trimmed
|
|
746
|
-
unless averaged_samples.empty?
|
|
747
|
-
beg_of_signal_hz = averaged_samples.first[:hz]
|
|
748
|
-
end_of_signal_hz = averaged_samples.last[:hz]
|
|
749
|
-
end
|
|
750
|
-
|
|
751
|
-
# Recalculate best_sample after trim
|
|
752
|
-
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
753
|
-
|
|
754
|
-
# Check for improvement
|
|
755
|
-
if best_sample[:hz] == prev_best_sample[:hz]
|
|
756
|
-
consecutive_best += 1
|
|
757
|
-
else
|
|
758
|
-
consecutive_best = 0
|
|
759
|
-
end
|
|
760
|
-
|
|
761
|
-
# Dup to avoid reference issues
|
|
762
|
-
prev_best_sample = best_sample.dup
|
|
763
|
-
|
|
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}"
|
|
765
|
-
|
|
766
|
-
# Break if no improvement in 3 consecutive passes or theres only one sample left
|
|
767
|
-
break if consecutive_best.positive? || averaged_samples.size == 1
|
|
768
|
-
end
|
|
769
|
-
|
|
770
|
-
best_sample
|
|
771
|
-
end
|
|
772
|
-
|
|
773
834
|
# Begin scanning range
|
|
774
835
|
puts "\n"
|
|
775
836
|
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 #{
|
|
837
|
+
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_hz_direction.abs.to_i.cast_to_pretty_hz} Hz."
|
|
777
838
|
puts "If scans are slow and/or you're experiencing false positives/negatives,"
|
|
778
839
|
puts 'consider adjusting the following:'
|
|
779
840
|
puts "1. The SDR's sample rate in GQRX"
|
|
@@ -788,17 +849,15 @@ module PWN
|
|
|
788
849
|
|
|
789
850
|
signals_arr = []
|
|
790
851
|
hz = hz_start
|
|
791
|
-
while hz <= hz_target
|
|
792
|
-
gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
|
|
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
852
|
|
|
801
|
-
|
|
853
|
+
while step_hz_direction.positive? ? hz <= hz_target : hz >= hz_target
|
|
854
|
+
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
855
|
+
strength_db = measure_signal_strength(
|
|
856
|
+
gqrx_sock: gqrx_sock,
|
|
857
|
+
freq: hz,
|
|
858
|
+
strength_lock: strength_lock,
|
|
859
|
+
phase: :find_candidates
|
|
860
|
+
)
|
|
802
861
|
|
|
803
862
|
if strength_db >= strength_lock
|
|
804
863
|
puts '-' * 86
|
|
@@ -810,23 +869,11 @@ module PWN
|
|
|
810
869
|
strength_lock: strength_lock
|
|
811
870
|
)
|
|
812
871
|
elsif candidate_signals.length.positive?
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
872
|
+
best_peak = find_best_peak(
|
|
873
|
+
gqrx_sock: gqrx_sock,
|
|
874
|
+
candidate_signals: candidate_signals,
|
|
875
|
+
step_hz: step_hz,
|
|
876
|
+
strength_lock: strength_lock
|
|
830
877
|
)
|
|
831
878
|
|
|
832
879
|
if best_peak[:hz] && best_peak[:strength_db] > strength_lock
|
|
@@ -860,12 +907,12 @@ module PWN
|
|
|
860
907
|
timestamp_start: timestamp_start,
|
|
861
908
|
scan_log: scan_log
|
|
862
909
|
)
|
|
863
|
-
hz =
|
|
910
|
+
hz = candidate_signals.last[:hz]
|
|
864
911
|
# gets
|
|
865
912
|
end
|
|
866
913
|
candidate_signals.clear
|
|
867
914
|
end
|
|
868
|
-
hz +=
|
|
915
|
+
hz += step_hz_direction
|
|
869
916
|
end
|
|
870
917
|
|
|
871
918
|
log_signals(
|
|
@@ -978,7 +1025,7 @@ module PWN
|
|
|
978
1025
|
freq: 'required - Frequency to set',
|
|
979
1026
|
demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
980
1027
|
rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
981
|
-
bandwidth: 'optional - Bandwidth (defaults to
|
|
1028
|
+
bandwidth: 'optional - Bandwidth (defaults to \"200.000\")',
|
|
982
1029
|
decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
983
1030
|
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
984
1031
|
suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
|
|
@@ -990,7 +1037,7 @@ module PWN
|
|
|
990
1037
|
start_freq: 'required - Starting frequency',
|
|
991
1038
|
target_freq: 'required - Target frequency',
|
|
992
1039
|
demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
993
|
-
bandwidth: 'optional - Bandwidth in Hz (Defaults to
|
|
1040
|
+
bandwidth: 'optional - Bandwidth in Hz (Defaults to \"200.000\")',
|
|
994
1041
|
precision: 'optional - Precision (Defaults to 1)',
|
|
995
1042
|
strength_lock: 'optional - Strength lock (defaults to -70.0)',
|
|
996
1043
|
squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
|
data/lib/pwn/version.rb
CHANGED