diff --git a/docs/tuning-software-protocol.md b/docs/tuning-software-protocol.md new file mode 100644 index 0000000..2beef3b --- /dev/null +++ b/docs/tuning-software-protocol.md @@ -0,0 +1,63 @@ +# Radio Tuning Software 241219 — реверс протокола калибровки + +Декомпиляция `Radio Tuning Software 241219.exe` (dnfile). Сборка **`UV5118_4819_Tune`** +(«4819» = шина управления DMR-модулем `DIG-SCN/SDA/SCK4819` со схемы RT4DDLT01). + +## Главное +Тюнер калибрует модуль **тем же 0x68-протоколом**, что и `tools/fm100b.py` (НЕ 0xAAAA, как +считалось раньше) — значит **достижимо через наш мост** (режим «Обновление DMR чипа»). +Формат кадра тот же: `68 cmd b sub CKhi CKlo lenHi lenLo 10`, CRC как в fm100b.py. + +## Команды калибровки (0xB0–0xBD), извлечены из статических шаблонов (FieldRVA) + +| cmd | назначение (по контексту UI/методов) | пример payload | +|------|--------------------------------------|----------------| +| 0xBD | Connect / вход в режим калибровки | `01` | +| 0xB0 | старт/стоп калибровки (on/off) | `01` / `00` | +| 0xB1 | таблица границ диапазонов (band, N, частоты ×N по 4Б BE, Гц) | UHF: 400.125/435.125/469.975 МГц; VHF: 136.125/150.125/174.125 МГц | +| 0xB2 | девиация/модуляция набор 1 (band, idx, val) | `01 01 80`; read: `00 02` | +| 0xB4 | девиация/модуляция набор 2 | `01 01 80`; read: `00 02` | +| 0xB6 | мощность/параметр (band, idx, val16?) | `01 01 01 96`; read: `00 01 00` | +| 0xB7 | частотные точки/оффсеты (band, idx, …) | `01 00 05 9a 01 08 75`; read: `00` | +| 0xB8 | мощность/параметр | `01 01 01 02 78`; read: `00 01 01 00` | +| 0xB3 | SQ/шум набор 1 | `01 01 02 02` | +| 0xB5 | SQ/шум набор 2 | `01 01 00 02` | + +Первый байт payload = **band**: `01`=UHF, `02`=VHF (в 0xB1); в set-командах `01`=UHF-set, +`00`=read/VHF. Частоты в 0xB1 — 32-битные BE в Гц. + +## Модель калибровки (из UI-методов и дефолтов) +- Панель делит диапазон на **15 частотных точек** (методы `cboxNN_1..15`, `nPowerH/L_1..15`, + `nSQN/W_1..15`, `nNoiseN/W_1..15`) — для каждой точки свои: мощность H/M/L, девиация (Mod1..3), + SQ, шумоподавление. +- Отдельно `num_U_DigFreq` / `num_V_DigFreq` — **смещение ЦИФРОВОЙ (DMR) частоты** UHF/VHF + (наш параметр!), `num_U_AnaFreq` — аналоговой. Плюс `chbox_U_DigFreq` (вкл). +- Кнопки: `btnCal_U_Freq` (автокалибровка частоты по прибору), `btnRead_U_Freq`, + `btnStop_U_Freq`; `btnRead_U_Mod/PWR`, `btnCal_U_Mod/PWR`; `btnTouchuan` = 透传 (прозрачный + режим TX для замера). `ButtonRead`/`btnWriteAll`/`ButtonWrite`; `SaveTuneData`/`SaveDigTuneData` + = запись+сохранение (это и есть «Finish»). +- Приём/разбор ответов: `Port_DataReceived` → `RecvDataOperation` (≈74 обработчика по cmd), + `DataSum` = контрольная сумма. + +## Заводские дефолты (из блоба) +``` +mod: b9 b9 b9 b9 b9 b9 (6×) # девиация-дефолт + 32 32 32 32 32 32 + 9b 9b 9b 9b 9b 9b +flags 00 ff 00 + 37 a0 38 6b 40 ab # ? + 06 02 06 02 0a 00 05 00 +15pt: 3a 3c 3f 41 44 47 4a 4b 4c 4d 4e 4f 50 51 52 # 15 частотных точек (растущие) + 80 00 80 + 12 e2 34 40 2a 4f b3 (×2) +``` + +## Что осталось (для скриптования) +Точное соответствие «num_U_DigFreq → cmd+байт» — в IL методов `btnRead_U_Freq_Click` / +`ReadDigDataToUI` / `SaveDigTuneData`. Кандидат — `0xB7` (частотные точки/оффсет) или отдельный +freq-cmd. Для ручной калибровки штатным тулом это не нужно: `num_U_DigFreq` крутится в UI. + +## План применения (см. dmr-tx-recovery.md, путь через 0x68) +Вариант A (штатный тул): пользователь открывает Tuning SW → Read → крутит `num_U_DigFreq` → +WriteAll → Save → сменить канал → PTT → BER на MMDVM. Перебор до минимума BER (без прибора). +Вариант B (скрипт): дореверсить cmd DigFreq → `tools/` слать 0xB7/0xBx через мост, MMDVM-фидбек. diff --git a/tools/a6_probe.py b/tools/a6_probe.py new file mode 100644 index 0000000..d85a226 --- /dev/null +++ b/tools/a6_probe.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Проверка: достижим ли RDA-debug/ATE-интерфейс модуля HF6853 через мост RT-4D. + +Рация в режиме «Обновление DMR чипа» (вкл. с зажатой MENU). Под WSL COM идёт +через powershell.exe. Отправляем сырые байты, печатаем сырой ответ. + +Тесты: + 1) 0x68 ping — жив ли мост (база). + 2) RDA knock (reg3) — отвечает ли чип на низкоуровневый debug (ждём content=0x80 + либо ack 11 13). Пробуем 115200 и 921600, с XON/XOFF и без. + +Ничего НЕ пишет в калибровку — только читающие кадры. +""" +import argparse, subprocess, sys + +PS = r''' +$p = New-Object System.IO.Ports.SerialPort "{port}",{baud},"None",8,"one" +$p.ReadTimeout = 150 +$p.Handshake = [System.IO.Ports.Handshake]::{hs} +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(): + 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, tx, baud=115200, hs="None", tmo=1500, idle=300): + txlist = ",".join(str(x) for x in tx) if tx else "" + script = PS.format(port=port, baud=baud, hs=hs, txlist=txlist, tmo=tmo, idle=idle) + 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 main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port") + a = ap.parse_args() + port = a.port or find_port() + print(f"[порт {port}]\n") + + ping = bytes([0x68,0x84,0x01,0x01,0x00,0x00,0x00,0x01,0x00,0x10]) + print("1) 0x68 ping @115200:") + rx = xfer(port, ping, 115200, "None") + print(" TX:", ping.hex(' ')) + print(" RX:", rx.hex(' ') if rx else "(нет ответа)") + alive = bool(rx and rx[0]==0x68) + print(" ->", "МОСТ ЖИВ" if alive else "нет 0x68-ответа") + + knock = bytes.fromhex("ad0007ff0403000000 01 f9".replace(" ","")) + print("\n2) RDA knock (read reg3), ждём content=0x80 или ack 11 13:") + for baud in (115200, 921600): + for hs in ("None","XOnXOff"): + rx = xfer(port, knock, baud, hs, tmo=800, idle=250) + tag = f" {baud} {hs:8}" + if not rx: + print(tag, "-> (нет ответа)") + else: + hexs = rx.hex(' ') + hit = " <== ПОХОЖЕ НА RDA!" if (b'\x80' in rx or rx[:2]==b'\x11\x13' or rx[:1]==b'\xad') else "" + print(tag, "->", hexs, hit) + + +if __name__ == "__main__": + main()