Files
opengd77-uv380-sms/tools/dmr_dump.py
T
viktor b5a8e92e45 DMR-данные (SMS) на TYT MD-UV380 под OpenGD77
Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки.

Что внутри:
- четыре новых модуля прошивки (приём, сборка исходящего, меню Messages);
- правки существующих файлов в виде скриптов, с порядком применения;
- инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета;
- NOTES.md: что проверено в эфире, что не доделано, где грабли.

Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота,
рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном
сервера побайтово, но в эфире не проверялась.

Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
2026-09-11 02:39:24 +09:00

113 строки
3.9 KiB
Python

#!/usr/bin/env python3
"""Listen on the radio's MMDVM port and dump every received DMR burst verbatim.
Prints the MMDVM control byte and the raw 33-byte burst as hex, so the frames can be
decoded and compared byte for byte elsewhere. Read-only towards the air: the only thing
sent to the radio is a periodic GET_STATUS, which is required to put/keep it in modem
mode (the firmware leaves modem mode if the host goes quiet, and only enters it when it
sees an MMDVM frame).
"""
import os, sys, termios, time, argparse
MMDVM_FRAME_START = 0xE0
MMDVM_GET_STATUS = 0x01
MMDVM_DMR_DATA1 = 0x18
MMDVM_DMR_DATA2 = 0x1A
DATA_TYPES = {
0x0: "PI header", 0x1: "voice LC header", 0x2: "terminator with LC",
0x3: "CSBK", 0x4: "MBC header", 0x5: "MBC continuation",
0x6: "data header", 0x7: "rate 1/2 data", 0x8: "rate 3/4 data",
0x9: "idle", 0xA: "rate 1 data",
}
def open_port(path):
fd = os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
a = termios.tcgetattr(fd)
a[0] = 0
a[1] = 0
a[2] = termios.CS8 | termios.CREAD | termios.CLOCAL
a[3] = 0
a[6][termios.VMIN] = 0
a[6][termios.VTIME] = 0
termios.tcsetattr(fd, termios.TCSANOW, a)
termios.tcflush(fd, termios.TCIOFLUSH)
return fd
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", default="/dev/ttyACM0")
ap.add_argument("--secs", type=int, default=600)
args = ap.parse_args()
fd = open_port(args.port)
buf = b""
deadline = time.time() + args.secs
next_poll = 0.0
counts = {}
total = 0
print("dumping raw DMR bursts for %d s (GET_STATUS keepalive every 3 s)" % args.secs, flush=True)
try:
while time.time() < deadline:
now = time.time()
if now >= next_poll:
# keepalive: also what puts the radio into modem mode in the first place
try:
os.write(fd, bytes([MMDVM_FRAME_START, 3, MMDVM_GET_STATUS]))
except OSError:
pass
next_poll = now + 3.0
try:
chunk = os.read(fd, 4096)
except (BlockingIOError, OSError):
chunk = b""
if chunk:
buf += chunk
else:
time.sleep(0.002)
# parse MMDVM frames: E0 <len> <type> <payload...>
while len(buf) >= 3:
if buf[0] != MMDVM_FRAME_START:
buf = buf[1:]
continue
length = buf[1]
if length < 3:
buf = buf[1:]
continue
if len(buf) < length:
break
frame, buf = buf[:length], buf[length:]
ftype = frame[2]
if ftype in (MMDVM_DMR_DATA1, MMDVM_DMR_DATA2):
total += 1
control = frame[3] if len(frame) > 3 else 0
burst = frame[4:37]
slot = 1 if ftype == MMDVM_DMR_DATA1 else 2
if control & 0x40: # data sync burst
dt = control & 0x0F
label = "data/%s" % DATA_TYPES.get(dt, "type %d" % dt)
elif control & 0x20:
label = "voice sync"
else:
label = "voice seq %d" % (control & 0x0F)
counts[label] = counts.get(label, 0) + 1
print("%s slot%d ctrl=%02X %-28s %s" % (
time.strftime("%H:%M:%S", time.gmtime()), slot, control, label, burst.hex()),
flush=True)
finally:
os.close(fd)
print("\n--- summary: %d DMR bursts ---" % total, flush=True)
for k in sorted(counts, key=lambda x: -counts[x]):
print(" %-28s %d" % (k, counts[k]), flush=True)
if __name__ == "__main__":
main()