pwn 0.5.621 → 0.5.626

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Gemfile +1 -1
  4. data/README.md +7 -1
  5. data/bin/pwn_gqrx_scanner +78 -9
  6. data/documentation/Agent-Tool-Registry.md +3 -3
  7. data/documentation/Configuration.md +301 -47
  8. data/documentation/Cron.md +5 -3
  9. data/documentation/Diagrams.md +1 -1
  10. data/documentation/Extrospection.md +472 -26
  11. data/documentation/FFI.md +72 -8
  12. data/documentation/Home.md +4 -4
  13. data/documentation/How-PWN-Works.md +7 -3
  14. data/documentation/Mistakes.md +9 -0
  15. data/documentation/SDR.md +83 -10
  16. data/documentation/Skills-Memory-Learning.md +101 -11
  17. data/documentation/diagrams/agent-tool-registry.svg +56 -55
  18. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  19. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  20. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  21. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  22. data/documentation/diagrams/cron-scheduling.svg +23 -23
  23. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  24. data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -6
  25. data/documentation/diagrams/dot/hardware-hacking.dot +3 -1
  26. data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -2
  27. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +4 -2
  28. data/documentation/diagrams/dot/overall-pwn-architecture.dot +3 -3
  29. data/documentation/diagrams/dot/persistence-filesystem.dot +3 -3
  30. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
  31. data/documentation/diagrams/dot/sdr-radio-flow.dot +39 -16
  32. data/documentation/diagrams/driver-framework.svg +13 -13
  33. data/documentation/diagrams/extrospection-world-awareness.svg +383 -177
  34. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  35. data/documentation/diagrams/hardware-hacking.svg +84 -69
  36. data/documentation/diagrams/history-to-drivers.svg +18 -18
  37. data/documentation/diagrams/memory-skills-detailed.svg +137 -120
  38. data/documentation/diagrams/mistakes-negative-feedback.svg +152 -137
  39. data/documentation/diagrams/network-infra-testing.svg +27 -27
  40. data/documentation/diagrams/overall-pwn-architecture.svg +97 -97
  41. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  42. data/documentation/diagrams/persistence-filesystem.svg +100 -94
  43. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  44. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +254 -207
  45. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  46. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  47. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  48. data/documentation/diagrams/sdr-radio-flow.svg +223 -91
  49. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  50. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  51. data/documentation/diagrams/web-application-testing.svg +26 -26
  52. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  53. data/documentation/pwn-ai-Agent.md +16 -10
  54. data/documentation/pwn_silent_help_learn_demo.gif +0 -0
  55. data/lib/pwn/ai/agent/assembly.rb +1 -1
  56. data/lib/pwn/ai/agent/btc.rb +1 -1
  57. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  58. data/lib/pwn/ai/agent/extrospection.rb +2562 -57
  59. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  60. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  61. data/lib/pwn/ai/agent/learning.rb +16 -16
  62. data/lib/pwn/ai/agent/loop.rb +1 -1
  63. data/lib/pwn/ai/agent/{introspection.rb → reflect.rb} +12 -12
  64. data/lib/pwn/ai/agent/sast.rb +1 -1
  65. data/lib/pwn/ai/agent/tools/extrospection.rb +311 -8
  66. data/lib/pwn/ai/agent/tools/learning.rb +7 -7
  67. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  68. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  69. data/lib/pwn/ai/agent.rb +1 -1
  70. data/lib/pwn/ai/open_ai.rb +10 -6
  71. data/lib/pwn/config.rb +5 -4
  72. data/lib/pwn/ffi/adalm_pluto.rb +498 -0
  73. data/lib/pwn/ffi/fftw.rb +192 -0
  74. data/lib/pwn/ffi/hack_rf.rb +193 -0
  75. data/lib/pwn/ffi/liquid.rb +244 -0
  76. data/lib/pwn/ffi/rtl_sdr.rb +210 -0
  77. data/lib/pwn/ffi/soapy_sdr.rb +177 -0
  78. data/lib/pwn/ffi/stdio.rb +9 -1
  79. data/lib/pwn/ffi/volk.rb +241 -0
  80. data/lib/pwn/ffi.rb +45 -2
  81. data/lib/pwn/plugins/burp_suite.rb +2 -2
  82. data/lib/pwn/sast/pom_version.rb +2 -2
  83. data/lib/pwn/sast/test_case_engine.rb +1 -1
  84. data/lib/pwn/sdr/decoder/adsb.rb +201 -12
  85. data/lib/pwn/sdr/decoder/apt.rb +19 -5
  86. data/lib/pwn/sdr/decoder/base.rb +314 -1
  87. data/lib/pwn/sdr/decoder/bluetooth.rb +91 -18
  88. data/lib/pwn/sdr/decoder/dect.rb +81 -23
  89. data/lib/pwn/sdr/decoder/dsp.rb +229 -15
  90. data/lib/pwn/sdr/decoder/flex.rb +23 -6
  91. data/lib/pwn/sdr/decoder/gps.rb +80 -18
  92. data/lib/pwn/sdr/decoder/gsm.rb +83 -28
  93. data/lib/pwn/sdr/decoder/iridium.rb +80 -18
  94. data/lib/pwn/sdr/decoder/lora.rb +84 -28
  95. data/lib/pwn/sdr/decoder/lte.rb +80 -18
  96. data/lib/pwn/sdr/decoder/morse.rb +23 -6
  97. data/lib/pwn/sdr/decoder/p25.rb +90 -31
  98. data/lib/pwn/sdr/decoder/pager.rb +23 -6
  99. data/lib/pwn/sdr/decoder/pocsag.rb +23 -6
  100. data/lib/pwn/sdr/decoder/rds.rb +219 -52
  101. data/lib/pwn/sdr/decoder/rfid.rb +81 -27
  102. data/lib/pwn/sdr/decoder/rtl433.rb +85 -27
  103. data/lib/pwn/sdr/decoder/rtty.rb +23 -6
  104. data/lib/pwn/sdr/decoder/wifi.rb +86 -32
  105. data/lib/pwn/sdr/decoder/zigbee.rb +88 -35
  106. data/lib/pwn/sdr/decoder.rb +6 -5
  107. data/lib/pwn/sdr/frequency_allocation.rb +124 -81
  108. data/lib/pwn/sdr/gqrx.rb +1513 -165
  109. data/lib/pwn/version.rb +1 -1
  110. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +96 -0
  111. data/spec/lib/pwn/ai/agent/{introspection_spec.rb → reflect_spec.rb} +3 -3
  112. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +8 -24
  113. data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +37 -0
  114. data/spec/lib/pwn/ffi/fftw_spec.rb +26 -0
  115. data/spec/lib/pwn/ffi/hack_rf_spec.rb +25 -0
  116. data/spec/lib/pwn/ffi/liquid_spec.rb +42 -0
  117. data/spec/lib/pwn/ffi/rtl_sdr_spec.rb +24 -0
  118. data/spec/lib/pwn/ffi/soapy_sdr_spec.rb +25 -0
  119. data/spec/lib/pwn/ffi/volk_spec.rb +36 -0
  120. data/spec/lib/pwn/sdr/decoder/base_iq_spec.rb +65 -0
  121. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +50 -4
  122. data/spec/lib/pwn/sdr/decoder/rds_spec.rb +45 -4
  123. data/third_party/pwn_rdoc.jsonl +279 -30
  124. metadata +21 -5
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.621'
4
+ VERSION = '0.5.626'
5
5
  end
@@ -12,4 +12,100 @@ describe PWN::AI::Agent::Extrospection do
12
12
  help_response = PWN::AI::Agent::Extrospection
13
13
  expect(help_response).to respond_to :help
14
14
  end
15
+
16
+ it 'should expose rf_tune as a public RF sense organ' do
17
+ expect(PWN::AI::Agent::Extrospection).to respond_to :rf_tune
18
+ end
19
+
20
+ it 'should expose osint / serial / telecomm / packet / vision / voice sense organs' do
21
+ %i[osint serial_sense telecomm packet_sense vision voice_sense].each do |m|
22
+ expect(PWN::AI::Agent::Extrospection).to respond_to m
23
+ end
24
+ end
25
+
26
+ it 'should auto-detect OSINT kind for common query shapes' do
27
+ det = ->(q) { PWN::AI::Agent::Extrospection.send(:detect_osint_kind, query: q) }
28
+ expect(det.call('8.8.8.8')).to eq :ip
29
+ expect(det.call('example.com')).to eq :domain
30
+ expect(det.call('+13125551212')).to eq :phone
31
+ expect(det.call('Jane Doe')).to eq :person
32
+ expect(det.call('US10123456')).to eq :patent
33
+ expect(det.call('2ABIP-ESP32')).to eq :fcc_id
34
+ expect(det.call('someone@example.com')).to eq :email
35
+ expect(det.call('https://evil.example/path')).to eq :url
36
+ expect(det.call('Acme Robotics LLC')).to eq :company
37
+ expect(det.call('0000320193')).to eq :cik
38
+ expect(det.call('123 Main Street Springfield')).to eq :geo
39
+ expect(det.call('birth record Jane Doe')).to eq :vital_records
40
+ expect(det.call('1HGCM82633A004352')).to eq :vin
41
+ expect(det.call('00:11:22:33:44:55')).to eq :mac
42
+ expect(det.call('W1AW')).to eq :callsign
43
+ expect(det.call('CVE-2021-44228')).to eq :cve
44
+ expect(det.call('NPI 1679576722')).to eq :npi
45
+ end
46
+
47
+ it 'should route new public feeds through osint_dispatch' do
48
+ feeds = %i[
49
+ otx urlhaus threatfox urlscan hackertarget openfda nominatim
50
+ opencorporates courtlistener sec_edgar vital_records
51
+ ipapi_is iplocate ipwhois abuseipdb virustotal greynoise
52
+ certspotter epss cisa_kev nhtsa nppes federal_register
53
+ uk_police callook mac_vendor universities microlink
54
+ agify genderize nationalize haveibeenpwned securitytrails
55
+ ]
56
+ feeds.each do |f|
57
+ # Dispatch should not raise — network failures become error hashes.
58
+ query = case f
59
+ when :epss, :cisa_kev then 'CVE-2021-44228'
60
+ when :nhtsa then '1HGCM82633A004352'
61
+ when :mac_vendor then '00:11:22:33:44:55'
62
+ when :callook then 'W1AW'
63
+ when :nppes then 'John Smith'
64
+ when :agify, :genderize, :nationalize then 'Michael'
65
+ when :uk_police then 'metropolitan'
66
+ else 'example.com'
67
+ end
68
+ res = PWN::AI::Agent::Extrospection.send(
69
+ :osint_dispatch,
70
+ feed: f,
71
+ query: query,
72
+ limit: 1,
73
+ keys: {}
74
+ )
75
+ expect(res).to be_a(Hash)
76
+ end
77
+ end
78
+
79
+ it 'should include expanded DEFAULT_OSINT_FEEDS' do
80
+ feeds = PWN::AI::Agent::Extrospection.const_get(:DEFAULT_OSINT_FEEDS)
81
+ %i[
82
+ otx urlhaus threatfox urlscan openfda nominatim opencorporates
83
+ courtlistener sec_edgar vital_records
84
+ ipapi_is iplocate ipwhois abuseipdb virustotal greynoise
85
+ certspotter epss cisa_kev nhtsa nppes federal_register
86
+ uk_police callook mac_vendor universities microlink
87
+ agify genderize nationalize haveibeenpwned securitytrails
88
+ ].each do |f|
89
+ expect(feeds).to include(f)
90
+ end
91
+ end
92
+
93
+ it 'should skip keyed public-api-lists feeds when no API key is present' do
94
+ %i[abuseipdb virustotal haveibeenpwned securitytrails].each do |f|
95
+ res = PWN::AI::Agent::Extrospection.send(
96
+ :osint_dispatch, feed: f, query: '8.8.8.8', limit: 1, keys: {}
97
+ )
98
+ expect(res).to be_a(Hash)
99
+ expect(res[:skipped]).to eq(true)
100
+ end
101
+ end
102
+
103
+ it 'should select public-api-lists-aware feeds per kind' do
104
+ ip_feeds = PWN::AI::Agent::Extrospection.send(:osint_feeds_for, kind: :ip)
105
+ expect(ip_feeds).to include(:ipapi_is, :iplocate, :abuseipdb, :greynoise)
106
+ cve_feeds = PWN::AI::Agent::Extrospection.send(:osint_feeds_for, kind: :cve)
107
+ expect(cve_feeds).to include(:epss, :cisa_kev)
108
+ vin_feeds = PWN::AI::Agent::Extrospection.send(:osint_feeds_for, kind: :vin)
109
+ expect(vin_feeds).to eq(%i[nhtsa])
110
+ end
15
111
  end
@@ -2,14 +2,14 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe PWN::AI::Agent::Introspection do
5
+ describe PWN::AI::Agent::Reflect do
6
6
  it 'should display information for authors' do
7
- authors_response = PWN::AI::Agent::Introspection
7
+ authors_response = PWN::AI::Agent::Reflect
8
8
  expect(authors_response).to respond_to :authors
9
9
  end
10
10
 
11
11
  it 'should display information for existing help method' do
12
- help_response = PWN::AI::Agent::Introspection
12
+ help_response = PWN::AI::Agent::Reflect
13
13
  expect(help_response).to respond_to :help
14
14
  end
15
15
  end
@@ -1,40 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'pwn/ai/agent/tools/extrospection'
4
5
 
5
- describe 'PWN::AI::Agent::Tools extrospection' do
6
+ describe 'PWN::AI::Agent::Tools::Extrospection' do
6
7
  it 'registers the extro_snapshot tool' do
7
- PWN::AI::Agent::Registry.discover(force: true)
8
8
  expect(PWN::AI::Agent::Registry.lookup(name: 'extro_snapshot')).not_to be_nil
9
9
  end
10
10
 
11
- it 'registers the extro_drift tool' do
12
- PWN::AI::Agent::Registry.discover(force: true)
13
- expect(PWN::AI::Agent::Registry.lookup(name: 'extro_drift')).not_to be_nil
14
- end
15
-
16
11
  it 'registers the extro_observe tool' do
17
- PWN::AI::Agent::Registry.discover(force: true)
18
12
  expect(PWN::AI::Agent::Registry.lookup(name: 'extro_observe')).not_to be_nil
19
13
  end
20
14
 
21
- it 'registers the extro_observations tool' do
22
- PWN::AI::Agent::Registry.discover(force: true)
23
- expect(PWN::AI::Agent::Registry.lookup(name: 'extro_observations')).not_to be_nil
24
- end
25
-
26
- it 'registers the extro_intel tool' do
27
- PWN::AI::Agent::Registry.discover(force: true)
28
- expect(PWN::AI::Agent::Registry.lookup(name: 'extro_intel')).not_to be_nil
29
- end
30
-
31
- it 'registers the extro_correlate tool' do
32
- PWN::AI::Agent::Registry.discover(force: true)
33
- expect(PWN::AI::Agent::Registry.lookup(name: 'extro_correlate')).not_to be_nil
15
+ it 'registers the extro_rf_tune tool' do
16
+ expect(PWN::AI::Agent::Registry.lookup(name: 'extro_rf_tune')).not_to be_nil
34
17
  end
35
18
 
36
- it 'exposes the extrospection toolset in the registry' do
37
- PWN::AI::Agent::Registry.discover(force: true)
38
- expect(PWN::AI::Agent::Registry.toolsets).to include('extrospection')
19
+ it 'registers the six new limb tools' do
20
+ %w[extro_osint extro_serial extro_telecomm extro_packet extro_vision extro_voice].each do |name|
21
+ expect(PWN::AI::Agent::Registry.lookup(name: name)).not_to be_nil
22
+ end
39
23
  end
40
24
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::FFI::AdalmPluto do
6
+ it 'should display information for authors' do
7
+ expect(PWN::FFI::AdalmPluto).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(PWN::FFI::AdalmPluto).to respond_to :help
12
+ end
13
+
14
+ it 'should respond to available?' do
15
+ expect(PWN::FFI::AdalmPluto).to respond_to :available?
16
+ end
17
+
18
+ it 'should report library info when libiio is present' do
19
+ skip 'libiio not installed' unless PWN::FFI::AdalmPluto.available?
20
+
21
+ info = PWN::FFI::AdalmPluto.info
22
+ expect(info[:available]).to eq(true)
23
+ expect(info[:major]).to be_a(Integer)
24
+ expect(info[:minor]).to be_a(Integer)
25
+ end
26
+
27
+ it 'should list URIs (possibly empty) when libiio is present' do
28
+ skip 'libiio not installed' unless PWN::FFI::AdalmPluto.available?
29
+
30
+ list = PWN::FFI::AdalmPluto.list_uris
31
+ expect(list).to be_a(Array)
32
+ end
33
+
34
+ it 'should appear in PWN::FFI.backends' do
35
+ expect(PWN::FFI.backends).to have_key(:AdalmPluto)
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::FFI::FFTW do
6
+ it 'should display information for authors' do
7
+ expect(PWN::FFI::FFTW).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(PWN::FFI::FFTW).to respond_to :help
12
+ end
13
+
14
+ it 'should respond to available?' do
15
+ expect(PWN::FFI::FFTW).to respond_to :available?
16
+ end
17
+
18
+ it 'should compute an rfft impulse response when libfftw3f is present' do
19
+ skip 'libfftw3f not installed' unless PWN::FFI::FFTW.available?
20
+
21
+ spec = PWN::FFI::FFTW.rfft(samples: [1.0, 0, 0, 0, 0, 0, 0, 0])
22
+ expect(spec.length).to eq(5) # n/2+1
23
+ expect(spec[0][0]).to be_within(1e-5).of(1.0)
24
+ expect(spec[0][1]).to be_within(1e-5).of(0.0)
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::FFI::HackRF do
6
+ it 'should display information for authors' do
7
+ expect(PWN::FFI::HackRF).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(PWN::FFI::HackRF).to respond_to :help
12
+ end
13
+
14
+ it 'should respond to available?' do
15
+ expect(PWN::FFI::HackRF).to respond_to :available?
16
+ end
17
+
18
+ it 'should report library info when libhackrf is present' do
19
+ skip 'libhackrf not installed' unless PWN::FFI::HackRF.available?
20
+
21
+ info = PWN::FFI::HackRF.info
22
+ expect(info[:available]).to eq(true)
23
+ expect(info[:library_version]).to be_a(String)
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::FFI::Liquid do
6
+ it 'should display information for authors' do
7
+ expect(PWN::FFI::Liquid).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(PWN::FFI::Liquid).to respond_to :help
12
+ end
13
+
14
+ it 'should respond to available?' do
15
+ expect(PWN::FFI::Liquid).to respond_to :available?
16
+ end
17
+
18
+ it 'should resample at half rate when libliquid is present' do
19
+ skip 'libliquid not installed' unless PWN::FFI::Liquid.available?
20
+
21
+ samples = Array.new(64) { |i| Math.sin(2 * Math::PI * i / 16.0) }
22
+ out = PWN::FFI::Liquid.resample(samples: samples, rate: 0.5)
23
+ expect(out.length).to be_between(24, 40)
24
+ end
25
+
26
+ it 'should FM-demod a complex tone when libliquid is present' do
27
+ skip 'libliquid not installed' unless PWN::FFI::Liquid.available?
28
+
29
+ iq = []
30
+ phase = 0.0
31
+ dphi = 2 * Math::PI * 0.05
32
+ 64.times do
33
+ iq << Math.cos(phase)
34
+ iq << Math.sin(phase)
35
+ phase += dphi
36
+ end
37
+ audio = PWN::FFI::Liquid.freq_demod(iq: iq, kf: 0.5)
38
+ expect(audio.length).to eq(64)
39
+ # after settle, demod of constant dphi ≈ 0.1 (2*dphi when kf=0.5 → dphi/kf)
40
+ expect(audio[10]).to be_within(0.05).of(0.1)
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::FFI::RTLSdr do
6
+ it 'should display information for authors' do
7
+ expect(PWN::FFI::RTLSdr).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(PWN::FFI::RTLSdr).to respond_to :help
12
+ end
13
+
14
+ it 'should respond to available?' do
15
+ expect(PWN::FFI::RTLSdr).to respond_to :available?
16
+ end
17
+
18
+ it 'should list devices (possibly empty) when librtlsdr is present' do
19
+ skip 'librtlsdr not installed' unless PWN::FFI::RTLSdr.available?
20
+
21
+ list = PWN::FFI::RTLSdr.list_devices
22
+ expect(list).to be_a(Array)
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::FFI::SoapySDR do
6
+ it 'should display information for authors' do
7
+ expect(PWN::FFI::SoapySDR).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(PWN::FFI::SoapySDR).to respond_to :help
12
+ end
13
+
14
+ it 'should respond to available?' do
15
+ expect(PWN::FFI::SoapySDR).to respond_to :available?
16
+ end
17
+
18
+ it 'should report API info when libSoapySDR is present' do
19
+ skip 'libSoapySDR not installed' unless PWN::FFI::SoapySDR.available?
20
+
21
+ info = PWN::FFI::SoapySDR.info
22
+ expect(info[:available]).to eq(true)
23
+ expect(info[:api]).to match(/\d+\.\d+/)
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::FFI::Volk do
6
+ it 'should display information for authors' do
7
+ expect(PWN::FFI::Volk).to respond_to :authors
8
+ end
9
+
10
+ it 'should display information for existing help method' do
11
+ expect(PWN::FFI::Volk).to respond_to :help
12
+ end
13
+
14
+ it 'should respond to available?' do
15
+ expect(PWN::FFI::Volk).to respond_to :available?
16
+ end
17
+
18
+ it 'should unpack s16le to unit-range floats when libvolk is present' do
19
+ skip 'libvolk not installed' unless PWN::FFI::Volk.available?
20
+
21
+ raw = [0, 16_384, -16_384, 32_767].pack('s<*')
22
+ out = PWN::FFI::Volk.unpack_s16le(data: raw)
23
+ expect(out.length).to eq(4)
24
+ expect(out[0]).to be_within(1e-5).of(0.0)
25
+ expect(out[1]).to be_within(1e-3).of(0.5)
26
+ expect(out[2]).to be_within(1e-3).of(-0.5)
27
+ expect(out[3]).to be_within(1e-3).of(1.0)
28
+ end
29
+
30
+ it 'should accumulate floats when libvolk is present' do
31
+ skip 'libvolk not installed' unless PWN::FFI::Volk.available?
32
+
33
+ sum = PWN::FFI::Volk.accumulate(samples: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
34
+ expect(sum).to be_within(1e-4).of(36.0)
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::SDR::Decoder::Base do
6
+ it 'responds to run_iq / resolve_iq_source' do
7
+ expect(PWN::SDR::Decoder::Base).to respond_to(:run_iq)
8
+ expect(PWN::SDR::Decoder::Base).to respond_to(:resolve_iq_source)
9
+ expect(PWN::SDR::Decoder::Base).to respond_to(:unpack_iq)
10
+ expect(PWN::SDR::Decoder::Base).to respond_to(:close_iq_source)
11
+ end
12
+
13
+ it 'resolves a cu8 capture file as an I/Q source' do
14
+ path = '/tmp/pwn_iq_test.cu8'
15
+ File.binwrite(path, ([127, 127] * 1024).pack('C*'))
16
+ src = PWN::SDR::Decoder::Base.resolve_iq_source(
17
+ freq_obj: { freq: 433_920_000 },
18
+ source: :file,
19
+ file: path,
20
+ sample_rate: 250_000
21
+ )
22
+ expect(src).to be_a(Hash)
23
+ expect(src[:kind]).to eq(:file)
24
+ expect(src[:format]).to eq(:cu8)
25
+ chunk = PWN::SDR::Decoder::Base.read_iq_chunk(source: src, bytes: 512)
26
+ expect(chunk.bytesize).to eq(512)
27
+ iq = PWN::SDR::Decoder::Base.unpack_iq(source: src, data: chunk)
28
+ expect(iq.length).to eq(512) # interleaved I/Q floats
29
+ PWN::SDR::Decoder::Base.close_iq_source(source: src)
30
+ ensure
31
+ FileUtils.rm_f(path)
32
+ end
33
+ end
34
+
35
+ describe PWN::SDR::Decoder::DSP do
36
+ it 'unpacks cu8 / cs16 and computes mag_sq / fm_demod_iq' do
37
+ cu8 = [127, 127, 200, 50].pack('C*')
38
+ expect(PWN::SDR::Decoder::DSP.unpack_cu8(data: cu8).length).to eq(4)
39
+ cs16 = [1000, -1000].pack('s<*')
40
+ expect(PWN::SDR::Decoder::DSP.unpack_cs16le(data: cs16).length).to eq(2)
41
+ iq = [0.5, 0.0, 0.0, 0.5, -0.5, 0.0]
42
+ m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: iq)
43
+ expect(m2.length).to eq(3)
44
+ # Liquid freqdem returns n samples; pure-Ruby path returns n-1
45
+ fm = PWN::SDR::Decoder::DSP.fm_demod_iq(iq: iq)
46
+ expect(fm.length).to be_between(2, 3)
47
+ end
48
+ end
49
+
50
+ describe PWN::SDR::Decoder::ADSB do
51
+ it 'decodes Mode-S DF17 identity fields from a bit vector' do
52
+ bits = Array.new(112, 0)
53
+ # DF = 17 (10001)
54
+ [1, 0, 0, 0, 1].each_with_index { |b, i| bits[i] = b }
55
+ # ICAO = 0xABC123 → 24 bits starting at bit 8
56
+ icao = 0xABC123
57
+ 24.times { |i| bits[8 + i] = (icao >> (23 - i)) & 1 }
58
+ # TC = 4 (identity) at bits 32..36
59
+ [0, 0, 1, 0, 0].each_with_index { |b, i| bits[32 + i] = b }
60
+ h = PWN::SDR::Decoder::ADSB.decode_modes(bits: bits)
61
+ expect(h[:df]).to eq(17)
62
+ expect(h[:icao24]).to eq('ABC123')
63
+ expect(h[:type_code]).to eq(4)
64
+ end
65
+ end
@@ -4,12 +4,58 @@ require 'spec_helper'
4
4
 
5
5
  describe PWN::SDR::Decoder::DSP do
6
6
  it 'should display information for authors' do
7
- authors_response = PWN::SDR::Decoder::DSP
8
- expect(authors_response).to respond_to :authors
7
+ expect(PWN::SDR::Decoder::DSP).to respond_to :authors
9
8
  end
10
9
 
11
10
  it 'should display information for existing help method' do
12
- help_response = PWN::SDR::Decoder::DSP
13
- expect(help_response).to respond_to :help
11
+ expect(PWN::SDR::Decoder::DSP).to respond_to :help
12
+ end
13
+
14
+ it 'should expose a native toggle' do
15
+ expect(PWN::SDR::Decoder::DSP).to respond_to :native
16
+ expect(PWN::SDR::Decoder::DSP).to respond_to :native=
17
+ end
18
+
19
+ it 'should unpack s16le to unit-range floats (ruby path)' do
20
+ was = PWN::SDR::Decoder::DSP.native
21
+ PWN::SDR::Decoder::DSP.native = false
22
+ raw = [0, 16_384, -16_384, 32_767].pack('s<*')
23
+ out = PWN::SDR::Decoder::DSP.unpack_s16le(data: raw)
24
+ expect(out[0]).to be_within(1e-6).of(0.0)
25
+ expect(out[1]).to be_within(1e-3).of(0.5)
26
+ expect(out[2]).to be_within(1e-3).of(-0.5)
27
+ expect(out[3]).to be_within(1e-3).of(1.0)
28
+ ensure
29
+ PWN::SDR::Decoder::DSP.native = was
30
+ end
31
+
32
+ it 'should unpack s16le via Volk when available' do
33
+ skip 'libvolk not installed' unless PWN::FFI.available?(mod: :Volk)
34
+
35
+ was = PWN::SDR::Decoder::DSP.native
36
+ PWN::SDR::Decoder::DSP.native = true
37
+ raw = [0, 16_384, -16_384, 32_767].pack('s<*')
38
+ out = PWN::SDR::Decoder::DSP.unpack_s16le(data: raw)
39
+ expect(out[1]).to be_within(1e-3).of(0.5)
40
+ ensure
41
+ PWN::SDR::Decoder::DSP.native = was
42
+ end
43
+
44
+ it 'should resample pure-ruby and liquid paths to comparable length' do
45
+ samples = Array.new(200) { |i| Math.sin(2 * Math::PI * i / 40.0) }
46
+
47
+ was = PWN::SDR::Decoder::DSP.native
48
+ PWN::SDR::Decoder::DSP.native = false
49
+ ruby_out = PWN::SDR::Decoder::DSP.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
50
+ expect(ruby_out.length).to eq(100)
51
+
52
+ if PWN::FFI.available?(mod: :Liquid)
53
+ PWN::SDR::Decoder::DSP.native = true
54
+ liq_out = PWN::SDR::Decoder::DSP.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
55
+ # multi-stage resampler length can differ by a few samples due to delay
56
+ expect(liq_out.length).to be_between(90, 120)
57
+ end
58
+ ensure
59
+ PWN::SDR::Decoder::DSP.native = was
14
60
  end
15
61
  end
@@ -4,12 +4,53 @@ require 'spec_helper'
4
4
 
5
5
  describe PWN::SDR::Decoder::RDS do
6
6
  it 'should display information for authors' do
7
- authors_response = PWN::SDR::Decoder::RDS
8
- expect(authors_response).to respond_to :authors
7
+ expect(PWN::SDR::Decoder::RDS).to respond_to :authors
9
8
  end
10
9
 
11
10
  it 'should display information for existing help method' do
12
- help_response = PWN::SDR::Decoder::RDS
13
- expect(help_response).to respond_to :help
11
+ expect(PWN::SDR::Decoder::RDS).to respond_to :help
12
+ end
13
+
14
+ it 'exposes a non-interactive .sample entry point (agents / automation)' do
15
+ expect(PWN::SDR::Decoder::RDS).to respond_to :sample
16
+ end
17
+
18
+ it 'exposes the interactive .decode entry point (TTY spinner)' do
19
+ expect(PWN::SDR::Decoder::RDS).to respond_to :decode
20
+ end
21
+
22
+ it 'aggregates RDS poll samples into pi/ps_name/radiotext/station' do
23
+ samples = [
24
+ { pi: '0000', ps: '', rt: '' },
25
+ { pi: '131D', ps: 'KBER ', rt: 'KBER 101' },
26
+ { pi: '131D', ps: 'NIRVANA ', rt: 'KBER 101: Nirvana All Apologies' }
27
+ ]
28
+ result = PWN::SDR::Decoder::RDS.send(
29
+ :aggregate,
30
+ samples: samples,
31
+ settle_secs: 8.0
32
+ )
33
+ expect(result[:pi]).to eq('131D')
34
+ expect(result[:station]).to eq('KBER')
35
+ expect(result[:radiotext]).to include('Nirvana')
36
+ expect(result[:samples]).to eq(3)
37
+ expect(result[:settle_secs]).to eq(8.0)
38
+ end
39
+
40
+ it 'returns an error Hash from .sample when gqrx_sock is missing' do
41
+ # sample rescues ArgumentError only after resolve — missing sock raises first
42
+ expect do
43
+ PWN::SDR::Decoder::RDS.sample({})
44
+ end.to raise_error(ArgumentError, /gqrx_sock/)
45
+ end
46
+
47
+ it 'returns an error Hash when the RDS backend refuses enable' do
48
+ sock = Object.new
49
+ # Stub GQRX.cmd so U RDS 1 fails
50
+ allow(PWN::SDR::GQRX).to receive(:cmd).and_raise(StandardError.new('no rds'))
51
+ result = PWN::SDR::Decoder::RDS.sample(gqrx_sock: sock, settle_secs: 0.5)
52
+ expect(result).to be_a(Hash)
53
+ expect(result[:error]).to match(/RDS not supported/)
54
+ expect(result[:samples]).to eq(0)
14
55
  end
15
56
  end