From 6aee60c84ec957ac8d74f51fe2f633164b448b9e Mon Sep 17 00:00:00 2001
From: Mike Zingman <mike.zingman@gmail.com>
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 <n0mjs@me.com>
+#   and
+#   Copyright (C) 2017  Mike Zingman, N4IRR <Not.A.Chance@NoWhere.com>
+#
+#   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 <n0mjs@me.com>
-#
-#   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()