feat(talker-alias): embed TA on REPEAT and fix both-mode VHEAD inject

Overlay Talker Alias in repeated DMRD bursts so WPSD/MMDVMHost can display
aliases on RX, buffer TA from OBP voice, and inject the template at VHEAD
when the source sends no TA.
pull/1/head
Rodrigo Pérez 4 months ago
parent 663e030ef9
commit 6eec98400f

@ -41,7 +41,7 @@ Under **`GLOBAL`** (optional per-system override with the same keys):
GLOBAL:
TALKER_ALIAS: false
TALKER_ALIAS_MODE: both
TALKER_ALIAS_FORMAT: "{callsign} {fname}"
TALKER_ALIAS_FORMAT: "TA {callsign} {fname}"
TALKER_ALIAS_TEXT_FORMAT: "utf8,iso8"
```

@ -10,6 +10,7 @@ from typing import Any
from bitarray import bitarray
from ...domain.dmr.const import LC_OPT
from ...domain.talker_alias import DMRA_BLOCK_COUNT
from ...domain import int_id
from ..talker_alias_use_cases import passthrough_complete, talker_alias_settings
@ -29,13 +30,12 @@ class BridgeLcTaMixin:
def _both_ta_key(self, source_system: str, stream_id: bytes) -> tuple[str, bytes]:
return (source_system, stream_id)
def _source_cannot_carry_ta(self, source_system: str) -> bool:
"""OpenBridge carries no Talker Alias (no DMRA UDP nor embedded LC TA).
In `both` mode there is nothing to wait for, so inject the template right away
instead of deferring (OBP streams are often short and would expire first).
"""
return self._config.get("SYSTEMS", {}).get(source_system, {}).get("MODE") == "OPENBRIDGE"
def _ta_capable_source(self, source_system: str) -> bool:
"""True when the source leg can buffer TA (MASTER DMRA/voice or OBP voice)."""
return self._config.get("SYSTEMS", {}).get(source_system, {}).get("MODE") in (
"MASTER",
"OPENBRIDGE",
)
def _cancel_both_ta_wait(self, source_system: str, stream_id: bytes) -> None:
wait = self._both_ta_wait.pop(self._both_ta_key(source_system, stream_id), None)
@ -107,14 +107,17 @@ class BridgeLcTaMixin:
for st in status.values():
if not isinstance(st, dict):
continue
if st.get("TX_STREAM_ID") != stream_id or st.get("TX_RFS") != rf_src:
continue
if st.get("TX_TA_ON"):
continue
target = st.get("_ta_target_system", source_system)
self._init_talker_alias_embed(
st, source_system, target, rf_src, stream_id, force_inject=force_inject,
)
if st.get("TX_STREAM_ID") == stream_id and st.get("TX_RFS") == rf_src:
target = st.get("_ta_target_system", source_system)
self._init_talker_alias_embed(
st, source_system, target, rf_src, stream_id, force_inject=force_inject,
)
elif st.get("REP_STREAM_ID") == stream_id:
self._init_talker_alias_embed(
st, source_system, source_system, rf_src, stream_id, force_inject=force_inject,
)
def on_dmra_fragment_stored(
self,
@ -136,17 +139,48 @@ class BridgeLcTaMixin:
return
key = self._both_ta_key(source_system, stream_id)
wait = self._both_ta_wait.get(key)
if not wait:
self._apply_both_ta_embed(source_system, rf_src, stream_id)
return
wait["peer"] = peer_id
self._cancel_both_ta_wait(source_system, stream_id)
for target_system in wait["targets"]:
if self._talker_alias.should_send_on_vhead(target_system, stream_id):
if wait:
wait["peer"] = peer_id
self._cancel_both_ta_wait(source_system, stream_id)
for target_system in wait["targets"]:
self._send_talker_alias_to_target(
source_system, target_system, rf_src, stream_id, peer_id, force=True,
)
self._apply_both_ta_embed(source_system, rf_src, stream_id)
self._relay_passthrough_dmra(source_system, peer_id, rf_src, stream_id)
def _relay_passthrough_dmra(
self,
source_system: str,
peer_id: bytes,
rf_src: bytes,
stream_id: bytes,
) -> None:
"""After the source TA buffer is complete, relay DMRA to bridge/repeat targets."""
blocks = self._get_stream_dmra_blocks(source_system, stream_id)
if not blocks or not passthrough_complete(blocks):
return
if self._get_protocols:
for proto in self._get_protocols().values():
status = getattr(proto, "STATUS", None)
if not isinstance(status, dict):
continue
for st in status.values():
if not isinstance(st, dict):
continue
if st.get("TX_STREAM_ID") != stream_id or st.get("TX_RFS") != rf_src:
continue
target = st.get("_ta_target_system")
if not isinstance(target, str) or not target:
continue
tx_peer = st.get("TX_PEER", peer_id)
self._send_talker_alias_to_target(
source_system, target, rf_src, stream_id, tx_peer, force=True,
)
src_cfg = self._config.get("SYSTEMS", {}).get(source_system, {})
if src_cfg.get("MODE") == "MASTER" and src_cfg.get("REPEAT", True):
self._talker_alias.clear_stream(source_system, stream_id)
self.send_talker_alias_local_repeat(source_system, peer_id, rf_src, stream_id)
def _send_talker_alias_to_target(
self,
@ -165,23 +199,15 @@ class BridgeLcTaMixin:
tgt_mode = self._config.get("SYSTEMS", {}).get(target_system, {}).get("MODE")
if tgt_mode not in ("MASTER", "PEER"):
return
if not self._talker_alias.should_send_on_vhead(target_system, stream_id):
blocks = self._get_stream_dmra_blocks(source_system, stream_id)
have_passthrough = bool(blocks and passthrough_complete(blocks))
if have_passthrough and force:
self._talker_alias.clear_stream(target_system, stream_id)
elif not self._talker_alias.should_send_on_vhead(target_system, stream_id):
return
if (
not force
and talker_alias_settings(self._config, source_system)["mode"] == "both"
and not (self._get_stream_dmra_blocks(source_system, stream_id) and passthrough_complete(
self._get_stream_dmra_blocks(source_system, stream_id) or {}
))
):
if self._source_cannot_carry_ta(source_system):
# OBP source can never supply TA: inject the template immediately.
fallback_inject = True
else:
self._register_both_ta_wait(
source_system, target_system, rf_src, stream_id, source_peer,
)
return
if not have_passthrough:
# Legacy resolve_ta (both): inject at VHEAD when the buffer is still empty.
fallback_inject = True
packets = self._talker_alias.packets_for_stream(
source_system,
rf_src,
@ -223,6 +249,65 @@ class BridgeLcTaMixin:
system_name, system_name, rf_src, stream_id, source_peer,
)
def prepare_talker_alias_local_repeat(
self,
system_name: str,
source_peer: bytes,
rf_src: bytes,
dst_id: bytes,
slot: int,
stream_id: bytes,
) -> None:
"""REPEAT on VHEAD: standalone DMRA plus embedded TA state for downlink DMRD.
WPSD/MMDVMHost ignores standalone DMRA UDP; it only displays Talker Alias decoded
from embedded LC inside repeated voice bursts (B–E).
"""
settings = talker_alias_settings(self._config, system_name)
if settings["enabled"] and self._get_protocols:
proto = self._get_protocols().get(system_name)
status = getattr(proto, "STATUS", None) if proto else None
if isinstance(status, dict) and slot in status:
st = status[slot]
if not isinstance(st, dict):
st = {}
status[slot] = st
if st.get("REP_STREAM_ID") != stream_id:
dst_lc = LC_OPT + dst_id + rf_src
st["REP_STREAM_ID"] = stream_id
st["REP_EMB_LC"] = self._encode_emblc(dst_lc)
self._init_talker_alias_embed(
st, system_name, system_name, rf_src, stream_id,
)
self.send_talker_alias_local_repeat(system_name, source_peer, rf_src, stream_id)
def rewrite_repeat_voice_burst(
self,
system_name: str,
slot: int,
stream_id: bytes,
dtype_vseq: int,
dmrpkt: bytes,
) -> bytes:
"""Overlay Talker Alias on embedded LC for REPEAT copies (voice bursts B–E)."""
if dtype_vseq not in (1, 2, 3, 4) or len(dmrpkt) < 33:
return dmrpkt
if not self._get_protocols:
return dmrpkt
proto = self._get_protocols().get(system_name)
status = getattr(proto, "STATUS", None) if proto else None
if not isinstance(status, dict):
return dmrpkt
st = status.get(slot)
if not isinstance(st, dict) or st.get("REP_STREAM_ID") != stream_id:
return dmrpkt
if "REP_EMB_LC" not in st or st.get("TX_TA_EMB") is None:
return dmrpkt
dmrbits = bitarray(endian="big")
dmrbits.frombytes(dmrpkt)
self._rewrite_embed_lc(dmrbits, st, dtype_vseq, "REP_EMB_LC")
return dmrbits.tobytes()
def clear_talker_alias_stream(self, system_name: str, stream_id: bytes) -> None:
"""Release per-stream TA dedupe state after VTERM."""
self._cancel_both_ta_wait(system_name, stream_id)
@ -239,7 +324,13 @@ class BridgeLcTaMixin:
return
for slot in (1, 2):
st = status.get(slot)
if isinstance(st, dict) and st.get("TX_STREAM_ID") == stream_id:
if not isinstance(st, dict):
continue
if st.get("TX_STREAM_ID") == stream_id:
self._clear_talker_alias_embed(st)
if st.get("REP_STREAM_ID") == stream_id:
st.pop("REP_STREAM_ID", None)
st.pop("REP_EMB_LC", None)
self._clear_talker_alias_embed(st)
def _init_talker_alias_embed(
@ -267,12 +358,7 @@ class BridgeLcTaMixin:
if not settings["enabled"]:
return
target_mode = self._config.get("SYSTEMS", {}).get(target_system, {}).get("MODE")
if (
settings["mode"] == "both"
and self._source_cannot_carry_ta(source_system)
and target_mode in ("MASTER", "PEER")
):
# OBP source can never supply TA: inject the template immediately.
if settings["mode"] == "both" and not self._ta_capable_source(source_system):
force_inject = True
st.pop("TX_TA_EMB", None)
st.pop("TX_TA_PHASE", None)

@ -190,18 +190,14 @@ class TalkerAliasUseCases:
source_system, text, via, target, int_id(stream_id),
)
return passthrough_packets_from_blocks(rf_src, blocks)
if fallback_inject:
text = format_talker_alias_text(self._config, rf_src)
logger.debug(
"(%s) *TALKER ALIAS* inject '%s' (no source TA) via %s -> %s stream %s",
source_system, text, via, target, int_id(stream_id),
)
return build_dmra_packets(rf_src, text, settings["text_formats"][0])
# Legacy resolve_ta (both): inject template when no DMRA buffer yet (VHEAD).
text = format_talker_alias_text(self._config, rf_src)
suffix = " (no source TA yet)" if fallback_inject else ""
logger.debug(
"(%s) *TALKER ALIAS* passthrough (source embedded TA) via %s -> %s stream %s",
source_system, via, target, int_id(stream_id),
"(%s) *TALKER ALIAS* inject '%s'%s via %s -> %s stream %s",
source_system, text, suffix, via, target, int_id(stream_id),
)
return None
return build_dmra_packets(rf_src, text, settings["text_formats"][0])
def embedded_emblc_for_stream(
self,
@ -244,8 +240,8 @@ class TalkerAliasUseCases:
if mode in ("passthrough", "both") and blocks and passthrough_complete(blocks):
_log(decode_ta_from_blocks(blocks), " (source TA)")
return self._ta_emblc.encode_blocks(blocks)
if mode == "inject" or fallback_inject:
suffix = "" if mode == "inject" else " (no source TA)"
if mode in ("inject", "both") or fallback_inject:
suffix = "" if mode == "inject" else " (no source TA yet)"
text = format_talker_alias_text(self._config, rf_src)
_log(text, suffix)
return self._ta_emblc.encode_text(text, text_formats=settings["text_formats"])

@ -161,6 +161,8 @@ class HBPProtocol(DatagramProtocol):
on_deactivate_dynamic_bridges: Callable[[str], None] | None = None,
on_obp_bcsq_received: Callable[[str, bytes, bytes], None] | None = None,
on_talker_alias_local_repeat: Callable[[str, bytes, bytes, bytes], None] | None = None,
on_talker_alias_repeat_prepare: Callable[[str, bytes, bytes, bytes, int, bytes], None] | None = None,
on_talker_alias_repeat_burst: Callable[[str, int, bytes, int, bytes], bytes] | None = None,
on_talker_alias_stream_end: Callable[[str, bytes], None] | None = None,
on_dmra_fragment_stored: Callable[[str, bytes, bytes, bytes], None] | None = None,
) -> None:
@ -177,6 +179,8 @@ class HBPProtocol(DatagramProtocol):
self._on_deactivate_dynamic_bridges = on_deactivate_dynamic_bridges
self._on_obp_bcsq_received = on_obp_bcsq_received
self._on_talker_alias_local_repeat = on_talker_alias_local_repeat
self._on_talker_alias_repeat_prepare = on_talker_alias_repeat_prepare
self._on_talker_alias_repeat_burst = on_talker_alias_repeat_burst
self._on_talker_alias_stream_end = on_talker_alias_stream_end
self._on_dmra_fragment_stored = on_dmra_fragment_stored
self._config = config.get("SYSTEMS", {}).get(system_name, {})
@ -193,12 +197,14 @@ class HBPProtocol(DatagramProtocol):
self.STATUS = {1: _make_slot_status(), 2: _make_slot_status()}
if self._config.get("MODE") == "MASTER":
self._peers = self._config.setdefault("PEERS", {})
else:
self._peers = {}
if self._config.get("MODE") in ("MASTER", "OPENBRIDGE"):
self._dmra_by_stream: dict[bytes, dict[str, Any]] = {}
self._dmra_rf_stream: dict[tuple[bytes, bytes], bytes] = {}
self._ta_voice_acc: dict[bytes, dict[int, Any]] = {}
self._ta_decoded_logged: set[bytes] = set()
else:
self._peers = {}
self._dmra_by_stream = {}
self._dmra_rf_stream = {}
self._ta_voice_acc = {}
@ -260,9 +266,12 @@ class HBPProtocol(DatagramProtocol):
_packet = b"".join([_packet[:11], _peer, _packet[15:]])
self.transport.write(_packet, self._peers[_peer]["SOCKADDR"])
def _ta_buffer_enabled(self) -> bool:
return self._config.get("MODE") in ("MASTER", "OPENBRIDGE")
def note_dmrd_stream(self, peer_id: bytes, rf_src: bytes, stream_id: bytes) -> None:
"""Associate active stream with source for DMRA pass-through buffering."""
if self._config.get("MODE") != "MASTER" or not stream_id:
if not self._ta_buffer_enabled() or not stream_id:
return
self._dmra_rf_stream[(peer_id, rf_src)] = stream_id
@ -296,7 +305,7 @@ class HBPProtocol(DatagramProtocol):
dmrpkt: bytes,
) -> None:
"""Buffer TA from embedded LC in voice bursts B–E (MMDVM DMRSlot path)."""
if self._config.get("MODE") != "MASTER" or not stream_id:
if not self._ta_buffer_enabled() or not stream_id:
return
if not self._CONFIG.get("GLOBAL", {}).get("TALKER_ALIAS", False):
return
@ -347,7 +356,7 @@ class HBPProtocol(DatagramProtocol):
def trim_dmra_streams(self, max_age: float = 180.0) -> None:
"""Drop stale DMRA buffers (same order of magnitude as stream trimmer)."""
if self._config.get("MODE") != "MASTER":
if not self._ta_buffer_enabled():
return
now = time.time()
cutoff = now - max_age
@ -667,22 +676,37 @@ class HBPProtocol(DatagramProtocol):
self.store_ta_from_voice_burst(
_peer_id, _rf_src, _stream_id, _dtype_vseq, _data[20:53],
)
if self._config.get("REPEAT", True):
pkt = [_data[:11], b"", _data[15:]]
for _peer in self._peers:
if _peer != _peer_id:
pkt[1] = _peer
self.transport.write(b"".join(pkt), self._peers[_peer]["SOCKADDR"])
if (
self._config.get("REPEAT", True)
and _call_type in ("group", "vcsbk")
and _frame_type == HBPF_DATA_SYNC
and _dtype_vseq == HBPF_SLT_VHEAD
and self._on_talker_alias_local_repeat
):
self._on_talker_alias_local_repeat(
self._system, _peer_id, _rf_src, _stream_id,
)
if self._on_talker_alias_repeat_prepare:
self._on_talker_alias_repeat_prepare(
self._system, _peer_id, _rf_src, _dst_id, _slot, _stream_id,
)
elif self._on_talker_alias_local_repeat:
self._on_talker_alias_local_repeat(
self._system, _peer_id, _rf_src, _stream_id,
)
if self._config.get("REPEAT", True):
_repeat_tail = _data[15:]
if (
_call_type in ("group", "vcsbk")
and _dtype_vseq in (1, 2, 3, 4)
and len(_data) >= 53
and self._on_talker_alias_repeat_burst
):
_dmrpkt_out = self._on_talker_alias_repeat_burst(
self._system, _slot, _stream_id, _dtype_vseq, _data[20:53],
)
_repeat_tail = b"".join([_data[15:20], _dmrpkt_out, _data[53:]])
pkt = [_data[:11], b"", _repeat_tail]
for _peer in self._peers:
if _peer != _peer_id:
pkt[1] = _peer
self.transport.write(b"".join(pkt), self._peers[_peer]["SOCKADDR"])
# TG 4000: deactivate after REPEAT so peers see the packet (legacy order)
if _int_dst_id == 4000 and self._on_deactivate_dynamic_bridges:
_kind = "Private call to ID" if _call_type == "unit" else "Group call to TG"
@ -1369,6 +1393,16 @@ class HBPProtocol(DatagramProtocol):
"(%s) CALL RX (OBP) src %s -> TG %s slot %s",
self._system, int_id(_rf_src), int_id(_dst_id), _slot,
)
self.note_dmrd_stream(_peer_id, _rf_src, _stream_id)
if (
_call_type in ("group", "vcsbk")
and _frame_type != HBPF_DATA_SYNC
and _dtype_vseq in (1, 2, 3, 4)
and len(_data) >= 53
):
self.store_ta_from_voice_burst(
_peer_id, _rf_src, _stream_id, _dtype_vseq, _data[20:53],
)
# Group/vcsbk stream state, LC, duplicates: bridge_use_cases._obp_group_voice_router_obp (legacy routerOBP.dmrd_received)
if self._dmrd_received:
# Legacy hblink DMRD v1: SERVER_ID + default rptr/hops/ber/rssi (`hblink.py` ~338–345, ~416)
@ -1550,6 +1584,16 @@ class HBPProtocol(DatagramProtocol):
return
if _call_type == "group" and _frame_type == HBPF_DATA_SYNC and _dtype_vseq == HBPF_SLT_VHEAD:
logger.info("(%s) CALL RX (OBP DMRE) src %s -> TG %s slot %s", self._system, int_id(_rf_src), int_id(_dst_id), _slot)
self.note_dmrd_stream(_peer_id, _rf_src, _stream_id)
if (
_call_type in ("group", "vcsbk")
and _frame_type != HBPF_DATA_SYNC
and _dtype_vseq in (1, 2, 3, 4)
and len(_data) >= 53
):
self.store_ta_from_voice_burst(
_peer_id, _rf_src, _stream_id, _dtype_vseq, _data[20:53],
)
_data_dmrd = DMRD + _data[4:]
_hops_out = _inthops.to_bytes(1, "big")
if self._dmrd_received:
@ -1662,6 +1706,8 @@ def HBPProtocolFactory(
on_deactivate_dynamic_bridges: Callable[[str], None] | None = None,
on_obp_bcsq_received: Callable[[str, bytes, bytes], None] | None = None,
on_talker_alias_local_repeat: Callable[[str, bytes, bytes, bytes], None] | None = None,
on_talker_alias_repeat_prepare: Callable[[str, bytes, bytes, bytes, int, bytes], None] | None = None,
on_talker_alias_repeat_burst: Callable[[str, int, bytes, int, bytes], bytes] | None = None,
on_talker_alias_stream_end: Callable[[str, bytes], None] | None = None,
on_dmra_fragment_stored: Callable[[str, bytes, bytes, bytes], None] | None = None,
) -> HBPProtocol:
@ -1680,6 +1726,8 @@ def HBPProtocolFactory(
on_deactivate_dynamic_bridges=on_deactivate_dynamic_bridges,
on_obp_bcsq_received=on_obp_bcsq_received,
on_talker_alias_local_repeat=on_talker_alias_local_repeat,
on_talker_alias_repeat_prepare=on_talker_alias_repeat_prepare,
on_talker_alias_repeat_burst=on_talker_alias_repeat_burst,
on_talker_alias_stream_end=on_talker_alias_stream_end,
on_dmra_fragment_stored=on_dmra_fragment_stored,
)

@ -84,7 +84,7 @@ def test_both_mode_without_ta_still_rewrites_group_embedded_lc() -> None:
)
ts_st = scenario.protocols["MASTER-B"].STATUS[2]
assert ts_st.get("TX_TA_EMB") is None # no source TA buffered, no inject
assert ts_st.get("TX_TA_EMB") is not None # both mode injects template at VHEAD
bursts = [p for p in scenario.capture.for_system("MASTER-B") if p.fields["dtype_vseq"] == 1]
assert bursts, "voice burst B should be forwarded to MASTER-B"
bits = bitarray(endian="big")
@ -107,3 +107,35 @@ def test_talker_alias_local_repeat_excludes_source_peer() -> None:
assert len(scenario.dmra_capture) == 1
assert scenario.dmra_capture[0].exclude_peer == peer
def test_repeat_embed_prepares_slot_state_for_mmdvm() -> None:
"""REPEAT must set TX_TA_EMB on the source MASTER slot for embedded downlink DMRD."""
from bitarray import bitarray
scenario = DeterministicScenario(
config=talker_alias_config(),
bridges=active_bridge(7304, (("MASTER-A", 2),)),
)
stream_id = bytes_4(0xA1A2A3A4)
peer = bytes_4(1001)
rf_src = bytes_3(7300392)
dst_id = bytes_3(7304)
scenario.bridge.prepare_talker_alias_local_repeat(
"MASTER-A", peer, rf_src, dst_id, 2, stream_id,
)
slot_st = scenario.protocols["MASTER-A"].STATUS[2]
assert slot_st.get("REP_STREAM_ID") == stream_id
assert "REP_EMB_LC" in slot_st
assert slot_st.get("TX_TA_EMB") is not None
raw = b"\x00" * 33
out = scenario.bridge.rewrite_repeat_voice_burst("MASTER-A", 2, stream_id, 1, raw)
bits = bitarray(endian="big")
bits.frombytes(out)
assert bits[116:148] == slot_st["REP_EMB_LC"][1]
scenario.bridge.clear_talker_alias_stream("MASTER-A", stream_id)
assert "REP_STREAM_ID" not in slot_st
assert "TX_TA_EMB" not in slot_st

@ -76,12 +76,12 @@ def test_embedded_emblc_both_source_ta_then_fallback() -> None:
"MASTER-A", bytes_3(9999999), bytes_4(0xD4D4D4D4), lambda _s, _st: blocks,
)
assert source is not None
# No source TA yet, no fallback -> None (group LC only).
# No source TA yet -> legacy both injects the template embedded LC at VHEAD.
assert (
ta.embedded_emblc_for_stream(
"MASTER-A", bytes_3(9999999), bytes_4(0xD4D4D4D5), lambda _s, _st: None,
)
is None
is not None
)
# No source TA, fallback -> inject the template embedded LC.
fallback = ta.embedded_emblc_for_stream(

@ -111,22 +111,21 @@ def test_packets_for_stream_both_prefers_passthrough() -> None:
assert packets == expected
def test_packets_for_stream_both_without_blocks_preserves_source() -> None:
"""both + no DMRA buffer (still waiting): do not inject; source embedded TA may arrive."""
def test_packets_for_stream_both_without_blocks_injects_like_legacy() -> None:
"""both + empty buffer at VHEAD: legacy resolve_ta injects the template immediately."""
config = talker_alias_config()
config["GLOBAL"]["TALKER_ALIAS_MODE"] = "both"
ta = make_talker_alias_use_cases(config)
assert (
ta.packets_for_stream(
"MASTER-A",
bytes_3(3120001),
bytes_4(0xD4D4D4D4),
lambda _s, _st: None,
target_system="MASTER-B",
)
is None
packets = ta.packets_for_stream(
"MASTER-A",
bytes_3(3120001),
bytes_4(0xD4D4D4D4),
lambda _s, _st: None,
target_system="MASTER-B",
)
assert packets is not None
assert all(p[:4] == DMRA_OPCODE for p in packets)
def test_packets_for_stream_both_fallback_inject_when_no_source_ta() -> None:
@ -156,7 +155,7 @@ def test_embedded_emblc_both_fallback_inject_returns_template() -> None:
config["GLOBAL"]["TALKER_ALIAS_MODE"] = "both"
ta = make_talker_alias_use_cases(config)
# Without fallback the embedded LC is preserved (None); with fallback it injects.
# Legacy both injects the template embedded LC even without fallback_inject.
assert (
ta.embedded_emblc_for_stream(
"MASTER-A",
@ -165,7 +164,7 @@ def test_embedded_emblc_both_fallback_inject_returns_template() -> None:
lambda _s, _st: None,
target_system="MASTER-B",
)
is None
is not None
)
emblcs = ta.embedded_emblc_for_stream(
"MASTER-A",

Loading…
Cancel
Save

Powered by TurnKey Linux.