DMR-сервер: мастер Homebrew на stdlib для Pi-Star
Ядро протокола форкнуто из dmrhubv2 (~1300 строк из 31700): analyzer, server, parrot, dmr_voice, bptc. Отрезаны портал, БД, мосты, алерты, динамические подписки. Своё: конфиг, состояние, маршрутизатор, панель, установщик. Ноль зависимостей — на Debian 13 действует PEP 668, pip в систему запрещён. Две правки под Pi-Star, которых в исходном коде не было: • peer_id переписывается на получателя (байты 11-14) через хук encode_for — так делают HBlink3/HBlink4 и BrandMeister, а DMRGateway на Pi-Star стоит между MMDVMHost и сетью; • приватный вызов при неизвестном абоненте уходит всем, а не в никуда (взято из HBlink4: user cache with broadcast fallback). docs/PROTOCOL.md фиксирует, что тест-вектор auth-хеша в спецификации 2015 года ОШИБОЧЕН (посчитан от ASCII-строки соли вместо четырёх байт). Правка кода по нему сломала бы вход всем хотспотам. tests/test_e2e.py: 15 проверок настоящими UDP-кадрами MMDVMHost. Все проходят. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvX94Vwy7vXWEuiFm1g9Av
Этот коммит содержится в:
@@ -0,0 +1,52 @@
|
||||
"""Логирование: в journald (stdout) и одновременно в живой журнал панели.
|
||||
|
||||
Перенесённые из DMRhub модули зовут `get_logger(__name__)` и пишут через него —
|
||||
эта прослойка позволяет не трогать их код и при этом показывать всё в панели.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
_sink = None # выставляется в main(): объект State с методом .event(kind, text)
|
||||
|
||||
|
||||
def set_sink(state) -> None:
|
||||
global _sink
|
||||
_sink = state
|
||||
|
||||
|
||||
class _PanelHandler(logging.Handler):
|
||||
"""Дублирует записи в журнал панели. Уровень -> цвет строки в интерфейсе."""
|
||||
|
||||
_KIND = {
|
||||
logging.DEBUG: "info",
|
||||
logging.INFO: "info",
|
||||
logging.WARNING: "warn",
|
||||
logging.ERROR: "error",
|
||||
logging.CRITICAL: "error",
|
||||
}
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
if _sink is None:
|
||||
return
|
||||
try:
|
||||
_sink.event(self._KIND.get(record.levelno, "info"), record.getMessage())
|
||||
except Exception: # логирование не должно ронять сервер
|
||||
pass
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
def setup(level: str = "INFO") -> None:
|
||||
root = logging.getLogger()
|
||||
root.setLevel(getattr(logging, level.upper(), logging.INFO))
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in root.handlers):
|
||||
h = logging.StreamHandler(sys.stdout)
|
||||
h.setFormatter(logging.Formatter("%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||
datefmt="%H:%M:%S"))
|
||||
root.addHandler(h)
|
||||
if not any(isinstance(h, _PanelHandler) for h in root.handlers):
|
||||
root.addHandler(_PanelHandler())
|
||||
Ссылка в новой задаче
Block a user