IPSC_Bridge Updates

This commit is contained in:
Mike Zingman 2017-06-18 20:37:03 -04:00
parent a3a296161a
commit 6aee60c84e
7 changed files with 1352 additions and 763 deletions

22
IPSC_Bridge.cfg Normal file
View File

@ -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!

350
IPSC_Bridge.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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'

701
ambe_bridge.py Normal file
View File

@ -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()

279
ambe_utils.py Normal file
View File

@ -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()