719 lines
38 KiB
Python
Executable File
719 lines
38 KiB
Python
Executable File
#!/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
|
|
TAG_DMR_TEST = 9
|
|
|
|
# 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, _frames=1):
|
|
_rx_slot.stream_id = _stream_id
|
|
self.send_voice_header(_rx_slot)
|
|
sleep(0.06)
|
|
silence = '\xAC\AA\x40\x20\x00\x44\x40\x80\x80'
|
|
self._logger.info('(%s) Playing %d frames', self._system, _frames)
|
|
while _frames > 0:
|
|
self.send_voice72(_rx_slot, silence+silence+silence)
|
|
sleep(0.06)
|
|
_frames = _frames - 1
|
|
self.send_voice_term(_rx_slot)
|
|
self._logger.info('(%s) Playback done', self._system)
|
|
# 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_DMR_TEST):
|
|
_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)
|
|
thread.start_new_thread(self.sendBlankAmbe, (_rx_slot, hex_str_4(randint(0,0xFFFFFFFF)), 5 * 60 * 500))
|
|
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, int_id(_src_id)))
|
|
|
|
_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) & 0xff):
|
|
_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
|
|
self._DMOTimeout = 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) or (time() > self._DMOTimeout): # are we idle?
|
|
self._DMOStreamID = _rx_slot.stream_id
|
|
self._DMOTimeout = time() + 0.50
|
|
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)
|
|
self._DMOTimeout = time() + 0.50
|
|
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) & 0xff):
|
|
self._logger.info('(%s) Seq number not found. got %d expected %d', self._system, _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 = 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_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, _newSourceID, _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, 1)
|
|
# re-Write the source subscriber ID + destination Group ID combo in the IPSC Header
|
|
_frame = _frame.replace(_src_sub + _group, _newSourceID + _newGroup, 1)
|
|
# Re-Write the destination Group ID + source subscriber ID combo in the decoded LCs
|
|
_frame = _frame.replace(_group + _src_sub, _newGroup + _newSourceID, 1)
|
|
|
|
|
|
_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')
|
|
exit()
|
|
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) & 0xff):
|
|
_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()
|