DMR-данные (SMS) на TYT MD-UV380 под OpenGD77
Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки. Что внутри: - четыре новых модуля прошивки (приём, сборка исходящего, меню Messages); - правки существующих файлов в виде скриптов, с порядком применения; - инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета; - NOTES.md: что проверено в эфире, что не доделано, где грабли. Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота, рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном сервера побайтово, но в эфире не проверялась. Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
Этот коммит содержится в:
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Grab the radio's screen over USB and save it as a PNG.
|
||||
|
||||
Uses the CPS read command (area 6 = display buffer), which the firmware answers only
|
||||
while the radio is NOT in modem mode - and, importantly, such requests do not switch it
|
||||
into modem mode either. So the screen can be watched while the radio does its normal job.
|
||||
|
||||
Display is 160x128, RGB565, 2 bytes per pixel.
|
||||
"""
|
||||
import os, sys, termios, time, zlib, struct, argparse
|
||||
|
||||
WIDTH, HEIGHT = 160, 128
|
||||
CPS_ACCESS_DISPLAY_BUFFER = 6
|
||||
CHUNK = 1024
|
||||
|
||||
|
||||
def open_port(path):
|
||||
fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
||||
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] = 20
|
||||
termios.tcsetattr(fd, termios.TCSANOW, a)
|
||||
termios.tcflush(fd, termios.TCIOFLUSH)
|
||||
return fd
|
||||
|
||||
|
||||
def read_exact(fd, n, timeout=3.0):
|
||||
buf = b""
|
||||
deadline = time.time() + timeout
|
||||
while len(buf) < n and time.time() < deadline:
|
||||
try:
|
||||
part = os.read(fd, n - len(buf))
|
||||
except BlockingIOError:
|
||||
part = b""
|
||||
if part:
|
||||
buf += part
|
||||
else:
|
||||
time.sleep(0.003)
|
||||
return buf
|
||||
|
||||
|
||||
def read_block(fd, area, addr, length):
|
||||
req = bytes([ord('R'), area,
|
||||
(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF,
|
||||
(length >> 8) & 0xFF, length & 0xFF])
|
||||
termios.tcflush(fd, termios.TCIFLUSH)
|
||||
os.write(fd, req)
|
||||
head = read_exact(fd, 3)
|
||||
if len(head) < 3 or head[0:1] != b'R':
|
||||
return None
|
||||
got = (head[1] << 8) | head[2]
|
||||
data = read_exact(fd, got)
|
||||
return data if len(data) == got else None
|
||||
|
||||
|
||||
def write_png(path, rgb_rows):
|
||||
"""Minimal PNG writer (stdlib only)."""
|
||||
raw = b"".join(b"\x00" + row for row in rgb_rows)
|
||||
|
||||
def chunk(tag, data):
|
||||
c = struct.pack(">I", len(data)) + tag + data
|
||||
return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
|
||||
png = b"\x89PNG\r\n\x1a\n"
|
||||
png += chunk(b"IHDR", struct.pack(">IIBBBBB", WIDTH, HEIGHT, 8, 2, 0, 0, 0))
|
||||
png += chunk(b"IDAT", zlib.compress(raw, 6))
|
||||
png += chunk(b"IEND", b"")
|
||||
open(path, "wb").write(png)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", default="/dev/ttyACM0")
|
||||
ap.add_argument("--out", default="/home/viktor/screen.png")
|
||||
args = ap.parse_args()
|
||||
|
||||
fd = open_port(args.port)
|
||||
try:
|
||||
total = WIDTH * HEIGHT * 2
|
||||
data = b""
|
||||
while len(data) < total:
|
||||
n = min(CHUNK, total - len(data))
|
||||
part = read_block(fd, CPS_ACCESS_DISPLAY_BUFFER, len(data), n)
|
||||
if part is None:
|
||||
print("read failed at offset %d - is the radio in modem mode?" % len(data))
|
||||
return 2
|
||||
data += part
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
rows = []
|
||||
for y in range(HEIGHT):
|
||||
row = bytearray()
|
||||
for x in range(WIDTH):
|
||||
i = (y * WIDTH + x) * 2
|
||||
# RGB565, little endian as stored by the display driver
|
||||
v = data[i] | (data[i + 1] << 8)
|
||||
r = ((v >> 11) & 0x1F) << 3
|
||||
g = ((v >> 5) & 0x3F) << 2
|
||||
b = (v & 0x1F) << 3
|
||||
row += bytes((r, g, b))
|
||||
rows.append(bytes(row))
|
||||
|
||||
write_png(args.out, rows)
|
||||
nonblack = sum(1 for i in range(0, len(data), 2) if data[i] or data[i + 1])
|
||||
print("saved %s (%d of %d pixels non-black)" % (args.out, nonblack, WIDTH * HEIGHT))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Ссылка в новой задаче
Block a user