Browse Source

Major Update

main
Mike 2 years ago
committed by GitHub
parent
commit
665721b5ff
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 391 additions and 173 deletions
  1. +391
    -173
      sms.py

+ 391
- 173
sms.py View File

@ -5,22 +5,30 @@ from twilio.rest import Client
import time
import threading
import setproctitle
import json
# Set the custom process name
setproctitle.setproctitle("sms")
app = Flask(__name__)
# Set the expiration time in seconds (e.g., 90 seconds)
MESSAGE_EXPIRATION_TIME = 3600
# 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
tocall = 'APOSMS'
user_callsign = 'YOUR_CALLSIGN'
# Twilio credentials
TWILIO_ACCOUNT_SID = 'SID'
TWILIO_AUTH_TOKEN = 'AUTH'
TWILIO_PHONE_NUMBER = '+NUMBER' # Your Twilio phone number
TWILIO_PHONE_NUMBER_UK = '+UKNUMBER' #UK SUPPORT
# APRS credentials
APRS_CALLSIGN = 'CALL'
@ -51,6 +59,93 @@ RETRY_INTERVAL = 90 # Adjust this as needed
MAX_RETRIES = 4 # Adjust this as needed
def handle_alias_update(from_callsign, verbose_message):
global alias_map, reverse_alias_map # Access the global dictionaries
# Extract alias information from the received message
alias_info = verbose_message.split('#alias', 1)[1].strip() # Remove the '#alias' prefix and trim whitespace
# Split the alias information into parts
alias_parts = alias_info.split()
if len(alias_parts) == 3:
action, alias_name, alias_phone = alias_parts
alias_name = alias_name.lower()
# Ensure the action is valid
if action in ("#add", "#remove"):
# Check if the alias phone is either 10 or 12 digits long #UK Support
if len(alias_phone) not in {10, 12} or not alias_phone.isdigit():
print("Invalid alias phone number. It must be either exactly 10 or 12 digits long.")
return
# Extract the SSID from the from_callsign
from_callsign_parts = from_callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = from_callsign # No SSID found
if from_callsign_strip in alias_map:
existing_aliases = alias_map[from_callsign_strip]
if action == "#add":
# Adding an alias
if alias_name in existing_aliases:
existing_phone = existing_aliases[alias_name]
if existing_phone != alias_phone:
# Alias name is found, but with a different phone number, update both name and phone
existing_aliases[alias_name] = alias_phone
elif alias_phone in existing_aliases.values():
# Alias phone number is found, update the associated alias name
for name, phone in existing_aliases.items():
if phone == alias_phone:
existing_aliases[alias_name] = alias_phone
del existing_aliases[name] # Remove the old alias name
break # Stop searching after the first match is found
else:
# Neither the alias name nor the alias phone is found, add the new alias
existing_aliases[alias_name] = alias_phone
elif action == "#remove":
# Removing an alias
if alias_name in existing_aliases:
if existing_aliases[alias_name] == alias_phone:
# Check if the provided alias and phone match the existing alias
alias_map[from_callsign_strip].pop(alias_name)
if not alias_map[from_callsign_strip]:
# If there are no more aliases for this callsign, remove the callsign entry
alias_map.pop(from_callsign_strip)
else:
print("Alias not found for removal:", alias_name, alias_phone)
else:
# If the callsign is not in the alias map, create a new entry
if action == "#add":
alias_map[from_callsign_strip] = {alias_name: alias_phone}
# Update the reverse_alias_map to reflect the changes
reverse_alias_map = generate_reverse_alias_map(alias_map)
# Save the updated alias map to a file
save_alias_map_to_file(alias_map, alias_map_filename)
# Print a message indicating the update
print("Alias map updated:")
print(alias_map)
else:
print("Invalid action:", action)
else:
print("Invalid alias information:", alias_info)
def save_alias_map_to_file(alias_map, filename):
# Save the alias map to the specified file in JSON format with proper formatting
with open(filename, 'w') as file:
formatted_json = json.dumps(alias_map, indent=4)
file.write(formatted_json)
def send_ack_message(sender, message_id):
ack_message = 'ack{}'.format(message_id)
sender_length = len(sender)
@ -71,6 +166,77 @@ def send_rej_message(sender, message_id):
print("Sent REJ to {}: {}".format(sender, rej_message))
print("Outgoing REJ packet: {}".format(rej_packet.decode()))
def send_aprs_messages(callsign, from_phone_number, sender_phone_number, aprs_message, last_message_id):
#New Chunk Method
chunk_size = 67
# Additional characters for portion information
portion_info_chars = 4
# Calculate the constant value considering portion information
constant_value = chunk_size - len(sender_phone_number) - 2 - portion_info_chars
# Split the APRS message into chunks of 67 characters
message_chunks = [aprs_message[i:i + constant_value] for i in range(0, len(aprs_message), constant_value)]
#End Chunk Method
#New Portion Calc
total_chunks = len(message_chunks)
# Get the last APRS message ID sent to this user
last_message_id = user_last_message_id.get(from_phone_number, 0)
user_last_message_id[from_phone_number] = last_message_id
# Initialize a separate counter variable for the message ID within the loop
current_message_id = last_message_id
for i, chunk in enumerate(message_chunks):
# Calculate portion information
portion_info = " {}/{}".format(i + 1, total_chunks) if total_chunks > 1 else ""
print("Chunks", len(message_chunks))
# Format the APRS packet and send it to the APRS server
aprs_packet = format_aprs_packet(callsign, "@{}{} {}{}".format(sender_phone_number, portion_info, chunk, "{" + str(current_message_id)))
print("chunks ID: ", current_message_id)
print(aprs_packet)
aprs_socket.sendall(aprs_packet.encode())
# Not a good delay, maybe seek alternative options.
time.sleep(10) # Sleeping here allows time for incoming ack before retry
print("sleep ID: ", current_message_id)
retry_count = 0
ack_received = False
while retry_count < MAX_RETRIES and not ack_received:
if str(current_message_id) in received_acks.get(callsign, set()):
print("Message ACK received. No further retries needed.")
ack_received = True
retry_count = 0
received_acks.get(callsign, set()).discard(str(current_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)
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.")
# Increment the counter variable for the next chunk
current_message_id += 1
# Update the last_message_id outside the loop
last_message_id += len(message_chunks) - 1
user_last_message_id[from_phone_number] = last_message_id
print("Last ID: ", last_message_id)
print("Next ID: ", current_message_id)
print(user_last_message_id)
def send_sms(twilio_phone_number, to_phone_number, from_callsign, body_message):
# Initialize the Twilio client
@ -89,28 +255,73 @@ def send_sms(twilio_phone_number, to_phone_number, from_callsign, body_message):
print("Error sending SMS:", str(e))
def send_sms_uk(twilio_phone_number_uk, to_phone_number, from_callsign, body_message): #UK SUPPORT
# 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_uk,
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) #1,9 - Changed 9-16
aprs_packet_format = '{}>APOSMS::{}{}:{}\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': '9876543210', # 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())
def load_alias_map_from_file(filename):
try:
with open(filename, 'r') as file:
alias_map = json.load(file)
return alias_map
except (FileNotFoundError, json.JSONDecodeError):
return {} # Return an empty dictionary if the file is not found or cannot be parsed
# Define the filename for the alias map file
alias_map_filename = '/root/app/sms_map.json'
# Load the alias map from the file
alias_map = load_alias_map_from_file(alias_map_filename)
# Create a new dictionary to store the reverse mapping of phone numbers and aliases to callsigns
def generate_reverse_alias_map(alias_map):
reverse_alias_map = {}
for callsign, aliases_and_numbers in alias_map.items():
reverse_alias_map[callsign] = {}
for alias, phone_number in aliases_and_numbers.items():
reverse_alias_map[callsign][phone_number] = alias
return reverse_alias_map
# Whenever you update the alias map, you can regenerate the reverse_alias_map
# For example, after updating the alias map with new data, call this function
reverse_alias_map = generate_reverse_alias_map(alias_map)
def extract_sender_phone_number(from_phone_number):
# Extract the phone number from the sender's phone number based on the prefix
if from_phone_number.startswith('+1'):
return from_phone_number[2:]
elif from_phone_number.startswith('+44'):
return from_phone_number[1:]
else:
return from_phone_number[-10:]
# 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()}
def is_message_expired(timestamp):
return time.time() - timestamp > MESSAGE_EXPIRATION_TIME
@app.route('/sms', methods=['POST'])
def receive_sms():
global last_message_number #Questioning this. Consider options later.
# Parse the incoming SMS message
data = request.form
from_phone_number = data['From']
@ -120,57 +331,52 @@ def receive_sms():
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:]
# Extract the phone number from the sender's phone number
sender_phone_number = extract_sender_phone_number(from_phone_number)
callsign = parts[0][1:].upper()
aprs_message = parts[1]
print(callsign)
last_message_number[sender_phone_number] = callsign #Questioning this. Consider options later.
print(last_message_number)
# Get the last APRS message ID sent to this user
last_message_id = user_last_message_id.get(from_phone_number, 0)
last_message_id += 1
user_last_message_id[from_phone_number] = last_message_id
print("RX SMS ID: ", last_message_id)
print("RX USR ID: ", user_last_message_id)
# Extract the SSID from the from_callsign
from_callsign_parts = callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = callsign # No SSID found
print("No SSID Found")
# 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())
alias = reverse_alias_map.get(from_callsign_strip, {}).get(sender_phone_number.lower())
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())
time.sleep(5) # Sleeping here allows time for incoming ack before retry
retry_count = 0
ack_received = False
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
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)
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.")
# Format and send APRS packets
send_aprs_messages(callsign, from_phone_number, sender_phone_number, aprs_message, last_message_id)
return jsonify({'status': 'success'})
else:
return jsonify({'status': 'error', 'message': 'Invalid SMS format'})
else:
print ("no callsign found")
# Message without @callsign prefix
callsign = last_message_number.get(from_phone_number[-10:], None)
sender_phone_number = from_phone_number[-10:]
sender_phone_number = extract_sender_phone_number(from_phone_number)
callsign = last_message_number.get(sender_phone_number, None)
print("From:", from_phone_number[-10:])
print("From Full:", from_phone_number)
print("Dictionary:", last_message_number)
print("Callsign Found:", callsign)
@ -182,40 +388,22 @@ def receive_sms():
last_message_id = user_last_message_id.get(from_phone_number, 0)
last_message_id += 1
user_last_message_id[from_phone_number] = last_message_id
# Extract the SSID from the from_callsign
from_callsign_parts = callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = callsign # No SSID found
# 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())
alias = reverse_alias_map.get(from_callsign_strip, {}).get(sender_phone_number.lower())
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())
sender_phone_number = alias
print("Sent APRS message to {}: {}".format(callsign, aprs_message))
print("Outgoing APRS packet: {}".format(aprs_packet.strip()))
# Format and send APRS packets
send_aprs_messages(callsign, from_phone_number, sender_phone_number, aprs_message, last_message_id)
time.sleep(5) # Sleeping here allows time for incoming ack before retry
retry_count = 0
ack_received = False
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
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)
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:
@ -232,7 +420,7 @@ def establish_aprs_connection():
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 1.0 Beta\r\n'.format(APRS_CALLSIGN, APRS_PASSCODE)
login_str = 'user {} pass {} vers SMS-Gateway 1.4 Beta\r\n'.format(APRS_CALLSIGN, APRS_PASSCODE)
aprs_socket.sendall(login_str.encode())
print("Sent login information.")
@ -253,7 +441,7 @@ def establish_aprs_connection():
time.sleep(1) # Wait for a while before attempting to reconnect
def receive_aprs_messages():
global socket_ready, last_message_number # Declare that you're using the global variable
global socket_ready, last_message_number, alias_map # Declare that you're using the global variable
while True:
try:
@ -294,104 +482,134 @@ def receive_aprs_messages():
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]
else:
message_id = '1'
if ":" in message_text and APRS_CALLSIGN in message_text:
pass
# 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)
# Update the dictionary with the last message number for the callsign
# Use the reverse alias mapping to check if the sender's phone number has an associated alias
alias = alias_map.get(recipient.lower())
if alias:
recipient = alias
last_message_number[recipient.lower()] = from_callsign
print ("To #", recipient)
print ("From", from_callsign)
print ("Dictionary", last_message_number)
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 if verbose_message starts with ":SMS:" (or the appropriate APRS_CALLSIGN)
if message_text.startswith(":{}".format(APRS_CALLSIGN)):
# Check if the message contains "{"
if "{" in message_text[-6:]:
message_id = message_text.split('{')[1]
# 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)
# 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)
# Initialize match
match = None
# Check if the verbose message contains the desired format with a number or an alias
#alias_pattern = r'#alias (.+)'
alias_pattern = re.compile(r'#alias (.+)', re.IGNORECASE)
alias_match = re.search(alias_pattern, verbose_message)
print("alias pattern")
if alias_match:
print("did we make it to 1")
# Call a function to handle alias updates
handle_alias_update(from_callsign, verbose_message.lower()) #added lower
else:
# Mark the message as received
received_aprs_messages.setdefault(from_callsign, set()).add((aprs_message, message_id))
pattern = r'@(\d{10}|\w+) (.+)'
match = re.match(pattern, verbose_message)
# 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)
# Send ACK
send_ack_message(from_callsign, message_id)
print("did we send this ack?")
if match:
recipient = match.group(1)
# Update the dictionary with the last message number for the callsign
# Extract the SSID from the from_callsign
from_callsign_parts = from_callsign.split('-')
if len(from_callsign_parts) == 2:
from_callsign_strip = from_callsign_parts[0]
else:
from_callsign_strip = from_callsign # No SSID found
# Use the reverse alias mapping to check if the sender's phone number has an associated alias
alias = alias_map.get(from_callsign_strip, {}).get(recipient.lower())
if alias:
recipient = alias
print("Recipient:", recipient)
# 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
#last_message_number[recipient.lower()] = from_callsign
print ("To #", recipient)
print ("From", from_callsign)
print ("Dictionary", last_message_number)
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
last_message_number[recipient.lower()] = from_callsign
else:
# Recipient is an alias
phone_number = alias_map.get(from_callsign, {}).get(recipient)
print("phone", phone_number)
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
if not phone_number:
print("Recipient not found in alias map or not a 10 or 12 digit number: {}".format(recipient))
continue # Skip processing the current message and move on to the next one in the loop
# Check for duplicate messages
messages_for_callsign = received_aprs_messages.get(from_callsign, [])
current_time = time.time()
# Check if the message has been received in the last 90 seconds
if any(not is_message_expired(stored_timestamp) and stored_message_id == message_id for stored_message, stored_message_id, stored_timestamp in messages_for_callsign):
print("Message received in the last 3600 seconds. Skipping further processing.")
#UK SUPPORT
else:
# Mark the message as received
received_aprs_messages.setdefault(from_callsign, []).append((aprs_message, message_id, current_time))
print(received_aprs_messages)
if len(phone_number) == 12 and phone_number.startswith("44"):
# UK phone number format: 12 digits and starts with "44"
send_sms_uk(TWILIO_PHONE_NUMBER_UK, phone_number, from_callsign, aprs_message)
else:
# Default behavior
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)
print("Process MSG 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)
print("Process ACK ID")
pass
# Send ACK
# The last line might be an incomplete packet, so keep it in the buffer
buffer = lines[-1]
@ -444,8 +662,8 @@ def send_beacon():
try:
if socket_ready:
# Send a keepalive packet to the APRS server
beacon_packet = 'POSITION BEACON\r\n'
status_beacon = 'STATUS BEACON\r\n'
beacon_packet = '{}>{}:Your Position {}\r\n'.format(APRS_CALLSIGN, tocall, user_callsign)
status_beacon = '{}>{}:>Status Beacon\r\n'.format(APRS_CALLSIGN, tocall)
aprs_socket.sendall(beacon_packet.encode())
aprs_socket.sendall(status_beacon.encode())
@ -457,6 +675,9 @@ def send_beacon():
if __name__ == '__main__':
print("APRS bot is running. Waiting for APRS messages...")
# Establish the initial connection to the APRS server
establish_aprs_connection()
# Start a separate thread for sending keepalive packets
keepalive_thread = threading.Thread(target=send_keepalive)
keepalive_thread.start()
@ -467,11 +688,8 @@ if __name__ == '__main__':
# 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 = Thread(target=app.run, kwargs={'host': '0.0.0.0', 'port': 12345})
webhook_thread.start()
# Establish the initial connection to the APRS server
establish_aprs_connection()
# Start listening for APRS messages
receive_aprs_messages()

Loading…
Cancel
Save