Node SDRs: pin OP25 and each service to a dongle by serial (node-26#11)
Pairs with node-26 feat/sdr-pins. NodeRecord gains sdr_pins
(service -> serial), sdr_devices and op25_sdr_serial, mirrored from the
node's checkin. PATCH /nodes/{id} validates pins (known services, one
dongle per service) and sends priority/pins as a 'set_sdr_config'
command, never a config re-push. The node restarts OP25 only when OP25's
own dongle changes. The node page's section becomes 'SDRs' with an OP25
SDR dropdown ('Automatic (first SDR)' + detected dongles) and a
per-service dongle dropdown ('Any spare SDR'), plus duplicate-serial and
double-pin warnings.
Verified: c2-core pytest 490 passed; frontend tsc --noEmit clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c95498e7fe
commit
fb2d737d6f
@@ -1,5 +1,5 @@
|
||||
import secrets
|
||||
from typing import List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from app.models import CommandPayload
|
||||
@@ -193,6 +193,7 @@ async def assign_system(
|
||||
|
||||
|
||||
SECONDARY_SDR_MODES = ("adsb", "ais")
|
||||
SDR_PIN_KEYS = ("op25",) + SECONDARY_SDR_MODES
|
||||
|
||||
|
||||
class NodeUpdateBody(BaseModel):
|
||||
@@ -201,6 +202,9 @@ class NodeUpdateBody(BaseModel):
|
||||
secondary_sdr_mode: Optional[str] = None # legacy: none | adsb | ais
|
||||
# Ordered, e.g. ["adsb", "ais"]: SDRs beyond op25's run these top-down.
|
||||
secondary_sdr_priority: Optional[List[str]] = None
|
||||
# node-26#11: service -> dongle serial; null/"" = automatic. Moving OP25's
|
||||
# dongle restarts OP25 on the node; the other pins never do.
|
||||
sdr_pins: Optional[Dict[str, Optional[str]]] = None
|
||||
|
||||
|
||||
@router.patch("/{node_id}")
|
||||
@@ -224,14 +228,32 @@ async def update_node(
|
||||
raise HTTPException(400, f"secondary_sdr_priority must be distinct values from {SECONDARY_SDR_MODES}.")
|
||||
updates["secondary_sdr_mode"] = priority[0] if priority else "none"
|
||||
|
||||
if "sdr_pins" in updates:
|
||||
raw = updates["sdr_pins"] or {}
|
||||
unknown = [k for k in raw if k not in SDR_PIN_KEYS]
|
||||
if unknown:
|
||||
raise HTTPException(400, f"sdr_pins keys must be from {SDR_PIN_KEYS}.")
|
||||
pins = {k: str(v).strip() for k, v in raw.items() if v and str(v).strip()}
|
||||
if len(set(pins.values())) != len(pins):
|
||||
raise HTTPException(400, "Two services can't be pinned to the same SDR.")
|
||||
updates["sdr_pins"] = pins
|
||||
|
||||
await fstore.doc_update("nodes", node_id, updates)
|
||||
|
||||
# Priority goes as its own command: a config re-push restarts OP25, and
|
||||
# changing what the spare dongles do must never interrupt P25 recording.
|
||||
# The node applies it, then its checkin reports back what's really running.
|
||||
if priority is not None:
|
||||
mqtt_handler.send_command(node_id, {"action": "set_secondary_priority", "priority": priority})
|
||||
if set(updates) <= {"secondary_sdr_priority", "secondary_sdr_mode"}:
|
||||
# SDR settings go as their own command: a config re-push restarts OP25, and
|
||||
# changing what the spare dongles do must never interrupt P25 recording
|
||||
# (only moving OP25's own dongle restarts it, on the node's side). The node
|
||||
# applies it, then its checkin reports back what's really running.
|
||||
sdr_keys = {"secondary_sdr_priority", "secondary_sdr_mode", "sdr_pins"}
|
||||
if sdr_keys & set(updates):
|
||||
command = {"action": "set_sdr_config"}
|
||||
if priority is not None:
|
||||
command["priority"] = priority
|
||||
if "sdr_pins" in updates:
|
||||
# Explicit nulls so a cleared pin reaches the node as "automatic".
|
||||
command["pins"] = {k: updates["sdr_pins"].get(k) for k in SDR_PIN_KEYS}
|
||||
mqtt_handler.send_command(node_id, command)
|
||||
if set(updates) <= sdr_keys:
|
||||
return {"ok": True}
|
||||
|
||||
# Re-push config to apply new node settings locally
|
||||
|
||||
Reference in New Issue
Block a user