initial commit
initial commit
This commit is contained in:
Executable
+278
@@ -0,0 +1,278 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user