Главное: - Опознан DMR-чип: FM100B = Sicomm SCT3258TD. Датащит Packet Interface (126 стр) в docs/. Открывает калибровку/AMBE/вокодер по документации. Родня: UV-4R, китайские клоны. Реализовано: - Приём Talker Alias (cmd 0x60) — позывной крупно на экране (build_ta.py) - Интерактивный агент — управление FM100B по COM без перешивки (build_agent.py + agent.py) - Полная карта команд FM100B с живой рации (fm100b-commands.md) - Работа с SPI: чтение/запись/восстановление codeplug и калибровки (spi_read/spi_wr/spi_restore) - Конструктор дисплея + редактор прошивки (tools/display-editor) Документация: - docs/SCT3258_packet_interface.txt/.pdf — спека DMR-чипа - docs/fm100b-commands.md — карта команд - docs/dmr-tx-recovery.md — диагноз/восстановление DMR-передачи Инструменты: flash_ps.py (прошивка через WSL/powershell), sniff.py (снифер кадров), hotspot_rxoffset_sweep.py, apply_patch.py, build_* сборщики. Важно: DMR TX-калибровка сбита в экспериментах со SET-командами модулю; восстановление через offset-компенсацию / .updat Radtel / калибровку по прибору (docs/dmr-tx-recovery.md).
112 строки
4.5 KiB
Python
112 строки
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Захват кадров DMR-модуля с рации, прошитой снифером.
|
|
|
|
python3 tools/sniff.py [секунды] [--port COM4] [--raw]
|
|
|
|
Рация должна быть прошита stock-fw/rt4d_sniffer.bin и включена обычным образом.
|
|
Формат потока: F1 F2 <len> <кадр len байт>, кадр — обычный `68 cmd b sub CK CK len len … 10`.
|
|
|
|
Под WSL порт открывается через powershell.exe.
|
|
"""
|
|
import argparse, subprocess, sys
|
|
from collections import Counter
|
|
|
|
# что уже известно про команды модуля (из re/dmr.md и разбора образа FM100B)
|
|
KNOWN = {
|
|
0x05: "конфигурация канала", 0x06: "установка вызова", 0x07: "данные канала",
|
|
0x09: "разное", 0x0A: "передача/PTT", 0x25: "версия модуля",
|
|
0x3E: "обновление прошивки модуля", 0x42: "параметр", 0x49: "свой DMR ID",
|
|
0x51: "вокодер", 0x59: "активность приёма", 0x62: "параметр",
|
|
0x66: "девиация", 0x67: "компандер", 0x81: "блок SMS", 0x82: "отправка SMS",
|
|
0x84: "запрос контакта",
|
|
}
|
|
|
|
PS = r'''
|
|
$p = New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
|
|
$p.ReadTimeout = 100
|
|
try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit 1 }}
|
|
$sw = [Diagnostics.Stopwatch]::StartNew()
|
|
$sb = New-Object System.Text.StringBuilder
|
|
while ($sw.Elapsed.TotalSeconds -lt {secs}) {{
|
|
try {{
|
|
$b = $p.ReadByte()
|
|
[void]$sb.Append("{{0:x2}}" -f $b)
|
|
if ($sb.Length -ge 512) {{ Write-Output $sb.ToString(); [void]$sb.Clear() }}
|
|
}} catch {{
|
|
if ($sb.Length -gt 0) {{ Write-Output $sb.ToString(); [void]$sb.Clear() }}
|
|
}}
|
|
}}
|
|
if ($sb.Length -gt 0) {{ Write-Output $sb.ToString() }}
|
|
$p.Close()
|
|
'''
|
|
|
|
|
|
def find_port():
|
|
out = subprocess.run(["powershell.exe", "-NoProfile", "-Command",
|
|
"[System.IO.Ports.SerialPort]::GetPortNames() -join ','"],
|
|
capture_output=True, text=True).stdout
|
|
ports = [p for p in out.strip().replace("\r", "").split(",") if p]
|
|
if not ports:
|
|
sys.exit("COM-порт не найден — подключите рацию.")
|
|
return ports[0]
|
|
|
|
|
|
def parse(stream: bytes):
|
|
"""выделить кадры по маркеру F1 F2 <len>"""
|
|
out, i = [], 0
|
|
while i < len(stream) - 3:
|
|
if stream[i] == 0xF1 and stream[i + 1] == 0xF2:
|
|
n = stream[i + 2]
|
|
if 9 <= n <= 220 and i + 3 + n <= len(stream):
|
|
out.append(stream[i + 3:i + 3 + n])
|
|
i += 3 + n
|
|
continue
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("secs", nargs="?", type=int, default=30)
|
|
ap.add_argument("--port")
|
|
ap.add_argument("--raw", action="store_true", help="печатать каждый кадр")
|
|
a = ap.parse_args()
|
|
port = a.port or find_port()
|
|
print(f"[слушаю {port} {a.secs} с — начинайте передачу]", flush=True)
|
|
|
|
r = subprocess.run(["powershell.exe", "-NoProfile", "-Command",
|
|
PS.format(port=port, secs=a.secs)],
|
|
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
hexs = "".join(l.strip() for l in r.stdout.splitlines()
|
|
if l.strip() and not l.startswith("ERR:"))
|
|
if "ERR:" in r.stdout:
|
|
sys.exit(r.stdout.strip())
|
|
raw = bytes.fromhex(hexs) if hexs else b""
|
|
frames = parse(raw)
|
|
print(f"принято {len(raw)} байт, кадров {len(frames)}\n")
|
|
|
|
cnt = Counter(f[1] for f in frames if len(f) > 1)
|
|
print("команды (код — сколько раз — что известно):")
|
|
for cmd, n in cnt.most_common():
|
|
print(f" 0x{cmd:02X} x{n:<5d} {KNOWN.get(cmd,'— НЕИЗВЕСТНО —')}")
|
|
|
|
print("\nпервые кадры каждого кода:")
|
|
seen = set()
|
|
for f in frames:
|
|
if len(f) < 2 or f[1] in seen:
|
|
continue
|
|
seen.add(f[1])
|
|
pl = f[8:-1] if len(f) > 9 else b""
|
|
txt = "".join(chr(c) if 32 <= c < 127 else "." for c in pl)
|
|
print(f" 0x{f[1]:02X}: {f.hex(' ')}")
|
|
if pl:
|
|
print(f" payload {len(pl)} Б «{txt}»")
|
|
if a.raw:
|
|
print("\nвсе кадры:")
|
|
for f in frames:
|
|
print(" " + f.hex(" "))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|