From e9911c5df8413dd5c9ddd4c7d37db4259d2a989b Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 29 Apr 2017 15:57:35 -0400 Subject: [PATCH] rx.py replaces scope.py --- op25/gr-op25_repeater/apps/gr_gnuplot.py | 181 ++ op25/gr-op25_repeater/apps/rx.py | 635 +++++ op25/gr-op25_repeater/apps/scope.py | 2760 ---------------------- op25/gr-op25_repeater/apps/terminal.py | 176 ++ 4 files changed, 992 insertions(+), 2760 deletions(-) create mode 100644 op25/gr-op25_repeater/apps/gr_gnuplot.py create mode 100755 op25/gr-op25_repeater/apps/rx.py delete mode 100755 op25/gr-op25_repeater/apps/scope.py create mode 100644 op25/gr-op25_repeater/apps/terminal.py diff --git a/op25/gr-op25_repeater/apps/gr_gnuplot.py b/op25/gr-op25_repeater/apps/gr_gnuplot.py new file mode 100644 index 0000000..168941e --- /dev/null +++ b/op25/gr-op25_repeater/apps/gr_gnuplot.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python + +# Copyright 2011, 2012, 2013, 2014, 2015 Max H. Parke KA1RBI +# +# This file is part of OP25 +# +# OP25 is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# OP25 is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OP25; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Boston, MA +# 02110-1301, USA. + +import sys +import subprocess + +from gnuradio import gr, gru, eng_notation +from gnuradio import blocks, audio +from gnuradio.eng_option import eng_option +import numpy as np +from gnuradio import gr + +_def_debug = 0 +_def_sps = 10 + +GNUPLOT = '/usr/bin/gnuplot' + +class wrap_gp(object): + def __init__(self, sps=_def_sps): + self.sps = sps + + self.attach_gp() + self.buf = [] + + def attach_gp(self): + args = (GNUPLOT, '-noraise') + exe = GNUPLOT + self.gp = subprocess.Popen(args, executable=exe, stdin=subprocess.PIPE) + + def kill(self): + self.gp.kill() + self.gp.wait() + + def plot(self, buf, bufsz, mode='eye'): + BUFSZ = bufsz + consumed = min(len(buf), BUFSZ-len(self.buf)) + if len(self.buf) < BUFSZ: + self.buf.extend(buf[:consumed]) + if len(self.buf) < BUFSZ: + return consumed + plots = [] + s = '' + while(len(self.buf)): + if mode == 'eye': + if len(self.buf) < self.sps: + break + for i in range(self.sps): + s += '%f\n' % self.buf[i] + s += 'e\n' + self.buf=self.buf[self.sps:] + plots.append('"-" with lines') + elif mode == 'constellation': + for b in self.buf: + s += '%f\t%f\n' % (b.real, b.imag) + s += 'e\n' + self.buf = [] + plots.append('"-" with points') + elif mode == 'symbol': + for b in self.buf: + s += '%f\n' % (b) + s += 'e\n' + self.buf = [] + plots.append('"-" with dots') + elif mode == 'fft': + ffbuf = np.fft.fft(self.buf) + for b in ffbuf: + s += '%f\n' % (b.real**2 + b.imag**2) + s += 'e\n' + self.buf = [] + plots.append('"-" with lines') + self.buf = [] + + h= 'set terminal x11 noraise\n' + h+= 'set size square\n' + h += 'set object 1 rectangle from screen 0,0 to screen 1,1 fillcolor rgb"black"\n' + h+= 'set key off\n' + if mode == 'constellation': + h+= 'set xrange [-1:1]\n' + h+= 'set yrange [-1:1]\n' + elif mode == 'eye': + h+= 'set yrange [-4:4]\n' + elif mode == 'symbol': + h+= 'set yrange [-4:4]\n' + dat = '%splot %s\n%s' % (h, ','.join(plots), s) + self.gp.stdin.write(dat) + return consumed + +class eye_sink_f(gr.sync_block): + """ + """ + def __init__(self, debug = _def_debug, sps = _def_sps): + gr.sync_block.__init__(self, + name="eye_sink_f", + in_sig=[np.float32], + out_sig=None) + self.debug = debug + self.sps = sps + self.gnuplot = wrap_gp(sps=self.sps) + + def work(self, input_items, output_items): + in0 = input_items[0] + consumed = self.gnuplot.plot(in0, 100 * self.sps, mode='eye') + return consumed ### len(input_items[0]) + + def kill(self): + self.gnuplot.kill() + +class constellation_sink_c(gr.sync_block): + """ + """ + def __init__(self, debug = _def_debug): + gr.sync_block.__init__(self, + name="constellation_sink_c", + in_sig=[np.complex64], + out_sig=None) + self.debug = debug + self.gnuplot = wrap_gp() + + def work(self, input_items, output_items): + in0 = input_items[0] + self.gnuplot.plot(in0, 1000, mode='constellation') + return len(input_items[0]) + + def kill(self): + self.gnuplot.kill() + +class fft_sink_c(gr.sync_block): + """ + """ + def __init__(self, debug = _def_debug): + gr.sync_block.__init__(self, + name="fft_sink_c", + in_sig=[np.complex64], + out_sig=None) + self.debug = debug + self.gnuplot = wrap_gp() + + def work(self, input_items, output_items): + in0 = input_items[0] + self.gnuplot.plot(in0, 512, mode='fft') + return len(input_items[0]) + + def kill(self): + self.gnuplot.kill() + +class symbol_sink_f(gr.sync_block): + """ + """ + def __init__(self, debug = _def_debug): + gr.sync_block.__init__(self, + name="symbol_sink_f", + in_sig=[np.float32], + out_sig=None) + self.debug = debug + self.gnuplot = wrap_gp() + + def work(self, input_items, output_items): + in0 = input_items[0] + self.gnuplot.plot(in0, 2400, mode='symbol') + return len(input_items[0]) + + def kill(self): + self.gnuplot.kill() diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py new file mode 100755 index 0000000..6cf6aaf --- /dev/null +++ b/op25/gr-op25_repeater/apps/rx.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python + +# Copyright 2008-2011 Steve Glass +# +# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI +# +# Copyright 2003,2004,2005,2006 Free Software Foundation, Inc. +# (from radiorausch) +# +# This file is part of OP25 and part of GNU Radio +# +# OP25 is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# OP25 is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OP25; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Boston, MA +# 02110-1301, USA. + +import os +import pickle +import sys +import threading +import math +import numpy +import time +import re +import json +try: + import Hamlib +except: + pass + +try: + import Numeric +except: + pass + +from gnuradio import audio, eng_notation, gr, gru, filter, blocks, fft, analog, digital +from gnuradio.eng_option import eng_option +from math import pi +from optparse import OptionParser + +import op25 +import op25_repeater + +import trunking + +import p25_demodulator +import p25_decoder + +sys.path.append('tdma') +import lfsr + +from gr_gnuplot import constellation_sink_c +from gr_gnuplot import fft_sink_c +from gr_gnuplot import symbol_sink_f +from gr_gnuplot import eye_sink_f + +from terminal import curses_terminal + +#speeds = [300, 600, 900, 1200, 1440, 1800, 1920, 2400, 2880, 3200, 3600, 3840, 4000, 4800, 6000, 6400, 7200, 8000, 9600, 14400, 19200] +speeds = [4800, 6000] + +os.environ['IMBE'] = 'soft' + +WIRESHARK_PORT = 23456 + +# The P25 receiver +# +class p25_rx_block (gr.top_block): + + # Initialize the P25 receiver + # + def __init__(self): + + gr.top_block.__init__(self) + + # command line argument parsing + parser = OptionParser(option_class=eng_option) + parser.add_option("--args", type="string", default="", help="device args") + parser.add_option("--antenna", type="string", default="", help="select antenna") + parser.add_option("-a", "--audio", action="store_true", default=False, help="use direct audio input") + parser.add_option("-A", "--audio-if", action="store_true", default=False, help="soundcard IF mode (use --calibration to set IF freq)") + parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") + parser.add_option("-i", "--input", default=None, help="input file name") + parser.add_option("-b", "--excess-bw", type="eng_float", default=0.2, help="for RRC filter", metavar="Hz") + parser.add_option("-c", "--calibration", type="eng_float", default=0.0, help="USRP offset or audio IF frequency", metavar="Hz") + parser.add_option("-C", "--costas-alpha", type="eng_float", default=0.04, help="value of alpha for Costas loop", metavar="Hz") + parser.add_option("-D", "--demod-type", type="choice", default="cqpsk", choices=('cqpsk', 'fsk4'), help="cqpsk | fsk4") + parser.add_option("-P", "--plot-mode", type="choice", default=None, choices=(None, 'constellation', 'symbol', 'datascope'), help="constellation | symbol | datascope") + parser.add_option("-f", "--frequency", type="eng_float", default=0.0, help="USRP center frequency", metavar="Hz") + parser.add_option("-F", "--ifile", type="string", default=None, help="read input from complex capture file") + parser.add_option("-H", "--hamlib-model", type="int", default=None, help="specify model for hamlib") + parser.add_option("-s", "--seek", type="int", default=0, help="ifile seek in K") + parser.add_option("-L", "--logfile-workers", type="int", default=None, help="number of demodulators to instantiate") + parser.add_option("-S", "--sample-rate", type="int", default=320e3, help="source samp rate") + parser.add_option("-t", "--tone-detect", action="store_true", default=False, help="use experimental tone detect algorithm") + parser.add_option("-T", "--trunk-conf-file", type="string", default=None, help="trunking config file name") + parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level") + parser.add_option("-V", "--vocoder", action="store_true", default=False, help="voice codec") + parser.add_option("-o", "--offset", type="eng_float", default=0.0, help="tuning offset frequency [to circumvent DC offset]", metavar="Hz") + parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") + parser.add_option("-w", "--wireshark", action="store_true", default=False, help="output data to Wireshark") + parser.add_option("-W", "--wireshark-host", type="string", default="127.0.0.1", help="Wireshark host") + parser.add_option("-r", "--raw-symbols", type="string", default=None, help="dump decoded symbols to file") + parser.add_option("-R", "--rx-subdev-spec", type="subdev", default=(0, 0), help="select USRP Rx side A or B (default=A)") + parser.add_option("-g", "--gain", type="eng_float", default=None, help="set USRP gain in dB (default is midpoint) or set audio gain") + parser.add_option("-G", "--gain-mu", type="eng_float", default=0.025, help="gardner gain") + parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") + parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name") + parser.add_option("-q", "--freq-corr", type="eng_float", default=0.0, help="frequency correction") + parser.add_option("-2", "--phase2-tdma", action="store_true", default=False, help="enable phase2 tdma decode") + parser.add_option("-Z", "--decim-amt", type="int", default=1, help="spectrum decimation") + (options, args) = parser.parse_args() + if len(args) != 0: + parser.print_help() + sys.exit(1) + + self.channel_rate = 0 + self.baseband_input = False + self.rtl_found = False + self.channel_rate = options.sample_rate + + self.src = None + if not options.input: + # check if osmocom is accessible + try: + import osmosdr + self.src = osmosdr.source(options.args) + except Exception: + print "osmosdr source_c creation failure" + ignore = True + + if "rtl" in options.args.lower(): + #print "'rtl' has been found in options.args (%s)" % (options.args) + self.rtl_found = True + + gain_names = self.src.get_gain_names() + for name in gain_names: + range = self.src.get_gain_range(name) + print "gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step()) + if options.gains: + for tuple in options.gains.split(","): + name, gain = tuple.split(":") + gain = int(gain) + print "setting gain %s to %d" % (name, gain) + self.src.set_gain(gain, name) + + rates = self.src.get_sample_rates() + try: + print 'supported sample rates %d-%d step %d' % (rates.start(), rates.stop(), rates.step()) + except: + pass # ignore + + if options.freq_corr: + self.src.set_freq_corr(options.freq_corr) + + if options.audio: + self.channel_rate = 48000 + self.baseband_input = True + + if options.audio_if: + self.channel_rate = 96000 + + if options.ifile: + self.channel_rate = 96000 # TODO: fixme + + # setup (read-only) attributes + self.symbol_rate = 4800 + self.symbol_deviation = 600.0 + self.basic_rate = 48000 + _default_speed = 4800 + + # keep track of flow graph connections + self.cnxns = [] + + self.datascope_raw_input = False + self.data_scope_connected = False + + self.constellation_scope_connected = False + + self.options = options + + for i in xrange(len(speeds)): + if speeds[i] == _default_speed: + self.current_speed = i + self.default_speed_idx = i + + if options.hamlib_model: + self.hamlib_attach(options.hamlib_model) + + # wait for gdb + if options.pause: + print 'Ready for GDB to attach (pid = %d)' % (os.getpid(),) + raw_input("Press 'Enter' to continue...") + + # attach terminal thread + self.input_q = gr.msg_queue(10) + self.output_q = gr.msg_queue(10) + self.terminal = curses_terminal(self.input_q, self.output_q) + + # configure specified data source + if options.input: + self.open_file(options.input) + elif options.frequency: + self.open_usrp() + elif options.audio_if: + self.open_audio_c(self.channel_rate, options.gain, options.audio_input) + elif options.audio: + self.open_audio(self.channel_rate, options.gain, options.audio_input) + elif options.ifile: + self.open_ifile(self.channel_rate, options.gain, options.ifile, options.seek) + else: + pass + + # setup common flow graph elements + # + def __build_graph(self, source, capture_rate): + global speeds + global WIRESHARK_PORT + # tell the scope the source rate + + self.rx_q = gr.msg_queue(100) + udp_port = 0 + if self.options.wireshark: + udp_port = WIRESHARK_PORT + + self.tdma_state = False + self.xor_cache = {} + + self.fft_state = False + self.c4fm_state = False + self.fscope_state = False + self.corr_state = False + self.fac_state = False + self.fsk4_demod_connected = False + self.psk_demod_connected = False + self.fsk4_demod_mode = False + self.corr_i_chan = False + + if self.baseband_input: + self.demod = p25_demodulator.p25_demod_fb(input_rate=capture_rate) + else: # complex input + # local osc + self.lo_freq = self.options.offset + if self.options.audio_if or self.options.ifile or self.options.input: + self.lo_freq += self.options.calibration + self.demod = p25_demodulator.p25_demod_cb( input_rate = capture_rate, + demod_type = self.options.demod_type, + relative_freq = self.lo_freq, + offset = self.options.offset, + if_rate = 48000, + gain_mu = self.options.gain_mu, + costas_alpha = self.options.costas_alpha, + symbol_rate = self.symbol_rate) + + udp_port = 0 + if self.options.wireshark: + udp_port = WIRESHARK_PORT + + num_ambe = 0 + if self.options.phase2_tdma: + num_ambe = 1 + + self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=True, num_ambe=num_ambe, wireshark_host=self.options.wireshark_host, udp_port=udp_port, do_msgq = True, msgq=self.rx_q, audio_output=self.options.audio_output, debug=self.options.verbosity) + + # connect it all up + self.connect(source, self.demod, self.decoder) + + self.kill_sink = None + if self.options.plot_mode == 'constellation': + assert self.options.demod_type == 'cqpsk' ## constellation requires cqpsk demod-type + self.constellation_sink = constellation_sink_c() + self.demod.connect_complex('diffdec', self.constellation_sink) + self.kill_sink = self.constellation_sink + elif self.options.plot_mode == 'symbol': + self.symbol_sink = symbol_sink_f() + self.demod.connect_float(self.symbol_sink) + self.kill_sink = self.symbol_sink + elif self.options.plot_mode == 'datascope': + assert self.options.demod_type == 'fsk4' ## datascope requires fsk4 demod-type + self.eye_sink = eye_sink_f(sps=10) + self.demod.connect_bb('symbol_filter', self.eye_sink) + self.kill_sink = self.eye_sink + + if self.options.raw_symbols: + self.sink_sf = blocks.file_sink(gr.sizeof_char, self.options.raw_symbols) + self.connect(self.demod, self.sink_sf) + + logfile_workers = [] + if self.options.phase2_tdma: + num_ambe = 2 + if self.options.logfile_workers: + for i in xrange(self.options.logfile_workers): + demod = p25_demodulator.p25_demod_cb(input_rate=capture_rate, + demod_type=self.options.demod_type, + offset=self.options.offset) + decoder = p25_decoder.p25_decoder_sink_b(debug = self.options.verbosity, do_imbe = self.options.vocoder, num_ambe=num_ambe) + logfile_workers.append({'demod': demod, 'decoder': decoder, 'active': False}) + self.connect(source, demod, decoder) + + self.trunk_rx = trunking.rx_ctl(frequency_set = self.change_freq, debug = self.options.verbosity, conf_file = self.options.trunk_conf_file, logfile_workers=logfile_workers) + + self.du_watcher = du_queue_watcher(self.rx_q, self.trunk_rx.process_qmsg) + + # Connect up the flow graph + # + def __connect(self, cnxns): + for l in cnxns: + for b in l: + if b == l[0]: + p = l[0] + else: + self.connect(p, b) + p = b + self.cnxns.extend(cnxns) + + # Disconnect the flow graph + # + def __disconnect(self): + for l in self.cnxns: + for b in l: + if b == l[0]: + p = l[0] + else: + self.disconnect(p, b) + p = b + self.cnxns = [] + + def set_speed(self, new_speed): + # assumes that lock is held, or that we are in init + self.disconnect_demods() + self.current_speed = new_speed + self.connect_fsk4_demod() + + def configure_tdma(self, params): + if params['tdma'] is not None and not self.options.phase2_tdma: + print '***TDMA request for frequency %d failed- phase2_tdma option not enabled' % params['freq'] + return + set_tdma = False + if params['tdma'] is not None: + set_tdma = True + if set_tdma == self.tdma_state: + return # already in desired state + self.tdma_state = set_tdma + if set_tdma: + self.decoder.set_slotid(params['tdma']) + hash = '%x%x%x' % (params['nac'], params['sysid'], params['wacn']) + if hash not in self.xor_cache: + self.xor_cache[hash] = lfsr.p25p2_lfsr(params['nac'], params['sysid'], params['wacn']).xor_chars + self.decoder.set_xormask(self.xor_cache[hash], hash) + sps = self.basic_rate / 6000 + else: + sps = self.basic_rate / 4800 + self.demod.clock.set_omega(float(sps)) + + def change_freq(self, params): + freq = params['freq'] + offset = params['offset'] + center_freq = params['center_frequency'] + + if self.options.hamlib_model: + self.hamlib.set_freq(freq) + elif params['center_frequency']: + relative_freq = center_freq - freq + if abs(relative_freq + self.options.offset) > self.channel_rate / 2: + print '***unable to tune Local Oscillator to offset %d Hz' % (relative_freq + self.options.offset) + print '***limit is one half of sample-rate %d = %d' % (self.channel_rate, self.channel_rate / 2) + print '***request for frequency %d rejected' % freq + + self.lo_freq = self.options.offset + relative_freq + self.demod.set_relative_frequency(self.lo_freq) + self.set_freq(center_freq + offset) + #self.spectrum.set_baseband_freq(center_freq) + else: + self.set_freq(freq + offset) + + self.configure_tdma(params) + + params['json_type'] = 'change_freq' + js = json.dumps(params) + msg = gr.message().make_from_string(js, -4, 0, 0) + self.input_q.insert_tail(msg) + + def hamlib_attach(self, model): + Hamlib.rig_set_debug (Hamlib.RIG_DEBUG_NONE) # RIG_DEBUG_TRACE + + self.hamlib = Hamlib.Rig (model) + self.hamlib.set_conf ("serial_speed","9600") + self.hamlib.set_conf ("retry","5") + + self.hamlib.open () + + def q_action(self, action): + msg = gr.message().make_from_string(action, -2, 0, 0) + self.rx_q.insert_tail(msg) + + def set_gain(self, gain): + if self.rtl_found: + self.src.set_gain(gain, 'LNA') + if self.options.verbosity: + print 'RTL Gain of %d set to: %.1f' % (gain, self.src.get_gain('LNA')) + else: + if self.baseband_input: + f = 1.0 + else: + f = 0.1 + self.demod.set_baseband_gain(float(gain) * f) + + def set_audio_scaler(self, vol): + #print 'audio scaler: %f' % ((1 / 32768.0) * (vol * 0.1)) + self.decoder.set_scaler_k((1 / 32768.0) * (vol * 0.1)) + + def set_rtl_ppm(self, ppm): + self.src.set_freq_corr(ppm) + + def set_freq_tune(self, val): + self.demod.set_relative_frequency(val + self.lo_freq) + + def set_freq(self, target_freq): + """ + Set the center frequency we're interested in. + + @param target_freq: frequency in Hz + @rypte: bool + + Tuning is a two step process. First we ask the front-end to + tune as close to the desired frequency as it can. Then we use + the result of that operation and our target_frequency to + determine the value for the digital down converter. + """ + if not self.src: + return False + tune_freq = target_freq + self.options.calibration + self.options.offset + r = self.src.set_center_freq(tune_freq) + + if r: + #self.myform['freq'].set_value(target_freq) # update displayed va + #if self.show_debug_info: + # self.myform['baseband'].set_value(r.baseband_freq) + # self.myform['ddc'].set_value(r.dxc_freq) + return True + + return False + + # read capture file properties (decimation etc.) + # + def __read_file_properties(self, filename): + f = open(filename, "r") + self.info = pickle.load(f) + ToDo = True + f.close() + + # setup to rx from file + # + def __set_rx_from_file(self, filename, capture_rate): + file = blocks.file_source(gr.sizeof_gr_complex, filename, True) + gain = blocks.multiply_const_cc(self.options.gain) + throttle = blocks.throttle(gr.sizeof_gr_complex, capture_rate) + self.__connect([[file, gain, throttle]]) + self.__build_graph(throttle, capture_rate) + + # setup to rx from Audio + # + def __set_rx_from_audio(self, capture_rate): + self.__build_graph(self.source, capture_rate) + + # setup to rx from USRP + # + def __set_rx_from_osmosdr(self): + # setup osmosdr + capture_rate = self.src.set_sample_rate(self.options.sample_rate) + if self.options.antenna: + self.src.set_antenna(self.options.antenna) + self.info["capture-rate"] = capture_rate + self.src.set_bandwidth(capture_rate) + r = self.src.set_center_freq(self.options.frequency + self.options.calibration+ self.options.offset) + print 'set_center_freq: %d' % r + if not r: + raise RuntimeError("failed to set USRP frequency") + # capture file + # if preserve: + if 0: + try: + self.capture_filename = os.tmpnam() + except RuntimeWarning: + ignore = True + capture_file = blocks.file_sink(gr.sizeof_gr_complex, self.capture_filename) + self.__connect([[self.usrp, capture_file]]) + else: + self.capture_filename = None + # everything else + self.__build_graph(self.src, capture_rate) + + # Write capture file properties + # + def __write_file_properties(self, filename): + f = open(filename, "w") + pickle.dump(self.info, f) + f.close() + + # Adjust the channel offset + # + def adjust_channel_offset(self, delta_hz): + max_delta_hz = 12000.0 + delta_hz *= self.symbol_deviation + delta_hz = max(delta_hz, -max_delta_hz) + delta_hz = min(delta_hz, max_delta_hz) + self.channel_filter.set_center_freq(self.channel_offset - delta_hz+ self.options.offset) + + def open_ifile(self, capture_rate, gain, input_filename, file_seek): + speed = 96000 # TODO: fixme + ifile = blocks.file_source(gr.sizeof_gr_complex, input_filename, 1) + if file_seek > 0: + rc = ifile.seek(file_seek*1024, gr.SEEK_SET) + assert rc == True + #print "seek: %d, rc = %d" % (file_seek, rc) + throttle = blocks.throttle(gr.sizeof_gr_complex, speed) + self.source = blocks.multiply_const_cc(gain) + self.connect(ifile, throttle, self.source) + self.__set_rx_from_audio(speed) + + def open_audio_c(self, capture_rate, gain, audio_input_filename): + self.info = { + "capture-rate": capture_rate, + "center-freq": 0, + "source-dev": "AUDIO", + "source-decim": 1 } + self.audio_source = audio.source(capture_rate, audio_input_filename) + self.audio_cvt = blocks.float_to_complex() + self.connect((self.audio_source, 0), (self.audio_cvt, 0)) + self.connect((self.audio_source, 1), (self.audio_cvt, 1)) + self.source = blocks.multiply_const_cc(gain) + self.connect(self.audio_cvt, self.source) + self.__set_rx_from_audio(capture_rate) + + def open_audio(self, capture_rate, gain, audio_input_filename): + self.info = { + "capture-rate": capture_rate, + "center-freq": 0, + "source-dev": "AUDIO", + "source-decim": 1 } + self.audio_source = audio.source(capture_rate, audio_input_filename) + self.source = blocks.multiply_const_ff(gain) + self.connect(self.audio_source, self.source) + self.__set_rx_from_audio(capture_rate) + + # Open the USRP + # + def open_usrp(self): + # try: + self.info = { + "capture-rate": "unknown", + "center-freq": self.options.frequency, + "source-dev": "USRP", + "source-decim": 1 } + self.__set_rx_from_osmosdr() + # except Exception, x: + # wx.MessageBox("Cannot open USRP: " + x.message, "USRP Error", wx.CANCEL | wx.ICON_EXCLAMATION) + + # Set the channel offset + # + def set_channel_offset(self, offset_hz, scale, units): + self.channel_offset = -offset_hz + self.channel_filter.set_center_freq(self.channel_offset+ self.options.offset) + self.frame.SetStatusText("Channel offset: " + str(offset_hz * scale) + units, 1) + + # Set the RF squelch threshold level + # + def set_squelch_threshold(self, squelch_db): + self.squelch.set_threshold(squelch_db) + self.frame.SetStatusText("Squelch: " + str(squelch_db) + "dB", 2) + + def process_qmsg(self, msg): + # return true = end top block + RX_COMMANDS = 'skip lockout hold' + s = msg.to_string() + if s == 'quit': return True + elif s == 'update': + js = self.trunk_rx.to_json() + msg = gr.message().make_from_string(js, -4, 0, 0) + self.input_q.insert_tail(msg) + elif s == 'set_freq': + freq = msg.arg1() + self.set_freq(freq) + elif s == 'add_default_config': + nac = msg.arg1() + self.trunk_rx.add_default_config(nac) + elif s in RX_COMMANDS: + self.rx_q.insert_tail(msg) + return False + +############################################################################ + +# data unit receive queue +# +class du_queue_watcher(threading.Thread): + + def __init__(self, msgq, callback, **kwds): + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.msgq = msgq + self.callback = callback + self.keep_running = True + self.start() + + def run(self): + while(self.keep_running): + msg = self.msgq.delete_head() + self.callback(msg) + +# Start the receiver +# + +if __name__ == "__main__": + tb = p25_rx_block() + tb.start() + try: + while True: + msg = tb.output_q.delete_head() + if tb.process_qmsg(msg): + break + except KeyboardInterrupt: + print 'keyboard interrupt' + tb.stop() + if tb.kill_sink: + tb.kill_sink.kill() diff --git a/op25/gr-op25_repeater/apps/scope.py b/op25/gr-op25_repeater/apps/scope.py deleted file mode 100755 index 3c8c6b4..0000000 --- a/op25/gr-op25_repeater/apps/scope.py +++ /dev/null @@ -1,2760 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2008-2011 Steve Glass -# -# Copyright 2011, 2012, 2013, 2014, 2015 Max H. Parke KA1RBI -# -# Copyright 2003,2004,2005,2006 Free Software Foundation, Inc. -# (from radiorausch) -# -# This file is part of OP25 and part of GNU Radio -# -# OP25 is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3, or (at your option) -# any later version. -# -# OP25 is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public -# License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OP25; see the file COPYING. If not, write to the Free -# Software Foundation, Inc., 51 Franklin Street, Boston, MA -# 02110-1301, USA. - -import os -import pickle -import sys -import threading -import wx -import wx.html -import wx.wizard -import math -import numpy -import time -import re -try: - import Hamlib -except: - pass - -try: - import Numeric -except: - pass - -from gnuradio import audio, eng_notation, gr, gru, filter, blocks, fft, analog, digital -from gnuradio.eng_option import eng_option -from gnuradio.wxgui import stdgui2, fftsink2, scopesink2, form -from math import pi -from optparse import OptionParser - -import op25 -import op25_repeater - -import gnuradio.wxgui.plot as plot - -import trunking - -import p25_demodulator -import p25_decoder - -sys.path.append('tdma') -import lfsr - -#speeds = [300, 600, 900, 1200, 1440, 1800, 1920, 2400, 2880, 3200, 3600, 3840, 4000, 4800, 6000, 6400, 7200, 8000, 9600, 14400, 19200] -speeds = [4800, 6000] - -os.environ['IMBE'] = 'soft' - -WIRESHARK_PORT = 23456 - -msg_wxDATA_EVENT = wx.NewEventType() - -def msg_EVT_DATA_EVENT(win, func): - win.Connect(-1, -1, msg_wxDATA_EVENT, func) - -class msg_DataEvent(wx.PyEvent): - def __init__(self, data): - wx.PyEvent.__init__(self) - self.SetEventType (msg_wxDATA_EVENT) - self.data = data - - def Clone (self): - self.__class__ (self.GetId()) - -# The P25 receiver -# -class p25_rx_block (stdgui2.std_top_block): - - # Initialize the P25 receiver - # - def __init__(self, frame, panel, vbox, argv): - - stdgui2.std_top_block.__init__(self, frame, panel, vbox, argv) - - # command line argument parsing - parser = OptionParser(option_class=eng_option) - parser.add_option("--args", type="string", default="", help="device args") - parser.add_option("--antenna", type="string", default="", help="select antenna") - parser.add_option("-a", "--audio", action="store_true", default=False, help="use direct audio input") - parser.add_option("-A", "--audio-if", action="store_true", default=False, help="soundcard IF mode (use --calibration to set IF freq)") - parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") - parser.add_option("-i", "--input", default=None, help="input file name") - parser.add_option("-b", "--excess-bw", type="eng_float", default=0.2, help="for RRC filter", metavar="Hz") - parser.add_option("-c", "--calibration", type="eng_float", default=0.0, help="USRP offset or audio IF frequency", metavar="Hz") - parser.add_option("-C", "--costas-alpha", type="eng_float", default=0.04, help="value of alpha for Costas loop", metavar="Hz") - parser.add_option("-f", "--frequency", type="eng_float", default=0.0, help="USRP center frequency", metavar="Hz") - parser.add_option("-F", "--ifile", type="string", default=None, help="read input from complex capture file") - parser.add_option("-H", "--hamlib-model", type="int", default=None, help="specify model for hamlib") - parser.add_option("-s", "--seek", type="int", default=0, help="ifile seek in K") - parser.add_option("-L", "--logfile-workers", type="int", default=None, help="number of demodulators to instantiate") - parser.add_option("-S", "--sample-rate", type="int", default=320e3, help="source samp rate") - parser.add_option("-t", "--tone-detect", action="store_true", default=False, help="use experimental tone detect algorithm") - parser.add_option("-T", "--trunk-conf-file", type="string", default=None, help="trunking config file name") - parser.add_option("-v", "--verbosity", type="int", default=10, help="message debug level") - parser.add_option("-V", "--vocoder", action="store_true", default=False, help="voice codec") - parser.add_option("-o", "--offset", type="eng_float", default=0.0, help="tuning offset frequency [to circumvent DC offset]", metavar="Hz") - parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") - parser.add_option("-w", "--wireshark", action="store_true", default=False, help="output data to Wireshark") - parser.add_option("-W", "--wireshark-host", type="string", default="127.0.0.1", help="Wireshark host") - parser.add_option("-r", "--raw-symbols", type="string", default=None, help="dump decoded symbols to file") - parser.add_option("-R", "--rx-subdev-spec", type="subdev", default=(0, 0), help="select USRP Rx side A or B (default=A)") - parser.add_option("-g", "--gain", type="eng_float", default=None, help="set USRP gain in dB (default is midpoint) or set audio gain") - parser.add_option("-G", "--gain-mu", type="eng_float", default=0.025, help="gardner gain") - parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") - #parser.add_option("-O", "--audio-output", type="string", default="plughw:0,0", help="audio output device name") - parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name") - parser.add_option("-q", "--freq-corr", type="eng_float", default=0.0, help="frequency correction") - parser.add_option("-2", "--phase2-tdma", action="store_true", default=False, help="enable phase2 tdma decode") - parser.add_option("-Z", "--decim-amt", type="int", default=1, help="spectrum decimation") - (options, args) = parser.parse_args() - if len(args) != 0: - parser.print_help() - sys.exit(1) - - self.channel_rate = 0 - self.baseband_input = False - self.rtl_found = False - self.channel_rate = options.sample_rate - - self.src = None - if not options.input: - # check if osmocom is accessible - try: - import osmosdr - self.src = osmosdr.source(options.args) - except Exception: - print "osmosdr source_c creation failure" - ignore = True - - if "rtl" in options.args.lower(): - #print "'rtl' has been found in options.args (%s)" % (options.args) - self.rtl_found = True - - gain_names = self.src.get_gain_names() - for name in gain_names: - range = self.src.get_gain_range(name) - print "gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step()) - if options.gains: - for tuple in options.gains.split(","): - name, gain = tuple.split(":") - gain = int(gain) - print "setting gain %s to %d" % (name, gain) - self.src.set_gain(gain, name) - - rates = self.src.get_sample_rates() - try: - print 'supported sample rates %d-%d step %d' % (rates.start(), rates.stop(), rates.step()) - except: - pass # ignore - - if options.freq_corr: - self.src.set_freq_corr(options.freq_corr) - - if options.audio: - self.channel_rate = 48000 - self.baseband_input = True - - if options.audio_if: - self.channel_rate = 96000 - - if options.ifile: - self.channel_rate = 96000 # TODO: fixme - - # setup (read-only) attributes - self.symbol_rate = 4800 - self.symbol_deviation = 600.0 - self.basic_rate = 48000 - _default_speed = 4800 - - # keep track of flow graph connections - self.cnxns = [] - - self.datascope_raw_input = False - self.data_scope_connected = False - - self.constellation_scope_connected = False - - self.options = options - - for i in xrange(len(speeds)): - if speeds[i] == _default_speed: - self.current_speed = i - self.default_speed_idx = i - - if options.hamlib_model: - self.hamlib_attach(options.hamlib_model) - - # initialize the UI - # - self.__init_gui(frame, panel, vbox) - - # wait for gdb - if options.pause: - print 'Ready for GDB to attach (pid = %d)' % (os.getpid(),) - raw_input("Press 'Enter' to continue...") - - # configure specified data source - if options.input: - self.open_file(options.input) - elif options.frequency: - self._set_state("CAPTURING") - self.open_usrp() - elif options.audio_if: - self._set_state("CAPTURING") - self.open_audio_c(self.channel_rate, options.gain, options.audio_input) - elif options.audio: - self._set_state("CAPTURING") - self.open_audio(self.channel_rate, options.gain, options.audio_input) - # skip past unused FFT spectrum plot - self.notebook.AdvanceSelection() - elif options.ifile: - self._set_state("CAPTURING") - self.open_ifile(self.channel_rate, options.gain, options.ifile, options.seek) - else: - self._set_state("STOPPED") - - # setup common flow graph elements - # - def __build_graph(self, source, capture_rate): - global speeds - global WIRESHARK_PORT - # tell the scope the source rate - self.spectrum.set_sample_rate(capture_rate / self.options.decim_amt) - - self.rx_q = gr.msg_queue(100) - msg_EVT_DATA_EVENT(self.frame, self.msg_data) - udp_port = 0 - if self.options.wireshark: - udp_port = WIRESHARK_PORT - - self.tdma_state = False - self.xor_cache = {} - - self.fft_state = False - self.c4fm_state = False - self.fscope_state = False - self.corr_state = False - self.fac_state = False - self.fsk4_demod_connected = False - self.psk_demod_connected = False - self.fsk4_demod_mode = False - self.corr_i_chan = False - - if self.baseband_input: - self.demod = p25_demodulator.p25_demod_fb(input_rate=capture_rate) - self.set_connection(c4fm=1) - else: # complex input - # local osc - self.lo_freq = self.options.offset - if self.options.audio_if or self.options.ifile or self.options.input: - self.lo_freq += self.options.calibration - self.demod = p25_demodulator.p25_demod_cb( input_rate = capture_rate, - demod_type = 'cqpsk', ### FIXME - relative_freq = self.lo_freq, - offset = self.options.offset, - if_rate = 48000, - gain_mu = self.options.gain_mu, - costas_alpha = self.options.costas_alpha, - symbol_rate = self.symbol_rate) - self.set_connection(fft=1) - self.connect_demods() - - udp_port = 0 - if self.options.wireshark: - udp_port = WIRESHARK_PORT - - num_ambe = 0 - if self.options.phase2_tdma: - num_ambe = 1 - - self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=True, num_ambe=num_ambe, wireshark_host=self.options.wireshark_host, udp_port=udp_port, do_msgq = True, msgq=self.rx_q, audio_output=self.options.audio_output, debug=self.options.verbosity) - - # connect it all up - self.connect(source, self.demod, self.decoder) - - if self.options.raw_symbols: - self.sink_sf = blocks.file_sink(gr.sizeof_char, self.options.raw_symbols) - self.connect(self.demod, self.sink_sf) - - logfile_workers = [] - if self.options.phase2_tdma: - num_ambe = 2 - if self.options.logfile_workers: - for i in xrange(self.options.logfile_workers): - demod = p25_demodulator.p25_demod_cb(input_rate=capture_rate, - demod_type='cqpsk', ### FIXME - offset=self.options.offset) - decoder = p25_decoder.p25_decoder_sink_b(debug = self.options.verbosity, do_imbe = self.options.vocoder, num_ambe=num_ambe) - logfile_workers.append({'demod': demod, 'decoder': decoder, 'active': False}) - self.connect(source, demod, decoder) - - self.trunk_rx = trunking.rx_ctl(frequency_set = self.change_freq, debug = self.options.verbosity, conf_file = self.options.trunk_conf_file, logfile_workers=logfile_workers) - - self.du_watcher = du_queue_watcher(self.rx_q, self.trunk_rx.process_qmsg) - - # Connect up the flow graph - # - def __connect(self, cnxns): - for l in cnxns: - for b in l: - if b == l[0]: - p = l[0] - else: - self.connect(p, b) - p = b - self.cnxns.extend(cnxns) - - # Disconnect the flow graph - # - def __disconnect(self): - for l in self.cnxns: - for b in l: - if b == l[0]: - p = l[0] - else: - self.disconnect(p, b) - p = b - self.cnxns = [] - - def msg_data(self, evt): - params = evt.data - freq = params['freq'] - self.myform['freq'].set_value('%s' % (freq / 1000000.0)) - talkgroup = params['tgid'] - tag = params['tag'] - if not talkgroup: - talkgroup = 0 - if not tag: - tag = '' - tg = '%s (%d)' % (tag, talkgroup) - if talkgroup == 0 and tag == '': - tg = '' - self.myform['talkgroup'].set_value(tg) - nac = params['nac'] - system = params['system'] - self.myform['system'].set_value('%X:%s' % (nac, system)) - if 'tdma' in params and params['tdma'] is not None: - self.myform['tdma'].set_value('TDMA Slot %d' % (params['tdma'])) - else: - self.myform['tdma'].set_value('') - - def set_speed(self, new_speed): - # assumes that lock is held, or that we are in init - self.disconnect_demods() - self.current_speed = new_speed - self.connect_fsk4_demod() - - def set_connection(self, - fscope=False, - fft=False, - corr=False, - fac=False, - c4fm=False): - # assumes that lock is held, or that we are in init - if fac != self.fac_state: - self.fac_state = fac - if fac: - self.demod.connect_complex('mixer', self.fac_scope) - else: - self.demod.disconnect_complex() - if corr != self.corr_state: - self.corr_state = corr - if corr: - if self.corr_i_chan: - self.connect(self.arb_resampler, self.to_real, self.real_amp, self.correlation_scope) - else: - self.demod.connect_bb('symbol_filter', self.correlation_scope) - else: - if self.corr_i_chan: - self.disconnect(self.arb_resampler, self.to_real, self.real_amp, self.correlation_scope) - else: - self.demod.disconnect_bb() - - if fscope != self.fscope_state: - self.fscope_state = fscope - if fscope == 0: - self.demod.disconnect_float() - else: - self.demod.connect_float(self.float_scope) - - if fft != self.fft_state: - self.fft_state = fft - if fft == 0: - self.demod.disconnect_complex() - self.disconnect(self.spectrum_decim, self.spectrum) - else: - self.connect(self.spectrum_decim, self.spectrum) - self.demod.connect_complex('mixer', self.spectrum_decim) - - if c4fm != self.c4fm_state: - self.c4fm_state = c4fm - if c4fm == 0: - self.demod.disconnect_bb() - else: - self.demod.connect_bb('symbol_filter', self.signal_scope) - - def notebook_changed(self, evt): - sel = self.notebook.GetSelection() - self.lock() - self.disconnect_data_scope() - if not self.baseband_input: - self.disconnect_constellation_scope() - if sel == 0: # spectrum - if not self.baseband_input: - self.set_connection(fft=1) - self.connect_demods() - elif sel == 1: # c4fm - self.set_connection(c4fm=1) - self.connect_fsk4_demod() - elif sel == 2: # datascope - self.set_connection() - self.connect_fsk4_demod() - self.connect_data_scope() - elif sel == 3: # constellation (complex) - if not self.baseband_input: - self.set_connection() - self.connect_psk_demod() - self.connect_constellation_scope() - elif sel == 4: # demodulated symbols - self.connect_demods() - self.set_connection(fscope=1) - elif sel == 5: # traffic pane - self.connect_demods() - self.set_connection(fscope=1) - self.update_traffic(None) - elif sel == 6: # correlation - self.disconnect_demods() - self.current_speed = self.default_speed_idx # reset speed for corr - self.data_scope.win.radio_box_speed.SetSelection(self.current_speed) - self.connect_fsk4_demod() - self.set_connection(corr=1) - elif sel == 7: # fac - fast auto correlation - if not self.baseband_input: - self.set_connection(fac=1) - self.connect_demods() - self.unlock() - - # initialize the UI - # - def __init_gui(self, frame, panel, vbox): - - def _form_set_freq(kv): - return self.set_freq(kv['freq']) - - self.frame = frame - if int(wx.__version__.split('.')[0]) < 3: - self.frame.CreateStatusBar() - self.panel = panel - self.vbox = vbox - - # setup the menu bar - menubar = self.frame.GetMenuBar() - - # setup the "File" menu - file_menu = menubar.GetMenu(0) - self.file_new = file_menu.Insert(0, wx.ID_NEW) - self.frame.Bind(wx.EVT_MENU, self._on_file_new, self.file_new) - self.file_open = file_menu.Insert(1, wx.ID_OPEN) - self.frame.Bind(wx.EVT_MENU, self._on_file_open, self.file_open) - file_menu.InsertSeparator(2) - self.file_properties = file_menu.Insert(3, wx.ID_PROPERTIES) - self.frame.Bind(wx.EVT_MENU, self._on_file_properties, self.file_properties) - file_menu.InsertSeparator(4) - self.file_close = file_menu.Insert(5, wx.ID_CLOSE) - self.frame.Bind(wx.EVT_MENU, self._on_file_close, self.file_close) - - # setup the "Edit" menu - edit_menu = wx.Menu() - self.edit_undo = edit_menu.Insert(0, wx.ID_UNDO) - self.frame.Bind(wx.EVT_MENU, self._on_edit_undo, self.edit_undo) - self.edit_redo = edit_menu.Insert(1, wx.ID_REDO) - self.frame.Bind(wx.EVT_MENU, self._on_edit_redo, self.edit_redo) - edit_menu.InsertSeparator(2) - self.edit_cut = edit_menu.Insert(3, wx.ID_CUT) - self.frame.Bind(wx.EVT_MENU, self._on_edit_cut, self.edit_cut) - self.edit_copy = edit_menu.Insert(4, wx.ID_COPY) - self.frame.Bind(wx.EVT_MENU, self._on_edit_copy, self.edit_copy) - self.edit_paste = edit_menu.Insert(5, wx.ID_PASTE) - self.frame.Bind(wx.EVT_MENU, self._on_edit_paste, self.edit_paste) - self.edit_delete = edit_menu.Insert(6, wx.ID_DELETE) - self.frame.Bind(wx.EVT_MENU, self._on_edit_delete, self.edit_delete) - edit_menu.InsertSeparator(7) - self.edit_select_all = edit_menu.Insert(8, wx.ID_SELECTALL) - self.frame.Bind(wx.EVT_MENU, self._on_edit_select_all, self.edit_select_all) - edit_menu.InsertSeparator(9) - self.edit_prefs = edit_menu.Insert(10, wx.ID_PREFERENCES) - self.frame.Bind(wx.EVT_MENU, self._on_edit_prefs, self.edit_prefs) - menubar.Append(edit_menu, "&Edit"); # ToDo use wx.ID_EDIT stuff - - # setup the toolbar - if True: - self.toolbar = wx.ToolBar(frame, -1, style = wx.TB_DOCKABLE | wx.TB_HORIZONTAL) - frame.SetToolBar(self.toolbar) - icon_size = wx.Size(24, 24) -# new_icon = wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR, icon_size) -# toolbar_new = self.toolbar.AddSimpleTool(wx.ID_NEW, new_icon, "New Capture") -# open_icon = wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, icon_size) -# toolbar_open = self.toolbar.AddSimpleTool(wx.ID_OPEN, open_icon, "Open") - - self.toolbar.Realize() - else: - self.toolbar = None - - # setup the notebook - self.notebook = wx.Notebook(self.panel) - self.vbox.Add(self.notebook, 1, wx.EXPAND) - # add spectrum scope - #self.spectrum = fftsink2.fft_sink_c(self.notebook, sample_rate = self.channel_rate, fft_size=512, fft_rate=2, average=False, peak_hold=False) - spectrum_rate = self.channel_rate / self.options.decim_amt - self.spectrum_decim = filter.rational_resampler_ccf(1, self.options.decim_amt) - self.spectrum = fftsink2.fft_sink_c(self.notebook, sample_rate = spectrum_rate, fft_size=1024, fft_rate=10, avg_alpha=0.35, ref_level=0, average=True, peak_hold=False) - try: - self.spectrum_plotter = self.spectrum.win.plotter - except: - self.spectrum_plotter = self.spectrum.win.plot - #self.spectrum_plotter.enable_point_label(False) - self.spectrum_plotter.Bind(wx.EVT_LEFT_DOWN, self._on_spectrum_left_click) - self.notebook.AddPage(self.spectrum.win, "Spectrum") - # add C4FM scope - self.signal_scope = scopesink2.scope_sink_f(self.notebook, sample_rate = self.basic_rate, v_scale=5, t_scale=0.001) - try: - self.signal_plotter = self.signal_scope.win.plotter - except: - self.signal_plotter = self.signal_scope.win.graph - self.notebook.AddPage(self.signal_scope.win, "C4FM") - # add datascope - self.data_scope = datascope_sink_f(self.notebook, samples_per_symbol = 10, num_plots = 100) - self.data_plotter = self.data_scope.win.graph - wx.EVT_RADIOBOX(self.data_scope.win.radio_box, 11103, self.filter_select) - wx.EVT_RADIOBOX(self.data_scope.win.radio_box_speed, 11104, self.speed_select) - self.data_scope.win.radio_box_speed.SetSelection(self.current_speed) - self.notebook.AddPage(self.data_scope.win, "Datascope") - # add complex scope - self.complex_scope = constellation_plot_c(self.notebook, title="Constellation", num_plots=250) - self.notebook.AddPage(self.complex_scope.win, "Constellation") - wx.EVT_RADIOBOX(self.complex_scope.win.radio_box_source, 11108, self.source_select) - # add float scope - self.float_scope = scopesink2.scope_sink_f(self.notebook, frame_decim=1, sample_rate=self.symbol_rate, v_scale=1, t_scale=0.05) - try: #gl - self.float_plotter = self.float_scope.win.plotter - self.float_scope.win['marker_1'] = 3.0 # set type = large dots - except: #nongl - self.float_plotter = self.float_scope.win.graph - self.float_scope.win.set_format_plus() - self.notebook.AddPage(self.float_scope.win, "Symbols") - # Traffic snapshot - self.traffic = TrafficPane(self.notebook, trunk_traffic=True) - self.notebook.AddPage(self.traffic, "Traffic") - wx.EVT_BUTTON (self.traffic, 11109, self.update_traffic) - # add corr scope - self.correlation_scope = correlation_plot_f(self.notebook, frame_decim=4, sps=10, v_scale=1, t_scale=0.05) - # self.correlation_plotter = self.correlation_scope.win.plotter - wx.EVT_RADIOBOX(self.correlation_scope.win.radio_box_corr, 11105, self.corr_select) - self.notebook.AddPage(self.correlation_scope.win, "Correlation") - # add fac scope - self.fac_scope = fac_sink_c(self.notebook, fac_size=32768, sample_rate=self.channel_rate, title="Auto Correlation") - self.notebook.AddPage(self.fac_scope.win, "FAC") - # Setup the decoder and report the TUN/TAP device name - msgq = gr.msg_queue(2) - # self.decode_watcher = decode_watcher(msgq, self.traffic) - # self.p25_decoder = op25.decoder_ff(msgq) - # self.frame.SetStatusText("TUN/TAP: " + self.p25_decoder.device_name()) - - self.notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.notebook_changed) - - self.myform = myform = form.form() - hbox = wx.BoxSizer(wx.HORIZONTAL) - - vbox_form = wx.BoxSizer(wx.VERTICAL) - myform['system'] = form.static_text_field( - parent=self.panel, sizer=vbox_form, label="System", weight=0) - myform['system'].set_value("........................................") - myform['talkgroup'] = form.static_text_field( - parent=self.panel, sizer=vbox_form, label="Talkgroup", weight=0) - myform['talkgroup'].set_value("........................................") - - if self.baseband_input: - min_gain = 0 - max_gain = 200 - initial_val = 50 - else: - if self.rtl_found: - min_gain = 0 - max_gain = 49 - initial_val = self.src.get_gain('LNA') - else: - min_gain = 0 - max_gain = 25 - initial_val = 10 - if self.options.trunk_conf_file: - myform['freq'] = form.static_text_field( - parent=self.panel, sizer=vbox_form, label="Frequency", weight=0) - myform['freq'].set_value('%s' % self.options.frequency) - else: - myform['freq'] = form.float_field( - parent=self.panel, sizer=vbox_form, label="Frequency", weight=0, - callback=myform.check_input_and_call(_form_set_freq, self._set_status_msg)) - myform['freq'].set_value(self.options.frequency) - myform['tdma'] = form.static_text_field( - parent=self.panel, sizer=vbox_form, label=None, weight=0) - myform['tdma'].set_value("") - - hbox.Add(vbox_form, 0, 0) - - vbox_buttons = wx.BoxSizer(wx.VERTICAL) - skip_button = form.button_with_callback( - parent=self.panel, label="Skip", - callback=self.form_skip) - vbox_buttons.Add(skip_button, 0, 0) - lockout_button = form.button_with_callback( - parent=self.panel, label="Lockout", - callback=self.form_lockout) - vbox_buttons.Add(lockout_button, 0, 0) - myform['hold'] = form.checkbox_field( - parent=self.panel, sizer=vbox_buttons, label="Hold", weight=0, - callback=myform.check_input_and_call(self.form_hold)) - hbox.Add(vbox_buttons, 0, 0) - - vbox_sliders = wx.BoxSizer(wx.VERTICAL) - myform['signal_gain'] = form.slider_field(parent=self.panel, sizer=vbox_sliders, label="Signal Gain", - weight=0, - min=min_gain, max=max_gain, - callback=self.set_gain) - self.myform['signal_gain'].set_value(initial_val) - if not self.baseband_input: - myform['freq_tune'] = form.slider_field(parent=self.panel, sizer=vbox_sliders, label="Fine Tune", - weight=0, - min=-3000, max=3000, - callback=self.set_freq_tune) - if not self.options.trunk_conf_file: - myform['demod_type'] = form.radiobox_field(parent=self.panel, sizer=hbox, label="Demod", - weight=0, choices=['FSK4','PSK'], specify_rows=True, - callback=self.demod_type_chg) - - hbox.Add(vbox_sliders, 0, 0) - - vbox_sliders = wx.BoxSizer(wx.VERTICAL) - if self.options.vocoder or self.options.phase2_tdma: - myform['volume'] = form.slider_field(parent=self.panel, sizer=vbox_sliders, label="Volume", - weight=0, min=0, max=20, value=15, - callback=self.set_audio_scaler) - if self.rtl_found: - myform['ppm'] = form.slider_field(parent=self.panel, sizer=vbox_sliders, label="PPM", - weight=0, min=0, max=120, value=self.options.freq_corr, - callback=self.set_rtl_ppm) - if (self.options.vocoder or self.options.phase2_tdma) or self.rtl_found: - hbox.Add(vbox_sliders, 0, 0) - - vbox.Add(hbox, 0, 0) - - def configure_tdma(self, params): - if params['tdma'] is not None and not self.options.phase2_tdma: - print '***TDMA request for frequency %d failed- phase2_tdma option not enabled' % params['freq'] - return - set_tdma = False - if params['tdma'] is not None: - set_tdma = True - if set_tdma == self.tdma_state: - return # already in desired state - self.tdma_state = set_tdma - if set_tdma: - self.decoder.set_slotid(params['tdma']) - hash = '%x%x%x' % (params['nac'], params['sysid'], params['wacn']) - if hash not in self.xor_cache: - self.xor_cache[hash] = lfsr.p25p2_lfsr(params['nac'], params['sysid'], params['wacn']).xor_chars - self.decoder.set_xormask(self.xor_cache[hash], hash) - sps = self.basic_rate / 6000 - else: - sps = self.basic_rate / 4800 - self.demod.clock.set_omega(float(sps)) - - def change_freq(self, params): - freq = params['freq'] - offset = params['offset'] - center_freq = params['center_frequency'] - - if self.options.hamlib_model: - self.hamlib.set_freq(freq) - elif params['center_frequency']: - relative_freq = center_freq - freq - if abs(relative_freq + self.options.offset) > self.channel_rate / 2: - print '***unable to tune Local Oscillator to offset %d Hz' % (relative_freq + self.options.offset) - print '***limit is one half of sample-rate %d = %d' % (self.channel_rate, self.channel_rate / 2) - print '***request for frequency %d rejected' % freq - - self.lo_freq = self.options.offset + relative_freq - self.demod.set_relative_frequency(self.lo_freq + self.myform['freq_tune'].get_value()) - self.set_freq(center_freq + offset) - #self.spectrum.set_baseband_freq(center_freq) - else: - self.set_freq(freq + offset) - - self.configure_tdma(params) - - # send msg as event to avoid thread safety problems updating form - evt = msg_DataEvent(params) - wx.PostEvent(self.frame, evt) - - def hamlib_attach(self, model): - Hamlib.rig_set_debug (Hamlib.RIG_DEBUG_NONE) # RIG_DEBUG_TRACE - - self.hamlib = Hamlib.Rig (model) - self.hamlib.set_conf ("serial_speed","9600") - self.hamlib.set_conf ("retry","5") - - self.hamlib.open () - - def q_action(self, action): - msg = gr.message().make_from_string(action, -2, 0, 0) - self.rx_q.insert_tail(msg) - - def form_hold(self, kv): - if kv['hold']: - cmd = 'set_hold' - else: - cmd = 'unset_hold' - self.q_action(cmd) - - def form_skip(self): - self.q_action('skip') - - def form_lockout(self): - self.q_action('lockout') - - def update_traffic(self, evt): - s = self.trunk_rx.to_string() - t = {} - t['string'] = s - self.traffic.update(t) - - def set_gain(self, gain): - if self.rtl_found: - self.src.set_gain(gain, 'LNA') - if self.options.verbosity: - print 'RTL Gain of %d set to: %.1f' % (gain, self.src.get_gain('LNA')) - else: - if self.baseband_input: - f = 1.0 - else: - f = 0.1 - self.demod.set_baseband_gain(float(gain) * f) - - def set_audio_scaler(self, vol): - #print 'audio scaler: %f' % ((1 / 32768.0) * (vol * 0.1)) - self.decoder.set_scaler_k((1 / 32768.0) * (vol * 0.1)) - - def set_rtl_ppm(self, ppm): - self.src.set_freq_corr(ppm) - - def set_freq_tune(self, val): - self.myform['freq_tune'].set_value(val) - self.demod.set_relative_frequency(val + self.lo_freq) - - def set_freq(self, target_freq): - """ - Set the center frequency we're interested in. - - @param target_freq: frequency in Hz - @rypte: bool - - Tuning is a two step process. First we ask the front-end to - tune as close to the desired frequency as it can. Then we use - the result of that operation and our target_frequency to - determine the value for the digital down converter. - """ - if not self.src: - return False - tune_freq = target_freq + self.options.calibration + self.options.offset - r = self.src.set_center_freq(tune_freq) - - if r: - #self.myform['freq'].set_value(target_freq) # update displayed va - #if self.show_debug_info: - # self.myform['baseband'].set_value(r.baseband_freq) - # self.myform['ddc'].set_value(r.dxc_freq) - return True - - return False - - def demod_type_chg(self, val): - if val == 'FSK4': - new_demod_mode = True - else: - new_demod_mode = False - if self.fsk4_demod_mode == new_demod_mode: - return - self.fsk4_demod_mode = new_demod_mode - self.lock() - self.disconnect_demods() - notebook_sel = self.notebook.GetSelection() - if notebook_sel == 0 or notebook_sel == 4: # spectrum or demod symbols - self.connect_demods() - elif notebook_sel == 1 or notebook_sel == 2 or notebook_sel == 6: - self.connect_fsk4_demod() - elif notebook_sel == 3: # constellation - self.connect_psk_demod() - - self.unlock() - - def _set_status_msg(self, msg): - self.frame.GetStatusBar().SetStatusText(msg, 0) - - # read capture file properties (decimation etc.) - # - def __read_file_properties(self, filename): - f = open(filename, "r") - self.info = pickle.load(f) - ToDo = True - f.close() - - # setup to rx from file - # - def __set_rx_from_file(self, filename, capture_rate): - file = blocks.file_source(gr.sizeof_gr_complex, filename, True) - gain = blocks.multiply_const_cc(self.options.gain) - throttle = blocks.throttle(gr.sizeof_gr_complex, capture_rate) - self.__connect([[file, gain, throttle]]) - self.__build_graph(throttle, capture_rate) - - # setup to rx from Audio - # - def __set_rx_from_audio(self, capture_rate): - self.__build_graph(self.source, capture_rate) - - # setup to rx from USRP - # - def __set_rx_from_osmosdr(self): - # setup osmosdr - capture_rate = self.src.set_sample_rate(self.options.sample_rate) - if self.options.antenna: - self.src.set_antenna(self.options.antenna) - self.info["capture-rate"] = capture_rate - self.src.set_bandwidth(capture_rate) - r = self.src.set_center_freq(self.options.frequency + self.options.calibration+ self.options.offset) - print 'set_center_freq: %d' % r - if not r: - raise RuntimeError("failed to set USRP frequency") - # capture file - # if preserve: - if 0: - try: - self.capture_filename = os.tmpnam() - except RuntimeWarning: - ignore = True - capture_file = blocks.file_sink(gr.sizeof_gr_complex, self.capture_filename) - self.__connect([[self.usrp, capture_file]]) - else: - self.capture_filename = None - # everything else - self.__build_graph(self.src, capture_rate) - - # Change the UI state - # - def _set_state(self, new_state): - self.state = new_state - if "STOPPED" == self.state: - # menu items - can_capture = False # self.usrp is not None - self.file_new.Enable(can_capture) - self.file_open.Enable(True) - self.file_properties.Enable(False) - self.file_close.Enable(False) - # toolbar - if self.toolbar: - self.toolbar.EnableTool(wx.ID_NEW, can_capture) - self.toolbar.EnableTool(wx.ID_OPEN, True) - # Visually reflect "no file" - self.frame.SetStatusText("", 1) - self.frame.SetStatusText("", 2) - self.spectrum_plotter.ClearBackground() - self.signal_plotter.ClearBackground() - # self.symbol_plotter.ClearBackground() - # self.traffic.clear() - elif "RUNNING" == self.state: - # menu items - self.file_new.Enable(False) - self.file_open.Enable(False) - self.file_properties.Enable(True) - self.file_close.Enable(True) - # toolbar - if self.toolbar: - self.toolbar.EnableTool(wx.ID_NEW, False) - self.toolbar.EnableTool(wx.ID_OPEN, False) - elif "CAPTURING" == self.state: - # menu items - self.file_new.Enable(False) - self.file_open.Enable(False) - self.file_properties.Enable(True) - self.file_close.Enable(True) - # toolbar - if self.toolbar: - self.toolbar.EnableTool(wx.ID_NEW, False) - self.toolbar.EnableTool(wx.ID_OPEN, False) - - - # Append filename to default title bar - # - def _set_titlebar(self, filename): - ToDo = True - - # Write capture file properties - # - def __write_file_properties(self, filename): - f = open(filename, "w") - pickle.dump(self.info, f) - f.close() - - # Adjust the channel offset - # - def adjust_channel_offset(self, delta_hz): - max_delta_hz = 12000.0 - delta_hz *= self.symbol_deviation - delta_hz = max(delta_hz, -max_delta_hz) - delta_hz = min(delta_hz, max_delta_hz) - self.channel_filter.set_center_freq(self.channel_offset - delta_hz+ self.options.offset) - - # Close an open file - # - def _on_file_close(self, event): - self.stop() - self.wait() - self.__disconnect() - if "CAPTURING" == self.state and self.capture_filename: - dialog = wx.MessageDialog(self.frame, "Save capture file before closing?", style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION) - if wx.ID_YES == dialog.ShowModal(): - save_dialog = wx.FileDialog(self.frame, "Save capture file as:", wildcard="*.dat", style=wx.SAVE|wx.OVERWRITE_PROMPT) - if save_dialog.ShowModal() == wx.ID_OK: - path = str(save_dialog.GetPath()) - save_dialog.Destroy() - os.rename(self.capture_filename, path) - self.__write_file_properties(path + ".info") - else: - os.remove(self.capture_filename) - self.capture_filename = None - self._set_state("STOPPED") - - # New capture from USRP - # - def _on_file_new(self, event): -# wizard = wx.wizard.Wizard(self.frame, -1, "New Capture from USRP") -# page1 = wizard_intro_page(wizard) -# page2 = wizard_details_page(wizard) -# page3 = wizard_preserve_page(wizard) -# page4 = wizard_finish_page(wizard) -# wx.wizard.WizardPageSimple_Chain(page1, page2) -# wx.wizard.WizardPageSimple_Chain(page2, page3) -# wx.wizard.WizardPageSimple_Chain(page3, page4) -# wizard.FitToPage(page1) -# if wizard.RunWizard(page1): - self.stop() - self.wait() - # ToDo: get open_usrp() arguments from wizard - self.open_usrp((0,0), 200, None, 434.08e06, True) # Test freq - self.start() - - # Open an existing capture - # - def _on_file_open(self, event): - dialog = wx.FileDialog(self.frame, "Choose a capture file:", wildcard="*.dat", style=wx.OPEN) - if dialog.ShowModal() == wx.ID_OK: - file = str(dialog.GetPath()) - dialog.Destroy() - self.stop() - self.wait() - self.open_file(file) - self.start() - - # Present file properties dialog - # - def _on_file_properties(self, event): - # ToDo: show what info we have about the capture file (name, - # capture source, capture rate, date(?), size(?),) - todo = True - - # Undo the last edit - # - def _on_edit_undo(self, event): - todo = True - - # Redo the edit - # - def _on_edit_redo(self, event): - todo = True - - # Cut the current selection - # - def _on_edit_cut(self, event): - todo = True - - # Copy the current selection - # - def _on_edit_copy(self, event): - todo = True - - # Paste into the current sample - # - def _on_edit_paste(self, event): - todo = True - - # Delete the current selection - # - def _on_edit_delete(self, event): - todo = True - - # Select all - # - def _on_edit_select_all(self, event): - todo = True - - # Open the preferences dialog - # - def _on_edit_prefs(self, event): - todo = True - - - # Set channel offset and RF squelch threshold - # - def _on_spectrum_left_click(self, event): - if "STOPPED" != self.state: - # set frequency - x,y = self.spectrum_plotter.GetXY(event) - xmin, xmax = self.spectrum_plotter.GetXCurrentRange() - x = min(x, xmax) - x = max(x, xmin) - scale_factor = self.spectrum.win._scale_factor - chan_width = 6.25e3 - x /= scale_factor - x += chan_width / 2 - x = (x // chan_width) * chan_width - self.set_channel_offset(x, scale_factor, self.spectrum.win._units) - # set squelch threshold - ymin, ymax = self.spectrum_plotter.GetYCurrentRange() - y = min(y, ymax) - y = max(y, ymin) - squelch_increment = 5 - y += squelch_increment / 2 - y = (y // squelch_increment) * squelch_increment - self.set_squelch_threshold(int(y)) - - # Open an existing capture file - # - def open_file(self, capture_file): - try: - capture_rate = self.options.sample_rate - self.__set_rx_from_file(capture_file, capture_rate) - self._set_titlebar(capture_file) - self._set_state("RUNNING") - except Exception, x: - wx.MessageBox("Cannot open capture file: " + x.message, "File Error", wx.CANCEL | wx.ICON_EXCLAMATION) - - def open_ifile(self, capture_rate, gain, input_filename, file_seek): - speed = 96000 # TODO: fixme - ifile = blocks.file_source(gr.sizeof_gr_complex, input_filename, 1) - if file_seek > 0: - rc = ifile.seek(file_seek*1024, gr.SEEK_SET) - assert rc == True - #print "seek: %d, rc = %d" % (file_seek, rc) - throttle = blocks.throttle(gr.sizeof_gr_complex, speed) - self.source = blocks.multiply_const_cc(gain) - self.connect(ifile, throttle, self.source) - self.__set_rx_from_audio(speed) - self._set_titlebar("Playing") - self._set_state("PLAYING") - - def open_audio_c(self, capture_rate, gain, audio_input_filename): - self.info = { - "capture-rate": capture_rate, - "center-freq": 0, - "source-dev": "AUDIO", - "source-decim": 1 } - self.audio_source = audio.source(capture_rate, audio_input_filename) - self.audio_cvt = blocks.float_to_complex() - self.connect((self.audio_source, 0), (self.audio_cvt, 0)) - self.connect((self.audio_source, 1), (self.audio_cvt, 1)) - self.source = blocks.multiply_const_cc(gain) - self.connect(self.audio_cvt, self.source) - self.__set_rx_from_audio(capture_rate) - self._set_titlebar("Capturing") - self._set_state("CAPTURING") - - def open_audio(self, capture_rate, gain, audio_input_filename): - self.info = { - "capture-rate": capture_rate, - "center-freq": 0, - "source-dev": "AUDIO", - "source-decim": 1 } - self.audio_source = audio.source(capture_rate, audio_input_filename) - self.source = blocks.multiply_const_ff(gain) - self.connect(self.audio_source, self.source) - self.__set_rx_from_audio(capture_rate) - self._set_titlebar("Capturing") - self._set_state("CAPTURING") - - # Open the USRP - # - def open_usrp(self): - # try: - self.info = { - "capture-rate": "unknown", - "center-freq": self.options.frequency, - "source-dev": "USRP", - "source-decim": 1 } - self.__set_rx_from_osmosdr() - self._set_titlebar("Capturing") - self._set_state("CAPTURING") - # except Exception, x: - # wx.MessageBox("Cannot open USRP: " + x.message, "USRP Error", wx.CANCEL | wx.ICON_EXCLAMATION) - - # Set the channel offset - # - def set_channel_offset(self, offset_hz, scale, units): - self.channel_offset = -offset_hz - self.channel_filter.set_center_freq(self.channel_offset+ self.options.offset) - self.frame.SetStatusText("Channel offset: " + str(offset_hz * scale) + units, 1) - - # Set the RF squelch threshold level - # - def set_squelch_threshold(self, squelch_db): - self.squelch.set_threshold(squelch_db) - self.frame.SetStatusText("Squelch: " + str(squelch_db) + "dB", 2) - - def disconnect_demods(self): -# assumes lock held or init - if self.baseband_input: - return - self.demod.disconnect_chain() - - def connect_psk_demod(self): -# assumes lock held or init - if self.baseband_input: - return - self.demod.connect_chain('cqpsk') - - def connect_fsk4_demod(self): -# assumes lock held or init - if self.baseband_input: - return - self.demod.connect_chain('fsk4') - - def connect_demods(self): - if self.baseband_input: - self.connect_fsk4_demod() - else: - if self.fsk4_demod_mode: - self.connect_fsk4_demod() - else: - self.connect_psk_demod() - - def disconnect_constellation_scope(self): - self.demod.disconnect_complex() - - def connect_constellation_scope(self): - sel = self.complex_scope.win.radio_box_source.GetSelection() - if sel: - self.demod.connect_complex('diffdec', self.complex_scope) - else: - self.demod.connect_complex('clock', self.complex_scope) - - def disconnect_data_scope(self): - self.demod.disconnect_bb() - - def connect_data_scope(self): - sel = self.data_scope.win.radio_box.GetSelection() - if sel: - self.demod.connect_bb('symbol_filter', self.data_scope) - else: - self.demod.connect_bb('baseband_amp', self.data_scope) - - # for datascope, choose monitor viewpoint - def filter_select(self, evt): - self.lock() - self.connect_data_scope() - self.unlock() - - def corr_select(self, evt): - new_corr = self.correlation_scope.win.radio_box_corr.GetSelection() - self.correlation_scope.win.correlation = self.correlation_scope.win.signatures[new_corr] - if self.baseband_input: - return - self.lock() - self.set_connection() - if new_corr == len(self.correlation_scope.win.signatures) - 1: - # special iden mode - self.corr_i_chan = True - else: - self.corr_i_chan = False - self.set_connection(corr=True) - self.unlock() - - def source_select(self, evt): - self.lock() - self.connect_constellation_scope() - self.unlock() - - def speed_select(self, evt): - new_speed = self.data_scope.win.radio_box_speed.GetSelection() - self.lock() - self.set_speed(new_speed) - self.unlock() - -class window_with_ctlbox(wx.Panel): - def __init__(self, parent, id = -1): - wx.Panel.__init__(self, parent, id) - - def make_control_box (self): - global speeds - ctrlbox = wx.BoxSizer (wx.HORIZONTAL) - - ctrlbox.Add ((5,0) ,0) - - run_stop = wx.Button (self, 11102, "Run/Stop") - run_stop.SetToolTipString ("Toggle Run/Stop mode") - wx.EVT_BUTTON (self, 11102, self.run_stop) - ctrlbox.Add (run_stop, 0, wx.EXPAND) - - self.radio_box = wx.RadioBox(self, 11103, "Viewpoint", style=wx.RA_SPECIFY_ROWS, - choices = ["Raw", "Filtered"] ) - self.radio_box.SetToolTipString("Viewpoint Before Or After Symbol Filter") - self.radio_box.SetSelection(1) - ctrlbox.Add (self.radio_box, 0, wx.EXPAND) - - ctrlbox.Add ((5, 0) ,0) # stretchy space - - speed_str = [] - for speed in speeds: - speed_str.append("%d" % speed) - - self.radio_box_speed = wx.RadioBox(self, 11104, "Symbol Rate", style=wx.RA_SPECIFY_ROWS, majorDimension=2, choices = speed_str) - self.radio_box_speed.SetToolTipString("Symbol Rate") - ctrlbox.Add (self.radio_box_speed, 0, wx.EXPAND) - ctrlbox.Add ((10, 0) ,1) # stretchy space - - return ctrlbox - -# A snapshot of important fields in current traffic -# -class TrafficPane(wx.Panel): - - # Initializer - # - def __init__(self, parent, trunk_traffic=False, voice_traffic=False, update_callback=None): - - wx.Panel.__init__(self, parent) - sizer = wx.GridBagSizer(hgap=10, vgap=10) - self.fields = {} - - if trunk_traffic: - #label = wx.StaticText(self, -1, "DUID:") - #sizer.Add(label, pos=(1,1)) - self.update_b = wx.Button (self, 11109, "Update") - sizer.Add(self.update_b, pos=(1,1)) - field = wx.TextCtrl(self, -1, "", size=(500,400), style=wx.TE_MULTILINE+wx.TE_READONLY) - sizer.Add(field, pos=(1,2)) - self.fields["string"] = field; - - if voice_traffic: - label = wx.StaticText(self, -1, "DUID:") - sizer.Add(label, pos=(1,1)) - field = wx.TextCtrl(self, -1, "", size=(72, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(1,2)) - self.fields["duid"] = field; - - label = wx.StaticText(self, -1, "NAC:") - sizer.Add(label, pos=(2,1)) - field = wx.TextCtrl(self, -1, "", size=(175, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(2,2)) - self.fields["nac"] = field; - - label = wx.StaticText(self, -1, "Source:") - sizer.Add(label, pos=(3,1)) - field = wx.TextCtrl(self, -1, "", size=(175, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(3,2)) - self.fields["source"] = field; - - label = wx.StaticText(self, -1, "Destination:") - sizer.Add(label, pos=(4,1)) - field = wx.TextCtrl(self, -1, "", size=(175, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(4,2)) - self.fields["dest"] = field; - -# label = wx.StaticText(self, -1, "ToDo:") -# sizer.Add(label, pos=(5,1)) -# field = wx.TextCtrl(self, -1, "", size=(175, -1), style=wx.TE_READONLY) -# sizer.Add(field, pos=(5,2)) -# self.fields["nid"] = field; - - label = wx.StaticText(self, -1, "MFID:") - sizer.Add(label, pos=(1,4)) - field = wx.TextCtrl(self, -1, "", size=(175, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(1,5)) - self.fields["mfid"] = field; - - label = wx.StaticText(self, -1, "ALGID:") - sizer.Add(label, pos=(2,4)) - field = wx.TextCtrl(self, -1, "", size=(175, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(2,5)) - self.fields["algid"] = field; - - label = wx.StaticText(self, -1, "KID:") - sizer.Add(label, pos=(3,4)) - field = wx.TextCtrl(self, -1, "", size=(72, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(3,5)) - self.fields["kid"] = field; - - label = wx.StaticText(self, -1, "MI:") - sizer.Add(label, pos=(4,4)) - field = wx.TextCtrl(self, -1, "", size=(216, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(4,5)) - self.fields["mi"] = field; - - label = wx.StaticText(self, -1, "TGID:") - sizer.Add(label, pos=(5,4)) - field = wx.TextCtrl(self, -1, "", size=(72, -1), style=wx.TE_READONLY) - sizer.Add(field, pos=(5,5)) - self.fields["tgid"] = field; - - self.SetSizer(sizer) - self.Fit() - - # Clear the field values - # - def clear(self): - for v in self.fields.values(): - v.Clear() - - # Update the field values - # - def update(self, field_values): - for k,v in self.fields.items(): - f = field_values.get(k, None) - if f: - v.SetValue(f) - else: - v.SetValue("") - - -# Introduction page for USRP capture wizard -# -class wizard_intro_page(wx.wizard.WizardPageSimple): - - # Initializer - # - def __init__(self, parent): - wx.wizard.WizardPageSimple.__init__(self, parent) - html = wx.html.HtmlWindow(self) - html.SetPage(''' - - -

Capture from USRP

-

- We will guide you through the process of capturing a sample from the USRP. - Please ensure that the USRP is switched on and connected to this computer. -

- - - ''') - sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(sizer) - sizer.Add(html, 1, wx.ALIGN_CENTER | wx.EXPAND | wx.FIXED_MINSIZE) - - -# USRP wizard details page -# -class wizard_details_page(wx.wizard.WizardPageSimple): - - # Initializer - # - def __init__(self, parent): - wx.wizard.WizardPageSimple.__init__(self, parent) - sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(sizer) - - # Return a tuple containing the subdev_spec, gain, frequency, decimation factor - # - def get_details(self): - ToDo = True - - -# data unit receive queue -# -class du_queue_watcher(threading.Thread): - - def __init__(self, msgq, callback, **kwds): - threading.Thread.__init__ (self, **kwds) - self.setDaemon(1) - self.msgq = msgq - self.callback = callback - self.keep_running = True - self.start() - - def run(self): - while(self.keep_running): - msg = self.msgq.delete_head() - self.callback(msg) - -# Frequency tracker -# -class demod_watcher(threading.Thread): - - def __init__(self, msgq, callback, **kwds): - threading.Thread.__init__ (self, **kwds) - self.setDaemon(1) - self.msgq = msgq - self.callback = callback - self.keep_running = True - self.start() - - def run(self): - while(self.keep_running): - msg = self.msgq.delete_head() - frequency_correction = msg.arg1() - self.callback(frequency_correction) - - -# Decoder watcher -# -class decode_watcher(threading.Thread): - - def __init__(self, msgq, traffic_pane, **kwds): - threading.Thread.__init__ (self, **kwds) - self.setDaemon(1) - self.msgq = msgq - self.traffic_pane = traffic_pane - self.keep_running = True - self.start() - - def run(self): - while(self.keep_running): - msg = self.msgq.delete_head() - pickled_dict = msg.to_string() - attrs = pickle.loads(pickled_dict) - self.traffic_pane.update(attrs) - -############################################################################ -# following code modified from GNURadio sources - -default_scopesink_size = (640, 240) -default_v_scale = 1000 -default_frame_decim = gr.prefs().get_long('wxgui', 'frame_decim', 1) - -class datascope_sink_f(gr.hier_block2): - def __init__(self, parent, title='', sample_rate=1, - size=default_scopesink_size, frame_decim=default_frame_decim, - samples_per_symbol=10, num_plots=100, - v_scale=default_v_scale, t_scale=None, num_inputs=1, **kwargs): - - gr.hier_block2.__init__(self, "datascope_sink_f", - gr.io_signature(num_inputs, num_inputs, gr.sizeof_float), - gr.io_signature(0,0,0)) - - msgq = gr.msg_queue(2) # message queue that holds at most 2 messages - self.st = blocks.message_sink(gr.sizeof_float, msgq, True) - self.connect((self, 0), self.st) - - self.win = datascope_window(datascope_win_info (msgq, sample_rate, frame_decim, - v_scale, t_scale, None, title), parent, samples_per_symbol=samples_per_symbol, num_plots=num_plots) - - def set_sample_rate(self, sample_rate): - self.guts.set_sample_rate(sample_rate) - self.win.info.set_sample_rate(sample_rate) - -# ======================================================================== - -wxDATA_EVENT = wx.NewEventType() - -def EVT_DATA_EVENT(win, func): - win.Connect(-1, -1, wxDATA_EVENT, func) - -class datascope_DataEvent(wx.PyEvent): - def __init__(self, data): - wx.PyEvent.__init__(self) - self.SetEventType (wxDATA_EVENT) - self.data = data - - def Clone (self): - self.__class__ (self.GetId()) - -class datascope_win_info (object): - __slots__ = ['msgq', 'sample_rate', 'frame_decim', 'v_scale', - 'scopesink', 'title', - 'time_scale_cursor', 'v_scale_cursor', 'marker', 'xy', - 'autorange', 'running'] - - def __init__ (self, msgq, sample_rate, frame_decim, v_scale, t_scale, - scopesink, title = "Oscilloscope", xy=False): - self.msgq = msgq - self.sample_rate = sample_rate - self.frame_decim = frame_decim - self.scopesink = scopesink - self.title = title; - - self.marker = 'line' - self.xy = xy - self.autorange = not v_scale - self.running = True - - def set_sample_rate(self, sample_rate): - self.sample_rate = sample_rate - - def get_sample_rate (self): - return self.sample_rate - - def get_decimation_rate (self): - return 1.0 - - def set_marker (self, s): - self.marker = s - - def get_marker (self): - return self.marker - - -class datascope_input_watcher (threading.Thread): - def __init__ (self, msgq, event_receiver, frame_decim, num_plots, samples_per_symbol, **kwds): - threading.Thread.__init__ (self, **kwds) - self.setDaemon (1) - self.msgq = msgq - self.event_receiver = event_receiver - self.frame_decim = frame_decim - self.samples_per_symbol = samples_per_symbol - self.num_plots = num_plots - self.iscan = 0 - self.keep_running = True - self.skip = 0 - self.totsamp = 0 - self.skip_samples = 0 - self.start () - self.msg_string = "" - - def run (self): - # print "datascope_input_watcher: pid = ", os.getpid () - while (self.keep_running): - msg = self.msgq.delete_head() # blocking read of message queue - nchan = int(msg.arg1()) # number of channels of data in msg - nsamples = int(msg.arg2()) # number of samples in each channel - self.totsamp += nsamples - if self.skip_samples >= nsamples: - self.skip_samples -= nsamples - continue - - self.msg_string += msg.to_string() # body of the msg as a string - - bytes_needed = (self.num_plots*self.samples_per_symbol) * gr.sizeof_float - if (len(self.msg_string) < bytes_needed): - continue - - records = [] - # start = self.skip * gr.sizeof_float - start = 0 - chan_data = self.msg_string[start:start+bytes_needed] - rec = numpy.fromstring (chan_data, numpy.float32) - records.append (rec) - self.msg_string = "" - - unused = nsamples - (self.num_plots*self.samples_per_symbol) - unused -= (start/gr.sizeof_float) - self.skip = self.samples_per_symbol - (unused % self.samples_per_symbol) - # print "reclen = %d totsamp %d appended %d skip %d start %d unused %d" % (nsamples, self.totsamp, len(rec), self.skip, start/gr.sizeof_float, unused) - - de = datascope_DataEvent (records) - wx.PostEvent (self.event_receiver, de) - records = [] - del de - - # lower values = more frequent plots, but higher CPU usage - self.skip_samples = self.num_plots * self.samples_per_symbol * 20 - -class datascope_window (window_with_ctlbox): - - def __init__ (self, info, parent, id = -1, - samples_per_symbol=10, num_plots=100, - pos = wx.DefaultPosition, size = wx.DefaultSize, name = ""): - window_with_ctlbox.__init__ (self, parent, -1) - self.info = info - - vbox = wx.BoxSizer (wx.VERTICAL) - - self.graph = datascope_graph_window (info, self, -1, samples_per_symbol=samples_per_symbol, num_plots=num_plots) - - vbox.Add (self.graph, 1, wx.EXPAND) - vbox.Add (self.make_control_box(), 0, wx.EXPAND) - vbox.Add (self.make_control2_box(), 0, wx.EXPAND) - - self.sizer = vbox - self.SetSizer (self.sizer) - self.SetAutoLayout (True) - self.sizer.Fit (self) - - - # second row of control buttons etc. appears BELOW control_box - def make_control2_box (self): - ctrlbox = wx.BoxSizer (wx.HORIZONTAL) - - ctrlbox.Add ((5,0) ,0) # left margin space - - return ctrlbox - - def run_stop (self, evt): - self.info.running = not self.info.running - -class datascope_graph_window (plot.PlotCanvas): - - def __init__ (self, info, parent, id = -1, - pos = wx.DefaultPosition, size = (140, 140), - samples_per_symbol=10, num_plots=100, - style = wx.DEFAULT_FRAME_STYLE, name = ""): - plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name) - - self.SetXUseScopeTicks (True) - self.SetEnableGrid (False) - self.SetEnableZoom (True) - self.SetEnableLegend(True) - # self.SetBackgroundColour ('black') - - self.info = info; - - self.total_points = 0 - - self.samples_per_symbol = samples_per_symbol - self.num_plots = num_plots - - EVT_DATA_EVENT (self, self.format_data) - - self.input_watcher = datascope_input_watcher (info.msgq, self, info.frame_decim, self.samples_per_symbol, self.num_plots) - - def format_data (self, evt): - if not self.info.running: - return - - info = self.info - records = evt.data - nchannels = len (records) - npoints = len (records[0]) - self.total_points += npoints - - x_vals = numpy.arange (0, self.samples_per_symbol) - - self.SetXUseScopeTicks (True) # use 10 divisions, no labels - - objects = [] - colors = ['red','orange','yellow','green','blue','violet','cyan','magenta','brown','black'] - - r = records[0] # input data - for i in range(self.num_plots): - points = [] - for j in range(self.samples_per_symbol): - p = [ j, r[ i*self.samples_per_symbol + j ] ] - points.append(p) - objects.append (plot.PolyLine (points, colour=colors[i % len(colors)], legend=(''))) - - graphics = plot.PlotGraphics (objects, - title='Data Scope', - xLabel = 'Time', yLabel = 'Amplitude') - - x_range = (0., 0. + (self.samples_per_symbol-1)) # ranges are tuples! - self.y_range = (-4., 4.) # for standard -3/-1/+1/+3 - # self.y_range = (-10., 10.) # for standard -3/-1/+1/+3 - self.Draw (graphics, xAxis=x_range, yAxis=self.y_range) -############################################################################ -class constellation_plot_c(gr.hier_block2): - def __init__(self, parent, title='', sample_rate=1, - frame_decim=10, - num_plots=100, - num_inputs=1, **kwargs): - - gr.hier_block2.__init__(self, "constellation_plot_c", - gr.io_signature(num_inputs, num_inputs, gr.sizeof_gr_complex), - gr.io_signature(0,0,0)) - - msgq = gr.msg_queue(2) # message queue that holds at most 2 messages - self.st = blocks.message_sink(gr.sizeof_gr_complex, msgq, True) - self.connect((self, 0), self.st) - - self.win = constellation_plot_window(constellation_plot_win_info (msgq, sample_rate, frame_decim, None, title), parent, num_plots=num_plots) - - def set_sample_rate(self, sample_rate): - self.guts.set_sample_rate(sample_rate) - self.win.info.set_sample_rate(sample_rate) - -# ======================================================================== - -wxDATA_EVENT = wx.NewEventType() - -def EVT_DATA_EVENT(win, func): - win.Connect(-1, -1, wxDATA_EVENT, func) - -class constellation_plot_DataEvent(wx.PyEvent): - def __init__(self, data): - wx.PyEvent.__init__(self) - self.SetEventType (wxDATA_EVENT) - self.data = data - - def Clone (self): - self.__class__ (self.GetId()) - -class constellation_plot_win_info (object): - __slots__ = ['msgq', 'sample_rate', 'frame_decim', - 'scopesink', 'title', - 'time_scale_cursor', 'marker', 'xy', - 'autorange', 'running'] - - def __init__ (self, msgq, sample_rate, frame_decim, - scopesink, title = "Oscilloscope", xy=True): - self.msgq = msgq - self.sample_rate = sample_rate - self.frame_decim = frame_decim - self.scopesink = scopesink - self.title = title; - - self.marker = 'line' - self.xy = xy - self.autorange = False - self.running = True - - def set_sample_rate(self, sample_rate): - self.sample_rate = sample_rate - - def get_sample_rate (self): - return self.sample_rate - - def get_decimation_rate (self): - return 1.0 - - def set_marker (self, s): - self.marker = s - - def get_marker (self): - return self.marker - - -class constellation_plot_input_watcher (threading.Thread): - def __init__ (self, msgq, event_receiver, frame_decim, num_plots, **kwds): - threading.Thread.__init__ (self, **kwds) - self.setDaemon (1) - self.msgq = msgq - self.event_receiver = event_receiver - self.frame_decim = frame_decim - self.num_plots = num_plots - self.iscan = 0 - self.keep_running = True - self.skip = 0 - self.totsamp = 0 - self.skip_samples = 0 - self.start () - self.msg_string = "" - self.skip_mode = False - - def run (self): - # print "constellation_plot_input_watcher: pid = ", os.getpid () - time.sleep(1) - while (self.keep_running): - bytes_needed = self.num_plots * gr.sizeof_float * 2 - if self.skip_mode: - bytes_needed = 500 * gr.sizeof_float * 2 - - if len(self.msg_string) < bytes_needed: - msg = self.msgq.delete_head() # blocking read of message queue - nchan = int(msg.arg1()) # number of channels of data in msg - nsamples = int(msg.arg2()) # number of samples in each channel - self.totsamp += nsamples - - self.msg_string += msg.to_string() # body of the msg as a string - continue - - chan_data = self.msg_string[:bytes_needed] - self.msg_string = self.msg_string[bytes_needed:] - - if self.skip_mode: - self.skip_mode = False - continue - - records = [] - # start = self.skip * gr.sizeof_gr_complex - # start = 0 - # chan_data = self.msg_string[start:start+bytes_needed] - rec = numpy.fromstring (chan_data, numpy.float32) - records.append (rec) - # self.msg_string = "" - - # unused = nsamples - self.num_plots - # unused -= (start/gr.sizeof_gr_complex) - # print "reclen = %d totsamp %d appended %d skip %d start %d unused %d" % (nsamples, self.totsamp, len(rec), self.skip, start/gr.sizeof_float, unused) - - de = constellation_plot_DataEvent (records) - wx.PostEvent (self.event_receiver, de) - records = [] - del de - - # lower values = more frequent plots, but higher CPU usage - # self.skip_samples = 5000 - self.skip_mode = True - -class constellation_plot_window (wx.Panel): - - constellation_window_size = wx.DefaultSize - def __init__ (self, info, parent, id = -1, - num_plots=100, - pos = wx.DefaultPosition, size = constellation_window_size, name = ""): - wx.Panel.__init__ (self, parent, -1) - self.info = info - - hbox = wx.BoxSizer (wx.HORIZONTAL) - - self.graph = constellation_plot_graph_window (info, self, -1, num_plots=num_plots) - - hbox.Add (self.graph, 1, wx.SHAPED) - hbox.Add (self.make_control_box(), 0, wx.EXPAND) - hbox.Add (self.make_control2_box(), 0, wx.EXPAND) - - self.sizer = hbox - self.SetSizer (self.sizer) - self.SetAutoLayout (True) - self.sizer.Fit (self) - - - # second row of control buttons etc. appears BELOW control_box - def make_control2_box (self): - ctrlbox = wx.BoxSizer (wx.HORIZONTAL) - - ctrlbox.Add ((5,0) ,0) # left margin space - - return ctrlbox - - def make_control_box (self): - ctrlbox = wx.BoxSizer (wx.HORIZONTAL) - - ctrlbox.Add ((5,0) ,0) - - run_stop = wx.Button (self, 11102, "Run/Stop") - run_stop.SetToolTipString ("Toggle Run/Stop mode") - wx.EVT_BUTTON (self, 11102, self.run_stop) - ctrlbox.Add (run_stop, 0, wx.EXPAND) - - # self.radio_box.SetToolTipString("Viewpoint Before Or After Symbol Filter") - - self.radio_box_mode = wx.RadioBox(self, 11106, "Mode", style=wx.RA_SPECIFY_ROWS, - choices = ["Standard", "Population"] ) - ctrlbox.Add (self.radio_box_mode, 0, wx.EXPAND) - - self.radio_box_color = wx.RadioBox(self, 11107, "Color", style=wx.RA_SPECIFY_ROWS, - choices = ["Mono", "2 Color"] ) - ctrlbox.Add (self.radio_box_color, 0, wx.EXPAND) - wx.EVT_RADIOBOX(self.radio_box_color, 11107, self.color_select) - - self.radio_box_source = wx.RadioBox(self, 11108, "Source", style=wx.RA_SPECIFY_ROWS, - choices = ["Direct", "Differential"] ) - ctrlbox.Add (self.radio_box_source, 0, wx.EXPAND) - - ctrlbox.Add ((10, 0) ,1) # stretchy space - - return ctrlbox - - def run_stop (self, evt): - self.info.running = not self.info.running - - def color_select(self, evt): - sel = self.radio_box_color.GetSelection() - if sel: - self.graph.color1 = 'red' - self.graph.color2 = 'green' - else: - self.graph.color1 = 'blue' - self.graph.color2 = 'blue' - -class constellation_plot_graph_window (plot.PlotCanvas): - - def __init__ (self, info, parent, id = -1, - pos = wx.DefaultPosition, size = (140, 140), - num_plots=100, - style = wx.DEFAULT_FRAME_STYLE, name = ""): - plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name) - - self.SetXUseScopeTicks (True) - self.SetEnableGrid (False) - self.SetEnableZoom (True) - self.SetEnableLegend(True) - # self.SetBackgroundColour ('black') - - self.info = info; - self.plot_window = parent - - self.total_points = 0 - - self.num_plots = num_plots - - EVT_DATA_EVENT (self, self.format_data) - - self.input_watcher = constellation_plot_input_watcher (info.msgq, self, info.frame_decim, self.num_plots) - - self.flag = False - - self.color1 = 'blue' - self.color2 = 'blue' - - def format_data (self, evt): - if not self.info.running: - return - if self.plot_window.radio_box_mode.GetSelection(): - self.format_data_pop(evt) - else: - self.format_data_std(evt) - - def format_data_std (self, evt): - info = self.info - records = evt.data - nchannels = len (records) - npoints = len (records[0]) - self.total_points += npoints - - self.SetXUseScopeTicks (True) # use 10 divisions, no labels - - objects = [] - - r = records[0] # input data - l = len(r) / 2 - p0 = [] - p1 = [] - for i in range(l): - p = [ r[ i*2 ], r[ i*2+1 ] ] - if self.flag: - p1.append(p) - else: - p0.append(p) - self.flag = not self.flag - - objects.append (plot.PolyMarker (p0, marker='plus', colour=self.color1)) - objects.append (plot.PolyMarker (p1, marker='plus', colour=self.color2)) - - graphics = plot.PlotGraphics (objects, - title='Constellation', - xLabel = 'I', yLabel = 'Q') - - x_range = (-1.0, 1.0) - y_range = (-1.0, 1.0) - self.Draw (graphics, xAxis=x_range, yAxis=y_range) - - def format_data_pop (self, evt): - if not self.info.running: - return - - info = self.info - records = evt.data - nchannels = len (records) - npoints = len (records[0]) - self.total_points += npoints - - self.SetXUseScopeTicks (True) # use 10 divisions, no labels - - objects = [] - - r = records[0] # input data - l = len(r) / 2 - b0 = [] - b1 = [] - max_buckets = 6.0 - m = int(2 * pi * max_buckets) - for i in range(m+1): - b0.append(0) - b1.append(0) - for i in range(l): - # p = [ r[ i*2 ], r[ i*2+1 ] ] - # if self.flag: - # p1.append(p) - # else: - # p0.append(p) - theta = math.atan2 ( r[ i*2 ], r[ i*2+1 ] ) - bucket = int((theta + pi) * max_buckets) - if 1: - if self.flag: - b0[bucket] += 1 - else: - b1[bucket] += 1 - self.flag = not self.flag - - # determine avg. "power" - for later rescaling of the values - tot = ct = 0 - for b in b0+b1: - tot += b - ct += 1 - avg = float(tot) / float(ct) - - p0 = [] - p1 = [] - r = len(b0) - for i in range(r): - theta = ((float(i)/ r) * 2 * pi) - pi - abs = 0.5 * b0[i] / avg - p = [ abs * math.cos(theta), abs * math.sin(theta) ] - if i == 0: - sp = p - p0.append(p) - p0.append(sp) - r = len(b1) - for i in range(r): - theta = ((float(i) / r) * 2 * pi) - pi - abs = 0.5 * b1[i] / avg - p = [ abs * math.cos(theta), abs * math.sin(theta) ] - if i == 0: - sp = p - p1.append(p) - p1.append(sp) - objects.append (plot.PolyLine (p0, colour=self.color1, legend='')) - objects.append (plot.PolyLine (p1, colour=self.color2, legend='')) - - graphics = plot.PlotGraphics (objects, - title='Constellation', - xLabel = 'I', yLabel = 'Q') - - x_range = (-2.5, 2.5) - y_range = (-2.5, 2.5) - self.Draw (graphics, xAxis=x_range, yAxis=y_range) -############################################################################ -class correlation_plot_f(gr.hier_block2): - def __init__(self, parent, title='', sps=10, - frame_decim=4, - num_inputs=1, **kwargs): - - gr.hier_block2.__init__(self, "correlation_plot_f", - gr.io_signature(num_inputs, num_inputs, gr.sizeof_float), - gr.io_signature(0,0,0)) - - msgq = gr.msg_queue(2) # message queue that holds at most 2 messages - self.st = blocks.message_sink(gr.sizeof_float, msgq, True) - self.connect((self, 0), self.st) - - self.win = correlation_plot_window(correlation_plot_win_info (msgq, sps, frame_decim, None, title), parent, sps=sps) - -# ======================================================================== - -wxDATA_EVENT = wx.NewEventType() - -def EVT_DATA_EVENT(win, func): - win.Connect(-1, -1, wxDATA_EVENT, func) - -class correlation_plot_DataEvent(wx.PyEvent): - def __init__(self, data): - wx.PyEvent.__init__(self) - self.SetEventType (wxDATA_EVENT) - self.data = data - - def Clone (self): - self.__class__ (self.GetId()) - -class correlation_plot_win_info (object): - __slots__ = ['msgq', 'sps', 'frame_decim', - 'scopesink', 'title', - 'time_scale_cursor', 'marker', 'xy', - 'autorange', 'running'] - - def __init__ (self, msgq, sps, frame_decim, - scopesink, title = "Oscilloscope", xy=True): - self.msgq = msgq - self.sps = sps - self.frame_decim = frame_decim - self.scopesink = scopesink - self.title = title; - - self.marker = 'line' - self.xy = xy - self.autorange = False - self.running = True - - def get_decimation_rate (self): - return 1.0 - - def set_marker (self, s): - self.marker = s - - def get_marker (self): - return self.marker - -class correlation_plot_input_watcher (threading.Thread): - def __init__ (self, msgq, event_receiver, frame_decim, **kwds): - threading.Thread.__init__ (self, **kwds) - self.setDaemon (1) - self.msgq = msgq - self.event_receiver = event_receiver - self.frame_decim = frame_decim - self.iscan = 0 - self.keep_running = True - self.skip = 0 - self.totsamp = 0 - self.skip_samples = 0 - self.start () - self.msg_string = "" - self.skip_mode = False - - def run (self): - # print "correlation_plot_input_watcher: pid = ", os.getpid () - time.sleep(1) - while (self.keep_running): - bytes_needed = 24000 * gr.sizeof_float - - if len(self.msg_string) < bytes_needed: - msg = self.msgq.delete_head() # blocking read of message queue - nchan = int(msg.arg1()) # number of channels of data in msg - nsamples = int(msg.arg2()) # number of samples in each channel - self.totsamp += nsamples - - self.msg_string += msg.to_string() # body of the msg as a string - continue - - chan_data = self.msg_string[:bytes_needed] - self.msg_string = self.msg_string[bytes_needed:] - -# if self.skip_mode: -# self.skip_mode = False -# continue - - records = [] - # start = self.skip * gr.sizeof_gr_complex - # start = 0 - # chan_data = self.msg_string[start:start+bytes_needed] - rec = numpy.fromstring (chan_data, numpy.float32) - records.append (rec) - # self.msg_string = "" - - # unused = nsamples - self.num_plots - # unused -= (start/gr.sizeof_gr_complex) - # print "reclen = %d totsamp %d appended %d skip %d start %d unused %d" % (nsamples, self.totsamp, len(rec), self.skip, start/gr.sizeof_float, unused) - - de = correlation_plot_DataEvent (records) - wx.PostEvent (self.event_receiver, de) - records = [] - del de - - # lower values = more frequent plots, but higher CPU usage - # self.skip_samples = 5000 -# self.skip_mode = True - -class correlation_plot_window (wx.Panel): - - def __init__ (self, info, parent, id = -1, - sps=10, - pos = wx.DefaultPosition, size = wx.DefaultSize, name = ""): - wx.Panel.__init__ (self, parent, -1) - self.info = info - - vbox = wx.BoxSizer (wx.HORIZONTAL) - - self.graph = correlation_plot_graph_window (info, self, -1, sps=sps) - - vbox.Add (self.graph, 1, wx.EXPAND) - vbox.Add (self.make_control_box(), 0, wx.EXPAND) -# vbox.Add (self.make_control2_box(), 0, wx.EXPAND) - - self.sizer = vbox - self.SetSizer (self.sizer) - self.SetAutoLayout (True) - self.sizer.Fit (self) - - - # second row of control buttons etc. appears BELOW control_box - def make_control2_box (self): - ctrlbox = wx.BoxSizer (wx.HORIZONTAL) - - ctrlbox.Add ((5,0) ,0) # left margin space - - return ctrlbox - - def make_control_box (self): - # 48k iden sync sig - iden_frame_sync = [0.131053, 0.762875, 0.985880, 0.692932, 0.021247, -0.509172, -0.436476, 0.121728, 0.574703, 0.545912, 0.008813, -0.676659, -0.920639, -0.490609, 0.182287, 0.632788, 0.737212, 0.681760, 0.737237, 0.937172, 1.009479, 0.794382, 0.339788, 0.026356, 0.178487, 0.627079, 0.902744, 0.742624, 0.165377, -0.442614, -0.691702, -0.454418, -0.135002] - ctrlbox = wx.BoxSizer (wx.HORIZONTAL) - - ctrlbox.Add ((5,0) ,0) - - # read directory of correlation signatures - ents = [] - self.signatures = [] - r = re.compile(r'^[13]+$') - path = "corr" - # another hack, add support for 6000 symbol rate in correlation sigs - sps_6k = int((self.info.sps * 4800) / 6000) - for fn in os.listdir(path): - sps = self.info.sps - fn_check = fn - if fn.endswith("-6k"): - sps = sps_6k - fn_check = fn_check.replace("-6k", "") - if not r.match(fn_check): - continue - f = open("%s/%s" % (path, fn)) - line = f.readline() - f.close() - ents.append(line.strip()) - - frame_sync = [] - for c in fn: - if c == '1': - frame_sync.append(1) - else: # 3 - frame_sync.append(-1) - correlation = [] - for symbol in frame_sync: - for i in xrange(sps): - correlation.append(symbol) - correlation.reverse() # reverse order for convolve() - self.signatures.append(correlation) - - #special final entry for iden - ents.append('iDEN') - correlation = iden_frame_sync - correlation.reverse() # reverse order for convolve() - self.signatures.append(correlation) - - self.radio_box_corr = wx.RadioBox(self, 11105, "Sync Signature", style=wx.RA_SPECIFY_COLS, - majorDimension=2, choices = ents ) - self.radio_box_corr.SetToolTipString("Signatures of Known Signal Types") - - ctrlbox.Add (self.radio_box_corr, 0, wx.EXPAND) - - ctrlbox.Add ((10, 0) ,1) # stretchy space - - return ctrlbox - - def run_stop (self, evt): - self.info.running = not self.info.running - -class correlation_plot_graph_window (plot.PlotCanvas): - - def __init__ (self, info, parent, id = -1, - pos = wx.DefaultPosition, size = (140, 140), - sps=10, - style = wx.DEFAULT_FRAME_STYLE, name = ""): - plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name) - - self.SetXUseScopeTicks (True) - self.SetEnableGrid (False) - self.SetEnableZoom (True) - self.SetEnableLegend(True) - # self.SetBackgroundColour ('black') -# self.Zoom([0, 0], [1.0, 1.0]) - - self.info = info; - self.parent = parent; - - self.total_points = 0 - - EVT_DATA_EVENT (self, self.format_data) - - self.input_watcher = correlation_plot_input_watcher (info.msgq, self, info.frame_decim) - - def format_data (self, evt): - if not self.info.running: - return - - info = self.info - records = evt.data - nchannels = len (records) - npoints = len (records[0]) - self.total_points += npoints - - self.SetXUseScopeTicks (True) # use 10 divisions, no labels - - objects = [] - - r = records[0] # input data - - sig = self.parent.signatures[self.parent.radio_box_corr.GetSelection()] - res = numpy.convolve(r, sig, mode='valid') - p0 = [] - i = 0 - for p in res: - p0.append([i, p]) - i += 1 - - objects.append (plot.PolyLine (p0, colour='blue')) - - graphics = plot.PlotGraphics (objects, - title='Correlation', - xLabel = '', yLabel = '') - - x_range = (0, len(res)) - y_range = (-800.0, 800.0) - self.Draw (graphics, xAxis=x_range, yAxis=y_range) - -# -# following code copied from radiorausch file facsink.py -# source: http://sites.google.com/site/radiorausch/ -# -# KA1RBI modified Jul. 2011 to current GR (to fix error messages) -# -# Copyright 2003,2004,2005,2006 Free Software Foundation, Inc. -# -# This file is part of GNU Radio -# -# GNU Radio is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3, or (at your option) -# any later version. -# -# GNU Radio is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with GNU Radio; see the file COPYING. If not, write to -# the Free Software Foundation, Inc., 51 Franklin Street, -# Boston, MA 02110-1301, USA. -# - -# default_facsink_size = (640,240) -default_facsink_size = wx.DefaultSize -default_fac_rate = gr.prefs().get_long('wxgui', 'fac_rate', 3) # was 15 - -class fac_sink_base(object): - def __init__(self, input_is_real=False, baseband_freq=0, y_per_div=10, ref_level=50, - sample_rate=1, fac_size=512, - fac_rate=default_fac_rate, - average=False, avg_alpha=None, title='', peak_hold=False): - - # initialize common attributes - self.baseband_freq = baseband_freq - self.y_divs = 8 - self.y_per_div=y_per_div - self.ref_level = ref_level - self.sample_rate = sample_rate - self.fac_size = fac_size - self.fac_rate = fac_rate - self.average = average - if avg_alpha is None: - self.avg_alpha = 0.20 / fac_rate # averaging needed to be slowed down for very slow rates - else: - self.avg_alpha = avg_alpha - self.title = title - self.peak_hold = peak_hold - self.input_is_real = input_is_real - self.msgq = gr.msg_queue(2) # queue that holds a maximum of 2 messages - - def set_y_per_div(self, y_per_div): - self.y_per_div = y_per_div - - def set_ref_level(self, ref_level): - self.ref_level = ref_level - - def set_average(self, average): - self.average = average - if average: - self.avg.set_taps(self.avg_alpha) - self.set_peak_hold(False) - else: - self.avg.set_taps(1.0) - - def set_peak_hold(self, enable): - self.peak_hold = enable - if enable: - self.set_average(False) - self.win.set_peak_hold(enable) - - def set_avg_alpha(self, avg_alpha): - self.avg_alpha = avg_alpha - - def set_baseband_freq(self, baseband_freq): - self.baseband_freq = baseband_freq - - def set_sample_rate(self, sample_rate): - self.sample_rate = sample_rate - self._set_n() - - def _set_n(self): - self.one_in_n.set_n(max(1, int(self.sample_rate/self.fac_size/self.fac_rate))) - - -class fac_sink_f(gr.hier_block2, fac_sink_base): - def __init__(self, parent, baseband_freq=0, - y_per_div=10, ref_level=50, sample_rate=1, fac_size=512, - fac_rate=default_fac_rate, - average=False, avg_alpha=None, - title='', size=default_facsink_size, peak_hold=False): - - fac_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq, - y_per_div=y_per_div, ref_level=ref_level, - sample_rate=sample_rate, fac_size=fac_size, - fac_rate=fac_rate, - average=average, avg_alpha=avg_alpha, title=title, - peak_hold=peak_hold) - - s2p = gr.stream_to_vector(gr.sizeof_float, self.fac_size) - self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fac_size, - max(1, int(self.sample_rate/self.fac_size/self.fac_rate))) - - - # windowing removed... - - fac = gr.fft_vfc(self.fac_size, True, ()) - - c2mag = gr.complex_to_mag(self.fac_size) - self.avg = gr.single_pole_iir_filter_ff(1.0, self.fac_size) - - # - fac_fac = gr.fft_vfc(self.fac_size, True, ()) - fac_c2mag = gr.complex_to_mag(fac_size) - - - # FIXME We need to add 3dB to all bins but the DC bin - log = gr.nlog10_ff(20, self.fac_size, - -20*math.log10(self.fac_size) ) - sink = gr.message_sink(gr.sizeof_float * self.fac_size, self.msgq, True) - - self.connect(self, s2p, self.one_in_n, fac, c2mag, fac_fac, fac_c2mag, self.avg, log, sink) - # gr.hier_block.__init__(self, fg, s2p, sink) - gr.hier_block2.__init__(self, "fac_sink_f", - gr.io_signature(1, 1, gr.sizeof_float), - gr.io_signature(0, 0, 0)) - - self.win = fac_window(self, parent, size=size) - self.set_average(self.average) - - - -class fac_sink_c(gr.hier_block2, fac_sink_base): - def __init__(self, parent, baseband_freq=0, - y_per_div=10, ref_level=90, sample_rate=1, fac_size=512, - fac_rate=default_fac_rate, - average=False, avg_alpha=None, - title='', size=default_facsink_size, peak_hold=False): - - fac_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq, - y_per_div=y_per_div, ref_level=ref_level, - sample_rate=sample_rate, fac_size=fac_size, - fac_rate=fac_rate, - average=average, avg_alpha=avg_alpha, title=title, - peak_hold=peak_hold) - gr.hier_block2.__init__(self, "fac_sink_c", - gr.io_signature(1, 1, gr.sizeof_gr_complex), - gr.io_signature(0, 0, 0)) - - s2p = blocks.stream_to_vector(gr.sizeof_gr_complex, self.fac_size) - #s2p = repeater.s2v(gr.sizeof_gr_complex, self.fac_size) - self.one_in_n = blocks.keep_one_in_n(gr.sizeof_gr_complex * self.fac_size, - max(1, int(self.sample_rate/self.fac_size/self.fac_rate))) - - - # windowing removed ... - - fac = fft.fft_vcc(self.fac_size, True, ()) - c2mag = blocks.complex_to_mag(fac_size) - - # Things go off into the weeds if we try for an inverse FFT so a forward FFT will have to do... - fac_fac = fft.fft_vfc(self.fac_size, True, ()) - fac_c2mag = blocks.complex_to_mag(fac_size) - - - self.avg = filter.single_pole_iir_filter_ff(1.0, fac_size) - - log = blocks.nlog10_ff(20, self.fac_size, - -20*math.log10(self.fac_size) ) # - 20*math.log10(norm) ) # - self.avg[0] ) - sink = blocks.message_sink(gr.sizeof_float * fac_size, self.msgq, True) - - self.connect(self, s2p, self.one_in_n, fac, c2mag, fac_fac, fac_c2mag, self.avg) - self.connect(self.avg, log, sink) - - # gr.hier_block.__init__(self, fg, s2p, sink) - - self.win = fac_window(self, parent, size=size) - self.set_average(self.average) - - -# ------------------------------------------------------------------------ - -fac_myDATA_EVENT = wx.NewEventType() -fac_EVT_DATA_EVENT = wx.PyEventBinder (fac_myDATA_EVENT, 0) - - -class fac_DataEvent(wx.PyEvent): - def __init__(self, data): - wx.PyEvent.__init__(self) - self.SetEventType (fac_myDATA_EVENT) - self.data = data - - def Clone (self): - self.__class__ (self.GetId()) - - -class fac_input_watcher (threading.Thread): - def __init__ (self, msgq, fac_size, event_receiver, **kwds): - threading.Thread.__init__ (self, **kwds) - self.setDaemon (1) - self.msgq = msgq - self.fac_size = fac_size - self.event_receiver = event_receiver - self.keep_running = True - self.start () - - def run (self): - while (self.keep_running): - msg = self.msgq.delete_head() # blocking read of message queue - itemsize = int(msg.arg1()) - nitems = int(msg.arg2()) - - s = msg.to_string() # get the body of the msg as a string - - # There may be more than one fac frame in the message. - # If so, we take only the last one - if nitems > 1: - start = itemsize * (nitems - 1) - s = s[start:start+itemsize] - - complex_data = Numeric.fromstring (s, Numeric.Float32) - de = fac_DataEvent (complex_data) - wx.PostEvent (self.event_receiver, de) - del de - - -class fac_window (plot.PlotCanvas): - def __init__ (self, facsink, parent, id = -1, - pos = wx.DefaultPosition, size = wx.DefaultSize, - style = wx.DEFAULT_FRAME_STYLE, name = ""): - plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name) - - self.y_range = None - self.facsink = facsink - self.peak_hold = False - self.peak_vals = None - - self.SetEnableGrid (True) - # self.SetEnableZoom (True) - # self.SetBackgroundColour ('black') - - self.build_popup_menu() - - fac_EVT_DATA_EVENT (self, self.set_data) - wx.EVT_CLOSE (self, self.on_close_window) - self.Bind(wx.EVT_RIGHT_UP, self.on_right_click) - - self.input_watcher = fac_input_watcher(facsink.msgq, facsink.fac_size, self) - - - def on_close_window (self, event): - print "fac_window:on_close_window" - self.keep_running = False - - - def set_data (self, evt): - dB = evt.data - L = len (dB) - - if self.peak_hold: - if self.peak_vals is None: - self.peak_vals = dB - else: - self.peak_vals = Numeric.maximum(dB, self.peak_vals) - dB = self.peak_vals - - x = max(abs(self.facsink.sample_rate), abs(self.facsink.baseband_freq)) - sf = 1000.0 - units = "ms" - - x_vals = ((Numeric.arrayrange (L/2) - * ( (sf / self.facsink.sample_rate ) )) ) - points = Numeric.zeros((len(x_vals), 2), Numeric.Float64) - points[:,0] = x_vals - points[:,1] = dB[0:L/2] - - - lines = plot.PolyLine (points, colour='DARKRED') - - - graphics = plot.PlotGraphics ([lines], - title=self.facsink.title, - xLabel = units, yLabel = "dB") - - self.Draw (graphics, xAxis=None, yAxis=self.y_range) - self.update_y_range () - - def set_peak_hold(self, enable): - self.peak_hold = enable - self.peak_vals = None - - def update_y_range (self): - ymax = self.facsink.ref_level - ymin = self.facsink.ref_level - self.facsink.y_per_div * self.facsink.y_divs - self.y_range = self._axisInterval ('min', ymin, ymax) - - def on_average(self, evt): - # print "on_average" - self.facsink.set_average(evt.IsChecked()) - - def on_peak_hold(self, evt): - # print "on_peak_hold" - self.facsink.set_peak_hold(evt.IsChecked()) - - def on_incr_ref_level(self, evt): - # print "on_incr_ref_level" - self.facsink.set_ref_level(self.facsink.ref_level - + self.facsink.y_per_div) - - def on_decr_ref_level(self, evt): - # print "on_decr_ref_level" - self.facsink.set_ref_level(self.facsink.ref_level - - self.facsink.y_per_div) - - def on_incr_y_per_div(self, evt): - # print "on_incr_y_per_div" - self.facsink.set_y_per_div(next_up(self.facsink.y_per_div, (1,2,5,10,20))) - - def on_decr_y_per_div(self, evt): - # print "on_decr_y_per_div" - self.facsink.set_y_per_div(next_down(self.facsink.y_per_div, (1,2,5,10,20))) - - def on_y_per_div(self, evt): - # print "on_y_per_div" - Id = evt.GetId() - if Id == self.id_y_per_div_1: - self.facsink.set_y_per_div(1) - elif Id == self.id_y_per_div_2: - self.facsink.set_y_per_div(2) - elif Id == self.id_y_per_div_5: - self.facsink.set_y_per_div(5) - elif Id == self.id_y_per_div_10: - self.facsink.set_y_per_div(10) - elif Id == self.id_y_per_div_20: - self.facsink.set_y_per_div(20) - - - def on_right_click(self, event): - menu = self.popup_menu - for id, pred in self.checkmarks.items(): - item = menu.FindItemById(id) - item.Check(pred()) - self.PopupMenu(menu, event.GetPosition()) - - - def build_popup_menu(self): - self.id_incr_ref_level = wx.NewId() - self.id_decr_ref_level = wx.NewId() - self.id_incr_y_per_div = wx.NewId() - self.id_decr_y_per_div = wx.NewId() - self.id_y_per_div_1 = wx.NewId() - self.id_y_per_div_2 = wx.NewId() - self.id_y_per_div_5 = wx.NewId() - self.id_y_per_div_10 = wx.NewId() - self.id_y_per_div_20 = wx.NewId() - self.id_average = wx.NewId() - self.id_peak_hold = wx.NewId() - - self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average) - self.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold) - self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level) - self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level) - self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div) - self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div) - self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1) - self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2) - self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5) - self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10) - self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20) - - - # make a menu - menu = wx.Menu() - self.popup_menu = menu - menu.AppendCheckItem(self.id_average, "Average") - menu.AppendCheckItem(self.id_peak_hold, "Peak Hold") - menu.Append(self.id_incr_ref_level, "Incr Ref Level") - menu.Append(self.id_decr_ref_level, "Decr Ref Level") - # menu.Append(self.id_incr_y_per_div, "Incr dB/div") - # menu.Append(self.id_decr_y_per_div, "Decr dB/div") - menu.AppendSeparator() - # we'd use RadioItems for these, but they're not supported on Mac - menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div") - menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div") - menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div") - menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div") - menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div") - - self.checkmarks = { - self.id_average : lambda : self.facsink.average, - self.id_peak_hold : lambda : self.facsink.peak_hold, - self.id_y_per_div_1 : lambda : self.facsink.y_per_div == 1, - self.id_y_per_div_2 : lambda : self.facsink.y_per_div == 2, - self.id_y_per_div_5 : lambda : self.facsink.y_per_div == 5, - self.id_y_per_div_10 : lambda : self.facsink.y_per_div == 10, - self.id_y_per_div_20 : lambda : self.facsink.y_per_div == 20, - } - - -def next_up(v, seq): - """ - Return the first item in seq that is > v. - """ - for s in seq: - if s > v: - return s - return v - -def next_down(v, seq): - """ - Return the last item in seq that is < v. - """ - rseq = list(seq[:]) - rseq.reverse() - - for s in rseq: - if s < v: - return s - return v - - -# ---------------------------------------------------------------- -# Deprecated interfaces -# ---------------------------------------------------------------- - -# returns (block, win). -# block requires a single input stream of float -# win is a subclass of wxWindow - -def make_fac_sink_f(fg, parent, title, fac_size, input_rate, ymin = 0, ymax=50): - - block = fac_sink_f(fg, parent, title=title, fac_size=fac_size, sample_rate=input_rate, - y_per_div=(ymax - ymin)/8, ref_level=ymax) - return (block, block.win) - -# returns (block, win). -# block requires a single input stream of gr_complex -# win is a subclass of wxWindow - -def make_fac_sink_c(fg, parent, title, fac_size, input_rate, ymin=0, ymax=50): - block = fac_sink_c(fg, parent, title=title, fac_size=fac_size, sample_rate=input_rate, - y_per_div=(ymax - ymin)/8, ref_level=ymax) - return (block, block.win) - - -# ---------------------------------------------------------------- -# Standalone test app - deleted -# ---------------------------------------------------------------- - - -############################################################################ - -# Start the receiver -# -if '__main__' == __name__: - app = stdgui2.stdapp(p25_rx_block, "APCO P25 Receiver", 3) - app.MainLoop() diff --git a/op25/gr-op25_repeater/apps/terminal.py b/op25/gr-op25_repeater/apps/terminal.py new file mode 100644 index 0000000..f26482a --- /dev/null +++ b/op25/gr-op25_repeater/apps/terminal.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python + +# Copyright 2008-2011 Steve Glass +# +# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI +# +# This file is part of OP25 +# +# OP25 is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# OP25 is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OP25; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Boston, MA +# 02110-1301, USA. + +import sys +import curses +import curses.textpad +import time +import json +import threading + +from gnuradio import gr + +class curses_terminal(threading.Thread): + def __init__(self, input_q, output_q, **kwds): + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.input_q = input_q + self.output_q = output_q + self.keep_running = True + self.last_update = 0 + self.auto_update = True + self.setup_curses() + self.current_nac = None + self.start() + + def setup_curses(self): + self.stdscr = curses.initscr() + + curses.noecho() + curses.halfdelay(1) + + self.top_bar = curses.newwin(1, 80, 0, 0) + self.freq_list = curses.newwin(20, 80, 1, 0) + self.active1 = curses.newwin(1, 80, 21, 0) + self.active2 = curses.newwin(1, 80, 22, 0) + self.prompt = curses.newwin(1, 10, 23, 0) + self.text_win = curses.newwin(1, 70, 23, 10) + + self.textpad = curses.textpad.Textbox(self.text_win) + + def do_auto_update(self): + UPDATE_INTERVAL = 1 # sec. + if not self.auto_update: + return False + if self.last_update + UPDATE_INTERVAL > time.time(): + return False + self.last_update = time.time() + return True + + def process_terminal_events(self): + # return true signifies end of main event loop + _ORD_S = ord('s') + _ORD_L = ord('l') + _ORD_H = ord('h') + COMMANDS = {_ORD_S: 'skip', _ORD_L: 'lockout', _ORD_H: 'hold'} + c = self.stdscr.getch() + if c == ord('u') or self.do_auto_update(): + msg = gr.message().make_from_string('update', -2, 0, 0) + self.output_q.insert_tail(msg) + if c in COMMANDS.keys(): + msg = gr.message().make_from_string(COMMANDS[c], -2, 0, 0) + self.output_q.insert_tail(msg) + elif c == ord('q'): + return True + elif c == ord('t'): + if self.current_nac: + msg = gr.message().make_from_string('add_default_config', -2, int(self.current_nac), 0) + self.output_q.insert_tail(msg) + elif c == ord('f'): + self.prompt.addstr(0, 0, 'Frequency') + self.prompt.refresh() + self.text_win.clear() + response = self.textpad.edit() + self.prompt.clear() + self.prompt.refresh() + self.text_win.clear() + self.text_win.refresh() + try: + freq = float(response) + if freq < 10000: + freq *= 1000000.0 + except: + freq = None + if freq: + msg = gr.message().make_from_string('set_freq', -2, freq, 0) + self.output_q.insert_tail(msg) + return False + + def process_json(self, js): + # return true signifies end of main event loop + msg = json.loads(js) + if msg['json_type'] == 'trunk_update': + nacs = [x for x in msg.keys() if x != 'json_type'] + if not nacs: + return + times = {msg[nac]['last_tsbk']:nac for nac in nacs} + current_nac = times[ sorted(times.keys(), reverse=True)[0] ] + self.current_nac = current_nac + s = 'NAC 0x%x' % (int(current_nac)) + s += ' WACN 0x%x' % (msg[current_nac]['wacn']) + s += ' SYSID 0x%x' % (msg[current_nac]['sysid']) + s += ' %f' % (msg[current_nac]['rxchan']/ 1000000.0) + s += '/%f' % (msg[current_nac]['txchan']/ 1000000.0) + s += ' tsbks %d' % (msg[current_nac]['tsbks']) + freqs = sorted(msg[current_nac]['frequencies'].keys()) + s = s[:79] + self.top_bar.clear() + self.top_bar.addstr(0, 0, s) + self.top_bar.refresh() + self.freq_list.clear() + for i in xrange(len(freqs)): + s=msg[current_nac]['frequencies'][freqs[i]] + s = s[:79] + self.freq_list.addstr(i, 0, s) + self.freq_list.refresh() + self.stdscr.refresh() + elif msg['json_type'] == 'change_freq': + s = 'Frequency %f' % (msg['freq'] / 1000000.0) + if not msg['tgid']: + self.active1.clear() + self.active2.clear() + return False + s += ' Talkgroup ID %s' % (msg['tgid']) + if msg['tdma'] is not None: + s += 'TDMA Slot %s' % msg['tdma'] + self.active1.clear() + self.active2.clear() + self.active1.addstr(0, 0, s) + self.active1.refresh() + if msg['tag']: + s = msg['tag'] + s = s[:79] + self.active2.addstr(0, 0, s) + self.active2.refresh() + self.stdscr.refresh() + return False + + def process_q_events(self): + # return true signifies end of main event loop + while True: + if self.input_q.empty_p(): + break + msg = self.input_q.delete_head_nowait() + if msg.type() == -4: + return self.process_json(msg.to_string()) + return False + + def run(self): + while(self.keep_running): + if self.process_terminal_events(): + break + if self.process_q_events(): + break + curses.endwin() + msg = gr.message().make_from_string('quit', -2, 0, 0) + self.output_q.insert_tail(msg)