Compare commits

...

31 Commits

Author SHA1 Message Date
Steve N4IRS b2f3bf89da Update to json 2019-07-09 15:22:43 -04:00
Cort Buffington d756529701
Merge pull request #25 from marrold/HB_Bridge
Update hblink.cfg to use ham-digital DMR ID CSVs
2018-08-05 12:37:56 -05:00
Matthew 80c6e6c7b7 Update hblink.cfg to use ham-digital DMR ID CSVs 2018-08-05 17:54:19 +01:00
Cort Buffington 260fe8f8cd
Change alternate bridge config to single letter CLI flag. 2018-06-29 10:37:49 -05:00
Cort Buffington 0445739fa2
Merge pull request #15 from enhanced/HB_Bridge
Added the capability to specify a different bridge config
2018-06-29 10:35:23 -05:00
Cort Buffington b0a12e5dd5
Merge pull request #19 from marrold/Toggle_validation-HB_Bridge
Minor comment update for new "Loose" mode
2018-06-25 16:33:29 -05:00
Cort Buffington 6b48a98b0d
Merge branch 'HB_Bridge' into Toggle_validation-HB_Bridge 2018-06-25 16:33:18 -05:00
Matthew 819dd1401e Adds comment for Loose config option. 2018-06-25 22:26:12 +01:00
Matthew 394cf13317 Refactor validation to optimise code 2018-06-25 17:21:10 +01:00
Cort Buffington 046163b3dd
Merge pull request #18 from marrold/Toggle_validation-HB_Bridge
Allows user to disable strict validation when connecting to master (HB_Bridge Branch)
2018-06-25 11:09:47 -05:00
Matthew dc753bf2db Refactor validation to optimise code 2018-06-25 16:44:19 +01:00
Matthew 508172e195 Allows user to disable strict validation when connecting to master 2018-06-23 20:35:36 +01:00
Steve Zingman 5b18a7cf41
Adjust fromgateway and to gateway to match Analog_Bridge 2018-06-05 13:13:44 -04:00
Mike Zingman 037f01ba1f Merge critical changes from Cort 2018-02-02 16:43:49 -05:00
Mike Zingman 2de3737e57 Update to match master branch 2018-02-02 16:43:18 -05:00
enhanced 81f778fcec Added the capability to specify a different bridge config (HB_Bridge.cfg) file/path 2018-01-03 14:24:23 -07:00
Michael Zingman e0b979591e Sequence number fix (error count was inflated because I was not counting non-voice frames) 2017-12-10 18:04:39 -05:00
Steve Zingman f73a0b987b
Update hblink-SAMPLE.cfg 2017-11-02 09:55:42 -04:00
Cort Buffington ba82df201f Merge pull request #14 from MichaelZingman/patch-1
Update hblink.py
2017-08-18 08:54:03 -05:00
MichaelZingman 6685829a05 Update hblink.py 2017-08-18 09:52:29 -04:00
Michael Zingman f4151e2071 Add global exception handler to make sure everything goes to the log and not stderr. 2017-07-24 11:39:43 -04:00
MrBungle42 b1822af576 Update hblink.py
Data type error
2017-07-17 20:19:22 -04:00
MrBungle42 7610c25a19 Update hblink.py
Add warning when RADIO_ID is rejected.
2017-07-17 16:41:42 -04:00
Steve Zingman 04e98b66bc Update hblink-SAMPLE.cfg 2017-06-29 12:54:29 -04:00
Steve Zingman 2c850bfb8e Add script for required
chmod +x mk-required
2017-06-24 19:02:09 -04:00
Steve N4IRS e66e352e17 Fix SOFTWARE_ID and PACKAGE_ID 2017-06-20 21:02:12 -04:00
Steve Zingman 146fcbb0c0 reversed SOFTWARE_ID and PACKAGE_ID 2017-06-20 18:48:07 -04:00
Steve Zingman 03eb14c408 Update hblink-SAMPLE.cfg 2017-06-20 17:03:23 -04:00
Steve Zingman c1f0062af7 Update version number 2017-06-20 14:29:33 -04:00
N4IRS 76a1356507 ambe support programs moved to dmr_utils 2017-06-20 14:01:49 -04:00
Mike Zingman a3e41b66a0 HB_Bridge updates 2017-06-18 20:46:50 -04:00
12 changed files with 66211 additions and 50037 deletions

15
HB_Bridge.cfg Normal file
View File

@ -0,0 +1,15 @@
################################################
# HB_Bridge configuration file.
################################################
[DEFAULTS]
gateway = 127.0.0.1 # IP address of Partner Application (IPSC_Bridge, Analog_Bridge)
fromGatewayPort = 31103 # Port HB_Bridge is listening on for data (HB_Bridge <--- Partner)
toGatewayPort = 31100 # Port Partner is listening on for data (HB_Bridge ---> Partner)
[RULES]
# Name = Old TG, New TG, New Slot
TG_SE = 3174, 3174, 2
TG_NA = 3,3,1
TG_ATL = 8,8,1
TG_WW = 1,1,1

247
HB_Bridge.py Normal file
View File

@ -0,0 +1,247 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2017 Mike Zingman N4IRR
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
'''
'''
from __future__ import print_function
# Python modules we need
import sys
from bitarray import bitarray
from bitstring import BitArray
from bitstring import BitString
import struct
from time import time, sleep
from importlib import import_module
from binascii import b2a_hex as ahex
from random import randint
import sys, socket, ConfigParser, thread, traceback
from threading import Lock
from time import time, sleep, clock, localtime, strftime
# Twisted is pretty important, so I keep it separate
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from twisted.internet import task
# Things we import from the main hblink module
from hblink import HBSYSTEM, systems, int_id, hblink_handler
from dmr_utils.utils import hex_str_3, hex_str_4, int_id, get_alias
from dmr_utils import decode, bptc, const, golay, qr
import hb_config
import hb_log
import hb_const
from dmr_utils import ambe_utils
from dmr_utils.ambe_bridge import AMBE_HB
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
__author__ = 'Mike Zingman, N4IRR and Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2017 Mike Zingman N4IRR'
__credits__ = 'Cortney T. Buffington, N0MJS; Colin Durbridge, G4EML, Steve Zingman, N4IRS; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'pre-alpha'
__version__ = '20170620'
mutex = Lock() # Used to synchronize Peer I/O in different threads
class TRANSLATE:
def __init__(self, config_file):
self.translate = {}
self.load_config(config_file)
pass
def add_rule( self, tg, export_rule):
self.translate[str(tg)] = export_rule
#print(int_id(tg), export_rule)
def delete_rule(self, tg):
if str(tg) in self.translate:
del self.translate[str(tg)]
def find_rule(self, tg, slot):
if str(tg) in self.translate:
return self.translate[str(tg)]
return (tg, slot)
def load_config(self, config_file):
print('load config file', config_file)
pass
# translation structure. IMPORT_TO translates foreign (TG,TS) to local. EXPORT_AS translates local (TG,TS) to foreign values
translate = TRANSLATE('config.file')
class HB_BRIDGE(HBSYSTEM):
def __init__(self, _name, _config, _logger):
HBSYSTEM.__init__(self, _name, _config, _logger)
self._ambeRxPort = 31003 # Port to listen on for AMBE frames to transmit to all peers
self._gateway = "127.0.0.1" # IP address of Analog_Bridge app
self._gateway_port = 31000 # Port Analog_Bridge is listening on for AMBE frames to decode
self.load_configuration(cli_args.BRIDGE_CONFIG_FILE)
self.hb_ambe = AMBE_HB(self, _name, _config, _logger, self._ambeRxPort)
self._sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
def get_globals(self):
return (subscriber_ids, talkgroup_ids, peer_ids)
def get_repeater_id(self, import_id):
if self._config['MODE'] == 'CLIENT': # only clients have radio_id defined, masters do not
return self._config['RADIO_ID']
return import_id
# Load configuration from file
def load_configuration( self, _file_name ):
config = ConfigParser.ConfigParser()
if not config.read(_file_name):
sys.exit('Configuration file \''+_file_name+'\' is not a valid configuration file! Exiting...')
try:
for section in config.sections():
if section == 'DEFAULTS':
self._ambeRxPort = int(config.get(section, 'fromGatewayPort').split(None)[0]) # Port to listen on for AMBE frames to transmit to all peers
self._gateway = config.get(section, 'gateway').split(None)[0] # IP address of Analog_Bridge app
self._gateway_port = int(config.get(section, 'toGatewayPort').split(None)[0]) # Port Analog_Bridge is listening on for AMBE frames to decode
if section == 'RULES':
for rule in config.items(section):
_old_tg, _new_tg, _new_slot = rule[1].split(',')
translate.add_rule(hex_str_3(int(_old_tg)), (hex_str_3(int(_new_tg)), int(_new_slot)))
except ConfigParser.Error, err:
traceback.print_exc()
sys.exit('Could not parse configuration file, ' + _file_name + ', exiting...')
# HBLink callback with DMR data from perr/master. Send this data to any partner listening
def dmrd_received(self, _radio_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data):
_dst_id, _slot = translate.find_rule(_dst_id,_slot)
_tx_slot = self.hb_ambe.tx[_slot]
_seq = ord(_data[4])
_tx_slot.frame_count += 1
if (_stream_id != _tx_slot.stream_id):
self.hb_ambe.begin_call(_slot, _rf_src, _dst_id, _radio_id, _tx_slot.cc, _seq, _stream_id)
_tx_slot.lastSeq = _seq
if (_frame_type == hb_const.HBPF_DATA_SYNC) and (_dtype_vseq == hb_const.HBPF_SLT_VTERM) and (_tx_slot.type != hb_const.HBPF_SLT_VTERM):
self.hb_ambe.end_call(_tx_slot)
if (int_id(_data[15]) & 0x20) == 0:
_dmr_frame = BitArray('0x'+ahex(_data[20:]))
_ambe = _dmr_frame[0:108] + _dmr_frame[156:264]
self.hb_ambe.export_voice(_tx_slot, _seq, _ambe.tobytes())
else:
_tx_slot.lastSeq = _seq
# The methods below are overridden becuse the launchUDP thread can also wite to a master or client async and confuse the master
# A lock is used to synchronize the two threads so that the resource is protected
def send_master(self, _packet):
mutex.acquire()
HBSYSTEM.send_master(self, _packet)
mutex.release()
def send_clients(self, _packet):
mutex.acquire()
HBSYSTEM.send_clients(self, _packet)
mutex.release()
############################################################################################################
# MAIN PROGRAM LOOP STARTS HERE
############################################################################################################
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from dmr_utils.utils import try_download, mk_id_dict
# 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
# Added the capability to define a custom bridge config file, multiple bridges are needed when doing things like Analog_Bridge
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CONFIG_FILE', help='/full/path/to/config.file (default hblink.cfg)')
parser.add_argument('-l', '--logging', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
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.CONFIG_FILE:
cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/hblink.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__))+'/HB_Bridge.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = hb_config.build_config(cli_args.CONFIG_FILE)
# Start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
logger = hb_log.config_logging(CONFIG['LOGGER'])
logger.debug('Logging system started, anything from here on gets logged')
# Set up the signal handler
def sig_handler(_signal, _frame):
logger.info('SHUTDOWN: HB_Bridge IS TERMINATING WITH SIGNAL %s', str(_signal))
hblink_handler(_signal, _frame, logger)
logger.info('SHUTDOWN: ALL SYSTEM HANDLERS EXECUTED - STOPPING REACTOR')
reactor.stop()
# Set signal handers so that we can gracefully exit if need be
for sig in [signal.SIGTERM, signal.SIGINT]:
signal.signal(sig, sig_handler)
# 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')
# HBlink instance creation
logger.info('HBlink \'HB_Bridge.py\' (c) 2017 Mike Zingman N4IRR, N0MJS - SYSTEM STARTING...')
logger.info('Version %s', __version__)
for system in CONFIG['SYSTEMS']:
if CONFIG['SYSTEMS'][system]['ENABLED']:
systems[system] = HB_BRIDGE(system, CONFIG, logger)
reactor.listenUDP(CONFIG['SYSTEMS'][system]['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['IP'])
logger.debug('%s instance created: %s, %s', CONFIG['SYSTEMS'][system]['MODE'], system, systems[system])
reactor.run()

View File

@ -134,7 +134,7 @@ class bridgeallSYSTEM(HBSYSTEM):
for _target in self._CONFIG['SYSTEMS']:
if _target != self._system:
systems[_target].send_system(_data)
self._logger.debug('(%s) Packet routed to system: %s', self._system, _target)
#self._logger.debug('(%s) Packet routed to system: %s', self._system, _target)
# Final actions - Is this a voice terminator?
@ -228,4 +228,4 @@ if __name__ == '__main__':
reactor.listenUDP(CONFIG['SYSTEMS'][system]['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['IP'])
logger.debug('%s instance created: %s, %s', CONFIG['SYSTEMS'][system]['MODE'], system, systems[system])
reactor.run()
reactor.run()

View File

@ -98,10 +98,25 @@ def make_bridges(_hb_confbridge_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')
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'
@ -109,13 +124,13 @@ def build_acl(_sub_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
global allow_sub
if acl_file.ACL_ACTION == 'PERMIT':
if ACL_ACTION == 'PERMIT':
def allow_sub(_sub):
if _sub in ACL:
return True
else:
return False
elif acl_file.ACL_ACTION == 'DENY':
elif ACL_ACTION == 'DENY':
def allow_sub(_sub):
if _sub not in ACL:
return True
@ -125,7 +140,7 @@ def build_acl(_sub_acl):
def allow_sub(_sub):
return True
return acl_file.ACL
return ACL
# Run this every minute for rule timer updates

View File

@ -93,6 +93,7 @@ def build_config(_config_file):
CONFIG['SYSTEMS'].update({section: {
'MODE': config.get(section, 'MODE'),
'ENABLED': config.getboolean(section, 'ENABLED'),
'LOOSE': config.getboolean(section, 'LOOSE'),
'EXPORT_AMBE': config.getboolean(section, 'EXPORT_AMBE'),
'IP': gethostbyname(config.get(section, 'IP')),
'PORT': config.getint(section, 'PORT'),
@ -121,6 +122,7 @@ def build_config(_config_file):
'CONNECTION': 'NO', # NO, RTPL_SENT, AUTHENTICATED, CONFIG-SENT, YES
'PINGS_SENT': 0,
'PINGS_ACKD': 0,
'NUM_OUTSTANDING': 0,
'PING_OUTSTANDING': False,
'LAST_PING_TX_TIME': 0,
'LAST_PING_ACK_TIME': 0,

View File

@ -101,10 +101,25 @@ def make_rules(_hb_routing_rules):
# 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')
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'
@ -112,13 +127,13 @@ def build_acl(_sub_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
global allow_sub
if acl_file.ACL_ACTION == 'PERMIT':
if ACL_ACTION == 'PERMIT':
def allow_sub(_sub):
if _sub in ACL:
return True
else:
return False
elif acl_file.ACL_ACTION == 'DENY':
elif ACL_ACTION == 'DENY':
def allow_sub(_sub):
if _sub not in ACL:
return True
@ -128,7 +143,7 @@ def build_acl(_sub_acl):
def allow_sub(_sub):
return True
return acl_file.ACL
return ACL
# Run this every minute for rule timer updates

View File

@ -28,7 +28,7 @@ MAX_MISSED: 3
[LOGGER]
LOG_FILE: /tmp/hblink.log
LOG_HANDLERS: console-timed
LOG_LEVEL: DEBUG
LOG_LEVEL: INFO
LOG_NAME: HBlink
# DOWNLOAD AND IMPORT SUBSCRIBER, PEER and TGID ALIASES
@ -40,11 +40,11 @@ LOG_NAME: HBlink
[ALIASES]
TRY_DOWNLOAD: True
PATH: ./
PEER_FILE: peer_ids.csv
SUBSCRIBER_FILE: subscriber_ids.csv
TGID_FILE: talkgroup_ids.csv
PEER_URL: http://www.dmr-marc.net/cgi-bin/trbo-database/datadump.cgi?table=repeaters&format=csv&header=0
SUBSCRIBER_URL: http://www.dmr-marc.net/cgi-bin/trbo-database/datadump.cgi?table=users&format=csv&header=0
PEER_FILE: peer_ids.json
SUBSCRIBER_FILE: subscriber_ids.json
TGID_FILE: talkgroup_ids.json
PEER_URL: https://www.radioid.net/static/rptrs.json
SUBSCRIBER_URL: https://www.radioid.net/static/users.json
STALE_DAYS: 7
# EXPORT AMBE DATA
@ -77,29 +77,32 @@ GROUP_HANGTIME: 5
# Latitude is an 8-digit unsigned floating point number.
# Longitude is a 9-digit signed floating point number.
# Height is in meters
# Setting Loose to True relaxes the validation on packets received from the master.
# This will allow HBlink to connect to a non-compliant system such as XLXD, DMR+ etc.
[REPEATER-1]
MODE: CLIENT
ENABLED: True
ENABLED: False
LOOSE: False
EXPORT_AMBE: False
IP:
PORT: 54001
MASTER_IP: 172.16.1.1
MASTER_PORT: 54000
PASSPHRASE: homebrew
CALLSIGN: W1ABC
RADIO_ID: 312000
RX_FREQ: 449000000
TX_FREQ: 444000000
CALLSIGN: W1AW
RADIO_ID: 1234567
RX_FREQ: 222340000
TX_FREQ: 223940000
TX_POWER: 25
COLORCODE: 1
SLOTS: 1
LATITUDE: 38.0000
LONGITUDE: -095.0000
SLOTS: 3
LATITUDE: 41.7333
LONGITUDE: -50.3999
HEIGHT: 75
LOCATION: Anywhere, USA
DESCRIPTION: This is a cool repeater
URL: www.w1abc.org
SOFTWARE_ID: HBlink
PACKAGE_ID: v0.1
LOCATION: Iceberg, USA
DESCRIPTION: HBlink repeater
URL: https://groups.io/g/DVSwitch
SOFTWARE_ID: 20170620
PACKAGE_ID: MMDVM_HBlink
GROUP_HANGTIME: 5
OPTIONS:
OPTIONS:

61
hblink.py Executable file → Normal file
View File

@ -37,6 +37,7 @@ from hashlib import sha256
from time import time
from bitstring import BitArray
import socket
import sys
# Twisted is pretty important, so I keep it separate
from twisted.internet.protocol import DatagramProtocol
@ -115,6 +116,7 @@ class HBSYSTEM(DatagramProtocol):
self._system = _name
self._logger = _logger
self._config = self._CONFIG['SYSTEMS'][self._system]
sys.excepthook = self.handle_exception
# Define shortcuts and generic function names based on the type of system we are
if self._config['MODE'] == 'MASTER':
@ -135,6 +137,12 @@ class HBSYSTEM(DatagramProtocol):
if self._config['EXPORT_AMBE']:
self._ambe = AMBE()
def handle_exception(self, exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
self._logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
def startProtocol(self):
# Set up periodic loop for tracking pings from clients. Run every 'PING_TIME' seconds
self._system_maintenance = task.LoopingCall(self.maintenance_loop)
@ -154,18 +162,23 @@ class HBSYSTEM(DatagramProtocol):
# Aliased in __init__ to maintenance_loop if system is a client
def client_maintenance_loop(self):
self._logger.debug('(%s) Client maintenance loop started', self._system)
if self._stats['PING_OUTSTANDING']:
self._stats['NUM_OUTSTANDING'] += 1
# If we're not connected, zero out the stats and send a login request RPTL
if self._stats['CONNECTION'] == 'NO' or self._stats['CONNECTION'] == 'RTPL_SENT':
if self._stats['CONNECTION'] == 'NO' or self._stats['CONNECTION'] == 'RPTL_SENT' or self._stats['NUM_OUTSTANDING'] >= self._CONFIG['GLOBAL']['MAX_MISSED']:
self._stats['PINGS_SENT'] = 0
self._stats['PINGS_ACKD'] = 0
self._stats['CONNECTION'] = 'RTPL_SENT'
self._stats['NUM_OUTSTANDING'] = 0
self._stats['PING_OUTSTANDING'] = False
self._stats['CONNECTION'] = 'RPTL_SENT'
self.send_master('RPTL'+self._config['RADIO_ID'])
self._logger.info('(%s) Sending login request to master %s:%s', self._system, self._config['MASTER_IP'], self._config['MASTER_PORT'])
# If we are connected, sent a ping to the master and increment the counter
if self._stats['CONNECTION'] == 'YES':
self.send_master('RPTPING'+self._config['RADIO_ID'])
self._logger.debug('(%s) RPTPING Sent to Master. Total Sent: %s, Total Missed: %s, Currently Outstanding: %s', self._system, self._stats['PINGS_SENT'], self._stats['PINGS_SENT'] - self._stats['PINGS_ACKD'], self._stats['NUM_OUTSTANDING'])
self._stats['PINGS_SENT'] += 1
self._logger.debug('(%s) RPTPING Sent to Master. Pings Since Connected: %s', self._system, self._stats['PINGS_SENT'])
self._stats['PING_OUTSTANDING'] = True
def send_clients(self, _packet):
for _client in self._clients:
@ -229,6 +242,9 @@ class HBSYSTEM(DatagramProtocol):
if self._config['REPEAT'] == True:
for _client in self._clients:
if _client != _radio_id:
_data = _data[0:11] + _client + _data[15:]
self.send_client(_client, _data)
self._logger.debug('(%s) Packet on TS%s from %s (%s) for destination ID %s repeated to client: %s (%s) [Stream ID: %s]', self._system, _slot, self._clients[_radio_id]['CALLSIGN'], int_id(_radio_id), int_id(_dst_id), self._clients[_client]['CALLSIGN'], int_id(_client), int_id(_stream_id))
@ -348,7 +364,7 @@ class HBSYSTEM(DatagramProtocol):
self._logger.warning('(%s) Client info from Radio ID that has not logged in: %s', self._system, int_id(_radio_id))
else:
self._logger.error('(%s) Unrecognized command from: %s. Packet: %s', self._system, int_id(_radio_id), ahex(_data))
self._logger.error('(%s) Unrecognized command. Raw HBP PDU: %s', self._system, ahex(_data))
# Aliased in __init__ to datagramReceived if system is a client
def client_datagramReceived(self, _data, (_host, _port)):
@ -361,7 +377,7 @@ class HBSYSTEM(DatagramProtocol):
_command = _data[:4]
if _command == 'DMRD': # DMRData -- encapsulated DMR data frame
_radio_id = _data[11:15]
if _radio_id == self._config['RADIO_ID']: # Validate the source and intended target
if self._config['LOOSE'] or _radio_id == self._config['RADIO_ID']: # Validate the Radio_ID unless using loose validation
_seq = _data[4:5]
_rf_src = _data[5:8]
_dst_id = _data[8:11]
@ -379,16 +395,21 @@ class HBSYSTEM(DatagramProtocol):
# Userland actions -- typically this is the function you subclass for an application
self.dmrd_received(_radio_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data)
else:
if (ord(_data[15]) & 0x2F) == 0x21: # call initiator flag?
self._logger.warning('(%s) Packet received for wrong RADIO_ID. Got %d should be %d', self._system, int_id(_radio_id), int_id(self._config['RADIO_ID']))
elif _command == 'MSTN': # Actually MSTNAK -- a NACK from the master
_radio_id = _data[4:8]
if _radio_id == self._config['RADIO_ID']: # Validate the source and intended target
self._logger.warning('(%s) MSTNAK Received', self._system)
_radio_id = _data[6:10] #
if self._config['LOOSE'] or _radio_id == self._config['RADIO_ID']: # Validate the Radio_ID unless using loose validation
self._logger.warning('(%s) MSTNAK Received. Resetting connection to the Master.', self._system)
self._stats['CONNECTION'] = 'NO' # Disconnect ourselves and re-register
else:
self._logger.debug('(%s) MSTNAK contained wrong ID - Ignoring', self._system)
elif _command == 'RPTA': # Actually RPTACK -- an ACK from the master
# Depending on the state, an RPTACK means different things, in each clause, we check and/or set the state
if self._stats['CONNECTION'] == 'RTPL_SENT': # If we've sent a login request...
if self._stats['CONNECTION'] == 'RPTL_SENT': # If we've sent a login request...
_login_int32 = _data[6:10]
self._logger.info('(%s) Repeater Login ACK Received with 32bit ID: %s', self._system, int_id(_login_int32))
_pass_hash = sha256(_login_int32+self._config['PASSPHRASE']).hexdigest()
@ -397,7 +418,8 @@ class HBSYSTEM(DatagramProtocol):
self._stats['CONNECTION'] = 'AUTHENTICATED'
elif self._stats['CONNECTION'] == 'AUTHENTICATED': # If we've sent the login challenge...
if _data[6:10] == self._config['RADIO_ID']:
_radio_id = _data[6:10]
if self._config['LOOSE'] or _radio_id == self._config['RADIO_ID']: # Validate the Radio_ID unless using loose validation
self._logger.info('(%s) Repeater Authentication Accepted', self._system)
_config_packet = self._config['RADIO_ID']+\
self._config['CALLSIGN']+\
@ -423,8 +445,8 @@ class HBSYSTEM(DatagramProtocol):
self._logger.error('(%s) Master ACK Contained wrong ID - Connection Reset', self._system)
elif self._stats['CONNECTION'] == 'CONFIG-SENT': # If we've sent out configuration to the master
if _data[6:10] == self._config['RADIO_ID']:
self._logger.info('(%s) Repeater Configuration Accepted', self._system)
_radio_id = _data[6:10]
if self._config['LOOSE'] or _radio_id == self._config['RADIO_ID']: # Validate the Radio_ID unless using loose validation
if self._config['OPTIONS']:
self.send_master('RPTO'+self._config['RADIO_ID']+self._config['OPTIONS'])
self._stats['CONNECTION'] = 'OPTIONS-SENT'
@ -437,7 +459,8 @@ class HBSYSTEM(DatagramProtocol):
self._logger.error('(%s) Master ACK Contained wrong ID - Connection Reset', self._system)
elif self._stats['CONNECTION'] == 'OPTIONS-SENT': # If we've sent out options to the master
if _data[6:10] == self._config['RADIO_ID']:
_radio_id = _data[6:10]
if self._config['LOOSE'] or _radio_id == self._config['RADIO_ID']: # Validate the Radio_ID unless using loose validation
self._logger.info('(%s) Repeater Options Accepted', self._system)
self._stats['CONNECTION'] = 'YES'
self._logger.info('(%s) Connection to Master Completed with options', self._system)
@ -446,14 +469,22 @@ class HBSYSTEM(DatagramProtocol):
self._logger.error('(%s) Master ACK Contained wrong ID - Connection Reset', self._system)
elif _command == 'MSTP': # Actually MSTPONG -- a reply to RPTPING (send by client)
if _data [7:11] == self._config['RADIO_ID']:
_radio_id = _data[7:11]
if self._config['LOOSE'] or _radio_id == self._config['RADIO_ID']: # Validate the Radio_ID unless using loose validation
self._stats['PING_OUTSTANDING'] = False
self._stats['NUM_OUTSTANDING'] = 0
self._stats['PINGS_ACKD'] += 1
self._logger.debug('(%s) MSTPONG Received. Pongs Since Connected: %s', self._system, self._stats['PINGS_ACKD'])
else:
self._logger.debug('(%s) MSTPONG contained wrong ID - Ignoring', self._system)
elif _command == 'MSTC': # Actually MSTCL -- notify us the master is closing down
if _data[5:9] == self._config['RADIO_ID']:
_radio_id = _data[5:9]
if self._config['LOOSE'] or _radio_id == self._config['RADIO_ID']: # Validate the Radio_ID unless using loose validation
self._stats['CONNECTION'] = 'NO'
self._logger.info('(%s) MSTCL Recieved', self._system)
else:
self._logger.debug('(%s) MSTCL contained wrong ID - Ignoring', self._system)
else:
self._logger.error('(%s) Received an invalid command in packet: %s', self._system, ahex(_data))

10
mk-required Normal file
View File

@ -0,0 +1,10 @@
apt-get install python-dev -y
apt-get install python-pip -y
apt-get install python-twisted -y
pip install bitstring
pip install bitarray
cd /opt
git clone https://github.com/n0mjs710/dmr_utils.git
cd dmr_utils/
pip install --upgrade .

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,5 @@
'''
This is the Access Control List (ACL) file for limiting call
routing/bridging in various hblink.py-based applications. It
is a VERY simple format. The action may be to PERMIT or DENY
and the ACL itself is a list of subscriber IDs that may be
permitted or denied.
'''
ACL_ACTION = "DENY" # May be PERMIT|DENY
ACL = [
1,2,3,4,5,6,7,8,9,10,100
]
# 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:0-2999,4000000-9999999'

File diff suppressed because it is too large Load Diff