mirror of
https://github.com/TheCommsChannel/TC2-APRS-BBS.git
synced 2025-02-05 01:45:24 -05:00
Add files via upload
This commit is contained in:
commit
635e7f42d2
99
README.md
Normal file
99
README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# TC²-BBS Meshtastic Version
|
||||
|
||||
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B1OZ22Z)
|
||||
|
||||
This is the TC²-BBS for APRS. The system allows for basic mail message for specific users and bulletin boards.
|
||||
This is an APRS BBS only because it uses the APRS protocol and it's not meant to be used on the nationwide APRS freq 144.390MHz.
|
||||
Ideally, this is meant to be used within the following frequency ranges (US users):
|
||||
2M - 144.90-145.10 & 145.50-145.80
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
### Requirements
|
||||
|
||||
- Python 3.x
|
||||
- `requests`
|
||||
- `aprs3`
|
||||
|
||||
### Update and Install Git
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt upgrade
|
||||
sudo apt install git
|
||||
```
|
||||
|
||||
### Installation (Linux)
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```sh
|
||||
cd ~
|
||||
git clone https://github.com/TheCommsChannel/TC2-APRS-BBS.git
|
||||
cd TC2-APRS-BBS
|
||||
```
|
||||
|
||||
2. Set up a Python virtual environment:
|
||||
|
||||
```sh
|
||||
python -m venv venv
|
||||
```
|
||||
|
||||
3. Activate the virtual environment:
|
||||
|
||||
```sh
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
4. Install the required packages:
|
||||
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
5. Rename `example_config.ini`:
|
||||
|
||||
```sh
|
||||
mv example_config.ini config.ini
|
||||
```
|
||||
|
||||
6. Set up the configuration in `config.ini`:
|
||||
|
||||
You'll need to open up the config.ini file in a text editor and make your changes following the instructions below
|
||||
|
||||
**MYCALL**
|
||||
This is where you enter the callsign of your BBS. This can be your FCC callsign or eventually a tactical callsign (like BBS). If using a tactical call the BBS will need to transmit your FCC call every 10 minutes while in operation. This hasn't been implemented yet however, so it's best to use your FCC call until then.
|
||||
|
||||
**KISS_HOST & KISS PORT**
|
||||
IP Address and Port of the host running direwolf (127.0.0.1 if the BBS is running on the same system)
|
||||
|
||||
**BULLETIN_EXPIRATION_DAYS**
|
||||
Number of days to have bulletins expire
|
||||
|
||||
**APRS_PATH**
|
||||
The WIDEN-n path for digipeater hops
|
||||
|
||||
**RAW_PACKET_DISPLAY**
|
||||
IP Address of the host running direwolf (127.0.0.1 if the BBS is running on the same system)
|
||||
|
||||
|
||||
### Running the Server
|
||||
|
||||
Run the server with:
|
||||
|
||||
```sh
|
||||
python main.py
|
||||
```
|
||||
|
||||
Be sure you've followed the Python virtual environment steps above and activated it before running.
|
||||
You should see (venv) at the beginning of the command prompt
|
||||
|
||||
|
||||
## Automatically run at boot
|
||||
|
||||
Instructions coming soon....
|
||||
|
||||
## License
|
||||
|
||||
GNU General Public License v3.0
|
245
aprs_comm.py
Normal file
245
aprs_comm.py
Normal file
@ -0,0 +1,245 @@
|
||||
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
|
45
commands.py
Normal file
45
commands.py
Normal file
@ -0,0 +1,45 @@
|
||||
import database
|
||||
import aprs_comm
|
||||
|
||||
def handle_command(callsign, command):
|
||||
normalized_callsign = callsign.split('-')[0].upper()
|
||||
parts = command.split(' ', 1)
|
||||
cmd = parts[0].upper()
|
||||
arg = parts[1] if len(parts) > 1 else ''
|
||||
|
||||
if cmd in ['LIST', 'L']:
|
||||
bulletins = database.get_bulletins()
|
||||
return [
|
||||
f"{b[2]} {b[1]}: {b[0]}" for b in bulletins
|
||||
] or ["No bulletins available."]
|
||||
|
||||
elif cmd in ['MSG', 'M']:
|
||||
messages = database.get_messages_for_user(normalized_callsign)
|
||||
return [
|
||||
f"From {m[0]} ({m[2]}): {m[1]}" for m in messages
|
||||
] or ["No messages for you."]
|
||||
|
||||
elif cmd in ['POST', 'P']:
|
||||
if arg:
|
||||
database.add_bulletin(normalized_callsign, arg)
|
||||
return ["Bulletin posted."]
|
||||
return ["Usage: POST <text>"]
|
||||
|
||||
elif cmd in ['SEND', 'S']:
|
||||
args = arg.split(' ', 1)
|
||||
if len(args) == 2:
|
||||
recipient, message = args
|
||||
database.add_message(normalized_callsign, recipient, message)
|
||||
|
||||
notification = f"You have a new message from {normalized_callsign}."
|
||||
aprs_comm.send_direct_message(recipient, notification)
|
||||
|
||||
return [f"Message sent to {recipient}."]
|
||||
return ["Usage: SEND <callsign> <text>"]
|
||||
|
||||
else:
|
||||
return [
|
||||
"Hello and Welcome to the TC2-BBS!",
|
||||
"Please send a message with one of the commands below.",
|
||||
"Commands: (L)IST, (M)SG, (P)OST <text>, (S)SEND <callsign> <text>"
|
||||
]
|
26
config.py
Normal file
26
config.py
Normal file
@ -0,0 +1,26 @@
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
|
||||
config_file = "config.ini"
|
||||
config = ConfigParser()
|
||||
|
||||
# Check if config.ini exists and create one if it doesnt.
|
||||
if not os.path.exists(config_file):
|
||||
with open(config_file, "w") as file:
|
||||
file.write("""
|
||||
[DEFAULT]
|
||||
MYCALL = BBS
|
||||
KISS_HOST = 192.168.1.94
|
||||
KISS_PORT = 8001
|
||||
BULLETIN_EXPIRATION_DAYS = 7
|
||||
APRS_PATH = WIDE1-1
|
||||
""")
|
||||
|
||||
config.read(config_file)
|
||||
|
||||
MYCALL = config.get("DEFAULT", "MYCALL", fallback="BBS")
|
||||
KISS_HOST = config.get("DEFAULT", "KISS_HOST", fallback="127.0.0.1")
|
||||
KISS_PORT = config.getint("DEFAULT", "KISS_PORT", fallback=8001)
|
||||
BULLETIN_EXPIRATION_DAYS = config.getint("DEFAULT", "BULLETIN_EXPIRATION_DAYS", fallback=7)
|
||||
APRS_PATH = config.get("DEFAULT", "APRS_PATH", fallback="WIDE1-1").split(",")
|
||||
RAW_PACKET_DISPLAY = config.getboolean("DEFAULT", "RAW_PACKET_DISPLAY", fallback=False)
|
81
database.py
Normal file
81
database.py
Normal file
@ -0,0 +1,81 @@
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
import config
|
||||
|
||||
def init_db():
|
||||
conn = sqlite3.connect('aprs.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS bulletins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
poster TEXT NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender TEXT NOT NULL,
|
||||
recipient TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def format_timestamp(timestamp):
|
||||
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
|
||||
return dt.strftime("%b%d %H:%M").upper()
|
||||
|
||||
def add_bulletin(callsign, text):
|
||||
conn = sqlite3.connect('aprs.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT INTO bulletins (text, poster) VALUES (?, ?)", (text, callsign))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_bulletins():
|
||||
conn = sqlite3.connect('aprs.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT text, poster, timestamp FROM bulletins ORDER BY timestamp DESC")
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
return [(text, poster, format_timestamp(ts)) for text, poster, ts in results]
|
||||
|
||||
|
||||
def add_message(sender, recipient, text):
|
||||
conn = sqlite3.connect('aprs.db')
|
||||
cursor = conn.cursor()
|
||||
normalized_recipient = normalize_callsign(recipient)
|
||||
cursor.execute("INSERT INTO messages (sender, recipient, text) VALUES (?, ?, ?)", (sender, normalized_recipient, text))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_messages_for_user(callsign):
|
||||
conn = sqlite3.connect('aprs.db')
|
||||
cursor = conn.cursor()
|
||||
normalized_callsign = normalize_callsign(callsign)
|
||||
cursor.execute("""
|
||||
SELECT sender, text, timestamp
|
||||
FROM messages
|
||||
WHERE UPPER(recipient) = ?
|
||||
ORDER BY timestamp DESC
|
||||
""", (normalized_callsign,))
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
return [(normalize_callsign(sender), text, format_timestamp(ts)) for sender, text, ts in results]
|
||||
|
||||
def normalize_callsign(callsign):
|
||||
return callsign.split('-')[0].upper()
|
||||
|
||||
def delete_expired_bulletins():
|
||||
"""Delete bulletins older than the configured expiration time."""
|
||||
expiration_threshold = datetime.now() - timedelta(days=config.BULLETIN_EXPIRATION_DAYS)
|
||||
conn = sqlite3.connect('aprs.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM bulletins WHERE timestamp < ?", (expiration_threshold,))
|
||||
conn.commit()
|
||||
conn.close()
|
28
example_config.ini
Normal file
28
example_config.ini
Normal file
@ -0,0 +1,28 @@
|
||||
# This is the config file for the TC² APRS-BBS.
|
||||
# Edit the values below to customize the behavior of the system.
|
||||
|
||||
[DEFAULT]
|
||||
# MYCALL: The callsign of the BBS. This is the identifier used to communicate
|
||||
# with the APRS network. Change it to your assigned ham radio callsign or a tactical callsign
|
||||
# Note on tactical callsigns:
|
||||
# Your FCC callsign will need to be transmitted every 10 minutes during operation if using a tactical call.
|
||||
# This feature hasn't been implemented yet, so it is best to enter in your FCC call here until then.
|
||||
MYCALL = BBS
|
||||
|
||||
# KISS_HOST: The hostname or IP address of the KISS TNC (Terminal Node Controller)
|
||||
# used for APRS communications. Typically, this is a local device or a remote server.
|
||||
KISS_HOST = 192.168.1.94
|
||||
|
||||
# KISS_PORT: The port number used to connect to the KISS TNC.
|
||||
# Ensure the port matches the configuration of your TNC.
|
||||
KISS_PORT = 8001
|
||||
|
||||
# BULLETIN_EXPIRATION_DAYS: The number of days after which bulletins will
|
||||
# automatically expire and be deleted from the database. Set to 0 to disable expiration.
|
||||
BULLETIN_EXPIRATION_DAYS = 7
|
||||
|
||||
# APRS Path
|
||||
APRS_PATH = WIDE1-1,WIDE2-1
|
||||
|
||||
# Enable or disable raw packet display (True or False)
|
||||
RAW_PACKET_DISPLAY = True
|
41
main.py
Normal file
41
main.py
Normal file
@ -0,0 +1,41 @@
|
||||
import database
|
||||
import aprs_comm
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def scheduled_cleanup():
|
||||
"""Periodically run cleanup of expired bulletins."""
|
||||
while True:
|
||||
try:
|
||||
print("Running periodic cleanup of expired bulletins...")
|
||||
database.delete_expired_bulletins()
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup: {e}")
|
||||
time.sleep(24 * 60 * 60) # Run cleanup every 24 hours
|
||||
|
||||
def main():
|
||||
banner = """
|
||||
\033[96m
|
||||
████████╗ ██████╗██████╗ ██████╗ ██████╗ ███████╗
|
||||
╚══██╔══╝██╔════╝╚════██╗ ██╔══██╗██╔══██╗██╔════╝
|
||||
██║ ██║ █████╔╝█████╗██████╔╝██████╔╝███████╗
|
||||
██║ ██║ ██╔═══╝ ╚════╝██╔══██╗██╔══██╗╚════██║
|
||||
██║ ╚██████╗███████╗ ██████╔╝██████╔╝███████║
|
||||
╚═╝ ╚═════╝╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
||||
\033[93mAPRS Version\033[0m
|
||||
"""
|
||||
print(banner)
|
||||
|
||||
print("Initializing database...")
|
||||
database.init_db()
|
||||
|
||||
# Start periodic bulletin cleanup in a separate thread
|
||||
cleanup_thread = threading.Thread(target=scheduled_cleanup, daemon=True)
|
||||
cleanup_thread.start()
|
||||
|
||||
print("Starting APRS communications...")
|
||||
aprs_comm.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
requests
|
||||
aprs3
|
1832
tocalls_cache.json
Normal file
1832
tocalls_cache.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user