Use programs in ipsc directory and dmr_utils
This commit is contained in:
parent
a826489424
commit
942e26348b
|
@ -24,6 +24,8 @@
|
|||
# frames and metadata to an external program/network. It also knows how to import
|
||||
# AMBE and metadata from an external network and send the DMR frames to IPSC networks.
|
||||
|
||||
#####################################################################################################
|
||||
|
||||
from __future__ import print_function
|
||||
from twisted.internet import reactor
|
||||
from binascii import b2a_hex as h
|
||||
|
@ -32,15 +34,15 @@ from bitstring import BitArray
|
|||
import sys, socket, ConfigParser, thread, traceback
|
||||
import cPickle as pickle
|
||||
|
||||
from dmrlink import IPSC, systems
|
||||
from dmrlink import IPSC, systems, config_reports, reportFactory
|
||||
from dmr_utils.utils import int_id, hex_str_3, hex_str_4, get_alias, get_info
|
||||
|
||||
from time import time, sleep, clock, localtime, strftime
|
||||
import csv
|
||||
import struct
|
||||
from random import randint
|
||||
import ambe_utils
|
||||
from ambe_bridge import AMBE_IPSC
|
||||
from dmr_utils import ambe_utils
|
||||
from dmr_utils.ambe_bridge import AMBE_IPSC
|
||||
|
||||
__author__ = 'Cortney T. Buffington, N0MJS'
|
||||
__copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
|
||||
|
@ -71,7 +73,6 @@ class ambeIPSC(IPSC):
|
|||
_debug = False # Debug output for each VOICE frame
|
||||
_outToFile = False # Write each AMBE frame to a file called ambe.bin
|
||||
_outToUDP = True # Send each AMBE frame to the _sock object (turn on/off Analog_Bridge operation)
|
||||
#_gateway = "192.168.1.184"
|
||||
_gateway = "127.0.0.1" # IP address of app
|
||||
_gateway_port = 31000 # Port Analog_Bridge is listening on for AMBE frames to decode
|
||||
_remote_control_port = 31002 # Port that ambe_audio is listening on for remote control commands
|
||||
|
@ -100,8 +101,8 @@ class ambeIPSC(IPSC):
|
|||
#_d = None
|
||||
###### DEBUGDEBUGDEBUG
|
||||
|
||||
def __init__(self, _name, _config, _logger):
|
||||
IPSC.__init__(self, _name, _config, _logger)
|
||||
def __init__(self, _name, _config, _logger, _report):
|
||||
IPSC.__init__(self, _name, _config, _logger, _report)
|
||||
self.CALL_DATA = []
|
||||
|
||||
#
|
||||
|
@ -115,6 +116,7 @@ class ambeIPSC(IPSC):
|
|||
logger.info('DMRLink IPSC Bridge')
|
||||
if self._gateway_dmr_id == 0:
|
||||
sys.exit( "Error: gatewayDmrId must be set (greater than zero)" )
|
||||
|
||||
#
|
||||
# Open output sincs
|
||||
#
|
||||
|
@ -159,7 +161,7 @@ class ambeIPSC(IPSC):
|
|||
if sec == None:
|
||||
sec = self.defaultOption(config, 'DEFAULTS', 'section', networkName)
|
||||
if config.has_section(sec) == False:
|
||||
logger.error('Section ' + sec + ' was not found, using DEFAULTS')
|
||||
logger.info('Section ' + sec + ' was not found, using DEFAULTS')
|
||||
sec = 'DEFAULTS'
|
||||
self._debug = bool(self.defaultOption(config, sec,'debug', self._debug) == 'True')
|
||||
self._outToFile = bool(self.defaultOption(config, sec,'outToFile', self._outToFile) == 'True')
|
||||
|
@ -273,8 +275,8 @@ if __name__ == '__main__':
|
|||
import signal
|
||||
from dmr_utils.utils import try_download, mk_id_dict
|
||||
|
||||
import dmrlink_log
|
||||
import dmrlink_config
|
||||
from ipsc.dmrlink_log import config_logging
|
||||
from ipsc.dmrlink_config import build_config
|
||||
|
||||
# Change the current directory to the location of the application
|
||||
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
|
||||
|
@ -290,14 +292,14 @@ if __name__ == '__main__':
|
|||
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
|
||||
|
||||
# Call the external routine to build the configuration dictionary
|
||||
CONFIG = dmrlink_config.build_config(cli_args.CFG_FILE)
|
||||
CONFIG = build_config(cli_args.CFG_FILE)
|
||||
|
||||
# Call the external routing to start the system logger
|
||||
if cli_args.LOG_LEVEL:
|
||||
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
|
||||
if cli_args.LOG_HANDLERS:
|
||||
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
|
||||
logger = dmrlink_log.config_logging(CONFIG['LOGGER'])
|
||||
logger = config_logging(CONFIG['LOGGER'])
|
||||
|
||||
logger.info('DMRlink \'IPSC_Bridge.py\' (c) 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
|
||||
logger.info('Version %s', __version__)
|
||||
|
@ -340,11 +342,15 @@ if __name__ == '__main__':
|
|||
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
|
||||
signal.signal(sig, sig_handler)
|
||||
|
||||
# INITIALIZE THE REPORTING LOOP
|
||||
report_server = config_reports(CONFIG, logger, reportFactory)
|
||||
|
||||
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGUED IPSC
|
||||
for system in CONFIG['SYSTEMS']:
|
||||
if CONFIG['SYSTEMS'][system]['LOCAL']['ENABLED']:
|
||||
systems[system] = ambeIPSC(system, CONFIG, logger)
|
||||
systems[system] = ambeIPSC(system, CONFIG, logger, report_server)
|
||||
reactor.listenUDP(CONFIG['SYSTEMS'][system]['LOCAL']['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['LOCAL']['IP'])
|
||||
|
||||
reactor.run()
|
||||
|
||||
|
||||
|
|
701
ambe_bridge.py
701
ambe_bridge.py
|
@ -1,701 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
###############################################################################
|
||||
# Copyright (C) 2017 Mike Zingman N4IRR
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
###############################################################################
|
||||
|
||||
'''
|
||||
'''
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
# Python modules we need
|
||||
import sys
|
||||
from bitarray import bitarray
|
||||
from bitstring import BitArray
|
||||
from bitstring import BitString
|
||||
import struct
|
||||
from time import time, sleep
|
||||
from importlib import import_module
|
||||
from binascii import b2a_hex as ahex
|
||||
from random import randint
|
||||
import sys, socket, ConfigParser, thread, traceback
|
||||
from threading import Lock
|
||||
from time import time, sleep, clock, localtime, strftime
|
||||
|
||||
# Twisted is pretty important, so I keep it separate
|
||||
from twisted.internet.protocol import DatagramProtocol
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet import task
|
||||
|
||||
# Things we import from the main hblink module
|
||||
from dmr_utils.utils import hex_str_3, hex_str_4, int_id, get_alias
|
||||
from dmr_utils import decode, bptc, const, golay, qr
|
||||
import ambe_utils
|
||||
|
||||
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
|
||||
__author__ = 'Mike Zingman, N4IRR and Cortney T. Buffington, N0MJS'
|
||||
__copyright__ = 'Copyright (c) 2017 Mike Zingman N4IRR'
|
||||
__credits__ = 'Cortney T. Buffington, N0MJS; Colin Durbridge, G4EML, Steve Zingman, N4IRS; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT'
|
||||
__license__ = 'GNU GPLv3'
|
||||
__maintainer__ = 'Cort Buffington, N0MJS'
|
||||
__email__ = 'n0mjs@me.com'
|
||||
__status__ = 'pre-alpha'
|
||||
__version__ = '20170529'
|
||||
|
||||
|
||||
'''
|
||||
Take ambe from external source (ASL or IPSC) and import it into an HB network
|
||||
Take ambe from HB network and export it to a foreign network (ie IPSC or ASL)
|
||||
Need to support both slots. This means segmenting the data structures using slot based keys
|
||||
Every slot should remember its TG, slot, cc, source ID, destination ID and repeater ID
|
||||
Export should just pass through metadata unless a rule is found which could change the TG or slot being idetified.
|
||||
Import should use the current metadata (last seen) for a slot untill it sees a new set
|
||||
|
||||
The app can be configured as a master:
|
||||
This is useful when connecting a MMDVM repeater or hotspot to the network
|
||||
Configure the MMDVMHost to point to this instance
|
||||
Or a peer on an existing master
|
||||
This is useful when connecting to Brandmeister, DMRPlus or an existing HB network.
|
||||
Use this when you want to share your IPSC repeater on an HB network
|
||||
USe this when you want to use dongle mode to access Brandmeister or any HB nework
|
||||
|
||||
Import:
|
||||
Wait for metadata from external network
|
||||
Once seen, set up slot based values for source, destination and repeater IDs, color code
|
||||
For each AMBE packet from that foreign source, read the data and construct DMR and HB structures around the new metadata
|
||||
Send the HB packet to the network
|
||||
Export
|
||||
For each session, construct a metadata packet to pass to the foreign repeater with source, destination, repeater IDs, slot and CC
|
||||
Send AMBE to the foreign reprater over UDP (decorated with slot)
|
||||
At end of session signal termination to the foreign repeater
|
||||
|
||||
Translation of TG/Slot information
|
||||
Used when
|
||||
local and foreigh repeaters do not have same mapping
|
||||
Need to block export or import of a specific TG
|
||||
DMO where only one slot is supported (map import to slot 2, export to foreign specs)
|
||||
|
||||
'''
|
||||
|
||||
############################################################################################################
|
||||
# Constants
|
||||
############################################################################################################
|
||||
DMR_DATA_SYNC_MS = '\xD5\xD7\xF7\x7F\xD7\x57'
|
||||
DMR_VOICE_SYNC_MS = '0x7F7D5DD57DFD'
|
||||
|
||||
# TLV tag definitions
|
||||
TAG_BEGIN_TX = 0 # Begin transmission with optional metadata
|
||||
TAG_AMBE = 1 # AMBE frame to transmit (old tag now uses 49 or 72)
|
||||
TAG_END_TX = 2 # End transmission, close session
|
||||
TAG_TG_TUNE = 3 # Send blank start/end frames to network for a specific talk group
|
||||
TAG_PLAY_AMBE = 4 # Play an AMBE file
|
||||
TAG_REMOTE_CMD = 5 # SubCommand for remote configuration
|
||||
TAG_AMBE_49 = 6 # AMBE frame of 49 bit samples (IPSC)
|
||||
TAG_AMBE_72 = 7 # AMBE frame of 72 bit samples (HB)
|
||||
TAG_SET_INFO = 8 # Set DMR Info for slot
|
||||
|
||||
# Burst Data Types
|
||||
BURST_DATA_TYPE = {
|
||||
'VOICE_HEAD': '\x01',
|
||||
'VOICE_TERM': '\x02',
|
||||
'SLOT1_VOICE': '\x0A',
|
||||
'SLOT2_VOICE': '\x8A'
|
||||
}
|
||||
|
||||
############################################################################################################
|
||||
# Globals
|
||||
############################################################################################################
|
||||
'''
|
||||
Flag bits
|
||||
SGTT NNNN S = Slot (0 = slot 1, 1 = slot 2)
|
||||
G = Group call = 0, Private = 1
|
||||
T = Type (Voice = 00, Data Sync = 10, ,Voice Sync = 01, Unused = 11)
|
||||
NNNN = Sequence Number or data type (from slot type)
|
||||
'''
|
||||
header_flag = lambda _slot: (0xA0 if (_slot == 2) else 0x20) | ord(const.DMR_SLT_VHEAD)
|
||||
terminator_flag = lambda _slot: (0xA0 if (_slot == 2) else 0x20) | ord(const.DMR_SLT_VTERM)
|
||||
voice_flag = lambda _slot, _vf: (0x80 if (_slot == 2) else 0) | (0x10 if (_vf == 0) else 0) | _vf
|
||||
|
||||
############################################################################################################
|
||||
# Classes
|
||||
############################################################################################################
|
||||
class SLOT:
|
||||
def __init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc):
|
||||
self.rf_src = hex_str_3(_rf_src) # DMR ID of sender
|
||||
self.dst_id = hex_str_3(_dst_id) # Talk group to send to
|
||||
self.repeater_id = hex_str_4(_repeater_id) # Repeater ID
|
||||
self.slot = _slot # Slot to use
|
||||
self.cc = _cc # Color code to use
|
||||
self.type = 0 # 1=voice header, 2=voice terminator; voice, 0=burst A ... 5=burst F
|
||||
self.stream_id = hex_str_4(0) # Stream id is same across a single session
|
||||
self.frame_count = 0 # Count of frames in a session
|
||||
self.start_time = 0 # Start of session
|
||||
self.time = 0 # Current time in session. Used to calculate duration
|
||||
|
||||
class RX_SLOT(SLOT):
|
||||
def __init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc):
|
||||
SLOT.__init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc)
|
||||
self.vf = 0 # Voice Frame (A-F in DMR spec)
|
||||
self.seq = 0 # Incrementing sequence number for each DMR frame
|
||||
self.emblc = [None] * 6 # Storage for embedded LC
|
||||
|
||||
class TX_SLOT(SLOT):
|
||||
def __init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc):
|
||||
SLOT.__init__(self, _slot, _rf_src, _dst_id, _repeater_id, _cc)
|
||||
self.lastSeq = 0 # Used to look for gaps in seq numbers
|
||||
self.lostFrame = 0 # Number of lost frames in a single session
|
||||
|
||||
class AMBE_BASE:
|
||||
def __init__(self, _parent, _name, _config, _logger, _port):
|
||||
self._parent = _parent
|
||||
self._logger = _logger
|
||||
self._config = _config
|
||||
self._system = _name
|
||||
|
||||
self._gateways = [(self._parent._gateway, self._parent._gateway_port)]
|
||||
self._ambeRxPort = _port # Port to listen on for AMBE frames to transmit to all peers
|
||||
self._dmrgui = '127.0.0.1'
|
||||
|
||||
self._sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
||||
|
||||
self._slot = 2 # "current slot"
|
||||
self.rx = [0, RX_SLOT(1, 0, 0, 0, 1), RX_SLOT(2, 0, 0, 0, 1)]
|
||||
self.tx = [0, TX_SLOT(1, 0, 0, 0, 1), TX_SLOT(2, 0, 0, 0, 1)]
|
||||
|
||||
class UDP_IMPORT(DatagramProtocol):
|
||||
def __init__(self, callback_function):
|
||||
self.func = callback_function
|
||||
def datagramReceived(self, _data, (_host, _port)):
|
||||
self.func(_data, (_host, _port))
|
||||
|
||||
self.udp_port = reactor.listenUDP(self._ambeRxPort, UDP_IMPORT(self.import_datagramReceived))
|
||||
|
||||
pass
|
||||
def stop_listening(self):
|
||||
self.udp_port.stopListening()
|
||||
def send_voice_header(self, _rx_slot):
|
||||
_rx_slot.vf = 0 # voice frame (A-F)
|
||||
_rx_slot.seq = 0 # Starts at zero for each incoming transmission, wraps back to zero when 256 is reached.
|
||||
_rx_slot.frame_count = 0 # Number of voice frames in this session (will be greater than zero of header is sent)
|
||||
def send_voice72(self, _rx_slot, _ambe):
|
||||
pass
|
||||
def send_voice49(self, _rx_slot, _ambe):
|
||||
pass
|
||||
def send_voice_term(self, _rx_slot):
|
||||
pass
|
||||
# Play the contents of a AMBE file to all peers. This function is expected to be launched from a thread
|
||||
def play_ambe_file(self, _fileName, _rx_slot):
|
||||
try:
|
||||
self._logger.info('(%s) play_ambe_file: %s', self._system, _fileName)
|
||||
_file = open(_fileName, 'r')
|
||||
_strSlot = struct.pack("I",_rx_slot.slot)[0]
|
||||
metadata = ahex(_rx_slot.rf_src[0:3]) + ahex(_rx_slot.repeater_id[0:4]) + ahex(_rx_slot.dst_id[0:3]) + ('%02x' % _rx_slot.slot) + ('%02x' % _rx_slot.cc)
|
||||
|
||||
self._sock.sendto(bytearray.fromhex('000C'+metadata), ('127.0.0.1', self._ambeRxPort)) # begin transmission TLV
|
||||
_notEOF = True
|
||||
while (_notEOF):
|
||||
_data = _file.read(27)
|
||||
if (_data):
|
||||
self._sock.sendto(bytearray.fromhex('071C')+_strSlot+_data, ('127.0.0.1', self._ambeRxPort)) # send AMBE72
|
||||
sleep(0.06)
|
||||
else:
|
||||
_notEOF = False
|
||||
self._sock.sendto(bytearray.fromhex('0201')+_strSlot, ('127.0.0.1', self._ambeRxPort)) # end transmission TLV
|
||||
_file.close()
|
||||
self._logger.info('(%s) File playback done', self._system)
|
||||
except:
|
||||
self._logger.error('(%s) file %s not found', self._system, _fileName)
|
||||
traceback.print_exc()
|
||||
# TG selection, send a simple blank voice frame to network
|
||||
def sendBlankAmbe(self, _rx_slot, _stream_id):
|
||||
_rx_slot.stream_id = _stream_id
|
||||
self.send_voice_header(_rx_slot)
|
||||
silence = '\xAC\AA\x40\x20\x00\x44\x40\x80\x80'
|
||||
self.send_voice72(_rx_slot, silence+silence+silence)
|
||||
self.send_voice_term(_rx_slot)
|
||||
# Twisted callback with data from socket
|
||||
def import_datagramReceived(self, _data, (_host, _port)):
|
||||
subscriber_ids, talkgroup_ids, peer_ids = self._parent.get_globals()
|
||||
self._logger.debug('(%s) import_datagramReceived', self._system)
|
||||
_slot = self._slot
|
||||
_rx_slot = self.rx[_slot]
|
||||
|
||||
# Parse out the TLV
|
||||
t = _data[0]
|
||||
if (t):
|
||||
l = _data[1]
|
||||
if (l):
|
||||
v = _data[2:]
|
||||
if (v):
|
||||
t = ord(t)
|
||||
if (t == TAG_BEGIN_TX) or (t == TAG_SET_INFO):
|
||||
|
||||
if ord(l) > 1:
|
||||
_slot = int_id(v[10:11])
|
||||
_rx_slot = self.rx[_slot]
|
||||
_rx_slot.slot = _slot
|
||||
_rx_slot.rf_src = hex_str_3(int_id(v[0:3]))
|
||||
_rx_slot.repeater_id = self._parent.get_repeater_id( hex_str_4(int_id(v[3:7])) )
|
||||
_rx_slot.dst_id = hex_str_3(int_id(v[7:10]))
|
||||
_rx_slot.cc = int_id(v[11:12])
|
||||
|
||||
if t == TAG_BEGIN_TX:
|
||||
_rx_slot.stream_id = hex_str_4(randint(0,0xFFFFFFFF)) # Every stream has a unique ID
|
||||
self._logger.info('(%s) Begin AMBE encode STREAM ID: %s SUB: %s (%s) REPEATER: %s (%s) TGID %s (%s), TS %s', \
|
||||
self._system, int_id(_rx_slot.stream_id), get_alias(_rx_slot.rf_src, subscriber_ids), int_id(_rx_slot.rf_src), get_alias(_rx_slot.repeater_id, peer_ids), int_id(_rx_slot.repeater_id), get_alias(_rx_slot.dst_id, talkgroup_ids), int_id(_rx_slot.dst_id), _slot)
|
||||
self.send_voice_header(_rx_slot)
|
||||
else:
|
||||
self._logger.info('(%s) Set DMR Info SUB: %s (%s) REPEATER: %s (%s) TGID %s (%s), TS %s', \
|
||||
self._system, get_alias(_rx_slot.rf_src, subscriber_ids), int_id(_rx_slot.rf_src), get_alias(_rx_slot.repeater_id, peer_ids), int_id(_rx_slot.repeater_id), get_alias(_rx_slot.dst_id, talkgroup_ids), int_id(_rx_slot.dst_id), _slot)
|
||||
elif ((t == TAG_AMBE) or (t == TAG_AMBE_72)): # generic AMBE or specific AMBE72
|
||||
_slot = int_id(v[0])
|
||||
_rx_slot = self.rx[_slot]
|
||||
if _rx_slot.frame_count > 0:
|
||||
self.send_voice72(_rx_slot, v[1:])
|
||||
elif (t == TAG_AMBE_49): # AMBE49
|
||||
_slot = int_id(v[0])
|
||||
_rx_slot = self.rx[_slot]
|
||||
if _rx_slot.frame_count > 0:
|
||||
self.send_voice49(_rx_slot, v[1:])
|
||||
elif (t == TAG_END_TX):
|
||||
_slot = int_id(v[0])
|
||||
_rx_slot = self.rx[_slot]
|
||||
if _rx_slot.frame_count > 0:
|
||||
self.send_voice_term(_rx_slot)
|
||||
self._logger.debug('(%s) End AMBE encode STREAM ID: %d FRAMES: %d', self._system, int_id(_rx_slot.stream_id), _rx_slot.frame_count)
|
||||
_rx_slot.frame_count = 0 # set it back to zero so any random AMBE frames are ignored.
|
||||
elif (t == TAG_TG_TUNE):
|
||||
_rx_slot.dst_id = hex_str_3(int(v.split('=')[1]))
|
||||
self._logger.info('(%s) New txTg = %d on Slot %d', self._system, int_id(_rx_slot.dst_id), _rx_slot.slot)
|
||||
self.sendBlankAmbe(_rx_slot, hex_str_4(randint(0,0xFFFFFFFF)))
|
||||
elif (t == TAG_PLAY_AMBE):
|
||||
thread.start_new_thread( self.play_ambe_file, (v.split('=')[1], _rx_slot) )
|
||||
elif (t == TAG_REMOTE_CMD):
|
||||
_tmp = v.split(None)[0] #first get rid of whitespace
|
||||
_cmd = _tmp.split('=')[0]
|
||||
if _cmd == "foobar":
|
||||
pass
|
||||
elif _cmd == 'get_info': # get section name, repeater ID, subscriber ID, subscriber callsign
|
||||
self._sock.sendto('reply dmr_info {} {} {} {}'.format(self._system,
|
||||
int_id(_rx_slot.repeater_id),
|
||||
int_id(_rx_slot.rf_src),
|
||||
get_alias(_rx_slot.rf_src, subscriber_ids)), (self._dmrgui, 34003))
|
||||
elif _cmd == 'section': # set current section to argument passed
|
||||
pass
|
||||
elif _cmd == 'tgs': # set current rx talkgroups to argument
|
||||
pass
|
||||
elif _cmd == 'txTg': # set current transmit talkgroup to argument
|
||||
_rx_slot.dst_id = hex_str_3(int(v.split('=')[1]))
|
||||
self._logger.info('(%s) New txTg = %d on Slot %d', self._system, int_id(_rx_slot.dst_id), _rx_slot.slot)
|
||||
self.sendBlankAmbe(_rx_slot, hex_str_4(randint(0,0xFFFFFFFF)))
|
||||
elif _cmd == 'txTs': # set current slot to passed argument
|
||||
self._slot = int(v.split('=')[1])
|
||||
elif _cmd == 'gateway_dmr_id':
|
||||
id = int(v.split('=')[1])
|
||||
_rx_slot.repeater_id = hex_str_4(id)
|
||||
elif _cmd == 'gateway_peer_id':
|
||||
id = int(v.split('=')[1])
|
||||
_rx_slot.rf_src = hex_str_3(id)
|
||||
|
||||
else:
|
||||
self._logger.info('(%s) unknown remote command: %s', self._system, v)
|
||||
|
||||
else:
|
||||
self._logger.info('(%s) unknown TLV t=%d, l=%d, v=%s (%s)', self._system, t, ord(l), ahex(v), v)
|
||||
else:
|
||||
self._logger.info('(%s) EOF on UDP stream', self._system)
|
||||
|
||||
# Begin export call to partner
|
||||
def begin_call(self, _slot, _src_id, _dst_id, _repeater_id, _cc, _seq, _stream_id):
|
||||
subscriber_ids, talkgroup_ids, peer_ids = self._parent.get_globals()
|
||||
_src_alias = get_alias(_src_id, subscriber_ids)
|
||||
metadata = _src_id[0:3] + _repeater_id[0:4] + _dst_id[0:3] + struct.pack("b", _slot) + struct.pack("b", _cc)
|
||||
self.send_tlv(TAG_BEGIN_TX, metadata) # start transmission
|
||||
self._sock.sendto('reply log2 {} {}'.format(_src_alias, int_id(_dst_id)), (self._dmrgui, 34003))
|
||||
self._logger.info('Voice Transmission Start on TS {} and TG {} ({}) from {}'.format(_slot, get_alias(_dst_id, talkgroup_ids), int_id(_dst_id), _src_alias))
|
||||
|
||||
_tx_slot = self.tx[_slot]
|
||||
_tx_slot.slot = _slot
|
||||
_tx_slot.rf_src = _src_id
|
||||
_tx_slot.repeater_id = _repeater_id
|
||||
_tx_slot.dst_id = _dst_id
|
||||
_tx_slot.cc = _cc
|
||||
_tx_slot.stream_id = _stream_id
|
||||
|
||||
_tx_slot.start_time = time()
|
||||
_tx_slot.frame_count = 0
|
||||
_tx_slot.lostFrame = 0
|
||||
_tx_slot.lastSeq = _seq
|
||||
|
||||
# Export voice frame to partner (actually done in sub classes for 49 or 72 bits)
|
||||
def export_voice(self, _tx_slot, _seq, _ambe):
|
||||
if _seq != (_tx_slot.lastSeq+1):
|
||||
_tx_slot.lostFrame += 1
|
||||
_tx_slot.lastSeq = _seq
|
||||
|
||||
# End export call to partner
|
||||
def end_call(self, _tx_slot):
|
||||
subscriber_ids, talkgroup_ids, peer_ids = self._parent.get_globals()
|
||||
self.send_tlv(TAG_END_TX, struct.pack("b",_tx_slot.slot)) # end transmission
|
||||
call_duration = time() - _tx_slot.start_time
|
||||
_lost_percentage = ((_tx_slot.lostFrame / float(_tx_slot.frame_count)) * 100.0) if _tx_slot.frame_count > 0 else 0.0
|
||||
self._sock.sendto("reply log" +
|
||||
strftime(" %m/%d/%y %H:%M:%S", localtime(_tx_slot.start_time)) +
|
||||
' {} {} "{}"'.format(get_alias(_tx_slot.rf_src, subscriber_ids), _tx_slot.slot, get_alias(_tx_slot.dst_id, talkgroup_ids)) +
|
||||
' {:.2f}%'.format(_lost_percentage) +
|
||||
' {:.2f}s'.format(call_duration), (self._dmrgui, 34003))
|
||||
self._logger.info('Voice Transmission End {:.2f} seconds loss rate: {:.2f}% ({}/{})'.format(call_duration, _lost_percentage, _tx_slot.frame_count - _tx_slot.lostFrame, _tx_slot.frame_count))
|
||||
def send_tlv(self, _tag, _value):
|
||||
_tlv = struct.pack("bb", _tag, len(_value)) + _value
|
||||
for _gateway in self._gateways:
|
||||
self._sock.sendto(_tlv, _gateway)
|
||||
|
||||
class AMBE_HB(AMBE_BASE):
|
||||
def __init__(self, _parent, _name, _config, _logger, _port):
|
||||
AMBE_BASE.__init__(self, _parent, _name, _config, _logger, _port)
|
||||
|
||||
self.lcss = [
|
||||
0b11111111, # not used (place holder)
|
||||
0b01, # First fragment
|
||||
0b11, # Continuation fragment
|
||||
0b11, # Continuation fragment
|
||||
0b10, # Last fragment
|
||||
0b00 # Null message
|
||||
]
|
||||
self._DMOStreamID = 0
|
||||
|
||||
def send_voice_header(self, _rx_slot):
|
||||
AMBE_BASE.send_voice_header(self, _rx_slot)
|
||||
flag = header_flag(_rx_slot.slot) # DT_VOICE_LC_HEADER
|
||||
dmr = self.encode_voice_header( _rx_slot )
|
||||
for j in range(0,2):
|
||||
self.send_frameTo_system(_rx_slot, flag, dmr)
|
||||
sleep(0.06)
|
||||
def send_voice72(self, _rx_slot, _ambe):
|
||||
flag = voice_flag(_rx_slot.slot, _rx_slot.vf) # calc flag value
|
||||
_new_frame = self.encode_voice( BitArray('0x'+ahex(_ambe)), _rx_slot ) # Construct the dmr frame from AMBE(108 bits) + sync/CACH (48 bits) + AMBE(108 bits)
|
||||
self.send_frameTo_system(_rx_slot, flag, _new_frame.tobytes())
|
||||
_rx_slot.vf = (_rx_slot.vf + 1) % 6 # the voice frame counter which is always mod 6
|
||||
def send_voice49(self, _rx_slot, _ambe):
|
||||
ambe49_1 = BitArray('0x' + ahex(_ambe[0:7]))[0:49]
|
||||
ambe49_2 = BitArray('0x' + ahex(_ambe[7:14]))[0:49]
|
||||
ambe49_3 = BitArray('0x' + ahex(_ambe[14:21]))[0:49]
|
||||
|
||||
ambe72_1 = ambe_utils.convert49BitTo72BitAMBE(ambe49_1)
|
||||
ambe72_2 = ambe_utils.convert49BitTo72BitAMBE(ambe49_2)
|
||||
ambe72_3 = ambe_utils.convert49BitTo72BitAMBE(ambe49_3)
|
||||
|
||||
v = ambe72_1 + ambe72_2 + ambe72_3
|
||||
self.send_voice72(_rx_slot, v)
|
||||
def send_voice_term(self, _rx_slot):
|
||||
flag = terminator_flag(_rx_slot.slot) # DT_TERMINATOR_WITH_LC
|
||||
dmr = self.encode_voice_term( _rx_slot )
|
||||
self.send_frameTo_system(_rx_slot, flag, dmr)
|
||||
|
||||
# Construct DMR frame, HB header and send result to all peers on network
|
||||
def send_frameTo_system(self, _rx_slot, _flag, _dmr_frame):
|
||||
frame = self.make_dmrd(_rx_slot.seq, _rx_slot.rf_src, _rx_slot.dst_id, _rx_slot.repeater_id, _flag, _rx_slot.stream_id, _dmr_frame) # Make the HB frame, ready to send
|
||||
self.send_system( _rx_slot, frame ) # Send the frame to all peers or master
|
||||
_rx_slot.seq += 1 # Convienent place for this increment
|
||||
_rx_slot.frame_count += 1 # update count (used for stats and to make sure header was sent)
|
||||
|
||||
# Override the super class because (1) DMO must be placed on slot 2 and (2) repeater_id must be the ID of the client (TODO)
|
||||
def send_system(self, _rx_slot, _frame):
|
||||
if hasattr(self._parent, '_clients'):
|
||||
_orig_flag = _frame[15] # Save off the flag since _frame is a reference
|
||||
for _client in self._parent._clients:
|
||||
_clientDict = self._parent._clients[_client]
|
||||
if _clientDict['TX_FREQ'] == _clientDict['RX_FREQ']:
|
||||
|
||||
if self._DMOStreamID == 0: # are we idle?
|
||||
self._DMOStreamID = _rx_slot.stream_id
|
||||
self._logger.info('(%s) DMO Transition from idle to stream %d', self._system, int_id(_rx_slot.stream_id))
|
||||
if _rx_slot.stream_id != self._DMOStreamID: # packet is from wrong stream?
|
||||
if (_frame[15] & 0x2F) == 0x21: # Call start?
|
||||
self._logger.info('(%s) DMO Ignore traffic on stream %d', self._system, int_id(_rx_slot.stream_id))
|
||||
continue
|
||||
if (_frame[15] & 0x2F) == 0x22: # call terminator flag?
|
||||
self._DMOStreamID = 0 # we are idle again
|
||||
self._logger.info('(%s) DMO End of call, back to IDLE', self._system)
|
||||
|
||||
_frame[15] = (_frame[15] & 0x7f) | 0x80 # force to slot 2 if client in DMO mode
|
||||
else:
|
||||
_frame[15] = _orig_flag # Use the origional flag value if not DMO
|
||||
|
||||
_repeaterID = hex_str_4( int(_clientDict['RADIO_ID']) )
|
||||
for _index in range(0,4): # Force the repeater ID to be the "destination" ID of the client (hblink will not accept it otherwise)
|
||||
_frame[_index+11] = _repeaterID[_index]
|
||||
|
||||
self._parent.send_client(_client, _frame)
|
||||
else:
|
||||
self._parent.send_master(_frame)
|
||||
|
||||
# Construct a complete HB frame from passed parameters
|
||||
def make_dmrd( self, _seq, _rf_src, _dst_id, _repeater_id, _flag, _stream_id, _dmr_frame):
|
||||
frame = bytearray('DMRD') # HB header type DMRD
|
||||
frame += struct.pack("i", _seq)[0] # add sequence number
|
||||
frame += _rf_src[0:3] # add source ID
|
||||
frame += _dst_id[0:3] # add destination ID
|
||||
frame += _repeater_id[0:4] # add repeater ID (4 bytes)
|
||||
frame += struct.pack("i", _flag)[0:1] # add flag to packet
|
||||
frame += _stream_id[0:4] # add stream ID (same for all packets in a transmission)
|
||||
frame += _dmr_frame # add the dmr frame
|
||||
frame += struct.pack("i", 0)[0:2] # add in the RSSI and err count
|
||||
return frame
|
||||
|
||||
# Private function to create a voice header or terminator DMR frame
|
||||
def __encode_voice_header( self, _rx_slot, _sync, _dtype ):
|
||||
_src_id = _rx_slot.rf_src
|
||||
_dst_id = _rx_slot.dst_id
|
||||
_cc = _rx_slot.cc
|
||||
# create lc
|
||||
lc = '\x00\x00\x00' + _dst_id + _src_id # PF + Reserved + FLCO + FID + Service Options + Group Address + Source Address
|
||||
# encode lc into info
|
||||
full_lc_encode = bptc.encode_header_lc(lc)
|
||||
_rx_slot.emblc = bptc.encode_emblc(lc) # save off the emb lc for voice frames B-E
|
||||
_rx_slot.emblc[5] = bitarray(32) # NULL message (F)
|
||||
# create slot_type
|
||||
slot_type = chr((_cc << 4) | (_dtype & 0x0f)) # data type is Header or Term
|
||||
# generate FEC for slot type
|
||||
slot_with_fec = BitArray(uint=golay.encode_2087(slot_type), length=20)
|
||||
# construct final frame - info[0:98] + slot_type[0:10] + DMR_DATA_SYNC_MS + slot_type[10:20] + info[98:196]
|
||||
frame_bits = full_lc_encode[0:98] + slot_with_fec[0:10] + decode.to_bits(_sync) + slot_with_fec[10:20] + full_lc_encode[98:196]
|
||||
return decode.to_bytes(frame_bits)
|
||||
|
||||
# Create a voice header DMR frame
|
||||
def encode_voice_header( self, _rx_slot ):
|
||||
return self.__encode_voice_header( _rx_slot, DMR_DATA_SYNC_MS, 1 ) # data_type=Voice_LC_Header
|
||||
|
||||
def encode_voice( self, _ambe1, _ambe2, _ambe3, _emb ):
|
||||
pass
|
||||
|
||||
# Create a voice DMR frame A-F frame type
|
||||
def encode_voice( self, _ambe, _rx_slot ):
|
||||
_frame_type = _rx_slot.vf
|
||||
if _frame_type > 0: # if not a SYNC frame cccxss
|
||||
index = (_rx_slot.cc << 3) | self.lcss[_frame_type] # index into the encode table makes this a simple lookup
|
||||
emb = bitarray(format(qr.ENCODE_1676[ index ], '016b')) # create emb of 16 bits
|
||||
embedded = emb[8:16] + _rx_slot.emblc[_frame_type] + emb[0:8] # Take emb and a chunk of the embedded LC and combine them into 48 bits
|
||||
else:
|
||||
embedded = BitArray(DMR_VOICE_SYNC_MS) # Voice SYNC (48 bits)
|
||||
_new_frame = _ambe[0:108] + embedded + _ambe[108:216] # Construct the dmr frame from AMBE(108 bits) + sync/emb (48 bits) + AMBE(108 bits)
|
||||
return _new_frame
|
||||
|
||||
# Create a voice terminator DMR frame
|
||||
def encode_voice_term( self, _rx_slot ):
|
||||
return self.__encode_voice_header( _rx_slot, DMR_DATA_SYNC_MS, 2 ) # data_type=Voice_LC_Terminator
|
||||
def export_voice(self, _tx_slot, _seq, _ambe):
|
||||
self.send_tlv(TAG_AMBE_72, struct.pack("b",_tx_slot.slot) + _ambe) # send AMBE
|
||||
if _seq != (_tx_slot.lastSeq+1):
|
||||
_tx_slot.lostFrame += 1
|
||||
_tx_slot.lastSeq = _seq
|
||||
|
||||
class AMBE_IPSC(AMBE_BASE):
|
||||
def __init__(self, _parent, _name, _config, _logger, _port):
|
||||
AMBE_BASE.__init__(self, _parent, _name, _config, _logger, _port)
|
||||
self._tempHead = [0] * 3 # It appears that there 3 frames of HEAD (mostly the same)
|
||||
self._tempVoice = [0] * 6
|
||||
self._tempTerm = [0]
|
||||
|
||||
self._seq = 0 # RPT Transmit frame sequence number (auto-increments for each frame). 16 bit
|
||||
self.ipsc_seq = 0 # Same for all frames in a transmit session (sould use stream_id). 8 bit
|
||||
|
||||
self.load_template()
|
||||
pass
|
||||
def send_voice_header(self, _rx_slot):
|
||||
AMBE_BASE.send_voice_header(self, _rx_slot)
|
||||
self._seq = randint(0,32767) # A transmission uses a random number to begin its sequence (16 bit)
|
||||
self.ipsc_seq = (self.ipsc_seq + 1) & 0xff # this is an 8 bit value which wraps around.
|
||||
|
||||
for i in range(0, 3): # Output the 3 HEAD frames to our peers
|
||||
self.rewriteFrame(self._tempHead[i], _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id)
|
||||
sleep(0.06)
|
||||
pass
|
||||
def send_voice72(self, _rx_slot, _ambe):
|
||||
ambe72_1 = BitArray('0x' + ahex(_ambe[0:9]))[0:72]
|
||||
ambe72_2 = BitArray('0x' + ahex(_ambe[9:18]))[0:72]
|
||||
ambe72_3 = BitArray('0x' + ahex(_ambe[18:27]))[0:72]
|
||||
|
||||
ambe49_1 = ambe_utils.convert72BitTo49BitAMBE(ambe72_1)
|
||||
ambe49_2 = ambe_utils.convert72BitTo49BitAMBE(ambe72_2)
|
||||
ambe49_3 = ambe_utils.convert72BitTo49BitAMBE(ambe72_3)
|
||||
|
||||
ambe49_1.append(False)
|
||||
ambe49_2.append(False)
|
||||
ambe49_3.append(False)
|
||||
|
||||
ambe = ambe49_1 + ambe49_2 + ambe49_3
|
||||
_frame = self._tempVoice[_rx_slot.vf][:33] + ambe.tobytes() + self._tempVoice[_rx_slot.vf][52:] # Insert the 3 49 bit AMBE frames
|
||||
self.rewriteFrame(_frame, _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id)
|
||||
_rx_slot.vf = (_rx_slot.vf + 1) % 6 # the voice frame counter which is always mod 6
|
||||
pass
|
||||
def send_voice49(self, _rx_slot, _ambe):
|
||||
ambe49_1 = BitArray('0x' + ahex(_ambe[0:7]))[0:50]
|
||||
ambe49_2 = BitArray('0x' + ahex(_ambe[7:14]))[0:50]
|
||||
ambe49_3 = BitArray('0x' + ahex(_ambe[14:21]))[0:50]
|
||||
ambe = ambe49_1 + ambe49_2 + ambe49_3
|
||||
|
||||
_frame = _tempVoice[_rx_slot.vf][:33] + ambe.tobytes() + self._tempVoice[_rx_slot.vf][52:] # Insert the 3 49 bit AMBE frames
|
||||
self.rewriteFrame(_frame, _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id)
|
||||
_rx_slot.vf = (_rx_slot.vf + 1) % 6 # the voice frame counter which is always mod 6
|
||||
pass
|
||||
def send_voice_term(self, _rx_slot):
|
||||
self.rewriteFrame(self._tempTerm, _rx_slot.slot, _rx_slot.dst_id, _rx_slot.rf_src, _rx_slot.repeater_id)
|
||||
pass
|
||||
def rewriteFrame( self, _frame, _newSlot, _newGroup, _newSouceID, _newPeerID ):
|
||||
|
||||
_peerid = _frame[1:5] # int32 peer who is sending us a packet
|
||||
_src_sub = _frame[6:9] # int32 Id of source
|
||||
_burst_data_type = _frame[30]
|
||||
_group = _frame[9:12]
|
||||
|
||||
########################################################################
|
||||
# re-Write the peer radio ID to that of this program
|
||||
_frame = _frame.replace(_peerid, _newPeerID)
|
||||
# re-Write the source subscriber ID to that of this program
|
||||
_frame = _frame.replace(_src_sub, _newSouceID)
|
||||
# Re-Write the destination Group ID
|
||||
_frame = _frame.replace(_group, _newGroup)
|
||||
_frame = _frame[:5] + struct.pack("i", self.ipsc_seq)[0] + _frame[6:] # ipsc sequence number increments on each transmission (stream id)
|
||||
|
||||
# Re-Write IPSC timeslot value
|
||||
_call_info = int_id(_frame[17:18])
|
||||
if _newSlot == 1:
|
||||
_call_info &= ~(1 << 5)
|
||||
elif _newSlot == 2:
|
||||
_call_info |= 1 << 5
|
||||
_call_info = chr(_call_info)
|
||||
_frame = _frame[:17] + _call_info + _frame[18:]
|
||||
|
||||
_x = struct.pack("i", self._seq)
|
||||
_frame = _frame[:20] + _x[1] + _x[0] + _frame[22:] # rtp sequence number increments for EACH frame sent out
|
||||
self._seq = self._seq + 1
|
||||
|
||||
# Re-Write DMR timeslot value
|
||||
# Determine if the slot is present, so we can translate if need be
|
||||
if _burst_data_type == BURST_DATA_TYPE['SLOT1_VOICE'] or _burst_data_type == BURST_DATA_TYPE['SLOT2_VOICE']:
|
||||
# Re-Write timeslot if necessary...
|
||||
if _newSlot == 1:
|
||||
_burst_data_type = BURST_DATA_TYPE['SLOT1_VOICE']
|
||||
elif _newSlot == 2:
|
||||
_burst_data_type = BURST_DATA_TYPE['SLOT2_VOICE']
|
||||
_frame = _frame[:30] + _burst_data_type + _frame[31:]
|
||||
|
||||
if (time() - self._parent._busy_slots[_newSlot]) >= 0.10 : # slot is not busy so it is safe to transmit
|
||||
# Send the packet to all peers in the target IPSC
|
||||
self._parent.send_to_ipsc(_frame)
|
||||
else:
|
||||
self._logger.info('Slot {} is busy, will not transmit packet from gateway'.format(_newSlot))
|
||||
self.rx[_newSlot].frame_count += 1 # update count (used for stats and to make sure header was sent)
|
||||
|
||||
# Read a record from the captured IPSC file looking for a payload type that matches the filter
|
||||
def readRecord(self, _file, _match_type):
|
||||
_notEOF = True
|
||||
# _file.seek(0)
|
||||
while (_notEOF):
|
||||
_data = ""
|
||||
_bLen = _file.read(4)
|
||||
if _bLen:
|
||||
_len, = struct.unpack("i", _bLen)
|
||||
if _len > 0:
|
||||
_data = _file.read(_len)
|
||||
_payload_type = _data[30]
|
||||
if _payload_type == _match_type:
|
||||
return _data
|
||||
else:
|
||||
_notEOF = False
|
||||
else:
|
||||
_notEOF = False
|
||||
return _data
|
||||
def load_template(self):
|
||||
try:
|
||||
_t = open('template.bin', 'rb') # Open the template file. This was recorded OTA
|
||||
|
||||
for i in range(0, 3):
|
||||
self._tempHead[i] = self.readRecord(_t, BURST_DATA_TYPE['VOICE_HEAD'])
|
||||
|
||||
for i in range(0, 6): # Then there are 6 frames of AMBE. We will just use them in order
|
||||
self._tempVoice[i] = self.readRecord(_t, BURST_DATA_TYPE['SLOT2_VOICE'])
|
||||
|
||||
self._tempTerm = self.readRecord(_t, BURST_DATA_TYPE['VOICE_TERM'])
|
||||
_t.close()
|
||||
except IOError:
|
||||
self._logger.error('Can not open template.bin file')
|
||||
return
|
||||
self._logger.debug('IPSC templates loaded')
|
||||
def export_voice(self, _tx_slot, _seq, _ambe):
|
||||
self.send_tlv(TAG_AMBE_49, struct.pack("b",_tx_slot.slot) + _ambe) # send AMBE
|
||||
if _seq != (_tx_slot.lastSeq+1):
|
||||
_tx_slot.lostFrame += 1
|
||||
_tx_slot.lastSeq = _seq
|
||||
|
||||
|
||||
############################################################################################################
|
||||
# MAIN PROGRAM LOOP STARTS HERE
|
||||
############################################################################################################
|
||||
|
||||
class TEST_HARNESS:
|
||||
def get_globals(self):
|
||||
return (subscriber_ids, talkgroup_ids, peer_ids)
|
||||
def get_repeater_id(self, import_id):
|
||||
return import_id
|
||||
def error(self, *_str):
|
||||
print('Error', _str[0] % _str[1:])
|
||||
def info(self, *_str):
|
||||
print('Info', _str[0] % _str[1:])
|
||||
def debug(self, *_str):
|
||||
print('Debug', _str[0] % _str[1:])
|
||||
def send_system(self, _frame):
|
||||
print('send system', ahex(_frame),'\n')
|
||||
def send_to_ipsc(self, _frame):
|
||||
print('send_to_ipsc', ahex(_frame),'\n')
|
||||
def play_thread(self,obj):
|
||||
obj.play_ambe_file('ambe_capture.bin', obj.rx[1])
|
||||
obj.stop_listening()
|
||||
def runTest(self, obj):
|
||||
obj._logger.info('mike was here')
|
||||
_rx_slot = obj.rx[1]
|
||||
|
||||
_rx_slot.slot = 1
|
||||
_rx_slot.rf_src = hex_str_3(3113043)
|
||||
_rx_slot.repeater_id = hex_str_4(311317)
|
||||
_rx_slot.dst_id = hex_str_3(9)
|
||||
_rx_slot.cc = 1
|
||||
|
||||
obj.sendBlankAmbe(_rx_slot, hex_str_4(randint(0,0xFFFFFFFF)))
|
||||
thread.start_new_thread( self.play_thread, (obj,) )
|
||||
def testIPSC(self):
|
||||
self._busy_slots = [0,0,0] # Keep track of activity on each slot. Make sure app is polite
|
||||
self.runTest( AMBE_IPSC(self, 'TEST_HARNESS', '', self, 37003) )
|
||||
def testHB(self):
|
||||
self.runTest( AMBE_HB(self, 'TEST_HARNESS', '', self, 37003) )
|
||||
|
||||
if __name__ == '__main__':
|
||||
subscriber_ids = {3113043:'N4IRR'}
|
||||
peer_ids = {311317:'N4IRR'}
|
||||
talkgroup_ids = {9:'Non-Routed'}
|
||||
|
||||
harness = TEST_HARNESS()
|
||||
##harness.testHB()
|
||||
##harness.testIPSC()
|
||||
## I am too lazy to do a state machine
|
||||
task.deferLater(reactor, 1, harness.testHB)
|
||||
task.deferLater(reactor, 15, harness.testIPSC)
|
||||
task.deferLater(reactor, 30, reactor.stop)
|
||||
|
||||
reactor.run()
|
279
ambe_utils.py
279
ambe_utils.py
|
@ -1,279 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
###############################################################################
|
||||
# Copyright (C) 2017 Mike Zingman N4IRR
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
###############################################################################
|
||||
|
||||
'''
|
||||
'''
|
||||
from binascii import b2a_hex as ahex
|
||||
from bitarray import bitarray
|
||||
from bitstring import BitArray
|
||||
from bitstring import BitString
|
||||
|
||||
__author__ = 'Mike Zingman, N4IRR and Cortney T. Buffington, N0MJS'
|
||||
__copyright__ = 'Copyright (c) 2017 Mike Zingman N4IRR'
|
||||
__credits__ = 'Cortney T. Buffington, N0MJS; Colin Durbridge, G4EML, Steve Zingman, N4IRS; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT'
|
||||
__license__ = 'GNU GPLv3'
|
||||
__maintainer__ = 'Cort Buffington, N0MJS'
|
||||
__email__ = 'n0mjs@me.com'
|
||||
__status__ = 'pre-alpha'
|
||||
__version__ = '20170508'
|
||||
|
||||
##
|
||||
# DMR AMBE interleave schedule
|
||||
##
|
||||
rW = [
|
||||
0, 1, 0, 1, 0, 1,
|
||||
0, 1, 0, 1, 0, 1,
|
||||
0, 1, 0, 1, 0, 1,
|
||||
0, 1, 0, 1, 0, 2,
|
||||
0, 2, 0, 2, 0, 2,
|
||||
0, 2, 0, 2, 0, 2
|
||||
]
|
||||
|
||||
rX = [
|
||||
23, 10, 22, 9, 21, 8,
|
||||
20, 7, 19, 6, 18, 5,
|
||||
17, 4, 16, 3, 15, 2,
|
||||
14, 1, 13, 0, 12, 10,
|
||||
11, 9, 10, 8, 9, 7,
|
||||
8, 6, 7, 5, 6, 4
|
||||
]
|
||||
|
||||
rY = [
|
||||
0, 2, 0, 2, 0, 2,
|
||||
0, 2, 0, 3, 0, 3,
|
||||
1, 3, 1, 3, 1, 3,
|
||||
1, 3, 1, 3, 1, 3,
|
||||
1, 3, 1, 3, 1, 3,
|
||||
1, 3, 1, 3, 1, 3
|
||||
]
|
||||
|
||||
rZ = [
|
||||
5, 3, 4, 2, 3, 1,
|
||||
2, 0, 1, 13, 0, 12,
|
||||
22, 11, 21, 10, 20, 9,
|
||||
19, 8, 18, 7, 17, 6,
|
||||
16, 5, 15, 4, 14, 3,
|
||||
13, 2, 12, 1, 11, 0
|
||||
]
|
||||
|
||||
|
||||
# This function calculates [23,12] Golay codewords.
|
||||
# The format of the returned longint is [checkbits(11),data(12)].
|
||||
def golay2312(cw):
|
||||
POLY = 0xAE3 #/* or use the other polynomial, 0xC75 */
|
||||
cw = cw & 0xfff # Strip off check bits and only use data
|
||||
c = cw #/* save original codeword */
|
||||
for i in range(1,13): #/* examine each data bit */
|
||||
if (cw & 1): #/* test data bit */
|
||||
cw = cw ^ POLY #/* XOR polynomial */
|
||||
cw = cw >> 1 #/* shift intermediate result */
|
||||
return((cw << 12) | c) #/* assemble codeword */
|
||||
|
||||
# This function checks the overall parity of codeword cw.
|
||||
# If parity is even, 0 is returned, else 1.
|
||||
def parity(cw):
|
||||
#/* XOR the bytes of the codeword */
|
||||
p = cw & 0xff
|
||||
p = p ^ ((cw >> 8) & 0xff)
|
||||
p = p ^ ((cw >> 16) & 0xff)
|
||||
|
||||
#/* XOR the halves of the intermediate result */
|
||||
p = p ^ (p >> 4)
|
||||
p = p ^ (p >> 2)
|
||||
p = p ^ (p >> 1)
|
||||
|
||||
#/* return the parity result */
|
||||
return(p & 1)
|
||||
|
||||
# Demodulate ambe frame (C1)
|
||||
# Frame is an array [4][24]
|
||||
def demodulateAmbe3600x2450(ambe_fr):
|
||||
pr = [0] * 115
|
||||
foo = 0
|
||||
|
||||
# create pseudo-random modulator
|
||||
for i in range(23, 11, -1):
|
||||
foo = foo << 1
|
||||
foo = foo | ambe_fr[0][i]
|
||||
pr[0] = (16 * foo)
|
||||
for i in range(1, 24):
|
||||
pr[i] = (173 * pr[i - 1]) + 13849 - (65536 * (((173 * pr[i - 1]) + 13849) / 65536))
|
||||
for i in range(1, 24):
|
||||
pr[i] = pr[i] / 32768
|
||||
|
||||
# demodulate ambe_fr with pr
|
||||
k = 1
|
||||
for j in range(22, -1, -1):
|
||||
ambe_fr[1][j] = ((ambe_fr[1][j]) ^ pr[k])
|
||||
k = k + 1
|
||||
return ambe_fr # Pass it back since there is no pass by reference
|
||||
|
||||
def eccAmbe3600x2450Data(ambe_fr):
|
||||
ambe = bitarray()
|
||||
|
||||
# just copy C0
|
||||
for j in range(23, 11, -1):
|
||||
ambe.append(ambe_fr[0][j])
|
||||
|
||||
# # ecc and copy C1
|
||||
# gin = 0
|
||||
# for j in range(23):
|
||||
# gin = (gin << 1) | ambe_fr[1][j]
|
||||
#
|
||||
# gout = BitArray(hex(golay2312(gin)))
|
||||
# for j in range(22, 10, -1):
|
||||
# ambe[bitIndex] = gout[j]
|
||||
# bitIndex += 1
|
||||
for j in range(22, 10, -1):
|
||||
ambe.append(ambe_fr[1][j])
|
||||
|
||||
# just copy C2
|
||||
for j in range(10, -1, -1):
|
||||
ambe.append(ambe_fr[2][j])
|
||||
|
||||
# just copy C3
|
||||
for j in range(13, -1, -1):
|
||||
ambe.append(ambe_fr[3][j])
|
||||
|
||||
return ambe
|
||||
|
||||
# Convert a 49 bit raw AMBE frame into a deinterleaved structure (ready for decode by AMBE3000)
|
||||
def convert49BitAmbeTo72BitFrames( ambe_d ):
|
||||
index = 0
|
||||
ambe_fr = [[None for x in range(24)] for y in range(4)]
|
||||
|
||||
#Place bits into the 4x24 frames. [bit0...bit23]
|
||||
#fr0: [P e10 e9 e8 e7 e6 e5 e4 e3 e2 e1 e0 11 10 9 8 7 6 5 4 3 2 1 0]
|
||||
#fr1: [e10 e9 e8 e7 e6 e5 e4 e3 e2 e1 e0 23 22 21 20 19 18 17 16 15 14 13 12 xx]
|
||||
#fr2: [34 33 32 31 30 29 28 27 26 25 24 x x x x x x x x x x x x x]
|
||||
#fr3: [48 47 46 45 44 43 42 41 40 39 38 37 36 35 x x x x x x x x x x]
|
||||
|
||||
# ecc and copy C0: 12bits + 11ecc + 1 parity
|
||||
# First get the 12 bits that actually exist
|
||||
# Then calculate the golay codeword
|
||||
# And then add the parity bit to get the final 24 bit pattern
|
||||
|
||||
tmp = 0
|
||||
for i in range(11, -1, -1): #grab the 12 MSB
|
||||
tmp = (tmp << 1) | ambe_d[i]
|
||||
tmp = golay2312(tmp) #Generate the 23 bit result
|
||||
parityBit = parity(tmp)
|
||||
tmp = tmp | (parityBit << 23) #And create a full 24 bit value
|
||||
for i in range(23, -1, -1):
|
||||
ambe_fr[0][i] = (tmp & 1)
|
||||
tmp = tmp >> 1
|
||||
|
||||
# C1: 12 bits + 11ecc (no parity)
|
||||
tmp = 0
|
||||
for i in range(23,11, -1) : #grab the next 12 bits
|
||||
tmp = (tmp << 1) | ambe_d[i]
|
||||
tmp = golay2312(tmp) #Generate the 23 bit result
|
||||
for j in range(22, -1, -1):
|
||||
ambe_fr[1][j] = (tmp & 1)
|
||||
tmp = tmp >> 1;
|
||||
|
||||
#C2: 11 bits (no ecc)
|
||||
for j in range(10, -1, -1):
|
||||
ambe_fr[2][j] = ambe_d[34 - j]
|
||||
|
||||
#C3: 14 bits (no ecc)
|
||||
for j in range(13, -1, -1):
|
||||
ambe_fr[3][j] = ambe_d[48 - j];
|
||||
|
||||
return ambe_fr
|
||||
|
||||
def interleave(ambe_fr):
|
||||
bitIndex = 0
|
||||
w = 0
|
||||
x = 0
|
||||
y = 0
|
||||
z = 0
|
||||
data = bytearray(9)
|
||||
for i in range(36):
|
||||
bit1 = ambe_fr[rW[w]][rX[x]] # bit 1
|
||||
bit0 = ambe_fr[rY[y]][rZ[z]] # bit 0
|
||||
|
||||
|
||||
data[bitIndex / 8] = ((data[bitIndex / 8] << 1) & 0xfe) | (1 if (bit1 == 1) else 0)
|
||||
bitIndex += 1
|
||||
|
||||
data[bitIndex / 8] = ((data[bitIndex / 8] << 1) & 0xfe) | (1 if (bit0 == 1) else 0)
|
||||
bitIndex += 1
|
||||
|
||||
w += 1
|
||||
x += 1
|
||||
y += 1
|
||||
z += 1
|
||||
return data
|
||||
|
||||
def deinterleave(data):
|
||||
|
||||
ambe_fr = [[None for x in range(24)] for y in range(4)]
|
||||
|
||||
bitIndex = 0
|
||||
w = 0
|
||||
x = 0
|
||||
y = 0
|
||||
z = 0
|
||||
for i in range(36):
|
||||
bit1 = 1 if data[bitIndex] else 0
|
||||
bitIndex += 1
|
||||
|
||||
bit0 = 1 if data[bitIndex] else 0
|
||||
bitIndex += 1
|
||||
|
||||
ambe_fr[rW[w]][rX[x]] = bit1; # bit 1
|
||||
ambe_fr[rY[y]][rZ[z]] = bit0; # bit 0
|
||||
|
||||
w += 1
|
||||
x += 1
|
||||
y += 1
|
||||
z += 1
|
||||
|
||||
return ambe_fr
|
||||
|
||||
def convert72BitTo49BitAMBE( ambe72 ):
|
||||
ambe_fr = deinterleave(ambe72) # take 72 bit ambe and lay it out in C0-C3
|
||||
ambe_fr = demodulateAmbe3600x2450(ambe_fr) # demodulate C1
|
||||
ambe49 = eccAmbe3600x2450Data(ambe_fr) # pick out the 49 bits of raw ambe
|
||||
return ambe49
|
||||
|
||||
def convert49BitTo72BitAMBE( ambe49 ):
|
||||
ambe_fr = convert49BitAmbeTo72BitFrames(ambe49) # take raw ambe 49 + ecc and place it into C0-C3
|
||||
ambe_fr = demodulateAmbe3600x2450(ambe_fr) # demodulate C1
|
||||
ambe72 = interleave(ambe_fr); # Re-interleave it, returning 72 bits
|
||||
return ambe72
|
||||
|
||||
def testit():
|
||||
ambe72 = BitArray('0xACAA40200044408080') #silence frame
|
||||
print('ambe72=',ambe72)
|
||||
|
||||
ambe49 = convert72BitTo49BitAMBE(ambe72)
|
||||
print('ambe49=',ahex(ambe49))
|
||||
|
||||
ambe72 = convert49BitTo72BitAMBE(ambe49)
|
||||
print('ambe72=',ahex(ambe72))
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Used to execute the module directly to run built-in tests
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
testit()
|
202
dmrlink.py
202
dmrlink.py
|
@ -25,17 +25,11 @@
|
|||
|
||||
from __future__ import print_function
|
||||
|
||||
import ConfigParser
|
||||
import argparse
|
||||
import sys
|
||||
import csv
|
||||
import os
|
||||
# Full imports
|
||||
import logging
|
||||
import signal
|
||||
|
||||
import cPickle as pickle
|
||||
|
||||
from logging.config import dictConfig
|
||||
# Function Imports
|
||||
from hmac import new as hmac_new
|
||||
from binascii import b2a_hex as ahex
|
||||
from binascii import a2b_hex as bhex
|
||||
|
@ -44,15 +38,18 @@ from socket import inet_ntoa as IPAddr
|
|||
from socket import inet_aton as IPHexStr
|
||||
from time import time
|
||||
|
||||
# Twisted Imports
|
||||
from twisted.internet.protocol import DatagramProtocol, Factory, Protocol
|
||||
from twisted.protocols.basic import NetstringReceiver
|
||||
from twisted.internet import reactor, task
|
||||
|
||||
# Imports files in the dmrlink subdirectory (these things shouldn't change often)
|
||||
from ipsc.ipsc_const import *
|
||||
from ipsc.ipsc_mask import *
|
||||
from dmrlink_config import build_config
|
||||
from dmrlink_log import config_logging
|
||||
from dmr_utils.utils import hex_str_2, hex_str_3, hex_str_4, int_id
|
||||
from ipsc.reporting_const import *
|
||||
|
||||
# Imports from DMR Utilities package
|
||||
from dmr_utils.utils import hex_str_2, hex_str_3, hex_str_4, int_id, try_download, mk_id_dict
|
||||
|
||||
|
||||
__author__ = 'Cortney T. Buffington, N0MJS'
|
||||
|
@ -66,37 +63,83 @@ __email__ = 'n0mjs@me.com'
|
|||
# Global variables used whether we are a module or __main__
|
||||
systems = {}
|
||||
|
||||
|
||||
# Timed loop used for reporting IPSC status
|
||||
#
|
||||
# REPORT BASED ON THE TYPE SELECTED IN THE MAIN CONFIG FILE
|
||||
def config_reports(_config):
|
||||
if _config['REPORTS']['REPORT_NETWORKS'] == 'PICKLE':
|
||||
def reporting_loop(_logger):
|
||||
_logger.debug('Periodic Reporting Loop Started (PICKLE)')
|
||||
try:
|
||||
with open(_config['REPORTS']['REPORT_PATH']+'dmrlink_stats.pickle', 'wb') as file:
|
||||
pickle.dump(_config['SYSTEMS'], file, 2)
|
||||
file.close()
|
||||
except IOError as detail:
|
||||
_logger.error('I/O Error: %s', detail)
|
||||
|
||||
elif _config['REPORTS']['REPORT_NETWORKS'] == 'PRINT':
|
||||
def config_reports(_config, _logger, _factory):
|
||||
if _config['REPORTS']['REPORT_NETWORKS'] == 'PRINT':
|
||||
def reporting_loop(_logger):
|
||||
_logger.debug('Periodic Reporting Loop Started (PRINT)')
|
||||
for system in _config['SYSTEMS']:
|
||||
print_master(_config, system)
|
||||
print_peer_list(_config, system)
|
||||
|
||||
reporting = task.LoopingCall(reporting_loop, _logger)
|
||||
reporting.start(_config['REPORTS']['REPORT_INTERVAL'])
|
||||
report_server = False
|
||||
|
||||
elif _config['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
|
||||
def reporting_loop(_logger, _server):
|
||||
_logger.debug('Periodic Reporting Loop Started (NETWORK)')
|
||||
_server.send_config()
|
||||
|
||||
_logger.info('DMRlink TCP reporting server starting')
|
||||
|
||||
report_server = _factory(_config, _logger)
|
||||
report_server.clients = []
|
||||
reactor.listenTCP(_config['REPORTS']['REPORT_PORT'], report_server)
|
||||
|
||||
reporting = task.LoopingCall(reporting_loop, _logger, report_server)
|
||||
reporting.start(_config['REPORTS']['REPORT_INTERVAL'])
|
||||
|
||||
else:
|
||||
def reporting_loop(_logger):
|
||||
_logger.debug('Periodic Reporting Loop Started (NULL)')
|
||||
report_server = False
|
||||
|
||||
return reporting_loop
|
||||
return report_server
|
||||
|
||||
|
||||
# ID ALIAS CREATION
|
||||
# Download
|
||||
def build_aliases(_config, _logger):
|
||||
if _config['ALIASES']['TRY_DOWNLOAD'] == True:
|
||||
# Try updating peer aliases file
|
||||
result = try_download(_config['ALIASES']['PATH'], _config['ALIASES']['PEER_FILE'], _config['ALIASES']['PEER_URL'], _config['ALIASES']['STALE_TIME'])
|
||||
_logger.info(result)
|
||||
# Try updating subscriber aliases file
|
||||
result = try_download(_config['ALIASES']['PATH'], _config['ALIASES']['SUBSCRIBER_FILE'], _config['ALIASES']['SUBSCRIBER_URL'], _config['ALIASES']['STALE_TIME'])
|
||||
_logger.info(result)
|
||||
|
||||
# Make Dictionaries
|
||||
peer_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['PEER_FILE'])
|
||||
if peer_ids:
|
||||
_logger.info('ID ALIAS MAPPER: peer_ids dictionary is available')
|
||||
|
||||
subscriber_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['SUBSCRIBER_FILE'])
|
||||
if subscriber_ids:
|
||||
_logger.info('ID ALIAS MAPPER: subscriber_ids dictionary is available')
|
||||
|
||||
talkgroup_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['TGID_FILE'])
|
||||
if talkgroup_ids:
|
||||
_logger.info('ID ALIAS MAPPER: talkgroup_ids dictionary is available')
|
||||
|
||||
local_ids = mk_id_dict(_config['ALIASES']['PATH'], _config['ALIASES']['LOCAL_FILE'])
|
||||
if local_ids:
|
||||
_logger.info('ID ALIAS MAPPER: local_ids dictionary is available')
|
||||
|
||||
return(peer_ids, subscriber_ids, talkgroup_ids, local_ids)
|
||||
|
||||
|
||||
# Make the IPSC systems from the config and the class used to build them.
|
||||
#
|
||||
def mk_ipsc_systems(_config, _logger, _systems, _ipsc, _report_server):
|
||||
for system in _config['SYSTEMS']:
|
||||
if _config['SYSTEMS'][system]['LOCAL']['ENABLED']:
|
||||
_systems[system] = _ipsc(system, _config, _logger, _report_server)
|
||||
reactor.listenUDP(_config['SYSTEMS'][system]['LOCAL']['PORT'], _systems[system], interface=_config['SYSTEMS'][system]['LOCAL']['IP'])
|
||||
return _systems
|
||||
|
||||
# Process the MODE byte in registration/peer list packets for determining master and peer capabilities
|
||||
#
|
||||
|
@ -236,7 +279,7 @@ def print_master(_config, _network):
|
|||
#************************************************
|
||||
|
||||
class IPSC(DatagramProtocol):
|
||||
def __init__(self, _name, _config, _logger):
|
||||
def __init__(self, _name, _config, _logger, _report):
|
||||
|
||||
# Housekeeping: create references to the configuration and status data for this IPSC instance.
|
||||
# Some configuration objects that are used frequently and have lengthy names are shortened
|
||||
|
@ -246,7 +289,9 @@ class IPSC(DatagramProtocol):
|
|||
self._system = _name
|
||||
self._CONFIG = _config
|
||||
self._logger = _logger
|
||||
self._report = _report
|
||||
self._config = self._CONFIG['SYSTEMS'][self._system]
|
||||
self._rcm = self._CONFIG['REPORTS']['REPORT_RCM'] and self._report
|
||||
#
|
||||
self._local = self._config['LOCAL']
|
||||
self._local_id = self._local['RADIO_ID']
|
||||
|
@ -321,6 +366,12 @@ class IPSC(DatagramProtocol):
|
|||
self._logger.warning('(%s) Peer De-Registration Requested for: %s, but we don\'t have a listing for this peer', self._system, int_id(_peerid))
|
||||
pass
|
||||
|
||||
# De-register ourselves from the IPSC
|
||||
def de_register_self(self):
|
||||
self._logger.info('(%s) De-Registering self from the IPSC system', self._system)
|
||||
de_reg_req_pkt = self.hashed_packet(self._local['AUTH_KEY'], self.DE_REG_REQ_PKT)
|
||||
self.send_to_ipsc(de_reg_req_pkt)
|
||||
|
||||
# Take a received peer list and the network it belongs to, process and populate the
|
||||
# data structure in my_ipsc_config with the results, and return a simple list of peers.
|
||||
#
|
||||
|
@ -392,14 +443,22 @@ class IPSC(DatagramProtocol):
|
|||
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
|
||||
#************************************************
|
||||
|
||||
# If RCM reporting and reporting is network-based in the global configuration,
|
||||
# send the RCM packet to the monitoring server
|
||||
def call_mon_status(self, _data):
|
||||
self._logger.debug('(%s) Repeater Call Monitor Origin Packet Received: %s', self._system, ahex(_data))
|
||||
if self._rcm:
|
||||
self._report.send_rcm(self._system + ','+ _data)
|
||||
|
||||
def call_mon_rpt(self, _data):
|
||||
self._logger.debug('(%s) Repeater Call Monitor Repeating Packet Received: %s', self._system, ahex(_data))
|
||||
if self._rcm:
|
||||
self._report.send_rcm(self._system + ',' + _data)
|
||||
|
||||
def call_mon_nack(self, _data):
|
||||
self._logger.debug('(%s) Repeater Call Monitor NACK Packet Received: %s', self._system, ahex(_data))
|
||||
if self._rcm:
|
||||
self._report.send_rcm(self._system + ',' + _data)
|
||||
|
||||
def xcmp_xnl(self, _data):
|
||||
self._logger.debug('(%s) XCMP/XNL Packet Received: %s', self._system, ahex(_data))
|
||||
|
@ -938,45 +997,51 @@ class IPSC(DatagramProtocol):
|
|||
# Socket-based reporting section
|
||||
#
|
||||
class report(NetstringReceiver):
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, factory):
|
||||
self._factory = factory
|
||||
|
||||
def connectionMade(self):
|
||||
report_server.clients.append(self)
|
||||
logger.info('DMRlink reporting client connected: %s', self.transport.getPeer())
|
||||
self._factory.clients.append(self)
|
||||
self._factory._logger.info('DMRlink reporting client connected: %s', self.transport.getPeer())
|
||||
|
||||
def connectionLost(self, reason):
|
||||
logger.info('DMRlink reporting client disconnected: %s', self.transport.getPeer())
|
||||
report_server.clients.remove(self)
|
||||
self._factory._logger.info('DMRlink reporting client disconnected: %s', self.transport.getPeer())
|
||||
self._factory.clients.remove(self)
|
||||
|
||||
def stringReceived(self, data):
|
||||
self.process_message(data)
|
||||
|
||||
def process_message(self, _message):
|
||||
opcode = _message[:1]
|
||||
if opcode == REP_OPC['CONFIG_REQ']:
|
||||
logger.info('DMRlink reporting client sent \'CONFIG_REQ\': %s', self.transport.getPeer())
|
||||
if opcode == REPORT_OPCODES['CONFIG_REQ']:
|
||||
self._factory._logger.info('DMRlink reporting client sent \'CONFIG_REQ\': %s', self.transport.getPeer())
|
||||
self.send_config()
|
||||
else:
|
||||
print('got unknown opcode')
|
||||
|
||||
class reportFactory(Factory):
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, config, logger):
|
||||
self._config = config
|
||||
self._logger = logger
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
if (addr.host) in CONFIG['REPORTS']['REPORT_CLIENTS']:
|
||||
return report()
|
||||
if (addr.host) in self._config['REPORTS']['REPORT_CLIENTS'] or '*' in self._config['REPORTS']['REPORT_CLIENTS']:
|
||||
self._logger.debug('Permitting report server connection attempt from: %s:%s', addr.host, addr.port)
|
||||
return report(self)
|
||||
else:
|
||||
self._logger.error('Invalid report server connection attempt from: %s:%s', addr.host, addr.port)
|
||||
return None
|
||||
|
||||
def send_clients(self, _message):
|
||||
for client in report_server.clients:
|
||||
for client in self.clients:
|
||||
client.sendString(_message)
|
||||
|
||||
def send_config(self):
|
||||
serialized = pickle.dumps(CONFIG['SYSTEMS'], protocol=pickle.HIGHEST_PROTOCOL)
|
||||
self.send_clients(REP_OPC['CONFIG_SND']+serialized)
|
||||
serialized = pickle.dumps(self._config['SYSTEMS'], protocol=pickle.HIGHEST_PROTOCOL)
|
||||
self.send_clients(REPORT_OPCODES['CONFIG_SND']+serialized)
|
||||
|
||||
def send_rcm(self, _data):
|
||||
self.send_clients(REPORT_OPCODES['RCM_SND']+_data)
|
||||
|
||||
|
||||
#************************************************
|
||||
|
@ -984,6 +1049,13 @@ class reportFactory(Factory):
|
|||
#************************************************
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
|
||||
from ipsc.dmrlink_config import build_config
|
||||
from ipsc.dmrlink_log import config_logging
|
||||
|
||||
# Change the current directory to the location of the application
|
||||
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
|
||||
|
@ -1007,50 +1079,28 @@ if __name__ == '__main__':
|
|||
if cli_args.LOG_HANDLERS:
|
||||
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
|
||||
logger = config_logging(CONFIG['LOGGER'])
|
||||
|
||||
config_reports(CONFIG)
|
||||
|
||||
|
||||
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
|
||||
|
||||
# Shut ourselves down gracefully with the IPSC peers.
|
||||
def sig_handler(_signal, _frame):
|
||||
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
|
||||
|
||||
for system in systems:
|
||||
this_ipsc = systems[system]
|
||||
logger.info('De-Registering from IPSC %s', system)
|
||||
de_reg_req_pkt = this_ipsc.hashed_packet(this_ipsc._local['AUTH_KEY'], this_ipsc.DE_REG_REQ_PKT)
|
||||
this_ipsc.send_to_ipsc(de_reg_req_pkt)
|
||||
reactor.stop()
|
||||
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2017 N0MJS & the K0USY Group - SYSTEM STARTING...')
|
||||
|
||||
# Set signal handers so that we can gracefully exit if need be
|
||||
def sig_handler(_signal, _frame):
|
||||
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
|
||||
for system in systems:
|
||||
systems[system].de_register_self()
|
||||
reactor.stop()
|
||||
|
||||
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
|
||||
signal.signal(sig, sig_handler)
|
||||
|
||||
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGUED IPSC
|
||||
for system in CONFIG['SYSTEMS']:
|
||||
if CONFIG['SYSTEMS'][system]['LOCAL']['ENABLED']:
|
||||
systems[system] = IPSC(system, CONFIG, logger)
|
||||
reactor.listenUDP(CONFIG['SYSTEMS'][system]['LOCAL']['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['LOCAL']['IP'])
|
||||
# INITIALIZE THE REPORTING LOOP
|
||||
report_server = config_reports(CONFIG, logger, reportFactory)
|
||||
|
||||
# INITIALIZE THE REPORTING LOOP IF CONFIGURED
|
||||
if CONFIG['REPORTS']['REPORT_NETWORKS'] == 'PRINT' or CONFIG['REPORTS']['REPORT_NETWORKS'] == 'PICKLE':
|
||||
reporting_loop = config_reports(CONFIG)
|
||||
reporting = task.LoopingCall(reporting_loop, logger)
|
||||
reporting.start(CONFIG['REPORTS']['REPORT_INTERVAL'])
|
||||
# Build ID Aliases
|
||||
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
|
||||
|
||||
# INITIALIZE THE NETWORK-BASED REPORTING SERVER
|
||||
elif CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
|
||||
logger.info('(confbridge.py) TCP reporting server starting')
|
||||
from ipsc.reporting_const import REPORT_OPCODES as REP_OPC
|
||||
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
|
||||
systems = mk_ipsc_systems(CONFIG, logger, systems, IPSC, report_server)
|
||||
|
||||
report_server = reportFactory()
|
||||
report_server.clients = []
|
||||
reactor.listenTCP(CONFIG['REPORTS']['REPORT_PORT'], reportFactory())
|
||||
|
||||
reporting_loop = config_reports(CONFIG)
|
||||
reporting = task.LoopingCall(reporting_loop, logger, report_server)
|
||||
reporting.start(CONFIG['REPORTS']['REPORT_INTERVAL'])
|
||||
|
||||
# INITIALIZATION COMPLETE -- START THE REACTOR
|
||||
reactor.run()
|
||||
|
|
|
@ -16,9 +16,8 @@ PATH: /opt/dmrlink/
|
|||
# NETWORK REPORTING CONFIGURATION
|
||||
# Enabling "REPORT_NETWORKS" will cause a reporting action for
|
||||
# IPSC each time the periodic reporting loop runs, that period is
|
||||
# specifiec by "REPORT_INTERVAL" in seconds. Possible values
|
||||
# specified by "REPORT_INTERVAL" in seconds. Possible values
|
||||
# for "REPORT_NETWORKS" are:
|
||||
# PICKLE - a Python pickle file of the network's data structure
|
||||
#
|
||||
# PRINT - a pretty print (STDOUT) of the data structure
|
||||
# "PRINT_PEERS_INC_MODE" - Boolean to include mode bits
|
||||
|
@ -31,20 +30,23 @@ PATH: /opt/dmrlink/
|
|||
# goal here is a web dashboard that doesn't live on the
|
||||
# dmrlink machine itself.
|
||||
#
|
||||
# PRINT is the odd man out because it sends prettily formatted stuff
|
||||
# to STDOUT. The others send the internal data structure of the IPSC
|
||||
# instance and let some program on the other end sort it out.
|
||||
# PRINT should only be used for debugging; it sends prettily formatted
|
||||
# stuff to STDOUT. The others send the internal data structure of the
|
||||
# IPSC instance and let some program on the other end sort it out.
|
||||
#
|
||||
# REPORT_RCM - If True, and REPORT_NETWORKS = 'NETWORK', will send RCM
|
||||
# Packets to connected reporting clients. This also requires
|
||||
# individual IPSC systems to have RCM and CON_APP both set 'True'
|
||||
#
|
||||
# REPORT_INTERVAL - Seconds between reports
|
||||
# REPORT_PATH - Absolute path save data (pickle and json)
|
||||
# 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.
|
||||
#
|
||||
[REPORTS]
|
||||
REPORT_NETWORKS:
|
||||
REPORT_RCM:
|
||||
REPORT_INTERVAL: 60
|
||||
REPORT_PATH:
|
||||
REPORT_PORT: 4321
|
||||
REPORT_CLIENTS: 127.0.0.1, 192.168.1.1
|
||||
PRINT_PEERS_INC_MODE: 0
|
||||
|
@ -69,7 +71,7 @@ PRINT_PEERS_INC_FLAGS: 0
|
|||
#
|
||||
[LOGGER]
|
||||
LOG_FILE: /var/log/dmrlink/dmrlink.log
|
||||
LOG_HANDLERS: file
|
||||
LOG_HANDLERS: console-timed,file-timed
|
||||
LOG_LEVEL: INFO
|
||||
LOG_NAME: DMRlink
|
||||
|
||||
|
@ -82,6 +84,7 @@ LOG_NAME: DMRlink
|
|||
# download again. Don't be an ass and change this to less than a few days.
|
||||
[ALIASES]
|
||||
TRY_DOWNLOAD: True
|
||||
LOCAL_FILE: False
|
||||
PATH: ./
|
||||
PEER_FILE: peer_ids.csv
|
||||
SUBSCRIBER_FILE: subscriber_ids.csv
|
||||
|
@ -97,13 +100,13 @@ STALE_DAYS: 7
|
|||
#
|
||||
# [NAME] The name you want to use to identify the IPSC instance (use
|
||||
# something better than "IPSC1"...)
|
||||
# ENABLED: Should we communiate with this network? Handy if you need to
|
||||
# ENABLED: Should we communicate with this network? Handy if you need to
|
||||
# shut one down but don't want to lose the config
|
||||
# RADIO_ID: This is the radio ID that DMRLink should use to communicate
|
||||
# IP: This is the local IPv4 address to listen on. It may be left
|
||||
# blank if you do not need or wish to specify. It is mostly
|
||||
# useful when DMRlink uses multiple interfaces to serve as an
|
||||
# application gatway/proxy from private and/or VPN networks
|
||||
# application gateway/proxy from private and/or VPN networks
|
||||
# to the real world.
|
||||
# PORT: This is the UDP source port for DMRLink to use for this
|
||||
# PSC network, must be unique!!!
|
||||
|
@ -117,9 +120,10 @@ STALE_DAYS: 7
|
|||
# CSBK_CALL: Should be False, we cannot process these, but may be useful
|
||||
# for debugging.
|
||||
# RCM: Repeater Call Monitoring - don't unable unless you plan to
|
||||
# actually use it, this craetes extra network traffic.
|
||||
# actually use it, this creates extra network traffic.
|
||||
# CON_APP: Third Party Console App - exactly what DMRlink is, should
|
||||
# be set to True.
|
||||
# be set to True, and must be if you intend to process RCM
|
||||
# packets (like with network-based reporting)
|
||||
# XNL_CALL: Can cause problems if not set to False, DMRlink does not
|
||||
# process XCMP/XNL calls.
|
||||
# XNL_MASTER: Obviously, should also be False, see XNL_CALL.
|
||||
|
@ -129,14 +133,14 @@ STALE_DAYS: 7
|
|||
# AUTH_ENABLED: Do we use authenticated IPSC?
|
||||
# AUTH_KEY: The Authentication key (up to 40 hex characters)
|
||||
# MASTER_IP: IP address of the IPSC master (ignored if DMRlink is the master)
|
||||
# MASTER_PORT: UDP port of the IPSC master (ignored if DMRlinkn is the master)
|
||||
# MASTER_PORT: UDP port of the IPSC master (ignored if DMRlink is the master)
|
||||
# GROUP_HANGTIME: Group hangtime, per DMR configuration
|
||||
#
|
||||
# ...Repeat the block for each IPSC network to join.
|
||||
#
|
||||
|
||||
[SAMPLE_PEER]
|
||||
ENABLED: True
|
||||
ENABLED: False
|
||||
RADIO_ID: 12345
|
||||
IP:
|
||||
PORT: 50000
|
||||
|
@ -162,7 +166,7 @@ GROUP_HANGTIME: 5
|
|||
|
||||
|
||||
[SAMPLE_MASTER]
|
||||
ENABLED: False
|
||||
ENABLED: True
|
||||
RADIO_ID: 54321
|
||||
IP: 192.168.1.1
|
||||
PORT: 50000
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
###############################################################################
|
||||
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
###############################################################################
|
||||
|
||||
import ConfigParser
|
||||
import sys
|
||||
|
||||
from socket import gethostbyname
|
||||
|
||||
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
|
||||
__author__ = 'Cortney T. Buffington, N0MJS'
|
||||
__copyright__ = 'Copyright (c) 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
|
||||
__license__ = 'GNU GPLv3'
|
||||
__maintainer__ = 'Cort Buffington, N0MJS'
|
||||
__email__ = 'n0mjs@me.com'
|
||||
|
||||
|
||||
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'] = {}
|
||||
|
||||
try:
|
||||
for section in config.sections():
|
||||
if section == 'GLOBAL':
|
||||
CONFIG['GLOBAL'].update({
|
||||
'PATH': config.get(section, 'PATH')
|
||||
})
|
||||
|
||||
elif section == 'REPORTS':
|
||||
CONFIG['REPORTS'].update({
|
||||
'REPORT_NETWORKS': config.get(section, 'REPORT_NETWORKS'),
|
||||
'REPORT_INTERVAL': config.getint(section, 'REPORT_INTERVAL'),
|
||||
'REPORT_PATH': config.get(section, 'REPORT_PATH'),
|
||||
'REPORT_PORT': config.get(section, 'REPORT_PORT'),
|
||||
'REPORT_CLIENTS': config.get(section, 'REPORT_CLIENTS').split(','),
|
||||
'PRINT_PEERS_INC_MODE': config.getboolean(section, 'PRINT_PEERS_INC_MODE'),
|
||||
'PRINT_PEERS_INC_FLAGS': config.getboolean(section, 'PRINT_PEERS_INC_FLAGS')
|
||||
})
|
||||
if CONFIG['REPORTS']['REPORT_PORT']:
|
||||
CONFIG['REPORTS']['REPORT_PORT'] = int(CONFIG['REPORTS']['REPORT_PORT'])
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
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'),
|
||||
'STALE_TIME': config.getint(section, 'STALE_DAYS') * 86400,
|
||||
})
|
||||
|
||||
elif config.getboolean(section, 'ENABLED'):
|
||||
CONFIG['SYSTEMS'].update({section: {'LOCAL': {}, 'MASTER': {}, 'PEERS': {}}})
|
||||
|
||||
CONFIG['SYSTEMS'][section]['LOCAL'].update({
|
||||
# In case we want to keep config, but not actually connect to the network
|
||||
'ENABLED': config.getboolean(section, 'ENABLED'),
|
||||
|
||||
# These items are used to create the MODE byte
|
||||
'PEER_OPER': config.getboolean(section, 'PEER_OPER'),
|
||||
'IPSC_MODE': config.get(section, 'IPSC_MODE'),
|
||||
'TS1_LINK': config.getboolean(section, 'TS1_LINK'),
|
||||
'TS2_LINK': config.getboolean(section, 'TS2_LINK'),
|
||||
'MODE': '',
|
||||
|
||||
# These items are used to create the multi-byte FLAGS field
|
||||
'AUTH_ENABLED': config.getboolean(section, 'AUTH_ENABLED'),
|
||||
'CSBK_CALL': config.getboolean(section, 'CSBK_CALL'),
|
||||
'RCM': config.getboolean(section, 'RCM'),
|
||||
'CON_APP': config.getboolean(section, 'CON_APP'),
|
||||
'XNL_CALL': config.getboolean(section, 'XNL_CALL'),
|
||||
'XNL_MASTER': config.getboolean(section, 'XNL_MASTER'),
|
||||
'DATA_CALL': config.getboolean(section, 'DATA_CALL'),
|
||||
'VOICE_CALL': config.getboolean(section, 'VOICE_CALL'),
|
||||
'MASTER_PEER': config.getboolean(section, 'MASTER_PEER'),
|
||||
'FLAGS': '',
|
||||
|
||||
# Things we need to know to connect and be a peer in this IPSC
|
||||
'RADIO_ID': hex(int(config.get(section, 'RADIO_ID')))[2:].rjust(8,'0').decode('hex'),
|
||||
'IP': gethostbyname(config.get(section, 'IP')),
|
||||
'PORT': config.getint(section, 'PORT'),
|
||||
'ALIVE_TIMER': config.getint(section, 'ALIVE_TIMER'),
|
||||
'MAX_MISSED': config.getint(section, 'MAX_MISSED'),
|
||||
'AUTH_KEY': (config.get(section, 'AUTH_KEY').rjust(40,'0')).decode('hex'),
|
||||
'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME'),
|
||||
'NUM_PEERS': 0,
|
||||
})
|
||||
# Master means things we need to know about the master peer of the network
|
||||
CONFIG['SYSTEMS'][section]['MASTER'].update({
|
||||
'RADIO_ID': '\x00\x00\x00\x00',
|
||||
'MODE': '\x00',
|
||||
'MODE_DECODE': '',
|
||||
'FLAGS': '\x00\x00\x00\x00',
|
||||
'FLAGS_DECODE': '',
|
||||
'STATUS': {
|
||||
'CONNECTED': False,
|
||||
'PEER_LIST': False,
|
||||
'KEEP_ALIVES_SENT': 0,
|
||||
'KEEP_ALIVES_MISSED': 0,
|
||||
'KEEP_ALIVES_OUTSTANDING': 0,
|
||||
'KEEP_ALIVES_RECEIVED': 0,
|
||||
'KEEP_ALIVE_RX_TIME': 0
|
||||
},
|
||||
'IP': '',
|
||||
'PORT': ''
|
||||
})
|
||||
if not CONFIG['SYSTEMS'][section]['LOCAL']['MASTER_PEER']:
|
||||
CONFIG['SYSTEMS'][section]['MASTER'].update({
|
||||
'IP': gethostbyname(config.get(section, 'MASTER_IP')),
|
||||
'PORT': config.getint(section, 'MASTER_PORT')
|
||||
})
|
||||
|
||||
# Temporary locations for building MODE and FLAG data
|
||||
MODE_BYTE = 0
|
||||
FLAG_1 = 0
|
||||
FLAG_2 = 0
|
||||
|
||||
# Construct and store the MODE field
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['PEER_OPER']:
|
||||
MODE_BYTE |= 1 << 6
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['IPSC_MODE'] == 'ANALOG':
|
||||
MODE_BYTE |= 1 << 4
|
||||
elif CONFIG['SYSTEMS'][section]['LOCAL']['IPSC_MODE'] == 'DIGITAL':
|
||||
MODE_BYTE |= 1 << 5
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['TS1_LINK']:
|
||||
MODE_BYTE |= 1 << 3
|
||||
else:
|
||||
MODE_BYTE |= 1 << 2
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['TS2_LINK']:
|
||||
MODE_BYTE |= 1 << 1
|
||||
else:
|
||||
MODE_BYTE |= 1 << 0
|
||||
CONFIG['SYSTEMS'][section]['LOCAL']['MODE'] = chr(MODE_BYTE)
|
||||
|
||||
# Construct and store the FLAGS field
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['CSBK_CALL']:
|
||||
FLAG_1 |= 1 << 7
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['RCM']:
|
||||
FLAG_1 |= 1 << 6
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['CON_APP']:
|
||||
FLAG_1 |= 1 << 5
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL']:
|
||||
FLAG_2 |= 1 << 7
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL'] and CONFIG['SYSTEMS'][section]['LOCAL']['XNL_MASTER']:
|
||||
FLAG_2 |= 1 << 6
|
||||
elif CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL'] and not CONFIG['SYSTEMS'][section]['LOCAL']['XNL_MASTER']:
|
||||
FLAG_2 |= 1 << 5
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['AUTH_ENABLED']:
|
||||
FLAG_2 |= 1 << 4
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['DATA_CALL']:
|
||||
FLAG_2 |= 1 << 3
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['VOICE_CALL']:
|
||||
FLAG_2 |= 1 << 2
|
||||
if CONFIG['SYSTEMS'][section]['LOCAL']['MASTER_PEER']:
|
||||
FLAG_2 |= 1 << 0
|
||||
CONFIG['SYSTEMS'][section]['LOCAL']['FLAGS'] = '\x00\x00'+chr(FLAG_1)+chr(FLAG_2)
|
||||
|
||||
except ConfigParser.Error, err:
|
||||
sys.exit('Could not parse configuration file, exiting...')
|
||||
|
||||
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
|
||||
|
||||
# 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 dmrlink.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__))+'/dmrlink.cfg'
|
||||
|
||||
|
||||
pprint(build_config(cli_args.CONFIG_FILE))
|
|
@ -1,87 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
###############################################################################
|
||||
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
###############################################################################
|
||||
|
||||
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 Cortney T. Buffington, N0MJS and the K0USY Group'
|
||||
__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',
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
_logger['LOG_NAME']: {
|
||||
'handlers': _logger['LOG_HANDLERS'].split(','),
|
||||
'level': _logger['LOG_LEVEL'],
|
||||
'propagate': True,
|
||||
}
|
||||
}
|
||||
})
|
||||
return logging.getLogger(_logger['LOG_NAME'])
|
35
mk-dmrlink
35
mk-dmrlink
|
@ -12,19 +12,6 @@ echo ""
|
|||
# Application #
|
||||
#################################################
|
||||
|
||||
# Install the required support programs
|
||||
apt-get install unzip -y
|
||||
apt-get install python-dev -y
|
||||
apt-get install python-pip -y
|
||||
apt-get install python-twisted -y
|
||||
pip install bitstring
|
||||
pip install bitarray
|
||||
|
||||
cd /opt
|
||||
git clone https://github.com/n0mjs710/dmr_utils.git
|
||||
cd dmr_utils/
|
||||
pip install .
|
||||
|
||||
echo "Required programs installed, continuing"
|
||||
|
||||
# To allow multiple instances of DMRlink to run
|
||||
|
@ -32,7 +19,7 @@ echo "Required programs installed, continuing"
|
|||
# The needed files are copied to /opt/dmrlink
|
||||
|
||||
# Make needed directories
|
||||
mkdir -p /opt/dmrlink/ambe_audio/
|
||||
mkdir -p /opt/dmrlink/IPSC_Bridge/
|
||||
mkdir -p /opt/dmrlink/bridge/
|
||||
mkdir -p /opt/dmrlink/confbridge/
|
||||
mkdir -p /opt/dmrlink/log/
|
||||
|
@ -52,7 +39,7 @@ cd /opt/dmrlink
|
|||
# cp $currentdir/talkgroup_ids.csv /opt/dmrlink
|
||||
|
||||
# Copy ipsc directory into each app directory
|
||||
cp -rf $currentdir/ipsc/ /opt/dmrlink/ambe_audio/
|
||||
cp -rf $currentdir/ipsc/ /opt/dmrlink/IPSC_Bridge/
|
||||
cp -rf $currentdir/ipsc/ /opt/dmrlink/bridge/
|
||||
cp -rf $currentdir/ipsc/ /opt/dmrlink/confbridge/
|
||||
cp -rf $currentdir/ipsc/ /opt/dmrlink/log/
|
||||
|
@ -68,23 +55,23 @@ cp $currentdir/confbridge_rules_SAMPLE.py /opt/dmrlink/samples
|
|||
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/samples
|
||||
cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/samples
|
||||
cp $currentdir/playback_config_SAMPLE.py /opt/dmrlink/samples
|
||||
cp $currentdir/ambe_audio.cfg /opt/dmrlink/samples
|
||||
cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/samples
|
||||
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/samples
|
||||
|
||||
# Put the doc together for easy reference
|
||||
cp -rf $currentdir/documents /opt/dmrlink
|
||||
cp $currentdir/LICENSE.txt /opt/dmrlink/documents
|
||||
cp $currentdir/requirements.txt /opt/dmrlink/documents
|
||||
cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/documents
|
||||
# cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/documents
|
||||
|
||||
# ambe_audio
|
||||
cp $currentdir/dmrlink.py /opt/dmrlink/ambe_audio/
|
||||
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/ambe_audio/
|
||||
# IPSC_Bridge
|
||||
cp $currentdir/dmrlink.py /opt/dmrlink/IPSC_Bridge/
|
||||
cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/IPSC_Bridge/
|
||||
#
|
||||
cp $currentdir/ambe_audio.cfg /opt/dmrlink/ambe_audio/
|
||||
cp $currentdir/ambe_audio.py /opt/dmrlink/ambe_audio/
|
||||
cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/ambe_audio/
|
||||
cp $currentdir/template.bin /opt/dmrlink/ambe_audio/
|
||||
cp $currentdir/IPSC_Bridge.cfg /opt/dmrlink/IPSC_Bridge/
|
||||
cp $currentdir/IPSC_Bridge.py /opt/dmrlink/IPSC_Bridge/
|
||||
# cp $currentdir/IPSC_Bridge_commands.txt /opt/dmrlink/IPSC_Bridge/
|
||||
# cp $currentdir/template.bin /opt/dmrlink/IPSC_Bridge/
|
||||
|
||||
# Bridge app
|
||||
cp $currentdir/dmrlink.py /opt/dmrlink/bridge/
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
[Unit]
|
||||
Description=DMRlink ambe audio Service
|
||||
# Description=Place this file in /lib/systemd/system
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
StandardOutput=null
|
||||
WorkingDirectory=/opt/dmrlink/ambe_audio
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
ExecStart=/usr/bin/python /opt/dmrlink/ambe_audio/ambe_audio.py
|
||||
ExecReload=/bin/kill -2 $MAINPID
|
||||
KillMode=process
|
||||
|
||||
[Install]
|
||||
WantedBy=network-online.target
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=DMRlink IPSC_Bridge Service
|
||||
# Place this file in /lib/systemd/system
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
StandardOutput=null
|
||||
WorkingDirectory=/opt/dmrlink/IPSC_Bridge
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
ExecStart=/usr/bin/python /opt/dmrlink/IPSC_Bridge/IPSC_Bridge.py
|
||||
ExecReload=/bin/kill -2 $MAINPID
|
||||
KillMode=process
|
||||
|
||||
[Install]
|
||||
WantedBy=network-online.target
|
||||
|
Loading…
Reference in New Issue