diff --git a/data_gateway.py b/data_gateway.py new file mode 100644 index 0000000..9e8f801 --- /dev/null +++ b/data_gateway.py @@ -0,0 +1,933 @@ +#!/usr/bin/env python3 +# +############################################################################### +# HBLink - Copyright (C) 2020 Cortney T. Buffington, N0MJS +# GPS/Data - Copyright (C) 2020 Eric Craw, KF7EEL +# Annotated modifications Copyright (C) 2021 Xavier FRS2013 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################### + +''' +This is a data application. It decodes and reassambles DMR GPS packets and +uploads them to APRS-IS. Also does miscelaneous SMS functions. +''' + +# Python modules we need +import sys +from bitarray import bitarray +from time import time, strftime +from importlib import import_module + +# Twisted is pretty important, so I keep it separate +from twisted.internet.protocol import Factory, Protocol +from twisted.protocols.basic import NetstringReceiver +from twisted.internet import reactor, task + +# Things we import from the main hblink module +from hblink import HBSYSTEM, OPENBRIDGE, systems, hblink_handler, reportFactory, REPORT_OPCODES, mk_aliases, config_reports +from dmr_utils3.utils import bytes_3, int_id, get_alias +from dmr_utils3 import decode, bptc, const +import config +import log +from const import * + +# Stuff for socket reporting +import pickle +# REMOVE LATER from datetime import datetime +# The module needs logging, but handlers, etc. are controlled by the parent +import logging +logger = logging.getLogger(__name__) + +#### Modules for data gateway ### +# modules from gps_data.py +from bitarray import bitarray +from binascii import b2a_hex as ahex +import re +##from binascii import a2b_hex as bhex +import aprslib +import datetime +from bitarray.util import ba2int as ba2num +from bitarray.util import ba2hex as ba2hx +import codecs +#Needed for working with NMEA +import pynmea2 + +# Modules for executing commands/scripts +import os +##from gps_functions import cmd_list + +# Module for maidenhead grids +try: + import maidenhead as mh +except: + logger.info('Error importing maidenhead module, make sure it is installed.') +# Module for sending email +try: + import smtplib +except: + logger.info('Error importing smtplib module, make sure it is installed.') + +#Modules for APRS settings +import ast +from pathlib import Path +# Used for APRS +import threading +# Used for SMS encoding +import libscrc +import random +from bitarray.util import hex2ba as hex2bits +################################# + + +# Does anybody read this stuff? There's a PEP somewhere that says I should do this. +__author__ = 'Cortney T. Buffington, N0MJS' +__copyright__ = 'Copyright (c) 2016-2018 Cortney T. Buffington, N0MJS and the K0USY Group' +__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT' +__license__ = 'GNU GPLv3' +__maintainer__ = 'Cort Buffington, N0MJS' +__email__ = 'n0mjs@me.com' + + +################################################################################################## + +# Headers for GPS by model of radio: +# AT-D878 - Compressed UDP +# MD-380 - Unified Data Transport +hdr_type = '' +btf = -1 +ssid = '' + +# From dmr_utils3, modified to decode entire packet. Works for 1/2 rate coded data. +def decode_full(_data): + binlc = bitarray(endian='big') + binlc.extend([_data[136],_data[121],_data[106],_data[91], _data[76], _data[61], _data[46], _data[31]]) + binlc.extend([_data[152],_data[137],_data[122],_data[107],_data[92], _data[77], _data[62], _data[47], _data[32], _data[17], _data[2] ]) + binlc.extend([_data[123],_data[108],_data[93], _data[78], _data[63], _data[48], _data[33], _data[18], _data[3], _data[184],_data[169]]) + binlc.extend([_data[94], _data[79], _data[64], _data[49], _data[34], _data[19], _data[4], _data[185],_data[170],_data[155],_data[140]]) + binlc.extend([_data[65], _data[50], _data[35], _data[20], _data[5], _data[186],_data[171],_data[156],_data[141],_data[126],_data[111]]) + binlc.extend([_data[36], _data[21], _data[6], _data[187],_data[172],_data[157],_data[142],_data[127],_data[112],_data[97], _data[82] ]) + binlc.extend([_data[7], _data[188],_data[173],_data[158],_data[143],_data[128],_data[113],_data[98], _data[83]]) + #This is the rest of the Full LC data -- the RS1293 FEC that we don't need + # This is extremely important for SMS and GPS though. + binlc.extend([_data[68],_data[53],_data[174],_data[159],_data[144],_data[129],_data[114],_data[99],_data[84],_data[69],_data[54],_data[39]]) + binlc.extend([_data[24],_data[145],_data[130],_data[115],_data[100],_data[85],_data[70],_data[55],_data[40],_data[25],_data[10],_data[191]]) + return binlc + + +n_packet_assembly = 0 + +packet_assembly = '' + +final_packet = '' + +#Convert DMR packet to binary from MMDVM packet and remove Slot Type and EMB Sync stuff to allow for BPTC 196,96 decoding +def bptc_decode(_data): + binary_packet = bitarray(decode.to_bits(_data[20:])) + del binary_packet[98:166] + return decode_full(binary_packet) +# Placeholder for future header id +def header_ID(_data): + hex_hdr = str(ahex(bptc_decode(_data))) + return hex_hdr[2:6] + # Work in progress, used to determine data format +## pass + +def aprs_send(packet): + if 'N0CALL' in aprs_callsign: + logger.info('APRS callsighn set to N0CALL, packet not sent.') + pass + else: + AIS.sendall(packet) + logger.info('Packet sent to APRS-IS.') + +def dashboard_loc_write(call, lat, lon, time, comment): + dash_entries = ast.literal_eval(os.popen('cat ' + loc_file).read()) + dash_entries.insert(0, {'call': call, 'lat': lat, 'lon': lon, 'time':time, 'comment':comment}) +# Clear old entries + list_index = 0 + call_count = 0 + new_dash_entries = [] + for i in dash_entries: + if i['call'] == call: + if call_count >= 25: + pass + else: + new_dash_entries.append(i) + call_count = call_count + 1 + + if call != i['call']: + new_dash_entries.append(i) + pass + list_index = list_index + 1 + with open(loc_file, 'w') as user_loc_file: + user_loc_file.write(str(new_dash_entries[:500])) + user_loc_file.close() + logger.info('User location saved for dashboard') + #logger.info(dash_entries) + +def dashboard_bb_write(call, dmr_id, time, bulletin): + #try: + dash_bb = ast.literal_eval(os.popen('cat ' + bb_file).read()) + # except: + # dash_entries = [] + dash_bb.insert(0, {'call': call, 'dmr_id': dmr_id, 'time': time, 'bulletin':bulletin}) + with open(bb_file, 'w') as user_bb_file: + user_bb_file.write(str(dash_bb[:20])) + user_bb_file.close() + logger.info('User bulletin entry saved.') + #logger.info(dash_bb) + +def dashboard_sms_write(snd_call, rcv_call, rcv_dmr_id, snd_dmr_id, sms, time): + #try: + dash_sms = ast.literal_eval(os.popen('cat ' + sms_file).read()) + # except: + # dash_entries = [] + dash_sms.insert(0, {'snd_call': snd_call, 'rcv_call':rcv_call, 'snd_dmr_id': snd_dmr_id, 'rcv_dmr_id':rcv_dmr_id, 'time': time, 'sms':sms}) + with open(sms_file, 'w') as user_sms_file: + user_sms_file.write(str(dash_sms[:25])) + user_sms_file.close() + logger.info('User SMS entry saved.') + + +def mailbox_write(call, dmr_id, time, message, recipient): + #try: + mail_file = ast.literal_eval(os.popen('cat ' + the_mailbox_file).read()) + mail_file.insert(0, {'call': call, 'dmr_id': dmr_id, 'time': time, 'message':message, 'recipient': recipient}) + with open(the_mailbox_file, 'w') as mailbox_file: + mailbox_file.write(str(mail_file[:100])) + mailbox_file.close() + logger.info('User mail saved.') + +def mailbox_delete(dmr_id): + mail_file = ast.literal_eval(os.popen('cat ' + the_mailbox_file).read()) + call = str(get_alias((dmr_id), subscriber_ids)) + new_data = [] + for message in mail_file: + if message['recipient'] != call: + new_data.append(message) + with open(the_mailbox_file, 'w') as mailbox_file: + mailbox_file.write(str(new_data[:100])) + mailbox_file.close() + logger.info('Mailbox updated. Delete occurred.') + + +def sos_write(dmr_id, time, message): + user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read()) + try: + if user_settings[dmr_id][1]['ssid'] == '': + sos_call = user_settings[dmr_id][0]['call'] + '-' + user_ssid + else: + sos_call = user_settings[dmr_id][0]['call'] + '-' + user_settings[dmr_id][1]['ssid'] + except: + sos_call = str(get_alias((dmr_id), subscriber_ids)) + sos_info = {'call': sos_call, 'dmr_id': dmr_id, 'time': time, 'message':message} + with open(emergency_sos_file, 'w') as sos_file: + sos_file.write(str(sos_info)) + sos_file.close() + logger.info('Saved SOS.') +def send_app_request(url, message, source_id): + #url = url + '/app' + #Load current AUTH token list + auth_file = ast.literal_eval(os.popen('cat ' + auth_token_file).read()) + the_token = str(hashlib.md5(str(time()).encode('utf-8')).hexdigest()) + new_auth_file = auth_file + new_auth_file.append(the_token) + # Write new list to file + with open(auth_token_file, 'w') as auth_token: + auth_token.write(str(auth_file)) + auth_token.close() + app_request = { + 'mode':'app', + 'system_shortcut':CONFIG['GPS_DATA']['MY_SERVER_SHORTCUT'], + 'server_name':CONFIG['GPS_DATA']['SERVER_NAME'], + 'response_url':CONFIG['GPS_DATA']['DASHBOARD_URL'] + '/api', + 'auth_token':the_token, + 'data':{ + 'source_id':source_id, + 'slot':0, + 'msg_type':'unit', + 'msg_format':'motorola', + 'message':message + } + } + json_object = json.dumps(app_request, indent = 4) + print(json_object) + requests.post(url, data=json_object, headers={'Content-Type': 'application/json'}) + + +def send_msg_xfer(url, user, password, message, source_id, dest_id): + url = url + '/api/msg_xfer' + msg_xfer = { + 'mode':'msg_xfer', + 'system_shortcut':CONFIG['GPS_DATA']['MY_SERVER_SHORTCUT'], + 'response_url':CONFIG['GPS_DATA']['DASHBOARD_URL'] + '/api', + 'auth_type':'private', + 'credentials': { + 'user':user, + 'password':password, + }, + 'data':{ + 1:{'source_id':source_id, + 'destination_id':dest_id, + 'slot':0, + 'msg_type':'unit', + 'msg_format':'motorola', + 'message':message + } + } + } + json_object = json.dumps(msg_xfer, indent = 4) + requests.post(url, data=json_object, headers={'Content-Type': 'application/json'}) + + +# Send email via SMTP function +def send_email(to_email, email_subject, email_message): + global smtp_server + sender_address = email_sender + account_password = email_password + smtp_server = smtplib.SMTP_SSL(smtp_server, int(smtp_port)) + smtp_server.login(sender_address, account_password) + message = "From: " + aprs_callsign + " D-APRS Gateway\nTo: " + to_email + "\nContent-type: text/html\nSubject: " + email_subject + "\n\n" + '' + email_subject + '

 

' + email_message + '

 

This message was sent to you from a D-APRS gateway operated by ' + aprs_callsign + '. Do not reply as this gateway is only one way at this time.

' + smtp_server.sendmail(sender_address, to_email, message) + smtp_server.close() + +def generate_apps(): + global access_systems + #local_apps = ast.literal_eval(os.popen('cat ' + access_systems_file).read()) + public_systems_file = requests.get(CONFIG['GPS_DATA']['PUBLIC_APPS_LIST']) + public_apps = ast.literal_eval(public_systems_file.text) + access_systems = {} + #combined = public_apps.items() + local_acess_systems.items() + if CONFIG['GPS_DATA']['USE_PUBLIC_APPS'] == True: + for i in public_apps.items(): + key = str(i[0]) + access_systems[key] = i[1] + for i in local_apps.items(): + key = str(i[0]) + access_systems[key] = i[1] + print(access_systems) + + return access_systems + +# Thanks for this forum post for this - https://stackoverflow.com/questions/2579535/convert-dd-decimal-degrees-to-dms-degrees-minutes-seconds-in-python + +def decdeg2dms(dd): + is_positive = dd >= 0 + dd = abs(dd) + minutes,seconds = divmod(dd*3600,60) + degrees,minutes = divmod(minutes,60) + degrees = degrees if is_positive else -degrees + return (degrees,minutes,seconds) + +def user_setting_write(dmr_id, setting, value, call_type): +## try: + # Open file and load as dict for modification + logger.info(setting.upper()) + with open(user_settings_file, 'r') as f: +## if f.read() == '{}': +## user_dict = {} + user_dict = ast.literal_eval(f.read()) + logger.info('Current settings: ' + str(user_dict)) + if dmr_id not in user_dict: + user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}, {'pin': ''}, {'APRS': False}] + if setting.upper() == 'ICON': + user_dict[dmr_id][2]['icon'] = value + if setting.upper() == 'SSID': + user_dict[dmr_id][1]['ssid'] = value + if setting.upper() == 'COM': + user_comment = user_dict[dmr_id][3]['comment'] = value[0:35] + if setting.upper() == 'APRS ON': + user_dict[dmr_id][5] = {'APRS': True} + if call_type == 'unit': + send_sms(False, dmr_id, 0000, 0000, 'unit', 'APRS MSG TX/RX Enabled') + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'APRS MSG TX/RX Enabled') + if setting.upper() == 'APRS OFF': + user_dict[dmr_id][5] = {'APRS': False} + if call_type == 'unit': + send_sms(False, dmr_id, 0000, 0000, 'unit', 'APRS MSG TX/RX Disabled') + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'APRS MSG TX/RX Disabled') + if setting.upper() == 'PIN': + #try: + #if user_dict[dmr_id]: + user_dict[dmr_id][4]['pin'] = value + if call_type == 'unit': + send_sms(False, dmr_id, 0000, 0000, 'unit', 'You can now use your pin on the dashboard.') + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'You can now use your pin on the dashboard.') + #if not user_dict[dmr_id]: + # user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}, {'pin': pin}] + #except: + # user_dict[dmr_id].append({'pin': value}) + f.close() + logger.info('Loaded user settings. Write changes.') + # Write modified dict to file + with open(user_settings_file, 'w') as user_dict_file: + user_dict_file.write(str(user_dict)) + user_dict_file.close() + logger.info('User setting saved') + f.close() + packet_assembly = '' + +# Process SMS, do something bases on message + +def process_sms(_rf_src, sms, call_type): + logger.info(call_type) + parse_sms = sms.split(' ') + logger.info(parse_sms) + if parse_sms[0] == 'ID': + logger.info(str(get_alias(int_id(_rf_src), subscriber_ids)) + ' - ' + str(int_id(_rf_src))) + if call_type == 'unit': + send_sms(False, int_id(_rf_src), 0000, 0000, 'unit', 'Your DMR ID: ' + str(int_id(_rf_src)) + ' - ' + str(get_alias(int_id(_rf_src), subscriber_ids))) + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'Your DMR ID: ' + str(int_id(_rf_src)) + ' - ' + str(get_alias(int_id(_rf_src), subscriber_ids))) + elif parse_sms[0] == 'TEST': + logger.info('It works!') + if call_type == 'unit': + send_sms(False, int_id(_rf_src), 0000, 0000, 'unit', 'It works') + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'It works') + elif '@ICON' in parse_sms[0]: + user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@ICON| ','',sms), call_type) + elif '@SSID' in parse_sms[0]: + user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@SSID| ','',sms), call_type) + elif '@COM' in parse_sms[0]: + user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@COM |@COM','',sms), call_type) + elif '@PIN' in parse_sms[0]: + user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), int(re.sub('@PIN |@PIN','',sms)), call_type) + # Write blank entry to cause APRS receive to look for packets for this station. + elif '@APRS ON' in sms or '@APRS on' in sms: + user_setting_write(int_id(_rf_src), 'APRS ON', True, call_type) + elif '@APRS OFF' in sms or '@APRS off' in sms: + user_setting_write(int_id(_rf_src), 'APRS OFF', False, call_type) + elif '@BB' in sms: + dashboard_bb_write(get_alias(int_id(_rf_src), subscriber_ids), int_id(_rf_src), time(), re.sub('@BB|@BB ','',sms)) + elif '@' in parse_sms[0][1:] and '.' in parse_sms[0]: # and ' E-' in sms: + s = ' ' + email_message = s.join(parse_sms[1:])#str(re.sub('.*@|.* E-', '', sms)) + to_email = parse_sms[0]#str(re.sub(' E-.*', '', sms)) + email_subject = 'New message from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + logger.info('Email to: ' + to_email) + logger.info('Message: ' + email_message) + try: + send_email(to_email, email_subject, email_message) + logger.info('Email sent.') + except Exception as error_exception: + logger.info('Failed to send email.') + logger.info(error_exception) + logger.info(str(traceback.extract_tb(error_exception.__traceback__))) + elif '@SOS' in sms or '@NOTICE' in sms: + sos_write(int_id(_rf_src), time(), sms) + elif '@REM SOS' == sms: + os.remove(emergency_sos_file) + logger.info('Removing SOS or Notice') + elif '@' in parse_sms[0][0:1] and 'M-' in parse_sms[1][0:2]: + message = re.sub('^@|.* M-|','',sms) + recipient = re.sub('@| M-.*','',sms) + mailbox_write(get_alias(int_id(_rf_src), subscriber_ids), int_id(_rf_src), time(), message, str(recipient).upper()) + elif '@REM MAIL' == sms: + mailbox_delete(_rf_src) + elif '@MH' in parse_sms[0]: + grid_square = re.sub('@MH ', '', sms) + if len(grid_square) < 6: + pass + else: + lat = decdeg2dms(mh.to_location(grid_square)[0]) + lon = decdeg2dms(mh.to_location(grid_square)[1]) + + if lon[0] < 0: + lon_dir = 'W' + if lon[0] > 0: + lon_dir = 'E' + if lat[0] < 0: + lat_dir = 'S' + if lat[0] > 0: + lat_dir = 'N' + #logger.info(lat) + #logger.info(lat_dir) + aprs_lat = str(str(re.sub('\..*|-', '', str(lat[0]))) + str(re.sub('\..*', '', str(lat[1])) + '.')).zfill(5) + ' ' + lat_dir + aprs_lon = str(str(re.sub('\..*|-', '', str(lon[0]))) + str(re.sub('\..*', '', str(lon[1])) + '.')).zfill(6) + ' ' + lon_dir + logger.info('Latitude: ' + str(aprs_lat)) + logger.info('Longitude: ' + str(aprs_lon)) + # 14FRS2013 simplified and moved settings retrieval + user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read()) + if int_id(_rf_src) not in user_settings: + ssid = str(user_ssid) + icon_table = '/' + icon_icon = '[' + comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src)) + else: + if user_settings[int_id(_rf_src)][1]['ssid'] == '': + ssid = user_ssid + if user_settings[int_id(_rf_src)][3]['comment'] == '': + comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src)) + if user_settings[int_id(_rf_src)][2]['icon'] == '': + icon_table = '/' + icon_icon = '[' + if user_settings[int_id(_rf_src)][2]['icon'] != '': + icon_table = user_settings[int_id(_rf_src)][2]['icon'][0] + icon_icon = user_settings[int_id(_rf_src)][2]['icon'][1] + if user_settings[int_id(_rf_src)][1]['ssid'] != '': + ssid = user_settings[int_id(_rf_src)][1]['ssid'] + if user_settings[int_id(_rf_src)][3]['comment'] != '': + comment = user_settings[int_id(_rf_src)][3]['comment'] + aprs_loc_packet = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid + '>APHBL3,TCPIP*:@' + str(datetime.datetime.utcnow().strftime("%H%M%Sh")) + str(aprs_lat) + icon_table + str(aprs_lon) + icon_icon + '/' + str(comment) + logger.info(aprs_loc_packet) + logger.info('User comment: ' + comment) + logger.info('User SSID: ' + ssid) + logger.info('User icon: ' + icon_table + icon_icon) + try: + aprslib.parse(aprs_loc_packet) + aprs_send(aprs_loc_packet) + dashboard_loc_write(str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid, aprs_lat, aprs_lon, time(), comment) + #logger.info('Sent manual position to APRS') + except Exception as error_exception: + logger.info('Exception. Not uploaded') + logger.info(error_exception) + logger.info(str(traceback.extract_tb(error_exception.__traceback__))) + packet_assembly = '' + + elif '?' in parse_sms[0][0:1]: + use_api = CONFIG['GPS_DATA']['USE_API'] + print(use_api) + if use_api == True: + auth_tokens = ast.literal_eval(os.popen('cat ' + auth_token_file).read()) + #access_systems = ast.literal_eval(os.popen('cat ' + access_systems_file).read()) + #authorized_users = ast.literal_eval(os.popen('cat ' + authorized_users_file).read()) + system = parse_sms[0][1:] + #print(access_systems[system]) + #print(authorized_users) + # Determin msg_xfer or app + if access_systems[system]['mode'] == 'msg_xfer': + s = ' ' + message_to_send = s.join(parse_sms[2:]) + dest_id = int(parse_sms[1]) + source_id = int_id(_rf_src) + send_msg_xfer(access_systems[system]['url'], access_systems[system]['user'], access_systems[system]['password'], message_to_send, source_id, dest_id) + if access_systems[system]['mode'] == 'app': + s = ' ' + message_to_send = s.join(parse_sms[1:]) + source_id = int_id(_rf_src) + send_app_request(access_systems[system]['url'], message_to_send, source_id) + + + if use_api == False: + if call_type == 'unit': + send_sms(False, int_id(_rf_src), 0000, 0000, 'unit', 'API not enabled. Contact server admin.') + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'API not enabled. Contact server admin.') + + elif '@' in parse_sms[0][0:1] and 'M-' not in parse_sms[1][0:2] or '@' not in parse_sms[0][1:]: + #Example SMS text: @ARMDS A-This is a test. + s = ' ' + aprs_dest = re.sub('@', '', parse_sms[0])#re.sub('@| A-.*','',sms) + aprs_msg = s.join(parse_sms[1:])#re.sub('^@|.* A-|','',sms) + logger.info(aprs_msg) + logger.info('APRS message to ' + aprs_dest.upper() + '. Message: ' + aprs_msg) + user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read()) + if int_id(_rf_src) in user_settings and user_settings[int_id(_rf_src)][1]['ssid'] != '': + ssid = user_settings[int_id(_rf_src)][1]['ssid'] + else: + ssid = user_ssid + try: + if user_settings[int_id(_rf_src)][5]['APRS'] == True: + aprs_msg_pkt = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + str(ssid) + '>APHBL3,TCPIP*::' + str(aprs_dest).ljust(9).upper() + ':' + aprs_msg[0:73] + logger.info(aprs_msg_pkt) + try: + aprslib.parse(aprs_msg_pkt) + aprs_send(aprs_msg_pkt) + #logger.info('Packet sent.') + except Exception as error_exception: + logger.info('Error uploading MSG packet.') + logger.info(error_exception) + logger.info(str(traceback.extract_tb(error_exception.__traceback__))) + else: + if call_type == 'unit': + send_sms(False, int_id(_rf_src), 0000, 0000, 'unit', 'APRS Messaging must be enabled. Send command "@APRS ON" or use dashboard to enable.') + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'APRS Messaging must be enabled. Send command "@APRS ON" or use dashboard to enable.') + + except Exception as e: + if call_type == 'unit': + send_sms(False, int_id(_rf_src), 0000, 0000, 'unit', 'APRS Messaging must be enabled. Send command "@APRS ON" or use dashboard to enable.') + if call_type == 'vcsbk': + send_sms(False, 9, 0000, 0000, 'group', 'APRS Messaging must be enabled. Send command "@APRS ON" or use dashboard to enable.') + + try: + if sms in cmd_list: + logger.info('Executing command/script.') + os.popen(cmd_list[sms]).read() + packet_assembly = '' + except Exception as error_exception: + logger.info('Exception. Command possibly not in list, or other error.') + logger.info(error_exception) + logger.info(str(traceback.extract_tb(error_exception.__traceback__))) + packet_assembly = '' + else: + pass + +# Module gobal varaibles + +##class DATA(): +##### DMR data function #### +def data_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data): + # Capture data headers + global n_packet_assembly, hdr_type + #logger.info(_dtype_vseq) + #logger.info(_call_type) + #logger.info(_frame_type) + logger.info(strftime('%H:%M:%S - %m/%d/%y')) + #logger.info('Special debug for developement:') + logger.info(ahex(bptc_decode(_data))) + #logger.info(_rf_src) + #logger.info((ba2num(bptc_decode(_data)[8:12]))) +################################################################3###### CHNGED ######### +## if int_id(_dst_id) == data_id: + #logger.info(type(_seq)) + if type(_seq) is bytes: + pckt_seq = int.from_bytes(_seq, 'big') + else: + pckt_seq = _seq + # Try to classify header + # UDT header has DPF of 0101, which is 5. + # If 5 is at position 3, then this should be a UDT header for MD-380 type radios. + # Coordinates are usually in the very next block after the header, we will discard the rest. + #logger.info(ahex(bptc_decode(_data)[0:10])) + if _call_type == call_type and header_ID(_data)[3] == '5' and ba2num(bptc_decode(_data)[69:72]) == 0 and ba2num(bptc_decode(_data)[8:12]) == 0 or (_call_type == 'vcsbk' and header_ID(_data)[3] == '5' and ba2num(bptc_decode(_data)[69:72]) == 0 and ba2num(bptc_decode(_data)[8:12]) == 0): + global udt_block + logger.info('MD-380 type UDT header detected. Very next packet should be location.') + hdr_type = '380' + if _dtype_vseq == 6 and hdr_type == '380' or _dtype_vseq == 'group' and hdr_type == '380': + udt_block = 1 + if _dtype_vseq == 7 and hdr_type == '380': + udt_block = udt_block - 1 + if udt_block == 0: + logger.info('MD-380 type packet. This should contain the GPS location.') + logger.info('Packet: ' + str(ahex(bptc_decode(_data)))) + if ba2num(bptc_decode(_data)[1:2]) == 1: + lat_dir = 'N' + if ba2num(bptc_decode(_data)[1:2]) == 0: + lat_dir = 'S' + if ba2num(bptc_decode(_data)[2:3]) == 1: + lon_dir = 'E' + if ba2num(bptc_decode(_data)[2:3]) == 0: + lon_dir = 'W' + lat_deg = ba2num(bptc_decode(_data)[11:18]) + lon_deg = ba2num(bptc_decode(_data)[38:46]) + lat_min = ba2num(bptc_decode(_data)[18:24]) + lon_min = ba2num(bptc_decode(_data)[46:52]) + lat_min_dec = str(ba2num(bptc_decode(_data)[24:38])).zfill(4) + lon_min_dec = str(ba2num(bptc_decode(_data)[52:66])).zfill(4) + # Old MD-380 coordinate format, keep here until new is confirmed working. + #aprs_lat = str(str(lat_deg) + str(lat_min) + '.' + str(lat_min_dec)[0:2]).zfill(7) + lat_dir + #aprs_lon = str(str(lon_deg) + str(lon_min) + '.' + str(lon_min_dec)[0:2]).zfill(8) + lon_dir + # Fix for MD-380 by G7HIF + aprs_lat = str(str(lat_deg) + str(lat_min).zfill(2) + '.' + str(lat_min_dec)[0:2]).zfill(7) + lat_dir + aprs_lon = str(str(lon_deg) + str(lon_min).zfill(2) + '.' + str(lon_min_dec)[0:2]).zfill(8) + lon_dir + + # Form APRS packet + #logger.info(aprs_loc_packet) + logger.info('Lat: ' + str(aprs_lat) + ' Lon: ' + str(aprs_lon)) + # 14FRS2013 simplified and moved settings retrieval + user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read()) + if int_id(_rf_src) not in user_settings: + ssid = str(user_ssid) + icon_table = '/' + icon_icon = '[' + comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src)) + else: + if user_settings[int_id(_rf_src)][1]['ssid'] == '': + ssid = user_ssid + if user_settings[int_id(_rf_src)][3]['comment'] == '': + comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src)) + if user_settings[int_id(_rf_src)][2]['icon'] == '': + icon_table = '/' + icon_icon = '[' + if user_settings[int_id(_rf_src)][2]['icon'] != '': + icon_table = user_settings[int_id(_rf_src)][2]['icon'][0] + icon_icon = user_settings[int_id(_rf_src)][2]['icon'][1] + if user_settings[int_id(_rf_src)][1]['ssid'] != '': + ssid = user_settings[int_id(_rf_src)][1]['ssid'] + if user_settings[int_id(_rf_src)][3]['comment'] != '': + comment = user_settings[int_id(_rf_src)][3]['comment'] + aprs_loc_packet = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid + '>APHBL3,TCPIP*:@' + str(datetime.datetime.utcnow().strftime("%H%M%Sh")) + str(aprs_lat) + icon_table + str(aprs_lon) + icon_icon + '/' + str(comment) + logger.info(aprs_loc_packet) + logger.info('User comment: ' + comment) + logger.info('User SSID: ' + ssid) + logger.info('User icon: ' + icon_table + icon_icon) + # Attempt to prevent malformed packets from being uploaded. + try: + aprslib.parse(aprs_loc_packet) + float(lat_deg) < 91 + float(lon_deg) < 121 + aprs_send(aprs_loc_packet) + dashboard_loc_write(str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid, aprs_lat, aprs_lon, time(), comment) + #logger.info('Sent APRS packet') + except Exception as error_exception: + logger.info('Error. Failed to send packet. Packet may be malformed.') + logger.info(error_exception) + logger.info(str(traceback.extract_tb(error_exception.__traceback__))) + udt_block = 1 + hdr_type = '' + else: + pass + #NMEA type packets for Anytone like radios. + #if _call_type == call_type or (_call_type == 'vcsbk' and pckt_seq > 3): #int.from_bytes(_seq, 'big') > 3 ): + # 14FRS2013 contributed improved header filtering, KF7EEL added conditions to allow both call types at the same time + if _call_type == call_type or (_call_type == 'vcsbk' and pckt_seq > 3 and call_type != 'unit') or (_call_type == 'group' and pckt_seq > 3 and call_type != 'unit') or (_call_type == 'group' and pckt_seq > 3 and call_type == 'both') or (_call_type == 'vcsbk' and pckt_seq > 3 and call_type == 'both') or (_call_type == 'unit' and pckt_seq > 3 and call_type == 'both'): #int.from_bytes(_seq, 'big') > 3 ): + global packet_assembly, btf + if _dtype_vseq == 6 or _dtype_vseq == 'group': + global btf, hdr_start + hdr_start = str(header_ID(_data)) + logger.info('Header from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + '. DMR ID: ' + str(int_id(_rf_src))) + logger.info(ahex(bptc_decode(_data))) + logger.info('Blocks to follow: ' + str(ba2num(bptc_decode(_data)[65:72]))) + btf = ba2num(bptc_decode(_data)[65:72]) + # Try resetting packet_assembly + packet_assembly = '' + # Data blocks at 1/2 rate, see https://github.com/g4klx/MMDVM/blob/master/DMRDefines.h for data types. _dtype_seq defined here also + if _dtype_vseq == 7: + btf = btf - 1 + logger.info('Block #: ' + str(btf)) + #logger.info(_seq) + logger.info('Data block from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + '. DMR ID: ' + str(int_id(_rf_src)) + '. Destination: ' + str(int_id(_dst_id))) + logger.info(ahex(bptc_decode(_data))) + if _seq == 0: + n_packet_assembly = 0 + packet_assembly = '' + + #if btf < btf + 1: + # 14FRS2013 removed condition, works great! + n_packet_assembly = n_packet_assembly + 1 + packet_assembly = packet_assembly + str(bptc_decode(_data)) #str((decode_full_lc(b_packet)).strip('bitarray(')) + # Use block 0 as trigger. $GPRMC must also be in string to indicate NMEA. + # This triggers the APRS upload + if btf == 0: + final_packet = str(bitarray(re.sub("\)|\(|bitarray|'", '', packet_assembly)).tobytes().decode('utf-8', 'ignore')) + sms_hex = str(ba2hx(bitarray(re.sub("\)|\(|bitarray|'", '', packet_assembly)))) + sms_hex_string = re.sub("b'|'", '', str(sms_hex)) + #NMEA GPS sentence + if '$GPRMC' in final_packet or '$GNRMC' in final_packet: + logger.info(final_packet + '\n') + # Eliminate excess bytes based on NMEA type + # GPRMC + if 'GPRMC' in final_packet: + logger.info('GPRMC location') + #nmea_parse = re.sub('A\*.*|.*\$', '', str(final_packet)) + nmea_parse = re.sub('A\*.*|.*\$|\n.*', '', str(final_packet)) + # GNRMC + if 'GNRMC' in final_packet: + logger.info('GNRMC location') + nmea_parse = re.sub('.*\$|\n.*|V\*.*', '', final_packet) + loc = pynmea2.parse(nmea_parse, check=False) + logger.info('Latitude: ' + str(loc.lat) + str(loc.lat_dir) + ' Longitude: ' + str(loc.lon) + str(loc.lon_dir) + ' Direction: ' + str(loc.true_course) + ' Speed: ' + str(loc.spd_over_grnd) + '\n') + try: + # Begin APRS format and upload + # Disable opening file for reading to reduce "collision" or reading and writing at same time. + # 14FRS2013 simplified and moved settings retrieval + user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read()) + if int_id(_rf_src) not in user_settings: + ssid = str(user_ssid) + icon_table = '/' + icon_icon = '[' + comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src)) + else: + if user_settings[int_id(_rf_src)][1]['ssid'] == '': + ssid = user_ssid + if user_settings[int_id(_rf_src)][3]['comment'] == '': + comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src)) + if user_settings[int_id(_rf_src)][2]['icon'] == '': + icon_table = '/' + icon_icon = '[' + if user_settings[int_id(_rf_src)][2]['icon'] != '': + icon_table = user_settings[int_id(_rf_src)][2]['icon'][0] + icon_icon = user_settings[int_id(_rf_src)][2]['icon'][1] + if user_settings[int_id(_rf_src)][1]['ssid'] != '': + ssid = user_settings[int_id(_rf_src)][1]['ssid'] + if user_settings[int_id(_rf_src)][3]['comment'] != '': + comment = user_settings[int_id(_rf_src)][3]['comment'] + aprs_loc_packet = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid + '>APHBL3,TCPIP*:@' + str(datetime.datetime.utcnow().strftime("%H%M%Sh")) + str(loc.lat[0:7]) + str(loc.lat_dir) + icon_table + str(loc.lon[0:8]) + str(loc.lon_dir) + icon_icon + str(round(loc.true_course)).zfill(3) + '/' + str(round(loc.spd_over_grnd)).zfill(3) + '/' + str(comment) + logger.info(aprs_loc_packet) + logger.info('User comment: ' + comment) + logger.info('User SSID: ' + ssid) + logger.info('User icon: ' + icon_table + icon_icon) + except Exception as error_exception: + logger.info('Error or user settings file not found, proceeding with default settings.') + aprs_loc_packet = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + str(user_ssid) + '>APHBL3,TCPIP*:@' + str(datetime.datetime.utcnow().strftime("%H%M%Sh")) + str(loc.lat[0:7]) + str(loc.lat_dir) + '/' + str(loc.lon[0:8]) + str(loc.lon_dir) + '[' + str(round(loc.true_course)).zfill(3) + '/' + str(round(loc.spd_over_grnd)).zfill(3) + '/' + aprs_comment + ' DMR ID: ' + str(int_id(_rf_src)) + logger.info(error_exception) + logger.info(str(traceback.extract_tb(error_exception.__traceback__))) + try: + # Try parse of APRS packet. If it fails, it will not upload to APRS-IS + aprslib.parse(aprs_loc_packet) + # Float values of lat and lon. Anything that is not a number will cause it to fail. + float(loc.lat) + float(loc.lon) + aprs_send(aprs_loc_packet) + dashboard_loc_write(str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid, str(loc.lat[0:7]) + str(loc.lat_dir), str(loc.lon[0:8]) + str(loc.lon_dir), time(), comment) + except Exception as error_exception: + logger.info('Failed to parse packet. Packet may be deformed. Not uploaded.') + logger.info(error_exception) + logger.info(str(traceback.extract_tb(error_exception.__traceback__))) + # Get callsign based on DMR ID + # End APRS-IS upload + # Assume this is an SMS message + elif '$GPRMC' not in final_packet or '$GNRMC' not in final_packet: + +#### # Motorola type SMS header +## if '824a' in hdr_start or '024a' in hdr_start: +## logger.info('\nMotorola type SMS') +## sms = codecs.decode(bytes.fromhex(''.join(sms_hex[74:-8].split('00'))), 'utf-8') +## logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(sms) + '\n') +## process_sms(_rf_src, sms) +## packet_assembly = '' +## # ETSI? type SMS header +## elif '0244' in hdr_start or '8244' in hdr_start: +## logger.info('ETSI? type SMS') +## sms = codecs.decode(bytes.fromhex(''.join(sms_hex[64:-8].split('00'))), 'utf-8') +## logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(sms) + '\n') +## #logger.info(final_packet) +## #logger.info(sms_hex[64:-8]) +## process_sms(_rf_src, sms) +## packet_assembly = '' +#### +## else: + logger.info('\nSMS detected. Attempting to parse.') + #logger.info(final_packet) + logger.info(sms_hex) +## logger.info(type(sms_hex)) + logger.info('Attempting to find command...') +## sms = codecs.decode(bytes.fromhex(''.join(sms_hex[:-8].split('00'))), 'utf-8', 'ignore') + sms = codecs.decode(bytes.fromhex(''.join(sms_hex_string[:-8].split('00'))), 'utf-8', 'ignore') + msg_found = re.sub('.*\n', '', sms) + logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(msg_found) + '\n') +## process_sms(_rf_src, msg_found, _call_type) + #packet_assembly = '' + pass + #logger.info(bitarray(re.sub("\)|\(|bitarray|'", '', str(bptc_decode(_data)).tobytes().decode('utf-8', 'ignore')))) + #logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(sms) + '\n') + # Reset the packet assembly to prevent old data from returning. + # 14FRS2013 moved variable reset + hdr_start = '' + n_packet_assembly = 0 + packet_assembly = '' + btf = 0 + #logger.info(_seq) + #packet_assembly = '' #logger.info(_dtype_vseq) + #logger.info(ahex(bptc_decode(_data)).decode('utf-8', 'ignore')) + #logger.info(bitarray(re.sub("\)|\(|bitarray|'", '', str(bptc_decode(_data)).tobytes().decode('utf-8', 'ignore')))) + + +###### + +call_type = 'unit' + +class OBP(OPENBRIDGE): + + def __init__(self, _name, _config, _report): + OPENBRIDGE.__init__(self, _name, _config, _report) + + + def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data): + print('OBP RCVD') + data_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data) +## pass + + +class HBP(HBSYSTEM): + + def __init__(self, _name, _config, _report): + HBSYSTEM.__init__(self, _name, _config, _report) + + def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data): + print('MMDVM RCVD') + data_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data) +## pass + + + +#************************************************ +# MAIN PROGRAM LOOP STARTS HERE +#************************************************ + +if __name__ == '__main__': + + import argparse + import sys + import os + import signal + + # Change the current directory to the location of the application + os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) + + # CLI argument parser - handles picking up the config file from the command line, and sending a "help" message + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', action='store', dest='CONFIG_FILE', help='/full/path/to/config.file (usually hblink.cfg)') + parser.add_argument('-l', '--logging', action='store', dest='LOG_LEVEL', help='Override config file logging level.') + cli_args = parser.parse_args() + + # Ensure we have a path for the config file, if one wasn't specified, then use the default (top of file) + if not cli_args.CONFIG_FILE: + cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/hblink.cfg' + + # Call the external routine to build the configuration dictionary + CONFIG = config.build_config(cli_args.CONFIG_FILE) + + # Start the system logger + if cli_args.LOG_LEVEL: + CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL + logger = log.config_logging(CONFIG['LOGGER']) + logger.info('\n\nCopyright (c) 2013, 2014, 2015, 2016, 2018\n\tThe Regents of the K0USY Group. All rights reserved.\n') + logger.debug('(GLOBAL) Logging system started, anything from here on gets logged') + + # Set up the signal handler + def sig_handler(_signal, _frame): + logger.info('(GLOBAL) SHUTDOWN: CONFBRIDGE IS TERMINATING WITH SIGNAL %s', str(_signal)) + hblink_handler(_signal, _frame) + logger.info('(GLOBAL) SHUTDOWN: ALL SYSTEM HANDLERS EXECUTED - STOPPING REACTOR') + reactor.stop() + + # Set signal handers so that we can gracefully exit if need be + for sig in [signal.SIGINT, signal.SIGTERM]: + signal.signal(sig, sig_handler) + + # Create the name-number mapping dictionaries + peer_ids, subscriber_ids, talkgroup_ids = mk_aliases(CONFIG) + + # INITIALIZE THE REPORTING LOOP + if CONFIG['REPORTS']['REPORT']: + report_server = config_reports(CONFIG, reportFactory) + else: + report_server = None + logger.info('(REPORT) TCP Socket reporting not configured') + + # HBlink instance creation + logger.info('(GLOBAL) HBlink \'bridge.py\' -- SYSTEM STARTING...') + for system in CONFIG['SYSTEMS']: + if CONFIG['SYSTEMS'][system]['ENABLED']: + if CONFIG['SYSTEMS'][system]['MODE'] == 'OPENBRIDGE': + systems[system] = OBP(system, CONFIG, report_server) + else: + systems[system] = HBP(system, CONFIG, report_server) + reactor.listenUDP(CONFIG['SYSTEMS'][system]['PORT'], systems[system], interface=CONFIG['SYSTEMS'][system]['IP']) + logger.debug('(GLOBAL) %s instance created: %s, %s', CONFIG['SYSTEMS'][system]['MODE'], system, systems[system]) + + def loopingErrHandle(failure): + logger.error('(GLOBAL) STOPPING REACTOR TO AVOID MEMORY LEAK: Unhandled error in timed loop.\n %s', failure) + reactor.stop() + + + reactor.run()