diff --git a/ambe_audio.cfg b/ambe_audio.cfg index 626e25c..463b45c 100644 --- a/ambe_audio.cfg +++ b/ambe_audio.cfg @@ -1,9 +1,53 @@ -[SETTINGS] -_debug = False -_outToFile = False -_outToUDP = True -_gateway = 127.0.0.1 -_gateway_port = 1234 -_remote_control_port = 1235 -_tg_filter = 2,3,13,3174,3777215,3100,9,9998,3112 +################################################ +# ambe_audio configuration file. +################################################ + +# DEFAULTS - General settings. These values are +#inherited in each subsequent section (defined by section value). +[DEFAULTS] +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 DMRGateway operation) +gateway = 127.0.0.1 # IP address of DMRGateway app +toGatewayPort = 31000 # Port DMRGateway is listening on for AMBE frames to decode +remoteControlPort = 31002 # Port that ambe_audio is listening on for remote control commands +fromGatewayPort = 31003 # Port to listen on for AMBE frames to transmit to all peers +gatewayDmrId = 0 # id to use when transmitting from the gateway +tgFilter = 9 # A list of TG IDs to monitor. All TGs will be passed to DMRGateway +txTg = 9 # TG to use for all frames received from DMRGateway -> IPSC +txTs = 2 # Slot to use for frames received from DMRGateway -> IPSC +# +# The section setting defines the current section to use. By default, the ‘ENABLED’ section in dmrlink.cfg is used. +# Any values in the named section override the values from the DEFAULTS section. For example, if the BM section +# has a value for gatewayDmrId it would override the value above. Only one section should be set here. Think +# of this as an easy way to switch between several different configurations with a single line. +# +#section = BM # Use BM section values +#section = Sandbox # Use SANDBOX section values + +[BM] # BrandMeister +tgFilter = 3100,31094 # A list of TG IDs to monitor. All TGs will be passed to DMRGateway +txTg = 3100 # TG to use for all frames received from DMRGateway -> IPSC +txTs = 2 # Slot to use for frames received from DMRGateway -> IPSC + +[BM2] # Alternate BM configuration +tgFilter = 31094 +txTg = 31094 +txTs = 2 + +[Sandbox] # DMR MARC sandbox network +tgFilter = 3120 +txTg = 3120 +txTs = 2 + +[Sandbox2] # DMR MARC sandbox network +tgFilter = 1 +txTg = 1 +txTs = 1 + +[N4IRS] # N4IRS/INAD network +tgFilter = 1,2,3,13,3174,3777215,3100,9,9998,3112,3136,310,311,312,9997 +txTg = 9998 +txTs = 2 + diff --git a/ambe_audio.py b/ambe_audio.py index 5c60c30..7dff76f 100755 --- a/ambe_audio.py +++ b/ambe_audio.py @@ -16,10 +16,11 @@ from bitstring import BitArray import sys, socket, ConfigParser, thread, traceback import cPickle as pickle -from dmrlink import IPSC, NETWORK, networks, logger, int_id, hex_str_3, get_info, talkgroup_ids, subscriber_ids, peer_ids, PATH -from time import time +from dmrlink import IPSC, NETWORK, networks, logger, int_id, hex_str_3, hex_str_4, get_info, talkgroup_ids, peer_ids, PATH, get_subscriber_info, reread_subscribers +from time import time, sleep, clock, localtime, strftime import csv - +import struct +from random import randint __author__ = 'Cortney T. Buffington, N0MJS' __copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group' @@ -35,55 +36,84 @@ try: except ImportError: sys.exit('IPSC message types file not found or invalid') +try: + from ipsc.ipsc_mask import * +except ImportError: + sys.exit('IPSC mask values file not found or invalid') + + # # ambeIPSC class, # class ambeIPSC(IPSC): - _configFile='ambe_audio.cfg' - _debug = False - _outToFile = False - _outToUDP = True + _configFile='ambe_audio.cfg' # Name of the config file to over-ride these default values + _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 DMRGateway operation) #_gateway = "192.168.1.184" - _gateway = "127.0.0.1" - _gateway_port = 1234 - _remote_control_port = 1235 + _gateway = "127.0.0.1" # IP address of DMRGateway app + _gateway_port = 31000 # Port DMRGateway is listening on for AMBE frames to decode + _remote_control_port = 31002 # Port that ambe_audio is listening on for remote control commands + _ambeRxPort = 31003 # Port to listen on for AMBE frames to transmit to all peers + _gateway_dmr_id = 0 # id to use when transmitting from the gateway _tg_filter = [2,3,13,3174,3777215,3100,9,9998,3112] #set this to the tg to monitor - _no_tg = -99 - _sock = -1; - lastPacketTimeout = 0 - _transmitStartTime = 0 + + _no_tg = -99 # Flag (const) that defines a value for "no tg is currently active" + _busy_slots = [0,0,0] # Keep track of activity on each slot. Make sure app is polite + _sock = -1; # Socket object to send AMBE to DMRGateway + lastPacketTimeout = 0 # Time of last packet. Used to trigger an artifical TERM if one was not seen + _transmitStartTime = 0 # Used for info on transmission duration + _start_seq = 0 # Used to maintain error statistics for a transmission + _packet_count = 0 # Used to maintain error statistics for a transmission + _seq = 0 # Transmit frame sequence number (auto-increments for each frame) + _f = None # File handle for debug AMBE binary output + + _tx_tg = hex_str_3(9998) # Hard code the destination TG. This ensures traffic will not show up on DMR-MARC + _tx_ts = 2 # Time Slot 2 + _currentNetwork = "" + _dmrgui = '' + + ###### DEBUGDEBUGDEBUG + #_d = None + ###### DEBUGDEBUGDEBUG def __init__(self, *args, **kwargs): IPSC.__init__(self, *args, **kwargs) self.CALL_DATA = [] - # # Define default values for operation. These will be overridden by the .cfg file if found # self._currentTG = self._no_tg - self._sequenceNr = 0 - self.readConfigFile(self._configFile) + self._currentNetwork = str(args[0]) + self.readConfigFile(self._configFile, None, self._currentNetwork) - print('DMRLink ambe server') - + logger.info('DMRLink ambe server') + if self._gateway_dmr_id == 0: + sys.exit( "Error: gatewayDmrId must be set (greater than zero)" ) # # Open output sincs # if self._outToFile == True: - f = open('ambe.bin', 'wb') - print('Opening output file: ambe.bin') + self._f = open('ambe.bin', 'wb') + logger.info('Opening output file: ambe.bin') if self._outToUDP == True: self._sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) - print('Send UDP frames to DMR gateway {}:{}'.format(self._gateway, self._gateway_port)) - + logger.info('Send UDP frames to DMR gateway {}:{}'.format(self._gateway, self._gateway_port)) + + ###### DEBUGDEBUGDEBUG + #self._d = open('recordData.bin', 'wb') + ###### DEBUGDEBUGDEBUG + try: - thread.start_new_thread( self.remote_control, (self._remote_control_port, ) ) + thread.start_new_thread( self.remote_control, (self._remote_control_port, ) ) # Listen for remote control commands + thread.start_new_thread( self.launchUDP, (args[0], ) ) # Package AMBE into IPSC frames and send to all peers except: traceback.print_exc() - print( "Error: unable to start thread" ) + logger.error( "Error: unable to start thread" ) + # Utility function to convert bytes to string of hex values (for debug) def ByteToHex( self, byteStr ): @@ -92,33 +122,264 @@ class ambeIPSC(IPSC): # # Now read the configuration file and parse out the values we need # - def readConfigFile(self, configFileName): + def defaultOption( self, config, sec, opt, defaultValue ): + try: + _value = config.get(sec, opt).split(None)[0] # Get the value from the named section + except ConfigParser.NoOptionError as e: + try: + _value = config.get('DEFAULTS', opt).split(None)[0] # Try the global DEFAULTS section + except ConfigParser.NoOptionError as e: + _value = defaultValue # Not found anywhere, use the default value + logger.info(opt + ' = ' + str(_value)) + return _value + + def readConfigFile(self, configFileName, sec, networkName='DEFAULTS'): config = ConfigParser.ConfigParser() try: - self._tg_filter=[] config.read(configFileName) - for sec in config.sections(): - for key, val in config.items(sec): - if self._debug == True: - print( '%s="%s"' % (key, val) ) - self._debug = (config.get(sec, '_debug') == "True") - self._outToFile = (config.get(sec, '_outToFile') == "True") - self._outToUDP = (config.get(sec, '_outToUDP') == "True") - self._gateway = config.get(sec, '_gateway') - self._gateway_port = int(config.get(sec, '_gateway_port')) - _tgs = config.get(sec, '_tg_filter') - self._tg_filter = map(int, _tgs.split(',')) + + 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') + sec = 'DEFAULTS' + self._debug = bool(self.defaultOption(config, sec,'debug', self._debug) == 'True') + self._outToFile = bool(self.defaultOption(config, sec,'outToFile', self._outToFile) == 'True') + self._outToUDP = bool(self.defaultOption(config, sec,'outToUDP', self._outToUDP) == 'True') + self._gateway = self.defaultOption(config, sec,'gateway', self._gateway) + self._gateway_port = int(self.defaultOption(config, sec,'toGatewayPort', self._gateway_port)) + + self._remote_control_port = int(self.defaultOption(config, sec,'remoteControlPort', self._remote_control_port)) + self._ambeRxPort = int(self.defaultOption(config, sec,'fromGatewayPort', self._ambeRxPort)) + self._gateway_dmr_id = int(self.defaultOption(config, sec, 'gatewayDmrId', self._gateway_dmr_id)) + + _tgs = self.defaultOption(config, sec,'tgFilter', str(self._tg_filter).strip('[]')) + self._tg_filter = map(int, _tgs.split(',')) + + self._tx_tg = hex_str_3(int(self.defaultOption(config, sec, 'txTg', int_id(self._tx_tg)))) + self._tx_ts = int(self.defaultOption(config, sec, 'txTs', self._tx_ts)) + + except ConfigParser.NoOptionError as e: + print('Using a default value:', e) except: traceback.print_exc() sys.exit('Configuration file \''+configFileName+'\' is not a valid configuration file! Exiting...') + def rewriteFrame( self, _frame, _network, _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] + + ######################################################################## + # 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(_frame[9:12], _newGroup) + + # 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:] + 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:] + + _frame = self.hashed_packet(NETWORK[_network]['LOCAL']['AUTH_KEY'], _frame) + + if (time() - self._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.send_to_ipsc(_frame) + else: + logger.info('Slot {} is busy, will not transmit packet from gateway'.format(_newSlot)) + + ######################################################################## + + # 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 + + # Read bytes from the socket with "timeout" I hate this code. + def readSock( self, _sock, len ): + counter = 0 + while(counter < 3): + _ambe = _sock.recv(len) + if _ambe: break + sleep(0.1) + counter = counter + 1 + return _ambe + + # Concatenate 3 frames from the stream into a bit array and return the bytes + def readAmbeFrameFromUDP( self, _sock ): + _ambeAll = BitArray() # Start with an empty array + for i in range(0, 3): + _ambe = self.readSock(_sock,7) # Read AMBE from the socket + if _ambe: + _ambe1 = BitArray('0x'+h(_ambe[0:49])) + _ambeAll += _ambe1[0:50] # Append the 49 bits to the string + else: + break + return _ambeAll.tobytes() # Return the 49 * 3 as an array of bytes + + # Set up the socket and run the method to gather the AMBE. Sending it to all peers + def launchUDP(self, _network): + s = socket.socket() # Create a socket object + s.bind(('', self._ambeRxPort)) # Bind to the port + + while (1): # Forever! + s.listen(5) # Now wait for client connection. + _sock, addr = s.accept() # Establish connection with client. + if int_id(self._tx_tg) > 0: # Test if we are allowed to transmit + self.playbackFromUDP(_sock, _network) + else: + self.transmitDisabled(_sock, _network) #tg is zero, so just eat the network trafic + _sock.close() + + # This represents a full transmission (HEAD, VOICE and TERM) + def playbackFromUDP(self, _sock, _network): + _delay = 0.055 # Yes, I know it should be 0.06, but there seems to be some latency, so this is a hack + _src_sub = hex_str_3(self._gateway_dmr_id) # DMR ID to sign this transmission with + _src_peer = NETWORK[_network]['LOCAL']['RADIO_ID'] # Use this peers ID as the source repeater + + logger.info('Transmit from gateway to TG {}:'.format(int_id(self._tx_tg)) ) + try: + + try: + _t = open('template.bin', 'rb') # Open the template file. This was recorded OTA + + _tempHead = [0] * 3 # It appears that there 3 frames of HEAD (mostly the same) + for i in range(0, 3): + _tempHead[i] = self.readRecord(_t, BURST_DATA_TYPE['VOICE_HEAD']) + + _tempVoice = [0] * 6 + for i in range(0, 6): # Then there are 6 frames of AMBE. We will just use them in order + _tempVoice[i] = self.readRecord(_t, BURST_DATA_TYPE['SLOT2_VOICE']) + + _tempTerm = self.readRecord(_t, BURST_DATA_TYPE['VOICE_TERM']) + _t.close() + except IOError: + logger.error('Can not open template.bin file') + return + logger.debug('IPSC templates loaded') + + _eof = False + self._seq = randint(0,32767) # A transmission uses a random number to begin its sequence (16 bit) + + for i in range(0, 3): # Output the 3 HEAD frames to our peers + self.rewriteFrame(_tempHead[i], _network, self._tx_ts, self._tx_tg, _src_sub, _src_peer) + #self.group_voice(_network, _src_sub, self._tx_tg, True, '', hex_str_3(0), _tempHead[i]) + sleep(_delay) + + i = 0 # Initialize the VOICE template index + while(_eof == False): + _ambe = self.readAmbeFrameFromUDP(_sock) # Read the 49*3 bit sample from the stream + if _ambe: + i = (i + 1) % 6 # Round robbin with the 6 VOICE templates + _frame = _tempVoice[i][:33] + _ambe + _tempVoice[i][52:] # Insert the 3 49 bit AMBE frames + + self.rewriteFrame(_frame, _network, self._tx_ts, self._tx_tg, _src_sub, _src_peer) + #self.group_voice(_network, _src_sub, self._tx_tg, True, '', hex_str_3(0), _frame) + + sleep(_delay) # Since this comes from a file we have to add delay between IPSC frames + else: + _eof = True # There are no more AMBE frames, so terminate the loop + + self.rewriteFrame(_tempTerm, _network, self._tx_ts, self._tx_tg, _src_sub, _src_peer) + #self.group_voice(_network, _src_sub, self._tx_tg, True, '', hex_str_3(0), _tempTerm) + + except IOError: + logger.error('Can not transmit to peers') + logger.info('Transmit complete') + + def transmitDisabled(self, _sock, _network): + _eof = False + logger.debug('Transmit disabled begin') + while(_eof == False): + if self.readAmbeFrameFromUDP(_sock): + pass + else: + _eof = True # There are no more AMBE frames, so terminate the loop + logger.debug('Transmit disabled end') + + # Debug method used to test the AMBE code. + def playbackFromFile(self, _fileName): + _r = open(_fileName, 'rb') + _eof = False + + host = socket.gethostbyname(socket.gethostname()) # Get local machine name + _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _sock.connect((host, self._ambeRxPort)) + + while(_eof == False): + + for i in range(0, 3): + _ambe = _r.read(7) + if _ambe: + _sock.send(_ambe) + else: + _eof = True + sleep(0.055) + logger.info('File playback complete') + + def dumpTemplate(self, _fileName): + _file = open(_fileName, 'rb') + _eof = False + + while(_eof == False): + _data = "" + _bLen = _file.read(4) + if _bLen: + _len, = struct.unpack("i", _bLen) + if _len > 0: + _data = _file.read(_len) + self.dumpIPSCFrame(_data) + else: + _eof = True + logger.info('File dump complete') + #************************************************ # CALLBACK FUNCTIONS FOR USER PACKET TYPES #************************************************ # def group_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data): + + #self.dumpIPSCFrame(_data) + # THIS FUNCTION IS NOT COMPLETE!!!! _payload_type = _data[30:31] # _ambe_frames = _data[33:52] @@ -128,91 +389,222 @@ class ambeIPSC(IPSC): _ambe_frame3 = _ambe_frames[100:149] _tg_id = int_id(_dst_sub) + _ts = 2 if _ts else 1 + + self._busy_slots[_ts] = time() + + ###### DEBUGDEBUGDEBUG +# if _tg_id == 2: +# __iLen = len(_data) +# self._d.write(struct.pack("i", __iLen)) +# self._d.write(_data) +# else: +# self.rewriteFrame(_data, _network, 1, 9) + ###### DEBUGDEBUGDEBUG + + if _tg_id in self._tg_filter: #All TGs _dst_sub = get_info(int_id(_dst_sub), talkgroup_ids) if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']: if self._currentTG == self._no_tg: - _src_sub = get_info(int_id(_src_sub), subscriber_ids) - print('Voice Transmission Start on TS {} and TG {} ({}) from {}'.format("2" if _ts else "1", _dst_sub, _tg_id, _src_sub)) + _src_sub = get_subscriber_info(_src_sub) + logger.info('Voice Transmission Start on TS {} and TG {} ({}) from {}'.format(_ts, _dst_sub, _tg_id, _src_sub)) + self._sock.sendto('reply log2 {} {}'.format(_src_sub, _tg_id), (self._dmrgui, 34003)) + self._currentTG = _tg_id self._transmitStartTime = time() + self._start_seq = int_id(_data[20:22]) + self._packet_count = 0 else: if self._currentTG != _tg_id: if time() > self.lastPacketTimeout: self._currentTG = self._no_tg #looks like we never saw an EOT from the last stream - print('EOT timeout') + logger.warning('EOT timeout') else: - print('Transmission in progress, will not decode stream on TG {}'.format(_tg_id)) + logger.warning('Transmission in progress, will not decode stream on TG {}'.format(_tg_id)) if self._currentTG == _tg_id: if _payload_type == BURST_DATA_TYPE['VOICE_TERM']: - print('Voice Transmission End %.2f seconds' % (time() - self._transmitStartTime)) + _source_packets = ( int_id(_data[20:22]) - self._start_seq ) - 3 # the 3 is because the start and end are not part of the voice but counted in the RTP + if self._packet_count > _source_packets: + self._packet_count = _source_packets + if _source_packets > 0: + _lost_percentage = 100.0 - ((self._packet_count / float(_source_packets)) * 100.0) + else: + _lost_percentage = 0.0 + _duration = (time() - self._transmitStartTime) + logger.info('Voice Transmission End {:.2f} seconds loss rate: {:.2f}% ({}/{})'.format(_duration, _lost_percentage, _source_packets - self._packet_count, _source_packets)) + self._sock.sendto("reply log" + + strftime(" %m/%d/%y %H:%M:%S", localtime(self._transmitStartTime)) + + ' {} {} "{}"'.format(get_subscriber_info(_src_sub), _ts, _dst_sub) + + ' {:.2f}%'.format(_lost_percentage) + + ' {:.2f}s'.format(_duration), (self._dmrgui, 34003)) self._currentTG = self._no_tg if _payload_type == BURST_DATA_TYPE['SLOT1_VOICE']: self.outputFrames(_ambe_frames, _ambe_frame1, _ambe_frame2, _ambe_frame3) + self._packet_count += 1 if _payload_type == BURST_DATA_TYPE['SLOT2_VOICE']: self.outputFrames(_ambe_frames, _ambe_frame1, _ambe_frame2, _ambe_frame3) + self._packet_count += 1 self.lastPacketTimeout = time() + 10 else: if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']: _dst_sub = get_info(int_id(_dst_sub), talkgroup_ids) - print('Ignored Voice Transmission Start on TS {} and TG {}'.format("2" if _ts else "1", _dst_sub)) + logger.warning('Ignored Voice Transmission Start on TS {} and TG {}'.format(_ts, _dst_sub)) def outputFrames(self, _ambe_frames, _ambe_frame1, _ambe_frame2, _ambe_frame3): if self._debug == True: - print(_ambe_frames) - print('Frame 1:', self.ByteToHex(_ambe_frame1.tobytes())) - print('Frame 2:', self.ByteToHex(_ambe_frame2.tobytes())) - print('Frame 3:', self.ByteToHex(_ambe_frame3.tobytes())) + logger.debug(_ambe_frames) + logger.debug('Frame 1:', self.ByteToHex(_ambe_frame1.tobytes())) + logger.debug('Frame 2:', self.ByteToHex(_ambe_frame2.tobytes())) + logger.debug('Frame 3:', self.ByteToHex(_ambe_frame3.tobytes())) if self._outToFile == True: - f.write( _ambe_frame1.tobytes() ) - f.write( _ambe_frame2.tobytes() ) - f.write( _ambe_frame3.tobytes() ) + self._f.write( _ambe_frame1.tobytes() ) + self._f.write( _ambe_frame2.tobytes() ) + self._f.write( _ambe_frame3.tobytes() ) if self._outToUDP == True: self._sock.sendto(_ambe_frame1.tobytes(), (self._gateway, self._gateway_port)) self._sock.sendto(_ambe_frame2.tobytes(), (self._gateway, self._gateway_port)) self._sock.sendto(_ambe_frame3.tobytes(), (self._gateway, self._gateway_port)) - - def reread_subscribers(self): - try: - with open(PATH+'subscriber_ids.csv', 'rU') as subscriber_ids_csv: - subscribers = csv.reader(subscriber_ids_csv, dialect='excel', delimiter=',') - subscriber_ids = {} - for row in subscribers: - subscriber_ids[int(row[0])] = (row[1]) - print('Subscriber file has been updated') - except ImportError: - logger.warning('subscriber_ids.csv not found: Subscriber aliases will not be available') + def private_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data): + print('private voice') +# __iLen = len(_data) +# self._d.write(struct.pack("i", __iLen)) +# self._d.write(_data) # - # Define a function for the thread - # Use netcat to dynamically change the TGs that are forwarded to Allstar - # echo -n "x,y,z" | nc 127.0.0.1 1235 + # Remote control thread + # Use netcat to dynamically change ambe_audio without a restart + # echo -n "tgs=x,y,z" | nc 127.0.0.1 31002 + # echo -n "reread_subscribers" | nc 127.0.0.1 31002 + # echo -n "reread_config" | nc 127.0.0.1 31002 + # echo -n "txTg=##" | nc 127.0.0.1 31002 + # echo -n "txTs=#" | nc 127.0.0.1 31002 + # echo -n "section=XX" | nc 127.0.0.1 31002 # def remote_control(self, port): s = socket.socket() # Create a socket object - host = socket.gethostname() # Get local machine name - s.bind((host, port)) # Bind to the port + s.bind(('', port)) # Bind to the port s.listen(5) # Now wait for client connection. - print('Remote control is listening on:', host, port) + logger.info('Remote control is listening on {}:{}'.format(socket.getfqdn(), port)) + while True: c, addr = s.accept() # Establish connection with client. - print( 'Got connection from', addr ) - tgs = c.recv(1024) - print('Command:"{}"'.format(tgs)) - if tgs: - if tgs == 'reread_subscribers': - self.reread_subscribers() + logger.info( 'Got connection from {}'.format(addr) ) + self._dmrgui = addr[0] + _tmp = c.recv(1024) + _tmp = _tmp.split(None)[0] #first get rid of whitespace + _cmd = _tmp.split('=')[0] + logger.info('Command:"{}"'.format(_cmd)) + if _cmd: + if _cmd == 'reread_subscribers': + reread_subscribers() + elif _cmd == 'reread_config': + self.readConfigFile(self._configFile, None, self._currentNetwork) + elif _cmd == 'txTg': + self._tx_tg = hex_str_3(int(_tmp.split('=')[1])) + print('New txTg = ' + str(int_id(self._tx_tg))) + elif _cmd == 'txTs': + self._tx_ts = int(_tmp.split('=')[1]) + print('New txTs = ' + str(self._tx_ts)) + elif _cmd == 'section': + self.readConfigFile(self._configFile, _tmp.split('=')[1]) + elif _cmd == 'gateway_dmr_id': + self._gateway_dmr_id = int(_tmp.split('=')[1]) + print('New gateway_dmr_id = ' + str(self._gateway_dmr_id)) + elif _cmd == 'gateway_peer_id': + peerID = int(_tmp.split('=')[1]) + NETWORK[_network]['LOCAL']['RADIO_ID'] = hex_str_3(peerID) + print('New peer_id = ' + str(peerID)) + elif _cmd == 'restart': + reactor.callFromThread(reactor.stop) + elif _cmd == 'playbackFromFile': + self.playbackFromFile('ambe.bin') + elif _cmd == 'tgs': + _args = _tmp.split('=')[1] + self._tg_filter = map(int, _args.split(',')) + logger.info( 'New TGs={}'.format(self._tg_filter) ) + elif _cmd == 'dump_template': + self.dumpTemplate('PrivateVoice.bin') + elif _cmd == 'get_info': + self._sock.sendto('reply dmr_info {} {} {} {}'.format(self._currentNetwork, + int_id(NETWORK[self._currentNetwork]['LOCAL']['RADIO_ID']), + self._gateway_dmr_id, + get_subscriber_info(hex_str_3(self._gateway_dmr_id))), (self._dmrgui, 34003)) + elif _cmd == 'eval': + _sz = len(_tmp)-5 + _evalExpression = _tmp[-_sz:] + _evalResult = eval(_evalExpression) + print("eval of {} is {}".format(_evalExpression, _evalResult)) + self._sock.sendto('reply eval {}'.format(_evalResult), (self._dmrgui, 34003)) + elif _cmd == 'exec': + _sz = len(_tmp)-5 + _evalExpression = _tmp[-_sz:] + exec(_evalExpression) + print("exec of {}".format(_evalExpression)) else: - self._tg_filter = map(int, tgs.split(',')) - print( 'New TGs=', self._tg_filter ) + logger.error('Unknown command') c.close() # Close the connection + #************************************************ + # Debug: print IPSC frame on console + #************************************************ + def dumpIPSCFrame( self, _frame ): + + _packettype = int_id(_frame[0:1]) # int8 GROUP_VOICE, PVT_VOICE, GROUP_DATA, PVT_DATA, CALL_MON_STATUS, CALL_MON_RPT, CALL_MON_NACK, XCMP_XNL, RPT_WAKE_UP, DE_REG_REQ + _peerid = int_id(_frame[1:5]) # int32 peer who is sending us a packet + _ipsc_seq = int_id(_frame[5:6]) # int8 looks like a sequence number for a packet + _src_sub = int_id(_frame[6:9]) # int32 Id of source + _dst_sub = int_id(_frame[9:12]) # int32 Id of destination + _call_type = int_id(_frame[12:13]) # int8 Priority Voice/Data + _call_ctrl_info = int_id(_frame[13:17]) # int32 + _call_info = int_id(_frame[17:18]) # int8 Bits 6 and 7 defined as TS and END + + # parse out the RTP values + _rtp_byte_1 = int_id(_frame[18:19]) # Call Ctrl Src + _rtp_byte_2 = int_id(_frame[19:20]) # Type + _rtp_seq = int_id(_frame[20:22]) # Call Seq No + _rtp_tmstmp = int_id(_frame[22:26]) # Timestamp + _rtp_ssid = int_id(_frame[26:30]) # Sync Src Id + + _payload_type = _frame[30] # int8 VOICE_HEAD, VOICE_TERM, SLOT1_VOICE, SLOT2_VOICE + + _ts = bool(_call_info & TS_CALL_MSK) + _end = bool(_call_info & END_MSK) + + if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']: + print('HEAD:', h(_frame)) + if _payload_type == BURST_DATA_TYPE['VOICE_TERM']: + + _ipsc_rssi_threshold_and_parity = int_id(_frame[31]) + _ipsc_length_to_follow = int_id(_frame[32:34]) + _ipsc_rssi_status = int_id(_frame[34]) + _ipsc_slot_type_sync = int_id(_frame[35]) + _ipsc_data_size = int_id(_frame[36:38]) + _ipsc_data = _frame[38:38+(_ipsc_length_to_follow * 2)-4] + _ipsc_full_lc_byte1 = int_id(_frame[38]) + _ipsc_full_lc_fid = int_id(_frame[39]) + _ipsc_voice_pdu_service_options = int_id(_frame[40]) + _ipsc_voice_pdu_dst = int_id(_frame[41:44]) + _ipsc_voice_pdu_src = int_id(_frame[44:47]) + + print('{} {} {} {} {} {} {} {} {} {} {}'.format(_ipsc_rssi_threshold_and_parity,_ipsc_length_to_follow,_ipsc_rssi_status,_ipsc_slot_type_sync,_ipsc_data_size,h(_ipsc_data),_ipsc_full_lc_byte1,_ipsc_full_lc_fid,_ipsc_voice_pdu_service_options,_ipsc_voice_pdu_dst,_ipsc_voice_pdu_src)) + print('TERM:', h(_frame)) + if _payload_type == BURST_DATA_TYPE['SLOT1_VOICE']: + _rtp_len = _frame[31:32] + _ambe = _frame[33:52] + print('SLOT1:', h(_frame)) + if _payload_type == BURST_DATA_TYPE['SLOT2_VOICE']: + _rtp_len = _frame[31:32] + _ambe = _frame[33:52] + print('SLOT2:', h(_frame)) + print("pt={:02X} pid={} seq={:02X} src={} dst={} ct={:02X} uk={} ci={} rsq={}".format(_packettype, _peerid,_ipsc_seq, _src_sub,_dst_sub,_call_type,_call_ctrl_info,_call_info,_rtp_seq)) + if __name__ == '__main__': logger.info('DMRlink \'ambe_audio.py\' (c) 2015 N0MJS & the K0USY Group - SYSTEM STARTING...') for ipsc_network in NETWORK: diff --git a/ambe_audio_commands.txt b/ambe_audio_commands.txt new file mode 100644 index 0000000..55e698e --- /dev/null +++ b/ambe_audio_commands.txt @@ -0,0 +1,31 @@ +AllStar DTMF command examples: +82=cmd,/bin/bash -c 'do something here' +82=cmd,/bin/bash -c 'echo -n "section=Shutup" | nc 127.0.0.1 31002' + +Shell command examples: +# Use netcat to dynamically change ambe_audio without a restart +# echo -n "tgs=x,y,z" | nc 127.0.0.1 31002 +# echo -n "reread_subscribers" | nc 127.0.0.1 31002 +# echo -n "reread_config" | nc 127.0.0.1 31002 +# echo -n "txTg=##" | nc 127.0.0.1 31002 +# echo -n "txTs=#" | nc 127.0.0.1 31002 +# echo -n "section=XX" | nc 127.0.0.1 31002 + +Remote control commands: +'reread_subscribers' +'reread_config' +'txTg' +'txTs' +'section' +'gateway_dmr_id' +'gateway_peer_id' +'restart' +'playbackFromFile' +'tgs' +'dump_template' +'get_info' + + + + + diff --git a/dmrlink.py b/dmrlink.py index 7522d4e..d25534e 100755 --- a/dmrlink.py +++ b/dmrlink.py @@ -289,30 +289,47 @@ subscriber_ids = {} peer_ids = {} talkgroup_ids = {} -try: - with open(PATH+'subscriber_ids.csv', 'rU') as subscriber_ids_csv: - subscribers = csv.reader(subscriber_ids_csv, dialect='excel', delimiter=',') - for row in subscribers: - subscriber_ids[int(row[0])] = (row[1]) -except ImportError: - logger.warning('subscriber_ids.csv not found: Subscriber aliases will not be available') +def reread_peers(): + global peer_ids + try: + with open(PATH+'peer_ids.csv', 'rU') as peer_ids_csv: + peers = csv.reader(peer_ids_csv, dialect='excel', delimiter=',') + peer_ids = {} + for row in peers: + peer_ids[int(row[0])] = (row[1]) + except ImportError: + logger.warning('peer_ids.csv not found: Peer aliases will not be available') -try: - with open(PATH+'peer_ids.csv', 'rU') as peer_ids_csv: - peers = csv.reader(peer_ids_csv, dialect='excel', delimiter=',') - for row in peers: - peer_ids[int(row[0])] = (row[1]) -except ImportError: - logger.warning('peer_ids.csv not found: Peer aliases will not be available') +def reread_talkgroups(): + global talkgroup_ids + try: + with open(PATH+'talkgroup_ids.csv', 'rU') as talkgroup_ids_csv: + talkgroups = csv.reader(talkgroup_ids_csv, dialect='excel', delimiter=',') + talkgroup_ids = {} + for row in talkgroups: + talkgroup_ids[int(row[1])] = (row[0]) + except ImportError: + logger.warning('talkgroup_ids.csv not found: Talkgroup aliases will not be available') -try: - with open(PATH+'talkgroup_ids.csv', 'rU') as talkgroup_ids_csv: - talkgroups = csv.reader(talkgroup_ids_csv, dialect='excel', delimiter=',') - for row in talkgroups: - talkgroup_ids[int(row[1])] = (row[0]) -except ImportError: - logger.warning('talkgroup_ids.csv not found: Talkgroup aliases will not be available') +def reread_subscribers(): + global subscriber_ids + try: + with open(PATH+'subscriber_ids.csv', 'rU') as subscriber_ids_csv: + subscribers = csv.reader(subscriber_ids_csv, dialect='excel', delimiter=',') + subscriber_ids = {} + for row in subscribers: + subscriber_ids[int(row[0])] = (row[1]) + print('Subscriber file has been updated.', len(subscriber_ids), 'IDs imported') + except ImportError: + logger.warning('subscriber_ids.csv not found: Subscriber aliases will not be available') + +reread_peers() +reread_talkgroups() +reread_subscribers() + +def get_subscriber_info(_src_sub): + return get_info(int_id(_src_sub), subscriber_ids) #************************************************ # UTILITY FUNCTIONS FOR INTERNAL USE @@ -449,8 +466,8 @@ def process_flags_bytes(_hex_flags): 'VOICE': _voice, 'MASTER': _master } - - + + # 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. # @@ -601,7 +618,7 @@ if REPORTS['REPORT_NETWORKS'] == 'PICKLE': file.close() except IOError as detail: logger.error('I/O Error: %s', detail) - + elif REPORTS['REPORT_NETWORKS'] == 'PRINT': def reporting_loop(): logger.debug('Periodic Reporting Loop Started (PRINT)') @@ -813,7 +830,7 @@ class IPSC(DatagramProtocol): if _peerid in self._peers.keys(): self._peers[_peerid]['STATUS']['CONNECTED'] = True logger.info('(%s) Registration Reply From: %s, %s:%s', self._network, int_id(_peerid), self._peers[_peerid]['IP'], self._peers[_peerid]['PORT']) - + # OUR MASTER HAS ANSWERED OUR KEEP-ALIVE REQUEST - KEEP TRACK OF IT def master_alive_reply(self, _peerid): self.reset_keep_alive(_peerid) @@ -1117,14 +1134,15 @@ class IPSC(DatagramProtocol): _packettype = data[0:1] _peerid = data[1:5] _ipsc_seq = data[5:6] - + # AUTHENTICATE THE PACKET if not self.validate_auth(self._local['AUTH_KEY'], data): logger.warning('(%s) AuthError: IPSC packet failed authentication. Type %s: Peer: %s, %s:%s', self._network, h(_packettype), int_id(_peerid), host, port) - return + # return # REMOVE SHA-1 AUTHENTICATION HASH: WE NO LONGER NEED IT - data = self.strip_hash(data) + else: + data = self.strip_hash(data) # PACKETS THAT WE RECEIVE FROM ANY VALID PEER OR VALID MASTER if _packettype in ANY_PEER_REQUIRED: diff --git a/template.bin b/template.bin new file mode 100644 index 0000000..89bc05c Binary files /dev/null and b/template.bin differ