Initial commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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}")
|
||||
Reference in New Issue
Block a user