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.
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
- puts 'WARNING: RF Gain is not supported by the radio backend.' if e.message.include?('Command: L RF_GAIN')
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?('Command: L RF_GAIN') ||
166
- e.message.include?('Command: L IF_GAIN') ||
167
- e.message.include?('Command: L BB_GAIN')
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
- # strength_lock: 'optional - Strength lock in dBFS to determine signal edges (defaults to -70.0)'
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
- # Suprisingly accurate but takes longer
189
- # `break if strength_db < prev_strength_db` || attempts >= 300
190
- # is VERY accurate with
191
- # `sleep 0.0001`
192
- # but with more sampling == longer time
193
- # break if attempts >= 100 && (strength_lock > strength_db || strength_db < prev_strength_db)
194
-
195
- break if attempts >= 30 && strength_lock > strength_db
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 attempts >= 300 || strength_db < prev_strength_db
206
+ break if strength_measured
198
207
 
199
- sleep 0.001
200
- prev_strength_db = strength_db
208
+ # Sleep a tiny bit to allow strength_db values to fluctuate
209
+ sleep 0.0001
201
210
  end
202
- puts "Strength Measurement Attempts: #{attempts}"
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 'Finding Beginning Edge of Signal...'
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
- strength_lock: strength_lock
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
- side: :left
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 'Finding Ending Edge of Signal...'
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
- strength_lock: strength_lock
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
- side: :right
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 = 90
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 200_000)',
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] ||= 200_000
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
- bandwidth: bandwidth,
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 200_000)',
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] ||= 200_000
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
- step = hz_start > hz_target ? -step_hz : step_hz
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 #{step.abs.to_i.cast_to_pretty_hz} Hz."
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
- while hz <= hz_target
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
- strength_lock: strength_lock
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
- beg_of_signal_hz = candidate_signals.first[:hz]
833
- top_of_signal_hz_idx = (candidate_signals.length - 1) / 2
834
- top_of_signal_hz = candidate_signals[top_of_signal_hz_idx][:hz]
835
- end_of_signal_hz = candidate_signals.last[:hz]
836
- puts 'Candidate Signal(s) Detected:'
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 = end_of_signal_hz
910
+ hz = candidate_signals.last[:hz]
883
911
  # gets
884
912
  end
885
913
  candidate_signals.clear
886
914
  end
887
- hz += step_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 200_000)',
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 200_000)',
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.515'
4
+ VERSION = '0.5.516'
5
5
  end
@@ -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.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)',\nbandwidth: 'optional - Bandwidth in Hz (Defaults to 200_000)',\noverlap_protection: 'optional - Boolean to enable/disable bandwidth overlap protection (defaults to false)',\nprecision: 'optional - Frequency step precision (number of digits; defaults to 1)',\nlock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',\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"}]}
1301
- {"messages":[{"role":"user","content":"PWN::SDR::GQRX.pretty_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.pretty_hz`: "}]}
1302
- {"messages":[{"role":"user","content":"PWN::SDR::GQRX.raw_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.raw_hz`: "}]}
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.515
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.1
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.1
879
+ version: 7.0.2
880
880
  - !ruby/object:Gem::Dependency
881
881
  name: rest-client
882
882
  requirement: !ruby/object:Gem::Requirement