- Полный 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>
128 строки
4.5 KiB
Python
128 строки
4.5 KiB
Python
#!/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()
|