""" FR24 Capture Service — Step 2: Real ADS-B via dump1090 Launches dump1090-fa as a subprocess, reads SBS-1 (BaseStation) messages from its TCP port 30003, and writes real raw_packets to PostgreSQL. """ import os import time import base64 import logging import signal import sys import socket import subprocess import threading from datetime import datetime, timezone from queue import Queue, Empty import psycopg2 import psycopg2.extras logging.basicConfig( level=logging.INFO, format="%(asctime)s [capture] %(levelname)s %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) log = logging.getLogger("capture") # ── config ──────────────────────────────────────────────────────────────────── DB_DSN = ( f"host={os.environ['POSTGRES_HOST']} " f"port={os.environ.get('POSTGRES_PORT', 5432)} " f"dbname={os.environ['POSTGRES_DB']} " f"user={os.environ['POSTGRES_USER']} " f"password={os.environ['POSTGRES_PASSWORD']}" ) CENTER_FREQ = int(os.environ.get("RTLSDR_CENTER_FREQUENCY", 1090000000)) SAMPLE_RATE = int(os.environ.get("RTLSDR_SAMPLE_RATE", 2000000)) DEVICE_INDEX = int(os.environ.get("RTLSDR_DEVICE_INDEX", 0)) GAIN_RAW = os.environ.get("RTLSDR_GAIN", "auto") GAIN_DB = None if GAIN_RAW == "auto" else float(GAIN_RAW) ENABLE_BIAS_T = os.environ.get("RTLSDR_BIAS_T", "0") == "1" DUMP1090_HOST = "127.0.0.1" DUMP1090_SBS_PORT = 30003 DUMP1090_STARTUP_WAIT = 5 # seconds to wait for dump1090 to bind DUMP1090_RECONNECT_DELAY = 3 HEALTHCHECK_FILE = "/tmp/capture-ready" # ── dump1090 process ────────────────────────────────────────────────────────── def build_dump1090_cmd() -> list[str]: """Build dump1090-fa command for RTL-SDR Blog V4.""" cmd = [ "dump1090-fa", "--device-index", str(DEVICE_INDEX), "--freq", str(CENTER_FREQ), "--net", # enable network output "--net-sbs-port", str(DUMP1090_SBS_PORT), "--net-ro-port", "0", # disable raw output port "--net-ri-port", "0", "--net-bi-port", "0", "--quiet", # suppress per-message stdout noise "--write-json", "/tmp/dump1090-json", # optional JSON output ] if GAIN_RAW == "auto": cmd += ["--gain", "-10"] # dump1090 uses -10 for AGC else: cmd += ["--gain", GAIN_RAW] if ENABLE_BIAS_T: cmd += ["--enable-bias-t"] return cmd def start_dump1090() -> subprocess.Popen: cmd = build_dump1090_cmd() log.info("Starting dump1090: %s", " ".join(cmd)) proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) # drain dump1090 stdout in a background thread so it never blocks def _drain(p): for line in p.stdout: line = line.rstrip() if line: log.debug("[dump1090] %s", line) threading.Thread(target=_drain, args=(proc,), daemon=True).start() return proc # ── SBS-1 reader ───────────────────────────────────────────────────────────── def sbs_reader(queue: Queue, shutdown: dict): """ Connect to dump1090 SBS port, read lines, push to queue. Reconnects automatically on disconnect. """ while not shutdown["flag"]: try: log.info("Connecting to dump1090 SBS port %s:%d …", DUMP1090_HOST, DUMP1090_SBS_PORT) with socket.create_connection((DUMP1090_HOST, DUMP1090_SBS_PORT), timeout=10) as sock: log.info("Connected to dump1090 SBS port") buf = "" sock.settimeout(2.0) while not shutdown["flag"]: try: chunk = sock.recv(4096) if not chunk: log.warning("dump1090 SBS connection closed") break buf += chunk.decode("ascii", errors="replace") while "\n" in buf: line, buf = buf.split("\n", 1) line = line.strip() if line: queue.put(line) except socket.timeout: continue except (ConnectionRefusedError, OSError) as e: if not shutdown["flag"]: log.warning("SBS connect failed: %s — retry in %ds", e, DUMP1090_RECONNECT_DELAY) time.sleep(DUMP1090_RECONNECT_DELAY) # ── SBS-1 parser ───────────────────────────────────────────────────────────── # SBS-1 BaseStation format: # MSG,,,,,,,