#!/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()