"""Точка входа: python3 -m dmrsrv Связывает мастер, маршрутизатор, попугая, состояние и панель. Всё на stdlib — на Debian 13 действует PEP 668, ставить что-либо через pip в систему нельзя, а venv ради пары строк кода — лишний слой, который однажды сломается. """ from __future__ import annotations import asyncio import contextlib import signal import sys import time from . import config as cfgmod from . import log as logmod from . import panel as panelmod from .analyzer import TAG_MSTCL from .parrot import Parrot from .router import Router, retarget from .server import HomebrewMaster, PeerState, run_server from .state import Peer as StatePeer from .state import State def _wire_state(master: HomebrewMaster, state: State) -> None: """Зеркалим реестр пиров мастера в State для панели. Мастер — форк из DMRhub, его не трогаем: снимаем состояние снаружи, чтобы обновление форка не конфликтовало. """ def sync() -> None: for pid, p in list(master.peers.items()): sp = state.peer_get(pid) cfgp = p.config or {} if sp is None: sp = StatePeer(peer_id=pid, addr=p.addr) state.peer_add(sp) sp.addr = p.addr sp.stage = {PeerState.NEW: "вход", PeerState.CHALLENGE: "пароль", PeerState.AUTHED: "конфигурация", PeerState.CONNECTED: "в сети"}.get(p.state, p.state) sp.callsign = cfgp.get("callsign", "") sp.location = cfgp.get("location", "") sp.description = cfgp.get("description", "") sp.software = cfgp.get("package_id", "") or cfgp.get("software_id", "") sp.colorcode = cfgp.get("colorcode", "") sp.rx_freq = cfgp.get("rx_freq", "") sp.tx_freq = cfgp.get("tx_freq", "") # Мастер считает время по monotonic, панель — по time.time(). # Без пересчёта панель показывала «простой» равным времени в сети, # то есть живой хотспот выглядел мёртвым. if p.last_seen: sp.last_seen = time.time() - max(0.0, master._now() - p.last_seen) for pid in list(state.peers): if pid not in master.peers: gone = state.peer_del(pid) if gone: state.event("peer", f"хотспот {pid} ({gone.callsign}) отключился") master._state_sync = sync # зовём из фоновой задачи ниже async def _sync_loop(master: HomebrewMaster, interval: float = 1.0) -> None: while True: await asyncio.sleep(interval) with contextlib.suppress(Exception): master._state_sync() def main() -> int: cfg = cfgmod.load() state = State(log_keep=int(cfg.get("log_keep", 500))) logmod.setup() logmod.set_sink(state) router = Router(cfg, state) passphrase = cfg["passphrase"].encode("utf-8") allow = {int(x) for x in cfg.get("peers_allowlist", [])} def password_for(peer_id: int) -> bytes | None: """Пароль хотспота. Пустой список разрешённых = пускать всех, кто знает пароль.""" if allow and peer_id not in allow: state.event("warn", f"хотспот {peer_id} не в списке разрешённых — отказ") return None return passphrase master = HomebrewMaster( password_for=password_for, router=router, # Подставляем номер ПОЛУЧАТЕЛЯ в байты 11-14. Так делают HBlink3/HBlink4 # и BrandMeister; DMRGateway на Pi-Star стоит между MMDVMHost и сетью. encode_for=lambda tgt, dmr: retarget(dmr.raw, tgt.peer_id), ) _wire_state(master, state) parrot = None if cfg.get("parrot", {}).get("enabled", True): parrot = Parrot(master) master.on_dmrd = lambda peer, dmr: parrot.feed(dmr) state.event("info", f"попугай включён на TG{cfg['parrot'].get('tg', 9990)}") def drop_peer(pid: int) -> bool: p = master.peers.pop(pid, None) if p is None: return False with contextlib.suppress(Exception): if master._send: master._send(TAG_MSTCL + pid.to_bytes(4, "big"), p.addr) return True panelmod.start(cfg, state, drop_peer) state.event("info", f"мастер слушает UDP {cfg['bind']}:{cfg['port']}, " f"групп в списке: {len(cfg.get('groups', []))}") if not cfg.get("groups"): state.event("warn", "групп нет — групповые вызовы никуда не пойдут, добавь в панели") async def runner() -> None: loop = asyncio.get_running_loop() stop = loop.create_future() for sig in (signal.SIGINT, signal.SIGTERM): with contextlib.suppress(NotImplementedError): loop.add_signal_handler(sig, lambda: stop.done() or stop.set_result(None)) tasks = [ asyncio.create_task(run_server(master, cfg["bind"], int(cfg["port"]))), asyncio.create_task(_sync_loop(master)), ] await stop state.event("info", "останавливаюсь") for t in tasks: t.cancel() await asyncio.gather(*tasks, return_exceptions=True) try: asyncio.run(runner()) except KeyboardInterrupt: pass return 0 if __name__ == "__main__": sys.exit(main())