Ядро протокола форкнуто из 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
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.
Три вещи, которые легко испортить
- Переписывать peer_id (11–14) на номер получателя. Смысл поля — «ретранслятор,
которого касается кадр на этом соединении». Делает
router.retarget()через хукencode_for. MMDVMHost поле на приёме не проверяет, но DMRGateway на многих Pi-Star стоит между ним и сетью. - Не возвращать кадр отправителю — мгновенная петля в эфире на дуплексном ретрансляторе. Исключение — попугай, он отвечает намеренно и напрямую.
- Отправлять 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, HBlink3hblink.py/const.py/playback.py - Проверенная реализация:
viktor/dmrhubv2, откуда взятыanalyzer.py,server.py,parrot.py,dmr_voice.py,bptc.py