#!/usr/bin/env python # Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 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 select import traceback import osmosdr from gnuradio import audio, eng_notation, gr, filter, blocks, fft, analog, digital from gnuradio.eng_option import eng_option from math import pi from optparse import OptionParser import trunking import op25 import op25_repeater import p25_demodulator import p25_decoder from sockaudio import audio_thread from sql_dbi import sql_dbi 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 from gr_gnuplot import setup_correlation from gr_gnuplot import sync_plot from nxdn_trunking import cac_message from terminal import op25_terminal sys.path.append('tdma') import lfsr os.environ['IMBE'] = 'soft' _def_symbol_rate = 4800 _def_interval = 3.0 # sec _def_file_dir = '../www/images' _def_audio_port = 23456 # udp port for audio thread _def_audio_output = 'default' # output device name for audio thread # The P25 receiver # def byteify(input): # thx so if sys.version[0] != '2': # hack, must be a better way return input 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.tunable = config['tunable'] self.tb = tb self.frequency = 0 if config['args'].startswith('audio-if:'): self.init_audio_if(config) elif config['args'].startswith('audio:'): self.init_audio(config) elif config['args'].startswith('file:'): self.init_file(config) elif config['args'].startswith('udp:'): self.init_udp(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_if(self, config): filename = config['args'].replace('audio-if:', '') self.audio_source = audio.source(config['rate'], filename) self.null_source = blocks.null_source (gr.sizeof_float) self.audio_cvt = blocks.float_to_complex() self.tb.connect(self.audio_source, (self.audio_cvt, 0)) self.tb.connect(self.null_source, (self.audio_cvt, 1)) self.src = self.audio_cvt self.frequency = config['frequency'] self.offset = config['offset'] def init_audio(self, config): filename = config['args'].replace('audio:', '') if filename.startswith('file:'): filename = filename.replace('file:', '') repeat = False s2f = blocks.short_to_float() K = 1 / 32767.0 src = blocks.multiply_const_ff(K) throttle = blocks.throttle(gr.sizeof_short, self.sample_rate) # may be redundant in stdin case ? if filename == '-': fd = 0 # stdin fsrc = blocks.file_descriptor_source(gr.sizeof_short, fd, repeat) else: fsrc = blocks.file_source(gr.sizeof_short, filename, repeat) self.tb.connect(fsrc, throttle, s2f, src) else: 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_udp(self, config): hostinfo = config['args'].split(':') hostname = hostinfo[1] udp_port = int(hostinfo[2]) bufsize = 32000 # might try enlarging this if packet loss self.src = blocks.udp_source(gr.sizeof_gr_complex, hostname, udp_port, payload_size = bufsize) self.ppm = 0 self.frequency = config['frequency'] self.offset = 0 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'] def set_frequency(self, frequency): if frequency == self.frequency: return if not self.tunable: return self.frequency = frequency self.src.set_center_freq(frequency) class channel(object): def __init__(self, config, dev, verbosity, msgq = None, process_msg=None, msgq_id=-1, role=''): sys.stderr.write('channel (dev %s): %s\n' % (dev.name, config)) self.device = dev self.name = config['name'] self.symbol_rate = _def_symbol_rate self.process_msg = process_msg self.role = role self.dev = '' self.sysid = [] self.nac = [] if 'symbol_rate' in config.keys(): self.symbol_rate = config['symbol_rate'] self.config = config self.verbosity = verbosity self.frequency = config['frequency'] if self.device.args.startswith('audio-if') else 0 self.tdma_state = False self.xor_cache = {} self.tuning_error = 0 self.freq_correction = 0 self.error_band = 0 self.last_error_update = 0 self.last_set_freq_at = time.time() self.warned_frequencies = {} self.msgq_id = msgq_id self.next_band_change = time.time() self.audio_port = _def_audio_port self.audio_output = _def_audio_output self.audio_gain = 1.0 if 'audio_gain' in config: self.audio_gain = float(config['audio_gain']) if dev.args.startswith('audio:'): self.demod = p25_demodulator.p25_demod_fb( input_rate = dev.sample_rate, filter_type = config['filter_type'], if_rate = config['if_rate'], symbol_rate = self.symbol_rate) 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, use_old_decim = True if self.device.args.startswith('audio-if') else False) if msgq is not None: q = msgq else: q = gr.msg_queue(20) if 'decode' in config.keys() and config['decode'].startswith('p25_decoder'): num_ambe = 1 (proto, wireshark_host, udp_port) = config['destination'].split(':') assert proto == 'udp' wireshark_host = wireshark_host.replace('/', '') udp_port = int(udp_port) if role == 'vc': self.audio_port = udp_port if 'audio_output' in config.keys(): self.audio_output = config['audio_output'] self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=True, num_ambe=num_ambe, wireshark_host=wireshark_host, udp_port=udp_port, do_msgq = True, msgq=q, audio_output=self.audio_output, debug=verbosity, msgq_id=self.msgq_id) else: self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q, self.msgq_id) if self.symbol_rate == 6000 and role == 'cc': sps = config['if_rate'] // self.symbol_rate self.demod.set_symbol_rate(self.symbol_rate) # this and the foll. call should be merged? self.demod.clock.set_omega(float(sps)) self.demod.clock.set_tdma(True) sys.stderr.write('initializing TDMA control channel %s channel ID %d\n' % (self.name, self.msgq_id)) if self.process_msg is not None and msgq is None: self.q_watcher = du_queue_watcher(q, lambda msg: self.process_msg(msg, sender=self)) self.kill_sink = [] if 'blacklist' in config.keys(): for g in config['blacklist'].split(','): self.decoder.insert_blacklist(int(g)) if 'whitelist' in config.keys(): for g in config['whitelist'].split(','): self.decoder.insert_whitelist(int(g)) self.sinks = [] if 'plot' not in config.keys(): return 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) sink.set_title(self.name) self.sinks.append(sink) self.demod.connect_bb('symbol_filter', sink) self.kill_sink.append(sink) elif plot == 'symbol': sink = symbol_sink_f() sink.set_title(self.name) self.sinks.append(sink) 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) sink = fft_sink_c() sink.set_title(self.name) self.sinks.append(sink) 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) sink = mixer_sink_c() sink.set_title(self.name) self.sinks.append(sink) 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 sink = constellation_sink_c() sink.set_title(self.name) self.sinks.append(sink) self.demod.connect_complex('diffdec', self.sinks[i]) self.kill_sink.append(self.sinks[i]) elif plot == 'correlation': assert config['demod_type'] == 'fsk4' ## correlation plot requires fsk4 demod type assert config['symbol_rate'] == 4800 ## 4800 required for correlation plot sps=config['if_rate'] // self.symbol_rate sinks = setup_correlation(sps, self.name, self.demod.connect_bb) self.kill_sink += sinks self.sinks += sinks elif plot == 'sync': assert config['demod_type'] == 'cqpsk' ## sync plot requires cqpsk demod type i = len(self.sinks) sink = sync_plot(block = self.demod.clock) sink.set_title(self.name) self.sinks.append(sink) self.kill_sink.append(self.sinks[i]) # does not issue self.connect() else: sys.stderr.write('unrecognized plot type %s\n' % plot) return def set_frequency(self, frequency): assert frequency if self.device.tunable: self.device.set_frequency(frequency) f = self.frequency if self.device.args.startswith('audio-if') else frequency relative_freq = self.device.frequency + self.device.offset + self.tuning_error - f if (not self.device.tunable) and (not self.device.args.startswith('audio-if')) and abs(relative_freq) > ((self.demod.input_rate / 2) - (self.demod.if1 / 2)): if frequency not in self.warned_frequencies: sys.stderr.write('warning: set frequency %f to non-tunable device %s rejected.\n' % (frequency / 1000000.0, self.device.name)) self.warned_frequencies[frequency] = 0 self.warned_frequencies[frequency] += 1 #print 'set_relative_frequency: error, relative frequency %d exceeds limit %d' % (relative_freq, self.demod.input_rate/2) return False self.demod.set_relative_frequency(relative_freq) self.last_set_freq_at = time.time() if not self.device.args.startswith('audio-if'): self.frequency = frequency def error_tracking(self, last_change_freq): curr_time = time.time() if self.config['demod_type'] == 'fsk4': return None # todo: allow tracking in fsk4 demod UPDATE_TIME = 3 if self.last_error_update + UPDATE_TIME > curr_time: return None self.last_error_update = time.time() if not self.demod.is_muted(): band = self.demod.get_error_band() freq_error = self.demod.get_freq_error() if band and curr_time >= self.next_band_change: self.next_band_change = curr_time + 20.0 self.error_band += band sys.stderr.write('channel %d set error band %d\n' % (self.msgq_id, self.error_band)) self.freq_correction += freq_error * 0.15 self.freq_correction = int(self.freq_correction) if self.freq_correction > 600: self.freq_correction -= 1200 self.error_band += 1 elif self.freq_correction < -600: self.freq_correction += 1200 self.error_band -= 1 self.error_band = min(self.error_band, 2) self.error_band = max(self.error_band, -2) self.tuning_error = int(self.error_band * 1200 + self.freq_correction) e = 0 if last_change_freq > 0: e = (self.tuning_error*1e6) / float(last_change_freq) else: e = 0 freq_error = 0 band = 0 ### self.set_frequency(self.frequency) # adjust relative frequency with updated tuning_error if self.verbosity >= 10: sys.stderr.write('%f\terror_tracking\t%s\t%d\t%d\t%d\t%d\t%d\t%f\n' % (curr_time, self.name, self.msgq_id, freq_error, self.error_band, self.tuning_error, self.freq_correction, e)) d = {'time': time.time(), 'json_type': 'freq_error_tracking', 'name': self.name, 'device': self.device.name, 'freq_error': freq_error, 'band': band, 'error_band': self.error_band, 'tuning_error': self.tuning_error, 'freq_correction': self.freq_correction} if self.frequency: self.set_frequency(self.frequency) return d def configure_tdma(self, params): set_tdma = False if params['tdma'] is not None: set_tdma = True self.decoder.set_slotid(params['tdma']) self.demod.clock.set_tdma(set_tdma) if set_tdma == self.tdma_state: return # already in desired state self.tdma_state = set_tdma if set_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) self.decoder.set_nac(params['nac']) rate = 6000 else: rate = 4800 sps = self.config['if_rate'] / rate self.demod.set_symbol_rate(rate) # this and the foll. call should be merged? self.demod.clock.set_omega(float(sps)) 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() if not self.keep_running: break self.callback(msg) class rx_block (gr.top_block): # Initialize the receiver # def __init__(self, verbosity, config, trunk_conf_file=None, terminal_type=None, track_errors=False, udp_player=None): self.verbosity = verbosity gr.top_block.__init__(self) self.device_id_by_name = {} self.msg_types = {} self.terminal_type = terminal_type self.last_process_update = 0 self.last_freq_params = {'freq' : 0.0, 'tgid' : None, 'tag' : "", 'tdma' : None} self.trunk_rx = None self.track_errors = track_errors self.last_change_freq = 0 self.sql_db = sql_dbi() self.input_q = gr.msg_queue(20) self.output_q = gr.msg_queue(20) self.last_voice_channel_id = 0 self.terminal = op25_terminal(self.input_q, self.output_q, terminal_type) self.configure_devices(config['devices']) self.configure_channels(config['channels']) if trunk_conf_file: self.trunk_rx = trunking.rx_ctl(frequency_set = self.change_freq, debug = self.verbosity, conf_file = trunk_conf_file, logfile_workers=[], send_event=self.send_event) self.sinks = [] for chan in self.channels: if len(chan.sinks): self.sinks += chan.sinks if self.is_http_term(): for sink in self.sinks: sink.gnuplot.set_interval(_def_interval) sink.gnuplot.set_output_dir(_def_file_dir) if udp_player: chan = self.find_audio_channel() # find chan used for audio self.audio = audio_thread("127.0.0.1", chan.audio_port, chan.audio_output, False, chan.audio_gain) else: self.audio = None def find_channel_cc(self, params): channels = [] for chan in self.channels: if chan.role != 'cc': continue if len(chan.nac) and params['nac'] not in chan.nac: continue if len(chan.sysid) and params['sysid'] not in chan.sysid: continue channels.append(chan) if self.verbosity > 0: sys.stderr.write('%f find_channel_cc: selected channel %d (%s) for tuning request type %s frequency %f\n' % (time.time(), chan.msgq_id, chan.name, 'cc', params['freq'] / 1000000.0)) return channels def find_channel_vc(self, params): channels = [] for chan in self.channels: # pass1 - search for vc on non-tunable dev having frequency within band if chan.role != 'vc': continue if chan.device.tunable: continue if abs(params['freq'] - chan.device.frequency) >= chan.demod.relative_limit: #sys.stderr.write('%f skipping channel %d frequency %f dev freq %f limit %f\n' % (time.time(), chan.msgq_id, params['freq'] / 1000000.0, chan.device.frequency / 1000000.0, chan.demod.relative_limit / 1000000.0)) continue channels.append(chan) if self.verbosity > 0: sys.stderr.write('%f find_channel_vc: selected channel %d (%s) for tuning request type %s frequency %f (1)\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['freq'] / 1000000.0)) return channels for chan in self.channels: # pass2 - search for vc on tunable dev if chan.role != 'vc': continue if not chan.device.tunable: continue channels.append(chan) if self.verbosity > 0: sys.stderr.write('%f find_channel_vc: selected channel %d (%s) for tuning request type %s frequency %f (2)\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['freq'] / 1000000.0)) return channels return [] # pass 1 and 2 failed def do_error_tracking(self): if not self.track_errors: return for chan in self.channels: d = chan.error_tracking(self.last_change_freq) if d is not None and not self.input_q.full_p(): msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0) self.input_q.insert_tail(msg) def change_freq(self, params): self.last_freq_params = params freq = params['freq'] self.last_change_freq = freq channel_type = params['channel_type'] # vc or cc if channel_type == 'vc': channels = self.find_channel_vc(params) elif channel_type == 'cc': channels = self.find_channel_cc(params) else: raise ValueError('change_freq: invalid channel_type: %s' % channel_type) if len(channels) == 0: sys.stderr.write('change_freq: no channel(s) found for %s frequency %f\n' % (channel_type, freq/1000000.0)) return for chan in channels: chan.device.set_frequency(freq) chan.set_frequency(freq) chan.configure_tdma(params) self.freq_update() if channel_type == 'vc': self.last_voice_channel_id = chan.msgq_id #return if self.trunk_rx is None: return voice_chans = [chan for chan in self.channels if chan.role == 'vc'] voice_state = channel_type == 'vc' # FIXME: fsk4 case needs work/testing for chan in voice_chans: if voice_state and chan.msgq_id == self.last_voice_channel_id: chan.demod.set_muted(False) else: chan.demod.set_muted(True) def is_http_term(self): if self.terminal_type.startswith('http:'): return True else: return False def process_terminal_msg(self, msg): # return true = end top block RX_COMMANDS = 'skip lockout hold'.split() s = msg.to_string() t = msg.type() if t == -4: d = json.loads(s) s = d['command'] if type(s) is not str and isinstance(s, bytes): # should only get here if python3 s = s.decode() if s == 'quit': return True elif s == 'update': ## deprecated here: to be removed pass # self.process_update() elif s == 'set_freq': sys.stderr.write('set_freq not supported\n') return #freq = msg.arg1() #self.last_freq_params['freq'] = freq #self.set_freq(freq) elif s == 'adj_tune': freq = msg.arg1() elif s == 'dump_tgids': self.trunk_rx.dump_tgids() elif s == 'reload_tags': nac = msg.arg1() self.trunk_rx.reload_tags(int(nac)) elif s == 'add_default_config': nac = msg.arg1() self.trunk_rx.add_default_config(int(nac)) elif s in RX_COMMANDS: if self.trunk_rx is not None: self.trunk_rx.process_qmsg(msg) elif s == 'settings-enable' and self.trunk_rx is not None: self.trunk_rx.enable_status(d['data']) return False def process_ajax(self): if not self.is_http_term(): return if self.input_q.full_p(): return filenames = [sink.gnuplot.filename for sink in self.sinks if sink.gnuplot.filename] error = [] for chan in self.channels: if hasattr(chan.demod, 'get_freq_error'): error.append(chan.demod.get_freq_error()) d = {'json_type': 'rx_update', 'error': error, 'files': filenames, 'time': time.time()} msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0) self.input_q.insert_tail(msg) def process_update(self): UPDATE_INTERVAL = 1.0 # sec. now = time.time() if now < self.last_process_update + UPDATE_INTERVAL: return self.last_process_update = now self.freq_update() if self.input_q.full_p(): return if self.trunk_rx is None: return ## possible race cond - just ignore js = self.trunk_rx.to_json() msg = gr.message().make_from_string(js, -4, 0, 0) self.input_q.insert_tail(msg) self.process_ajax() def send_event(self, d): ## called from trunking module to send json msgs / updates to client if d is not None: self.sql_db.event(d) if d and not self.input_q.full_p(): msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0) self.input_q.insert_tail(msg) self.process_update() def freq_update(self): if self.input_q.full_p(): return params = self.last_freq_params params['json_type'] = 'change_freq' params['current_time'] = time.time() js = json.dumps(params) msg = gr.message().make_from_string(js, -4, 0, 0) self.input_q.insert_tail(msg) def process_msg(self, msg): mtype = msg.type() if mtype == -2 or mtype == -4: self.process_terminal_msg(msg) else: self.process_channel_msg(msg, mtype) def process_channel_msg(self, msg, mtype): msgtext = msg.to_string() aa55 = trunking.get_ordinals(msgtext[:2]) assert aa55 == 0xaa55 msgq_id = trunking.get_ordinals(msgtext[2:4]) msgtext = msgtext[4:] if mtype == -5: self.process_nxdn_msg(msgtext) else: self.process_trunked_qmsg(msg, msgq_id) def process_nxdn_msg(self, s): if isinstance(s[0], str): # for python 2/3 s = [ord(x) for x in s] msgtype = chr(s[0]) lich = s[1] if self.verbosity > 2: sys.stderr.write ('process_nxdn_msg %s lich %x\n' % (msgtype, lich)) if msgtype == 'c': # CAC type ran = s[2] & 0x3f msg = cac_message(s[2:]) if msg['msg_type'] == 'CCH_INFO' and self.verbosity: sys.stderr.write ('%-10s %-10s system %d site %d ran %d\n' % (msg['cc1']/1e6, msg['cc2']/1e6, msg['location_id']['system'], msg['location_id']['site'], ran)) if self.verbosity > 1: sys.stderr.write('%s\n' % json.dumps(msg)) def filtered(self, msg, msgq_id): # return True if msg should be suppressed chan = self.channels[msgq_id-1] t = msg.type() if chan.role == 'vc' and t in [7, 12]: ## suppress tsbk/mbt/pdu received over vc return True return False def process_trunked_qmsg(self, msg, msgq_id): # p25 trunked message if self.trunk_rx is None: return if self.filtered(msg, msgq_id): return self.trunk_rx.process_qmsg(msg) self.trunk_rx.parallel_hunt_cc() self.do_error_tracking() 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_trunked_device(self, chan, requested_dev): if len(self.devices) == 1: # single SDR return self.devices[0] for dev in self.devices: if dev.name == requested_dev: return dev return None def find_device(self, chan, requested_dev): if 'decode' in chan.keys() and chan['decode'].startswith('p25_decoder'): return self.find_trunked_device(chan, requested_dev) 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: decode_d = {'role': '', 'dev': ''} if 'decode' in cfg.keys() and cfg['decode'].startswith('p25_decoder'): decode_p = cfg['decode'].split(':')[1:] for p in decode_p: # possible keys: dev, role, nac, sysid; valid roles: cc vc (k, v) = p.split('=') if k == 'nac' or k == 'sysid': v = [int(x, base=0) for x in v.split(',')] decode_d[k] = v dev = self.find_device(cfg, decode_d['dev']) if dev is None: sys.stderr.write('* * * No device found for channel %s- ignoring!\n' % cfg['name']) continue msgq_id = len(self.channels) + 1 chan = channel(cfg, dev, self.verbosity, msgq=self.output_q, msgq_id = msgq_id, role=decode_d['role']) for k in decode_d.keys(): setattr(chan, k, decode_d[k]) self.channels.append(chan) self.connect(dev.src, chan.demod, chan.decoder) sys.stderr.write('assigning channel "%s" (channel id %d) to device "%s"\n' % (chan.name, chan.msgq_id, dev.name)) if 'log_if' in cfg.keys(): chan.logfile_if = blocks.file_sink(gr.sizeof_gr_complex, 'if-%d-%s' % (chan.config['if_rate'], cfg['log_if'])) chan.demod.connect_complex('agc', chan.logfile_if) if 'log_symbols' in cfg.keys(): chan.logfile = blocks.file_sink(gr.sizeof_char, cfg['log_symbols']) self.connect(chan.demod, chan.logfile) def find_audio_channel(self): for chan in self.channels: # pass1 - look for 'vc' if chan.role == 'vc' and chan.audio_port: return chan for chan in self.channels: # pass2 - any chan with audio port specified if chan.audio_port: return chan return self.channels[0] 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") parser.add_option("-M", "--monitor-stdin", action="store_false", default=True, help="enable press ENTER to quit") parser.add_option("-T", "--trunk-conf-file", type="string", default=None, help="trunking config file name") parser.add_option("-l", "--terminal-type", type="string", default="curses", help="'curses' or udp port or 'http:host:port'") parser.add_option("-X", "--freq-error-tracking", action="store_true", default=False, help="enable experimental frequency error tracking") parser.add_option("-U", "--udp-player", action="store_true", default=False, help="enable built-in udp audio player") (options, args) = parser.parse_args() self.options = options # 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), trunk_conf_file=options.trunk_conf_file, terminal_type=options.terminal_type, track_errors=options.freq_error_tracking, udp_player = options.udp_player) sys.stderr.write('python version detected: %s\n' % sys.version) sys.stderr.flush() def run(self): self.tb.start() if self.options.monitor_stdin: print("Running. press ENTER to quit") while self.keep_running: if self.options.monitor_stdin and select.select([sys.stdin,],[],[],0.0)[0]: c = sys.stdin.read(1) self.keep_running = False break msg = self.tb.output_q.delete_head() if self.tb.process_msg(msg): self.keep_running = False break print('Quitting - now stopping top block') self.tb.stop() if __name__ == "__main__": rx = rx_main() try: rx.run() except KeyboardInterrupt: rx.keep_running = False print('Program ending') time.sleep(1)