TC2-APRS-BBS/aprs_comm.py
2025-01-07 14:53:16 -05:00

246 lines
10 KiB
Python

import time
from threading import Lock
import aprs
import os
import json
import requests
import commands
import config
# Global dictionary to track unacknowledged messages
unacknowledged_messages = {}
unack_lock = Lock()
# Message numbering for ACKS
message_counter = 1
from datetime import datetime
JSON_URL = "https://aprs-deviceid.aprsfoundation.org/tocalls.pretty.json"
def fetch_device_data():
local_file = "tocalls_cache.json"
if os.path.exists(local_file):
try:
if time.time() - os.path.getmtime(local_file) > 7 * 24 * 60 * 60: # Check if the JSON is older than 7 days
print("Cache is outdated. Refreshing...")
raise Exception("Cache expired")
with open(local_file, "r") as file:
print("Loading device data from cache...")
return json.load(file)
except Exception as e:
print(f"Error with cache: {e}")
print("Fetching device data online...")
try:
response = requests.get(JSON_URL)
response.raise_for_status()
data = response.json()
with open(local_file, "w") as file:
json.dump(data, file, indent=4)
return data
except Exception as e:
print(f"Error fetching device data: {e}")
return {}
def get_device_info(destination, device_data):
tocalls = device_data.get("tocalls", {})
for pattern, info in tocalls.items():
if pattern.replace("?", "") in destination:
model = info.get("model", "Unknown Model")
vendor = info.get("vendor", "Unknown Vendor")
device_class = info.get("class", "Unknown Class")
return f"{vendor} {model}, {device_class.capitalize()}"
return "Unknown Device Type"
def send_ack(ki, aprs_frame):
try:
source_callsign = aprs_frame.source.callsign.decode('utf-8') if isinstance(aprs_frame.source.callsign,
bytes) else aprs_frame.source.callsign
source_ssid = aprs_frame.source.ssid
source = f"{source_callsign}-{source_ssid}" if source_ssid else source_callsign
message_number = aprs_frame.info.number.decode('utf-8') if isinstance(aprs_frame.info.number, bytes) else str(
aprs_frame.info.number)
ack_info = f":{source:<9}:ack{message_number}".encode('utf-8')
frame = aprs.APRSFrame.ui(
destination=source,
source=config.MYCALL,
path=config.APRS_PATH,
info=ack_info,
)
ki.write(frame)
except Exception as e:
print(f"Failed to send ACK: {e}")
def start():
ki = aprs.TCPKISS(host=config.KISS_HOST, port=config.KISS_PORT)
ki.start()
device_data = fetch_device_data()
print("Listening for APRS frames...\n")
COLOR_SEPARATOR = "\033[96m" # Cyan
COLOR_TIMESTAMP = "\033[92m" # Green
COLOR_MESSAGE = "\033[94m" # Blue
COLOR_DEVICE = "\033[93m" # Yellow
COLOR_RAW = "\033[91m" # Bright Red
COLOR_RESET = "\033[0m" # Reset to Default
separator_line = f"{COLOR_SEPARATOR}{'=' * 110}{COLOR_RESET}"
sub_separator_line = f"{COLOR_SEPARATOR}{'-' * 110}{COLOR_RESET}"
def normalize_callsign(callsign):
return callsign.split('-')[0].upper() if callsign else ""
my_callsign = normalize_callsign(config.MYCALL)
print(f"BBS Callsign: {my_callsign}")
while True:
for frame in ki.read(min_frames=1):
try:
if config.RAW_PACKET_DISPLAY:
print(f"{COLOR_RAW}RAW PACKET:{COLOR_RESET} {frame}")
aprs_frame = aprs.APRSFrame.from_bytes(bytes(frame))
if isinstance(aprs_frame.info, aprs.Message):
message_text = aprs_frame.info.text.decode('utf-8') if isinstance(aprs_frame.info.text,
bytes) else aprs_frame.info.text
if message_text.startswith("ack"):
ack_number = message_text[3:].strip() # Extract the message number after "ack"
# Ensure ACK is intended for this BBS
recipient = aprs_frame.info.addressee.decode('utf-8').strip() if isinstance(
aprs_frame.info.addressee, bytes) else aprs_frame.info.addressee.strip()
if normalize_callsign(recipient) != my_callsign:
continue
with unack_lock:
for tracked_recipient, (msg, msg_num) in unacknowledged_messages.items():
if str(msg_num) == ack_number:
print(
f"ACK received for {tracked_recipient}'s message #{msg_num}. Removing from tracking.")
del unacknowledged_messages[tracked_recipient]
continue
if isinstance(aprs_frame.info, aprs.Message):
recipient = aprs_frame.info.addressee.decode('utf-8') if isinstance(aprs_frame.info.addressee, bytes) else aprs_frame.info.addressee
recipient = recipient.strip()
# print(f"DEBUG: Extracted recipient from addressee: {recipient}")
normalized_recipient = normalize_callsign(recipient)
# print(f"DEBUG: Normalized recipient: {normalized_recipient}, My callsign: {my_callsign}")
if normalized_recipient != my_callsign:
continue
source_callsign = aprs_frame.source.callsign.decode('utf-8') if isinstance(aprs_frame.source.callsign, bytes) else aprs_frame.source.callsign
source_ssid = aprs_frame.source.ssid
source = normalize_callsign(f"{source_callsign}-{source_ssid}")
message = aprs_frame.info.text.decode('utf-8') if isinstance(aprs_frame.info.text, bytes) else aprs_frame.info.text
send_ack(ki, aprs_frame)
destination = aprs_frame.destination.callsign.decode('utf-8') if isinstance(aprs_frame.destination.callsign, bytes) else str(aprs_frame.destination.callsign)
device_info = get_device_info(destination, device_data)
iso_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(separator_line)
print(f"{COLOR_TIMESTAMP}{iso_timestamp}{COLOR_RESET} {COLOR_MESSAGE}Message from {source}: {message}{COLOR_RESET}")
print(f"{COLOR_DEVICE}({device_info}){COLOR_RESET}")
print(sub_separator_line)
responses = commands.handle_command(source, message)
if isinstance(responses, str):
responses = [responses]
for response in responses:
dec_timestamp = datetime.now().strftime("%b%d %H:%M")
response_info = f":{source:<9}:{response}".encode('utf-8')
response_frame = aprs.APRSFrame.ui(
destination=source,
source=config.MYCALL,
path=config.APRS_PATH,
info=response_info,
)
ki.write(response_frame)
print(f"{COLOR_TIMESTAMP}{iso_timestamp}{COLOR_RESET} Response to {source}: {response}")
print(separator_line, "\n")
except Exception as e:
print(f"Error processing frame: {e}")
def send_direct_message(recipient, message):
"""Send a direct APRS message to a recipient with ACK request and monitor for retries."""
global message_counter
try:
message_number = message_counter
message_counter += 1
ack_info = f":{recipient:<9}:{message}{{{message_number}".encode('utf-8')
frame = aprs.APRSFrame.ui(
destination=recipient.upper(),
source=config.MYCALL,
path=config.APRS_PATH,
info=ack_info
)
ki = aprs.TCPKISS(host=config.KISS_HOST, port=config.KISS_PORT)
ki.start()
ki.write(frame)
print(f"Direct message sent to {recipient} with ACK request (msg #{message_number}): {message}")
if not wait_for_ack(ki, recipient, message_number, timeout=5):
print(f"No ACK received from {recipient} for message #{message_number}. Monitoring for activity...")
with unack_lock:
unacknowledged_messages[recipient.upper()] = (message, message_number)
ki.stop()
except Exception as e:
print(f"Failed to send direct message to {recipient}: {e}")
def wait_for_ack(ki, recipient, message_number, timeout=5):
"""Wait for an acknowledgment from the recipient."""
try:
start_time = time.time()
while time.time() - start_time < timeout:
for frame in ki.read(min_frames=1):
aprs_frame = aprs.APRSFrame.from_bytes(bytes(frame))
if isinstance(aprs_frame.info, aprs.Message):
message_text = aprs_frame.info.text.decode('utf-8') if isinstance(aprs_frame.info.text, bytes) else aprs_frame.info.text
if message_text.startswith("ack"):
ack_number = message_text[3:].strip()
if ack_number == str(message_number):
print(f"ACK received from {recipient} for message #{message_number}.")
return True
print(f"Timeout reached: No ACK received from {recipient} for message #{message_number}.")
return False
except Exception as e:
print(f"Error while waiting for ACK: {e}")
return False