diff options
Diffstat (limited to 'op25/gr-op25_repeater/apps/trunking.py')
-rw-r--r-- | op25/gr-op25_repeater/apps/trunking.py | 569 |
1 files changed, 569 insertions, 0 deletions
diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py new file mode 100644 index 0000000..33cd681 --- /dev/null +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -0,0 +1,569 @@ + +# Copyright 2011, 2012, 2013 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. +# +# FIXME: hideously mixes indentation, some is tabs and some is spaces +# + +import time + +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, config=None): + self.debug = debug + 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 + self.voice_frequencies = {} + self.blacklist = {} + self.whitelist = None + self.tgid_map = None + self.offset = 0 + self.sysname = 0 + self.trunk_cc = 0 + if config: + self.blacklist = config['blacklist'] + self.whitelist = config['whitelist'] + self.tgid_map = config['tgid_map'] + self.offset = config['offset'] + self.sysname = config['sysname'] + self.trunk_cc = config['cclist'][0] # TODO: scan thru list + + def to_string(self): + s = [] + s.append('rf: syid %x rfid %d stid %d 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: syid %x wacn %x frequency %f' % ( self.ns_syid, self.ns_wacn, float(self.ns_chan) / 1000000.0)) + s.append('secondary control channel(s): %s' % ','.join(['%f' % (float(k) / 1000000.0) for k in self.secondary.keys()])) + s.append('stats: tsbks %d crc %d' % (self.stats['tsbks'], self.stats['crc'])) + s.append('') + t = time.time() + for f in self.voice_frequencies: + s.append('voice frequency %f tgid %d %4.1fs ago count %d' % (f / 1000000.0, self.voice_frequencies[f]['tgid'], t - self.voice_frequencies[f]['time'], self.voice_frequencies[f]['counter'])) + s.append('') + 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) + for f in self.adjacent: + s.append('adjacent %f: %s' % (float(f) / 1000000.0, self.adjacent[f])) + 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 get_tag(self, tgid): + if not tgid: + return "" + if tgid not in self.tgid_map: + return "Talkgroup ID %d [0x%x]" % (tgid, tgid) + return self.tgid_map[tgid] + + def update_voice_frequency(self, frequency, tgid=None): + if frequency is None: + return + if frequency not in self.voice_frequencies: + self.voice_frequencies[frequency] = {'counter':0} + self.voice_frequencies[frequency]['tgid'] = tgid + self.voice_frequencies[frequency]['counter'] += 1 + self.voice_frequencies[frequency]['time'] = time.time() + + def find_voice_frequency(self, start_time, tgid=None): + for frequency in self.voice_frequencies: + if self.voice_frequencies[frequency]['time'] < start_time: + continue + active_tgid = self.voice_frequencies[frequency]['tgid'] + if active_tgid in self.blacklist: + continue + if self.whitelist and active_tgid not in self.whitelist: + continue + if tgid is None or tgid == active_tgid: + return frequency, active_tgid + return None, None + + def add_blacklist(self, tgid): + if not tgid: + return + self.blacklist[tgid] = 1 + + def decode_mbt_data(self, opcode, header, mbt_data): + if self.debug > 10: + print "decode_mbt_data: %x %x" %(opcode, mbt_data) + if opcode == 0x0: # grp voice channel grant + ch1 = (mbt_data >> 64) & 0xffff + ch2 = (mbt_data >> 48) & 0xffff + ga = (mbt_data >> 32) & 0xffff + if self.debug > 10: + print "mbt00 voice grant ch1 %x ch2 %x addr 0x%x" %(ch1, ch2, ga) + elif opcode == 0x3c: # adjacent status + syid = (header >> 48) & 0xfff + rfid = (header >> 24) & 0xff + stid = (header >> 16) & 0xff + ch1 = (mbt_data >> 80) & 0xffff + ch2 = (mbt_data >> 64) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + if f1 and f2: + self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f' % (rfid, stid, f2 / 1000000.0) + if self.debug > 10: + print "mbt3c adjacent sys %x rfid %x stid %x ch1 %x ch2 %x f1 %s f2 %s" %(syid, rfid, stid, ch1, ch2, self.channel_id_to_string(ch1), self.channel_id_to_string(ch2)) + elif opcode == 0x3b: # network status + syid = (header >> 48) & 0xfff + wacn = (mbt_data >> 76) & 0xfffff + ch1 = (mbt_data >> 56) & 0xffff + ch2 = (mbt_data >> 40) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + if f1 and f2: + self.ns_syid = syid + self.ns_wacn = wacn + self.ns_chan = f1 + if self.debug > 10: + print "mbt3b net stat sys %x wacn %x ch1 %s ch2 %s" %(syid, wacn, self.channel_id_to_string(ch1), self.channel_id_to_string(ch2)) + elif opcode == 0x3a: # rfss status + syid = (header >> 48) & 0xfff + rfid = (mbt_data >> 88) & 0xff + stid = (mbt_data >> 80) & 0xff + ch1 = (mbt_data >> 64) & 0xffff + ch2 = (mbt_data >> 48) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + if f1 and f2: + self.rfss_syid = syid + self.rfss_rfid = rfid + self.rfss_stid = stid + self.rfss_chan = f1 + self.rfss_txchan = f2 + if self.debug > 10: + print "mbt3a rfss stat sys %x rfid %x stid %x ch1 %s ch2 %s" %(syid, rfid, stid, self.channel_id_to_string(ch1), self.channel_id_to_string(ch2)) + #else: + # print "mbt other %x" % opcode + + def decode_tsbk(self, tsbk): + self.stats['tsbks'] += 1 + updated = 0 +# 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 + mfrid = (tsbk >> 80) & 0xff + ch1 = (tsbk >> 64) & 0xffff + ga1 = (tsbk >> 48) & 0xffff + ch2 = (tsbk >> 32) & 0xffff + ga2 = (tsbk >> 16) & 0xffff + if mfrid == 0x90: + if self.debug > 10: + ch1 = (tsbk >> 56) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + if f1 == None: f1 = 0 + print "tsbk02[90] %x %f" % (ch1, f1 / 1000000.0) + else: + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + self.update_voice_frequency(f1, tgid=ga1) + if f1 != f2: + self.update_voice_frequency(f2, tgid=ga2) + if f1: + updated += 1 + if f2: + updated += 1 + 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 + table = (ch1 >> 12) & 0xf + f1 = self.channel_id_to_frequency(ch1) + if f1 and table in self.freq_table: + self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f tbl:%d' % (rfid, stid, (f1 + self.freq_table[table]['offset']) / 1000000.0, table) + if self.debug > 10: + print "tsbk3c adjacent: rfid %x stid %d ch1 %x(%s)" %(rfid, stid, ch1, self.channel_id_to_string(ch1)) + if table in self.freq_table: + print "tsbk3c : %s %s" % (self.freq_table[table]['frequency'] , self.freq_table[table]['step'] ) + #else: + # print "tsbk other %x" % opcode + return updated + + +class rx_ctl (object): + def __init__(self, debug=0, frequency_set=None, conf_file=None): + class _states(object): + ACQ = 0 + CC = 1 + TO_VC = 2 + VC = 3 + self.states = _states + + self.state = self.states.CC + self.trunked_systems = {} + self.frequency_set = frequency_set + self.debug = debug + self.tgid_hold = None + self.tgid_hold_until = time.time() + self.TGID_HOLD_TIME = 2.0 # TODO: make more configurable + self.current_nac = None + self.current_id = 0 + self.TSYS_HOLD_TIME = 3.0 # TODO: make more configurable + self.wait_until = time.time() + self.configs = {} + + if conf_file: + self.build_config(conf_file) + self.nacs = self.configs.keys() + self.current_nac = self.nacs[0] + self.current_state = self.states.CC + + def set_frequency(self, params): + frequency = params['freq'] + if frequency and self.frequency_set: + self.frequency_set(params) + + def add_trunked_system(self, nac): + assert nac not in self.trunked_systems # duplicate nac not allowed + blacklist = {} + whitelist = None + tgid_map = {} + cfg = None + if nac in self.configs: + cfg = self.configs[nac] + self.trunked_systems[nac] = trunked_system(debug = self.debug, config=cfg) + + def build_config(self, config_filename): + import ConfigParser + config = ConfigParser.ConfigParser() + config.read(config_filename) + configs = {} + for section in config.sections(): + nac = int(config.get(section, 'nac'), 0) # nac required + assert nac != 0 # nac=0 not allowed + assert nac not in configs # duplicate nac not allowed + configs[nac] = {} + for option in config.options(section): + configs[nac][option] = config.get(section, option).lower() + configs[nac]['sysname'] = section + + for nac in configs: + self.configs[nac] = {'cclist':[], 'offset':0, 'whitelist':None, 'blacklist':{}, 'tgid_map':{}, 'sysname': configs[nac]['sysname']} + for f in configs[nac]['control_channel_list'].split(','): + if f.find('.') == -1: # assume in Hz + self.configs[nac]['cclist'].append(int(f)) + else: # assume in MHz due to '.' + self.configs[nac]['cclist'].append(int(float(f) * 1000000)) + if 'offset' in configs[nac]: + self.configs[nac]['offset'] = int(configs[nac]['offset']) + if 'modulation' in configs[nac]: + self.configs[nac]['modulation'] = configs[nac]['modulation'] + else: + self.configs[nac]['modulation'] = 'cqpsk' + if 'whitelist' in configs[nac]: + self.configs[nac]['whitelist'] = dict.fromkeys([int(d) for d in configs[nac]['whitelist'].split(',')]) + if 'blacklist' in configs[nac]: + self.configs[nac]['blacklist'] = dict.fromkeys([int(d) for d in configs[nac]['blacklist'].split(',')]) + if 'tgid_tags_file' in configs[nac]: + import csv + with open(configs[nac]['tgid_tags_file'], 'rb') as csvfile: + sreader = csv.reader(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL) + for row in sreader: + tgid = int(row[0]) + txt = row[1] + self.configs[nac]['tgid_map'][tgid] = txt + + self.add_trunked_system(nac) + + def find_next_tsys(self): + self.current_id += 1 + if self.current_id >= len(self.nacs): + self.current_id = 0 + return self.nacs[self.current_id] + + def to_string(self): + s = '' + for nac in self.trunked_systems: + s += '\n====== NAC 0x%x ====== %s ======\n' % (nac, self.trunked_systems[nac].sysname) + s += self.trunked_systems[nac].to_string() + return s + + def process_qmsg(self, msg): + type = msg.type() + updated = 0 + curr_time = time.time() + if type == -2: + cmd = msg.to_string() + if self.debug > 10: + print "process_qmsg: command: %s" % cmd + self.update_state(cmd, curr_time) + return + elif type == -1: + print "process_data_unit timeout" + self.update_state('timeout', curr_time) + return + s = msg.to_string() + # nac is always 1st two bytes + nac = (ord(s[0]) << 8) + ord(s[1]) + s = s[2:] + if self.debug > 10: + print "nac %x type %d at %f state %d len %d" %(nac, type, time.time(), self.state, len(s)) + if (type == 7 or type == 12) and nac not in self.trunked_systems: + if not self.configs: + # TODO: allow whitelist/blacklist rather than blind automatic-add + self.add_trunked_system(nac) + else: + return + if type == 7: # trunk: TSBK + t = 0 + for c in s: + t = (t << 8) + ord(c) + updated += self.trunked_systems[nac].decode_tsbk(t) + elif type == 12: # trunk: MBT + s1 = s[:10] + s2 = s[10:] + header = mbt_data = 0 + for c in s1: + header = (header << 8) + ord(c) + for c in s2: + mbt_data = (mbt_data << 8) + ord(c) + opcode = (header >> 16) & 0x3f + if self.debug > 10: + print "type %d at %f state %d len %d/%d opcode %x [%x/%x]" %(type, time.time(), self.state, len(s1), len(s2), opcode, header,mbt_data) + self.trunked_systems[nac].decode_mbt_data(opcode, header << 16, mbt_data << 32) + + if nac != self.current_nac: + return + + if updated: + self.update_state('update', curr_time) + else: + self.update_state('duid%d' % type, curr_time) + + def update_state(self, command, curr_time): + if not self.configs: + return # run in "manual mode" if no conf + + nac = self.current_nac + tsys = self.trunked_systems[nac] + + new_frequency = None + new_tgid = None + new_state = None + new_nac = None + + if command == 'timeout' or command == 'duid15': + if self.current_state != self.states.CC: + new_state = self.states.CC + new_frequency = tsys.trunk_cc + elif command == 'update': + if self.current_state == self.states.CC: + desired_tgid = None + if self.tgid_hold_until > curr_time: + desired_tgid = self.tgid_hold + new_frequency, new_tgid = tsys.find_voice_frequency(curr_time, tgid=desired_tgid) + if new_frequency: + new_state = self.states.TO_VC + self.current_tgid = new_tgid + elif command == 'duid3': + if self.current_state != self.states.CC: + new_state = self.states.CC + new_frequency = tsys.trunk_cc + elif command == 'duid0' or command == 'duid5' or command == 'duid10': + if self.state == self.states.TO_VC: + new_state = self.states.VC + self.tgid_hold = self.current_tgid + self.tgid_hold_until = max(curr_time + self.TGID_HOLD_TIME, self.tgid_hold_until) + self.wait_until = curr_time + self.TSYS_HOLD_TIME + elif command == 'duid7' or command == 'duid12': + pass + elif command == 'set_hold': + if self.current_tgid: + self.tgid_hold = self.current_tgid + self.tgid_hold_until = curr_time + 86400 * 10000 + print 'set hold until %f' % self.tgid_hold_until + elif command == 'unset_hold': + if self.current_tgid: + self.current_tgid = None + self.tgid_hold = None + self.tgid_hold_until = curr_time + elif command == 'skip': + pass # TODO + elif command == 'lockout': + if self.current_tgid: + tsys.add_blacklist(self.current_tgid) + self.current_tgid = None + self.tgid_hold = None + self.tgid_hold_until = curr_time + if self.current_state != self.states.CC: + new_state = self.states.CC + new_frequency = tsys.trunk_cc + else: + print 'update_state: unknown command: %s\n' % command + assert 0 == 1 + + if self.wait_until <= curr_time and self.tgid_hold_until <= curr_time: + self.wait_until = curr_time + self.TSYS_HOLD_TIME + new_nac = self.find_next_tsys() + + if new_nac: + nac = self.current_nac = new_nac + tsys = self.trunked_systems[nac] + new_frequency = tsys.trunk_cc + self.current_tgid = None + + if new_frequency: + self.set_frequency({'freq': new_frequency, 'tgid': self.current_tgid, 'offset': tsys.offset, 'tag': tsys.get_tag(self.current_tgid), 'nac': nac, 'system': tsys.sysname}) + + if new_state: + self.current_state = new_state + +def main(): + q = 0x3a000012ae01013348704a54 + rc = crc16(q,12) + print "should be zero: %x" % rc + assert rc == 0 + + q = 0x3a001012ae01013348704a54 + rc = crc16(q,12) + print "should be nonzero: %x" % rc + assert rc != 0 + + t = trunked_system(debug=255) + q = 0x3a000012ae0101334870 + t.decode_tsbk(q) + + q = 0x02900031210020018e7c + t.decode_tsbk(q) + +if __name__ == '__main__': + main() |