rx.py replaces scope.py
This commit is contained in:
parent
45794418ae
commit
e9911c5df8
|
@ -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()
|
|
@ -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()
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
Loading…
Reference in New Issue