parent
00f2e46b8b
commit
011219ad41
@ -0,0 +1,107 @@
|
|||||||
|
import mysql.connector
|
||||||
|
from mysql.connector import errorcode
|
||||||
|
#import mysql.connector.pooling
|
||||||
|
|
||||||
|
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
|
||||||
|
__author__ = 'Simon Adlem - G7RZU'
|
||||||
|
__copyright__ = 'Copyright (c) Simon Adlem, G7RZU 2020,2021'
|
||||||
|
__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT; Jon Lee, G4TSN; Norman Williams, M6NBP'
|
||||||
|
__license__ = 'GNU GPLv3'
|
||||||
|
__maintainer__ = 'Simon Adlem G7RZU'
|
||||||
|
__email__ = 'simon@gb7fr.org.uk'
|
||||||
|
|
||||||
|
|
||||||
|
class useMYSQL:
|
||||||
|
#Init new object
|
||||||
|
def __init__(self, server,user,password,database,table,logger):
|
||||||
|
self.server = server
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.database = database
|
||||||
|
self.table = table
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
#Connect
|
||||||
|
def con(self):
|
||||||
|
logger = self.logger
|
||||||
|
try:
|
||||||
|
self.db = mysql.connector.connect(
|
||||||
|
host=self.server,
|
||||||
|
user=self.user,
|
||||||
|
password=self.password,
|
||||||
|
database=self.database,
|
||||||
|
# pool_name = "hblink_master",
|
||||||
|
# pool_size = 2
|
||||||
|
)
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
|
||||||
|
logger.info('(MYSQL) username or password error')
|
||||||
|
return (False)
|
||||||
|
elif err.errno == errorcode.ER_BAD_DB_ERROR:
|
||||||
|
logger.info('(MYSQL) DB Error')
|
||||||
|
return (False)
|
||||||
|
else:
|
||||||
|
logger.info('(MYSQL) error: %s',err)
|
||||||
|
return(False)
|
||||||
|
|
||||||
|
return(True)
|
||||||
|
|
||||||
|
#Close DB connection
|
||||||
|
def close(self):
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
#Get config from DB
|
||||||
|
def getConfig(self):
|
||||||
|
|
||||||
|
CONFIG = {}
|
||||||
|
CONFIG['SYSTEMS'] = {}
|
||||||
|
|
||||||
|
_cursor = self.db.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_cursor.execute("select * from {} where MODE='MASTER'".format(self.table))
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
_cursor.close()
|
||||||
|
logger.info('(MYSQL) error, problem with cursor execute')
|
||||||
|
raise Exception('Problem with cursor execute')
|
||||||
|
|
||||||
|
for (callsign, mode, enabled, _repeat, max_peers, export_ambe, ip, port, passphrase, group_hangtime, use_acl, reg_acl, sub_acl, tgid_ts1_acl, tgid_ts2_acl, default_ua_timer, single_mode, voice_ident,ts1_static,ts2_static,default_reflector, announce_lang) in _cursor:
|
||||||
|
try:
|
||||||
|
CONFIG['SYSTEMS'].update({callsign: {
|
||||||
|
'MODE': mode,
|
||||||
|
'ENABLED': bool(enabled),
|
||||||
|
'REPEAT': bool(_repeat),
|
||||||
|
'MAX_PEERS': int(max_peers),
|
||||||
|
'IP': ip,
|
||||||
|
'PORT': int(port),
|
||||||
|
'PASSPHRASE': bytes(passphrase, 'utf-8'),
|
||||||
|
'GROUP_HANGTIME': int(group_hangtime),
|
||||||
|
'USE_ACL': bool(use_acl),
|
||||||
|
'REG_ACL': reg_acl,
|
||||||
|
'SUB_ACL': sub_acl,
|
||||||
|
'TG1_ACL': tgid_ts1_acl,
|
||||||
|
'TG2_ACL': tgid_ts2_acl,
|
||||||
|
'DEFAULT_UA_TIMER': int(default_ua_timer),
|
||||||
|
'SINGLE_MODE': bool(single_mode),
|
||||||
|
'VOICE_IDENT': bool(voice_ident),
|
||||||
|
'TS1_STATIC': ts1_static,
|
||||||
|
'TS2_STATIC': ts2_static,
|
||||||
|
'DEFAULT_REFLECTOR': int(default_reflector),
|
||||||
|
'GENERATOR': int(1),
|
||||||
|
'ANNOUNCEMENT_LANGUAGE': announce_lang
|
||||||
|
}})
|
||||||
|
CONFIG['SYSTEMS'][callsign].update({'PEERS': {}})
|
||||||
|
except TypeError:
|
||||||
|
logger.info('(MYSQL) Problem with data from MySQL - TypeError, carrying on to next row')
|
||||||
|
|
||||||
|
return(CONFIG['SYSTEMS'])
|
||||||
|
|
||||||
|
|
||||||
|
#For testing
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
sql = useMYSQL("ip","user","pass","db")
|
||||||
|
|
||||||
|
sql.con()
|
||||||
|
|
||||||
|
print( sql.getConfig())
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Copyright (C) 2020 Simon Adlem, G7RZU <g7rzu@gb7fr.org.uk>
|
||||||
|
#
|
||||||
|
# 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 bitarray import bitarray
|
||||||
|
from itertools import islice
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
class readAMBE:
|
||||||
|
|
||||||
|
def __init__(self, lang,path):
|
||||||
|
self.langcsv = lang
|
||||||
|
self.langs = lang.split(',')
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def _make_bursts(self,data):
|
||||||
|
it = iter(data)
|
||||||
|
for i in range(0, len(data), 108):
|
||||||
|
yield bitarray([k for k in islice(it, 108)] )
|
||||||
|
|
||||||
|
#Read indexed files
|
||||||
|
def readfiles(self):
|
||||||
|
|
||||||
|
_AMBE_LENGTH = 9
|
||||||
|
|
||||||
|
_wordBADictofDicts = {}
|
||||||
|
|
||||||
|
for _lang in self.langs:
|
||||||
|
|
||||||
|
_prefix = self.path+_lang
|
||||||
|
_wordBADict = {}
|
||||||
|
|
||||||
|
indexDict = {}
|
||||||
|
|
||||||
|
if os.path.isdir(_prefix):
|
||||||
|
ambeBytearray = {}
|
||||||
|
_wordBitarray = bitarray(endian='big')
|
||||||
|
_wordBADict = {}
|
||||||
|
_glob = _prefix + "/*.ambe"
|
||||||
|
for ambe in glob.glob(_glob):
|
||||||
|
basename = os.path.basename(ambe)
|
||||||
|
_voice,ext = basename.split('.')
|
||||||
|
inambe = open(ambe,'rb')
|
||||||
|
_wordBitarray.frombytes(inambe.read())
|
||||||
|
inambe.close()
|
||||||
|
_wordBADict[_voice] = []
|
||||||
|
pairs = 1
|
||||||
|
_lastburst = ''
|
||||||
|
for _burst in self._make_bursts(_wordBitarray):
|
||||||
|
#Not sure if we need to pad or not? Seems to make little difference.
|
||||||
|
if len(_burst) < 108:
|
||||||
|
pad = (108 - len(_burst))
|
||||||
|
for i in range(0,pad,1):
|
||||||
|
_burst.append(False)
|
||||||
|
if pairs == 2:
|
||||||
|
_wordBADict[_voice].append([_lastburst,_burst])
|
||||||
|
_lastburst = ''
|
||||||
|
pairs = 1
|
||||||
|
next
|
||||||
|
else:
|
||||||
|
pairs = pairs + 1
|
||||||
|
_lastburst = _burst
|
||||||
|
|
||||||
|
_wordBitarray.clear()
|
||||||
|
_wordBADict['silence'] = ([
|
||||||
|
[bitarray('101011000000101010100000010000000000001000000000000000000000010001000000010000000000100000000000100000000000'),
|
||||||
|
bitarray('001010110000001010101000000100000000000010000000000000000000000100010000000100000000001000000000001000000000')]
|
||||||
|
])
|
||||||
|
_wordBADictofDicts[_lang] = _wordBADict
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(_prefix+'.indx') as index:
|
||||||
|
for line in index:
|
||||||
|
(voice,start,length) = line.split()
|
||||||
|
indexDict[voice] = [int(start) * _AMBE_LENGTH ,int(length) * _AMBE_LENGTH]
|
||||||
|
index.close()
|
||||||
|
except IOError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ambeBytearray = {}
|
||||||
|
_wordBitarray = bitarray(endian='big')
|
||||||
|
_wordBADict = {}
|
||||||
|
try:
|
||||||
|
with open(_prefix+'.ambe','rb') as ambe:
|
||||||
|
for _voice in indexDict:
|
||||||
|
ambe.seek(indexDict[_voice][0])
|
||||||
|
_wordBitarray.frombytes(ambe.read(indexDict[_voice][1]))
|
||||||
|
#108
|
||||||
|
_wordBADict[_voice] = []
|
||||||
|
pairs = 1
|
||||||
|
_lastburst = ''
|
||||||
|
for _burst in self._make_bursts(_wordBitarray):
|
||||||
|
#Not sure if we need to pad or not? Seems to make little difference.
|
||||||
|
if len(_burst) < 108:
|
||||||
|
pad = (108 - len(_burst))
|
||||||
|
for i in range(0,pad,1):
|
||||||
|
_burst.append(False)
|
||||||
|
if pairs == 2:
|
||||||
|
_wordBADict[_voice].append([_lastburst,_burst])
|
||||||
|
_lastburst = ''
|
||||||
|
pairs = 1
|
||||||
|
next
|
||||||
|
else:
|
||||||
|
pairs = pairs + 1
|
||||||
|
_lastburst = _burst
|
||||||
|
|
||||||
|
_wordBitarray.clear()
|
||||||
|
ambe.close()
|
||||||
|
except IOError:
|
||||||
|
return False
|
||||||
|
_wordBADict['silence'] = ([
|
||||||
|
[bitarray('101011000000101010100000010000000000001000000000000000000000010001000000010000000000100000000000100000000000'),
|
||||||
|
bitarray('001010110000001010101000000100000000000010000000000000000000000100010000000100000000001000000000001000000000')]
|
||||||
|
])
|
||||||
|
_wordBADictofDicts[_lang] = _wordBADict
|
||||||
|
|
||||||
|
return _wordBADictofDicts
|
||||||
|
|
||||||
|
#Read a single ambe file from the audio directory
|
||||||
|
def readSingleFile(self,filename):
|
||||||
|
ambeBytearray = {}
|
||||||
|
_wordBitarray = bitarray(endian='big')
|
||||||
|
_wordBA= []
|
||||||
|
try:
|
||||||
|
with open(self.path+filename,'rb') as ambe:
|
||||||
|
_wordBitarray.frombytes(ambe.read())
|
||||||
|
#108
|
||||||
|
_wordBA = []
|
||||||
|
pairs = 1
|
||||||
|
_lastburst = ''
|
||||||
|
for _burst in self._make_bursts(_wordBitarray):
|
||||||
|
#Not sure if we need to pad or not? Seems to make little difference.
|
||||||
|
if len(_burst) < 108:
|
||||||
|
pad = (108 - len(_burst))
|
||||||
|
for i in range(0,pad,1):
|
||||||
|
_burst.append(False)
|
||||||
|
if pairs == 2:
|
||||||
|
_wordBA.append([_lastburst,_burst])
|
||||||
|
_lastburst = ''
|
||||||
|
pairs = 1
|
||||||
|
next
|
||||||
|
else:
|
||||||
|
pairs = pairs + 1
|
||||||
|
_lastburst = _burst
|
||||||
|
|
||||||
|
_wordBitarray.clear()
|
||||||
|
ambe.close()
|
||||||
|
except IOError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return(_wordBA)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
#test = readAMBE('en_GB','./Audio/')
|
||||||
|
|
||||||
|
#print(test.readfiles())
|
||||||
|
test = readAMBE('en_GB_2','./Audio/')
|
||||||
|
print(test.readfiles())
|
||||||
|
print(test.readSingleFile('44xx.ambe'))
|
||||||
@ -0,0 +1,408 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
# 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
|
||||||
|
# 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 module generates the configuration data structure for hblink.py and
|
||||||
|
assoicated programs that use it. It has been seaparated into a different
|
||||||
|
module so as to keep hblink.py easeier to navigate. This file only needs
|
||||||
|
updated if the items in the main configuraiton file (usually hblink.cfg)
|
||||||
|
change.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import sys
|
||||||
|
import FreeDMR.Const.const as const
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import ipaddress
|
||||||
|
from socket import gethostbyname
|
||||||
|
from languages import languages
|
||||||
|
|
||||||
|
|
||||||
|
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
|
||||||
|
__author__ = 'Cortney T. Buffington, N0MJS'
|
||||||
|
__copyright__ = '(c) Simon Adlem, G7RZU 2020-2021, Copyright (c) 2016-2018 Cortney T. Buffington, N0MJS and the K0USY Group'
|
||||||
|
__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT'
|
||||||
|
__license__ = 'GNU GPLv3'
|
||||||
|
__maintainer__ = 'Simon Adlem, G7RZU'
|
||||||
|
__email__ = 'simon@gb7fr.org.uk'
|
||||||
|
|
||||||
|
# Processing of ALS goes here. It's separated from the acl_build function because this
|
||||||
|
# code is hblink config-file format specific, and acl_build is abstracted
|
||||||
|
def process_acls(_config):
|
||||||
|
# Global registration ACL
|
||||||
|
_config['GLOBAL']['REG_ACL'] = acl_build(_config['GLOBAL']['REG_ACL'], const.PEER_MAX)
|
||||||
|
|
||||||
|
# Global subscriber and TGID ACLs
|
||||||
|
for acl in ['SUB_ACL', 'TG1_ACL', 'TG2_ACL']:
|
||||||
|
_config['GLOBAL'][acl] = acl_build(_config['GLOBAL'][acl], const.ID_MAX)
|
||||||
|
|
||||||
|
# System level ACLs
|
||||||
|
for system in _config['SYSTEMS']:
|
||||||
|
# Registration ACLs (which make no sense for peer systems)
|
||||||
|
if _config['SYSTEMS'][system]['MODE'] == 'MASTER':
|
||||||
|
_config['SYSTEMS'][system]['REG_ACL'] = acl_build(_config['SYSTEMS'][system]['REG_ACL'], const.PEER_MAX)
|
||||||
|
|
||||||
|
# Subscriber and TGID ACLs (valid for all system types)
|
||||||
|
for acl in ['SUB_ACL', 'TG1_ACL', 'TG2_ACL']:
|
||||||
|
_config['SYSTEMS'][system][acl] = acl_build(_config['SYSTEMS'][system][acl], const.ID_MAX)
|
||||||
|
|
||||||
|
# Create an access control list that is programatically useable from human readable:
|
||||||
|
# ORIGINAL: 'DENY:1-5,3120101,3120124'
|
||||||
|
# PROCESSED: (False, set([(1, 5), (3120124, 3120124), (3120101, 3120101)]))
|
||||||
|
def acl_build(_acl, _max):
|
||||||
|
if not _acl:
|
||||||
|
return(True, set((const.ID_MIN, _max)))
|
||||||
|
|
||||||
|
acl = [] #set()
|
||||||
|
sections = _acl.split(':')
|
||||||
|
|
||||||
|
if sections[0] == 'PERMIT':
|
||||||
|
action = True
|
||||||
|
else:
|
||||||
|
action = False
|
||||||
|
|
||||||
|
for entry in sections[1].split(','):
|
||||||
|
if entry == 'ALL':
|
||||||
|
acl.append((const.ID_MIN, _max))
|
||||||
|
break
|
||||||
|
|
||||||
|
elif '-' in entry:
|
||||||
|
start,end = entry.split('-')
|
||||||
|
start,end = int(start), int(end)
|
||||||
|
if (const.ID_MIN <= start <= _max) or (const.ID_MIN <= end <= _max):
|
||||||
|
acl.append((start, end))
|
||||||
|
else:
|
||||||
|
sys.exit('ACL CREATION ERROR, VALUE OUT OF RANGE ({} - {})IN RANGE-BASED ENTRY: {}'.format(const.ID_MIN, _max, entry))
|
||||||
|
else:
|
||||||
|
id = int(entry)
|
||||||
|
if (const.ID_MIN <= id <= _max):
|
||||||
|
acl.append((id, id))
|
||||||
|
else:
|
||||||
|
sys.exit('ACL CREATION ERROR, VALUE OUT OF RANGE ({} - {}) IN SINGLE ID ENTRY: {}'.format(const.ID_MIN, _max, entry))
|
||||||
|
|
||||||
|
return (action, acl)
|
||||||
|
|
||||||
|
def IsIPv4Address(ip):
|
||||||
|
try:
|
||||||
|
ipaddress.IPv4Address(ip)
|
||||||
|
return True
|
||||||
|
except ValueError as errorCode:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def IsIPv6Address(ip):
|
||||||
|
try:
|
||||||
|
ipaddress.IPv6Address(ip)
|
||||||
|
return True
|
||||||
|
except ValueError as errorCode:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def build_config(_config_file):
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
|
if not config.read(_config_file):
|
||||||
|
sys.exit('Configuration file \''+_config_file+'\' is not a valid configuration file! Exiting...')
|
||||||
|
|
||||||
|
CONFIG = {}
|
||||||
|
CONFIG['GLOBAL'] = {}
|
||||||
|
CONFIG['REPORTS'] = {}
|
||||||
|
CONFIG['LOGGER'] = {}
|
||||||
|
CONFIG['ALIASES'] = {}
|
||||||
|
CONFIG['SYSTEMS'] = {}
|
||||||
|
CONFIG['MYSQL'] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
for section in config.sections():
|
||||||
|
if section == 'GLOBAL':
|
||||||
|
CONFIG['GLOBAL'].update({
|
||||||
|
'PATH': config.get(section, 'PATH'),
|
||||||
|
'PING_TIME': config.getint(section, 'PING_TIME'),
|
||||||
|
'MAX_MISSED': config.getint(section, 'MAX_MISSED'),
|
||||||
|
'USE_ACL': config.get(section, 'USE_ACL'),
|
||||||
|
'REG_ACL': config.get(section, 'REG_ACL'),
|
||||||
|
'SUB_ACL': config.get(section, 'SUB_ACL'),
|
||||||
|
'TG1_ACL': config.get(section, 'TGID_TS1_ACL'),
|
||||||
|
'TG2_ACL': config.get(section, 'TGID_TS2_ACL'),
|
||||||
|
'GEN_STAT_BRIDGES': config.getboolean(section, 'GEN_STAT_BRIDGES'),
|
||||||
|
'ALLOW_NULL_PASSPHRASE': config.getboolean(section, 'ALLOW_NULL_PASSPHRASE'),
|
||||||
|
'ANNOUNCEMENT_LANGUAGES': config.get(section, 'ANNOUNCEMENT_LANGUAGES'),
|
||||||
|
'SERVER_ID': config.getint(section, 'SERVER_ID').to_bytes(4, 'big')
|
||||||
|
|
||||||
|
})
|
||||||
|
if not CONFIG['GLOBAL']['ANNOUNCEMENT_LANGUAGES']:
|
||||||
|
CONFIG['GLOBAL']['ANNOUNCEMENT_LANGUAGES'] = languages
|
||||||
|
|
||||||
|
elif section == 'REPORTS':
|
||||||
|
CONFIG['REPORTS'].update({
|
||||||
|
'REPORT': config.getboolean(section, 'REPORT'),
|
||||||
|
'REPORT_INTERVAL': config.getint(section, 'REPORT_INTERVAL'),
|
||||||
|
'REPORT_PORT': config.getint(section, 'REPORT_PORT'),
|
||||||
|
'REPORT_CLIENTS': config.get(section, 'REPORT_CLIENTS').split(',')
|
||||||
|
})
|
||||||
|
|
||||||
|
elif section == 'LOGGER':
|
||||||
|
CONFIG['LOGGER'].update({
|
||||||
|
'LOG_FILE': config.get(section, 'LOG_FILE'),
|
||||||
|
'LOG_HANDLERS': config.get(section, 'LOG_HANDLERS'),
|
||||||
|
'LOG_LEVEL': config.get(section, 'LOG_LEVEL'),
|
||||||
|
'LOG_NAME': config.get(section, 'LOG_NAME')
|
||||||
|
})
|
||||||
|
if not CONFIG['LOGGER']['LOG_FILE']:
|
||||||
|
CONFIG['LOGGER']['LOG_FILE'] = '/dev/null'
|
||||||
|
|
||||||
|
elif section == 'ALIASES':
|
||||||
|
CONFIG['ALIASES'].update({
|
||||||
|
'TRY_DOWNLOAD': config.getboolean(section, 'TRY_DOWNLOAD'),
|
||||||
|
'PATH': config.get(section, 'PATH'),
|
||||||
|
'PEER_FILE': config.get(section, 'PEER_FILE'),
|
||||||
|
'SUBSCRIBER_FILE': config.get(section, 'SUBSCRIBER_FILE'),
|
||||||
|
'TGID_FILE': config.get(section, 'TGID_FILE'),
|
||||||
|
'PEER_URL': config.get(section, 'PEER_URL'),
|
||||||
|
'SUBSCRIBER_URL': config.get(section, 'SUBSCRIBER_URL'),
|
||||||
|
'TGID_URL': config.get(section, 'TGID_URL'),
|
||||||
|
'STALE_TIME': config.getint(section, 'STALE_DAYS') * 86400,
|
||||||
|
'SUB_MAP_FILE': config.get(section, 'SUB_MAP_FILE')
|
||||||
|
})
|
||||||
|
|
||||||
|
elif section == 'MYSQL':
|
||||||
|
CONFIG['MYSQL'].update({
|
||||||
|
'USE_MYSQL': config.getboolean(section, 'USE_MYSQL'),
|
||||||
|
'USER': config.get(section, 'USER'),
|
||||||
|
'PASS': config.get(section, 'PASS'),
|
||||||
|
'DB': config.get(section, 'DB'),
|
||||||
|
'SERVER': config.get(section, 'SERVER'),
|
||||||
|
'PORT': config.getint(section,'PORT'),
|
||||||
|
'TABLE': config.get(section, 'TABLE')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
elif config.getboolean(section, 'ENABLED'):
|
||||||
|
if config.get(section, 'MODE') == 'PEER':
|
||||||
|
CONFIG['SYSTEMS'].update({section: {
|
||||||
|
'MODE': config.get(section, 'MODE'),
|
||||||
|
'ENABLED': config.getboolean(section, 'ENABLED'),
|
||||||
|
'LOOSE': config.getboolean(section, 'LOOSE'),
|
||||||
|
'SOCK_ADDR': (gethostbyname(config.get(section, 'IP')), config.getint(section, 'PORT')),
|
||||||
|
'IP': gethostbyname(config.get(section, 'IP')),
|
||||||
|
'PORT': config.getint(section, 'PORT'),
|
||||||
|
'MASTER_SOCKADDR': (gethostbyname(config.get(section, 'MASTER_IP')), config.getint(section, 'MASTER_PORT')),
|
||||||
|
'MASTER_IP': gethostbyname(config.get(section, 'MASTER_IP')),
|
||||||
|
'_MASTER_IP': config.get(section, 'MASTER_IP'),
|
||||||
|
'MASTER_PORT': config.getint(section, 'MASTER_PORT'),
|
||||||
|
'PASSPHRASE': bytes(config.get(section, 'PASSPHRASE'), 'utf-8'),
|
||||||
|
'CALLSIGN': bytes(config.get(section, 'CALLSIGN').ljust(8)[:8], 'utf-8'),
|
||||||
|
'RADIO_ID': config.getint(section, 'RADIO_ID').to_bytes(4, 'big'),
|
||||||
|
'RX_FREQ': bytes(config.get(section, 'RX_FREQ').ljust(9)[:9], 'utf-8'),
|
||||||
|
'TX_FREQ': bytes(config.get(section, 'TX_FREQ').ljust(9)[:9], 'utf-8'),
|
||||||
|
'TX_POWER': bytes(config.get(section, 'TX_POWER').rjust(2,'0'), 'utf-8'),
|
||||||
|
'COLORCODE': bytes(config.get(section, 'COLORCODE').rjust(2,'0'), 'utf-8'),
|
||||||
|
'LATITUDE': bytes(config.get(section, 'LATITUDE').ljust(8)[:8], 'utf-8'),
|
||||||
|
'LONGITUDE': bytes(config.get(section, 'LONGITUDE').ljust(9)[:9], 'utf-8'),
|
||||||
|
'HEIGHT': bytes(config.get(section, 'HEIGHT').rjust(3,'0'), 'utf-8'),
|
||||||
|
'LOCATION': bytes(config.get(section, 'LOCATION').ljust(20)[:20], 'utf-8'),
|
||||||
|
'DESCRIPTION': bytes(config.get(section, 'DESCRIPTION').ljust(19)[:19], 'utf-8'),
|
||||||
|
'SLOTS': bytes(config.get(section, 'SLOTS'), 'utf-8'),
|
||||||
|
'URL': bytes(config.get(section, 'URL').ljust(124)[:124], 'utf-8'),
|
||||||
|
'SOFTWARE_ID': bytes(config.get(section, 'SOFTWARE_ID').ljust(40)[:40], 'utf-8'),
|
||||||
|
'PACKAGE_ID': bytes(config.get(section, 'PACKAGE_ID').ljust(40)[:40], 'utf-8'),
|
||||||
|
'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME'),
|
||||||
|
'OPTIONS': bytes(config.get(section, 'OPTIONS'), 'utf-8'),
|
||||||
|
'USE_ACL': config.getboolean(section, 'USE_ACL'),
|
||||||
|
'SUB_ACL': config.get(section, 'SUB_ACL'),
|
||||||
|
'TG1_ACL': config.get(section, 'TGID_TS1_ACL'),
|
||||||
|
'TG2_ACL': config.get(section, 'TGID_TS2_ACL'),
|
||||||
|
'ANNOUNCEMENT_LANGUAGE': config.get(section, 'ANNOUNCEMENT_LANGUAGE')
|
||||||
|
}})
|
||||||
|
CONFIG['SYSTEMS'][section].update({'STATS': {
|
||||||
|
'CONNECTION': 'NO', # NO, RTPL_SENT, AUTHENTICATED, CONFIG-SENT, YES
|
||||||
|
'CONNECTED': None,
|
||||||
|
'PINGS_SENT': 0,
|
||||||
|
'PINGS_ACKD': 0,
|
||||||
|
'NUM_OUTSTANDING': 0,
|
||||||
|
'PING_OUTSTANDING': False,
|
||||||
|
'LAST_PING_TX_TIME': 0,
|
||||||
|
'LAST_PING_ACK_TIME': 0,
|
||||||
|
}})
|
||||||
|
|
||||||
|
if config.get(section, 'MODE') == 'XLXPEER':
|
||||||
|
CONFIG['SYSTEMS'].update({section: {
|
||||||
|
'MODE': config.get(section, 'MODE'),
|
||||||
|
'ENABLED': config.getboolean(section, 'ENABLED'),
|
||||||
|
'LOOSE': config.getboolean(section, 'LOOSE'),
|
||||||
|
'SOCK_ADDR': (gethostbyname(config.get(section, 'IP')), config.getint(section, 'PORT')),
|
||||||
|
'IP': gethostbyname(config.get(section, 'IP')),
|
||||||
|
'PORT': config.getint(section, 'PORT'),
|
||||||
|
'MASTER_SOCKADDR': (gethostbyname(config.get(section, 'MASTER_IP')), config.getint(section, 'MASTER_PORT')),
|
||||||
|
'MASTER_IP': gethostbyname(config.get(section, 'MASTER_IP')),
|
||||||
|
'_MASTER_IP': config.get(section, 'MASTER_IP'),
|
||||||
|
'MASTER_PORT': config.getint(section, 'MASTER_PORT'),
|
||||||
|
'PASSPHRASE': bytes(config.get(section, 'PASSPHRASE'), 'utf-8'),
|
||||||
|
'CALLSIGN': bytes(config.get(section, 'CALLSIGN').ljust(8)[:8], 'utf-8'),
|
||||||
|
'RADIO_ID': config.getint(section, 'RADIO_ID').to_bytes(4, 'big'),
|
||||||
|
'RX_FREQ': bytes(config.get(section, 'RX_FREQ').ljust(9)[:9], 'utf-8'),
|
||||||
|
'TX_FREQ': bytes(config.get(section, 'TX_FREQ').ljust(9)[:9], 'utf-8'),
|
||||||
|
'TX_POWER': bytes(config.get(section, 'TX_POWER').rjust(2,'0'), 'utf-8'),
|
||||||
|
'COLORCODE': bytes(config.get(section, 'COLORCODE').rjust(2,'0'), 'utf-8'),
|
||||||
|
'LATITUDE': bytes(config.get(section, 'LATITUDE').ljust(8)[:8], 'utf-8'),
|
||||||
|
'LONGITUDE': bytes(config.get(section, 'LONGITUDE').ljust(9)[:9], 'utf-8'),
|
||||||
|
'HEIGHT': bytes(config.get(section, 'HEIGHT').rjust(3,'0'), 'utf-8'),
|
||||||
|
'LOCATION': bytes(config.get(section, 'LOCATION').ljust(20)[:20], 'utf-8'),
|
||||||
|
'DESCRIPTION': bytes(config.get(section, 'DESCRIPTION').ljust(19)[:19], 'utf-8'),
|
||||||
|
'SLOTS': bytes(config.get(section, 'SLOTS'), 'utf-8'),
|
||||||
|
'URL': bytes(config.get(section, 'URL').ljust(124)[:124], 'utf-8'),
|
||||||
|
'SOFTWARE_ID': bytes(config.get(section, 'SOFTWARE_ID').ljust(40)[:40], 'utf-8'),
|
||||||
|
'PACKAGE_ID': bytes(config.get(section, 'PACKAGE_ID').ljust(40)[:40], 'utf-8'),
|
||||||
|
'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME'),
|
||||||
|
'XLXMODULE': config.getint(section, 'XLXMODULE'),
|
||||||
|
'OPTIONS': '',
|
||||||
|
'USE_ACL': config.getboolean(section, 'USE_ACL'),
|
||||||
|
'SUB_ACL': config.get(section, 'SUB_ACL'),
|
||||||
|
'TG1_ACL': config.get(section, 'TGID_TS1_ACL'),
|
||||||
|
'TG2_ACL': config.get(section, 'TGID_TS2_ACL'),
|
||||||
|
'ANNOUNCEMENT_LANGUAGE': config.get(section, 'ANNOUNCEMENT_LANGUAGE')
|
||||||
|
}})
|
||||||
|
CONFIG['SYSTEMS'][section].update({'XLXSTATS': {
|
||||||
|
'CONNECTION': 'NO', # NO, RTPL_SENT, AUTHENTICATED, CONFIG-SENT, YES
|
||||||
|
'CONNECTED': None,
|
||||||
|
'PINGS_SENT': 0,
|
||||||
|
'PINGS_ACKD': 0,
|
||||||
|
'NUM_OUTSTANDING': 0,
|
||||||
|
'PING_OUTSTANDING': False,
|
||||||
|
'LAST_PING_TX_TIME': 0,
|
||||||
|
'LAST_PING_ACK_TIME': 0,
|
||||||
|
}})
|
||||||
|
|
||||||
|
elif config.get(section, 'MODE') == 'MASTER':
|
||||||
|
CONFIG['SYSTEMS'].update({section: {
|
||||||
|
'MODE': config.get(section, 'MODE'),
|
||||||
|
'ENABLED': config.getboolean(section, 'ENABLED'),
|
||||||
|
'REPEAT': config.getboolean(section, 'REPEAT'),
|
||||||
|
'MAX_PEERS': config.getint(section, 'MAX_PEERS'),
|
||||||
|
'IP': config.get(section, 'IP'),
|
||||||
|
'PORT': config.getint(section, 'PORT'),
|
||||||
|
'PASSPHRASE': bytes(config.get(section, 'PASSPHRASE'), 'utf-8'),
|
||||||
|
'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME'),
|
||||||
|
'USE_ACL': config.getboolean(section, 'USE_ACL'),
|
||||||
|
'REG_ACL': config.get(section, 'REG_ACL'),
|
||||||
|
'SUB_ACL': config.get(section, 'SUB_ACL'),
|
||||||
|
'TG1_ACL': config.get(section, 'TGID_TS1_ACL'),
|
||||||
|
'TG2_ACL': config.get(section, 'TGID_TS2_ACL'),
|
||||||
|
'DEFAULT_UA_TIMER': config.getint(section, 'DEFAULT_UA_TIMER'),
|
||||||
|
'SINGLE_MODE': config.getboolean(section, 'SINGLE_MODE'),
|
||||||
|
'VOICE_IDENT': config.getboolean(section, 'VOICE_IDENT'),
|
||||||
|
'TS1_STATIC': config.get(section,'TS1_STATIC'),
|
||||||
|
'TS2_STATIC': config.get(section,'TS2_STATIC'),
|
||||||
|
'DEFAULT_REFLECTOR': config.getint(section, 'DEFAULT_REFLECTOR'),
|
||||||
|
'GENERATOR': config.getint(section, 'GENERATOR'),
|
||||||
|
'ANNOUNCEMENT_LANGUAGE': config.get(section, 'ANNOUNCEMENT_LANGUAGE')
|
||||||
|
}})
|
||||||
|
CONFIG['SYSTEMS'][section].update({'PEERS': {}})
|
||||||
|
|
||||||
|
elif config.get(section, 'MODE') == 'OPENBRIDGE':
|
||||||
|
CONFIG['SYSTEMS'].update({section: {
|
||||||
|
'MODE': config.get(section, 'MODE'),
|
||||||
|
'ENABLED': config.getboolean(section, 'ENABLED'),
|
||||||
|
'NETWORK_ID': config.getint(section, 'NETWORK_ID').to_bytes(4, 'big'),
|
||||||
|
#'OVERRIDE_SERVER_ID': config.getint(section, 'OVERRIDE_SERVER_ID').to_bytes(4, 'big'),
|
||||||
|
'IP': config.get(section, 'IP'),
|
||||||
|
'PORT': config.getint(section, 'PORT'),
|
||||||
|
'PASSPHRASE': bytes(config.get(section, 'PASSPHRASE').ljust(20,'\x00')[:20], 'utf-8'),
|
||||||
|
#'TARGET_SOCK': (gethostbyname(config.get(section, 'TARGET_IP')), config.getint(section, 'TARGET_PORT')),
|
||||||
|
'TARGET_IP': config.get(section, 'TARGET_IP'),
|
||||||
|
'TARGET_PORT': config.getint(section, 'TARGET_PORT'),
|
||||||
|
'USE_ACL': config.getboolean(section, 'USE_ACL'),
|
||||||
|
'SUB_ACL': config.get(section, 'SUB_ACL'),
|
||||||
|
'TG1_ACL': config.get(section, 'TGID_ACL'),
|
||||||
|
'TG2_ACL': 'PERMIT:ALL',
|
||||||
|
'RELAX_CHECKS': config.getboolean(section, 'RELAX_CHECKS'),
|
||||||
|
'ENHANCED_OBP': config.getboolean(section, 'ENHANCED_OBP')
|
||||||
|
}})
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
if CONFIG['SYSTEMS'][section]['IP'] == '::':
|
||||||
|
try:
|
||||||
|
addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET6, socket.IPPROTO_IP)
|
||||||
|
except gaierror:
|
||||||
|
addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET, socket.IPPROTO_IP)
|
||||||
|
|
||||||
|
elif CONFIG['SYSTEMS'][section]['IP'] and IsIPv6Address(CONFIG['SYSTEMS'][section]['IP']):
|
||||||
|
addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET6, socket.IPPROTO_IP)
|
||||||
|
|
||||||
|
elif not CONFIG['SYSTEMS'][section]['IP'] or IsIPv4Address(CONFIG['SYSTEMS'][section]['IP']):
|
||||||
|
addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET, socket.IPPROTO_IP)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
family, socktype, proto, canonname, sockaddr = addr_info[0]
|
||||||
|
CONFIG['SYSTEMS'][section]['TARGET_IP'] = sockaddr[0]
|
||||||
|
|
||||||
|
if CONFIG['SYSTEMS'][section]['IP'] == '::' and IsIPv4Address(CONFIG['SYSTEMS'][section]['TARGET_IP']):
|
||||||
|
CONFIG['SYSTEMS'][section]['TARGET_IP'] = '::ffff:' + CONFIG['SYSTEMS'][section]['TARGET_IP']
|
||||||
|
|
||||||
|
CONFIG['SYSTEMS'][section]['TARGET_SOCK'] = (CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'])
|
||||||
|
|
||||||
|
except:
|
||||||
|
CONFIG['SYSTEMS'][section]['TARGET_IP'] = False
|
||||||
|
CONFIG['SYSTEMS'][section]['TARGET_SOCK'] = (CONFIG['SYSTEMS'][section]['TARGET_IP'], CONFIG['SYSTEMS'][section]['TARGET_PORT'])
|
||||||
|
|
||||||
|
|
||||||
|
except configparser.Error as err:
|
||||||
|
sys.exit('Error processing configuration file -- {}'.format(err))
|
||||||
|
|
||||||
|
process_acls(CONFIG)
|
||||||
|
|
||||||
|
return CONFIG
|
||||||
|
|
||||||
|
# Used to run this file direclty and print the config,
|
||||||
|
# which might be useful for debugging
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from pprint import pprint
|
||||||
|
from dmr_utils3.utils import int_id
|
||||||
|
|
||||||
|
# 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='CONFIG_FILE', help='/full/path/to/config.file (usually hblink.cfg)')
|
||||||
|
cli_args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
# 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__))+'/hblink.cfg'
|
||||||
|
|
||||||
|
CONFIG = build_config(cli_args.CONFIG_FILE)
|
||||||
|
pprint(CONFIG)
|
||||||
|
|
||||||
|
def acl_check(_id, _acl):
|
||||||
|
id = int_id(_id)
|
||||||
|
for entry in _acl[1]:
|
||||||
|
if entry[0] <= id <= entry[1]:
|
||||||
|
return _acl[0]
|
||||||
|
return not _acl[0]
|
||||||
|
|
||||||
|
print(acl_check(b'\x00\x01\x37', CONFIG['GLOBAL']['TG1_ACL']))
|
||||||
@ -0,0 +1 @@
|
|||||||
|
languages = 'en_GB,en_US,es_ES,fr_FR,de_DE,dk_DK,it_IT,no_NO,pl_PL,se_SE,pt_PT,cy_GB,el_GR,th_TH,CW'
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
# 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
|
||||||
|
# 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 the logging configuration for hblink.py. It changes very infrequently,
|
||||||
|
so keeping in a separate module keeps hblink.py more concise. this file is
|
||||||
|
likely to never change.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from logging.config import dictConfig
|
||||||
|
|
||||||
|
# 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-2018 Cortney T. Buffington, N0MJS and the K0USY Group'
|
||||||
|
__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT'
|
||||||
|
__license__ = 'GNU GPLv3'
|
||||||
|
__maintainer__ = 'Cort Buffington, N0MJS'
|
||||||
|
__email__ = 'n0mjs@me.com'
|
||||||
|
|
||||||
|
|
||||||
|
def config_logging(_logger):
|
||||||
|
dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'filters': {
|
||||||
|
},
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
|
||||||
|
},
|
||||||
|
'timed': {
|
||||||
|
'format': '%(levelname)s %(asctime)s %(message)s'
|
||||||
|
},
|
||||||
|
'simple': {
|
||||||
|
'format': '%(levelname)s %(message)s'
|
||||||
|
},
|
||||||
|
'syslog': {
|
||||||
|
'format': '%(name)s (%(process)d): %(levelname)s %(message)s'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'null': {
|
||||||
|
'class': 'logging.NullHandler'
|
||||||
|
},
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'simple'
|
||||||
|
},
|
||||||
|
'console-timed': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'timed'
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'class': 'logging.FileHandler',
|
||||||
|
'formatter': 'simple',
|
||||||
|
'filename': _logger['LOG_FILE'],
|
||||||
|
},
|
||||||
|
'file-timed': {
|
||||||
|
'class': 'logging.FileHandler',
|
||||||
|
'formatter': 'timed',
|
||||||
|
'filename': _logger['LOG_FILE'],
|
||||||
|
},
|
||||||
|
'syslog': {
|
||||||
|
'class': 'logging.handlers.SysLogHandler',
|
||||||
|
'formatter': 'syslog',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'handlers': _logger['LOG_HANDLERS'].split(','),
|
||||||
|
'level': _logger['LOG_LEVEL'],
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return logging.getLogger(_logger['LOG_NAME'])
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
# Copyright (C) 2016-2019 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
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
from bitarray import bitarray
|
||||||
|
from dmr_utils3 import bptc, golay, qr
|
||||||
|
from dmr_utils3.utils import bytes_3, bytes_4
|
||||||
|
from dmr_utils3.const import EMB, SLOT_TYPE, BS_VOICE_SYNC, BS_DATA_SYNC, LC_OPT
|
||||||
|
from random import randint
|
||||||
|
from voice_lib import words
|
||||||
|
|
||||||
|
# Precalculated "dmrbits" (DMRD packet byte 15) -- just (slot << 7 | this value) and you're good to go!
|
||||||
|
HEADBITS = 0b00100001
|
||||||
|
BURSTBITS = [0b00010000,0b00000001,0b00000010,0b00000011,0b00000100,0b00000101]
|
||||||
|
TERMBITS = 0b00100010
|
||||||
|
|
||||||
|
# Need a bitstring of 4-bytes of zero for burst F
|
||||||
|
NULL_EMB_LC = bitarray(endian='big')
|
||||||
|
NULL_EMB_LC.frombytes(b'\x00\x00\x00\x00')
|
||||||
|
|
||||||
|
# This is where HBP encodes RSSI, it will need to be null
|
||||||
|
TAIL = b'\x00\x00'
|
||||||
|
|
||||||
|
# WARNING this funciton uses yeild to return a generator that will pass the next HBP packet for a phrase
|
||||||
|
# each time that it is called. Do NOT try to use it like a normal function.
|
||||||
|
def pkt_gen(_rf_src, _dst_id, _peer, _slot, _phrase):
|
||||||
|
|
||||||
|
# Calculate all of the static components up-front
|
||||||
|
STREAM_ID = bytes_4(randint(0x00, 0xFFFFFFFF))
|
||||||
|
SDP = _rf_src + _dst_id + _peer
|
||||||
|
LC = LC_OPT + _dst_id + _rf_src
|
||||||
|
|
||||||
|
HEAD_LC = bptc.encode_header_lc(LC)
|
||||||
|
HEAD_LC = [HEAD_LC[:98], HEAD_LC[-98:]]
|
||||||
|
|
||||||
|
TERM_LC = bptc.encode_terminator_lc(LC)
|
||||||
|
TERM_LC = [TERM_LC[:98], TERM_LC[-98:]]
|
||||||
|
|
||||||
|
EMB_LC = bptc.encode_emblc(LC)
|
||||||
|
|
||||||
|
EMBED = []
|
||||||
|
EMBED.append( BS_VOICE_SYNC )
|
||||||
|
EMBED.append(EMB['BURST_B'][:8] + EMB_LC[1] + EMB['BURST_B'][-8:])
|
||||||
|
EMBED.append(EMB['BURST_C'][:8] + EMB_LC[2] + EMB['BURST_C'][-8:])
|
||||||
|
EMBED.append(EMB['BURST_D'][:8] + EMB_LC[3] + EMB['BURST_D'][-8:])
|
||||||
|
EMBED.append(EMB['BURST_E'][:8] + EMB_LC[4] + EMB['BURST_E'][-8:])
|
||||||
|
EMBED.append(EMB['BURST_F'][:8] + NULL_EMB_LC + EMB['BURST_F'][-8:])
|
||||||
|
|
||||||
|
|
||||||
|
#initialize the HBP calls stream sequence to 0
|
||||||
|
SEQ = 0
|
||||||
|
|
||||||
|
# Send the Call Stream
|
||||||
|
|
||||||
|
# Send 3 Voice Header Frames
|
||||||
|
for i in range(3):
|
||||||
|
pkt = b'DMRD' + bytes([SEQ]) + SDP + bytes([_slot << 7 | HEADBITS]) + STREAM_ID + (HEAD_LC[0] + SLOT_TYPE['VOICE_LC_HEAD'][:10] + BS_DATA_SYNC + SLOT_TYPE['VOICE_LC_HEAD'][-10:] + HEAD_LC[1]).tobytes() + TAIL
|
||||||
|
SEQ = (SEQ + 1) % 0x100
|
||||||
|
yield pkt
|
||||||
|
|
||||||
|
# Send each burst, six bursts per Superframe rotating through with the proper EMBED value per burst A-F
|
||||||
|
for word in _phrase:
|
||||||
|
for burst in range(0, len(word)):
|
||||||
|
pkt = b'DMRD' + bytes([SEQ]) + SDP + bytes([_slot << 7 | BURSTBITS[burst % 6]]) + STREAM_ID + (word[burst + 0][0] + EMBED[burst % 6] + word[burst + 0][1]).tobytes() + TAIL
|
||||||
|
SEQ = (SEQ + 1) % 0x100
|
||||||
|
yield pkt
|
||||||
|
|
||||||
|
# Send a single Voice Terminator Frame
|
||||||
|
pkt = b'DMRD' + bytes([SEQ]) + SDP + bytes([_slot << 7 | TERMBITS]) + STREAM_ID + (TERM_LC[0] + SLOT_TYPE['VOICE_LC_TERM'][:10] + BS_DATA_SYNC + SLOT_TYPE['VOICE_LC_TERM'][-10:] + TERM_LC[1]).tobytes() + TAIL
|
||||||
|
SEQ = (SEQ + 1) % 0x100
|
||||||
|
yield pkt
|
||||||
|
|
||||||
|
if SEQ == 255:
|
||||||
|
SEQ = 0
|
||||||
|
|
||||||
|
# Return False to indicate we're done.
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
speech = pkt_gen(bytes_3(3120101), bytes_3(3120), bytes_4(312000), 0, [words['all_circuits'], words['all_circuits']])
|
||||||
|
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
pkt = next(speech)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
print(len(pkt), pkt[4], pkt)
|
||||||
Loading…
Reference in new issue