feat: add station location and distance tracking to radiosonde mode

- Pass observer location and gpsd status to radiosonde_auto_rx station config
- Add station marker on radiosonde map with GPS live position updates
- Display distance from station to each balloon in cards and popups
- Update aircraft database

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-27 19:49:50 +00:00
parent c6e8602184
commit 7d1fcfe895
4 changed files with 111 additions and 11 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
{ {
"version": "2026-02-15_ae16bb62", "version": "2026-02-22_17194a71",
"downloaded": "2026-02-20T00:29:06.228007Z" "downloaded": "2026-02-27T10:41:04.872620Z"
} }

View File

@@ -12,8 +12,8 @@ import os
import queue import queue
import shutil import shutil
import socket import socket
import sys
import subprocess import subprocess
import sys
import threading import threading
import time import time
from typing import Any from typing import Any
@@ -29,10 +29,16 @@ from utils.constants import (
SSE_KEEPALIVE_INTERVAL, SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT, SSE_QUEUE_TIMEOUT,
) )
from utils.gps import is_gpsd_running
from utils.logging import get_logger from utils.logging import get_logger
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_gain from utils.validation import (
validate_device_index,
validate_gain,
validate_latitude,
validate_longitude,
)
logger = get_logger('intercept.radiosonde') logger = get_logger('intercept.radiosonde')
@@ -83,6 +89,10 @@ def generate_station_cfg(
ppm: int = 0, ppm: int = 0,
bias_t: bool = False, bias_t: bool = False,
udp_port: int = RADIOSONDE_UDP_PORT, udp_port: int = RADIOSONDE_UDP_PORT,
latitude: float = 0.0,
longitude: float = 0.0,
station_alt: float = 0.0,
gpsd_enabled: bool = False,
) -> str: ) -> str:
"""Generate a station.cfg for radiosonde_auto_rx and return the file path.""" """Generate a station.cfg for radiosonde_auto_rx and return the file path."""
cfg_dir = os.path.abspath(os.path.join('data', 'radiosonde')) cfg_dir = os.path.abspath(os.path.join('data', 'radiosonde'))
@@ -116,10 +126,10 @@ always_scan = []
always_decode = [] always_decode = []
[location] [location]
station_lat = 0.0 station_lat = {latitude}
station_lon = 0.0 station_lon = {longitude}
station_alt = 0.0 station_alt = {station_alt}
gpsd_enabled = False gpsd_enabled = {str(gpsd_enabled)}
gpsd_host = localhost gpsd_host = localhost
gpsd_port = 2947 gpsd_port = 2947
@@ -471,6 +481,20 @@ def start_radiosonde():
bias_t = data.get('bias_t', False) bias_t = data.get('bias_t', False)
ppm = int(data.get('ppm', 0)) ppm = int(data.get('ppm', 0))
# Validate optional location
latitude = 0.0
longitude = 0.0
if data.get('latitude') is not None and data.get('longitude') is not None:
try:
latitude = validate_latitude(data['latitude'])
longitude = validate_longitude(data['longitude'])
except ValueError:
latitude = 0.0
longitude = 0.0
# Check if gpsd is available for live position updates
gpsd_enabled = is_gpsd_running()
# Find auto_rx # Find auto_rx
auto_rx_path = find_auto_rx() auto_rx_path = find_auto_rx()
if not auto_rx_path: if not auto_rx_path:
@@ -515,6 +539,9 @@ def start_radiosonde():
device_index=device_int, device_index=device_int,
ppm=ppm, ppm=ppm,
bias_t=bias_t, bias_t=bias_t,
latitude=latitude,
longitude=longitude,
gpsd_enabled=gpsd_enabled,
) )
# Build command - auto_rx -c expects a file path, not a directory # Build command - auto_rx -c expects a file path, not a directory

View File

@@ -148,6 +148,8 @@
freq_min: freqMin, freq_min: freqMin,
freq_max: freqMax, freq_max: freqMax,
bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false, bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false,
latitude: radiosondeStationLocation.lat,
longitude: radiosondeStationLocation.lon,
}) })
}) })
.then(r => r.json()) .then(r => r.json())
@@ -230,15 +232,23 @@
let radiosondeMarkers = new Map(); let radiosondeMarkers = new Map();
let radiosondeTracks = new Map(); let radiosondeTracks = new Map();
let radiosondeTrackPoints = new Map(); let radiosondeTrackPoints = new Map();
let radiosondeStationLocation = { lat: 0, lon: 0 };
let radiosondeStationMarker = null;
function initRadiosondeMap() { function initRadiosondeMap() {
if (radiosondeMap) return; if (radiosondeMap) return;
const container = document.getElementById('radiosondeMapContainer'); const container = document.getElementById('radiosondeMapContainer');
if (!container) return; if (!container) return;
// Resolve observer location
if (window.ObserverLocation && ObserverLocation.getForModule) {
radiosondeStationLocation = ObserverLocation.getForModule('radiosonde_observerLocation');
}
const hasLocation = radiosondeStationLocation.lat !== 0 || radiosondeStationLocation.lon !== 0;
radiosondeMap = L.map('radiosondeMapContainer', { radiosondeMap = L.map('radiosondeMapContainer', {
center: [40, -95], center: hasLocation ? [radiosondeStationLocation.lat, radiosondeStationLocation.lon] : [40, -95],
zoom: 4, zoom: hasLocation ? 7 : 4,
zoomControl: true, zoomControl: true,
}); });
@@ -246,6 +256,50 @@
attribution: '&copy; OpenStreetMap &copy; CARTO', attribution: '&copy; OpenStreetMap &copy; CARTO',
maxZoom: 18, maxZoom: 18,
}).addTo(radiosondeMap); }).addTo(radiosondeMap);
// Add station marker if we have a location
if (hasLocation) {
radiosondeStationMarker = L.circleMarker(
[radiosondeStationLocation.lat, radiosondeStationLocation.lon], {
radius: 8,
fillColor: '#00e5ff',
color: '#00e5ff',
weight: 2,
opacity: 1,
fillOpacity: 0.5,
}).addTo(radiosondeMap);
radiosondeStationMarker.bindTooltip('Station', { permanent: false, direction: 'top' });
}
// Try GPS for live position updates
fetch('/gps/position')
.then(r => r.json())
.then(data => {
if (data.status === 'ok' && data.position && data.position.latitude != null) {
radiosondeStationLocation = { lat: data.position.latitude, lon: data.position.longitude };
const ll = [data.position.latitude, data.position.longitude];
if (radiosondeStationMarker) {
radiosondeStationMarker.setLatLng(ll);
} else {
radiosondeStationMarker = L.circleMarker(ll, {
radius: 8,
fillColor: '#00e5ff',
color: '#00e5ff',
weight: 2,
opacity: 1,
fillOpacity: 0.5,
}).addTo(radiosondeMap);
radiosondeStationMarker.bindTooltip('Station (GPS)', { permanent: false, direction: 'top' });
}
if (!radiosondeMap._gpsInitialized) {
radiosondeMap.setView(ll, 7);
radiosondeMap._gpsInitialized = true;
}
// Re-render cards with updated distances
updateRadiosondeCards();
}
})
.catch(() => {});
} }
function updateRadiosondeMap(balloon) { function updateRadiosondeMap(balloon) {
@@ -284,12 +338,19 @@
const tempStr = balloon.temp != null ? `${balloon.temp.toFixed(1)} °C` : '--'; const tempStr = balloon.temp != null ? `${balloon.temp.toFixed(1)} °C` : '--';
const humStr = balloon.humidity != null ? `${balloon.humidity.toFixed(0)}%` : '--'; const humStr = balloon.humidity != null ? `${balloon.humidity.toFixed(0)}%` : '--';
const velStr = balloon.vel_v != null ? `${balloon.vel_v.toFixed(1)} m/s` : '--'; const velStr = balloon.vel_v != null ? `${balloon.vel_v.toFixed(1)} m/s` : '--';
let distStr = '';
if ((radiosondeStationLocation.lat !== 0 || radiosondeStationLocation.lon !== 0) && balloon.lat && balloon.lon) {
const distM = radiosondeMap.distance(
[radiosondeStationLocation.lat, radiosondeStationLocation.lon], latlng);
distStr = `Dist: ${(distM / 1000).toFixed(1)} km<br>`;
}
radiosondeMarkers.get(id).bindPopup( radiosondeMarkers.get(id).bindPopup(
`<strong>${id}</strong><br>` + `<strong>${id}</strong><br>` +
`Type: ${balloon.sonde_type || '--'}<br>` + `Type: ${balloon.sonde_type || '--'}<br>` +
`Alt: ${altStr}<br>` + `Alt: ${altStr}<br>` +
`Temp: ${tempStr} | Hum: ${humStr}<br>` + `Temp: ${tempStr} | Hum: ${humStr}<br>` +
`Vert: ${velStr}<br>` + `Vert: ${velStr}<br>` +
distStr +
(balloon.freq ? `Freq: ${balloon.freq.toFixed(3)} MHz` : '') (balloon.freq ? `Freq: ${balloon.freq.toFixed(3)} MHz` : '')
); );
@@ -322,6 +383,7 @@
if (!container) return; if (!container) return;
const sorted = Object.values(radiosondeBalloons).sort((a, b) => (b.alt || 0) - (a.alt || 0)); const sorted = Object.values(radiosondeBalloons).sort((a, b) => (b.alt || 0) - (a.alt || 0));
const hasStation = radiosondeStationLocation.lat !== 0 || radiosondeStationLocation.lon !== 0;
container.innerHTML = sorted.map(b => { container.innerHTML = sorted.map(b => {
const alt = b.alt ? `${Math.round(b.alt).toLocaleString()} m` : '--'; const alt = b.alt ? `${Math.round(b.alt).toLocaleString()} m` : '--';
const temp = b.temp != null ? `${b.temp.toFixed(1)}°C` : '--'; const temp = b.temp != null ? `${b.temp.toFixed(1)}°C` : '--';
@@ -329,6 +391,13 @@
const press = b.pressure != null ? `${b.pressure.toFixed(1)} hPa` : '--'; const press = b.pressure != null ? `${b.pressure.toFixed(1)} hPa` : '--';
const vel = b.vel_v != null ? `${b.vel_v > 0 ? '+' : ''}${b.vel_v.toFixed(1)} m/s` : '--'; const vel = b.vel_v != null ? `${b.vel_v > 0 ? '+' : ''}${b.vel_v.toFixed(1)} m/s` : '--';
const freq = b.freq ? `${b.freq.toFixed(3)} MHz` : '--'; const freq = b.freq ? `${b.freq.toFixed(3)} MHz` : '--';
let dist = '--';
if (hasStation && b.lat && b.lon && radiosondeMap) {
const distM = radiosondeMap.distance(
[radiosondeStationLocation.lat, radiosondeStationLocation.lon],
[b.lat, b.lon]);
dist = `${(distM / 1000).toFixed(1)} km`;
}
return ` return `
<div class="radiosonde-card" onclick="radiosondeMap && radiosondeMap.setView([${b.lat || 0}, ${b.lon || 0}], 10)"> <div class="radiosonde-card" onclick="radiosondeMap && radiosondeMap.setView([${b.lat || 0}, ${b.lon || 0}], 10)">
<div class="radiosonde-card-header"> <div class="radiosonde-card-header">
@@ -360,6 +429,10 @@
<span class="radiosonde-stat-value">${freq}</span> <span class="radiosonde-stat-value">${freq}</span>
<span class="radiosonde-stat-label">FREQ</span> <span class="radiosonde-stat-label">FREQ</span>
</div> </div>
<div class="radiosonde-stat">
<span class="radiosonde-stat-value">${dist}</span>
<span class="radiosonde-stat-label">DIST</span>
</div>
</div> </div>
</div> </div>
`; `;