pwn 0.5.618 → 0.5.620

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/README.md +14 -6
  4. data/bin/pwn_gqrx_scanner +25 -5
  5. data/documentation/Agent-Tool-Registry.md +3 -3
  6. data/documentation/Diagrams.md +6 -2
  7. data/documentation/Extrospection.md +30 -11
  8. data/documentation/Home.md +5 -4
  9. data/documentation/How-PWN-Works.md +7 -4
  10. data/documentation/Mistakes.md +96 -0
  11. data/documentation/Persistence.md +3 -1
  12. data/documentation/SDR.md +23 -5
  13. data/documentation/Skills-Memory-Learning.md +15 -5
  14. data/documentation/diagrams/agent-tool-registry.svg +94 -92
  15. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  16. data/documentation/diagrams/dot/extrospection-world-awareness.dot +9 -7
  17. data/documentation/diagrams/dot/memory-skills-detailed.dot +12 -5
  18. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
  19. data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
  20. data/documentation/diagrams/dot/persistence-filesystem.dot +3 -2
  21. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +19 -7
  22. data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
  23. data/documentation/diagrams/extrospection-world-awareness.svg +111 -93
  24. data/documentation/diagrams/memory-skills-detailed.svg +146 -97
  25. data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
  26. data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
  27. data/documentation/diagrams/persistence-filesystem.svg +77 -62
  28. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +217 -149
  29. data/documentation/diagrams/sdr-radio-flow.svg +76 -56
  30. data/documentation/pwn-ai-Agent.md +28 -15
  31. data/lib/pwn/ai/agent/extrospection.rb +65 -6
  32. data/lib/pwn/ai/agent/learning.rb +67 -7
  33. data/lib/pwn/ai/agent/loop.rb +72 -6
  34. data/lib/pwn/ai/agent/mistakes.rb +369 -0
  35. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  36. data/lib/pwn/ai/agent/tools/extrospection.rb +2 -2
  37. data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
  38. data/lib/pwn/ai/agent.rb +1 -0
  39. data/lib/pwn/plugins/jira_data_center.rb +2 -2
  40. data/lib/pwn/sdr/decoder/adsb.rb +101 -0
  41. data/lib/pwn/sdr/decoder/apt.rb +75 -0
  42. data/lib/pwn/sdr/decoder/bluetooth.rb +78 -0
  43. data/lib/pwn/sdr/decoder/dect.rb +84 -0
  44. data/lib/pwn/sdr/decoder/gps.rb +83 -0
  45. data/lib/pwn/sdr/decoder/gsm.rb +50 -29
  46. data/lib/pwn/sdr/decoder/iridium.rb +81 -0
  47. data/lib/pwn/sdr/decoder/lora.rb +87 -0
  48. data/lib/pwn/sdr/decoder/lte.rb +88 -0
  49. data/lib/pwn/sdr/decoder/morse.rb +64 -0
  50. data/lib/pwn/sdr/decoder/p25.rb +71 -0
  51. data/lib/pwn/sdr/decoder/pager.rb +77 -0
  52. data/lib/pwn/sdr/decoder/pocsag.rb +26 -11
  53. data/lib/pwn/sdr/decoder/rfid.rb +88 -0
  54. data/lib/pwn/sdr/decoder/rtl433.rb +95 -0
  55. data/lib/pwn/sdr/decoder/rtty.rb +66 -0
  56. data/lib/pwn/sdr/decoder/wifi.rb +98 -0
  57. data/lib/pwn/sdr/decoder/zigbee.rb +96 -0
  58. data/lib/pwn/sdr/decoder.rb +67 -7
  59. data/lib/pwn/sdr/frequency_allocation.rb +82 -41
  60. data/lib/pwn/sdr/gqrx.rb +186 -95
  61. data/lib/pwn/version.rb +1 -1
  62. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
  63. data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
  64. data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
  65. data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
  66. data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
  67. data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
  68. data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
  69. data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
  70. data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
  71. data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
  72. data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
  73. data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
  74. data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
  75. data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
  76. data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
  77. data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
  78. data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
  79. data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
  80. data/third_party/pwn_rdoc.jsonl +92 -3
  81. metadata +44 -6
  82. data/README.md.bak +0 -200
data/lib/pwn/sdr/gqrx.rb CHANGED
@@ -675,24 +675,12 @@ module PWN
675
675
 
676
676
  # Start recording and decoding if decoder provided
677
677
  if decoder
678
- decoder = decoder.to_s.to_sym
679
- decoder_module = nil
680
-
681
- # Resolve decoder module via case statement for extensibility
682
- case decoder
683
- when :flex
684
- decoder_module = PWN::SDR::Decoder::Flex
685
- when :gsm
686
- decoder_module = PWN::SDR::Decoder::GSM
687
- when :pocsag
688
- decoder_module = PWN::SDR::Decoder::POCSAG
689
- when :rds
690
- decoder_module = PWN::SDR::Decoder::RDS
691
- else
692
- raise "ERROR: Unknown decoder key: #{decoder}. Supported: :gsm"
693
- end
678
+ # Resolve decoder module via central registry (see
679
+ # PWN::SDR::Decoder::REGISTRY) so new protocols only need an
680
+ # autoload + REGISTRY entry — no edit here.
681
+ decoder_module = PWN::SDR::Decoder.resolve(decoder: decoder)
694
682
 
695
- # Initialize and start decoder (module style: .start returns thread)
683
+ # Initialize and start decoder (uniform .decode(freq_obj:) API).
696
684
  freq_obj[:gqrx_sock] = gqrx_sock
697
685
  freq_obj[:udp_ip] = udp_ip
698
686
  freq_obj[:udp_port] = udp_port
@@ -1247,8 +1235,8 @@ module PWN
1247
1235
 
1248
1236
  sample_rate = (opts[:sample_rate] || 1_000_000).to_i
1249
1237
  nfft = (opts[:nfft] || 2048).to_i
1250
- avg = (opts[:avg] || 4).to_i
1251
- capture_secs = (opts[:capture_secs] || 0.05).to_f
1238
+ avg = (opts[:avg] || 8).to_i
1239
+ capture_secs = (opts[:capture_secs] || 0.10).to_f
1252
1240
  strength_offset_db = (opts[:strength_offset_db] || 0.0).to_f
1253
1241
 
1254
1242
  num_samples = (sample_rate * capture_secs).to_i
@@ -1370,16 +1358,29 @@ module PWN
1370
1358
  }
1371
1359
  end
1372
1360
 
1373
- # Noise floor: 12th percentile of dB
1361
+ # Null DC / LO-leakage bin and band-edge guard bins BEFORE detection so
1362
+ # they neither skew the noise-floor estimate nor register as phantom
1363
+ # signals at the centre of every retune step.
1364
+ guard = [(nfft * 0.02).to_i, 2].max
1365
+ dc = nfft / 2
1374
1366
  sorted_db = db.sort
1375
- pct_idx = ((sorted_db.length - 1) * 0.12).floor
1376
- noise_floor = sorted_db[pct_idx].to_f
1367
+ median_nf = sorted_db[sorted_db.length / 2].to_f
1368
+ db[dc] = median_nf
1369
+ (0...guard).each do |gi|
1370
+ db[gi] = median_nf
1371
+ db[nfft - 1 - gi] = median_nf
1372
+ end
1373
+
1374
+ # Noise floor: median of dB (robust; 12th-percentile put nf+6dB ~= mean
1375
+ # noise so ~half of noise bins already cleared the height threshold).
1376
+ noise_floor = median_nf
1377
1377
 
1378
- # SDRangel-style peak detection: local maxima above noise_floor+6dB with
1379
- # min bin separation and >= 4 dB prominence.
1380
- height_thr = noise_floor + 6.0
1378
+ # SDRangel-style peak detection: local maxima above noise_floor+10dB with
1379
+ # min bin separation and >= 6 dB prominence. Thresholds are relative to
1380
+ # the MEDIAN noise floor so avg>=8 yields ~0 false positives on pure noise.
1381
+ height_thr = noise_floor + 10.0
1381
1382
  min_dist = [3, (6000.0 / res_hz).to_i].max
1382
- prom_thr = 4.0
1383
+ prom_thr = 6.0
1383
1384
 
1384
1385
  candidates = []
1385
1386
  (1...(nfft - 1)).each do |i|
@@ -1435,19 +1436,7 @@ module PWN
1435
1436
  }
1436
1437
  end
1437
1438
 
1438
- # Fallback: simple threshold if prominence-based detection found nothing
1439
- if signals.empty?
1440
- bins_out.each do |b|
1441
- next unless b[:power_db] > (noise_floor + 7.0)
1442
-
1443
- signals << b.merge(
1444
- hz: b[:freq_hz],
1445
- noise_floor_db: noise_floor.round(2),
1446
- bw_hz: (res_hz * 2).to_i,
1447
- snr_db: (b[:power_db] - noise_floor).round(2)
1448
- )
1449
- end
1450
- end
1439
+ # NOTE: no fallback. A quiet chunk correctly returns signals: [].
1451
1440
 
1452
1441
  {
1453
1442
  center_freq_hz: center_hz,
@@ -1482,12 +1471,28 @@ module PWN
1482
1471
  # avg: 'optional',
1483
1472
  # capture_secs: 'optional',
1484
1473
  # strength_lock: 'optional',
1474
+ # min_snr_db: 'optional - Minimum SNR in dB above per-chunk noise floor to report (defaults to 12.0)',
1475
+ # precision: 'optional - Band-plan channel raster; detections snapped to 10**(precision-1) Hz grid (defaults to 5)',
1476
+ # min_bw_ratio: 'optional - Reject FFT peaks narrower than min_bw_ratio * plan bandwidth as spurs (defaults to 0.30)',
1477
+ # demodulator_mode: 'optional - Demodulator mode to attribute to detections (defaults to WFM)',
1478
+ # bandwidth: 'optional - Passband bandwidth to attribute to detections (defaults to "200.000")',
1479
+ # squelch: 'optional - Squelch level (defaults to strength_lock - 3.0)',
1480
+ # decoder: 'optional - Decoder key (e.g. :gsm) to attribute to detections',
1481
+ # location: 'optional - Location string for AI analysis',
1485
1482
  # keep_spectrum: 'optional - if true return raw spectrum arrays too (large)'
1486
1483
  # )
1487
1484
  #
1488
1485
  # Uses chunk-wise retuning where chunk = sample_rate so that the *entire visible band* (waterfall width)
1489
1486
  # is captured via a single FFT each time rather than point-by-point hops.
1490
1487
  # This yields near real-time panoramic coverage. Update rate is roughly (retune + capture + fft) per chunk.
1488
+ #
1489
+ # Per-signal output shape is INTENTIONALLY IDENTICAL to #scan_range /
1490
+ # #init_freq (:freq, :demodulator_mode, :bandwidth, :strength_db,
1491
+ # :decoder, :squelch, :strength_lock, :iteration, :ai_analysis) so that
1492
+ # #analyze_scan / #analyze_log and downstream decoders behave the same
1493
+ # regardless of which scan mode produced the log. FFT-specific extras
1494
+ # (:hz, :bw_hz, :snr_db, :prominence_db, :noise_floor_db, :chunk_center,
1495
+ # :method) are appended for provenance.
1491
1496
  public_class_method def self.fast_scan_range(opts = {})
1492
1497
  gqrx_sock = opts[:gqrx_sock]
1493
1498
  raise 'gqrx_sock required' if gqrx_sock.nil?
@@ -1496,17 +1501,55 @@ module PWN
1496
1501
  raise 'ranges required as array of hashes' unless ranges.is_a?(Array) && !ranges.empty?
1497
1502
 
1498
1503
  sr = (opts[:sample_rate] || 1_000_000).to_i
1499
- nfft = opts[:nfft] || 2048
1500
- avgs = opts[:avg] || 2
1501
- cap = opts[:capture_secs] || 0.03
1502
- strength_lock = opts[:strength_lock] || -70.0
1504
+ nfft = (opts[:nfft] || 2048).to_i
1505
+ avgs = (opts[:avg] || 8).to_i
1506
+ cap = (opts[:capture_secs] || 0.10).to_f
1507
+ strength_lock = (opts[:strength_lock] || -70.0).to_f
1508
+ min_snr_db = (opts[:min_snr_db] || 12.0).to_f
1503
1509
  keep_spec = opts[:keep_spectrum] ? true : false
1504
- scan_log = opts[:scan_log] || "/tmp/pwn_fastscan_#{Time.now.to_i}.json"
1510
+ res_hz = sr / nfft.to_f
1511
+ demodulator_mode = opts[:demodulator_mode] ||= :WFM
1512
+ bandwidth = opts[:bandwidth] ||= '200.000'
1513
+ squelch = (opts[:squelch] || (strength_lock - 3.0)).to_f
1514
+ decoder = opts[:decoder]
1515
+ location = opts[:location] ||= 'United States'
1516
+ log_timestamp = Time.now.strftime('%Y-%m-%d')
1517
+
1518
+ # ---- Band-plan-aware candidate validation -------------------------
1519
+ # `:precision` and `:bandwidth` come straight from
1520
+ # PWN::SDR::FrequencyAllocation.band_plans. They encode two facts the
1521
+ # raw FFT peak detector cannot know:
1522
+ # 1. The channel *raster* (step_hz = 10**(precision-1)) that real
1523
+ # emitters are aligned to. Detections are snapped to this grid so
1524
+ # the same station seen in overlapping chunks lands on ONE hz.
1525
+ # 2. The expected *occupied bandwidth* of a single legitimate
1526
+ # emitter. Any FFT peak narrower than min_bw_ratio * plan_bw_hz
1527
+ # is a spur / pilot / IMD product, not a channel; any two peaks
1528
+ # closer than ~half a plan_bw_hz are sub-components of the SAME
1529
+ # emitter (e.g. WFM stereo pilot @19k, RDS @57k) and are merged.
1530
+ precision = (opts[:precision] || 5).to_i
1531
+ precision = precision.clamp(1, 12)
1532
+ step_hz = 10**(precision - 1)
1533
+ plan_bw_hz = PWN::SDR.hz_to_i(freq: bandwidth)
1534
+ plan_bw_hz = step_hz if plan_bw_hz.zero?
1535
+ min_bw_ratio = (opts[:min_bw_ratio] || 0.30).to_f
1536
+ min_bw_hz = [(plan_bw_hz * min_bw_ratio).to_i, res_hz.ceil].max
1537
+
1538
+ range_str = ranges.map do |rr|
1539
+ a = PWN::SDR.hz_to_s(freq: PWN::SDR.hz_to_i(freq: rr[:start_freq]))
1540
+ b = PWN::SDR.hz_to_s(freq: PWN::SDR.hz_to_i(freq: rr[:target_freq]))
1541
+ "#{a}-#{b}"
1542
+ end.join('_')
1543
+ scan_log = opts[:scan_log] ||= "/tmp/pwn_sdr_gqrx_scan_#{range_str}_#{log_timestamp}.json"
1505
1544
 
1506
1545
  ts_start = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
1507
1546
  detected = []
1508
1547
  all_specs = [] if keep_spec
1509
1548
 
1549
+ puts "[FAST-SCAN] band-plan gate: raster=#{PWN::SDR.hz_to_s(freq: step_hz)} Hz (precision #{precision}), " \
1550
+ "plan_bw=#{PWN::SDR.hz_to_s(freq: plan_bw_hz)} Hz, min_occupied_bw>=#{PWN::SDR.hz_to_s(freq: min_bw_hz)} Hz, " \
1551
+ "min_snr>=#{min_snr_db} dB"
1552
+
1510
1553
  ranges.each do |r|
1511
1554
  s_hz = PWN::SDR.hz_to_i(freq: r[:start_freq])
1512
1555
  t_hz = PWN::SDR.hz_to_i(freq: r[:target_freq])
@@ -1535,60 +1578,100 @@ module PWN
1535
1578
  all_specs << snap if keep_spec
1536
1579
 
1537
1580
  sigs = snap[:signals] || []
1538
- if sigs.any?
1539
- # Prefer SDRangel-style identified signals (with BW / SNR / noise_floor)
1540
- sigs.each do |sig|
1541
- next if sig[:power_db] && sig[:power_db] < strength_lock
1542
-
1543
- detected << {
1544
- hz: sig[:hz] || sig[:freq_hz],
1545
- freq: sig[:freq],
1546
- strength: sig[:power_db],
1547
- bw_hz: sig[:bw_hz],
1548
- snr_db: sig[:snr_db],
1549
- noise_floor_db: sig[:noise_floor_db],
1550
- chunk_center: PWN::SDR.hz_to_s(freq: h),
1551
- method: :fast_spectrum_sdrangel_like
1552
- }
1553
- end
1554
- else
1555
- # legacy fallback on raw spectrum bins
1556
- snap[:spectrum].each do |bin|
1557
- next if bin[:power_db] < strength_lock
1558
-
1559
- detected << {
1560
- hz: bin[:freq_hz],
1561
- freq: bin[:freq],
1562
- strength: bin[:power_db],
1563
- chunk_center: PWN::SDR.hz_to_s(freq: h),
1564
- method: :fast_spectrum
1565
- }
1566
- end
1581
+ # Gate on SNR (scale-independent) rather than absolute power_db,
1582
+ # because 10*log10(|FFT|^2) is uncalibrated and cannot be compared
1583
+ # against the -70 dBFS S-meter strength_lock without a
1584
+ # user-supplied strength_offset_db. NO fallback: a quiet chunk
1585
+ # correctly contributes zero detections.
1586
+ sigs.each do |sig|
1587
+ next if sig[:snr_db] && sig[:snr_db] < min_snr_db
1588
+ next if sig[:power_db] && sig[:power_db] < strength_lock
1589
+ # Band-plan width gate: reject spurs / pilots / IMD narrower
1590
+ # than a plausible fraction of the expected channel bandwidth.
1591
+ next if sig[:bw_hz] && sig[:bw_hz] < min_bw_hz
1592
+
1593
+ raw_hz = sig[:hz] || sig[:freq_hz]
1594
+ # Snap to the band-plan channel raster so the same emitter seen
1595
+ # in multiple overlapping chunks / at multiple sub-peaks lands
1596
+ # on ONE canonical frequency before dedup.
1597
+ hz = ((raw_hz.to_f / step_hz).round * step_hz).to_i
1598
+ # Shape MUST match #scan_range / #init_freq freq_obj so that
1599
+ # analyze_scan / analyze_log and downstream decoders work
1600
+ # identically regardless of which scan mode produced the log.
1601
+ detected << {
1602
+ freq: PWN::SDR.hz_to_s(freq: hz),
1603
+ demodulator_mode: demodulator_mode,
1604
+ bandwidth: bandwidth,
1605
+ strength_db: sig[:power_db],
1606
+ decoder: decoder,
1607
+ squelch: squelch,
1608
+ strength_lock: strength_lock,
1609
+ iteration: 1,
1610
+ hz: hz,
1611
+ raw_peak_hz: raw_hz,
1612
+ bw_hz: sig[:bw_hz],
1613
+ snr_db: sig[:snr_db],
1614
+ prominence_db: sig[:prominence_db],
1615
+ noise_floor_db: sig[:noise_floor_db],
1616
+ chunk_center: PWN::SDR.hz_to_s(freq: h),
1617
+ method: :fast_spectrum_sdrangel_like
1618
+ }
1567
1619
  end
1568
1620
  h += step * dir
1569
1621
  end
1570
1622
  end
1571
1623
 
1572
- detected.uniq! { |d| d[:hz] }
1624
+ # Cross-chunk / intra-emitter merge. A single legitimate emitter
1625
+ # occupies ~plan_bw_hz, so ANY peaks within half that width (or one
1626
+ # raster step, or half the *measured* width, whichever is largest) are
1627
+ # the same channel. Keep the highest-SNR representative.
1573
1628
  detected.sort_by! { |d| d[:hz] }
1629
+ merged = []
1630
+ detected.each do |d|
1631
+ prev = merged.last
1632
+ tol = [
1633
+ (plan_bw_hz / 2),
1634
+ step_hz,
1635
+ (d[:bw_hz].to_i / 2),
1636
+ (res_hz * 2).ceil
1637
+ ].max
1638
+ if prev && (d[:hz] - prev[:hz]).abs <= tol
1639
+ merged[-1] = d if (d[:snr_db] || -999) > (prev[:snr_db] || -999)
1640
+ else
1641
+ merged << d
1642
+ end
1643
+ end
1644
+ detected = merged
1645
+
1646
+ # Attach AI analysis per detection AFTER merge so we only spend
1647
+ # inference on the deduplicated set (matches #scan_range behaviour).
1648
+ detected.each do |freq_obj|
1649
+ puts "\n**** Detected Signal ****"
1650
+ ai_analysis = PWN::AI::Agent::GQRX.analyze(
1651
+ request: freq_obj.to_json,
1652
+ location: location
1653
+ )
1654
+ freq_obj[:ai_analysis] = ai_analysis unless ai_analysis.nil?
1655
+ puts JSON.pretty_generate(freq_obj)
1656
+ puts '-' * 86
1657
+ rescue StandardError
1658
+ # AI analysis is best-effort; never let it kill the scan.
1659
+ puts JSON.pretty_generate(freq_obj)
1660
+ end
1574
1661
 
1575
- resp = {
1576
- signals: detected,
1577
- total: detected.length,
1578
- spectrums: keep_spec ? all_specs : nil,
1662
+ resp = log_signals(
1663
+ signals_detected: detected,
1579
1664
  timestamp_start: ts_start,
1580
- timestamp_end: Time.now.strftime('%Y-%m-%d %H:%M:%S%z'),
1581
- sample_rate_used: sr,
1582
- nfft: nfft,
1583
- method: :fast_scan_range
1584
- }
1585
-
1665
+ scan_log: scan_log
1666
+ )
1667
+ resp[:spectrums] = all_specs if keep_spec
1668
+ resp[:sample_rate_used] = sr
1669
+ resp[:nfft] = nfft
1670
+ resp[:precision] = precision
1671
+ resp[:plan_bw_hz] = plan_bw_hz
1672
+ resp[:method] = :fast_scan_range
1586
1673
  File.write(scan_log, JSON.pretty_generate(resp))
1587
- begin
1588
- log_signals(signals_detected: detected, timestamp_start: ts_start, scan_log: scan_log)
1589
- rescue StandardError
1590
- nil
1591
- end
1674
+ File.write(scan_log, "\n", mode: 'a')
1592
1675
  resp
1593
1676
  rescue StandardError => e
1594
1677
  raise e
@@ -1649,8 +1732,8 @@ module PWN
1649
1732
  center_freq: 'optional - Center frequency (Hz) for snapshot (defaults to current tuned freq)',
1650
1733
  sample_rate: 'optional - Instantaneous bandwidth / sample rate in Hz (defaults to 1_000_000)',
1651
1734
  nfft: 'optional - FFT bin size (defaults to 2048)',
1652
- avg: 'optional - Number of FFT averages (defaults to 4)',
1653
- capture_secs: 'optional - Duration of I/Q capture in seconds (defaults to 0.05)',
1735
+ avg: 'optional - Number of FFT averages (defaults to 8)',
1736
+ capture_secs: 'optional - Duration of I/Q capture in seconds (defaults to 0.10)',
1654
1737
  strength_offset_db: 'optional - Add this many dB to all power levels (defaults to 0.0)'
1655
1738
  )
1656
1739
 
@@ -1659,12 +1742,20 @@ module PWN
1659
1742
  ranges: 'required - Array of Hash objects with :start_freq and :target_freq keys defining scan ranges',
1660
1743
  sample_rate: 'optional - Chunk size / visible span in Hz (defaults to 1_000_000)',
1661
1744
  nfft: 'optional - FFT size (defaults to 2048)',
1662
- avg: 'optional - Number of averages (defaults to 2)',
1663
- capture_secs: 'optional - Seconds of capture per chunk (defaults to 0.03)',
1664
- strength_lock: 'optional - Minimum signal strength in dBFS to report (defaults to -70.0)',
1745
+ avg: 'optional - Number of averages (defaults to 8)',
1746
+ capture_secs: 'optional - Seconds of capture per chunk (defaults to 0.10)',
1747
+ strength_lock: 'optional - Minimum signal strength in dBFS to report (defaults to -70.0; only meaningful with strength_offset_db calibration)',
1748
+ min_snr_db: 'optional - Minimum SNR in dB above per-chunk noise floor to report (defaults to 12.0)',
1749
+ precision: 'optional - Band-plan channel raster; detections snapped to 10**(precision-1) Hz grid (defaults to 5)',
1750
+ min_bw_ratio: 'optional - Reject FFT peaks narrower than min_bw_ratio * plan bandwidth as spurs (defaults to 0.30)',
1751
+ demodulator_mode: 'optional - Demodulator mode to attribute to detections (defaults to WFM)',
1752
+ bandwidth: 'optional - Passband bandwidth (defaults to "200.000")',
1753
+ squelch: 'optional - Squelch level in dBFS (defaults to strength_lock - 3.0)',
1754
+ decoder: 'optional - Decoder key (e.g. :gsm) to attribute to each detection',
1755
+ location: 'optional - Location string to include in AI analysis',
1665
1756
  keep_spectrum: 'optional - If true, include full spectrum data in result (can be large, defaults to false)',
1666
1757
  strength_offset_db: 'optional - Add this many dB to all power levels (defaults to 0.0)',
1667
- scan_log: 'optional - Path to save fast scan results (defaults to /tmp/pwn_fastscan_<timestamp>.json)'
1758
+ scan_log: 'optional - Path to save detected signals log (defaults to /tmp/pwn_sdr_gqrx_scan_<start_freq>-<target_freq>_<timestamp>.json)'
1668
1759
  )
1669
1760
 
1670
1761
  #{self}.analyze_scan(
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.618'
4
+ VERSION = '0.5.620'
5
5
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::AI::Agent::Mistakes do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::AI::Agent::Mistakes
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::AI::Agent::Mistakes
13
+ expect(help_response).to respond_to :help
14
+ end
15
+
16
+ it 'fingerprints, counts and resolves recurring failures' do
17
+ stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(Dir.mktmpdir, 'mistakes.json'))
18
+ PWN::AI::Agent::Mistakes.reset
19
+ a = PWN::AI::Agent::Mistakes.record(tool: 'shell', error: 'nmpa: command not found at /tmp/x:42')
20
+ b = PWN::AI::Agent::Mistakes.record(tool: 'shell', error: 'nmpa: command not found at /var/y:99')
21
+ expect(a[:signature]).to eq b[:signature]
22
+ top = PWN::AI::Agent::Mistakes.top
23
+ expect(top.first[:count]).to eq 2
24
+ expect(PWN::AI::Agent::Mistakes.to_context).to include('shell')
25
+ PWN::AI::Agent::Mistakes.resolve(signature: a[:signature], fix: 'use `nmap`, not `nmpa`')
26
+ expect(PWN::AI::Agent::Mistakes.top(unresolved_only: true)).to be_empty
27
+ # recurrence re-opens
28
+ PWN::AI::Agent::Mistakes.record(tool: 'shell', error: 'nmpa: command not found')
29
+ expect(PWN::AI::Agent::Mistakes.top(unresolved_only: true).length).to eq 1
30
+ end
31
+
32
+ it 'detects user-correction language' do
33
+ expect(PWN::AI::Agent::Mistakes.correction?(request: "no that's wrong, try again")).to be true
34
+ expect(PWN::AI::Agent::Mistakes.correction?(request: 'please scan 10.0.0.0/24')).to be false
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'PWN::AI::Agent::Tools mistakes' do
6
+ it 'registers mistakes_list / mistakes_resolve / mistakes_reset' do
7
+ PWN::AI::Agent::Registry.discover(force: true)
8
+ %w[mistakes_list mistakes_resolve mistakes_reset].each do |n|
9
+ expect(PWN::AI::Agent::Registry.lookup(name: n)).not_to be_nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::ADSB do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::ADSB
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::ADSB
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::APT do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::APT
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::APT
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::Bluetooth do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::Bluetooth
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::Bluetooth
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::DECT do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::DECT
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::DECT
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::GPS do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::GPS
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::GPS
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::Iridium do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::Iridium
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::Iridium
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::LoRa do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::LoRa
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::LoRa
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::LTE do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::LTE
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::LTE
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::Morse do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::Morse
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::Morse
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::P25 do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::P25
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::P25
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::Pager do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::Pager
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::Pager
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::RFID do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::RFID
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::RFID
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::RTL433 do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::RTL433
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::RTL433
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::RTTY do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::SDR::Decoder::RTTY
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::SDR::Decoder::RTTY
13
+ expect(help_response).to respond_to :help
14
+ end
15
+ end