From bbd2e4eea80353a475365ea94cce1ddd05ee376b Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 26 Apr 2022 11:32:11 +0100 Subject: [PATCH] Add playback_file.py - play AMBE file over network. use playback_file.cfg --- playback_file.cfg | 230 ++++++++++++++++++++++++++++++++++++ playback_file.py | 289 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 519 insertions(+) create mode 100644 playback_file.cfg create mode 100644 playback_file.py diff --git a/playback_file.cfg b/playback_file.cfg new file mode 100644 index 0000000..9392656 --- /dev/null +++ b/playback_file.cfg @@ -0,0 +1,230 @@ +# PROGRAM-WIDE PARAMETERS GO HERE +# PATH - working path for files, leave it alone unless you NEED to change it +# PING_TIME - the interval that peers will ping the master, and re-try registraion +# - how often the Master maintenance loop runs +# MAX_MISSED - how many pings are missed before we give up and re-register +# - number of times the master maintenance loop runs before de-registering a peer +# +# ACLs: +# +# Access Control Lists are a very powerful tool for administering your system. +# But they consume packet processing time. Disable them if you are not using them. +# But be aware that, as of now, the configuration stanzas still need the ACL +# sections configured even if you're not using them. +# +# REGISTRATION ACLS ARE ALWAYS USED, ONLY SUBSCRIBER AND TGID MAY BE DISABLED!!! +# +# 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,....' +# --for example-- +# SUB_ACL: DENY:1,1000-2000,4500-60000,17 +# +# ACL Types: +# REG_ACL: peer radio IDs for registration (only used on HBP master systems) +# SUB_ACL: subscriber IDs for end-users +# TGID_TS1_ACL: destination talkgroup IDs on Timeslot 1 +# TGID_TS2_ACL: destination talkgroup IDs on Timeslot 2 +# +# ACLs may be repeated for individual systems if needed for granularity +# Global ACLs will be processed BEFORE the system level ACLs +# Packets will be matched against all ACLs, GLOBAL first. If a packet 'passes' +# All elements, processing continues. Packets are discarded at the first +# negative match, or 'reject' from an ACL element. +# +# If you do not wish to use ACLs, set them to 'PERMIT:ALL' +# TGID_TS1_ACL in the global stanza is used for OPENBRIDGE systems, since all +# traffic is passed as TS 1 between OpenBridges +[GLOBAL] +PATH: ./ +PING_TIME: 10 +MAX_MISSED: 3 +USE_ACL: True +REG_ACL: PERMIT:ALL +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +GEN_STAT_BRIDGES: False +ALLOW_NULL_PASSPHRASE: False +ANNOUNCEMENT_LANGUAGES: es_ES +SERVER_ID: 9990 +DATA_GATEWAY: False + + + +# NOT YET WORKING: NETWORK REPORTING CONFIGURATION +# Enabling "REPORT" will configure a socket-based reporting +# system that will send the configuration and other items +# to a another process (local or remote) that may process +# the information for some useful purpose, like a web dashboard. +# +# REPORT - True to enable, False to disable +# REPORT_INTERVAL - Seconds between reports +# REPORT_PORT - TCP port to listen on if "REPORT_NETWORKS" = NETWORK +# REPORT_CLIENTS - comma separated list of IPs you will allow clients +# to connect on. Entering a * will allow all. +# +# ****FOR NOW MUST BE TRUE - USE THE LOOPBACK IF YOU DON'T USE THIS!!!**** +[REPORTS] +REPORT: False +REPORT_INTERVAL: 60 +REPORT_PORT: 4821 +REPORT_CLIENTS: 127.0.0.1 + + +# SYSTEM LOGGER CONFIGURAITON +# This allows the logger to be configured without chaning the individual +# python logger stuff. LOG_FILE should be a complete path/filename for *your* +# system -- use /dev/null for non-file handlers. +# LOG_HANDLERS may be any of the following, please, no spaces in the +# list if you use several: +# null +# console +# console-timed +# file +# file-timed +# syslog +# LOG_LEVEL may be any of the standard syslog logging levels, though +# as of now, DEBUG, INFO, WARNING and CRITICAL are the only ones +# used. +# +[LOGGER] +LOG_FILE: /dev/null +LOG_HANDLERS: console-timed +LOG_LEVEL: DEBUG +LOG_NAME: HBlink + +# DOWNLOAD AND IMPORT SUBSCRIBER, PEER and TGID ALIASES +# Ok, not the TGID, there's no master list I know of to download +# This is intended as a facility for other applcations built on top of +# HBlink to use, and will NOT be used in HBlink directly. +# STALE_DAYS is the number of days since the last download before we +# download again. Don't be an ass and change this to less than a few days. +[ALIASES] +TRY_DOWNLOAD: False +PATH: ./ +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 +TGID_URL: http://downloads.freedmr.uk/downloads/talkgroup_ids.json +LOCAL_SUBSCRIBER_FILE: local_subscriber_ids.json +STALE_DAYS: 7 +SUB_MAP_FILE: + +#Read further repeater configs from MySQL +[MYSQL] +USE_MYSQL: False +USER: hblink +PASS: mypassword +DB: hblink +SERVER: 127.0.0.1 +PORT: 3306 +TABLE: repeaters + +# OPENBRIDGE INSTANCES - DUPLICATE SECTION FOR MULTIPLE CONNECTIONS +# OpenBridge is a protocol originall created by DMR+ for connection between an +# IPSC2 server and Brandmeister. It has been implemented here at the suggestion +# of the Brandmeister team as a way to legitimately connect HBlink to the +# Brandemiester network. +# It is recommended to name the system the ID of the Brandmeister server that +# it connects to, but is not necessary. TARGET_IP and TARGET_PORT are of the +# Brandmeister or IPSC2 server you are connecting to. PASSPHRASE is the password +# that must be agreed upon between you and the operator of the server you are +# connecting to. NETWORK_ID is a number in the format of a DMR Radio ID that +# will be sent to the other server to identify this connection. +# other parameters follow the other system types. +# +# ACLs: +# OpenBridge does not 'register', so registration ACL is meaningless. +# OpenBridge passes all traffic on TS1, so there is only 1 TGID ACL. +# Otherwise ACLs work as described in the global stanza +[OBP-TEST] +MODE: OPENBRIDGE +ENABLED: False +IP: +PORT: 62044 +NETWORK_ID: 1 +PASSPHRASE: mypass +TARGET_IP: +TARGET_PORT: 62044 +USE_ACL: True +SUB_ACL: DENY:1 +TGID_ACL: PERMIT:ALL +RELAX_CHECKS: False + +# MASTER INSTANCES - DUPLICATE SECTION FOR MULTIPLE MASTERS +# HomeBrew Protocol Master instances go here. +# IP may be left blank if there's one interface on your system. +# Port should be the port you want this master to listen on. It must be unique +# and unused by anything else. +# Repeat - if True, the master repeats traffic to peers, False, it does nothing. +# +# MAX_PEERS -- maximun number of peers that may be connect to this master +# at any given time. This is very handy if you're allowing hotspots to +# connect, or using a limited computer like a Raspberry Pi. +# +# ACLs: +# See comments in the GLOBAL stanza +[PARROT] +MODE: MASTER +ENABLED: False +REPEAT: True +MAX_PEERS: 1 +EXPORT_AMBE: False +IP: bmproject.freedmr.uk +PORT: 54915 +PASSPHRASE: passw0rd +GROUP_HANGTIME: 5 +USE_ACL: True +REG_ACL: DENY:1 +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +DEFAULT_UA_TIMER: 10 +SINGLE_MODE: True +VOICE_IDENT: False +TS1_STATIC: +TS2_STATIC: +DEFAULT_REFLECTOR: 0 +GENERATOR: 1 +ANNOUNCEMENT_LANGUAGE:es_ES +ALLOW_UNREG_ID: True +PROXY_CONTROL: False + + +[PEER-1] +MODE: PEER +ENABLED: True +LOOSE: False +EXPORT_AMBE: False +IP: +PORT: 54002 +MASTER_IP: localhost +MASTER_PORT: 54001 +PASSPHRASE: homebrew +CALLSIGN: TEST +RADIO_ID: 312000 +RX_FREQ: 449000000 +TX_FREQ: 444000000 +TX_POWER: 25 +COLORCODE: 1 +SLOTS: 1 +LATITUDE: 38.0000 +LONGITUDE: -095.0000 +HEIGHT: 75 +LOCATION: Anywhere, USA +DESCRIPTION: This is a cool repeater +URL: www.w1abc.org +SOFTWARE_ID: 20170620 +PACKAGE_ID: MMDVM_FreeDMR +GROUP_HANGTIME: 5 +OPTIONS: +USE_ACL: True +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +ANNOUNCEMENT_LANGUAGE: en_GB + diff --git a/playback_file.py b/playback_file.py new file mode 100644 index 0000000..ee5e0ad --- /dev/null +++ b/playback_file.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +# +############################################################################### +# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS (and 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 +############################################################################### + + +# Python modules we need +import sys +from bitarray import bitarray +from time import time, sleep +from importlib import import_module +from random import randint +from setproctitle import setproctitle + +# Twisted is pretty important, so I keep it separate +from twisted.internet.protocol import Factory, Protocol +from twisted.protocols.basic import NetstringReceiver +from twisted.internet import reactor, task + +# Things we import from the main hblink module +from hblink import HBSYSTEM, systems, hblink_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 config +import log +import const + +from mk_voice import pkt_gen +from read_ambe import readAMBE + +# The module needs logging logging, but handlers, etc. are controlled by the parent +import logging +logger = logging.getLogger(__name__) + + +# Does anybody read this stuff? There's a PEP somewhere that says I should do this. +__author__ = 'Cortney T. Buffington, N0MJS and Mike Zingman, N4IRR' +__copyright__ = 'Copyright (c) 2016-2019 Cortney T. Buffington, N0MJS and the K0USY Group' +__license__ = 'GNU GPLv3' +__maintainer__ = 'Cort Buffington, N0MJS' +__email__ = 'n0mjs@me.com' +__status__ = 'pre-alpha' + +# Module gobal varaibles + +def playFile(fileName,dstTG,subid): + pkt_time = time() + for system in systems: + reactor.callInThread(playFileOnRequest,system,fileName,dstTG,subid) + +def playFileOnRequest(system,fileName,dstTG,subid): + _dst_id = bytes_3(dstTG) + _source_id = bytes_3(subid) + logger.debug('(%s) Sending contents of AMBE file: %s',system,fileName) + sleep(1) + _say = [] + try: + _say.append(AMBEobj.readSingleFile(fileName)) + except IOError: + logger.warning('(%s) cannot read file %s',system,fileName) + return + speech = pkt_gen(_source_id, _dst_id, bytes_4(5000), 0, _say) + sleep(1) + _slot = systems[system].STATUS[1] + while True: + try: + pkt = next(speech) + except StopIteration: + break + #Packet every 60ms + sleep(0.058) + _stream_id = pkt[16:20] + _pkt_time = time() + reactor.callFromThread(sendVoicePacket,systems[system],pkt,_source_id,_dst_id,_slot) + logger.debug('(%s) Sending AMBE file %s end',system,fileName) + + if ONESHOT: + reactor.stop() + +def sendVoicePacket(self,pkt,_source_id,_dest_id,_slot): + _stream_id = pkt[16:20] + _pkt_time = time() + #if _stream_id not in systems[system].STATUS: + # systems[system].STATUS[_stream_id] = { + # 'START': _pkt_time, + # 'CONTENTION':False, + # 'RFS': _source_id, + # 'TGID': _dest_id, + # 'LAST': _pkt_time + # } + # _slot['TX_TGID'] = _dest_id + #else: + # systems[system].STATUS[_stream_id]['LAST'] = _pkt_time + # _slot['TX_TIME'] = _pkt_time + + self.send_system(pkt) + +class playback(HBSYSTEM): + def __init__(self, _name, _config, _report): + HBSYSTEM.__init__(self, _name, _config, _report) + + # Status information for the system, TS1 & TS2 + # 1 & 2 are "timeslot" + # In TX_EMB_LC, 2-5 are burst B-E + self.STATUS = { + 1: { + 'RX_START': time(), + 'RX_SEQ': '\x00', + 'RX_RFS': '\x00', + 'TX_RFS': '\x00', + 'RX_STREAM_ID': '\x00', + 'TX_STREAM_ID': '\x00', + 'RX_TGID': '\x00\x00\x00', + 'TX_TGID': '\x00\x00\x00', + 'RX_TIME': time(), + 'TX_TIME': time(), + 'RX_TYPE': const.HBPF_SLT_VTERM, + 'RX_LC': '\x00', + 'TX_H_LC': '\x00', + 'TX_T_LC': '\x00', + 'TX_EMB_LC': { + 1: '\x00', + 2: '\x00', + 3: '\x00', + 4: '\x00', + } + }, + 2: { + 'RX_START': time(), + 'RX_SEQ': '\x00', + 'RX_RFS': '\x00', + 'TX_RFS': '\x00', + 'RX_STREAM_ID': '\x00', + 'TX_STREAM_ID': '\x00', + 'RX_TGID': '\x00\x00\x00', + 'TX_TGID': '\x00\x00\x00', + 'RX_TIME': time(), + 'TX_TIME': time(), + 'RX_TYPE': const.HBPF_SLT_VTERM, + 'RX_LC': '\x00', + 'TX_H_LC': '\x00', + 'TX_T_LC': '\x00', + 'TX_EMB_LC': { + 1: '\x00', + 2: '\x00', + 3: '\x00', + 4: '\x00', + } + } + } + self.CALL_DATA = [] + + + + def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data): + pass + + + +#************************************************ +# MAIN PROGRAM LOOP STARTS HERE +#************************************************ + +if __name__ == '__main__': + + import argparse + import sys + import os + import signal + from dmr_utils3.utils import try_download, mk_id_dict + + #Set process title early + setproctitle(__file__) + + # 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)') + parser.add_argument('-l', '--logging', action='store', dest='LOG_LEVEL', help='Override config file logging level.') + parser.add_argument('-f', '--file', action='store', dest='FILE', help='Filename to play') + parser.add_argument('-o', '--oneshot', action='store', dest='FILE', help='play once then exit [0|1]') + parser.add_argument('-i', '--interval', action='store', dest='INTERVAL', help='play every N seconds') + parser.add_argument('-t', '--talkgroup', action='store', dest='TALKGROUP', help='target talkgroup') + parser.add_argument('-s', '--source', action='store', dest='SUBID', help='subscriber (source) ID') + + + 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' + + # Call the external routine to build the configuration dictionary + CONFIG = config.build_config(cli_args.CONFIG_FILE) + + FILE = cli_args.FILE + ONESHOT = False + INTERVAL = 30 + TALKGROUP = 9 + SUBID = int(cli_args.SUBID) + if 'ONESHOT' in cli_args: + ONESHOT = cli_args.ONESHOT + if 'INTERVAL' in cli_args and INTERVAL > 30: + INTERVAL = int(cli_args.INTERVAL) + if 'TALKGROUP' in cli_args: + TALKGROUP = int(cli_args.TALKGROUP) + + # Start the system logger + if cli_args.LOG_LEVEL: + CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL + logger = log.config_logging(CONFIG['LOGGER']) + logger.info('\n\nCopyright (c) 2013, 2014, 2015, 2016, 2018, 2019\n\tThe Founding Members of the K0USY Group. All rights reserved.\n') + logger.debug('Logging system started, anything from here on gets logged') + + # Set up the signal handler + def sig_handler(_signal, _frame): + logger.info('SHUTDOWN: HBROUTER IS TERMINATING WITH SIGNAL %s', str(_signal)) + hblink_handler(_signal, _frame) + logger.info('SHUTDOWN: ALL SYSTEM HANDLERS EXECUTED - STOPPING REACTOR') + reactor.stop() + + def loopingErrHandle(failure): + logger.error('(GLOBAL) STOPPING REACTOR TO AVOID MEMORY LEAK: Unhandled error in timed loop.\n %s', failure) + 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) + + # Create the name-number mapping dictionaries + #peer_ids, subscriber_ids, talkgroup_ids = mk_aliases(CONFIG) + + peer_ids = {} + subscriber_ids = {} + talkgroup_ids = {} + + # INITIALIZE THE REPORTING LOOP + report_server = config_reports(CONFIG, reportFactory) + + # HBlink instance creation + logger.info('HBlink \'playback.py\' (c) 2017-2019 Cort Buffington, N0MJS & Mike Zingman, N4IRR -- SYSTEM STARTING...') + for system in CONFIG['SYSTEMS']: + if CONFIG['SYSTEMS'][system]['ENABLED']: + if CONFIG['SYSTEMS'][system]['MODE'] == 'OPENBRIDGE': + logger.critical('%s FATAL: Instance is mode \'OPENBRIDGE\', \n\t\t...Which would be tragic for playback, since it carries multiple call\n\t\tstreams simultaneously. playback.py onlyl works with MMDVM-based systems', system) + sys.exit('playback.py cannot function with systems that are not MMDVM devices. System {} is configured as an OPENBRIDGE'.format(system)) + else: + systems[system] = playback(system, CONFIG, report_server) + 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]) + + #Read AMBE + AMBEobj = readAMBE(CONFIG['GLOBAL']['ANNOUNCEMENT_LANGUAGES'],'./Audio/') + if ONESHOT: + reactor.callLater(10,playFile,FILE,TALKGROUP) + else: + logger.info('(PLAYBACK) Setting interval to %s seconds',INTERVAL) + ambe_task = task.LoopingCall(playFile,FILE,TALKGROUP,SUBID) + ambe = ambe_task.start(INTERVAL) + ambe.addErrback(loopingErrHandle) + + reactor.run()