From 6aee60c84ec957ac8d74f51fe2f633164b448b9e Mon Sep 17 00:00:00 2001 From: Mike Zingman Date: Sun, 18 Jun 2017 20:37:03 -0400 Subject: [PATCH] IPSC_Bridge Updates --- IPSC_Bridge.cfg | 22 ++ IPSC_Bridge.py | 350 ++++++++++++++++++++ ambe_audio.cfg | 54 ---- ambe_audio.py | 678 -------------------------------------- ambe_audio_commands.txt | 31 -- ambe_bridge.py | 701 ++++++++++++++++++++++++++++++++++++++++ ambe_utils.py | 279 ++++++++++++++++ 7 files changed, 1352 insertions(+), 763 deletions(-) create mode 100644 IPSC_Bridge.cfg create mode 100644 IPSC_Bridge.py delete mode 100644 ambe_audio.cfg delete mode 100755 ambe_audio.py delete mode 100644 ambe_audio_commands.txt create mode 100644 ambe_bridge.py create mode 100644 ambe_utils.py diff --git a/IPSC_Bridge.cfg b/IPSC_Bridge.cfg new file mode 100644 index 0000000..95f6c59 --- /dev/null +++ b/IPSC_Bridge.cfg @@ -0,0 +1,22 @@ +################################## +# IPSC_Bridge 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 IPSC_Bridge operation) + +gateway = 127.0.0.1 # IP address of Partner Application (HB_Bridge, Analog_Bridge) +fromGatewayPort = 31000 # Port IPSC_Bridge is listening on for data (IPSC_Bridge <--- Partner) +toGatewayPort = 31003 # Port Partner is listening on for data (IPSC_Bridge ---> Partner) + +gatewayDmrId = 12345 # Deprecated no longer used. Required for now. + +# remoteControlPort = 31002 # Deprecated, no longer used. Will be removed IGNORE! +# tgFilter = 2,3,9 # Deprecated, no longer used. Will be removed IGNORE! +# txTg = 9 # Deprecated, no longer used. Will be removed IGNORE! +# txTs = 2 # Deprecated, no longer used. Will be removed IGNORE! \ No newline at end of file diff --git a/IPSC_Bridge.py b/IPSC_Bridge.py new file mode 100644 index 0000000..413dd8d --- /dev/null +++ b/IPSC_Bridge.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python +# +############################################################################### +# Copyright (C) 2016 Cortney T. Buffington, N0MJS +# and +# 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 +############################################################################### + +# This is a bridge application for IPSC networks. It knows how to export AMBE +# 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 +from bitstring import BitArray + +import sys, socket, ConfigParser, thread, traceback +import cPickle as pickle + +from dmrlink import IPSC, systems +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 + +__author__ = 'Cortney T. Buffington, N0MJS' +__copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group' +__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski, KD8EYF; Robert Garcia, N5QM; Steve Zingman, N4IRS; Mike Zingman, N4IRR' +__license__ = 'GNU GPLv3' +__maintainer__ = 'Cort Buffington, N0MJS' +__email__ = 'n0mjs@me.com' +__version__ = '20170609' + + +try: + from ipsc.ipsc_const import * +except ImportError: + sys.exit('IPSC constants 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='IPSC_Bridge.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 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 + _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 # 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 Analog_Bridge + 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 = '' + cc = 1 + ipsc_seq = 0 + + ###### DEBUGDEBUGDEBUG + #_d = None + ###### DEBUGDEBUGDEBUG + + def __init__(self, _name, _config, _logger): + IPSC.__init__(self, _name, _config, _logger) + self.CALL_DATA = [] + + # + # Define default values for operation. These will be overridden by the .cfg file if found + # + + self._currentTG = self._no_tg + self._currentNetwork = str(_name) + self.readConfigFile(self._configFile, None, self._currentNetwork) + + logger.info('DMRLink IPSC Bridge') + if self._gateway_dmr_id == 0: + sys.exit( "Error: gatewayDmrId must be set (greater than zero)" ) + # + # Open output sincs + # + if self._outToFile == True: + 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) + logger.info('Send UDP frames to Partner Bridge {}:{}'.format(self._gateway, self._gateway_port)) + + self.ipsc_ambe = AMBE_IPSC(self, _name, _config, _logger, self._ambeRxPort) + + def get_globals(self): + return (subscriber_ids, talkgroup_ids, peer_ids) + + def get_repeater_id(self, import_id): + return self._config['LOCAL']['RADIO_ID'] + + # Utility function to convert bytes to string of hex values (for debug) + def ByteToHex( self, byteStr ): + return ''.join( [ "%02X " % ord(x) for x in byteStr ] ).strip() + + # + # Now read the configuration file and parse out the values we need + # + 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: + config.read(configFileName) + + 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...') + + #************************************************ + # CALLBACK FUNCTIONS FOR USER PACKET TYPES + #************************************************ + # + + def group_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data): + _tx_slot = self.ipsc_ambe.tx[_ts] + _payload_type = _data[30:31] + _seq = int_id(_data[20:22]) + _tx_slot.frame_count += 1 + if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']: + _stream_id = int_id(_data[5:6]) # int8 looks like a sequence number for a packet + if (_stream_id != _tx_slot.stream_id): + self.ipsc_ambe.begin_call(_ts, _src_sub, _dst_sub, _peerid, self.cc, _seq, _stream_id) + _tx_slot.lastSeq = _seq + if _payload_type == BURST_DATA_TYPE['VOICE_TERM']: + self.ipsc_ambe.end_call(_tx_slot) + if (_payload_type == BURST_DATA_TYPE['SLOT1_VOICE']) or (_payload_type == BURST_DATA_TYPE['SLOT2_VOICE']): + _ambe_frames = BitArray('0x'+h(_data[33:52])) + _ambe_frame1 = _ambe_frames[0:49] + _ambe_frame2 = _ambe_frames[50:99] + _ambe_frame3 = _ambe_frames[100:149] + self.ipsc_ambe.export_voice(_tx_slot, _seq, _ambe_frame1.tobytes() + _ambe_frame2.tobytes() + _ambe_frame3.tobytes()) + + + def private_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data): + print('private voice') + + #************************************************ + # 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__': + import argparse + import os + import sys + import signal + from dmr_utils.utils import try_download, mk_id_dict + + import dmrlink_log + import dmrlink_config + + # 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='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)') + parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.') + parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.') + cli_args = parser.parse_args() + + if not cli_args.CFG_FILE: + 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) + + # 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.info('DMRlink \'IPSC_Bridge.py\' (c) 2015 N0MJS & the K0USY Group - SYSTEM STARTING...') + logger.info('Version %s', __version__) + + # ID ALIAS CREATION + # Download + 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') + + # Shut ourselves down gracefully with the IPSC peers. + 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) + 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] = ambeIPSC(system, CONFIG, logger) + reactor.listenUDP(CONFIG['SYSTEMS'][system]['LOCAL']['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['LOCAL']['IP']) + + reactor.run() diff --git a/ambe_audio.cfg b/ambe_audio.cfg deleted file mode 100644 index efc1226..0000000 --- a/ambe_audio.cfg +++ /dev/null @@ -1,54 +0,0 @@ -################################################ -# 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 deleted file mode 100755 index 8594f37..0000000 --- a/ambe_audio.py +++ /dev/null @@ -1,678 +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 -############################################################################### - -# This is a sample applicaiton that dumps all raw AMBE+2 voice frame data -# It is useful for things like, decoding the audio stream with a DVSI dongle, etc. - -from __future__ import print_function -from twisted.internet import reactor -from binascii import b2a_hex as h -from bitstring import BitArray - -import sys, socket, ConfigParser, thread, traceback -import cPickle as pickle - -from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports -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 - -__author__ = 'Cortney T. Buffington, N0MJS' -__copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group' -__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski, KD8EYF; Robert Garcia, N5QM; Steve Zingman, N4IRS; Mike Zingman, N4IRR' -__license__ = 'GNU GPLv3' -__maintainer__ = 'Cort Buffington, N0MJS' -__email__ = 'n0mjs@me.com' - - - -try: - from ipsc.ipsc_const import * -except ImportError: - sys.exit('IPSC constants 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' # 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" # 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 # 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, _name, _config, _logger, _report): - IPSC.__init__(self, _name, _config, _logger, _report) - self.CALL_DATA = [] - - # - # Define default values for operation. These will be overridden by the .cfg file if found - # - - self._currentTG = self._no_tg - self._currentNetwork = str(_name) - self.readConfigFile(self._configFile, None, self._currentNetwork) - - 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: - 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) - 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, ) ) # Listen for remote control commands - thread.start_new_thread( self.launchUDP, (_name, ) ) # Package AMBE into IPSC frames and send to all peers - except: - traceback.print_exc() - logger.error( "Error: unable to start thread" ) - - - # Utility function to convert bytes to string of hex values (for debug) - def ByteToHex( self, byteStr ): - return ''.join( [ "%02X " % ord(x) for x in byteStr ] ).strip() - - # - # Now read the configuration file and parse out the values we need - # - 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: - config.read(configFileName) - - 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, _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:] - - 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, _name): - 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) # SSZ was here. - else: - self.transmitDisabled(_sock, self._system) #tg is zero, so just eat the network trafic - _sock.close() - - # This represents a full transmission (HEAD, VOICE and TERM) - def playbackFromUDP(self, _sock): - _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 = self._config['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], self._tx_ts, self._tx_tg, _src_sub, _src_peer) - #self.group_voice(self._system, _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, self._tx_ts, self._tx_tg, _src_sub, _src_peer) - #self.group_voice(self._system, _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, self._tx_ts, self._tx_tg, _src_sub, _src_peer) - #self.group_voice(self._system, _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): - _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, _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] - _ambe_frames = BitArray('0x'+h(_data[33:52])) - _ambe_frame1 = _ambe_frames[0:49] - _ambe_frame2 = _ambe_frames[50:99] - _ambe_frame3 = _ambe_frames[100:149] - - _tg_id = int_id(_dst_sub) - - 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, 1, 9) - ###### DEBUGDEBUGDEBUG - - - if _tg_id in self._tg_filter: #All TGs - _dst_sub = get_alias(_dst_sub, talkgroup_ids) - if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']: - if self._currentTG == self._no_tg: - _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 - logger.warning('EOT timeout') - else: - 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']: - _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_alias(_dst_sub, talkgroup_ids) - 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: - 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: - 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 private_voice(self, _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) - - # - # 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 - - s.bind(('', port)) # Bind to the port - s.listen(5) # Now wait for client connection. - logger.info('Remote control is listening on {}:{}'.format(socket.getfqdn(), port)) - - while True: - c, addr = s.accept() # Establish connection with client. - 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]) - self._config['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_alias': - self._sock.sendto('reply dmr_info {} {} {} {}'.format(self._currentNetwork, - int_id(self._CONFIG[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: - 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)) - -def get_subscriber_info(_src_sub): - return get_info(int_id(_src_sub), subscriber_ids) - -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]))) - - # 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='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)') - parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.') - parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.') - cli_args = parser.parse_args() - - if not cli_args.CFG_FILE: - cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg' - - # Call the external routine to build the configuration dictionary - 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 = config_logging(CONFIG['LOGGER']) - logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...') - - # 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: - systems[system].de_register_self() - reactor.stop() - - 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) - - # Build ID Aliases - peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger) - - # INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC - systems = mk_ipsc_systems(CONFIG, logger, systems, ambeIPSC, report_server) - - - - # INITIALIZATION COMPLETE -- START THE REACTOR - reactor.run() \ No newline at end of file diff --git a/ambe_audio_commands.txt b/ambe_audio_commands.txt deleted file mode 100644 index 55e698e..0000000 --- a/ambe_audio_commands.txt +++ /dev/null @@ -1,31 +0,0 @@ -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/ambe_bridge.py b/ambe_bridge.py new file mode 100644 index 0000000..9cd9df7 --- /dev/null +++ b/ambe_bridge.py @@ -0,0 +1,701 @@ +#!/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 new file mode 100644 index 0000000..acfaad9 --- /dev/null +++ b/ambe_utils.py @@ -0,0 +1,279 @@ +#!/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()