Use programs in ipsc directory and dmr_utils

This commit is contained in:
Steve N4IRS 2017-06-20 08:40:28 -04:00
parent a826489424
commit 942e26348b
10 changed files with 201 additions and 1442 deletions

View File

@ -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
@ -95,13 +96,13 @@ class ambeIPSC(IPSC):
_dmrgui = ''
cc = 1
ipsc_seq = 0
###### DEBUGDEBUGDEBUG
#_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')
@ -272,9 +274,9 @@ if __name__ == '__main__':
import sys
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__)
@ -339,12 +341,16 @@ if __name__ == '__main__':
# Set signal handers so that we can gracefully exit if need be
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()

View File

@ -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()

View File

@ -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()

View File

@ -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']
@ -320,7 +365,13 @@ class IPSC(DatagramProtocol):
else:
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.
#
@ -391,15 +442,23 @@ 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'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2017 N0MJS & the K0USY Group - SYSTEM STARTING...')
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.
# 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:
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)
systems[system].de_register_self()
reactor.stop()
# Set signal handers so that we can gracefully exit if need be
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 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'])
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# 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
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'])
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, IPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

View File

@ -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_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

View File

@ -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))

View 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'])

View File

@ -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/

View File

@ -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

View File

@ -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