#!/usr/bin/env python3 """Assemble the fully-russified RT-4D app image (bold font v2). - narrow single-byte CP1251 render (dispatch patch + blitter code-cave) - bold uppercase Cyrillic font: 13 letters reuse stock ASCII glyphs, 20 drawn - all 485 menu/option records translated to uppercase Russian All patches in the recoverable MCU app region; no SPI writes. (Selection-highlight patch intentionally omitted pending correct RE.) """ import struct, json from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB import glyphs_ru2 as GR BASE = 0x08002800 CAVE_ADDR = 0x08028860 CYR_BASE = 0x08028900 SPI_READ = 0x08021828 IMG_END = 0x08029000 ROOT = "C:/Users/vikto/Documents/Claude/rt-4d" DUMP = f"{ROOT}/radio-spi-dump.bin" ASCII_FONT = 0x19C000 def enc_glyph(rows): out = bytearray() for c in range(7): v = sum((1 << r) for r in range(14) if c < len(rows[r]) and rows[r][c] == '#') out += struct.pack('> 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 = bytes(ks.asm(cave_asm, CAVE_ADDR)[0]); w(CAVE_ADDR, cave) w(0x08007FDA, bytes(ks.asm(f"bl #{CAVE_ADDR}", 0x08007FDA)[0])) assert img[0x08008A6A-BASE] == 0x80; img[0x08008A6A-BASE] = 0xFF # bold Cyrillic font (reuse stock ASCII bytes where identical) -------------- for ch in GR.LETTERS: b = ch.encode("cp1251")[0] g = ascii_glyph(GR.REUSE[ch]) if ch in GR.REUSE else enc_glyph(GR.unique_rows(ch)) w(CYR_BASE + (b-0x80)*14, g) # translate menu records --------------------------------------------------- START = 0x080253ED - BASE; END = 0x0802723D - BASE changed = kept = 0 for i in range((END-START)//16): off = START + i*16; r = bytes(img[off:off+16]) if r[14:16].isdigit(): label = r[:14].rstrip(b' \x00').decode('latin1'); idd = r[14:16]; maxc = 14 else: label = r.rstrip(b' \x00').decode('latin1'); idd = b''; maxc = 16 if not label or label not in ru: kept += 1; continue rus = ru[label].encode('cp1251'); assert len(rus) <= maxc rec = rus + b' '*(maxc-len(rus)) + idd; assert len(rec) == 16 w(BASE+off, rec); changed += 1 # translate scattered rodata display strings — ALL occurrences (in place, # keep NUL terminator). Lookup covers both menu and rodata translations. rod = json.load(open(f"{ROOT}/rodata_ru.json", encoding="utf-8")) allx = json.load(open(f"{ROOT}/rodata_all.json", encoding="utf-8")) lut = {**ru, **rod} rchg = rskip = 0 for it in allx: va, text, maxc = it["va"], it["text"], it["maxc"] rus = lut.get(text) if not rus or rus == text: rskip += 1; continue b = rus.encode("cp1251") if len(b) > maxc or bytes(img[va-BASE:va-BASE+maxc]) != text.encode("latin1"): rskip += 1; continue w(va, b + b" "*(maxc-len(b))) rchg += 1 # inline fixed-length strings (not NUL-terminated) — exact-length patch INLINE = { 0x0800C767: (" Remote Stun: ", " УДАЛ.БЛОК: "), 0x0800C77C: ("Remote Stun: ", "УДАЛ.БЛОК: "), 0x0800C790: ("Remote Kill: ", "УДАЛ.ОТКЛ: "), 0x0800C7B8: ("Wake Up: ", "БУДИЛ: "), 0x08028705: ("Freq Mode", "ЧАСТОТА"), 0x0802871E: ("DTMF Input: _", "DTMF ВВОД: _"), 0x0802872E: ("Dial Number", "НАБОР НОМ."), 0x08028767: ("VFO MODE", "VFO РЕЖ"), 0x08028815: ("Unknown", "НЕИЗВ."), 0x0801FB44: ("Unread SMS : ","НЕПРОЧ.СМС :"), 0x08017AB8: ("Sending", "ОТПРАВ."), } ichg = 0 for va, (en, rus) in INLINE.items(): b = rus.encode("cp1251"); assert len(b) <= len(en), (en, rus) if bytes(img[va-BASE:va-BASE+len(en)]) != en.encode("latin1"): continue w(va, b + b" "*(len(en)-len(b))) ichg += 1 # version rebrand: "RT-4D V3.25" -> "Ru-4D V3.25" vva = 0x0800BA88 if bytes(img[vva-BASE:vva-BASE+16]) == b"VER :RT-4D V3.25": w(vva, b"VER :Ru-4D V3.25") assert struct.unpack_from(" {out}") if __name__ == "__main__": main()