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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/Gemfile +1 -1
- data/README.md +7 -1
- data/bin/pwn_gqrx_scanner +78 -9
- data/documentation/Agent-Tool-Registry.md +3 -3
- data/documentation/Configuration.md +301 -47
- data/documentation/Cron.md +5 -3
- data/documentation/Diagrams.md +1 -1
- data/documentation/Extrospection.md +472 -26
- data/documentation/FFI.md +72 -8
- data/documentation/Home.md +4 -4
- data/documentation/How-PWN-Works.md +7 -3
- data/documentation/Mistakes.md +9 -0
- data/documentation/SDR.md +83 -10
- data/documentation/Skills-Memory-Learning.md +101 -11
- data/documentation/diagrams/agent-tool-registry.svg +56 -55
- data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
- data/documentation/diagrams/aws-cloud-security.svg +25 -25
- data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
- data/documentation/diagrams/code-scanning-sast.svg +25 -25
- data/documentation/diagrams/cron-scheduling.svg +23 -23
- data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -6
- data/documentation/diagrams/dot/hardware-hacking.dot +3 -1
- data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -2
- data/documentation/diagrams/dot/mistakes-negative-feedback.dot +4 -2
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +3 -3
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
- data/documentation/diagrams/dot/sdr-radio-flow.dot +39 -16
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +383 -177
- data/documentation/diagrams/fuzzing-workflow.svg +24 -24
- data/documentation/diagrams/hardware-hacking.svg +84 -69
- data/documentation/diagrams/history-to-drivers.svg +18 -18
- data/documentation/diagrams/memory-skills-detailed.svg +137 -120
- data/documentation/diagrams/mistakes-negative-feedback.svg +152 -137
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +97 -97
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +100 -94
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +254 -207
- data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
- data/documentation/diagrams/reporting-pipeline.svg +18 -18
- data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
- data/documentation/diagrams/sdr-radio-flow.svg +223 -91
- data/documentation/diagrams/sessions-cron-automation.svg +18 -18
- data/documentation/diagrams/swarm-multi-agent.svg +39 -39
- data/documentation/diagrams/web-application-testing.svg +26 -26
- data/documentation/diagrams/zero-day-research-flow.svg +25 -25
- data/documentation/pwn-ai-Agent.md +16 -10
- data/documentation/pwn_silent_help_learn_demo.gif +0 -0
- data/lib/pwn/ai/agent/assembly.rb +1 -1
- data/lib/pwn/ai/agent/btc.rb +1 -1
- data/lib/pwn/ai/agent/burp_suite.rb +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +2562 -57
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/learning.rb +16 -16
- data/lib/pwn/ai/agent/loop.rb +1 -1
- data/lib/pwn/ai/agent/{introspection.rb → reflect.rb} +12 -12
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/tools/extrospection.rb +311 -8
- data/lib/pwn/ai/agent/tools/learning.rb +7 -7
- data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
- data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
- data/lib/pwn/ai/agent.rb +1 -1
- data/lib/pwn/ai/open_ai.rb +10 -6
- data/lib/pwn/config.rb +5 -4
- data/lib/pwn/ffi/adalm_pluto.rb +498 -0
- data/lib/pwn/ffi/fftw.rb +192 -0
- data/lib/pwn/ffi/hack_rf.rb +193 -0
- data/lib/pwn/ffi/liquid.rb +244 -0
- data/lib/pwn/ffi/rtl_sdr.rb +210 -0
- data/lib/pwn/ffi/soapy_sdr.rb +177 -0
- data/lib/pwn/ffi/stdio.rb +9 -1
- data/lib/pwn/ffi/volk.rb +241 -0
- data/lib/pwn/ffi.rb +45 -2
- data/lib/pwn/plugins/burp_suite.rb +2 -2
- data/lib/pwn/sast/pom_version.rb +2 -2
- data/lib/pwn/sast/test_case_engine.rb +1 -1
- data/lib/pwn/sdr/decoder/adsb.rb +201 -12
- data/lib/pwn/sdr/decoder/apt.rb +19 -5
- data/lib/pwn/sdr/decoder/base.rb +314 -1
- data/lib/pwn/sdr/decoder/bluetooth.rb +91 -18
- data/lib/pwn/sdr/decoder/dect.rb +81 -23
- data/lib/pwn/sdr/decoder/dsp.rb +229 -15
- data/lib/pwn/sdr/decoder/flex.rb +23 -6
- data/lib/pwn/sdr/decoder/gps.rb +80 -18
- data/lib/pwn/sdr/decoder/gsm.rb +83 -28
- data/lib/pwn/sdr/decoder/iridium.rb +80 -18
- data/lib/pwn/sdr/decoder/lora.rb +84 -28
- data/lib/pwn/sdr/decoder/lte.rb +80 -18
- data/lib/pwn/sdr/decoder/morse.rb +23 -6
- data/lib/pwn/sdr/decoder/p25.rb +90 -31
- data/lib/pwn/sdr/decoder/pager.rb +23 -6
- data/lib/pwn/sdr/decoder/pocsag.rb +23 -6
- data/lib/pwn/sdr/decoder/rds.rb +219 -52
- data/lib/pwn/sdr/decoder/rfid.rb +81 -27
- data/lib/pwn/sdr/decoder/rtl433.rb +85 -27
- data/lib/pwn/sdr/decoder/rtty.rb +23 -6
- data/lib/pwn/sdr/decoder/wifi.rb +86 -32
- data/lib/pwn/sdr/decoder/zigbee.rb +88 -35
- data/lib/pwn/sdr/decoder.rb +6 -5
- data/lib/pwn/sdr/frequency_allocation.rb +124 -81
- data/lib/pwn/sdr/gqrx.rb +1513 -165
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +96 -0
- data/spec/lib/pwn/ai/agent/{introspection_spec.rb → reflect_spec.rb} +3 -3
- data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +8 -24
- data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +37 -0
- data/spec/lib/pwn/ffi/fftw_spec.rb +26 -0
- data/spec/lib/pwn/ffi/hack_rf_spec.rb +25 -0
- data/spec/lib/pwn/ffi/liquid_spec.rb +42 -0
- data/spec/lib/pwn/ffi/rtl_sdr_spec.rb +24 -0
- data/spec/lib/pwn/ffi/soapy_sdr_spec.rb +25 -0
- data/spec/lib/pwn/ffi/volk_spec.rb +36 -0
- data/spec/lib/pwn/sdr/decoder/base_iq_spec.rb +65 -0
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +50 -4
- data/spec/lib/pwn/sdr/decoder/rds_spec.rb +45 -4
- data/third_party/pwn_rdoc.jsonl +279 -30
- metadata +21 -5
data/lib/pwn/version.rb
CHANGED
|
@@ -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::
|
|
5
|
+
describe PWN::AI::Agent::Reflect do
|
|
6
6
|
it 'should display information for authors' do
|
|
7
|
-
authors_response = PWN::AI::Agent::
|
|
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::
|
|
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
|
|
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
|
|
22
|
-
PWN::AI::Agent::Registry.
|
|
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 '
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|