Files
dmr-server/docs/PROTOCOL.md
T
viktorиClaude Opus 4.8 8e3b9144c3 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
2026-07-28 23:32:04 +09:00

6.4 KiB

Homebrew/MMDVM — что реально в эфире

Сверено с исходниками MMDVMHost (DMRDirectNetwork.cpp), DMRGateway, HBlink3 (hblink.py) и спецификацией DL5DI/G4KLX/DG1HT 2015 года. Где спецификация расходится с кодом — авторитетен код: именно он крутится на каждом Pi-Star, а спецификацию никто до конца не реализовал.

⚠️ Тест-вектор из спецификации ОШИБОЧЕН — не «чините» по нему код

Спецификация приводит: salt 0A7ED498, пароль DL5DI → cbf0e29abbd11c6573825d36a664e3064441f91fc815ac5dc5ca570d4c4a5f85.

Это значение получается из ASCII-строки "0A7ED498", хотя тремя абзацами выше та же спецификация требует «трактовать 32-битное значение как битовый шаблон, а не как число». Проверено:

sha256(b'\x0a\x7e\xd4\x98' + b'DL5DI') = a763d5c7…   ← так делают ВСЕ реализации
sha256(b'0A7ED498'        + b'DL5DI') = cbf0e29a…   ← так написано в тест-векторе

Сырые байты используют MMDVMHost (memcpy(in, m_salt, 4)), HBlink3 (bytes_4(SALT) + PASSPHRASE) и наш analyzer.auth_hash. Выбивается только бумажка. Если «поправить» код под тест-вектор — вход перестанут проходить все хотспоты.

Расхождения спецификации и реальности

спецификация на самом деле
Ответ мастера MSTACK RPTACK
Keepalive хотспот MSTPING, мастер RPTPONG хотспот RPTPING, мастер MSTPONG
RPTL «семь байт» 8 байт
Порядок в RPTC callsign, затем id id@4, затем callsign@8
Описание/слоты description 20 байт, поля слотов нет description 19@78 + slots 1@97
Длина DMRD 53 53 ядро + BER/RSSI = 55 в эфире
Маски байта 15 0x01/0x02/0x0C/0xF0 0x80/0x40/0x30/0x0F
Период пинга «раз в минуту» 5–15 с

Кадр DMRD

Смещение Длина Поле
0 4 DMRD
4 1 последовательность
5 3 src (uint24 BE)
8 3 dst — talkgroup или абонент (uint24 BE)
11 4 peer_id (uint32 BE) — переписывается на получателя
15 1 биты
16 4 stream_id — непрозрачные байты, не число
20 33 бёрст DMR
53 1 BER (расширение G4KLX)
54 1 RSSI (расширение G4KLX)

Байт 15: 0x80 слот (0=TS1, 1=TS2) · 0x40 тип вызова (0=группа, 1=приват) · 0x30 тип кадра (00 голос, 01 voice sync, 10 data sync) · 0x0F тип данных либо номер голосового кадра. Начало передачи — data sync + 0x01, конец — data sync + 0x02.

Три вещи, которые легко испортить

  1. Переписывать peer_id (11–14) на номер получателя. Смысл поля — «ретранслятор, которого касается кадр на этом соединении». Делает router.retarget() через хук encode_for. MMDVMHost поле на приёме не проверяет, но DMRGateway на многих Pi-Star стоит между ним и сетью.
  2. Не возвращать кадр отправителю — мгновенная петля в эфире на дуплексном ретрансляторе. Исключение — попугай, он отвечает намеренно и напрямую.
  3. Отправлять 55 байт, принимать 53/55/71. DMRGateway молча выбрасывает 53-байтные кадры (length == HOMEBREW_DATA_PACKET_LENGTH).

Терминаторы и заголовки регулярно теряются в эфире, поэтому начало передачи определяется ещё и сменой stream_id, а конец — таймаутом 360 мс (6 пропущенных голосовых кадров).

Попугай (TG 9990)

Записанные кадры проигрываются дословно — payload[20:53] как есть, меняется только stream_id. Это не оптимизация, а необходимость: в DMRhub пробовали пересобирать фрейминг, и получалось «секундное молчание» — рация видела вызов, но голос не декодировала. У кадров из рации уже корректные sync/EMB/slot-type/AMBE, трогать их нечего.

Исключение — приватный эхотест: там нужен обратный адрес (src=9990 → dst=вызывающий), дословный повтор не годится, потому что в нём dst=9990 и рация не примет вызов как «мне». Только для этого случая пересобираются LC-заголовок и терминатор (dmr_voice + bptc).

Темп проигрывания — 60 мс на кадр (шаг слота TDMA), пауза перед началом — 1 с. Доставка — напрямую в хотспот вызывавшего, минуя маршрутизатор: так исключены петли и не портится last-heard.

Источники

  • Спецификация: DMRplus_IPSC_Protocol_for_HB_repeater.pdf (DL5DI/G4KLX/DG1HT, 2015-07-26)
  • MMDVMHost DMRDirectNetwork.cpp @ 7008c62 (до удаления прямого доступа к мастерам)
  • DMRGateway DMRNetwork.cpp, HBlink3 hblink.py/const.py/playback.py
  • Проверенная реализация: viktor/dmrhubv2, откуда взяты analyzer.py, server.py, parrot.py, dmr_voice.py, bptc.py