117 lines
3.1 KiB
Python
117 lines
3.1 KiB
Python
"""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
|