Module: PWN::SDR::Decoder::Base
- Defined in:
- lib/pwn/sdr/decoder/base.rb
Overview
Shared, 100 % Ruby-native pipeline plumbing for every PWN::SDR::Decoder::* module.
Two entry points, neither of which shell out to any external binary:
run_native — Bind the GQRX 48 kHz s16le mono UDP audio tap, unpack
the samples with PWN::SDR::Decoder::DSP, hand each
chunk to a caller-supplied `demod:` object that
responds to `#feed(samples, &emit)`. Every Hash the
demodulator emits is merged with freq_obj, JSON-
pretty-printed, JSONL-logged, and shown on the
spinner. [ENTER] stops cleanly.
run_detector — For protocols whose bit-rate/bandwidth cannot be
recovered from a 48 kHz demodulated-audio tap (GSM,
LTE, ADS-B, WiFi, LoRa, GPS, DECT, ZigBee, Bluetooth,
Iridium, P25, ISM/RFID …). Pure-Ruby energy / burst
characteriser: polls GQRX `l STRENGTH`, and (when the
UDP tap is enabled) computes RMS-dBFS on the audio.
Emits `{event: 'burst', dbfs:, duration_ms:}` frames
whenever the signal crosses an adaptive threshold, so
the operator still gets structured, logged intel
without ANY external decoding binary.
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.help ⇒ Object
Display Usage for this Module.
-
.match_line?(opts = {}) ⇒ Boolean
- Supported Method Parameters
PWN::SDR::Decoder::Base.match_line?(line: str, matcher: Regexp|String|Array).
-
.run_detector(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.run_detector( freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq', protocol: 'required - short name for banner / log filename', note: 'optional - one-line explanation shown once', threshold: 'optional - dBFS above rolling floor to call a burst (default 8.0)', describe: 'optional - Proc.new { |burst_hash| Hash } extra fields' ).
-
.run_native(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.run_native( freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq', protocol: 'required - short name for banner / log filename', demod: 'required - object responding to #feed(samples,&emit)', rate: 'optional - assumed UDP sample rate (default 48000)' ).
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. support@0dayinc.com
307 308 309 |
# File 'lib/pwn/sdr/decoder/base.rb', line 307 public_class_method def self. "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n" end |
.help ⇒ Object
Display Usage for this Module
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/pwn/sdr/decoder/base.rb', line 313 public_class_method def self.help puts "USAGE (100 % ruby-native — no external binaries): #{self}.run_native( freq_obj: 'required - freq_obj from PWN::SDR::GQRX.init_freq', protocol: 'required - short protocol name', demod: 'required - object with #feed(samples,&emit)', rate: 'optional - UDP sample rate (default 48000)' ) #{self}.run_detector( freq_obj: 'required - freq_obj from PWN::SDR::GQRX.init_freq', protocol: 'required - short protocol name', note: 'optional - explanation shown to operator', threshold: 'optional - dB above floor for burst (default 8.0)', describe: 'optional - Proc { |burst| Hash } extra fields' ) #{self}.match_line?(line: str, matcher: Regexp|String|Array) #{self}.authors " end |
.match_line?(opts = {}) ⇒ Boolean
- Supported Method Parameters
PWN::SDR::Decoder::Base.match_line?(line: str, matcher: Regexp|String|Array)
215 216 217 218 219 220 221 222 223 224 |
# File 'lib/pwn/sdr/decoder/base.rb', line 215 public_class_method def self.match_line?(opts = {}) line = opts[:line].to_s matcher = opts[:matcher] case matcher when Regexp then line.match?(matcher) when String then line.start_with?(matcher) when Array then matcher.any? { |m| match_line?(line: line, matcher: m) } else true end end |
.run_detector(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.run_detector( freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq', protocol: 'required - short name for banner / log filename', note: 'optional - one-line explanation shown once', threshold: 'optional - dBFS above rolling floor to call a burst (default 8.0)', describe: 'optional - Proc.new { |burst_hash| Hash } extra fields' )
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/pwn/sdr/decoder/base.rb', line 119 public_class_method def self.run_detector(opts = {}) freq_obj = opts[:freq_obj] protocol = opts[:protocol] || 'SIGNAL' note = opts[:note] threshold = (opts[:threshold] || 8.0).to_f describe = opts[:describe] raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash) gqrx_sock = freq_obj[:gqrx_sock] udp_ip = freq_obj[:udp_ip] || '127.0.0.1' udp_port = freq_obj[:udp_port] || 7355 log_obj = strip_freq_obj(freq_obj: freq_obj) puts JSON.pretty_generate(log_obj) puts "\n*** #{protocol} Signal Detector (ruby-native) ***" puts "[i] #{note}" if note puts 'Press [ENTER] to continue to next frequency...' spinner, max_len = build_spinner( banner: "INFO: Characterising #{protocol} activity on #{log_obj[:freq] || "udp://#{udp_ip}:#{udp_port}"} (native)" ) log_file = log_path(protocol: protocol) udp_listener = begin PWN::SDR::GQRX.listen_udp(udp_ip: udp_ip, udp_port: udp_port) rescue StandardError nil end current_line = 'Establishing noise floor...' floor = nil in_burst = false burst_t0 = nil peak = -200.0 burst_n = 0 detector_thread = Thread.new do emit = proc do |msg| final = log_obj.merge(decoded_at: Time.now.strftime('%Y-%m-%d %H:%M:%S%z')).merge(msg) spinner.stop puts JSON.pretty_generate(final) spinner.auto_spin File.open(log_file, 'a') { |f| f.puts("#{JSON.generate(final)},") } current_line = (msg[:summary] || '').to_s[0...max_len] end loop do lvl = read_level(gqrx_sock: gqrx_sock, udp_listener: udp_listener) if lvl floor = floor.nil? ? lvl : ((floor * 0.98) + (lvl * 0.02)) delta = lvl - floor if delta >= threshold unless in_burst in_burst = true burst_t0 = Time.now peak = lvl end peak = lvl if lvl > peak current_line = format('%<p>s BURST %<l>+.1f dBFS (Δ%<d>+.1f, floor %<f>+.1f)', p: protocol, l: lvl, d: delta, f: floor)[0...max_len] elsif in_burst in_burst = false burst_n += 1 dur_ms = ((Time.now - burst_t0) * 1000).round msg = { protocol: protocol, event: 'burst', burst_no: burst_n, peak_dbfs: peak.round(1), floor_dbfs: floor.round(1), delta_db: (peak - floor).round(1), duration_ms: dur_ms, summary: format('%<p>s burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms', p: protocol, n: burst_n, pk: peak, d: peak - floor, ms: dur_ms) } msg.merge!(describe.call(msg)) if describe.respond_to?(:call) emit.call(msg) else current_line = format('%<p>s idle %<l>+.1f dBFS (floor %<f>+.1f, Δ%<d>+.1f)', p: protocol, l: lvl, f: floor, d: delta)[0...max_len] end end sleep(udp_listener ? 0 : 0.1) end rescue StandardError => e current_line = "detector error: #{e.class}: #{e.}" end wait_for_enter(spinner: spinner, title_ref: -> { current_line }) spinner.success('Detector stopped') rescue StandardError => e spinner&.error("Detector failed: #{e.}") if defined?(spinner) raise ensure detector_thread&.kill if defined?(detector_thread) && detector_thread&.alive? PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if udp_listener spinner&.stop if defined?(spinner) end |
.run_native(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.run_native( freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq', protocol: 'required - short name for banner / log filename', demod: 'required - object responding to #feed(samples,&emit)', rate: 'optional - assumed UDP sample rate (default 48000)' )
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/pwn/sdr/decoder/base.rb', line 43 public_class_method def self.run_native(opts = {}) freq_obj = opts[:freq_obj] protocol = opts[:protocol] || 'SIGNAL' demod = opts[:demod] rate = (opts[:rate] || 48_000).to_i raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash) raise 'ERROR: :demod must respond to #feed' unless demod.respond_to?(:feed) udp_ip = freq_obj[:udp_ip] || '127.0.0.1' udp_port = freq_obj[:udp_port] || 7355 log_obj = strip_freq_obj(freq_obj: freq_obj) puts JSON.pretty_generate(log_obj) puts "\n*** #{protocol} Decoder (ruby-native) ***" puts 'Press [ENTER] to continue to next frequency...' spinner, max_len = build_spinner( banner: "INFO: Decoding #{protocol} on udp://#{udp_ip}:#{udp_port} @ #{rate} Hz (native)" ) log_file = log_path(protocol: protocol) udp_listener = PWN::SDR::GQRX.listen_udp(udp_ip: udp_ip, udp_port: udp_port) audio_q = Queue.new current_line = 'Waiting for data frames...' receiver_thread = Thread.new do loop do data, = udp_listener.recv(4096) audio_q.push(data) if data.to_s.bytesize.positive? end rescue IOError, Errno::ECONNRESET, Errno::EBADF nil end decoder_thread = Thread.new do emit = proc do |msg| next unless msg.is_a?(Hash) final = log_obj.merge(decoded_at: Time.now.strftime('%Y-%m-%d %H:%M:%S%z')).merge(msg) spinner.stop puts JSON.pretty_generate(final) spinner.auto_spin File.open(log_file, 'a') { |f| f.puts("#{JSON.generate(final)},") } disp = (msg[:summary] || msg[:raw] || msg.values.compact.first).to_s current_line = disp[0...max_len] end loop do raw = audio_q.pop samples = PWN::SDR::Decoder::DSP.unpack_s16le(data: raw) demod.feed(samples, &emit) end rescue StandardError => e current_line = "demod error: #{e.class}: #{e.}" end wait_for_enter(spinner: spinner, title_ref: -> { current_line }) spinner.success('Decoding stopped') rescue StandardError => e spinner&.error("Decoding failed: #{e.}") if defined?(spinner) raise ensure [receiver_thread, decoder_thread].compact.each { |t| t.kill if t&.alive? } PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if udp_listener spinner&.stop if defined?(spinner) end |