#!/usr/bin/env ruby # frozen_string_literal: false require 'pwn' require 'json' require 'optparse' opts = {} OptionParser.new do |options| options.banner = "USAGE: #{$PROGRAM_NAME} [opts] " options.on('-tFREQ', '--target-freq=FREQ', '') do |e| opts[:target_freq] = e end options.on('-sFREQ', '--start-freq=FREQ', '') do |s| opts[:start_freq] = s end options.on('-hHOST', '--host=HOST', '') do |h| opts[:host] = h end options.on('-pPORT', '--port=PORT', '') do |p| opts[:port] = p end options.on('-AFLOAT', '--audio-gain=FLOAT', '') do |a| opts[:audio_gain_db] = a end options.on('-bHZ', '--bandwidth=HZ', '') do |b| opts[:bandwidth] = b end options.on('-DMODE', '--demodulator-mode=MODE', '') do |d| opts[:demodulator_mode] = d end options.on('-PINT', '--precision=INT', '') do |p| opts[:precision] = p end options.on('-SFLOAT', '--strength-lock=FLOAT', '') do |s| opts[:strength_lock] = s end options.on('-LFLOAT', '--lock-freq-duration=FLOAT', '') do |l| opts[:lock_freq_duration] = l end options.on('-QFLOAT', '--squelch=FLOAT', '') do |q| opts[:squelch] = q end options.on('-RFLOAT', '--rf-gain=FLOAT', '') do |r| opts[:rf_gain] = r end options.on('-IFLOAT', '--intermediate-gain=FLOAT', '') do |i| opts[:intermediate_gain] = i end options.on('-BFLOAT', '--basedband-gain=FLOAT', '') do |b| opts[:baseband_gain] = b end end.parse! if opts.empty? puts `#{$PROGRAM_NAME} --help` exit 1 end def gqrx_cmd(opts = {}) gqrx_sock = opts[:gqrx_sock] cmd = opts[:cmd] resp_ok = opts[:resp_ok] # Most Recent GQRX Command Set: # https://raw.githubusercontent.com/gqrx-sdr/gqrx/master/resources/remote-control.txt # Supported commands: # f Get frequency [Hz] # F Set frequency [Hz] # m Get demodulator mode and passband # M [passband] # Set demodulator mode and passband [Hz] # Passing a '?' as the first argument instead of 'mode' will return # a space separated list of radio backend supported modes. # l|L ? # Get a space separated list of settings available for reading (l) or writing (L). # l STRENGTH # Get signal strength [dBFS] # l SQL # Get squelch threshold [dBFS] # L SQL # Set squelch threshold to [dBFS] # l AF # Get audio gain [dB] # L AF # Set audio gain to [dB] # l _GAIN # Get the value of the gain setting with the name # L _GAIN # Set the value of the gain setting with the name to # p RDS_PI # Get the RDS PI code (in hexadecimal). Returns 0000 if not applicable. # u RECORD # Get status of audio recorder # U RECORD # Set status of audio recorder to # u DSP # Get DSP (SDR receiver) status # U DSP # Set DSP (SDR receiver) status to # u RDS # Get RDS decoder to . Only functions in WFM mode. # U RDS # Set RDS decoder to . Only functions in WFM mode. # q|Q # Close connection # AOS # Acquisition of signal (AOS) event, start audio recording # LOS # Loss of signal (LOS) event, stop audio recording # LNB_LO [frequency] # If frequency [Hz] is specified set the LNB LO frequency used for # display. Otherwise print the current LNB LO frequency [Hz]. # \chk_vfo # Get VFO option status (only usable for hamlib compatibility) # \dump_state # Dump state (only usable for hamlib compatibility) # \get_powerstat # Get power status (only usable for hamlib compatibility) # v # Get 'VFO' (only usable for hamlib compatibility) # V # Set 'VFO' (only usable for hamlib compatibility) # s # Get 'Split' mode (only usable for hamlib compatibility) # S # Set 'Split' mode (only usable for hamlib compatibility) # _ # Get version # # Reply: # RPRT 0 # Command successful # RPRT 1 # Command failed gqrx_sock.write("#{cmd}\n") response = [] got_freq = false # Read all responses from gqrx_sock.write timeout = 0.001 if timeout.nil? begin response.push(gqrx_sock.readline.chomp) while gqrx_sock.wait_readable(timeout) raise IOError if response.empty? rescue IOError timeout += 0.001 retry end got_int_value_in_resp = true if response.first.to_i.positive? response = response.first if response.length == 1 raise "ERROR!!! Command: #{cmd} Expected Resp: #{resp_ok}, Got: #{response}" if resp_ok && response != resp_ok if got_int_value_in_resp fixed_len_freq = format('%0.12d', response.to_i) freq_segments = fixed_len_freq.scan(/.{3}/) first_non_zero_index = freq_segments.index { |s| s.to_i.positive? } freq_segments = freq_segments[first_non_zero_index..-1] freq_segments[0] = freq_segments.first.to_i.to_s response = freq_segments.join('.') end # DEBUG # puts response.inspect # puts response.length response rescue RuntimeError => e puts 'WARNING: RF Gain is not supported by the radio backend.' if e.message.include?('Command: L RF_GAIN') puts 'WARNING: Intermediate Gain is not supported by the radio backend.' if e.message.include?('Command: L IF_GAIN') puts 'WARNING: Baseband Gain is not supported by the radio backend.' if e.message.include?('Command: L BB_GAIN') raise e unless e.message.include?('Command: L RF_GAIN') || e.message.include?('Command: L IF_GAIN') || e.message.include?('Command: L BB_GAIN') end def init_freq(opts = {}) gqrx_sock = opts[:gqrx_sock] demodulator_mode = opts[:demodulator_mode] bandwidth = opts[:bandwidth] this_freq = opts[:this_freq] lock_freq_duration = opts[:lock_freq_duration] strength_lock = opts[:strength_lock] demod_n_passband = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'm' ) change_freq_resp = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: "F #{this_freq}", resp_ok: 'RPRT 0' ) current_freq = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'f' ) audio_gain_db = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'l AF' ).to_f current_strength = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'l STRENGTH' ).to_f current_squelch = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'l SQL' ).to_f rf_gain = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'l RF_GAIN' ).to_f if_gain = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'l IF_GAIN' ).to_f bb_gain = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: 'l BB_GAIN' ).to_f init_freq_hash = { demod_mode_n_passband: demod_n_passband, frequency: current_freq, bandwidth: bandwidth, audio_gain_db: audio_gain_db, squelch: current_squelch, rf_gain: rf_gain, if_gain: if_gain, bb_gain: bb_gain, strength: current_strength, strength_lock: strength_lock, lock_freq_duration: lock_freq_duration } print '.' sleep lock_freq_duration if current_strength > strength_lock init_freq_hash end def scan_range(opts = {}) gqrx_sock = opts[:gqrx_sock] demodulator_mode = opts[:demodulator_mode] bandwidth = opts[:bandwidth] start_freq = opts[:start_freq] target_freq = opts[:target_freq] precision = opts[:precision] lock_freq_duration = opts[:lock_freq_duration] strength_lock = opts[:strength_lock] multiplier = 10**(precision - 1) prev_freq_hash = { demod_mode_n_passband: demodulator_mode, frequency: start_freq, bandwidth: bandwidth, audio_gain_db: 0.0, squelch: 0.0, rf_gain: 0.0, if_gain: 0.0, bb_gain: 0.0, strength: 0.0, strength_lock: strength_lock, lock_freq_duration: lock_freq_duration } if start_freq > target_freq start_freq.downto(target_freq) do |this_freq| next unless (this_freq % multiplier).zero? init_freq_hash = init_freq( gqrx_sock: gqrx_sock, demodulator_mode: demodulator_mode, bandwidth: bandwidth, this_freq: this_freq, lock_freq_duration: lock_freq_duration, strength_lock: strength_lock ) current_strength = init_freq_hash[:strength] prev_strength = prev_freq_hash[:strength] prev_freq = prev_freq_hash[:frequency] approaching_detection = true if current_strength > prev_strength && current_strength > strength_lock if approaching_detection && current_strength <= prev_strength puts "\n**** Found a signal ~ #{prev_freq} Hz ****" puts JSON.pretty_generate(prev_freq_hash) approaching_detection = false end prev_freq_hash = init_freq_hash end else this_freq = start_freq while this_freq <= target_freq init_freq_hash = init_freq( gqrx_sock: gqrx_sock, demodulator_mode: demodulator_mode, bandwidth: bandwidth, this_freq: this_freq, lock_freq_duration: lock_freq_duration, strength_lock: strength_lock ) current_strength = init_freq_hash[:strength] prev_strength = prev_freq_hash[:strength] prev_freq = prev_freq_hash[:frequency] approaching_detection = true if current_strength > prev_strength && current_strength > strength_lock if approaching_detection && current_strength < prev_strength puts "\n**** Discovered a signal ~ #{prev_freq} Hz ****" puts JSON.pretty_generate(prev_freq_hash) approaching_detection = false end prev_freq_hash = init_freq_hash this_freq += multiplier end end end begin pwn_provider = 'ruby-gem' pwn_provider = ENV.fetch('PWN_PROVIDER') if ENV.keys.any? { |s| s == 'PWN_PROVIDER' } target_freq = opts[:target_freq] target_freq = target_freq.to_s.delete('.') unless target_freq.nil? target_freq = target_freq.to_i raise "ERROR: Invalid target frequency #{target_freq}" if target_freq.zero? host = opts[:host] ||= '127.0.0.1' port = opts[:port] ||= 7356 puts "Connecting to GQRX at #{host}:#{port}..." gqrx_sock = PWN::Plugins::Sock.connect(target: host, port: port) start_freq = opts[:start_freq] start_freq = start_freq.to_s.delete('.') unless start_freq.nil? start_freq = start_freq.to_i start_freq = gqrx_cmd(gqrx_sock: gqrx_sock, cmd: 'f', resp_ok: 'RPRT 0').to_i if start_freq.zero? demodulator_mode = opts[:demodulator_mode] ||= 'WFM_ST' demodulator_mode.upcase! raise "ERROR: Invalid demodulator mode: #{demodulator_mode}" unless %w[OFF RAW AM FM WFM WFM_ST WFM_ST_OIRT LSB USB CW CWL CWU].include?(demodulator_mode) bandwidth = opts[:bandwidth] ||= '200.000' puts "Setting demodulator mode to #{demodulator_mode} and bandwidth to #{bandwidth}..." bandwidth = bandwidth.to_s.delete('.').to_i unless bandwidth.nil? demod_resp = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: "M #{demodulator_mode} #{bandwidth}", resp_ok: 'RPRT 0' ) audio_gain_db = opts[:audio_gain_db] ||= 1.0 audio_gain_db = audio_gain_db.to_f audio_gain_db_resp = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: "L AF #{audio_gain_db}", resp_ok: 'RPRT 0' ) squelch = opts[:squelch] ||= -63.0 squelch = squelch.to_f squelch_resp = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: "L SQL #{squelch}", resp_ok: 'RPRT 0' ) precision = opts[:precision] ||= 5 precision = precision.to_i raise "ERROR: Invalid precision: #{precision}" unless (1..12).include?(precision) lock_freq_duration = opts[:lock_freq_duration] ||= 0.5 lock_freq_duration = lock_freq_duration.to_f strength_lock = opts[:strength_lock] ||= -60.0 strength_lock = strength_lock.to_f rf_gain = opts[:rf_gain] ||= 0.0 rf_gain = rf_gain.to_f rf_gain_resp = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: "L RF_GAIN #{rf_gain}", resp_ok: 'RPRT 0' ) intermediate_gain = opts[:intermediate_gain] ||= 32.0 intermediate_gain = intermediate_gain.to_f intermediate_resp = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: "L IF_GAIN #{intermediate_gain}", resp_ok: 'RPRT 0' ) baseband_gain = opts[:baseband_gain] ||= 10.0 baseband_gain = baseband_gain.to_f baseband_resp = gqrx_cmd( gqrx_sock: gqrx_sock, cmd: "L BB_GAIN #{baseband_gain}", resp_ok: 'RPRT 0' ) s_freq_pretty = start_freq.to_s.chars.insert(-4, '.').insert(-8, '.').join t_freq_pretty = target_freq.to_s.chars.insert(-4, '.').insert(-8, '.').join puts "*** Scanning from #{s_freq_pretty} to #{t_freq_pretty}\n\n\n" scan_range( gqrx_sock: gqrx_sock, demodulator_mode: demodulator_mode, bandwidth: bandwidth, start_freq: start_freq, target_freq: target_freq, precision: precision, lock_freq_duration: lock_freq_duration, strength_lock: strength_lock, squelch: squelch ) puts 'Scan Complete.' rescue StandardError => e raise e rescue Interrupt, SystemExit puts "\nGoodbye." ensure gqrx_sock = PWN::Plugins::Sock.disconnect(sock_obj: gqrx_sock) end