mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 17:18:11 -07:00
style: apply ruff-format to entire codebase
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>
This commit is contained in:
@@ -21,7 +21,7 @@ from utils.logging import get_logger
|
||||
from utils.process import register_process, safe_terminate, unregister_process
|
||||
from utils.waterfall_fft import cu8_to_complex
|
||||
|
||||
logger = get_logger('intercept.ground_station.fm_demod')
|
||||
logger = get_logger("intercept.ground_station.fm_demod")
|
||||
|
||||
AUDIO_RATE = 48_000 # Hz — standard rate for direwolf / multimon-ng
|
||||
|
||||
@@ -33,7 +33,7 @@ class FMDemodConsumer:
|
||||
self,
|
||||
decoder_cmd: list[str],
|
||||
*,
|
||||
modulation: str = 'fm',
|
||||
modulation: str = "fm",
|
||||
on_decoded: Callable[[str], None] | None = None,
|
||||
):
|
||||
"""
|
||||
@@ -104,10 +104,9 @@ class FMDemodConsumer:
|
||||
|
||||
def _start_proc(self) -> None:
|
||||
import shutil
|
||||
|
||||
if not shutil.which(self._decoder_cmd[0]):
|
||||
logger.warning(
|
||||
f"FMDemodConsumer: decoder '{self._decoder_cmd[0]}' not found — disabled"
|
||||
)
|
||||
logger.warning(f"FMDemodConsumer: decoder '{self._decoder_cmd[0]}' not found — disabled")
|
||||
return
|
||||
try:
|
||||
self._proc = subprocess.Popen(
|
||||
@@ -117,9 +116,7 @@ class FMDemodConsumer:
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
register_process(self._proc)
|
||||
self._stdout_thread = threading.Thread(
|
||||
target=self._read_stdout, daemon=True, name='fm-demod-stdout'
|
||||
)
|
||||
self._stdout_thread = threading.Thread(target=self._read_stdout, daemon=True, name="fm-demod-stdout")
|
||||
self._stdout_thread.start()
|
||||
except Exception as e:
|
||||
logger.error(f"FMDemodConsumer: failed to start decoder: {e}")
|
||||
@@ -130,7 +127,7 @@ class FMDemodConsumer:
|
||||
assert self._proc.stdout is not None
|
||||
try:
|
||||
for line in self._proc.stdout:
|
||||
decoded = line.decode('utf-8', errors='replace').rstrip()
|
||||
decoded = line.decode("utf-8", errors="replace").rstrip()
|
||||
if decoded and self._on_decoded:
|
||||
try:
|
||||
self._on_decoded(decoded)
|
||||
@@ -186,14 +183,14 @@ def _demodulate(
|
||||
if shifted.size < 16:
|
||||
return None, next_phase
|
||||
|
||||
if mod == 'fm':
|
||||
if mod == "fm":
|
||||
audio = np.angle(shifted[1:] * np.conj(shifted[:-1])).astype(np.float32)
|
||||
elif mod == 'am':
|
||||
elif mod == "am":
|
||||
envelope = np.abs(shifted).astype(np.float32)
|
||||
audio = envelope - float(np.mean(envelope))
|
||||
elif mod == 'usb':
|
||||
elif mod == "usb":
|
||||
audio = np.real(shifted).astype(np.float32)
|
||||
elif mod == 'lsb':
|
||||
elif mod == "lsb":
|
||||
audio = -np.real(shifted).astype(np.float32)
|
||||
else:
|
||||
audio = np.real(shifted).astype(np.float32)
|
||||
|
||||
@@ -23,9 +23,9 @@ import numpy as np
|
||||
from utils.logging import get_logger
|
||||
from utils.process import register_process, safe_terminate, unregister_process
|
||||
|
||||
logger = get_logger('intercept.ground_station.gr_satellites')
|
||||
logger = get_logger("intercept.ground_station.gr_satellites")
|
||||
|
||||
GR_SATELLITES_BIN = 'gr_satellites'
|
||||
GR_SATELLITES_BIN = "gr_satellites"
|
||||
|
||||
|
||||
class GrSatConsumer:
|
||||
@@ -106,10 +106,11 @@ class GrSatConsumer:
|
||||
cmd = [
|
||||
GR_SATELLITES_BIN,
|
||||
self._satellite_name,
|
||||
'--samplerate', str(sample_rate),
|
||||
'--iq',
|
||||
'--json',
|
||||
'-',
|
||||
"--samplerate",
|
||||
str(sample_rate),
|
||||
"--iq",
|
||||
"--json",
|
||||
"-",
|
||||
]
|
||||
try:
|
||||
self._proc = subprocess.Popen(
|
||||
@@ -124,7 +125,7 @@ class GrSatConsumer:
|
||||
target=self._read_stdout,
|
||||
args=(_json,),
|
||||
daemon=True,
|
||||
name='gr-sat-stdout',
|
||||
name="gr-sat-stdout",
|
||||
)
|
||||
self._stdout_thread.start()
|
||||
logger.info(f"GrSatConsumer started for '{self._satellite_name}'")
|
||||
@@ -138,14 +139,14 @@ class GrSatConsumer:
|
||||
assert self._proc.stdout is not None
|
||||
try:
|
||||
for line in self._proc.stdout:
|
||||
text = line.decode('utf-8', errors='replace').rstrip()
|
||||
text = line.decode("utf-8", errors="replace").rstrip()
|
||||
if not text:
|
||||
continue
|
||||
if self._on_decoded:
|
||||
try:
|
||||
data = _json.loads(text)
|
||||
except _json.JSONDecodeError:
|
||||
data = {'raw': text}
|
||||
data = {"raw": text}
|
||||
try:
|
||||
self._on_decoded(data)
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from utils.logging import get_logger
|
||||
from utils.sigmf import SigMFMetadata, SigMFWriter
|
||||
|
||||
logger = get_logger('intercept.ground_station.sigmf_consumer')
|
||||
logger = get_logger("intercept.ground_station.sigmf_consumer")
|
||||
|
||||
|
||||
class SigMFConsumer:
|
||||
|
||||
@@ -20,7 +20,7 @@ from utils.waterfall_fft import (
|
||||
quantize_to_uint8,
|
||||
)
|
||||
|
||||
logger = get_logger('intercept.ground_station.waterfall_consumer')
|
||||
logger = get_logger("intercept.ground_station.waterfall_consumer")
|
||||
|
||||
FFT_SIZE = 1024
|
||||
AVG_COUNT = 4
|
||||
@@ -52,7 +52,7 @@ class WaterfallConsumer:
|
||||
self._start_freq = 0.0
|
||||
self._end_freq = 0.0
|
||||
self._sample_rate = 0
|
||||
self._buffer = b''
|
||||
self._buffer = b""
|
||||
self._required_bytes = 0
|
||||
self._frame_interval = 1.0 / max(1, fps)
|
||||
self._last_frame_time = 0.0
|
||||
@@ -80,7 +80,7 @@ class WaterfallConsumer:
|
||||
)
|
||||
self._required_bytes = required_samples * 2 # 1 byte I + 1 byte Q
|
||||
self._frame_interval = 1.0 / max(1, self._fps)
|
||||
self._buffer = b''
|
||||
self._buffer = b""
|
||||
self._last_frame_time = 0.0
|
||||
|
||||
def on_chunk(self, raw: bytes) -> None:
|
||||
@@ -91,15 +91,13 @@ class WaterfallConsumer:
|
||||
if len(self._buffer) < self._required_bytes:
|
||||
return
|
||||
|
||||
chunk = self._buffer[-self._required_bytes:]
|
||||
self._buffer = b''
|
||||
chunk = self._buffer[-self._required_bytes :]
|
||||
self._buffer = b""
|
||||
self._last_frame_time = now
|
||||
|
||||
try:
|
||||
samples = cu8_to_complex(chunk)
|
||||
power_db = compute_power_spectrum(
|
||||
samples, fft_size=self._fft_size, avg_count=self._avg_count
|
||||
)
|
||||
power_db = compute_power_spectrum(samples, fft_size=self._fft_size, avg_count=self._avg_count)
|
||||
quantized = quantize_to_uint8(power_db, db_min=self._db_min, db_max=self._db_max)
|
||||
frame = build_binary_frame(self._start_freq, self._end_freq, quantized)
|
||||
except Exception as e:
|
||||
@@ -118,4 +116,4 @@ class WaterfallConsumer:
|
||||
pass
|
||||
|
||||
def on_stop(self) -> None:
|
||||
self._buffer = b''
|
||||
self._buffer = b""
|
||||
|
||||
@@ -21,7 +21,7 @@ from typing import Protocol, runtime_checkable
|
||||
from utils.logging import get_logger
|
||||
from utils.process import register_process, safe_terminate, unregister_process
|
||||
|
||||
logger = get_logger('intercept.ground_station.iq_bus')
|
||||
logger = get_logger("intercept.ground_station.iq_bus")
|
||||
|
||||
CHUNK_SIZE = 65_536 # bytes per read (~27 ms @ 2.4 Msps CU8)
|
||||
|
||||
@@ -73,7 +73,7 @@ class IQBus:
|
||||
sample_rate: int = 2_400_000,
|
||||
gain: float | None = None,
|
||||
device_index: int = 0,
|
||||
sdr_type: str = 'rtlsdr',
|
||||
sdr_type: str = "rtlsdr",
|
||||
ppm: int | None = None,
|
||||
bias_t: bool = False,
|
||||
):
|
||||
@@ -113,12 +113,12 @@ class IQBus:
|
||||
def start(self) -> tuple[bool, str]:
|
||||
"""Start IQ capture. Returns (success, error_message)."""
|
||||
if self._running:
|
||||
return True, ''
|
||||
return True, ""
|
||||
|
||||
try:
|
||||
cmd = self._build_command(self._center_mhz)
|
||||
except Exception as e:
|
||||
return False, f'Failed to build IQ capture command: {e}'
|
||||
return False, f"Failed to build IQ capture command: {e}"
|
||||
|
||||
if not shutil.which(cmd[0]):
|
||||
return False, f'Required tool "{cmd[0]}" not found. Install SoapySDR (rx_sdr) or rtl-sdr.'
|
||||
@@ -132,21 +132,21 @@ class IQBus:
|
||||
)
|
||||
register_process(self._proc)
|
||||
except Exception as e:
|
||||
return False, f'Failed to spawn IQ capture: {e}'
|
||||
return False, f"Failed to spawn IQ capture: {e}"
|
||||
|
||||
# Brief check that the process actually started
|
||||
time.sleep(0.3)
|
||||
if self._proc.poll() is not None:
|
||||
stderr_out = ''
|
||||
stderr_out = ""
|
||||
if self._proc.stderr:
|
||||
try:
|
||||
stderr_out = self._proc.stderr.read().decode('utf-8', errors='replace').strip()
|
||||
stderr_out = self._proc.stderr.read().decode("utf-8", errors="replace").strip()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(self._proc)
|
||||
self._proc = None
|
||||
detail = f': {stderr_out}' if stderr_out else ''
|
||||
return False, f'IQ capture process exited immediately{detail}'
|
||||
detail = f": {stderr_out}" if stderr_out else ""
|
||||
return False, f"IQ capture process exited immediately{detail}"
|
||||
|
||||
self._stop_event.clear()
|
||||
self._running = True
|
||||
@@ -167,15 +167,13 @@ class IQBus:
|
||||
except Exception as e:
|
||||
logger.warning(f"Consumer on_start error: {e}")
|
||||
|
||||
self._producer_thread = threading.Thread(
|
||||
target=self._producer_loop, daemon=True, name='iq-bus-producer'
|
||||
)
|
||||
self._producer_thread = threading.Thread(target=self._producer_loop, daemon=True, name="iq-bus-producer")
|
||||
self._producer_thread.start()
|
||||
logger.info(
|
||||
f"IQBus started: {self._center_mhz} MHz, sr={self._sample_rate}, "
|
||||
f"device={self._sdr_type}:{self._device_index}"
|
||||
)
|
||||
return True, ''
|
||||
return True, ""
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop IQ capture and notify all consumers."""
|
||||
@@ -201,7 +199,7 @@ class IQBus:
|
||||
"""Retune by stopping and restarting the capture process."""
|
||||
self._current_freq_mhz = new_freq_mhz
|
||||
if not self._running:
|
||||
return False, 'Not running'
|
||||
return False, "Not running"
|
||||
|
||||
# Stop the current process
|
||||
self._stop_event.set()
|
||||
@@ -225,14 +223,12 @@ class IQBus:
|
||||
register_process(self._proc)
|
||||
except Exception as e:
|
||||
self._running = False
|
||||
return False, f'Retune failed: {e}'
|
||||
return False, f"Retune failed: {e}"
|
||||
|
||||
self._producer_thread = threading.Thread(
|
||||
target=self._producer_loop, daemon=True, name='iq-bus-producer'
|
||||
)
|
||||
self._producer_thread = threading.Thread(target=self._producer_loop, daemon=True, name="iq-bus-producer")
|
||||
self._producer_thread.start()
|
||||
logger.info(f"IQBus retuned to {new_freq_mhz:.6f} MHz")
|
||||
return True, ''
|
||||
return True, ""
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
@@ -279,12 +275,12 @@ class IQBus:
|
||||
from utils.sdr.base import SDRDevice
|
||||
|
||||
type_map = {
|
||||
'rtlsdr': SDRType.RTL_SDR,
|
||||
'rtl_sdr': SDRType.RTL_SDR,
|
||||
'hackrf': SDRType.HACKRF,
|
||||
'limesdr': SDRType.LIME_SDR,
|
||||
'airspy': SDRType.AIRSPY,
|
||||
'sdrplay': SDRType.SDRPLAY,
|
||||
"rtlsdr": SDRType.RTL_SDR,
|
||||
"rtl_sdr": SDRType.RTL_SDR,
|
||||
"hackrf": SDRType.HACKRF,
|
||||
"limesdr": SDRType.LIME_SDR,
|
||||
"airspy": SDRType.AIRSPY,
|
||||
"sdrplay": SDRType.SDRPLAY,
|
||||
}
|
||||
sdr_type = type_map.get(self._sdr_type.lower(), SDRType.RTL_SDR)
|
||||
builder = SDRFactory.get_builder(sdr_type)
|
||||
@@ -292,8 +288,8 @@ class IQBus:
|
||||
device = SDRDevice(
|
||||
sdr_type=sdr_type,
|
||||
index=self._device_index,
|
||||
name=f'{sdr_type.value}-{self._device_index}',
|
||||
serial='N/A',
|
||||
name=f"{sdr_type.value}-{self._device_index}",
|
||||
serial="N/A",
|
||||
driver=sdr_type.value,
|
||||
capabilities=caps,
|
||||
)
|
||||
|
||||
@@ -11,14 +11,14 @@ from pathlib import Path
|
||||
from utils.logging import get_logger
|
||||
from utils.weather_sat import WeatherSatDecoder
|
||||
|
||||
logger = get_logger('intercept.ground_station.meteor_backend')
|
||||
logger = get_logger("intercept.ground_station.meteor_backend")
|
||||
|
||||
OUTPUT_ROOT = Path('instance/ground_station/weather_outputs')
|
||||
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',
|
||||
57166: "METEOR-M2-3",
|
||||
59051: "METEOR-M2-4",
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ def resolve_meteor_satellite_key(norad_id: int, satellite_name: str) -> str | No
|
||||
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'
|
||||
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
|
||||
|
||||
|
||||
@@ -48,31 +48,33 @@ def launch_meteor_decode(
|
||||
decode_job_id = _create_decode_job(
|
||||
observation_id=obs_db_id,
|
||||
norad_id=norad_id,
|
||||
backend='meteor_lrpt',
|
||||
backend="meteor_lrpt",
|
||||
input_path=data_path,
|
||||
)
|
||||
emit_event({
|
||||
'type': 'weather_decode_queued',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'input_path': str(data_path),
|
||||
})
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_queued",
|
||||
"decode_job_id": decode_job_id,
|
||||
"norad_id": norad_id,
|
||||
"satellite": satellite_name,
|
||||
"backend": "meteor_lrpt",
|
||||
"input_path": str(data_path),
|
||||
}
|
||||
)
|
||||
t = threading.Thread(
|
||||
target=_run_decode,
|
||||
kwargs={
|
||||
'decode_job_id': decode_job_id,
|
||||
'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,
|
||||
"decode_job_id": decode_job_id,
|
||||
"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}',
|
||||
name=f"gs-meteor-decode-{norad_id}",
|
||||
)
|
||||
t.start()
|
||||
|
||||
@@ -89,84 +91,92 @@ def _run_decode(
|
||||
register_output,
|
||||
) -> None:
|
||||
latest_status: dict[str, str | int | None] = {
|
||||
'message': None,
|
||||
'status': None,
|
||||
'phase': None,
|
||||
"message": None,
|
||||
"status": None,
|
||||
"phase": None,
|
||||
}
|
||||
sat_key = resolve_meteor_satellite_key(norad_id, satellite_name)
|
||||
if not sat_key:
|
||||
_update_decode_job(
|
||||
decode_job_id,
|
||||
status='failed',
|
||||
error_message='No Meteor satellite mapping is available for this observation.',
|
||||
details={'reason': 'unknown_satellite_mapping'},
|
||||
status="failed",
|
||||
error_message="No Meteor satellite mapping is available for this observation.",
|
||||
details={"reason": "unknown_satellite_mapping"},
|
||||
completed=True,
|
||||
)
|
||||
emit_event({
|
||||
'type': 'weather_decode_failed',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'message': 'No Meteor satellite mapping is available for this observation.',
|
||||
})
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_failed",
|
||||
"decode_job_id": decode_job_id,
|
||||
"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())}'
|
||||
output_dir = OUTPUT_ROOT / f"{norad_id}_{int(time.time())}"
|
||||
decoder = WeatherSatDecoder(output_dir=output_dir)
|
||||
if decoder.decoder_available is None:
|
||||
_update_decode_job(
|
||||
decode_job_id,
|
||||
status='failed',
|
||||
error_message='SatDump backend is not available for Meteor LRPT decode.',
|
||||
details={'reason': 'backend_unavailable', 'output_dir': str(output_dir)},
|
||||
status="failed",
|
||||
error_message="SatDump backend is not available for Meteor LRPT decode.",
|
||||
details={"reason": "backend_unavailable", "output_dir": str(output_dir)},
|
||||
completed=True,
|
||||
)
|
||||
emit_event({
|
||||
'type': 'weather_decode_failed',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'message': 'SatDump backend is not available for Meteor LRPT decode.',
|
||||
})
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_failed",
|
||||
"decode_job_id": decode_job_id,
|
||||
"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):
|
||||
latest_status['message'] = progress.message or latest_status.get('message')
|
||||
latest_status['status'] = progress.status
|
||||
latest_status['phase'] = progress.capture_phase or latest_status.get('phase')
|
||||
latest_status["message"] = progress.message or latest_status.get("message")
|
||||
latest_status["status"] = progress.status
|
||||
latest_status["phase"] = progress.capture_phase or latest_status.get("phase")
|
||||
progress_event = progress.to_dict()
|
||||
progress_event.pop('type', None)
|
||||
emit_event({
|
||||
'type': 'weather_decode_progress',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
**progress_event,
|
||||
})
|
||||
progress_event.pop("type", None)
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_progress",
|
||||
"decode_job_id": decode_job_id,
|
||||
"norad_id": norad_id,
|
||||
"satellite": satellite_name,
|
||||
"backend": "meteor_lrpt",
|
||||
**progress_event,
|
||||
}
|
||||
)
|
||||
|
||||
decoder.set_callback(_progress_cb)
|
||||
_update_decode_job(
|
||||
decode_job_id,
|
||||
status='decoding',
|
||||
status="decoding",
|
||||
output_dir=output_dir,
|
||||
details={
|
||||
'sample_rate': sample_rate,
|
||||
'input_path': str(data_path),
|
||||
'satellite': satellite_name,
|
||||
"sample_rate": sample_rate,
|
||||
"input_path": str(data_path),
|
||||
"satellite": satellite_name,
|
||||
},
|
||||
started=True,
|
||||
)
|
||||
emit_event({
|
||||
'type': 'weather_decode_started',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'input_path': str(data_path),
|
||||
})
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_started",
|
||||
"decode_job_id": decode_job_id,
|
||||
"norad_id": norad_id,
|
||||
"satellite": satellite_name,
|
||||
"backend": "meteor_lrpt",
|
||||
"input_path": str(data_path),
|
||||
}
|
||||
)
|
||||
|
||||
ok, error = decoder.start_from_file(
|
||||
satellite=sat_key,
|
||||
@@ -180,20 +190,22 @@ def _run_decode(
|
||||
decoder=decoder,
|
||||
latest_status=latest_status,
|
||||
)
|
||||
emit_event({
|
||||
'type': 'weather_decode_failed',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'message': error or details['message'],
|
||||
'failure_reason': details['reason'],
|
||||
'details': details,
|
||||
})
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_failed",
|
||||
"decode_job_id": decode_job_id,
|
||||
"norad_id": norad_id,
|
||||
"satellite": satellite_name,
|
||||
"backend": "meteor_lrpt",
|
||||
"message": error or details["message"],
|
||||
"failure_reason": details["reason"],
|
||||
"details": details,
|
||||
}
|
||||
)
|
||||
_update_decode_job(
|
||||
decode_job_id,
|
||||
status='failed',
|
||||
error_message=error or details['message'],
|
||||
status="failed",
|
||||
error_message=error or details["message"],
|
||||
details=details,
|
||||
completed=True,
|
||||
)
|
||||
@@ -210,23 +222,25 @@ def _run_decode(
|
||||
sample_rate=sample_rate,
|
||||
decoder=decoder,
|
||||
latest_status=latest_status,
|
||||
override_reason='timeout',
|
||||
override_message='Meteor decode timed out.',
|
||||
override_reason="timeout",
|
||||
override_message="Meteor decode timed out.",
|
||||
)
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_failed",
|
||||
"decode_job_id": decode_job_id,
|
||||
"norad_id": norad_id,
|
||||
"satellite": satellite_name,
|
||||
"backend": "meteor_lrpt",
|
||||
"message": details["message"],
|
||||
"failure_reason": details["reason"],
|
||||
"details": details,
|
||||
}
|
||||
)
|
||||
emit_event({
|
||||
'type': 'weather_decode_failed',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'message': details['message'],
|
||||
'failure_reason': details['reason'],
|
||||
'details': details,
|
||||
})
|
||||
_update_decode_job(
|
||||
decode_job_id,
|
||||
status='failed',
|
||||
error_message=details['message'],
|
||||
status="failed",
|
||||
error_message=details["message"],
|
||||
details=details,
|
||||
completed=True,
|
||||
)
|
||||
@@ -240,20 +254,22 @@ def _run_decode(
|
||||
decoder=decoder,
|
||||
latest_status=latest_status,
|
||||
)
|
||||
emit_event({
|
||||
'type': 'weather_decode_failed',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'message': details['message'],
|
||||
'failure_reason': details['reason'],
|
||||
'details': details,
|
||||
})
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_failed",
|
||||
"decode_job_id": decode_job_id,
|
||||
"norad_id": norad_id,
|
||||
"satellite": satellite_name,
|
||||
"backend": "meteor_lrpt",
|
||||
"message": details["message"],
|
||||
"failure_reason": details["reason"],
|
||||
"details": details,
|
||||
}
|
||||
)
|
||||
_update_decode_job(
|
||||
decode_job_id,
|
||||
status='failed',
|
||||
error_message=details['message'],
|
||||
status="failed",
|
||||
error_message=details["message"],
|
||||
details=details,
|
||||
completed=True,
|
||||
)
|
||||
@@ -262,50 +278,54 @@ def _run_decode(
|
||||
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,
|
||||
"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',
|
||||
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,
|
||||
})
|
||||
outputs.append(
|
||||
{
|
||||
"id": output_id,
|
||||
"file_path": str(image.path),
|
||||
"filename": image.filename,
|
||||
"product": image.product,
|
||||
}
|
||||
)
|
||||
|
||||
completion_details = {
|
||||
'sample_rate': sample_rate,
|
||||
'input_path': str(data_path),
|
||||
'output_dir': str(output_dir),
|
||||
'output_count': len(outputs),
|
||||
"sample_rate": sample_rate,
|
||||
"input_path": str(data_path),
|
||||
"output_dir": str(output_dir),
|
||||
"output_count": len(outputs),
|
||||
}
|
||||
_update_decode_job(
|
||||
decode_job_id,
|
||||
status='complete',
|
||||
status="complete",
|
||||
details=completion_details,
|
||||
completed=True,
|
||||
)
|
||||
|
||||
emit_event({
|
||||
'type': 'weather_decode_complete',
|
||||
'decode_job_id': decode_job_id,
|
||||
'norad_id': norad_id,
|
||||
'satellite': satellite_name,
|
||||
'backend': 'meteor_lrpt',
|
||||
'outputs': outputs,
|
||||
})
|
||||
emit_event(
|
||||
{
|
||||
"type": "weather_decode_complete",
|
||||
"decode_job_id": decode_job_id,
|
||||
"norad_id": norad_id,
|
||||
"satellite": satellite_name,
|
||||
"backend": "meteor_lrpt",
|
||||
"outputs": outputs,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _build_failure_details(
|
||||
@@ -319,75 +339,75 @@ def _build_failure_details(
|
||||
) -> dict[str, str | int | None]:
|
||||
file_size = data_path.stat().st_size if data_path.exists() else 0
|
||||
status = decoder.get_status()
|
||||
last_error = str(status.get('last_error') or latest_status.get('message') or '').strip()
|
||||
return_code = status.get('last_returncode')
|
||||
last_error = str(status.get("last_error") or latest_status.get("message") or "").strip()
|
||||
return_code = status.get("last_returncode")
|
||||
|
||||
if override_reason:
|
||||
reason = override_reason
|
||||
elif sample_rate < 200_000:
|
||||
reason = 'sample_rate_too_low'
|
||||
reason = "sample_rate_too_low"
|
||||
elif not data_path.exists():
|
||||
reason = 'missing_recording'
|
||||
reason = "missing_recording"
|
||||
elif file_size < 5_000_000:
|
||||
reason = 'recording_too_small'
|
||||
reason = "recording_too_small"
|
||||
elif return_code not in (None, 0):
|
||||
reason = 'satdump_failed'
|
||||
elif 'samplerate' in last_error.lower() or 'sample rate' in last_error.lower():
|
||||
reason = 'invalid_sample_rate'
|
||||
elif 'not found' in last_error.lower():
|
||||
reason = 'input_missing'
|
||||
elif 'permission' in last_error.lower():
|
||||
reason = 'permission_error'
|
||||
reason = "satdump_failed"
|
||||
elif "samplerate" in last_error.lower() or "sample rate" in last_error.lower():
|
||||
reason = "invalid_sample_rate"
|
||||
elif "not found" in last_error.lower():
|
||||
reason = "input_missing"
|
||||
elif "permission" in last_error.lower():
|
||||
reason = "permission_error"
|
||||
else:
|
||||
reason = 'no_imagery_produced'
|
||||
reason = "no_imagery_produced"
|
||||
|
||||
if override_message:
|
||||
message = override_message
|
||||
elif reason == 'sample_rate_too_low':
|
||||
message = f'Sample rate {sample_rate} Hz is too low for Meteor LRPT decoding.'
|
||||
elif reason == 'missing_recording':
|
||||
message = 'The recording file was not found when decode started.'
|
||||
elif reason == 'recording_too_small':
|
||||
elif reason == "sample_rate_too_low":
|
||||
message = f"Sample rate {sample_rate} Hz is too low for Meteor LRPT decoding."
|
||||
elif reason == "missing_recording":
|
||||
message = "The recording file was not found when decode started."
|
||||
elif reason == "recording_too_small":
|
||||
message = (
|
||||
f'Recording is very small ({_format_bytes(file_size)}); this usually means the pass '
|
||||
'ended early or little usable IQ was captured.'
|
||||
f"Recording is very small ({_format_bytes(file_size)}); this usually means the pass "
|
||||
"ended early or little usable IQ was captured."
|
||||
)
|
||||
elif reason == 'satdump_failed':
|
||||
message = last_error or f'SatDump exited with code {return_code}.'
|
||||
elif reason == 'invalid_sample_rate':
|
||||
message = last_error or 'SatDump rejected the recording sample rate.'
|
||||
elif reason == 'input_missing':
|
||||
message = last_error or 'Input recording was not accessible to the decoder.'
|
||||
elif reason == 'permission_error':
|
||||
message = last_error or 'Decoder could not access the recording or output path.'
|
||||
elif reason == "satdump_failed":
|
||||
message = last_error or f"SatDump exited with code {return_code}."
|
||||
elif reason == "invalid_sample_rate":
|
||||
message = last_error or "SatDump rejected the recording sample rate."
|
||||
elif reason == "input_missing":
|
||||
message = last_error or "Input recording was not accessible to the decoder."
|
||||
elif reason == "permission_error":
|
||||
message = last_error or "Decoder could not access the recording or output path."
|
||||
else:
|
||||
message = (
|
||||
last_error or
|
||||
'Decode completed without any image outputs. This usually indicates weak signal, '
|
||||
'incorrect sample rate, or a SatDump pipeline mismatch.'
|
||||
last_error
|
||||
or "Decode completed without any image outputs. This usually indicates weak signal, "
|
||||
"incorrect sample rate, or a SatDump pipeline mismatch."
|
||||
)
|
||||
|
||||
return {
|
||||
'reason': reason,
|
||||
'message': message,
|
||||
'sample_rate': sample_rate,
|
||||
'file_size_bytes': file_size,
|
||||
'file_size_human': _format_bytes(file_size),
|
||||
'last_error': last_error or None,
|
||||
'last_returncode': return_code,
|
||||
'capture_phase': status.get('capture_phase') or latest_status.get('phase'),
|
||||
'input_path': str(data_path),
|
||||
"reason": reason,
|
||||
"message": message,
|
||||
"sample_rate": sample_rate,
|
||||
"file_size_bytes": file_size,
|
||||
"file_size_human": _format_bytes(file_size),
|
||||
"last_error": last_error or None,
|
||||
"last_returncode": return_code,
|
||||
"capture_phase": status.get("capture_phase") or latest_status.get("phase"),
|
||||
"input_path": str(data_path),
|
||||
}
|
||||
|
||||
|
||||
def _format_bytes(size_bytes: int) -> str:
|
||||
if size_bytes < 1024:
|
||||
return f'{size_bytes} B'
|
||||
return f"{size_bytes} B"
|
||||
if size_bytes < 1024 * 1024:
|
||||
return f'{size_bytes / 1024:.1f} KB'
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
if size_bytes < 1024 * 1024 * 1024:
|
||||
return f'{size_bytes / (1024 * 1024):.1f} MB'
|
||||
return f'{size_bytes / (1024 * 1024 * 1024):.2f} GB'
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
||||
|
||||
|
||||
def _create_decode_job(
|
||||
@@ -402,16 +422,16 @@ def _create_decode_job(
|
||||
|
||||
with get_db() as conn:
|
||||
cur = conn.execute(
|
||||
'''
|
||||
"""
|
||||
INSERT INTO ground_station_decode_jobs
|
||||
(observation_id, norad_id, backend, status, input_path, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''',
|
||||
""",
|
||||
(
|
||||
observation_id,
|
||||
norad_id,
|
||||
backend,
|
||||
'queued',
|
||||
"queued",
|
||||
str(input_path),
|
||||
_utcnow_iso(),
|
||||
_utcnow_iso(),
|
||||
@@ -438,33 +458,33 @@ def _update_decode_job(
|
||||
try:
|
||||
from utils.database import get_db
|
||||
|
||||
fields = ['status = ?', 'updated_at = ?']
|
||||
fields = ["status = ?", "updated_at = ?"]
|
||||
values: list[object] = [status, _utcnow_iso()]
|
||||
|
||||
if output_dir is not None:
|
||||
fields.append('output_dir = ?')
|
||||
fields.append("output_dir = ?")
|
||||
values.append(str(output_dir))
|
||||
if error_message is not None:
|
||||
fields.append('error_message = ?')
|
||||
fields.append("error_message = ?")
|
||||
values.append(error_message)
|
||||
if details is not None:
|
||||
fields.append('details_json = ?')
|
||||
fields.append("details_json = ?")
|
||||
values.append(json.dumps(details))
|
||||
if started:
|
||||
fields.append('started_at = ?')
|
||||
fields.append("started_at = ?")
|
||||
values.append(_utcnow_iso())
|
||||
if completed:
|
||||
fields.append('completed_at = ?')
|
||||
fields.append("completed_at = ?")
|
||||
values.append(_utcnow_iso())
|
||||
|
||||
values.append(decode_job_id)
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
f'''
|
||||
f"""
|
||||
UPDATE ground_station_decode_jobs
|
||||
SET {", ".join(fields)}
|
||||
WHERE id = ?
|
||||
''',
|
||||
""",
|
||||
values,
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -14,50 +14,50 @@ from typing import Any
|
||||
|
||||
from utils.logging import get_logger
|
||||
|
||||
logger = get_logger('intercept.ground_station.profile')
|
||||
logger = get_logger("intercept.ground_station.profile")
|
||||
|
||||
|
||||
VALID_TASK_TYPES = {
|
||||
'telemetry_ax25',
|
||||
'telemetry_gmsk',
|
||||
'telemetry_bpsk',
|
||||
'weather_meteor_lrpt',
|
||||
'record_iq',
|
||||
"telemetry_ax25",
|
||||
"telemetry_gmsk",
|
||||
"telemetry_bpsk",
|
||||
"weather_meteor_lrpt",
|
||||
"record_iq",
|
||||
}
|
||||
|
||||
|
||||
def legacy_decoder_to_tasks(decoder_type: str | None, record_iq: bool = False) -> list[str]:
|
||||
decoder = (decoder_type or 'fm').lower()
|
||||
decoder = (decoder_type or "fm").lower()
|
||||
tasks: list[str] = []
|
||||
if decoder in ('fm', 'afsk'):
|
||||
tasks.append('telemetry_ax25')
|
||||
elif decoder == 'gmsk':
|
||||
tasks.append('telemetry_gmsk')
|
||||
elif decoder == 'bpsk':
|
||||
tasks.append('telemetry_bpsk')
|
||||
elif decoder == 'iq_only':
|
||||
tasks.append('record_iq')
|
||||
if decoder in ("fm", "afsk"):
|
||||
tasks.append("telemetry_ax25")
|
||||
elif decoder == "gmsk":
|
||||
tasks.append("telemetry_gmsk")
|
||||
elif decoder == "bpsk":
|
||||
tasks.append("telemetry_bpsk")
|
||||
elif decoder == "iq_only":
|
||||
tasks.append("record_iq")
|
||||
|
||||
if record_iq and 'record_iq' not in tasks:
|
||||
tasks.append('record_iq')
|
||||
if record_iq and "record_iq" not in tasks:
|
||||
tasks.append("record_iq")
|
||||
return tasks
|
||||
|
||||
|
||||
def tasks_to_legacy_decoder(tasks: list[str]) -> str:
|
||||
normalized = normalize_tasks(tasks)
|
||||
if 'telemetry_bpsk' in normalized:
|
||||
return 'bpsk'
|
||||
if 'telemetry_gmsk' in normalized:
|
||||
return 'gmsk'
|
||||
if 'telemetry_ax25' in normalized:
|
||||
return 'afsk'
|
||||
return 'iq_only'
|
||||
if "telemetry_bpsk" in normalized:
|
||||
return "bpsk"
|
||||
if "telemetry_gmsk" in normalized:
|
||||
return "gmsk"
|
||||
if "telemetry_ax25" in normalized:
|
||||
return "afsk"
|
||||
return "iq_only"
|
||||
|
||||
|
||||
def normalize_tasks(tasks: list[str] | None) -> list[str]:
|
||||
result: list[str] = []
|
||||
for task in tasks or []:
|
||||
value = str(task or '').strip().lower()
|
||||
value = str(task or "").strip().lower()
|
||||
if value and value in VALID_TASK_TYPES and value not in result:
|
||||
result.append(value)
|
||||
return result
|
||||
@@ -68,9 +68,9 @@ class ObservationProfile:
|
||||
"""Per-satellite capture configuration."""
|
||||
|
||||
norad_id: int
|
||||
name: str # Human-readable label
|
||||
name: str # Human-readable label
|
||||
frequency_mhz: float
|
||||
decoder_type: str # 'fm', 'afsk', 'bpsk', 'gmsk', 'iq_only'
|
||||
decoder_type: str # 'fm', 'afsk', 'bpsk', 'gmsk', 'iq_only'
|
||||
gain: float = 40.0
|
||||
bandwidth_hz: int = 200_000
|
||||
min_elevation: float = 10.0
|
||||
@@ -79,62 +79,60 @@ class ObservationProfile:
|
||||
iq_sample_rate: int = 2_400_000
|
||||
tasks: list[str] = field(default_factory=list)
|
||||
id: int | None = None
|
||||
created_at: str = field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
normalized_tasks = self.get_tasks()
|
||||
return {
|
||||
'id': self.id,
|
||||
'norad_id': self.norad_id,
|
||||
'name': self.name,
|
||||
'frequency_mhz': self.frequency_mhz,
|
||||
'decoder_type': self.decoder_type,
|
||||
'legacy_decoder_type': self.decoder_type,
|
||||
'gain': self.gain,
|
||||
'bandwidth_hz': self.bandwidth_hz,
|
||||
'min_elevation': self.min_elevation,
|
||||
'enabled': self.enabled,
|
||||
'record_iq': self.record_iq,
|
||||
'iq_sample_rate': self.iq_sample_rate,
|
||||
'tasks': normalized_tasks,
|
||||
'created_at': self.created_at,
|
||||
"id": self.id,
|
||||
"norad_id": self.norad_id,
|
||||
"name": self.name,
|
||||
"frequency_mhz": self.frequency_mhz,
|
||||
"decoder_type": self.decoder_type,
|
||||
"legacy_decoder_type": self.decoder_type,
|
||||
"gain": self.gain,
|
||||
"bandwidth_hz": self.bandwidth_hz,
|
||||
"min_elevation": self.min_elevation,
|
||||
"enabled": self.enabled,
|
||||
"record_iq": self.record_iq,
|
||||
"iq_sample_rate": self.iq_sample_rate,
|
||||
"tasks": normalized_tasks,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
def get_tasks(self) -> list[str]:
|
||||
tasks = normalize_tasks(self.tasks)
|
||||
if not tasks:
|
||||
tasks = legacy_decoder_to_tasks(self.decoder_type, self.record_iq)
|
||||
if self.record_iq and 'record_iq' not in tasks:
|
||||
tasks.append('record_iq')
|
||||
if 'weather_meteor_lrpt' in tasks and 'record_iq' not in tasks:
|
||||
tasks.append('record_iq')
|
||||
if self.record_iq and "record_iq" not in tasks:
|
||||
tasks.append("record_iq")
|
||||
if "weather_meteor_lrpt" in tasks and "record_iq" not in tasks:
|
||||
tasks.append("record_iq")
|
||||
return tasks
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row) -> ObservationProfile:
|
||||
tasks = []
|
||||
raw_tasks = row.get('tasks_json', None)
|
||||
raw_tasks = row.get("tasks_json", None)
|
||||
if raw_tasks:
|
||||
try:
|
||||
tasks = normalize_tasks(json.loads(raw_tasks))
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
tasks = []
|
||||
return cls(
|
||||
id=row['id'],
|
||||
norad_id=row['norad_id'],
|
||||
name=row['name'],
|
||||
frequency_mhz=row['frequency_mhz'],
|
||||
decoder_type=row['decoder_type'],
|
||||
gain=row['gain'],
|
||||
bandwidth_hz=row['bandwidth_hz'],
|
||||
min_elevation=row['min_elevation'],
|
||||
enabled=bool(row['enabled']),
|
||||
record_iq=bool(row['record_iq']),
|
||||
iq_sample_rate=row['iq_sample_rate'],
|
||||
id=row["id"],
|
||||
norad_id=row["norad_id"],
|
||||
name=row["name"],
|
||||
frequency_mhz=row["frequency_mhz"],
|
||||
decoder_type=row["decoder_type"],
|
||||
gain=row["gain"],
|
||||
bandwidth_hz=row["bandwidth_hz"],
|
||||
min_elevation=row["min_elevation"],
|
||||
enabled=bool(row["enabled"]),
|
||||
record_iq=bool(row["record_iq"]),
|
||||
iq_sample_rate=row["iq_sample_rate"],
|
||||
tasks=tasks,
|
||||
created_at=row['created_at'],
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
|
||||
|
||||
@@ -146,32 +144,32 @@ class ObservationProfile:
|
||||
def list_profiles() -> list[ObservationProfile]:
|
||||
"""Return all observation profiles from the database."""
|
||||
from utils.database import get_db
|
||||
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
'SELECT * FROM observation_profiles ORDER BY created_at DESC'
|
||||
).fetchall()
|
||||
rows = conn.execute("SELECT * FROM observation_profiles ORDER BY created_at DESC").fetchall()
|
||||
return [ObservationProfile.from_row(r) for r in rows]
|
||||
|
||||
|
||||
def get_profile(norad_id: int) -> ObservationProfile | None:
|
||||
"""Return the profile for a NORAD ID, or None if not found."""
|
||||
from utils.database import get_db
|
||||
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
'SELECT * FROM observation_profiles WHERE norad_id = ?', (norad_id,)
|
||||
).fetchone()
|
||||
row = conn.execute("SELECT * FROM observation_profiles WHERE norad_id = ?", (norad_id,)).fetchone()
|
||||
return ObservationProfile.from_row(row) if row else None
|
||||
|
||||
|
||||
def save_profile(profile: ObservationProfile) -> ObservationProfile:
|
||||
"""Insert or replace an observation profile. Returns the saved profile."""
|
||||
from utils.database import get_db
|
||||
|
||||
normalized_tasks = profile.get_tasks()
|
||||
profile.tasks = normalized_tasks
|
||||
profile.record_iq = 'record_iq' in normalized_tasks
|
||||
profile.record_iq = "record_iq" in normalized_tasks
|
||||
profile.decoder_type = tasks_to_legacy_decoder(normalized_tasks)
|
||||
with get_db() as conn:
|
||||
conn.execute('''
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO observation_profiles
|
||||
(norad_id, name, frequency_mhz, decoder_type, tasks_json, gain,
|
||||
bandwidth_hz, min_elevation, enabled, record_iq,
|
||||
@@ -188,28 +186,29 @@ def save_profile(profile: ObservationProfile) -> ObservationProfile:
|
||||
enabled=excluded.enabled,
|
||||
record_iq=excluded.record_iq,
|
||||
iq_sample_rate=excluded.iq_sample_rate
|
||||
''', (
|
||||
profile.norad_id,
|
||||
profile.name,
|
||||
profile.frequency_mhz,
|
||||
profile.decoder_type,
|
||||
json.dumps(normalized_tasks),
|
||||
profile.gain,
|
||||
profile.bandwidth_hz,
|
||||
profile.min_elevation,
|
||||
int(profile.enabled),
|
||||
int(profile.record_iq),
|
||||
profile.iq_sample_rate,
|
||||
profile.created_at,
|
||||
))
|
||||
""",
|
||||
(
|
||||
profile.norad_id,
|
||||
profile.name,
|
||||
profile.frequency_mhz,
|
||||
profile.decoder_type,
|
||||
json.dumps(normalized_tasks),
|
||||
profile.gain,
|
||||
profile.bandwidth_hz,
|
||||
profile.min_elevation,
|
||||
int(profile.enabled),
|
||||
int(profile.record_iq),
|
||||
profile.iq_sample_rate,
|
||||
profile.created_at,
|
||||
),
|
||||
)
|
||||
return get_profile(profile.norad_id) or profile
|
||||
|
||||
|
||||
def delete_profile(norad_id: int) -> bool:
|
||||
"""Delete a profile by NORAD ID. Returns True if a row was deleted."""
|
||||
from utils.database import get_db
|
||||
|
||||
with get_db() as conn:
|
||||
cur = conn.execute(
|
||||
'DELETE FROM observation_profiles WHERE norad_id = ?', (norad_id,)
|
||||
)
|
||||
cur = conn.execute("DELETE FROM observation_profiles WHERE norad_id = ?", (norad_id,))
|
||||
return cur.rowcount > 0
|
||||
|
||||
Reference in New Issue
Block a user