import socket import re from flask import Flask, request, jsonify from twilio.rest import Client import time import threading app = Flask(__name__) # Set this variable to enable/disable private mode private_mode = False # Change this value as needed # List of callsigns allowed to send messages if private_mode is TRUE. Accepts ALL SSIDs for a CALLSIGN listed. allowed_callsigns = ['CALLSIGN0', 'CALLSIGN1', 'CALLSIGN2'] # Add more callsigns as needed # Twilio credentials TWILIO_ACCOUNT_SID = 'SID' TWILIO_AUTH_TOKEN = 'TOKEN' TWILIO_PHONE_NUMBER = '+NUMBER' # Your Twilio phone number # APRS credentials APRS_CALLSIGN = 'CALL' APRS_PASSCODE = 'PASS' APRS_SERVER = 'rotate.aprs2.net' APRS_PORT = 14580 # Initialize the socket aprs_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Declare socket_ready as a global variable socket_ready = False # Dictionary to store the last received APRS message ID for each user user_last_message_id = {} processed_message_ids = set() # Outside the main loop, initialize a dictionary to store message history received_aprs_messages = {} received_acks = {} RETRY_INTERVAL = 90 # Adjust this as needed MAX_RETRIES = 4 # Adjust this as needed def send_ack_message(sender, message_id): ack_message = 'ack{}'.format(message_id) sender_length = len(sender) spaces_after_sender = ' ' * max(0, 9 - sender_length) ack_packet_format = '{}>APRS::{}{}:{}\r\n'.format(APRS_CALLSIGN, sender, spaces_after_sender, ack_message) ack_packet = ack_packet_format.encode() aprs_socket.sendall(ack_packet) print("Sent ACK to {}: {}".format(sender, ack_message)) print("Outgoing ACK packet: {}".format(ack_packet.decode())) def send_rej_message(sender, message_id): rej_message = 'rej{}'.format(message_id) sender_length = len(sender) spaces_after_sender = ' ' * max(0, 9 - sender_length) rej_packet_format = '{}>APRS::{}{}:{}\r\n'.format(APRS_CALLSIGN, sender, spaces_after_sender, rej_message) rej_packet = rej_packet_format.encode() aprs_socket.sendall(rej_packet) print("Sent REJ to {}: {}".format(sender, rej_message)) print("Outgoing REJ packet: {}".format(rej_packet.decode())) def send_sms(twilio_phone_number, to_phone_number, from_callsign, body_message): # Initialize the Twilio client client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) try: # Send SMS using the Twilio API message = client.messages.create( body="@{} {}".format(from_callsign, body_message), from_=twilio_phone_number, to=to_phone_number ) print("SMS sent successfully.") print("SMS SID:", message.sid) except Exception as e: print("Error sending SMS:", str(e)) def format_aprs_packet(callsign, message): sender_length = len(callsign) spaces_after_sender = ' ' * max(0, 9 - sender_length) aprs_packet_format = '{}>APRS::{}{}:{}\r\n'.format(APRS_CALLSIGN, callsign, spaces_after_sender, message) return aprs_packet_format # Dictionary to store the mapping of aliases (callsigns) to phone numbers alias_map = { 'alias1': '123456789', # Replace 'alias1' with the desired alias and '1234567890' with the corresponding phone number. 'alias2': '9876543210', # Add more entries as needed for other aliases and phone numbers. # Add more entries as needed. } def find_phone_number_from_alias(alias): return alias_map.get(alias.lower()) # Create a new dictionary to store the reverse mapping of phone numbers to aliases reverse_alias_map = {v: k for k, v in alias_map.items()} @app.route('/sms', methods=['POST']) def receive_sms(): # Parse the incoming SMS message data = request.form from_phone_number = data['From'] body_message = data['Body'] # If the message is in the correct format, the function extracts the callsign and APRS message content from the SMS body. if body_message.startswith('@'): parts = body_message.split(' ', 1) if len(parts) == 2: # Extract the 10-digit phone number from the sender's phone number sender_phone_number = from_phone_number[-10:] callsign = parts[0][1:].upper() #Convert to UPPERCASE aprs_message = parts[1] # Get the last APRS message ID sent to this user last_message_id = user_last_message_id.get(from_phone_number, 0) # Increment the message ID to avoid duplicate messages last_message_id += 1 user_last_message_id[from_phone_number] = last_message_id # Use the reverse alias mapping to check if the sender's phone number has an associated alias alias = reverse_alias_map.get(sender_phone_number.lower()) if alias: sender_phone_number = alias # If an alias is found, use it; otherwise, use the phone number itself as the alias if alias: sender_phone_number = alias # Format the APRS packet and send it to the APRS server aprs_packet = format_aprs_packet(callsign, "@{} {}".format(sender_phone_number, aprs_message + "{" + str(last_message_id))) aprs_socket.sendall(aprs_packet.encode()) #print(user_last_message_id) #print(last_message_id) print("Sent APRS message to {}: {}".format(callsign, aprs_message)) print("Outgoing APRS packet: {}".format(aprs_packet.strip())) time.sleep(5) # Sleeping here allows time for incoming ack before retry # Retry sending the message if ACK is not received retry_count = 0 ack_received = False # Flag to track whether ACK is received while retry_count < MAX_RETRIES and not ack_received: if str(last_message_id) in received_acks.get(callsign, set()): print("Message ACK received. No further retries needed.") ack_received = True # Reset retry count and remove the ACK ID retry_count = 0 received_acks.get(callsign, set()).discard(str(last_message_id)) else: print("ACK not received. Retrying in {} seconds.".format(RETRY_INTERVAL)) aprs_socket.sendall(aprs_packet.encode()) retry_count += 1 time.sleep(RETRY_INTERVAL) # Pause for the defined interval if ack_received: print("ACK received during retries. No further retries needed.") elif retry_count >= MAX_RETRIES: print("Max retries reached. No ACK received for the message.") return jsonify({'status': 'success'}) else: return jsonify({'status': 'error', 'message': 'Invalid SMS format'}) else: return jsonify({'status': 'error', 'message': 'SMS does not start with "@" symbol'}) def receive_aprs_messages(): global socket_ready # Declare that you're using the global variable # Connect to the APRS server aprs_socket.connect((APRS_SERVER, APRS_PORT)) print("Connected to APRS server with callsign: {}".format(APRS_CALLSIGN)) # Send login information with APRS callsign and passcode login_str = 'user {} pass {} vers SMS-Gateway 0.1b\r\n'.format(APRS_CALLSIGN, APRS_PASSCODE) aprs_socket.sendall(login_str.encode()) print("Sent login information.") # Set the socket_ready flag to indicate that the socket is ready for keepalives socket_ready = True buffer = "" try: while True: data = aprs_socket.recv(1024) print (data) if not data: break # Add received data to the buffer buffer += data.decode() # Split buffer into lines lines = buffer.split('\n') # Process each line for line in lines[:-1]: if line.startswith('#'): continue # Process APRS message print("Received raw APRS packet: {}".format(line.strip())) parts = line.strip().split(':') if len(parts) >= 2: from_callsign = parts[0].split('>')[0].strip() message_text = ':'.join(parts[1:]).strip() # Extract and process ACK ID if present if "ack" in message_text: parts = message_text.split("ack", 1) if len(parts) == 2 and parts[1].isdigit(): ack_id = parts[1] process_ack_id(from_callsign, ack_id) # End RXd ACK ID for MSG Retries # Check if the message contains "{" if "{" in message_text: message_id = message_text.split('{')[1].strip('}') # Remove the first 11 characters from the message to exclude the "Callsign :" prefix verbose_message = message_text[11:].split('{')[0].strip() # Inside the receive_aprs_messages function if private_mode: # Use regular expression to match main callsign and accept all SSIDs callsign_pattern = re.compile(r'^({})(-\d+)?$'.format('|'.join(map(re.escape, allowed_callsigns)))) if not callsign_pattern.match(from_callsign): print("Unauthorized sender:", from_callsign) send_rej_message(from_callsign, message_id) continue # Skip processing messages from unauthorized senders # Display verbose message content print("From: {}".format(from_callsign)) print("Message: {}".format(verbose_message)) print("Message ID: {}".format(message_id)) print(user_last_message_id) # Check if the verbose message contains the desired format with a number or an alias pattern = r'@(\d{10}|\w+) (.+)' match = re.match(pattern, verbose_message) # Send ACK send_ack_message(from_callsign, message_id) if match: recipient = match.group(1) aprs_message = match.group(2) # Check if the recipient is a 10-digit number or an alias if recipient.isdigit(): # Recipient is a 10-digit number phone_number = recipient else: # Recipient is an alias phone_number = find_phone_number_from_alias(recipient) if phone_number: # Get the last APRS message ID sent to this user last_message_id = user_last_message_id.get(from_callsign, 0) last_message_id += 1 user_last_message_id[from_callsign] = last_message_id else: print("Recipient not found in alias map or not a 10-digit number: {}".format(recipient)) # Check for duplicate messages if (aprs_message, message_id) in received_aprs_messages.get(from_callsign, set()): print("Duplicate message detected. Skipping SMS sending.") send_ack_message(from_callsign, message_id) else: # Mark the message as received received_aprs_messages.setdefault(from_callsign, set()).add((aprs_message, message_id)) # Send SMS send_sms(TWILIO_PHONE_NUMBER, phone_number, from_callsign, aprs_message) # Add this line to mark the message ID as processed processed_message_ids.add(message_id) # Extract and process ACK ID if present if message_text.startswith("ack"): ack_id = message_text[3:] # Remove the "ack" prefix process_ack_id(from_callsign, ack_id) pass # Send ACK # The last line might be an incomplete packet, so keep it in the buffer buffer = lines[-1] except Exception as e: print("Error receiving APRS messages: {}".format(e)) finally: # Close the socket connection when done aprs_socket.close() #Implementation for ack check with Message Retries #TODO def process_ack_id(from_callsign, ack_id): print("Received ACK from {}: {}".format(from_callsign, ack_id)) received_acks.setdefault(from_callsign, set()).add(ack_id) # Update your records or take any other necessary action def send_keepalive(): global socket_ready # Declare that you're using the global variable while True: try: if socket_ready: # Send a keepalive packet to the APRS server keepalive_packet = '#\r\n' aprs_socket.sendall(keepalive_packet.encode()) print("Sent keepalive packet.") except Exception as e: print("Error sending keepalive:", str(e)) time.sleep(30) # Send keepalive every 30 seconds def send_beacon(): global socket_ready # Declare that you're using the global variable while True: try: if socket_ready: # Send a keepalive packet to the APRS server keepalive_packet = 'SMS>APRS:!0000.00N/00000.00W$Bidirectional SMS Gateway (BETA)(US ONLY) - CALL\r\n' aprs_socket.sendall(keepalive_packet.encode()) print("Sent Beacon Packet.") except Exception as e: print("Error sending beacon:", str(e)) time.sleep(601) # Send keepalive every 10 minutes if __name__ == '__main__': print("APRS bot is running. Waiting for APRS messages...") # Start a separate thread for sending keepalive packets keepalive_thread = threading.Thread(target=send_keepalive) keepalive_thread.start() # Start a separate thread for sending keepalive packets beacon_thread = threading.Thread(target=send_beacon) beacon_thread.start() # Run the Flask web application in a separate thread to handle incoming SMS messages from threading import Thread webhook_thread = Thread(target=app.run, kwargs={'host': '0.0.0.0', 'port': 5000}) webhook_thread.start() # Start listening for APRS messages receive_aprs_messages()