Replaces the single secondary_sdr_mode with secondary_sdr_priority, e.g. ["adsb", "ais"]. OP25 always keeps its own dongle; the secondary-sdr container starts decoders top-down until it runs out of free SDRs, so a 3-SDR node runs ADS-B and AIS at once and a 2-SDR node runs the top pick. - secondary-sdr: one decoder per mode, POST /secondary/apply(priority) (no-op when the right prefix is already running), orphaned decoders from a uvicorn reload are reaped on start, /status reports sdr_count via lsusb (op25's :stable image has none). - edge-node: one apply path (set_secondary_priority) for the local dashboard, a new C2 'set_secondary_priority' MQTT command, and config pushes; it never restarts op25. Legacy mode migrates on load. Checkin reports priority, what's running, and sdr_count. Uplink forwards aircraft and vessels whenever either is present. - Local dashboard: 'Secondary SDRs' card to enable/reorder/save. Verified: edge-node pytest 190 passed, flake8 clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
from pydantic import BaseModel, model_validator
|
|
from typing import Optional, Dict, Any, Iterable, List
|
|
from enum import Enum
|
|
from datetime import datetime
|
|
|
|
|
|
class NodeStatus(str, Enum):
|
|
ONLINE = "online"
|
|
OFFLINE = "offline"
|
|
RECORDING = "recording"
|
|
UNCONFIGURED = "unconfigured"
|
|
|
|
|
|
class CallStatus(str, Enum):
|
|
ACTIVE = "active"
|
|
ENDED = "ended"
|
|
|
|
|
|
class SystemConfig(BaseModel):
|
|
system_id: str
|
|
name: str
|
|
type: str # P25, DMR, NBFM
|
|
config: Dict[str, Any] # OP25-compatible config blob passed through to op25-container
|
|
|
|
|
|
# Decoders a node can run on the SDRs beyond op25's (node-26#9). op25 always
|
|
# keeps its own dongle; each further dongle runs the next entry of the node's
|
|
# secondary_sdr_priority, so a 3-SDR node with ["adsb", "ais"] runs both.
|
|
SECONDARY_SDR_MODES = ("adsb", "ais")
|
|
|
|
|
|
def normalize_secondary_priority(items: Iterable[str]) -> List[str]:
|
|
"""Known modes only, first occurrence wins, order preserved."""
|
|
out: List[str] = []
|
|
for m in items or []:
|
|
if m in SECONDARY_SDR_MODES and m not in out:
|
|
out.append(m)
|
|
return out
|
|
|
|
|
|
class NodeConfig(BaseModel):
|
|
node_id: str
|
|
node_name: str
|
|
lat: float
|
|
lon: float
|
|
assigned_system_id: Optional[str] = None
|
|
system_config: Optional[SystemConfig] = None
|
|
configured: bool = False
|
|
hardware_preset: str = "rtl-sdr-v3"
|
|
ppm_override: Optional[float] = None
|
|
node_type: str = "fixed" # fixed or portable
|
|
secondary_sdr_priority: List[str] = [] # ordered; SDRs beyond op25's run these top-down
|
|
# Legacy single-mode field (pre-priority). Still written as priority[0] so
|
|
# an older C2 reading checkins sees something sensible; read only to
|
|
# migrate a node_config.json saved before priority existed.
|
|
secondary_sdr_mode: str = "none"
|
|
enforce_override_timeout: bool = True
|
|
override_system_id: Optional[str] = None
|
|
override_config: Optional[SystemConfig] = None
|
|
offline_call_buffer_size: int = 35 # max call_end events to buffer while MQTT is offline
|
|
|
|
@model_validator(mode="after")
|
|
def _migrate_secondary_mode(self) -> "NodeConfig":
|
|
if not self.secondary_sdr_priority and self.secondary_sdr_mode in SECONDARY_SDR_MODES:
|
|
self.secondary_sdr_priority = [self.secondary_sdr_mode]
|
|
return self
|
|
|
|
|
|
class CallEvent(BaseModel):
|
|
call_id: str
|
|
node_id: str
|
|
system_id: Optional[str] = None
|
|
talkgroup_id: Optional[int] = None
|
|
talkgroup_name: Optional[str] = None
|
|
started_at: datetime
|
|
ended_at: Optional[datetime] = None
|
|
audio_url: Optional[str] = None
|
|
status: CallStatus = CallStatus.ACTIVE
|
|
|
|
|
|
class DiscordCommand(BaseModel):
|
|
action: str # "join" or "leave"
|
|
guild_id: Optional[str] = None
|
|
channel_id: Optional[str] = None
|