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