diff --git a/config.py b/config.py index db9db3f..76635b8 100644 --- a/config.py +++ b/config.py @@ -331,7 +331,7 @@ SATELLITE_ORBIT_MINUTES = _get_env_int('SATELLITE_ORBIT_MINUTES', 45) # Weather satellite settings WEATHER_SAT_DEFAULT_GAIN = _get_env_float('WEATHER_SAT_GAIN', 40.0) -WEATHER_SAT_SAMPLE_RATE = _get_env_int('WEATHER_SAT_SAMPLE_RATE', 1000000) +WEATHER_SAT_SAMPLE_RATE = _get_env_int('WEATHER_SAT_SAMPLE_RATE', 2400000) WEATHER_SAT_MIN_ELEVATION = _get_env_float('WEATHER_SAT_MIN_ELEVATION', 15.0) WEATHER_SAT_PREDICTION_HOURS = _get_env_int('WEATHER_SAT_PREDICTION_HOURS', 24) WEATHER_SAT_SCHEDULE_REFRESH_MINUTES = _get_env_int('WEATHER_SAT_SCHEDULE_REFRESH_MINUTES', 30) diff --git a/routes/weather_sat.py b/routes/weather_sat.py index 6222b52..9522539 100644 --- a/routes/weather_sat.py +++ b/routes/weather_sat.py @@ -18,6 +18,7 @@ from utils.weather_sat import ( is_weather_sat_available, CaptureProgress, WEATHER_SATELLITES, + DEFAULT_SAMPLE_RATE, ) logger = get_logger('intercept.weather_sat') @@ -164,6 +165,7 @@ def start_capture(): satellite=satellite, device_index=device_index, gain=gain, + sample_rate=DEFAULT_SAMPLE_RATE, bias_t=bias_t, ) @@ -563,26 +565,26 @@ def enable_schedule(): 'message': 'Invalid parameter value' }), 400 - scheduler = get_weather_sat_scheduler() - scheduler.set_callbacks(_progress_callback, _scheduler_event_callback) - - try: - result = scheduler.enable( - lat=lat, - lon=lon, - min_elevation=min_elev, - device=device, - gain=gain_val, - bias_t=bool(data.get('bias_t', False)), - ) - except Exception as e: - logger.exception("Failed to enable weather sat scheduler") - return jsonify({ - 'status': 'error', - 'message': 'Failed to enable scheduler' - }), 500 - - return jsonify({'status': 'ok', **result}) + scheduler = get_weather_sat_scheduler() + scheduler.set_callbacks(_progress_callback, _scheduler_event_callback) + + try: + result = scheduler.enable( + lat=lat, + lon=lon, + min_elevation=min_elev, + device=device, + gain=gain_val, + bias_t=bool(data.get('bias_t', False)), + ) + except Exception as e: + logger.exception("Failed to enable weather sat scheduler") + return jsonify({ + 'status': 'error', + 'message': 'Failed to enable scheduler' + }), 500 + + return jsonify({'status': 'ok', **result}) @weather_sat_bp.route('/schedule/disable', methods=['POST']) diff --git a/utils/weather_sat.py b/utils/weather_sat.py index c03fcd0..9ebbdee 100644 --- a/utils/weather_sat.py +++ b/utils/weather_sat.py @@ -85,7 +85,11 @@ WEATHER_SATELLITES = { } # Default sample rate for weather satellite reception -DEFAULT_SAMPLE_RATE = 1000000 # 1 MHz +try: + from config import WEATHER_SAT_SAMPLE_RATE as _configured_rate + DEFAULT_SAMPLE_RATE = _configured_rate +except ImportError: + DEFAULT_SAMPLE_RATE = 2400000 # 2.4 MHz — minimum for Meteor LRPT @dataclass diff --git a/utils/weather_sat_scheduler.py b/utils/weather_sat_scheduler.py index d4ccb24..898019c 100644 --- a/utils/weather_sat_scheduler.py +++ b/utils/weather_sat_scheduler.py @@ -4,12 +4,12 @@ Automatically captures satellite passes based on predicted pass times. Uses threading.Timer for scheduling — no external dependencies required. """ -from __future__ import annotations - -import threading -import uuid -from datetime import datetime, timezone, timedelta -from typing import Any, Callable +from __future__ import annotations + +import threading +import uuid +from datetime import datetime, timezone, timedelta +from typing import Any, Callable from utils.logging import get_logger from utils.weather_sat import get_weather_sat_decoder, WEATHER_SATELLITES, CaptureProgress @@ -21,13 +21,15 @@ try: from config import ( WEATHER_SAT_SCHEDULE_REFRESH_MINUTES, WEATHER_SAT_CAPTURE_BUFFER_SECONDS, + WEATHER_SAT_SAMPLE_RATE, ) except ImportError: WEATHER_SAT_SCHEDULE_REFRESH_MINUTES = 30 WEATHER_SAT_CAPTURE_BUFFER_SECONDS = 30 + WEATHER_SAT_SAMPLE_RATE = 2400000 -class ScheduledPass: +class ScheduledPass: """A pass scheduled for automatic capture.""" def __init__(self, pass_data: dict[str, Any]): @@ -46,13 +48,13 @@ class ScheduledPass: self._timer: threading.Timer | None = None self._stop_timer: threading.Timer | None = None - @property - def start_dt(self) -> datetime: - return _parse_utc_iso(self.start_time) - - @property - def end_dt(self) -> datetime: - return _parse_utc_iso(self.end_time) + @property + def start_dt(self) -> datetime: + return _parse_utc_iso(self.start_time) + + @property + def end_dt(self) -> datetime: + return _parse_utc_iso(self.end_time) def to_dict(self) -> dict[str, Any]: return { @@ -71,7 +73,7 @@ class ScheduledPass: } -class WeatherSatScheduler: +class WeatherSatScheduler: """Auto-scheduler for weather satellite captures.""" def __init__(self): @@ -200,10 +202,10 @@ class WeatherSatScheduler: with self._lock: return [p.to_dict() for p in self._passes] - def _refresh_passes(self) -> None: - """Recompute passes and schedule timers.""" - if not self._enabled: - return + def _refresh_passes(self) -> None: + """Recompute passes and schedule timers.""" + if not self._enabled: + return try: from utils.weather_sat_predict import predict_passes @@ -227,39 +229,39 @@ class WeatherSatScheduler: p._stop_timer.cancel() # Keep completed/skipped for history, replace scheduled - history = [p for p in self._passes if p.status in ('complete', 'skipped', 'capturing')] - self._passes = history - - now = datetime.now(timezone.utc) - buffer = WEATHER_SAT_CAPTURE_BUFFER_SECONDS - - for pass_data in passes: - try: - sp = ScheduledPass(pass_data) - start_dt = sp.start_dt - end_dt = sp.end_dt - except Exception as e: - logger.warning(f"Skipping invalid pass data: {e}") - continue - - capture_start = start_dt - timedelta(seconds=buffer) - capture_end = end_dt + timedelta(seconds=buffer) - - # Skip passes that are already over - if capture_end <= now: - continue - - # Check if already in history - if any(h.id == sp.id for h in history): - continue - - # Schedule capture timer. If we're already inside the capture - # window, trigger immediately instead of skipping the pass. - delay = max(0.0, (capture_start - now).total_seconds()) - sp._timer = threading.Timer(delay, self._execute_capture, args=[sp]) - sp._timer.daemon = True - sp._timer.start() - self._passes.append(sp) + history = [p for p in self._passes if p.status in ('complete', 'skipped', 'capturing')] + self._passes = history + + now = datetime.now(timezone.utc) + buffer = WEATHER_SAT_CAPTURE_BUFFER_SECONDS + + for pass_data in passes: + try: + sp = ScheduledPass(pass_data) + start_dt = sp.start_dt + end_dt = sp.end_dt + except Exception as e: + logger.warning(f"Skipping invalid pass data: {e}") + continue + + capture_start = start_dt - timedelta(seconds=buffer) + capture_end = end_dt + timedelta(seconds=buffer) + + # Skip passes that are already over + if capture_end <= now: + continue + + # Check if already in history + if any(h.id == sp.id for h in history): + continue + + # Schedule capture timer. If we're already inside the capture + # window, trigger immediately instead of skipping the pass. + delay = max(0.0, (capture_start - now).total_seconds()) + sp._timer = threading.Timer(delay, self._execute_capture, args=[sp]) + sp._timer.daemon = True + sp._timer.start() + self._passes.append(sp) logger.info( f"Scheduler refreshed: {sum(1 for p in self._passes if p.status == 'scheduled')} " @@ -330,6 +332,7 @@ class WeatherSatScheduler: satellite=sp.satellite, device_index=self._device, gain=self._gain, + sample_rate=WEATHER_SAT_SAMPLE_RATE, bias_t=self._bias_t, ) @@ -374,31 +377,31 @@ class WeatherSatScheduler: def _emit_event(self, event: dict[str, Any]) -> None: """Emit scheduler event to callback.""" - if self._event_callback: - try: - self._event_callback(event) - except Exception as e: - logger.error(f"Error in scheduler event callback: {e}") - - -def _parse_utc_iso(value: str) -> datetime: - """Parse UTC ISO8601 timestamp robustly across Python versions.""" - if not value: - raise ValueError("missing timestamp") - - text = str(value).strip() - # Backward compatibility for malformed legacy strings. - text = text.replace('+00:00Z', 'Z') - # Python <3.11 does not accept trailing 'Z' in fromisoformat. - if text.endswith('Z'): - text = text[:-1] + '+00:00' - - dt = datetime.fromisoformat(text) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - else: - dt = dt.astimezone(timezone.utc) - return dt + if self._event_callback: + try: + self._event_callback(event) + except Exception as e: + logger.error(f"Error in scheduler event callback: {e}") + + +def _parse_utc_iso(value: str) -> datetime: + """Parse UTC ISO8601 timestamp robustly across Python versions.""" + if not value: + raise ValueError("missing timestamp") + + text = str(value).strip() + # Backward compatibility for malformed legacy strings. + text = text.replace('+00:00Z', 'Z') + # Python <3.11 does not accept trailing 'Z' in fromisoformat. + if text.endswith('Z'): + text = text[:-1] + '+00:00' + + dt = datetime.fromisoformat(text) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + else: + dt = dt.astimezone(timezone.utc) + return dt # Singleton