Ядро протокола форкнуто из 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
94 строки
6.4 KiB
Markdown
94 строки
6.4 KiB
Markdown
# 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`
|