-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAPRS_Groundstation.py
More file actions
184 lines (153 loc) · 6.58 KB
/
Copy pathAPRS_Groundstation.py
File metadata and controls
184 lines (153 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import tkinter as tk
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import re
import os
import subprocess
import signal
from datetime import datetime
class APRSTelemetryGUI:
def __init__(self, root, log_file, bash_proc):
self.root = root
self.root.title("APRS Telemetry Display")
self.bash_proc = bash_proc
# Data storage
self.times = []
self.altitudes = []
self.velocities = []
self.lats = []
self.lons = []
self.last_alt = None
self.log_file = log_file
self.file_pos = 0
self.csv_file = None # created on first packet
self.latest_url = None
# --- GUI Layout ---
self.data_frame = tk.Frame(root)
self.data_frame.pack(pady=5)
self.lat_label = tk.Label(self.data_frame, text="Lat: --", font=("Arial", 12))
self.lat_label.pack(anchor="w")
self.lon_label = tk.Label(self.data_frame, text="Lon: --", font=("Arial", 12))
self.lon_label.pack(anchor="w")
self.alt_label = tk.Label(self.data_frame, text="Alt: -- m", font=("Arial", 12))
self.alt_label.pack(anchor="w")
self.vel_label = tk.Label(self.data_frame, text="Vel: -- m/s", font=("Arial", 12))
self.vel_label.pack(anchor="w")
self.map_button = tk.Button(root, text="Open in Maps", command=self.open_map, state="disabled")
self.map_button.pack(pady=5)
# Matplotlib Figure
self.fig, (self.ax1, self.ax2) = plt.subplots(2, 1, figsize=(6, 6))
self.canvas = FigureCanvasTkAgg(self.fig, master=root)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Update loop
self.update_gui()
# Kill bash script when window closes
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def parse_aprs(self, line):
"""
Parse APRS line like: !4827.68N/12318.61W-/A=000213*
"""
match = re.search(r"!([0-9]{4,5}\.\d+)([NS])\/([0-9]{5,6}\.\d+)([EW]).*A=(\d+)", line)
if match:
lat_raw, ns, lon_raw, ew, alt_str = match.groups()
lat = float(lat_raw[:2]) + float(lat_raw[2:]) / 60.0
if ns == "S":
lat = -lat
lon = float(lon_raw[:3]) + float(lon_raw[3:]) / 60.0
if ew == "E":
lon = -lon
alt_m = int(alt_str) * 0.3048 # feet → meters
return lat, lon, alt_m
return None
def open_map(self):
if self.latest_url:
os.system(f"xdg-open '{self.latest_url}'")
def update_gui(self):
with open(self.log_file, "r") as f:
f.seek(self.file_pos)
new_lines = f.readlines()
self.file_pos = f.tell()
for line in new_lines:
parsed = self.parse_aprs(line)
if parsed:
lat, lon, alt = parsed
self.lats.append(lat)
self.lons.append(lon)
self.altitudes.append(alt)
now = datetime.utcnow().strftime("%H:%M:%S")
self.times.append(now)
# vertical velocity
if self.last_alt is not None:
dt = 2 # assume 2s between packets
velocity = (alt - self.last_alt) / dt
self.velocities.append(velocity)
else:
self.velocities.append(0)
self.last_alt = alt
# update GUI labels
self.lat_label.config(text=f"Lat: {lat:.5f}")
self.lon_label.config(text=f"Lon: {lon:.5f}")
self.alt_label.config(text=f"Alt: {alt:.1f} m")
self.vel_label.config(text=f"Vel: {self.velocities[-1]:.1f} m/s")
# update map button
self.latest_url = f"https://www.google.com/maps?q={lat},{lon}"
self.map_button.config(state="normal")
# Save data to CSV
if self.csv_file is None:
start_time = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
self.csv_file = f"logs/aprs_log_{start_time}.csv"
with open(self.csv_file, "w") as out:
out.write("UTC Time,Latitude,Longitude,Altitude (m),Velocity (m/s)\n")
with open(self.csv_file, "a") as out:
out.write(f"{now},{lat},{lon},{alt},{self.velocities[-1]}\n")
self.update_graphs()
self.root.after(1000, self.update_gui)
def update_graphs(self):
self.ax1.clear()
self.ax2.clear()
if self.times and self.altitudes:
self.ax1.plot(self.times, self.altitudes, linestyle='-')
self.ax1.set_title("Altitude over Time (UTC)")
self.ax1.set_xlabel("UTC Time")
self.ax1.set_ylabel("Altitude (m)")
self.ax1.tick_params(axis='x', rotation=45)
# min/max only
self.ax1.set_xticks([0, len(self.times)-1])
self.ax1.set_xticklabels([self.times[0], self.times[-1]])
self.ax1.set_ylim(min(self.altitudes), max(self.altitudes))
if self.times and self.velocities:
self.ax2.plot(self.times, self.velocities, linestyle='-', color="orange")
self.ax2.set_title("Vertical Velocity over Time (UTC)")
self.ax2.set_xlabel("UTC Time")
self.ax2.set_ylabel("Velocity (m/s)")
self.ax2.tick_params(axis='x', rotation=45)
# min/max only
self.ax2.set_xticks([0, len(self.times)-1])
self.ax2.set_xticklabels([self.times[0], self.times[-1]])
self.ax2.set_ylim(min(self.velocities), max(self.velocities))
self.fig.tight_layout()
self.canvas.draw()
def on_close(self):
# Kill the bash script cleanly
if self.bash_proc:
try:
os.killpg(os.getpgid(self.bash_proc.pid), signal.SIGTERM)
except Exception as e:
print(f"Failed to kill background process: {e}")
self.root.destroy()
if __name__ == "__main__":
log_file = "logs/aprs.log" # text file that grows with new packets
if not os.path.exists(log_file):
os.makedirs("logs", exist_ok=True)
open(log_file, "w").close()
# Start bash script in background, detached, no stdout/stderr noise
bash_proc = subprocess.Popen(
["bash", "./start.sh"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setpgrp
)
root = tk.Tk()
app = APRSTelemetryGUI(root, log_file, bash_proc)
root.mainloop()