diff --git a/CHANGELOG.md b/CHANGELOG.md index 9842190..c6d4395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to **adn-server** are documented here. +## v2.3.3 (2026-07-13) + +### Bug Fixes + +- Gate OBP source ensure when bridge source is already active + ([`e140822`](https://github.com/ce5rpy/ADN-DMR-Peer-Server/commit/e140822e68fc65331123f12ee1ece8b7122a2690)) + + +## v2.3.2 (2026-07-13) + +### Bug Fixes + +- Inject scheduled voice via routing and restore monitor TX events + ([`62f429a`](https://github.com/ce5rpy/ADN-DMR-Peer-Server/commit/62f429aad90272e1684f554ac8a1999cf15eed43)) + + ## v2.3.1 (2026-07-13) ### Bug Fixes diff --git a/README.md b/README.md index dc257b4..6c4258d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ADN DMR Peer Server -**Version 2.3.1** — pairs with **adn-monitor 2.0.0-rc.4** (report v2 slim wire + JSON HELLO). +**Version 2.3.3** — pairs with **adn-monitor 2.0.0-rc.4** (report v2 slim wire + JSON HELLO). ADN DMR conference bridge server. Configuration is YAML; the codebase follows clean architecture (domain, application, infrastructure). v2 adds integrated **PROXY**, **SubscriptionStore** routing, report v2 to the monitor, and a unified **`adn-server.py`** entrypoint (`--echo`, `--doctor`, `--no-proxy`). See [CHANGELOG.md](CHANGELOG.md). diff --git a/pyproject.toml b/pyproject.toml index 266eca4..92ae522 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [project] name = "adn-server" -version = "2.3.1" +version = "2.3.3" description = "ADN DMR Peer Server" readme = "README.md" license = { text = "GPL-3.0-or-later" } diff --git a/src/adn_server/application/routing/obp_forward.py b/src/adn_server/application/routing/obp_forward.py index 66cd09f..496e1ce 100644 --- a/src/adn_server/application/routing/obp_forward.py +++ b/src/adn_server/application/routing/obp_forward.py @@ -81,9 +81,16 @@ class ObpForwardMixin: return if not (79 <= dst_int < 9990 or dst_int > 9999): return - from ..subscription.obp_source_ops import ensure_obp_source_for_tg_store + from ..subscription.obp_source_ops import ( + ensure_obp_source_for_tg_store, + obp_source_needs_ensure, + ) + + store = self._subscription_store + if not obp_source_needs_ensure(store, system_name, relay_table_key, dst_int): + return ensure_obp_source_for_tg_store( - self._subscription_store, + store, system_name, relay_table_key, dst_id_b, diff --git a/src/adn_server/application/subscription/obp_source_ops.py b/src/adn_server/application/subscription/obp_source_ops.py index e03404c..e029c2b 100644 --- a/src/adn_server/application/subscription/obp_source_ops.py +++ b/src/adn_server/application/subscription/obp_source_ops.py @@ -48,6 +48,23 @@ def _tgid_match(entry_tgid: Any, dst_id_b: bytes, dst_int: int) -> bool: return False +def obp_source_needs_ensure( + store: SubscriptionStore, + system_name: str, + relay_table_key: str, + dst_int: int, +) -> bool: + """True when a bridge table exists but OBP lacks an ACTIVE TS1 source for ``dst_int``.""" + active_tables = set(store.relay_tables_with_active_source(system_name, 1, dst_int)) + pending_keys: list[str] = [] + for key in (relay_table_key, "#" + relay_table_key): + if store.legs_in_table(key): + pending_keys.append(key) + if not pending_keys: + return False + return any(key not in active_tables for key in pending_keys) + + def ensure_obp_source_for_tg_store( store: SubscriptionStore, system_name: str, diff --git a/tests/application/test_subscription_table_store_ops.py b/tests/application/test_subscription_table_store_ops.py index a1adf41..8cc96fc 100644 --- a/tests/application/test_subscription_table_store_ops.py +++ b/tests/application/test_subscription_table_store_ops.py @@ -24,7 +24,10 @@ from __future__ import annotations from tests.harness.deterministic import active_routing_table, minimal_config -from adn_server.application.subscription.obp_source_ops import ensure_obp_source_for_tg_store +from adn_server.application.subscription.obp_source_ops import ( + ensure_obp_source_for_tg_store, + obp_source_needs_ensure, +) from adn_server.application.subscription.store_sync import replace_store_from_routing_table from adn_server.application.subscription.subscription_table_ops import ( ensure_dynamic_relay_store, @@ -64,9 +67,22 @@ def test_ensure_obp_source_activates_ts1_leg() -> None: if entry["SYSTEM"] == "OBP-CL": entry["ACTIVE"] = False store = _store(bridges) + assert obp_source_needs_ensure(store, "OBP-CL", "7305", 7305) is True ensure_obp_source_for_tg_store(store, "OBP-CL", "7305", bytes_3(7305), 7305, now=500.0) obp = next(s for s in store.snapshot() if s.system.value == "OBP-CL") assert obp.is_active() + assert obp_source_needs_ensure(store, "OBP-CL", "7305", 7305) is False + + +def test_obp_source_needs_ensure_false_when_no_bridge_table() -> None: + store = _store() + assert obp_source_needs_ensure(store, "OBP-CL", "7305", 7305) is False + + +def test_obp_source_needs_ensure_false_when_obp_source_already_active() -> None: + bridges = active_routing_table(7305, (("OBP-CL", 1), ("MASTER-A", 2))) + store = _store(bridges) + assert obp_source_needs_ensure(store, "OBP-CL", "7305", 7305) is False def test_ensure_dynamic_relay_store_obp_leg() -> None: