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.
Three entry points, none 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 and written
as flushed JSONL, then delivered to on_frame.
run_iq — True-air path. Opens a real SDR front-end via
PWN::FFI::{RTLSdr,HackRF,AdalmPluto,SoapySDR} (or
reads a capture file), streams interleaved I/Q into
a demod that responds to `#feed_iq(iq, rate:, &emit)`
(or `#feed` after optional FM-demod). Falls back to
run_detector when no hardware/file source is present
so the operator still gets structured output.
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
characterizer: 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.
All runners accept on_frame (callable), output (IO, default $stdout),
interactive (default true), duration (monotonic seconds), stop (callable),
queue_size (positive Integer, default 8), and log_file (nil: default
/tmp/
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.close_iq_source(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.close_iq_source(source: handle).
-
.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).
-
.read_iq_chunk(opts = {}) ⇒ Object
- Supported Method Parameters
chunk = PWN::SDR::Decoder::Base.read_iq_chunk(source: handle, bytes: 262_144) Returns raw binary String in the source's native format, or nil on EOF.
-
.resolve_iq_source(opts = {}) ⇒ Object
- Supported Method Parameters
src = PWN::SDR::Decoder::Base.resolve_iq_source( freq_obj: 'required', source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:soapy|:file', sample_rate: 'optional - desired rate Hz', file: 'optional - path to .cu8/.cs16/.iq capture' ) Returns { kind:, rate_hz:, ... } handle, or nil if nothing available.
-
.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_iq(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.run_iq( freq_obj: 'required - freq_obj Hash', protocol: 'required - short name', demod: 'required - object with #feed_iq(iq, rate:, &emit) OR #feed(samples, &emit) when fm_demod:true', sample_rate: 'optional - Hz (default 2_048_000)', source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:soapy|:file', file: 'optional - path to capture', fm_demod: 'optional - FM-demod I/Q→audio then #feed (default false)', chunk_bytes: 'optional - bytes per read (default 16384)', fallback: 'optional - :detector|:raise|:silent (default :detector)', note: 'optional - shown once when falling back', describe: 'optional - Proc for detector fallback' ).
-
.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)' ).
-
.unpack_iq(opts = {}) ⇒ Object
- Supported Method Parameters
iq = PWN::SDR::Decoder::Base.unpack_iq(source: handle, data: raw_string) → interleaved Array
[I0,Q0,…].
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. [email protected]
661 662 663 |
# File 'lib/pwn/sdr/decoder/base.rb', line 661 public_class_method def self. "AUTHOR(S):\n 0day Inc. <[email protected]>\n" end |
.close_iq_source(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.close_iq_source(source: handle)
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 |
# File 'lib/pwn/sdr/decoder/base.rb', line 409 public_class_method def self.close_iq_source(opts = {}) src = opts[:source] return unless src.is_a?(Hash) case src[:kind] when :file, :io src[:io]&.close unless src[:io]&.closed? when :rtlsdr (src[:read_mutex] ||= Mutex.new).synchronize do PWN::FFI::RTLSdr.close(device: src[:device]) if src[:device] end when :adalm_pluto PWN::FFI::AdalmPluto.stop_rx(handle: src[:handle]) if src[:handle] PWN::FFI::AdalmPluto.close(context: src[:context]) if src[:context] when :hackrf PWN::FFI::HackRF.stop_rx(handle: src[:handle]) if src[:handle] PWN::FFI::HackRF.close(device: src[:device]) if src[:device] when :soapy (src[:read_mutex] ||= Mutex.new).synchronize do PWN::FFI::SoapySDR.close(handle: src[:handle]) if src[:handle] end end rescue StandardError nil ensure src[:io] = nil if src.is_a?(Hash) end |
.help ⇒ Object
Display Usage for this Module
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 |
# File 'lib/pwn/sdr/decoder/base.rb', line 667 public_class_method def self.help puts "USAGE: # Common options for run_native, run_detector and run_iq: # on_frame: callable receiving each metadata-enriched Hash after output/log flush # output: IO (default $stdout), written as JSONL; not closed by the runner # interactive: true by default; ENTER cancels, stdin EOF does not cancel # duration: optional finite nonnegative seconds measured with a monotonic clock # stop: optional quick, nonblocking callable; truthy cancels pending work # queue_size: positive Integer (default 8); full queue blocks the producer # log_file: nil uses /tmp/<protocol>_decoder_<date>.log, false disables, path appends JSONL # source: input IO or source handle; file: raw fixture path; inputs are closed on exit # Audio fixtures: s16le mono; IQ: cu8 (default), cs8 or cs16 via iq_format # EOF drains and exits automatically; worker errors propagate after cleanup # Returns a Hash: reason (:eof/:stop/:duration/:enter), bytes_processed, # chunks_processed, frames, queue_size, queue_high_water, backpressure_waits # IQ also returns overruns/dropped_bytes (nil if unknown), discontinuity, # capture_complete (true only on clean EOF). Loss aborts; never bridge a gap. # IQ exceptions expose stream_stats with telemetry and original error details. # Queue metrics do not measure RF/UDP packet loss or guarantee hardware throughput. # Run run native and return its result #{self}.run_native( freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq', protocol: 'required - short name for banner / log filename (defaults to SIGNAL)', demod: 'required - object responding to #feed(samples,&emit)', rate: 'optional - assumed UDP sample rate (default 48000)' ) # Run run detector and return its result #{self}.run_detector( freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq', protocol: 'required - short name for banner / log filename (defaults to SIGNAL)', 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 match line and return its result #{self}.match_line?( line: 'optional - line value consumed by #match_line?', matcher: 'optional - matcher value consumed by #match_line?' ) # Run resolve iq source and return its result #{self}.resolve_iq_source( freq_obj: 'required - freq obj value consumed by #resolve_iq_source', source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:soapy|:file', sample_rate: 'optional - desired rate Hz', file: 'optional - path to .cu8/.cs16/.iq capture (defaults to freq_obj[:iq_file])', iq_format: 'optional - iq format value consumed by #resolve_iq_source (defaults to freq_obj[:iq_format])', index: 'optional - index value consumed by #resolve_iq_source', gain_db: 'optional - gain db value consumed by #resolve_iq_source (defaults to freq_obj[:gain_db])', ppm: 'optional - ppm value consumed by #resolve_iq_source', serial: 'optional - serial value consumed by #resolve_iq_source', lna_gain: 'optional - lna gain value consumed by #resolve_iq_source (defaults to 16)', vga_gain: 'optional - vga gain value consumed by #resolve_iq_source (defaults to 20)', amp: 'optional - amp value consumed by #resolve_iq_source', uri: 'optional - URI or URL string', chunk_samples: 'optional - chunk samples value consumed by #resolve_iq_source', soapy_args: 'optional - soapy args value consumed by #resolve_iq_source (defaults to freq_obj[:soapy_args])', channel: 'optional - channel value consumed by #resolve_iq_source (defaults to 0)' ) # Run read iq chunk and return its result #{self}.read_iq_chunk( source: 'required - source value consumed by #read_iq_chunk', bytes: 'optional - bytes value consumed by #read_iq_chunk' ) # Run unpack iq and return its result #{self}.unpack_iq( source: 'optional - source value consumed by #unpack_iq', data: 'optional - data value consumed by #unpack_iq' ) # Run close iq source and return its result #{self}.close_iq_source( source: 'optional - source value consumed by #close_iq_source' ) # OR #feed(samples, &emit) when fm_demod:true, #{self}.run_iq( freq_obj: 'required - freq_obj Hash', protocol: 'required - short name (defaults to SIGNAL)', demod: 'required - object with #feed_iq(iq, rate:, &emit', sample_rate: 'optional - Hz (default 2_048_000)', source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:soapy|:file', file: 'optional - path to capture', fm_demod: 'optional - FM-demod I/Q→audio then #feed (default false)', chunk_bytes: 'optional - bytes per read (default 16384; audio runners default 4096)', fallback: 'optional - :detector|:raise|:silent (default :detector)', note: 'optional - shown once when falling back', describe: 'optional - Proc for detector fallback', gain_db: 'optional - gain db value consumed by #run_iq', uri: 'optional - URI or URL string', index: 'optional - index value consumed by #run_iq', threshold: 'optional - threshold value consumed by #run_iq' ) # Print the AUTHOR(S) string for this module. #{self}.authors " constants.sort end |
.match_line?(opts = {}) ⇒ Boolean
- Supported Method Parameters
PWN::SDR::Decoder::Base.match_line?(line: str, matcher: Regexp|String|Array)
192 193 194 195 196 197 198 199 200 201 |
# File 'lib/pwn/sdr/decoder/base.rb', line 192 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 |
.read_iq_chunk(opts = {}) ⇒ Object
- Supported Method Parameters
chunk = PWN::SDR::Decoder::Base.read_iq_chunk(source: handle, bytes: 262_144) Returns raw binary String in the source's native format, or nil on EOF.
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/pwn/sdr/decoder/base.rb', line 349 public_class_method def self.read_iq_chunk(opts = {}) src = opts[:source] bytes = (opts[:bytes] || 262_144).to_i raise 'ERROR: :source required' unless src.is_a?(Hash) raise IOError, 'I/Q continuity lost: source reports dropped samples' if iq_stream_status(source: src)[:discontinuity] data = case src[:kind] when :file, :io src[:io] ||= File.open(src[:path], 'rb') data = src[:io].respond_to?(:readpartial) ? src[:io].readpartial(bytes) : src[:io].read(bytes) data.to_s.empty? ? nil : data when :rtlsdr (src[:read_mutex] ||= Mutex.new).synchronize do PWN::FFI::RTLSdr.read_sync(device: src[:device], bytes: bytes) end when :adalm_pluto PWN::FFI::AdalmPluto.read_sync(handle: src[:handle]) when :hackrf PWN::FFI::HackRF.read_sync(handle: src[:handle]) when :soapy (src[:read_mutex] ||= Mutex.new).synchronize do PWN::FFI::SoapySDR.read_sync(handle: src[:handle], timeout_us: 100_000) end else raise ArgumentError, "Unknown I/Q source: #{src[:kind]}" end raise IOError, 'I/Q continuity lost: source reports dropped samples' if iq_stream_status(source: src)[:discontinuity] if data.nil? && %i[rtlsdr hackrf adalm_pluto soapy].include?(src[:kind]) && !src.dig(:handle, :stopped) sleep 0.005 return ''.b # A hardware timeout is not capture EOF. end data rescue EOFError nil end |
.resolve_iq_source(opts = {}) ⇒ Object
- Supported Method Parameters
src = PWN::SDR::Decoder::Base.resolve_iq_source( freq_obj: 'required', source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:soapy|:file', sample_rate: 'optional - desired rate Hz', file: 'optional - path to .cu8/.cs16/.iq capture' ) Returns { kind:, rate_hz:, ... } handle, or nil if nothing available.
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/pwn/sdr/decoder/base.rb', line 234 public_class_method def self.resolve_iq_source(opts = {}) freq_obj = opts[:freq_obj] || {} supplied = opts[:source] return supplied if supplied.is_a?(Hash) if supplied.respond_to?(:readpartial) || supplied.respond_to?(:read) return { kind: :io, io: supplied, format: opts[:iq_format] || :cu8, rate_hz: opts[:sample_rate] || 2_048_000 } end want = (supplied || freq_obj[:iq_source] || :auto).to_s.downcase.to_sym rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_048_000).to_i file = opts[:file] || freq_obj[:iq_file] if want == :file || file raise ArgumentError, ':file is required for a file source' if file.to_s.empty? raise Errno::ENOENT, file.to_s unless File.file?(file.to_s) want = :file if want == :auto end freq_hz = begin PWN::SDR.hz_to_i(freq: freq_obj[:freq]) rescue StandardError freq_obj[:freq].to_i end try = lambda do |kind| owned = nil case kind when :file return nil if file.to_s.empty? || !File.file?(file.to_s) fmt = opts[:iq_format] || freq_obj[:iq_format] fmt ||= file.to_s.end_with?('.cs16', '.sc16') ? :cs16 : :cu8 { kind: :file, path: file.to_s, format: fmt.to_sym, rate_hz: rate, freq_hz: freq_hz } when :rtlsdr return nil unless PWN::FFI.available?(mod: :RTLSdr) return nil if PWN::FFI::RTLSdr.list_devices.empty? dev = PWN::FFI::RTLSdr.open(index: (opts[:index] || 0).to_i) owned = { kind: kind, device: dev } PWN::FFI::RTLSdr.configure( device: dev, freq_hz: freq_hz, rate_hz: rate, gain_db: opts[:gain_db] || freq_obj[:gain_db], ppm: opts[:ppm] || freq_obj[:ppm] || 0 ) { kind: :rtlsdr, device: dev, rate_hz: rate, freq_hz: freq_hz, format: :cu8 } when :hackrf return nil unless PWN::FFI.available?(mod: :HackRF) dev = PWN::FFI::HackRF.open(serial: opts[:serial]) owned = { kind: kind, device: dev } PWN::FFI::HackRF.configure( device: dev, freq_hz: freq_hz, rate_hz: rate, lna_gain: opts[:lna_gain] || 16, vga_gain: opts[:vga_gain] || 20, amp: opts[:amp] ) rx = PWN::FFI::HackRF.start_rx(device: dev) { kind: :hackrf, device: dev, handle: rx, rate_hz: rate, freq_hz: freq_hz, format: :cs8 } when :adalm_pluto, :pluto return nil unless PWN::FFI.available?(mod: :AdalmPluto) ctx = PWN::FFI::AdalmPluto.open(uri: opts[:uri] || freq_obj[:pluto_uri]) owned = { kind: :adalm_pluto, context: ctx } PWN::FFI::AdalmPluto.configure( context: ctx, freq_hz: freq_hz, rate_hz: rate, gain_db: opts[:gain_db] || freq_obj[:gain_db] ) handle = PWN::FFI::AdalmPluto.start_rx( context: ctx, samples: (opts[:chunk_samples] || 262_144).to_i ) { kind: :adalm_pluto, context: ctx, handle: handle, rate_hz: rate, freq_hz: freq_hz, format: :cs16 } when :soapy return nil unless PWN::FFI.available?(mod: :SoapySDR) return nil if PWN::FFI::SoapySDR.list_devices.empty? h = PWN::FFI::SoapySDR.open( args: opts[:soapy_args] || freq_obj[:soapy_args], channel: opts[:channel] || 0 ) owned = { kind: kind, handle: h } PWN::FFI::SoapySDR.configure( handle: h, freq_hz: freq_hz, rate_hz: rate, gain_db: opts[:gain_db] || freq_obj[:gain_db] ) PWN::FFI::SoapySDR.start_rx( handle: h, samples: (opts[:chunk_samples] || 65_536).to_i ) { kind: :soapy, handle: h, rate_hz: rate, freq_hz: freq_hz, format: :cs16, streaming: true } end rescue StandardError close_iq_source(source: owned) if owned raise unless want == :auto nil end order = case want when :auto %i[file rtlsdr adalm_pluto hackrf soapy] else [want] end order.each do |k| h = try.call(k) return h if h end nil 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' )
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/pwn/sdr/decoder/base.rb', line 87 public_class_method def self.run_detector(opts = {}) protocol = opts[:protocol] || 'SIGNAL' threshold = (opts[:threshold] || 8.0).to_f rate = (opts[:rate] || 48_000).to_f raise 'ERROR: :freq_obj is required' unless opts[:freq_obj].is_a?(Hash) raise ArgumentError, ':rate must be positive' unless rate.positive? floor = nil burst_start = nil peak = -200.0 count = 0 elapsed = 0.0 run_audio(opts.merge(allow_level: true)) do |samples, emit| level = samples.is_a?(Numeric) ? samples : samples && PWN::SDR::Decoder::DSP.rms_dbfs(samples: samples) floor = floor.nil? ? level : (floor * 0.98) + (level * 0.02) if level if level && level - floor >= threshold burst_start ||= elapsed peak = [peak, level].max elsif burst_start count += 1 duration_ms = ((elapsed - burst_start) * 1000).round msg = { protocol: protocol, event: 'burst', burst_no: count, peak_dbfs: peak.round(1), floor_dbfs: floor.round(1), delta_db: (peak - floor).round(1), duration_ms: duration_ms, summary: "#{protocol} burst ##{count} peak=#{peak.round(1)} dBFS duration=#{duration_ms} ms" } msg.merge!(opts[:describe].call(msg)) if opts[:describe].respond_to?(:call) emit.call(msg) burst_start = nil peak = -200.0 end elapsed += samples.is_a?(Numeric) ? 0.1 : samples.length / rate if samples end end |
.run_iq(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::Base.run_iq( freq_obj: 'required - freq_obj Hash', protocol: 'required - short name', demod: 'required - object with #feed_iq(iq, rate:, &emit) OR #feed(samples, &emit) when fm_demod:true', sample_rate: 'optional - Hz (default 2_048_000)', source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:soapy|:file', file: 'optional - path to capture', fm_demod: 'optional - FM-demod I/Q→audio then #feed (default false)', chunk_bytes: 'optional - bytes per read (default 16384)', fallback: 'optional - :detector|:raise|:silent (default :detector)', note: 'optional - shown once when falling back', describe: 'optional - Proc for detector fallback' )
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 |
# File 'lib/pwn/sdr/decoder/base.rb', line 453 public_class_method def self.run_iq(opts = {}) freq_obj = opts[:freq_obj] protocol = opts[:protocol] || 'SIGNAL' demod = opts[:demod] rate = (opts[:sample_rate] || 2_048_000).to_i fm_demod = opts[:fm_demod] ? true : false chunk_b = (opts[:chunk_bytes] || 16_384).to_i fallback = (opts[:fallback] || :detector).to_sym raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash) raise 'ERROR: :demod required' if demod.nil? src = resolve_iq_source(opts.merge(sample_rate: rate, chunk_samples: opts[:chunk_samples] || (chunk_b / 4))) unless src && src[:streaming] != false case fallback when :raise raise 'ERROR: no I/Q source available (RTL-SDR / ADALM-Pluto / HackRF / SoapySDR / file)' when :silent return nil else return run_detector(opts.merge(source: nil, file: nil, note: opts[:note] || "No I/Q source — energy detector only for #{protocol}.")) end end # Audio demods have fixed timing and cannot consume feed_iq's rate # keyword. No implicit resampling or private demod state mutation. actual_rate = src[:rate_hz] || rate # Rates are configuration values, not computed DSP measurements. raise ArgumentError, "Audio source rate #{actual_rate} differs from configured sample_rate #{rate}" if (fm_demod || !demod.respond_to?(:feed_iq)) && Float(actual_rate) != rate # rubocop:disable Lint/FloatComparison rate = actual_rate log_obj = strip_freq_obj(freq_obj: freq_obj).merge( iq_source: src[:kind], iq_rate: rate, iq_format: src[:format] ) raise ArgumentError, ':chunk_bytes must be positive' unless chunk_b.positive? raise ArgumentError, 'demod must respond to #feed_iq or #feed' unless demod.respond_to?(:feed_iq) || demod.respond_to?(:feed) carry = ''.b previous = nil width = src[:format].to_s == 'cs16' ? 4 : 2 cancel_read = -> { PWN::FFI::AdalmPluto.stop_rx(handle: src[:handle]) } if src[:kind] == :adalm_pluto result = run_stream(opts.merge(log_obj: log_obj, cancel_read: cancel_read, reader: -> { read_iq_chunk(source: src, bytes: chunk_b) })) do |raw, emit| raise IOError, 'I/Q continuity lost: source reports dropped samples' if iq_stream_status(source: src)[:discontinuity] if raw.nil? raise IOError, "Incomplete I/Q sample at EOF (#{carry.bytesize} bytes)" unless carry.empty? demod.flush(&emit) if demod.respond_to?(:flush) next end carry << raw length = carry.bytesize / width * width next if length.zero? iq = unpack_iq(source: src, data: carry.slice!(0, length)) if fm_demod && demod.respond_to?(:feed) continuous = previous ? previous + iq : iq previous = iq.last(2) demod.feed(PWN::SDR::Decoder::DSP.fm_demod_iq(iq: continuous), &emit) elsif demod.respond_to?(:feed_iq) demod.feed_iq(iq, rate: rate, &emit) else demod.feed(PWN::SDR::Decoder::DSP.mag_sq(iq: iq).map { |v| Math.sqrt(v) }, &emit) end end status = iq_stream_status(source: src) raise IOError, 'I/Q continuity lost: source reports dropped samples' if status[:discontinuity] result.merge(status).merge(capture_complete: result[:reason] == :eof) rescue StandardError => e status = iq_stream_status(source: src).merge(reason: :error, capture_complete: false, error_class: e.class.name, error_message: e.).freeze e.define_singleton_method(:stream_stats) { status } raise ensure close_iq_source(source: src) if defined?(src) && src 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)' )
64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/pwn/sdr/decoder/base.rb', line 64 public_class_method def self.run_native(opts = {}) demod = opts[:demod] raise 'ERROR: :freq_obj is required' unless opts[:freq_obj].is_a?(Hash) raise 'ERROR: :demod must respond to #feed' unless demod.respond_to?(:feed) run_audio(opts) do |samples, emit| if samples.nil? demod.flush(&emit) if demod.respond_to?(:flush) else demod.feed(samples, &emit) end end end |
.unpack_iq(opts = {}) ⇒ Object
- Supported Method Parameters
iq = PWN::SDR::Decoder::Base.unpack_iq(source: handle, data: raw_string)
→ interleaved Array
391 392 393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/pwn/sdr/decoder/base.rb', line 391 public_class_method def self.unpack_iq(opts = {}) src = opts[:source] || {} data = opts[:data].to_s case (src[:format] || :cu8).to_sym when :cs16 then PWN::SDR::Decoder::DSP.unpack_cs16le(data: data) when :cs8 # HackRF signed 8-bit interleaved I/Q data.unpack('c*').map { |v| v / 128.0 } when :cu8 PWN::SDR::Decoder::DSP.unpack_cu8(data: data) else raise ArgumentError, "Unsupported IQ format: #{src[:format]}" end end |