Ядро протокола форкнуто из 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
73 строки
4.7 KiB
Markdown
73 строки
4.7 KiB
Markdown
# DMR-сервер
|
|
|
|
Простой Homebrew/MMDVM мастер для внутренней сети: несколько хотспотов **Pi-Star**,
|
|
веб-панель с группами и живым журналом, попугай (эхотест) на TG 9990.
|
|
|
|
**Ноль зависимостей** — только `python3` (3.10+). На Debian 13 действует PEP 668,
|
|
и `pip install` в систему запрещён; venv ради пары библиотек — лишний слой, который
|
|
однажды сломается. Поэтому всё на stdlib.
|
|
|
|
## Установка
|
|
|
|
```bash
|
|
sudo bash install.sh
|
|
```
|
|
|
|
Ставит код в `/opt/dmr-server`, конфиг в `/etc/dmr-server/config.json`, заводит юнит
|
|
systemd под отдельным пользователем и печатает адрес панели с паролем. Повторный запуск
|
|
безопасен — конфиг не перезаписывается.
|
|
|
|
## Настройка Pi-Star
|
|
|
|
`Configuration → DMR Master`: адрес сервера, порт **62031**, пароль — значение
|
|
`passphrase` из конфига (**обязательно смени умолчание `passw0rd`**).
|
|
|
|
## Что умеет
|
|
|
|
- Рукопожатие Homebrew: `RPTL → RPTACK+salt → RPTK → RPTC → RPTPING/MSTPONG`.
|
|
- Групповые вызовы: кадр от одного хотспота уходит всем остальным, если TG есть в списке групп.
|
|
- Приватные вызовы: туда, где абонента слышали последним; если не слышали ни разу —
|
|
всем хотспотам (как в HBlink4).
|
|
- Попугай на 9990, групповой и приватный: записывает передачу и проигрывает обратно.
|
|
- Панель: список хотспотов, добавление/удаление групп, последние вызовы, живой журнал,
|
|
принудительное отключение хотспота.
|
|
|
|
## Проверка
|
|
|
|
```bash
|
|
python3 tests/test_e2e.py
|
|
```
|
|
|
|
Поднимает настоящий мастер и говорит с ним по UDP кадрами, которые шлёт MMDVMHost:
|
|
рукопожатие, отказ по неверному паролю, keepalive, доставка группового вызова между
|
|
двумя хотспотами, отсутствие эха отправителю, фильтр по разрешённым группам, приват.
|
|
Проверяет и то, что легко испортить: длину кадра 55 байт, переписывание `peer_id`,
|
|
дословную копию `stream_id` и неизменность бёрста.
|
|
|
|
## Устройство
|
|
|
|
| Файл | Что делает |
|
|
|---|---|
|
|
| `dmrsrv/analyzer.py` | разбор и сборка кадров Homebrew/DMR |
|
|
| `dmrsrv/server.py` | ядро мастера: рукопожатие, реестр хотспотов, маршрут в эфир |
|
|
| `dmrsrv/router.py` | кому пересылать кадр + подстановка `peer_id` получателя |
|
|
| `dmrsrv/parrot.py` | эхотест 9990 |
|
|
| `dmrsrv/dmr_voice.py`, `bptc.py` | пересборка LC — нужна только приватному попугаю |
|
|
| `dmrsrv/panel.py` | веб-панель на `http.server` |
|
|
| `dmrsrv/config.py` | конфиг с атомарным сохранением |
|
|
| `dmrsrv/state.py` | состояние и журнал для панели |
|
|
|
|
Ядро протокола форкнуто из `viktor/dmrhubv2` (~1300 строк из 31700) — там оно уже
|
|
обкатано на живом эфире. Отрезаны портал, БД, мосты, алерты, динамические подписки.
|
|
|
|
**Перед правкой протокола прочти [`docs/PROTOCOL.md`](docs/PROTOCOL.md).** Спецификация
|
|
2015 года врёт в нескольких местах, включая собственный тест-вектор auth-хеша;
|
|
там записано, где именно и почему прав код, а не бумажка.
|
|
|
|
## Ограничения
|
|
|
|
- Один сервер = одна сеть, без транков между серверами (OpenBridge не реализован).
|
|
- Группы статические, из панели. Динамических подписок через `RPTO` нет.
|
|
- Панель по HTTP с Basic-авторизацией — рассчитана на доверенную внутреннюю сеть.
|
|
Наружу выставлять только за HTTPS-прокси.
|