Compare commits

..

41 Commits

Author SHA1 Message Date
Cort Buffington
a55f73d059 RadioID changes 2019-05-09 09:31:47 -05:00
Steve Zingman
1c684486ca
Merge pull request #27 from lorenzocipriani/master
Commented some cp commands on moved files
2019-04-15 09:42:59 -04:00
Lorenzo Cipriani
67fd6d62a5 Updated install script to work on RedHat and Debian derivates 2019-04-14 23:39:23 +00:00
Lorenzo Cipriani
785f44e7e6 Commented some cp commands on moved files 2019-04-14 18:35:58 +01:00
Cort Buffington
c023d4a565 Change ID file format to JSON 2019-03-02 13:26:10 -06:00
Cort Buffington
ad399792c9 Update dmrlink_config.py 2018-11-26 10:19:35 -06:00
Cort Buffington
198278c288 Update dmrlink_SAMPLE.cfg
because we can't just have one place to get config files that doens't change constantly
2018-10-12 22:58:06 -05:00
Cort Buffington
2520f394ed update 2018-10-08 22:12:56 -05:00
Cort Buffington
bc59c75eb0 Added IPv6 Support
Someone made a comment… I added it.
2018-06-15 12:14:10 -05:00
Cort Buffington
01a3fff754
Fixing DMR-MARC database changes
....again... and again...
2018-04-16 09:09:47 -05:00
Cort Buffington
2e1a4c3a58 Revert "Accommodate DMR-MARC JSON only database changes"
This reverts commit 59a59e4100.
2018-04-16 08:46:42 -05:00
Cort Buffington
59a59e4100 Accommodate DMR-MARC JSON only database changes
Talkgroup files are samples – pick one format or the other if you want
those aliases. this changes the type of dump you do with DMR-MARC and
the way you save files locally. CSV still works, but DMR-MARC will stop
supporting it!!! DO NOT mix .csv filenames and .json downloads. The
extension must match the file data type.
2018-03-16 20:29:46 -05:00
Cort Buffington
81c3467ec0 Clean up formatting 2018-03-12 21:29:05 -05:00
Cort Buffington
25d6bc08d0 cleaned up superfluous spaces 2018-03-12 21:26:33 -05:00
Cort Buffington
e8311b1f54 Last commit was MESSED UP! 2018-03-12 19:32:20 -05:00
Cort Buffington
cd11397416 Improve IPSC/DMR Re-Write 2018-03-11 17:37:22 -05:00
Cort Buffington
cdd65d8edf Added support for IPSC "Trunks"
A “TRUNK” is a variant on IPSC unique to this project. They are used to
pass traffic between DMRlink instances. They do no contention handling
regarding TS/TGID, they take any traffic sent to them.
2018-02-05 12:27:44 -06:00
Cort Buffington
2b63b5c111 Fix Bug where confbridge.py would not start if there was not an ACL defined. 2018-02-05 11:20:42 -06:00
Cort Buffington
3fc0bdc63d
Update playback_config_SAMPLE.py 2018-01-26 07:49:35 -06:00
Cort Buffington
f5bc547d4d
Update DO_NOT_README.md 2018-01-08 13:29:34 -06:00
Cort Buffington
0acc6042e8
Update DO_NOT_README.md 2018-01-08 13:29:02 -06:00
Cort Buffington
b4ab2d31a3
Update DO_NOT_README.md 2018-01-08 13:27:55 -06:00
Cort Buffington
5950240787
Update DO_NOT_README.md 2018-01-08 13:24:37 -06:00
Steve Zingman
63611f2c6c
Fix typo 2017-12-13 12:42:08 -05:00
Steve Zingman
eb2ffe4ecb
Create README.MD 2017-12-09 07:58:53 -05:00
Cort Buffington
bf699dcfbc Move retired file 2017-08-29 07:42:34 -05:00
Steve N4IRS
c6a543527f Move retired applications to a seperate directory 2017-08-25 15:27:53 -04:00
Steve Zingman
3279aeb527 bitstring and bitarray moved to dmr_utils setup.py 2017-08-22 09:24:40 -04:00
Cort Buffington
f216300539 Logic error where RESET TGID caused unintended action 2017-07-30 11:02:37 -05:00
Cort Buffington
8e858e48a2 Missed building hex values for RESET TGID Addition (see last) 2017-07-30 10:52:48 -05:00
Cort Buffington
c16d549e94 Added additional way to reset running timers
Added a list of RESET TGIDs used, additionally, to reset running
timers. All previous actions still work the same way, this is only
ANOTHER way to reset timers.
2017-07-30 10:29:52 -05:00
Cort Buffington
ae9f71d715 Merge branch 'socket-reporting' 2017-06-29 13:03:33 -05:00
Cort Buffington
4ac862b93c update ACL format and action to add "ranges" 2017-06-29 13:02:53 -05:00
Cort Buffington
8fbd7ccf33 more formatting.... UGH! 2017-06-28 16:35:51 -05:00
Cort Buffington
b8d1449d2f format fix from last update. 2017-06-28 16:32:23 -05:00
Cort Buffington
c8804d7231 Network logging format update
Make it a format that webtables.py can more easily re-format/manipulate.
2017-06-28 16:30:15 -05:00
Cort Buffington
7fde9d1ce8 Fixing syntax problems in network logging. Incremental 2017-06-28 16:22:38 -05:00
Cort Buffington
d1143ddb7c bug fix - last update 2017-06-28 16:11:23 -05:00
Cort Buffington
0079ad1baa Change Network Logging Format 2017-06-28 16:07:29 -05:00
Cort Buffington
547d6e23ed Merge branch 'master' into socket-reporting 2017-06-28 16:03:42 -05:00
Cort Buffington
62fc209b4f fix TS parsing during trigger action 2017-06-23 16:04:45 -05:00
36 changed files with 1071 additions and 539 deletions

0
.gitignore vendored Normal file → Executable file
View File

2
DO_NOT_README.md Normal file → Executable file
View File

@ -81,7 +81,7 @@ The following illustrates the communication that a peer (us, for example) has wi
^ v | v |
| +--------------+ ++-------------+ +---------+ |
| NO |Did The Master| YES |Set Keep Alive| |Peer List| NO |
+-------------+ Respond ? +---->| Counter To 0 | |Received?+----------+
+---------+ Respond ? +---->| Counter To 0 | |Received?+----------+
+--------------+ +--------------+ +---------+
*COMMUNICATION WITH PEERS:*

View File

@ -1,11 +0,0 @@
##################################
# 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)

View File

@ -1,313 +0,0 @@
#!/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()

0
LICENSE.txt Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

5
Retired/README.MD Executable file
View File

@ -0,0 +1,5 @@
**Retired files:**
The files in this directory are being kept for reference ONLY. They contain routines that may have been of use to someone.
Do not try to use these programs as is. They will not work!

54
Retired/ambe_audio.cfg Executable file
View File

@ -0,0 +1,54 @@
################################################
# 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

678
Retired/ambe_audio.py Executable file
View File

@ -0,0 +1,678 @@
#!/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()

31
Retired/ambe_audio_commands.txt Executable file
View File

@ -0,0 +1,31 @@
AllStar DTMF command examples:
82=cmd,/bin/bash -c 'do something here'
82=cmd,/bin/bash -c 'echo -n "section=Shutup" | nc 127.0.0.1 31002'
Shell command examples:
# Use netcat to dynamically change ambe_audio without a restart
# echo -n "tgs=x,y,z" | nc 127.0.0.1 31002
# echo -n "reread_subscribers" | nc 127.0.0.1 31002
# echo -n "reread_config" | nc 127.0.0.1 31002
# echo -n "txTg=##" | nc 127.0.0.1 31002
# echo -n "txTs=#" | nc 127.0.0.1 31002
# echo -n "section=XX" | nc 127.0.0.1 31002
Remote control commands:
'reread_subscribers'
'reread_config'
'txTg'
'txTs'
'section'
'gateway_dmr_id'
'gateway_peer_id'
'restart'
'playbackFromFile'
'tgs'
'dump_template'
'get_info'

View File

@ -127,16 +127,28 @@ def build_bridges(_known_bridges):
# are not yet implemented.
def build_acl(_sub_acl):
try:
logger.info('ACL file found, importing entries. This will take about 1.5 seconds per 1 million IDs')
acl_file = import_module(_sub_acl)
for i, e in enumerate(acl_file.ACL):
acl_file.ACL[i] = hex_str_3(acl_file.ACL[i])
logger.info('ACL file found and ACL entries imported')
ACL_ACTION = acl_file.ACL_ACTION
ACL = acl_file.ACL
sections = acl_file.ACL.split(':')
ACL_ACTION = sections[0]
entries_str = sections[1]
ACL = set()
for entry in entries_str.split(','):
if '-' in entry:
start,end = entry.split('-')
start,end = int(start), int(end)
for id in range(start, end+1):
ACL.add(hex_str_3(id))
else:
id = int(entry)
ACL.add(hex_str_3(id))
logger.info('ACL loaded: action "{}" for {:,} radio IDs'.format(ACL_ACTION, len(ACL)))
except ImportError:
logger.info('ACL file not found or invalid - all subscriber IDs are valid')
ACL_ACTION = 'NONE'
ACL = []
# Depending on which type of ACL is used (PERMIT, DENY... or there isn't one)
# define a differnet function to be used to check the ACL

0
Retired/bridge_rules_SAMPLE.py Normal file → Executable file
View File

0
Retired/known_bridges_SAMPLE.py Normal file → Executable file
View File

0
Retired/template.py Normal file → Executable file
View File

View File

@ -137,10 +137,12 @@ def make_bridge_config(_confbridge_rules):
_system['ON'][i] = hex_str_3(_system['ON'][i])
for i, e in enumerate(_system['OFF']):
_system['OFF'][i] = hex_str_3(_system['OFF'][i])
for i, e in enumerate(_system['RESET']):
_system['RESET'][i] = hex_str_3(_system['RESET'][i])
_system['TIMEOUT'] = _system['TIMEOUT']*60
_system['TIMER'] = time()
return {'BRIDGE_CONF': bridge_file.BRIDGE_CONF, 'BRIDGES': bridge_file.BRIDGES}
return {'BRIDGE_CONF': bridge_file.BRIDGE_CONF, 'BRIDGES': bridge_file.BRIDGES, 'TRUNKS': bridge_file.TRUNKS}
# Import subscriber ACL
@ -148,17 +150,28 @@ def make_bridge_config(_confbridge_rules):
# Global action is to allow or deny them. Multiple lists with different actions and ranges
# are not yet implemented.
def build_acl(_sub_acl):
ACL = set()
try:
logger.info('ACL file found, importing entries. This will take about 1.5 seconds per 1 million IDs')
acl_file = import_module(_sub_acl)
for i, e in enumerate(acl_file.ACL):
acl_file.ACL[i] = hex_str_3(acl_file.ACL[i])
logger.info('ACL file found and ACL entries imported')
ACL_ACTION = acl_file.ACL_ACTION
ACL = acl_file.ACL_ACTION
sections = acl_file.ACL.split(':')
ACL_ACTION = sections[0]
entries_str = sections[1]
for entry in entries_str.split(','):
if '-' in entry:
start,end = entry.split('-')
start,end = int(start), int(end)
for id in range(start, end+1):
ACL.add(hex_str_3(id))
else:
id = int(entry)
ACL.add(hex_str_3(id))
logger.info('ACL loaded: action "{}" for {:,} radio IDs'.format(ACL_ACTION, len(ACL)))
except ImportError:
logger.info('ACL file not found or invalid - all subscriber IDs are valid')
ACL_ACTION = 'NONE'
ACL = []
# Depending on which type of ACL is used (PERMIT, DENY... or there isn't one)
# define a differnet function to be used to check the ACL
@ -240,7 +253,7 @@ class confbridgeIPSC(IPSC):
return
# Process the packet
self._logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
#self._logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
_burst_data_type = _data[30] # Determine the type of voice packet this is (see top of file for possible types)
_seq_id = _data[5]
@ -259,6 +272,8 @@ class confbridgeIPSC(IPSC):
# BEGIN CONTENTION HANDLING
#
# If the system is listed as a "TRUNK", there will be no contention handling. All traffic is forwarded to it
#
# The rules for each of the 4 "ifs" below are listed here for readability. The Frame To Send is:
# From a different group than last RX from this IPSC, but it has been less than Group Hangtime
# From a different group than last TX to this IPSC, but it has been less than Group Hangtime
@ -266,6 +281,7 @@ class confbridgeIPSC(IPSC):
# From the same group as the last TX to this IPSC, but from a different subscriber, and it has been less than TS Clear Time
# The "continue" at the end of each means the next iteration of the for loop that tests for matching rules
#
if _target not in TRUNKS:
if ((_target['TGID'] != _target_status[_target['TS']]['RX_TGID']) and ((now - _target_status[_target['TS']]['RX_TIME']) < _target_system['LOCAL']['GROUP_HANGTIME'])):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged to TGID%s, target active or in group hangtime: IPSC: %s, TS: %s, TGID: %s', self._system, int_id(_target['TGID']), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['RX_TGID']))
@ -291,12 +307,14 @@ class confbridgeIPSC(IPSC):
#
# Make a copy of the payload
_tmp_data = _data
# Re-Write the PEER ID in the IPSC Header:
_tmp_data = _tmp_data.replace(_peerid, _target_system['LOCAL']['RADIO_ID'], 1)
# Re-Write the IPSC SRC to match the target network's ID
_tmp_data = _tmp_data.replace(_peerid, _target_system['LOCAL']['RADIO_ID'])
# Re-Write the IPSC SRC + DST GROUP in IPSC Headers:
_tmp_data = _tmp_data.replace(_src_sub + _dst_group, _src_sub + _target['TGID'], 1)
# Re-Write the destination Group ID
_tmp_data = _tmp_data.replace(_dst_group, _target['TGID'])
# Re-Write the DST GROUP + IPSC SRC in DMR LC (Header, Terminator and Voice Burst E):
_tmp_data = _tmp_data.replace(_dst_group + _src_sub, _target['TGID'] + _src_sub, 1)
# Re-Write IPSC timeslot value
_call_info = int_id(_data[17:18])
@ -327,7 +345,6 @@ class confbridgeIPSC(IPSC):
# END FRAME FORWARDING
#
# Set values for the contention handler to test next time there is a frame to forward
_target_status[_target['TS']]['TX_TGID'] = _target['TGID']
_target_status[_target['TS']]['TX_TIME'] = now
@ -366,14 +383,16 @@ class confbridgeIPSC(IPSC):
if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
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
for _bridge in BRIDGES:
for _system in BRIDGES[_bridge]:
if _system['SYSTEM'] == self._system:
# TGID matches an ACTIVATION trigger
if _dst_group in _system['ON']:
if (_dst_group in _system['ON'] or _dst_group in _system['RESET']) and _ts == _system['TS']:
# Set the matching rule as ACTIVE
if _dst_group in _system['ON']:
if _system['ACTIVE'] == False:
_system['ACTIVE'] = True
self._logger.info('(%s) Bridge: %s, connection changed to state: %s', self._system, _bridge, _system['ACTIVE'])
@ -387,8 +406,9 @@ class confbridgeIPSC(IPSC):
self._logger.info('(%s) Bridge: %s, timeout timer reset to: %s', self._system, _bridge, _system['TIMER'] - now)
# TGID matches an DE-ACTIVATION trigger
if _dst_group in _system['OFF']:
if (_dst_group in _system['OFF'] or _dst_group in _system['RESET']) and _ts == _system['TS']:
# Set the matching rule as ACTIVE
if _dst_group in _system['OFF']:
if _system['ACTIVE'] == True:
_system['ACTIVE'] = False
self._logger.info('(%s) Bridge: %s, connection changed to state: %s', self._system, _bridge, _system['ACTIVE'])
@ -396,12 +416,12 @@ class confbridgeIPSC(IPSC):
if _system['TO_TYPE'] == 'ON':
_system['TIMER'] = now
self._logger.info('(%s) Bridge: %s set to ON with and "OFF" timer rule: timeout timer cancelled', self._system, _bridge)
# Reset tge timer for the rule
# Reset the timer for the rule
if _system['ACTIVE'] == False and _system['TO_TYPE'] == 'OFF':
_system['TIMER'] = now + _system['TIMEOUT']
self._logger.info('(%s) Bridge: %s, timeout timer reset to: %s', self._system, _bridge, _system['TIMER'] - now)
# Cancel the timer if we've enabled an "ON" type timeout
if _system['ACTIVE'] == True and _system['TO_TYPE'] == 'ON':
if _system['ACTIVE'] == True and _system['TO_TYPE'] == 'ON' and _dst_group in _system['OFF']:
_system['TIMER'] = now
self._logger.info('(%s) Bridge: %s set to ON with and "OFF" timer rule: timeout timer cancelled', self._system, _bridge)
@ -478,6 +498,7 @@ if __name__ == '__main__':
# Build the routing rules and other configuration
CONFIG_DICT = make_bridge_config('confbridge_rules')
BRIDGE_CONF = CONFIG_DICT['BRIDGE_CONF']
TRUNKS = CONFIG_DICT['TRUNKS']
BRIDGES = CONFIG_DICT['BRIDGES']
# Build the Access Control List

22
confbridge_rules_SAMPLE.py Normal file → Executable file
View File

@ -19,6 +19,9 @@ configuration file.
* ON and OFF are LISTS of Talkgroup IDs used to trigger this system off and on. Even if you
only want one (as shown in the ON example), it has to be in list format. None can be
handled with an empty list, such as " 'ON': [] ".
* RESET is a list of Talkgroup IDs that, in addition to the ON and OFF lists will cause a running
timer to be reset. This is useful if you are using different TGIDs for voice traffic than
triggering. If you are not, there is NO NEED to use this feature.
* TO_TYPE is timeout type. If you want to use timers, ON means when it's turned on, it will
turn off afer the timout period and OFF means it will turn back on after the timout
period. If you don't want to use timers, set it to anything else, but 'NONE' might be
@ -38,18 +41,25 @@ BRIDGE_CONF = {
'REPORT': True,
}
# TRUNK IPSC Systems -- trunk bypasses the contention handler and always transmits traffic
#
# This is a python LIST data type. It needs to be here, but just leave it empty if not used.
# The contents are a quoted, comma separated list of IPSC systems that are traffic trunks.
# Example: TRUNKS = ['MASTER-1', 'CLIENT-2']
TRUNKS = []
BRIDGES = {
'WORLDWIDE': [
{'SYSTEM': 'MASTER-1', 'TS': 1, 'TGID': 1, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10]},
{'SYSTEM': 'CLIENT-1', 'TS': 1, 'TGID': 3100, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10]},
{'SYSTEM': 'MASTER-1', 'TS': 1, 'TGID': 1, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10], 'RESET': []},
{'SYSTEM': 'CLIENT-1', 'TS': 1, 'TGID': 3100, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10], 'RESET': []}
],
'ENGLISH': [
{'SYSTEM': 'MASTER-1', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10]},
{'SYSTEM': 'CLIENT-2', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10]},
{'SYSTEM': 'MASTER-1', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10], 'RESET': []},
{'SYSTEM': 'CLIENT-2', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10], 'RESET': []}
],
'STATEWIDE': [
{'SYSTEM': 'MASTER-1', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10]},
{'SYSTEM': 'CLIENT-2', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10]},
{'SYSTEM': 'MASTER-1', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10], 'RESET': []},
{'SYSTEM': 'CLIENT-2', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10], 'RESET': []}
]
}

16
dmrlink_SAMPLE.cfg Normal file → Executable file
View File

@ -70,8 +70,8 @@ PRINT_PEERS_INC_FLAGS: 0
# used.
#
[LOGGER]
LOG_FILE: /tmp/dmrlink.log
LOG_HANDLERS: console-timed,file-timed
LOG_FILE: /var/log/dmrlink/dmrlink.log
LOG_HANDLERS: file
LOG_LEVEL: INFO
LOG_NAME: DMRlink
@ -140,7 +140,7 @@ STALE_DAYS: 7
#
[SAMPLE_PEER]
ENABLED: False
ENABLED: True
RADIO_ID: 12345
IP:
PORT: 50000
@ -151,8 +151,8 @@ IPSC_MODE: DIGITAL
TS1_LINK: True
TS2_LINK: True
CSBK_CALL: False
RCM: False
CON_APP: False
RCM: True
CON_APP: True
XNL_CALL: False
XNL_MASTER: False
DATA_CALL: True
@ -166,7 +166,7 @@ GROUP_HANGTIME: 5
[SAMPLE_MASTER]
ENABLED: True
ENABLED: False
RADIO_ID: 54321
IP: 192.168.1.1
PORT: 50000
@ -177,8 +177,8 @@ IPSC_MODE: DIGITAL
TS1_LINK: True
TS2_LINK: True
CSBK_CALL: False
RCM: False
CON_APP: False
RCM: True
CON_APP: True
XNL_CALL: False
XNL_MASTER: False
DATA_CALL: True

0
documents/FAQ.md Normal file → Executable file
View File

0
documents/internal_data_decode.txt Normal file → Executable file
View File

0
documents/voice_burst_decoding.txt Normal file → Executable file
View File

0
documents/voice_packets.txt Normal file → Executable file
View File

0
ipsc/.gitignore vendored Normal file → Executable file
View File

0
ipsc/__init__.py Normal file → Executable file
View File

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
# Copyright (C) 2016-2018 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
@ -21,16 +21,32 @@
import ConfigParser
import sys
from socket import gethostbyname
from socket import getaddrinfo, IPPROTO_UDP
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__copyright__ = 'Copyright (c) 2016-2018 Cortney T. Buffington, N0MJS and the K0USY Group'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
def get_address(_config):
ipv4 = ''
ipv6 = ''
socket_info = getaddrinfo(_config, None, 0, 0, IPPROTO_UDP)
for item in socket_info:
if item[0] == 2:
ipv4 = item[4][0]
elif item[0] == 30:
ipv6 = item[4][0]
if ipv4:
return ipv4
if ipv6:
return ipv6
return 'invalid address'
def build_config(_config_file):
config = ConfigParser.ConfigParser()
@ -115,7 +131,7 @@ def build_config(_config_file):
# Things we need to know to connect and be a peer in this IPSC
'RADIO_ID': hex(int(config.get(section, 'RADIO_ID')))[2:].rjust(8,'0').decode('hex'),
'IP': gethostbyname(config.get(section, 'IP')),
'IP': config.get(section, 'IP'),
'PORT': config.getint(section, 'PORT'),
'ALIVE_TIMER': config.getint(section, 'ALIVE_TIMER'),
'MAX_MISSED': config.getint(section, 'MAX_MISSED'),
@ -144,7 +160,7 @@ def build_config(_config_file):
})
if not CONFIG['SYSTEMS'][section]['LOCAL']['MASTER_PEER']:
CONFIG['SYSTEMS'][section]['MASTER'].update({
'IP': gethostbyname(config.get(section, 'MASTER_IP')),
'IP': get_address(config.get(section, 'MASTER_IP')),
'PORT': config.getint(section, 'MASTER_PORT')
})
@ -219,7 +235,7 @@ if __name__ == '__main__':
# Ensure we have a path for the config file, if one wasn't specified, then use the execution directory
if not cli_args.CONFIG_FILE:
cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/../dmrlink.cfg'
pprint(build_config(cli_args.CONFIG_FILE))

0
ipsc/ipsc_const.py Normal file → Executable file
View File

0
ipsc/ipsc_mask.py Normal file → Executable file
View File

0
ipsc/reporting_const.py Normal file → Executable file
View File

View File

@ -1,8 +1,10 @@
#! /bin/bash
currentdir=`pwd`
PREFIX=/opt/dmrlink
echo "DMRlink will be installed in: $PREFIX"
echo "Current working directory is" $currentdir
currentdir=`pwd`
echo "Current working directory is: $currentdir"
echo ""
@ -13,17 +15,44 @@ echo ""
#################################################
# Install the required support programs
apt-get install unzip -y
apt-get install python-dev -y
apt-get install python-pip -y
apt-get install python-twisted -y
distro=$(lsb_release -i | awk -F":" '{ gsub(/^[ \t]+/, "", $2); print $2 }')
release=$(lsb_release -r | awk -F":" '{ gsub(/^[ \t]+/, "", $2); print $2 }')
echo "Current Linux distribution is: $distro $release"
if [[ "$distro" =~ ^(CentOS|Fedora|openSUSE|)$ ]]; then
echo "$distro uses yum"
yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(echo $release | awk -F"." '{print $1}').noarch.rpm
yum install -y gcc gcc-c++ glibc-devel make
yum install -y unzip
yum install -y python-devel
yum install -y python-pip
yum install -y python-twisted
# pip install bitstring
# pip install bitarray
else
echo "$distro uses apt"
apt-get install -y build-essential
apt-get install -y unzip
apt-get install -y python-dev
apt-get install -y python-pip
apt-get install -y python-twisted
# pip install bitstring
# pip install bitarray
fi
cd /opt
git clone https://github.com/HBLink-org/dmr_utils.git
cd dmr_utils/
pip install .
# Install dmr_utils with pip install
pip install dmr_utils
###############################################################################
# Following lines should be removed due to the pip install method for dmr_utils
#cd /opt
#if [ ! -d /opt/dmr_utils ]; then
# git clone https://github.com/n0mjs710/dmr_utils.git
#fi
#cd dmr_utils/
#git pull
#pip install .
###############################################################################
echo "Required programs installed, continuing"
@ -32,19 +61,13 @@ echo "Required programs installed, continuing"
# The needed files are copied to /opt/dmrlink
# Make needed directories
mkdir -p /opt/dmrlink/IPSC_Bridge/
# mkdir -p /opt/dmrlink/bridge/
mkdir -p /opt/dmrlink/confbridge/
# mkdir -p /opt/dmrlink/log/
mkdir -p /opt/dmrlink/playback/
# mkdir -p /opt/dmrlink/play_group/
mkdir -p /opt/dmrlink/proxy/
# mkdir -p /opt/dmrlink/rcm/
# mkdir -p /opt/dmrlink/record/
mkdir -p /opt/dmrlink/samples
mkdir -p $PREFIX/confbridge/
mkdir -p $PREFIX/playback/
mkdir -p $PREFIX/proxy/
mkdir -p $PREFIX/samples
mkdir -p /var/log/dmrlink
cd /opt/dmrlink
cd $PREFIX
# Put common files in /opt/dmrlink
# cp $currentdir/peer_ids.csv /opt/dmrlink
@ -52,39 +75,33 @@ cd /opt/dmrlink
# cp $currentdir/talkgroup_ids.csv /opt/dmrlink
# Copy ipsc directory into each app directory
cp -rf $currentdir/ipsc/ /opt/dmrlink/IPSC_Bridge/
#cp -rf $currentdir/ipsc/ /opt/dmrlink/bridge/
cp -rf $currentdir/ipsc/ /opt/dmrlink/confbridge/
#cp -rf $currentdir/ipsc/ /opt/dmrlink/log/
cp -rf $currentdir/ipsc/ /opt/dmrlink/playback/
#cp -rf $currentdir/ipsc/ /opt/dmrlink/play_group/
cp -rf $currentdir/ipsc/ /opt/dmrlink/proxy/
#cp -rf $currentdir/ipsc/ /opt/dmrlink/rcm/
#cp -rf $currentdir/ipsc/ /opt/dmrlink/record/
cp -rf $currentdir/ipsc/ $PREFIX/confbridge/
cp -rf $currentdir/ipsc/ $PREFIX/playback/
cp -rf $currentdir/ipsc/ $PREFIX/proxy/
# Put a copy of the samples together for easy reference
#cp $currentdir/bridge_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/known_bridges_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/samples
cp $currentdir/confbridge_rules_SAMPLE.py $PREFIX/samples
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/samples
#cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/playback_config_SAMPLE.py $PREFIX/samples
#cp $currentdir/ambe_audio.cfg /opt/dmrlink/samples
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/samples
# Put the doc together for easy reference
cp -rf $currentdir/documents /opt/dmrlink
cp $currentdir/LICENSE.txt /opt/dmrlink/documents
cp $currentdir/requirements.txt /opt/dmrlink/documents
# cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/documents
cp -rf $currentdir/documents $PREFIX
cp $currentdir/LICENSE.txt $PREFIX/documents
cp $currentdir/requirements.txt $PREFIX/documents
#cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/documents
# IPSC_Bridge
cp $currentdir/dmrlink.py /opt/dmrlink/IPSC_Bridge/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/IPSC_Bridge/
# ambe_audio
#cp $currentdir/dmrlink.py /opt/dmrlink/ambe_audio/
#cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/ambe_audio/
#
cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/IPSC_Bridge/
cp $currentdir/IPSC_Bridge.py /opt/dmrlink/IPSC_Bridge/
# cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/IPSC_Bridge/
cp $currentdir/template.bin /opt/dmrlink/IPSC_Bridge/
#cp $currentdir/ambe_audio.cfg /opt/dmrlink/ambe_audio/
#cp $currentdir/ambe_audio.py /opt/dmrlink/ambe_audio/
#cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/ambe_audio/
#cp $currentdir/template.bin /opt/dmrlink/ambe_audio/
# Bridge app
#cp $currentdir/dmrlink.py /opt/dmrlink/bridge/
@ -96,13 +113,13 @@ cp $currentdir/template.bin /opt/dmrlink/IPSC_Bridge/
#cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/bridge/
# ConfBridge app
cp $currentdir/dmrlink.py /opt/dmrlink/confbridge/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/confbridge/
cp $currentdir/dmrlink.py $PREFIX/confbridge/
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/confbridge/
#
cp $currentdir/confbridge.py /opt/dmrlink/confbridge/
cp $currentdir/confbridge_rules_SAMPLE.py /opt/dmrlink/confbridge/
cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/confbridge/
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/confbridge/
cp $currentdir/confbridge.py $PREFIX/confbridge/
cp $currentdir/confbridge_rules_SAMPLE.py $PREFIX/confbridge/
#cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/confbridge/
cp $currentdir/sub_acl_SAMPLE.py $PREFIX/confbridge/
# Log app
#cp $currentdir/dmrlink.py /opt/dmrlink/log/
@ -111,11 +128,11 @@ cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/confbridge/
#cp $currentdir/log.py /opt/dmrlink/log/
# Playback (Parrot)
cp $currentdir/dmrlink.py /opt/dmrlink/playback/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/playback/
cp $currentdir/dmrlink.py $PREFIX/playback/
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/playback/
#
cp $currentdir/playback.py /opt/dmrlink/playback/
cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/playback/
cp $currentdir/playback.py $PREFIX/playback/
cp $currentdir/playback_config_SAMPLE.py $PREFIX/playback/
# Play Group app
#cp $currentdir/dmrlink.py /opt/dmrlink/play_group/
@ -124,12 +141,12 @@ cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/playback/
#cp $currentdir/play_group.py /opt/dmrlink/play_group/
# proxy app
cp $currentdir/dmrlink.py /opt/dmrlink/proxy/
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/proxy/
cp $currentdir/dmrlink.py $PREFIX/proxy/
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/proxy/
#
cp $currentdir/proxy.py /opt/dmrlink/proxy/
cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/proxy/
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/proxy/
cp $currentdir/proxy.py $PREFIX/proxy/
#cp $currentdir/known_bridges_SAMPLE.py $PREFIX/proxy/
cp $currentdir/sub_acl_SAMPLE.py $PREFIX/proxy/
# rcm app
#cp $currentdir/dmrlink.py /opt/dmrlink/rcm/

View File

@ -72,16 +72,28 @@ __email__ = 'n0mjs@me.com'
# are not yet implemented.
def build_acl(_sub_acl):
try:
logger.info('ACL file found, importing entries. This will take about 1.5 seconds per 1 million IDs')
acl_file = import_module(_sub_acl)
for i, e in enumerate(acl_file.ACL):
acl_file.ACL[i] = hex_str_3(acl_file.ACL[i])
logger.info('ACL file found and ACL entries imported')
ACL_ACTION = acl_file.ACL_ACTION
ACL = acl_file.ACL_ACTION
sections = acl_file.ACL.split(':')
ACL_ACTION = sections[0]
entries_str = sections[1]
ACL = set()
for entry in entries_str.split(','):
if '-' in entry:
start,end = entry.split('-')
start,end = int(start), int(end)
for id in range(start, end+1):
ACL.add(hex_str_3(id))
else:
id = int(entry)
ACL.add(hex_str_3(id))
logger.info('ACL loaded: action "{}" for {:,} radio IDs'.format(ACL_ACTION, len(ACL)))
except ImportError:
logger.info('ACL file not found or invalid - all subscriber IDs are valid')
ACL_ACTION = 'NONE'
ACL = []
# Depending on which type of ACL is used (PERMIT, DENY... or there isn't one)
# define a differnet function to be used to check the ACL

0
requirements.txt Normal file → Executable file
View File

12
sub_acl_SAMPLE.py Normal file → Executable file
View File

@ -1,6 +1,6 @@
ACL_ACTION = "DENY" # May be PERMIT|DENY
ACL = [
1234001,
1234002,
1234003
]
# The 'action' May be PERMIT|DENY
# Each entry may be a single radio id, or a hypenated range (e.g. 1-2999)
# Format:
# ACL = 'action:id|start-end|,id|start-end,....'
ACL = 'DENY:1-2999,16777215'

17
systemd/ambe_audio.service Executable file
View File

@ -0,0 +1,17 @@
[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

0
systemd/bridge.service Normal file → Executable file
View File

View File

@ -1,17 +0,0 @@
[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

0
systemd/playback.service Normal file → Executable file
View File

0
template.bin Normal file → Executable file
View File