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 удалений
+141
Просмотреть файл
@@ -0,0 +1,141 @@
import re, sys, json
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
assert len(data) == 4194304
REGIONS = [
("calibration", 0x000000, 0x001000),
("main_settings", 0x002000, 0x001000),
("channels", 0x004000, 0x00C000),
("zones", 0x01C000, 0x020000),
("contacts", 0x05C000, 0x010000),
("groups", 0x07C000, 0x003000),
("dmr_keys", 0x082000, 0x003000),
("call_log", 0x088000, 0x00C000),
("default_sms", 0x094000, 0x001000),
("msg_drafts", 0x095000, 0x010000),
("msg_inbox", 0x0A5000, 0x010000),
("msg_outbox", 0x0B5000, 0x010000),
("schedules", 0x0C6000, 0x008000),
("dtmf_names", 0x0C7000, 0x000100),
("fm_settings", 0x0D6000, 0x001000),
]
def region_of(addr):
for name, a, s in REGIONS:
if a <= addr < a+s:
return name
return "UNMAPPED"
# printable set for ascii
def is_print(b):
return 0x20 <= b <= 0x7E
def looks_real(s):
"""Filter out punctuation-run / repeated-char noise."""
s2 = s.strip()
if len(s2) < 3:
return False
# must contain at least one letter or digit
if not re.search(r"[A-Za-z0-9一-鿿]", s2):
return False
# count alnum ratio
alnum = sum(1 for c in s2 if c.isalnum() or ord(c) > 0x2000)
if alnum / len(s2) < 0.4:
return False
# reject if single char repeated (e.g. "7777", "((((")
uniq = set(s2)
if len(uniq) <= 2 and len(s2) >= 4:
# allow things like "10" no; but "AA" reject; require >2 unique for long
return False
# reject runs of same punctuation
if re.fullmatch(r"(.)\1*", s2):
return False
return True
results = [] # (addr, encoding, region, text)
# ---- ASCII runs (min 3) ----
run_start = None
for i, b in enumerate(data):
if is_print(b):
if run_start is None:
run_start = i
else:
if run_start is not None:
s = data[run_start:i].decode("latin-1")
if len(s) >= 3 and looks_real(s):
results.append((run_start, "ascii", region_of(run_start), s))
run_start = None
if run_start is not None:
s = data[run_start:len(data)].decode("latin-1")
if len(s) >= 3 and looks_real(s):
results.append((run_start, "ascii", region_of(run_start), s))
# ---- UTF-16LE runs ----
# pattern: printable-ascii byte followed by 0x00, repeated
i = 0
n = len(data)
while i < n-1:
if is_print(data[i]) and data[i+1] == 0x00:
j = i
chars = []
while j < n-1 and data[j+1] == 0x00 and (is_print(data[j]) or data[j]==0x00):
if data[j] == 0x00:
break
chars.append(chr(data[j]))
j += 2
s = "".join(chars)
if len(s) >= 3 and looks_real(s):
results.append((i, "utf16le", region_of(i), s))
i = j + 2 if j > i else i+2
else:
i += 1
# ---- GBK / chinese: try decode ascii runs that had high bytes? separate scan ----
# Scan for GBK multibyte sequences (lead 0x81-0xFE, trail 0x40-0xFE)
def gbk_scan():
out=[]
i=0
while i < n:
b=data[i]
# start a candidate if ascii-print or gbk lead
if is_print(b) or (0x81<=b<=0xFE and i+1<n and 0x40<=data[i+1]<=0xFE and data[i+1]!=0x7F):
start=i
raw=bytearray()
while i<n:
b=data[i]
if is_print(b):
raw.append(b); i+=1
elif 0x81<=b<=0xFE and i+1<n and 0x40<=data[i+1]<=0xFE and data[i+1]!=0x7F:
raw.append(b); raw.append(data[i+1]); i+=2
else:
break
try:
s=bytes(raw).decode("gbk")
except Exception:
s=None
if s and re.search(r"[一-鿿]", s): # only keep if real chinese char present
if looks_real(s):
out.append((start,"gbk",region_of(start),s))
else:
i+=1
return out
results += gbk_scan()
# dedup: prefer utf16/gbk over ascii at same-ish addr
results.sort(key=lambda r:(r[0], r[1]))
# Remove ascii substrings that are actually part of utf16 (every other byte) -- keep both but mark
for addr, enc, reg, text in results:
pass
# Print
with open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/strings_out.txt","w",encoding="utf-8") as f:
for addr, enc, reg, text in results:
f.write(f"0x{addr:06X}\t{enc}\t{reg}\t{repr(text)}\n")
print(f"TOTAL candidate real strings: {len(results)}")
from collections import Counter
print("By region:", dict(Counter(r[2] for r in results)))
print("By encoding:", dict(Counter(r[1] for r in results)))