pwn 0.5.514 → 0.5.516

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