tr_config: cut a too-wide trunked control-channel list to the run one SDR covers instead of refusing (node-26#45)
Found on node-002 (MTA Zone 2): five control channels span 4.15 MHz, so the whole system was refused. TR needs one live control channel; use the largest set that fits and log the rest. Conventional channels must still all fit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e2a5b63067
commit
010a6b0be5
@@ -19,6 +19,7 @@ import csv
|
||||
import io
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from app.internal.logger import logger
|
||||
from app.internal.conventional import channel_tgid
|
||||
from app.internal.radio_config import (
|
||||
ANALOG, CONVENTIONAL, DMR, TRUNKED, analog_frequency, conventional_channels, system_mode, to_hz,
|
||||
@@ -50,6 +51,21 @@ def _fits(center: int, freqs: List[int], half_band: float) -> bool:
|
||||
return all(abs(f - center) <= half_band for f in freqs)
|
||||
|
||||
|
||||
def widest_cover(freqs: List[int], rate: int) -> List[int]:
|
||||
"""The largest run of frequencies one SDR at `rate` can hold at once
|
||||
(ties: the lowest). A trunked system lists every control channel it might
|
||||
use, but TR needs only one live at a time, so a list wider than the SDR is
|
||||
cut down to what fits instead of refusing the whole system."""
|
||||
half = rate * USABLE_BANDWIDTH_SHARE / 2
|
||||
fs = sorted(set(freqs))
|
||||
best: List[int] = []
|
||||
for i, lo in enumerate(fs):
|
||||
run = [f for f in fs[i:] if f - lo <= 2 * half]
|
||||
if len(run) > len(best):
|
||||
best = run
|
||||
return best
|
||||
|
||||
|
||||
def choose_center(freqs: List[int], rate: int, override: Optional[int] = None) -> int:
|
||||
"""A centre frequency that puts every channel in the passband and none on
|
||||
the DC spike. ValueError if they cannot all fit on one SDR."""
|
||||
@@ -208,7 +224,15 @@ def build_tr_config(
|
||||
# channels are known here, so the SDR is centred on those. A system
|
||||
# whose voice channels sit outside that ~2 MHz loses those calls (TR
|
||||
# logs "no source covering" for each) — needs a second source.
|
||||
freqs = ccs
|
||||
covered = ccs if center_override else widest_cover(ccs, int(sample_rate))
|
||||
if len(covered) < len(ccs):
|
||||
system["control_channels"] = covered
|
||||
logger.warning(
|
||||
f"Control channels span {(max(ccs) - min(ccs)) / 1e6:.3f} MHz, wider than one SDR; "
|
||||
f"using the {len(covered)} of {len(ccs)} that fit "
|
||||
f"({min(covered) / 1e6:.5f}-{max(covered) / 1e6:.5f} MHz)."
|
||||
)
|
||||
freqs = covered
|
||||
source["digitalRecorders"] = int(digital_recorders)
|
||||
elif mode == CONVENTIONAL:
|
||||
chans = conventional_channels(config)
|
||||
|
||||
@@ -136,7 +136,7 @@ def test_legacy_analog_and_unpinned_sdr():
|
||||
@pytest.mark.parametrize("stype,cfg", [
|
||||
("DMR", {"control_channels": [450.0]}),
|
||||
("P25", {"control_channels": []}),
|
||||
("P25", {"control_channels": [851.0, 855.0]}), # 4 MHz: wider than one RTL-SDR
|
||||
("P25", {"mode": "conventional", "channels": [{"freq": 851.0}, {"freq": 855.0}]}), # 4 MHz: every channel must fit
|
||||
("P25", {"mode": "conventional", "channels": []}),
|
||||
])
|
||||
def test_unbuildable_systems_raise(stype, cfg):
|
||||
@@ -374,7 +374,8 @@ async def test_apply_config_writes_files_and_restart(tr_env, monkeypatch, tmp_pa
|
||||
assert cfg["sources"][0]["device"] == "rtl=00000101" and (d / "talkgroups.csv").exists()
|
||||
|
||||
bad = await t.apply_config(SystemConfig(system_id="s2", name="Wide", type="P25",
|
||||
config={"control_channels": [851.0, 856.0]}))
|
||||
config={"mode": "conventional",
|
||||
"channels": [{"freq": 851.0}, {"freq": 856.0}]}))
|
||||
assert not bad and "MHz" in t.config_error
|
||||
# radio_backend reports the config error instead of pretending to start.
|
||||
monkeypatch.setattr(trmod, "trunk_recorder", t)
|
||||
@@ -492,3 +493,9 @@ def test_health_tr_failures(tr, cid, state):
|
||||
|
||||
def test_health_tr_conventional_has_no_decode_rate():
|
||||
assert _facts(_tr(decode_rate=None), mode="conventional")["decode"]["state"] == "ok"
|
||||
|
||||
|
||||
def test_wide_control_channel_list_is_cut_to_what_one_sdr_covers():
|
||||
# MTA Zone 2 (node-002): five control channels spanning 4.15 MHz.
|
||||
ccs = [770_156_250, 770_431_250, 773_293_750, 773_843_750, 774_306_250]
|
||||
assert tr_config.widest_cover(ccs, 2_400_000) == ccs[2:]
|
||||
|
||||
Reference in New Issue
Block a user