From 011219ad418a7f44de33ccf073b9aad871ba20a3 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 27 Dec 2021 18:11:17 +0000 Subject: [PATCH] Revert "more tidying up" This reverts commit 7280f835d7f557c1dfeca082e584d713867d7e3c. --- FreeDMR/Protocol/HomeBrewProtocol.py | 4 +- FreeDMR/Protocol/OpenBridge.py | 4 +- FreeDMR/Utilities/mysql_config.py | 107 +++++++ FreeDMR/Utilities/read_ambe.py | 175 ++++++++++++ FreeDMR/freedmr.py | 4 +- bridge.py | 4 +- bridge_master.py | 12 +- config.py | 408 +++++++++++++++++++++++++++ languages.py | 1 + log.py | 93 ++++++ mk_voice.py | 107 +++++++ playback.py | 8 +- 12 files changed, 909 insertions(+), 18 deletions(-) create mode 100644 FreeDMR/Utilities/mysql_config.py create mode 100644 FreeDMR/Utilities/read_ambe.py create mode 100755 config.py create mode 100644 languages.py create mode 100755 log.py create mode 100755 mk_voice.py diff --git a/FreeDMR/Protocol/HomeBrewProtocol.py b/FreeDMR/Protocol/HomeBrewProtocol.py index 158bb79..bef5868 100644 --- a/FreeDMR/Protocol/HomeBrewProtocol.py +++ b/FreeDMR/Protocol/HomeBrewProtocol.py @@ -42,8 +42,8 @@ from twisted.protocols.basic import NetstringReceiver from twisted.internet import reactor, task # Other files we pull from -- this is mostly for readability and segmentation -import FreeDMR.Utilities.log as log -import FreeDMR.Config.config as config +import log +import config from FreeDMR.Const.const import * from dmr_utils3.utils import int_id, bytes_4, try_download, mk_id_dict diff --git a/FreeDMR/Protocol/OpenBridge.py b/FreeDMR/Protocol/OpenBridge.py index b4b7cf4..4699db4 100644 --- a/FreeDMR/Protocol/OpenBridge.py +++ b/FreeDMR/Protocol/OpenBridge.py @@ -42,8 +42,8 @@ from twisted.protocols.basic import NetstringReceiver from twisted.internet import reactor, task # Other files we pull from -- this is mostly for readability and segmentation -import FreeDMR.Utilities.log as log -import FreeDMR.Config.config as config +import log +import config from FreeDMR.Const.const import * from dmr_utils3.utils import int_id, bytes_4, try_download, mk_id_dict diff --git a/FreeDMR/Utilities/mysql_config.py b/FreeDMR/Utilities/mysql_config.py new file mode 100644 index 0000000..fe8553b --- /dev/null +++ b/FreeDMR/Utilities/mysql_config.py @@ -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()) diff --git a/FreeDMR/Utilities/read_ambe.py b/FreeDMR/Utilities/read_ambe.py new file mode 100644 index 0000000..0442aba --- /dev/null +++ b/FreeDMR/Utilities/read_ambe.py @@ -0,0 +1,175 @@ +############################################################################### +# Copyright (C) 2020 Simon Adlem, G7RZU +# +# 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')) diff --git a/FreeDMR/freedmr.py b/FreeDMR/freedmr.py index bfab9e7..919e74a 100644 --- a/FreeDMR/freedmr.py +++ b/FreeDMR/freedmr.py @@ -42,8 +42,8 @@ from twisted.protocols.basic import NetstringReceiver from twisted.internet import reactor, task # Other files we pull from -- this is mostly for readability and segmentation -import FreeDMR.Utilities.log as log -import FreeDMR.Config.config as config +import log +import config from FreeDMR.Const.const import * from dmr_utils3.utils import int_id, bytes_4, try_download, mk_id_dict diff --git a/bridge.py b/bridge.py index 84c275b..1a66388 100755 --- a/bridge.py +++ b/bridge.py @@ -45,8 +45,8 @@ from twisted.internet import reactor, task from FreeDMR.freedmr import HBSYSTEM, OPENBRIDGE, systems, freedmr_handler, reportFactory, REPORT_OPCODES, mk_aliases from dmr_utils3.utils import bytes_3, int_id, get_alias from dmr_utils3 import decode, bptc, const -import FreeDMR.Config.config as config -import FreeDMR.Utilities.log as log +import config +import log from FreeDMR.Const.const import * # Stuff for socket reporting diff --git a/bridge_master.py b/bridge_master.py index a04f0e3..211ff68 100644 --- a/bridge_master.py +++ b/bridge_master.py @@ -49,21 +49,21 @@ from twisted.internet import reactor, task from FreeDMR.freedmr import HBSYSTEM, OPENBRIDGE, systems, freedmr_handler, reportFactory, REPORT_OPCODES, mk_aliases, acl_check from dmr_utils3.utils import bytes_3, int_id, get_alias, bytes_4 from dmr_utils3 import decode, bptc, const -import FreeDMR.Config.config as config -from FreeDMR.Config.config import acl_build -import FreeDMR.Utilities.log as log +import config +from config import acl_build +import log from FreeDMR.Const.const import * -from FreeDMR.AMBE.mk_voice import pkt_gen +from mk_voice import pkt_gen #from voice_lib import words #Read voices -from FreeDMR.AMBE.read_ambe import readAMBE +from FreeDMR.Utilities.read_ambe import readAMBE #Remap some words for certain languages from FreeDMR.i8n.i8n_voice_map import voiceMap #MySQL -from FreeDMR.Config.mysql_config import useMYSQL +from FreeDMR.Utilities.mysql_config import useMYSQL # Stuff for socket reporting import pickle diff --git a/config.py b/config.py new file mode 100755 index 0000000..81964f3 --- /dev/null +++ b/config.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python +# +############################################################################### +# Copyright (C) 2016-2018 Cortney T. Buffington, N0MJS +# +# 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'])) diff --git a/languages.py b/languages.py new file mode 100644 index 0000000..a1dbcd8 --- /dev/null +++ b/languages.py @@ -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' diff --git a/log.py b/log.py new file mode 100755 index 0000000..96d5c21 --- /dev/null +++ b/log.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +############################################################################### +# Copyright (C) 2016-2018 Cortney T. Buffington, N0MJS +# +# 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']) diff --git a/mk_voice.py b/mk_voice.py new file mode 100755 index 0000000..f4f4b27 --- /dev/null +++ b/mk_voice.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# +############################################################################### +# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS +# +# 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) diff --git a/playback.py b/playback.py index 6f206ba..cb9d1ba 100755 --- a/playback.py +++ b/playback.py @@ -33,12 +33,12 @@ from twisted.protocols.basic import NetstringReceiver from twisted.internet import reactor, task # Things we import from the main freedmr module -from FreeDMR.freedmr import HBSYSTEM, systems, freedmr_handler, reportFactory, REPORT_OPCODES, config_reports, mk_aliases +from freedmr import HBSYSTEM, systems, freedmr_handler, reportFactory, REPORT_OPCODES, config_reports, mk_aliases from dmr_utils3.utils import bytes_3, bytes_4, int_id, get_alias from dmr_utils3 import decode, bptc, const -import FreeDMR.Config.config as config -import FreeDMR.Utilities.log as log -from FreeDMR.Const.const import * +import config +import log +import const # The module needs logging logging, but handlers, etc. are controlled by the parent import logging