Compare commits

...

21 Commits

Author SHA1 Message Date
Randy Hall
688f25b57f
Update mk-dmrlink for new HBLink-org repo location 2021-02-16 19:30:31 -08:00
Steve N4IRS
624cf923b5 Update to json 2019-07-09 15:21:54 -04:00
Cort Buffington
c44aadc4fc Allow alternate Bridge config file to be speficied on the CLI 2018-11-26 10:29:41 -06:00
Cort Buffington
ba6dd682ae
Merge pull request #26 from coolacid/IPSC_Bridge
TS for playback.py is not off by one
2018-01-26 07:47:57 -06:00
CoolAcid
c637ca99dc TS for playback.py is not off by one 2018-01-25 18:17:04 -05:00
Cort Buffington
b1458c510a Update Socket Report Format to Match Master 2017-11-07 08:37:11 -06:00
Steve N4IRS
71361c9e63 Move retired file 2017-10-18 13:56:07 -04:00
Steve Zingman
93eacfb01f Update mk-dmrlink 2017-08-25 14:41:16 -04:00
Steve N4IRS
30a2c253f3 Move retired applications to a seperate directory 2017-08-25 14:20:59 -04:00
Steve Zingman
3a1097cd2c bitstring and bit array move to dmr_utils setup.py 2017-08-22 09:25:48 -04:00
Steve Zingman
da1c24b84f Update dmrlink_SAMPLE.cfg 2017-06-29 13:11:18 -04:00
Steve Zingman
77f7d612c9 point log at /tmp no errors 2017-06-28 22:00:27 -04:00
MrBungle42
dff5501349 Update IPSC_Bridge.cfg
Remove unused values
2017-06-20 20:00:17 -04:00
Michael Zingman
6e046756d4 Cleanup 2017-06-20 16:14:59 -04:00
Steve Zingman
f5aea1d789 Restore copying template.bin 2017-06-20 14:40:22 -04:00
Steve Zingman
337e777504 Update version number 2017-06-20 14:30:19 -04:00
Steve N4IRS
830fe5bd2d Replace setup required 2017-06-20 08:46:58 -04:00
Steve N4IRS
942e26348b Use programs in ipsc directory and dmr_utils 2017-06-20 08:40:28 -04:00
Steve N4IRS
a826489424 Temporary fix 2017-06-19 06:33:41 -04:00
Steve N4IRS
bf1b2189a1 Temporary fix until merge 2017-06-19 06:29:02 -04:00
Mike Zingman
6aee60c84e IPSC_Bridge Updates 2017-06-18 20:37:03 -04:00
20 changed files with 413 additions and 853 deletions

11
IPSC_Bridge.cfg Normal file
View File

@ -0,0 +1,11 @@
##################################
# IPSC_Bridge configuration file #
##################################
# DEFAULTS - General settings. These values are
# inherited in each subsequent section (defined by section value).
[DEFAULTS]
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)

313
IPSC_Bridge.py Executable file
View File

@ -0,0 +1,313 @@
#!/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, config_reports, reportFactory
from dmr_utils.utils import int_id, hex_str_3, hex_str_4, get_alias, get_info
from time import time, sleep, clock, localtime, strftime
import csv
import struct
from random import randint
from dmr_utils import ambe_utils
from dmr_utils.ambe_bridge import AMBE_IPSC
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__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__ = '20170620'
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):
_gateway = "127.0.0.1" # IP address of app
_gateway_port = 31000 # Port Analog_Bridge is listening on for AMBE frames to decode
_ambeRxPort = 31003 # Port to listen on for AMBE frames to transmit to all peers
_busy_slots = [0,0,0] # Keep track of activity on each slot. Make sure app is polite
_currentNetwork = ""
cc = 1
###### DEBUGDEBUGDEBUG
#_d = None
###### DEBUGDEBUGDEBUG
def __init__(self, _name, _config, _logger, _report):
IPSC.__init__(self, _name, _config, _logger, _report)
#
# Define default values for operation. These will be overridden by the .cfg file if found
#
self._configFile=cli_args.BRIDGE_CONFIG_FILE
self._currentNetwork = str(_name)
self.readConfigFile(self._configFile, None, self._currentNetwork)
logger.info('DMRLink IPSC Bridge')
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']
#
# 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.info('Section ' + sec + ' was not found, using DEFAULTS')
sec = 'DEFAULTS'
self._gateway = self.defaultOption(config, sec,'gateway', self._gateway)
self._gateway_port = int(self.defaultOption(config, sec,'toGatewayPort', self._gateway_port))
self._ambeRxPort = int(self.defaultOption(config, sec,'fromGatewayPort', self._ambeRxPort))
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
from ipsc.dmrlink_log import config_logging
from ipsc.dmrlink_config import build_config
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# 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.')
parser.add_argument('-b','--bridge_config', action='store', dest='BRIDGE_CONFIG_FILE', help='/full/path/to/bridgeconfig.cfg (default HB_Bridge.cfg)')
cli_args = parser.parse_args()
# Ensure we have a path for the config file, if one wasn't specified, then use the default (top of file)
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Ensure we have a path for the bridge config file, if one wasn't specified, then use the default (top of file)
if not cli_args.BRIDGE_CONFIG_FILE:
cli_args.BRIDGE_CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/IPSC_Bridge.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 \'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 THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGUED IPSC
for system in CONFIG['SYSTEMS']:
if CONFIG['SYSTEMS'][system]['LOCAL']['ENABLED']:
systems[system] = ambeIPSC(system, CONFIG, logger, report_server)
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'

View File

@ -352,7 +352,7 @@ class confbridgeIPSC(IPSC):
self.call_start = now self.call_start = now
self._logger.info('(%s) GROUP VOICE START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)) self._logger.info('(%s) GROUP VOICE START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))
if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK': if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
self._report.send_bridgeEvent('({}) GROUP VOICE START: CallID: {} PEER: {}, SUB: {}, TS: {}, TGID: {}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))) self._report.send_bridgeEvent('GROUP VOICE,START,{},{},{},{},{},{}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)))
# Action happens on un-key # Action happens on un-key
if _burst_data_type == BURST_DATA_TYPE['VOICE_TERM']: if _burst_data_type == BURST_DATA_TYPE['VOICE_TERM']:
@ -360,12 +360,11 @@ class confbridgeIPSC(IPSC):
self.call_duration = now - self.call_start self.call_duration = now - self.call_start
self._logger.info('(%s) GROUP VOICE END: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s Duration: %.2fs', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration) self._logger.info('(%s) GROUP VOICE END: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s Duration: %.2fs', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration)
if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK': if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
self._report.send_bridgeEvent('({}) GROUP VOICE END: CallID: {} PEER: {}, SUB: {}, TS: {}, TGID: {} Duration: {:.2f}s'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration)) self._report.send_bridgeEvent('GROUP VOICE,END,{},{},{},{},{},{},{:.2f}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration))
else: else:
self._logger.warning('(%s) GROUP VOICE END WITHOUT MATCHING START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)) self._logger.warning('(%s) GROUP VOICE END WITHOUT MATCHING START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))
if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK': if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
self._report.send_bridgeEvent('(%s) GROUP VOICE END WITHOUT MATCHING START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))) self._report.send_bridgeEvent('GROUP VOICE,UNMATCHED END,{},{},{},{},{},{}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)))
# Iterate the rules dictionary # Iterate the rules dictionary
for _bridge in BRIDGES: for _bridge in BRIDGES:

View File

@ -70,8 +70,8 @@ PRINT_PEERS_INC_FLAGS: 0
# used. # used.
# #
[LOGGER] [LOGGER]
LOG_FILE: /var/log/dmrlink/dmrlink.log LOG_FILE: /tmp/dmrlink.log
LOG_HANDLERS: file LOG_HANDLERS: console-timed,file-timed
LOG_LEVEL: INFO LOG_LEVEL: INFO
LOG_NAME: DMRlink LOG_NAME: DMRlink
@ -86,11 +86,11 @@ LOG_NAME: DMRlink
TRY_DOWNLOAD: True TRY_DOWNLOAD: True
LOCAL_FILE: False LOCAL_FILE: False
PATH: ./ PATH: ./
PEER_FILE: peer_ids.csv PEER_FILE: peer_ids.json
SUBSCRIBER_FILE: subscriber_ids.csv SUBSCRIBER_FILE: subscriber_ids.json
TGID_FILE: talkgroup_ids.csv TGID_FILE: talkgroup_ids.json
PEER_URL: http://www.dmr-marc.net/cgi-bin/trbo-database/datadump.cgi?table=repeaters&format=csv&header=0 PEER_URL: https://www.radioid.net/static/rptrs.json
SUBSCRIBER_URL: http://www.dmr-marc.net/cgi-bin/trbo-database/datadump.cgi?table=users&format=csv&header=0 SUBSCRIBER_URL: https://www.radioid.net/static/users.json
STALE_DAYS: 7 STALE_DAYS: 7
@ -140,7 +140,7 @@ STALE_DAYS: 7
# #
[SAMPLE_PEER] [SAMPLE_PEER]
ENABLED: True ENABLED: False
RADIO_ID: 12345 RADIO_ID: 12345
IP: IP:
PORT: 50000 PORT: 50000
@ -151,8 +151,8 @@ IPSC_MODE: DIGITAL
TS1_LINK: True TS1_LINK: True
TS2_LINK: True TS2_LINK: True
CSBK_CALL: False CSBK_CALL: False
RCM: True RCM: False
CON_APP: True CON_APP: False
XNL_CALL: False XNL_CALL: False
XNL_MASTER: False XNL_MASTER: False
DATA_CALL: True DATA_CALL: True
@ -166,7 +166,7 @@ GROUP_HANGTIME: 5
[SAMPLE_MASTER] [SAMPLE_MASTER]
ENABLED: False ENABLED: True
RADIO_ID: 54321 RADIO_ID: 54321
IP: 192.168.1.1 IP: 192.168.1.1
PORT: 50000 PORT: 50000
@ -177,8 +177,8 @@ IPSC_MODE: DIGITAL
TS1_LINK: True TS1_LINK: True
TS2_LINK: True TS2_LINK: True
CSBK_CALL: False CSBK_CALL: False
RCM: True RCM: False
CON_APP: True CON_APP: False
XNL_CALL: False XNL_CALL: False
XNL_MASTER: False XNL_MASTER: False
DATA_CALL: True DATA_CALL: True

View File

@ -17,11 +17,11 @@ echo ""
apt-get install python-dev -y apt-get install python-dev -y
apt-get install python-pip -y apt-get install python-pip -y
apt-get install python-twisted -y apt-get install python-twisted -y
pip install bitstring # pip install bitstring
pip install bitarray # pip install bitarray
cd /opt cd /opt
git clone https://github.com/n0mjs710/dmr_utils.git git clone https://github.com/HBLink-org/dmr_utils.git
cd dmr_utils/ cd dmr_utils/
pip install . pip install .
@ -32,15 +32,15 @@ echo "Required programs installed, continuing"
# The needed files are copied to /opt/dmrlink # The needed files are copied to /opt/dmrlink
# Make needed directories # Make needed directories
mkdir -p /opt/dmrlink/ambe_audio/ mkdir -p /opt/dmrlink/IPSC_Bridge/
mkdir -p /opt/dmrlink/bridge/ # mkdir -p /opt/dmrlink/bridge/
mkdir -p /opt/dmrlink/confbridge/ mkdir -p /opt/dmrlink/confbridge/
mkdir -p /opt/dmrlink/log/ # mkdir -p /opt/dmrlink/log/
mkdir -p /opt/dmrlink/playback/ mkdir -p /opt/dmrlink/playback/
mkdir -p /opt/dmrlink/play_group/ # mkdir -p /opt/dmrlink/play_group/
mkdir -p /opt/dmrlink/proxy/ mkdir -p /opt/dmrlink/proxy/
mkdir -p /opt/dmrlink/rcm/ # mkdir -p /opt/dmrlink/rcm/
mkdir -p /opt/dmrlink/record/ # mkdir -p /opt/dmrlink/record/
mkdir -p /opt/dmrlink/samples mkdir -p /opt/dmrlink/samples
mkdir -p /var/log/dmrlink mkdir -p /var/log/dmrlink
@ -52,48 +52,48 @@ cd /opt/dmrlink
# cp $currentdir/talkgroup_ids.csv /opt/dmrlink # cp $currentdir/talkgroup_ids.csv /opt/dmrlink
# Copy ipsc directory into each app directory # Copy ipsc directory into each app directory
cp -rf $currentdir/ipsc/ /opt/dmrlink/ambe_audio/ cp -rf $currentdir/ipsc/ /opt/dmrlink/IPSC_Bridge/
cp -rf $currentdir/ipsc/ /opt/dmrlink/bridge/ #cp -rf $currentdir/ipsc/ /opt/dmrlink/bridge/
cp -rf $currentdir/ipsc/ /opt/dmrlink/confbridge/ cp -rf $currentdir/ipsc/ /opt/dmrlink/confbridge/
cp -rf $currentdir/ipsc/ /opt/dmrlink/log/ #cp -rf $currentdir/ipsc/ /opt/dmrlink/log/
cp -rf $currentdir/ipsc/ /opt/dmrlink/playback/ cp -rf $currentdir/ipsc/ /opt/dmrlink/playback/
cp -rf $currentdir/ipsc/ /opt/dmrlink/play_group/ #cp -rf $currentdir/ipsc/ /opt/dmrlink/play_group/
cp -rf $currentdir/ipsc/ /opt/dmrlink/proxy/ cp -rf $currentdir/ipsc/ /opt/dmrlink/proxy/
cp -rf $currentdir/ipsc/ /opt/dmrlink/rcm/ #cp -rf $currentdir/ipsc/ /opt/dmrlink/rcm/
cp -rf $currentdir/ipsc/ /opt/dmrlink/record/ #cp -rf $currentdir/ipsc/ /opt/dmrlink/record/
# Put a copy of the samples together for easy reference # Put a copy of the samples together for easy reference
cp $currentdir/bridge_rules_SAMPLE.py /opt/dmrlink/samples #cp $currentdir/bridge_rules_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/confbridge_rules_SAMPLE.py /opt/dmrlink/samples cp $currentdir/confbridge_rules_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/samples cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/samples
cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/samples cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/samples cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/ambe_audio.cfg /opt/dmrlink/samples cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/samples
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/samples cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/samples
# Put the doc together for easy reference # Put the doc together for easy reference
cp -rf $currentdir/documents /opt/dmrlink cp -rf $currentdir/documents /opt/dmrlink
cp $currentdir/LICENSE.txt /opt/dmrlink/documents cp $currentdir/LICENSE.txt /opt/dmrlink/documents
cp $currentdir/requirements.txt /opt/dmrlink/documents cp $currentdir/requirements.txt /opt/dmrlink/documents
cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/documents # cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/documents
# ambe_audio # IPSC_Bridge
cp $currentdir/dmrlink.py /opt/dmrlink/ambe_audio/ cp $currentdir/dmrlink.py /opt/dmrlink/IPSC_Bridge/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/ambe_audio/ cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/IPSC_Bridge/
# #
cp $currentdir/ambe_audio.cfg /opt/dmrlink/ambe_audio/ cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/IPSC_Bridge/
cp $currentdir/ambe_audio.py /opt/dmrlink/ambe_audio/ cp $currentdir/IPSC_Bridge.py /opt/dmrlink/IPSC_Bridge/
cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/ambe_audio/ # cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/IPSC_Bridge/
cp $currentdir/template.bin /opt/dmrlink/ambe_audio/ cp $currentdir/template.bin /opt/dmrlink/IPSC_Bridge/
# Bridge app # Bridge app
cp $currentdir/dmrlink.py /opt/dmrlink/bridge/ #cp $currentdir/dmrlink.py /opt/dmrlink/bridge/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/bridge/ #cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/bridge/
# #
cp $currentdir/bridge.py /opt/dmrlink/bridge/ #cp $currentdir/bridge.py /opt/dmrlink/bridge/
cp $currentdir/bridge_rules_SAMPLE.py /opt/dmrlink/bridge/ #cp $currentdir/bridge_rules_SAMPLE.py /opt/dmrlink/bridge/
cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/bridge/ #cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/bridge/
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/bridge/ #cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/bridge/
# ConfBridge app # ConfBridge app
cp $currentdir/dmrlink.py /opt/dmrlink/confbridge/ cp $currentdir/dmrlink.py /opt/dmrlink/confbridge/
@ -105,10 +105,10 @@ cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/confbridge/
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/confbridge/ cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/confbridge/
# Log app # Log app
cp $currentdir/dmrlink.py /opt/dmrlink/log/ #cp $currentdir/dmrlink.py /opt/dmrlink/log/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/log/ #cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/log/
# #
cp $currentdir/log.py /opt/dmrlink/log/ #cp $currentdir/log.py /opt/dmrlink/log/
# Playback (Parrot) # Playback (Parrot)
cp $currentdir/dmrlink.py /opt/dmrlink/playback/ cp $currentdir/dmrlink.py /opt/dmrlink/playback/
@ -118,10 +118,10 @@ cp $currentdir/playback.py /opt/dmrlink/playback/
cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/playback/ cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/playback/
# Play Group app # Play Group app
cp $currentdir/dmrlink.py /opt/dmrlink/play_group/ #cp $currentdir/dmrlink.py /opt/dmrlink/play_group/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/play_group/ #cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/play_group/
# #
cp $currentdir/play_group.py /opt/dmrlink/play_group/ #cp $currentdir/play_group.py /opt/dmrlink/play_group/
# proxy app # proxy app
cp $currentdir/dmrlink.py /opt/dmrlink/proxy/ cp $currentdir/dmrlink.py /opt/dmrlink/proxy/
@ -132,15 +132,15 @@ cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/proxy/
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/proxy/ cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/proxy/
# rcm app # rcm app
cp $currentdir/dmrlink.py /opt/dmrlink/rcm/ #cp $currentdir/dmrlink.py /opt/dmrlink/rcm/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/rcm/ #cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/rcm/
# #
cp $currentdir/rcm_db_log.py /opt/dmrlink/rcm/ #cp $currentdir/rcm_db_log.py /opt/dmrlink/rcm/
cp $currentdir/rcm.py /opt/dmrlink/rcm/ #cp $currentdir/rcm.py /opt/dmrlink/rcm/
# record app # record app
cp $currentdir/dmrlink.py /opt/dmrlink/record/ #cp $currentdir/dmrlink.py /opt/dmrlink/record/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/record/ #cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/record/
# #
cp $currentdir/record.py /opt/dmrlink/record/ #cp $currentdir/record.py /opt/dmrlink/record/

View File

@ -11,8 +11,8 @@ TGID = 12345
# TIMESLOT TO LISTEN FOR GROUP VOICE AND REPEAT # TIMESLOT TO LISTEN FOR GROUP VOICE AND REPEAT
# This is a tuple of timeslots to listen to. Note, if there's only # This is a tuple of timeslots to listen to. Note, if there's only
# one, you still have to use the parenthesis and comma. Just # one, you still have to use the parenthesis and comma. Just
# deal with it, or make it better. TS1 = 0, TS2 = 1. # deal with it, or make it better. TS1 = 1, TS2 = 2.
GROUP_TS = (1,) GROUP_TS = (2,)
# ALTERNATE SOURCE SUBSCRIBER ID FOR REPEATED TRANSMISSION # ALTERNATE SOURCE SUBSCRIBER ID FOR REPEATED TRANSMISSION
# Some folks have radios that don't respond to their own subscriber # Some folks have radios that don't respond to their own subscriber
# IDs. Some just don't want to have the playback come from the same # IDs. Some just don't want to have the playback come from the same
@ -31,5 +31,5 @@ SUB = 12345
# TIMESLOT TO LISTEN FOR PRIVATE VOICE AND REPEAT # TIMESLOT TO LISTEN FOR PRIVATE VOICE AND REPEAT
# This is a tuple of timeslots to listen to. Note, if there's only # This is a tuple of timeslots to listen to. Note, if there's only
# one, you still have to use the parenthesis and comma. Just # one, you still have to use the parenthesis and comma. Just
# deal with it, or make it better. TS1 = 0, TS2 = 1. # deal with it, or make it better. TS1 = 1, TS2 = 2.
PRIVATE_TS = (0,1) PRIVATE_TS = (1,2)

View File

@ -1,17 +0,0 @@
[Unit]
Description=DMRlink ambe audio Service
# Description=Place this file in /lib/systemd/system
[Service]
Type=simple
StandardOutput=null
WorkingDirectory=/opt/dmrlink/ambe_audio
Restart=always
RestartSec=3
ExecStart=/usr/bin/python /opt/dmrlink/ambe_audio/ambe_audio.py
ExecReload=/bin/kill -2 $MAINPID
KillMode=process
[Install]
WantedBy=network-online.target

View File

@ -0,0 +1,17 @@
[Unit]
Description=DMRlink IPSC_Bridge Service
# Place this file in /lib/systemd/system
[Service]
Type=simple
StandardOutput=null
WorkingDirectory=/opt/dmrlink/IPSC_Bridge
Restart=always
RestartSec=3
ExecStart=/usr/bin/python /opt/dmrlink/IPSC_Bridge/IPSC_Bridge.py
ExecReload=/bin/kill -2 $MAINPID
KillMode=process
[Install]
WantedBy=network-online.target