- Полный RE стока V3.25 (Cortex-M4F) + FM100B: карта памяти, протокол, codeplug, UI-архитектура - Русификация: свой CP1251-шрифт + патч рендера, перевод меню и надписей, ребренд Ru-4D V3.25 - Блюпринт переделки UI + C-тулчейн (clang thumbv7em), доказан инъекцией - Готовые флешеры: WebSerial .html и Windows .exe со вшитой прошивкой - Дамп SPI рации, стоковая прошивка, инструменты сборки Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 строки
3.3 KiB
Python
111 строки
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Robust RT-4D application flasher (bootloader / 0x39 protocol).
|
|
|
|
Replicates the official DualTachyon flasher-cli handshake:
|
|
1. 0xFF probe — spam 0xFF until the bootloader echoes 0xFF
|
|
2. erase — two-part official-updater sequence (checksum seed 0x48)
|
|
[0x39,0x33,0x05,0x10] then [0x39,0x33,0x05,0x55]
|
|
3. write — [0x57, off>>8, off] + 1024B, checksum seed 0x48, ACK 0x06
|
|
|
|
Only the APPLICATION region is erased/written; the bootloader at
|
|
0x08000000..0x08002800 is never touched, so a failed flash is always
|
|
recoverable by reflashing the stock app image.
|
|
|
|
Usage: python flash_rt4d.py COM7 <firmware.bin>
|
|
"""
|
|
import sys, time, serial
|
|
|
|
SEED = 0x48
|
|
ACK = 0x06
|
|
|
|
|
|
def cksum(payload):
|
|
s = SEED
|
|
for b in payload:
|
|
s = (s + b) & 0xFF
|
|
return bytes(payload) + bytes([s])
|
|
|
|
|
|
def handshake(p, timeout=5.0):
|
|
p.reset_input_buffer()
|
|
t0 = time.time()
|
|
while time.time() - t0 < timeout:
|
|
b = p.read(1)
|
|
if not b:
|
|
p.write(bytes([0xFF]))
|
|
elif b == b"\xff":
|
|
return True
|
|
# ignore any other stray byte and keep probing
|
|
return False
|
|
|
|
|
|
def erase(p):
|
|
for trig in (0x10, 0x55):
|
|
p.reset_input_buffer()
|
|
p.write(cksum([0x39, 0x33, 0x05, trig]))
|
|
# wait up to ~4s for ACK
|
|
ok = False
|
|
t0 = time.time()
|
|
while time.time() - t0 < 4.0:
|
|
b = p.read(1)
|
|
if b == bytes([ACK]):
|
|
ok = True
|
|
break
|
|
if b: # unexpected byte
|
|
pass
|
|
if not ok:
|
|
raise SystemExit(f"[E] erase phase 0x{trig:02X}: no ACK")
|
|
print(f"[i] erase 0x{trig:02X}: ACK")
|
|
|
|
|
|
def write_fw(p, fw):
|
|
# pad up to a 1024 multiple with 0xFF (erased-flash value)
|
|
pad = (-len(fw)) % 1024
|
|
data = fw + b"\xff" * pad
|
|
total = len(data)
|
|
for off in range(0, total, 1024):
|
|
chunk = data[off:off + 1024]
|
|
payload = bytes([0x57, (off >> 8) & 0xFF, off & 0xFF]) + chunk
|
|
p.reset_input_buffer()
|
|
p.write(cksum(payload))
|
|
# ACK
|
|
ok = False
|
|
t0 = time.time()
|
|
while time.time() - t0 < 2.0:
|
|
b = p.read(1)
|
|
if b == bytes([ACK]):
|
|
ok = True
|
|
break
|
|
if not ok:
|
|
raise SystemExit(f"\n[E] write at 0x{off:05X}: no ACK "
|
|
f"(wrote {off}/{total})")
|
|
pct = (off + len(chunk)) * 100 // total
|
|
print(f"\r[i] writing 0x{off:05X}/{total:#07x} ({pct:3d}%)", end="", flush=True)
|
|
print("\n[i] write complete")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
raise SystemExit(__doc__)
|
|
port, path = sys.argv[1], sys.argv[2]
|
|
fw = open(path, "rb").read()
|
|
print(f"[i] firmware {path}: {len(fw)} bytes (0x{len(fw):X})")
|
|
p = serial.Serial(port, 115200, timeout=0.3, write_timeout=3)
|
|
try:
|
|
print("[i] handshake (0xFF probe)...")
|
|
if not handshake(p):
|
|
raise SystemExit("[E] no bootloader (radio not in flash mode). "
|
|
"Power off, hold * (or the flash-mode key) and power on.")
|
|
print("[i] bootloader ready")
|
|
print("[i] erasing application region...")
|
|
erase(p)
|
|
print("[i] flashing...")
|
|
write_fw(p, fw)
|
|
print("[i] DONE. Power-cycle the radio normally.")
|
|
finally:
|
|
p.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|