Files

96 lines
4.0 KiB
Python

"""ADS-B context for airband recordings (snapshot of aircraft near the receiver) and the airband frequency test."""
import json
import time
from radio.config import ADSB_FILE
from radio.util import write_json_compact
# ---- ADS-B context for airband recordings -------------------------------------------------
# Snapshot of aircraft near the receiver when an airband recording is classified (seconds after it
# ended). Source order: local readsb (future dedicated 1090 MHz RTL-SDR) -> adsb.lol public API.
# At most one remote request per minute (cache), short timeouts; failures only leave a note.
# NOTE 2026-10-20: adsb.lol / adsb.fi / OpenSky have NO coverage over St. Petersburg (0 aircraft
# while London returns dozens), so until a local receiver exists the list will usually be empty.
RX_LAT, RX_LON = 0.0, 0.0 # receiver (home area) -- adjust if the site is elsewhere
ULLI_LAT, ULLI_LON = 59.8003, 30.2625 # Pulkovo
ADSB_RADIUS_NM = 60
LOCAL_ADSB = ["/run/readsb/aircraft.json", "http://127.0.0.1:8080/data/aircraft.json"]
REMOTE_ADSB = f"https://api.adsb.lol/v2/point/{RX_LAT}/{RX_LON}/{ADSB_RADIUS_NM}"
_adsb_cache = {"t": 0.0, "data": None}
def _nm(lat1, lon1, lat2, lon2):
import math
p1, p2 = math.radians(lat1), math.radians(lat2)
dp, dl = p2 - p1, math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
d = 2 * 3440.065 * math.asin(min(1.0, math.sqrt(a)))
y = math.sin(dl) * math.cos(p2)
x = math.cos(p1) * math.sin(p2) - math.sin(p1) * math.cos(p2) * math.cos(dl)
return d, (math.degrees(math.atan2(y, x)) + 360) % 360
def _fetch_json(url, timeout=4):
import urllib.request
if url.startswith("/"):
with open(url, encoding="utf-8") as fh:
return json.load(fh)
req = urllib.request.Request(url, headers={"User-Agent": "home-sdr-scanner/1"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read(2_000_000).decode("utf-8"))
def adsb_snapshot():
now = time.time()
if _adsb_cache["data"] is not None and now - _adsb_cache["t"] < 60:
return _adsb_cache["data"]
src, raw, err = None, None, None
for u in LOCAL_ADSB + [REMOTE_ADSB]:
try:
raw = _fetch_json(u)
src = "readsb" if u in LOCAL_ADSB else "adsb.lol"
break
except Exception as e: # noqa: BLE001
err = f"{type(e).__name__}"
continue
out = {"t": int(now), "src": src, "list": []}
if raw is None:
out["err"] = err or "unavailable"
else:
for a in raw.get("ac", raw.get("aircraft", [])) or []:
lat, lon = a.get("lat"), a.get("lon")
if lat is None or lon is None:
continue
d, b = _nm(RX_LAT, RX_LON, lat, lon)
if d > ADSB_RADIUS_NM:
continue
alt = a.get("alt_baro")
alt = 0 if alt == "ground" else alt
dp, _ = _nm(ULLI_LAT, ULLI_LON, lat, lon)
sq = str(a.get("squawk") or "")
out["list"].append({
"cs": (a.get("flight") or "").strip() or a.get("r") or a.get("hex"),
"hex": a.get("hex"), "sq": sq or None, "alt": alt, "gs": a.get("gs"),
"trk": a.get("track"), "d": round(d, 1), "b": int(b), "cat": a.get("category"),
"t": a.get("t"),
"emg": sq in ("7500", "7600", "7700") or (a.get("emergency") not in (None, "none")),
"ulli": bool(alt is not None and isinstance(alt, (int, float)) and alt < 10000 and dp < 25),
})
out["list"].sort(key=lambda x: x["d"])
out["n"] = len(out["list"])
out["list"] = out["list"][:20]
_adsb_cache.update(t=now, data=out)
try:
write_json_compact(ADSB_FILE, out)
except OSError:
pass
return out
def is_avia(name):
try:
f = int(name.rsplit("_", 1)[1][:-4])
except (ValueError, IndexError):
return False
return 108_000_000 <= f < 137_000_000