Files
suphicakir 96b396149e initial commit
initial commit
2026-07-13 22:20:40 +03:00

279 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import sys
import socket
import time
import json
import argparse
import subprocess
import threading
from datetime import datetime, timedelta
# Path configuration
BASE_DIR = os.path.expanduser('~/.aprs-beacon')
PROFILES_DIR = os.path.join(BASE_DIR, 'profiles')
LOGS_DIR = os.path.join(BASE_DIR, 'logs')
os.makedirs(PROFILES_DIR, exist_ok=True)
os.makedirs(LOGS_DIR, exist_ok=True)
# Migration for older configuration
old_config = os.path.join(BASE_DIR, 'config.json')
old_log = os.path.join(BASE_DIR, 'aprs_beacon.log')
if os.path.exists(old_config):
try:
import shutil
shutil.move(old_config, os.path.join(PROFILES_DIR, 'default.json'))
if os.path.exists(old_log):
shutil.move(old_log, os.path.join(LOGS_DIR, 'default.log'))
except Exception as e:
print(f"Migration error: {e}", file=sys.stderr)
# Parse profile name
PROFILE_NAME = 'default'
for idx, arg in enumerate(sys.argv):
if arg == '--profile' and idx + 1 < len(sys.argv):
PROFILE_NAME = sys.argv[idx + 1]
CONFIG_FILE = os.path.join(PROFILES_DIR, f"{PROFILE_NAME}.json")
LOG_FILE = os.path.join(LOGS_DIR, f"{PROFILE_NAME}.log")
def clean_old_logs(log_file):
if not os.path.exists(log_file):
return
try:
thirty_days_ago = datetime.now() - timedelta(days=30)
new_lines = []
changed = False
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
line_stripped = line.strip()
if not line_stripped:
continue
if line_stripped.startswith('[') and ']' in line_stripped:
try:
end_idx = line_stripped.index(']')
date_str = line_stripped[1:end_idx]
line_date = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
if line_date < thirty_days_ago:
changed = True
continue
except Exception:
pass
new_lines.append(line)
if changed:
with open(log_file, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
except Exception as e:
print(f"Error cleaning old logs: {e}", file=sys.stderr)
def log_message(message):
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_line = f"[{timestamp}] {message}"
print(log_line)
try:
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(log_line + '\n')
except Exception as e:
print(f"Error writing to log: {e}", file=sys.stderr)
def dec2deg_lat(lat):
direction = 'N' if lat >= 0 else 'S'
lat = abs(lat)
degrees = int(lat)
minutes = (lat - degrees) * 60.0
return f"{degrees:02d}{minutes:05.2f}{direction}"
def dec2deg_lon(lon):
direction = 'E' if lon >= 0 else 'W'
lon = abs(lon)
degrees = int(lon)
minutes = (lon - degrees) * 60.0
return f"{degrees:03d}{minutes:05.2f}{direction}"
def generate_aprs_passcode(callsign):
callsign = callsign.upper().split('-')[0]
hash_val = 0x73e2
for i in range(0, len(callsign), 2):
char1 = ord(callsign[i]) << 8
char2 = ord(callsign[i+1]) if (i + 1 < len(callsign)) else 0
hash_val ^= (char1 + char2)
return hash_val & 0x7fff
def send_aprs_raw_packet(config, packet):
callsign = config['callsign'].upper()
passcode = config.get('passcode')
if not passcode:
passcode = generate_aprs_passcode(callsign)
primary_server = config.get('server', 'rotate.aprs2.net')
port = int(config.get('port', 14580))
servers_to_try = [primary_server]
backup_servers = ['euro.aprs2.net', 'noam.aprs2.net', 'asia.aprs2.net']
for b_srv in backup_servers:
if b_srv != primary_server:
servers_to_try.append(b_srv)
for server in servers_to_try:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((server, port))
greeting = s.recv(1024).decode('utf-8', errors='ignore').strip()
login_str = f"user {callsign} pass {passcode} vers PyAPRSBeacon 1.0 filter b/{callsign}\r\n"
s.sendall(login_str.encode('utf-8'))
response = s.recv(1024).decode('utf-8', errors='ignore').strip()
if "verified" not in response.lower() and "unverified" in response.lower():
log_message("WARNING: Authentication unverified! Please double-check your passcode and callsign.")
log_message(f"Sending APRS Packet: {packet} via {server}")
s.sendall(f"{packet}\r\n".encode('utf-8'))
time.sleep(2)
s.close()
return True
except Exception as e:
log_message(f"ERROR: Failed to send packet via {server}: {e}. Trying backup server if available...")
log_message("ERROR: All APRS servers are unreachable.")
return False
def send_aprs_thursday_sequence(config, today_str):
callsign = config['callsign'].upper()
log_message("[APRS Thursday] Starting. Subscribing to ANSRVR group...")
thursday_msg = config.get('aprs_thursday_msg', 'CQ HOTG 73 FROM TURKIYE #APRSTHURSDAY').strip()
if not thursday_msg:
thursday_msg = 'CQ HOTG 73 FROM TURKIYE #APRSTHURSDAY'
# Message packet format: SENDER>APRS,TCPIP*::RECIPIENT:MESSAGE
msg_packet1 = f"{callsign}>APRS,TCPIP*::ANSRVR :{thursday_msg}"
success = send_aprs_raw_packet(config, msg_packet1)
if success:
log_message(f"[APRS Thursday] Join message sent: {thursday_msg}")
log_message("[APRS Thursday] Waiting 5 minutes (300 seconds)...")
time.sleep(300)
msg_packet2 = f"{callsign}>APRS,TCPIP*::ANSRVR :U HOTG"
success2 = send_aprs_raw_packet(config, msg_packet2)
if success2:
log_message("[APRS Thursday] Leave message sent: U HOTG")
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
current_config = json.load(f)
current_config['last_aprs_thursday_sent'] = today_str
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(current_config, f, indent=4)
log_message(f"[APRS Thursday] State saved successfully: {today_str}")
except Exception as e:
log_message(f"[APRS Thursday] ERROR: Failed to save state: {e}")
else:
log_message("[APRS Thursday] ERROR: Failed to send leave message.")
else:
log_message("[APRS Thursday] ERROR: Failed to send join message. Retrying in next cycle.")
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
current_config = json.load(f)
current_config['last_aprs_thursday_sent'] = ""
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(current_config, f, indent=4)
except:
pass
def send_beacon(config):
callsign = config['callsign'].upper()
lat = float(config.get('latitude', 0.0))
lon = float(config.get('longitude', 0.0))
log_message(f"Beacon Location Info: Latitude={lat}, Longitude={lon}")
symbol_table = config.get('symbol_table', '/')
symbol_code = config.get('symbol_code', '-')
comment = config.get('comment', 'APRS Background Beacon')
lat_str = dec2deg_lat(lat)
lon_str = dec2deg_lon(lon)
packet = f"{callsign}>APRS,TCPIP*:!{lat_str}{symbol_table}{lon_str}{symbol_code}{comment}"
log_message("Sending location beacon...")
success = send_aprs_raw_packet(config, packet)
if success:
log_message("Packet successfully sent. Connection closed.")
return True
return False
def main():
parser = argparse.ArgumentParser(description="APRS Background Beacon Daemon")
parser.add_argument('--once', action='store_true', help="Send a single beacon and exit")
parser.add_argument('--profile', type=str, default='default', help="Profile name to run")
args = parser.parse_args()
if not os.path.exists(CONFIG_FILE):
print(f"Error: Configuration file ({CONFIG_FILE}) not found.", file=sys.stderr)
sys.exit(1)
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
except Exception as e:
print(f"Error: Failed to read configuration file: {e}", file=sys.stderr)
sys.exit(1)
if args.once:
success = send_beacon(config)
sys.exit(0 if success else 1)
clean_old_logs(LOG_FILE)
interval = int(config.get('interval_minutes', 5)) * 60
log_message("APRS Beacon Daemon started.")
last_cleanup_time = time.time()
while True:
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
except Exception:
pass
now_ts = time.time()
if now_ts - last_cleanup_time > 86400:
clean_old_logs(LOG_FILE)
last_cleanup_time = now_ts
if config.get('aprs_thursday', False):
now = datetime.now()
if now.weekday() == 3: # Thursday
today_str = now.strftime('%Y-%m-%d')
if config.get('last_aprs_thursday_sent', '') != today_str:
sched_time = config.get('aprs_thursday_time', '20:00')
try:
sched_hour, sched_min = map(int, sched_time.split(':'))
except Exception:
sched_hour, sched_min = 20, 0
if now.hour > sched_hour or (now.hour == sched_hour and now.minute >= sched_min):
try:
config['last_aprs_thursday_sent'] = today_str
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
threading.Thread(target=send_aprs_thursday_sequence, args=(config, today_str), daemon=True).start()
except Exception as e:
log_message(f"Thursday check error: {e}")
send_beacon(config)
log_message(f"Waiting for {config.get('interval_minutes')} minute(s)...")
time.sleep(interval)
if __name__ == '__main__':
main()