From 942e26348ba261617fbf51356de04eb3d477a93d Mon Sep 17 00:00:00 2001 From: Steve N4IRS Date: Tue, 20 Jun 2017 08:40:28 -0400 Subject: [PATCH] Use programs in ipsc directory and dmr_utils --- IPSC_Bridge.py | 38 +- ambe_bridge.py | 701 ------------------------------------ ambe_utils.py | 279 -------------- dmrlink.py | 212 ++++++----- dmrlink_SAMPLE.cfg | 36 +- dmrlink_config.py | 221 ------------ dmrlink_log.py | 87 ----- mk-dmrlink | 35 +- systemd/ambe_audio.service | 17 - systemd/ipsc_bridge.service | 17 + 10 files changed, 201 insertions(+), 1442 deletions(-) delete mode 100644 ambe_bridge.py delete mode 100644 ambe_utils.py delete mode 100755 dmrlink_config.py delete mode 100755 dmrlink_log.py delete mode 100644 systemd/ambe_audio.service create mode 100644 systemd/ipsc_bridge.service diff --git a/IPSC_Bridge.py b/IPSC_Bridge.py index 413dd8d..e06c2a5 100644 --- a/IPSC_Bridge.py +++ b/IPSC_Bridge.py @@ -24,6 +24,8 @@ # frames and metadata to an external program/network. It also knows how to import # AMBE and metadata from an external network and send the DMR frames to IPSC networks. +##################################################################################################### + from __future__ import print_function from twisted.internet import reactor from binascii import b2a_hex as h @@ -32,15 +34,15 @@ from bitstring import BitArray import sys, socket, ConfigParser, thread, traceback import cPickle as pickle -from dmrlink import IPSC, systems +from dmrlink import IPSC, systems, config_reports, reportFactory from dmr_utils.utils import int_id, hex_str_3, hex_str_4, get_alias, get_info from time import time, sleep, clock, localtime, strftime import csv import struct from random import randint -import ambe_utils -from ambe_bridge import AMBE_IPSC +from dmr_utils import ambe_utils +from dmr_utils.ambe_bridge import AMBE_IPSC __author__ = 'Cortney T. Buffington, N0MJS' __copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group' @@ -71,7 +73,6 @@ class ambeIPSC(IPSC): _debug = False # Debug output for each VOICE frame _outToFile = False # Write each AMBE frame to a file called ambe.bin _outToUDP = True # Send each AMBE frame to the _sock object (turn on/off Analog_Bridge operation) - #_gateway = "192.168.1.184" _gateway = "127.0.0.1" # IP address of app _gateway_port = 31000 # Port Analog_Bridge is listening on for AMBE frames to decode _remote_control_port = 31002 # Port that ambe_audio is listening on for remote control commands @@ -95,13 +96,13 @@ class ambeIPSC(IPSC): _dmrgui = '' cc = 1 ipsc_seq = 0 - + ###### DEBUGDEBUGDEBUG #_d = None ###### DEBUGDEBUGDEBUG - def __init__(self, _name, _config, _logger): - IPSC.__init__(self, _name, _config, _logger) + def __init__(self, _name, _config, _logger, _report): + IPSC.__init__(self, _name, _config, _logger, _report) self.CALL_DATA = [] # @@ -115,6 +116,7 @@ class ambeIPSC(IPSC): logger.info('DMRLink IPSC Bridge') if self._gateway_dmr_id == 0: sys.exit( "Error: gatewayDmrId must be set (greater than zero)" ) + # # Open output sincs # @@ -159,7 +161,7 @@ class ambeIPSC(IPSC): if sec == None: sec = self.defaultOption(config, 'DEFAULTS', 'section', networkName) if config.has_section(sec) == False: - logger.error('Section ' + sec + ' was not found, using DEFAULTS') + logger.info('Section ' + sec + ' was not found, using DEFAULTS') sec = 'DEFAULTS' self._debug = bool(self.defaultOption(config, sec,'debug', self._debug) == 'True') self._outToFile = bool(self.defaultOption(config, sec,'outToFile', self._outToFile) == 'True') @@ -272,9 +274,9 @@ if __name__ == '__main__': import sys import signal from dmr_utils.utils import try_download, mk_id_dict - - import dmrlink_log - import dmrlink_config + + from ipsc.dmrlink_log import config_logging + from ipsc.dmrlink_config import build_config # Change the current directory to the location of the application os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) @@ -290,14 +292,14 @@ if __name__ == '__main__': cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg' # Call the external routine to build the configuration dictionary - CONFIG = dmrlink_config.build_config(cli_args.CFG_FILE) + CONFIG = build_config(cli_args.CFG_FILE) # Call the external routing to start the system logger if cli_args.LOG_LEVEL: CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL if cli_args.LOG_HANDLERS: CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS - logger = dmrlink_log.config_logging(CONFIG['LOGGER']) + logger = config_logging(CONFIG['LOGGER']) logger.info('DMRlink \'IPSC_Bridge.py\' (c) 2015 N0MJS & the K0USY Group - SYSTEM STARTING...') logger.info('Version %s', __version__) @@ -339,12 +341,16 @@ if __name__ == '__main__': # Set signal handers so that we can gracefully exit if need be for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]: signal.signal(sig, sig_handler) - - + + # INITIALIZE THE REPORTING LOOP + report_server = config_reports(CONFIG, logger, reportFactory) + # INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGUED IPSC for system in CONFIG['SYSTEMS']: if CONFIG['SYSTEMS'][system]['LOCAL']['ENABLED']: - systems[system] = ambeIPSC(system, CONFIG, logger) + systems[system] = ambeIPSC(system, CONFIG, logger, report_server) reactor.listenUDP(CONFIG['SYSTEMS'][system]['LOCAL']['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['LOCAL']['IP']) reactor.run() + + diff --git a/ambe_bridge.py b/ambe_bridge.py deleted file mode 100644 index 9cd9df7..0000000 --- a/ambe_bridge.py +++ /dev/null @@ -1,701 +0,0 @@ -#!/usr/bin/env python -# -############################################################################### -# Copyright (C) 2017 Mike Zingman N4IRR -# -# This program 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 of the License, or -# (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -############################################################################### - -''' -''' - -from __future__ import print_function - -# Python modules we need -import sys -from bitarray import bitarray -from bitstring import BitArray -from bitstring import BitString -import struct -from time import time, sleep -from importlib import import_module -from binascii import b2a_hex as ahex -from random import randint -import sys, socket, ConfigParser, thread, traceback -from threading import Lock -from time import time, sleep, clock, localtime, strftime - -# Twisted is pretty important, so I keep it separate -from twisted.internet.protocol import DatagramProtocol -from twisted.internet import reactor -from twisted.internet import task - -# Things we import from the main hblink module -from dmr_utils.utils import hex_str_3, hex_str_4, int_id, get_alias -from dmr_utils import decode, bptc, const, golay, qr -import ambe_utils - -# Does anybody read this stuff? There's a PEP somewhere that says I should do this. -__author__ = 'Mike Zingman, N4IRR and Cortney T. Buffington, N0MJS' -__copyright__ = 'Copyright (c) 2017 Mike Zingman N4IRR' -__credits__ = 'Cortney T. Buffington, N0MJS; Colin Durbridge, G4EML, Steve Zingman, N4IRS; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT' -__license__ = 'GNU GPLv3' -__maintainer__ = 'Cort Buffington, N0MJS' -__email__ = 'n0mjs@me.com' -__status__ = 'pre-alpha' -__version__ = '20170529' - - -''' -Take ambe from external source (ASL or IPSC) and import it into an HB network -Take ambe from HB network and export it to a foreign network (ie IPSC or ASL) -Need to support both slots. This means segmenting the data structures using slot based keys -Every slot should remember its TG, slot, cc, source ID, destination ID and repeater ID -Export should just pass through metadata unless a rule is found which could change the TG or slot being idetified. -Import should use the current metadata (last seen) for a slot untill it sees a new set - -The app can be configured as a master: - This is useful when connecting a MMDVM repeater or hotspot to the network - Configure the MMDVMHost to point to this instance -Or a peer on an existing master - This is useful when connecting to Brandmeister, DMRPlus or an existing HB network. - Use this when you want to share your IPSC repeater on an HB network - USe this when you want to use dongle mode to access Brandmeister or any HB nework - -Import: - Wait for metadata from external network - Once seen, set up slot based values for source, destination and repeater IDs, color code - For each AMBE packet from that foreign source, read the data and construct DMR and HB structures around the new metadata - Send the HB packet to the network -Export - For each session, construct a metadata packet to pass to the foreign repeater with source, destination, repeater IDs, slot and CC - Send AMBE to the foreign reprater over UDP (decorated with slot) - At end of session signal termination to the foreign repeater - -Translation of TG/Slot information - Used when - local and foreigh repeaters do not have same mapping - Need to block export or import of a specific TG - DMO where only one slot is supported (map import to slot 2, export to foreign specs) - -''' - -############################################################################################################ -# Constants -############################################################################################################ -DMR_DATA_SYNC_MS = '\xD5\xD7\xF7\x7F\xD7\x57' -DMR_VOICE_SYNC_MS = '0x7F7D5DD57DFD' - -# TLV tag definitions -TAG_BEGIN_TX = 0 # Begin transmission with optional metadata -TAG_AMBE = 1 # AMBE frame to transmit (old tag now uses 49 or 72) -TAG_END_TX = 2 # End transmission, close session -TAG_TG_TUNE = 3 # Send blank start/end frames to network for a specific talk group -TAG_PLAY_AMBE = 4 # Play an AMBE file -TAG_REMOTE_CMD = 5 # SubCommand for remote configuration -TAG_AMBE_49 = 6 # AMBE frame of 49 bit samples (IPSC) -TAG_AMBE_72 = 7 # AMBE frame of 72 bit samples (HB) -TAG_SET_INFO = 8 # Set DMR Info for slot - -# Burst Data Types -BURST_DATA_TYPE = { - 'VOICE_HEAD': '\x01', - 'VOICE_TERM': '\x02', - 'SLOT1_VOICE': '\x0A', - 'SLOT2_VOICE': '\x8A' -} - -############################################################################################################ -# Globals -############################################################################################################ -''' - Flag bits - SGTT NNNN S = Slot (0 = slot 1, 1 = slot 2) - G = Group call = 0, Private = 1 - T = Type (Voice = 00, Data Sync = 10, ,Voice Sync = 01, Unused = 11) - NNNN = Sequence Number or data type (from slot type) -''' -header_flag = lambda _slot: (0xA0 if (_slot == 2) else 0x20) | ord(const.DMR_SLT_VHEAD) -terminator_flag = lambda _slot: (0xA0 if (_slot == 2) else 0x20) | ord(const.DMR_SLT_VTERM) -voice_flag = lambda _slot, _vf: (0x80 if (_slot == 2) else 0) | (0x10 if (_vf == 0) else 0) | _vf - -############################################################################################################ -# Classes -############################################################################################################ -class SLOT: - def __init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc): - self.rf_src = hex_str_3(_rf_src) # DMR ID of sender - self.dst_id = hex_str_3(_dst_id) # Talk group to send to - self.repeater_id = hex_str_4(_repeater_id) # Repeater ID - self.slot = _slot # Slot to use - self.cc = _cc # Color code to use - self.type = 0 # 1=voice header, 2=voice terminator; voice, 0=burst A ... 5=burst F - self.stream_id = hex_str_4(0) # Stream id is same across a single session - self.frame_count = 0 # Count of frames in a session - self.start_time = 0 # Start of session - self.time = 0 # Current time in session. Used to calculate duration - -class RX_SLOT(SLOT): - def __init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc): - SLOT.__init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc) - self.vf = 0 # Voice Frame (A-F in DMR spec) - self.seq = 0 # Incrementing sequence number for each DMR frame - self.emblc = [None] * 6 # Storage for embedded LC - -class TX_SLOT(SLOT): - def __init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc): - SLOT.__init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc) - self.lastSeq = 0 # Used to look for gaps in seq numbers - self.lostFrame = 0 # Number of lost frames in a single session - -class AMBE_BASE: - def __init__(self, _parent, _name, _config, _logger, _port): - self._parent = _parent - self._logger = _logger - self._config = _config - self._system = _name - - self._gateways = [(self._parent._gateway, self._parent._gateway_port)] - self._ambeRxPort = _port # Port to listen on for AMBE frames to transmit to all peers - self._dmrgui = '127.0.0.1' - - self._sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) - - self._slot = 2 # "current slot" - self.rx = [0, RX_SLOT(1, 0, 0, 0, 1), RX_SLOT(2, 0, 0, 0, 1)] - self.tx = [0, TX_SLOT(1, 0, 0, 0, 1), TX_SLOT(2, 0, 0, 0, 1)] - - class UDP_IMPORT(DatagramProtocol): - def __init__(self, callback_function): - self.func = callback_function - def datagramReceived(self, _data, (_host, _port)): - self.func(_data, (_host, _port)) - - self.udp_port = reactor.listenUDP(self._ambeRxPort, UDP_IMPORT(self.import_datagramReceived)) - - pass - def stop_listening(self): - self.udp_port.stopListening() - def send_voice_header(self, _rx_slot): - _rx_slot.vf = 0 # voice frame (A-F) - _rx_slot.seq = 0 # Starts at zero for each incoming transmission, wraps back to zero when 256 is reached. - _rx_slot.frame_count = 0 # Number of voice frames in this session (will be greater than zero of header is sent) - def send_voice72(self, _rx_slot, _ambe): - pass - def send_voice49(self, _rx_slot, _ambe): - pass - def send_voice_term(self, _rx_slot): - pass - # Play the contents of a AMBE file to all peers. This function is expected to be launched from a thread - def play_ambe_file(self, _fileName, _rx_slot): - try: - self._logger.info('(%s) play_ambe_file: %s', self._system, _fileName) - _file = open(_fileName, 'r') - _strSlot = struct.pack("I",_rx_slot.slot)[0] - metadata = ahex(_rx_slot.rf_src[0:3]) + ahex(_rx_slot.repeater_id[0:4]) + ahex(_rx_slot.dst_id[0:3]) + ('%02x' % _rx_slot.slot) + ('%02x' % _rx_slot.cc) - - self._sock.sendto(bytearray.fromhex('000C'+metadata), ('127.0.0.1', self._ambeRxPort)) # begin transmission TLV - _notEOF = True - while (_notEOF): - _data = _file.read(27) - if (_data): - self._sock.sendto(bytearray.fromhex('071C')+_strSlot+_data, ('127.0.0.1', self._ambeRxPort)) # send AMBE72 - sleep(0.06) - else: - _notEOF = False - self._sock.sendto(bytearray.fromhex('0201')+_strSlot, ('127.0.0.1', self._ambeRxPort)) # end transmission TLV - _file.close() - self._logger.info('(%s) File playback done', self._system) - except: - self._logger.error('(%s) file %s not found', self._system, _fileName) - traceback.print_exc() - # TG selection, send a simple blank voice frame to network - def sendBlankAmbe(self, _rx_slot, _stream_id): - _rx_slot.stream_id = _stream_id - self.send_voice_header(_rx_slot) - silence = '\xAC\AA\x40\x20\x00\x44\x40\x80\x80' - self.send_voice72(_rx_slot, silence+silence+silence) - self.send_voice_term(_rx_slot) - # Twisted callback with data from socket - def import_datagramReceived(self, _data, (_host, _port)): - subscriber_ids, talkgroup_ids, peer_ids = self._parent.get_globals() - self._logger.debug('(%s) import_datagramReceived', self._system) - _slot = self._slot - _rx_slot = self.rx[_slot] - - # Parse out the TLV - t = _data[0] - if (t): - l = _data[1] - if (l): - v = _data[2:] - if (v): - t = ord(t) - if (t == TAG_BEGIN_TX) or (t == TAG_SET_INFO): - - if ord(l) > 1: - _slot = int_id(v[10:11]) - _rx_slot = self.rx[_slot] - _rx_slot.slot = _slot - _rx_slot.rf_src = hex_str_3(int_id(v[0:3])) - _rx_slot.repeater_id = self._parent.get_repeater_id( hex_str_4(int_id(v[3:7])) ) - _rx_slot.dst_id = hex_str_3(int_id(v[7:10])) - _rx_slot.cc = int_id(v[11:12]) - - if t == TAG_BEGIN_TX: - _rx_slot.stream_id = hex_str_4(randint(0,0xFFFFFFFF)) # Every stream has a unique ID - self._logger.info('(%s) Begin AMBE encode STREAM ID: %s SUB: %s (%s) REPEATER: %s (%s) TGID %s (%s), TS %s', \ - self._system, int_id(_rx_slot.stream_id), get_alias(_rx_slot.rf_src, subscriber_ids), int_id(_rx_slot.rf_src), get_alias(_rx_slot.repeater_id, peer_ids), int_id(_rx_slot.repeater_id), get_alias(_rx_slot.dst_id, talkgroup_ids), int_id(_rx_slot.dst_id), _slot) - self.send_voice_header(_rx_slot) - else: - self._logger.info('(%s) Set DMR Info SUB: %s (%s) REPEATER: %s (%s) TGID %s (%s), TS %s', \ - self._system, get_alias(_rx_slot.rf_src, subscriber_ids), int_id(_rx_slot.rf_src), get_alias(_rx_slot.repeater_id, peer_ids), int_id(_rx_slot.repeater_id), get_alias(_rx_slot.dst_id, talkgroup_ids), int_id(_rx_slot.dst_id), _slot) - elif ((t == TAG_AMBE) or (t == TAG_AMBE_72)): # generic AMBE or specific AMBE72 - _slot = int_id(v[0]) - _rx_slot = self.rx[_slot] - if _rx_slot.frame_count > 0: - self.send_voice72(_rx_slot, v[1:]) - elif (t == TAG_AMBE_49): # AMBE49 - _slot = int_id(v[0]) - _rx_slot = self.rx[_slot] - if _rx_slot.frame_count > 0: - self.send_voice49(_rx_slot, v[1:]) - elif (t == TAG_END_TX): - _slot = int_id(v[0]) - _rx_slot = self.rx[_slot] - if _rx_slot.frame_count > 0: - self.send_voice_term(_rx_slot) - self._logger.debug('(%s) End AMBE encode STREAM ID: %d FRAMES: %d', self._system, int_id(_rx_slot.stream_id), _rx_slot.frame_count) - _rx_slot.frame_count = 0 # set it back to zero so any random AMBE frames are ignored. - elif (t == TAG_TG_TUNE): - _rx_slot.dst_id = hex_str_3(int(v.split('=')[1])) - self._logger.info('(%s) New txTg = %d on Slot %d', self._system, int_id(_rx_slot.dst_id), _rx_slot.slot) - self.sendBlankAmbe(_rx_slot, hex_str_4(randint(0,0xFFFFFFFF))) - elif (t == TAG_PLAY_AMBE): - thread.start_new_thread( self.play_ambe_file, (v.split('=')[1], _rx_slot) ) - elif (t == TAG_REMOTE_CMD): - _tmp = v.split(None)[0] #first get rid of whitespace - _cmd = _tmp.split('=')[0] - if _cmd == "foobar": - pass - elif _cmd == 'get_info': # get section name, repeater ID, subscriber ID, subscriber callsign - self._sock.sendto('reply dmr_info {} {} {} {}'.format(self._system, - int_id(_rx_slot.repeater_id), - int_id(_rx_slot.rf_src), - get_alias(_rx_slot.rf_src, subscriber_ids)), (self._dmrgui, 34003)) - elif _cmd == 'section': # set current section to argument passed - pass - elif _cmd == 'tgs': # set current rx talkgroups to argument - pass - elif _cmd == 'txTg': # set current transmit talkgroup to argument - _rx_slot.dst_id = hex_str_3(int(v.split('=')[1])) - self._logger.info('(%s) New txTg = %d on Slot %d', self._system, int_id(_rx_slot.dst_id), _rx_slot.slot) - self.sendBlankAmbe(_rx_slot, hex_str_4(randint(0,0xFFFFFFFF))) - elif _cmd == 'txTs': # set current slot to passed argument - self._slot = int(v.split('=')[1]) - elif _cmd == 'gateway_dmr_id': - id = int(v.split('=')[1]) - _rx_slot.repeater_id = hex_str_4(id) - elif _cmd == 'gateway_peer_id': - id = int(v.split('=')[1]) - _rx_slot.rf_src = hex_str_3(id) - - else: - self._logger.info('(%s) unknown remote command: %s', self._system, v) - - else: - self._logger.info('(%s) unknown TLV t=%d, l=%d, v=%s (%s)', self._system, t, ord(l), ahex(v), v) - else: - self._logger.info('(%s) EOF on UDP stream', self._system) - - # Begin export call to partner - def begin_call(self, _slot, _src_id, _dst_id, _repeater_id, _cc, _seq, _stream_id): - subscriber_ids, talkgroup_ids, peer_ids = self._parent.get_globals() - _src_alias = get_alias(_src_id, subscriber_ids) - metadata = _src_id[0:3] + _repeater_id[0:4] + _dst_id[0:3] + struct.pack("b", _slot) + struct.pack("b", _cc) - self.send_tlv(TAG_BEGIN_TX, metadata) # start transmission - self._sock.sendto('reply log2 {} {}'.format(_src_alias, int_id(_dst_id)), (self._dmrgui, 34003)) - self._logger.info('Voice Transmission Start on TS {} and TG {} ({}) from {}'.format(_slot, get_alias(_dst_id, talkgroup_ids), int_id(_dst_id), _src_alias)) - - _tx_slot = self.tx[_slot] - _tx_slot.slot = _slot - _tx_slot.rf_src = _src_id - _tx_slot.repeater_id = _repeater_id - _tx_slot.dst_id = _dst_id - _tx_slot.cc = _cc - _tx_slot.stream_id = _stream_id - - _tx_slot.start_time = time() - _tx_slot.frame_count = 0 - _tx_slot.lostFrame = 0 - _tx_slot.lastSeq = _seq - - # Export voice frame to partner (actually done in sub classes for 49 or 72 bits) - def export_voice(self, _tx_slot, _seq, _ambe): - if _seq != (_tx_slot.lastSeq+1): - _tx_slot.lostFrame += 1 - _tx_slot.lastSeq = _seq - - # End export call to partner - def end_call(self, _tx_slot): - subscriber_ids, talkgroup_ids, peer_ids = self._parent.get_globals() - self.send_tlv(TAG_END_TX, struct.pack("b",_tx_slot.slot)) # end transmission - call_duration = time() - _tx_slot.start_time - _lost_percentage = ((_tx_slot.lostFrame / float(_tx_slot.frame_count)) * 100.0) if _tx_slot.frame_count > 0 else 0.0 - self._sock.sendto("reply log" + - strftime(" %m/%d/%y %H:%M:%S", localtime(_tx_slot.start_time)) + - ' {} {} "{}"'.format(get_alias(_tx_slot.rf_src, subscriber_ids), _tx_slot.slot, get_alias(_tx_slot.dst_id, talkgroup_ids)) + - ' {:.2f}%'.format(_lost_percentage) + - ' {:.2f}s'.format(call_duration), (self._dmrgui, 34003)) - self._logger.info('Voice Transmission End {:.2f} seconds loss rate: {:.2f}% ({}/{})'.format(call_duration, _lost_percentage, _tx_slot.frame_count - _tx_slot.lostFrame, _tx_slot.frame_count)) - def send_tlv(self, _tag, _value): - _tlv = struct.pack("bb", _tag, len(_value)) + _value - for _gateway in self._gateways: - self._sock.sendto(_tlv, _gateway) - -class AMBE_HB(AMBE_BASE): - def __init__(self, _parent, _name, _config, _logger, _port): - AMBE_BASE.__init__(self, _parent, _name, _config, _logger, _port) - - self.lcss = [ - 0b11111111, # not used (place holder) - 0b01, # First fragment - 0b11, # Continuation fragment - 0b11, # Continuation fragment - 0b10, # Last fragment - 0b00 # Null message - ] - self._DMOStreamID = 0 - - def send_voice_header(self, _rx_slot): - AMBE_BASE.send_voice_header(self, _rx_slot) - flag = header_flag(_rx_slot.slot) # DT_VOICE_LC_HEADER - dmr = self.encode_voice_header( _rx_slot ) - for j in range(0,2): - self.send_frameTo_system(_rx_slot, flag, dmr) - sleep(0.06) - def send_voice72(self, _rx_slot, _ambe): - flag = voice_flag(_rx_slot.slot, _rx_slot.vf) # calc flag value - _new_frame = self.encode_voice( BitArray('0x'+ahex(_ambe)), _rx_slot ) # Construct the dmr frame from AMBE(108 bits) + sync/CACH (48 bits) + AMBE(108 bits) - self.send_frameTo_system(_rx_slot, flag, _new_frame.tobytes()) - _rx_slot.vf = (_rx_slot.vf + 1) % 6 # the voice frame counter which is always mod 6 - def send_voice49(self, _rx_slot, _ambe): - ambe49_1 = BitArray('0x' + ahex(_ambe[0:7]))[0:49] - ambe49_2 = BitArray('0x' + ahex(_ambe[7:14]))[0:49] - ambe49_3 = BitArray('0x' + ahex(_ambe[14:21]))[0:49] - - ambe72_1 = ambe_utils.convert49BitTo72BitAMBE(ambe49_1) - ambe72_2 = ambe_utils.convert49BitTo72BitAMBE(ambe49_2) - ambe72_3 = ambe_utils.convert49BitTo72BitAMBE(ambe49_3) - - v = ambe72_1 + ambe72_2 + ambe72_3 - self.send_voice72(_rx_slot, v) - def send_voice_term(self, _rx_slot): - flag = terminator_flag(_rx_slot.slot) # DT_TERMINATOR_WITH_LC - dmr = self.encode_voice_term( _rx_slot ) - self.send_frameTo_system(_rx_slot, flag, dmr) - - # Construct DMR frame, HB header and send result to all peers on network - def send_frameTo_system(self, _rx_slot, _flag, _dmr_frame): - frame = self.make_dmrd(_rx_slot.seq, _rx_slot.rf_src, _rx_slot.dst_id, _rx_slot.repeater_id, _flag, _rx_slot.stream_id, _dmr_frame) # Make the HB frame, ready to send - self.send_system( _rx_slot, frame ) # Send the frame to all peers or master - _rx_slot.seq += 1 # Convienent place for this increment - _rx_slot.frame_count += 1 # update count (used for stats and to make sure header was sent) - - # Override the super class because (1) DMO must be placed on slot 2 and (2) repeater_id must be the ID of the client (TODO) - def send_system(self, _rx_slot, _frame): - if hasattr(self._parent, '_clients'): - _orig_flag = _frame[15] # Save off the flag since _frame is a reference - for _client in self._parent._clients: - _clientDict = self._parent._clients[_client] - if _clientDict['TX_FREQ'] == _clientDict['RX_FREQ']: - - if self._DMOStreamID == 0: # are we idle? - self._DMOStreamID = _rx_slot.stream_id - self._logger.info('(%s) DMO Transition from idle to stream %d', self._system, int_id(_rx_slot.stream_id)) - if _rx_slot.stream_id != self._DMOStreamID: # packet is from wrong stream? - if (_frame[15] & 0x2F) == 0x21: # Call start? - self._logger.info('(%s) DMO Ignore traffic on stream %d', self._system, int_id(_rx_slot.stream_id)) - continue - if (_frame[15] & 0x2F) == 0x22: # call terminator flag? - self._DMOStreamID = 0 # we are idle again - self._logger.info('(%s) DMO End of call, back to IDLE', self._system) - - _frame[15] = (_frame[15] & 0x7f) | 0x80 # force to slot 2 if client in DMO mode - else: - _frame[15] = _orig_flag # Use the origional flag value if not DMO - - _repeaterID = hex_str_4( int(_clientDict['RADIO_ID']) ) - for _index in range(0,4): # Force the repeater ID to be the "destination" ID of the client (hblink will not accept it otherwise) - _frame[_index+11] = _repeaterID[_index] - - self._parent.send_client(_client, _frame) - else: - self._parent.send_master(_frame) - - # Construct a complete HB frame from passed parameters - def make_dmrd( self, _seq, _rf_src, _dst_id, _repeater_id, _flag, _stream_id, _dmr_frame): - frame = bytearray('DMRD') # HB header type DMRD - frame += struct.pack("i", _seq)[0] # add sequence number - frame += _rf_src[0:3] # add source ID - frame += _dst_id[0:3] # add destination ID - frame += _repeater_id[0:4] # add repeater ID (4 bytes) - frame += struct.pack("i", _flag)[0:1] # add flag to packet - frame += _stream_id[0:4] # add stream ID (same for all packets in a transmission) - frame += _dmr_frame # add the dmr frame - frame += struct.pack("i", 0)[0:2] # add in the RSSI and err count - return frame - - # Private function to create a voice header or terminator DMR frame - def __encode_voice_header( self, _rx_slot, _sync, _dtype ): - _src_id = _rx_slot.rf_src - _dst_id = _rx_slot.dst_id - _cc = _rx_slot.cc - # create lc - lc = '\x00\x00\x00' + _dst_id + _src_id # PF + Reserved + FLCO + FID + Service Options + Group Address + Source Address - # encode lc into info - full_lc_encode = bptc.encode_header_lc(lc) - _rx_slot.emblc = bptc.encode_emblc(lc) # save off the emb lc for voice frames B-E - _rx_slot.emblc[5] = bitarray(32) # NULL message (F) - # create slot_type - slot_type = chr((_cc << 4) | (_dtype & 0x0f)) # data type is Header or Term - # generate FEC for slot type - slot_with_fec = BitArray(uint=golay.encode_2087(slot_type), length=20) - # construct final frame - info[0:98] + slot_type[0:10] + DMR_DATA_SYNC_MS + slot_type[10:20] + info[98:196] - frame_bits = full_lc_encode[0:98] + slot_with_fec[0:10] + decode.to_bits(_sync) + slot_with_fec[10:20] + full_lc_encode[98:196] - return decode.to_bytes(frame_bits) - - # Create a voice header DMR frame - def encode_voice_header( self, _rx_slot ): - return self.__encode_voice_header( _rx_slot, DMR_DATA_SYNC_MS, 1 ) # data_type=Voice_LC_Header - - def encode_voice( self, _ambe1, _ambe2, _ambe3, _emb ): - pass - - # Create a voice DMR frame A-F frame type - def encode_voice( self, _ambe, _rx_slot ): - _frame_type = _rx_slot.vf - if _frame_type > 0: # if not a SYNC frame cccxss - index = (_rx_slot.cc << 3) | self.lcss[_frame_type] # index into the encode table makes this a simple lookup - emb = bitarray(format(qr.ENCODE_1676[ index ], '016b')) # create emb of 16 bits - embedded = emb[8:16] + _rx_slot.emblc[_frame_type] + emb[0:8] # Take emb and a chunk of the embedded LC and combine them into 48 bits - else: - embedded = BitArray(DMR_VOICE_SYNC_MS) # Voice SYNC (48 bits) - _new_frame = _ambe[0:108] + embedded + _ambe[108:216] # Construct the dmr frame from AMBE(108 bits) + sync/emb (48 bits) + AMBE(108 bits) - return _new_frame - - # Create a voice terminator DMR frame - def encode_voice_term( self, _rx_slot ): - return self.__encode_voice_header( _rx_slot, DMR_DATA_SYNC_MS, 2 ) # data_type=Voice_LC_Terminator - def export_voice(self, _tx_slot, _seq, _ambe): - self.send_tlv(TAG_AMBE_72, struct.pack("b",_tx_slot.slot) + _ambe) # send AMBE - if _seq != (_tx_slot.lastSeq+1): - _tx_slot.lostFrame += 1 - _tx_slot.lastSeq = _seq - -class AMBE_IPSC(AMBE_BASE): - def __init__(self, _parent, _name, _config, _logger, _port): - AMBE_BASE.__init__(self, _parent, _name, _config, _logger, _port) - self._tempHead = [0] * 3 # It appears that there 3 frames of HEAD (mostly the same) - self._tempVoice = [0] * 6 - self._tempTerm = [0] - - self._seq = 0 # RPT Transmit frame sequence number (auto-increments for each frame). 16 bit - self.ipsc_seq = 0 # Same for all frames in a transmit session (sould use stream_id). 8 bit - - self.load_template() - pass - def send_voice_header(self, _rx_slot): - AMBE_BASE.send_voice_header(self, _rx_slot) - self._seq = randint(0,32767) # A transmission uses a random number to begin its sequence (16 bit) - self.ipsc_seq = (self.ipsc_seq + 1) & 0xff # this is an 8 bit value which wraps around. - - for i in range(0, 3): # Output the 3 HEAD frames to our peers - self.rewriteFrame(self._tempHead[i], _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id) - sleep(0.06) - pass - def send_voice72(self, _rx_slot, _ambe): - ambe72_1 = BitArray('0x' + ahex(_ambe[0:9]))[0:72] - ambe72_2 = BitArray('0x' + ahex(_ambe[9:18]))[0:72] - ambe72_3 = BitArray('0x' + ahex(_ambe[18:27]))[0:72] - - ambe49_1 = ambe_utils.convert72BitTo49BitAMBE(ambe72_1) - ambe49_2 = ambe_utils.convert72BitTo49BitAMBE(ambe72_2) - ambe49_3 = ambe_utils.convert72BitTo49BitAMBE(ambe72_3) - - ambe49_1.append(False) - ambe49_2.append(False) - ambe49_3.append(False) - - ambe = ambe49_1 + ambe49_2 + ambe49_3 - _frame = self._tempVoice[_rx_slot.vf][:33] + ambe.tobytes() + self._tempVoice[_rx_slot.vf][52:] # Insert the 3 49 bit AMBE frames - self.rewriteFrame(_frame, _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id) - _rx_slot.vf = (_rx_slot.vf + 1) % 6 # the voice frame counter which is always mod 6 - pass - def send_voice49(self, _rx_slot, _ambe): - ambe49_1 = BitArray('0x' + ahex(_ambe[0:7]))[0:50] - ambe49_2 = BitArray('0x' + ahex(_ambe[7:14]))[0:50] - ambe49_3 = BitArray('0x' + ahex(_ambe[14:21]))[0:50] - ambe = ambe49_1 + ambe49_2 + ambe49_3 - - _frame = _tempVoice[_rx_slot.vf][:33] + ambe.tobytes() + self._tempVoice[_rx_slot.vf][52:] # Insert the 3 49 bit AMBE frames - self.rewriteFrame(_frame, _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id) - _rx_slot.vf = (_rx_slot.vf + 1) % 6 # the voice frame counter which is always mod 6 - pass - def send_voice_term(self, _rx_slot): - self.rewriteFrame(self._tempTerm, _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id) - pass - def rewriteFrame( self, _frame, _newSlot, _newGroup, _newSouceID, _newPeerID ): - - _peerid = _frame[1:5] # int32 peer who is sending us a packet - _src_sub = _frame[6:9] # int32 Id of source - _burst_data_type = _frame[30] - _group = _frame[9:12] - - ######################################################################## - # re-Write the peer radio ID to that of this program - _frame = _frame.replace(_peerid, _newPeerID) - # re-Write the source subscriber ID to that of this program - _frame = _frame.replace(_src_sub, _newSouceID) - # Re-Write the destination Group ID - _frame = _frame.replace(_group, _newGroup) - _frame = _frame[:5] + struct.pack("i", self.ipsc_seq)[0] + _frame[6:] # ipsc sequence number increments on each transmission (stream id) - - # Re-Write IPSC timeslot value - _call_info = int_id(_frame[17:18]) - if _newSlot == 1: - _call_info &= ~(1 << 5) - elif _newSlot == 2: - _call_info |= 1 << 5 - _call_info = chr(_call_info) - _frame = _frame[:17] + _call_info + _frame[18:] - - _x = struct.pack("i", self._seq) - _frame = _frame[:20] + _x[1] + _x[0] + _frame[22:] # rtp sequence number increments for EACH frame sent out - self._seq = self._seq + 1 - - # Re-Write DMR timeslot value - # Determine if the slot is present, so we can translate if need be - if _burst_data_type == BURST_DATA_TYPE['SLOT1_VOICE'] or _burst_data_type == BURST_DATA_TYPE['SLOT2_VOICE']: - # Re-Write timeslot if necessary... - if _newSlot == 1: - _burst_data_type = BURST_DATA_TYPE['SLOT1_VOICE'] - elif _newSlot == 2: - _burst_data_type = BURST_DATA_TYPE['SLOT2_VOICE'] - _frame = _frame[:30] + _burst_data_type + _frame[31:] - - if (time() - self._parent._busy_slots[_newSlot]) >= 0.10 : # slot is not busy so it is safe to transmit - # Send the packet to all peers in the target IPSC - self._parent.send_to_ipsc(_frame) - else: - self._logger.info('Slot {} is busy, will not transmit packet from gateway'.format(_newSlot)) - self.rx[_newSlot].frame_count += 1 # update count (used for stats and to make sure header was sent) - - # Read a record from the captured IPSC file looking for a payload type that matches the filter - def readRecord(self, _file, _match_type): - _notEOF = True - # _file.seek(0) - while (_notEOF): - _data = "" - _bLen = _file.read(4) - if _bLen: - _len, = struct.unpack("i", _bLen) - if _len > 0: - _data = _file.read(_len) - _payload_type = _data[30] - if _payload_type == _match_type: - return _data - else: - _notEOF = False - else: - _notEOF = False - return _data - def load_template(self): - try: - _t = open('template.bin', 'rb') # Open the template file. This was recorded OTA - - for i in range(0, 3): - self._tempHead[i] = self.readRecord(_t, BURST_DATA_TYPE['VOICE_HEAD']) - - for i in range(0, 6): # Then there are 6 frames of AMBE. We will just use them in order - self._tempVoice[i] = self.readRecord(_t, BURST_DATA_TYPE['SLOT2_VOICE']) - - self._tempTerm = self.readRecord(_t, BURST_DATA_TYPE['VOICE_TERM']) - _t.close() - except IOError: - self._logger.error('Can not open template.bin file') - return - self._logger.debug('IPSC templates loaded') - def export_voice(self, _tx_slot, _seq, _ambe): - self.send_tlv(TAG_AMBE_49, struct.pack("b",_tx_slot.slot) + _ambe) # send AMBE - if _seq != (_tx_slot.lastSeq+1): - _tx_slot.lostFrame += 1 - _tx_slot.lastSeq = _seq - - -############################################################################################################ -# MAIN PROGRAM LOOP STARTS HERE -############################################################################################################ - -class TEST_HARNESS: - def get_globals(self): - return (subscriber_ids, talkgroup_ids, peer_ids) - def get_repeater_id(self, import_id): - return import_id - def error(self, *_str): - print('Error', _str[0] % _str[1:]) - def info(self, *_str): - print('Info', _str[0] % _str[1:]) - def debug(self, *_str): - print('Debug', _str[0] % _str[1:]) - def send_system(self, _frame): - print('send system', ahex(_frame),'\n') - def send_to_ipsc(self, _frame): - print('send_to_ipsc', ahex(_frame),'\n') - def play_thread(self,obj): - obj.play_ambe_file('ambe_capture.bin', obj.rx[1]) - obj.stop_listening() - def runTest(self, obj): - obj._logger.info('mike was here') - _rx_slot = obj.rx[1] - - _rx_slot.slot = 1 - _rx_slot.rf_src = hex_str_3(3113043) - _rx_slot.repeater_id = hex_str_4(311317) - _rx_slot.dst_id = hex_str_3(9) - _rx_slot.cc = 1 - - obj.sendBlankAmbe(_rx_slot, hex_str_4(randint(0,0xFFFFFFFF))) - thread.start_new_thread( self.play_thread, (obj,) ) - def testIPSC(self): - self._busy_slots = [0,0,0] # Keep track of activity on each slot. Make sure app is polite - self.runTest( AMBE_IPSC(self, 'TEST_HARNESS', '', self, 37003) ) - def testHB(self): - self.runTest( AMBE_HB(self, 'TEST_HARNESS', '', self, 37003) ) - -if __name__ == '__main__': - subscriber_ids = {3113043:'N4IRR'} - peer_ids = {311317:'N4IRR'} - talkgroup_ids = {9:'Non-Routed'} - - harness = TEST_HARNESS() - ##harness.testHB() - ##harness.testIPSC() - ## I am too lazy to do a state machine - task.deferLater(reactor, 1, harness.testHB) - task.deferLater(reactor, 15, harness.testIPSC) - task.deferLater(reactor, 30, reactor.stop) - - reactor.run() diff --git a/ambe_utils.py b/ambe_utils.py deleted file mode 100644 index acfaad9..0000000 --- a/ambe_utils.py +++ /dev/null @@ -1,279 +0,0 @@ -#!/usr/bin/env python -# -############################################################################### -# Copyright (C) 2017 Mike Zingman N4IRR -# -# This program 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 of the License, or -# (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -############################################################################### - -''' -''' -from binascii import b2a_hex as ahex -from bitarray import bitarray -from bitstring import BitArray -from bitstring import BitString - -__author__ = 'Mike Zingman, N4IRR and Cortney T. Buffington, N0MJS' -__copyright__ = 'Copyright (c) 2017 Mike Zingman N4IRR' -__credits__ = 'Cortney T. Buffington, N0MJS; Colin Durbridge, G4EML, Steve Zingman, N4IRS; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT' -__license__ = 'GNU GPLv3' -__maintainer__ = 'Cort Buffington, N0MJS' -__email__ = 'n0mjs@me.com' -__status__ = 'pre-alpha' -__version__ = '20170508' - -## -# DMR AMBE interleave schedule -## -rW = [ - 0, 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, 2, - 0, 2, 0, 2, 0, 2, - 0, 2, 0, 2, 0, 2 - ] - -rX = [ - 23, 10, 22, 9, 21, 8, - 20, 7, 19, 6, 18, 5, - 17, 4, 16, 3, 15, 2, - 14, 1, 13, 0, 12, 10, - 11, 9, 10, 8, 9, 7, - 8, 6, 7, 5, 6, 4 - ] - -rY = [ - 0, 2, 0, 2, 0, 2, - 0, 2, 0, 3, 0, 3, - 1, 3, 1, 3, 1, 3, - 1, 3, 1, 3, 1, 3, - 1, 3, 1, 3, 1, 3, - 1, 3, 1, 3, 1, 3 - ] - -rZ = [ - 5, 3, 4, 2, 3, 1, - 2, 0, 1, 13, 0, 12, - 22, 11, 21, 10, 20, 9, - 19, 8, 18, 7, 17, 6, - 16, 5, 15, 4, 14, 3, - 13, 2, 12, 1, 11, 0 - ] - - -# This function calculates [23,12] Golay codewords. -# The format of the returned longint is [checkbits(11),data(12)]. -def golay2312(cw): - POLY = 0xAE3 #/* or use the other polynomial, 0xC75 */ - cw = cw & 0xfff # Strip off check bits and only use data - c = cw #/* save original codeword */ - for i in range(1,13): #/* examine each data bit */ - if (cw & 1): #/* test data bit */ - cw = cw ^ POLY #/* XOR polynomial */ - cw = cw >> 1 #/* shift intermediate result */ - return((cw << 12) | c) #/* assemble codeword */ - -# This function checks the overall parity of codeword cw. -# If parity is even, 0 is returned, else 1. -def parity(cw): - #/* XOR the bytes of the codeword */ - p = cw & 0xff - p = p ^ ((cw >> 8) & 0xff) - p = p ^ ((cw >> 16) & 0xff) - - #/* XOR the halves of the intermediate result */ - p = p ^ (p >> 4) - p = p ^ (p >> 2) - p = p ^ (p >> 1) - - #/* return the parity result */ - return(p & 1) - -# Demodulate ambe frame (C1) -# Frame is an array [4][24] -def demodulateAmbe3600x2450(ambe_fr): - pr = [0] * 115 - foo = 0 - - # create pseudo-random modulator - for i in range(23, 11, -1): - foo = foo << 1 - foo = foo | ambe_fr[0][i] - pr[0] = (16 * foo) - for i in range(1, 24): - pr[i] = (173 * pr[i - 1]) + 13849 - (65536 * (((173 * pr[i - 1]) + 13849) / 65536)) - for i in range(1, 24): - pr[i] = pr[i] / 32768 - - # demodulate ambe_fr with pr - k = 1 - for j in range(22, -1, -1): - ambe_fr[1][j] = ((ambe_fr[1][j]) ^ pr[k]) - k = k + 1 - return ambe_fr # Pass it back since there is no pass by reference - -def eccAmbe3600x2450Data(ambe_fr): - ambe = bitarray() - - # just copy C0 - for j in range(23, 11, -1): - ambe.append(ambe_fr[0][j]) - -# # ecc and copy C1 -# gin = 0 -# for j in range(23): -# gin = (gin << 1) | ambe_fr[1][j] -# -# gout = BitArray(hex(golay2312(gin))) -# for j in range(22, 10, -1): -# ambe[bitIndex] = gout[j] -# bitIndex += 1 - for j in range(22, 10, -1): - ambe.append(ambe_fr[1][j]) - - # just copy C2 - for j in range(10, -1, -1): - ambe.append(ambe_fr[2][j]) - - # just copy C3 - for j in range(13, -1, -1): - ambe.append(ambe_fr[3][j]) - - return ambe - -# Convert a 49 bit raw AMBE frame into a deinterleaved structure (ready for decode by AMBE3000) -def convert49BitAmbeTo72BitFrames( ambe_d ): - index = 0 - ambe_fr = [[None for x in range(24)] for y in range(4)] - - #Place bits into the 4x24 frames. [bit0...bit23] - #fr0: [P e10 e9 e8 e7 e6 e5 e4 e3 e2 e1 e0 11 10 9 8 7 6 5 4 3 2 1 0] - #fr1: [e10 e9 e8 e7 e6 e5 e4 e3 e2 e1 e0 23 22 21 20 19 18 17 16 15 14 13 12 xx] - #fr2: [34 33 32 31 30 29 28 27 26 25 24 x x x x x x x x x x x x x] - #fr3: [48 47 46 45 44 43 42 41 40 39 38 37 36 35 x x x x x x x x x x] - - # ecc and copy C0: 12bits + 11ecc + 1 parity - # First get the 12 bits that actually exist - # Then calculate the golay codeword - # And then add the parity bit to get the final 24 bit pattern - - tmp = 0 - for i in range(11, -1, -1): #grab the 12 MSB - tmp = (tmp << 1) | ambe_d[i] - tmp = golay2312(tmp) #Generate the 23 bit result - parityBit = parity(tmp) - tmp = tmp | (parityBit << 23) #And create a full 24 bit value - for i in range(23, -1, -1): - ambe_fr[0][i] = (tmp & 1) - tmp = tmp >> 1 - - # C1: 12 bits + 11ecc (no parity) - tmp = 0 - for i in range(23,11, -1) : #grab the next 12 bits - tmp = (tmp << 1) | ambe_d[i] - tmp = golay2312(tmp) #Generate the 23 bit result - for j in range(22, -1, -1): - ambe_fr[1][j] = (tmp & 1) - tmp = tmp >> 1; - - #C2: 11 bits (no ecc) - for j in range(10, -1, -1): - ambe_fr[2][j] = ambe_d[34 - j] - - #C3: 14 bits (no ecc) - for j in range(13, -1, -1): - ambe_fr[3][j] = ambe_d[48 - j]; - - return ambe_fr - -def interleave(ambe_fr): - bitIndex = 0 - w = 0 - x = 0 - y = 0 - z = 0 - data = bytearray(9) - for i in range(36): - bit1 = ambe_fr[rW[w]][rX[x]] # bit 1 - bit0 = ambe_fr[rY[y]][rZ[z]] # bit 0 - - - data[bitIndex / 8] = ((data[bitIndex / 8] << 1) & 0xfe) | (1 if (bit1 == 1) else 0) - bitIndex += 1 - - data[bitIndex / 8] = ((data[bitIndex / 8] << 1) & 0xfe) | (1 if (bit0 == 1) else 0) - bitIndex += 1 - - w += 1 - x += 1 - y += 1 - z += 1 - return data - -def deinterleave(data): - - ambe_fr = [[None for x in range(24)] for y in range(4)] - - bitIndex = 0 - w = 0 - x = 0 - y = 0 - z = 0 - for i in range(36): - bit1 = 1 if data[bitIndex] else 0 - bitIndex += 1 - - bit0 = 1 if data[bitIndex] else 0 - bitIndex += 1 - - ambe_fr[rW[w]][rX[x]] = bit1; # bit 1 - ambe_fr[rY[y]][rZ[z]] = bit0; # bit 0 - - w += 1 - x += 1 - y += 1 - z += 1 - - return ambe_fr - -def convert72BitTo49BitAMBE( ambe72 ): - ambe_fr = deinterleave(ambe72) # take 72 bit ambe and lay it out in C0-C3 - ambe_fr = demodulateAmbe3600x2450(ambe_fr) # demodulate C1 - ambe49 = eccAmbe3600x2450Data(ambe_fr) # pick out the 49 bits of raw ambe - return ambe49 - -def convert49BitTo72BitAMBE( ambe49 ): - ambe_fr = convert49BitAmbeTo72BitFrames(ambe49) # take raw ambe 49 + ecc and place it into C0-C3 - ambe_fr = demodulateAmbe3600x2450(ambe_fr) # demodulate C1 - ambe72 = interleave(ambe_fr); # Re-interleave it, returning 72 bits - return ambe72 - -def testit(): - ambe72 = BitArray('0xACAA40200044408080') #silence frame - print('ambe72=',ambe72) - - ambe49 = convert72BitTo49BitAMBE(ambe72) - print('ambe49=',ahex(ambe49)) - - ambe72 = convert49BitTo72BitAMBE(ambe49) - print('ambe72=',ahex(ambe72)) - -#------------------------------------------------------------------------------ -# Used to execute the module directly to run built-in tests -#------------------------------------------------------------------------------ - -if __name__ == '__main__': - testit() diff --git a/dmrlink.py b/dmrlink.py index 0b324df..0954fa6 100755 --- a/dmrlink.py +++ b/dmrlink.py @@ -25,17 +25,11 @@ from __future__ import print_function -import ConfigParser -import argparse -import sys -import csv -import os +# Full imports import logging -import signal - import cPickle as pickle -from logging.config import dictConfig +# Function Imports from hmac import new as hmac_new from binascii import b2a_hex as ahex from binascii import a2b_hex as bhex @@ -44,15 +38,18 @@ from socket import inet_ntoa as IPAddr from socket import inet_aton as IPHexStr from time import time +# Twisted Imports from twisted.internet.protocol import DatagramProtocol, Factory, Protocol from twisted.protocols.basic import NetstringReceiver from twisted.internet import reactor, task +# Imports files in the dmrlink subdirectory (these things shouldn't change often) from ipsc.ipsc_const import * from ipsc.ipsc_mask import * -from dmrlink_config import build_config -from dmrlink_log import config_logging -from dmr_utils.utils import hex_str_2, hex_str_3, hex_str_4, int_id +from ipsc.reporting_const import * + +# Imports from DMR Utilities package +from dmr_utils.utils import hex_str_2, hex_str_3, hex_str_4, int_id, try_download, mk_id_dict __author__ = 'Cortney T. Buffington, N0MJS' @@ -66,37 +63,83 @@ __email__ = 'n0mjs@me.com' # Global variables used whether we are a module or __main__ systems = {} + # Timed loop used for reporting IPSC status # # REPORT BASED ON THE TYPE SELECTED IN THE MAIN CONFIG FILE -def config_reports(_config): - if _config['REPORTS']['REPORT_NETWORKS'] == 'PICKLE': - def reporting_loop(_logger): - _logger.debug('Periodic Reporting Loop Started (PICKLE)') - try: - with open(_config['REPORTS']['REPORT_PATH']+'dmrlink_stats.pickle', 'wb') as file: - pickle.dump(_config['SYSTEMS'], file, 2) - file.close() - except IOError as detail: - _logger.error('I/O Error: %s', detail) - - elif _config['REPORTS']['REPORT_NETWORKS'] == 'PRINT': +def config_reports(_config, _logger, _factory): + if _config['REPORTS']['REPORT_NETWORKS'] == 'PRINT': def reporting_loop(_logger): _logger.debug('Periodic Reporting Loop Started (PRINT)') for system in _config['SYSTEMS']: print_master(_config, system) print_peer_list(_config, system) + + reporting = task.LoopingCall(reporting_loop, _logger) + reporting.start(_config['REPORTS']['REPORT_INTERVAL']) + report_server = False elif _config['REPORTS']['REPORT_NETWORKS'] == 'NETWORK': def reporting_loop(_logger, _server): _logger.debug('Periodic Reporting Loop Started (NETWORK)') _server.send_config() + + _logger.info('DMRlink TCP reporting server starting') + + report_server = _factory(_config, _logger) + report_server.clients = [] + reactor.listenTCP(_config['REPORTS']['REPORT_PORT'], report_server) + + reporting = task.LoopingCall(reporting_loop, _logger, report_server) + reporting.start(_config['REPORTS']['REPORT_INTERVAL']) else: def reporting_loop(_logger): _logger.debug('Periodic Reporting Loop Started (NULL)') + report_server = False - return reporting_loop + return report_server + + +# ID ALIAS CREATION +# Download +def build_aliases(_config, _logger): + if _config['ALIASES']['TRY_DOWNLOAD'] == True: + # Try updating peer aliases file + result = try_download(_config['ALIASES']['PATH'], _config['ALIASES']['PEER_FILE'], _config['ALIASES']['PEER_URL'], _config['ALIASES']['STALE_TIME']) + _logger.info(result) + # Try updating subscriber aliases file + result = try_download(_config['ALIASES']['PATH'], _config['ALIASES']['SUBSCRIBER_FILE'], _config['ALIASES']['SUBSCRIBER_URL'], _config['ALIASES']['STALE_TIME']) + _logger.info(result) + + # Make Dictionaries + peer_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['PEER_FILE']) + if peer_ids: + _logger.info('ID ALIAS MAPPER: peer_ids dictionary is available') + + subscriber_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['SUBSCRIBER_FILE']) + if subscriber_ids: + _logger.info('ID ALIAS MAPPER: subscriber_ids dictionary is available') + + talkgroup_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['TGID_FILE']) + if talkgroup_ids: + _logger.info('ID ALIAS MAPPER: talkgroup_ids dictionary is available') + + local_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['LOCAL_FILE']) + if local_ids: + _logger.info('ID ALIAS MAPPER: local_ids dictionary is available') + + return(peer_ids, subscriber_ids, talkgroup_ids, local_ids) + + +# Make the IPSC systems from the config and the class used to build them. +# +def mk_ipsc_systems(_config, _logger, _systems, _ipsc, _report_server): + for system in _config['SYSTEMS']: + if _config['SYSTEMS'][system]['LOCAL']['ENABLED']: + _systems[system] = _ipsc(system, _config, _logger, _report_server) + reactor.listenUDP(_config['SYSTEMS'][system]['LOCAL']['PORT'], _systems[system], interface=_config['SYSTEMS'][system]['LOCAL']['IP']) + return _systems # Process the MODE byte in registration/peer list packets for determining master and peer capabilities # @@ -236,7 +279,7 @@ def print_master(_config, _network): #************************************************ class IPSC(DatagramProtocol): - def __init__(self, _name, _config, _logger): + def __init__(self, _name, _config, _logger, _report): # Housekeeping: create references to the configuration and status data for this IPSC instance. # Some configuration objects that are used frequently and have lengthy names are shortened @@ -246,7 +289,9 @@ class IPSC(DatagramProtocol): self._system = _name self._CONFIG = _config self._logger = _logger + self._report = _report self._config = self._CONFIG['SYSTEMS'][self._system] + self._rcm = self._CONFIG['REPORTS']['REPORT_RCM'] and self._report # self._local = self._config['LOCAL'] self._local_id = self._local['RADIO_ID'] @@ -320,7 +365,13 @@ class IPSC(DatagramProtocol): else: self._logger.warning('(%s) Peer De-Registration Requested for: %s, but we don\'t have a listing for this peer', self._system, int_id(_peerid)) pass - + + # De-register ourselves from the IPSC + def de_register_self(self): + self._logger.info('(%s) De-Registering self from the IPSC system', self._system) + de_reg_req_pkt = self.hashed_packet(self._local['AUTH_KEY'], self.DE_REG_REQ_PKT) + self.send_to_ipsc(de_reg_req_pkt) + # Take a received peer list and the network it belongs to, process and populate the # data structure in my_ipsc_config with the results, and return a simple list of peers. # @@ -391,15 +442,23 @@ class IPSC(DatagramProtocol): #************************************************ # CALLBACK FUNCTIONS FOR USER PACKET TYPES #************************************************ - + + # If RCM reporting and reporting is network-based in the global configuration, + # send the RCM packet to the monitoring server def call_mon_status(self, _data): self._logger.debug('(%s) Repeater Call Monitor Origin Packet Received: %s', self._system, ahex(_data)) - + if self._rcm: + self._report.send_rcm(self._system + ','+ _data) + def call_mon_rpt(self, _data): self._logger.debug('(%s) Repeater Call Monitor Repeating Packet Received: %s', self._system, ahex(_data)) - + if self._rcm: + self._report.send_rcm(self._system + ',' + _data) + def call_mon_nack(self, _data): self._logger.debug('(%s) Repeater Call Monitor NACK Packet Received: %s', self._system, ahex(_data)) + if self._rcm: + self._report.send_rcm(self._system + ',' + _data) def xcmp_xnl(self, _data): self._logger.debug('(%s) XCMP/XNL Packet Received: %s', self._system, ahex(_data)) @@ -938,45 +997,51 @@ class IPSC(DatagramProtocol): # Socket-based reporting section # class report(NetstringReceiver): - def __init__(self): - pass + def __init__(self, factory): + self._factory = factory def connectionMade(self): - report_server.clients.append(self) - logger.info('DMRlink reporting client connected: %s', self.transport.getPeer()) + self._factory.clients.append(self) + self._factory._logger.info('DMRlink reporting client connected: %s', self.transport.getPeer()) def connectionLost(self, reason): - logger.info('DMRlink reporting client disconnected: %s', self.transport.getPeer()) - report_server.clients.remove(self) + self._factory._logger.info('DMRlink reporting client disconnected: %s', self.transport.getPeer()) + self._factory.clients.remove(self) def stringReceived(self, data): self.process_message(data) def process_message(self, _message): opcode = _message[:1] - if opcode == REP_OPC['CONFIG_REQ']: - logger.info('DMRlink reporting client sent \'CONFIG_REQ\': %s', self.transport.getPeer()) + if opcode == REPORT_OPCODES['CONFIG_REQ']: + self._factory._logger.info('DMRlink reporting client sent \'CONFIG_REQ\': %s', self.transport.getPeer()) self.send_config() else: print('got unknown opcode') class reportFactory(Factory): - def __init__(self): - pass + def __init__(self, config, logger): + self._config = config + self._logger = logger def buildProtocol(self, addr): - if (addr.host) in CONFIG['REPORTS']['REPORT_CLIENTS']: - return report() + if (addr.host) in self._config['REPORTS']['REPORT_CLIENTS'] or '*' in self._config['REPORTS']['REPORT_CLIENTS']: + self._logger.debug('Permitting report server connection attempt from: %s:%s', addr.host, addr.port) + return report(self) else: + self._logger.error('Invalid report server connection attempt from: %s:%s', addr.host, addr.port) return None def send_clients(self, _message): - for client in report_server.clients: + for client in self.clients: client.sendString(_message) def send_config(self): - serialized = pickle.dumps(CONFIG['SYSTEMS'], protocol=pickle.HIGHEST_PROTOCOL) - self.send_clients(REP_OPC['CONFIG_SND']+serialized) + serialized = pickle.dumps(self._config['SYSTEMS'], protocol=pickle.HIGHEST_PROTOCOL) + self.send_clients(REPORT_OPCODES['CONFIG_SND']+serialized) + + def send_rcm(self, _data): + self.send_clients(REPORT_OPCODES['RCM_SND']+_data) #************************************************ @@ -984,6 +1049,13 @@ class reportFactory(Factory): #************************************************ if __name__ == '__main__': + import argparse + import sys + import os + import signal + + from ipsc.dmrlink_config import build_config + from ipsc.dmrlink_log import config_logging # Change the current directory to the location of the application os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) @@ -1007,50 +1079,28 @@ if __name__ == '__main__': if cli_args.LOG_HANDLERS: CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS logger = config_logging(CONFIG['LOGGER']) + logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2017 N0MJS & the K0USY Group - SYSTEM STARTING...') - config_reports(CONFIG) - - - logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...') - - # Shut ourselves down gracefully with the IPSC peers. + # Set signal handers so that we can gracefully exit if need be def sig_handler(_signal, _frame): logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal)) - for system in systems: - this_ipsc = systems[system] - logger.info('De-Registering from IPSC %s', system) - de_reg_req_pkt = this_ipsc.hashed_packet(this_ipsc._local['AUTH_KEY'], this_ipsc.DE_REG_REQ_PKT) - this_ipsc.send_to_ipsc(de_reg_req_pkt) + systems[system].de_register_self() reactor.stop() - - # Set signal handers so that we can gracefully exit if need be + for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]: signal.signal(sig, sig_handler) - # INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGUED IPSC - for system in CONFIG['SYSTEMS']: - if CONFIG['SYSTEMS'][system]['LOCAL']['ENABLED']: - systems[system] = IPSC(system, CONFIG, logger) - reactor.listenUDP(CONFIG['SYSTEMS'][system]['LOCAL']['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['LOCAL']['IP']) - - # INITIALIZE THE REPORTING LOOP IF CONFIGURED - if CONFIG['REPORTS']['REPORT_NETWORKS'] == 'PRINT' or CONFIG['REPORTS']['REPORT_NETWORKS'] == 'PICKLE': - reporting_loop = config_reports(CONFIG) - reporting = task.LoopingCall(reporting_loop, logger) - reporting.start(CONFIG['REPORTS']['REPORT_INTERVAL']) + # INITIALIZE THE REPORTING LOOP + report_server = config_reports(CONFIG, logger, reportFactory) + + # Build ID Aliases + peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger) - # INITIALIZE THE NETWORK-BASED REPORTING SERVER - elif CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK': - logger.info('(confbridge.py) TCP reporting server starting') - from ipsc.reporting_const import REPORT_OPCODES as REP_OPC - - report_server = reportFactory() - report_server.clients = [] - reactor.listenTCP(CONFIG['REPORTS']['REPORT_PORT'], reportFactory()) - - reporting_loop = config_reports(CONFIG) - reporting = task.LoopingCall(reporting_loop, logger, report_server) - reporting.start(CONFIG['REPORTS']['REPORT_INTERVAL']) - + # INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC + systems = mk_ipsc_systems(CONFIG, logger, systems, IPSC, report_server) + + + + # INITIALIZATION COMPLETE -- START THE REACTOR reactor.run() diff --git a/dmrlink_SAMPLE.cfg b/dmrlink_SAMPLE.cfg index ee980c1..4482f40 100644 --- a/dmrlink_SAMPLE.cfg +++ b/dmrlink_SAMPLE.cfg @@ -16,9 +16,8 @@ PATH: /opt/dmrlink/ # NETWORK REPORTING CONFIGURATION # Enabling "REPORT_NETWORKS" will cause a reporting action for # IPSC each time the periodic reporting loop runs, that period is -# specifiec by "REPORT_INTERVAL" in seconds. Possible values +# specified by "REPORT_INTERVAL" in seconds. Possible values # for "REPORT_NETWORKS" are: -# PICKLE - a Python pickle file of the network's data structure # # PRINT - a pretty print (STDOUT) of the data structure # "PRINT_PEERS_INC_MODE" - Boolean to include mode bits @@ -31,20 +30,23 @@ PATH: /opt/dmrlink/ # goal here is a web dashboard that doesn't live on the # dmrlink machine itself. # -# PRINT is the odd man out because it sends prettily formatted stuff -# to STDOUT. The others send the internal data structure of the IPSC -# instance and let some program on the other end sort it out. +# PRINT should only be used for debugging; it sends prettily formatted +# stuff to STDOUT. The others send the internal data structure of the +# IPSC instance and let some program on the other end sort it out. # +# REPORT_RCM - If True, and REPORT_NETWORKS = 'NETWORK', will send RCM +# Packets to connected reporting clients. This also requires +# individual IPSC systems to have RCM and CON_APP both set 'True' +# # REPORT_INTERVAL - Seconds between reports -# REPORT_PATH - Absolute path save data (pickle and json) # REPORT_PORT - TCP port to listen on if "REPORT_NETWORKS" = NETWORK # REPORT_CLIENTS - comma separated list of IPs you will allow clients # to connect on. # [REPORTS] -REPORT_NETWORKS: +REPORT_NETWORKS: +REPORT_RCM: REPORT_INTERVAL: 60 -REPORT_PATH: REPORT_PORT: 4321 REPORT_CLIENTS: 127.0.0.1, 192.168.1.1 PRINT_PEERS_INC_MODE: 0 @@ -69,7 +71,7 @@ PRINT_PEERS_INC_FLAGS: 0 # [LOGGER] LOG_FILE: /var/log/dmrlink/dmrlink.log -LOG_HANDLERS: file +LOG_HANDLERS: console-timed,file-timed LOG_LEVEL: INFO LOG_NAME: DMRlink @@ -82,6 +84,7 @@ LOG_NAME: DMRlink # download again. Don't be an ass and change this to less than a few days. [ALIASES] TRY_DOWNLOAD: True +LOCAL_FILE: False PATH: ./ PEER_FILE: peer_ids.csv SUBSCRIBER_FILE: subscriber_ids.csv @@ -97,13 +100,13 @@ STALE_DAYS: 7 # # [NAME] The name you want to use to identify the IPSC instance (use # something better than "IPSC1"...) -# ENABLED: Should we communiate with this network? Handy if you need to +# ENABLED: Should we communicate with this network? Handy if you need to # shut one down but don't want to lose the config # RADIO_ID: This is the radio ID that DMRLink should use to communicate # IP: This is the local IPv4 address to listen on. It may be left # blank if you do not need or wish to specify. It is mostly # useful when DMRlink uses multiple interfaces to serve as an -# application gatway/proxy from private and/or VPN networks +# application gateway/proxy from private and/or VPN networks # to the real world. # PORT: This is the UDP source port for DMRLink to use for this # PSC network, must be unique!!! @@ -117,9 +120,10 @@ STALE_DAYS: 7 # CSBK_CALL: Should be False, we cannot process these, but may be useful # for debugging. # RCM: Repeater Call Monitoring - don't unable unless you plan to -# actually use it, this craetes extra network traffic. +# actually use it, this creates extra network traffic. # CON_APP: Third Party Console App - exactly what DMRlink is, should -# be set to True. +# be set to True, and must be if you intend to process RCM +# packets (like with network-based reporting) # XNL_CALL: Can cause problems if not set to False, DMRlink does not # process XCMP/XNL calls. # XNL_MASTER: Obviously, should also be False, see XNL_CALL. @@ -129,14 +133,14 @@ STALE_DAYS: 7 # AUTH_ENABLED: Do we use authenticated IPSC? # AUTH_KEY: The Authentication key (up to 40 hex characters) # MASTER_IP: IP address of the IPSC master (ignored if DMRlink is the master) -# MASTER_PORT: UDP port of the IPSC master (ignored if DMRlinkn is the master) +# MASTER_PORT: UDP port of the IPSC master (ignored if DMRlink is the master) # GROUP_HANGTIME: Group hangtime, per DMR configuration # # ...Repeat the block for each IPSC network to join. # [SAMPLE_PEER] -ENABLED: True +ENABLED: False RADIO_ID: 12345 IP: PORT: 50000 @@ -162,7 +166,7 @@ GROUP_HANGTIME: 5 [SAMPLE_MASTER] -ENABLED: False +ENABLED: True RADIO_ID: 54321 IP: 192.168.1.1 PORT: 50000 diff --git a/dmrlink_config.py b/dmrlink_config.py deleted file mode 100755 index 652712d..0000000 --- a/dmrlink_config.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python -# -############################################################################### -# Copyright (C) 2016 Cortney T. Buffington, N0MJS -# -# This program 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 of the License, or -# (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -############################################################################### - -import ConfigParser -import sys - -from socket import gethostbyname - -# Does anybody read this stuff? There's a PEP somewhere that says I should do this. -__author__ = 'Cortney T. Buffington, N0MJS' -__copyright__ = 'Copyright (c) 2016 Cortney T. Buffington, N0MJS and the K0USY Group' -__license__ = 'GNU GPLv3' -__maintainer__ = 'Cort Buffington, N0MJS' -__email__ = 'n0mjs@me.com' - - -def build_config(_config_file): - config = ConfigParser.ConfigParser() - - if not config.read(_config_file): - sys.exit('Configuration file \''+_config_file+'\' is not a valid configuration file! Exiting...') - - CONFIG = {} - CONFIG['GLOBAL'] = {} - CONFIG['REPORTS'] = {} - CONFIG['LOGGER'] = {} - CONFIG['ALIASES'] = {} - CONFIG['SYSTEMS'] = {} - - try: - for section in config.sections(): - if section == 'GLOBAL': - CONFIG['GLOBAL'].update({ - 'PATH': config.get(section, 'PATH') - }) - - elif section == 'REPORTS': - CONFIG['REPORTS'].update({ - 'REPORT_NETWORKS': config.get(section, 'REPORT_NETWORKS'), - 'REPORT_INTERVAL': config.getint(section, 'REPORT_INTERVAL'), - 'REPORT_PATH': config.get(section, 'REPORT_PATH'), - 'REPORT_PORT': config.get(section, 'REPORT_PORT'), - 'REPORT_CLIENTS': config.get(section, 'REPORT_CLIENTS').split(','), - 'PRINT_PEERS_INC_MODE': config.getboolean(section, 'PRINT_PEERS_INC_MODE'), - 'PRINT_PEERS_INC_FLAGS': config.getboolean(section, 'PRINT_PEERS_INC_FLAGS') - }) - if CONFIG['REPORTS']['REPORT_PORT']: - CONFIG['REPORTS']['REPORT_PORT'] = int(CONFIG['REPORTS']['REPORT_PORT']) - - elif section == 'LOGGER': - CONFIG['LOGGER'].update({ - 'LOG_FILE': config.get(section, 'LOG_FILE'), - 'LOG_HANDLERS': config.get(section, 'LOG_HANDLERS'), - 'LOG_LEVEL': config.get(section, 'LOG_LEVEL'), - 'LOG_NAME': config.get(section, 'LOG_NAME') - }) - - elif section == 'ALIASES': - CONFIG['ALIASES'].update({ - 'TRY_DOWNLOAD': config.getboolean(section, 'TRY_DOWNLOAD'), - 'PATH': config.get(section, 'PATH'), - 'PEER_FILE': config.get(section, 'PEER_FILE'), - 'SUBSCRIBER_FILE': config.get(section, 'SUBSCRIBER_FILE'), - 'TGID_FILE': config.get(section, 'TGID_FILE'), - 'PEER_URL': config.get(section, 'PEER_URL'), - 'SUBSCRIBER_URL': config.get(section, 'SUBSCRIBER_URL'), - 'STALE_TIME': config.getint(section, 'STALE_DAYS') * 86400, - }) - - elif config.getboolean(section, 'ENABLED'): - CONFIG['SYSTEMS'].update({section: {'LOCAL': {}, 'MASTER': {}, 'PEERS': {}}}) - - CONFIG['SYSTEMS'][section]['LOCAL'].update({ - # In case we want to keep config, but not actually connect to the network - 'ENABLED': config.getboolean(section, 'ENABLED'), - - # These items are used to create the MODE byte - 'PEER_OPER': config.getboolean(section, 'PEER_OPER'), - 'IPSC_MODE': config.get(section, 'IPSC_MODE'), - 'TS1_LINK': config.getboolean(section, 'TS1_LINK'), - 'TS2_LINK': config.getboolean(section, 'TS2_LINK'), - 'MODE': '', - - # These items are used to create the multi-byte FLAGS field - 'AUTH_ENABLED': config.getboolean(section, 'AUTH_ENABLED'), - 'CSBK_CALL': config.getboolean(section, 'CSBK_CALL'), - 'RCM': config.getboolean(section, 'RCM'), - 'CON_APP': config.getboolean(section, 'CON_APP'), - 'XNL_CALL': config.getboolean(section, 'XNL_CALL'), - 'XNL_MASTER': config.getboolean(section, 'XNL_MASTER'), - 'DATA_CALL': config.getboolean(section, 'DATA_CALL'), - 'VOICE_CALL': config.getboolean(section, 'VOICE_CALL'), - 'MASTER_PEER': config.getboolean(section, 'MASTER_PEER'), - 'FLAGS': '', - - # Things we need to know to connect and be a peer in this IPSC - 'RADIO_ID': hex(int(config.get(section, 'RADIO_ID')))[2:].rjust(8,'0').decode('hex'), - 'IP': gethostbyname(config.get(section, 'IP')), - 'PORT': config.getint(section, 'PORT'), - 'ALIVE_TIMER': config.getint(section, 'ALIVE_TIMER'), - 'MAX_MISSED': config.getint(section, 'MAX_MISSED'), - 'AUTH_KEY': (config.get(section, 'AUTH_KEY').rjust(40,'0')).decode('hex'), - 'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME'), - 'NUM_PEERS': 0, - }) - # Master means things we need to know about the master peer of the network - CONFIG['SYSTEMS'][section]['MASTER'].update({ - 'RADIO_ID': '\x00\x00\x00\x00', - 'MODE': '\x00', - 'MODE_DECODE': '', - 'FLAGS': '\x00\x00\x00\x00', - 'FLAGS_DECODE': '', - 'STATUS': { - 'CONNECTED': False, - 'PEER_LIST': False, - 'KEEP_ALIVES_SENT': 0, - 'KEEP_ALIVES_MISSED': 0, - 'KEEP_ALIVES_OUTSTANDING': 0, - 'KEEP_ALIVES_RECEIVED': 0, - 'KEEP_ALIVE_RX_TIME': 0 - }, - 'IP': '', - 'PORT': '' - }) - if not CONFIG['SYSTEMS'][section]['LOCAL']['MASTER_PEER']: - CONFIG['SYSTEMS'][section]['MASTER'].update({ - 'IP': gethostbyname(config.get(section, 'MASTER_IP')), - 'PORT': config.getint(section, 'MASTER_PORT') - }) - - # Temporary locations for building MODE and FLAG data - MODE_BYTE = 0 - FLAG_1 = 0 - FLAG_2 = 0 - - # Construct and store the MODE field - if CONFIG['SYSTEMS'][section]['LOCAL']['PEER_OPER']: - MODE_BYTE |= 1 << 6 - if CONFIG['SYSTEMS'][section]['LOCAL']['IPSC_MODE'] == 'ANALOG': - MODE_BYTE |= 1 << 4 - elif CONFIG['SYSTEMS'][section]['LOCAL']['IPSC_MODE'] == 'DIGITAL': - MODE_BYTE |= 1 << 5 - if CONFIG['SYSTEMS'][section]['LOCAL']['TS1_LINK']: - MODE_BYTE |= 1 << 3 - else: - MODE_BYTE |= 1 << 2 - if CONFIG['SYSTEMS'][section]['LOCAL']['TS2_LINK']: - MODE_BYTE |= 1 << 1 - else: - MODE_BYTE |= 1 << 0 - CONFIG['SYSTEMS'][section]['LOCAL']['MODE'] = chr(MODE_BYTE) - - # Construct and store the FLAGS field - if CONFIG['SYSTEMS'][section]['LOCAL']['CSBK_CALL']: - FLAG_1 |= 1 << 7 - if CONFIG['SYSTEMS'][section]['LOCAL']['RCM']: - FLAG_1 |= 1 << 6 - if CONFIG['SYSTEMS'][section]['LOCAL']['CON_APP']: - FLAG_1 |= 1 << 5 - if CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL']: - FLAG_2 |= 1 << 7 - if CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL'] and CONFIG['SYSTEMS'][section]['LOCAL']['XNL_MASTER']: - FLAG_2 |= 1 << 6 - elif CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL'] and not CONFIG['SYSTEMS'][section]['LOCAL']['XNL_MASTER']: - FLAG_2 |= 1 << 5 - if CONFIG['SYSTEMS'][section]['LOCAL']['AUTH_ENABLED']: - FLAG_2 |= 1 << 4 - if CONFIG['SYSTEMS'][section]['LOCAL']['DATA_CALL']: - FLAG_2 |= 1 << 3 - if CONFIG['SYSTEMS'][section]['LOCAL']['VOICE_CALL']: - FLAG_2 |= 1 << 2 - if CONFIG['SYSTEMS'][section]['LOCAL']['MASTER_PEER']: - FLAG_2 |= 1 << 0 - CONFIG['SYSTEMS'][section]['LOCAL']['FLAGS'] = '\x00\x00'+chr(FLAG_1)+chr(FLAG_2) - - except ConfigParser.Error, err: - sys.exit('Could not parse configuration file, exiting...') - - return CONFIG - - -# Used to run this file direclty and print the config, -# which might be useful for debugging -if __name__ == '__main__': - import sys - import os - import argparse - from pprint import pprint - - # Change the current directory to the location of the application - os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) - - # CLI argument parser - handles picking up the config file from the command line, and sending a "help" message - parser = argparse.ArgumentParser() - parser.add_argument('-c', '--config', action='store', dest='CONFIG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)') - cli_args = parser.parse_args() - - - # Ensure we have a path for the config file, if one wasn't specified, then use the execution directory - if not cli_args.CONFIG_FILE: - cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg' - - - pprint(build_config(cli_args.CONFIG_FILE)) \ No newline at end of file diff --git a/dmrlink_log.py b/dmrlink_log.py deleted file mode 100755 index 05fabd3..0000000 --- a/dmrlink_log.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python -# -############################################################################### -# Copyright (C) 2016 Cortney T. Buffington, N0MJS -# -# This program 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 of the License, or -# (at your option) any later version. -# -# This program 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 this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -############################################################################### - -import logging -from logging.config import dictConfig - -# Does anybody read this stuff? There's a PEP somewhere that says I should do this. -__author__ = 'Cortney T. Buffington, N0MJS' -__copyright__ = 'Copyright (c) 2016 Cortney T. Buffington, N0MJS and the K0USY Group' -__license__ = 'GNU GPLv3' -__maintainer__ = 'Cort Buffington, N0MJS' -__email__ = 'n0mjs@me.com' - - -def config_logging(_logger): - dictConfig({ - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - }, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' - }, - 'timed': { - 'format': '%(levelname)s %(asctime)s %(message)s' - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - 'syslog': { - 'format': '%(name)s (%(process)d): %(levelname)s %(message)s' - } - }, - 'handlers': { - 'null': { - 'class': 'logging.NullHandler' - }, - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'simple' - }, - 'console-timed': { - 'class': 'logging.StreamHandler', - 'formatter': 'timed' - }, - 'file': { - 'class': 'logging.FileHandler', - 'formatter': 'simple', - 'filename': _logger['LOG_FILE'], - }, - 'file-timed': { - 'class': 'logging.FileHandler', - 'formatter': 'timed', - 'filename': _logger['LOG_FILE'], - }, - 'syslog': { - 'class': 'logging.handlers.SysLogHandler', - 'formatter': 'syslog', - } - }, - 'loggers': { - _logger['LOG_NAME']: { - 'handlers': _logger['LOG_HANDLERS'].split(','), - 'level': _logger['LOG_LEVEL'], - 'propagate': True, - } - } - }) - return logging.getLogger(_logger['LOG_NAME']) \ No newline at end of file diff --git a/mk-dmrlink b/mk-dmrlink index fe2d080..d03317b 100755 --- a/mk-dmrlink +++ b/mk-dmrlink @@ -12,19 +12,6 @@ echo "" # Application # ################################################# -# Install the required support programs - apt-get install unzip -y - apt-get install python-dev -y - apt-get install python-pip -y - apt-get install python-twisted -y - pip install bitstring - pip install bitarray - - cd /opt - git clone https://github.com/n0mjs710/dmr_utils.git - cd dmr_utils/ - pip install . - echo "Required programs installed, continuing" # To allow multiple instances of DMRlink to run @@ -32,7 +19,7 @@ echo "Required programs installed, continuing" # The needed files are copied to /opt/dmrlink # Make needed directories - mkdir -p /opt/dmrlink/ambe_audio/ + mkdir -p /opt/dmrlink/IPSC_Bridge/ mkdir -p /opt/dmrlink/bridge/ mkdir -p /opt/dmrlink/confbridge/ mkdir -p /opt/dmrlink/log/ @@ -52,7 +39,7 @@ cd /opt/dmrlink # cp $currentdir/talkgroup_ids.csv /opt/dmrlink # Copy ipsc directory into each app directory -cp -rf $currentdir/ipsc/ /opt/dmrlink/ambe_audio/ +cp -rf $currentdir/ipsc/ /opt/dmrlink/IPSC_Bridge/ cp -rf $currentdir/ipsc/ /opt/dmrlink/bridge/ cp -rf $currentdir/ipsc/ /opt/dmrlink/confbridge/ cp -rf $currentdir/ipsc/ /opt/dmrlink/log/ @@ -68,23 +55,23 @@ cp $currentdir/confbridge_rules_SAMPLE.py /opt/dmrlink/samples cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/samples cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/samples cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/samples -cp $currentdir/ambe_audio.cfg /opt/dmrlink/samples +cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/samples cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/samples # Put the doc together for easy reference cp -rf $currentdir/documents /opt/dmrlink cp $currentdir/LICENSE.txt /opt/dmrlink/documents cp $currentdir/requirements.txt /opt/dmrlink/documents -cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/documents +# cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/documents -# ambe_audio -cp $currentdir/dmrlink.py /opt/dmrlink/ambe_audio/ -cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/ambe_audio/ +# IPSC_Bridge +cp $currentdir/dmrlink.py /opt/dmrlink/IPSC_Bridge/ +cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/IPSC_Bridge/ # -cp $currentdir/ambe_audio.cfg /opt/dmrlink/ambe_audio/ -cp $currentdir/ambe_audio.py /opt/dmrlink/ambe_audio/ -cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/ambe_audio/ -cp $currentdir/template.bin /opt/dmrlink/ambe_audio/ +cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/IPSC_Bridge/ +cp $currentdir/IPSC_Bridge.py /opt/dmrlink/IPSC_Bridge/ +# cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/IPSC_Bridge/ +# cp $currentdir/template.bin /opt/dmrlink/IPSC_Bridge/ # Bridge app cp $currentdir/dmrlink.py /opt/dmrlink/bridge/ diff --git a/systemd/ambe_audio.service b/systemd/ambe_audio.service deleted file mode 100644 index bc465f0..0000000 --- a/systemd/ambe_audio.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=DMRlink ambe audio Service -# Description=Place this file in /lib/systemd/system - -[Service] -Type=simple -StandardOutput=null -WorkingDirectory=/opt/dmrlink/ambe_audio -Restart=always -RestartSec=3 -ExecStart=/usr/bin/python /opt/dmrlink/ambe_audio/ambe_audio.py -ExecReload=/bin/kill -2 $MAINPID -KillMode=process - -[Install] -WantedBy=network-online.target - diff --git a/systemd/ipsc_bridge.service b/systemd/ipsc_bridge.service new file mode 100644 index 0000000..0b65d1b --- /dev/null +++ b/systemd/ipsc_bridge.service @@ -0,0 +1,17 @@ +[Unit] +Description=DMRlink IPSC_Bridge Service +# Place this file in /lib/systemd/system + +[Service] +Type=simple +StandardOutput=null +WorkingDirectory=/opt/dmrlink/IPSC_Bridge +Restart=always +RestartSec=3 +ExecStart=/usr/bin/python /opt/dmrlink/IPSC_Bridge/IPSC_Bridge.py +ExecReload=/bin/kill -2 $MAINPID +KillMode=process + +[Install] +WantedBy=network-online.target +