"""Точка входа: 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 .access import Guard, make_password_for from .analyzer import TAG_MSTCL from .lastheard import LastHeard 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) # Имена полей — ровно как у analyzer.parse_rptc_config. Раньше читались # colorcode/latitude/longitude, которых там нет, и поля молча пустовали. for fld in ("callsign", "location", "description", "url", "software_id", "package_id", "rx_freq", "tx_freq", "tx_power", "color_code", "lat", "lon", "height", "slots"): val = cfgp.get(fld) if val is not None: setattr(sp, fld, val) # Мастер считает время по 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() with contextlib.suppress(Exception): master._lastheard_sweep() 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) guard = Guard(cfg, state) master = HomebrewMaster( password_for=make_password_for(cfg, state), router=router, guard=guard, # Подставляем номер ПОЛУЧАТЕЛЯ в байты 11-14. Так делают HBlink3/HBlink4 # и BrandMeister; DMRGateway на Pi-Star стоит между MMDVMHost и сетью. encode_for=lambda tgt, dmr: retarget(dmr.raw, tgt.peer_id), ) _wire_state(master, state) lastheard = LastHeard(state) master._lastheard_sweep = lastheard.sweep parrot = Parrot(master) if cfg.get("parrot", {}).get("enabled", True) else None if parrot is not None: state.event("info", f"попугай включён на TG{cfg['parrot'].get('tg', 9990)}") def on_dmrd(peer, dmr): lastheard.feed(peer, dmr) if parrot is not None: parrot.feed(dmr) master.on_dmrd = on_dmrd 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())