RT-4D: реверс прошивки, русификация, кастомный UI, флешеры

- Полный 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>
Этот коммит содержится в:
2026-07-08 15:47:22 +09:00
co-authored by Claude Opus 4.8
Коммит ae36c3b729
72 изменённых файлов: 24124 добавлений и 0 удалений
+127
Просмотреть файл
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Build RT-4D app image with narrow single-byte CP1251 Cyrillic support.
Mechanism (all in the recoverable MCU app region, no SPI writes):
1. dispatch patch @0x08008A6A: cmp #0x80 -> #0xFF (bytes 0x80..0xFE take the
narrow 7px single-byte path instead of the 14px CJK double-byte path)
2. blitter redirect @0x08007FDA: bl SPI_read -> bl CAVE
3. CAVE routine (appended): for glyph src addr >= 0x0019C540 (font index >=96,
i.e. byte>=0x80) memcpy the 14-byte glyph from our in-flash Cyrillic table;
otherwise tail-call the original SPI_read (ASCII unchanged).
4. Cyrillic glyph table (appended): 128 cells (byte 0x80..0xFF) x 14 bytes,
column-major 7x14 (2 bytes/col LE, bit0=top) — same format as the ASCII font.
This test only fills the few glyphs needed to render one CP1251 word.
"""
import sys, struct
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB
BASE = 0x08002800
CAVE_ADDR = 0x08028860 # file 0x26060
CYR_BASE = 0x08028900 # file 0x26100 (glyphs for byte 0x80..0xFF)
SPI_READ = 0x08021828 # original glyph-load routine (thumb entry+1)
IMG_END = 0x08029000 # extend image to cover the font table
# ---- glyph editor: 14 rows x 7 cols, '#'=on ; encode column-major 2B/col LE --
# Glyphs are drawn <=6 columns wide (col 6 stays blank) so the fixed 7px advance
# always leaves >=1px between letters.
def enc(rows):
assert len(rows) == 14
out = bytearray()
for c in range(7):
v = 0
for r in range(14):
if c < len(rows[r]) and rows[r][c] == '#':
v |= 1 << r
out += struct.pack('<H', v)
assert struct.unpack('<H', out[12:14])[0] == 0, "col 6 must stay blank (<=6px wide)"
return bytes(out)
BL = " " # blank row (6 wide)
def G(*body, top=3):
rows = [BL]*14
for i, r in enumerate(body):
rows[top+i] = r
return enc(rows)
GLYPHS = { # CP1251 code -> 6px-wide 14-tall bitmap (col 6 blank => 1px gap)
0xC7: G(".####.","#....#",".....#","..###.",".....#","#....#",".####."), # З
0xEE: G(".####.","#....#","#....#","#....#",".####.", top=5), # о
0xED: G("#....#","#....#","######","#....#","#....#", top=5), # н
0xFB: G("#....#","#....#","##...#","#.#..#","##...#", top=5), # ы
}
def main():
inp = sys.argv[1] if len(sys.argv) > 1 else "rt4d_stock_v3.25_abs_0x08002800.bin"
outp = sys.argv[2] if len(sys.argv) > 2 else "rt4d_ru_font_test.bin"
stock = open(inp, "rb").read()
img = bytearray(b"\xff" * (IMG_END - BASE))
img[:len(stock)] = stock
def w(va, data):
img[va-BASE:va-BASE+len(data)] = data
ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
# --- CAVE ---
cave_asm = f"""
movw r3, #0xC540
movt r3, #0x0019
cmp r1, r3
blo do_spi
subs r1, r1, r3
movw r3, #{CYR_BASE & 0xFFFF}
movt r3, #{CYR_BASE >> 16}
adds r1, r1, r3
copy:
ldrb r3, [r1]
strb r3, [r0]
adds r1, r1, #1
adds r0, r0, #1
subs r2, r2, #1
bne copy
bx lr
do_spi:
movw r3, #{SPI_READ & 0xFFFF}
movt r3, #{SPI_READ >> 16}
orr r3, r3, #1
bx r3
"""
cave, _ = ks.asm(cave_asm, CAVE_ADDR)
cave = bytes(cave)
assert len(cave) <= (CYR_BASE - CAVE_ADDR), "cave too big"
w(CAVE_ADDR, cave)
# --- blitter redirect: bl CAVE at 0x08007FDA ---
bl, _ = ks.asm(f"bl #{CAVE_ADDR}", 0x08007FDA)
assert len(bl) == 4
orig = bytes(img[0x08007FDA-BASE:0x08007FDA-BASE+4])
w(0x08007FDA, bytes(bl))
# --- dispatch patch: cmp r0,#0x80 -> #0xFF at 0x08008A6A ---
dpo = 0x08008A6A - BASE
assert img[dpo] == 0x80 and img[dpo+1] == 0x28, f"unexpected {img[dpo]:02x} {img[dpo+1]:02x}"
img[dpo] = 0xFF
# --- Cyrillic glyph table ---
for code, g in GLYPHS.items():
w(CYR_BASE + (code-0x80)*14, g)
# --- menu string: "Zone Set 06" -> CP1251 "Зоны"+pad+"06" ---
rec = 0x0802543D
old = b"Zone Set 06"
assert bytes(img[rec-BASE:rec-BASE+16]) == old
new = "Зоны".encode("cp1251") + b" "*10 + b"06"
assert len(new) == 16, len(new)
w(rec, new)
# sanity: vectors intact
sp, rst = struct.unpack_from("<II", img, 0)
assert (sp, rst) == struct.unpack_from("<II", stock, 0)
open(outp, "wb").write(img)
print(f"cave {len(cave)}B @0x{CAVE_ADDR:08X}; bl {orig.hex()}->{bytes(bl).hex()}")
print(f'"Зоны" cp1251 = {new[:8].hex(" ")}')
print(f"image {len(img)} bytes -> {outp}")
if __name__ == "__main__":
main()