Files
intercept/utils/ground_station/meteor_backend.py
2026-03-18 22:01:52 +00:00

190 lines
5.2 KiB
Python

"""Meteor LRPT offline decode backend for ground-station observations."""
from __future__ import annotations
import threading
import time
from pathlib import Path
from utils.logging import get_logger
from utils.weather_sat import WeatherSatDecoder
logger = get_logger('intercept.ground_station.meteor_backend')
OUTPUT_ROOT = Path('instance/ground_station/weather_outputs')
DECODE_TIMEOUT_SECONDS = 30 * 60
_NORAD_TO_SAT_KEY = {
57166: 'METEOR-M2-3',
59051: 'METEOR-M2-4',
}
def resolve_meteor_satellite_key(norad_id: int, satellite_name: str) -> str | None:
if norad_id in _NORAD_TO_SAT_KEY:
return _NORAD_TO_SAT_KEY[norad_id]
upper = str(satellite_name or '').upper()
if 'M2-4' in upper:
return 'METEOR-M2-4'
if 'M2-3' in upper or 'METEOR' in upper:
return 'METEOR-M2-3'
return None
def launch_meteor_decode(
*,
obs_db_id: int | None,
norad_id: int,
satellite_name: str,
sample_rate: int,
data_path: Path,
emit_event,
register_output,
) -> None:
"""Run Meteor LRPT offline decode in a background thread."""
t = threading.Thread(
target=_run_decode,
kwargs={
'obs_db_id': obs_db_id,
'norad_id': norad_id,
'satellite_name': satellite_name,
'sample_rate': sample_rate,
'data_path': data_path,
'emit_event': emit_event,
'register_output': register_output,
},
daemon=True,
name=f'gs-meteor-decode-{norad_id}',
)
t.start()
def _run_decode(
*,
obs_db_id: int | None,
norad_id: int,
satellite_name: str,
sample_rate: int,
data_path: Path,
emit_event,
register_output,
) -> None:
sat_key = resolve_meteor_satellite_key(norad_id, satellite_name)
if not sat_key:
emit_event({
'type': 'weather_decode_failed',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
'message': 'No Meteor satellite mapping is available for this observation.',
})
return
output_dir = OUTPUT_ROOT / f'{norad_id}_{int(time.time())}'
decoder = WeatherSatDecoder(output_dir=output_dir)
if decoder.decoder_available is None:
emit_event({
'type': 'weather_decode_failed',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
'message': 'SatDump backend is not available for Meteor LRPT decode.',
})
return
def _progress_cb(progress):
progress_event = progress.to_dict()
progress_event.pop('type', None)
emit_event({
'type': 'weather_decode_progress',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
**progress_event,
})
decoder.set_callback(_progress_cb)
emit_event({
'type': 'weather_decode_started',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
'input_path': str(data_path),
})
ok, error = decoder.start_from_file(
satellite=sat_key,
input_file=data_path,
sample_rate=sample_rate,
)
if not ok:
emit_event({
'type': 'weather_decode_failed',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
'message': error or 'Meteor decode failed to start.',
})
return
started = time.time()
while decoder.is_running and (time.time() - started) < DECODE_TIMEOUT_SECONDS:
time.sleep(1.0)
if decoder.is_running:
decoder.stop()
emit_event({
'type': 'weather_decode_failed',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
'message': 'Meteor decode timed out.',
})
return
images = decoder.get_images()
if not images:
emit_event({
'type': 'weather_decode_failed',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
'message': 'Decode completed but no image outputs were produced.',
})
return
outputs = []
for image in images:
metadata = {
'satellite': image.satellite,
'mode': image.mode,
'frequency': image.frequency,
'product': image.product,
'timestamp': image.timestamp.isoformat(),
'size_bytes': image.size_bytes,
}
output_id = register_output(
observation_id=obs_db_id,
norad_id=norad_id,
output_type='image',
backend='meteor_lrpt',
file_path=image.path,
preview_path=image.path,
metadata=metadata,
)
outputs.append({
'id': output_id,
'file_path': str(image.path),
'filename': image.filename,
'product': image.product,
})
emit_event({
'type': 'weather_decode_complete',
'norad_id': norad_id,
'satellite': satellite_name,
'backend': 'meteor_lrpt',
'outputs': outputs,
})