Рабочий рид-онли тюнер-протокол через мост + baseline калибровки модуля
tools/tuning_read.py: реплика Read-последовательности тюнера через 0x68-мост (--direct, без 34.. Connect, который в режиме Обновление DMR не отвечает). РАБОТАЕТ: модуль отвечает на CmdOpenCal + Read-команды. Снят baseline: DMR VCCN=0x40 vs аналог 0x80 (аналог рабочий) — вероятный виновник сдвига DMR TX. research/tuning-re/module-cal-baseline.txt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NikMGoqQWWk9wy2ww2vJAr
Этот коммит содержится в:
@@ -0,0 +1,14 @@
|
|||||||
|
RT-4D DMR-модуль (HF6853) — ЖИВАЯ калибровка, снята через мост (tools/tuning_read.py --direct)
|
||||||
|
Дата: 24.07.2026, режим «Обновление DMR чипа», 0x68 напрямую. Стабильно (3 прогона).
|
||||||
|
|
||||||
|
CmdOpenCal -> st=0x00 OK
|
||||||
|
ReadDigVCCN -> pl 00 02 40 => DMR VCCN(freq/VCXO trim) = 0x40 (64)
|
||||||
|
ReadAnaVCCN -> pl 00 02 80 => ANA VCCN = 0x80 (128) <- аналог работает
|
||||||
|
ReadPWR -> pl 00 01 01 00 05
|
||||||
|
ReadSlot -> pl 00 00 03 42 01 08 d9
|
||||||
|
ReadMod -> st=0x01 (нужен параметр/иначе)
|
||||||
|
CmdColseCal -> OK (+ async 68 06 .. ee ee.. = DetectDevice/пусто)
|
||||||
|
|
||||||
|
Гипотеза: VCCN = частотный трим VCXO (btnRead_U_Freq шлёт ReadDigVCCN). DMR(0x40) сильно ниже
|
||||||
|
аналога(0x80). Проверять сравнением BER на MMDVM при переборе SetDigVCCN. baseline для отката = 0x40.
|
||||||
|
ВНИМАНИЕ: писать только с возможностью отката; тест требует нормального режима рации + PTT.
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""RT-4D: РИД-ОНЛИ снятие калибровки DMR-модуля протоколом штатного тюнера.
|
||||||
|
|
||||||
|
Реплика последовательности Radio Tuning Software (реверс, docs/tuning-software-protocol.md):
|
||||||
|
одна сессия COM @115200, шлём кадры и ловим ответы. ТОЛЬКО ЧТЕНИЕ — ни одной write/set-команды
|
||||||
|
калибровки (CmdWriteAll/Set* НЕ шлём). Рация в режиме «Обновление DMR чипа».
|
||||||
|
|
||||||
|
Под WSL COM через powershell.exe. Порядок:
|
||||||
|
CmdConnect(34 52 05 10 9b) -> [опц. CmdOpenCal 68 b0..01] -> Read* (68 b?..) -> CmdColseCal -> CmdEnd
|
||||||
|
"""
|
||||||
|
import argparse, subprocess, sys, json
|
||||||
|
|
||||||
|
# --- команды (из реверса, research/tuning-re/command-templates.txt) ---
|
||||||
|
CMDS = {
|
||||||
|
"Ping": "68 84 01 01 00 00 00 01 00 10",
|
||||||
|
"CmdConnect": "34 52 05 10 9b",
|
||||||
|
"CmdOpenCal": "68 b0 01 01 00 00 00 01 01 10",
|
||||||
|
"ReadDigVCCN": "68 b4 01 01 00 00 00 02 00 02 10",
|
||||||
|
"ReadAnaVCCN": "68 b2 01 01 00 00 00 02 00 02 10",
|
||||||
|
"ReadMod": "68 b6 01 01 00 00 00 03 00 01 00 10",
|
||||||
|
"ReadPWR": "68 b8 01 01 00 00 00 04 00 01 01 00 10",
|
||||||
|
"ReadSlot": "68 b7 01 01 00 00 00 01 00 10",
|
||||||
|
"CmdColseCal": "68 b0 01 01 00 00 00 01 00 10",
|
||||||
|
"CmdEnd": "34 52 05 ee 79",
|
||||||
|
}
|
||||||
|
|
||||||
|
def hx(name): return bytes.fromhex(CMDS[name].replace(" ", ""))
|
||||||
|
|
||||||
|
# powershell: open port, run a sequence of (write, wait) steps, capture each response
|
||||||
|
PS = r'''
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$p = New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
|
||||||
|
$p.ReadTimeout = 60
|
||||||
|
try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit }}
|
||||||
|
$p.DiscardInBuffer(); $p.DiscardOutBuffer()
|
||||||
|
$steps = New-Object System.Collections.ArrayList
|
||||||
|
{steps}
|
||||||
|
foreach ($s in $steps) {{
|
||||||
|
$name = $s[0]; $tx = [byte[]]$s[1]
|
||||||
|
$p.DiscardInBuffer()
|
||||||
|
$p.Write($tx,0,$tx.Length)
|
||||||
|
Start-Sleep -Milliseconds {dwell}
|
||||||
|
$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 }}
|
||||||
|
}}
|
||||||
|
$hex = ($buf | ForEach-Object {{ "{{0:x2}}" -f $_ }}) -join ""
|
||||||
|
Write-Output ("RSP:"+$name+":"+$hex)
|
||||||
|
}}
|
||||||
|
$p.Close()
|
||||||
|
Write-Output "DONE"
|
||||||
|
'''
|
||||||
|
|
||||||
|
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 ps_arr(b): return "[byte[]]("+",".join(str(x) for x in b)+")"
|
||||||
|
|
||||||
|
def run(port, seq, dwell=120, tmo=700, idle=200):
|
||||||
|
steps = "\n".join(f"$null = $steps.Add(@('{n}',{ps_arr(hx(n))}))" for n in seq)
|
||||||
|
script = PS.format(port=port, steps=steps, dwell=dwell, tmo=tmo, idle=idle)
|
||||||
|
r = subprocess.run(["powershell.exe","-NoProfile","-Command",script],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
out=r.stdout.replace("\r","")
|
||||||
|
res=[]
|
||||||
|
for line in out.split("\n"):
|
||||||
|
if line.startswith("ERR:"): sys.exit(line)
|
||||||
|
if line.startswith("RSP:"):
|
||||||
|
_,name,hexs=line.split(":",2)
|
||||||
|
res.append((name, bytes.fromhex(hexs) if hexs else b""))
|
||||||
|
return res
|
||||||
|
|
||||||
|
def parse68(rx):
|
||||||
|
if len(rx)<9 or rx[0]!=0x68: return None
|
||||||
|
cmd=rx[1]; ln=(rx[6]<<8)|rx[7]; pl=rx[8:8+ln]
|
||||||
|
return dict(cmd=cmd, status=rx[3], length=ln, payload=pl)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap=argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--port")
|
||||||
|
ap.add_argument("--connect-only", action="store_true", help="только CmdConnect (проверка отклика)")
|
||||||
|
ap.add_argument("--direct", action="store_true", help="без 34.. Connect: 0x68 напрямую в модуль (режим Обновление DMR)")
|
||||||
|
ap.add_argument("--seq", help="произвольная последовательность имён через запятую")
|
||||||
|
a=ap.parse_args()
|
||||||
|
port=a.port or find_port()
|
||||||
|
print(f"[порт {port} @115200] РИД-ОНЛИ снятие калибровки\n")
|
||||||
|
|
||||||
|
if a.seq:
|
||||||
|
seq=[s.strip() for s in a.seq.split(",")]
|
||||||
|
elif a.connect_only:
|
||||||
|
seq=["CmdConnect"]
|
||||||
|
elif a.direct:
|
||||||
|
seq=["Ping","CmdOpenCal","ReadDigVCCN","ReadAnaVCCN","ReadMod","ReadPWR",
|
||||||
|
"ReadSlot","CmdColseCal"]
|
||||||
|
else:
|
||||||
|
seq=["CmdConnect","CmdOpenCal","ReadDigVCCN","ReadAnaVCCN","ReadMod","ReadPWR",
|
||||||
|
"ReadSlot","CmdColseCal","CmdEnd"]
|
||||||
|
res=run(port, seq)
|
||||||
|
if not res:
|
||||||
|
print("Нет ответов от рации (powershell не вернул RSP). Порт/режим?")
|
||||||
|
return
|
||||||
|
for name,rx in res:
|
||||||
|
tag=f" {name:12s} TX={CMDS[name]}"
|
||||||
|
if not rx:
|
||||||
|
print(tag+" RX=(нет ответа)")
|
||||||
|
else:
|
||||||
|
p=parse68(rx)
|
||||||
|
extra=f" [cmd=0x{p['cmd']:02X} st=0x{p['status']:02X} len={p['length']} pl={p['payload'].hex(' ')}]" if p else ""
|
||||||
|
print(tag+f" RX={rx.hex(' ')}"+extra)
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
main()
|
||||||
Ссылка в новой задаче
Block a user