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.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/README.md +14 -6
- data/bin/pwn_gqrx_scanner +25 -5
- data/documentation/Agent-Tool-Registry.md +3 -3
- data/documentation/Diagrams.md +6 -2
- data/documentation/Extrospection.md +30 -11
- data/documentation/Home.md +5 -4
- data/documentation/How-PWN-Works.md +7 -4
- data/documentation/Mistakes.md +96 -0
- data/documentation/Persistence.md +3 -1
- data/documentation/SDR.md +23 -5
- data/documentation/Skills-Memory-Learning.md +15 -5
- data/documentation/diagrams/agent-tool-registry.svg +94 -92
- data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +9 -7
- data/documentation/diagrams/dot/memory-skills-detailed.dot +12 -5
- data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +3 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +19 -7
- data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
- data/documentation/diagrams/extrospection-world-awareness.svg +111 -93
- data/documentation/diagrams/memory-skills-detailed.svg +146 -97
- data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
- data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
- data/documentation/diagrams/persistence-filesystem.svg +77 -62
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +217 -149
- data/documentation/diagrams/sdr-radio-flow.svg +76 -56
- data/documentation/pwn-ai-Agent.md +28 -15
- data/lib/pwn/ai/agent/extrospection.rb +65 -6
- data/lib/pwn/ai/agent/learning.rb +67 -7
- data/lib/pwn/ai/agent/loop.rb +72 -6
- data/lib/pwn/ai/agent/mistakes.rb +369 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
- data/lib/pwn/ai/agent/tools/extrospection.rb +2 -2
- data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/plugins/jira_data_center.rb +2 -2
- data/lib/pwn/sdr/decoder/adsb.rb +101 -0
- data/lib/pwn/sdr/decoder/apt.rb +75 -0
- data/lib/pwn/sdr/decoder/bluetooth.rb +78 -0
- data/lib/pwn/sdr/decoder/dect.rb +84 -0
- data/lib/pwn/sdr/decoder/gps.rb +83 -0
- data/lib/pwn/sdr/decoder/gsm.rb +50 -29
- data/lib/pwn/sdr/decoder/iridium.rb +81 -0
- data/lib/pwn/sdr/decoder/lora.rb +87 -0
- data/lib/pwn/sdr/decoder/lte.rb +88 -0
- data/lib/pwn/sdr/decoder/morse.rb +64 -0
- data/lib/pwn/sdr/decoder/p25.rb +71 -0
- data/lib/pwn/sdr/decoder/pager.rb +77 -0
- data/lib/pwn/sdr/decoder/pocsag.rb +26 -11
- data/lib/pwn/sdr/decoder/rfid.rb +88 -0
- data/lib/pwn/sdr/decoder/rtl433.rb +95 -0
- data/lib/pwn/sdr/decoder/rtty.rb +66 -0
- data/lib/pwn/sdr/decoder/wifi.rb +98 -0
- data/lib/pwn/sdr/decoder/zigbee.rb +96 -0
- data/lib/pwn/sdr/decoder.rb +67 -7
- data/lib/pwn/sdr/frequency_allocation.rb +82 -41
- data/lib/pwn/sdr/gqrx.rb +186 -95
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
- data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +92 -3
- metadata +44 -6
- 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
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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 (
|
|
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] ||
|
|
1251
|
-
capture_secs = (opts[:capture_secs] || 0.
|
|
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
|
-
#
|
|
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
|
-
|
|
1376
|
-
|
|
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+
|
|
1379
|
-
# min bin separation and >=
|
|
1380
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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] ||
|
|
1501
|
-
cap = opts[:capture_secs] || 0.
|
|
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
|
-
|
|
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
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
#
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
-
|
|
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
|
|
1653
|
-
capture_secs: 'optional - Duration of I/Q capture in seconds (defaults to 0.
|
|
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
|
|
1663
|
-
capture_secs: 'optional - Seconds of capture per chunk (defaults to 0.
|
|
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
|
|
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
|
@@ -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
|