pwn 0.5.531 → 0.5.534
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 +1 -1
- data/.rubocop_todo.yml +25 -3
- data/Gemfile +6 -5
- data/README.md +3 -3
- data/lib/pwn/plugins/burp_suite.rb +24 -10
- data/lib/pwn/plugins/file_fu.rb +50 -0
- data/lib/pwn/plugins/sock.rb +111 -48
- data/lib/pwn/sdr/decoder/flex.rb +257 -0
- data/lib/pwn/sdr/decoder/pocsag.rb +183 -55
- data/lib/pwn/sdr/decoder/rds.rb +11 -4
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/frequency_allocation.rb +30 -1
- data/lib/pwn/sdr/gqrx.rb +426 -170
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/sdr/decoder/flex_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +10 -7
- metadata +28 -12
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'tty-spinner'
|
|
6
|
+
require 'io/wait'
|
|
7
|
+
|
|
8
|
+
module PWN
|
|
9
|
+
module SDR
|
|
10
|
+
module Decoder
|
|
11
|
+
# Flex Decoder Module for Pagers
|
|
12
|
+
module Flex
|
|
13
|
+
# Supported Method Parameters::
|
|
14
|
+
# pocsag_resp = PWN::SDR::Decoder::Flex.decode(
|
|
15
|
+
# freq_obj: 'required - GQRX socket object returned from #connect method'
|
|
16
|
+
# )
|
|
17
|
+
|
|
18
|
+
public_class_method def self.decode(opts = {})
|
|
19
|
+
freq_obj = opts[:freq_obj]
|
|
20
|
+
gqrx_sock = freq_obj[:gqrx_sock]
|
|
21
|
+
udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
|
|
22
|
+
udp_port = freq_obj[:udp_port] || 7355
|
|
23
|
+
|
|
24
|
+
freq_obj.delete(:gqrx_sock)
|
|
25
|
+
|
|
26
|
+
skip_freq_char = "\n"
|
|
27
|
+
|
|
28
|
+
puts JSON.pretty_generate(freq_obj)
|
|
29
|
+
puts 'Press [ENTER] to Continue to next frequency...'
|
|
30
|
+
|
|
31
|
+
# Spinner setup with dynamic terminal width awareness
|
|
32
|
+
spinner = TTY::Spinner.new(
|
|
33
|
+
'[:spinner] :status',
|
|
34
|
+
format: :arrow_pulse,
|
|
35
|
+
clear: true,
|
|
36
|
+
hide_cursor: true
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
spinner_overhead = 12
|
|
40
|
+
max_title_length = [TTY::Screen.width - spinner_overhead, 50].max
|
|
41
|
+
|
|
42
|
+
initial_title = "INFO: Decoding #{self.to_s.split('::').last} on udp://#{udp_ip}:#{udp_port} ..."
|
|
43
|
+
initial_title = initial_title[0...max_title_length] if initial_title.length > max_title_length
|
|
44
|
+
spinner.update(status: initial_title)
|
|
45
|
+
spinner.auto_spin
|
|
46
|
+
|
|
47
|
+
skip_freq = false
|
|
48
|
+
|
|
49
|
+
# === Replace netcat with PWN::Plugins::Sock.listen ===
|
|
50
|
+
udp_listener = PWN::SDR::GQRX.listen_udp(
|
|
51
|
+
udp_ip: udp_ip,
|
|
52
|
+
udp_port: udp_port
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Combined processing pipeline: sox → multimon-ng
|
|
56
|
+
decode_cmd = '
|
|
57
|
+
sox -t raw -e signed-integer -b 16 -r 48000 -c 1 - \
|
|
58
|
+
-t raw -e signed-integer -b 16 -r 22050 -c 1 - | \
|
|
59
|
+
multimon-ng -t raw \
|
|
60
|
+
-a FLEX \
|
|
61
|
+
-a FLEX_NEXT \
|
|
62
|
+
-
|
|
63
|
+
'
|
|
64
|
+
|
|
65
|
+
mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(decode_cmd)
|
|
66
|
+
|
|
67
|
+
current_title = 'Waiting for data frames...'
|
|
68
|
+
|
|
69
|
+
# Thread: read from UDP listener and feed to sox|multimon pipeline
|
|
70
|
+
receiver_thread = Thread.new do
|
|
71
|
+
begin
|
|
72
|
+
loop do
|
|
73
|
+
data, _sender = udp_listener.recv(4096)
|
|
74
|
+
next unless data.to_s.bytesize.positive?
|
|
75
|
+
|
|
76
|
+
mm_stdin.write(data)
|
|
77
|
+
mm_stdin.flush rescue nil
|
|
78
|
+
end
|
|
79
|
+
rescue IOError, Errno::EPIPE, EOFError, Errno::ECONNRESET
|
|
80
|
+
# normal exit path when shutting down
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Thread: read decoded output from multimon-ng and display it
|
|
85
|
+
decoder_thread = Thread.new do
|
|
86
|
+
# buffer = +''
|
|
87
|
+
buffer = ''
|
|
88
|
+
|
|
89
|
+
valid_types = %w[ALN BIN HEX NUM TON TONE UNK]
|
|
90
|
+
loop do
|
|
91
|
+
begin
|
|
92
|
+
chunk = mm_stdout.readpartial(4096)
|
|
93
|
+
# buffer << chunk
|
|
94
|
+
buffer = "#{buffer}#{chunk}"
|
|
95
|
+
|
|
96
|
+
while (line = buffer.slice!(/^.*\n/))
|
|
97
|
+
line = line.chomp
|
|
98
|
+
next if line.empty? || !line.start_with?('FLEX')
|
|
99
|
+
|
|
100
|
+
decoded_at = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
101
|
+
dec_msg = { decoded_at: decoded_at }
|
|
102
|
+
dec_msg[:raw_inspected] = line.inspect
|
|
103
|
+
|
|
104
|
+
protocol = line[0..8]
|
|
105
|
+
protocol = 'FLEX' unless protocol == 'FLEX_NEXT'
|
|
106
|
+
dec_msg[:protocol] = protocol
|
|
107
|
+
|
|
108
|
+
# ────────────────────────────── Detect format ──────────────────────────────
|
|
109
|
+
# Sometimes Flex is space delimited, sometimes pipe delimited
|
|
110
|
+
# FLEX_NEXT appears to always be pipe delimited
|
|
111
|
+
|
|
112
|
+
delimiter = '|'
|
|
113
|
+
space_delim = false
|
|
114
|
+
if line.start_with?('FLEX: ')
|
|
115
|
+
delimiter = ' '
|
|
116
|
+
space_delim = true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
flex_pipe_delim = false
|
|
120
|
+
flex_pipe_delim = true if line.start_with?('FLEX|')
|
|
121
|
+
|
|
122
|
+
parts = line.split(delimiter)
|
|
123
|
+
|
|
124
|
+
# protocol index already used
|
|
125
|
+
idx_already_used = [0]
|
|
126
|
+
target_parts_idx = 1
|
|
127
|
+
target_parts_idx += 1 if flex_pipe_delim
|
|
128
|
+
target_parts_idx += 2 if space_delim
|
|
129
|
+
dec_msg[:speed] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
130
|
+
idx_already_used.push(target_parts_idx)
|
|
131
|
+
|
|
132
|
+
target_parts_idx += 2
|
|
133
|
+
dec_msg[:capcode] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
134
|
+
idx_already_used.push(target_parts_idx)
|
|
135
|
+
|
|
136
|
+
target_parts_idx -= 1
|
|
137
|
+
dec_msg[:capcode_loc] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
138
|
+
idx_already_used.push(target_parts_idx)
|
|
139
|
+
|
|
140
|
+
while target_parts_idx < parts.size
|
|
141
|
+
if idx_already_used.include?(target_parts_idx)
|
|
142
|
+
target_parts_idx += 1
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
key = parts[target_parts_idx]
|
|
147
|
+
key = 'long_sequence_number' if key == 'LS'
|
|
148
|
+
|
|
149
|
+
if key && valid_types.include?(key)
|
|
150
|
+
dec_msg[:type] = key
|
|
151
|
+
|
|
152
|
+
dec_msg[:type_desc] = case key
|
|
153
|
+
when 'ALN'
|
|
154
|
+
'Human-readable text'
|
|
155
|
+
when 'BIN'
|
|
156
|
+
'Binary / data payload (typically 32 bit words)'
|
|
157
|
+
when 'HEX'
|
|
158
|
+
'Raw hex representation of data'
|
|
159
|
+
when 'NUM'
|
|
160
|
+
'Numbers only'
|
|
161
|
+
when 'TON', 'TONE'
|
|
162
|
+
'Just alert tone, no message'
|
|
163
|
+
when 'UNK'
|
|
164
|
+
'Decoded but type unknown / unsupported format'
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
target_parts_idx += 1
|
|
168
|
+
payload_parts = parts[target_parts_idx..]
|
|
169
|
+
dec_msg[:type_payload] = payload_parts.join(delimiter)
|
|
170
|
+
|
|
171
|
+
break
|
|
172
|
+
else
|
|
173
|
+
target_parts_idx += 1
|
|
174
|
+
dec_msg[key] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
idx_already_used.push(target_parts_idx)
|
|
178
|
+
target_parts_idx += 1
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
final_msg = freq_obj.merge(dec_msg)
|
|
182
|
+
puts JSON.pretty_generate(final_msg)
|
|
183
|
+
# TODO: Append dec_msg to a log file in a better way
|
|
184
|
+
flex_log_file = "/tmp/flex_decoder_#{Time.now.strftime('%Y%m%d')}.log"
|
|
185
|
+
File.open(flex_log_file, 'a') do |f|
|
|
186
|
+
f.puts("#{JSON.generate(final_msg)},")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
rescue EOFError, IOError
|
|
190
|
+
break
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
loop do
|
|
196
|
+
spinner.update(status: current_title)
|
|
197
|
+
|
|
198
|
+
# Non-blocking ENTER check to exit gracefully
|
|
199
|
+
next unless $stdin.wait_readable(0)
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
char = $stdin.read_nonblock(1)
|
|
203
|
+
next unless char == skip_freq_char
|
|
204
|
+
|
|
205
|
+
skip_freq = true
|
|
206
|
+
puts "\n[!] ENTER pressed → stopping decoder..."
|
|
207
|
+
|
|
208
|
+
break
|
|
209
|
+
rescue IO::WaitReadable, EOFError
|
|
210
|
+
# ignore
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
break if skip_freq
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
spinner.success('Decoding stopped')
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
spinner.error("Decoding failed: #{e.message}") if defined?(spinner)
|
|
219
|
+
raise
|
|
220
|
+
ensure
|
|
221
|
+
# Cleanup
|
|
222
|
+
[receiver_thread, decoder_thread].each do |thread|
|
|
223
|
+
thread.kill if thread.alive?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
[mm_stdin, mm_stdout, mm_stderr].each { |io| io.close rescue nil }
|
|
227
|
+
|
|
228
|
+
mm_wait_thr&.value rescue nil
|
|
229
|
+
|
|
230
|
+
PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if defined?(udp_listener) && udp_listener
|
|
231
|
+
|
|
232
|
+
spinner.stop if defined?(spinner) && spinner
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
236
|
+
|
|
237
|
+
public_class_method def self.authors
|
|
238
|
+
"AUTHOR(S):
|
|
239
|
+
0day Inc. <support@0dayinc.com>
|
|
240
|
+
"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Display Usage for this Module
|
|
244
|
+
|
|
245
|
+
public_class_method def self.help
|
|
246
|
+
puts "USAGE:
|
|
247
|
+
#{self}.decode(
|
|
248
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq method'
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
#{self}.authors
|
|
252
|
+
"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'open3'
|
|
4
5
|
require 'tty-spinner'
|
|
6
|
+
require 'io/wait'
|
|
5
7
|
|
|
6
8
|
module PWN
|
|
7
9
|
module SDR
|
|
@@ -16,91 +18,217 @@ module PWN
|
|
|
16
18
|
public_class_method def self.decode(opts = {})
|
|
17
19
|
freq_obj = opts[:freq_obj]
|
|
18
20
|
gqrx_sock = freq_obj[:gqrx_sock]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
|
|
22
|
+
udp_port = freq_obj[:udp_port] || 7355
|
|
23
|
+
|
|
21
24
|
freq_obj.delete(:gqrx_sock)
|
|
25
|
+
|
|
22
26
|
skip_freq_char = "\n"
|
|
23
|
-
puts JSON.pretty_generate(freq_obj)
|
|
24
|
-
puts "\n*** Pager POCSAG Decoder ***"
|
|
25
|
-
puts 'Press [ENTER] to continue...'
|
|
26
|
-
|
|
27
|
-
# Toggle POCSAG off and on to reset the decoder
|
|
28
|
-
PWN::SDR::GQRX.cmd(
|
|
29
|
-
gqrx_sock: gqrx_sock,
|
|
30
|
-
cmd: 'U RECORD 0',
|
|
31
|
-
resp_ok: 'RPRT 0'
|
|
32
|
-
)
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cmd: 'U RECORD 1',
|
|
37
|
-
resp_ok: 'RPRT 0'
|
|
38
|
-
)
|
|
28
|
+
puts JSON.pretty_generate(freq_obj)
|
|
29
|
+
puts 'Press [ENTER] to Continue to next frequency...'
|
|
39
30
|
|
|
40
31
|
# Spinner setup with dynamic terminal width awareness
|
|
41
32
|
spinner = TTY::Spinner.new(
|
|
42
|
-
'[:spinner] :
|
|
33
|
+
'[:spinner] :status',
|
|
43
34
|
format: :arrow_pulse,
|
|
44
35
|
clear: true,
|
|
45
36
|
hide_cursor: true
|
|
46
37
|
)
|
|
47
38
|
|
|
48
|
-
record_header_size = 44
|
|
49
|
-
wav_header = File.binread(record_path, record_header_size)
|
|
50
|
-
raise 'ERROR: WAV file header is invalid!' unless wav_header[0, 4] == 'RIFF' && wav_header[8, 4] == 'WAVE'
|
|
51
|
-
|
|
52
|
-
bytes_read = wav_header_size
|
|
53
|
-
|
|
54
|
-
# Conservative overhead for spinner animation, colors, and spacing
|
|
55
39
|
spinner_overhead = 12
|
|
56
40
|
max_title_length = [TTY::Screen.width - spinner_overhead, 50].max
|
|
57
41
|
|
|
58
|
-
initial_title =
|
|
42
|
+
initial_title = "INFO: Decoding #{self.to_s.split('::').last} on udp://#{udp_ip}:#{udp_port} ..."
|
|
59
43
|
initial_title = initial_title[0...max_title_length] if initial_title.length > max_title_length
|
|
60
|
-
spinner.update(
|
|
44
|
+
spinner.update(status: initial_title)
|
|
61
45
|
spinner.auto_spin
|
|
62
46
|
|
|
63
|
-
|
|
47
|
+
skip_freq = false
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
# === Replace netcat with PWN::Plugins::Sock.listen ===
|
|
50
|
+
udp_listener = PWN::SDR::GQRX.listen_udp(
|
|
51
|
+
udp_ip: udp_ip,
|
|
52
|
+
udp_port: udp_port
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Combined processing pipeline: sox → multimon-ng
|
|
56
|
+
decode_cmd = '
|
|
57
|
+
sox -t raw -e signed-integer -b 16 -r 48000 -c 1 - \
|
|
58
|
+
-t raw -e signed-integer -b 16 -r 22050 -c 1 - | \
|
|
59
|
+
multimon-ng -t raw \
|
|
60
|
+
-a POCSAG512 \
|
|
61
|
+
-a POCSAG1200 \
|
|
62
|
+
-a POCSAG2400 \
|
|
63
|
+
-
|
|
64
|
+
'
|
|
65
|
+
|
|
66
|
+
mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(decode_cmd)
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
if current_wav_size > bytes_read
|
|
71
|
-
new_bytes = current_wav_size - bytes_read
|
|
72
|
-
# Ensure full I/Q pairs (8 bytes each)
|
|
73
|
-
new_bytes -= new_bytes % 8
|
|
74
|
-
data = File.binread(record_path, new_bytes, bytes_read)
|
|
68
|
+
current_title = 'Waiting for data frames...'
|
|
75
69
|
|
|
76
|
-
|
|
70
|
+
# Thread: read from UDP listener and feed to sox|multimon pipeline
|
|
71
|
+
receiver_thread = Thread.new do
|
|
72
|
+
begin
|
|
73
|
+
loop do
|
|
74
|
+
data, _sender = udp_listener.recv(4096)
|
|
75
|
+
next unless data.to_s.bytesize.positive?
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
mm_stdin.write(data)
|
|
78
|
+
mm_stdin.flush rescue nil
|
|
79
|
+
end
|
|
80
|
+
rescue IOError, Errno::EPIPE, EOFError, Errno::ECONNRESET
|
|
81
|
+
# normal exit path when shutting down
|
|
80
82
|
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Thread: read decoded output from multimon-ng and display it
|
|
86
|
+
decoder_thread = Thread.new do
|
|
87
|
+
# buffer = +''
|
|
88
|
+
buffer = ''
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
valid_types = %w[ALN BIN HEX NUM TON TONE UNK]
|
|
91
|
+
loop do
|
|
84
92
|
begin
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
chunk = mm_stdout.readpartial(4096)
|
|
94
|
+
# buffer << chunk
|
|
95
|
+
buffer = "#{buffer}#{chunk}"
|
|
96
|
+
|
|
97
|
+
while (line = buffer.slice!(/^.*\n/))
|
|
98
|
+
line = line.chomp
|
|
99
|
+
next if line.empty? || !line.start_with?('POCSAG')
|
|
100
|
+
|
|
101
|
+
decoded_at = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
102
|
+
dec_msg = { decoded_at: decoded_at }
|
|
103
|
+
dec_msg[:raw_inspected] = line.inspect
|
|
104
|
+
|
|
105
|
+
protocol = line[0..8]
|
|
106
|
+
protocol = line[0..9] unless protocol == 'POCSAG512'
|
|
107
|
+
dec_msg[:protocol] = protocol
|
|
108
|
+
|
|
109
|
+
# ────────────────────────────── Detect format ──────────────────────────────
|
|
110
|
+
# Sometimes POCSAG is space delimited, sometimes pipe delimited
|
|
111
|
+
# FLEX_NEXT appears to always be pipe delimited
|
|
112
|
+
|
|
113
|
+
delimiter = '|'
|
|
114
|
+
space_delim = false
|
|
115
|
+
if line.start_with?('FLEX: ')
|
|
116
|
+
delimiter = ' '
|
|
117
|
+
space_delim = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
flex_pipe_delim = false
|
|
121
|
+
flex_pipe_delim = true if line.start_with?('FLEX|')
|
|
122
|
+
|
|
123
|
+
parts = line.split(delimiter)
|
|
124
|
+
|
|
125
|
+
# protocol index already used
|
|
126
|
+
idx_already_used = [0]
|
|
127
|
+
target_parts_idx = 1
|
|
128
|
+
target_parts_idx += 1 if flex_pipe_delim
|
|
129
|
+
target_parts_idx += 2 if space_delim
|
|
130
|
+
dec_msg[:speed] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
131
|
+
idx_already_used.push(target_parts_idx)
|
|
132
|
+
|
|
133
|
+
target_parts_idx += 2
|
|
134
|
+
dec_msg[:capcode] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
135
|
+
idx_already_used.push(target_parts_idx)
|
|
136
|
+
|
|
137
|
+
target_parts_idx -= 1
|
|
138
|
+
dec_msg[:capcode_loc] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
139
|
+
idx_already_used.push(target_parts_idx)
|
|
140
|
+
|
|
141
|
+
while target_parts_idx < parts.size
|
|
142
|
+
if idx_already_used.include?(target_parts_idx)
|
|
143
|
+
target_parts_idx += 1
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
key = parts[target_parts_idx]
|
|
148
|
+
key = 'long_sequence_number' if key == 'LS'
|
|
149
|
+
|
|
150
|
+
if key && valid_types.include?(key)
|
|
151
|
+
dec_msg[:type] = key
|
|
152
|
+
|
|
153
|
+
dec_msg[:type_desc] = case key
|
|
154
|
+
when 'ALN'
|
|
155
|
+
'Human-readable text'
|
|
156
|
+
when 'BIN'
|
|
157
|
+
'Binary / data payload (typically 32 bit words)'
|
|
158
|
+
when 'HEX'
|
|
159
|
+
'Raw hex representation of data'
|
|
160
|
+
when 'NUM'
|
|
161
|
+
'Numbers only'
|
|
162
|
+
when 'TON', 'TONE'
|
|
163
|
+
'Just alert tone, no message'
|
|
164
|
+
when 'UNK'
|
|
165
|
+
'Decoded but type unknown / unsupported format'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
target_parts_idx += 1
|
|
169
|
+
payload_parts = parts[target_parts_idx..]
|
|
170
|
+
dec_msg[:type_payload] = payload_parts.join(delimiter)
|
|
171
|
+
|
|
172
|
+
break
|
|
173
|
+
else
|
|
174
|
+
target_parts_idx += 1
|
|
175
|
+
dec_msg[key] = parts[target_parts_idx] if parts[target_parts_idx]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
idx_already_used.push(target_parts_idx)
|
|
179
|
+
target_parts_idx += 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
final_msg = freq_obj.merge(dec_msg)
|
|
183
|
+
puts JSON.pretty_generate(final_msg)
|
|
184
|
+
# TODO: Append dec_msg to a log file in a better way
|
|
185
|
+
flex_log_file = "/tmp/flex_decoder_#{Time.now.strftime('%Y%m%d')}.log"
|
|
186
|
+
File.open(flex_log_file, 'a') do |f|
|
|
187
|
+
f.puts("#{JSON.generate(final_msg)},")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
rescue EOFError, IOError
|
|
191
|
+
break
|
|
89
192
|
end
|
|
90
193
|
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
loop do
|
|
197
|
+
spinner.update(status: current_title)
|
|
91
198
|
|
|
92
|
-
|
|
199
|
+
# Non-blocking ENTER check to exit gracefully
|
|
200
|
+
next unless $stdin.wait_readable(0)
|
|
201
|
+
|
|
202
|
+
begin
|
|
203
|
+
char = $stdin.read_nonblock(1)
|
|
204
|
+
next unless char == skip_freq_char
|
|
205
|
+
|
|
206
|
+
skip_freq = true
|
|
207
|
+
puts "\n[!] ENTER pressed → stopping decoder..."
|
|
208
|
+
|
|
209
|
+
break
|
|
210
|
+
rescue IO::WaitReadable, EOFError
|
|
211
|
+
# ignore
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
break if skip_freq
|
|
93
215
|
end
|
|
216
|
+
|
|
217
|
+
spinner.success('Decoding stopped')
|
|
94
218
|
rescue StandardError => e
|
|
95
|
-
spinner.error(
|
|
96
|
-
raise
|
|
219
|
+
spinner.error("Decoding failed: #{e.message}") if defined?(spinner)
|
|
220
|
+
raise
|
|
97
221
|
ensure
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
222
|
+
# Cleanup
|
|
223
|
+
[receiver_thread, decoder_thread].each do |thread|
|
|
224
|
+
thread.kill if thread.alive?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
[mm_stdin, mm_stdout, mm_stderr].each { |io| io.close rescue nil }
|
|
228
|
+
|
|
229
|
+
mm_wait_thr&.value rescue nil
|
|
230
|
+
|
|
231
|
+
PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if defined?(udp_listener) && udp_listener
|
|
104
232
|
|
|
105
233
|
spinner.stop if defined?(spinner) && spinner
|
|
106
234
|
end
|
data/lib/pwn/sdr/decoder/rds.rb
CHANGED
|
@@ -17,12 +17,12 @@ module PWN
|
|
|
17
17
|
freq_obj = opts[:freq_obj]
|
|
18
18
|
gqrx_sock = freq_obj[:gqrx_sock]
|
|
19
19
|
|
|
20
|
-
freq_obj = freq_obj.dup
|
|
20
|
+
# freq_obj = freq_obj.dup
|
|
21
21
|
freq_obj.delete(:gqrx_sock)
|
|
22
22
|
skip_freq_char = "\n"
|
|
23
23
|
puts JSON.pretty_generate(freq_obj)
|
|
24
24
|
puts "\n*** FM Radio RDS Decoder ***"
|
|
25
|
-
puts 'Press [ENTER] to continue...'
|
|
25
|
+
puts 'Press [ENTER] to continue to next frequency...'
|
|
26
26
|
|
|
27
27
|
# Toggle RDS off and on to reset the decoder
|
|
28
28
|
PWN::SDR::GQRX.cmd(
|
|
@@ -39,7 +39,7 @@ module PWN
|
|
|
39
39
|
|
|
40
40
|
# Spinner setup with dynamic terminal width awareness
|
|
41
41
|
spinner = TTY::Spinner.new(
|
|
42
|
-
'[:spinner] :
|
|
42
|
+
'[:spinner] :status',
|
|
43
43
|
format: :arrow_pulse,
|
|
44
44
|
clear: true,
|
|
45
45
|
hide_cursor: true
|
|
@@ -88,7 +88,7 @@ module PWN
|
|
|
88
88
|
rt_display = "#{rt_display[0...available_for_term]}..." if rt_display.length > available_for_term
|
|
89
89
|
|
|
90
90
|
msg = "#{prefix}#{rt_display}"
|
|
91
|
-
spinner.update(
|
|
91
|
+
spinner.update(status: msg)
|
|
92
92
|
last_resp = rds_resp.dup
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -108,6 +108,13 @@ module PWN
|
|
|
108
108
|
spinner.error('Decoding failed') if defined?(spinner)
|
|
109
109
|
raise e
|
|
110
110
|
ensure
|
|
111
|
+
# Toggle RDS off and on to reset the decoder
|
|
112
|
+
PWN::SDR::GQRX.cmd(
|
|
113
|
+
gqrx_sock: gqrx_sock,
|
|
114
|
+
cmd: 'U RDS 0',
|
|
115
|
+
resp_ok: 'RPRT 0'
|
|
116
|
+
)
|
|
117
|
+
|
|
111
118
|
spinner.stop if defined?(spinner) && spinner
|
|
112
119
|
end
|
|
113
120
|
|
data/lib/pwn/sdr/decoder.rb
CHANGED
|
@@ -417,7 +417,7 @@ module PWN
|
|
|
417
417
|
bandwidth: '16.000',
|
|
418
418
|
precision: 4
|
|
419
419
|
},
|
|
420
|
-
|
|
420
|
+
pager_all: {
|
|
421
421
|
ranges: [
|
|
422
422
|
# Low-power / unlicensed / CB-related (very limited POCSAG usage)
|
|
423
423
|
# RCRS channels (shared data/telemetry, occasional POCSAG)
|
|
@@ -449,6 +449,35 @@ module PWN
|
|
|
449
449
|
],
|
|
450
450
|
demodulator_mode: :FM,
|
|
451
451
|
bandwidth: '25.000',
|
|
452
|
+
precision: 4
|
|
453
|
+
},
|
|
454
|
+
pager_flex: {
|
|
455
|
+
ranges: [
|
|
456
|
+
# 900 MHz exclusive paging band (mostly FLEX, but some legacy POCSAG remains)
|
|
457
|
+
# Primary nationwide commercial paging band
|
|
458
|
+
{ start_freq: '929.000.000', target_freq: '932.000.000' }
|
|
459
|
+
],
|
|
460
|
+
demodulator_mode: :FM,
|
|
461
|
+
bandwidth: '20.000',
|
|
462
|
+
precision: 4,
|
|
463
|
+
decoder: :flex
|
|
464
|
+
},
|
|
465
|
+
pager_pocsag: {
|
|
466
|
+
ranges: [
|
|
467
|
+
# High-band VHF paging — most common for public-safety POCSAG today
|
|
468
|
+
# Classic VHF paging band (152/157/158 MHz very common)
|
|
469
|
+
{ start_freq: '152.000.000', target_freq: '159.000.000' },
|
|
470
|
+
|
|
471
|
+
# UHF paging bands (hospital, on-site, restaurant/coaster pagers, some public safety)
|
|
472
|
+
# UHF low — occasional private systems
|
|
473
|
+
{ start_freq: '400.000.000', target_freq: '430.000.000' },
|
|
474
|
+
# UHF high — very common for local POCSAG (incl. 450–470 & 467.xxx for on-site)
|
|
475
|
+
{ start_freq: '440.000.000', target_freq: '470.000.000' },
|
|
476
|
+
# Classic UHF paging allocation (454/459 MHz pairs — many reallocated)
|
|
477
|
+
{ start_freq: '454.000.000', target_freq: '460.000.000' }
|
|
478
|
+
],
|
|
479
|
+
demodulator_mode: :FM,
|
|
480
|
+
bandwidth: '12.500',
|
|
452
481
|
precision: 4,
|
|
453
482
|
decoder: :pocsag
|
|
454
483
|
},
|