initial commit
initial commit
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set timezone env
|
||||
ENV TZ=Europe/Istanbul
|
||||
|
||||
# Install git and minimal system packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy application files
|
||||
COPY aprs_beacon.py aprs_manager.py web_manager.py ./
|
||||
COPY templates/ ./templates/
|
||||
|
||||
# Install python dependencies (Flask)
|
||||
RUN pip install --no-cache-dir flask
|
||||
|
||||
# Expose web manager portal port
|
||||
EXPOSE 8080
|
||||
|
||||
# Environment variables
|
||||
ENV DOCKER_MODE=true
|
||||
ENV PORT=8080
|
||||
|
||||
# Run the web manager
|
||||
CMD ["python3", "web_manager.py"]
|
||||
@@ -0,0 +1,108 @@
|
||||
# 📻 Linux ve Windows Arka Plan APRS Beacon İstemcisi
|
||||
|
||||
Bu proje; Linux ve Windows tabanlı cihazlarda sistem açılışında arka planda tamamen otomatik çalışan, sistem kaynaklarını tüketmeyen hafif bir **APRS-IS (Automatic Packet Reporting System - Internet Service)** konum bildirim istemcisidir.
|
||||
|
||||
Herhangi bir grafik arayüze (GUI) ihtiyaç duymadan çalışır. İstasyonunuzun konumunu, durum mesajını ve harita simgesini periyodik olarak gönderir.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Özellikler
|
||||
|
||||
- **Sıfır Bağımlılık:** Sadece standart Python 3 kütüphanelerini kullanır (pip ile ek paket yüklemeye gerek yoktur).
|
||||
- **Otomatik Başlangıç:**
|
||||
- **Linux:** `systemd` kullanıcı servisi (`systemd --user`) ve `loginctl linger` kullanır. Bilgisayar açıldığında kullanıcı oturum açmasa dahi arka planda otomatik başlar.
|
||||
- **Windows:** Windows Görev Zamanlayıcı (Task Scheduler) ile kullanıcı oturum açtığında görünmez şekilde (konsol açılmadan) çalışır.
|
||||
- **Passcode Üretici:** Kurulum esnasında çağrı işaretiniz için gerekli olan APRS-IS şifresini (passcode) otomatik hesaplar.
|
||||
- **Düşük Kaynak Tüketimi:** Sürekli TCP bağlantısı açık tutmak yerine belirtilen dakikada bir sunucuya bağlanıp paketi iletir ve bağlantıyı kapatır.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Kurulum ve Çalıştırma
|
||||
|
||||
### 🐧 Linux Kurulumu
|
||||
Bu projeyi tek bir komutla doğrudan kurabilir veya güncelleyebilirsiniz:
|
||||
```bash
|
||||
bash <(curl -sL https://raw.githubusercontent.com/mcturan/aprs/main/install.sh)
|
||||
```
|
||||
*(Kurulum bittikten sonra log takibi için: `tail -f ~/.aprs-beacon/aprs_beacon.log`)*
|
||||
|
||||
---
|
||||
|
||||
### 💻 Windows Kurulumu
|
||||
Bu projeyi tek bir komutla doğrudan kurabilir veya güncelleyebilirsiniz. **PowerShell** ekranında aşağıdaki komutu çalıştırmanız yeterlidir:
|
||||
```powershell
|
||||
PowerShell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/mcturan/aprs/main/install.ps1 | iex"
|
||||
```
|
||||
*(Görev Zamanlayıcıya eklenir. Log takibi için: `Get-Content -Path "$HOME\.aprs-beacon\aprs_beacon.log" -Wait -Tail 10`)*
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Harita Üzerinde İzleme
|
||||
|
||||
Servis ilk paketi gönderdikten sonra, istasyonunuzu canlı olarak [aprs.fi](https://aprs.fi/) üzerinden takip edebilirsiniz:
|
||||
|
||||
👉 `https://aprs.fi/#!call=a%2F<ÇAGRI_İŞARETİNİZ>` (Örn: `https://aprs.fi/#!call=a%2FN0CALL-9`)
|
||||
|
||||
---
|
||||
|
||||
## 🎛️ Çoklu Profil ve Yönetim Arayüzü (APRS Manager)
|
||||
|
||||
Bu güncelleme ile artık aynı anda birden fazla APRS beacon'ını (örneğin farklı SSID veya çağrı işaretleri ile) çalıştırabilir ve bunları görsel arayüzden yönetebilirsiniz.
|
||||
|
||||
### Özellikler:
|
||||
- **Grafik Arayüz (GUI):** Sisteminizdeki tüm profilleri listeler, aktif/pasif durumlarını gösterir, tek tıklamayla başlatıp durdurmanızı sağlar. **Aydınlık (light) temalı, temiz ve modern bir tasarıma sahiptir.**
|
||||
- **APRS Chat (Mesajlaşma):** Her profile ait özel "Chat" butonu ile diğer çağrı işaretlerine veya SMS/E-posta ağ geçitlerine (örneğin SMSGTE) doğrudan APRS kısa mesajı gönderip alabilirsiniz.
|
||||
- **Özelleştirilebilir Perşembe Etkinliği (APRS Thursday):** ANSRVR grubuna perşembe günleri gönderilen katılım mesajı metnini ve saatini her profil için ayrı ayrı özelleştirebilirsiniz.
|
||||
- **Otomatik Güncelleme Kontrolü:** Program günlük olarak arka planda en son sürümü kontrol eder, yeni bir sürüm varsa otomatik yükler ve sizi bilgilendirir.
|
||||
- **Sistem Tepsisi (Tray Icon):** Uygulama kapatıldığında arka planda sistem tepsisinde (görev çubuğunda) çalışmaya devam eder. Hızlıca durumları izleyebilir ve profilleri kontrol edebilirsiniz.
|
||||
- **Log İzleyici:** Her profile ait logları arayüz üzerinden canlı ve anlık olarak takip edebilirsiniz.
|
||||
- **CLI Modu:** GUI kütüphaneleri yüklü olmayan veya grafik arayüzü bulunmayan (headless) sunucularda terminal komutları ile tüm profilleri kolayca yönetebilirsiniz.
|
||||
|
||||
### Kullanım:
|
||||
Yönetim panelini başlatmak için:
|
||||
```bash
|
||||
python3 ~/.aprs-beacon/aprs_manager.py gui
|
||||
```
|
||||
*(Uygulama menüsündeki "APRS Multi-Beacon Manager" kısayolunu da kullanabilirsiniz).*
|
||||
|
||||
Terminal üzerinden (CLI) yönetim komutları:
|
||||
- Profilleri Listele: `python3 ~/.aprs-beacon/aprs_manager.py list`
|
||||
- Yeni Profil Ekle: `python3 ~/.aprs-beacon/aprs_manager.py create`
|
||||
- Profil Düzenle (Güncelle): `python3 ~/.aprs-beacon/aprs_manager.py edit`
|
||||
- Profili Başlat: `python3 ~/.aprs-beacon/aprs_manager.py start <profil_adi>`
|
||||
- Profili Durdur: `python3 ~/.aprs-beacon/aprs_manager.py stop <profil_adi>`
|
||||
- Profili Sil: `python3 ~/.aprs-beacon/aprs_manager.py delete <profil_adi>`
|
||||
- Ayarları Dışa Aktar: `python3 ~/.aprs-beacon/aprs_manager.py export [yedek_yolu.json]`
|
||||
- Ayarları İçe Aktar: `python3 ~/.aprs-beacon/aprs_manager.py import <yedek_yolu.json>`
|
||||
- Uygulamayı Güncelle (Git Pull): `python3 ~/.aprs-beacon/aprs_manager.py update`
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ Tamamen Kaldırma (Uninstall)
|
||||
|
||||
Uygulamayı, arka plan servislerini, zamanlanmış görevleri ve tüm yapılandırma/günlük dosyalarını sisteminizden tamamen temizlemek için aşağıdaki adımları uygulayabilirsiniz:
|
||||
|
||||
### 🐧 Linux (Tek Komutla Kaldırma)
|
||||
Aşağıdaki komutu terminale yapıştırarak tüm servisleri durdurup uygulamayı silebilirsiniz:
|
||||
```bash
|
||||
systemctl --user stop aprs-beacon.service 2>/dev/null
|
||||
systemctl --user disable aprs-beacon.service 2>/dev/null
|
||||
rm -f ~/.config/systemd/user/aprs-beacon.service
|
||||
systemctl --user daemon-reload
|
||||
rm -rf ~/.aprs-beacon
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💻 Windows (PowerShell ile Kaldırma)
|
||||
**PowerShell** ekranında aşağıdaki komutları sırasıyla çalıştırarak zamanlanmış görevi kaldırıp dosyaları temizleyebilirsiniz:
|
||||
```powershell
|
||||
Stop-ScheduledTask -TaskName "APRSBeacon" -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName "APRSBeacon" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "$env:USERPROFILE\.aprs-beacon" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
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()
|
||||
Executable
+2828
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
aprs-beacon-manager:
|
||||
image: ghcr.io/mcturan/aprs:latest
|
||||
container_name: aprs-beacon-manager
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- aprs-data:/root/.aprs-beacon
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
aprs-data:
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
# Windows APRS Beacon Kurulum Sihirbazı
|
||||
# Bu betik, Windows üzerinde arka planda sessizce (konsol penceresi açılmadan)
|
||||
# çalışacak şekilde APRS beacon servisini kurar.
|
||||
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " Windows APRS Multi-Profile Beacon Sihirbazı" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host "Bu sihirbaz, APRS beacon'ınızı yapılandıracak ve bilgisayarınız"
|
||||
Write-Host "başladığında arka planda otomatik çalışması için Görev Zamanlayıcı'ya ekleyecektir."
|
||||
Write-Host "----------------------------------------------------------------`n"
|
||||
|
||||
# Python kontrolü
|
||||
try {
|
||||
$pythonTest = python --version 2>$null
|
||||
if ($null -eq $pythonTest) {
|
||||
throw "Python bulunamadı"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[!] Hata: Sistemde Python yüklü değil veya PATH'e eklenmemiş!" -ForegroundColor Red
|
||||
Write-Host "Lütfen python.org sitesinden Python'ı indirin ve yüklerken 'Add Python to PATH' seçeneğini işaretleyin." -ForegroundColor Yellow
|
||||
Exit
|
||||
}
|
||||
|
||||
$installDir = "$env:USERPROFILE\.aprs-beacon"
|
||||
$profilesDir = "$installDir\profiles"
|
||||
$logsDir = "$installDir\logs"
|
||||
|
||||
if (!(Test-Path $installDir)) { New-Item -ItemType Directory -Force -Path $installDir | Out-Null }
|
||||
if (!(Test-Path $profilesDir)) { New-Item -ItemType Directory -Force -Path $profilesDir | Out-Null }
|
||||
if (!(Test-Path $logsDir)) { New-Item -ItemType Directory -Force -Path $logsDir | Out-Null }
|
||||
if ((Test-Path ".\aprs_beacon.py") -and (Test-Path ".\.git")) {
|
||||
$pwd.Path | Out-File -FilePath "$installDir\.repo_path" -Encoding utf8
|
||||
} else {
|
||||
if (Test-Path "$installDir\.repo_path") {
|
||||
Remove-Item -Path "$installDir\.repo_path" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# GUI Bağımlılıkları Kurulumu (Windows için)
|
||||
Write-Host "[i] Görsel arayüz için kütüphaneler kontrol ediliyor (pystray, Pillow)..." -ForegroundColor Blue
|
||||
try {
|
||||
python -c "import pystray, PIL" 2>$null
|
||||
Write-Host "[+] Gerekli paketler kurulu." -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[!] Gerekli kütüphaneler kuruluyor (pystray, Pillow)..." -ForegroundColor Yellow
|
||||
pip install pystray Pillow --quiet
|
||||
}
|
||||
|
||||
# 0. Profil Adı
|
||||
Write-Host "`n=== 0. Profil Yapılandırması ===" -ForegroundColor Cyan
|
||||
while ($true) {
|
||||
$profileName = (Read-Host "Kurulacak Profil Adı [Varsayılan: default]").Trim().ToLower()
|
||||
if ($profileName -eq "") {
|
||||
$profileName = "default"
|
||||
}
|
||||
if ($profileName -match '^[a-z0-9_-]+$') {
|
||||
break
|
||||
}
|
||||
Write-Host "Hata: Profil adı sadece küçük harf, rakam, tire veya alt çizgi içerebilir!" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# 1. Çağrı İşareti
|
||||
while ($true) {
|
||||
$callsign = (Read-Host "1. Çağrı İşaretiniz (Örn: N0CALL-9)").Trim().ToUpper()
|
||||
if ($callsign -ne "") { break }
|
||||
Write-Host "Hata: Çağrı işareti boş olamaz!" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# 2. Şifre (Passcode)
|
||||
$passcode = (Read-Host "2. Şifreniz (APRS-IS Passcode) [Otomatik hesaplamak için Enter]").Trim()
|
||||
if ($passcode -eq "") {
|
||||
Write-Host "[i] Çağrı işaretinizden şifre otomatik hesaplanıyor..." -ForegroundColor Blue
|
||||
$callsignClean = $callsign.Split("-")[0]
|
||||
$hashVal = 0x73e2
|
||||
for ($i = 0; $i -lt $callsignClean.Length; $i += 2) {
|
||||
$char1 = [int][char]$callsignClean[$i] -shl 8
|
||||
$char2 = 0
|
||||
if (($i + 1) -lt $callsignClean.Length) {
|
||||
$char2 = [int][char]$callsignClean[$i+1]
|
||||
}
|
||||
$hashVal = $hashVal -bxor ($char1 + $char2)
|
||||
}
|
||||
$passcode = $hashVal -band 0x7fff
|
||||
Write-Host "[+] Hesaplanan Şifre: $passcode" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# 3. Mesaj
|
||||
Write-Host "`n3. Durum Mesajı Ayarı:" -ForegroundColor Cyan
|
||||
Write-Host " İpucu: Frekans ve ton bilgisi eklemek için mesajın başına ekleyin (Örn: 145.550MHz T088)" -ForegroundColor Yellow
|
||||
Write-Host " İpucu: Haritada tıklanabilir link göstermek için 'https://' ekleyin (Örn: https://example.com)" -ForegroundColor Yellow
|
||||
$comment = (Read-Host "Durum Mesajınız [Varsayılan: Windows APRS Beacon]").Trim()
|
||||
if ($comment -eq "") {
|
||||
$comment = "Windows APRS Beacon"
|
||||
}
|
||||
|
||||
# 4. Simge (Symbol)
|
||||
Write-Host "`n4. Haritada görünecek Simgeyi seçin:" -ForegroundColor Cyan
|
||||
Write-Host " 1) Helikopter (X) [Varsayılan]"
|
||||
Write-Host " 2) Otomobil / Araba (>)"
|
||||
Write-Host " 3) Yaya / Yürüyüşçü ([)"
|
||||
Write-Host " 4) Ev / QTH İstasyonu (-)"
|
||||
Write-Host " 5) Sabit Telsiz / Cihaz (#)"
|
||||
$symChoice = (Read-Host "Seçiminiz [1-5]").Trim()
|
||||
|
||||
$symbolTable = "/"
|
||||
$symbolCode = "X"
|
||||
switch ($symChoice) {
|
||||
"2" { $symbolCode = ">" }
|
||||
"3" { $symbolCode = "[" }
|
||||
"4" { $symbolCode = "-" }
|
||||
"5" { $symbolCode = "#" }
|
||||
Default { $symbolCode = "X" }
|
||||
}
|
||||
|
||||
# 5. Konum (Location)
|
||||
Write-Host "`n5. Konum ayarları:" -ForegroundColor Cyan
|
||||
$autoLoc = (Read-Host "Sistem konumunuzu internet üzerinden otomatik tespit etsin mi? [Y/n]").Trim().ToLower()
|
||||
|
||||
$latitude = ""
|
||||
$longitude = ""
|
||||
|
||||
if ($autoLoc -ne "n") {
|
||||
Write-Host "[i] Konumunuz internet üzerinden otomatik tespit ediliyor..." -ForegroundColor Blue
|
||||
$apis = @(
|
||||
"http://ip-api.com/json",
|
||||
"https://ipapi.co/json/"
|
||||
)
|
||||
foreach ($api in $apis) {
|
||||
try {
|
||||
$ipLoc = Invoke-RestMethod -Uri $api -TimeoutSec 4
|
||||
if ($ipLoc.status -eq "success" -or $ipLoc.latitude -ne $null) {
|
||||
$lat = if ($ipLoc.lat) { $ipLoc.lat } else { $ipLoc.latitude }
|
||||
$lon = if ($ipLoc.lon) { $ipLoc.lon } else { $ipLoc.longitude }
|
||||
$city = if ($ipLoc.city) { $ipLoc.city } else { "Bilinmiyor" }
|
||||
$country = if ($ipLoc.country_name) { $ipLoc.country_name } else { $ipLoc.country }
|
||||
|
||||
Write-Host "[+] Otomatik Konum Tespit Edildi: $city, $country ($lat, $lon)" -ForegroundColor Green
|
||||
$latitude = $lat
|
||||
$longitude = $lon
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ($latitude -eq "" -or $longitude -eq "") {
|
||||
Write-Host "[!] Otomatik konum tespiti başarısız oldu." -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
if ($latitude -eq "" -or $longitude -eq "") {
|
||||
Write-Host "[i] Koordinatlarınızı kolayca bulabilmeniz için tarayıcıda OpenStreetMap açılıyor..." -ForegroundColor Blue
|
||||
try {
|
||||
Start-Process "https://www.openstreetmap.org"
|
||||
} catch {}
|
||||
|
||||
Write-Host "[!] Lütfen koordinatlarınızı manuel girin (Açılan haritadan enlem/boylam kopyalayın):" -ForegroundColor Yellow
|
||||
while ($true) {
|
||||
$latitude = (Read-Host " Enlem (Latitude, Örn: 41.037002)").Trim()
|
||||
if ([double]::TryParse($latitude, [ref]0.0)) { break }
|
||||
Write-Host "Hata: Geçersiz enlem değeri!" -ForegroundColor Red
|
||||
}
|
||||
while ($true) {
|
||||
$longitude = (Read-Host " Boylam (Longitude, Örn: 28.985012)").Trim()
|
||||
if ([double]::TryParse($longitude, [ref]0.0)) { break }
|
||||
Write-Host "Hata: Geçersiz boylam değeri!" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
# 6. Sıklık (Interval)
|
||||
while ($true) {
|
||||
$intervalInput = (Read-Host "6. Kaç dakikada bir beacon gönderilsin? [Varsayılan: 5]").Trim()
|
||||
if ($intervalInput -eq "") {
|
||||
$interval = 5
|
||||
break
|
||||
}
|
||||
if ([int]::TryParse($intervalInput, [ref]0) -and [int]$intervalInput -ge 1) {
|
||||
$interval = [int]$intervalInput
|
||||
break
|
||||
}
|
||||
Write-Host "Hata: Aralık en az 1 dakika olmalıdır!" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# 6.5 APRS Perşembe Etkinliği
|
||||
$thursdayInput = (Read-Host "6.5 Her Perşembe APRS Perşembe etkinliğine katılım sağlansın mı? (ANSRVR) [y/N]").Trim().ToLower()
|
||||
$aprsThursday = $false
|
||||
if ($thursdayInput -eq "y") {
|
||||
$aprsThursday = $true
|
||||
}
|
||||
|
||||
# Yapılandırmayı JSON olarak kaydet
|
||||
$configJson = @"
|
||||
{
|
||||
"callsign": "$callsign",
|
||||
"passcode": $passcode,
|
||||
"latitude": $latitude,
|
||||
"longitude": $longitude,
|
||||
"use_termux_gps": false,
|
||||
"symbol_table": "$symbolTable",
|
||||
"symbol_code": "$symbolCode",
|
||||
"comment": "$comment",
|
||||
"interval_minutes": $interval,
|
||||
"aprs_thursday": $($aprsThursday.ToString().ToLower()),
|
||||
"server": "rotate.aprs2.net",
|
||||
"port": 14580
|
||||
}
|
||||
"@
|
||||
|
||||
$configJson | Out-File -FilePath "$profilesDir\$profileName.json" -Encoding utf8
|
||||
Write-Host "`n[+] Yapılandırma dosyası kaydedildi: $profilesDir\$profileName.json" -ForegroundColor Green
|
||||
|
||||
# Dosyaları kopyala veya indir
|
||||
if (Test-Path ".\aprs_beacon.py") {
|
||||
Copy-Item -Path ".\aprs_beacon.py" -Destination "$installDir\aprs_beacon.py" -Force
|
||||
} else {
|
||||
Write-Host "[i] aprs_beacon.py dosyası GitHub'dan indiriliyor..." -ForegroundColor Blue
|
||||
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/mcturan/aprs/main/aprs_beacon.py" -OutFile "$installDir\aprs_beacon.py" -UseBasicParsing
|
||||
}
|
||||
|
||||
if (Test-Path ".\aprs_manager.py") {
|
||||
Copy-Item -Path ".\aprs_manager.py" -Destination "$installDir\aprs_manager.py" -Force
|
||||
} else {
|
||||
Write-Host "[i] aprs_manager.py dosyası GitHub'dan indiriliyor..." -ForegroundColor Blue
|
||||
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/mcturan/aprs/main/aprs_manager.py" -OutFile "$installDir\aprs_manager.py" -UseBasicParsing
|
||||
}
|
||||
|
||||
# Geriye dönük uyumluluk (Eski tekli config varsa taşıyalım)
|
||||
if (Test-Path "$installDir\config.json") {
|
||||
Move-Item -Path "$installDir\config.json" -Destination "$profilesDir\default.json" -Force -ErrorAction SilentlyContinue
|
||||
Move-Item -Path "$installDir\aprs_beacon.log" -Destination "$logsDir\default.log" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Test Gönderimi
|
||||
Write-Host "`n[i] Ayarların doğruluğunu onaylamak için test paketi gönderiliyor..." -ForegroundColor Blue
|
||||
python "$installDir\aprs_beacon.py" --profile "$profileName" --once
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "[+] Test Başarılı! Konum paketi APRS-IS ağına iletildi." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[!] Test Başarısız! Paket sunucuya ulaştırılamadı." -ForegroundColor Red
|
||||
$proceed = (Read-Host "Yine de devam etmek istiyor musunuz? [y/N]").Trim().ToLower()
|
||||
if ($proceed -ne "y") {
|
||||
Write-Host "Kurulum iptal edildi."
|
||||
Exit
|
||||
}
|
||||
}
|
||||
|
||||
# 7. Başlangıç Ayarı
|
||||
$autoStart = (Read-Host "`n7. Sistem açılışında otomatik başlasın mı? [Y/n]").Trim().ToLower()
|
||||
|
||||
# Windows Görev Zamanlayıcıya ekleme (Sadece kullanıcı oturum açtığında arka planda çalıştır)
|
||||
if ($autoStart -ne "n") {
|
||||
Write-Host "[i] Görev Zamanlayıcı ayarlanıyor..." -ForegroundColor Blue
|
||||
|
||||
# pythonw.exe konsol ekranı açmadan Python çalıştırır
|
||||
$action = New-ScheduledTaskAction -Execute "pythonw.exe" -Argument "`"$installDir\aprs_beacon.py`" --profile $profileName"
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
|
||||
# Görevi kaydet
|
||||
Register-ScheduledTask -TaskName "APRSBeacon-$profileName" -Action $action -Trigger $trigger -Settings $settings -Description "APRS Background Beacon ($profileName)" -Force | Out-Null
|
||||
|
||||
Write-Host "`n================================================================" -ForegroundColor Green
|
||||
Write-Host " APRS WINDOWS SERVİSİ ($profileName) BAŞARIYLA AKTİF EDİLDİ!" -ForegroundColor Green -Bold
|
||||
Write-Host "================================================================" -ForegroundColor Green
|
||||
Write-Host "Profil Adı : $profileName"
|
||||
Write-Host "Çağrı İşareti : $callsign"
|
||||
Write-Host "Durum : Arka planda sessizce çalışacak (Görev Zamanlayıcı)"
|
||||
Write-Host "Gönderim Sıklığı: $interval dakikada bir"
|
||||
Write-Host "Otomatik Başlama: Kullanıcı oturum açtığında otomatik başlayacak."
|
||||
Write-Host "----------------------------------------------------------------"
|
||||
Write-Host "Yönetim Panelini açmak için: python `"$installDir\aprs_manager.py`" gui"
|
||||
Write-Host "Canlı Log Takibi (PowerShell):"
|
||||
Write-Host " Get-Content -Path `"$logsDir\$profileName.log`" -Wait -Tail 10"
|
||||
Write-Host "================================================================" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[!] Görev zamanlayıcı kurulumu atlandı." -ForegroundColor Yellow
|
||||
}
|
||||
Executable
+362
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Terminal Colors
|
||||
C_GREEN='\033[92m'
|
||||
C_YELLOW='\033[93m'
|
||||
C_BLUE='\033[94m'
|
||||
C_CYAN='\033[96m'
|
||||
C_RED='\033[91m'
|
||||
C_BOLD='\033[1m'
|
||||
C_RESET='\033[0m'
|
||||
|
||||
clear
|
||||
echo -e "${C_CYAN}${C_BOLD}"
|
||||
echo " _ ___ ___ ___ ___ "
|
||||
echo " /_\ | _ \| _ \/ __| | _ ) ___ __ _ __ ___ _ _ "
|
||||
echo " / _ \| _/| /\__ \ | _ \/ -_) _\` / _/ _ \ ' \ "
|
||||
echo "/_/ \_\_| |_|_\|___/ |___/\___\__,_\__\___/_||_| "
|
||||
echo -e " Linux/Android APRS Multi-Profile Beacon Setup Wizard${C_RESET}\n"
|
||||
echo "----------------------------------------------------------------"
|
||||
echo "This wizard will help you configure your APRS beacon as a"
|
||||
echo "background service and manage it via the control panel."
|
||||
echo "----------------------------------------------------------------\n"
|
||||
|
||||
# Check and install Python3
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo -e "${C_YELLOW}[!] Python3 not found on the system. Installing...${C_RESET}"
|
||||
if command -v apt-get &> /dev/null; then
|
||||
sudo apt-get update && sudo apt-get install -y python3
|
||||
else
|
||||
echo -e "${C_RED}[!] Error: Python3 could not be installed automatically. Please install it manually.${C_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
INSTALL_DIR="$HOME/.aprs-beacon"
|
||||
PROFILES_DIR="$INSTALL_DIR/profiles"
|
||||
LOGS_DIR="$INSTALL_DIR/logs"
|
||||
mkdir -p "$PROFILES_DIR" "$LOGS_DIR"
|
||||
echo "$(pwd)" > "$INSTALL_DIR/.repo_path"
|
||||
|
||||
# GUI Dependencies Installation (for Linux)
|
||||
HAS_GUI=true
|
||||
# Graphic environment check
|
||||
if [ -z "$DISPLAY" ]; then
|
||||
HAS_GUI=false
|
||||
echo -e "${C_YELLOW}[!] Display environment (DISPLAY) not detected. Performing headless install.${C_RESET}"
|
||||
fi
|
||||
|
||||
if [ "$HAS_GUI" = true ]; then
|
||||
echo -e "${C_BLUE}[i] Checking GUI, System Tray, and Image libraries (pystray, tkinter, pillow)...${C_RESET}"
|
||||
if ! python3 -c "import tkinter; import pystray; import PIL" &>/dev/null; then
|
||||
echo -e "${C_YELLOW}[!] Required GUI packages are missing. Installing...${C_RESET}"
|
||||
if command -v apt-get &> /dev/null; then
|
||||
echo -e "${C_BLUE}[i] Administrative privileges (sudo) may be requested:${C_RESET}"
|
||||
sudo apt-get update && sudo apt-get install -y python3-tk python3-pystray python3-pil
|
||||
else
|
||||
echo -e "${C_RED}[!] Error: Package manager (apt-get) not found. Please install python3-tk, python3-pystray, and python3-pil manually.${C_RESET}"
|
||||
fi
|
||||
else
|
||||
echo -e "${C_GREEN}[+] Required GUI packages are ready.${C_RESET}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 0. Profile Name
|
||||
echo -e "\n=== 0. Profile Configuration ==="
|
||||
while true; do
|
||||
read -p "Profile Name to Install (e.g. mobile, qth, default) [Default: default]: " PROFILE_NAME
|
||||
PROFILE_NAME=$(echo "$PROFILE_NAME" | tr 'A-Z' 'a-z' | xargs)
|
||||
if [ -z "$PROFILE_NAME" ]; then
|
||||
PROFILE_NAME="default"
|
||||
fi
|
||||
if [[ "$PROFILE_NAME" =~ ^[a-z0-9_-]+$ ]]; then
|
||||
break
|
||||
fi
|
||||
echo -e "${C_RED}Error: Profile name can only contain lowercase letters, numbers, hyphens, or underscores!${C_RESET}"
|
||||
done
|
||||
|
||||
# 1. Callsign
|
||||
while true; do
|
||||
read -p "1. Callsign (e.g. N0CALL-9): " CALLSIGN
|
||||
CALLSIGN=$(echo "$CALLSIGN" | tr 'a-z' 'A-Z' | xargs)
|
||||
if [ -n "$CALLSIGN" ]; then
|
||||
break
|
||||
fi
|
||||
echo -e "${C_RED}Error: Callsign cannot be empty!${C_RESET}"
|
||||
done
|
||||
|
||||
# 2. APRS-IS Passcode
|
||||
read -p "2. Password (APRS-IS Passcode) [Press Enter to calculate automatically]: " PASSCODE
|
||||
PASSCODE=$(echo "$PASSCODE" | xargs)
|
||||
if [ -z "$PASSCODE" ]; then
|
||||
echo -e "${C_BLUE}[i] Calculating passcode automatically from callsign...${C_RESET}"
|
||||
PASSCODE=$(python3 -c "
|
||||
callsign = '${CALLSIGN}'.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)
|
||||
print(hash_val & 0x7fff)
|
||||
")
|
||||
echo -e "${C_GREEN}[+] Calculated Passcode: $PASSCODE${C_RESET}"
|
||||
fi
|
||||
|
||||
# 3. Status Comment
|
||||
echo -e "\n3. Status Comment Setting:"
|
||||
echo -e " ${C_YELLOW}Tip: Add frequency and tone info at start of message (e.g. 145.550MHz T088)${C_RESET}"
|
||||
echo -e " ${C_YELLOW}Tip: Add 'https://' to show a clickable link on the map (e.g. https://example.com)${C_RESET}"
|
||||
read -p "Status Comment [Default: APRS Background Beacon]: " COMMENT
|
||||
COMMENT=$(echo "$COMMENT" | xargs)
|
||||
if [ -z "$COMMENT" ]; then
|
||||
COMMENT="APRS Background Beacon"
|
||||
fi
|
||||
|
||||
# 4. Symbol
|
||||
echo -e "\n4. Select Symbol to Display on Map:"
|
||||
echo " 1) Helicopter (X) [Default]"
|
||||
echo " 2) Car / Vehicle (>)"
|
||||
echo " 3) Pedestrian / Hiker ([)"
|
||||
echo " 4) House / QTH Station (-)"
|
||||
echo " 5) Fixed Radio / Station (#)"
|
||||
echo " 6) Other (Enter your own character)"
|
||||
read -p "Choice [1-6]: " SYM_CHOICE
|
||||
SYM_CHOICE=$(echo "$SYM_CHOICE" | xargs)
|
||||
|
||||
SYMBOL_TABLE="/"
|
||||
SYMBOL_CODE="X"
|
||||
|
||||
case "$SYM_CHOICE" in
|
||||
2) SYMBOL_CODE=">" ;;
|
||||
3) SYMBOL_CODE="[" ;;
|
||||
4) SYMBOL_CODE="-" ;;
|
||||
5) SYMBOL_CODE="#" ;;
|
||||
6)
|
||||
read -p "Enter Symbol Character (e.g. > or [ or X): " CUSTOM_SYM
|
||||
SYMBOL_CODE=$(echo "$CUSTOM_SYM" | xargs)
|
||||
if [ -z "$SYMBOL_CODE" ]; then
|
||||
SYMBOL_CODE="X"
|
||||
fi
|
||||
;;
|
||||
*) SYMBOL_CODE="X" ;;
|
||||
esac
|
||||
|
||||
LATITUDE=""
|
||||
LONGITUDE=""
|
||||
|
||||
# 5. Coordinates
|
||||
read -p "5. Do you want to automatically detect your coordinates via IP? [Y/n]: " AUTO_LOC
|
||||
AUTO_LOC=$(echo "$AUTO_LOC" | tr 'A-Z' 'a-z' | xargs)
|
||||
|
||||
if [ "$AUTO_LOC" != "n" ]; then
|
||||
echo -e "${C_BLUE}[i] Detecting coordinates via IP...${C_RESET}"
|
||||
IP_LOC=$(python3 -c "
|
||||
import urllib.request, json
|
||||
urls = ['http://ip-api.com/json', 'https://ipapi.co/json/']
|
||||
for url in urls:
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req, timeout=4) as r:
|
||||
d = json.loads(r.read().decode('utf-8'))
|
||||
if d.get('status') == 'success' or 'latitude' in d:
|
||||
lat = d.get('lat') or d.get('latitude')
|
||||
lon = d.get('lon') or d.get('longitude')
|
||||
city = d.get('city') or 'Unknown'
|
||||
country = d.get('country_name') or d.get('country') or 'Unknown'
|
||||
print(f'{lat},{lon},{city},{country}')
|
||||
break
|
||||
except:
|
||||
continue
|
||||
")
|
||||
if [ -n "$IP_LOC" ]; then
|
||||
IFS=',' read -r LAT LON CITY COUNTRY <<< "$IP_LOC"
|
||||
echo -e "${C_GREEN}[+] Automatically Detected Location: $CITY, $COUNTRY ($LAT, $LON)${C_RESET}"
|
||||
LATITUDE="$LAT"
|
||||
LONGITUDE="$LON"
|
||||
else
|
||||
echo -e "${C_RED}[!] IP location detection failed.${C_RESET}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$LATITUDE" ] || [ -z "$LONGITUDE" ]; then
|
||||
echo -e "${C_BLUE}[i] Opening OpenStreetMap in browser to help find coordinates...${C_RESET}"
|
||||
python3 -m webbrowser "https://www.openstreetmap.org" &>/dev/null &
|
||||
|
||||
echo -e "${C_YELLOW}[!] Please enter coordinates manually (copy latitude/longitude from map):${C_RESET}"
|
||||
while true; do
|
||||
read -p " Latitude (e.g. 41.037002): " LATITUDE
|
||||
LATITUDE=$(echo "$LATITUDE" | xargs)
|
||||
if python3 -c "float('$LATITUDE')" &>/dev/null; then
|
||||
break
|
||||
fi
|
||||
echo -e "${C_RED}Error: Invalid latitude value!${C_RESET}"
|
||||
done
|
||||
while true; do
|
||||
read -p " Longitude (e.g. 28.985012): " LONGITUDE
|
||||
LONGITUDE=$(echo "$LONGITUDE" | xargs)
|
||||
if python3 -c "float('$LONGITUDE')" &>/dev/null; then
|
||||
break
|
||||
fi
|
||||
echo -e "${C_RED}Error: Invalid longitude value!${C_RESET}"
|
||||
done
|
||||
fi
|
||||
|
||||
# 6. Interval
|
||||
while true; do
|
||||
read -p "6. How often should beacons be sent (minutes)? [Default: 5]: " INTERVAL_MINUTES
|
||||
INTERVAL_MINUTES=$(echo "$INTERVAL_MINUTES" | xargs)
|
||||
if [ -z "$INTERVAL_MINUTES" ]; then
|
||||
INTERVAL_MINUTES=5
|
||||
break
|
||||
fi
|
||||
if python3 -c "int('$INTERVAL_MINUTES') >= 1" &>/dev/null; then
|
||||
break
|
||||
fi
|
||||
echo -e "${C_RED}Error: Interval must be at least 1 minute!${C_RESET}"
|
||||
done
|
||||
|
||||
# 6.5 APRS Thursday
|
||||
read -p "6.5 Join APRS Thursday event? (ANSRVR) [y/N]: " OPT_THURSDAY
|
||||
OPT_THURSDAY=$(echo "$OPT_THURSDAY" | tr 'A-Z' 'a-z' | xargs)
|
||||
APRS_THURSDAY="false"
|
||||
if [ "$OPT_THURSDAY" = "y" ]; then
|
||||
APRS_THURSDAY="true"
|
||||
fi
|
||||
|
||||
# Save Configuration
|
||||
cat <<EOF > "$PROFILES_DIR/$PROFILE_NAME.json"
|
||||
{
|
||||
"callsign": "$CALLSIGN",
|
||||
"passcode": $PASSCODE,
|
||||
"latitude": $LATITUDE,
|
||||
"longitude": $LONGITUDE,
|
||||
"use_termux_gps": false,
|
||||
"symbol_table": "$SYMBOL_TABLE",
|
||||
"symbol_code": "$SYMBOL_CODE",
|
||||
"comment": "$COMMENT",
|
||||
"interval_minutes": $INTERVAL_MINUTES,
|
||||
"aprs_thursday": $APRS_THURSDAY,
|
||||
"server": "rotate.aprs2.net",
|
||||
"port": 14580
|
||||
}
|
||||
EOF
|
||||
|
||||
echo -e "\n${C_GREEN}[+] Configuration file saved: $PROFILES_DIR/$PROFILE_NAME.json${C_RESET}"
|
||||
|
||||
# Copy / Download files
|
||||
echo -e "${C_BLUE}[i] Installing application scripts...${C_RESET}"
|
||||
if [ -f "./aprs_beacon.py" ]; then
|
||||
cp "./aprs_beacon.py" "$INSTALL_DIR/aprs_beacon.py"
|
||||
else
|
||||
curl -sL -o "$INSTALL_DIR/aprs_beacon.py" "https://raw.githubusercontent.com/mcturan/aprs/main/aprs_beacon.py"
|
||||
fi
|
||||
chmod +x "$INSTALL_DIR/aprs_beacon.py"
|
||||
|
||||
if [ -f "./aprs_manager.py" ]; then
|
||||
cp "./aprs_manager.py" "$INSTALL_DIR/aprs_manager.py"
|
||||
else
|
||||
curl -sL -o "$INSTALL_DIR/aprs_manager.py" "https://raw.githubusercontent.com/mcturan/aprs/main/aprs_manager.py"
|
||||
fi
|
||||
chmod +x "$INSTALL_DIR/aprs_manager.py"
|
||||
|
||||
# Backward compatibility (migration of old single config)
|
||||
if [ -f "$INSTALL_DIR/config.json" ]; then
|
||||
mv "$INSTALL_DIR/config.json" "$PROFILES_DIR/default.json" 2>/dev/null
|
||||
mv "$INSTALL_DIR/aprs_beacon.log" "$LOGS_DIR/default.log" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Send Test Beacon
|
||||
echo -e "\n${C_BLUE}[i] Sending test beacon to verify settings...${C_RESET}"
|
||||
python3 "$INSTALL_DIR/aprs_beacon.py" --profile "$PROFILE_NAME" --once
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${C_GREEN}[+] Test Successful! Position packet transmitted to APRS-IS.${C_RESET}"
|
||||
else
|
||||
echo -e "${C_RED}[!] Test Failed! Could not transmit packet to server.${C_RESET}"
|
||||
echo -e "${C_YELLOW}[i] For details: cat $LOGS_DIR/$PROFILE_NAME.log${C_RESET}"
|
||||
read -p "Do you want to proceed anyway? [y/N]: " PROCEED
|
||||
PROCEED=$(echo "$PROCEED" | tr 'A-Z' 'a-z' | xargs)
|
||||
if [ "$PROCEED" != "y" ]; then
|
||||
echo "Setup cancelled."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Linux systemd startup
|
||||
echo -e "\n7. Startup Settings (Linux):"
|
||||
read -p "Start this profile automatically on system boot? [Y/n]: " AUTO_START
|
||||
AUTO_START=$(echo "$AUTO_START" | tr 'A-Z' 'a-z' | xargs)
|
||||
|
||||
# Generate systemd user service template
|
||||
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
|
||||
mkdir -p "$SYSTEMD_USER_DIR"
|
||||
|
||||
cat <<EOF > "$SYSTEMD_USER_DIR/aprs-beacon@.service"
|
||||
[Unit]
|
||||
Description=APRS Background Beacon Service (%i)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$(which python3) $INSTALL_DIR/aprs_beacon.py --profile %i
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
systemctl --user daemon-reload
|
||||
|
||||
if [ "$AUTO_START" != "n" ]; then
|
||||
echo -e "${C_BLUE}[i] Enabling and starting daemon service...${C_RESET}"
|
||||
systemctl --user enable aprs-beacon@$PROFILE_NAME.service
|
||||
systemctl --user restart aprs-beacon@$PROFILE_NAME.service
|
||||
loginctl enable-linger "$USER" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Create desktop shortcut launcher (if GUI available)
|
||||
if [ "$HAS_GUI" = true ]; then
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
mkdir -p "$DESKTOP_DIR"
|
||||
cat <<EOF > "$DESKTOP_DIR/aprs-manager.desktop"
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=APRS Multi-Beacon Manager
|
||||
Comment=APRS Multi-Profile Background Beacon Manager
|
||||
Exec=$INSTALL_DIR/aprs_manager.py gui
|
||||
Icon=radio
|
||||
Terminal=false
|
||||
Categories=Utility;Network;
|
||||
EOF
|
||||
chmod +x "$DESKTOP_DIR/aprs-manager.desktop"
|
||||
|
||||
# Add manager to session autostart
|
||||
AUTOSTART_DIR="$HOME/.config/autostart"
|
||||
mkdir -p "$AUTOSTART_DIR"
|
||||
cp "$DESKTOP_DIR/aprs-manager.desktop" "$AUTOSTART_DIR/"
|
||||
fi
|
||||
|
||||
# Post-install info summary
|
||||
echo -e "\n${C_GREEN}${C_BOLD}================================================================${C_RESET}"
|
||||
echo -e "${C_GREEN}${C_BOLD} APRS PROFILE ($PROFILE_NAME) SUCCESSFULLY ACTIVATED!${C_RESET}"
|
||||
echo -e "${C_GREEN}${C_BOLD}================================================================${C_RESET}"
|
||||
echo -e "${C_BOLD}Profile Name :${C_RESET} $PROFILE_NAME"
|
||||
echo -e "${C_BOLD}Callsign :${C_RESET} $CALLSIGN"
|
||||
echo -e "${C_BOLD}Interval :${C_RESET} Every $INTERVAL_MINUTES minute(s)"
|
||||
echo -e "${C_BOLD}APRS Thursday :${C_RESET} $([ "$APRS_THURSDAY" = "true" ] && echo "Enabled" || echo "Disabled")"
|
||||
echo -e "${C_BOLD}Autostart on Boot:${C_RESET} Yes"
|
||||
echo -e "----------------------------------------------------------------"
|
||||
if [ "$HAS_GUI" = true ]; then
|
||||
echo -e "${C_CYAN}Desktop Interface :${C_RESET} Launch 'APRS Multi-Beacon Manager' from application menu."
|
||||
echo -e "${C_CYAN}System Tray Icon :${C_RESET} System tray icon will appear upon launching."
|
||||
else
|
||||
echo -e "${C_YELLOW}Note: No graphic display detected. Launch control panel from terminal.${C_RESET}"
|
||||
fi
|
||||
echo -e "${C_CYAN}CLI / Terminal Management:${C_RESET}"
|
||||
echo -e " - Open Interactive Menu: python3 $INSTALL_DIR/aprs_manager.py"
|
||||
echo -e " - List Beacon Profiles: python3 $INSTALL_DIR/aprs_manager.py list"
|
||||
echo -e " - Start Profile Daemon: python3 $INSTALL_DIR/aprs_manager.py start $PROFILE_NAME"
|
||||
echo -e " - Stop Profile Daemon: python3 $INSTALL_DIR/aprs_manager.py stop $PROFILE_NAME"
|
||||
echo -e "${C_GREEN}${C_BOLD}================================================================${C_RESET}\n"
|
||||
File diff suppressed because it is too large
Load Diff
+414
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from flask import Flask, jsonify, request, render_template
|
||||
|
||||
# Ensure path to local modules is set
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import configuration constants and helper functions from aprs_manager
|
||||
from aprs_manager import (
|
||||
BASE_DIR, PROFILES_DIR, LOGS_DIR,
|
||||
get_profiles, save_profile, delete_profile_files,
|
||||
generate_aprs_passcode, send_aprs_message, get_packet_count,
|
||||
is_service_enabled, set_service_enabled
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Subprocess tracking for Docker / Non-systemd environments
|
||||
PIDS_FILE = os.path.join(BASE_DIR, 'pids.json')
|
||||
chat_history = {} # callsign -> list of message dicts
|
||||
active_listeners = {} # callsign -> { 'thread': t, 'socket': s, 'last_poll': timestamp }
|
||||
|
||||
def get_running_subprocesses():
|
||||
if not os.path.exists(PIDS_FILE):
|
||||
return {}
|
||||
try:
|
||||
with open(PIDS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return {}
|
||||
|
||||
def save_running_subprocesses(pids):
|
||||
try:
|
||||
with open(PIDS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(pids, f, indent=4)
|
||||
except:
|
||||
pass
|
||||
|
||||
def is_subprocess_running(profile_name):
|
||||
pids = get_running_subprocesses()
|
||||
pid = pids.get(profile_name)
|
||||
if not pid:
|
||||
return False
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
if profile_name in pids:
|
||||
del pids[profile_name]
|
||||
save_running_subprocesses(pids)
|
||||
return False
|
||||
|
||||
def is_docker():
|
||||
return os.path.exists('/.dockerenv') or os.environ.get('DOCKER_MODE') == 'true'
|
||||
|
||||
def is_profile_running(profile_name):
|
||||
if is_docker():
|
||||
return is_subprocess_running(profile_name)
|
||||
try:
|
||||
res = subprocess.run(['systemctl', '--user', 'is-active', f'aprs-beacon@{profile_name}.service'], capture_output=True, text=True)
|
||||
return res.stdout.strip() == 'active'
|
||||
except:
|
||||
return is_subprocess_running(profile_name)
|
||||
|
||||
def start_profile_service(profile_name):
|
||||
if is_docker():
|
||||
if is_subprocess_running(profile_name):
|
||||
return True
|
||||
log_file = os.path.join(LOGS_DIR, f'{profile_name}.log')
|
||||
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
||||
|
||||
beacon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'aprs_beacon.py')
|
||||
if not os.path.exists(beacon_path):
|
||||
beacon_path = os.path.join(BASE_DIR, 'aprs_beacon.py')
|
||||
|
||||
with open(log_file, 'a') as log_out:
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, beacon_path, '--profile', profile_name],
|
||||
stdout=log_out,
|
||||
stderr=log_out,
|
||||
start_new_session=True
|
||||
)
|
||||
pids = get_running_subprocesses()
|
||||
pids[profile_name] = proc.pid
|
||||
save_running_subprocesses(pids)
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
from aprs_manager import ensure_linux_systemd_template
|
||||
ensure_linux_systemd_template()
|
||||
subprocess.run(['systemctl', '--user', 'enable', f'aprs-beacon@{profile_name}.service'], check=True)
|
||||
subprocess.run(['systemctl', '--user', 'start', f'aprs-beacon@{profile_name}.service'], check=True)
|
||||
return True
|
||||
except:
|
||||
# Fallback to subprocess
|
||||
return start_profile_service(profile_name)
|
||||
|
||||
def stop_profile_service(profile_name):
|
||||
if is_docker():
|
||||
pids = get_running_subprocesses()
|
||||
pid = pids.get(profile_name)
|
||||
if pid:
|
||||
try:
|
||||
import signal
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
time.sleep(0.5)
|
||||
except OSError:
|
||||
pass
|
||||
if profile_name in pids:
|
||||
del pids[profile_name]
|
||||
save_running_subprocesses(pids)
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
subprocess.run(['systemctl', '--user', 'stop', f'aprs-beacon@{profile_name}.service'])
|
||||
subprocess.run(['systemctl', '--user', 'disable', f'aprs-beacon@{profile_name}.service'])
|
||||
return True
|
||||
except:
|
||||
return stop_profile_service(profile_name)
|
||||
|
||||
# --- Chat Listener Management ---
|
||||
def add_chat_msg(my_callsign, sender, text):
|
||||
my_callsign = my_callsign.upper()
|
||||
if my_callsign not in chat_history:
|
||||
chat_history[my_callsign] = []
|
||||
chat_history[my_callsign].append({
|
||||
"timestamp": datetime.now().strftime('%H:%M:%S'),
|
||||
"sender": sender,
|
||||
"text": text
|
||||
})
|
||||
|
||||
def parse_incoming_line(my_callsign, line):
|
||||
if "::" in line:
|
||||
try:
|
||||
parts = line.split("::", 1)
|
||||
header = parts[0]
|
||||
payload = parts[1]
|
||||
sender = header.split(">", 1)[0].strip()
|
||||
if ":" in payload:
|
||||
recipient_part, msg_text = payload.split(":", 1)
|
||||
recipient = recipient_part.strip()
|
||||
msg_text = msg_text.strip()
|
||||
if recipient.upper() == my_callsign.upper():
|
||||
add_chat_msg(my_callsign, sender, msg_text)
|
||||
except:
|
||||
pass
|
||||
|
||||
def chat_listener_worker(callsign, passcode):
|
||||
callsign = callsign.upper()
|
||||
server = "rotate.aprs2.net"
|
||||
port = 14580
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10.0)
|
||||
sock.connect((server, port))
|
||||
|
||||
if callsign in active_listeners:
|
||||
active_listeners[callsign]['socket'] = sock
|
||||
|
||||
sock.recv(1024)
|
||||
login_str = f"user {callsign} pass {passcode} vers PyAPRSWeb 1.0 filter b/{callsign}\r\n"
|
||||
sock.sendall(login_str.encode('utf-8'))
|
||||
sock.recv(1024)
|
||||
|
||||
add_chat_msg(callsign, "SYSTEM", f"Connected as {callsign}. Real-time chat active.")
|
||||
|
||||
sock.settimeout(None)
|
||||
buffer = ""
|
||||
while callsign in active_listeners:
|
||||
# Garbage collect if client hasn't polled in 30 seconds
|
||||
if time.time() - active_listeners[callsign]['last_poll'] > 30.0:
|
||||
add_chat_msg(callsign, "SYSTEM", "Chat connection closed due to inactivity.")
|
||||
break
|
||||
|
||||
data = sock.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
buffer += data.decode('utf-8', errors='ignore')
|
||||
while "\n" in buffer:
|
||||
line, buffer = buffer.split("\n", 1)
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parse_incoming_line(callsign, line)
|
||||
except Exception as e:
|
||||
add_chat_msg(callsign, "SYSTEM", f"Connection closed: {e}")
|
||||
finally:
|
||||
if sock:
|
||||
try:
|
||||
sock.close()
|
||||
except:
|
||||
pass
|
||||
if callsign in active_listeners:
|
||||
del active_listeners[callsign]
|
||||
|
||||
def manage_chat_listener(callsign, passcode):
|
||||
callsign = callsign.upper()
|
||||
if callsign in active_listeners:
|
||||
active_listeners[callsign]['last_poll'] = time.time()
|
||||
return
|
||||
|
||||
t = threading.Thread(target=chat_listener_worker, args=(callsign, passcode), daemon=True)
|
||||
active_listeners[callsign] = {
|
||||
'thread': t,
|
||||
'socket': None,
|
||||
'last_poll': time.time()
|
||||
}
|
||||
t.start()
|
||||
|
||||
# --- Flask Endpoints ---
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/status')
|
||||
def get_global_status():
|
||||
profiles = get_profiles()
|
||||
active_count = sum(1 for name in profiles.keys() if is_profile_running(name))
|
||||
|
||||
# Check APRS server latency
|
||||
latency = "Offline"
|
||||
try:
|
||||
start = time.time()
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(2.0)
|
||||
sock.connect(("rotate.aprs2.net", 14580))
|
||||
sock.close()
|
||||
latency = f"{int((time.time() - start) * 1000)}ms"
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
"total_profiles": len(profiles),
|
||||
"active_beacons": active_count,
|
||||
"latency": latency
|
||||
})
|
||||
|
||||
@app.route('/api/profiles')
|
||||
def api_profiles():
|
||||
profiles = get_profiles()
|
||||
result = []
|
||||
for name, data in profiles.items():
|
||||
running = is_profile_running(name)
|
||||
enabled = is_service_enabled(name)
|
||||
packets = get_packet_count(name)
|
||||
|
||||
result.append({
|
||||
"name": name,
|
||||
"callsign": data.get("callsign", ""),
|
||||
"passcode": data.get("passcode", ""),
|
||||
"latitude": data.get("latitude", 0),
|
||||
"longitude": data.get("longitude", 0),
|
||||
"symbol": data.get("symbol_code", "X"),
|
||||
"comment": data.get("comment", ""),
|
||||
"interval": data.get("interval_minutes", 5),
|
||||
"aprs_thursday": data.get("aprs_thursday", False),
|
||||
"aprs_thursday_time": data.get("aprs_thursday_time", "20:00"),
|
||||
"aprs_thursday_msg": data.get("aprs_thursday_msg", "CQ HOTG 73 FROM TURKIYE #APRSTHURSDAY"),
|
||||
"running": running,
|
||||
"autostart": enabled,
|
||||
"packets_sent": packets
|
||||
})
|
||||
return jsonify(result)
|
||||
|
||||
@app.route('/api/profiles/save', methods=['POST'])
|
||||
def api_save_profile():
|
||||
req = request.json
|
||||
name = req.get("name", "").strip().lower()
|
||||
callsign = req.get("callsign", "").strip().upper()
|
||||
passcode_in = req.get("passcode", "")
|
||||
lat = float(req.get("latitude", 0))
|
||||
lon = float(req.get("longitude", 0))
|
||||
comment = req.get("comment", "").strip() or "APRS Background Beacon"
|
||||
symbol = req.get("symbol", "X").strip() or "X"
|
||||
interval = int(req.get("interval", 5))
|
||||
aprs_thursday = bool(req.get("aprs_thursday", False))
|
||||
aprs_thursday_time = req.get("aprs_thursday_time", "20:00").strip()
|
||||
aprs_thursday_msg = req.get("aprs_thursday_msg", "").strip() or "CQ HOTG 73 FROM TURKIYE #APRSTHURSDAY"
|
||||
|
||||
if not name or not callsign:
|
||||
return jsonify({"success": False, "message": "Profile name and callsign are required."}), 400
|
||||
|
||||
if not passcode_in:
|
||||
passcode = generate_aprs_passcode(callsign)
|
||||
else:
|
||||
try:
|
||||
passcode = int(passcode_in)
|
||||
except:
|
||||
return jsonify({"success": False, "message": "Invalid passcode format."}), 400
|
||||
|
||||
data = {
|
||||
"callsign": callsign,
|
||||
"passcode": passcode,
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"use_termux_gps": False,
|
||||
"symbol_table": "/",
|
||||
"symbol_code": symbol,
|
||||
"comment": comment,
|
||||
"interval_minutes": interval,
|
||||
"aprs_thursday": aprs_thursday,
|
||||
"aprs_thursday_time": aprs_thursday_time,
|
||||
"aprs_thursday_msg": aprs_thursday_msg,
|
||||
"server": "rotate.aprs2.net",
|
||||
"port": 14580
|
||||
}
|
||||
|
||||
was_running = is_profile_running(name)
|
||||
save_profile(name, data)
|
||||
|
||||
# Apply autostart configurations
|
||||
autostart = bool(req.get("autostart", False))
|
||||
set_service_enabled(name, autostart)
|
||||
|
||||
if was_running:
|
||||
stop_profile_service(name)
|
||||
time.sleep(0.5)
|
||||
start_profile_service(name)
|
||||
|
||||
return jsonify({"success": True, "message": f"Profile '{name}' saved successfully."})
|
||||
|
||||
@app.route('/api/profiles/<name>/delete', methods=['POST'])
|
||||
def api_delete_profile(name):
|
||||
stop_profile_service(name)
|
||||
delete_profile_files(name)
|
||||
return jsonify({"success": True, "message": f"Profile '{name}' deleted."})
|
||||
|
||||
@app.route('/api/profiles/<name>/toggle', methods=['POST'])
|
||||
def api_toggle_profile(name):
|
||||
running = is_profile_running(name)
|
||||
if running:
|
||||
success = stop_profile_service(name)
|
||||
msg = f"Profile '{name}' stopped."
|
||||
else:
|
||||
success = start_profile_service(name)
|
||||
msg = f"Profile '{name}' started."
|
||||
return jsonify({"success": success, "message": msg})
|
||||
|
||||
@app.route('/api/profiles/<name>/autostart', methods=['POST'])
|
||||
def api_autostart_profile(name):
|
||||
req = request.json
|
||||
enable = bool(req.get("enable", False))
|
||||
set_service_enabled(name, enable)
|
||||
return jsonify({"success": True, "message": f"Autostart {'enabled' if enable else 'disabled'} for '{name}'."})
|
||||
|
||||
@app.route('/api/profiles/<name>/logs')
|
||||
def api_profile_logs(name):
|
||||
log_file = os.path.join(LOGS_DIR, f'{name}.log')
|
||||
if not os.path.exists(log_file):
|
||||
return jsonify({"logs": "No logs found for this profile yet."})
|
||||
try:
|
||||
with open(log_file, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()[-100:]
|
||||
return jsonify({"logs": "".join(lines)})
|
||||
except Exception as e:
|
||||
return jsonify({"logs": f"Error reading logs: {e}"})
|
||||
|
||||
@app.route('/api/chat/messages')
|
||||
def api_chat_messages():
|
||||
profile = request.args.get("profile", "").strip().lower()
|
||||
profiles = get_profiles()
|
||||
if profile not in profiles:
|
||||
return jsonify({"success": False, "message": "Profile not found."}), 404
|
||||
|
||||
data = profiles[profile]
|
||||
callsign = data.get("callsign", "").upper()
|
||||
passcode = data.get("passcode")
|
||||
if not passcode:
|
||||
passcode = generate_aprs_passcode(callsign)
|
||||
|
||||
manage_chat_listener(callsign, passcode)
|
||||
|
||||
msgs = chat_history.get(callsign, [])
|
||||
return jsonify(msgs)
|
||||
|
||||
@app.route('/api/chat/send', methods=['POST'])
|
||||
def api_chat_send():
|
||||
req = request.json
|
||||
profile = req.get("profile", "").strip().lower()
|
||||
to_callsign = req.get("to_callsign", "").strip().upper()
|
||||
message = req.get("message", "").strip()
|
||||
|
||||
profiles = get_profiles()
|
||||
if profile not in profiles:
|
||||
return jsonify({"success": False, "message": "Profile not found."}), 404
|
||||
|
||||
data = profiles[profile]
|
||||
from_callsign = data.get("callsign", "").upper()
|
||||
passcode = data.get("passcode")
|
||||
if not passcode:
|
||||
passcode = generate_aprs_passcode(from_callsign)
|
||||
|
||||
if not to_callsign or not message:
|
||||
return jsonify({"success": False, "message": "Recipient and message text are required."}), 400
|
||||
|
||||
success, res_msg = send_aprs_message(from_callsign, passcode, to_callsign, message)
|
||||
if success:
|
||||
add_chat_msg(from_callsign, "YOU", f"To {to_callsign}: {message}")
|
||||
return jsonify({"success": True})
|
||||
else:
|
||||
return jsonify({"success": False, "message": res_msg})
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.environ.get("PORT", 8080))
|
||||
app.run(host='0.0.0.0', port=port, debug=False)
|
||||
Reference in New Issue
Block a user