Stabilize Earth module and fix satellite TLE handling
This commit is contained in:
116
backend/app/core/satellite_tle.py
Normal file
116
backend/app/core/satellite_tle.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Helpers for building stable TLE lines from orbital elements."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def compute_tle_checksum(line: str) -> str:
|
||||
"""Compute the standard modulo-10 checksum for a TLE line."""
|
||||
total = 0
|
||||
|
||||
for char in line[:68]:
|
||||
if char.isdigit():
|
||||
total += int(char)
|
||||
elif char == "-":
|
||||
total += 1
|
||||
|
||||
return str(total % 10)
|
||||
|
||||
|
||||
def _parse_epoch(value: Any) -> Optional[datetime]:
|
||||
if not value:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
return None
|
||||
|
||||
|
||||
def build_tle_line1(norad_cat_id: Any, epoch: Any) -> Optional[str]:
|
||||
"""Build a valid TLE line 1 from the NORAD id and epoch."""
|
||||
epoch_date = _parse_epoch(epoch)
|
||||
if not norad_cat_id or epoch_date is None:
|
||||
return None
|
||||
|
||||
epoch_year = epoch_date.year % 100
|
||||
start_of_year = datetime(epoch_date.year, 1, 1, tzinfo=epoch_date.tzinfo)
|
||||
day_of_year = (epoch_date - start_of_year).days + 1
|
||||
ms_of_day = (
|
||||
epoch_date.hour * 3600000
|
||||
+ epoch_date.minute * 60000
|
||||
+ epoch_date.second * 1000
|
||||
+ int(epoch_date.microsecond / 1000)
|
||||
)
|
||||
day_fraction = ms_of_day / 86400000
|
||||
decimal_fraction = f"{day_fraction:.8f}"[1:]
|
||||
epoch_str = f"{epoch_year:02d}{day_of_year:03d}{decimal_fraction}"
|
||||
|
||||
core = (
|
||||
f"1 {int(norad_cat_id):05d}U 00001A {epoch_str}"
|
||||
" .00000000 00000-0 00000-0 0 999"
|
||||
)
|
||||
return core + compute_tle_checksum(core)
|
||||
|
||||
|
||||
def build_tle_line2(
|
||||
norad_cat_id: Any,
|
||||
inclination: Any,
|
||||
raan: Any,
|
||||
eccentricity: Any,
|
||||
arg_of_perigee: Any,
|
||||
mean_anomaly: Any,
|
||||
mean_motion: Any,
|
||||
) -> Optional[str]:
|
||||
"""Build a valid TLE line 2 from the standard orbital elements."""
|
||||
required = [
|
||||
norad_cat_id,
|
||||
inclination,
|
||||
raan,
|
||||
eccentricity,
|
||||
arg_of_perigee,
|
||||
mean_anomaly,
|
||||
mean_motion,
|
||||
]
|
||||
if any(value is None for value in required):
|
||||
return None
|
||||
|
||||
eccentricity_digits = str(round(float(eccentricity) * 10_000_000)).zfill(7)
|
||||
core = (
|
||||
f"2 {int(norad_cat_id):05d}"
|
||||
f" {float(inclination):8.4f}"
|
||||
f" {float(raan):8.4f}"
|
||||
f" {eccentricity_digits}"
|
||||
f" {float(arg_of_perigee):8.4f}"
|
||||
f" {float(mean_anomaly):8.4f}"
|
||||
f" {float(mean_motion):11.8f}"
|
||||
"00000"
|
||||
)
|
||||
return core + compute_tle_checksum(core)
|
||||
|
||||
|
||||
def build_tle_lines_from_elements(
|
||||
*,
|
||||
norad_cat_id: Any,
|
||||
epoch: Any,
|
||||
inclination: Any,
|
||||
raan: Any,
|
||||
eccentricity: Any,
|
||||
arg_of_perigee: Any,
|
||||
mean_anomaly: Any,
|
||||
mean_motion: Any,
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Build both TLE lines from a metadata payload."""
|
||||
line1 = build_tle_line1(norad_cat_id, epoch)
|
||||
line2 = build_tle_line2(
|
||||
norad_cat_id,
|
||||
inclination,
|
||||
raan,
|
||||
eccentricity,
|
||||
arg_of_perigee,
|
||||
mean_anomaly,
|
||||
mean_motion,
|
||||
)
|
||||
return line1, line2
|
||||
Reference in New Issue
Block a user