pwn 0.5.532 → 0.5.534
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +25 -3
- data/Gemfile +6 -5
- data/README.md +3 -3
- data/lib/pwn/plugins/burp_suite.rb +24 -10
- data/lib/pwn/plugins/file_fu.rb +50 -0
- data/lib/pwn/plugins/sock.rb +111 -48
- data/lib/pwn/sdr/decoder/flex.rb +257 -0
- data/lib/pwn/sdr/decoder/pocsag.rb +182 -57
- data/lib/pwn/sdr/decoder/rds.rb +4 -4
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/frequency_allocation.rb +13 -6
- data/lib/pwn/sdr/gqrx.rb +426 -170
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/sdr/decoder/flex_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +10 -7
- metadata +28 -12
data/lib/pwn/sdr/gqrx.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
3
4
|
require 'json'
|
|
4
5
|
require 'time'
|
|
5
6
|
|
|
@@ -144,6 +145,49 @@ module PWN
|
|
|
144
145
|
raise e
|
|
145
146
|
end
|
|
146
147
|
|
|
148
|
+
# Supported Method Parameters::
|
|
149
|
+
# noise_floor = PWN::SDR::GQRX.measure_noise_floor(
|
|
150
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
151
|
+
# freq: 'required - Frequency to measure noise floor',
|
|
152
|
+
# precision: 'required - Frequency step precision',
|
|
153
|
+
# step_hz_direction: 'required - Frequency step in Hz direction for noise floor measurement'
|
|
154
|
+
# )
|
|
155
|
+
private_class_method def self.measure_noise_floor(opts = {})
|
|
156
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
157
|
+
freq = opts[:freq]
|
|
158
|
+
precision = opts[:precision]
|
|
159
|
+
step_hz_direction = opts[:step_hz_direction]
|
|
160
|
+
freqs_to_sample = 10
|
|
161
|
+
samples_per_freq = 100
|
|
162
|
+
|
|
163
|
+
# Quickly sample multiple frequencies around target frequency
|
|
164
|
+
start_hz = PWN::SDR.hz_to_i(freq)
|
|
165
|
+
hz = start_hz
|
|
166
|
+
# puts step_hz_direction.class; gets
|
|
167
|
+
target_hz = start_hz + (step_hz_direction * freqs_to_sample)
|
|
168
|
+
noise_floors = []
|
|
169
|
+
puts "*** Sampling #{freqs_to_sample} Noise Floor to Dynamically Determine Squelch Level"
|
|
170
|
+
while (step_hz_direction.positive? && hz <= target_hz) || (step_hz_direction.negative? && hz >= target_hz)
|
|
171
|
+
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
172
|
+
print '.'
|
|
173
|
+
|
|
174
|
+
strengths = []
|
|
175
|
+
samples_per_freq.times do
|
|
176
|
+
strength_db = cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
|
|
177
|
+
strengths << strength_db
|
|
178
|
+
sleep 0.001
|
|
179
|
+
end
|
|
180
|
+
freq_noise_floor = strengths.sum / strengths.size
|
|
181
|
+
noise_floors.push(freq_noise_floor)
|
|
182
|
+
hz += step_hz_direction
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
noise_floor = noise_floors.min
|
|
186
|
+
noise_floor.round(1)
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
raise e
|
|
189
|
+
end
|
|
190
|
+
|
|
147
191
|
# Supported Method Parameters::
|
|
148
192
|
# strength_db = PWN::SDR::GQRX.measure_signal_strength(
|
|
149
193
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
@@ -318,36 +362,61 @@ module PWN
|
|
|
318
362
|
raise e
|
|
319
363
|
end
|
|
320
364
|
|
|
365
|
+
# Supported Method Parameters::
|
|
366
|
+
# duration = PWN::SDR::GQRX.duration_between(
|
|
367
|
+
# timestamp_start: 'required - Start timestamp',
|
|
368
|
+
# timestamp_end: 'required - End timestamp'
|
|
369
|
+
# )
|
|
370
|
+
private_class_method def self.duration_between(opts = {})
|
|
371
|
+
timestamp_start = opts[:timestamp_start]
|
|
372
|
+
timestamp_end = opts[:timestamp_end]
|
|
373
|
+
raise 'ERROR: timestamp_start && timestamp_end must are required parameters.' if timestamp_start.nil? || timestamp_end.nil?
|
|
374
|
+
|
|
375
|
+
duration_secs = Time.parse(timestamp_end).to_f - Time.parse(timestamp_start).to_f
|
|
376
|
+
|
|
377
|
+
# Convert duration seconds to hours minutes seconds
|
|
378
|
+
hours = (duration_secs / 3600).to_i
|
|
379
|
+
minutes = ((duration_secs % 3600) / 60).to_i
|
|
380
|
+
seconds = (duration_secs % 60).to_i
|
|
381
|
+
format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
|
|
382
|
+
rescue StandardError => e
|
|
383
|
+
raise e
|
|
384
|
+
end
|
|
385
|
+
|
|
321
386
|
# Supported Method Parameters::
|
|
322
387
|
# scan_resp = PWN::SDR::GQRX.log_signals(
|
|
323
|
-
#
|
|
388
|
+
# signals_detected: 'required - Array of detected signals',
|
|
324
389
|
# timestamp_start: 'required - Scan start timestamp',
|
|
325
|
-
# scan_log: 'required - Path to save detected signals log'
|
|
390
|
+
# scan_log: 'required - Path to save detected signals log',
|
|
391
|
+
# iteration_metrics: 'optional - Hash of iteration metrics per range iteration'
|
|
326
392
|
# )
|
|
327
393
|
private_class_method def self.log_signals(opts = {})
|
|
328
|
-
|
|
394
|
+
signals_detected = opts[:signals_detected]
|
|
329
395
|
timestamp_start = opts[:timestamp_start]
|
|
330
396
|
scan_log = opts[:scan_log]
|
|
331
397
|
|
|
332
|
-
|
|
398
|
+
existing_scan_resp = JSON.parse(File.read(scan_log), symbolize_names: true) if File.exist?(scan_log)
|
|
399
|
+
|
|
400
|
+
# BUG: iteration_metrics not being properly initialized from existing log
|
|
401
|
+
iteration_metrics = opts[:iteration_metrics]
|
|
402
|
+
iteration_metrics ||= existing_scan_resp[:iteration_metrics] if existing_scan_resp.is_a?(Hash) && existing_scan_resp.key?(:iteration_metrics) && existing_scan_resp[:iteration_metrics].is_a?(Array) && !existing_scan_resp[:iteration_metrics].empty?
|
|
403
|
+
iteration_metrics ||= []
|
|
404
|
+
|
|
405
|
+
signals = signals_detected.sort_by { |s| PWN::SDR.hz_to_i(s[:freq]) }
|
|
333
406
|
# Unique signals by frequency
|
|
334
407
|
signals.uniq! { |s| PWN::SDR.hz_to_i(s[:freq]) }
|
|
335
408
|
|
|
336
409
|
timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
# Convert duration seconds to hours minutes seconds
|
|
340
|
-
hours = (duration_secs / 3600).to_i
|
|
341
|
-
minutes = ((duration_secs % 3600) / 60).to_i
|
|
342
|
-
seconds = (duration_secs % 60).to_i
|
|
343
|
-
duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
|
|
410
|
+
duration = duration_between(timestamp_start: timestamp_start, timestamp_end: timestamp_end)
|
|
344
411
|
|
|
412
|
+
# TODO: Implement iteration_metrics logging per range iteration
|
|
345
413
|
scan_resp = {
|
|
346
414
|
signals: signals,
|
|
347
415
|
total: signals.length,
|
|
348
416
|
timestamp_start: timestamp_start,
|
|
349
417
|
timestamp_end: timestamp_end,
|
|
350
|
-
duration: duration
|
|
418
|
+
duration: duration,
|
|
419
|
+
iteration_metrics: iteration_metrics
|
|
351
420
|
}
|
|
352
421
|
|
|
353
422
|
File.write(
|
|
@@ -391,14 +460,16 @@ module PWN
|
|
|
391
460
|
|
|
392
461
|
pass_count = 0
|
|
393
462
|
infinite_loop_safeguard = false
|
|
463
|
+
# loop do
|
|
464
|
+
# Why doesn't this work with loop do ???
|
|
394
465
|
while true
|
|
395
466
|
pass_count += 1
|
|
396
467
|
|
|
397
468
|
# Safeguard against infinite loop
|
|
398
469
|
# infinite_loop_safeguard = true if pass_count >= 100
|
|
399
|
-
infinite_loop_safeguard = true if pass_count >= 10
|
|
400
|
-
puts 'WARNING: Infinite loop safeguard triggered in find_best_peak!' if infinite_loop_safeguard
|
|
401
|
-
break if infinite_loop_safeguard
|
|
470
|
+
# infinite_loop_safeguard = true if pass_count >= 10
|
|
471
|
+
# puts 'WARNING: Infinite loop safeguard triggered in find_best_peak!' if infinite_loop_safeguard
|
|
472
|
+
# break if infinite_loop_safeguard
|
|
402
473
|
|
|
403
474
|
direction_up = !direction_up
|
|
404
475
|
start_hz_direction = direction_up ? beg_of_signal_hz : end_of_signal_hz
|
|
@@ -450,6 +521,7 @@ module PWN
|
|
|
450
521
|
|
|
451
522
|
# Recalculate best_sample after trim
|
|
452
523
|
best_sample = averaged_samples.max_by { |s| s[:strength_db] }
|
|
524
|
+
next unless best_sample.is_a?(Hash)
|
|
453
525
|
|
|
454
526
|
# Check for improvement
|
|
455
527
|
if best_sample[:hz] == prev_best_sample[:hz]
|
|
@@ -494,7 +566,8 @@ module PWN
|
|
|
494
566
|
# bandwidth: 'optional - Bandwidth (defaults to "200.000")',
|
|
495
567
|
# squelch: 'optional - Squelch level to set (Defaults to current value)',
|
|
496
568
|
# decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
497
|
-
#
|
|
569
|
+
# udp_ip: 'optional - UDP IP address for decoder module (defaults to 127.0.0.1)',
|
|
570
|
+
# udp_port: 'optional - UDP port for decoder module (defaults to 7355)',
|
|
498
571
|
# suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
|
|
499
572
|
# keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
|
|
500
573
|
# )
|
|
@@ -523,12 +596,11 @@ module PWN
|
|
|
523
596
|
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
524
597
|
squelch = opts[:squelch]
|
|
525
598
|
decoder = opts[:decoder]
|
|
526
|
-
|
|
599
|
+
udp_ip = opts[:udp_ip]
|
|
600
|
+
udp_port = opts[:udp_port]
|
|
527
601
|
suppress_details = opts[:suppress_details] || false
|
|
528
602
|
keep_alive = opts[:keep_alive] || false
|
|
529
603
|
|
|
530
|
-
raise "ERROR: record_dir '#{record_dir}' does not exist. Please create it or provide a valid path." if decoder && !Dir.exist?(record_dir)
|
|
531
|
-
|
|
532
604
|
unless keep_alive
|
|
533
605
|
squelch = cmd(gqrx_sock: gqrx_sock, cmd: 'l SQL').to_f if squelch.nil?
|
|
534
606
|
change_squelch_resp = cmd(
|
|
@@ -559,7 +631,8 @@ module PWN
|
|
|
559
631
|
demodulator_mode: demodulator_mode,
|
|
560
632
|
bandwidth: bandwidth,
|
|
561
633
|
strength_db: strength_db,
|
|
562
|
-
decoder: decoder
|
|
634
|
+
decoder: decoder,
|
|
635
|
+
squelch: squelch
|
|
563
636
|
}
|
|
564
637
|
|
|
565
638
|
unless suppress_details
|
|
@@ -604,11 +677,11 @@ module PWN
|
|
|
604
677
|
if decoder
|
|
605
678
|
decoder = decoder.to_s.to_sym
|
|
606
679
|
decoder_module = nil
|
|
607
|
-
decoder_thread = nil
|
|
608
|
-
record_path = nil
|
|
609
680
|
|
|
610
681
|
# Resolve decoder module via case statement for extensibility
|
|
611
682
|
case decoder
|
|
683
|
+
when :flex
|
|
684
|
+
decoder_module = PWN::SDR::Decoder::Flex
|
|
612
685
|
when :gsm
|
|
613
686
|
decoder_module = PWN::SDR::Decoder::GSM
|
|
614
687
|
when :pocsag
|
|
@@ -619,21 +692,12 @@ module PWN
|
|
|
619
692
|
raise "ERROR: Unknown decoder key: #{decoder}. Supported: :gsm"
|
|
620
693
|
end
|
|
621
694
|
|
|
622
|
-
# Ensure recording is off before starting
|
|
623
|
-
unless decoder == :rds
|
|
624
|
-
record_path = "#{record_dir}/gqrx_recording.wav"
|
|
625
|
-
freq_obj[:record_path] = record_path
|
|
626
|
-
end
|
|
627
|
-
|
|
628
695
|
# Initialize and start decoder (module style: .start returns thread)
|
|
629
696
|
freq_obj[:gqrx_sock] = gqrx_sock
|
|
697
|
+
freq_obj[:udp_ip] = udp_ip
|
|
698
|
+
freq_obj[:udp_port] = udp_port
|
|
630
699
|
freq_obj[:decoder_module] = decoder_module
|
|
631
|
-
freq_obj
|
|
632
|
-
decoder_thread = decoder_module.decode(
|
|
633
|
-
freq_obj: freq_obj,
|
|
634
|
-
record_dir: record_dir
|
|
635
|
-
)
|
|
636
|
-
# freq_obj[:decoder_thread] = decoder_thread if decoder_thread.is_a?(Thread)
|
|
700
|
+
decoder_module.decode(freq_obj: freq_obj)
|
|
637
701
|
end
|
|
638
702
|
end
|
|
639
703
|
|
|
@@ -653,7 +717,7 @@ module PWN
|
|
|
653
717
|
# precision: 'optional - Frequency step precision (number of digits; defaults to 1)',
|
|
654
718
|
# strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
|
|
655
719
|
# squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
|
|
656
|
-
# audio_gain_db: 'optional - Audio gain in dB (defaults to
|
|
720
|
+
# audio_gain_db: 'optional - Audio gain in dB (defaults to 0.0)',
|
|
657
721
|
# rf_gain: 'optional - RF gain (defaults to 0.0)',
|
|
658
722
|
# intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
|
|
659
723
|
# baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
|
|
@@ -664,157 +728,177 @@ module PWN
|
|
|
664
728
|
|
|
665
729
|
public_class_method def self.scan_range(opts = {})
|
|
666
730
|
timestamp_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
731
|
+
range_timestamp_start = ''
|
|
667
732
|
|
|
668
|
-
|
|
669
|
-
loop do
|
|
670
|
-
gqrx_sock = opts[:gqrx_sock]
|
|
733
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
671
734
|
|
|
672
|
-
|
|
673
|
-
|
|
735
|
+
ranges = opts[:ranges]
|
|
736
|
+
raise 'ERROR: ranges must be an Array of Hash objects with :start_freq and :target_freq keys.' unless ranges.is_a?(Array) && ranges.all? { |r| r.is_a?(Hash) && r.key?(:start_freq) && r.key?(:target_freq) }
|
|
674
737
|
|
|
675
|
-
|
|
676
|
-
ranges.each do |r|
|
|
677
|
-
start_freq = r[:start_freq]
|
|
678
|
-
hz_start = PWN::SDR.hz_to_i(start_freq)
|
|
679
|
-
raise "ERROR: Invalid start_freq '#{start_freq}' provided." if hz_start.zero?
|
|
738
|
+
demodulator_mode = opts[:demodulator_mode]
|
|
680
739
|
|
|
681
|
-
|
|
682
|
-
hz_target = PWN::SDR.hz_to_i(target_freq)
|
|
683
|
-
raise "ERROR: Invalid target_freq '#{target_freq}' provided." if hz_target.zero?
|
|
684
|
-
end
|
|
740
|
+
bandwidth = opts[:bandwidth] ||= '200.000'
|
|
685
741
|
|
|
686
|
-
|
|
687
|
-
|
|
742
|
+
precision = opts[:precision] ||= 1
|
|
743
|
+
raise 'ERROR: precision must be an Integer between 1 and 12.' unless precision.is_a?(Integer) && precision.between?(1, 12)
|
|
688
744
|
|
|
689
|
-
|
|
690
|
-
hz_target = PWN::SDR.hz_to_i(first_target_freq)
|
|
745
|
+
step_hz = 10**(precision - 1)
|
|
691
746
|
|
|
692
|
-
|
|
747
|
+
strength_lock = opts[:strength_lock] ||= -70.0
|
|
748
|
+
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
749
|
+
raise 'ERROR: squelch must always be less than strength_lock.' if squelch >= strength_lock
|
|
693
750
|
|
|
694
|
-
|
|
751
|
+
decoder = opts[:decoder]
|
|
752
|
+
keep_looping = opts[:keep_looping] || false
|
|
753
|
+
log_timestamp = Time.now.strftime('%Y-%m-%d')
|
|
695
754
|
|
|
696
|
-
|
|
697
|
-
strength_lock = opts[:strength_lock] ||= -70.0
|
|
698
|
-
squelch = opts[:squelch] ||= (strength_lock - 3.0)
|
|
699
|
-
decoder = opts[:decoder]
|
|
755
|
+
location = opts[:location] ||= 'United States'
|
|
700
756
|
|
|
701
|
-
|
|
757
|
+
# This is for looping through ranges indefinitely if keep_looping is true
|
|
758
|
+
# Generate ranges strings for log filename
|
|
759
|
+
range_str = ''
|
|
760
|
+
ranges.each do |range|
|
|
761
|
+
start_freq = range[:start_freq]
|
|
762
|
+
hz_start = PWN::SDR.hz_to_i(start_freq)
|
|
763
|
+
hz_start_str = PWN::SDR.hz_to_s(hz_start)
|
|
702
764
|
|
|
703
|
-
|
|
704
|
-
|
|
765
|
+
target_freq = range[:target_freq]
|
|
766
|
+
hz_target = PWN::SDR.hz_to_i(target_freq)
|
|
767
|
+
hz_target_str = PWN::SDR.hz_to_s(hz_target)
|
|
705
768
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
scan_log = File.join(
|
|
710
|
-
File.dirname(scan_log),
|
|
711
|
-
"#{File.basename(scan_log, '.*')}_l#{loop_count}#{File.extname(scan_log)}"
|
|
712
|
-
)
|
|
713
|
-
end
|
|
769
|
+
range_str = "#{range_str}_#{hz_start_str}-#{hz_target_str}"
|
|
770
|
+
end
|
|
771
|
+
scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan#{range_str}_#{log_timestamp}.json"
|
|
714
772
|
|
|
715
|
-
|
|
773
|
+
iteration_metrics = []
|
|
774
|
+
candidate_signals = []
|
|
775
|
+
signals_detected = []
|
|
776
|
+
iteration_total = 1
|
|
777
|
+
signals_detected_total = 0
|
|
778
|
+
loop do
|
|
779
|
+
signals_detected_delta = 0
|
|
780
|
+
iter_metrics_hash = {}
|
|
781
|
+
ranges.each do |range|
|
|
782
|
+
range_timestamp_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
783
|
+
iter_metrics_hash[:iteration] = iteration_total
|
|
784
|
+
iter_metrics_hash[:range] = range
|
|
785
|
+
iter_metrics_hash[:timestamp_start] = range_timestamp_start
|
|
716
786
|
|
|
717
|
-
|
|
718
|
-
|
|
787
|
+
# Verify all frequencies are valid
|
|
788
|
+
start_freq = range[:start_freq]
|
|
789
|
+
hz_start = PWN::SDR.hz_to_i(start_freq)
|
|
790
|
+
raise "ERROR: Invalid start_freq '#{start_freq}' provided." if hz_start.zero?
|
|
719
791
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
puts ranges
|
|
725
|
-
puts "SESSION PARAMS >> Step Increment: #{PWN::SDR.hz_to_s(step_hz_direction.abs)} Hz."
|
|
726
|
-
puts "SESSION PARAMS >> Continuously Loop through Scan Range(s): #{keep_looping}"
|
|
727
|
-
puts "\nIf scans are slow and/or you're experiencing false positives/negatives,"
|
|
728
|
-
puts 'consider adjusting the following:'
|
|
729
|
-
puts "1. The SDR's sample rate in GQRX"
|
|
730
|
-
puts "\s\s- Click on `Configure I/O devices`."
|
|
731
|
-
puts "\s\s- A lower `Input rate` value seems counter-intuitive but works well (e.g. ADALM PLUTO ~ 1000000)."
|
|
732
|
-
puts '2. Adjust the :strength_lock parameter.'
|
|
733
|
-
puts '3. Adjust the :precision parameter.'
|
|
734
|
-
puts '4. Disable AI introspection in PWN::Env'
|
|
735
|
-
puts 'Happy scanning!'
|
|
736
|
-
puts '-' * 86
|
|
737
|
-
# print 'Pressing ENTER to begin scan...'
|
|
738
|
-
# gets
|
|
739
|
-
puts "\n\n\n"
|
|
740
|
-
|
|
741
|
-
# Set squelch once for the scan
|
|
742
|
-
change_squelch_resp = cmd(
|
|
743
|
-
gqrx_sock: gqrx_sock,
|
|
744
|
-
cmd: "L SQL #{squelch}",
|
|
745
|
-
resp_ok: 'RPRT 0'
|
|
746
|
-
)
|
|
792
|
+
target_freq = range[:target_freq]
|
|
793
|
+
hz_target = PWN::SDR.hz_to_i(target_freq)
|
|
794
|
+
hz_target_str = PWN::SDR.hz_to_s(hz_target)
|
|
795
|
+
raise "ERROR: Invalid target_freq '#{target_freq}' provided." if hz_target.zero?
|
|
747
796
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
797
|
+
step_hz_direction = hz_start > hz_target ? -step_hz : step_hz
|
|
798
|
+
noise_floor = measure_noise_floor(
|
|
799
|
+
gqrx_sock: gqrx_sock,
|
|
800
|
+
freq: start_freq,
|
|
801
|
+
precision: precision,
|
|
802
|
+
step_hz_direction: step_hz_direction
|
|
803
|
+
)
|
|
804
|
+
if squelch < noise_floor
|
|
805
|
+
squelch = noise_floor.round + 7
|
|
806
|
+
strength_lock = squelch + 3.0
|
|
807
|
+
puts "Adjusted strength_lock to #{strength_lock} dBFS and squelch to #{squelch} dBFS based on measured noise floor. This ensures proper signal detection..."
|
|
808
|
+
end
|
|
758
809
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
810
|
+
# Begin scanning range
|
|
811
|
+
puts "\n"
|
|
812
|
+
puts '-' * 86
|
|
813
|
+
puts 'SESSION PARAMS >> Scan Range(s):'
|
|
814
|
+
puts ranges
|
|
815
|
+
puts "SESSION PARAMS >> Step Increment: #{PWN::SDR.hz_to_s(step_hz_direction.abs)} Hz."
|
|
816
|
+
puts "SESSION PARAMS >> Continuously Loop through Scan Range(s): #{keep_looping}"
|
|
817
|
+
puts "\nIf scans are slow and/or you're experiencing false positives/negatives,"
|
|
818
|
+
puts 'consider adjusting the following:'
|
|
819
|
+
puts "1. The SDR's sample rate in GQRX"
|
|
820
|
+
puts "\s\s- Click on `Configure I/O devices`."
|
|
821
|
+
puts "\s\s- A lower `Input rate` value seems counter-intuitive but works well (e.g. ADALM PLUTO ~ 1000000)."
|
|
822
|
+
puts '2. Adjust the :strength_lock parameter.'
|
|
823
|
+
puts '3. Adjust the :precision parameter.'
|
|
824
|
+
puts '4. Disable AI introspection in PWN::Env'
|
|
825
|
+
puts 'Happy scanning!'
|
|
826
|
+
puts '-' * 86
|
|
827
|
+
# print 'Pressing ENTER to begin scan...'
|
|
828
|
+
# gets
|
|
829
|
+
puts "\n\n\n"
|
|
767
830
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
)
|
|
831
|
+
# Set squelch once for each range
|
|
832
|
+
change_squelch_resp = cmd(
|
|
833
|
+
gqrx_sock: gqrx_sock,
|
|
834
|
+
cmd: "L SQL #{squelch}",
|
|
835
|
+
resp_ok: 'RPRT 0'
|
|
836
|
+
)
|
|
775
837
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
838
|
+
# We always disable RDS decoding during the scan
|
|
839
|
+
# to prevent unnecessary processing overhead.
|
|
840
|
+
# We return the rds boolean in the scan_resp object
|
|
841
|
+
# so it will be picked up and used appropriately
|
|
842
|
+
# when calling analyze_scan or analyze_log methods.
|
|
843
|
+
rds_resp = cmd(
|
|
844
|
+
gqrx_sock: gqrx_sock,
|
|
845
|
+
cmd: 'U RDS 0',
|
|
846
|
+
resp_ok: 'RPRT 0'
|
|
847
|
+
)
|
|
783
848
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
849
|
+
# Set demodulator mode & passband once for the scan
|
|
850
|
+
mode_str = demodulator_mode.to_s.upcase
|
|
851
|
+
passband_hz = PWN::SDR.hz_to_i(bandwidth)
|
|
852
|
+
cmd(
|
|
853
|
+
gqrx_sock: gqrx_sock,
|
|
854
|
+
cmd: "M #{mode_str} #{passband_hz}",
|
|
855
|
+
resp_ok: 'RPRT 0'
|
|
856
|
+
)
|
|
791
857
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
858
|
+
audio_gain_db = opts[:audio_gain_db] ||= 0.0
|
|
859
|
+
audio_gain_db = audio_gain_db.to_f
|
|
860
|
+
audio_gain_db_resp = cmd(
|
|
861
|
+
gqrx_sock: gqrx_sock,
|
|
862
|
+
cmd: "L AF #{audio_gain_db}",
|
|
863
|
+
resp_ok: 'RPRT 0'
|
|
864
|
+
)
|
|
799
865
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
866
|
+
rf_gain = opts[:rf_gain] ||= 0.0
|
|
867
|
+
rf_gain = rf_gain.to_f
|
|
868
|
+
rf_gain_resp = cmd(
|
|
869
|
+
gqrx_sock: gqrx_sock,
|
|
870
|
+
cmd: "L RF_GAIN #{rf_gain}",
|
|
871
|
+
resp_ok: 'RPRT 0'
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
intermediate_gain = opts[:intermediate_gain] ||= 32.0
|
|
875
|
+
intermediate_gain = intermediate_gain.to_f
|
|
876
|
+
intermediate_resp = cmd(
|
|
877
|
+
gqrx_sock: gqrx_sock,
|
|
878
|
+
cmd: "L IF_GAIN #{intermediate_gain}",
|
|
879
|
+
resp_ok: 'RPRT 0'
|
|
880
|
+
)
|
|
811
881
|
|
|
812
|
-
|
|
882
|
+
baseband_gain = opts[:baseband_gain] ||= 10.0
|
|
883
|
+
baseband_gain = baseband_gain.to_f
|
|
884
|
+
baseband_resp = cmd(
|
|
885
|
+
gqrx_sock: gqrx_sock,
|
|
886
|
+
cmd: "L BB_GAIN #{baseband_gain}",
|
|
887
|
+
resp_ok: 'RPRT 0'
|
|
888
|
+
)
|
|
813
889
|
|
|
814
|
-
|
|
815
|
-
|
|
890
|
+
prev_freq_obj = init_freq(
|
|
891
|
+
gqrx_sock: gqrx_sock,
|
|
892
|
+
freq: hz_start,
|
|
893
|
+
precision: precision,
|
|
894
|
+
demodulator_mode: demodulator_mode,
|
|
895
|
+
bandwidth: bandwidth,
|
|
896
|
+
squelch: squelch,
|
|
897
|
+
decoder: decoder,
|
|
898
|
+
suppress_details: true,
|
|
899
|
+
keep_alive: true
|
|
900
|
+
)
|
|
816
901
|
|
|
817
|
-
ranges.each do |range|
|
|
818
902
|
start_freq = range[:start_freq]
|
|
819
903
|
hz_start = PWN::SDR.hz_to_i(start_freq)
|
|
820
904
|
hz = hz_start
|
|
@@ -824,7 +908,8 @@ module PWN
|
|
|
824
908
|
|
|
825
909
|
# puts "#{range} #{start_freq} (#{hz_start})to #{target_freq} (#{hz_target})"
|
|
826
910
|
# gets
|
|
827
|
-
while step_hz_direction.positive? ? hz <= hz_target : hz >= hz_target
|
|
911
|
+
# while step_hz_direction.positive? ? hz <= hz_target : hz >= hz_target
|
|
912
|
+
while (step_hz_direction.positive? && hz <= hz_target) || (step_hz_direction.negative? && hz >= hz_target)
|
|
828
913
|
tune_to(gqrx_sock: gqrx_sock, hz: hz)
|
|
829
914
|
strength_db = measure_signal_strength(
|
|
830
915
|
gqrx_sock: gqrx_sock,
|
|
@@ -870,6 +955,7 @@ module PWN
|
|
|
870
955
|
)
|
|
871
956
|
prev_freq_obj[:strength_lock] = strength_lock
|
|
872
957
|
prev_freq_obj[:strength_db] = best_strength_db.round(1)
|
|
958
|
+
prev_freq_obj[:iteration] = iteration_total
|
|
873
959
|
|
|
874
960
|
system_role_content = "Analyze signal data captured by a software-defined-radio using GQRX at the following location: #{location}. Respond with just FCC information about the transmission if available. If the frequency is unlicensed or not found in FCC records, state that clearly. Be clear and concise in your analysis."
|
|
875
961
|
ai_analysis = PWN::AI::Introspection.reflect_on(
|
|
@@ -882,9 +968,9 @@ module PWN
|
|
|
882
968
|
puts JSON.pretty_generate(prev_freq_obj)
|
|
883
969
|
puts '-' * 86
|
|
884
970
|
puts "\n\n\n"
|
|
885
|
-
|
|
971
|
+
signals_detected.push(prev_freq_obj)
|
|
886
972
|
log_signals(
|
|
887
|
-
|
|
973
|
+
signals_detected: signals_detected,
|
|
888
974
|
timestamp_start: timestamp_start,
|
|
889
975
|
scan_log: scan_log
|
|
890
976
|
)
|
|
@@ -897,20 +983,53 @@ module PWN
|
|
|
897
983
|
end
|
|
898
984
|
|
|
899
985
|
log_signals(
|
|
900
|
-
|
|
986
|
+
signals_detected: signals_detected,
|
|
901
987
|
timestamp_start: timestamp_start,
|
|
902
988
|
scan_log: scan_log
|
|
903
989
|
)
|
|
904
990
|
end
|
|
905
991
|
break unless keep_looping
|
|
906
992
|
|
|
907
|
-
|
|
908
|
-
|
|
993
|
+
# Determine how many new signals were detected this iteration
|
|
994
|
+
# Reduces signals_detected to an array of unique frequencies only
|
|
995
|
+
signals_detected.uniq! { |s| PWN::SDR.hz_to_i(s[:freq]) }
|
|
996
|
+
signals_detected_total = signals_detected.select { |s| s[:iteration] == iteration_total }.length
|
|
997
|
+
signals_detected_delta = signals_detected_total - signals_detected_delta
|
|
998
|
+
start_next_iteration = case signals_detected_delta
|
|
999
|
+
when 0
|
|
1000
|
+
30
|
|
1001
|
+
when 1..5
|
|
1002
|
+
10
|
|
1003
|
+
else
|
|
1004
|
+
5
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
range_timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
1008
|
+
iter_metrics_hash[:timestamp_end] = range_timestamp_end
|
|
1009
|
+
|
|
1010
|
+
duration = duration_between(timestamp_start: range_timestamp_start, timestamp_end: range_timestamp_end)
|
|
1011
|
+
iter_metrics_hash[:duration] = duration
|
|
1012
|
+
iter_metrics_hash[:signals_detected] = signals_detected_delta
|
|
1013
|
+
|
|
1014
|
+
iteration_metrics.push(iter_metrics_hash)
|
|
1015
|
+
puts "\nScan iteration(s) ##{iteration_total} complete."
|
|
1016
|
+
puts JSON.pretty_generate(iter_metrics_hash)
|
|
1017
|
+
|
|
1018
|
+
puts "Resuming next scan iteration in #{start_next_iteration} seconds. Press CTRL+C to exit"
|
|
1019
|
+
start_next_iteration.times do
|
|
909
1020
|
print '.'
|
|
910
1021
|
sleep 1
|
|
911
1022
|
end
|
|
912
1023
|
puts "\n"
|
|
913
|
-
|
|
1024
|
+
|
|
1025
|
+
# Log current signals one last time just to capture scan iterations accurately
|
|
1026
|
+
iteration_total += 1
|
|
1027
|
+
log_signals(
|
|
1028
|
+
signals_detected: signals_detected,
|
|
1029
|
+
timestamp_start: timestamp_start,
|
|
1030
|
+
scan_log: scan_log,
|
|
1031
|
+
iteration_metrics: iteration_metrics
|
|
1032
|
+
)
|
|
914
1033
|
end
|
|
915
1034
|
rescue Interrupt
|
|
916
1035
|
puts "\nCTRL+C detected - goodbye."
|
|
@@ -940,8 +1059,38 @@ module PWN
|
|
|
940
1059
|
scan_resp[:signals].each do |signal|
|
|
941
1060
|
# puts JSON.pretty_generate(signal)
|
|
942
1061
|
signal[:gqrx_sock] = gqrx_sock
|
|
1062
|
+
|
|
943
1063
|
# This is required to keep connection alive during analysis
|
|
944
1064
|
signal[:keep_alive] = true
|
|
1065
|
+
|
|
1066
|
+
# We do this because we need keep_alive true for init_freq calls below
|
|
1067
|
+
squelch = signal[:squelch]
|
|
1068
|
+
squelch = cmd(gqrx_sock: gqrx_sock, cmd: 'l SQL').to_f if squelch.nil?
|
|
1069
|
+
change_squelch_resp = cmd(
|
|
1070
|
+
gqrx_sock: gqrx_sock,
|
|
1071
|
+
cmd: "L SQL #{squelch}",
|
|
1072
|
+
resp_ok: 'RPRT 0'
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
audio_gain_db = signal[:audio_gain_db] ||= 0.0
|
|
1076
|
+
audio_gain_db = audio_gain_db.to_f
|
|
1077
|
+
audio_gain_db_resp = cmd(
|
|
1078
|
+
gqrx_sock: gqrx_sock,
|
|
1079
|
+
cmd: "L AF #{audio_gain_db}",
|
|
1080
|
+
resp_ok: 'RPRT 0'
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
demodulator_mode = signal[:demodulator_mode] || :WFM
|
|
1084
|
+
mode_str = demodulator_mode.to_s.upcase
|
|
1085
|
+
|
|
1086
|
+
bandwidth = signal[:bandwidth] ||= '200.000'
|
|
1087
|
+
passband_hz = PWN::SDR.hz_to_i(bandwidth)
|
|
1088
|
+
cmd(
|
|
1089
|
+
gqrx_sock: gqrx_sock,
|
|
1090
|
+
cmd: "M #{mode_str} #{passband_hz}",
|
|
1091
|
+
resp_ok: 'RPRT 0'
|
|
1092
|
+
)
|
|
1093
|
+
|
|
945
1094
|
freq_obj = init_freq(signal)
|
|
946
1095
|
freq_obj = signal.merge(freq_obj)
|
|
947
1096
|
# Redact gqrx_sock from output
|
|
@@ -986,6 +1135,96 @@ module PWN
|
|
|
986
1135
|
raise e
|
|
987
1136
|
end
|
|
988
1137
|
|
|
1138
|
+
# Supported Method Parameters::
|
|
1139
|
+
# udp_listener = PWN::SDR::GQRX.listen_udp(
|
|
1140
|
+
# udp_ip: 'optional - IP address to bind UDP listener (defaults to 127.0.0.1)',
|
|
1141
|
+
# upd_port: 'optional - Port to bind UDP listener (defaults to 7355)'
|
|
1142
|
+
# )
|
|
1143
|
+
|
|
1144
|
+
public_class_method def self.listen_udp(opts = {})
|
|
1145
|
+
udp_ip = opts[:udp_ip] ||= '127.0.0.1'
|
|
1146
|
+
udp_port = opts[:udp_port] ||= 7355
|
|
1147
|
+
|
|
1148
|
+
PWN::Plugins::Sock.listen(
|
|
1149
|
+
server_ip: udp_ip,
|
|
1150
|
+
port: udp_port,
|
|
1151
|
+
protocol: :udp,
|
|
1152
|
+
detach: true
|
|
1153
|
+
)
|
|
1154
|
+
rescue StandardError => e
|
|
1155
|
+
raise e
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
# Supported Method Parameters::
|
|
1159
|
+
# PWN::SDR::GQRX.disconnect_udp(
|
|
1160
|
+
# udp_listener: 'required - UDP socket object returned from #listen_udp method'
|
|
1161
|
+
# )
|
|
1162
|
+
|
|
1163
|
+
public_class_method def self.disconnect_udp(opts = {})
|
|
1164
|
+
udp_listener = opts[:udp_listener]
|
|
1165
|
+
raise 'ERROR: udp_sock is required!' if udp_listener.nil?
|
|
1166
|
+
|
|
1167
|
+
PWN::Plugins::Sock.disconnect(sock_obj: udp_listener) unless udp_listener.closed?
|
|
1168
|
+
rescue StandardError => e
|
|
1169
|
+
raise e
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
# Supported Method Parameters::
|
|
1173
|
+
# iq_raw_file = PWN::SDR::GQRX.record(
|
|
1174
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
1175
|
+
# )
|
|
1176
|
+
|
|
1177
|
+
public_class_method def self.record(opts = {})
|
|
1178
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
1179
|
+
raise 'ERROR: gqrx_sock is required!' if gqrx_sock.nil?
|
|
1180
|
+
|
|
1181
|
+
# Toggle I/Q RECORD on in GQRX for brevity
|
|
1182
|
+
cmd(
|
|
1183
|
+
gqrx_sock: gqrx_sock,
|
|
1184
|
+
cmd: 'U IQRECORD 0',
|
|
1185
|
+
resp_ok: 'RPRT 0'
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
cmd(
|
|
1189
|
+
gqrx_sock: gqrx_sock,
|
|
1190
|
+
cmd: 'U IQRECORD 1',
|
|
1191
|
+
resp_ok: 'RPRT 0'
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
record_dir = Dir.home
|
|
1195
|
+
iq_raw_file = Dir.glob("#{record_dir}/gqrx_*.raw").max_by { |f| File.mtime(f) }
|
|
1196
|
+
raise 'ERROR: No GQRX .raw I/Q data file found!' unless iq_raw_file
|
|
1197
|
+
|
|
1198
|
+
iq_raw_file
|
|
1199
|
+
rescue StandardError => e
|
|
1200
|
+
raise e
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
# Supported Method Parameters::
|
|
1204
|
+
# PWN::SDR::GQRX.stop_recording(
|
|
1205
|
+
# gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
1206
|
+
# iq_raw_file: 'required - iq_raw_file returned from #connect method'
|
|
1207
|
+
# )
|
|
1208
|
+
|
|
1209
|
+
public_class_method def self.stop_recording(opts = {})
|
|
1210
|
+
gqrx_sock = opts[:gqrx_sock]
|
|
1211
|
+
raise 'ERROR: gqrx_sock is required!' if gqrx_sock.nil?
|
|
1212
|
+
|
|
1213
|
+
iq_raw_file = opts[:iq_raw_file]
|
|
1214
|
+
raise 'ERROR: iq_raw_file is required!' if iq_raw_file.nil?
|
|
1215
|
+
|
|
1216
|
+
# Toggle IQRECORD off
|
|
1217
|
+
cmd(
|
|
1218
|
+
gqrx_sock: gqrx_sock,
|
|
1219
|
+
cmd: 'U IQRECORD 0',
|
|
1220
|
+
resp_ok: 'RPRT 0'
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
FileUtils.rm_f(iq_raw_file)
|
|
1224
|
+
rescue StandardError => e
|
|
1225
|
+
raise e
|
|
1226
|
+
end
|
|
1227
|
+
|
|
989
1228
|
# Supported Method Parameters::
|
|
990
1229
|
# PWN::SDR::GQRX.disconnect(
|
|
991
1230
|
# gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
@@ -1028,7 +1267,6 @@ module PWN
|
|
|
1028
1267
|
demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
|
|
1029
1268
|
bandwidth: 'optional - Bandwidth (defaults to \"200.000\")',
|
|
1030
1269
|
decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
|
|
1031
|
-
record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
|
|
1032
1270
|
suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
|
|
1033
1271
|
keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
|
|
1034
1272
|
)
|
|
@@ -1041,7 +1279,7 @@ module PWN
|
|
|
1041
1279
|
precision: 'optional - Precision (Defaults to 1)',
|
|
1042
1280
|
strength_lock: 'optional - Strength lock (defaults to -70.0)',
|
|
1043
1281
|
squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
|
|
1044
|
-
audio_gain_db: 'optional - Audio gain in dB (defaults to
|
|
1282
|
+
audio_gain_db: 'optional - Audio gain in dB (defaults to 0.0)',
|
|
1045
1283
|
rf_gain: 'optional - RF gain (defaults to 0.0)',
|
|
1046
1284
|
intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
|
|
1047
1285
|
baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
|
|
@@ -1062,6 +1300,24 @@ module PWN
|
|
|
1062
1300
|
port: 'optional - GQRX target port (defaults to 7356)'
|
|
1063
1301
|
)
|
|
1064
1302
|
|
|
1303
|
+
udp_listener = #{self}.listen_udp(
|
|
1304
|
+
udp_ip: 'optional - IP address to bind UDP listener (defaults to 127.0.0.1)',
|
|
1305
|
+
udp_port: 'optional - Port to bind UDP listener (defaults to 7355)'
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
#{self}.disconnect_udp(
|
|
1309
|
+
udp_listener: 'required - UDP socket object returned from #listen_udp method'
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
iq_raw_file = #{self}.record(
|
|
1313
|
+
gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
#{self}.stop_recording(
|
|
1317
|
+
gqrx_sock: 'required - GQRX socket object returned from #connect method',
|
|
1318
|
+
iq_raw_file: 'required - iq_raw_file returned from #record method'
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1065
1321
|
#{self}.disconnect(
|
|
1066
1322
|
gqrx_sock: 'required - GQRX socket object returned from #connect method'
|
|
1067
1323
|
)
|