mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
Fix Meteor LRPT decoding in Docker and enhance weather satellite UI
Docker fixes: - Add missing COPY for /usr/local/share/ (pipeline definitions were never reaching the runtime image — root cause of silent SatDump failures) - Add libfftw3-double3 and libfftw3-single3 runtime dependencies - Handle arm64 vs x86 install path differences (/usr vs /usr/local) - Split SatDump compile and staging into separate layers for better caching - Add build-time assertions to catch missing pipelines early UI enhancements: - Timezone selector (UTC, Local, Eastern, Central, Mountain, Pacific) with localStorage persistence — all time displays update instantly - Pass analysis bar showing 24h quality breakdown and best upcoming pass - Enhanced pass cards with cardinal direction (NW→SE), BEST badge - Console timestamps, log level filters (ALL/SIGNAL/PROG/ERR), COPY/CLR - Pass count in stats strip - Demo data mode for UI testing without SDR or live satellite pass - Meteor M2-4 80k baud fallback pipeline option Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+102
-93
@@ -1,14 +1,14 @@
|
||||
"""Weather satellite decoder focused on Meteor LRPT workflows.
|
||||
|
||||
Provides automated capture and decoding of weather imagery using SatDump.
|
||||
|
||||
Active satellites:
|
||||
- Meteor-M2-3: 137.900 MHz (LRPT)
|
||||
- Meteor-M2-4: 137.900 MHz (LRPT)
|
||||
|
||||
Legacy NOAA APT entries remain in ``WEATHER_SATELLITES`` for compatibility
|
||||
and historical metadata, but they are no longer active operational targets.
|
||||
"""
|
||||
"""Weather satellite decoder focused on Meteor LRPT workflows.
|
||||
|
||||
Provides automated capture and decoding of weather imagery using SatDump.
|
||||
|
||||
Active satellites:
|
||||
- Meteor-M2-3: 137.900 MHz (LRPT)
|
||||
- Meteor-M2-4: 137.900 MHz (LRPT)
|
||||
|
||||
Legacy NOAA APT entries remain in ``WEATHER_SATELLITES`` for compatibility
|
||||
and historical metadata, but they are no longer active operational targets.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,17 +29,17 @@ from typing import Callable
|
||||
from utils.logging import get_logger
|
||||
from utils.process import register_process, safe_terminate
|
||||
|
||||
logger = get_logger('intercept.weather_sat')
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
ALLOWED_OFFLINE_INPUT_DIRS = (
|
||||
PROJECT_ROOT / 'data',
|
||||
PROJECT_ROOT / 'instance' / 'ground_station' / 'recordings',
|
||||
)
|
||||
logger = get_logger('intercept.weather_sat')
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
ALLOWED_OFFLINE_INPUT_DIRS = (
|
||||
PROJECT_ROOT / 'data',
|
||||
PROJECT_ROOT / 'instance' / 'ground_station' / 'recordings',
|
||||
)
|
||||
|
||||
|
||||
# Weather satellite definitions.
|
||||
# NOAA APT entries are retained as inactive compatibility metadata.
|
||||
# Weather satellite definitions.
|
||||
# NOAA APT entries are retained as inactive compatibility metadata.
|
||||
WEATHER_SATELLITES = {
|
||||
'NOAA-15': {
|
||||
'name': 'NOAA 15',
|
||||
@@ -86,6 +86,15 @@ WEATHER_SATELLITES = {
|
||||
'description': 'Meteor-M2-4 LRPT (digital color imagery)',
|
||||
'active': True,
|
||||
},
|
||||
'METEOR-M2-4-80K': {
|
||||
'name': 'Meteor-M2-4 (80k)',
|
||||
'frequency': 137.900,
|
||||
'mode': 'LRPT',
|
||||
'pipeline': 'meteor_m2-x_lrpt_80k',
|
||||
'tle_key': 'METEOR-M2-4',
|
||||
'description': 'Meteor-M2-4 LRPT 80k baud (fallback symbol rate)',
|
||||
'active': True,
|
||||
},
|
||||
}
|
||||
|
||||
# Default sample rate for weather satellite reception
|
||||
@@ -153,12 +162,12 @@ class CaptureProgress:
|
||||
return result
|
||||
|
||||
|
||||
class WeatherSatDecoder:
|
||||
"""Weather satellite decoder using SatDump CLI.
|
||||
|
||||
Manages live SDR capture and offline decode for the active Meteor LRPT
|
||||
workflow, while preserving compatibility with older weather-sat metadata.
|
||||
"""
|
||||
class WeatherSatDecoder:
|
||||
"""Weather satellite decoder using SatDump CLI.
|
||||
|
||||
Manages live SDR capture and offline decode for the active Meteor LRPT
|
||||
workflow, while preserving compatibility with older weather-sat metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, output_dir: str | Path | None = None):
|
||||
self._process: subprocess.Popen | None = None
|
||||
@@ -175,14 +184,14 @@ class WeatherSatDecoder:
|
||||
self._pty_master_fd: int | None = None
|
||||
self._current_satellite: str = ''
|
||||
self._current_frequency: float = 0.0
|
||||
self._current_mode: str = ''
|
||||
self._capture_start_time: float = 0
|
||||
self._device_index: int = -1
|
||||
self._capture_output_dir: Path | None = None
|
||||
self._on_complete_callback: Callable[[], None] | None = None
|
||||
self._capture_phase: str = 'idle'
|
||||
self._last_error_message: str = ''
|
||||
self._last_process_returncode: int | None = None
|
||||
self._current_mode: str = ''
|
||||
self._capture_start_time: float = 0
|
||||
self._device_index: int = -1
|
||||
self._capture_output_dir: Path | None = None
|
||||
self._on_complete_callback: Callable[[], None] | None = None
|
||||
self._capture_phase: str = 'idle'
|
||||
self._last_error_message: str = ''
|
||||
self._last_process_returncode: int | None = None
|
||||
|
||||
# Ensure output directory exists
|
||||
self._output_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -251,7 +260,7 @@ class WeatherSatDecoder:
|
||||
No SDR hardware is required — SatDump runs in offline mode.
|
||||
|
||||
Args:
|
||||
satellite: Satellite key (for example ``'METEOR-M2-3'``)
|
||||
satellite: Satellite key (for example ``'METEOR-M2-3'``)
|
||||
input_file: Path to IQ baseband or WAV audio file
|
||||
sample_rate: Sample rate of the recording in Hz
|
||||
|
||||
@@ -283,16 +292,16 @@ class WeatherSatDecoder:
|
||||
|
||||
input_path = Path(input_file)
|
||||
|
||||
# Security: restrict offline decode inputs to application-owned
|
||||
# capture directories so external paths cannot be injected.
|
||||
try:
|
||||
resolved = input_path.resolve()
|
||||
if not any(resolved.is_relative_to(base) for base in ALLOWED_OFFLINE_INPUT_DIRS):
|
||||
logger.warning(f"Path traversal blocked in start_from_file: {input_file}")
|
||||
msg = 'Input file must be under INTERCEPT data or ground-station recordings'
|
||||
self._emit_progress(CaptureProgress(
|
||||
status='error',
|
||||
message=msg,
|
||||
# Security: restrict offline decode inputs to application-owned
|
||||
# capture directories so external paths cannot be injected.
|
||||
try:
|
||||
resolved = input_path.resolve()
|
||||
if not any(resolved.is_relative_to(base) for base in ALLOWED_OFFLINE_INPUT_DIRS):
|
||||
logger.warning(f"Path traversal blocked in start_from_file: {input_file}")
|
||||
msg = 'Input file must be under INTERCEPT data or ground-station recordings'
|
||||
self._emit_progress(CaptureProgress(
|
||||
status='error',
|
||||
message=msg,
|
||||
))
|
||||
return False, msg
|
||||
except (OSError, ValueError):
|
||||
@@ -314,13 +323,13 @@ class WeatherSatDecoder:
|
||||
|
||||
self._current_satellite = satellite
|
||||
self._current_frequency = sat_info['frequency']
|
||||
self._current_mode = sat_info['mode']
|
||||
self._device_index = -1 # Offline decode does not claim an SDR device
|
||||
self._capture_start_time = time.time()
|
||||
self._capture_phase = 'decoding'
|
||||
self._last_error_message = ''
|
||||
self._last_process_returncode = None
|
||||
self._stop_event.clear()
|
||||
self._current_mode = sat_info['mode']
|
||||
self._device_index = -1 # Offline decode does not claim an SDR device
|
||||
self._capture_start_time = time.time()
|
||||
self._capture_phase = 'decoding'
|
||||
self._last_error_message = ''
|
||||
self._last_process_returncode = None
|
||||
self._stop_event.clear()
|
||||
|
||||
try:
|
||||
self._running = True
|
||||
@@ -368,7 +377,7 @@ class WeatherSatDecoder:
|
||||
"""Start weather satellite capture and decode.
|
||||
|
||||
Args:
|
||||
satellite: Satellite key (for example ``'METEOR-M2-3'``)
|
||||
satellite: Satellite key (for example ``'METEOR-M2-3'``)
|
||||
device_index: RTL-SDR device index
|
||||
gain: SDR gain in dB
|
||||
sample_rate: Sample rate in Hz
|
||||
@@ -410,13 +419,13 @@ class WeatherSatDecoder:
|
||||
|
||||
self._current_satellite = satellite
|
||||
self._current_frequency = sat_info['frequency']
|
||||
self._current_mode = sat_info['mode']
|
||||
self._device_index = device_index
|
||||
self._capture_start_time = time.time()
|
||||
self._capture_phase = 'tuning'
|
||||
self._last_error_message = ''
|
||||
self._last_process_returncode = None
|
||||
self._stop_event.clear()
|
||||
self._current_mode = sat_info['mode']
|
||||
self._device_index = device_index
|
||||
self._capture_start_time = time.time()
|
||||
self._capture_phase = 'tuning'
|
||||
self._last_error_message = ''
|
||||
self._last_process_returncode = None
|
||||
self._stop_event.clear()
|
||||
|
||||
try:
|
||||
self._running = True
|
||||
@@ -890,17 +899,17 @@ class WeatherSatDecoder:
|
||||
|
||||
if was_running:
|
||||
# Collect exit status (returncode is only set after poll/wait)
|
||||
if process and process.returncode is None:
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
retcode = process.returncode if process else None
|
||||
self._last_process_returncode = retcode
|
||||
if retcode and retcode != 0:
|
||||
self._capture_phase = 'error'
|
||||
self._emit_progress(CaptureProgress(
|
||||
if process and process.returncode is None:
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
retcode = process.returncode if process else None
|
||||
self._last_process_returncode = retcode
|
||||
if retcode and retcode != 0:
|
||||
self._capture_phase = 'error'
|
||||
self._emit_progress(CaptureProgress(
|
||||
status='error',
|
||||
satellite=self._current_satellite,
|
||||
frequency=self._current_frequency,
|
||||
@@ -1143,15 +1152,15 @@ class WeatherSatDecoder:
|
||||
self._images.clear()
|
||||
return count
|
||||
|
||||
def _emit_progress(self, progress: CaptureProgress) -> None:
|
||||
"""Emit progress update to callback."""
|
||||
if progress.status == 'error' and progress.message:
|
||||
self._last_error_message = str(progress.message)
|
||||
if self._callback:
|
||||
try:
|
||||
self._callback(progress)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in progress callback: {e}")
|
||||
def _emit_progress(self, progress: CaptureProgress) -> None:
|
||||
"""Emit progress update to callback."""
|
||||
if progress.status == 'error' and progress.message:
|
||||
self._last_error_message = str(progress.message)
|
||||
if self._callback:
|
||||
try:
|
||||
self._callback(progress)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in progress callback: {e}")
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""Get current decoder status."""
|
||||
@@ -1159,19 +1168,19 @@ class WeatherSatDecoder:
|
||||
if self._running and self._capture_start_time:
|
||||
elapsed = int(time.time() - self._capture_start_time)
|
||||
|
||||
return {
|
||||
'available': self._decoder is not None,
|
||||
'decoder': self._decoder,
|
||||
'running': self._running,
|
||||
'satellite': self._current_satellite,
|
||||
'frequency': self._current_frequency,
|
||||
'mode': self._current_mode,
|
||||
'capture_phase': self._capture_phase,
|
||||
'elapsed_seconds': elapsed,
|
||||
'image_count': len(self._images),
|
||||
'last_error': self._last_error_message,
|
||||
'last_returncode': self._last_process_returncode,
|
||||
}
|
||||
return {
|
||||
'available': self._decoder is not None,
|
||||
'decoder': self._decoder,
|
||||
'running': self._running,
|
||||
'satellite': self._current_satellite,
|
||||
'frequency': self._current_frequency,
|
||||
'mode': self._current_mode,
|
||||
'capture_phase': self._capture_phase,
|
||||
'elapsed_seconds': elapsed,
|
||||
'image_count': len(self._images),
|
||||
'last_error': self._last_error_message,
|
||||
'last_returncode': self._last_process_returncode,
|
||||
}
|
||||
|
||||
|
||||
# Global decoder instance
|
||||
|
||||
Reference in New Issue
Block a user