mirror of
https://github.com/smittix/intercept.git
synced 2026-06-19 19:06:15 -07:00
fix: cache ISS position/schedule and parallelize SSTV init API calls
SSTV mode was slow to populate next-pass countdown and ISS location map due to uncached skyfield computation and sequential JS API calls. - Cache ISS position (10s TTL) and schedule (15min TTL, keyed by rounded lat/lon) - Cache skyfield timescale object (expensive to create on every request) - Reduce external API timeouts from 5s to 3s - Fire checkStatus, loadImages, loadIssSchedule, updateIssPosition in parallel via Promise.all Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+127
-61
@@ -7,9 +7,10 @@ ISS SSTV events occur during special commemorations and typically transmit on 14
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import queue
|
import queue
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator
|
from typing import Any
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, Response, send_file
|
from flask import Blueprint, jsonify, request, Response, send_file
|
||||||
|
|
||||||
@@ -36,6 +37,24 @@ ISS_SSTV_FREQ_TOLERANCE_MHZ = 0.05
|
|||||||
# Queue for SSE progress streaming
|
# Queue for SSE progress streaming
|
||||||
_sstv_queue: queue.Queue = queue.Queue(maxsize=100)
|
_sstv_queue: queue.Queue = queue.Queue(maxsize=100)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Caching — ISS position (external API) and schedule (skyfield computation)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_iss_position_cache: dict | None = None
|
||||||
|
_iss_position_cache_time: float = 0
|
||||||
|
_iss_position_lock = threading.Lock()
|
||||||
|
ISS_POSITION_CACHE_TTL = 10 # seconds
|
||||||
|
|
||||||
|
_iss_schedule_cache: dict | None = None
|
||||||
|
_iss_schedule_cache_time: float = 0
|
||||||
|
_iss_schedule_cache_key: str | None = None
|
||||||
|
_iss_schedule_lock = threading.Lock()
|
||||||
|
ISS_SCHEDULE_CACHE_TTL = 900 # 15 minutes
|
||||||
|
|
||||||
|
# Reusable skyfield timescale (expensive to create)
|
||||||
|
_timescale = None
|
||||||
|
_timescale_lock = threading.Lock()
|
||||||
|
|
||||||
# Track which device is being used
|
# Track which device is being used
|
||||||
sstv_active_device: int | None = None
|
sstv_active_device: int | None = None
|
||||||
|
|
||||||
@@ -441,12 +460,23 @@ def stream_progress():
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_timescale():
|
||||||
|
"""Return a cached skyfield timescale (expensive to create)."""
|
||||||
|
global _timescale
|
||||||
|
with _timescale_lock:
|
||||||
|
if _timescale is None:
|
||||||
|
from skyfield.api import load
|
||||||
|
_timescale = load.timescale()
|
||||||
|
return _timescale
|
||||||
|
|
||||||
|
|
||||||
@sstv_bp.route('/iss-schedule')
|
@sstv_bp.route('/iss-schedule')
|
||||||
def iss_schedule():
|
def iss_schedule():
|
||||||
"""
|
"""
|
||||||
Get ISS pass schedule for SSTV reception.
|
Get ISS pass schedule for SSTV reception.
|
||||||
|
|
||||||
Calculates ISS passes directly using skyfield.
|
Calculates ISS passes directly using skyfield.
|
||||||
|
Results are cached for 15 minutes per rounded location.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
latitude: Observer latitude (required)
|
latitude: Observer latitude (required)
|
||||||
@@ -456,6 +486,8 @@ def iss_schedule():
|
|||||||
Returns:
|
Returns:
|
||||||
JSON with ISS pass schedule.
|
JSON with ISS pass schedule.
|
||||||
"""
|
"""
|
||||||
|
global _iss_schedule_cache, _iss_schedule_cache_time, _iss_schedule_cache_key
|
||||||
|
|
||||||
lat = request.args.get('latitude', type=float)
|
lat = request.args.get('latitude', type=float)
|
||||||
lon = request.args.get('longitude', type=float)
|
lon = request.args.get('longitude', type=float)
|
||||||
hours = request.args.get('hours', 48, type=int)
|
hours = request.args.get('hours', 48, type=int)
|
||||||
@@ -466,8 +498,18 @@ def iss_schedule():
|
|||||||
'message': 'latitude and longitude parameters required'
|
'message': 'latitude and longitude parameters required'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
|
# Cache key: rounded lat/lon (1 decimal place) so nearby locations share cache
|
||||||
|
cache_key = f"{round(lat, 1)}:{round(lon, 1)}:{hours}"
|
||||||
|
|
||||||
|
with _iss_schedule_lock:
|
||||||
|
now = time.time()
|
||||||
|
if (_iss_schedule_cache is not None
|
||||||
|
and cache_key == _iss_schedule_cache_key
|
||||||
|
and (now - _iss_schedule_cache_time) < ISS_SCHEDULE_CACHE_TTL):
|
||||||
|
return jsonify(_iss_schedule_cache)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from skyfield.api import load, wgs84, EarthSatellite
|
from skyfield.api import wgs84, EarthSatellite
|
||||||
from skyfield.almanac import find_discrete
|
from skyfield.almanac import find_discrete
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from data.satellites import TLE_SATELLITES
|
from data.satellites import TLE_SATELLITES
|
||||||
@@ -480,7 +522,7 @@ def iss_schedule():
|
|||||||
'message': 'ISS TLE data not available'
|
'message': 'ISS TLE data not available'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
ts = load.timescale()
|
ts = _get_timescale()
|
||||||
satellite = EarthSatellite(iss_tle[1], iss_tle[2], iss_tle[0], ts)
|
satellite = EarthSatellite(iss_tle[1], iss_tle[2], iss_tle[0], ts)
|
||||||
observer = wgs84.latlon(lat, lon)
|
observer = wgs84.latlon(lat, lon)
|
||||||
|
|
||||||
@@ -543,13 +585,21 @@ def iss_schedule():
|
|||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return jsonify({
|
result = {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'passes': passes,
|
'passes': passes,
|
||||||
'count': len(passes),
|
'count': len(passes),
|
||||||
'sstv_frequency': ISS_SSTV_FREQ,
|
'sstv_frequency': ISS_SSTV_FREQ,
|
||||||
'note': 'ISS SSTV events are not continuous. Check ARISS.org for scheduled events.'
|
'note': 'ISS SSTV events are not continuous. Check ARISS.org for scheduled events.'
|
||||||
})
|
}
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
with _iss_schedule_lock:
|
||||||
|
_iss_schedule_cache = result
|
||||||
|
_iss_schedule_cache_time = time.time()
|
||||||
|
_iss_schedule_cache_key = cache_key
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -565,13 +615,65 @@ def iss_schedule():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_iss_position() -> dict | None:
|
||||||
|
"""Fetch raw ISS lat/lon/altitude from external APIs, with 10s cache."""
|
||||||
|
global _iss_position_cache, _iss_position_cache_time
|
||||||
|
|
||||||
|
with _iss_position_lock:
|
||||||
|
now = time.time()
|
||||||
|
if _iss_position_cache is not None and (now - _iss_position_cache_time) < ISS_POSITION_CACHE_TTL:
|
||||||
|
return _iss_position_cache
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
cached = None
|
||||||
|
|
||||||
|
# Try primary API: Where The ISS At
|
||||||
|
try:
|
||||||
|
response = requests.get('https://api.wheretheiss.at/v1/satellites/25544', timeout=3)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
cached = {
|
||||||
|
'lat': float(data['latitude']),
|
||||||
|
'lon': float(data['longitude']),
|
||||||
|
'altitude': float(data.get('altitude', 420)),
|
||||||
|
'source': 'wheretheiss',
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Where The ISS At API failed: {e}")
|
||||||
|
|
||||||
|
# Try fallback API: Open Notify
|
||||||
|
if cached is None:
|
||||||
|
try:
|
||||||
|
response = requests.get('http://api.open-notify.org/iss-now.json', timeout=3)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('message') == 'success':
|
||||||
|
cached = {
|
||||||
|
'lat': float(data['iss_position']['latitude']),
|
||||||
|
'lon': float(data['iss_position']['longitude']),
|
||||||
|
'altitude': 420,
|
||||||
|
'source': 'open-notify',
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Open Notify API failed: {e}")
|
||||||
|
|
||||||
|
if cached is not None:
|
||||||
|
with _iss_position_lock:
|
||||||
|
_iss_position_cache = cached
|
||||||
|
_iss_position_cache_time = time.time()
|
||||||
|
|
||||||
|
return cached
|
||||||
|
|
||||||
|
|
||||||
@sstv_bp.route('/iss-position')
|
@sstv_bp.route('/iss-position')
|
||||||
def iss_position():
|
def iss_position():
|
||||||
"""
|
"""
|
||||||
Get current ISS position from real-time API.
|
Get current ISS position from real-time API.
|
||||||
|
|
||||||
Uses the Open Notify API for accurate real-time position,
|
Uses the "Where The ISS At" API for accurate real-time position,
|
||||||
with fallback to "Where The ISS At" API.
|
with fallback to Open Notify API. Raw position is cached for 10 seconds;
|
||||||
|
observer-relative data (elevation/azimuth) is computed per-request.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
latitude: Observer latitude (optional, for elevation calc)
|
latitude: Observer latitude (optional, for elevation calc)
|
||||||
@@ -580,68 +682,32 @@ def iss_position():
|
|||||||
Returns:
|
Returns:
|
||||||
JSON with ISS current position.
|
JSON with ISS current position.
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
observer_lat = request.args.get('latitude', type=float)
|
observer_lat = request.args.get('latitude', type=float)
|
||||||
observer_lon = request.args.get('longitude', type=float)
|
observer_lon = request.args.get('longitude', type=float)
|
||||||
|
|
||||||
# Try primary API: Where The ISS At
|
pos = _fetch_iss_position()
|
||||||
try:
|
if pos is None:
|
||||||
response = requests.get('https://api.wheretheiss.at/v1/satellites/25544', timeout=5)
|
return jsonify({
|
||||||
if response.status_code == 200:
|
'status': 'error',
|
||||||
data = response.json()
|
'message': 'Unable to fetch ISS position from real-time APIs'
|
||||||
iss_lat = float(data['latitude'])
|
}), 503
|
||||||
iss_lon = float(data['longitude'])
|
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'lat': iss_lat,
|
'lat': pos['lat'],
|
||||||
'lon': iss_lon,
|
'lon': pos['lon'],
|
||||||
'altitude': float(data.get('altitude', 420)),
|
'altitude': pos['altitude'],
|
||||||
'timestamp': datetime.utcnow().isoformat(),
|
'timestamp': datetime.utcnow().isoformat(),
|
||||||
'source': 'wheretheiss'
|
'source': pos['source'],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate observer-relative data if location provided
|
# Calculate observer-relative data if location provided
|
||||||
if observer_lat is not None and observer_lon is not None:
|
if observer_lat is not None and observer_lon is not None:
|
||||||
result.update(_calculate_observer_data(iss_lat, iss_lon, observer_lat, observer_lon))
|
result.update(_calculate_observer_data(pos['lat'], pos['lon'], observer_lat, observer_lon))
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Where The ISS At API failed: {e}")
|
|
||||||
|
|
||||||
# Try fallback API: Open Notify
|
|
||||||
try:
|
|
||||||
response = requests.get('http://api.open-notify.org/iss-now.json', timeout=5)
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if data.get('message') == 'success':
|
|
||||||
iss_lat = float(data['iss_position']['latitude'])
|
|
||||||
iss_lon = float(data['iss_position']['longitude'])
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'status': 'ok',
|
|
||||||
'lat': iss_lat,
|
|
||||||
'lon': iss_lon,
|
|
||||||
'altitude': 420, # Approximate ISS altitude in km
|
|
||||||
'timestamp': datetime.utcnow().isoformat(),
|
|
||||||
'source': 'open-notify'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Calculate observer-relative data if location provided
|
|
||||||
if observer_lat is not None and observer_lon is not None:
|
|
||||||
result.update(_calculate_observer_data(iss_lat, iss_lon, observer_lat, observer_lon))
|
|
||||||
|
|
||||||
return jsonify(result)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Open Notify API failed: {e}")
|
|
||||||
|
|
||||||
# Both APIs failed
|
|
||||||
return jsonify({
|
|
||||||
'status': 'error',
|
|
||||||
'message': 'Unable to fetch ISS position from real-time APIs'
|
|
||||||
}), 503
|
|
||||||
|
|
||||||
|
|
||||||
def _calculate_observer_data(iss_lat: float, iss_lon: float, obs_lat: float, obs_lon: float) -> dict:
|
def _calculate_observer_data(iss_lat: float, iss_lon: float, obs_lat: float, obs_lon: float) -> dict:
|
||||||
|
|||||||
+14
-4
@@ -39,13 +39,23 @@ const SSTV = (function() {
|
|||||||
* Initialize the SSTV mode
|
* Initialize the SSTV mode
|
||||||
*/
|
*/
|
||||||
function init() {
|
function init() {
|
||||||
checkStatus();
|
// Load location inputs first (sync localStorage read needed for lat/lon params)
|
||||||
loadImages();
|
|
||||||
loadLocationInputs();
|
loadLocationInputs();
|
||||||
loadIssSchedule();
|
|
||||||
|
// Fire all API calls in parallel — schedule is the slowest, don't let it block
|
||||||
|
Promise.all([
|
||||||
|
checkStatus(),
|
||||||
|
loadImages(),
|
||||||
|
loadIssSchedule(),
|
||||||
|
updateIssPosition(),
|
||||||
|
]).catch(err => console.error('SSTV init error:', err));
|
||||||
|
|
||||||
|
// DOM-only setup (no network, fast)
|
||||||
initMap();
|
initMap();
|
||||||
startIssTracking();
|
|
||||||
startCountdown();
|
startCountdown();
|
||||||
|
// ISS tracking interval (first call already in Promise.all above)
|
||||||
|
if (issUpdateInterval) clearInterval(issUpdateInterval);
|
||||||
|
issUpdateInterval = setInterval(updateIssPosition, 5000);
|
||||||
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
|
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
|
||||||
setTimeout(() => invalidateMap(), 80);
|
setTimeout(() => invalidateMap(), 80);
|
||||||
setTimeout(() => invalidateMap(), 260);
|
setTimeout(() => invalidateMap(), 260);
|
||||||
|
|||||||
Reference in New Issue
Block a user