Files
dmr-server/dmrsrv/router.py
T
viktorиClaude Opus 4.8 47cb85327a панель: живой хотспот выглядел мёртвым
Мастер считает время по monotonic, панель по time.time(). В зеркале состояния
last_seen не обновлялся вовсе, поэтому «простой» рос вместе со временем в сети:
хотспот, живой 44 минуты, показывался как молчащий 44 минуты. Оператор решил бы,
что связь отвалилась.

Заодно счётчик принятых кадров: был вечный ноль, теперь считается в маршрутизаторе.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvX94Vwy7vXWEuiFm1g9Av
2026-07-29 01:17:07 +09:00

108 строки
5.7 KiB
Python

"""Маршрутизация эфира: кому пересылать принятый кадр.
Сервер намеренно простой — несколько хотспотов во внутренней сети:
• групповой вызов на разрешённую группу → всем остальным подключённым хотспотам;
• приватный вызов → только тому хотспоту, где абонента слышали последним;
• вызов на группу, которой нет в списке → никуда (в панели будет видно отказ).
Здесь же переписывается 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 self.state is not None:
sp = self.state.peer_get(peer_id)
if sp is not None:
sp.rx_packets += 1
# запоминаем, через какой хотспот слышали передающего
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 []