pwn 0.5.532 → 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 +182 -57
- data/lib/pwn/sdr/decoder/rds.rb +4 -4
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/frequency_allocation.rb +13 -6
- 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
|
|
@@ -14,96 +16,219 @@ module PWN
|
|
|
14
16
|
# )
|
|
15
17
|
|
|
16
18
|
public_class_method def self.decode(opts = {})
|
|
17
|
-
puts 'IM HERE'
|
|
18
19
|
freq_obj = opts[:freq_obj]
|
|
19
20
|
gqrx_sock = freq_obj[:gqrx_sock]
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
|
|
22
|
+
udp_port = freq_obj[:udp_port] || 7355
|
|
23
|
+
|
|
22
24
|
freq_obj.delete(:gqrx_sock)
|
|
25
|
+
|
|
23
26
|
skip_freq_char = "\n"
|
|
24
|
-
puts JSON.pretty_generate(freq_obj)
|
|
25
|
-
puts "\n*** Pager POCSAG Decoder ***"
|
|
26
|
-
puts 'Press [ENTER] to continue...'
|
|
27
|
-
|
|
28
|
-
# Toggle POCSAG off and on to reset the decoder
|
|
29
|
-
PWN::SDR::GQRX.cmd(
|
|
30
|
-
gqrx_sock: gqrx_sock,
|
|
31
|
-
cmd: 'U RECORD 0',
|
|
32
|
-
resp_ok: 'RPRT 0'
|
|
33
|
-
)
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
cmd: 'U RECORD 1',
|
|
38
|
-
resp_ok: 'RPRT 0'
|
|
39
|
-
)
|
|
28
|
+
puts JSON.pretty_generate(freq_obj)
|
|
29
|
+
puts 'Press [ENTER] to Continue to next frequency...'
|
|
40
30
|
|
|
41
31
|
# Spinner setup with dynamic terminal width awareness
|
|
42
32
|
spinner = TTY::Spinner.new(
|
|
43
|
-
'[:spinner] :
|
|
33
|
+
'[:spinner] :status',
|
|
44
34
|
format: :arrow_pulse,
|
|
45
35
|
clear: true,
|
|
46
36
|
hide_cursor: true
|
|
47
37
|
)
|
|
48
38
|
|
|
49
|
-
record_header_size = 44
|
|
50
|
-
wav_header = File.binread(record_path, record_header_size)
|
|
51
|
-
raise 'ERROR: WAV file header is invalid!' unless wav_header[0, 4] == 'RIFF' && wav_header[8, 4] == 'WAVE'
|
|
52
|
-
|
|
53
|
-
bytes_read = wav_header_size
|
|
54
|
-
|
|
55
|
-
# Conservative overhead for spinner animation, colors, and spacing
|
|
56
39
|
spinner_overhead = 12
|
|
57
40
|
max_title_length = [TTY::Screen.width - spinner_overhead, 50].max
|
|
58
41
|
|
|
59
|
-
initial_title =
|
|
42
|
+
initial_title = "INFO: Decoding #{self.to_s.split('::').last} on udp://#{udp_ip}:#{udp_port} ..."
|
|
60
43
|
initial_title = initial_title[0...max_title_length] if initial_title.length > max_title_length
|
|
61
|
-
spinner.update(
|
|
44
|
+
spinner.update(status: initial_title)
|
|
62
45
|
spinner.auto_spin
|
|
63
46
|
|
|
64
|
-
|
|
47
|
+
skip_freq = false
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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)
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
if current_wav_size > bytes_read
|
|
72
|
-
new_bytes = current_wav_size - bytes_read
|
|
73
|
-
# Ensure full I/Q pairs (8 bytes each)
|
|
74
|
-
new_bytes -= new_bytes % 8
|
|
75
|
-
data = File.binread(record_path, new_bytes, bytes_read)
|
|
68
|
+
current_title = 'Waiting for data frames...'
|
|
76
69
|
|
|
77
|
-
|
|
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?
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
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 = ''
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
valid_types = %w[ALN BIN HEX NUM TON TONE UNK]
|
|
91
|
+
loop do
|
|
85
92
|
begin
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
90
192
|
end
|
|
91
193
|
end
|
|
194
|
+
end
|
|
92
195
|
|
|
93
|
-
|
|
196
|
+
loop do
|
|
197
|
+
spinner.update(status: current_title)
|
|
198
|
+
|
|
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
|
|
94
215
|
end
|
|
216
|
+
|
|
217
|
+
spinner.success('Decoding stopped')
|
|
95
218
|
rescue StandardError => e
|
|
96
|
-
spinner.error(
|
|
97
|
-
raise
|
|
219
|
+
spinner.error("Decoding failed: #{e.message}") if defined?(spinner)
|
|
220
|
+
raise
|
|
98
221
|
ensure
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
105
230
|
|
|
106
|
-
|
|
231
|
+
PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if defined?(udp_listener) && udp_listener
|
|
107
232
|
|
|
108
233
|
spinner.stop if defined?(spinner) && spinner
|
|
109
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
|
|
data/lib/pwn/sdr/decoder.rb
CHANGED
|
@@ -451,6 +451,17 @@ module PWN
|
|
|
451
451
|
bandwidth: '25.000',
|
|
452
452
|
precision: 4
|
|
453
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
|
+
},
|
|
454
465
|
pager_pocsag: {
|
|
455
466
|
ranges: [
|
|
456
467
|
# High-band VHF paging — most common for public-safety POCSAG today
|
|
@@ -463,14 +474,10 @@ module PWN
|
|
|
463
474
|
# UHF high — very common for local POCSAG (incl. 450–470 & 467.xxx for on-site)
|
|
464
475
|
{ start_freq: '440.000.000', target_freq: '470.000.000' },
|
|
465
476
|
# Classic UHF paging allocation (454/459 MHz pairs — many reallocated)
|
|
466
|
-
{ start_freq: '454.000.000', target_freq: '460.000.000' }
|
|
467
|
-
|
|
468
|
-
# 900 MHz exclusive paging band (mostly FLEX, but some legacy POCSAG remains)
|
|
469
|
-
# Primary nationwide commercial paging band
|
|
470
|
-
{ start_freq: '929.000.000', target_freq: '932.000.000' }
|
|
477
|
+
{ start_freq: '454.000.000', target_freq: '460.000.000' }
|
|
471
478
|
],
|
|
472
479
|
demodulator_mode: :FM,
|
|
473
|
-
bandwidth: '
|
|
480
|
+
bandwidth: '12.500',
|
|
474
481
|
precision: 4,
|
|
475
482
|
decoder: :pocsag
|
|
476
483
|
},
|