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>
Этот коммит содержится в:
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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]])
|
||||
@@ -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
|
||||
@@ -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)])
|
||||
@@ -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()
|
||||
@@ -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})")
|
||||
@@ -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 ✓
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
@@ -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}")
|
||||
@@ -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.
|
||||
@@ -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]))
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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})")
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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}")
|
||||
@@ -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)))
|
||||
@@ -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]))
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Ссылка в новой задаче
Block a user