pwn 0.5.620 → 0.5.621

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/documentation/Agent-Tool-Registry.md +2 -2
  4. data/documentation/Cron.md +13 -0
  5. data/documentation/Diagrams.md +1 -1
  6. data/documentation/Extrospection.md +71 -5
  7. data/documentation/Home.md +2 -2
  8. data/documentation/How-PWN-Works.md +1 -1
  9. data/documentation/Mistakes.md +2 -1
  10. data/documentation/Persistence.md +2 -1
  11. data/documentation/Skills-Memory-Learning.md +1 -0
  12. data/documentation/Transparent-Browser.md +18 -1
  13. data/documentation/diagrams/agent-tool-registry.svg +55 -54
  14. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  15. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  16. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  17. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  18. data/documentation/diagrams/cron-scheduling.svg +23 -23
  19. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  20. data/documentation/diagrams/dot/extrospection-world-awareness.dot +29 -8
  21. data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -0
  22. data/documentation/diagrams/dot/overall-pwn-architecture.dot +2 -2
  23. data/documentation/diagrams/dot/persistence-filesystem.dot +2 -1
  24. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
  25. data/documentation/diagrams/driver-framework.svg +13 -13
  26. data/documentation/diagrams/extrospection-world-awareness.svg +229 -99
  27. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  28. data/documentation/diagrams/hardware-hacking.svg +18 -18
  29. data/documentation/diagrams/history-to-drivers.svg +18 -18
  30. data/documentation/diagrams/memory-skills-detailed.svg +124 -101
  31. data/documentation/diagrams/mistakes-negative-feedback.svg +55 -55
  32. data/documentation/diagrams/network-infra-testing.svg +27 -27
  33. data/documentation/diagrams/overall-pwn-architecture.svg +85 -85
  34. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  35. data/documentation/diagrams/persistence-filesystem.svg +94 -85
  36. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  37. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +226 -180
  38. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  39. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  40. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  41. data/documentation/diagrams/sdr-radio-flow.svg +31 -31
  42. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  43. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  44. data/documentation/diagrams/web-application-testing.svg +26 -26
  45. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  46. data/documentation/pwn-ai-Agent.md +1 -1
  47. data/lib/pwn/ai/agent/extrospection.rb +433 -4
  48. data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
  49. data/lib/pwn/sdr/decoder/adsb.rb +14 -33
  50. data/lib/pwn/sdr/decoder/apt.rb +97 -36
  51. data/lib/pwn/sdr/decoder/base.rb +258 -171
  52. data/lib/pwn/sdr/decoder/bluetooth.rb +13 -20
  53. data/lib/pwn/sdr/decoder/dect.rb +13 -27
  54. data/lib/pwn/sdr/decoder/dsp.rb +396 -0
  55. data/lib/pwn/sdr/decoder/flex.rb +177 -210
  56. data/lib/pwn/sdr/decoder/gps.rb +12 -21
  57. data/lib/pwn/sdr/decoder/gsm.rb +21 -73
  58. data/lib/pwn/sdr/decoder/iridium.rb +11 -26
  59. data/lib/pwn/sdr/decoder/lora.rb +16 -23
  60. data/lib/pwn/sdr/decoder/lte.rb +11 -32
  61. data/lib/pwn/sdr/decoder/morse.rb +95 -21
  62. data/lib/pwn/sdr/decoder/p25.rb +19 -16
  63. data/lib/pwn/sdr/decoder/pager.rb +22 -38
  64. data/lib/pwn/sdr/decoder/pocsag.rb +175 -68
  65. data/lib/pwn/sdr/decoder/rfid.rb +24 -33
  66. data/lib/pwn/sdr/decoder/rtl433.rb +21 -30
  67. data/lib/pwn/sdr/decoder/rtty.rb +100 -27
  68. data/lib/pwn/sdr/decoder/wifi.rb +21 -42
  69. data/lib/pwn/sdr/decoder/zigbee.rb +18 -40
  70. data/lib/pwn/sdr/decoder.rb +8 -0
  71. data/lib/pwn/version.rb +1 -1
  72. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
  73. data/third_party/pwn_rdoc.jsonl +24 -3
  74. metadata +3 -1
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'shellwords'
4
-
5
3
  module PWN
6
4
  module SDR
7
5
  module Decoder
8
- # Iridium L-band (1.616–1.6265 GHz) burst decoder. Drives gr-iridium's
9
- # `iridium-extractor` against the SDR to demodulate 25 kbit/s DE-QPSK
10
- # simplex/duplex bursts, then pipes them through iridium-toolkit's
11
- # `iridium-parser.py` for frame classification (IRA/IBC/IDA/ISY/VOC/...).
6
+ # Pure-Ruby Iridium L-band burst detector.
7
+ #
8
+ # Iridium simplex/duplex bursts are 25 kbit/s DE-QPSK across
9
+ # 1616–1626.5 MHz. Native mode reports burst count/duration/energy
10
+ # per channel. `parse_line` retained for offline iridium-toolkit
11
+ # text analysis.
12
12
  module Iridium
13
13
  # Supported Method Parameters::
14
14
  # PWN::SDR::Decoder::Iridium.decode(
@@ -17,24 +17,12 @@ module PWN
17
17
 
18
18
  public_class_method def self.decode(opts = {})
19
19
  freq_obj = opts[:freq_obj]
20
- raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
21
-
22
- hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
23
- gain = (freq_obj[:rf_gain] || 40).to_s.to_f
24
- sdr_args = (freq_obj[:sdr_args] || 'rtl=0').to_s
25
-
26
- inner = "iridium-extractor -D 4 --multi-frame -c #{hz} -r 2000000 " \
27
- "-g #{gain} -o - #{Shellwords.escape(sdr_args)} 2>/dev/null " \
28
- '| iridium-parser.py --harder /dev/stdin'
29
- direct_cmd = "bash -c #{Shellwords.escape(inner)}"
30
-
31
- PWN::SDR::Decoder::Base.run_pipeline(
20
+ PWN::SDR::Decoder::Base.run_detector(
32
21
  freq_obj: freq_obj,
33
22
  protocol: 'IRIDIUM',
34
- required_bins: %w[iridium-extractor iridium-parser.py],
35
- direct_cmd: direct_cmd,
36
- line_match: /^(IRA|IBC|IDA|ISY|ITL|MSG|VOC|RAW):/,
37
- parser: proc { |line| parse_line(line: line) }
23
+ note: '25 kbit/s DE-QPSK — native mode reports burst timing/energy only.',
24
+ threshold: 7.0,
25
+ describe: proc { |b| { modulation: 'DE-QPSK', symbol_rate: 25_000, classification: b[:duration_ms] < 10 ? 'simplex-burst' : 'duplex-frame' } }
38
26
  )
39
27
  end
40
28
 
@@ -62,14 +50,11 @@ module PWN
62
50
  # Display Usage for this Module
63
51
 
64
52
  public_class_method def self.help
65
- puts "USAGE:
53
+ puts "USAGE (ruby-native detector, no external binaries):
66
54
  #{self}.decode(
67
55
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
68
56
  )
69
57
 
70
- NOTE: Requires `iridium-extractor` (gr-iridium) and
71
- `iridium-parser.py` (iridium-toolkit). Owns the SDR.
72
-
73
58
  #{self}.parse_line(line: 'IRA: sat:23 beam:14 pos=(+32.1/-097.0) ...')
74
59
 
75
60
  #{self}.authors
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require 'shellwords'
5
4
 
6
5
  module PWN
7
6
  module SDR
8
7
  module Decoder
9
- # LoRa / LoRaWAN CSS decoder for the 433 / 868 / 902–928 MHz ISM
10
- # allocations. Chirp-spread-spectrum needs raw I/Q, so this drives
11
- # `rtl_433` (which ships a native LoRa demod, protocol #264) directly.
12
- # Emits one JSON line per decoded uplink containing SF/BW/CR, DevAddr,
13
- # FCnt, and raw PHYPayload hex.
8
+ # Pure-Ruby LoRa / LoRaWAN CSS activity detector.
9
+ #
10
+ # LoRa is chirp-spread-spectrum over 125/250/500 kHz recoverable
11
+ # only from raw I/Q at ≥250 ksps, not from a 48 kHz audio tap.
12
+ # Native mode reports chirp-burst duration/energy (from which SF
13
+ # can be estimated). `parse_line` retained for offline JSON analysis.
14
14
  module LoRa
15
15
  # Supported Method Parameters::
16
16
  # PWN::SDR::Decoder::LoRa.decode(
@@ -19,22 +19,17 @@ module PWN
19
19
 
20
20
  public_class_method def self.decode(opts = {})
21
21
  freq_obj = opts[:freq_obj]
22
- raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
23
-
24
- hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
25
- sdr_args = freq_obj[:sdr_args].to_s
26
-
27
- cmd = ['rtl_433', '-f', hz.to_s, '-s', '1024k',
28
- '-R', '264', '-F', 'json', '-M', 'level']
29
- cmd.push('-d', sdr_args) unless sdr_args.empty?
30
-
31
- PWN::SDR::Decoder::Base.run_pipeline(
22
+ PWN::SDR::Decoder::Base.run_detector(
32
23
  freq_obj: freq_obj,
33
24
  protocol: 'LoRa',
34
- required_bins: %w[rtl_433],
35
- direct_cmd: Shellwords.join(cmd),
36
- line_match: /^\s*{/,
37
- parser: proc { |line| parse_line(line: line) }
25
+ note: 'CSS over 125–500 kHz — native mode reports chirp bursts and estimates SF from duration.',
26
+ threshold: 8.0,
27
+ describe: proc { |b|
28
+ # Preamble 8 symbols; T_sym = 2^SF / BW. Assume BW=125k.
29
+ t_sym_ms = b[:duration_ms] / 20.0
30
+ sf_est = t_sym_ms.positive? ? Math.log2(t_sym_ms * 125).round.clamp(6, 12) : nil
31
+ { modulation: 'CSS', bw_khz_assumed: 125, sf_estimate: sf_est }.compact
32
+ }
38
33
  )
39
34
  end
40
35
 
@@ -69,13 +64,11 @@ module PWN
69
64
  # Display Usage for this Module
70
65
 
71
66
  public_class_method def self.help
72
- puts "USAGE:
67
+ puts "USAGE (ruby-native detector, no external binaries):
73
68
  #{self}.decode(
74
69
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
75
70
  )
76
71
 
77
- NOTE: Requires `rtl_433` >= 23.x (native LoRa demod). Owns SDR.
78
-
79
72
  #{self}.parse_line(line: '{\"model\":\"LoRa\",\"sf\":7,\"bw\":125,...}')
80
73
 
81
74
  #{self}.authors
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'shellwords'
4
-
5
3
  module PWN
6
4
  module SDR
7
5
  module Decoder
8
- # LTE / UMTS / CDMA / PCS / AWS wideband cellular cell-search decoder.
6
+ # Pure-Ruby LTE / UMTS / CDMA cellular activity detector.
9
7
  #
10
- # 1.4–20 MHz OFDMA/WCDMA cannot be recovered from GQRX audio; this
11
- # module drives srsRAN's `cell_search` (or `LTE-Cell-Scanner`'s
12
- # `CellSearch`) directly against the SDR to enumerate physical cells:
13
- # EARFCN, PCI, CP type, RSRP/RSRQ, PSS/SSS correlation.
8
+ # 1.4–20 MHz OFDMA/WCDMA is far beyond a 48 kHz audio tap and beyond
9
+ # interpreted Ruby's real-time I/Q throughput. Native mode reports
10
+ # channel occupancy / power only. `parse_line` retained for offline
11
+ # cell_search / CellSearch text analysis.
14
12
  module LTE
15
13
  # Supported Method Parameters::
16
14
  # PWN::SDR::Decoder::LTE.decode(
@@ -19,33 +17,17 @@ module PWN
19
17
 
20
18
  public_class_method def self.decode(opts = {})
21
19
  freq_obj = opts[:freq_obj]
22
- raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
23
-
24
- hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
25
- gain = (freq_obj[:rf_gain] || 40).to_s.to_f
26
- sdr_args = (freq_obj[:sdr_args] || 'driver=rtlsdr').to_s
27
-
28
- direct_cmd =
29
- if PWN::SDR::Decoder::Base.bin_available?(bin: 'cell_search')
30
- # srsRAN cell_search takes a band number; fall back to explicit
31
- # DL frequency via -s/-e range when unavailable.
32
- "cell_search -a #{Shellwords.escape(sdr_args)} -g #{gain} -s #{hz} -e #{hz}"
33
- else
34
- "CellSearch -s #{hz / 1_000_000.0} -e #{hz / 1_000_000.0} -g #{gain}"
35
- end
36
-
37
- PWN::SDR::Decoder::Base.run_pipeline(
20
+ PWN::SDR::Decoder::Base.run_detector(
38
21
  freq_obj: freq_obj,
39
22
  protocol: 'LTE',
40
- required_bins: %w[cell_search],
41
- direct_cmd: direct_cmd,
42
- line_match: /(CELL|PCI|EARFCN|Found|RSRP|CID)/i,
43
- parser: proc { |line| parse_line(line: line) }
23
+ note: 'OFDMA (1.4–20 MHz, 15 kHz subcarriers) — native mode reports channel occupancy only.',
24
+ threshold: 4.0,
25
+ describe: proc { |_b| { modulation: 'OFDMA', subcarrier_khz: 15 } }
44
26
  )
45
27
  end
46
28
 
47
29
  # Supported Method Parameters::
48
- # PWN::SDR::Decoder::LTE.parse_line(line: 'Found CELL ... PCI: 123 ... PRB: 50 ... -85.0 dBm')
30
+ # PWN::SDR::Decoder::LTE.parse_line(line: 'Found CELL ... PCI: 123 ...')
49
31
 
50
32
  public_class_method def self.parse_line(opts = {})
51
33
  line = opts[:line].to_s
@@ -69,14 +51,11 @@ module PWN
69
51
  # Display Usage for this Module
70
52
 
71
53
  public_class_method def self.help
72
- puts "USAGE:
54
+ puts "USAGE (ruby-native detector, no external binaries):
73
55
  #{self}.decode(
74
56
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
75
57
  )
76
58
 
77
- NOTE: Requires srsRAN `cell_search` (or LTE-Cell-Scanner
78
- `CellSearch`). Owns the SDR — set freq_obj[:sdr_args].
79
-
80
59
  #{self}.parse_line(line: 'Found CELL 739.0 MHz, EARFCN=5110, PCI=123, 50 PRB, -85.2 dBm')
81
60
 
82
61
  #{self}.authors
@@ -3,11 +3,93 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # CW / Morse decoder for the amateur CW sub-bands (cw20/cw40/cw80,
7
- # amateur_30m). GQRX's CW demodulator produces a ~700 Hz sidetone in the
8
- # 48 kHz UDP audio stream; multimon-ng's MORSE_CW demod recovers the
9
- # dit/dah timing and prints decoded characters one line at a time.
6
+ # Pure-Ruby CW / Morse decoder for the amateur CW sub-bands.
7
+ #
8
+ # GQRX's CW/USB demodulator produces a ~600–800 Hz sidetone in the
9
+ # 48 kHz UDP audio stream. This module envelope-detects the tone
10
+ # with a state-preserving single-pole low-pass, adaptively
11
+ # thresholds it into on/off runs, classifies each run as dit / dah
12
+ # / char-gap / word-gap by timing, and looks the resulting `.-`
13
+ # sequences up in DSP::MORSE_TABLE. No `multimon-ng`, no `sox`.
10
14
  module Morse
15
+ # Stateful, streaming CW demodulator fed by Base.run_native.
16
+ class Demod
17
+ def initialize(rate: 48_000)
18
+ @rate = rate
19
+ # single-pole low-pass on |x|; τ ≈ 5 ms
20
+ @env_a = Math.exp(-1.0 / (rate * 0.005))
21
+ @env = 0.0
22
+ @floor = 0.0
23
+ @peak = 0.0
24
+ @state = :off
25
+ @run = 0
26
+ @dit = rate * 60 / 1000 # seed 60 ms (≈ 20 WPM)
27
+ @sym = +''
28
+ @word = +''
29
+ end
30
+
31
+ def feed(samples, &)
32
+ samples.each do |x|
33
+ @env = (@env_a * @env) + ((1.0 - @env_a) * x.abs)
34
+ # slow trackers for adaptive threshold (attack fast, decay slow)
35
+ @peak = @env > @peak ? ((@peak * 0.7) + (@env * 0.3)) : (@peak * 0.9999)
36
+ @floor = @env < @floor ? @env : ((@floor * 0.9999) + (@env * 0.0001))
37
+ thresh = @floor + ((@peak - @floor) * 0.4)
38
+ on = @env > thresh && (@peak - @floor) > 0.01
39
+ if on == (@state == :on)
40
+ @run += 1
41
+ else
42
+ classify_run(&)
43
+ @state = on ? :on : :off
44
+ @run = 1
45
+ end
46
+ end
47
+ return unless @state == :off && @run > 5 * @dit && !(@sym.empty? && @word.empty?)
48
+
49
+ flush_char
50
+ flush_word(&)
51
+ end
52
+
53
+ private
54
+
55
+ def classify_run(&)
56
+ return if @run < @rate / 500 # <2 ms glitch
57
+
58
+ if @state == :on
59
+ @dit = ((@dit * 3) + @run) / 4 if @run < @dit * 1.5 && @run > @rate / 200
60
+ @sym << (@run > 2 * @dit ? '-' : '.')
61
+ else
62
+ return if @sym.empty? && @word.empty?
63
+
64
+ if @run > 5 * @dit
65
+ flush_char
66
+ flush_word(&)
67
+ elsif @run > 2 * @dit
68
+ flush_char
69
+ end
70
+ end
71
+ end
72
+
73
+ def flush_char
74
+ return if @sym.empty?
75
+
76
+ @word << (PWN::SDR::Decoder::DSP::MORSE_TABLE[@sym] || '_')
77
+ @sym = +''
78
+ end
79
+
80
+ def flush_word
81
+ return if @word.empty?
82
+
83
+ out = { protocol: 'MORSE-CW', text: @word.dup }
84
+ if (m = @word.match(/\b([A-Z0-9]{1,3}\d[A-Z]{1,4})\b/))
85
+ out[:callsign] = m[1]
86
+ end
87
+ out[:summary] = "CW #{@word}"[0, 120]
88
+ yield out
89
+ @word = +''
90
+ end
91
+ end
92
+
11
93
  # Supported Method Parameters::
12
94
  # PWN::SDR::Decoder::Morse.decode(
13
95
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -15,28 +97,20 @@ module PWN
15
97
 
16
98
  public_class_method def self.decode(opts = {})
17
99
  freq_obj = opts[:freq_obj]
18
-
19
- PWN::SDR::Decoder::Base.run_pipeline(
100
+ PWN::SDR::Decoder::Base.run_native(
20
101
  freq_obj: freq_obj,
21
102
  protocol: 'MORSE-CW',
22
- required_bins: %w[sox multimon-ng],
23
- decode_cmd: 'multimon-ng -q -t raw -a MORSE_CW -',
24
- line_match: /\S/,
25
- parser: proc { |line| parse_line(line: line) }
103
+ demod: Demod.new
26
104
  )
27
105
  end
28
106
 
29
107
  # Supported Method Parameters::
30
- # PWN::SDR::Decoder::Morse.parse_line(line: 'CQ CQ DE W1AW')
108
+ # h = PWN::SDR::Decoder::Morse.decode_string(pattern: '.- -...')
31
109
 
32
- public_class_method def self.parse_line(opts = {})
33
- line = opts[:line].to_s.gsub(/^MORSE(?:_CW)?:\s*/i, '').strip
34
- out = { protocol: 'MORSE-CW', text: line }
35
- if (m = line.match(/\b([A-Z0-9]{1,3}\d[A-Z]{1,4})\b/))
36
- out[:callsign] = m[1]
37
- end
38
- out[:summary] = "CW #{line}"[0, 120]
39
- out
110
+ public_class_method def self.decode_string(opts = {})
111
+ pattern = opts[:pattern].to_s
112
+ txt = pattern.strip.split(/\s+/).map { |s| PWN::SDR::Decoder::DSP::MORSE_TABLE[s] || '_' }.join
113
+ { protocol: 'MORSE-CW', text: txt, summary: "CW #{txt}" }
40
114
  end
41
115
 
42
116
  # Author(s):: 0day Inc. <support@0dayinc.com>
@@ -48,12 +122,12 @@ module PWN
48
122
  # Display Usage for this Module
49
123
 
50
124
  public_class_method def self.help
51
- puts "USAGE:
125
+ puts "USAGE (ruby-native, no external binaries):
52
126
  #{self}.decode(
53
127
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
54
128
  )
55
129
 
56
- #{self}.parse_line(line: 'CQ CQ DE W1AW W1AW K')
130
+ #{self}.decode_string(pattern: '.- -...')
57
131
 
58
132
  #{self}.authors
59
133
  "
@@ -3,10 +3,14 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # APCO Project 25 (P25) Phase-1 C4FM decoder for the 700/800 MHz
7
- # public-safety allocations. GQRX supplies NBFM-discriminator audio via
8
- # the UDP tap; `dsd` (Digital Speech Decoder) recovers the C4FM symbol
9
- # stream and prints trunking-control frames (NAC, TGID, RID, DUID, ...).
6
+ # Pure-Ruby APCO Project 25 Phase-1 C4FM activity detector.
7
+ #
8
+ # P25 is 4800 sym/s 4-level FSK with 1/2-rate trellis + RS + IMBE
9
+ # vocoder. Full symbol recovery, deinterleave, FEC and voice decode
10
+ # is out of scope for a portable pure-Ruby implementation; this
11
+ # module instead characterises C4FM keying activity (talkgroup key-
12
+ # ups, control-channel duty cycle) natively via Base.run_detector.
13
+ # `parse_line` is retained for offline dsd-format log analysis.
10
14
  module P25
11
15
  # Supported Method Parameters::
12
16
  # PWN::SDR::Decoder::P25.decode(
@@ -15,17 +19,18 @@ module PWN
15
19
 
16
20
  public_class_method def self.decode(opts = {})
17
21
  freq_obj = opts[:freq_obj]
18
-
19
- # dsd expects 48 kHz s16le on stdin — bypass Base's 22 050 Hz resample
20
- # by asking for 48 000 (sox becomes a passthrough / format guard).
21
- PWN::SDR::Decoder::Base.run_pipeline(
22
+ PWN::SDR::Decoder::Base.run_detector(
22
23
  freq_obj: freq_obj,
23
24
  protocol: 'P25',
24
- required_bins: %w[sox dsd],
25
- resample_hz: 48_000,
26
- decode_cmd: 'dsd -q -i - -o /dev/null -f1',
27
- line_match: /(NAC|TGID|TG:|RID|src:|P25|Sync:)/i,
28
- parser: proc { |line| parse_line(line: line) }
25
+ note: 'C4FM 4-FSK + trellis/RS/IMBE — native mode reports key-up bursts (duration/peak/duty) only. Feed captured dsd text to .parse_line for NAC/TG/RID.',
26
+ threshold: 6.0,
27
+ describe: proc { |b|
28
+ kind = if b[:duration_ms] > 1500 then 'voice-superframe'
29
+ elsif b[:duration_ms] > 150 then 'LDU/HDU'
30
+ else 'TSBK/control'
31
+ end
32
+ { modulation: 'C4FM', symbol_rate: 4800, classification: kind }
33
+ }
29
34
  )
30
35
  end
31
36
 
@@ -53,13 +58,11 @@ module PWN
53
58
  # Display Usage for this Module
54
59
 
55
60
  public_class_method def self.help
56
- puts "USAGE:
61
+ puts "USAGE (ruby-native detector, no external binaries):
57
62
  #{self}.decode(
58
63
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
59
64
  )
60
65
 
61
- NOTE: Requires `dsd`. Set GQRX demod to Narrow FM, ~12.5 kHz.
62
-
63
66
  #{self}.parse_line(line: 'Sync: +P25p1 NAC: 293 src: 1234 tg: 5678')
64
67
 
65
68
  #{self}.authors
@@ -3,12 +3,24 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # Combined pager decoder for the mixed-protocol `pager_all` band plan.
7
- #
8
- # Runs multimon-ng with FLEX + FLEX_NEXT + POCSAG512/1200/2400 enabled
9
- # simultaneously and dispatches each output line to the appropriate
10
- # per-protocol parser (::Flex-style or ::POCSAG.parse_line).
6
+ # Pure-Ruby combined pager decoder for the mixed-protocol `pager_all`
7
+ # band plan. Feeds every incoming 48 kHz audio chunk to BOTH the
8
+ # native POCSAG and FLEX demodulators concurrently; whichever locks
9
+ # emits messages. No `multimon-ng`, no `sox`.
11
10
  module Pager
11
+ # Composite demodulator wrapping POCSAG::Demod + Flex::Demod.
12
+ class Demod
13
+ def initialize(rate: 48_000)
14
+ @pocsag = PWN::SDR::Decoder::POCSAG::Demod.new(rate: rate)
15
+ @flex = PWN::SDR::Decoder::Flex::Demod.new(rate: rate)
16
+ end
17
+
18
+ def feed(samples, &)
19
+ @pocsag.feed(samples.dup, &)
20
+ @flex.feed(samples, &)
21
+ end
22
+ end
23
+
12
24
  # Supported Method Parameters::
13
25
  # PWN::SDR::Decoder::Pager.decode(
14
26
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -16,42 +28,13 @@ module PWN
16
28
 
17
29
  public_class_method def self.decode(opts = {})
18
30
  freq_obj = opts[:freq_obj]
19
-
20
- decode_cmd = 'multimon-ng -q -t raw -e -u -p ' \
21
- '-a FLEX -a FLEX_NEXT ' \
22
- '-a POCSAG512 -a POCSAG1200 -a POCSAG2400 -'
23
-
24
- PWN::SDR::Decoder::Base.run_pipeline(
31
+ PWN::SDR::Decoder::Base.run_native(
25
32
  freq_obj: freq_obj,
26
33
  protocol: 'PAGER',
27
- required_bins: %w[sox multimon-ng],
28
- decode_cmd: decode_cmd,
29
- line_match: /^(FLEX|POCSAG)/,
30
- parser: proc { |line| parse_line(line: line) }
34
+ demod: Demod.new
31
35
  )
32
36
  end
33
37
 
34
- # Supported Method Parameters::
35
- # PWN::SDR::Decoder::Pager.parse_line(line: 'FLEX|... or POCSAG1200: ...')
36
-
37
- public_class_method def self.parse_line(opts = {})
38
- line = opts[:line].to_s
39
- return PWN::SDR::Decoder::POCSAG.parse_line(line: line) if line.start_with?('POCSAG')
40
-
41
- # FLEX / FLEX_NEXT — pipe- or space-delimited (see ::Flex for format).
42
- delim = line.start_with?('FLEX: ') ? ' ' : '|'
43
- parts = line.split(delim)
44
- proto = line.start_with?('FLEX_NEXT') ? 'FLEX_NEXT' : 'FLEX'
45
- types = %w[ALN BIN HEX NUM TON TONE UNK]
46
- t_idx = parts.index { |p| types.include?(p) }
47
- out = { protocol: proto, raw_inspected: line.inspect }
48
- out[:capcode] = parts.find { |p| p.match?(/^\[?\d{7,10}\]?$/) }
49
- out[:type] = t_idx ? parts[t_idx] : nil
50
- out[:type_payload] = t_idx ? parts[(t_idx + 1)..].join(delim) : nil
51
- out[:summary] = "#{proto} RIC=#{out[:capcode]} #{out[:type]}: #{out[:type_payload]}"
52
- out.compact
53
- end
54
-
55
38
  # Author(s):: 0day Inc. <support@0dayinc.com>
56
39
 
57
40
  public_class_method def self.authors
@@ -61,12 +44,13 @@ module PWN
61
44
  # Display Usage for this Module
62
45
 
63
46
  public_class_method def self.help
64
- puts "USAGE:
47
+ puts "USAGE (ruby-native, no external binaries):
65
48
  #{self}.decode(
66
49
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
67
50
  )
68
51
 
69
- #{self}.parse_line(line: 'FLEX|3200|...|ALN|MESSAGE')
52
+ NOTE: Runs the native POCSAG (512/1200/2400) and FLEX (1600)
53
+ demodulators in parallel on the same GQRX NBFM audio tap.
70
54
 
71
55
  #{self}.authors
72
56
  "