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 удалений
+22
Просмотреть файл
@@ -0,0 +1,22 @@
import struct
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP, "rb") as f:
f.seek(0)
cal = f.read(0x1000)
print("len", len(cal))
# occupancy: find 0xFF runs
ff = sum(1 for b in cal if b == 0xff)
zero = sum(1 for b in cal if b == 0)
print("0xFF bytes:", ff, "0x00 bytes:", zero, "other:", 0x1000-ff-zero)
# hex dump first 0x400
def dump(off, length):
for i in range(off, off+length, 16):
row = cal[i:i+16]
hexs = " ".join(f"{b:02x}" for b in row)
asc = "".join(chr(b) if 32<=b<127 else "." for b in row)
print(f"{i:04x} {hexs} {asc}")
dump(0, 0x200)
+24
Просмотреть файл
@@ -0,0 +1,24 @@
import struct
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP, "rb") as f:
cal = f.read(0x1000)
MARK = bytes.fromhex("12e234402a4fb3")
# find all marker positions
positions = []
i = 0
while True:
j = cal.find(MARK, i)
if j < 0: break
positions.append(j)
i = j+1
print("marker positions (hex):", [hex(p) for p in positions])
print("count:", len(positions))
# deltas
deltas = [positions[k+1]-positions[k] for k in range(len(positions)-1)]
print("deltas:", deltas)
print("unique deltas:", sorted(set(deltas)))
# The marker appears TWICE back to back (7+7=14 bytes). So real record period is bigger.
# Let's look at where non-duplicate markers are. Marker at 0x24 and 0x2b (0x24+7). So pairs.
+48
Просмотреть файл
@@ -0,0 +1,48 @@
import struct
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP, "rb") as f:
cal = f.read(0x1000)
REC = 0x70 # 112
# marker pair at rec+0x02..0x0f (0x22 within a record starting at 0x20). Let's define records start at 0x20?
# But 0x00..0x1f is a header. Actually let's test both: if records start at 0, marker would be at 0x22 within rec0 -> offset 0x22.
# If records start at 0x20, then rec starts 0x20, marker at rec+0x02.
# Let's align records to start where the marker pair begins minus 2.
# positions of the FIRST marker of each pair: 0x22, 0x92, 0x102 ... step 0x70. So record_base = 0x22-0x02 = 0x20.
# Header region 0x00..0x1f (32 bytes).
hdr = cal[0:0x20]
print("HEADER 0x00-0x1f:")
print(" ".join(f"{b:02x}" for b in hdr))
print()
# number of records that fit: (0x1000-0x20)/0x70
base = 0x20
nrec = (0x1000 - base)//REC
print("records fit:", nrec, "leftover:", (0x1000-base) - nrec*REC)
# How many are non-zero (populated)?
def dump_rec(idx):
off = base + idx*REC
r = cal[off:off+REC]
print(f"--- record {idx} @ 0x{off:03x} ---")
for i in range(0, REC, 16):
row = r[i:i+16]
hexs = " ".join(f"{b:02x}" for b in row)
asc = "".join(chr(b) if 32<=b<127 else "." for b in row)
print(f" +{i:02x} {hexs} {asc}")
# Show all record byte0 (first byte) which seems to be an index/counter: 0a,11,00,00,...
print("\nrecord first bytes (offset +0x00) and +0x01:")
for idx in range(nrec):
off = base+idx*REC
print(f" rec{idx:2d} @0x{off:03x}: b0={cal[off]:02x} b1={cal[off+1]:02x}")
# detect last populated record (non all-zero)
last = -1
for idx in range(nrec):
off = base+idx*REC
if any(b!=0 for b in cal[off:off+REC]):
last = idx
print("last populated record index:", last)
+31
Просмотреть файл
@@ -0,0 +1,31 @@
import struct
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP, "rb") as f:
cal = f.read(0x1000)
base = 0x20
REC = 0x70
# HEADER 0x00-0x1f
hdr = cal[0:0x20]
print("HEADER bytes:", " ".join(f"{b:02x}" for b in hdr))
# try interpret as u16 LE and u32 LE
u16 = struct.unpack("<16H", hdr)
print("as u16 LE:", u16)
u32 = struct.unpack("<8I", hdr)
print("as u32 LE:", [hex(x) for x in u32])
# The tail 3a 3c 3f 41 44 47 4a 4b 4c 4d 4e 4f 50 51 52 80 looks like a small table of 16 ascending bytes
print("\nheader bytes 0x10-0x1f (ascending table?):", [hex(b) for b in hdr[0x10:0x20]])
print("diffs:", [hdr[0x10:0x20][i+1]-hdr[0x10:0x20][i] for i in range(15)])
# Dump all 15 populated records fully, grouped by 8-byte lines with offset labels
print("\n\n==== 15 populated records (each 0x70) ====")
for idx in range(15):
off = base+idx*REC
r = cal[off:off+REC]
print(f"\n### REC {idx} @0x{off:03x} b0={r[0]:02x} b1={r[1]:02x}")
for i in range(0, REC, 16):
row = r[i:i+16]
hexs = " ".join(f"{b:02x}" for b in row)
print(f" +{i:02x}: {hexs}")
+40
Просмотреть файл
@@ -0,0 +1,40 @@
import struct
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP, "rb") as f:
cal = f.read(0x1000)
base = 0x20
REC = 0x70
# Record layout hypothesis:
# +0x00 b0 : band index/tag (0a, 11, then 00)
# +0x01 b1 : flag 0x80 (band-valid?)
# +0x02..0x08 (7 bytes): const A "12 e2 34 40 2a 4f b3"
# +0x09..0x0f (7 bytes): const B (identical copy)
# +0x10.. : then 6 tables of 16 bytes each = 96 bytes -> 0x10+96 = 0x70. EXACT.
# So after 16-byte header (b0,b1 + 14 const bytes), 6 x 16-byte tables.
# 0x10-0x1f table0, 0x20-0x2f table1, 0x30-0x3f table2, 0x40-0x4f table3, 0x50-0x5f table4, 0x60-0x6f table5.
for idx in [0,1,2]:
off = base+idx*REC
r = cal[off:off+REC]
print(f"\n### REC {idx} @0x{off:03x} tag={r[0]:02x} flag={r[1]:02x}")
print(" constA:", " ".join(f"{b:02x}" for b in r[2:9]))
print(" constB:", " ".join(f"{b:02x}" for b in r[9:16]))
for t in range(6):
tab = r[0x10+t*16:0x10+t*16+16]
print(f" table{t} @+0x{0x10+t*16:02x}: " + " ".join(f"{b:02x}" for b in tab) + f" (dec: {[b for b in tab]})")
# The 16 header-tail bytes 3a..52 are an ascending curve of 16 => matches 16 columns.
# Interpret header:
hdr = cal[0:0x20]
print("\nHEADER analysis:")
print(" +0x00 u16:", struct.unpack("<H", hdr[0:2])[0])
print(" +0x02..0x07: ", " ".join(f"{b:02x}" for b in hdr[2:8]))
# 37 a0 38 6b 40 ab -> maybe two u24 or freq. as u32 at 0x02:
print(" +0x02 u32:", hex(struct.unpack("<I", hdr[2:6])[0]), struct.unpack("<I", hdr[2:6])[0])
print(" +0x04 u32:", hex(struct.unpack("<I", hdr[4:8])[0]), struct.unpack("<I", hdr[4:8])[0])
# 05 00 05 00 0a 00 05 00 -> four u16: 5,5,10,5
print(" +0x08 four u16:", struct.unpack("<4H", hdr[8:16]))
print(" +0x10..0x1f (16-col x-axis?):", [b for b in hdr[0x10:0x20]])
+48
Просмотреть файл
@@ -0,0 +1,48 @@
import struct
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP, "rb") as f:
cal = f.read(0x1000)
# The header +0x02: 37 a0 38 6b and +0x06: 40 ab ... Let's test freq*100000 interpretation.
# But RT-4D VHF band 136-174, UHF 400-480. As MHz*100000: 136MHz=13600000=0x00CF8500.
# header bytes 37 a0 38 6b 40 ab don't look like that. Try as MHz*? Let's see 0x6b38a037? no.
# Maybe the header tail 16 bytes [58,60,63,65,68,71,74,75,76,77,78,79,80,81,82,128] are the
# 15-point + terminator for RX/AGC or S-meter. The last is 0x80.
# KEY INSIGHT: tag bytes: rec0=0x0a, rec1=0x11. In the header at 0x0c-0x0f we saw "0a 00 05 00".
# Header +0x08: 05 00 05 00 0a 00 05 00 => (5,5,10,5). rec0 tag=0x0a=10, rec1 tag=0x11=17.
# Let's look: maybe tag = number of active frequency points? rec0=10, but table0 rec0 has 8 distinct then flat.
# Re-examine: table0 rec0: 2f 2d 2d 32 33 35 37 3e | 3e repeated? no: 3e then 37 37...
# Actually first 8 vary, last 8 are flat=0x37. rec0 tag=0x0a=10.
# rec1 tag=0x11=17, table0 rec1 all 16 vary (55..62..61).
# Hypothesis: tables have 16 columns = 16 frequency calibration points spanning the band.
# Let's just present the 6 tables clearly and label by likely meaning based on value ranges.
base=0x20; REC=0x70
labels_guess = ["TX_power_low? / bias","?","?","TX_power_high?","?","?"]
for idx in [0,1]:
off=base+idx*REC; r=cal[off:off+REC]
print(f"REC{idx} tag=0x{r[0]:02x}({r[0]}) flag=0x{r[1]:02x}")
for t in range(6):
tab=list(r[0x10+t*16:0x10+t*16+16])
rng=f"min={min(tab)} max={max(tab)}"
print(f" t{t} +0x{0x10+t*16:02x}: {tab} {rng}")
print()
# Check equality relationships between tables within rec1
r=cal[base+REC:base+2*REC]
t=[list(r[0x10+k*16:0x10+k*16+16]) for k in range(6)]
print("rec1 t3==t5?", t[3]==t[5])
r0=cal[base:base+REC]
t0=[list(r0[0x10+k*16:0x10+k*16+16]) for k in range(6)]
print("rec0 t3==t5?", t0[3]==t0[5])
print("rec0 t0 first8 vary, last8:", t0[0][8:])
# Reference const 12 e2 34 40 2a 4f b3 : could be a float? 40 34 e2 12 as BE float:
import struct as s
be = s.unpack(">f", bytes([0x40,0x34,0xe2,0x12]))[0]
le = s.unpack("<f", bytes([0x12,0xe2,0x34,0x40]))[0]
print("const first4 as float BE:", be, "LE:", le)
# 2a 4f b3 remaining
+31
Просмотреть файл
@@ -0,0 +1,31 @@
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP, "rb") as f:
cal = f.read(0x1000)
base=0x20; REC=0x70
# records 15..35 content
allzero=True
for idx in range(15,36):
off=base+idx*REC
seg=cal[off:min(off+REC,0x1000)]
if any(b!=0 for b in seg):
allzero=False
print("rec",idx,"nonzero")
print("records 15..35 all zero:", allzero)
# leftover tail
print("bytes 0xfe0..0xfff:", " ".join(f"{b:02x}" for b in cal[0xfe0:0x1000]))
# Where does last record 35 end? base+36*0x70 = 0x20+0xfc0 = 0xfe0. leftover 0xfe0..0xfff = 32 bytes
# check those 32 tail bytes
print("tail region 0xfe0-0xfff nonzero:", any(b!=0 for b in cal[0xfe0:0x1000]))
# Confirm const block identical across all 15 records
MARK=bytes.fromhex("12e234402a4fb3")
ok=True
for idx in range(15):
off=base+idx*REC
if cal[off+2:off+9]!=MARK or cal[off+9:off+16]!=MARK:
ok=False
print("const A&B identical in all 15 recs:", ok)
# tag bytes across records
print("tags:", [cal[base+i*REC] for i in range(15)])
print("flags:", [cal[base+i*REC+1] for i in range(15)])
+75
Просмотреть файл
@@ -0,0 +1,75 @@
import struct, collections
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
with open(DUMP,'rb') as f:
data = f.read()
assert len(data)==4194304
def hexdump(off, n=256, base=None):
if base is None: base=off
out=[]
for i in range(0,n,16):
chunk=data[off+i:off+i+16]
if not chunk: break
hexs=' '.join(f'{b:02x}' for b in chunk)
asc=''.join(chr(b) if 32<=b<127 else '.' for b in chunk)
out.append(f'{base+i:08x} {hexs:<47} {asc}')
return '\n'.join(out)
def stats(off, size):
seg=data[off:off+size]
c=collections.Counter(seg)
ff=c.get(0xff,0); zz=c.get(0x00,0)
return f'size={size} ff={ff}({100*ff/size:.1f}%) 00={zz}({100*zz/size:.1f}%) distinct={len(c)}'
def find_ascii_runs(off, size, minlen=4):
seg=data[off:off+size]
runs=[]; cur=b''; start=0
for i,b in enumerate(seg):
if 32<=b<127:
if not cur: start=i
cur+=bytes([b])
else:
if len(cur)>=minlen: runs.append((off+start, cur))
cur=b''
if len(cur)>=minlen: runs.append((off+start,cur))
return runs
def detect_period(off, size, maxp=1024):
"""Find best repeating period by autocorrelation-ish match on a window."""
seg=data[off:off+min(size,4096)]
best=[]
for p in range(2,maxp):
if p>=len(seg): break
match=sum(1 for i in range(len(seg)-p) if seg[i]==seg[i+p])
best.append((match/(len(seg)-p), p))
best.sort(reverse=True)
return best[:6]
def compare_region(a_off, b_off, size):
same=sum(1 for i in range(size) if data[a_off+i]==data[b_off+i])
return same, size, 100*same/size
regions = [
("0x010000", 0x010000, 0x1000),
("0x03E000", 0x03E000, 0x1000),
("0x0D0000", 0x0D0000, 0x6000),
("0x100000", 0x100000, 0x1000),
("0x126000", 0x126000, 0x5000),
("0x14C000", 0x14C000, 0xB000),
("0x164000", 0x164000, 0x32000),
("0x325000", 0x325000, 0x1000),
]
for name,off,size in regions:
print("="*100)
print(f"REGION {name} {stats(off,size)}")
print("-- head --")
print(hexdump(off,256))
print("-- period (top ratios) --")
print(detect_period(off,size))
runs=find_ascii_runs(off,min(size,0x8000),4)
print(f"-- ascii runs (first 20 of {len(runs)}) --")
for r in runs[:20]:
print(f' {r[0]:08x}: {r[1][:60]!r}')
print()
+29
Просмотреть файл
@@ -0,0 +1,29 @@
import struct, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
def hx(b): return " ".join(f"{x:02X}" for x in b)
CT=0x05C000
# raw region 0x5C000 .. 0x5F000
print("Search whole contacts region 0x5C000-0x6C000 for ascii runs & non-FF:")
region=data[CT:CT+0x10000]
# find first non-FF
first=None
for i,b in enumerate(region):
if b!=0xFF: first=i;break
print("first non-FF at region+0x%X = abs 0x%06X"%(first,CT+first))
# dump 0x5E000 area with 32B alignment relative to CT base
print("\n32B records from 0x5C000 index where data begins:")
# data begins at 0x5E000 => that's record (0x5E000-0x5C000)/32 = 0x2000/32=256
for idx in [255,256,257,258]:
off=CT+idx*32
d=data[off:off+32]
print(f"rec{idx} @0x{off:06X}:", hx(d))
# Try interpreting as: the two contacts are 'All Call' and 'TG6' and 'TG666'
# Layout guess A (like CPS .4rdmf): b0=?, b1=type, dmr@2 (4 LE or BCD), name@0x10 (16)
# But data misaligned. Let's realign: maybe record size is different or base offset differs.
# Locate ascii "All Call","TG6","TG666"
for tok in [b'All Call', b'TG6', b'TG666']:
p=data.find(tok, CT, CT+0x10000)
print(f"{tok} at 0x{p:06X} (region+0x{p-CT:X})")
+27
Просмотреть файл
@@ -0,0 +1,27 @@
import struct, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
def hx(b): return " ".join(f"{x:02X}" for x in b)
def bcd(bb):
if all(b==0xFF for b in bb): return 0
r=0
for bv in reversed(bb):
hi=(bv>>4)&0xF; lo=bv&0xF
if hi==0xF:hi=0
if lo==0xF:lo=0
r=r*100+hi*10+lo
return r
CT=0x05E000
STRIDE=21
print("21-byte contact records @0x5E000:")
for i in range(4):
off=CT+i*STRIDE
d=data[off:off+STRIDE]
typ=d[0]
idraw=d[1:5]
name=bytes(b for b in d[5:21] if b!=0xFF and b!=0).decode('gbk','ignore')
print(f"rec{i} @0x{off:06X} type={typ:02X} idbytes={hx(idraw)} bcd={bcd(idraw)} le={struct.unpack('<I',idraw)[0]} name={name!r}")
print(" raw:", hx(d))
# Confirm All Call id: AA AA AA AA. In DMR all-call id=0xFFFFFF(16777215). AA is not FF...
# maybe type: 0=private,1=group,2=allcall. All Call rec type=02 ✓, TG6/TG666 type=01=group ✓
+79
Просмотреть файл
@@ -0,0 +1,79 @@
import struct, collections
DUMP=r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data=open(DUMP,'rb').read()
def hd(off,n=128,base=None):
base=off if base is None else base
out=[]
for i in range(0,n,16):
c=data[off+i:off+i+16]
if not c:break
h=' '.join(f'{b:02x}' for b in c)
a=''.join(chr(b) if 32<=b<127 else '.' for b in c)
out.append(f'{base+i:08x} {h:<47} {a}')
return '\n'.join(out)
print("### 0x010000 vs channels 0x004000 (compare first 0x1000) ###")
same=sum(1 for i in range(0x1000) if data[0x010000+i]==data[0x004000+i])
print(f"match to channels@0x4000: {same}/{0x1000} = {100*same/0x1000:.1f}%")
# channel region 0x004000 first records
print("channels@0x004000 head:")
print(hd(0x004000,96))
print("region@0x010000 head:")
print(hd(0x010000,96))
print("\n### 0x03E000 DMRhub context: this looks like a contact/radioid record ###")
print(hd(0x03e000,32))
# Is contacts region at 0x05c000? compare structure - here name at offset 4
print("contacts region @0x05e000 (occupied):")
print(hd(0x05e000,128))
print("\n### 0x0D0000 Key records - 48 byte stride. Compare vs dmr_keys 0x082000 ###")
print("dmr_keys @0x082000:")
print(hd(0x082000,160))
print("encrypt-in-region @0x0D0000 rec0/rec1 detail:")
print(hd(0x0d0000,48))
# check stride: 'Key N' every 0x30
for i in range(5):
o=0x0d0000+i*0x30
print(f"rec{i} @{o:08x}: b0={data[o]:02x} b1={data[o+1]:02x} name={data[o+2:o+16]!r} tail={data[o+16:o+24].hex()}")
print("\n### how many Key records populated in 0x0D0000 (+24KB) ###")
cnt=0; last=None
for i in range(24576//0x30):
o=0x0d0000+i*0x30
if data[o]!=0xff and data[o]!=0x00:
cnt+=1; last=i
print(f"populated key records: {cnt}, last idx {last}")
print("\n### 0x126000 record analysis (32-byte stride) ###")
for i in range(8):
o=0x126000+i*32
r=data[o:o+32]
print(f"rec{i} @{o:08x}: {r.hex()}")
# decode fields: bytes 14,15,16 = 19 0a 0e -> 0x19=25,0x0a=10,0x0e=14 => date 2025-10-14
print("byte14-19 as date candidates (YY MM DD HH MM SS):")
for i in range(8):
o=0x126000+i*32
r=data[o:o+32]
print(f" rec{i}: {r[14]:02d}-{r[15]:02d}-{r[16]:02d} {r[17]:02d}:{r[18]:02d}:{r[19]:02d} b1(type)={r[1]} dmr_le={struct.unpack('<I',r[6:10])[0]} field10={struct.unpack('<I',r[10:14])[0]}")
# count records
cnt=sum(1 for i in range(20480//32) if data[0x126000+i*32]!=0x00 or any(b!=0 for b in data[0x126000+i*32:0x126000+i*32+16]))
print(f"nonzero-ish records among {20480//32}")
# call_log region is 0x088000; occupied run was 0x092000. compare
print("\ncall_log region occupied @0x092000:")
print(hd(0x092000,128))
print("\n### 0x14C000 GBK decode sample ###")
seg=data[0x14c000:0x14c000+256]
try:
print(seg.decode('gbk',errors='replace')[:120])
except Exception as e:
print("err",e)
print("\n### 0x164000 pinyin table structure ###")
# each entry seems 6 bytes ascii pinyin. period ratio peaked at 6
print(hd(0x164000,96))
# Check: is this indexed by GBK char? Look further in
print(hd(0x170000,96))
print("region 0x164000 size 200KB -> 200KB/6 ~ 34000 entries")
+95
Просмотреть файл
@@ -0,0 +1,95 @@
import struct, collections, sys, io
sys.stdout=io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP=r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data=open(DUMP,'rb').read()
def hd(off,n=128,base=None):
base=off if base is None else base
out=[]
for i in range(0,n,16):
c=data[off+i:off+i+16]
if not c:break
h=' '.join(f'{b:02x}' for b in c)
a=''.join(chr(b) if 32<=b<127 else '.' for b in c)
out.append(f'{base+i:08x} {h:<47} {a}')
return '\n'.join(out)
# 0x14C000: GBK char table. Decode as GBK 2-byte
print("### 0x14C000 GBK char list (first 40 chars) ###")
seg=data[0x14c000:0x14c000+80]
chars=[]
i=0
while i < len(seg)-1:
b=seg[i]
if b>=0x81:
try:
ch=bytes([seg[i],seg[i+1]]).decode('gbk')
chars.append(ch); i+=2; continue
except: pass
i+=1
print(''.join(chars))
# Size and how it ends
end=0x14c000+0xB000
print("tail of 0x14C000 region:")
print(hd(end-64,64))
# Correlate: 0x14C000 is a GBK char table (34KB?) and 0x164000 pinyin (200KB).
# The pinyin region: are entries fixed 6 bytes each -> index maps to char in char table?
print("\n### relationship: char count vs pinyin count ###")
# count 2-byte gbk chars in 0x14C000..0x157000 (44KB used, then zeros)
charseg=data[0x14c000:0x157000]
nchars=0
i=0
while i<len(charseg)-1:
if charseg[i]>=0x81 and charseg[i+1]!=0x00:
nchars+=1; i+=2
else:
i+=1
print(f"approx gbk chars in 0x14C000-0x157000 (44KB): {nchars}")
# pinyin at 0x164000, 6 bytes each, over 200KB
print(f"pinyin entries if 6-byte: {0x32000//6}")
print(f"pinyin entries if 8-byte: {0x32000//8}")
# Check pinyin stride precisely: look for the pattern of fixed-width fields
print("\n### pinyin stride check: line up 'kao','shang','xia' ###")
# they appeared as 6-char fields: 'kao ','shang ','xia '
for w in range(4,10):
# test if every field is exactly w and space padded
ok=True
for k in range(20):
f=data[0x164000+k*w:0x164000+k*w+w]
# must be ascii letters+spaces
if not all(32<=b<127 for b in f): ok=False;break
if ok:
fields=[data[0x164000+k*w:0x164000+k*w+w] for k in range(12)]
print(f"w={w}: {[f.decode() for f in fields]}")
# 0x100000 region: mostly zeros with some ascii. dump non-zero area
print("\n### 0x100000 non-zero content ###")
for i in range(0,0x1000,16):
c=data[0x100000+i:0x100000+i+16]
if any(b!=0 for b in c):
h=' '.join(f'{b:02x}' for b in c)
a=''.join(chr(b) if 32<=b<127 else '.' for b in c)
print(f'{0x100000+i:08x} {h:<47} {a}')
# 0x03E000 DMRhub: bytes 0-3 = 00 00 00 00 then name. This resembles radio identity/owner?
# main_settings radio_name at cfg+76. Check cfg region for DMRhub
print("\n### search whole dump for 'DMRhub' ###")
o=0
while True:
o=data.find(b'DMRhub',o)
if o<0:break
print(f" @{o:08x}: {hd(o-8,32,o-8)}")
o+=1
# 0x0D0000 keys: is this the 'groups'/grouplist backup? name 'Key N', b0=index, tail has index at +20
# Compare with grouplist format (b1==0x01 enabled, name @0x02). Here b0=idx,b1=00,name@2.
# Actually looks like encryption keys OR group lists. Check groups region 0x07C000
print("\n### groups region 0x07C000 ###")
print(hd(0x07c000,96))
# and encrypt region in .4rdmf is separate. Let's see full first key record 48 bytes across
print("\n### 0x0D0000 full stride verify: is it 0x30? show rec 100 ###")
print(hd(0x0d0000+100*0x30,48))
print("last populated:")
print(hd(0x0d0000+509*0x30,48))
+78
Просмотреть файл
@@ -0,0 +1,78 @@
import struct, sys, io
sys.stdout=io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP=r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data=open(DUMP,'rb').read()
# 0x0D0000 key region: list all populated records, their index byte0-1 (LE u16) and names
print("### 0x0D0000 key records: idx(le16)/name ###")
recs=[]
for i in range(24576//0x30):
o=0x0d0000+i*0x30
if data[o]==0xff and data[o+1]==0xff: continue
idx=data[o]|(data[o+1]<<8)
name=data[o+2:o+16].split(b'\xff')[0].decode('latin1','ignore')
tail=data[o+16:o+24].hex()
recs.append((i,idx,name,tail))
print(f"total populated: {len(recs)}")
print("first 3:", recs[:3])
print("records 252-258:", recs[252:258])
print("last 3:", recs[-3:])
# check monotonic idx
idxs=[r[1] for r in recs]
print("idx min",min(idxs),"max",max(idxs),"monotonic:",idxs==sorted(idxs))
print("count distinct idx:",len(set(idxs)))
# main_settings region: is 0x010000 exactly channels backup? 0x010000 is inside zones region (0x01C000 starts later).
# Actually 0x010000 is BEFORE channels end? channels=0x004000+0xC000=0x010000. So 0x010000 is the byte right AFTER channels!
print("\n### boundary check ###")
print("channels region: 0x004000 .. 0x010000 (end).")
print("So 0x010000 is a SEPARATE region right after channels, holding a 2nd copy of first 2 channels.")
# zones region 0x01C000; but DMRhub found at 0x01e004 (inside zones) and 0x03e004.
# 0x01C000+0x20000=0x03C000 end of zones. 0x03E000 is AFTER zones.
print("zones region: 0x01C000 .. 0x03C000. DMRhub at 0x01e004 is INSIDE zones region.")
print("0x03E000 (after zones) also has DMRhub -> a backup bank of the 0x01E000 data.")
print(" -> 0x01E000 and 0x03E000 are paired A/B banks, 0x20000 apart (ZONE_AB_BANK_OFFSET).")
print(f" 0x03E000-0x01E000 = 0x{0x03E000-0x01E000:X}")
# 0x010000: is there a matching bank 0x20000 later? 0x010000+0x20000=0x030000
print("\n### does 0x010000 have A/B twin at 0x030000? ###")
same=sum(1 for i in range(0x1000) if data[0x010000+i]==data[0x030000+i])
print(f"0x010000 vs 0x030000 match: {same}/{0x1000}")
# hexdump 0x030000
def hd(off,n=64):
out=[]
for i in range(0,n,16):
c=data[off+i:off+i+16]
h=' '.join(f'{b:02x}' for b in c)
a=''.join(chr(b) if 32<=b<127 else '.' for b in c)
out.append(f'{off+i:08x} {h:<47} {a}')
return '\n'.join(out)
print(hd(0x030000,80))
# What region is 0x010000 really? It has channel records. channels region is 1024*48=0xC000 exactly.
# 0x004000..0x010000 IS the channels region (0xC000). 0x010000 begins the NEXT thing.
# But 0x010000 duplicates ch0/ch1. This is the beta41 "channels bank B"? No - stock.
# More likely: 0x010000 = VFO/current-operating channel scratch (VFO A and VFO B saved as channel records)
print("\n### 0x010000 interpretation: VFO A/B saved channels ###")
print(hd(0x010000,96))
# 0x126000 records: confirm 32-byte call-log entries with timestamps. Count valid.
print("\n### 0x126000 call-log-style: count records with valid date ###")
valid=0
for i in range(20480//32):
o=0x126000+i*32
yy=data[o+14]
if 20<=yy<=40: # plausible year 2020-2040
valid+=1
print(f"records with plausible date byte (yy 20-40): {valid}")
# show last few valid
for i in range(20480//32):
o=0x126000+i*32
if data[o+14]==0 and all(b==0 for b in data[o:o+20]):
print(f"first all-zero record at idx {i} (@{o:08x})")
break
# 0x100000: bitmap? width guess. Non-zero starts 0x190. Try rendering as 1bpp.
print("\n### 0x100000 as bitmap: nonzero span ###")
nz=[i for i in range(0x1000) if data[0x100000+i]!=0]
print(f"nonzero bytes: {len(nz)}, span 0x{nz[0]:x}..0x{nz[-1]:x}")
+51
Просмотреть файл
@@ -0,0 +1,51 @@
import struct, sys, io
sys.stdout=io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP=r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data=open(DUMP,'rb').read()
# 0x126000 decode as call-log. Fields observed:
# b0=00, b1=type(1=priv/grp,2=?), b2=01, b3=00, b4-5=?, b6-9=dmr_id LE, b10-13=counter LE,
# b14-19 = YY MM DD HH MM SS, rest 0
print("### 0x126000 decoded call-log entries ###")
for i in range(16):
o=0x126000+i*32
r=data[o:o+32]
if r[14]==0 and all(b==0 for b in r[:14]): break
b1=r[1]; b4=struct.unpack('<H',r[4:6])[0]
dmr=struct.unpack('<I',r[6:10])[0]
cnt=struct.unpack('<I',r[10:14])[0]
date=f"20{r[14]:02d}-{r[15]:02d}-{r[16]:02d} {r[17]:02d}:{r[18]:02d}:{r[19]:02d}"
print(f" #{i:2d} type={b1} b4={b4:04x} dmr_id={dmr}(0x{dmr:x}) seq={cnt} {date}")
# region schedules is at 0xC6000. call_log at 0x88000 region. 0x126000 is unrelated location.
# Occupancy said call_log used @0x092000 (that was actually contacts-looking / All Call).
# Let's see total valid + gap pattern
print("\ntotal 32-byte records with a nonzero timestamp year 0x19:")
n=sum(1 for i in range(20480//32) if data[0x126000+i*32+14]==0x19)
print(n)
# Confirm the 0x100000 bitmap: render 1bpp. It has 5-row-tall column data (values <=0x1f then 0x3c,0x38 etc)
print("\n### 0x100000 bitmap render (bytes as vertical 8px columns, LSB=top) ###")
seg=data[0x100000:0x100000+0x300]
# print 8 rows
for row in range(8):
line=''
for col in range(0x190,0x2f8):
b=data[0x100000+col]
line+='#' if (b>>row)&1 else ' '
print(line.rstrip())
# 0x03E000 twin: what is at 0x01E000 exactly and how does it relate to zones?
print("\n### 0x01E000 (inside zones region) full ###")
def hd(off,n):
out=[]
for i in range(0,n,16):
c=data[off+i:off+i+16]
h=' '.join(f'{b:02x}' for b in c)
a=''.join(chr(b) if 32<=b<127 else '.' for b in c)
out.append(f'{off+i:08x} {h:<47} {a}')
return '\n'.join(out)
print(hd(0x01e000,48))
# Is 0x01E000 the tail of the last zone slot? zones start 0x01C000, 512B each. 0x01E000 = slot 16.
print(f"0x01E000 is zone slot #{(0x01E000-0x01C000)//512}")
# The DTCN A/B marker in beta is at page+0xFF8. 0x01dffc had ff ff ff ff before DMRhub.
# So 0x01E000 likely a settings/identity page, twinned at 0x03E000.
+42
Просмотреть файл
@@ -0,0 +1,42 @@
import struct, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
def hx(b): return " ".join(f"{x:02X}" for x in b)
def parse_bcd(bcd):
if all(b==0xFF for b in bcd): return 0
r=0
for bv in reversed(bcd):
hi=(bv>>4)&0xF; lo=bv&0xF
if hi==0xF: hi=0
if lo==0xF: lo=0
r=r*100+hi*10+lo
return r
# Are 0x1C000 blocks packed 48B channels identical to main channels 0x4000?
print("Compare 0x1C000 first 96 bytes vs main channels 0x4000:")
print(" 0x1C000:", hx(data[0x1C000:0x1C000+96]))
print(" 0x04000:", hx(data[0x04000:0x04000+96]))
print(" match:", data[0x1C000:0x1C000+96]==data[0x04000:0x04000+96])
# Contacts - the two records
print("\nCONTACT records decode:")
for off in (0x05E000,0x05E020):
d=data[off:off+32]
print(f"@0x{off:06X}:", hx(d))
print(f" b0={d[0]:02X} b1(type)={d[1]:02X} dmr_id(BCD@2)={parse_bcd(d[2:6])} raw_le@2={struct.unpack('<I',d[2:6])[0]}")
print(f" name@0x10:", bytes(b for b in d[0x10:0x20] if b!=0xFF).decode('gbk','ignore'))
print(f" bytes 6-F:", hx(d[6:16]))
# The ct256 record: b1=0xAA (=170) type not <=2, name shows 'All Call'? re-decode name area
d=data[0x05E000:0x05E000+32]
print("\nct256 name @0x05..0x0D area (looks like 'All Call' at 0x05):", bytes(b for b in d[0x05:0x0D]).decode('latin1'))
# contact ct257 @0x5E020: name at 0x0F='TG366'? decode 0x0D..
d=data[0x05E020:0x05E020+32]
print("ct257 full decode: b0-F=",hx(d[:16]), " tail=", bytes(b for b in d[0x0B:0x14] if b!=0xFF).decode('latin1','ignore'))
# scan whole contacts region for the real contact layout - look at more contact entries near 0x5E000
print("\nHex dump 0x05E000..0x05E080:")
for r in range(0,0x80,16):
print(f" +{r:04X}:", hx(data[0x05E000+r:0x05E000+r+16]))
+88
Просмотреть файл
@@ -0,0 +1,88 @@
import re
from collections import Counter
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
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),
# extended discovered
("key_aliases(0xD0000)",0x0D0000,0x006000),
("cjk_codepoint_table",0x14C000,0x00B000),
]
def region_of(a):
for n,s,sz in REGIONS:
if s<=a<s+sz: return n
return "UNMAPPED"
def is_word(s):
"""Genuine human string: has a >=3 alpha run and is not a punctuation/char-ramp."""
s=s.strip()
if len(s)<2: return False
if not re.search(r"[A-Za-z]{2,}", s) and not re.search(r"[A-Za-z][a-z]", s):
# allow pure digit/label like "TG6"
if not re.search(r"[A-Za-z]", s): return False
# reject monotone char ramps / repeats
if re.fullmatch(r"(.)\1*", s): return False
letters=[c for c in s if c.isalpha()]
if letters:
# reject alphabet ramps like KLMNOP / HIJK (consecutive ordinals dominating)
seq=sum(1 for a,b in zip(letters,letters[1:]) if abs(ord(a)-ord(b))<=1)
if seq/max(1,len(letters)-1) > 0.8 and len(set(letters))>3:
return False
if len(set(s))<=2 and len(s)>=4: return False
return True
found=[]
# ASCII
run=None
for i,b in enumerate(data):
if 0x20<=b<=0x7E:
run=i if run is None else run
else:
if run is not None:
s=data[run:i].decode('latin-1')
if len(s)>=3 and is_word(s): found.append((run,'ascii',region_of(run),s))
run=None
# UTF16LE
i=0;n=len(data)
while i<n-1:
if 0x20<=data[i]<=0x7E and data[i+1]==0:
j=i;ch=[]
while j<n-1 and data[j+1]==0 and 0x20<=data[j]<=0x7E:
ch.append(chr(data[j])); j+=2
s=''.join(ch)
if len(s)>=3 and is_word(s): found.append((i,'utf16',region_of(i),s))
i=max(j,i+2)
else: i+=1
# dedup identical (addr,text)
seen=set();uniq=[]
for r in found:
k=(r[0],r[3])
if k in seen: continue
seen.add(k); uniq.append(r)
print("TOTAL genuine human-readable strings:", len(uniq))
print("By region:", dict(Counter(r[2] for r in uniq)))
print("By encoding:", dict(Counter(r[1] for r in uniq)))
print()
# collapse Key-N and dump unique text values per region
byreg={}
for a,e,reg,t in uniq: byreg.setdefault(reg,[]).append((a,t))
for reg in byreg:
vals=byreg[reg]
print(f"--- {reg} ({len(vals)} strings) ---")
# show distinct text (first 30)
shown=0
for a,t in vals:
print(f" 0x{a:06X} {t!r}")
shown+=1
if shown>=30:
print(f" ... (+{len(vals)-30} more)")
break
+18
Просмотреть файл
@@ -0,0 +1,18 @@
import sys
f = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb")
data = f.read()
f.close()
print("filesize", len(data))
def hexdump(off, n=256):
for i in range(0, n, 16):
chunk = data[off+i:off+i+16]
h = ' '.join('%02x'%b for b in chunk)
a = ''.join(chr(b) if 32<=b<127 else '.' for b in chunk)
print('%08x %-47s %s'%(off+i, h, a))
for base in (0x198000, 0x352000, 0x3F0000):
print("="*80)
print("BLOB @ %08x"%base)
hexdump(base, 512)
+40
Просмотреть файл
@@ -0,0 +1,40 @@
import io
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
out=io.open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/out10.txt","w",encoding="utf-8")
# Render candidate glyphs at 0x19c000 as bitmaps to confirm font.
# Try 16x16 (32 bytes/glyph) and 12x12 etc.
def render(off, w_bytes, rows, label):
out.write("glyph @%#x %dx%d bytes/row=%d:\n"%(off,rows,w_bytes*8,w_bytes))
for r in range(rows):
line=""
for cb in range(w_bytes):
b=data[off+r*w_bytes+cb]
for bit in range(8):
line += "#" if (b>>bit)&1 else "."
out.write(line+"\n")
out.write("\n")
# 16x16 = 2 bytes/row, 16 rows = 32 bytes
for g in range(6):
render(0x19c000+g*32, 2, 16, "16x16 glyph %d"%g)
# Occupancy across 0x198000..0x242000: how much real font data
seg=data[0x198000:0x242000]
from collections import Counter
c=Counter(seg)
out.write("region 0x198000-0x242000 total=%d ff=%d zero=%d other=%d\n"%(len(seg),c[0xff],c[0],len(seg)-c[0xff]-c[0]))
# The font is likely: big CJK 16x16 bitmap set. count of 32-byte glyphs that fit in nonFF area
# Check periodicity of 32 in the 0x19c000+ area by counting nonzero glyph cells
out.write("\nSample far into region 0x200000:\n")
def hx(off,n=48):
s=""
for j in range(0,n,16):
ch=data[off+j:off+j+16]
s+="%08x %s\n"%(off+j,' '.join('%02x'%b for b in ch))
return s
out.write(hx(0x200000,0x40))
out.write("\n0x240000:\n"+hx(0x240000,0x40))
out.close()
print("done")
+29
Просмотреть файл
@@ -0,0 +1,29 @@
import io
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
out=io.open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/out11.txt","w",encoding="utf-8")
# The font bitmap likely starts on a clean boundary. Find where the pinyin/CJK code
# table ends and bitmap begins. The 0x19c000 area had a leading block of zeros then bitmaps.
# Let's find a good glyph base by scanning for a 32-byte-aligned region where rendering
# 16 rows x 2 bytes gives connected shapes. Test several bases and stride 32.
def render(off, w_bytes, rows):
lines=[]
for r in range(rows):
line=""
for cb in range(w_bytes):
b=data[off+r*w_bytes+cb]
for bit in range(8):
line += "#" if (b>>bit)&1 else "."
lines.append(line)
return lines
# Render 4 consecutive 16x16 glyphs side by side starting at a base, for several bases
for base in (0x19c400, 0x1a0000, 0x1b0000, 0x200000):
out.write("=== base %#x, four 16x16 glyphs (32B each) ===\n"%base)
gs=[render(base+g*32,2,16) for g in range(4)]
for r in range(16):
out.write(" ".join(gs[g][r] for g in range(4))+"\n")
out.write("\n")
out.close()
print("done")
+53
Просмотреть файл
@@ -0,0 +1,53 @@
import io
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
out=io.open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/out12.txt","w",encoding="utf-8")
def render(off):
lines=[]
for r in range(16):
line=""
for cb in range(2):
b=data[off+r*2+cb]
for bit in range(8):
line += "#" if (b>>bit)&1 else "."
lines.append(line)
return lines
# find best global alignment offset (0..31) that maximizes clean glyphs across 0x198000..0x242000
# metric: fraction of glyphs whose first & last rows are blank-ish and have connected mass
import statistics
def glyph_score(off):
g=data[off:off+32]
if all(b==0 for b in g) or all(b==0xff for b in g): return None
rows=[(g[2*r]|(g[2*r+1]<<8)) for r in range(16)]
setbits=sum(bin(x).count('1') for x in rows)
if setbits<8 or setbits>200: return 0
# top & bottom rows mostly empty is typical
edge = (bin(rows[0]).count('1')<=3) + (bin(rows[15]).count('1')<=3)
return edge
best=None
for align in range(0,32,2):
base=0x198000+0x4000+align # start well past pinyin table
scores=[]
for g in range(2000):
off=base+g*32
s=glyph_score(off)
if s is not None: scores.append(s)
if scores:
avg=sum(scores)/len(scores)
if best is None or avg>best[1]:
best=(align,avg)
out.write("best align offset within 32: %r\n"%(best,))
# count non-empty 32-byte glyph cells in whole region for a glyph-count estimate
region=data[0x19c000:0x242000]
ng=len(region)//32
nonempty=0
for g in range(ng):
cell=region[g*32:g*32+32]
if any(b not in (0,0xff) for b in cell): nonempty+=1
out.write("region 0x19c000-0x242000: %d 32-byte cells, %d non-empty (~glyphs)\n"%(ng,nonempty))
out.write("A GB2312 full font would be ~7000-8000 glyphs.\n")
out.close()
print("done")
+37
Просмотреть файл
@@ -0,0 +1,37 @@
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
def region_stats(start, end, label):
seg = data[start:end]
from collections import Counter
c = Counter(seg)
ff = c.get(0xff,0); zero=c.get(0x00,0); e80=c.get(0x80,0)
print(f"{label} {start:#08x}-{end:#08x} len={len(seg):#x} ff={ff} zero={zero} 0x80={e80} distinct={len(c)}")
# Find real extents of DATA (non-FF) around each blob by scanning 0xFF runs
def find_ff_boundaries(approx_start, search_end):
# scan forward to find where FF-run of >=256 begins (end of data)
i = approx_start
run = 0
end = search_end
while i < search_end:
if data[i]==0xff:
run+=1
if run>=1024:
end = i-run+1
break
else:
run=0
i+=1
return end
print("=== 0x198000 blob ===")
region_stats(0x198000, 0x242000, "raw")
# check where FF starts
e = find_ff_boundaries(0x198000, 0x260000)
print("first long FF run start ~", hex(e))
print("=== 0x352000 blob ===")
region_stats(0x352000, 0x3DD000, "raw")
print("=== 0x3F0000 blob ===")
region_stats(0x3F0000, 0x400000, "raw")
+33
Просмотреть файл
@@ -0,0 +1,33 @@
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
def runmap(start, end, minrun=256):
# classify each byte: F=0xff, Z=0x00, E=0x80, D=other; emit runs
def cls(b):
if b==0xff: return 'F'
if b==0x00: return 'Z'
if b==0x80: return 'E'
return 'D'
runs=[]
i=start
cur=cls(data[i]); rs=i
i+=1
while i<end:
c=cls(data[i])
if c!=cur:
runs.append((cur,rs,i))
cur=c; rs=i
i+=1
runs.append((cur,rs,end))
# merge: report runs, collapsing tiny D-noise? Just print runs >= minrun OR mark
out=[]
for c,s,e in runs:
out.append((c,s,e,e-s))
return out
for base,end,name in [(0x198000,0x242000,"BLOB1"),(0x352000,0x3dd000,"BLOB2"),(0x3f0000,0x400000,"BLOB3")]:
print("="*70, name, hex(base),hex(end))
runs=runmap(base,end)
# collapse: print segments where a class dominates for >=1KB
for c,s,e,l in runs:
if l>=512:
print(f" {c} {s:#08x}-{e:#08x} len={l:#x}({l})")
+26
Просмотреть файл
@@ -0,0 +1,26 @@
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
# --- BLOB2 stride analysis ---
# Find start of each 0x80-run to measure period
starts=[]
i=0x352000; end=0x3d5000
prev=None
inrun=False
# a "record boundary" — let's autocorrelate instead on a window
seg=data[0x353000:0x3a3000]
N=len(seg)
import statistics
best=[]
for stride in range(8, 4096):
# sample-based autocorr: count byte equality at offset stride
eq=0; tot=0
step=1
for k in range(0, N-stride, 7):
if seg[k]==seg[k+stride]:
eq+=1
tot+=1
best.append((eq/tot, stride))
best.sort(reverse=True)
print("BLOB2 top strides (score,stride):")
for s,st in best[:15]:
print(f" stride={st} ({st:#x}) score={s:.3f}")
+35
Просмотреть файл
@@ -0,0 +1,35 @@
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
def hexdump(off, n=128):
for i in range(0, n, 16):
chunk = data[off+i:off+i+16]
h = ' '.join('%02x'%b for b in chunk)
a = ''.join(chr(b) if 32<=b<127 else '.' for b in chunk)
print('%08x %-47s %s'%(off+i, h, a))
# The transition zones between 0x80 runs in BLOB2 - dump around a boundary
print("### BLOB2 boundary at ~0x352845 (0x80 run ends here)")
hexdump(0x352840-0x20, 0x120)
print("\n### BLOB2 boundary at 0x353b25")
hexdump(0x353b00, 0x80)
# check byte histogram of a single 'record' 0x352000..0x353b25
from collections import Counter
c=Counter(data[0x352000:0x353b25])
print("\nBLOB2 record0 (0x352000-0x353b25) len", 0x353b25-0x352000, "hist top:", c.most_common(10))
# BLOB1 - the 14KB dense CJK. Check for structure: is it pure 2-byte GBK codes?
print("\n### BLOB1 0x198000 first bytes stats")
seg=data[0x198000:0x19b800]
# count high-bit-set bytes
hi=sum(1 for b in seg if b>=0x80)
print("len",len(seg),"high-bit bytes",hi, "ratio", hi/len(seg))
# Interpret as GB2312: pairs where b0 in 0xA1..0xF7
pairs=0; valid=0
for i in range(0,len(seg)-1,2):
b0,b1=seg[i],seg[i+1]
pairs+=1
if 0xa1<=b0<=0xf7 and 0xa1<=b1<=0xfe:
valid+=1
print("2-byte pairs",pairs,"valid GB2312 lvl",valid, "ratio",valid/pairs)
+49
Просмотреть файл
@@ -0,0 +1,49 @@
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
# BLOB1 decode as GB2312
seg=data[0x198000:0x19b800]
try:
txt=seg.decode('gb2312', errors='replace')
print("BLOB1 as GB2312 (first 120 chars):")
print(repr(txt[:120]))
print("...middle:", repr(txt[3000:3060]))
except Exception as e:
print("err",e)
# Count of hanzi in BLOB1 (each 2 bytes)
n1 = len(seg)//2
print("BLOB1 char count:", n1)
# BLOB3: sorted UTF-16BE codepoints. Count them (until 00 00 fill)
seg3=data[0x3f0000:0x400000]
cps=[]
for i in range(0,len(seg3)-1,2):
v=(seg3[i]<<8)|seg3[i+1]
if v==0:
# zeros are padding within blocks; keep scanning but record
cps.append(0)
else:
cps.append(v)
nonzero=[v for v in cps if v!=0]
print("BLOB3 total 2-byte slots:",len(cps),"nonzero:",len(nonzero))
print("BLOB3 first cps:", [hex(v) for v in nonzero[:16]])
print("BLOB3 last cps:", [hex(v) for v in nonzero[-16:]])
print("BLOB3 min/max:", hex(min(nonzero)), hex(max(nonzero)))
# is it strictly ascending overall? check monotonic ignoring zeros
asc=all(nonzero[i]<=nonzero[i+1] for i in range(len(nonzero)-1))
print("BLOB3 nonzero ascending?", asc)
# decode a few as unicode chars
print("BLOB3 sample chars:", ''.join(chr(v) for v in nonzero[:20]))
# BLOB2 audio - measure total extent: from 0x352000 until sustained FF
i=0x352000
run=0; endaud=None
while i<0x3e0000:
if data[i]==0xff:
run+=1
if run>=2048:
endaud=i-run+1; break
else:
run=0
i+=1
print("BLOB2 audio ends ~", hex(endaud) if endaud else "not found within 0x3e0000")
+32
Просмотреть файл
@@ -0,0 +1,32 @@
import io,sys
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
out=io.open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/out7.txt","w",encoding="utf-8")
seg=data[0x198000:0x19b800]
txt=seg.decode('gb2312', errors='replace')
out.write("BLOB1 GB2312 first 200 chars:\n")
out.write(txt[:200]+"\n\n")
out.write("BLOB1 GB2312 chars 3000..3080:\n")
out.write(txt[3000:3080]+"\n\n")
# BLOB3 filter FFFF and 0
seg3=data[0x3f0000:0x400000]
cps=[]
for i in range(0,len(seg3)-1,2):
v=(seg3[i]<<8)|seg3[i+1]
cps.append(v)
real=[v for v in cps if v not in (0,0xffff)]
out.write("BLOB3 real cps count: %d\n"%len(real))
out.write("BLOB3 first 40 chars: "+''.join(chr(v) for v in real[:40])+"\n")
out.write("BLOB3 range: %04x..%04x\n"%(min(real),max(real)))
asc=all(real[i]<=real[i+1] for i in range(len(real)-1))
out.write("BLOB3 ascending(ignoring pad): %s\n\n"%asc)
# Does BLOB1's GB2312 chars correspond to BLOB3 unicode set? Convert BLOB1 chars to codepoints
b1cps=[ord(c) for c in txt if ord(c)>0x2000]
out.write("BLOB1 first 40 unicode cps: "+' '.join('%04x'%c for c in b1cps[:40])+"\n")
out.write("BLOB1 unique cps: %d, BLOB3 unique: %d\n"%(len(set(b1cps)),len(set(real))))
inter=set(b1cps)&set(real)
out.write("intersection size: %d\n"%len(inter))
out.close()
print("done")
+44
Просмотреть файл
@@ -0,0 +1,44 @@
import wave,io
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
out=io.open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/out8.txt","w",encoding="utf-8")
# BLOB2 audio extent
i=0x352000; run=0; endaud=None
while i<0x3e0000:
if data[i]==0xff:
run+=1
if run>=4096: endaud=i-run+1; break
else: run=0
i+=1
out.write("audio region 0x352000 .. %s (len %#x)\n"%(hex(endaud),endaud-0x352000))
# check for an index/header at very start of blob2 or just before it (0x351000?)
def hx(off,n=64):
s=""
for j in range(0,n,16):
ch=data[off+j:off+j+16]
s+="%08x %s %s\n"%(off+j,' '.join('%02x'%b for b in ch),''.join(chr(b) if 32<=b<127 else '.' for b in ch))
return s
out.write("\nBefore audio (0x351f80):\n"+hx(0x351f80,0x80))
# 8-bit unsigned PCM -> wav, first 64KB as a listen-test
seg=data[0x352000:0x352000+0x10000]
w=wave.open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/blob2_sample.wav","wb")
w.setnchannels(1); w.setsampwidth(1); w.setframerate(8000)
w.writeframes(seg)
w.close()
out.write("\nwrote blob2_sample.wav (8kHz u8, 64KB)\n")
# statistics: measure how 'audio-like' - mean near 128, derivative small
import statistics
vals=list(seg)
out.write("mean=%.1f min=%d max=%d stdev=%.1f\n"%(statistics.mean(vals),min(vals),max(vals),statistics.pstdev(vals)))
# fraction of adjacent-sample diffs <=8 (smoothness)
sm=sum(1 for k in range(len(vals)-1) if abs(vals[k]-vals[k+1])<=8)
out.write("smoothness (|diff|<=8): %.3f\n"%(sm/(len(vals)-1)))
# Total audio size and rough duration at 8k/16k
alen=endaud-0x352000
out.write("audio bytes=%d @8kHz=%.1fs @16kHz=%.1fs\n"%(alen,alen/8000,alen/16000))
out.close()
print("done")
+38
Просмотреть файл
@@ -0,0 +1,38 @@
import io
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
out=io.open(r"C:/Users/vikto/Documents/Claude/rt-4d/analyze/out9.txt","w",encoding="utf-8")
def hx(off,n=64):
s=""
for j in range(0,n,16):
ch=data[off+j:off+j+16]
s+="%08x %s %s\n"%(off+j,' '.join('%02x'%b for b in ch),''.join(chr(b) if 32<=b<127 else '.' for b in ch))
return s
# BLOB1 exact end of pinyin table (first non-GB2312-valid pair)
seg=data[0x198000:0x1a0000]
end=None
for i in range(0,len(seg)-1,2):
b0,b1=seg[i],seg[i+1]
if not(0xa1<=b0<=0xf7 and 0xa1<=b1<=0xfe):
end=0x198000+i; break
out.write("BLOB1 pinyin table valid GB2312 until %s (len %#x = %d chars)\n\n"%(hex(end),end-0x198000,(end-0x198000)//2))
out.write("At table end:\n"+hx(end-0x10,0x60)+"\n")
# What's at 0x1c7xxx (the Z-runs)? that's inside zones region? No, zones=0x1c000..0x3c000
# 0x1c78fe is within zones region (0x01C000+0x20000=0x3C000). So those Z runs belong to zones, not blob1.
# Real blob1 data is only 0x198000..~0x19d170. Show 0x19c000..0x19e000
out.write("0x19c000 area (after pinyin table+FF):\n"+hx(0x19c000,0x80)+"\n")
out.write("0x19d000 area:\n"+hx(0x19d000,0x60)+"\n")
# Is there anything between 0x19e000 and 0x242000 besides FF/00? sample a few
for a in (0x1a0000,0x1c0000,0x200000,0x240000):
nonff=sum(1 for b in data[a:a+0x1000] if b not in (0xff,0x00))
out.write("page %#x nonFF/00 count=%d\n"%(a,nonff))
# BLOB2: look for an index table of start offsets. Voice prompts often preceded by
# a table of u32 offsets. Scan 0x350000-0x352000 region.
out.write("\nBefore audio region 0x350000:\n"+hx(0x350000,0x80))
out.write("\n0x351000:\n"+hx(0x351000,0x40))
out.close()
print("done")
+118
Просмотреть файл
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""First-look recon of the RT-4D 4MB SPI dump."""
import sys, string
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
N = len(data)
print(f"== FILE == {N} bytes (0x{N:X}) = {N/1024/1024:.2f} MB\n")
# Known map (from jcalado constants.py)
SPI_REGIONS = {
"calibration": (0x000000, 0x001000, 0x40),
"main_settings": (0x002000, 0x001000, 0x90),
"channels": (0x004000, 0x00C000, 0x91),
"zones": (0x01C000, 0x020000, 0x92),
"contacts": (0x05C000, 0x010000, 0x93),
"groups": (0x07C000, 0x003000, 0x94),
"dmr_keys": (0x082000, 0x003000, 0x95),
"call_log": (0x088000, 0x00C000, 0x96),
"default_sms": (0x094000, 0x001000, 0x97),
"msg_drafts": (0x095000, 0x010000, None),
"msg_inbox": (0x0A5000, 0x010000, None),
"msg_outbox": (0x0B5000, 0x010000, None),
"schedules": (0x0C6000, 0x008000, 0x98),
"dtmf_names": (0x0C7000, 0x000100, 0x80),
"fm_settings": (0x0D6000, 0x001000, 0x99),
}
def classify(b):
if all(x == 0xFF for x in b): return "empty(FF)"
if all(x == 0x00 for x in b): return "zero(00)"
return "DATA"
# ---- occupancy map at 4KB granularity ----
PAGE = 0x1000
pages = N // PAGE
kinds = [classify(data[i*PAGE:(i+1)*PAGE]) for i in range(pages)]
# collapse to runs
print("== OCCUPANCY (4KB pages) ==")
runs = []
s = 0
for i in range(1, pages+1):
if i == pages or kinds[i] != kinds[s]:
runs.append((s*PAGE, i*PAGE, kinds[s]))
s = i
data_bytes = sum(1 for k in kinds if k == "DATA")*PAGE
for a, b, k in runs:
tag = ""
if k == "DATA":
# which known regions overlap
names = [n for n,(ra,rs,_) in SPI_REGIONS.items() if ra < b and ra+rs > a]
tag = " <- " + (",".join(names) if names else "??? UNKNOWN")
print(f" 0x{a:06X}-0x{b:06X} {(b-a)//1024:>5}KB {k}{tag}")
print(f"\n DATA pages total: {data_bytes/1024:.0f}KB of {N/1024:.0f}KB")
# ---- known region occupancy check ----
print("\n== KNOWN REGIONS (populated?) ==")
for name,(addr,size,rid) in SPI_REGIONS.items():
if addr+size > N:
print(f" {name:14} 0x{addr:06X} OUT OF RANGE"); continue
reg = data[addr:addr+size]
nonff = sum(1 for x in reg if x != 0xFF)
first = reg[:12].hex(' ')
rids = "None" if rid is None else hex(rid)
print(f" {name:14} 0x{addr:06X}+0x{size:05X} rid={rids:>4} nonFF={nonff:>6}/{size:<6} first={first}")
# ---- DTCN magic (settings bank detection) ----
print("\n== DTCN magic (beta bank markers) ==")
for off in (0x002FFC, 0x003FFC):
print(f" @0x{off:06X}: {data[off:off+4]!r}")
mcount = data.count(b'DTCN')
print(f" total 'DTCN' occurrences in dump: {mcount}")
# ---- calibration hexdump ----
print("\n== CALIBRATION region 0x000000..0x000100 ==")
for r in range(0, 0x100, 16):
chunk = data[r:r+16]
hexs = ' '.join(f'{x:02X}' for x in chunk)
asc = ''.join(chr(x) if 32<=x<127 else '.' for x in chunk)
print(f" {r:04X} {hexs} {asc}")
# ---- address book / high region probe ----
print("\n== HIGH REGION (0x0E0000+) probe ==")
for addr in (0x0D7000, 0x0E0000, 0x100000, 0x200000, 0x300000, 0x3F0000):
reg = data[addr:addr+64]
nonff = sum(1 for x in reg if x != 0xFF)
asc = ''.join(chr(x) if 32<=x<127 else '.' for x in reg[:32])
print(f" 0x{addr:06X} nonFF={nonff}/64 ascii={asc!r}")
# ---- strings ----
print("\n== STRINGS (printable runs >=4, first 60) ==")
printable = set(bytes(string.printable[:-6], 'ascii'))
runs_s = []
cur = bytearray(); start = 0
for i,x in enumerate(data):
if 32 <= x < 127:
if not cur: start = i
cur.append(x)
else:
if len(cur) >= 4: runs_s.append((start, bytes(cur)))
cur = bytearray()
if len(cur) >= 4: runs_s.append((start, bytes(cur)))
print(f" total string runs: {len(runs_s)}")
for off, s in runs_s[:60]:
try: txt = s.decode('ascii')
except: txt = repr(s)
print(f" 0x{off:06X} {txt}")
# ---- first channel records ----
print("\n== CHANNEL records @0x004000 (48 bytes each) ==")
CH = 0x004000; SZ = 48
for i in range(6):
rec = data[CH+i*SZ:CH+i*SZ+SZ]
if all(x==0xFF for x in rec):
print(f" ch{i}: <empty>"); continue
print(f" ch{i}: {rec.hex(' ')}")
asc = ''.join(chr(x) if 32<=x<127 else '.' for x in rec)
print(f" ascii: {asc}")
+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)))
+45
Просмотреть файл
@@ -0,0 +1,45 @@
import struct, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
CT_BASE = 0x05C000; ZN_BASE=0x01C000
def hx(b): return " ".join(f"{x:02X}" for x in b)
def parse_bcd(bcd):
if all(b==0xFF for b in bcd): return 0
r=0
for bv in reversed(bcd):
hi=(bv>>4)&0xF; lo=bv&0xF
if hi==0xF: hi=0
if lo==0xF: lo=0
r=r*100+hi*10+lo
return r
# Scan contacts region for any non-FF records
print("CONTACTS scan (non-0xFF b0):")
cnt=0
for i in range(2048):
off = CT_BASE + i*32
d = data[off:off+32]
if all(b==0xFF for b in d): continue
name = bytes(b for b in d[0x10:0x20] if b!=0xFF).decode('gbk','ignore').strip()
dmr = parse_bcd(d[0x02:0x06])
print(f"ct{i} @0x{off:06X} b0={d[0]:02X} type(b1)={d[1]} dmr(BCD@2)={dmr} name={name!r}")
print(" raw:", hx(d))
cnt+=1
if cnt>=8: break
print("total non-FF contact records:", sum(1 for i in range(2048) if not all(b==0xFF for b in data[CT_BASE+i*32:CT_BASE+i*32+32])))
print("\nZONES:")
for i in range(256):
off=ZN_BASE+i*512
d=data[off:off+512]
if d[0]==0xFF: continue
name=bytes(b for b in d[0x04:0x14] if b!=0xFF).decode('gbk','ignore')
count=d[0]|(d[1]<<8)
print(f"zone{i} @0x{off:06X} count={count} curA={d[2]} curB={d[3]} name={name!r}")
print(" head:", hx(d[0:0x14]))
chans=[d[0x14+k*2]|(d[0x15+k*2]<<8) for k in range(min(count,30))]
print(" chans:", chans)
print(" scan@0x1A4:", hx(d[0x1A4:0x1A4+25]))
# dump any nonzero bytes between end of chan list and scan
print(" 0x194-0x1A4:", hx(d[0x194:0x1A4]))
+87
Просмотреть файл
@@ -0,0 +1,87 @@
import struct, sys
sys.path.insert(0, r"C:/Users/vikto/Documents/Claude/rt-4d/rt4d-cps")
from rt4d_codeplug.tones import decode_subaudio_bytes
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
print("size", len(data))
CH_BASE = 0x004000
ZN_BASE = 0x01C000
CT_BASE = 0x05C000
def hx(b): return " ".join(f"{x:02X}" for x in b)
def parse_bcd(bcd):
if all(b==0xFF for b in bcd): return 0
r=0
for bv in reversed(bcd):
hi=(bv>>4)&0xF; lo=bv&0xF
if hi==0xF: hi=0
if lo==0xF: lo=0
r=r*100+hi*10+lo
return r
print("="*70)
print("CHANNELS")
for i in range(6):
off = CH_BASE + i*48
d = data[off:off+48]
if all(b==0xFF for b in d):
print(f"ch{i}: EMPTY"); continue
rx = struct.unpack('<I', d[5:9])[0]
tx = struct.unpack('<I', d[9:13])[0]
name = bytes(b for b in d[0x20:0x30] if b!=0xFF).decode('gbk','ignore').strip()
b0,b1,b2,b3,b4 = d[0],d[1],d[2],d[3],d[4]
print(f"\nch{i} @0x{off:06X} name={name!r}")
print(" raw:", hx(d))
print(f" RX={rx/1e5:.5f} TX={tx/1e5:.5f}")
print(f" b0=0x{b0:02X} mode={'ANALOG' if b0&0x40 else 'DIGITAL'} monitor={b0&1} ts={(b0>>1)&1} dmrmode={(b0>>2)&1} useChID={(b0>>3)&1} rxtx={(b0>>4)&3}")
print(f" b1=0x{b1:02X} scramble={b1&0xF} colorcode={(b1>>4)&0xF}")
print(f" b2=0x{b2:02X} tot={b2&0x3F} power={'HIGH' if b2&0x40 else 'LOW'}")
print(f" b3=0x{b3:02X} tailtone={b3&7} anaBusyLock={(b3>>3)&3} dmrBusyLock={(b3>>5)&3} scan={'REMOVE' if b3&0x80 else 'ADD'}")
print(f" b4=0x{b4:02X} ctdcs_sel={(b4>>1)&7} mod={(b4>>4)&3} bw={'Narrow' if (b4>>6)&1 else 'Wide'}")
print(f" rx_tone={decode_subaudio_bytes(d[0x0D:0x0F])} tx_tone={decode_subaudio_bytes(d[0x0F:0x11])}")
cslot=struct.unpack('<H',d[0x11:0x13])[0]
print(f" contact_slot(0x11)={cslot} (->idx {0 if cslot==0xFFFF else cslot+1}) grouplist(0x13)={d[0x13]} encrypt(0x14)={struct.unpack('<H',d[0x14:0x16])[0]}")
print(f" dmr_id(0x16 BCD)={parse_bcd(d[0x16:0x1A])} mute_code(0x1A)={struct.unpack('<I',d[0x1A:0x1E])[0]:08X}")
print(f" bytes 0x1E-0x1F: {hx(d[0x1E:0x20])}")
print("="*70)
print("CONTACTS")
cnt=0
for i in range(2048):
off = CT_BASE + i*32
d = data[off:off+32]
if d[1] > 2: # empty per parser
continue
name = bytes(b for b in d[0x10:0x20] if b!=0xFF).decode('gbk','ignore').strip()
dmr = parse_bcd(d[0x02:0x06])
print(f"\nct{i} @0x{off:06X}")
print(" raw:", hx(d))
print(f" type(b1)={d[1]} name={name!r} dmr_id(BCD 0x2)={dmr}")
print(f" b0=0x{d[0]:02X} bytes6-F: {hx(d[6:16])}")
cnt+=1
if cnt>=6: break
print("="*70)
print("ZONES")
zc=0
for i in range(256):
off = ZN_BASE + i*512
d = data[off:off+512]
if d[0]==0xFF: continue
name = bytes(b for b in d[0x04:0x14] if b!=0xFF).decode('gbk','ignore').strip()
count = d[0] | (d[1]<<8)
print(f"\nzone{i} @0x{off:06X} name={name!r} count(b0-1)={count} curA(b2)={d[2]} curB(b3)={d[3]}")
print(" head 0x00-0x13:", hx(d[0:0x14]))
chans=[]
for k in range(min(count,200)):
o=0x14+k*2
ci=d[o]|(d[o+1]<<8)
chans.append(ci)
print(" channel idx list:", chans[:30])
scan=d[0x1A4:0x1A4+25]
print(" scan bitmap @0x1A4:", hx(scan))
zc+=1
if zc>=4: break
+35
Просмотреть файл
@@ -0,0 +1,35 @@
data = open(r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin","rb").read()
# Check 0x14C000 region: is it sequential GBK codepoints?
seg = data[0x14C000:0x14C000+40]
print("bytes @0x14C000:", seg.hex(' '))
# decode as gbk pairs
try:
print("gbk:", seg.decode('gbk'))
except Exception as e:
print(e)
# Are the CJK chars strictly increasing in unicode order?
region = data[0x14C000:0x157000]
chars=[]
i=0
while i < len(region)-1:
pair = region[i:i+2]
try:
c = pair.decode('gbk')
if len(c)==1 and ord(c)>0x2000:
chars.append(ord(c))
except:
pass
i+=2
inc = sum(1 for a,b in zip(chars,chars[1:]) if b>a)
print(f"CJK chars in 0x14C000-0x157000: {len(chars)}, monotonic-increasing fraction: {inc/max(1,len(chars)-1):.2%}")
print("first codepoints:", [hex(c) for c in chars[:8]], "last:", [hex(c) for c in chars[-4:]])
# search whole dump for meaningful ascii keywords (case-insensitive)
kws = [b'RADTEL', b'Radtel', b'RT-4D', b'RT4D', b'FIRMWARE', b'VERSION', b'BOOT', b'COPYRIGHT',
b'WELCOME', b'BRANDMEISTER', b'OpenGD', b'DMR ID', b'CALLSIGN', b'\x00V1.', b'V2.', b'BETA', b'.4rdmf']
for k in kws:
idx = data.find(k)
if idx>=0:
print(f"FOUND {k!r} @0x{idx:06X}")
+25
Просмотреть файл
@@ -0,0 +1,25 @@
import struct, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
ZN_BASE=0x01C000
def hx(b): return " ".join(f"{x:02X}" for x in b)
# The occupancy said zones DATA is 0x1C000-0x1F000 (12KB). Dump each 512-byte record 0..23
print("Scanning zone region 0x1C000..0x1F000 as 512-byte records:")
for i in range(24):
off=ZN_BASE+i*512
d=data[off:off+512]
if all(b==0xFF for b in d):
continue
# look for ascii name anywhere
printable = bytes(b if 32<=b<127 else 0 for b in d)
# find longest run of ascii letters
txt=""
for b in d[:0x40]:
if 32<=b<127: txt+=chr(b)
else: txt+="."
print(f"\nrec{i} @0x{off:06X}")
print(" 0x00-0x1F:", hx(d[0:0x20]))
print(" 0x20-0x3F:", hx(d[0x20:0x40]))
print(" ascii0-40:", txt)
+24
Просмотреть файл
@@ -0,0 +1,24 @@
import struct, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
DUMP = r"C:/Users/vikto/Documents/Claude/rt-4d/radio-spi-dump.bin"
data = open(DUMP, "rb").read()
ZN_BASE=0x01C000
def hx(b): return " ".join(f"{x:02X}" for x in b)
# Full 256 zone records across 0x20000
named=0
for i in range(256):
off=ZN_BASE+i*512
d=data[off:off+512]
if all(b==0xFF for b in d): continue
name=bytes(b for b in d[0x04:0x14] if b!=0xFF).decode('gbk','ignore')
# skip the channel-copy records (rec0/rec8 look like packed 48B channels: byte0 in 0x0E/0x40 etc)
count=d[0]|(d[1]<<8)
hasname = any(32<=b<127 for b in d[0x04:0x14])
print(f"rec{i} @0x{off:06X} count={count} b0={d[0]:02X} b1={d[1]:02X} b2={d[2]} b3={d[3]} name={name!r} hasname={hasname}")
if hasname:
named+=1
chans=[d[0x14+k*2]|(d[0x15+k*2]<<8) for k in range(20)]
print(" head0-13:", hx(d[0:0x14]))
print(" chans:", chans)
print(" scan@0x1A4:", hx(d[0x1A4:0x1A4+25]))
print("named zones:", named)