Баузбенд FM100B: - найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём - расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации - инструмент tools/fm100b.py (ping/send/raw/scan) - cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом) - разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд - cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14 - исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md) Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md): - вызывается как функция горячей клавиши №22 Analog Spectrum - вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318 - спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC Декомпозиция (docs/firmware-parts.md): - два процессора + внешний SPI = три канала внедрения - карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode - найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки) - дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке - аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля Прочее: - везде исправлен режим прошивки: тангента PTT вместо клавиши "*" - устаревший Ru-4D_Flasher.exe удалён из репозитория - добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
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 PTT 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()
|