Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки
Баузбенд FM100B: - найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём - расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации - инструмент tools/fm100b.py (ping/send/raw/scan) - cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом) - разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд - cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14 - исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md) Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md): - вызывается как функция горячей клавиши №22 Analog Spectrum - вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318 - спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC Декомпозиция (docs/firmware-parts.md): - два процессора + внешний SPI = три канала внедрения - карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode - найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки) - дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке - аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля Прочее: - везде исправлен режим прошивки: тангента PTT вместо клавиши "*" - устаревший Ru-4D_Flasher.exe удалён из репозитория - добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
Этот коммит содержится в:
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""FM100B (DMR baseband) console — talk to the RT-4D's DMR chip from the PC.
|
||||
|
||||
Требуется: рация включена с зажатой клавишей МЕНЮ (режим «Обновление DMR чипа»),
|
||||
в котором прошивка работает прозрачным мостом USB ↔ FM100B (USART3).
|
||||
|
||||
Формат кадра: 68 | cmd | b | sub | CKh CKl | lenH lenL | payload… | 10
|
||||
CRC — «интернетная» one's-complement сумма 16-битных BE-слов по всему кадру,
|
||||
причём во время расчёта поле CRC = 0xFFFF; результат кладётся BE в [4..5].
|
||||
|
||||
python3 fm100b.py ping # штатный handshake (cmd 0x84)
|
||||
python3 fm100b.py send 84 --sub 1 --payload 00
|
||||
python3 fm100b.py raw 6884010100000001 0010
|
||||
python3 fm100b.py scan 01 20 # опрос диапазона команд (только чтение статуса)
|
||||
|
||||
Порт задаётся через --port (по умолчанию берётся первый CH343).
|
||||
Под WSL доступ к COM идёт через powershell.exe (WSL2 не пробрасывает COM напрямую).
|
||||
"""
|
||||
import argparse, subprocess, sys, time
|
||||
|
||||
DEFAULT_BAUD = 115200
|
||||
|
||||
|
||||
# ---------------- frame helpers ----------------
|
||||
def cksum(frame: bytes) -> int:
|
||||
"""one's complement sum of 16-bit big-endian words (CRC field must be 0xFFFF)"""
|
||||
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(cmd: int, b: int = 1, sub: int = 1, payload: bytes = b"") -> 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 parse(resp: bytes):
|
||||
"""decode a response frame -> dict (or None)"""
|
||||
if len(resp) < 9 or resp[0] != 0x68:
|
||||
return None
|
||||
cmd, b, status = resp[1], resp[2], resp[3]
|
||||
ck = (resp[4] << 8) | resp[5]
|
||||
ln = (resp[6] << 8) | resp[7]
|
||||
payload = resp[8:8 + ln]
|
||||
chk = bytearray(resp[:9 + ln])
|
||||
chk[4] = chk[5] = 0xFF
|
||||
ok = cksum(bytes(chk)) == ck
|
||||
return dict(cmd=cmd, b=b, status=status, length=ln,
|
||||
payload=bytes(payload), crc_ok=ok, raw=resp)
|
||||
|
||||
|
||||
# ---------------- serial via powershell (WSL2) ----------------
|
||||
PS = r'''
|
||||
$p = New-Object System.IO.Ports.SerialPort "{port}",{baud},"None",8,"one"
|
||||
$p.ReadTimeout = 120
|
||||
try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit }}
|
||||
$p.DiscardInBuffer()
|
||||
$tx = [byte[]]@({txlist})
|
||||
if ($tx.Length -gt 0) {{ $p.Write($tx,0,$tx.Length) }}
|
||||
$buf = New-Object System.Collections.Generic.List[byte]
|
||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||
$last = 0
|
||||
while ($sw.ElapsedMilliseconds -lt {tmo}) {{
|
||||
try {{ $buf.Add($p.ReadByte()); $last = $sw.ElapsedMilliseconds }} catch {{}}
|
||||
if ($buf.Count -gt 0 -and ($sw.ElapsedMilliseconds - $last) -gt {idle}) {{ break }}
|
||||
}}
|
||||
$p.Close()
|
||||
Write-Output ("OK:" + (($buf | ForEach-Object {{ "{{0:x2}}" -f $_ }}) -join ""))
|
||||
'''
|
||||
|
||||
|
||||
def find_port() -> str:
|
||||
out = subprocess.run(["powershell.exe", "-NoProfile", "-Command",
|
||||
"[System.IO.Ports.SerialPort]::GetPortNames() -join ','"],
|
||||
capture_output=True, text=True).stdout.strip().replace("\r", "")
|
||||
ports = [p for p in out.split(",") if p]
|
||||
if not ports:
|
||||
sys.exit("COM-порт не найден. Подключите рацию.")
|
||||
return ports[0]
|
||||
|
||||
|
||||
def xfer(port: str, tx: bytes, timeout_ms: int = 1200, idle_ms: int = 250) -> bytes:
|
||||
txlist = ",".join(str(x) for x in tx) if tx else ""
|
||||
script = PS.format(port=port, baud=DEFAULT_BAUD, txlist=txlist,
|
||||
tmo=timeout_ms, idle=idle_ms)
|
||||
r = subprocess.run(["powershell.exe", "-NoProfile", "-Command", script],
|
||||
capture_output=True, text=True)
|
||||
line = [l for l in r.stdout.replace("\r", "").split("\n") if l.startswith(("OK:", "ERR:"))]
|
||||
if not line:
|
||||
return b""
|
||||
if line[0].startswith("ERR:"):
|
||||
sys.exit(line[0])
|
||||
return bytes.fromhex(line[0][3:])
|
||||
|
||||
|
||||
def show(tx: bytes, rx: bytes):
|
||||
print(f" TX: {tx.hex(' ')}")
|
||||
if not rx:
|
||||
print(" RX: (нет ответа)")
|
||||
return None
|
||||
print(f" RX: {rx.hex(' ')}")
|
||||
p = parse(rx)
|
||||
if p:
|
||||
print(f" cmd=0x{p['cmd']:02X} status=0x{p['status']:02X} "
|
||||
f"len={p['length']} crc={'ok' if p['crc_ok'] else 'BAD'}"
|
||||
+ (f" payload={p['payload'].hex(' ')}" if p['payload'] else ""))
|
||||
return p
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--port")
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
sub.add_parser("ping")
|
||||
s = sub.add_parser("send"); s.add_argument("code"); s.add_argument("--b", default="1")
|
||||
s.add_argument("--sub", default="1"); s.add_argument("--payload", default="")
|
||||
r = sub.add_parser("raw"); r.add_argument("hex", nargs="+")
|
||||
sc = sub.add_parser("scan"); sc.add_argument("first"); sc.add_argument("last")
|
||||
a = ap.parse_args()
|
||||
port = a.port or find_port()
|
||||
print(f"[порт {port} @ {DEFAULT_BAUD}]")
|
||||
|
||||
if a.cmd == "ping":
|
||||
tx = bytes([0x68, 0x84, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10])
|
||||
show(tx, xfer(port, tx))
|
||||
elif a.cmd == "send":
|
||||
tx = build(int(a.code, 16), int(a.b, 0), int(a.sub, 0), bytes.fromhex(a.payload))
|
||||
show(tx, xfer(port, tx))
|
||||
elif a.cmd == "raw":
|
||||
tx = bytes.fromhex("".join(a.hex))
|
||||
show(tx, xfer(port, tx))
|
||||
elif a.cmd == "scan":
|
||||
lo, hi = int(a.first, 16), int(a.last, 16)
|
||||
print(f"опрос команд 0x{lo:02X}..0x{hi:02X} (b=1 sub=1, пустой payload)\n")
|
||||
for c in range(lo, hi + 1):
|
||||
tx = build(c)
|
||||
rx = xfer(port, tx, timeout_ms=600, idle_ms=150)
|
||||
p = parse(rx) if rx else None
|
||||
if p:
|
||||
print(f" cmd 0x{c:02X}: status=0x{p['status']:02X} len={p['length']}"
|
||||
+ (f" payload={p['payload'].hex(' ')}" if p['payload'] else ""))
|
||||
elif rx:
|
||||
print(f" cmd 0x{c:02X}: неразобранный ответ {rx.hex(' ')}")
|
||||
time.sleep(0.03)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Ссылка в новой задаче
Block a user