diff --git a/tasks/flightradar24/compose/docker-compose.yml b/tasks/flightradar24/compose/docker-compose.yml index 157b5ec..48c4409 100644 --- a/tasks/flightradar24/compose/docker-compose.yml +++ b/tasks/flightradar24/compose/docker-compose.yml @@ -18,6 +18,7 @@ x-common-env: &common-env RTLSDR_SAMPLE_RATE: ${RTLSDR_SAMPLE_RATE:-2000000} RTLSDR_CENTER_FREQUENCY: ${RTLSDR_CENTER_FREQUENCY:-1090000000} RTLSDR_GAIN: ${RTLSDR_GAIN:-auto} + RTLSDR_BIAS_T: ${RTLSDR_BIAS_T:-0} services: postgres: @@ -52,6 +53,7 @@ services: environment: <<: *common-env SERVICE_ROLE: capture + privileged: true devices: - "/dev/bus/usb:/dev/bus/usb" volumes: diff --git a/tasks/flightradar24/ingest/capture/Dockerfile b/tasks/flightradar24/ingest/capture/Dockerfile index d8aabbf..91cc65b 100644 --- a/tasks/flightradar24/ingest/capture/Dockerfile +++ b/tasks/flightradar24/ingest/capture/Dockerfile @@ -1,8 +1,44 @@ FROM python:3.11-slim +# ── system deps ─────────────────────────────────────────────────────────────── +# libusb-1.0 + udev needed for RTL-SDR USB access +# dump1090-fa from FlightAware PPA (Debian/Ubuntu compatible) RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq-dev gcc python3-dev && rm -rf /var/lib/apt/lists/* + libpq-dev gcc python3-dev \ + libusb-1.0-0 udev \ + wget gnupg ca-certificates \ + && rm -rf /var/lib/apt/lists/* +# ── rtl-sdr-blog driver (RTL-SDR Blog V4 / R828D tuner) ────────────────────── +# The standard rtl-sdr package does NOT support V4. We install the rtl-sdr-blog +# fork which adds R828D support and bias-T control. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake pkg-config \ + libusb-1.0-0-dev \ + git \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone https://github.com/rtlsdrblog/rtl-sdr-blog.git /tmp/rtl-sdr-blog \ + && cmake -S /tmp/rtl-sdr-blog -B /tmp/rtl-sdr-blog/build \ + -DINSTALL_UDEV_RULES=ON \ + -DDETACH_KERNEL_DRIVER=ON \ + && cmake --build /tmp/rtl-sdr-blog/build --parallel $(nproc) \ + && cmake --install /tmp/rtl-sdr-blog/build \ + && ldconfig \ + && rm -rf /tmp/rtl-sdr-blog + +# ── dump1090-fa (FlightAware fork — best SBS-1 output support) ─────────────── +# Build from source: works on any Debian/Ubuntu without PPA +RUN apt-get update && apt-get install -y --no-install-recommends \ + libncurses-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 https://github.com/flightaware/dump1090.git /tmp/dump1090 \ + && make -C /tmp/dump1090 -j$(nproc) \ + && cp /tmp/dump1090/dump1090 /usr/local/bin/dump1090-fa \ + && rm -rf /tmp/dump1090 + +# ── python app ──────────────────────────────────────────────────────────────── WORKDIR /app COPY requirements.txt . @@ -10,4 +46,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY main.py . +# dump1090 JSON output dir +RUN mkdir -p /tmp/dump1090-json + CMD ["python", "-u", "main.py"] diff --git a/tasks/flightradar24/ingest/capture/main.py b/tasks/flightradar24/ingest/capture/main.py index 15e9c4c..2203a72 100644 --- a/tasks/flightradar24/ingest/capture/main.py +++ b/tasks/flightradar24/ingest/capture/main.py @@ -1,16 +1,19 @@ """ -FR24 Capture Service -Connects to PostgreSQL, creates a capture session, and writes fake raw_packets. -In production: replace the fake packet loop with actual RTL-SDR / dump1090 input. +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 random 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 @@ -30,44 +33,128 @@ DB_DSN = ( 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) -PACKET_INTERVAL = float(os.environ.get("PACKET_INTERVAL_SECONDS", 2.0)) +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" -# ── fake ADS-B payload generator ───────────────────────────────────────────── -# Real ADS-B Mode-S messages are 7 or 14 bytes. -# We generate plausible-looking random bytes tagged as DF17 (extended squitter). -_ICAO_POOL = [f"{i:06X}" for i in random.sample(range(0x400000, 0xFFFFFF), 20)] +# ── dump1090 process ────────────────────────────────────────────────────────── -def _fake_adsb_bytes() -> bytes: - """14-byte fake Mode-S extended squitter (DF17).""" - df17_first_byte = 0x8D # downlink format 17 - icao = bytes.fromhex(random.choice(_ICAO_POOL)) - payload = bytes([random.randint(0, 255) for _ in range(7)]) - crc = bytes([random.randint(0, 255) for _ in range(3)]) - return bytes([df17_first_byte]) + icao + payload + crc +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] -def _fake_packet_row(capture_id: str) -> dict: - raw = _fake_adsb_bytes() + 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,,,,,,,