|
|
|
@ -84,6 +84,7 @@ DEFAULT_REFLECTOR: 0
|
|
|
|
self.assertEqual(parsed["ALIASES"]["STALE_TIME"], 86400)
|
|
|
|
self.assertEqual(parsed["ALIASES"]["STALE_TIME"], 86400)
|
|
|
|
self.assertTrue(parsed["SYSTEMS"]["MASTER-A"]["DIAL_A_TG"])
|
|
|
|
self.assertTrue(parsed["SYSTEMS"]["MASTER-A"]["DIAL_A_TG"])
|
|
|
|
self.assertTrue(parsed["SYSTEMS"]["MASTER-A"]["DYNAMIC_TG_ROUTING"])
|
|
|
|
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"]["DEFAULT_DIAL_TS1"], 0)
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS1"], 0)
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS2"], 0)
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS2"], 0)
|
|
|
|
|
|
|
|
|
|
|
|
@ -141,6 +142,35 @@ DEFAULT_DIAL_TS2: 92
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS2"], 92)
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS2"], 92)
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_REFLECTOR"], 92)
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["DEFAULT_REFLECTOR"], 92)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_config_invalid_network_direct_dial_slot_defaults_to_disabled(self):
|
|
|
|
|
|
|
|
for value in ("3", "A"):
|
|
|
|
|
|
|
|
with self.subTest(value=value):
|
|
|
|
|
|
|
|
config_text = f"""
|
|
|
|
|
|
|
|
[GLOBAL]
|
|
|
|
|
|
|
|
USE_ACL: False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[ALIASES]
|
|
|
|
|
|
|
|
TRY_DOWNLOAD: False
|
|
|
|
|
|
|
|
STALE_DAYS: 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[MASTER-A]
|
|
|
|
|
|
|
|
MODE: MASTER
|
|
|
|
|
|
|
|
ENABLED: True
|
|
|
|
|
|
|
|
NETWORK_DIRECT_DIAL_SLOT: {value}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
fd, path = tempfile.mkstemp(suffix=".cfg")
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
|
|
|
|
|
|
handle.write(config_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with self.assertLogs("config", level="WARNING") as logs:
|
|
|
|
|
|
|
|
parsed = freedmr_config.build_config(path)
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
|
|
|
os.unlink(path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(parsed["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 0)
|
|
|
|
|
|
|
|
self.assertIn("NETWORK_DIRECT_DIAL_SLOT", "\n".join(logs.output))
|
|
|
|
|
|
|
|
|
|
|
|
def test_set_alias_updates_bridge_master_globals_and_shared_hblink_config(self):
|
|
|
|
def test_set_alias_updates_bridge_master_globals_and_shared_hblink_config(self):
|
|
|
|
with DeterministicScenario() as scenario:
|
|
|
|
with DeterministicScenario() as scenario:
|
|
|
|
bm = scenario.bm
|
|
|
|
bm = scenario.bm
|
|
|
|
@ -596,6 +626,47 @@ DEFAULT_DIAL_TS2: 92
|
|
|
|
self.assertEqual(ts1_dial["TIMEOUT"], 40 * 60)
|
|
|
|
self.assertEqual(ts1_dial["TIMEOUT"], 40 * 60)
|
|
|
|
self.assertEqual(ts2_dial["TIMEOUT"], 6 * 60)
|
|
|
|
self.assertEqual(ts2_dial["TIMEOUT"], 6 * 60)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_compact_aliases_update_dial_timers_and_network_direct_dial(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["OPTIONS"] = (
|
|
|
|
|
|
|
|
"DIAL1=235;DIAL2=236;TGTO1=5;TGTO2=6;DIALTO1=7;DIALTO2=8;NDD_SLOT=2"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
scenario.bm.options_config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS1"], 235)
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS2"], 236)
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["TS1_TG_TIMER"], 5)
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["TS2_TG_TIMER"], 6)
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["TS1_DIAL_TIMER"], 7)
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["TS2_DIAL_TIMER"], 8)
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_invalid_network_direct_dial_slot_is_ignored(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"] = 1
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["OPTIONS"] = "NDD_SLOT=3;DIAL=0;TIMER=1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
with self.assertLogs(scenario.bm.logger, level="DEBUG") as logs:
|
|
|
|
|
|
|
|
scenario.bm.options_config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 1)
|
|
|
|
|
|
|
|
self.assertIn("NETWORK_DIRECT_DIAL_SLOT is not 0, 1, or 2", "\n".join(logs.output))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_overlength_string_is_ignored(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"] = 1
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["OPTIONS"] = "A" * 256
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
with self.assertLogs(scenario.bm.logger, level="WARNING") as logs:
|
|
|
|
|
|
|
|
scenario.bm.options_config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"], 1)
|
|
|
|
|
|
|
|
self.assertIn("overlength options", "\n".join(logs.output))
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_invalid_specific_timer_falls_back_without_blocking_valid_fields(self):
|
|
|
|
def test_options_invalid_specific_timer_falls_back_without_blocking_valid_fields(self):
|
|
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["OPTIONS"] = (
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["OPTIONS"] = (
|
|
|
|
@ -1271,6 +1342,116 @@ DEFAULT_DIAL_TS2: 92
|
|
|
|
self.assertEqual(fbp_entry["TO_TYPE"], "NONE")
|
|
|
|
self.assertEqual(fbp_entry["TO_TYPE"], "NONE")
|
|
|
|
self.assertEqual(scenario.capture.packets, [])
|
|
|
|
self.assertEqual(scenario.capture.packets, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_network_direct_dial_is_disabled_by_default(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A",))
|
|
|
|
|
|
|
|
add_openbridge_system(config, "OBP-1", network_id=3001)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
scenario.register_peer("MASTER-A", 1001)
|
|
|
|
|
|
|
|
scenario.clock.advance(6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scenario.inject_obp("OBP-1", PacketSpec(dst_id=1001, slot=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
scenario.register_peer("MASTER-A", 1001)
|
|
|
|
|
|
|
|
scenario.clock.advance(6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scenario.inject_obp("OBP-1", PacketSpec(dst_id=1001, slot=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
captured = scenario.capture.for_system("MASTER-A")
|
|
|
|
|
|
|
|
self.assertEqual(len(captured), 1)
|
|
|
|
|
|
|
|
self.assertEqual(captured[0].fields["dst_id"], bytes_3(9))
|
|
|
|
|
|
|
|
self.assertEqual(captured[0].fields["slot"], 1)
|
|
|
|
|
|
|
|
entry = next(
|
|
|
|
|
|
|
|
entry for entry in scenario.bridge_state["#1001"]
|
|
|
|
|
|
|
|
if entry["SYSTEM"] == "MASTER-A" and entry["TS"] == 1
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assertTrue(entry["ACTIVE"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_network_direct_dial_can_route_to_slot2_for_dmo_clients(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A",))
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"] = 2
|
|
|
|
|
|
|
|
add_openbridge_system(config, "OBP-1", network_id=3001)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
scenario.register_peer("MASTER-A", 1001)
|
|
|
|
|
|
|
|
scenario.clock.advance(6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scenario.inject_obp("OBP-1", PacketSpec(dst_id=1001, slot=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
captured = scenario.capture.for_system("MASTER-A")
|
|
|
|
|
|
|
|
self.assertEqual(len(captured), 1)
|
|
|
|
|
|
|
|
self.assertEqual(captured[0].fields["dst_id"], bytes_3(9))
|
|
|
|
|
|
|
|
self.assertEqual(captured[0].fields["slot"], 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_network_direct_dial_does_not_override_user_selected_dial_tg(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:
|
|
|
|
|
|
|
|
scenario.register_peer("MASTER-A", 1001)
|
|
|
|
|
|
|
|
scenario.bm.make_single_reflector(bytes_3(235), 1, "MASTER-A", 1)
|
|
|
|
|
|
|
|
scenario.clock.advance(6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scenario.inject_obp("OBP-1", PacketSpec(dst_id=1001, slot=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.assertNotIn("#1001", scenario.bridge_state)
|
|
|
|
|
|
|
|
source_entry = next(
|
|
|
|
|
|
|
|
entry for entry in scenario.bridge_state["#235"]
|
|
|
|
|
|
|
|
if entry["SYSTEM"] == "MASTER-A" and entry["TS"] == 1
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assertTrue(source_entry["ACTIVE"])
|
|
|
|
|
|
|
|
self.assertEqual(scenario.capture.for_system("MASTER-A"), [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_network_direct_dial_can_override_idle_default_dial_tg(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A",))
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"] = 1
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["DEFAULT_DIAL_TS1"] = 235
|
|
|
|
|
|
|
|
add_openbridge_system(config, "OBP-1", network_id=3001)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
scenario.register_peer("MASTER-A", 1001)
|
|
|
|
|
|
|
|
scenario.bm.make_default_reflector(235, 1, "MASTER-A", 1)
|
|
|
|
|
|
|
|
scenario.clock.advance(6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scenario.inject_obp("OBP-1", PacketSpec(dst_id=1001, slot=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
captured = scenario.capture.for_system("MASTER-A")
|
|
|
|
|
|
|
|
self.assertEqual(len(captured), 1)
|
|
|
|
|
|
|
|
self.assertEqual(captured[0].fields["dst_id"], bytes_3(9))
|
|
|
|
|
|
|
|
self.assertEqual(captured[0].fields["slot"], 1)
|
|
|
|
|
|
|
|
default_entry = next(
|
|
|
|
|
|
|
|
entry for entry in scenario.bridge_state["#235"]
|
|
|
|
|
|
|
|
if entry["SYSTEM"] == "MASTER-A" and entry["TS"] == 1
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
direct_entry = next(
|
|
|
|
|
|
|
|
entry for entry in scenario.bridge_state["#1001"]
|
|
|
|
|
|
|
|
if entry["SYSTEM"] == "MASTER-A" and entry["TS"] == 1
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assertFalse(default_entry["ACTIVE"])
|
|
|
|
|
|
|
|
self.assertTrue(direct_entry["ACTIVE"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rf_traffic_to_own_repeater_id_does_not_activate_network_direct_dial(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A",))
|
|
|
|
|
|
|
|
config["SYSTEMS"]["MASTER-A"]["NETWORK_DIRECT_DIAL_SLOT"] = 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
scenario.register_peer("MASTER-A", 1001)
|
|
|
|
|
|
|
|
scenario.clock.advance(6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scenario.inject_hbp("MASTER-A", PacketSpec(dst_id=1001, slot=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.assertNotIn("#1001", scenario.bridge_state)
|
|
|
|
|
|
|
|
|
|
|
|
def test_dial_a_tg_bcsq_uses_reflector_tg_for_fbp_target(self):
|
|
|
|
def test_dial_a_tg_bcsq_uses_reflector_tg_for_fbp_target(self):
|
|
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
|
|
config = minimal_config(("MASTER-A", "MASTER-B"))
|
|
|
|
add_openbridge_system(config, "OBP-1", network_id=3001)
|
|
|
|
add_openbridge_system(config, "OBP-1", network_id=3001)
|
|
|
|
@ -2104,6 +2285,23 @@ DEFAULT_DIAL_TS2: 92
|
|
|
|
self.assertIn("DMRD packet too short", "\n".join(logs.output))
|
|
|
|
self.assertIn("DMRD packet too short", "\n".join(logs.output))
|
|
|
|
self.assertEqual(scenario.capture.for_system("MASTER-A"), [])
|
|
|
|
self.assertEqual(scenario.capture.for_system("MASTER-A"), [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hbp_master_parser_acks_but_ignores_overlength_options(self):
|
|
|
|
|
|
|
|
config = minimal_config(("MASTER-A",))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with DeterministicScenario(config=config) as scenario:
|
|
|
|
|
|
|
|
sockaddr = ("127.0.0.1", 50000)
|
|
|
|
|
|
|
|
scenario.register_peer("MASTER-A", peer_id=1001, sockaddr=sockaddr)
|
|
|
|
|
|
|
|
packet = b"RPTO" + bytes_4(1001) + (b"A" * 256)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with self.assertLogs("hblink", level="WARNING") as logs:
|
|
|
|
|
|
|
|
scenario.inject_datagram("MASTER-A", packet, sockaddr=sockaddr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ack, ack_sockaddr = scenario.transports["MASTER-A"].writes[-1]
|
|
|
|
|
|
|
|
self.assertEqual(ack, b"RPTACK" + bytes_4(1001))
|
|
|
|
|
|
|
|
self.assertEqual(ack_sockaddr, sockaddr)
|
|
|
|
|
|
|
|
self.assertNotIn("OPTIONS", config["SYSTEMS"]["MASTER-A"])
|
|
|
|
|
|
|
|
self.assertIn("overlength options", "\n".join(logs.output))
|
|
|
|
|
|
|
|
|
|
|
|
def test_obp_bridge_control_bcst_sets_global_stun_flag(self):
|
|
|
|
def test_obp_bridge_control_bcst_sets_global_stun_flag(self):
|
|
|
|
config = minimal_config(())
|
|
|
|
config = minimal_config(())
|
|
|
|
add_openbridge_system(config, "OBP-1", network_id=3001)
|
|
|
|
add_openbridge_system(config, "OBP-1", network_id=3001)
|
|
|
|
|