"""Маршрутизация эфира: кому пересылать принятый кадр. Сервер намеренно простой — несколько хотспотов во внутренней сети: • групповой вызов на разрешённую группу → всем остальным подключённым хотспотам; • приватный вызов → только тому хотспоту, где абонента слышали последним; • вызов на группу, которой нет в списке → никуда (в панели будет видно отказ). Здесь же переписывается peer_id кадра под каждого получателя — см. retarget(). """ from __future__ import annotations import time from .analyzer import CALL_GROUP from .log import get_logger log = get_logger(__name__) RADIO_LOC_TTL = 15 * 60 # сколько помним, через какой хотспот слышали абонента def retarget(raw: bytes, peer_id: int) -> bytes: """Подставить в кадр номер ПОЛУЧАТЕЛЯ (байты 11-14). Смысл поля — «ретранслятор, которого касается этот кадр на этом соединении», поэтому принимающий хотспот должен увидеть там свой номер. Так делают HBlink3 (`send_peer`) и BrandMeister. MMDVMHost поле на приёме не проверяет, но DMRGateway на многих Pi-Star стоит между ним и сетью, так что делаем как эталон. """ if len(raw) < 15: return raw return raw[:11] + peer_id.to_bytes(4, "big") + raw[15:] class Router: def __init__(self, cfg: dict, state) -> None: self.cfg = cfg self.state = state self._radio_loc: dict[int, tuple[int, float]] = {} # dmr_id -> (peer_id, когда) # ── группы из конфига ─────────────────────────────────────────────────── def allowed(self, tg: int, slot: int) -> bool: for g in self.cfg.get("groups", []): if int(g["tg"]) == tg and int(g.get("slot", slot)) == slot: return True return False def group_names(self) -> dict[int, str]: return {int(g["tg"]): g.get("name", "") for g in self.cfg.get("groups", [])} # ── где слышали абонента (для приватных вызовов) ──────────────────────── def set_radio_loc(self, dmr_id: int, peer_id: int) -> None: self._radio_loc[dmr_id] = (peer_id, time.time()) def clear_radio_loc(self, dmr_id: int, peer_id: int) -> None: cur = self._radio_loc.get(dmr_id) if cur and cur[0] == peer_id: del self._radio_loc[dmr_id] def locate(self, dmr_id: int) -> int | None: cur = self._radio_loc.get(dmr_id) if cur is None: return None peer_id, when = cur if time.time() - when > RADIO_LOC_TTL: del self._radio_loc[dmr_id] return None return peer_id def radio_locs(self) -> dict[int, int]: return {rid: pid for rid, (pid, _) in self._radio_loc.items()} def active_calls(self) -> list: return [] # состояние вызовов ведёт State; здесь пусто для совместимости # ── собственно маршрут ────────────────────────────────────────────────── def route(self, peer_id: int, dmr, connected: list[int]) -> list[int]: # запоминаем, через какой хотспот слышали передающего if dmr.src: self.set_radio_loc(dmr.src, peer_id) parrot_tg = int(self.cfg.get("parrot", {}).get("tg", 9990)) if dmr.dst == parrot_tg: return [] # попугай отвечает сам, напрямую вызывавшему if dmr.call_type == CALL_GROUP: if not self.allowed(dmr.dst, dmr.slot): return [] # всем, кроме источника: возврат кадра отправителю вызывает петлю в эфире return [pid for pid in connected if pid != peer_id] # Приватный вызов — туда, где абонента слышали последним. Если не слышали # никогда (рация ещё не выходила в эфир) — рассылаем всем, иначе вызов молча # уходит в никуда. Так же поступает HBlink4 («user cache with broadcast # fallback»). Лишний трафик тут копеечный: хотспотов единицы. target = self.locate(dmr.dst) if target is None: log.debug("приват %s→%s: абонент не известен, рассылаю всем", dmr.src, dmr.dst) return [pid for pid in connected if pid != peer_id] if target == peer_id: return [] # абонент на том же хотспоте — эфир и так его слышит return [target] if target in connected else []