From 923069deeea585ae574a6aadcda309485cba3cb1 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Jul 2026 02:13:26 +0100 Subject: [PATCH] Enable network direct dial by default --- FreeDMR.cfg | 2 +- bridge_master.py | 8 ++++---- config.py | 10 +++++----- docker-configs/freedmr.cfg | 2 +- docs/codex-notes.md | 6 +++--- docs/options-cheatsheet.md | 4 ++-- docs/test-harness-design.md | 7 ++++--- docs/testing.md | 13 +++++++------ docs/v1x-codex-changelog.md | 7 +++++-- freedmr.cfg | 2 +- tests/harness/deterministic.py | 2 +- tests/harness/udp_blackbox.py | 4 ++-- tests/test_deterministic_harness.py | 12 ++++++------ 13 files changed, 42 insertions(+), 37 deletions(-) diff --git a/FreeDMR.cfg b/FreeDMR.cfg index 9dd9a97..67c5c90 100644 --- a/FreeDMR.cfg +++ b/FreeDMR.cfg @@ -61,7 +61,7 @@ SINGLE_MODE: True VOICE_IDENT: True DIAL_A_TG: True DYNAMIC_TG_ROUTING: True -NETWORK_DIRECT_DIAL_SLOT: 0 +NETWORK_DIRECT_DIAL_SLOT: 1 TS1_STATIC: TS2_STATIC: DEFAULT_DIAL_TS1: 0 diff --git a/bridge_master.py b/bridge_master.py index 3fe38a8..2f507fe 100644 --- a/bridge_master.py +++ b/bridge_master.py @@ -275,7 +275,7 @@ def activate_network_direct_dial(_dst_id, pkt_time): if CONFIG['SYSTEMS'][system]['MODE'] != 'MASTER': continue - slot = CONFIG['SYSTEMS'][system].get('NETWORK_DIRECT_DIAL_SLOT', 0) + slot = CONFIG['SYSTEMS'][system].get('NETWORK_DIRECT_DIAL_SLOT', 1) if slot not in (1, 2): continue @@ -1516,7 +1516,7 @@ def options_config(): if 'DYNAMIC_TG_ROUTING' not in _options: _options['DYNAMIC_TG_ROUTING'] = int(CONFIG['SYSTEMS'][_system].get('DYNAMIC_TG_ROUTING', True)) if 'NETWORK_DIRECT_DIAL_SLOT' not in _options: - _options['NETWORK_DIRECT_DIAL_SLOT'] = CONFIG['SYSTEMS'][_system].get('NETWORK_DIRECT_DIAL_SLOT', 0) + _options['NETWORK_DIRECT_DIAL_SLOT'] = CONFIG['SYSTEMS'][_system].get('NETWORK_DIRECT_DIAL_SLOT', 1) _default_dial_ts1 = parse_default_reflector_option(_options['DEFAULT_DIAL_TS1'], default=None) if _default_dial_ts1 is None: @@ -1590,7 +1590,7 @@ def options_config(): _network_direct_dial_slot = parse_network_direct_dial_slot(_options['NETWORK_DIRECT_DIAL_SLOT'], default=None) if _network_direct_dial_slot is None: logger.debug('(OPTIONS) %s - NETWORK_DIRECT_DIAL_SLOT is not 0, 1, or 2, ignoring',_system) - _network_direct_dial_slot = CONFIG['SYSTEMS'][_system].get('NETWORK_DIRECT_DIAL_SLOT', 0) + _network_direct_dial_slot = CONFIG['SYSTEMS'][_system].get('NETWORK_DIRECT_DIAL_SLOT', 1) if 'TS1_STATIC' not in _options or 'TS2_STATIC' not in _options or 'DEFAULT_DIAL_TS1' not in _options or 'DEFAULT_DIAL_TS2' not in _options or 'DEFAULT_UA_TIMER' not in _options: logger.debug('(OPTIONS) %s - Required field missing, ignoring',_system) @@ -1653,7 +1653,7 @@ def options_config(): if _dynamic_tg_routing != CONFIG['SYSTEMS'][_system].get('DYNAMIC_TG_ROUTING', True): logger.debug('(OPTIONS) %s DYNAMIC_TG_ROUTING changed to %s',_system,_dynamic_tg_routing) - if _network_direct_dial_slot != CONFIG['SYSTEMS'][_system].get('NETWORK_DIRECT_DIAL_SLOT', 0): + if _network_direct_dial_slot != CONFIG['SYSTEMS'][_system].get('NETWORK_DIRECT_DIAL_SLOT', 1): logger.debug('(OPTIONS) %s NETWORK_DIRECT_DIAL_SLOT changed to %s',_system,_network_direct_dial_slot) ts1 = [] diff --git a/config.py b/config.py index c29962a..5a7b942 100755 --- a/config.py +++ b/config.py @@ -49,14 +49,14 @@ logger = logging.getLogger(__name__) def config_network_direct_dial_slot(config, section): try: - slot = config.getint(section, 'NETWORK_DIRECT_DIAL_SLOT', fallback=0) + slot = config.getint(section, 'NETWORK_DIRECT_DIAL_SLOT', fallback=1) except ValueError: - logger.warning('(%s) NETWORK_DIRECT_DIAL_SLOT is not an integer, defaulting to 0', section) - return 0 + logger.warning('(%s) NETWORK_DIRECT_DIAL_SLOT is not an integer, defaulting to 1', section) + return 1 if slot not in (0, 1, 2): - logger.warning('(%s) NETWORK_DIRECT_DIAL_SLOT is not 0, 1, or 2, defaulting to 0', section) - return 0 + logger.warning('(%s) NETWORK_DIRECT_DIAL_SLOT is not 0, 1, or 2, defaulting to 1', section) + return 1 return slot def master_timer_config(config, section): diff --git a/docker-configs/freedmr.cfg b/docker-configs/freedmr.cfg index 9dd9a97..67c5c90 100644 --- a/docker-configs/freedmr.cfg +++ b/docker-configs/freedmr.cfg @@ -61,7 +61,7 @@ SINGLE_MODE: True VOICE_IDENT: True DIAL_A_TG: True DYNAMIC_TG_ROUTING: True -NETWORK_DIRECT_DIAL_SLOT: 0 +NETWORK_DIRECT_DIAL_SLOT: 1 TS1_STATIC: TS2_STATIC: DEFAULT_DIAL_TS1: 0 diff --git a/docs/codex-notes.md b/docs/codex-notes.md index c799423..5365f1b 100644 --- a/docs/codex-notes.md +++ b/docs/codex-notes.md @@ -16,12 +16,12 @@ Findings: routing policy knob for dynamic/dial behavior. Assumptions: -- `NETWORK_DIRECT_DIAL_SLOT=0` means disabled; `1` presents TG9 TS1; `2` - presents TG9 TS2 for DMO/simplex clients. +- `NETWORK_DIRECT_DIAL_SLOT=1` is the default and presents TG9 TS1; `0` means + disabled; `2` presents TG9 TS2 for DMO/simplex clients. - Network direct dial is client/session-overridable through `OPTIONS`; the compact alias is `NDD_SLOT`. - RF-originated traffic must not activate network direct dial. -- Invalid `NETWORK_DIRECT_DIAL_SLOT` values in config should fall back to `0` +- Invalid `NETWORK_DIRECT_DIAL_SLOT` values in config should fall back to `1` and log a warning; invalid OPTIONS values should leave the current session value unchanged. diff --git a/docs/options-cheatsheet.md b/docs/options-cheatsheet.md index de13b5e..892211b 100644 --- a/docs/options-cheatsheet.md +++ b/docs/options-cheatsheet.md @@ -100,7 +100,7 @@ NDD_SLOT=2 | `TS2_TG_TIMER` | Slot 2 conventional TG timeout in minutes. | | `TS1_DIAL_TIMER` | Slot 1 dial-a-TG timeout in minutes. | | `TS2_DIAL_TIMER` | Slot 2 dial-a-TG timeout in minutes. | -| `NETWORK_DIRECT_DIAL_SLOT` | Network direct dial slot. `0` disables it, `1` presents on TG9 TS1, `2` presents on TG9 TS2. | +| `NETWORK_DIRECT_DIAL_SLOT` | Network direct dial slot. Default is `1`. `0` disables it, `1` presents on TG9 TS1, `2` presents on TG9 TS2. | | `DIALTG` | Enable/disable dial-a-TG private-call control. Use `1` or `0`. | | `DYNAMIC` | Enable/disable automatic dynamic conventional TG routing. Use `1` or `0`. | | `VOICE` | Enable/disable voice ident. Use `1` or `0`. | @@ -160,7 +160,7 @@ KEY=secret;TS1=91,92;TS2=235;DIAL1=4400;DIAL2=2350;TGTO1=10;TGTO2=10;DIALTO1=30; configured slot. It does not enable general private voice routing. - Network direct dial does not override an active or hangtime user-selected dial-a-TG route. -- Invalid `NETWORK_DIRECT_DIAL_SLOT` values in server config fall back to `0` +- Invalid `NETWORK_DIRECT_DIAL_SLOT` values in server config fall back to `1` and are logged. Invalid `NDD_SLOT`/`NETWORK_DIRECT_DIAL_SLOT` values in client `OPTIONS` are ignored and the current session value remains in use. - `DEFAULT_REFLECTOR`, `DIAL`, and `StartRef` only affect the slot-2 default. diff --git a/docs/test-harness-design.md b/docs/test-harness-design.md index de49fc3..6c85520 100644 --- a/docs/test-harness-design.md +++ b/docs/test-harness-design.md @@ -146,9 +146,10 @@ for rule timeout checks without sleeping. - Network direct dial behaviour: when `NETWORK_DIRECT_DIAL_SLOT`/`NDD_SLOT` is set to `1` or `2`, network-originated group traffic to the connected repeater/hotspot ID may activate local TG9 on that slot. Tests must verify it - is disabled by default, does not affect RF-originated traffic, does not - override active/hangtime user-selected dial-a-TG state, and uses the existing - TG9 rewrite path rather than a new packet mutation path. + defaults to TS1, can be explicitly disabled with `0`, does not affect + RF-originated traffic, does not override active/hangtime user-selected + dial-a-TG state, and uses the existing TG9 rewrite path rather than a new + packet mutation path. - Static TG configuration bugs: startup and live options reload should reject prohibited local/control TGs consistently on both TS1 and TS2 after parsing the configured TG strings to integers. Invalid IDs at or above `16777215` should diff --git a/docs/testing.md b/docs/testing.md index 12d2587..2f04a8a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -91,12 +91,13 @@ Invalid specific timer fields are logged and ignored so valid broader timer fields still apply. Voice ident override coverage verifies valid override TGs are used, empty/false overrides use all-call, and malformed, control, or all-call override values are logged and fall back to all-call. -Network direct dial coverage verifies the feature is disabled by default, -routes network-originated calls to a connected repeater/hotspot ID as RF-visible -TG9 on the configured slot, supports TS2 for DMO/simplex clients, does not -override a user-selected dial-a-TG route, can replace an idle configured default -dial route, and is not activated by RF-originated traffic. OPTIONS coverage also -verifies compact aliases and overlength `OPTIONS` rejection. +Network direct dial coverage verifies the feature defaults to TS1, can be +explicitly disabled, routes network-originated calls to a connected +repeater/hotspot ID as RF-visible TG9 on the configured slot, supports TS2 for +DMO/simplex clients, does not override a user-selected dial-a-TG route, can +replace an idle configured default dial route, and is not activated by +RF-originated traffic. OPTIONS coverage also verifies compact aliases and +overlength `OPTIONS` rejection. Generated voice prompt helper coverage verifies prompt stream state is attached to the router instance sending the prompt, even if a stale module-level `system` variable names another master. This protects dial-a-TG prompts, idents, diff --git a/docs/v1x-codex-changelog.md b/docs/v1x-codex-changelog.md index eb5bcc1..8bbe891 100644 --- a/docs/v1x-codex-changelog.md +++ b/docs/v1x-codex-changelog.md @@ -30,7 +30,7 @@ acknowledged but ignored so clients do not reconnect-loop and oversized strings do not enter session state. - Added startup validation for `NETWORK_DIRECT_DIAL_SLOT`; invalid values fall - back to disabled (`0`) and are logged. + back to the default TS1 setting (`1`) and are logged. ## Dial-a-TG @@ -47,10 +47,13 @@ - Preserved the current FreeDMR dial-a-TG policy cap of `999999`. - Ensured FBP route targets created for dial-a-TG remain active across local retunes/disconnects, in line with the mesh "everything everywhere" model. -- Added opt-in network direct dial: a network-originated group call to a +- Added network direct dial: a network-originated group call to a connected repeater/hotspot's own ID can be presented locally as TG9 on the configured slot. This is configured with `NETWORK_DIRECT_DIAL_SLOT` or `NDD_SLOT` (`0` disabled, `1` TG9 TS1, `2` TG9 TS2). +- Changed network direct dial to default to enabled on TG9 TS1. Sysops can set + `NETWORK_DIRECT_DIAL_SLOT: 0`, or clients can send `NDD_SLOT=0`, to disable it + for a session. - Network direct dial is a deliberate, narrow exception where network traffic may activate the local RF TG9 presentation path. It does not override an active or hangtime user-selected dial-a-TG route and does not apply to diff --git a/freedmr.cfg b/freedmr.cfg index 9dd9a97..67c5c90 100644 --- a/freedmr.cfg +++ b/freedmr.cfg @@ -61,7 +61,7 @@ SINGLE_MODE: True VOICE_IDENT: True DIAL_A_TG: True DYNAMIC_TG_ROUTING: True -NETWORK_DIRECT_DIAL_SLOT: 0 +NETWORK_DIRECT_DIAL_SLOT: 1 TS1_STATIC: TS2_STATIC: DEFAULT_DIAL_TS1: 0 diff --git a/tests/harness/deterministic.py b/tests/harness/deterministic.py index abce580..f7fa21a 100644 --- a/tests/harness/deterministic.py +++ b/tests/harness/deterministic.py @@ -315,7 +315,7 @@ def minimal_config(system_names: tuple[str, ...] = ("MASTER-A", "MASTER-B")) -> "VOICE_IDENT": False, "DIAL_A_TG": True, "DYNAMIC_TG_ROUTING": True, - "NETWORK_DIRECT_DIAL_SLOT": 0, + "NETWORK_DIRECT_DIAL_SLOT": 1, "TS1_STATIC": "", "TS2_STATIC": "", "DEFAULT_DIAL_TS1": 0, diff --git a/tests/harness/udp_blackbox.py b/tests/harness/udp_blackbox.py index d34ee71..6d03f37 100644 --- a/tests/harness/udp_blackbox.py +++ b/tests/harness/udp_blackbox.py @@ -469,7 +469,7 @@ def write_bridge_master_config( ts2_static: str = "91", dial_a_tg: bool = True, dynamic_tg_routing: bool = True, - network_direct_dial_slot: int = 0, + network_direct_dial_slot: int = 1, master_extra_config: str = "", ) -> None: global_use_acl_text = "True" if global_use_acl else "False" @@ -976,7 +976,7 @@ class UdpBlackBoxScenario: ts2_static: str = "91", dial_a_tg: bool = True, dynamic_tg_routing: bool = True, - network_direct_dial_slot: int = 0, + network_direct_dial_slot: int = 1, master_extra_config: str = "", fbp_systems: dict[str, int] | None = None, fbp_proto_versions: dict[str, int] | None = None, diff --git a/tests/test_deterministic_harness.py b/tests/test_deterministic_harness.py index 633aacb..d8099d9 100644 --- a/tests/test_deterministic_harness.py +++ b/tests/test_deterministic_harness.py @@ -84,7 +84,7 @@ DEFAULT_REFLECTOR: 0 self.assertEqual(parsed["ALIASES"]["STALE_TIME"], 86400) self.assertTrue(parsed["SYSTEMS"]["MASTER-A"]["DIAL_A_TG"]) self.assertTrue(parsed["SYSTEMS"]["MASTER-A"]["DYNAMIC_TG_ROUTING"]) - self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 0) + self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 1) self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS1"], 0) self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS2"], 0) @@ -142,7 +142,7 @@ DEFAULT_DIAL_TS2: 92 self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS2"], 92) self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_REFLECTOR"], 92) - def test_config_invalid_network_direct_dial_slot_defaults_to_disabled(self): + def test_config_invalid_network_direct_dial_slot_defaults_to_ts1(self): for value in ("3", "A"): with self.subTest(value=value): config_text = f""" @@ -168,7 +168,7 @@ NETWORK_DIRECT_DIAL_SLOT: {value} finally: os.unlink(path) - self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 0) + self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 1) self.assertIn("NETWORK_DIRECT_DIAL_SLOT", "\n".join(logs.output)) def test_set_alias_updates_bridge_master_globals_and_shared_hblink_config(self): @@ -1342,8 +1342,9 @@ NETWORK_DIRECT_DIAL_SLOT: {value} self.assertEqual(fbp_entry["TO_TYPE"], "NONE") self.assertEqual(scenario.capture.packets, []) - def test_network_direct_dial_is_disabled_by_default(self): + def test_network_direct_dial_can_be_explicitly_disabled(self): config = minimal_config(("MASTER-A",)) + config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"] = 0 add_openbridge_system(config, "OBP-1", network_id=3001) with DeterministicScenario(config=config) as scenario: @@ -1355,9 +1356,8 @@ NETWORK_DIRECT_DIAL_SLOT: {value} self.assertNotIn("#1001", scenario.bridge_state) self.assertEqual(scenario.capture.for_system("MASTER-A"), []) - def test_network_direct_dial_routes_repeater_id_to_tg9_on_configured_slot(self): + def test_network_direct_dial_routes_repeater_id_to_tg9_on_default_ts1(self): config = minimal_config(("MASTER-A",)) - config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"] = 1 add_openbridge_system(config, "OBP-1", network_id=3001) with DeterministicScenario(config=config) as scenario: