From a4a575ec9f2ece9322669c91d2d9a15a62c0e933 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Tue, 27 Oct 2026 20:16:08 -0400 Subject: [PATCH] op25: pin gr310 to 5b58335; analog defaults to OP25 noise squelch (node-26#46, node-26#29) - op25-container/Dockerfile: clone gr310 then check out 5b58335 (OP25_COMMIT build arg). That is the gr310 merge tk_drb_conv was verified against for #29 and the first carrying PR #296's NBFM noise/voice squelch. Changing the RUN line also busts CI's cached unpinned clone. - Keys verified from op25_nbfm.py at that commit: nbfm_squelch_mode (power|noise|voice), nbfm_noise_squelch_db, nbfm_squelch_threshold, read from the channel dict. - op25-container: ConfigGenerator.nbfm_squelch_mode (enum, default noise) and nbfm_noise_squelch_db, written onto the analog channel. - edge-node: system config squelch_mode / squelch_db / squelch_threshold -> generate payload (validated, noise when absent); /api/listen and the dashboard's analog pane expose mode and level. #29's CI test of tk_drb_conv against the real tk_p25.py is not done here. Co-Authored-By: Claude Opus 5.5 --- drb-edge-node/app/internal/radio_config.py | 42 +++++- drb-edge-node/app/routers/api.py | 21 ++- drb-edge-node/app/templates/index.html | 28 +++- drb-edge-node/tests/test_conventional.py | 11 ++ drb-edge-node/tests/test_local_dashboard.py | 11 ++ op25-container/Dockerfile | 138 +++++++++--------- op25-container/app/models.py | 19 +++ op25-container/app/routers/op25_controller.py | 2 + op25-container/tests/test_conventional.py | 17 +++ 9 files changed, 218 insertions(+), 71 deletions(-) diff --git a/drb-edge-node/app/internal/radio_config.py b/drb-edge-node/app/internal/radio_config.py index 08b853b..4dc0892 100644 --- a/drb-edge-node/app/internal/radio_config.py +++ b/drb-edge-node/app/internal/radio_config.py @@ -9,7 +9,11 @@ C2 system documents (the `config` blob is passed through untouched): P25 conventional type "P25", config.mode "conventional", config.channels = [{freq: MHz, name, nac?}, ...] (node-26#24) Analog type "NBFM", config.channels = [{freq, name}] — or, from - before #24, config.control_channels[0] + before #24, config.control_channels[0]. Optional squelch + (node-26#46): config.squelch_mode "noise" (default) | + "voice" | "power", config.squelch_db (noise/voice: dB + of quieting to open) and config.squelch_threshold + (power: absolute dB, OP25 default -60) A missing mode is trunked, so every system saved before #24 behaves exactly as it did. Pure functions; main._generate_op25_config does the I/O. @@ -80,6 +84,41 @@ def conventional_channels(config: Dict[str, Any]) -> List[Dict[str, Any]]: return out +SQUELCH_MODES = ("noise", "voice", "power") +DEFAULT_SQUELCH_MODE = "noise" + + +def _optional_float(config: Dict[str, Any], key: str) -> Optional[float]: + value = (config or {}).get(key) + if value is None or (isinstance(value, str) and not value.strip()): + return None + try: + return float(value) + except (TypeError, ValueError): + raise ValueError(f"{key} {value!r} isn't a number.") + + +def analog_squelch(config: Dict[str, Any]) -> Dict[str, Any]: + """config -> the op25-container squelch fields for an analog channel. + + Noise squelch by default (node-26#46): OP25's power squelch compares an + absolute level that depends on dongle, gain and antenna, and left analog + open on static; noise squelch measures FM quieting and needs no + calibration. A config that says nothing gets noise mode. + """ + mode = str((config or {}).get("squelch_mode") or DEFAULT_SQUELCH_MODE).strip().lower() + if mode not in SQUELCH_MODES: + raise ValueError(f"Squelch mode {mode!r} isn't one of {', '.join(SQUELCH_MODES)}.") + out: Dict[str, Any] = {"nbfm_squelch_mode": mode} + db = _optional_float(config, "squelch_db") + if db is not None: + out["nbfm_noise_squelch_db"] = db + threshold = _optional_float(config, "squelch_threshold") + if threshold is not None: + out["nbfm_squelch"] = threshold + return out + + def analog_frequency(config: Dict[str, Any]) -> Optional[int]: chans = (config or {}).get("channels") or [] if chans and isinstance(chans[0], dict) and chans[0].get("freq") not in (None, ""): @@ -117,6 +156,7 @@ def build_generate_payload( if mode == ANALOG: freq = analog_frequency(config) payload["channels"] = [freq] if freq else [] + payload.update(analog_squelch(config)) return payload talkgroups = (config or {}).get("talkgroups", []) or [] diff --git a/drb-edge-node/app/routers/api.py b/drb-edge-node/app/routers/api.py index dddc78e..f0a8fbd 100644 --- a/drb-edge-node/app/routers/api.py +++ b/drb-edge-node/app/routers/api.py @@ -208,6 +208,12 @@ class ListenBody(BaseModel): frequencies: List[float] = [] talkgroups: List[Talkgroup] = [] # p25 only; empty = every talkgroup nac: Optional[str] = None # p25conv only, hex; empty = any NAC + # analog only (node-26#46): OP25 squelch. noise (default) needs no + # calibration; squelch_db = dB of quieting to open (OP25 default 8); + # squelch_threshold = absolute dB for power mode (OP25 default -60). + squelch_mode: Optional[Literal["noise", "voice", "power"]] = None + squelch_db: Optional[float] = None + squelch_threshold: Optional[float] = None # RTL-SDR (R820T/R828D) tuning range. @@ -242,14 +248,21 @@ async def listen(body: ListenBody): else: is_p25 = body.mode == "p25" label = (body.name or "").strip() or (f"P25 {first} MHz" if is_p25 else f"Analog {first} MHz") + sys_config = { + "control_channels": freqs, + "talkgroups": [tg.model_dump() for tg in body.talkgroups] if is_p25 else [], + } + if not is_p25: + sys_config["squelch_mode"] = body.squelch_mode or "noise" + if body.squelch_db is not None: + sys_config["squelch_db"] = body.squelch_db + if body.squelch_threshold is not None: + sys_config["squelch_threshold"] = body.squelch_threshold config = SystemConfig( system_id=f"local-{body.mode}", name=label, type="P25" if is_p25 else "NBFM", - config={ - "control_channels": freqs, - "talkgroups": [tg.model_dump() for tg in body.talkgroups] if is_p25 else [], - }, + config=sys_config, ) await _apply_override(config) return {"ok": True, "name": config.name} diff --git a/drb-edge-node/app/templates/index.html b/drb-edge-node/app/templates/index.html index 8847e2d..39e2333 100644 --- a/drb-edge-node/app/templates/index.html +++ b/drb-edge-node/app/templates/index.html @@ -216,6 +216,20 @@ +
+ + + Noise squelch opens on a real carrier and needs no tuning per site. +
+
+ + + Noise/voice: dB of quieting to open — 8 default, 12-15 for strong signals only, ~5 for weak ones. +