mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 17:18:11 -07:00
96172ca593
First-time run of ruff-format via pre-commit hook normalises quote style, trailing commas, and whitespace across 188 Python files. No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
776 lines
24 KiB
Python
776 lines
24 KiB
Python
"""Weather Satellite decoder routes.
|
|
|
|
Provides endpoints for capturing and decoding Meteor LRPT weather
|
|
imagery, including shared results produced by the ground-station
|
|
observation pipeline.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import queue
|
|
from pathlib import Path
|
|
|
|
from flask import Blueprint, Response, jsonify, request, send_file
|
|
|
|
from utils.logging import get_logger
|
|
from utils.responses import api_error
|
|
from utils.sse import sse_stream
|
|
from utils.validation import (
|
|
validate_device_index,
|
|
validate_elevation,
|
|
validate_gain,
|
|
validate_latitude,
|
|
validate_longitude,
|
|
validate_rtl_tcp_host,
|
|
validate_rtl_tcp_port,
|
|
)
|
|
from utils.weather_sat import (
|
|
DEFAULT_SAMPLE_RATE,
|
|
WEATHER_SATELLITES,
|
|
CaptureProgress,
|
|
get_weather_sat_decoder,
|
|
is_weather_sat_available,
|
|
)
|
|
|
|
logger = get_logger("intercept.weather_sat")
|
|
|
|
weather_sat_bp = Blueprint("weather_sat", __name__, url_prefix="/weather-sat")
|
|
|
|
# Queue for SSE progress streaming
|
|
_weather_sat_queue: queue.Queue = queue.Queue(maxsize=100)
|
|
|
|
METEOR_NORAD_IDS = {
|
|
"METEOR-M2-3": 57166,
|
|
"METEOR-M2-4": 59051,
|
|
}
|
|
ALLOWED_TEST_DECODE_DIRS = (
|
|
Path(__file__).resolve().parent.parent / "data",
|
|
Path(__file__).resolve().parent.parent / "instance" / "ground_station" / "recordings",
|
|
)
|
|
|
|
|
|
def _progress_callback(progress: CaptureProgress) -> None:
|
|
"""Callback to queue progress updates for SSE stream."""
|
|
try:
|
|
_weather_sat_queue.put_nowait(progress.to_dict())
|
|
except queue.Full:
|
|
try:
|
|
_weather_sat_queue.get_nowait()
|
|
_weather_sat_queue.put_nowait(progress.to_dict())
|
|
except queue.Empty:
|
|
pass
|
|
|
|
|
|
def _release_weather_sat_device(device_index: int) -> None:
|
|
"""Release an SDR device only if weather-sat currently owns it."""
|
|
if device_index < 0:
|
|
return
|
|
|
|
try:
|
|
import app as app_module
|
|
except ImportError:
|
|
return
|
|
|
|
owner = None
|
|
get_status = getattr(app_module, "get_sdr_device_status", None)
|
|
if callable(get_status):
|
|
try:
|
|
owner = get_status().get(device_index)
|
|
except Exception:
|
|
owner = None
|
|
|
|
if owner and owner != "weather_sat":
|
|
logger.debug(
|
|
"Skipping SDR release for device %s owned by %s",
|
|
device_index,
|
|
owner,
|
|
)
|
|
return
|
|
|
|
app_module.release_sdr_device(device_index)
|
|
|
|
|
|
@weather_sat_bp.route("/status")
|
|
def get_status():
|
|
"""Get weather satellite decoder status.
|
|
|
|
Returns:
|
|
JSON with decoder availability and current status.
|
|
"""
|
|
decoder = get_weather_sat_decoder()
|
|
return jsonify(decoder.get_status())
|
|
|
|
|
|
@weather_sat_bp.route("/satellites")
|
|
def list_satellites():
|
|
"""Get list of supported weather satellites with frequencies.
|
|
|
|
Returns:
|
|
JSON with satellite definitions.
|
|
"""
|
|
satellites = []
|
|
for key, info in WEATHER_SATELLITES.items():
|
|
satellites.append(
|
|
{
|
|
"key": key,
|
|
"name": info["name"],
|
|
"frequency": info["frequency"],
|
|
"mode": info["mode"],
|
|
"description": info["description"],
|
|
"active": info["active"],
|
|
}
|
|
)
|
|
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"satellites": satellites,
|
|
}
|
|
)
|
|
|
|
|
|
@weather_sat_bp.route("/start", methods=["POST"])
|
|
def start_capture():
|
|
"""Start weather satellite capture and decode.
|
|
|
|
JSON body:
|
|
{
|
|
"satellite": "METEOR-M2-3", // Required: satellite key
|
|
"device": 0, // RTL-SDR device index (default: 0)
|
|
"gain": 30.0, // SDR gain in dB (default: 30)
|
|
"bias_t": false // Enable bias-T for LNA (default: false)
|
|
}
|
|
|
|
Returns:
|
|
JSON with start status.
|
|
"""
|
|
if not is_weather_sat_available():
|
|
return jsonify(
|
|
{
|
|
"status": "error",
|
|
"message": "SatDump not installed. Build from source: https://github.com/SatDump/SatDump",
|
|
}
|
|
), 400
|
|
|
|
decoder = get_weather_sat_decoder()
|
|
|
|
if decoder.is_running:
|
|
return jsonify(
|
|
{
|
|
"status": "already_running",
|
|
"satellite": decoder.current_satellite,
|
|
"frequency": decoder.current_frequency,
|
|
}
|
|
)
|
|
|
|
data = request.get_json(silent=True) or {}
|
|
sdr_type_str = data.get("sdr_type", "rtlsdr")
|
|
|
|
if sdr_type_str != "rtlsdr":
|
|
return jsonify(
|
|
{
|
|
"status": "error",
|
|
"message": f"{sdr_type_str.replace('_', ' ').title()} is not yet supported for this mode. Please use an RTL-SDR device.",
|
|
}
|
|
), 400
|
|
|
|
# Validate satellite
|
|
satellite = data.get("satellite")
|
|
if not satellite or satellite not in WEATHER_SATELLITES:
|
|
return jsonify(
|
|
{"status": "error", "message": f"Invalid satellite. Must be one of: {', '.join(WEATHER_SATELLITES.keys())}"}
|
|
), 400
|
|
|
|
# Validate device index and gain
|
|
try:
|
|
device_index = validate_device_index(data.get("device", 0))
|
|
gain = validate_gain(data.get("gain", 30.0))
|
|
except ValueError as e:
|
|
logger.warning("Invalid parameter in start_capture: %s", e)
|
|
return jsonify({"status": "error", "message": "Invalid parameter value"}), 400
|
|
|
|
bias_t = bool(data.get("bias_t", False))
|
|
|
|
# Check for rtl_tcp (remote SDR) connection
|
|
rtl_tcp_host = data.get("rtl_tcp_host")
|
|
rtl_tcp_port = data.get("rtl_tcp_port", 1234)
|
|
|
|
if rtl_tcp_host:
|
|
try:
|
|
rtl_tcp_host = validate_rtl_tcp_host(rtl_tcp_host)
|
|
rtl_tcp_port = validate_rtl_tcp_port(rtl_tcp_port)
|
|
except ValueError as e:
|
|
return api_error(str(e), 400)
|
|
|
|
# Claim SDR device (skip for remote rtl_tcp)
|
|
if not rtl_tcp_host:
|
|
try:
|
|
import app as app_module
|
|
|
|
error = app_module.claim_sdr_device(device_index, "weather_sat", sdr_type_str)
|
|
if error:
|
|
return api_error(error, 409, error_type="DEVICE_BUSY")
|
|
except ImportError:
|
|
pass
|
|
|
|
# Clear queue
|
|
while not _weather_sat_queue.empty():
|
|
try:
|
|
_weather_sat_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
# Set callback and on-complete handler for SDR release
|
|
decoder.set_callback(_progress_callback)
|
|
|
|
def _release_device():
|
|
if not rtl_tcp_host:
|
|
_release_weather_sat_device(device_index)
|
|
|
|
decoder.set_on_complete(_release_device)
|
|
|
|
success, error_msg = decoder.start(
|
|
satellite=satellite,
|
|
device_index=device_index,
|
|
gain=gain,
|
|
sample_rate=DEFAULT_SAMPLE_RATE,
|
|
bias_t=bias_t,
|
|
rtl_tcp_host=rtl_tcp_host,
|
|
rtl_tcp_port=rtl_tcp_port,
|
|
)
|
|
|
|
if success:
|
|
sat_info = WEATHER_SATELLITES[satellite]
|
|
return jsonify(
|
|
{
|
|
"status": "started",
|
|
"satellite": satellite,
|
|
"frequency": sat_info["frequency"],
|
|
"mode": sat_info["mode"],
|
|
"device": device_index,
|
|
}
|
|
)
|
|
else:
|
|
# Release device on failure
|
|
_release_device()
|
|
return jsonify({"status": "error", "message": error_msg or "Failed to start capture"}), 500
|
|
|
|
|
|
@weather_sat_bp.route("/test-decode", methods=["POST"])
|
|
def test_decode():
|
|
"""Start weather satellite decode from a pre-recorded file.
|
|
|
|
No SDR hardware is required — decodes an IQ baseband or WAV file
|
|
using SatDump offline mode.
|
|
|
|
JSON body:
|
|
{
|
|
"satellite": "METEOR-M2-3", // Required: satellite key
|
|
"input_file": "/path/to/file", // Required: server-side file path
|
|
"sample_rate": 1000000 // Sample rate in Hz (default: 1000000)
|
|
}
|
|
|
|
Returns:
|
|
JSON with start status.
|
|
"""
|
|
if not is_weather_sat_available():
|
|
return jsonify(
|
|
{
|
|
"status": "error",
|
|
"message": "SatDump not installed. Build from source: https://github.com/SatDump/SatDump",
|
|
}
|
|
), 400
|
|
|
|
decoder = get_weather_sat_decoder()
|
|
|
|
if decoder.is_running:
|
|
return jsonify(
|
|
{
|
|
"status": "already_running",
|
|
"satellite": decoder.current_satellite,
|
|
"frequency": decoder.current_frequency,
|
|
}
|
|
)
|
|
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
# Validate satellite
|
|
satellite = data.get("satellite")
|
|
if not satellite or satellite not in WEATHER_SATELLITES:
|
|
return jsonify(
|
|
{"status": "error", "message": f"Invalid satellite. Must be one of: {', '.join(WEATHER_SATELLITES.keys())}"}
|
|
), 400
|
|
|
|
# Validate input file
|
|
input_file = data.get("input_file")
|
|
if not input_file:
|
|
return jsonify({"status": "error", "message": "input_file is required"}), 400
|
|
|
|
from pathlib import Path
|
|
|
|
input_path = Path(input_file)
|
|
|
|
# Restrict test-decode to application-owned sample and recording paths.
|
|
try:
|
|
resolved = input_path.resolve()
|
|
if not any(resolved.is_relative_to(base) for base in ALLOWED_TEST_DECODE_DIRS):
|
|
return jsonify(
|
|
{"status": "error", "message": "input_file must be under INTERCEPT data or ground-station recordings"}
|
|
), 403
|
|
except (OSError, ValueError):
|
|
return jsonify({"status": "error", "message": "Invalid file path"}), 400
|
|
|
|
if not input_path.is_file():
|
|
logger.warning("Test-decode file not found")
|
|
return jsonify({"status": "error", "message": "File not found"}), 404
|
|
|
|
# Validate sample rate
|
|
sample_rate = data.get("sample_rate", 1000000)
|
|
try:
|
|
sample_rate = int(sample_rate)
|
|
if sample_rate < 1000 or sample_rate > 20000000:
|
|
raise ValueError
|
|
except (TypeError, ValueError):
|
|
return jsonify({"status": "error", "message": "Invalid sample_rate (1000-20000000)"}), 400
|
|
|
|
# Clear queue
|
|
while not _weather_sat_queue.empty():
|
|
try:
|
|
_weather_sat_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
# Set callback — no on_complete needed (no SDR to release)
|
|
decoder.set_callback(_progress_callback)
|
|
decoder.set_on_complete(None)
|
|
|
|
success, error_msg = decoder.start_from_file(
|
|
satellite=satellite,
|
|
input_file=input_file,
|
|
sample_rate=sample_rate,
|
|
)
|
|
|
|
if success:
|
|
sat_info = WEATHER_SATELLITES[satellite]
|
|
return jsonify(
|
|
{
|
|
"status": "started",
|
|
"satellite": satellite,
|
|
"frequency": sat_info["frequency"],
|
|
"mode": sat_info["mode"],
|
|
"source": "file",
|
|
"input_file": str(input_file),
|
|
}
|
|
)
|
|
else:
|
|
return jsonify({"status": "error", "message": error_msg or "Failed to start file decode"}), 500
|
|
|
|
|
|
@weather_sat_bp.route("/stop", methods=["POST"])
|
|
def stop_capture():
|
|
"""Stop weather satellite capture.
|
|
|
|
Returns:
|
|
JSON confirmation.
|
|
"""
|
|
decoder = get_weather_sat_decoder()
|
|
device_index = decoder.device_index
|
|
|
|
decoder.stop()
|
|
|
|
_release_weather_sat_device(device_index)
|
|
|
|
return jsonify({"status": "stopped"})
|
|
|
|
|
|
@weather_sat_bp.route("/images")
|
|
def list_images():
|
|
"""Get list of decoded weather satellite images.
|
|
|
|
Query parameters:
|
|
limit: Maximum number of images (default: all)
|
|
satellite: Filter by satellite key (optional)
|
|
|
|
Returns:
|
|
JSON with list of decoded images.
|
|
"""
|
|
decoder = get_weather_sat_decoder()
|
|
images = [
|
|
{
|
|
**img.to_dict(),
|
|
"source": "weather_sat",
|
|
"deletable": True,
|
|
}
|
|
for img in decoder.get_images()
|
|
]
|
|
images.extend(_get_ground_station_images())
|
|
|
|
# Filter by satellite if specified
|
|
satellite_filter = request.args.get("satellite")
|
|
if satellite_filter:
|
|
images = [img for img in images if str(img.get("satellite", "")).upper() == satellite_filter.upper()]
|
|
|
|
images.sort(key=lambda img: img.get("timestamp") or "", reverse=True)
|
|
|
|
# Apply limit
|
|
limit = request.args.get("limit", type=int)
|
|
if limit and limit > 0:
|
|
images = images[:limit]
|
|
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"images": images,
|
|
"count": len(images),
|
|
}
|
|
)
|
|
|
|
|
|
@weather_sat_bp.route("/images/<filename>")
|
|
def get_image(filename: str):
|
|
"""Serve a decoded weather satellite image file.
|
|
|
|
Args:
|
|
filename: Image filename
|
|
|
|
Returns:
|
|
Image file or 404.
|
|
"""
|
|
decoder = get_weather_sat_decoder()
|
|
|
|
# Security: only allow safe filenames
|
|
if not filename.replace("_", "").replace("-", "").replace(".", "").isalnum():
|
|
return api_error("Invalid filename", 400)
|
|
|
|
if not (filename.endswith(".png") or filename.endswith(".jpg") or filename.endswith(".jpeg")):
|
|
return api_error("Only PNG/JPG files supported", 400)
|
|
|
|
image_path = decoder._output_dir / filename
|
|
|
|
if not image_path.exists():
|
|
return api_error("Image not found", 404)
|
|
|
|
mimetype = "image/png" if filename.endswith(".png") else "image/jpeg"
|
|
return send_file(image_path, mimetype=mimetype)
|
|
|
|
|
|
@weather_sat_bp.route("/images/shared/<int:output_id>")
|
|
def get_shared_image(output_id: int):
|
|
"""Serve a Meteor image stored in ground-station outputs."""
|
|
try:
|
|
from utils.database import get_db
|
|
|
|
with get_db() as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT file_path FROM ground_station_outputs
|
|
WHERE id=? AND output_type='image'
|
|
""",
|
|
(output_id,),
|
|
).fetchone()
|
|
except Exception as e:
|
|
logger.warning("Failed to load shared weather image %s: %s", output_id, e)
|
|
return api_error("Image not found", 404)
|
|
|
|
if not row:
|
|
return api_error("Image not found", 404)
|
|
|
|
image_path = Path(row["file_path"])
|
|
if not image_path.exists():
|
|
return api_error("Image not found", 404)
|
|
|
|
suffix = image_path.suffix.lower()
|
|
mimetype = "image/png" if suffix == ".png" else "image/jpeg"
|
|
return send_file(image_path, mimetype=mimetype)
|
|
|
|
|
|
@weather_sat_bp.route("/images/<filename>", methods=["DELETE"])
|
|
def delete_image(filename: str):
|
|
"""Delete a decoded image.
|
|
|
|
Args:
|
|
filename: Image filename
|
|
|
|
Returns:
|
|
JSON confirmation.
|
|
"""
|
|
decoder = get_weather_sat_decoder()
|
|
|
|
if not filename.replace("_", "").replace("-", "").replace(".", "").isalnum():
|
|
return api_error("Invalid filename", 400)
|
|
|
|
if decoder.delete_image(filename):
|
|
return jsonify({"status": "deleted", "filename": filename})
|
|
else:
|
|
return api_error("Image not found", 404)
|
|
|
|
|
|
@weather_sat_bp.route("/images", methods=["DELETE"])
|
|
def delete_all_images():
|
|
"""Delete all decoded weather satellite images.
|
|
|
|
Returns:
|
|
JSON with count of deleted images.
|
|
"""
|
|
decoder = get_weather_sat_decoder()
|
|
count = decoder.delete_all_images()
|
|
return jsonify({"status": "ok", "deleted": count})
|
|
|
|
|
|
def _get_ground_station_images() -> list[dict]:
|
|
try:
|
|
from utils.database import get_db
|
|
|
|
with get_db() as conn:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT id, norad_id, file_path, metadata_json, created_at
|
|
FROM ground_station_outputs
|
|
WHERE output_type='image' AND backend='meteor_lrpt'
|
|
ORDER BY created_at DESC
|
|
LIMIT 200
|
|
"""
|
|
).fetchall()
|
|
except Exception as e:
|
|
logger.debug("Failed to fetch ground-station weather outputs: %s", e)
|
|
return []
|
|
|
|
images: list[dict] = []
|
|
for row in rows:
|
|
file_path = Path(row["file_path"])
|
|
if not file_path.exists():
|
|
continue
|
|
|
|
metadata = {}
|
|
raw_metadata = row["metadata_json"]
|
|
if raw_metadata:
|
|
try:
|
|
metadata = json.loads(raw_metadata)
|
|
except json.JSONDecodeError:
|
|
metadata = {}
|
|
|
|
satellite = metadata.get("satellite") or _satellite_from_norad(row["norad_id"])
|
|
images.append(
|
|
{
|
|
"filename": file_path.name,
|
|
"satellite": satellite,
|
|
"mode": metadata.get("mode", "LRPT"),
|
|
"timestamp": metadata.get("timestamp") or row["created_at"],
|
|
"frequency": metadata.get("frequency", 137.9),
|
|
"size_bytes": metadata.get("size_bytes") or file_path.stat().st_size,
|
|
"product": metadata.get("product", ""),
|
|
"url": f"/weather-sat/images/shared/{row['id']}",
|
|
"source": "ground_station",
|
|
"deletable": False,
|
|
"output_id": row["id"],
|
|
}
|
|
)
|
|
return images
|
|
|
|
|
|
def _satellite_from_norad(norad_id: int | None) -> str:
|
|
for satellite, known_norad in METEOR_NORAD_IDS.items():
|
|
if known_norad == norad_id:
|
|
return satellite
|
|
return "METEOR"
|
|
|
|
|
|
@weather_sat_bp.route("/stream")
|
|
def stream_progress():
|
|
"""SSE stream of capture/decode progress.
|
|
|
|
Returns:
|
|
SSE stream (text/event-stream)
|
|
"""
|
|
response = Response(sse_stream(_weather_sat_queue), mimetype="text/event-stream")
|
|
response.headers["Cache-Control"] = "no-cache"
|
|
response.headers["X-Accel-Buffering"] = "no"
|
|
response.headers["Connection"] = "keep-alive"
|
|
return response
|
|
|
|
|
|
@weather_sat_bp.route("/passes")
|
|
def get_passes():
|
|
"""Get upcoming weather satellite passes for observer location.
|
|
|
|
Query parameters:
|
|
latitude: Observer latitude (required)
|
|
longitude: Observer longitude (required)
|
|
hours: Hours to predict ahead (default: 24, max: 72)
|
|
min_elevation: Minimum elevation in degrees (default: 15)
|
|
trajectory: Include az/el trajectory points (default: false)
|
|
ground_track: Include lat/lon ground track points (default: false)
|
|
|
|
Returns:
|
|
JSON with upcoming passes for all weather satellites.
|
|
"""
|
|
include_trajectory = request.args.get("trajectory", "false").lower() in ("true", "1")
|
|
include_ground_track = request.args.get("ground_track", "false").lower() in ("true", "1")
|
|
|
|
raw_lat = request.args.get("latitude")
|
|
raw_lon = request.args.get("longitude")
|
|
|
|
if raw_lat is None or raw_lon is None:
|
|
return api_error("latitude and longitude parameters required", 400)
|
|
|
|
try:
|
|
lat = validate_latitude(raw_lat)
|
|
lon = validate_longitude(raw_lon)
|
|
except ValueError as e:
|
|
logger.warning("Invalid coordinates in get_passes: %s", e)
|
|
return api_error("Invalid coordinates", 400)
|
|
|
|
hours = max(1, min(request.args.get("hours", 24, type=int), 72))
|
|
min_elevation = max(0, min(request.args.get("min_elevation", 15, type=float), 90))
|
|
|
|
try:
|
|
from utils.weather_sat_predict import predict_passes
|
|
|
|
all_passes = predict_passes(
|
|
lat=lat,
|
|
lon=lon,
|
|
hours=hours,
|
|
min_elevation=min_elevation,
|
|
include_trajectory=include_trajectory,
|
|
include_ground_track=include_ground_track,
|
|
)
|
|
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"passes": all_passes,
|
|
"count": len(all_passes),
|
|
"observer": {"latitude": lat, "longitude": lon},
|
|
"prediction_hours": hours,
|
|
"min_elevation": min_elevation,
|
|
}
|
|
)
|
|
|
|
except ImportError:
|
|
return jsonify({"status": "error", "message": "skyfield library not installed"}), 503
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error predicting passes: {e}")
|
|
return jsonify({"status": "error", "message": "Pass prediction failed"}), 500
|
|
|
|
|
|
# ========================
|
|
# Auto-Scheduler Endpoints
|
|
# ========================
|
|
|
|
|
|
def _scheduler_event_callback(event: dict) -> None:
|
|
"""Forward scheduler events to the SSE queue."""
|
|
try:
|
|
_weather_sat_queue.put_nowait(event)
|
|
except queue.Full:
|
|
try:
|
|
_weather_sat_queue.get_nowait()
|
|
_weather_sat_queue.put_nowait(event)
|
|
except queue.Empty:
|
|
pass
|
|
|
|
|
|
@weather_sat_bp.route("/schedule/enable", methods=["POST"])
|
|
def enable_schedule():
|
|
"""Enable auto-scheduling of weather satellite captures.
|
|
|
|
JSON body:
|
|
{
|
|
"latitude": 51.5, // Required
|
|
"longitude": -0.1, // Required
|
|
"min_elevation": 15, // Minimum pass elevation (default: 15)
|
|
"device": 0, // RTL-SDR device index (default: 0)
|
|
"gain": 30.0, // SDR gain (default: 30)
|
|
"bias_t": false // Enable bias-T (default: false)
|
|
}
|
|
|
|
Returns:
|
|
JSON with scheduler status.
|
|
"""
|
|
from utils.weather_sat_scheduler import get_weather_sat_scheduler
|
|
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
if data.get("latitude") is None or data.get("longitude") is None:
|
|
return jsonify({"status": "error", "message": "latitude and longitude required"}), 400
|
|
|
|
try:
|
|
lat = validate_latitude(data.get("latitude"))
|
|
lon = validate_longitude(data.get("longitude"))
|
|
min_elev = validate_elevation(data.get("min_elevation", 15))
|
|
device = validate_device_index(data.get("device", 0))
|
|
gain_val = validate_gain(data.get("gain", 40.0))
|
|
except ValueError as e:
|
|
logger.warning("Invalid parameter in enable_schedule: %s", e)
|
|
return jsonify({"status": "error", "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:
|
|
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"])
|
|
def disable_schedule():
|
|
"""Disable auto-scheduling."""
|
|
from utils.weather_sat_scheduler import get_weather_sat_scheduler
|
|
|
|
scheduler = get_weather_sat_scheduler()
|
|
result = scheduler.disable()
|
|
return jsonify(result)
|
|
|
|
|
|
@weather_sat_bp.route("/schedule/status")
|
|
def schedule_status():
|
|
"""Get current scheduler state."""
|
|
from utils.weather_sat_scheduler import get_weather_sat_scheduler
|
|
|
|
scheduler = get_weather_sat_scheduler()
|
|
return jsonify(scheduler.get_status())
|
|
|
|
|
|
@weather_sat_bp.route("/schedule/passes")
|
|
def schedule_passes():
|
|
"""List scheduled passes."""
|
|
from utils.weather_sat_scheduler import get_weather_sat_scheduler
|
|
|
|
scheduler = get_weather_sat_scheduler()
|
|
passes = scheduler.get_passes()
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"passes": passes,
|
|
"count": len(passes),
|
|
}
|
|
)
|
|
|
|
|
|
@weather_sat_bp.route("/schedule/skip/<pass_id>", methods=["POST"])
|
|
def skip_pass(pass_id: str):
|
|
"""Skip a scheduled pass."""
|
|
from utils.weather_sat_scheduler import get_weather_sat_scheduler
|
|
|
|
if not pass_id.replace("_", "").replace("-", "").isalnum():
|
|
return api_error("Invalid pass ID", 400)
|
|
|
|
scheduler = get_weather_sat_scheduler()
|
|
if scheduler.skip_pass(pass_id):
|
|
return jsonify({"status": "skipped", "pass_id": pass_id})
|
|
else:
|
|
return api_error("Pass not found or already processed", 404)
|