Вариант A: загрузчик HKLFBOOT write-only (read флеша нет) — дамп через него недостижим
Загрузчик ловится/держится на Linux (catch link-check + SetBaudRate смена бода). Но read-команды флеша нет (0x03/08/09/0a/0b не дают флеш; 0x08 = битовая карта). Сходится с DMR Upgrade Tool (только erase/write). Дамп только физически (SPI-клипса). Но BK4819 найден -> TX-фикс без дампа. tools/bk_module_dump.py, bk_bootloader_catch.py, bk_read_probe.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NikMGoqQWWk9wy2ww2vJAr
Этот коммит содержится в:
@@ -157,3 +157,15 @@ download-режим) при владельце, с риском. Пока вла
|
||||
`tools/bk_bootloader_catch.py` (ловит баннер/линк), `tools/bk_deinterleave.py` (снять CRC с дампа).
|
||||
- Дамп флеша модуля разблокируется, как найдём HKLF-хендшейк (реверс прошивки AT32 «Update DMR Chip»,
|
||||
либо аккуратный перебор в boot-окне). Тогда получим RF Firmware (AMBE) + NET info (NV).
|
||||
|
||||
## Вариант A (дамп через загрузчик) — РЕЗУЛЬТАТ (24.07, на Linux 10.0.0.63)
|
||||
Загрузчик HKLFBOOT ловится и держится (нативный Linux, /dev/ttyACM0, tools/bk_module_dump.py):
|
||||
catch link-check `01 e0 fc 01 00`→`04 0e 05...`, затем **SetBaudRate short CMD 0x0F <baud:4> <delay>**
|
||||
с РЕАЛЬНОЙ сменой бода (921600/2M) — фиксирует download-режим (иначе модуль грузит app).
|
||||
Фрейминг: preamble `01 E0 FC`, short `<len><cmd>`, long `FF F4 <len16 LE><cmd>`. Erase=long 0x0F, Write=0x10.
|
||||
**ПРОБЛЕМА: команды ЧТЕНИЯ флеша нет.** Пробовал read-опкоды 0x03/0x08/0x09/0x0a/0x0b (long, addr:4 LE):
|
||||
0x09/0x0a/0x0b/0x03 — пусто; 0x08@0x1000 вернул 580 байт ТОЛЬКО из 0x00/0x80 (битовая карта/статус,
|
||||
не флеш; другие адреса пусто). Сходится с DMR Upgrade Tool — у него только Connect/SetBaud/Erase/Write,
|
||||
**READ отсутствует**. Вывод: **загрузчик write-only**, дамп флеша через него недостижим.
|
||||
Остаётся физический дамп (SPI-клипса CH341A на флеш-чип модуля) — при вскрытии.
|
||||
НО: ВЧ-чип BK4819 уже найден → TX-фикс и калибровка достижимы БЕЗ дампа, через 0x68 SPCali + мануал BK4819.
|
||||
|
||||
@@ -1,61 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Поймать Beken-загрузчик модуля во время включения (через мост RT-4D, WSL/powershell).
|
||||
"""Поймать загрузчик модуля (HKLFBOOT) — ТАЙТОВЫЙ спам Connect (как DMR Upgrade Tool).
|
||||
|
||||
Формат флеша модуля = стандартный Beken (32+2 CRC), значит у него Beken UART-загрузчик,
|
||||
который слушает link-check КОРОТКОЕ окно после сброса. Этот тул одним процессом powershell
|
||||
в тайтовом цикле спамит link-check `01 E0 FC 01 00` и ловит ответ `04 0E 05 ...`.
|
||||
DMR Upgrade Tool: жмёшь Upgrade (спам Connect=01 E0 FC 01 00), затем MENU+включение —
|
||||
загрузчик ловит connect в окне до старта приложения. Наш powershell-цикл со Start-Sleep был
|
||||
слишком медленный и промахивался. Здесь — спам БЕЗ пауз (тысячи/сек), ловит короткое окно.
|
||||
|
||||
Использование: запусти тул, и в течение его работы ВЫКЛЮЧИ и включи рацию в режим
|
||||
«Обновление DMR чипа» (питание + MENU). Если поймаем ответ — загрузчик достижим через мост.
|
||||
|
||||
ТОЛЬКО ЧТЕНИЕ/handshake — ни erase, ни write не шлём.
|
||||
Процедура: запусти тул, затем зажми MENU и включи рацию. Успех = ответ загрузчика на link-check
|
||||
(04 0E 05 ...) и приложение (0x68 06 02) НЕ стартует. Только connect — ни erase, ни write.
|
||||
"""
|
||||
import argparse, subprocess, sys
|
||||
import argparse, subprocess
|
||||
|
||||
PS = r'''
|
||||
$port="{port}"; $sec={sec}
|
||||
$p = New-Object System.IO.Ports.SerialPort $port,115200,"None",8,"one"
|
||||
$p.ReadTimeout=20; $p.WriteTimeout=50
|
||||
$p = New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
|
||||
$p.ReadTimeout=1; $p.WriteTimeout=50; $p.ReadBufferSize=8192
|
||||
try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit }}
|
||||
$lc=[byte[]]@(1,224,252,1,0) # 01 E0 FC 01 00
|
||||
$lc=[byte[]]@(1,224,252,1,0)
|
||||
$sw=[Diagnostics.Stopwatch]::StartNew()
|
||||
$hit=$false
|
||||
while($sw.Elapsed.TotalSeconds -lt $sec){{
|
||||
try {{ $p.Write($lc,0,$lc.Length) }} catch {{}}
|
||||
Start-Sleep -Milliseconds 8
|
||||
$acc=New-Object System.Collections.Generic.List[byte]
|
||||
$caught=$false
|
||||
while($sw.Elapsed.TotalSeconds -lt {sec}){{
|
||||
if(-not $caught){{ try {{ $p.Write($lc,0,$lc.Length) }} catch {{}} }}
|
||||
$n=$p.BytesToRead
|
||||
if($n -gt 0){{
|
||||
$b=New-Object byte[] $n; $p.Read($b,0,$n)|Out-Null
|
||||
foreach($x in $b){{ $acc.Add($x) }}
|
||||
$hex=($b|ForEach-Object{{"{{0:x2}}"-f $_}}) -join ""
|
||||
if($hex -match "e0fc"){{ Write-Output ("HIT:"+$hex); $hit=$true; break }}
|
||||
else {{ Write-Output ("RX:"+$hex) }}
|
||||
Write-Output ("t"+[int]$sw.Elapsed.TotalSeconds+"|"+$hex)
|
||||
# ответ загрузчика на link-check: 04 0e 05 ...
|
||||
if($hex -match "040e05"){{ Write-Output "CAUGHT"; $caught=$true }}
|
||||
}}
|
||||
}}
|
||||
$p.Close()
|
||||
if(-not $hit){{ Write-Output "NOHIT" }}
|
||||
$p.Close(); Write-Output "END"
|
||||
'''
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser()
|
||||
ap.add_argument("--port", default="COM4")
|
||||
ap.add_argument("--sec", type=int, default=25)
|
||||
ap.add_argument("--port", default="COM4"); ap.add_argument("--sec", type=int, default=30)
|
||||
a=ap.parse_args()
|
||||
print(f"[спам link-check на {a.port} @115200, {a.sec}с]")
|
||||
print(">>> СЕЙЧАС выключи рацию и включи в режим «Обновление DMR чипа» (питание+MENU) <<<\n")
|
||||
print(f"[ТАЙТОВЫЙ спам 01 e0 fc 01 00 на {a.port}, {a.sec}с]")
|
||||
print(">>> ЗАЖМИ MENU и включи рацию СЕЙЧАС (как при прошивке модуля) <<<\n")
|
||||
r=subprocess.run(["powershell.exe","-NoProfile","-Command",PS.format(port=a.port,sec=a.sec)],
|
||||
capture_output=True, text=True)
|
||||
banner=app=caught=False; n=0
|
||||
for line in r.stdout.replace("\r","").split("\n"):
|
||||
line=line.strip()
|
||||
if line.startswith("HIT:"):
|
||||
print(f"✅ ЗАГРУЗЧИК ОТВЕТИЛ: {line[4:]}")
|
||||
print(" -> Beken-загрузчик достижим через мост. Можно читать флеш.")
|
||||
elif line.startswith("RX:") and line[3:]:
|
||||
print(f" (эхо/данные: {line[3:][:60]})")
|
||||
elif line=="NOHIT":
|
||||
print("❌ Ответа загрузчика не поймано. Либо мост не пробрасывает загрузчик,")
|
||||
print(" либо окно/бод другие. Попробуй ещё раз или обсудим физ.доступ.")
|
||||
elif line.startswith("ERR:"):
|
||||
print(line)
|
||||
if line.startswith("ERR:"): print(line); return
|
||||
if line=="CAUGHT": caught=True; continue
|
||||
if "|" not in line: continue
|
||||
t,hexs=line.split("|",1); n+=1
|
||||
try: asc=bytes.fromhex(hexs).decode("latin1")
|
||||
except: asc=""
|
||||
asc="".join(c if 32<=ord(c)<127 else "." for c in asc)
|
||||
print(f" {t}: {asc[:48]} [{hexs[:40]}]")
|
||||
if "HKLF" in asc: banner=True
|
||||
if "680602" in hexs: app=True
|
||||
print()
|
||||
if caught: print("✅✅✅ ЗАГРУЗЧИК ПОЙМАН (ответ 04 0e 05)! Держим сессию — можно читать флеш.")
|
||||
elif banner and app: print("⚠️ Баннер+приложение стартовало. Окно ещё промахнули — повтори (спам уже без пауз).")
|
||||
elif banner: print("✅ Баннер был, приложение не стартовало — возможно, поймали. Проверю ответ.")
|
||||
elif not n: print("Тишина — не попал во включение? Порт COM4? Повтори.")
|
||||
else: print("Данные без баннера — перезагрузка вне окна. Повтори.")
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Beken-загрузчик модуля RT-4D: catch -> SetBaudRate(hold) -> read (Linux, /dev/ttyACM0).
|
||||
Фрейминг из DMR Upgrade Tool: preamble 01 E0 FC; short: <len> <cmd> <payload>;
|
||||
long: FF F4 <len16 LE> <cmd> <payload>. SetBaudRate=short CMD 0x0F <baud:4 LE> <delay>.
|
||||
Erase=long CMD 0x0F. Read пробуем long CMD 0x09/0x03/0x0a. ТОЛЬКО чтение (никаких erase/write).
|
||||
"""
|
||||
import serial, time, sys, argparse
|
||||
|
||||
def long_frame(cmd, payload=b""):
|
||||
b=bytes([cmd])+payload; ln=len(b)
|
||||
return bytes([1,0xE0,0xFC,0xFF,0xF4, ln&0xFF,(ln>>8)&0xFF])+b
|
||||
def short_frame(cmd, payload=b""):
|
||||
b=bytes([cmd])+payload; ln=len(b)
|
||||
return bytes([1,0xE0,0xFC, ln&0xFF])+b
|
||||
def le32(v): return bytes([v&0xFF,(v>>8)&0xFF,(v>>16)&0xFF,(v>>24)&0xFF])
|
||||
|
||||
def catch(sp, secs):
|
||||
lc=bytes([1,0xE0,0xFC,1,0]); roll=b""; t0=time.time(); banner=False
|
||||
while time.time()-t0<secs:
|
||||
sp.write(lc)
|
||||
n=sp.in_waiting
|
||||
if n:
|
||||
roll=(roll+sp.read(n))[-400:]
|
||||
if b"HKLF" in roll: banner=True
|
||||
if b"\x04\x0e\x05" in roll: return True, banner
|
||||
return False, banner
|
||||
|
||||
def drain(sp, ms):
|
||||
q=time.time()
|
||||
while (time.time()-q)*1000<ms:
|
||||
n=sp.in_waiting
|
||||
if n: sp.read(n); q=time.time()
|
||||
sp.reset_input_buffer()
|
||||
|
||||
def readone(sp, cmd, addr, tries=3):
|
||||
for _ in range(tries):
|
||||
sp.reset_input_buffer()
|
||||
sp.write(long_frame(cmd, le32(addr)))
|
||||
time.sleep(0.04)
|
||||
buf=b""; q=time.time()
|
||||
while (time.time()-q)<0.25:
|
||||
n=sp.in_waiting
|
||||
if n: buf+=sp.read(n); q=time.time()
|
||||
else: time.sleep(0.003)
|
||||
if buf: return buf
|
||||
return b""
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser()
|
||||
ap.add_argument("--port",default="/dev/ttyACM0"); ap.add_argument("--secs",type=int,default=45)
|
||||
ap.add_argument("--baud",type=int,default=921600,help="baud для SetBaudRate (0=не слать)")
|
||||
ap.add_argument("--rcmds",default="9,3,10")
|
||||
a=ap.parse_args()
|
||||
sp=serial.Serial(a.port,115200,timeout=0)
|
||||
print(f"[{a.port}] MENU+включение рации СЕЙЧАС (окно {a.secs}с)",flush=True)
|
||||
ok,banner=catch(sp,a.secs)
|
||||
if not ok:
|
||||
print(f" не поймал (баннер={banner}) — повтори",flush=True); return
|
||||
print(" ✅ пойман link-check",flush=True)
|
||||
if a.baud:
|
||||
# SetBaudRate — фиксирует download-режим
|
||||
sb=short_frame(0x0F, le32(a.baud)+bytes([0x0F]))
|
||||
sp.write(sb); time.sleep(0.08)
|
||||
if a.baud!=115200:
|
||||
sp.baudrate=a.baud; time.sleep(0.05)
|
||||
drain(sp,150)
|
||||
print(f" SetBaudRate({a.baud}) + порт на {sp.baudrate}",flush=True)
|
||||
# comprehensive: опкоды на 0x1000 + диапазон для 0x08
|
||||
for c in (0x03,0x08,0x09,0x0a,0x0b):
|
||||
r=readone(sp,c,0x1000,tries=2)
|
||||
nz=sum(1 for x in r if x!=0)
|
||||
print(f" op0x{c:02x}@0x1000: len={len(r)} nz={nz} f64={r[:64].hex(" ")}",flush=True)
|
||||
print(" -- 0x08 диапазон --",flush=True)
|
||||
for addr in (0x1000,0x1400,0x1800,0x1c00,0x0400,0x0000):
|
||||
r=readone(sp,0x08,addr,tries=2)
|
||||
nz=sum(1 for x in r if x!=0)
|
||||
print(f" 0x08@0x{addr:04x}: len={len(r)} nz={nz} f32={r[:32].hex(" ")}",flush=True)
|
||||
sp.close()
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Зонд чтения флеша Beken-загрузчика модуля: catch link-check -> один read4k(0) -> сырой ответ.
|
||||
|
||||
Фрейминг (из DMR Upgrade Tool SendEraseFlashReq): 01 E0 FC FF F4 <len_lo> <len_hi> <CMD> <addr:4 LE> ...
|
||||
Erase=0x0F. Read4K пробуем CMD 0x09: 01 E0 FC FF F4 05 00 09 <addr:4 LE>. ТОЛЬКО ЧТЕНИЕ.
|
||||
|
||||
Процедура: запусти, затем зажми MENU и включи рацию. Тул поймает загрузчик и прочитает 1 сектор
|
||||
по адресу 0 — покажет формат ответа (по нему построим полный дамп).
|
||||
"""
|
||||
import argparse, subprocess
|
||||
|
||||
PS = r'''
|
||||
$p=New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
|
||||
$p.ReadTimeout=1; $p.WriteTimeout=50; $p.ReadBufferSize=16384
|
||||
try{{$p.Open()}}catch{{Write-Output "ERR:$($_.Exception.Message)";exit}}
|
||||
$lc=[byte[]]@(1,224,252,1,0)
|
||||
# read4k addr=0: 01 E0 FC FF F4 05 00 09 00 00 00 00
|
||||
$rd=[byte[]]@(1,224,252,255,244,5,0,{cmd},0,0,0,0)
|
||||
$sw=[Diagnostics.Stopwatch]::StartNew(); $state="catch"; $roll=""
|
||||
while($sw.Elapsed.TotalSeconds -lt {sec}){{
|
||||
if($state -eq "catch"){{
|
||||
try{{$p.Write($lc,0,$lc.Length)}}catch{{}}
|
||||
$n=$p.BytesToRead
|
||||
if($n -gt 0){{
|
||||
$b=New-Object byte[] $n;$p.Read($b,0,$n)|Out-Null
|
||||
$hex=($b|ForEach-Object{{"{{0:x2}}"-f $_}}) -join ""
|
||||
$roll=($roll+$hex); if($roll.Length -gt 400){{$roll=$roll.Substring($roll.Length-400)}}
|
||||
if($hex -match "484b4c46"){{ Write-Output "BANNER" }}
|
||||
if($roll -match "040e05"){{
|
||||
Write-Output "CATCH"
|
||||
# слить бэклог link-check ответов до тишины (~250мс без байт)
|
||||
$q=[Diagnostics.Stopwatch]::StartNew(); $last=0
|
||||
while($q.ElapsedMilliseconds -lt 400){{
|
||||
$m=$p.BytesToRead
|
||||
if($m -gt 0){{ $tb=New-Object byte[] $m;$p.Read($tb,0,$m)|Out-Null; $last=$q.ElapsedMilliseconds }}
|
||||
}}
|
||||
$p.DiscardInBuffer()
|
||||
$p.Write($rd,0,$rd.Length) # read4k
|
||||
$state="read"; $rsw=[Diagnostics.Stopwatch]::StartNew()
|
||||
$acc=New-Object System.Collections.Generic.List[byte]
|
||||
}}
|
||||
}}
|
||||
}} else {{
|
||||
$n=$p.BytesToRead
|
||||
if($n -gt 0){{ $b=New-Object byte[] $n;$p.Read($b,0,$n)|Out-Null; foreach($x in $b){{$acc.Add($x)}} }}
|
||||
if($rsw.Elapsed.TotalMilliseconds -gt 2500){{
|
||||
$hx=($acc.ToArray()|ForEach-Object{{"{{0:x2}}"-f $_}}) -join ""
|
||||
Write-Output ("RDLEN:"+$acc.Count)
|
||||
Write-Output ("RDHEAD:"+$hx.Substring(0,[Math]::Min(96,$hx.Length)))
|
||||
if($hx.Length -gt 96){{ Write-Output ("RDTAIL:"+$hx.Substring($hx.Length-32)) }}
|
||||
break
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
$p.Close(); Write-Output "END"
|
||||
'''
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser()
|
||||
ap.add_argument("--port",default="COM4"); ap.add_argument("--sec",type=int,default=30)
|
||||
ap.add_argument("--cmd",default="9",help="опкод чтения (9=read4k, 3=readsector)")
|
||||
a=ap.parse_args()
|
||||
print(f"[зонд чтения, CMD 0x{int(a.cmd):02x}, {a.port}, {a.sec}с] — ЗАЖМИ MENU и включи рацию\n")
|
||||
r=subprocess.run(["powershell.exe","-NoProfile","-Command",
|
||||
PS.format(port=a.port,sec=a.sec,cmd=int(a.cmd))],capture_output=True,text=True)
|
||||
for line in r.stdout.replace("\r","").split("\n"):
|
||||
line=line.strip()
|
||||
if line.startswith(("ERR:","CATCH","RDLEN:","RDHEAD:","RDTAIL:")): print(" "+line)
|
||||
print("\n(RDLEN ~4096+ и RDHEAD с данными = read4k работает. RDLEN 0/мало = опкод неверный, пробуй --cmd 3)")
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
Ссылка в новой задаче
Block a user