pwn 0.5.515 → 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 +188 -160
- 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
|
|
@@ -158,13 +158,11 @@ module PWN
|
|
|
158
158
|
|
|
159
159
|
response_str
|
|
160
160
|
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')
|
|
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')
|
|
164
162
|
|
|
165
|
-
raise e unless e.message.include?('
|
|
166
|
-
e.message.include?('
|
|
167
|
-
e.message.include?('
|
|
163
|
+
raise e unless e.message.include?('RF_GAIN') ||
|
|
164
|
+
e.message.include?('IF_GAIN') ||
|
|
165
|
+
e.message.include?('BB_GAIN')
|
|
168
166
|
rescue StandardError => e
|
|
169
167
|
raise e
|
|
170
168
|
end
|
|
@@ -172,36 +170,49 @@ module PWN
|
|
|
172
170
|
# Supported Method Parameters::
|
|
173
171
|
# strength_db = PWN::SDR::GQRX.measure_signal_strength(
|
|
174
172
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
175
|
-
#
|
|
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)'
|
|
176
176
|
# )
|
|
177
177
|
private_class_method def self.measure_signal_strength(opts = {})
|
|
178
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
|
+
|
|
179
182
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
183
|
+
phase = opts[:phase] ||= :find_candidates
|
|
180
184
|
|
|
181
185
|
attempts = 0
|
|
182
186
|
strength_db = -99.9
|
|
183
187
|
prev_strength_db = -99.9
|
|
188
|
+
distance_between_unique_samples = 0.0
|
|
189
|
+
samples = []
|
|
190
|
+
unique_samples = []
|
|
191
|
+
strength_measured = false
|
|
184
192
|
loop do
|
|
185
193
|
attempts += 1
|
|
186
194
|
strength_db = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
187
195
|
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
196
205
|
|
|
197
|
-
break if
|
|
206
|
+
break if strength_measured
|
|
198
207
|
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
# Sleep a tiny bit to allow strength_db values to fluctuate
|
|
209
|
+
sleep 0.0001
|
|
201
210
|
end
|
|
202
|
-
|
|
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}"
|
|
203
214
|
|
|
204
|
-
strength_db
|
|
215
|
+
strength_db.round(1)
|
|
205
216
|
rescue StandardError => e
|
|
206
217
|
raise e
|
|
207
218
|
end
|
|
@@ -253,21 +264,22 @@ module PWN
|
|
|
253
264
|
right_candidate_signals = []
|
|
254
265
|
candidate_signals = []
|
|
255
266
|
|
|
256
|
-
# left_candidate_signals.clear
|
|
257
267
|
original_hz = hz
|
|
258
268
|
strength_db = 99.9
|
|
259
|
-
puts '
|
|
269
|
+
puts '*** Edge Detection: Locating Beginning of Signal...'
|
|
260
270
|
while strength_db >= strength_lock
|
|
261
271
|
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
262
272
|
strength_db = measure_signal_strength(
|
|
263
273
|
gqrx_sock: gqrx_sock,
|
|
264
|
-
|
|
274
|
+
freq: hz,
|
|
275
|
+
strength_lock: strength_lock,
|
|
276
|
+
phase: :edge_left
|
|
265
277
|
)
|
|
266
278
|
candidate = {
|
|
267
279
|
hz: hz.to_s.cast_to_raw_hz,
|
|
268
280
|
freq: hz.to_i.cast_to_pretty_hz,
|
|
269
281
|
strength: strength_db,
|
|
270
|
-
|
|
282
|
+
edge: :left
|
|
271
283
|
}
|
|
272
284
|
left_candidate_signals.push(candidate)
|
|
273
285
|
hz -= step_hz
|
|
@@ -277,22 +289,23 @@ module PWN
|
|
|
277
289
|
|
|
278
290
|
# Now scan forwards to find the end of the signal
|
|
279
291
|
# The end of the signal is where the strength drops below strength_lock
|
|
280
|
-
# right_candidate_signals.clear
|
|
281
292
|
hz = original_hz
|
|
282
293
|
|
|
283
294
|
strength_db = 99.9
|
|
284
|
-
puts '
|
|
295
|
+
puts '*** Edge Detection: Locating End of Signal...'
|
|
285
296
|
while strength_db >= strength_lock
|
|
286
297
|
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
287
298
|
strength_db = measure_signal_strength(
|
|
288
299
|
gqrx_sock: gqrx_sock,
|
|
289
|
-
|
|
300
|
+
freq: hz,
|
|
301
|
+
strength_lock: strength_lock,
|
|
302
|
+
phase: :edge_right
|
|
290
303
|
)
|
|
291
304
|
candidate = {
|
|
292
305
|
hz: hz.to_s.cast_to_raw_hz,
|
|
293
306
|
freq: hz.to_i.cast_to_pretty_hz,
|
|
294
307
|
strength: strength_db,
|
|
295
|
-
|
|
308
|
+
edge: :right
|
|
296
309
|
}
|
|
297
310
|
right_candidate_signals.push(candidate)
|
|
298
311
|
hz += step_hz
|
|
@@ -324,7 +337,8 @@ module PWN
|
|
|
324
337
|
signals.uniq! { |s| s[:freq].to_s.cast_to_raw_hz }
|
|
325
338
|
|
|
326
339
|
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
327
|
-
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
|
+
|
|
328
342
|
# Convert duration seconds to hours minutes seconds
|
|
329
343
|
hours = (duration_secs / 3600).to_i
|
|
330
344
|
minutes = ((duration_secs % 3600) / 60).to_i
|
|
@@ -344,6 +358,10 @@ module PWN
|
|
|
344
358
|
JSON.pretty_generate(scan_resp)
|
|
345
359
|
)
|
|
346
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
|
+
|
|
347
365
|
scan_resp
|
|
348
366
|
rescue StandardError => e
|
|
349
367
|
raise e
|
|
@@ -372,7 +390,7 @@ module PWN
|
|
|
372
390
|
|
|
373
391
|
rds_resp = {}
|
|
374
392
|
attempts = 0
|
|
375
|
-
max_attempts =
|
|
393
|
+
max_attempts = 120
|
|
376
394
|
skip_rds = "\n"
|
|
377
395
|
print 'INFO: Decoding FM radio RDS data (Press ENTER to skip)...'
|
|
378
396
|
max_attempts.times do
|
|
@@ -397,6 +415,115 @@ module PWN
|
|
|
397
415
|
raise e
|
|
398
416
|
end
|
|
399
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
|
+
|
|
400
527
|
# Supported Method Parameters::
|
|
401
528
|
# gqrx_sock = PWN::SDR::GQRX.connect(
|
|
402
529
|
# target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
|
|
@@ -417,7 +544,7 @@ module PWN
|
|
|
417
544
|
# freq: 'required - Frequency to set',
|
|
418
545
|
# demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
419
546
|
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
420
|
-
# bandwidth: 'optional - Bandwidth (defaults to
|
|
547
|
+
# bandwidth: 'optional - Bandwidth (defaults to "200.000")',
|
|
421
548
|
# squelch: 'optional - Squelch level to set (Defaults to current value)',
|
|
422
549
|
# decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
423
550
|
# record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
@@ -447,7 +574,7 @@ module PWN
|
|
|
447
574
|
|
|
448
575
|
rds = opts[:rds] ||= false
|
|
449
576
|
|
|
450
|
-
bandwidth = opts[:bandwidth] ||=
|
|
577
|
+
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
451
578
|
squelch = opts[:squelch]
|
|
452
579
|
decoder = opts[:decoder]
|
|
453
580
|
record_dir = opts[:record_dir] ||= '/tmp'
|
|
@@ -475,15 +602,17 @@ module PWN
|
|
|
475
602
|
|
|
476
603
|
tune_to(gqrx_sock: gqrx_sock, hz: freq)
|
|
477
604
|
strength_db = measure_signal_strength(
|
|
478
|
-
gqrx_sock: gqrx_sock
|
|
605
|
+
gqrx_sock: gqrx_sock,
|
|
606
|
+
freq: freq,
|
|
607
|
+
phase: :init_freq
|
|
479
608
|
)
|
|
480
609
|
|
|
481
610
|
freq_obj = {
|
|
482
|
-
|
|
611
|
+
freq: freq,
|
|
483
612
|
demodulator_mode: demodulator_mode,
|
|
613
|
+
bandwidth: bandwidth,
|
|
484
614
|
rds: rds,
|
|
485
|
-
strength_db: strength_db
|
|
486
|
-
freq: freq
|
|
615
|
+
strength_db: strength_db
|
|
487
616
|
}
|
|
488
617
|
|
|
489
618
|
unless suppress_details
|
|
@@ -592,7 +721,7 @@ module PWN
|
|
|
592
721
|
# target_freq: 'required - Target frequency of scan range',
|
|
593
722
|
# demodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',
|
|
594
723
|
# rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
595
|
-
# bandwidth: 'optional - Bandwidth in Hz (Defaults to
|
|
724
|
+
# bandwidth: 'optional - Bandwidth in Hz (Defaults to "200.000")',
|
|
596
725
|
# precision: 'optional - Frequency step precision (number of digits; defaults to 1)',
|
|
597
726
|
# strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
|
|
598
727
|
# squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
|
|
@@ -619,7 +748,8 @@ module PWN
|
|
|
619
748
|
demodulator_mode = opts[:demodulator_mode]
|
|
620
749
|
rds = opts[:rds] ||= false
|
|
621
750
|
|
|
622
|
-
bandwidth = opts[:bandwidth] ||=
|
|
751
|
+
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
752
|
+
|
|
623
753
|
precision = opts[:precision] ||= 1
|
|
624
754
|
strength_lock = opts[:strength_lock] ||= -70.0
|
|
625
755
|
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
@@ -627,7 +757,7 @@ module PWN
|
|
|
627
757
|
location = opts[:location] ||= 'United States'
|
|
628
758
|
|
|
629
759
|
step_hz = 10**(precision - 1)
|
|
630
|
-
|
|
760
|
+
step_hz_direction = hz_start > hz_target ? -step_hz : step_hz
|
|
631
761
|
|
|
632
762
|
# Set squelch once for the scan
|
|
633
763
|
change_squelch_resp = gqrx_cmd(
|
|
@@ -701,103 +831,10 @@ module PWN
|
|
|
701
831
|
|
|
702
832
|
candidate_signals = []
|
|
703
833
|
|
|
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
834
|
# Begin scanning range
|
|
798
835
|
puts "\n"
|
|
799
836
|
puts '-' * 86
|
|
800
|
-
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."
|
|
801
838
|
puts "If scans are slow and/or you're experiencing false positives/negatives,"
|
|
802
839
|
puts 'consider adjusting the following:'
|
|
803
840
|
puts "1. The SDR's sample rate in GQRX"
|
|
@@ -812,11 +849,14 @@ module PWN
|
|
|
812
849
|
|
|
813
850
|
signals_arr = []
|
|
814
851
|
hz = hz_start
|
|
815
|
-
|
|
852
|
+
|
|
853
|
+
while step_hz_direction.positive? ? hz <= hz_target : hz >= hz_target
|
|
816
854
|
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
817
855
|
strength_db = measure_signal_strength(
|
|
818
856
|
gqrx_sock: gqrx_sock,
|
|
819
|
-
|
|
857
|
+
freq: hz,
|
|
858
|
+
strength_lock: strength_lock,
|
|
859
|
+
phase: :find_candidates
|
|
820
860
|
)
|
|
821
861
|
|
|
822
862
|
if strength_db >= strength_lock
|
|
@@ -829,23 +869,11 @@ module PWN
|
|
|
829
869
|
strength_lock: strength_lock
|
|
830
870
|
)
|
|
831
871
|
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
|
|
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
|
|
849
877
|
)
|
|
850
878
|
|
|
851
879
|
if best_peak[:hz] && best_peak[:strength_db] > strength_lock
|
|
@@ -879,12 +907,12 @@ module PWN
|
|
|
879
907
|
timestamp_start: timestamp_start,
|
|
880
908
|
scan_log: scan_log
|
|
881
909
|
)
|
|
882
|
-
hz =
|
|
910
|
+
hz = candidate_signals.last[:hz]
|
|
883
911
|
# gets
|
|
884
912
|
end
|
|
885
913
|
candidate_signals.clear
|
|
886
914
|
end
|
|
887
|
-
hz +=
|
|
915
|
+
hz += step_hz_direction
|
|
888
916
|
end
|
|
889
917
|
|
|
890
918
|
log_signals(
|
|
@@ -997,7 +1025,7 @@ module PWN
|
|
|
997
1025
|
freq: 'required - Frequency to set',
|
|
998
1026
|
demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
999
1027
|
rds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',
|
|
1000
|
-
bandwidth: 'optional - Bandwidth (defaults to
|
|
1028
|
+
bandwidth: 'optional - Bandwidth (defaults to \"200.000\")',
|
|
1001
1029
|
decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
1002
1030
|
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
1003
1031
|
suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
|
|
@@ -1009,7 +1037,7 @@ module PWN
|
|
|
1009
1037
|
start_freq: 'required - Starting frequency',
|
|
1010
1038
|
target_freq: 'required - Target frequency',
|
|
1011
1039
|
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
|
|
1040
|
+
bandwidth: 'optional - Bandwidth in Hz (Defaults to \"200.000\")',
|
|
1013
1041
|
precision: 'optional - Precision (Defaults to 1)',
|
|
1014
1042
|
strength_lock: 'optional - Strength lock (defaults to -70.0)',
|
|
1015
1043
|
squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
|
data/lib/pwn/version.rb
CHANGED
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -1292,14 +1292,18 @@
|
|
|
1292
1292
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.analyze_scan Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.analyze_scan`: Supported Method Parameters\n\nPWN::SDR::GQRX.analyze_scan(\n\nscan_resp: 'required - Scan response hash returned from #scan_range method',\ntarget: 'optional - GQRX target IP address (defaults to 127.0.0.1)',\nport: 'optional - GQRX target port (defaults to 7356)'\n\n)\n"}]}
|
|
1293
1293
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.authors Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
1294
1294
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.connect Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.connect`: Supported Method Parameters\n\ngqrx_sock = PWN::SDR::GQRX.connect(\n\ntarget: 'optional - GQRX target IP address (defaults to 127.0.0.1)',\nport: 'optional - GQRX target port (defaults to 7356)'\n\n)\n"}]}
|
|
1295
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.decode_rds Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.decode_rds`: Supported Method Parameters\n\nrds_resp = PWN::SDR::GQRX.decode_rds(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method'\n\n)\n"}]}
|
|
1295
1296
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.disconnect Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.disconnect`: Supported Method Parameters\n\nPWN::SDR::GQRX.disconnect(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method'\n\n)\n"}]}
|
|
1297
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.edge_detection Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.edge_detection`: Supported Method Parameters\n\ncandidate_signals = PWN::SDR::GQRX.edge_detection(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nhz: 'required - Frequency to start edge detection from',\nstep_hz: 'required - Frequency step in Hz for edge detection',\nstrength_lock: 'required - Strength lock in dBFS to determine signal edges'\n\n)\n"}]}
|
|
1296
1298
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.gqrx_cmd Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.gqrx_cmd`: Supported Method Parameters\n\ngqrx_resp = PWN::SDR::GQRX.gqrx_cmd(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\ncmd: 'required - GQRX command to execute',\nresp_ok: 'optional - Expected response from GQRX to indicate success'\n\n)\n"}]}
|
|
1297
1299
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.help Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.help`: "}]}
|
|
1298
|
-
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.init_freq Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.init_freq`: Supported Method Parameters\n\nfreq_obj = PWN::SDR::GQRX.init_freq(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nfreq: 'required - Frequency to set',\ndemodulator_mode: 'optional - Demodulator mode (defaults to WFM)',\nbandwidth: 'optional - Bandwidth (defaults to 200_000)',\nsquelch: 'optional - Squelch level to set (Defaults to current value)',\ndecoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',\nrecord_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',\nsuppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',\nkeep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'\n\n)\n"}]}
|
|
1300
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.init_freq Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.init_freq`: Supported Method Parameters\n\nfreq_obj = PWN::SDR::GQRX.init_freq(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nfreq: 'required - Frequency to set',\ndemodulator_mode: 'optional - Demodulator mode (defaults to WFM)',\nrds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',\nbandwidth: 'optional - Bandwidth (defaults to 200_000)',\nsquelch: 'optional - Squelch level to set (Defaults to current value)',\ndecoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',\nrecord_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',\nsuppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',\nkeep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'\n\n)\n"}]}
|
|
1299
1301
|
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.log_signals Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.log_signals`: Supported Method Parameters\n\nscan_resp = PWN::SDR::GQRX.log_signals(\n\nsignals_arr: 'required - Array of detected signals',\ntimestamp_start: 'required - Scan start timestamp',\nscan_log: 'required - Path to save detected signals log'\n\n)\n"}]}
|
|
1300
|
-
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.
|
|
1301
|
-
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.
|
|
1302
|
-
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.
|
|
1302
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.measure_signal_strength Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.measure_signal_strength`: Supported Method Parameters\n\nstrength_db = PWN::SDR::GQRX.measure_signal_strength(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nstrength_lock: 'optional - Strength lock in dBFS to determine signal edges (defaults to -70.0)'\n\n)\n"}]}
|
|
1303
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.scan_range Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.scan_range`: Supported Method Parameters\n\nscan_resp = PWN::SDR::GQRX.scan_range(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nstart_freq: 'required - Start frequency of scan range',\ntarget_freq: 'required - Target frequency of scan range',\ndemodulator_mode: 'optional - Demodulator mode (e.g. WFM, AM, FM, USB, LSB, RAW, CW, RTTY / defaults to WFM)',\nrds: 'optional - Boolean to enable/disable RDS decoding (defaults to false)',\nbandwidth: 'optional - Bandwidth in Hz (Defaults to 200_000)',\nprecision: 'optional - Frequency step precision (number of digits; defaults to 1)',\nstrength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',\nsquelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',\naudio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',\nrf_gain: 'optional - RF gain (defaults to 0.0)',\nintermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',\nbaseband_gain: 'optional - Baseband gain (defaults to 10.0)',\nscan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',\nlocation: 'optional - Location string to include in AI analysis (e.g., \"New York, NY\", 90210, GPS coords, etc.)'\n\n)\n"}]}
|
|
1304
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.tune_to Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.tune_to`: Supported Method Parameters\n\ntune_resp = PWN::SDR::GQRX.tune_to(\n\ngqrx_sock: 'required - GQRX socket object returned from #connect method',\nhz: 'required - Frequency to tune to'\n\n)\n"}]}
|
|
1305
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.cast_to_pretty_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.cast_to_pretty_hz`: "}]}
|
|
1306
|
+
{"messages":[{"role":"user","content":"PWN::SDR::GQRX.cast_to_raw_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.cast_to_raw_hz`: "}]}
|
|
1303
1307
|
{"messages":[{"role":"user","content":"PWN::SDR::RFIDler.authors Usage"},{"role":"assistant","content":"`PWN::SDR::RFIDler.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
1304
1308
|
{"messages":[{"role":"user","content":"PWN::SDR::RFIDler.connect_via_screen Usage"},{"role":"assistant","content":"`PWN::SDR::RFIDler.connect_via_screen`: Supported Method Parameters\n\nPWN::SDR::RFIDler.connect_via_screen(\n\nblock_dev: 'optional - serial block device path (defaults to /dev/ttyUSB0)'\n\n)\n"}]}
|
|
1305
1309
|
{"messages":[{"role":"user","content":"PWN::SDR::RFIDler.help Usage"},{"role":"assistant","content":"`PWN::SDR::RFIDler.help`: "}]}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pwn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.516
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -869,14 +869,14 @@ dependencies:
|
|
|
869
869
|
requirements:
|
|
870
870
|
- - '='
|
|
871
871
|
- !ruby/object:Gem::Version
|
|
872
|
-
version: 7.0.
|
|
872
|
+
version: 7.0.2
|
|
873
873
|
type: :development
|
|
874
874
|
prerelease: false
|
|
875
875
|
version_requirements: !ruby/object:Gem::Requirement
|
|
876
876
|
requirements:
|
|
877
877
|
- - '='
|
|
878
878
|
- !ruby/object:Gem::Version
|
|
879
|
-
version: 7.0.
|
|
879
|
+
version: 7.0.2
|
|
880
880
|
- !ruby/object:Gem::Dependency
|
|
881
881
|
name: rest-client
|
|
882
882
|
requirement: !ruby/object:Gem::Requirement
|