From f937581e4947d9915b7251559e8d98814e4bce3b Mon Sep 17 00:00:00 2001 From: KF7EEL Date: Mon, 26 Apr 2021 15:54:36 -0700 Subject: [PATCH] prepare for OPENBRIDGE support --- full_bridge.py | 254 +++++++++++++++++++++++++++++++++++- rules_full_bridge-SAMPLE.py | 5 +- 2 files changed, 255 insertions(+), 4 deletions(-) diff --git a/full_bridge.py b/full_bridge.py index f39e87f..9f8a19a 100644 --- a/full_bridge.py +++ b/full_bridge.py @@ -938,7 +938,7 @@ time_20 = 630720000 # Build a UNIT_MAP based on values in STATIC_MAP. try: for i in STATIC_UNIT: - UNIT_MAP[bytes_3(i[0])] = i[1], time() + time_20, i[2] + UNIT_MAP[bytes_3(i[0])] = i[1], time() + time_20, i[2] # If empty, return empty dictionary except: UNIT_MAP = {} @@ -1097,7 +1097,7 @@ class routerOBP(OPENBRIDGE): # list of self._targets for unit (subscriber, private) calls self._targets = [] - + # (self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data) def group_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _frame_type, _dtype_vseq, _stream_id, _data): pkt_time = time() dmrpkt = _data[20:53] @@ -1447,6 +1447,256 @@ class routerOBP(OPENBRIDGE): if CONFIG['REPORTS']['REPORT']: self._report.send_bridgeEvent('UNIT VOICE,END,RX,{},{},{},{},{},{},{:.2f}'.format(self._system, int_id(_stream_id), int_id(_peer_id), int_id(_rf_src), _slot, int_id(_dst_id), call_duration).encode(encoding='utf-8', errors='ignore')) +##### 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) + #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')))) + + +###### + def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data): diff --git a/rules_full_bridge-SAMPLE.py b/rules_full_bridge-SAMPLE.py index 5db794d..734caef 100755 --- a/rules_full_bridge-SAMPLE.py +++ b/rules_full_bridge-SAMPLE.py @@ -62,10 +62,11 @@ UNIT_TIME = 1 ''' Input the DMR ID and SYSTEM of a subscriber that you would like to have always have private calls routed. -This will not flood all systems. +This will not flood all systems. The last item in the entry is the timeslot. It has no effect on STATIC_UNIT. It +is only there to prevent an error in another function. ''' STATIC_UNIT = [ -# [ 9099, 'MASTER-1'] +# [ 9099, 'MASTER-1', 2] ] '''