FM100B = Sicomm SCT3258TD (документирован); Talker Alias, агент, SPI-инструменты, восстановление TX
Главное: - Опознан 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).
Этот коммит содержится в:
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Клиент интерактивного агента RT-4D (прошивка rt4d_agent.bin).
|
||||
|
||||
Шлёт рации `0xAA <len> <сырой FM100B-кадр>`, агент блитит кадр модулю, ответы
|
||||
модуля возвращаются в порт как `F1 F2 <len> <кадр>`. Работает через powershell.exe (WSL).
|
||||
|
||||
python3 tools/agent.py send 25 01 01 # cmd=25 b=01 sub=01, payload пуст -> версия
|
||||
python3 tools/agent.py send 25 01 01 --data 00
|
||||
python3 tools/agent.py raw 68 25 01 01 00 00 00 00 10 # готовый кадр целиком
|
||||
python3 tools/agent.py listen 10 # просто слушать порт N секунд
|
||||
"""
|
||||
import argparse, subprocess, sys, struct
|
||||
|
||||
|
||||
def cksum(frame: bytes) -> int:
|
||||
s = 0; i = 0
|
||||
while len(frame) - i > 1:
|
||||
s += (frame[i] << 8) | frame[i + 1]; i += 2
|
||||
if len(frame) - i:
|
||||
s += frame[i] << 8
|
||||
while s >> 16:
|
||||
s = (s & 0xFFFF) + (s >> 16)
|
||||
return (~s) & 0xFFFF
|
||||
|
||||
|
||||
def build_frame(cmd, b, sub, payload: bytes) -> bytes:
|
||||
f = bytearray([0x68, cmd, b, sub, 0xFF, 0xFF,
|
||||
(len(payload) >> 8) & 0xFF, len(payload) & 0xFF])
|
||||
f += payload; f.append(0x10)
|
||||
ck = cksum(bytes(f)); f[4], f[5] = (ck >> 8) & 0xFF, ck & 0xFF
|
||||
return bytes(f)
|
||||
|
||||
|
||||
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]
|
||||
|
||||
|
||||
PS = r'''
|
||||
$p = New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
|
||||
$p.ReadTimeout = 200
|
||||
try {{ $p.Open() }} catch {{ Write-Output "ERR"; exit 1 }}
|
||||
$p.DiscardInBuffer()
|
||||
$tx = [byte[]]@({txlist})
|
||||
if ($tx.Length -gt 0) {{ $p.Write($tx,0,$tx.Length) }}
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||
$last = 0
|
||||
while ($sw.Elapsed.TotalSeconds -lt {secs}) {{
|
||||
try {{ [void]$sb.Append("{{0:x2}}" -f $p.ReadByte()); $last=$sw.ElapsedMilliseconds }} catch {{}}
|
||||
if ($sb.Length -gt 0 -and ($sw.ElapsedMilliseconds - $last) -gt {idle}) {{ break }}
|
||||
}}
|
||||
$p.Close()
|
||||
Write-Output $sb.ToString()
|
||||
'''
|
||||
|
||||
|
||||
def xfer(port, tx: bytes, secs=3, idle=400):
|
||||
txlist = ",".join(str(x) for x in tx)
|
||||
r = subprocess.run(["powershell.exe", "-NoProfile", "-Command",
|
||||
PS.format(port=port, txlist=txlist, secs=secs, idle=idle)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
hexs = "".join(l.strip() for l in r.stdout.splitlines() if l.strip() and l.strip() != "ERR")
|
||||
return bytes.fromhex(hexs) if hexs else b""
|
||||
|
||||
|
||||
def parse_frames(raw: bytes):
|
||||
out, i = [], 0
|
||||
while i < len(raw) - 2:
|
||||
if raw[i] == 0xF1 and raw[i + 1] == 0xF2:
|
||||
n = raw[i + 2]
|
||||
if 9 <= n <= 220 and i + 3 + n <= len(raw):
|
||||
out.append(raw[i + 3:i + 3 + n]); i += 3 + n; continue
|
||||
i += 1
|
||||
return out
|
||||
|
||||
|
||||
def show(raw):
|
||||
print(f"принято {len(raw)} байт")
|
||||
frames = parse_frames(raw)
|
||||
if not frames:
|
||||
if raw: print("сырые байты:", raw.hex(' '))
|
||||
else: print("(тишина)")
|
||||
return
|
||||
for f in frames:
|
||||
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" cmd 0x{f[1]:02X} len={len(pl)} {f.hex(' ')}" + (f" «{txt}»" if pl else ""))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
sub = ap.add_subparsers(dest="op", required=True)
|
||||
s = sub.add_parser("send"); s.add_argument("cmd"); s.add_argument("b"); s.add_argument("sub")
|
||||
s.add_argument("--data", default=""); s.add_argument("--secs", type=float, default=3)
|
||||
r = sub.add_parser("raw"); r.add_argument("bytes", nargs="+"); r.add_argument("--secs", type=float, default=3)
|
||||
l = sub.add_parser("listen"); l.add_argument("secs", type=float, nargs="?", default=10)
|
||||
ap.add_argument("--port")
|
||||
a = ap.parse_args()
|
||||
port = a.port or find_port()
|
||||
|
||||
if a.op == "listen":
|
||||
show(xfer(port, b"", secs=a.secs, idle=99999))
|
||||
return
|
||||
if a.op == "raw":
|
||||
frame = bytes(int(x, 16) for x in a.bytes)
|
||||
else:
|
||||
payload = bytes.fromhex(a.data) if a.data else b""
|
||||
frame = build_frame(int(a.cmd, 16), int(a.b, 16), int(a.sub, 16), payload)
|
||||
tx = bytes([0xAA, len(frame)]) + frame
|
||||
print(f"TX кадр модулю: {frame.hex(' ')}")
|
||||
show(xfer(port, tx, secs=a.secs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Ссылка в новой задаче
Block a user