From ef927d63ec4ce5f4f194da508b0fdbb89eda58ff Mon Sep 17 00:00:00 2001 From: max Date: Sat, 28 Sep 2013 00:06:50 +0000 Subject: [PATCH] trunking git-svn-id: http://op25.osmocom.org/svn/trunk@327 65a5c917-d112-43f1-993d-58c26a4786be --- repeater/src/python/scope.py | 113 +++++++++++++++- repeater/src/python/trunking.py | 220 ++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 repeater/src/python/trunking.py diff --git a/repeater/src/python/scope.py b/repeater/src/python/scope.py index 5434196..4caa55f 100755 --- a/repeater/src/python/scope.py +++ b/repeater/src/python/scope.py @@ -35,6 +35,11 @@ import math import numpy import time import re +try: + import Hamlib +except: + pass + try: import Numeric except: @@ -49,6 +54,8 @@ from optparse import OptionParser import gnuradio.wxgui.plot as plot +import trunking + speeds = [300, 600, 900, 1200, 1440, 1800, 1920, 2400, 2880, 3200, 3600, 3840, 4000, 4800, 6000, 6400, 7200, 8000, 9600, 14400, 19200] WIRESHARK_PORT = 23456 @@ -76,9 +83,11 @@ class p25_rx_block (stdgui2.std_top_block): parser.add_option("-C", "--costas-alpha", type="eng_float", default=0.125, 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("-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-cc-freq", type="eng_float", default=None, help="trunk control channel frequency", metavar="Hz") 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") @@ -89,6 +98,8 @@ class p25_rx_block (stdgui2.std_top_block): 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") (options, args) = parser.parse_args() if len(args) != 0: parser.print_help() @@ -107,7 +118,16 @@ class p25_rx_block (stdgui2.std_top_block): print "osmosdr source_c creation failure" ignore = True - print 'gain names ', self.src.get_gain_names() + 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) if options.audio: self.channel_rate = 48000 @@ -140,6 +160,24 @@ class p25_rx_block (stdgui2.std_top_block): self.current_speed = i self.default_speed_idx = i + if options.hamlib_model: + self.hamlib_attach(options.hamlib_model) + + class _trunked_states(object): + ACQ = 0 + CC = 1 + TO_VC = 2 + VC = 3 + TO_CC = 4 + self.trunked_states = _trunked_states + + self.trunked_state = self.trunked_states.ACQ + if options.trunk_cc_freq: + if options.hamlib_model: + self.hamlib.set_freq(int(options.trunk_cc_freq)) + # acquire trunk CC based on list of primary and alternates + self.trunked_state = self.trunked_states.CC + # initialize the UI # self.__init_gui(frame, panel, vbox) @@ -200,7 +238,8 @@ class p25_rx_block (stdgui2.std_top_block): self.buffer = gr.copy(gr.sizeof_float) - msgq = gr.msg_queue(2) + msgq = gr.msg_queue(100) + self.du_watcher = du_queue_watcher(msgq, self.process_data_unit) udp_port = 0 if self.options.wireshark: udp_port = WIRESHARK_PORT @@ -208,12 +247,14 @@ class p25_rx_block (stdgui2.std_top_block): self.sink_sf = gr.file_sink(gr.sizeof_char, self.options.raw_symbols) do_imbe = 1 do_output = 1 - self.sink_s = repeater.p25_frame_assembler(self.options.wireshark_host, udp_port, self.options.verbosity, do_imbe, do_output, 0, msgq) + do_msgq = 1 + self.sink_s = repeater.p25_frame_assembler(self.options.wireshark_host, udp_port, self.options.verbosity, do_imbe, do_output, do_msgq, msgq) + self.trunk_ctl = trunking.trunked_system(frequency_set = self.change_freq) if self.options.vocoder: self.sink_imbe = repeater.vocoder(0, 0, 0, '', 0, 0) self.audio_s2f = gr.short_to_float() self.audio_scaler = gr.multiply_const_ff(1 / 32768.0) - self.audio_output = audio.sink(8000, "pulse", True) + self.audio_output = audio.sink(8000, self.options.audio_output, True) self.connect(self.sink_imbe, self.audio_s2f, self.audio_scaler, self.audio_output) else: self.sink_imbe = gr.null_sink(gr.sizeof_char) @@ -233,7 +274,7 @@ class p25_rx_block (stdgui2.std_top_block): self.fac_state = False self.fsk4_demod_connected = False self.psk_demod_connected = False - self.fsk4_demod_mode = True + self.fsk4_demod_mode = False self.corr_i_chan = False if self.baseband_input: @@ -320,7 +361,7 @@ class p25_rx_block (stdgui2.std_top_block): self.__connect([[source, (self.mixer, 0)], [self.lo, (self.mixer, 1)]]) self.set_connection(fft=1) - self.connect_fsk4_demod() + self.connect_demods() # Connect up the flow graph # @@ -589,6 +630,49 @@ class p25_rx_block (stdgui2.std_top_block): vbox.Add(hbox, 0, 0) + def change_freq(self, freq): + if self.trunked_state == self.trunked_states.CC: + print "%d: change frequency to: %f" % (self.trunked_state, float(freq) / 1000000.0) + self.trunked_state = self.trunked_states.TO_VC + if self.options.hamlib_model: + self.hamlib.set_freq(freq) + else: + self.set_freq(freq) + + 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 process_data_unit(self, msg): + type = msg.type() + #print "type %d at %f state %d" %(type, time.time(), self.trunked_state) + s = msg.to_string() + t = 0 + for c in s: + t = (t << 8) + ord(c) + nac = t & 0xffff + t = t >> 16 + if type == 7: + self.trunk_ctl.decode_tsbk(t) + if self.trunked_state == self.trunked_states.TO_CC: + self.trunked_state = self.trunked_states.CC + #print self.trunk_ctl.to_string() + else: + if self.trunked_state == self.trunked_states.TO_VC: + self.trunked_state = self.trunked_states.VC + if self.trunked_state == self.trunked_states.VC and (type == 3 or type == 15): + self.trunked_state = self.trunked_states.TO_CC + print "%d: change frequency to: %f" % (self.trunked_state, float(self.options.trunk_cc_freq) / 1000000.0) + if self.options.hamlib_model: + self.hamlib.set_freq(int(self.options.trunk_cc_freq)) + else: + self.set_freq(int(self.options.trunk_cc_freq)) + def set_gain(self, gain): if self.baseband_input: f = 1.0 @@ -1242,6 +1326,23 @@ class wizard_details_page(wx.wizard.WizardPageSimple): 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): diff --git a/repeater/src/python/trunking.py b/repeater/src/python/trunking.py new file mode 100644 index 0000000..7913428 --- /dev/null +++ b/repeater/src/python/trunking.py @@ -0,0 +1,220 @@ + +def crc16(dat,len): # slow version + poly = (1<<12) + (1<<5) + (1<<0) + crc = 0 + for i in range(len): + bits = (dat >> (((len-1)-i)*8)) & 0xff + for j in range(8): + bit = (bits >> (7-j)) & 1 + crc = ((crc << 1) | bit) & 0x1ffff + if crc & 0x10000: + crc = (crc & 0xffff) ^ poly + crc = crc ^ 0xffff + return crc + +class trunked_system (object): + def __init__(self, debug=0, frequency_set=None): + self.debug = debug + self.frequency_set = frequency_set + self.freq_table = {} + self.stats = {} + self.stats['tsbks'] = 0 + self.stats['crc'] = 0 + self.tsbk_cache = {} + self.secondary = {} + self.adjacent = {} + self.rfss_syid = 0 + self.rfss_rfid = 0 + self.rfss_stid = 0 + self.rfss_chan = 0 + self.rfss_txchan = 0 + self.ns_syid = 0 + self.ns_wacn = 0 + self.ns_chan = 0 + + def to_string(self): + s = [] + for table in self.freq_table: + a = self.freq_table[table]['frequency'] / 1000000.0 + b = self.freq_table[table]['step'] / 1000000.0 + c = self.freq_table[table]['offset'] / 1000000.0 + s.append('tbl-id: %x frequency: %f step %f offset %f' % ( table, a,b,c)) + #self.freq_table[table]['frequency'] / 1000000.0, self.freq_table[table]['step'] / 1000000.0, self.freq_table[table]['offset']) / 1000000.0) + s.append('stats: tsbks %d crc %d' % (self.stats['tsbks'], self.stats['crc'])) + s.append('secondary control channel(s): %s' % ','.join(['%f' % (float(k) / 1000000.0) for k in self.secondary.keys()])) + for f in self.adjacent: + s.append('adjacent %f: %s' % (float(f) / 1000000.0, self.adjacent[f])) + s.append('rf: sysid %x rfid %x stid %x frequency %f uplink %f' % ( self.rfss_syid, self.rfss_rfid, self.rfss_stid, float(self.rfss_chan) / 1000000.0, float(self.rfss_txchan) / 1000000.0)) + s.append('net: sysid %x wacn %x frequency %f' % ( self.ns_syid, self.ns_wacn, float(self.ns_chan) / 1000000.0)) + return '\n'.join(s) + +# return frequency in Hz + def channel_id_to_frequency(self, id): + table = (id >> 12) & 0xf + channel = id & 0xfff + if table not in self.freq_table: + return None + return self.freq_table[table]['frequency'] + self.freq_table[table]['step'] * channel + + def channel_id_to_string(self, id): + table = (id >> 12) & 0xf + channel = id & 0xfff + if table not in self.freq_table: + return "%x-%d" % (table, channel) + return "%f" % (self.freq_table[table]['frequency'] + self.freq_table[table]['step'] * channel) / 1000000.0 + + def decode_mbt_data(self, opcode, header, mbt_data): + # print "decode_mbt_data: %x %x" %(opcode, mbt_data) + if opcode == 0x0: # grp voice channel grant + ch1 = (mbt_data >> 32) & 0xffff + ch2 = (mbt_data >> 16) & 0xffff + ga = (mbt_data ) & 0xffff + if self.debug > 10: + print "mbt00 voice grant ch1 %x ch2 %x addr 0x%x" %(ch1, ch2, ga) + elif opcode == 0x3c: # adjacent status + rfid = (header >> 24) & 0xff + stid = (header >> 16) & 0xff + ch1 = (mbt_data >> 48) & 0xffff + ch2 = (mbt_data >> 32) & 0xffff + if self.debug > 10: + print "mbt3c adjacent rfid %x stid %x ch1 %x ch2 %x" %(rfid, stid, ch1, ch2) + elif opcode == 0x3b: # network status + wacn = (mbt_data >> 44) & 0xfffff + ch1 = (mbt_data >> 24) & 0xffff + ch2 = (mbt_data >> 8) & 0xffff + if self.debug > 10: + print "mbt3b net stat wacn %x ch1 %x ch2 %x" %(wacn, ch1, ch2) + elif opcode == 0x3a: # rfss status + rfid = (mbt_data >> 56) & 0xff + stid = (mbt_data >> 48) & 0xff + ch1 = (mbt_data >> 32) & 0xffff + ch2 = (mbt_data >> 16) & 0xffff + if self.debug > 10: + print "mbt3a rfss stat rfid %x stid %x ch1 %x ch2 %x" %(rfid, stid, ch1, ch2) + #else: + # print "mbt other %x" % opcode + + def decode_tsbk(self, tsbk): + self.stats['tsbks'] += 1 +# if crc16(tsbk, 12) != 0: +# self.stats['crc'] += 1 +# return # crc check failed + tsbk = tsbk << 16 # for missing crc + opcode = (tsbk >> 88) & 0x3f + if self.debug > 10: + print "TSBK: 0x%02x 0x%024x" % (opcode, tsbk) + if opcode == 0x02: # group voice chan grant update + ch1 = (tsbk >> 64) & 0xffff + ga1 = (tsbk >> 48) & 0xffff + ch2 = (tsbk >> 32) & 0xffff + ga2 = (tsbk >> 16) & 0xffff + if self.frequency_set: + frequency = self.channel_id_to_frequency(ch1) + if frequency: + self.frequency_set(frequency) + if self.debug > 10: + print "tsbk02 grant update: chan %s %d %s %d" %(self.channel_id_to_string(ch1), ga1, self.channel_id_to_string(ch2), ga2) + elif opcode == 0x16: # sndcp data ch + ch1 = (tsbk >> 48) & 0xffff + ch2 = (tsbk >> 32) & 0xffff + if self.debug > 10: + print "tsbk16 sndcp data ch: chan %x %x" %(ch1, ch2) + elif opcode == 0x34: # iden_up vhf uhf + iden = (tsbk >> 76) & 0xf + bwvu = (tsbk >> 72) & 0xf + toff0 = (tsbk >> 58) & 0x3fff + spac = (tsbk >> 48) & 0x3ff + freq = (tsbk >> 16) & 0xffffffff + toff_sign = (toff0 >> 13) & 1 + toff = toff0 & 0x1fff + if toff_sign == 0: + toff = 0 - toff + txt = ["mob Tx-", "mob Tx+"] + self.freq_table[iden] = {} + self.freq_table[iden]['offset'] = toff * spac * 125 + self.freq_table[iden]['step'] = spac * 125 + self.freq_table[iden]['frequency'] = freq * 5 + if self.debug > 10: + print "tsbk34 iden vhf/uhf id %d toff %f spac %f freq %f [%s]" % (iden, toff * spac * 0.125 * 1e-3, spac * 0.125, freq * 0.000005, txt[toff_sign]) + elif opcode == 0x3d: # iden_up + iden = (tsbk >> 76) & 0xf + bw = (tsbk >> 67) & 0x1ff + toff0 = (tsbk >> 58) & 0x1ff + spac = (tsbk >> 48) & 0x3ff + freq = (tsbk >> 16) & 0xffffffff + toff_sign = (toff0 >> 8) & 1 + toff = toff0 & 0xff + if toff_sign == 0: + toff = 0 - toff + txt = ["mob xmit < recv", "mob xmit > recv"] + self.freq_table[iden] = {} + self.freq_table[iden]['offset'] = toff * 250000 + self.freq_table[iden]['step'] = spac * 125 + self.freq_table[iden]['frequency'] = freq * 5 + if self.debug > 10: + print "tsbk3d iden id %d toff %f spac %f freq %f" % (iden, toff * 0.25, spac * 0.125, freq * 0.000005) + elif opcode == 0x3a: # rfss status + syid = (tsbk >> 56) & 0xfff + rfid = (tsbk >> 48) & 0xff + stid = (tsbk >> 40) & 0xff + chan = (tsbk >> 24) & 0xffff + f1 = self.channel_id_to_frequency(chan) + if f1: + self.rfss_syid = syid + self.rfss_rfid = rfid + self.rfss_stid = stid + self.rfss_chan = f1 + self.rfss_txchan = f1 + self.freq_table[chan >> 12]['offset'] + if self.debug > 10: + print "tsbk3a rfss status: syid: %x rfid %x stid %d ch1 %x(%s)" %(syid, rfid, stid, chan, self.channel_id_to_string(chan)) + elif opcode == 0x39: # secondary cc + rfid = (tsbk >> 72) & 0xff + stid = (tsbk >> 64) & 0xff + ch1 = (tsbk >> 48) & 0xffff + ch2 = (tsbk >> 24) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + if f1 and f2: + self.secondary[ f1 ] = 1 + self.secondary[ f2 ] = 1 + if self.debug > 10: + print "tsbk39 secondary cc: rfid %x stid %d ch1 %x(%s) ch2 %x(%s)" %(rfid, stid, ch1, self.channel_id_to_string(ch1), ch2, self.channel_id_to_string(ch2)) + elif opcode == 0x3b: # network status + wacn = (tsbk >> 52) & 0xfffff + syid = (tsbk >> 40) & 0xfff + ch1 = (tsbk >> 24) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + if f1: + self.ns_syid = syid + self.ns_wacn = wacn + self.ns_chan = f1 + if self.debug > 10: + print "tsbk3b net stat: wacn %x syid %x ch1 %x(%s)" %(wacn, syid, ch1, self.channel_id_to_string(ch1)) + elif opcode == 0x3c: # adjacent status + rfid = (tsbk >> 48) & 0xff + stid = (tsbk >> 40) & 0xff + ch1 = (tsbk >> 24) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + if f1: + self.adjacent[f1] = 'rfid: %x stid:%x' % (rfid, stid) + if self.debug > 10: + print "tsbk3c adjacent: rfid %x stid %d ch1 %x(%s)" %(rfid, stid, ch1, self.channel_id_to_string(ch1)) + #else: + # print "tsbk other %x" % opcode + +def main(): + q = 0x3a000012ae01013348704a54 + rc = crc16(q,12) + print "should be zero: %x" % rc + + q = 0x3a001012ae01013348704a54 + rc = crc16(q,12) + print "should be nonzero: %x" % rc + + t = trunked_system(debug=255) + q = 0x3a000012ae0101334870 + t.decode_tsbk(q) + + +if __name__ == '__main__': + main()