pwn 0.5.510 → 0.5.512

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
@@ -30,6 +30,43 @@ module PWN
30
30
  end
31
31
  end
32
32
 
33
+ # Supported Method Parameters::
34
+ # scan_resp = PWN::SDR::GQRX.log_signals(
35
+ # signals_arr: 'required - Array of detected signals',
36
+ # timestamp_start: 'required - Scan start timestamp',
37
+ # scan_log: 'required - Path to save detected signals log'
38
+ # )
39
+ private_class_method def self.log_signals(opts = {})
40
+ signals_arr = opts[:signals_arr]
41
+ timestamp_start = opts[:timestamp_start]
42
+ scan_log = opts[:scan_log]
43
+
44
+ signals = signals_arr.sort_by { |s| s[:freq].to_s.raw_hz }
45
+ timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
46
+ duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
47
+ # Convert duration seconds to hours minutes seconds
48
+ hours = (duration_secs / 3600).to_i
49
+ minutes = ((duration_secs % 3600) / 60).to_i
50
+ seconds = (duration_secs % 60).to_i
51
+ duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
52
+
53
+ scan_resp = {
54
+ signals: signals,
55
+ timestamp_start: timestamp_start,
56
+ timestamp_end: timestamp_end,
57
+ duration: duration
58
+ }
59
+
60
+ File.write(
61
+ scan_log,
62
+ JSON.pretty_generate(scan_resp)
63
+ )
64
+
65
+ scan_resp
66
+ rescue StandardError => e
67
+ raise e
68
+ end
69
+
33
70
  # Supported Method Parameters::
34
71
  # gqrx_sock = PWN::SDR::GQRX.connect(
35
72
  # target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
@@ -170,33 +207,48 @@ module PWN
170
207
  end
171
208
 
172
209
  # Supported Method Parameters::
173
- # init_freq_hash = PWN::SDR::GQRX.init_freq(
210
+ # freq_obj = PWN::SDR::GQRX.init_freq(
174
211
  # gqrx_sock: 'required - GQRX socket object returned from #connect method',
175
212
  # freq: 'required - Frequency to set',
176
213
  # demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
177
214
  # bandwidth: 'optional - Bandwidth (defaults to 200_000)',
178
215
  # squelch: 'optional - Squelch level to set (Defaults to current value)',
179
216
  # decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
180
- # record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to Dir.home)',
181
- # decoder_opts: 'optional - Hash of additional options for the decoder',
182
- # suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)'
217
+ # record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
218
+ # suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
219
+ # keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
183
220
  # )
184
221
  public_class_method def self.init_freq(opts = {})
185
222
  gqrx_sock = opts[:gqrx_sock]
186
223
  freq = opts[:freq]
187
- demodulator_mode = opts[:demodulator_mode] ||= 'WFM'
224
+ valid_demodulator_modes = %i[
225
+ AM
226
+ AM_SYNC
227
+ CW
228
+ CWL
229
+ CWU
230
+ FM
231
+ OFF
232
+ LSB
233
+ RAW
234
+ USB
235
+ WFM
236
+ WFM_ST
237
+ WFM_ST_OIRT
238
+ ]
239
+ demodulator_mode = opts[:demodulator_mode] ||= :WFM
240
+ raise "ERROR: Invalid demodulator_mode '#{demodulator_mode}'. Valid modes: #{valid_demodulator_modes.join(', ')}" unless valid_demodulator_modes.include?(demodulator_mode.to_sym)
241
+
188
242
  bandwidth = opts[:bandwidth] ||= 200_000
189
243
  squelch = opts[:squelch]
190
244
  decoder = opts[:decoder]
191
- record_dir = opts[:record_dir] ||= Dir.home
192
- decoder_opts = opts[:decoder_opts] ||= {}
245
+ record_dir = opts[:record_dir] ||= '/tmp'
193
246
  suppress_details = opts[:suppress_details] || false
247
+ keep_alive = opts[:keep_alive] || false
194
248
 
195
249
  raise "ERROR: record_dir '#{record_dir}' does not exist. Please create it or provide a valid path." if decoder && !Dir.exist?(record_dir)
196
250
 
197
- hz = freq.to_s.raw_hz
198
-
199
- if squelch.is_a?(Float) && squelch >= -100.0 && squelch <= 0.0
251
+ if squelch.is_a?(Float) && squelch >= -100.0 && squelch <= 0.0 && !keep_alive
200
252
  change_squelch_resp = gqrx_cmd(
201
253
  gqrx_sock: gqrx_sock,
202
254
  cmd: "L SQL #{squelch}",
@@ -206,18 +258,20 @@ module PWN
206
258
 
207
259
  change_freq_resp = gqrx_cmd(
208
260
  gqrx_sock: gqrx_sock,
209
- cmd: "F #{hz}",
261
+ cmd: "F #{freq.to_s.raw_hz}",
210
262
  resp_ok: 'RPRT 0'
211
263
  )
212
264
 
213
265
  # Set demod mode and bandwidth (always, using defaults if not provided)
214
- mode_str = demodulator_mode.to_s.upcase
215
- passband_hz = bandwidth.to_s.raw_hz
216
- gqrx_cmd(
217
- gqrx_sock: gqrx_sock,
218
- cmd: "M #{mode_str} #{passband_hz}",
219
- resp_ok: 'RPRT 0'
220
- )
266
+ unless keep_alive
267
+ mode_str = demodulator_mode.to_s.upcase
268
+ passband_hz = bandwidth.to_s.raw_hz
269
+ gqrx_cmd(
270
+ gqrx_sock: gqrx_sock,
271
+ cmd: "M #{mode_str} #{passband_hz}",
272
+ resp_ok: 'RPRT 0'
273
+ )
274
+ end
221
275
 
222
276
  # Get demodulator mode n passband
223
277
  demod_n_passband = gqrx_cmd(
@@ -231,9 +285,10 @@ module PWN
231
285
  cmd: 'f'
232
286
  )
233
287
 
234
- init_freq_hash = {
288
+ freq_obj = {
289
+ demodulator_mode: demodulator_mode,
235
290
  demod_mode_n_passband: demod_n_passband,
236
- frequency: current_freq,
291
+ freq: current_freq,
237
292
  bandwidth: bandwidth
238
293
  }
239
294
 
@@ -269,67 +324,67 @@ module PWN
269
324
  cmd: 'l BB_GAIN'
270
325
  )
271
326
 
272
- init_freq_hash[:audio_gain_db] = audio_gain_db
273
- init_freq_hash[:squelch_set] = current_squelch
274
- init_freq_hash[:rf_gain] = rf_gain
275
- init_freq_hash[:if_gain] = if_gain
276
- init_freq_hash[:bb_gain] = bb_gain
277
- init_freq_hash[:strength_db] = strength_db
278
-
279
- # Start recording and decoding if decoder provided
280
- decoder_module = nil
281
- decoder_thread = nil
282
- record_path = nil
283
- if decoder
284
- # Resolve decoder module via case statement for extensibility
285
- case decoder
286
- when :gsm
287
- decoder_module = PWN::SDR::Decoder::GSM
288
- else
289
- raise "ERROR: Unknown decoder key: #{decoder}. Supported: :gsm"
290
- end
327
+ freq_obj[:audio_gain_db] = audio_gain_db
328
+ freq_obj[:squelch_set] = current_squelch
329
+ freq_obj[:rf_gain] = rf_gain
330
+ freq_obj[:if_gain] = if_gain
331
+ freq_obj[:bb_gain] = bb_gain
332
+ freq_obj[:strength_db] = strength_db
333
+ end
291
334
 
292
- # Ensure recording is off before starting
293
- record_status = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'u RECORD')
294
- gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'U RECORD 0', resp_ok: 'RPRT 0') if record_status == '1'
295
-
296
- # Start recording
297
- gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'U RECORD 1', resp_ok: 'RPRT 0')
298
-
299
- # Prepare for decoder
300
- start_time = Time.now
301
- expected_filename = "gqrx_#{start_time.strftime('%Y%m%d_%H%M%S')}_#{current_freq_raw}.wav"
302
- record_path = File.join(record_dir, expected_filename)
303
-
304
- # Build partial gqrx_obj for decoder start
305
- gqrx_obj_partial = {
306
- gqrx_sock: gqrx_sock,
307
- record_path: record_path,
308
- frequency: current_freq,
309
- bandwidth: bandwidth,
310
- demodulator_mode: demodulator_mode
311
- }
312
-
313
- # Initialize and start decoder (module style: .start returns thread)
314
- decoder_thread = decoder_module.start(
315
- gqrx_obj: gqrx_obj_partial,
316
- **decoder_opts
317
- )
318
-
319
- init_freq_hash[:decoder] = decoder
320
- init_freq_hash[:decoder_module] = decoder_module
321
- init_freq_hash[:decoder_thread] = decoder_thread
322
- init_freq_hash[:record_path] = record_path
335
+ # Start recording and decoding if decoder provided
336
+ decoder_module = nil
337
+ decoder_thread = nil
338
+ record_path = nil
339
+ if decoder
340
+ # Resolve decoder module via case statement for extensibility
341
+ case decoder
342
+ when :gsm
343
+ decoder_module = PWN::SDR::Decoder::GSM
344
+ else
345
+ raise "ERROR: Unknown decoder key: #{decoder}. Supported: :gsm"
323
346
  end
347
+
348
+ # Ensure recording is off before starting
349
+ record_status = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'u RECORD')
350
+ gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'U RECORD 0', resp_ok: 'RPRT 0') if record_status == '1'
351
+
352
+ # Start recording
353
+ gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'U RECORD 1', resp_ok: 'RPRT 0')
354
+
355
+ # Prepare for decoder
356
+ start_time = Time.now
357
+ expected_filename = "gqrx_#{start_time.strftime('%Y%m%d_%H%M%S')}_#{current_freq_raw}.wav"
358
+ record_path = File.join(record_dir, expected_filename)
359
+
360
+ # Build partial gqrx_obj for decoder start
361
+ freq_obj[:record_path] = record_path
362
+
363
+ # Initialize and start decoder (module style: .start returns thread)
364
+ freq_obj[:gqrx_sock] = gqrx_sock
365
+ decoder_thread = decoder_module.start(freq_obj: freq_obj)
366
+ freq_obj.delete(:gqrx_sock)
367
+
368
+ freq_obj[:decoder] = decoder
369
+ freq_obj[:decoder_module] = decoder_module
370
+ freq_obj[:decoder_thread] = decoder_thread
371
+ freq_obj[:record_path] = record_path
324
372
  end
325
373
 
326
- init_freq_hash
374
+ freq_obj
327
375
  rescue StandardError => e
328
376
  raise e
329
377
  ensure
330
- # Ensure recording is stopped and decoder is stopped on error
331
- gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'U RECORD 0', resp_ok: 'RPRT 0') if gqrx_sock && decoder
332
- decoder_module.stop(thread: decoder_thread, gqrx_obj: init_freq_hash) if decoder_module && decoder_thread
378
+ # Ensure decoder recording stops
379
+ if decoder
380
+ gqrx_cmd(
381
+ gqrx_sock: gqrx_sock,
382
+ cmd: 'U RECORD 0',
383
+ resp_ok: 'RPRT 0'
384
+ )
385
+ decoder_module.stop(freq_obj: freq_obj)
386
+ end
387
+ disconnect(gqrx_sock: gqrx_sock) if gqrx_sock.is_a?(TCPSocket) && !keep_alive
333
388
  end
334
389
 
335
390
  # Supported Method Parameters::
@@ -344,13 +399,26 @@ module PWN
344
399
  # lock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',
345
400
  # strength_lock: 'optional - Strength lock in dBFS (defaults to -70.0)',
346
401
  # squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
402
+ # audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
403
+ # rf_gain: 'optional - RF gain (defaults to 0.0)',
404
+ # intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
405
+ # baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
406
+ # scan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',
347
407
  # location: 'optional - Location string to include in AI analysis (e.g., "New York, NY", 90210, GPS coords, etc.)'
348
408
  # )
349
409
 
350
410
  public_class_method def self.scan_range(opts = {})
411
+ timestamp_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
412
+ log_timestamp = Time.now.strftime('%Y-%m-%d')
413
+
351
414
  gqrx_sock = opts[:gqrx_sock]
415
+
352
416
  start_freq = opts[:start_freq]
417
+ hz_start = start_freq.to_s.raw_hz
418
+
353
419
  target_freq = opts[:target_freq]
420
+ hz_target = target_freq.to_s.raw_hz
421
+
354
422
  demodulator_mode = opts[:demodulator_mode]
355
423
  bandwidth = opts[:bandwidth] ||= 200_000
356
424
  overlap_protection = opts[:overlap_protection] || false
@@ -358,16 +426,20 @@ module PWN
358
426
  lock_freq_duration = opts[:lock_freq_duration] ||= 0.04
359
427
  strength_lock = opts[:strength_lock] ||= -70.0
360
428
  squelch = opts[:squelch] ||= (strength_lock - 3.0)
429
+ scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{hz_start.pretty_hz}-#{hz_target.pretty_hz}_#{log_timestamp}.json"
361
430
  location = opts[:location] ||= 'United States'
362
431
 
363
- timestamp_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
364
-
365
- hz_start = start_freq.to_s.raw_hz
366
- hz_target = target_freq.to_s.raw_hz
367
432
  step_hz = 10**(precision - 1)
368
433
  step = hz_start > hz_target ? -step_hz : step_hz
369
434
 
370
- # Set demodulator mode & passband once
435
+ # Set squelch once for the scan
436
+ change_squelch_resp = gqrx_cmd(
437
+ gqrx_sock: gqrx_sock,
438
+ cmd: "L SQL #{squelch}",
439
+ resp_ok: 'RPRT 0'
440
+ )
441
+
442
+ # Set demodulator mode & passband once for the scan
371
443
  mode_str = demodulator_mode.to_s.upcase
372
444
  passband_hz = bandwidth.to_s.raw_hz
373
445
  gqrx_cmd(
@@ -376,26 +448,46 @@ module PWN
376
448
  resp_ok: 'RPRT 0'
377
449
  )
378
450
 
379
- # Prime radio at starting frequency
380
- prev_freq_hash = init_freq(
451
+ audio_gain_db = opts[:audio_gain_db] ||= 6.0
452
+ audio_gain_db = audio_gain_db.to_f
453
+ audio_gain_db_resp = PWN::SDR::GQRX.gqrx_cmd(
381
454
  gqrx_sock: gqrx_sock,
382
- freq: start_freq,
383
- demodulator_mode: demodulator_mode,
384
- bandwidth: bandwidth,
385
- squelch: squelch,
386
- suppress_details: true
455
+ cmd: "L AF #{audio_gain_db}",
456
+ resp_ok: 'RPRT 0'
457
+ )
458
+
459
+ rf_gain = opts[:rf_gain] ||= 0.0
460
+ rf_gain = rf_gain.to_f
461
+ rf_gain_resp = PWN::SDR::GQRX.gqrx_cmd(
462
+ gqrx_sock: gqrx_sock,
463
+ cmd: "L RF_GAIN #{rf_gain}",
464
+ resp_ok: 'RPRT 0'
465
+ )
466
+
467
+ intermediate_gain = opts[:intermediate_gain] ||= 32.0
468
+ intermediate_gain = intermediate_gain.to_f
469
+ intermediate_resp = PWN::SDR::GQRX.gqrx_cmd(
470
+ gqrx_sock: gqrx_sock,
471
+ cmd: "L IF_GAIN #{intermediate_gain}",
472
+ resp_ok: 'RPRT 0'
387
473
  )
388
- prev_freq_hash[:lock_freq_duration] = lock_freq_duration
389
- prev_freq_hash[:strength_lock] = strength_lock
474
+
475
+ baseband_gain = opts[:baseband_gain] ||= 10.0
476
+ baseband_gain = baseband_gain.to_f
477
+ baseband_resp = PWN::SDR::GQRX.gqrx_cmd(
478
+ gqrx_sock: gqrx_sock,
479
+ cmd: "L BB_GAIN #{baseband_gain}",
480
+ resp_ok: 'RPRT 0'
481
+ )
482
+
483
+ prev_freq_obj = {}
390
484
 
391
485
  in_signal = false
392
486
  candidate_signals = []
393
487
  strength_history = []
394
488
 
395
- # ──────────────────────────────────────────────────────────────
396
489
  # Adaptive peak finder – trims weakest ends after each pass
397
490
  # Converges quickly to the true center of the bell curve
398
- # ──────────────────────────────────────────────────────────────
399
491
  find_best_peak = lambda do |opts = {}|
400
492
  beg_of_signal_hz = opts[:beg_of_signal_hz].to_s.raw_hz
401
493
  top_of_signal_hz = opts[:top_of_signal_hz].to_s.raw_hz
@@ -497,6 +589,7 @@ module PWN
497
589
  puts "INFO: Scanning from #{hz_start.pretty_hz} to #{hz_target.pretty_hz} in steps of #{step.abs.pretty_hz} Hz.\nIf scans are slow and/or you're experiencing false positives/negatives, consider adjusting:\n1. The SDR's sample rate in GQRX\n\s\s- Click on `Configure I/O devices`.\n\s\s- A lower `Input rate` value seems counter-intuitive but works well (e.g. ADALM PLUTO ~ 1000000).\n2. Adjust the :strength_lock parameter.\n3. Adjust the :lock_freq_duration parameter.\n4. Adjust the :precision parameter.\n5. Disable AI introspection in PWN::Env\nHappy scanning!\n\n"
498
590
 
499
591
  signals_arr = []
592
+ # Format timestamp_start for filename
500
593
  hz_start.step(by: step, to: hz_target) do |hz|
501
594
  gqrx_cmd(gqrx_sock: gqrx_sock, cmd: "F #{hz}")
502
595
  sleep lock_freq_duration
@@ -522,11 +615,11 @@ module PWN
522
615
  top_of_signal_hz = candidate_signals.map { |s| s[:hz] }.max - step_hz
523
616
 
524
617
  skip_signal = false
525
- prev_frequency = prev_freq_hash[:frequency].to_s.raw_hz
526
- distance_from_prev_detected_freq_hz = (beg_of_signal_hz - prev_frequency).abs
618
+ prev_freq = prev_freq_obj[:freq].to_s.raw_hz
619
+ distance_from_prev_detected_freq_hz = (beg_of_signal_hz - prev_freq).abs
527
620
  half_bandwidth = (bandwidth / 2).to_i
528
621
  skip_signal = true if distance_from_prev_detected_freq_hz < half_bandwidth && overlap_protection
529
- puts "Prev Dect Freq: #{prev_frequency} | New Freq Edge: #{beg_of_signal_hz} | Distance from Prev Dect Freq: #{distance_from_prev_detected_freq_hz} Hz | Step Hz: #{step_hz} | Bandwidth: #{bandwidth} Hz | Half Bandwidth: #{half_bandwidth} Hz | Overlap Protection? #{overlap_protection} | Skip Signal? #{skip_signal}"
622
+ puts "Prev Dect Freq: #{prev_freq} | New Freq Edge: #{beg_of_signal_hz} | Distance from Prev Dect Freq: #{distance_from_prev_detected_freq_hz} Hz | Step Hz: #{step_hz} | Bandwidth: #{bandwidth} Hz | Half Bandwidth: #{half_bandwidth} Hz | Overlap Protection? #{overlap_protection} | Skip Signal? #{skip_signal}"
530
623
  next if skip_signal
531
624
 
532
625
  best_peak = find_best_peak.call(
@@ -535,27 +628,33 @@ module PWN
535
628
  )
536
629
 
537
630
  if best_peak[:hz] && best_peak[:strength_db] > strength_lock
538
- prev_freq_hash = init_freq(
631
+ prev_freq_obj = init_freq(
539
632
  gqrx_sock: gqrx_sock,
540
633
  freq: best_peak[:hz],
541
634
  demodulator_mode: demodulator_mode,
542
635
  bandwidth: bandwidth,
543
636
  squelch: squelch,
544
- suppress_details: true
637
+ suppress_details: true,
638
+ keep_alive: true
545
639
  )
546
- prev_freq_hash[:lock_freq_duration] = lock_freq_duration
547
- prev_freq_hash[:strength_lock] = strength_lock
640
+ prev_freq_obj[:lock_freq_duration] = lock_freq_duration
641
+ prev_freq_obj[:strength_lock] = strength_lock
548
642
 
549
643
  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."
550
644
  ai_analysis = PWN::AI::Introspection.reflect_on(
551
- request: prev_freq_hash.to_json,
645
+ request: prev_freq_obj.to_json,
552
646
  system_role_content: system_role_content,
553
647
  suppress_pii_warning: true
554
648
  )
555
- prev_freq_hash[:ai_analysis] = ai_analysis unless ai_analysis.nil?
649
+ prev_freq_obj[:ai_analysis] = ai_analysis unless ai_analysis.nil?
556
650
  puts "\n**** Detected Signal ****"
557
- puts JSON.pretty_generate(prev_freq_hash)
558
- signals_arr.push(prev_freq_hash)
651
+ puts JSON.pretty_generate(prev_freq_obj)
652
+ signals_arr.push(prev_freq_obj)
653
+ log_signals(
654
+ signals_arr: signals_arr,
655
+ timestamp_start: timestamp_start,
656
+ scan_log: scan_log
657
+ )
559
658
  end
560
659
  candidate_signals.clear
561
660
  sleep lock_freq_duration
@@ -564,21 +663,73 @@ module PWN
564
663
  strength_history = []
565
664
  end
566
665
  end
567
- signals = signals_arr.sort_by { |s| s[:frequency].to_s.raw_hz }
568
- timestamp_end = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
569
- duration_secs = Time.parse(timestamp_end) - Time.parse(timestamp_start)
570
- # Convert duration seconds to hours minutes seconds
571
- hours = (duration_secs / 3600).to_i
572
- minutes = ((duration_secs % 3600) / 60).to_i
573
- seconds = (duration_secs % 60).to_i
574
- duration = format('%<hrs>02d:%<mins>02d:%<secs>02d', hrs: hours, mins: minutes, secs: seconds)
575
666
 
576
- {
577
- signals: signals,
667
+ log_signals(
668
+ signals_arr: signals_arr,
578
669
  timestamp_start: timestamp_start,
579
- timestamp_end: timestamp_end,
580
- duration: duration
581
- }
670
+ scan_log: scan_log
671
+ )
672
+ rescue StandardError => e
673
+ raise e
674
+ ensure
675
+ disconnect(gqrx_sock: gqrx_sock)
676
+ end
677
+
678
+ # Supported Method Parameters::
679
+ # PWN::SDR::GQRX.analyze_scan(
680
+ # scan_resp: 'required - Scan response hash returned from #scan_range method',
681
+ # target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
682
+ # port: 'optional - GQRX target port (defaults to 7356)'
683
+ # )
684
+ public_class_method def self.analyze_scan(opts = {})
685
+ scan_resp = opts[:scan_resp]
686
+ raise 'ERROR: scan_resp is required.' if scan_resp.nil? || scan_resp[:signals].nil? || scan_resp[:signals].empty?
687
+
688
+ target = opts[:target]
689
+ port = opts[:port]
690
+ gqrx_sock = connect(
691
+ target: target,
692
+ port: port
693
+ )
694
+
695
+ scan_resp[:signals].each do |signal|
696
+ freq_obj = { gqrx_sock: gqrx_sock, keep_alive: true }
697
+ freq_obj = signal.merge(freq_obj)
698
+ freq_obj = init_freq(freq_obj)
699
+ # Redact gqrx_sock from output
700
+ freq_obj.delete(:gqrx_sock)
701
+ puts JSON.pretty_generate(freq_obj)
702
+ print 'Press [ENTER] to continue...'
703
+ gets
704
+ puts "\n" * 3
705
+ end
706
+ rescue StandardError => e
707
+ raise e
708
+ ensure
709
+ disconnect(gqrx_sock: gqrx_sock)
710
+ end
711
+
712
+ # Supported Method Parameters::
713
+ # PWN::SDR::GQRX.analyze_log(
714
+ # scan_log: 'required - Path to signals log file',
715
+ # target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
716
+ # port: 'optional - GQRX target port (defaults to 7356)'
717
+ # )
718
+ public_class_method def self.analyze_log(opts = {})
719
+ scan_log = opts[:scan_log]
720
+ raise 'ERROR: scan_log path is required.' unless File.exist?(scan_log)
721
+
722
+ scan_resp = JSON.parse(File.read(scan_log), symbolize_names: true)
723
+ raise 'ERROR: No signals found in log.' if scan_resp[:signals].nil? || scan_resp[:signals].empty?
724
+
725
+ target = opts[:target]
726
+ port = opts[:port]
727
+
728
+ analyze_scan(
729
+ scan_resp: scan_resp,
730
+ target: target,
731
+ port: port
732
+ )
582
733
  rescue StandardError => e
583
734
  raise e
584
735
  end
@@ -608,7 +759,7 @@ module PWN
608
759
  public_class_method def self.help
609
760
  puts "USAGE:
610
761
  gqrx_sock = #{self}.connect(
611
- target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
762
+ target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
612
763
  port: 'optional - GQRX target port (defaults to 7356)'
613
764
  )
614
765
 
@@ -618,15 +769,15 @@ module PWN
618
769
  resp_ok: 'optional - Expected response from GQRX to indicate success'
619
770
  )
620
771
 
621
- init_freq_hash = #{self}.init_freq(
772
+ freq_obj = #{self}.init_freq(
622
773
  gqrx_sock: 'required - GQRX socket object returned from #connect method',
623
774
  freq: 'required - Frequency to set',
624
775
  demodulator_mode: 'optional - Demodulator mode (defaults to WFM)',
625
776
  bandwidth: 'optional - Bandwidth (defaults to 200_000)',
626
777
  decoder: 'optional - Decoder key (e.g., :gsm) to start live decoding (starts recording if provided)',
627
- record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to ~/gqrx_recordings)',
628
- decoder_opts: 'optional - Hash of additional options for the decoder',
629
- suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)'
778
+ record_dir: 'optional - Directory where GQRX saves recordings (required if decoder provided; defaults to /tmp/gqrx_recordings)',
779
+ suppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)',
780
+ keep_alive: 'optional - Boolean to keep GQRX connection alive after method completion (defaults to false)'
630
781
  )
631
782
 
632
783
  scan_resp = #{self}.scan_range(
@@ -640,9 +791,26 @@ module PWN
640
791
  lock_freq_duration: 'optional - Lock frequency duration in seconds (defaults to 0.04)',
641
792
  strength_lock: 'optional - Strength lock (defaults to -70.0)',
642
793
  squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
794
+ audio_gain_db: 'optional - Audio gain in dB (defaults to 6.0)',
795
+ rf_gain: 'optional - RF gain (defaults to 0.0)',
796
+ intermediate_gain: 'optional - Intermediate gain (defaults to 32.0)',
797
+ baseband_gain: 'optional - Baseband gain (defaults to 10.0)',
798
+ scan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)',
643
799
  location: 'optional - Location string to include in AI analysis (e.g., \"New York, NY\", 90210, GPS coords, etc.)'
644
800
  )
645
801
 
802
+ #{self}.analyze_scan(
803
+ scan_resp: 'required - Scan response object from #scan_range method',
804
+ target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
805
+ port: 'optional - GQRX target port (defaults to 7356)'
806
+ )
807
+
808
+ #{self}.analyze_log(
809
+ scan_log: 'required - Path to signals log file',
810
+ target: 'optional - GQRX target IP address (defaults to 127.0.0.1)',
811
+ port: 'optional - GQRX target port (defaults to 7356)'
812
+ )
813
+
646
814
  #{self}.disconnect(
647
815
  gqrx_sock: 'required - GQRX socket object returned from #connect method'
648
816
  )
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.510'
4
+ VERSION = '0.5.512'
5
5
  end
@@ -97,4 +97,4 @@ case $os in
97
97
  exit 1
98
98
  esac
99
99
 
100
- rvmsudo /bin/bash --login -c "cd ${pwn_root} && cp etc/userland/${pwn_provider}/metasploit/vagrant.yaml.EXAMPLE etc/userland/${pwn_provider}/metasploit/vagrant.yaml && ./reinstall_pwn_gemset.sh && ./build_pwn_gem.sh && rubocop"
100
+ rvmsudo /bin/bash --login -c "cd ${pwn_root} && cp etc/userland/${pwn_provider}/metasploit/vagrant.yaml.EXAMPLE etc/userland/${pwn_provider}/metasploit/vagrant.yaml && ./reinstall_gemset.sh && ./build_gem.sh && rubocop"
@@ -1287,15 +1287,17 @@
1287
1287
  {"messages":[{"role":"user","content":"PWN::SDR::FlipperZero.request Usage"},{"role":"assistant","content":"`PWN::SDR::FlipperZero.request`: Supported Method Parameters\n\nresponse = PWN::SDR::FlipperZero.request(\n\nflipper_zero_obj: 'required - flipper_zero_obj returned from #connect method',\npayload: 'optional - payload to send to the device (defaults to help)'\n\n)\n"}]}
1288
1288
  {"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.authors Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1289
1289
  {"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.help Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.help`: "}]}
1290
- {"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.load Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.load`: Supported Method Parameters\n\nopts = PWN::SDR::FrequencyAllocation.load(\n\nprofile: 'required - valid FrequencyAllocation profile name returned from #profiles method'\n\n)\n"}]}
1291
1290
  {"messages":[{"role":"user","content":"PWN::SDR::FrequencyAllocation.profiles Usage"},{"role":"assistant","content":"`PWN::SDR::FrequencyAllocation.profiles`: Supported Method Parameters\n\nprofiles = PWN::SDR::FrequencyAllocation.profiles\n"}]}
1291
+ {"messages":[{"role":"user","content":"PWN::SDR::GQRX.analyze_log Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.analyze_log`: Supported Method Parameters\n\nPWN::SDR::GQRX.analyze_log(\n\nscan_log: 'required - Path to signals log file',\ntarget: 'optional - GQRX target IP address (defaults to 127.0.0.1)',\nport: 'optional - GQRX target port (defaults to 7356)'\n\n)\n"}]}
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"}]}
1292
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"}]}
1293
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"}]}
1294
1295
  {"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"}]}
1295
1296
  {"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"}]}
1296
1297
  {"messages":[{"role":"user","content":"PWN::SDR::GQRX.help Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.help`: "}]}
1297
- {"messages":[{"role":"user","content":"PWN::SDR::GQRX.init_freq Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.init_freq`: Supported Method Parameters\n\ninit_freq_hash = 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 Dir.home)',\ndecoder_opts: 'optional - Hash of additional options for the decoder',\nsuppress_details: 'optional - Boolean to include extra frequency details in return hash (defaults to false)'\n\n)\n"}]}
1298
- {"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)',\nlocation: 'optional - Location string to include in AI analysis (e.g., \"New York, NY\", 90210, GPS coords, etc.)'\n\n)\n"}]}
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"}]}
1299
+ {"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"}]}
1299
1301
  {"messages":[{"role":"user","content":"PWN::SDR::GQRX.pretty_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.pretty_hz`: "}]}
1300
1302
  {"messages":[{"role":"user","content":"PWN::SDR::GQRX.raw_hz Usage"},{"role":"assistant","content":"`PWN::SDR::GQRX.raw_hz`: "}]}
1301
1303
  {"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"}]}
@@ -6,4 +6,4 @@ else
6
6
  fi
7
7
 
8
8
  export rvmsudo_secure_path=1
9
- rvmsudo /bin/bash --login -c "cd ${pwn_root} && ./build_pwn_gem.sh"
9
+ rvmsudo /bin/bash --login -c "cd ${pwn_root} && ./build_gem.sh"