mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 14:50:00 -07:00
Cache satellite pass predictions
This commit is contained in:
@@ -51,6 +51,8 @@ _tle_cache = dict(TLE_SATELLITES)
|
|||||||
# TTL is 1800 seconds (30 minutes)
|
# TTL is 1800 seconds (30 minutes)
|
||||||
_track_cache: dict = {}
|
_track_cache: dict = {}
|
||||||
_TRACK_CACHE_TTL = 1800
|
_TRACK_CACHE_TTL = 1800
|
||||||
|
_pass_cache: dict = {}
|
||||||
|
_PASS_CACHE_TTL = 300
|
||||||
|
|
||||||
_BUILTIN_NORAD_TO_KEY = {
|
_BUILTIN_NORAD_TO_KEY = {
|
||||||
25544: 'ISS',
|
25544: 'ISS',
|
||||||
@@ -173,6 +175,31 @@ def _resolve_satellite_request(sat: object, tracked_by_norad: dict[int, dict], t
|
|||||||
return display_name, norad_id, tle_data
|
return display_name, norad_id, tle_data
|
||||||
|
|
||||||
|
|
||||||
|
def _make_pass_cache_key(
|
||||||
|
lat: float,
|
||||||
|
lon: float,
|
||||||
|
hours: int,
|
||||||
|
min_el: float,
|
||||||
|
resolved_satellites: list[tuple[str, int, tuple[str, str, str]]],
|
||||||
|
) -> tuple:
|
||||||
|
"""Build a stable cache key for predicted passes."""
|
||||||
|
return (
|
||||||
|
round(lat, 4),
|
||||||
|
round(lon, 4),
|
||||||
|
int(hours),
|
||||||
|
round(float(min_el), 1),
|
||||||
|
tuple(
|
||||||
|
(
|
||||||
|
sat_name,
|
||||||
|
norad_id,
|
||||||
|
tle_data[1][:32],
|
||||||
|
tle_data[2][:32],
|
||||||
|
)
|
||||||
|
for sat_name, norad_id, tle_data in resolved_satellites
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _start_satellite_tracker():
|
def _start_satellite_tracker():
|
||||||
"""Background thread: push live satellite positions to satellite_queue every second."""
|
"""Background thread: push live satellite positions to satellite_queue every second."""
|
||||||
import app as app_module
|
import app as app_module
|
||||||
@@ -425,8 +452,8 @@ def predict_passes():
|
|||||||
|
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
|
|
||||||
# Validate inputs
|
|
||||||
try:
|
try:
|
||||||
|
# Validate inputs
|
||||||
lat = validate_latitude(data.get('latitude', data.get('lat', 51.5074)))
|
lat = validate_latitude(data.get('latitude', data.get('lat', 51.5074)))
|
||||||
lon = validate_longitude(data.get('longitude', data.get('lon', -0.1278)))
|
lon = validate_longitude(data.get('longitude', data.get('lon', -0.1278)))
|
||||||
hours = validate_hours(data.get('hours', 24))
|
hours = validate_hours(data.get('hours', 24))
|
||||||
@@ -434,54 +461,83 @@ def predict_passes():
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return api_error(str(e), 400)
|
return api_error(str(e), 400)
|
||||||
|
|
||||||
sat_input = data.get('satellites', ['ISS', 'METEOR-M2-3', 'METEOR-M2-4'])
|
|
||||||
passes = []
|
|
||||||
colors = {
|
|
||||||
'ISS': '#00ffff',
|
|
||||||
'METEOR-M2': '#9370DB',
|
|
||||||
'METEOR-M2-3': '#ff00ff',
|
|
||||||
'METEOR-M2-4': '#00ff88',
|
|
||||||
}
|
|
||||||
tracked_by_norad, tracked_by_name = _get_tracked_satellite_maps()
|
|
||||||
|
|
||||||
ts = _get_timescale()
|
|
||||||
observer = wgs84.latlon(lat, lon)
|
|
||||||
t0 = ts.now()
|
|
||||||
t1 = ts.utc(t0.utc_datetime() + timedelta(hours=hours))
|
|
||||||
|
|
||||||
for sat in sat_input:
|
|
||||||
sat_name, norad_id, tle_data = _resolve_satellite_request(sat, tracked_by_norad, tracked_by_name)
|
|
||||||
if not tle_data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Current position for map marker (computed once per satellite)
|
|
||||||
current_pos = None
|
|
||||||
try:
|
try:
|
||||||
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
sat_input = data.get('satellites', ['ISS', 'METEOR-M2-3', 'METEOR-M2-4'])
|
||||||
geo = satellite.at(ts.now())
|
passes = []
|
||||||
sp = wgs84.subpoint(geo)
|
colors = {
|
||||||
current_pos = {
|
'ISS': '#00ffff',
|
||||||
'lat': float(sp.latitude.degrees),
|
'METEOR-M2': '#9370DB',
|
||||||
'lon': float(sp.longitude.degrees),
|
'METEOR-M2-3': '#ff00ff',
|
||||||
|
'METEOR-M2-4': '#00ff88',
|
||||||
}
|
}
|
||||||
except Exception:
|
tracked_by_norad, tracked_by_name = _get_tracked_satellite_maps()
|
||||||
pass
|
|
||||||
|
|
||||||
sat_passes = _predict_passes(tle_data, observer, ts, t0, t1, min_el=min_el)
|
resolved_satellites: list[tuple[str, int, tuple[str, str, str]]] = []
|
||||||
for p in sat_passes:
|
for sat in sat_input:
|
||||||
p['satellite'] = sat_name
|
sat_name, norad_id, tle_data = _resolve_satellite_request(
|
||||||
p['norad'] = norad_id or 0
|
sat,
|
||||||
p['color'] = colors.get(sat_name, '#00ff00')
|
tracked_by_norad,
|
||||||
if current_pos:
|
tracked_by_name,
|
||||||
p['currentPos'] = current_pos
|
)
|
||||||
passes.extend(sat_passes)
|
if not tle_data:
|
||||||
|
continue
|
||||||
|
resolved_satellites.append((sat_name, norad_id or 0, tle_data))
|
||||||
|
|
||||||
passes.sort(key=lambda p: p['startTimeISO'])
|
if not resolved_satellites:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'passes': [],
|
||||||
|
'cached': False,
|
||||||
|
})
|
||||||
|
|
||||||
return jsonify({
|
cache_key = _make_pass_cache_key(lat, lon, hours, min_el, resolved_satellites)
|
||||||
'status': 'success',
|
cached = _pass_cache.get(cache_key)
|
||||||
'passes': passes
|
now_ts = time.time()
|
||||||
})
|
if cached and (now_ts - cached[1]) < _PASS_CACHE_TTL:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'passes': cached[0],
|
||||||
|
'cached': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
ts = _get_timescale()
|
||||||
|
observer = wgs84.latlon(lat, lon)
|
||||||
|
t0 = ts.now()
|
||||||
|
t1 = ts.utc(t0.utc_datetime() + timedelta(hours=hours))
|
||||||
|
|
||||||
|
for sat_name, norad_id, tle_data in resolved_satellites:
|
||||||
|
current_pos = None
|
||||||
|
try:
|
||||||
|
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
||||||
|
geo = satellite.at(t0)
|
||||||
|
sp = wgs84.subpoint(geo)
|
||||||
|
current_pos = {
|
||||||
|
'lat': float(sp.latitude.degrees),
|
||||||
|
'lon': float(sp.longitude.degrees),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sat_passes = _predict_passes(tle_data, observer, ts, t0, t1, min_el=min_el)
|
||||||
|
for p in sat_passes:
|
||||||
|
p['satellite'] = sat_name
|
||||||
|
p['norad'] = norad_id
|
||||||
|
p['color'] = colors.get(sat_name, '#00ff00')
|
||||||
|
if current_pos:
|
||||||
|
p['currentPos'] = current_pos
|
||||||
|
passes.extend(sat_passes)
|
||||||
|
|
||||||
|
passes.sort(key=lambda p: p['startTimeISO'])
|
||||||
|
_pass_cache[cache_key] = (passes, now_ts)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'passes': passes,
|
||||||
|
'cached': False,
|
||||||
|
})
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception('Satellite pass calculation failed')
|
||||||
|
return api_error(f'Failed to calculate passes: {exc}', 500)
|
||||||
|
|
||||||
|
|
||||||
@satellite_bp.route('/position', methods=['POST'])
|
@satellite_bp.route('/position', methods=['POST'])
|
||||||
|
|||||||
@@ -604,7 +604,7 @@
|
|||||||
let _passRequestId = 0;
|
let _passRequestId = 0;
|
||||||
let _passAbortController = null;
|
let _passAbortController = null;
|
||||||
let _passTimeoutId = null;
|
let _passTimeoutId = null;
|
||||||
const DASHBOARD_FETCH_TIMEOUT_MS = 10000;
|
const DASHBOARD_FETCH_TIMEOUT_MS = 30000;
|
||||||
const BUILTIN_TX_FALLBACK = {
|
const BUILTIN_TX_FALLBACK = {
|
||||||
25544: [
|
25544: [
|
||||||
{ description: 'APRS digipeater', downlink_low: 145.825, downlink_high: 145.825, uplink_low: null, uplink_high: null, mode: 'FM AX.25', baud: 1200, status: 'active', type: 'beacon', service: 'Packet' },
|
{ description: 'APRS digipeater', downlink_low: 145.825, downlink_high: 145.825, uplink_low: null, uplink_high: null, mode: 'FM AX.25', baud: 1200, status: 'active', type: 'beacon', service: 'Packet' },
|
||||||
|
|||||||
Reference in New Issue
Block a user