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/ffi.rb CHANGED
@@ -1,13 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ffi'
4
+
5
+ # Top-level alias so PWN::FFI::* modules can `extend PubFFI::Library`
6
+ # without the PWN::FFI ↔ ::FFI (gem) namespace collision. Every binding
7
+ # under lib/pwn/ffi/*.rb reuses this — define exactly once.
8
+ PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
9
+
3
10
  module PWN
4
11
  # This file, using the autoload directive loads FFI modules
5
12
  # into memory only when they're needed. For more information, see:
6
13
  # http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html
14
+ #
15
+ # Bindings under this namespace are *thin* — they attach functions from
16
+ # already-installed system shared objects (libliquid, libvolk, libfftw3f,
17
+ # librtlsdr, libhackrf, libSoapySDR) so PWN::SDR::Decoder::* can run
18
+ # MHz-rate DSP inner loops in native/SIMD code while Ruby stays in charge
19
+ # of orchestration. Nothing here shells out; nothing here compiles at
20
+ # `gem install` time. If a .so is missing the module still loads and
21
+ # `.available?` returns false so callers can fall back to pure Ruby.
7
22
  module FFI
8
- autoload :Stdio, 'pwn/ffi/stdio'
23
+ autoload :AdalmPluto, 'pwn/ffi/adalm_pluto'
24
+ autoload :FFTW, 'pwn/ffi/fftw'
25
+ autoload :HackRF, 'pwn/ffi/hack_rf'
26
+ autoload :Liquid, 'pwn/ffi/liquid'
27
+ autoload :RTLSdr, 'pwn/ffi/rtl_sdr'
28
+ autoload :SoapySDR, 'pwn/ffi/soapy_sdr'
29
+ autoload :Stdio, 'pwn/ffi/stdio'
30
+ autoload :Volk, 'pwn/ffi/volk'
9
31
 
10
- # Display a List of Every PWN::FFI Module
32
+ # Supported Method Parameters::
33
+ # ok = PWN::FFI.available?(
34
+ # mod: 'required - Symbol or Module (e.g. :Liquid or PWN::FFI::Liquid)'
35
+ # )
36
+
37
+ public_class_method def self.available?(opts = {})
38
+ mod = opts[:mod]
39
+ mod = const_get(mod) if mod.is_a?(Symbol) || mod.is_a?(String)
40
+ mod.respond_to?(:available?) && mod.available?
41
+ rescue NameError, LoadError
42
+ false
43
+ end
44
+
45
+ # Supported Method Parameters::
46
+ # PWN::FFI.backends
47
+ # Returns { ModuleName => true|false } for every registered binding.
48
+
49
+ public_class_method def self.backends
50
+ (constants - [:Stdio]).sort.to_h { |c| [c, available?(mod: c)] }
51
+ end
11
52
 
12
53
  # Author(s):: 0day Inc. <support@0dayinc.com>
13
54
 
@@ -15,6 +56,8 @@ module PWN
15
56
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
16
57
  end
17
58
 
59
+ # Display a List of Every PWN::FFI Module
60
+
18
61
  public_class_method def self.help
19
62
  constants.sort
20
63
  end
@@ -50,7 +50,7 @@ module PWN
50
50
  # type: 'required - type of history to introspect (:sitemap, :proxy_history, :websocket_history)'
51
51
  # )
52
52
  private_class_method def self.init_introspection_thread(opts = {})
53
- # if PWN::Env[:ai][:introspection] is true,
53
+ # if PWN::Env[:ai][:module_reflection] is true,
54
54
  # spin up Thread to:
55
55
  # 1. Periodically call get_proxy_history(burp_obj: burp_obj) method
56
56
  # 2. For each entry w/ empty comment,
@@ -65,7 +65,7 @@ module PWN
65
65
  type = opts[:type]
66
66
  raise "ERROR: type parameter is required and must be one of: #{valid_types.join(', ')}" unless valid_types.include?(type)
67
67
 
68
- if PWN::Env[:ai][:introspection]
68
+ if PWN::Env[:ai][:module_reflection]
69
69
  introspection_thread_arr = burp_obj[:introspection_threads] ||= []
70
70
  introspection_thread = Thread.new do
71
71
  get_highlight_color = lambda do |opts = {}|
@@ -19,8 +19,8 @@ module PWN
19
19
  dir_path = opts[:dir_path]
20
20
  git_repo_root_uri = opts[:git_repo_root_uri].to_s.scrub
21
21
  result_arr = []
22
- ai_introspection = PWN::Env[:ai][:introspection]
23
- logger_results = "AI Introspection => #{PWN::Env[:ai][:introspecction]} => "
22
+ ai_module_reflection = PWN::Env[:ai][:module_reflection]
23
+ logger_results = "AI Module Reflection => #{PWN::Env[:ai][:module_reflection]} => "
24
24
 
25
25
  PWN::Plugins::FileFu.recurse_in_dir(dir_path: dir_path) do |entry|
26
26
  if File.file?(entry) && File.basename(entry) == 'pom.xml' && entry !~ /test/i
@@ -63,7 +63,7 @@ module PWN
63
63
  git_repo_root_uri = opts[:git_repo_root_uri].to_s.scrub
64
64
 
65
65
  result_arr = []
66
- logger_results = "AI Introspection => #{PWN::Env[:ai][:introspection]} => "
66
+ logger_results = "AI Module Reflection => #{PWN::Env[:ai][:module_reflection]} => "
67
67
 
68
68
  PWN::Plugins::FileFu.recurse_in_dir(
69
69
  dir_path: dir_path,
@@ -3,14 +3,12 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # Pure-Ruby ADS-B (1090 MHz Mode-S / 978 MHz UAT) activity detector.
6
+ # ADS-B (1090 MHz Mode-S / 978 MHz UAT) true-air decoder.
7
7
  #
8
- # Mode-S Extended Squitter is 2 Mbit/s PPM with 8/120 μs frames on a
9
- # 2 MHz-wide channel physically unrecoverable from GQRX's 48 kHz
10
- # demodulated-audio tap. Rather than shell out to `dump1090`, this
11
- # module runs Base.run_detector to characterise squitter density
12
- # (bursts/sec, peak dBFS, floor) natively in Ruby. `parse_line` is
13
- # retained for offline SBS-1 CSV analysis.
8
+ # Prefer PWN::FFI::{RTLSdr,AdalmPluto,HackRF} at 2 Msps and run a
9
+ # pure-Ruby Mode-S preamble correlator + 112-bit PPM slicer over
10
+ # magnitude samples. Falls back to Base.run_detector energy mode
11
+ # when no I/Q source is available. Offline SBS-1 CSV → .parse_line.
14
12
  module ADSB
15
13
  SBS_FIELDS = %i[
16
14
  msg_type tx_type session_id aircraft_id icao24 flight_id
@@ -19,6 +17,188 @@ module PWN
19
17
  alert emergency spi on_ground
20
18
  ].freeze
21
19
 
20
+ # 8 μs Mode-S preamble at 2 Msps → 16 samples: 1 0 1 0 0 0 0 1 0 1 0 0 0 0 0 0
21
+ PREAMBLE = [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0].map(&:to_f).freeze
22
+ SAMPLES_PER_US = 2 # @ 2 Msps
23
+ # CRC-24 (Mode-S) generator 0x1FFF409 (poly over GF(2), 24-bit)
24
+ MODE_S_CRC_POLY = 0x1FFF409
25
+
26
+ # Streaming I/Q demod for Base.run_iq.
27
+ class DemodIQ
28
+ def initialize(rate: 2_000_000)
29
+ @rate = rate.to_f
30
+ @spb = @rate / 1_000_000.0 # samples per µs (expect ~2)
31
+ @mag = []
32
+ @seen = {}
33
+ end
34
+
35
+ def feed_iq(samples, rate: nil, &emit)
36
+ @rate = rate.to_f if rate
37
+ @spb = @rate / 1_000_000.0
38
+ m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
39
+ @mag.concat(m2)
40
+ # Scan the entire buffer FIRST so frames that land earlier in a
41
+ # multi-ms chunk are not discarded by the ring-buffer clamp below.
42
+ # Only AFTER scan() has consumed what it can do we cap residual
43
+ # history (frame + preamble headroom ≈ 0.5 ms, keep 50 ms).
44
+ scan(&emit)
45
+ max = (@rate * 0.05).to_i
46
+ @mag.shift(@mag.length - max) if @mag.length > max
47
+ end
48
+
49
+ private
50
+
51
+ def scan
52
+ plen = PREAMBLE.length
53
+ return if @mag.length < plen + (112 * 2)
54
+
55
+ # Sliding correlator for the 4-pulse Mode-S preamble. Require the
56
+ # on-pulse peaks to dominate the off-slots by a clear margin and
57
+ # require DF ∈ known set before emitting (cuts noise catastrophically).
58
+ i = 0
59
+ accepted = 0
60
+ while i < @mag.length - (plen + 224)
61
+ # Pulse peaks at samples 0, 2, 7, 9; valleys at 1,3,4,5,6,8,10..
62
+ p0 = @mag[i]
63
+ p2 = @mag[i + 2]
64
+ p7 = @mag[i + 7]
65
+ p9 = @mag[i + 9]
66
+ peak = (p0 + p2 + p7 + p9) / 4.0
67
+ valley = (
68
+ @mag[i + 1] + @mag[i + 3] + @mag[i + 4] + @mag[i + 5] +
69
+ @mag[i + 6] + @mag[i + 8] + @mag[i + 10] + @mag[i + 12]
70
+ ) / 8.0
71
+ if peak > valley * 2.5 && peak.positive?
72
+ bits = slice_ppm(@mag, i + plen, 112)
73
+ if bits && bits.length == 112
74
+ df = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[0, 5])
75
+ # DF 0/4/5/11/16/17/18/20/21 are the Mode-S set we care about
76
+ if [0, 4, 5, 11, 16, 17, 18, 20, 21].include?(df)
77
+ # ICAO all-zeros / all-ones is almost always garbage
78
+ icao_bits = bits[8, 24]
79
+ icao_int = PWN::SDR::Decoder::DSP.bits_to_int(bits: icao_bits)
80
+ if icao_int.positive? && icao_int != 0xFFFFFF && ADSB.crc_ok?(bits: bits)
81
+ msg = ADSB.decode_modes(bits: bits)
82
+ key = "#{msg[:icao24]}:#{msg[:df]}:#{msg[:raw_hex].to_s[0, 8]}"
83
+ unless @seen[key]
84
+ @seen[key] = true
85
+ @seen.shift if @seen.length > 512
86
+ yield msg
87
+ accepted += 1
88
+ end
89
+ i += plen + 224
90
+ next
91
+ end
92
+ end
93
+ end
94
+ end
95
+ i += 1
96
+ end
97
+ drop = [i - plen, 0].max
98
+ @mag.shift(drop) if drop.positive?
99
+ accepted
100
+ end
101
+
102
+ # Mode-S Pulse-Position Modulation: 1 µs = 2 samples; high-first → 1.
103
+ def slice_ppm(mag, start, nbits)
104
+ bits = Array.new(nbits)
105
+ nbits.times do |b|
106
+ a = start + (b * 2)
107
+ return nil if a + 1 >= mag.length
108
+
109
+ bits[b] = mag[a] >= mag[a + 1] ? 1 : 0
110
+ end
111
+ bits
112
+ end
113
+ end
114
+
115
+ # Supported Method Parameters::
116
+ # crc = PWN::SDR::Decoder::ADSB.crc24(bits: Array<0|1>)
117
+ # CRC over all bits except the final 24 (which hold the parity).
118
+
119
+ public_class_method def self.crc24(opts = {})
120
+ bits = opts[:bits] || []
121
+ return nil if bits.length < 32
122
+
123
+ # Mode-S CRC-24: left-shift register fed by every message bit
124
+ # (including the 24 parity bits). A valid frame leaves residual 0.
125
+ reg = 0
126
+ bits.each do |b|
127
+ reg <<= 1
128
+ reg |= (b & 1)
129
+ reg ^= MODE_S_CRC_POLY if reg.anybits?(0x1000000)
130
+ end
131
+ reg & 0xFFFFFF
132
+ end
133
+
134
+ # Supported Method Parameters::
135
+ # ok = PWN::SDR::Decoder::ADSB.crc_ok?(bits: Array<0|1>)
136
+
137
+ public_class_method def self.crc_ok?(opts = {})
138
+ bits = opts[:bits] || []
139
+ return false unless [56, 112].include?(bits.length)
140
+
141
+ crc24(bits: bits).zero?
142
+ end
143
+
144
+ # Supported Method Parameters::
145
+ # h = PWN::SDR::Decoder::ADSB.decode_modes(bits: Array<0|1> of length 56 or 112)
146
+
147
+ public_class_method def self.decode_modes(opts = {})
148
+ bits = opts[:bits] || []
149
+ return nil unless [56, 112].include?(bits.length)
150
+
151
+ # DF (5) + CA (3) + ICAO (24) + ...
152
+ df = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[0, 5])
153
+ icao = format('%06X', PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[8, 24]))
154
+ out = {
155
+ protocol: 'ADSB',
156
+ df: df,
157
+ icao24: icao,
158
+ bits: bits.length,
159
+ raw_hex: bits.each_slice(4).map { |n| PWN::SDR::Decoder::DSP.bits_to_int(bits: n).to_s(16) }.join.upcase
160
+ }
161
+ # DF17/18 ME field (56 bits starting at bit 32)
162
+ if [17, 18].include?(df) && bits.length >= 88
163
+ tc = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[32, 5])
164
+ out[:type_code] = tc
165
+ if tc.between?(1, 4)
166
+ # aircraft identification — 8× 6-bit AIS chars
167
+ cs = bits[40, 48].each_slice(6).map { |ch| ais_char(code: PWN::SDR::Decoder::DSP.bits_to_int(bits: ch)) }.join.strip
168
+ out[:callsign] = cs
169
+ elsif tc.between?(9, 18) || tc.between?(20, 22)
170
+ out[:altitude_ft] = modes_altitude(bits12: bits[40, 12])
171
+ end
172
+ end
173
+ bits_s = []
174
+ bits_s << "ICAO=#{out[:icao24]}"
175
+ bits_s << "DF=#{df}"
176
+ bits_s << "CS=#{out[:callsign]}" if out[:callsign]
177
+ bits_s << "ALT=#{out[:altitude_ft]}ft" if out[:altitude_ft]
178
+ bits_s << "TC=#{out[:type_code]}" if out[:type_code]
179
+ out[:summary] = "ADSB #{bits_s.join(' ')}"
180
+ out
181
+ end
182
+
183
+ public_class_method def self.ais_char(opts = {})
184
+ code = opts[:code]
185
+ table = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######'
186
+ table[code] || ' '
187
+ end
188
+
189
+ public_class_method def self.modes_altitude(opts = {})
190
+ bits12 = opts[:bits12]
191
+ return nil unless bits12.is_a?(Array) && bits12.length == 12
192
+
193
+ # Gillham / 25 ft encoding — simplified (bit 7 is Q)
194
+ q = bits12[7]
195
+ if q == 1
196
+ n = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits12[0, 7] + bits12[8, 4])
197
+ return (n * 25) - 1000
198
+ end
199
+ nil
200
+ end
201
+
22
202
  # Supported Method Parameters::
23
203
  # PWN::SDR::Decoder::ADSB.decode(
24
204
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -28,10 +208,16 @@ module PWN
28
208
  freq_obj = opts[:freq_obj]
29
209
  hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
30
210
  uat = hz.between?(977_000_000, 979_000_000)
31
- PWN::SDR::Decoder::Base.run_detector(
211
+ proto = uat ? 'ADSB-UAT978' : 'ADSB-1090ES'
212
+ rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
213
+ PWN::SDR::Decoder::Base.run_iq(
32
214
  freq_obj: freq_obj,
33
- protocol: uat ? 'ADSB-UAT978' : 'ADSB-1090ES',
34
- note: '2 Mbit/s PPM squitters exceed the 48 kHz audio-tap Nyquist limit; native mode reports squitter-burst density only. Feed captured SBS-1 CSV to .parse_line for full field decode.',
215
+ protocol: proto,
216
+ sample_rate: rate,
217
+ source: opts[:source],
218
+ file: opts[:file],
219
+ demod: DemodIQ.new(rate: rate),
220
+ note: 'Mode-S 2 Mbit/s PPM — true-air path uses RTL-SDR/Pluto I/Q; detector fallback characterises squitter density only.',
35
221
  describe: proc { |b| { modulation: 'PPM', frame_len_us: 120, classification: b[:duration_ms] < 5 ? 'squitter' : 'interrogation-train' } }
36
222
  )
37
223
  end
@@ -66,11 +252,14 @@ module PWN
66
252
  # Display Usage for this Module
67
253
 
68
254
  public_class_method def self.help
69
- puts "USAGE (ruby-native detector, no external binaries):
255
+ puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
70
256
  #{self}.decode(
71
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
257
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
258
+ source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
259
+ file: 'optional - .cu8/.cs16 capture at ≥2 Msps'
72
260
  )
73
261
 
262
+ #{self}.decode_modes(bits: [0,1,...]) # 56/112 Mode-S bits
74
263
  #{self}.parse_line(line: 'MSG,3,1,1,ABCDEF,1,...')
75
264
 
76
265
  #{self}.authors
@@ -102,11 +102,25 @@ module PWN
102
102
 
103
103
  public_class_method def self.decode(opts = {})
104
104
  freq_obj = opts[:freq_obj]
105
- PWN::SDR::Decoder::Base.run_native(
106
- freq_obj: freq_obj,
107
- protocol: 'NOAA-APT',
108
- demod: Demod.new
109
- )
105
+ want_iq = opts[:source] || opts[:file] || freq_obj[:iq_source] || freq_obj[:iq_file]
106
+ if want_iq
107
+ PWN::SDR::Decoder::Base.run_iq(
108
+ freq_obj: freq_obj,
109
+ protocol: 'NOAA-APT',
110
+ demod: Demod.new(out_path: opts[:out_path]),
111
+ sample_rate: (opts[:sample_rate] || freq_obj[:iq_rate] || 48_000).to_i,
112
+ source: opts[:source],
113
+ file: opts[:file],
114
+ fm_demod: true,
115
+ note: 'NOAA APT true-air: FM-demod I/Q then 2400 Hz AM envelope → PGM.'
116
+ )
117
+ else
118
+ PWN::SDR::Decoder::Base.run_native(
119
+ freq_obj: freq_obj,
120
+ protocol: 'NOAA-APT',
121
+ demod: Demod.new(out_path: opts[:out_path])
122
+ )
123
+ end
110
124
  end
111
125
 
112
126
  # Author(s):: 0day Inc. <support@0dayinc.com>