Главное: - Опознан DMR-чип: FM100B = Sicomm SCT3258TD. Датащит Packet Interface (126 стр) в docs/. Открывает калибровку/AMBE/вокодер по документации. Родня: UV-4R, китайские клоны. Реализовано: - Приём Talker Alias (cmd 0x60) — позывной крупно на экране (build_ta.py) - Интерактивный агент — управление FM100B по COM без перешивки (build_agent.py + agent.py) - Полная карта команд FM100B с живой рации (fm100b-commands.md) - Работа с SPI: чтение/запись/восстановление codeplug и калибровки (spi_read/spi_wr/spi_restore) - Конструктор дисплея + редактор прошивки (tools/display-editor) Документация: - docs/SCT3258_packet_interface.txt/.pdf — спека DMR-чипа - docs/fm100b-commands.md — карта команд - docs/dmr-tx-recovery.md — диагноз/восстановление DMR-передачи Инструменты: flash_ps.py (прошивка через WSL/powershell), sniff.py (снифер кадров), hotspot_rxoffset_sweep.py, apply_patch.py, build_* сборщики. Важно: DMR TX-калибровка сбита в экспериментах со SET-командами модулю; восстановление через offset-компенсацию / .updat Radtel / калибровку по прибору (docs/dmr-tx-recovery.md).
214 строки
7.1 KiB
Python
214 строки
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Зонд достижимости AMBE: при старте шлёт FM100B «enable» по диапазону команд,
|
|
затем работает обычной рацией + снифер логирует всё, что модуль отдаёт.
|
|
|
|
Идея: если какая-то из перебираемых команд включает выдачу голосового AMBE
|
|
(модемный примитив ATC_RecordEn/RecvAmbe), то при входящем вызове в порт пойдёт
|
|
поток ~27-байтовых кадров, которых в стоке нет. Диапазон сужаем бинарно, меняя LIST.
|
|
|
|
python3 build_probe.py "0x40-0x6F" # первый прогон — весь диапазон
|
|
python3 build_probe.py "0x50-0x5F" # сузить
|
|
python3 tools/sniff.py 30 --raw # смотреть поток при вызове
|
|
|
|
Безопасность: исключены 0x3E (прошивка модуля) и уже используемые команды.
|
|
Основа rt4d_ru_batt.bin; откат — она же.
|
|
"""
|
|
import struct, sys
|
|
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB
|
|
|
|
BASE = 0x08002800
|
|
SRC = "stock-fw/rt4d_ru_batt.bin"
|
|
OUT = "stock-fw/rt4d_probe.bin"
|
|
|
|
ON_FRAME = 0x08006348
|
|
FRAME_CALL = 0x08018DD8
|
|
UI_HOOK = 0x08020806
|
|
TICK_FUNC = 0x0801DFD0
|
|
FM_SEND1 = 0x0801B044 # void fm100b_send1(cmd,b,sub,data, respbuf, timeout)
|
|
TX_BYTE = 0x08018BFC # порт к ПК
|
|
MAGIC_ADDR = 0x2000B000
|
|
MAGIC_VAL = 0x50524F42 # 'PROB'
|
|
RESP_BUF = 0x2000B020 # scratch для respbuf fm100b_send1
|
|
|
|
USED = {0x3E, 0x42, 0x48, 0x49, 0x4C, 0x4D, 0x55, 0x57, 0x62,
|
|
0x05, 0x06, 0x07, 0x09, 0x0A, 0x25, 0x59, 0x60}
|
|
EQ, NE, HS, LS = 0, 1, 2, 9
|
|
|
|
|
|
def parse_range(s):
|
|
a, b = s.split("-")
|
|
lo, hi = int(a, 16), int(b, 16)
|
|
return [c for c in range(lo, hi + 1) if c not in USED]
|
|
|
|
|
|
def thumb_bl(src, dst):
|
|
off = (dst - (src + 4)) >> 1
|
|
s = (off >> 23) & 1; i1 = (off >> 22) & 1; i2 = (off >> 21) & 1
|
|
imm10 = (off >> 11) & 0x3FF; imm11 = off & 0x7FF
|
|
j1 = (~i1 ^ s) & 1; j2 = (~i2 ^ s) & 1
|
|
return struct.pack("<HH", 0xF000 | (s << 10) | imm10,
|
|
0xD000 | (j1 << 13) | (j2 << 11) | imm11)
|
|
|
|
|
|
class Asm:
|
|
def __init__(self, ks, base): self.ks=ks; self.base=base; self.items=[]
|
|
def asm(self, c): self.items.append(("a", c))
|
|
def label(self, n): self.items.append(("l", n))
|
|
def b(self, t, cond=None): self.items.append(("b", cond, t))
|
|
def raw(self, bs): self.items.append(("r", bs))
|
|
def _sz(self, it):
|
|
if it[0]=="a": return len(bytes(self.ks.asm(it[1],0)[0]))
|
|
if it[0]=="b": return 2
|
|
if it[0]=="r": return len(it[1])
|
|
return 0
|
|
def build(self):
|
|
pos={}; off=0
|
|
for it in self.items:
|
|
if it[0]=="l": pos[it[1]]=off
|
|
off+=self._sz(it)
|
|
out=bytearray(); off=0
|
|
for it in self.items:
|
|
if it[0]=="a": out+=bytes(self.ks.asm(it[1], self.base+off)[0])
|
|
elif it[0]=="r": out+=it[1]
|
|
elif it[0]=="b":
|
|
_,cond,tgt=it; d=(self.base+pos[tgt])-(self.base+off+4)
|
|
if cond is None: out+=struct.pack("<H",0xE000|((d>>1)&0x7FF))
|
|
else: out+=struct.pack("<H",0xD000|(cond<<8)|((d>>1)&0xFF))
|
|
off+=self._sz(it)
|
|
return bytes(out)
|
|
|
|
|
|
def main():
|
|
rng = sys.argv[1] if len(sys.argv) > 1 else "0x40-0x6F"
|
|
cand = parse_range(rng)
|
|
print(f"диапазон {rng} -> {len(cand)} кандидатов: {' '.join('%02X'%c for c in cand)}")
|
|
|
|
img = bytearray(open(SRC, "rb").read())
|
|
end = BASE + len(img); code_at = (end+3)&~3
|
|
img += b"\xff"*(code_at-end)
|
|
ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
|
|
def lo(a): return a & 0xFFFF
|
|
def hi(a): return (a>>16)&0xFFFF
|
|
|
|
# таблица кандидатов (байты) + завершающий 0x00
|
|
tbl = bytes(cand) + b"\x00"
|
|
|
|
# ---- probe_once: разослать enable всем кандидатам ----
|
|
# табличка лежит сразу после кода; адрес вычислим после сборки — двухпроходно:
|
|
# проще положить таблицу ПЕРЕД функцией и адресовать movw.
|
|
tbl_at = code_at
|
|
probe_at = tbl_at + len(tbl)
|
|
if probe_at & 1: probe_at += 1 # выравнивание для кода
|
|
pad = probe_at - (tbl_at + len(tbl))
|
|
|
|
p = Asm(ks, probe_at)
|
|
p.asm(f"""
|
|
push {{r4, r5, lr}}
|
|
movw r4, #{lo(tbl_at)}
|
|
movt r4, #{hi(tbl_at)}
|
|
""")
|
|
p.label("ploop")
|
|
p.asm("ldrb r5, [r4]\ncmp r5, #0")
|
|
p.b("pdone", EQ)
|
|
# маркер в порт: F3 F3 cmd
|
|
p.asm(f"""
|
|
movs r0, #0xF3
|
|
bl #{TX_BYTE}
|
|
movs r0, #0xF3
|
|
bl #{TX_BYTE}
|
|
mov r0, r5
|
|
bl #{TX_BYTE}
|
|
""")
|
|
# fm100b_send1(cmd=r5, b=1, sub=1, data=1, respbuf, timeout=0x64)
|
|
p.asm(f"""
|
|
sub sp, #8
|
|
movw r0, #{lo(RESP_BUF)}
|
|
movt r0, #{hi(RESP_BUF)}
|
|
str r0, [sp]
|
|
movs r0, #0x64
|
|
str r0, [sp, #4]
|
|
mov r0, r5
|
|
movs r1, #1
|
|
movs r2, #1
|
|
movs r3, #1
|
|
bl #{FM_SEND1}
|
|
add sp, #8
|
|
adds r4, #1
|
|
""")
|
|
p.b("ploop")
|
|
p.label("pdone")
|
|
p.asm("pop {r4, r5, pc}")
|
|
probe = p.build()
|
|
|
|
# ---- tick_router: один раз вызвать probe_once ----
|
|
tick_at = probe_at + len(probe)
|
|
r = Asm(ks, tick_at)
|
|
r.asm(f"""
|
|
push {{lr}}
|
|
bl #{TICK_FUNC}
|
|
movw r0, #{lo(MAGIC_ADDR)}
|
|
movt r0, #{hi(MAGIC_ADDR)}
|
|
ldr r1, [r0]
|
|
movw r2, #{lo(MAGIC_VAL)}
|
|
movt r2, #{hi(MAGIC_VAL)}
|
|
cmp r1, r2
|
|
""")
|
|
r.b("done", EQ)
|
|
r.asm(f"""
|
|
str r2, [r0]
|
|
bl #{probe_at | 1}
|
|
""")
|
|
r.label("done")
|
|
r.asm("pop {pc}")
|
|
tick = r.build()
|
|
|
|
# ---- ta_hook = снифер: F1 F2 len + кадр + passthrough ----
|
|
hook_at = tick_at + len(tick)
|
|
h = Asm(ks, hook_at)
|
|
h.asm(f"""
|
|
push {{r4, r5, r6, lr}}
|
|
mov r4, r0
|
|
ldrb r5, [r4, #6]
|
|
ldrb r6, [r4, #7]
|
|
orr.w r5, r6, r5, lsl #8
|
|
adds r5, #9
|
|
uxtb r5, r5
|
|
movs r0, #0xF1
|
|
bl #{TX_BYTE}
|
|
movs r0, #0xF2
|
|
bl #{TX_BYTE}
|
|
mov r0, r5
|
|
bl #{TX_BYTE}
|
|
movs r6, #0
|
|
""")
|
|
h.label("sloop")
|
|
h.asm("cmp r6, r5")
|
|
h.b("sdone", HS)
|
|
h.asm(f"""
|
|
ldrb r0, [r4, r6]
|
|
bl #{TX_BYTE}
|
|
adds r6, #1
|
|
""")
|
|
h.b("sloop")
|
|
h.label("sdone")
|
|
h.asm(f"""
|
|
mov r0, r4
|
|
bl #{ON_FRAME}
|
|
pop {{r4, r5, r6, pc}}
|
|
""")
|
|
hook = h.build()
|
|
|
|
code = tbl + b"\x00"*pad + probe + tick + hook
|
|
img += code
|
|
while len(img) % 4: img.append(0xFF)
|
|
print(f"таблица 0x{tbl_at:08X}, probe 0x{probe_at:08X}, tick 0x{tick_at:08X}, hook 0x{hook_at:08X}")
|
|
|
|
img[UI_HOOK-BASE:UI_HOOK-BASE+4] = thumb_bl(UI_HOOK, tick_at)
|
|
img[FRAME_CALL-BASE:FRAME_CALL-BASE+4] = thumb_bl(FRAME_CALL, hook_at)
|
|
open(OUT,"wb").write(bytes(img))
|
|
print(f"готово: {OUT}, {len(img)} Б")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|