from skyfield.api import load import asyncio SATELLITE_GROUPS = { "military": "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=tle", "stations": "https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle", "navigation": "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=tle", } GROUP_LIMITS = { "military": 800, "stations": 50, "navigation": 120, } satellites_data: dict = {} def load_satellites(): """ Blocking I/O: loads TLE data from Celestrak. Must be called via asyncio.to_thread to avoid blocking the event loop. """ global satellites_data try: ts = load.timescale() loaded = {} for group, url in SATELLITE_GROUPS.items(): try: filename = f"{group}.tle" sats = load.tle_file(url, filename=filename) loaded[group] = sats print(f"[SPACE] {group}: {len(sats)} satellites loaded.") except Exception as e: print(f"[SPACE] Failed to load {group}: {e}") satellites_data = {**loaded, "ts": ts} except Exception as e: print(f"[SPACE] Critical load failure: {e}") def _compute_positions_sync(sats: list, category: str, t, limit: int) -> list: """ CPU-bound position computation — runs in a thread executor. Returns dicts, does NOT mutate shared state. """ positions = [] for sat in sats[:limit]: try: geocentric = sat.at(t) subpoint = geocentric.subpoint() lat = subpoint.latitude.degrees lon = subpoint.longitude.degrees alt = subpoint.elevation.m if not (-90 <= lat <= 90) or not (-180 <= lon <= 180): continue positions.append({ "id": sat.model.satnum, "name": sat.name, "lat": lat, "lon": lon, "alt": alt, "category": category, "type": "satellite" }) except Exception: pass # skip satellites with bad TLE data return positions async def get_satellite_positions() -> list: if "ts" not in satellites_data: # First run: load everything in a background thread await asyncio.to_thread(load_satellites) if "ts" not in satellites_data: print("[SPACE] Satellite data unavailable.") return [] t = satellites_data["ts"].now() all_positions = [] for group, limit in GROUP_LIMITS.items(): if group in satellites_data: result = await asyncio.to_thread( _compute_positions_sync, satellites_data[group], group, t, limit ) all_positions.extend(result) print(f"[SPACE] {len(all_positions)} satellite positions computed.") return all_positions if __name__ == "__main__": load_satellites() positions = asyncio.run(get_satellite_positions()) print(f"Total: {len(positions)}") from collections import Counter for cat, count in Counter(p["category"] for p in positions).items(): print(f" {cat}: {count}")