#!/usr/bin/env python # 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 os import sys import threading import time import json import traceback import osmosdr 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 p25_demodulator import p25_decoder from gr_gnuplot import constellation_sink_c from gr_gnuplot import fft_sink_c from gr_gnuplot import mixer_sink_c from gr_gnuplot import symbol_sink_f from gr_gnuplot import eye_sink_f os.environ['IMBE'] = 'soft' _def_symbol_rate = 4800 # The P25 receiver # def byteify(input): # thx so if isinstance(input, dict): return {byteify(key): byteify(value) for key, value in input.iteritems()} elif isinstance(input, list): return [byteify(element) for element in input] elif isinstance(input, unicode): return input.encode('utf-8') else: return input class device(object): def __init__(self, config, tb): self.name = config['name'] self.sample_rate = config['rate'] self.args = config['args'] self.tb = tb if config['args'].startswith('audio:'): self.init_audio(config) elif config['args'].startswith('file:'): self.init_file(config) else: self.init_osmosdr(config) def init_file(self, config): filename = config['args'].replace('file:', '', 1) src = blocks.file_source(gr.sizeof_gr_complex, filename, repeat = False) throttle = blocks.throttle(gr.sizeof_gr_complex, config['rate']) self.tb.connect(src, throttle) self.src = throttle self.frequency = config['frequency'] self.offset = config['offset'] def init_audio(self, config): filename = config['args'].replace('audio:', '') src = audio.source(self.sample_rate, filename) gain = 1.0 if config['gains'].startswith('audio:'): gain = float(config['gains'].replace('audio:', '')) self.src = blocks.multiply_const_ff(gain) self.tb.connect(src, self.src) def init_osmosdr(self, config): speeds = [250000, 1000000, 1024000, 1800000, 1920000, 2000000, 2048000, 2400000, 2560000] sys.stderr.write('device: %s\n' % config) if config['args'].startswith('rtl') and config['rate'] not in speeds: sys.stderr.write('WARNING: requested sample rate %d for device %s may not\n' % (config['rate'], config['name'])) sys.stderr.write("be optimal. You may want to use one of the following rates\n") sys.stderr.write('%s\n' % speeds) self.src = osmosdr.source(config['args']) for tup in config['gains'].split(','): name, gain = tup.split(':') self.src.set_gain(int(gain), name) self.src.set_freq_corr(config['ppm']) self.ppm = config['ppm'] self.src.set_sample_rate(config['rate']) self.src.set_center_freq(config['frequency']) self.frequency = config['frequency'] self.offset = config['offset'] class channel(object): def __init__(self, config, dev, verbosity): sys.stderr.write('channel (dev %s): %s\n' % (dev.name, config)) self.device = dev self.name = config['name'] self.symbol_rate = _def_symbol_rate if 'symbol_rate' in config.keys(): self.symbol_rate = config['symbol_rate'] self.config = config if dev.args.startswith('audio:'): self.demod = p25_demodulator.p25_demod_fb( input_rate = dev.sample_rate, filter_type = config['filter_type']) else: self.demod = p25_demodulator.p25_demod_cb( input_rate = dev.sample_rate, demod_type = config['demod_type'], filter_type = config['filter_type'], excess_bw = config['excess_bw'], relative_freq = dev.frequency + dev.offset - config['frequency'], offset = dev.offset, if_rate = config['if_rate'], symbol_rate = self.symbol_rate) q = gr.msg_queue(1) self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q) self.kill_sink = [] if 'plot' not in config.keys(): return self.sinks = [] for plot in config['plot'].split(','): if plot == 'datascope': assert config['demod_type'] == 'fsk4' ## datascope plot requires fsk4 demod type sink = eye_sink_f(sps=config['if_rate'] / self.symbol_rate) self.demod.connect_bb('symbol_filter', sink) self.kill_sink.append(sink) elif plot == 'symbol': sink = symbol_sink_f() self.demod.connect_float(sink) self.kill_sink.append(sink) elif plot == 'fft': assert config['demod_type'] == 'cqpsk' ## fft plot requires cqpsk demod type i = len(self.sinks) self.sinks.append(fft_sink_c()) self.demod.connect_complex('src', self.sinks[i]) self.kill_sink.append(self.sinks[i]) elif plot == 'mixer': assert config['demod_type'] == 'cqpsk' ## mixer plot requires cqpsk demod type i = len(self.sinks) self.sinks.append(mixer_sink_c()) self.demod.connect_complex('mixer', self.sinks[i]) self.kill_sink.append(self.sinks[i]) elif plot == 'constellation': i = len(self.sinks) assert config['demod_type'] == 'cqpsk' ## constellation plot requires cqpsk demod type self.sinks.append(constellation_sink_c()) self.demod.connect_complex('diffdec', self.sinks[i]) self.kill_sink.append(self.sinks[i]) else: sys.stderr.write('unrecognized plot type %s\n' % plot) return class rx_block (gr.top_block): # Initialize the receiver # def __init__(self, verbosity, config): self.verbosity = verbosity gr.top_block.__init__(self) self.device_id_by_name = {} self.configure_devices(config['devices']) self.configure_channels(config['channels']) def configure_devices(self, config): self.devices = [] for cfg in config: self.device_id_by_name[cfg['name']] = len(self.devices) self.devices.append(device(cfg, self)) def find_device(self, chan): for dev in self.devices: if dev.args.startswith('audio:') and chan['demod_type'] == 'fsk4': return dev d = abs(chan['frequency'] - dev.frequency) nf = dev.sample_rate / 2 if d + 6250 <= nf: return dev return None def configure_channels(self, config): self.channels = [] for cfg in config: dev = self.find_device(cfg) if dev is None: sys.stderr.write('* * * Frequency %d not within spectrum band of any device - ignoring!\n' % cfg['frequency']) continue chan = channel(cfg, dev, self.verbosity) self.channels.append(chan) self.connect(dev.src, chan.demod, chan.decoder) def scan_channels(self): for chan in self.channels: sys.stderr.write('scan %s: error %d\n' % (chan.config['frequency'], chan.demod.get_freq_error())) class rx_main(object): def __init__(self): self.keep_running = True # command line argument parsing parser = OptionParser(option_class=eng_option) parser.add_option("-c", "--config-file", type="string", default=None, help="specify config file name") parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level") parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") (options, args) = parser.parse_args() # wait for gdb if options.pause: print 'Ready for GDB to attach (pid = %d)' % (os.getpid(),) raw_input("Press 'Enter' to continue...") if options.config_file == '-': config = json.loads(sys.stdin.read()) else: config = json.loads(open(options.config_file).read()) self.tb = rx_block(options.verbosity, config = byteify(config)) def run(self): try: self.tb.start() while self.keep_running: time.sleep(1) except: sys.stderr.write('main: exception occurred\n') sys.stderr.write('main: exception:\n%s\n' % traceback.format_exc()) if __name__ == "__main__": rx = rx_main() rx.run()