Files
rt-4d/tools/build_batt.py
T
viktor 6d49ba3cba FM100B = Sicomm SCT3258TD (документирован); Talker Alias, агент, SPI-инструменты, восстановление TX
Главное:
- Опознан 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).
2026-07-24 02:52:51 +09:00

162 строки
6.8 KiB
Python

#!/usr/bin/env python3
"""Напряжение батареи вместо штатной иконки + уборка полосы у всплывающего окна.
Патчи:
1. Свой код в свободном месте после конца образа (0x08029000):
stub — пустышка `bx lr`
batt_draw — рисует "8.4V" мелким шрифтом 5x8 на странице 0
tick_router — вызывает оригинал, затем batt_draw (только на главном экране)
2. `bl 0x08008300` @0x080148E2 — отрисовка штатной шкалы -> stub (иконки больше нет).
3. `bl 0x08010A00` @0x0801E2A4 — «напряжение -> индекс 0..3» убрано (2 x nop),
чтобы флаг перерисовки взводился на каждые 0.1 В, а не только на порогах.
4. `bl 0x0800842C` @0x080085DE — верхняя полоса рамки всплывающего окна убрана.
5. `bl 0x0801DFD0` @0x08020806 — последний вызов цепочки отрисовки в ui_tick_normal
перенаправлен на tick_router.
Почему через конец цикла: раньше мы рисовали из середины кадра (из цепочки обновления
батареи), и любой более поздний вывод затирал цифры — при входящем вызове «7» теряла
перекладину и читалась как «1». Теперь наш текст ложится последним и перекрывает всё.
"""
import struct
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB
BASE = 0x08002800
SRC = "stock-fw/rt4d_ru_full.bin"
OUT = "stock-fw/rt4d_ru_batt.bin"
SMALL_CHAR = 0x080089AC # void small_char(u8 page, u8 x, u8 ch, u8 dim)
BATT_VOLT = 0x200008B0 # u16, напряжение * 10
G_SCREEN = 0x200008B3 # u8, текущий экран (0 = главный)
GAUGE_CALL = 0x080148E2 # bl 0x08008300 — штатная шкала батареи
GAUGE_FUNC = 0x08008300
LEVEL_CALL = 0x0801E2A4 # bl 0x08010A00 — напряжение -> индекс
LEVEL_FUNC = 0x08010A00
TOPBAR_CALL = 0x080085DE # bl fill_col_run(page=1) — верхняя полоса рамки
FILL_RUN = 0x0800842C
TICK_CALL = 0x08020806 # bl 0x0801DFD0 — последний вызов отрисовки за кадр
TICK_FUNC = 0x0801DFD0
MYCODE = 0x08029000
X0, X1, X2, X3 = 108, 113, 118, 123 # позиции четырёх символов
def thumb_bl(src: int, dst: int) -> bytes:
off = dst - (src + 4)
assert -(1 << 24) <= off < (1 << 24), "цель BL слишком далеко"
off >>= 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)
def main():
img = bytearray(open(SRC, "rb").read())
print(f"исходный образ: {len(img)} Б, конец на 0x{BASE + len(img) - 1:08X}")
assert BASE + len(img) == MYCODE, "образ не заканчивается там, где ждём код"
ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
def check(addr, target, what):
o = addr - BASE
cur = bytes(img[o:o + 4])
assert cur == thumb_bl(addr, target), \
f"по 0x{addr:08X} ({what}) не тот вызов: {cur.hex(' ')}"
return o
o_gauge = check(GAUGE_CALL, GAUGE_FUNC, "шкала батареи")
o_level = check(LEVEL_CALL, LEVEL_FUNC, "напряжение->индекс")
o_bar = check(TOPBAR_CALL, FILL_RUN, "верхняя полоса рамки")
o_tick = check(TICK_CALL, TICK_FUNC, "хвост отрисовки")
print("все четыре точки патча подтверждены")
# ---- раскладка своего кода ----
stub_addr = MYCODE
stub = bytes(ks.asm("bx lr", stub_addr)[0])
batt_addr = stub_addr + len(stub)
batt_asm = f"""
push {{r4, r5, r6, lr}}
movw r4, #{BATT_VOLT & 0xFFFF}
movt r4, #{(BATT_VOLT >> 16) & 0xFFFF}
ldrh r4, [r4]
uxtb r4, r4
movs r1, #205
muls r1, r4, r1
lsrs r5, r1, #11
movs r1, #10
muls r1, r5, r1
subs r4, r4, r1
movs r0, #0
movs r1, #{X0}
mov r2, r5
add r2, #0x30
movs r3, #0
bl #{SMALL_CHAR}
movs r0, #0
movs r1, #{X1}
movs r2, #0x2E
movs r3, #0
bl #{SMALL_CHAR}
movs r0, #0
movs r1, #{X2}
mov r2, r4
add r2, #0x30
movs r3, #0
bl #{SMALL_CHAR}
movs r0, #0
movs r1, #{X3}
movs r2, #0x56
movs r3, #0
bl #{SMALL_CHAR}
pop {{r4, r5, r6, pc}}
"""
batt = bytes(ks.asm(batt_asm, batt_addr)[0])
# tick_router: оригинал -> проверка экрана -> batt_draw.
# keystone не умеет метки, поэтому условный переход кодируем вручную.
tick_addr = batt_addr + len(batt)
partA = bytes(ks.asm(f"""
push {{r4, lr}}
bl #{TICK_FUNC}
movw r4, #{G_SCREEN & 0xFFFF}
movt r4, #{(G_SCREEN >> 16) & 0xFFFF}
ldrb r4, [r4]
cmp r4, #0
""", tick_addr)[0])
bne_addr = tick_addr + len(partA)
partB = bytes(ks.asm(f"bl #{batt_addr}", bne_addr + 2)[0])
bne = struct.pack("<H", 0xD100 | (((len(partB) - 2) // 2) & 0xFF)) # bne -> за partB
partC = bytes(ks.asm("pop {r4, pc}", bne_addr + 2 + len(partB))[0])
tick = partA + bne + partB + partC
code = stub + batt + tick
print(f"свой код: {len(code)} Б (stub 0x{stub_addr:08X}, "
f"batt_draw 0x{batt_addr:08X}, tick_router 0x{tick_addr:08X})")
img += code
while len(img) % 4:
img.append(0xFF)
# ---- патчи ----
img[o_gauge:o_gauge + 4] = thumb_bl(GAUGE_CALL, stub_addr)
print("штатная шкала батареи отключена")
img[o_level:o_level + 4] = b"\x00\xbf\x00\xbf"
print("преобразование в индекс убрано — перерисовка на каждые 0.1 В")
img[o_bar:o_bar + 4] = b"\x00\xbf\x00\xbf"
print("верхняя полоса рамки убрана")
img[o_tick:o_tick + 4] = thumb_bl(TICK_CALL, tick_addr)
print("вывод напряжения перевешен на конец кадра")
open(OUT, "wb").write(bytes(img))
print(f"готово: {OUT}, {len(img)} Б")
if __name__ == "__main__":
main()