You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

368 lines
16 KiB

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>NA7Q:!4024.51N/14943.02W$Bidirectional SMS Gateway (BETA)(US ONLY) - NA7Q\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()