From 7ea977a470e847233cb4f51d29803af1cf9021d0 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 17 Sep 2023 09:28:40 -0700 Subject: [PATCH] Update sat2aprs.py --- sat2aprs.py | 283 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 239 insertions(+), 44 deletions(-) diff --git a/sat2aprs.py b/sat2aprs.py index b03703f..c03b4a3 100644 --- a/sat2aprs.py +++ b/sat2aprs.py @@ -2,38 +2,157 @@ import re import socket from flask import Flask, request from twilio.twiml.messaging_response import MessagingResponse +import time +import threading app = Flask(__name__) -def decimal_to_ddmm(value): +# APRS-IS login info +serverHost = 'theconnectdesk.com' +serverPort = 14580 +aprsUser = 'CALLSIGN' # Login call +aprsCall = None # Default RF Call +aprsPass = 'PASS' +text = 'Satellite Beacon' +symbol_chart = '\\' # Replace with your desired symbol chart (e.g., '/', '\\', '!', etc.) +symbol = 'S' # Replace with your desired symbol (e.g., 'S', 'O', 'P', etc.) + + +aprs_sock = None # Define the socket outside the try-except block + +def load_alias_map_from_file(): + alias_map = {} + print("Opening File") + with open('/root/app/map.txt', 'r') as file: + print("File Opened") + for line in file: + parts = line.strip().split(',') + if len(parts) == 2: + phone_number, aprs_callsign = parts + alias_map[phone_number] = aprs_callsign + return alias_map + +def update_alias_map_file(alias_map): + map_file_path = '/root/app/map.txt' + + # Read the existing data from the file + with open(map_file_path, 'r') as file: + existing_data = file.readlines() + + # Update or add entries in the existing data + updated_data = [] + for line in existing_data: + parts = line.strip().split(',') + if len(parts) == 2: + phone_number, aprs_callsign = parts + updated_call = alias_map.get(phone_number, aprs_callsign) + updated_line = "{},{}\n".format(phone_number, updated_call) + updated_data.append(updated_line) + # Add new entries for numbers not already in the alias map + for phone_number, aprs_callsign in alias_map.items(): + if not any(line.startswith(f"{phone_number},") for line in updated_data): + updated_data.append("{},{}\n".format(phone_number, aprs_callsign)) + + # Write the updated data back to the file + with open(map_file_path, 'w') as file: + file.writelines(updated_data) + + print("Wrote to file") + + +def connect_to_aprs_server(): + global aprs_sock + while True: + try: + # Create and connect the APRS-IS socket + aprs_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + aprs_sock.connect((serverHost, serverPort)) + login_str = 'user {} pass {} vers SatGate 0.1b\r\n'.format(aprsUser, aprsPass) + aprs_sock.send(login_str.encode()) + response = aprs_sock.recv(1024).decode('utf-8') + print('APRS-IS login response:', response) + return + except Exception as e: + print('Error connecting to APRS-IS:', e) + time.sleep(1) # Wait for a while before attempting reconnection + + +# Function to send a keepalive APRS packet +def send_keepalive_packet(): + global aprs_sock # Declare aprs_sock as global + while True: + if aprs_sock: + keepalive_packet = '#\r\n' + try: + aprs_sock.send(keepalive_packet.encode()) + print("Keepalive Sent") + except Exception as e: + print("Error sending keepalive:", e) + aprs_sock.close() + aprs_sock = None # Close and reset the socket + connect_to_aprs_server() # Attempt reconnection + else: + connect_to_aprs_server() # If socket is not active, attempt reconnection + time.sleep(30) + +# Function to send a beacon APRS packet +def send_beacon_packet(): + global aprs_sock # Declare aprs_sock as global + while True: + if aprs_sock: + beacon_packet = 'SATGTE>NA7Q:!4610.49N\\12334.72WSMotorola Defy Satellite Gateway - NA7Q\r\n' + try: + aprs_sock.send(beacon_packet.encode()) + print("Beacon Sent") + except Exception as e: + print("Error sending beacon:", e) + aprs_sock.close() + aprs_sock = None # Close and reset the socket + connect_to_aprs_server() # Attempt reconnection + else: + connect_to_aprs_server() # If socket is not active, attempt reconnection + time.sleep(600) + + +def latitude_to_ddmm(value): degrees = int(value) minutes_decimal = (value - degrees) * 60 abs_minutes = abs(minutes_decimal) minutes = round(abs_minutes, 2) - return "{:02d}{:05.2f}".format(abs(degrees), abs(minutes)) -# APRS-IS login info -serverHost = 'rotate.aprs2.net' -serverPort = 14580 -aprsUser = 'CALL' -aprsPass = 'PASS' -text = 'Sat2APRS' - -# Create and connect the APRS-IS socket -aprs_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -try: - aprs_sock.connect((serverHost, serverPort)) - login_str = 'user {} pass {} vers Sat2APRS 0.1b\r\n'.format(aprsUser, aprsPass) - aprs_sock.send(login_str.encode()) - response = aprs_sock.recv(1024).decode('utf-8') - print('APRS-IS login response:', response) -except Exception as e: - print('Error connecting to APRS-IS:', e) + # Ensure that degrees and minutes have leading zeros + degrees_str = "{:02d}".format(abs(degrees)) + minutes_str = "{:05.2f}".format(abs(minutes)) + + # Ensure that the latitude always has at least 4 digits before the decimal point + lat_ddmm = "{}{}".format(degrees_str, minutes_str) + while len(lat_ddmm.split('.')[0]) < 4: + lat_ddmm = "0" + lat_ddmm + + return lat_ddmm + +def longitude_to_ddmm(value): + degrees = int(value) + minutes_decimal = (value - degrees) * 60 + abs_minutes = abs(minutes_decimal) + minutes = round(abs_minutes, 2) + + # Ensure that degrees and minutes have leading zeros + degrees_str = "{:03d}".format(abs(degrees)) + minutes_str = "{:05.2f}".format(abs(minutes)) + + # Ensure that the longitude always has at least 5 digits before the decimal point + lon_ddmm = "{}{}".format(degrees_str, minutes_str) + while len(lon_ddmm.split('.')[0]) < 5: + lon_ddmm = "0" + lon_ddmm + + return lon_ddmm def send_aprs_packet(callsign, lat, lon, text): - lat_ddmm = decimal_to_ddmm(lat) - lon_ddmm = decimal_to_ddmm(lon) - aprs_packet = '{}>APRS:!{}{}\{}{}S{}\r\n'.format(callsign, lat_ddmm, "N" if lat >= 0 else "S", lon_ddmm, "E" if lon >= 0 else "W", text) + lat_ddmm = latitude_to_ddmm(lat) + lon_ddmm = longitude_to_ddmm(lon) + aprs_packet = '{}>APRS:!{}{}{}{}{}{}{}\r\n'.format(callsign, lat_ddmm, "N" if lat >= 0 else "S",symbol_chart, lon_ddmm, "E" if lon >= 0 else "W", symbol, text) + try: aprs_sock.sendall(aprs_packet.encode()) @@ -41,9 +160,10 @@ def send_aprs_packet(callsign, lat, lon, text): except Exception as e: print('Error sending APRS packet:', e) - @app.route('/sms', methods=['POST']) def webhook(): + global aprsCall # Declare aprsCall as a global variable + incoming_sms = request.form.get('Body', '') latitude = None longitude = None @@ -51,39 +171,114 @@ def webhook(): # Log incoming message print('Received Message:', incoming_sms) + + # Extract the sender's phone number from the Twilio request headers + from_number_header = request.form.get('From', '').lstrip('+') + + # Load the alias map from the file + alias_map = load_alias_map_from_file() + + # Extract the phone number in the format "From: +15032985265" + #from_number_match = re.search(r'From:\s*\+(\d+)', incoming_sms) + #from_number = from_number_match.group(1).lstrip('+') if from_number_match else from_number_header + #print('Sender\'s Phone Number:', from_number) + + from_number_match = re.search(r'From:\s*\+(\d+)', incoming_sms) + from_number = from_number_match.group(1).lstrip('+') if from_number_match else from_number_header + print('from_number_match:', from_number_match) + print('from_number_header:', from_number_header) + print('Final Sender\'s Phone Number:', from_number) + + + + # Look up the APRS callsign in the alias map based on the phone number + aprsCall = alias_map.get(from_number, None) + print('APRS Callsign for this number:', aprsCall) - # Search for latitude, longitude, and comment in the message using regular expressions - lat_long_comment_match = re.search(r'(-?\d+\.\d+),(-?\d+\.\d+)\s*([^.\"\n]+)', incoming_sms) - if lat_long_comment_match: - latitude = float(lat_long_comment_match.group(1)) - longitude = float(lat_long_comment_match.group(2)) - comment = lat_long_comment_match.group(3).strip() + # Extract the APRS call sign following the "@" symbol (ignores double quotes) + at_sign_match = re.search(r'@([^"]+)', incoming_sms) + if at_sign_match: + new_aprsCall = at_sign_match.group(1).strip().upper() # Extract and convert aprsCall to uppercase + print('Updated aprsCall:', new_aprsCall) - # If the comment is "undefined", use the default comment 'text' - if comment.lower() == "undefined": - comment = text + # Update the alias map if the new aprsCall is not in it + for number, call in alias_map.items(): + if call == aprsCall: + alias_map[number] = new_aprsCall + print('Updated alias map:', alias_map) + + # If the from_number is not in the alias map, add it with the new APRS call + if from_number not in alias_map: + alias_map[from_number] = new_aprsCall + print('Added new entry to alias map:', alias_map) + + # Set the new aprsCall + aprsCall = new_aprsCall + + # Update the map file with the new data + update_alias_map_file(alias_map) - print('Detected Lat Long: ({}, {})'.format(latitude, longitude)) - print('Detected Comment:', comment) + if from_number: + print('Found From Number:', from_number_header) - # Convert latitude and longitude to DDMM.MM format without dashes - lat_ddmm = decimal_to_ddmm(latitude) - long_ddmm = decimal_to_ddmm(longitude) + # Process the message as needed + # Example: Extract latitude, longitude, and comment from the message + #lat_long_comment_match = re.search(r'(-?\d+\.\d+),(-?\d+\.\d+)\s*([^"]+)', incoming_sms) + lat_long_comment_match = re.search(r'(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*([^"]+)', incoming_sms) - print('Converted Lat Long to DDMM.MM: {}, {}'.format(lat_ddmm, long_ddmm)) - print('{}/{}'.format(lat_ddmm, long_ddmm)) - print('APRS Comment:', comment) - # Send APRS packet - send_aprs_packet(aprsUser, latitude, longitude, comment) + if lat_long_comment_match: + latitude = float(lat_long_comment_match.group(1)) + longitude = float(lat_long_comment_match.group(2)) + comment = lat_long_comment_match.group(3).strip() + + # If the comment is "undefined," use the default comment 'text' + if comment.lower() == "undefined": + comment = text + + print('Detected Lat Long: ({}, {})'.format(latitude, longitude)) + print('Detected Comment:', comment) + + # Convert latitude and longitude to DDMM.MM format without dashes + lat_ddmm = latitude_to_ddmm(latitude) + long_ddmm = longitude_to_ddmm(longitude) + + print('Converted Lat Long to DDMM.MM: {}, {}'.format(lat_ddmm, long_ddmm)) + print('{}/{}'.format(lat_ddmm, long_ddmm)) + print('APRS Comment:', comment) + + # Send APRS packet using the mapped callsign 'aprsCall' only if it's found in the alias map + if aprsCall in alias_map.values(): + send_aprs_packet(aprsCall, latitude, longitude, comment) + else: + print("APRS Callsign not found in alias map. Skipping APRS packet.") + + else: + print('No Lat Long and Comment detected.') else: - print('No Lat Long and Comment detected.') + print('No From Number detected.') response = MessagingResponse() return str(response) if __name__ == '__main__': - app.run(host='0.0.0.0', port=5001, debug=True) + connect_to_aprs_server() # Initial connection + + + # Start a thread to send keepalive packets + print("Start thread for keepalive") + keepalive_thread = threading.Thread(target=send_keepalive_packet) + keepalive_thread.daemon = True + keepalive_thread.start() + + # Start a thread to send beacon packets + print("Start thread for beacons") + beacon_thread = threading.Thread(target=send_beacon_packet) + beacon_thread.daemon = True + beacon_thread.start() + + print("run host") + app.run(host='0.0.0.0', port=5000, debug=False)