Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
3.5 KiB
Python
69 lines
3.5 KiB
Python
"""Discovered sources (sdr-discover) and the queue of frequencies promoted into the scan table."""
|
|
import re
|
|
import threading
|
|
import time
|
|
|
|
from radio.channels import channels
|
|
from radio.config import ADSB_FILE, MAX_PROMOTED, PROMOTED_FILE, SOURCES_FILE
|
|
from radio.util import read_json, write_json_atomic
|
|
|
|
_promo_lock = threading.Lock()
|
|
|
|
|
|
def adsb_latest():
|
|
return read_json(ADSB_FILE, {})
|
|
|
|
|
|
def sources():
|
|
db = read_json(SOURCES_FILE, {})
|
|
promo = read_json(PROMOTED_FILE, [])
|
|
promo_f = {int(p.get("freq_hz", 0)) for p in promo if isinstance(p, dict)}
|
|
scan = channels().get("channels", {})
|
|
scan_f = {int(k) for k in scan}
|
|
out = []
|
|
for e in (db.get("sources") or {}).values():
|
|
f = int(e.get("f", 0))
|
|
in_scan = any(abs(f - k) < 3000 for k in scan_f)
|
|
out.append({
|
|
"f": f, "label": e.get("label"), "mod": e.get("mod"), "kind": e.get("kind"),
|
|
"bw_khz": e.get("bw_khz"), "max_db": e.get("max_db"), "last_db": e.get("last_db"),
|
|
"act": e.get("act"), "std": e.get("std"), "runs": e.get("runs"), "hits": e.get("hits"),
|
|
"seen_frac": e.get("seen_frac"), "first": e.get("first"), "last": e.get("last"),
|
|
"in_scan": in_scan, "promoted": f in promo_f and not in_scan,
|
|
})
|
|
out.sort(key=lambda x: (-(x["last"] or 0), -(x["hits"] or 0)))
|
|
scan_channels = sorted(({"f": int(k), "label": (v or {}).get("label"), "group": (v or {}).get("group")}
|
|
for k, v in scan.items()), key=lambda c: c["f"])
|
|
promo = [p for p in promo if isinstance(p, dict)]
|
|
# promoted.json is the persistent table of channels added from discovery (gen_config reads it on every scanner
|
|
# start); only entries not yet present in the scan table are really "queued"
|
|
queued = [{"f": int(p.get("freq_hz", 0)), "label": p.get("label"), "mod": p.get("mod"), "ts": p.get("ts")}
|
|
for p in promo if not any(abs(int(p.get("freq_hz", 0)) - k) < 3000 for k in scan_f)]
|
|
return {"runs": db.get("runs", 0), "last_run": db.get("last_run"), "last_sweeps": db.get("last_sweeps"),
|
|
"floor_dbfs": db.get("floor_dbfs"), "sources": out,
|
|
"promoted": promo, "scan_channels": scan_channels, "queued": queued}
|
|
|
|
|
|
def promote(freq):
|
|
"""Queue a discovered frequency for the scan table (picked up by gen_config on scanner restart)."""
|
|
src = (read_json(SOURCES_FILE, {}).get("sources") or {}).get(str(freq))
|
|
if not src:
|
|
return 404, "частота не найдена среди обнаруженных"
|
|
f = int(src["f"])
|
|
if not (108_000_000 <= f <= 175_000_000 or 400_000_000 <= f <= 470_000_000):
|
|
return 400, "вне поддерживаемых диапазонов"
|
|
mod = src.get("mod") if src.get("mod") in ("am", "nfm") else "nfm"
|
|
label = re.sub(r"[^\w .,()/+-]", "", str(src.get("label") or ""), flags=re.U)[:40]
|
|
label = f"{f / 1e6:.4f} {label}".strip()
|
|
with _promo_lock:
|
|
promo = read_json(PROMOTED_FILE, [])
|
|
if not isinstance(promo, list):
|
|
promo = []
|
|
if any(isinstance(p, dict) and p.get("freq_hz") == f for p in promo):
|
|
return 200, "уже в очереди"
|
|
if len(promo) >= MAX_PROMOTED:
|
|
return 409, f"очередь заполнена ({MAX_PROMOTED})"
|
|
promo.append({"freq_hz": f, "mod": mod, "label": label, "ts": int(time.time())})
|
|
write_json_atomic(PROMOTED_FILE, promo, ensure_ascii=False, indent=1)
|
|
return 200, "добавлено: сканер подхватит после следующего обзора (до 1 ч)"
|