Second SDR: ADS-B (dump1090) / AIS (AIS-catcher) support #10
@@ -213,7 +213,9 @@ class MQTTManager:
|
||||
async def _publish_checkin(self):
|
||||
from app.internal.discord_radio import radio_bot
|
||||
from app.internal.config_manager import load_node_config
|
||||
from app.internal.op25_client import op25_client
|
||||
config = load_node_config()
|
||||
devices = await op25_client.devices()
|
||||
payload = {
|
||||
"node_id": settings.node_id,
|
||||
"name": settings.node_name,
|
||||
@@ -225,7 +227,12 @@ class MQTTManager:
|
||||
"is_overridden": config.override_system_id is not None and config.node_type != "portable",
|
||||
"override_system_id": config.override_system_id,
|
||||
"enforce_override_timeout": config.enforce_override_timeout,
|
||||
"secondary_sdr_mode": config.secondary_sdr_mode,
|
||||
}
|
||||
# Best-effort — the first node-initiated hardware-report field. Omit
|
||||
# rather than guess if op25's control API is unreachable.
|
||||
if devices is not None:
|
||||
payload["sdr_count"] = devices.get("count")
|
||||
self._publish(self._t_checkin, payload, qos=1)
|
||||
|
||||
def _publish(self, topic: str, payload: dict, qos: int = 0, retain: bool = False):
|
||||
|
||||
@@ -65,6 +65,16 @@ class OP25Client:
|
||||
logger.error(f"OP25 status failed: {e}")
|
||||
return None
|
||||
|
||||
async def devices(self) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
r = await client.get(f"{self.api_url}/op25/devices")
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"OP25 device enumeration failed: {e}")
|
||||
return None
|
||||
|
||||
async def generate_config(self, config: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
|
||||
@@ -224,6 +224,7 @@ async def on_config_push(payload: dict):
|
||||
hardware_preset = payload.pop("hardware_preset", None)
|
||||
ppm_override = payload.pop("ppm_override", None)
|
||||
node_type = payload.pop("node_type", None)
|
||||
secondary_sdr_mode = payload.pop("secondary_sdr_mode", None)
|
||||
enforce_override_timeout = payload.pop("enforce_override_timeout", None)
|
||||
try:
|
||||
config = SystemConfig(**payload)
|
||||
@@ -243,6 +244,8 @@ async def on_config_push(payload: dict):
|
||||
node_cfg.ppm_override = float(ppm_override)
|
||||
if node_type is not None:
|
||||
node_cfg.node_type = node_type
|
||||
if secondary_sdr_mode is not None:
|
||||
node_cfg.secondary_sdr_mode = secondary_sdr_mode
|
||||
if enforce_override_timeout is not None:
|
||||
node_cfg.enforce_override_timeout = bool(enforce_override_timeout)
|
||||
save_node_config(node_cfg)
|
||||
|
||||
@@ -34,6 +34,7 @@ class NodeConfig(BaseModel):
|
||||
hardware_preset: str = "rtl-sdr-v3"
|
||||
ppm_override: Optional[float] = None
|
||||
node_type: str = "fixed" # fixed or portable
|
||||
secondary_sdr_mode: str = "none" # none | adsb | ais | op25_2 — requires a second physical SDR
|
||||
enforce_override_timeout: bool = True
|
||||
override_system_id: Optional[str] = None
|
||||
override_config: Optional[SystemConfig] = None
|
||||
|
||||
@@ -12,7 +12,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Install system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install git pulseaudio pulseaudio-utils liquidsoap -y
|
||||
apt-get install git pulseaudio pulseaudio-utils liquidsoap usbutils -y
|
||||
|
||||
# Install custom PulseAudio system config (enables anonymous access for edge-node)
|
||||
COPY system.pa /etc/pulse/system.pa
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from fastapi import HTTPException, APIRouter
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import json
|
||||
from models import ConfigGenerator, DecodeMode, ChannelConfig, DeviceConfig, TrunkingConfig, TrunkingChannelConfig, TerminalConfig, MetadataConfig, MetadataStreamConfig, HARDWARE_PRESETS
|
||||
@@ -69,6 +70,24 @@ def create_op25_router():
|
||||
async def get_status():
|
||||
return {"status": "running" if _is_running() else "stopped"}
|
||||
|
||||
@router.get("/devices")
|
||||
async def list_sdr_devices():
|
||||
"""Enumerate connected SDR-looking USB devices via lsusb.
|
||||
|
||||
Same match heuristic as install.sh's one-shot host check — good enough
|
||||
to answer "is a second SDR plugged in", not a serial-level device
|
||||
binding (op25's DeviceConfig.args has no serial concept yet either).
|
||||
"""
|
||||
devices = []
|
||||
try:
|
||||
out = subprocess.run(["lsusb"], capture_output=True, text=True, timeout=5).stdout
|
||||
for line in out.splitlines():
|
||||
if re.search(r"rtl2838|realtek.*283[28]|sdr", line, re.IGNORECASE):
|
||||
devices.append(line.strip())
|
||||
except Exception as e:
|
||||
LOGGER.warning(f"SDR device enumeration failed: {e}")
|
||||
return {"count": len(devices), "devices": devices}
|
||||
|
||||
@router.post("/generate-config")
|
||||
async def generate_config(generator: ConfigGenerator):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user