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
Этот коммит содержится в:
Executable
+140
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
RadioStar — DMR BPTC(196,96) codec (pure Python).
|
||||
|
||||
Ported from dmr_utils3 (N0MJS, GPL) + ETSI TS 102 361-1. Used to decode/encode
|
||||
the 96-bit payloads carried by DATA_HEADER and CSBK bursts (data headers, CSBK
|
||||
preambles, etc.). Lets the server READ and BUILD those control frames.
|
||||
|
||||
A burst's 196 info bits live at burst-bit positions [0:98] + [166:264]; the
|
||||
middle 68 bits (slot type + sync) are spliced from a real burst of the same type.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# interleaver index (on-air[INDEX_181[i]] = logical[i])
|
||||
INDEX_181 = (
|
||||
0, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 16, 1, 182, 167, 152, 137,
|
||||
122, 107, 92, 77, 62, 47, 32, 17, 2, 183, 168, 153, 138, 123, 108, 93, 78, 63,
|
||||
48, 33, 18, 3, 184, 169, 154, 139, 124, 109, 94, 79, 64, 49, 34, 19, 4, 185, 170,
|
||||
155, 140, 125, 110, 95, 80, 65, 50, 35, 20, 5, 186, 171, 156, 141, 126, 111, 96,
|
||||
81, 66, 51, 36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82, 67, 52, 37, 22, 7,
|
||||
188, 173, 158, 143, 128, 113, 98, 83, 68, 53, 38, 23, 8, 189, 174, 159, 144, 129,
|
||||
114, 99, 84, 69, 54, 39, 24, 9, 190, 175, 160, 145, 130, 115, 100, 85, 70, 55, 40,
|
||||
25, 10, 191, 176, 161, 146, 131, 116, 101, 86, 71, 56, 41, 26, 11, 192, 177, 162,
|
||||
147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 193, 178, 163, 148, 133, 118, 103, 88,
|
||||
73, 58, 43, 28, 13, 194, 179, 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14,
|
||||
195, 180, 165, 150, 135, 120, 105, 90, 75, 60, 45, 30, 15)
|
||||
|
||||
# the 96 data-bit positions in the deinterleaved matrix (72 LC/header + 24 RS/CRC)
|
||||
DATA_POS = [
|
||||
136, 121, 106, 91, 76, 61, 46, 31,
|
||||
152, 137, 122, 107, 92, 77, 62, 47, 32, 17, 2,
|
||||
123, 108, 93, 78, 63, 48, 33, 18, 3, 184, 169,
|
||||
94, 79, 64, 49, 34, 19, 4, 185, 170, 155, 140,
|
||||
65, 50, 35, 20, 5, 186, 171, 156, 141, 126, 111,
|
||||
36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82,
|
||||
7, 188, 173, 158, 143, 128, 113, 98, 83,
|
||||
68, 53, 174, 159, 144, 129, 114, 99, 84, 69, 54, 39,
|
||||
24, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 191,
|
||||
]
|
||||
|
||||
|
||||
def _enc_15113(d):
|
||||
return [
|
||||
d[0] ^ d[1] ^ d[2] ^ d[3] ^ d[5] ^ d[7] ^ d[8],
|
||||
d[1] ^ d[2] ^ d[3] ^ d[4] ^ d[6] ^ d[8] ^ d[9],
|
||||
d[2] ^ d[3] ^ d[4] ^ d[5] ^ d[7] ^ d[9] ^ d[10],
|
||||
d[0] ^ d[1] ^ d[2] ^ d[4] ^ d[6] ^ d[7] ^ d[10],
|
||||
]
|
||||
|
||||
|
||||
def _enc_1393(d):
|
||||
return [
|
||||
d[0] ^ d[1] ^ d[3] ^ d[5] ^ d[6],
|
||||
d[0] ^ d[1] ^ d[2] ^ d[4] ^ d[6] ^ d[7],
|
||||
d[0] ^ d[1] ^ d[2] ^ d[3] ^ d[5] ^ d[7] ^ d[8],
|
||||
d[0] ^ d[2] ^ d[4] ^ d[5] ^ d[8],
|
||||
]
|
||||
|
||||
|
||||
def _bytes_to_bits(data):
|
||||
out = []
|
||||
for byte in data:
|
||||
for i in range(7, -1, -1):
|
||||
out.append((byte >> i) & 1)
|
||||
return out
|
||||
|
||||
|
||||
def _bits_to_bytes(bits):
|
||||
out = bytearray()
|
||||
for i in range(0, len(bits), 8):
|
||||
b = 0
|
||||
for j in range(8):
|
||||
b = (b << 1) | bits[i + j]
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _burst_info_bits(burst33):
|
||||
"""Extract the 196 on-air info bits from a 33-byte burst ([0:98]+[166:264])."""
|
||||
allbits = _bytes_to_bits(burst33)
|
||||
return allbits[0:98] + allbits[166:264]
|
||||
|
||||
|
||||
def decode(burst33: bytes) -> bytes:
|
||||
"""Decode a BPTC(196,96) burst -> 12 bytes (96 bits). No error correction.
|
||||
DATA_POS index the on-air (interleaved) info bits directly (per dmr_utils)."""
|
||||
onair = _burst_info_bits(burst33) # 196 on-air bits
|
||||
bits = [onair[p] for p in DATA_POS] # extract 96 data bits
|
||||
return _bits_to_bytes(bits)
|
||||
|
||||
|
||||
def encode(data12: bytes) -> list:
|
||||
"""Encode 12 bytes -> 196 on-air info bits (list)."""
|
||||
bits = _bytes_to_bits(data12) # 96 bits
|
||||
for _ in range(4):
|
||||
bits.insert(0, 0) # R0-R3
|
||||
for index in range(9):
|
||||
spos = index * 15 + 1
|
||||
epos = spos + 11
|
||||
par = _enc_15113(bits[spos:epos])
|
||||
for pbit in range(4):
|
||||
bits.insert(epos + pbit, par[pbit])
|
||||
for _ in range(60):
|
||||
bits.append(0)
|
||||
for col in range(15):
|
||||
spos = col + 1
|
||||
column = []
|
||||
for _ in range(9):
|
||||
column.append(bits[spos])
|
||||
spos += 15
|
||||
colp = _enc_1393(column)
|
||||
cpar = 136 + col
|
||||
for pbit in range(4):
|
||||
bits[cpar] = colp[pbit]
|
||||
cpar += 15
|
||||
onair = [0] * 196
|
||||
for i in range(196):
|
||||
onair[INDEX_181[i]] = bits[i] # interleave
|
||||
return onair
|
||||
|
||||
|
||||
def splice_into_burst(template33: bytes, onair196: list) -> bytes:
|
||||
"""Build a burst: info bits from onair196, sync+slot-type from a template burst
|
||||
of the same data type (positions [98:166])."""
|
||||
allbits = _bytes_to_bits(template33)
|
||||
allbits[0:98] = onair196[0:98]
|
||||
allbits[166:264] = onair196[98:196]
|
||||
return _bits_to_bytes(allbits)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# decode the captured RT-4D DATA_HEADER (TEST123, 1400001 -> 1234567)
|
||||
hdr = bytes.fromhex("157a07fb5a6681a8b2828e4b058d5d7f77fd7573388475f5803549e54a87813418")
|
||||
dec = decode(hdr)
|
||||
print("DATA_HEADER 12 bytes:", dec.hex())
|
||||
# round-trip: encode(decode) info bits must match the burst's info bits
|
||||
rt = splice_into_burst(hdr, encode(dec))
|
||||
print("round-trip burst == original:", rt == hdr)
|
||||
# decode a captured CSBK preamble
|
||||
csbk = bytes.fromhex("47e7160d92e007ac4e398c7244cd5d7f77fd757ac996047099099220839a4a9b4e")
|
||||
print("CSBK 12 bytes:", decode(csbk).hex())
|
||||
Ссылка в новой задаче
Block a user