#!/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}: "); continue print(f" ch{i}: {rec.hex(' ')}") asc = ''.join(chr(x) if 32<=x<127 else '.' for x in rec) print(f" ascii: {asc}")