#!/usr/bin/env python3
"""Talker Alias — приём: показ позывного вызывающего на экране (шаг 1 из 3).

Механика (подтверждена снифером на живом вызове):
  DMR-модуль присылает позывной кадром cmd 0x60:
      68 60 02 00 CK CK 00 LEN  02 01 <len> <ASCII-позывной> 00.. 10
  Сток payload этой команды выбрасывает (обработчик = nop; b default).

Наши патчи (поверх rt4d_ru_batt.bin — там уже вольтметр и роутер отрисовки):
  • ta_hook перехватывает fm100b_on_frame: на 0x60 копирует позывной в буфер RAM
    и ставит ta_valid; на 0x59 payload=0 (конец приёма) сбрасывает ta_valid;
    затем всегда вызывает штатный обработчик.
  • new_tick_router (последним за кадр): самоинициализация настроек по «магии»,
    затем на главном экране рисует вольтметр и — если включено и есть свежий
    позывной — сам позывной внизу экрана.

Буфер RAM 0x2000C7FC..: [magic4][en][valid][len][txt16] (свободный зазор SRAM).
Настройка ta_en инициализируется в 1 при каждом включении (постоянное хранение —
шаг 2, вместе с меню). Откат — rt4d_ru_batt.bin.
"""
import struct
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB

BASE = 0x08002800
SRC  = "stock-fw/rt4d_ru_batt.bin"
OUT  = "stock-fw/rt4d_ta.bin"

DRAW_STRING = 0x08008A50     # void draw_string(page,x,s,len, mode@[sp])
BIG_CHAR    = 0x08008100     # void big_char(page,x,ch,mode) — крупный шрифт 11x24 частоты
ON_FRAME    = 0x08006348     # void fm100b_on_frame(u8 *frame)
FRAME_CALL  = 0x08018DD8     # единственный bl on_frame
UI_HOOK     = 0x08020806     # последний вызов отрисовки в ui_tick_normal
TICK_FUNC   = 0x0801DFD0     # оригинал этого вызова
BATT_DRAW   = 0x08029002     # уже в rt4d_ru_batt.bin
G_SCREEN    = 0x200008B3

# Буфер в окне [0x2000AE48 .. 0x2000B108): выше вершины стека (SP=0x2000AE48,
# растёт вниз), ниже первого глобала (0x2000B108). Ничья 192-байтовая зона —
# в отличие от прошлого 0x2000C7FC, который оказался внутри чужого буфера.
MAGIC_ADDR = 0x2000B000
MAGIC_VAL  = 0x52543444      # 'RT4D'
TA_EN  = 0x2000B004
TA_VAL = 0x2000B005
TA_LEN = 0x2000B006
TA_TXT = 0x2000B007          # 16 байт -> до 0x2000B017
TA_PAGE = 3                  # ниже ID, крупный шрифт 24px (страницы 3-5)

EQ, NE, HS, LS, GE = 0, 1, 2, 9, 10


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:
    """Мини-ассемблер с метками: keystone-чанки + ручные ветвления."""
    def __init__(self, ks, base):
        self.ks = ks; self.base = base; self.items = []
    def asm(self, code):        self.items.append(("a", code))
    def label(self, name):      self.items.append(("l", name))
    def b(self, target, cond=None): self.items.append(("b", cond, target))

    def _sizes(self):
        sz = []
        for it in self.items:
            if it[0] == "a":
                sz.append(len(bytes(self.ks.asm(it[1], 0)[0])))
            elif it[0] == "b":
                sz.append(2)
            else:
                sz.append(0)
        return sz

    def build(self):
        sz = self._sizes()
        pos = {}; off = 0
        for it, s in zip(self.items, sz):
            if it[0] == "l":
                pos[it[1]] = off
            off += s
        out = bytearray(); off = 0
        for it, s in zip(self.items, sz):
            if it[0] == "a":
                out += bytes(self.ks.asm(it[1], self.base + off)[0])
            elif it[0] == "b":
                _, cond, tgt = it
                frm = self.base + off
                d = (self.base + pos[tgt]) - (frm + 4)
                imm = (d >> 1) & 0xFF
                if cond is None:
                    out += struct.pack("<H", 0xE000 | ((d >> 1) & 0x7FF))
                else:
                    out += struct.pack("<H", 0xD000 | (cond << 8) | imm)
            off += s
        return bytes(out)


def main():
    img = bytearray(open(SRC, "rb").read())
    end = BASE + len(img)
    code_at = (end + 3) & ~3
    img += b"\xff" * (code_at - end)
    print(f"основа {SRC}: {len(img)} Б; свой код с 0x{code_at:08X}")

    # проверки точек патча
    assert bytes(img[FRAME_CALL - BASE: FRAME_CALL - BASE + 4]) == thumb_bl(FRAME_CALL, ON_FRAME), \
        "0x08018DD8 не вызов on_frame"
    assert bytes(img[UI_HOOK - BASE: UI_HOOK - BASE + 4])[0:1] != b"\xff", "UI hook пуст"
    print("точки перехвата на месте")

    ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)

    def hexlo(a): return a & 0xFFFF
    def hexhi(a): return (a >> 16) & 0xFFFF

    # ---- ta_draw: позывной крупным шрифтом (11x24) по центру экрана ----
    # ширина символа 12px, высота 24px (страницы 2-4 = вертикальный центр).
    # x0 = (128 - len*12)/2 = 64 - len*6.  Длину зажимаем к 10 (10*12=120<=128).
    ta_draw_at = code_at
    a = Asm(ks, ta_draw_at)
    a.asm(f"""
        push {{r4, r5, r6, r7, lr}}
        movw r4, #{hexlo(TA_LEN)}
        movt r4, #{hexhi(TA_LEN)}
        ldrb r4, [r4]
        cmp r4, #10
    """)
    a.b("len_ok", LS)
    a.asm("movs r4, #10")
    a.label("len_ok")
    a.asm(f"""
        movs r0, #6
        muls r0, r4, r0
        movs r5, #64
        subs r5, r5, r0
        movw r6, #{hexlo(TA_TXT)}
        movt r6, #{hexhi(TA_TXT)}
        movs r7, #0
    """)
    a.label("cloop")
    a.asm("cmp r7, r4")
    a.b("cdone", HS)
    a.asm(f"""
        ldrb r2, [r6, r7]
        mov r1, r5
        movs r0, #{TA_PAGE}
        movs r3, #0
        bl #{BIG_CHAR}
        adds r5, #12
        adds r7, #1
    """)
    a.b("cloop")
    a.label("cdone")
    a.asm("pop {r4, r5, r6, r7, pc}")
    ta_draw = a.build()

    # ---- new_tick_router ----
    tick_at = ta_draw_at + len(ta_draw)
    r = Asm(ks, tick_at)
    r.asm(f"""
        push {{r4, lr}}
        bl #{TICK_FUNC}
        movw r0, #{hexlo(MAGIC_ADDR)}
        movt r0, #{hexhi(MAGIC_ADDR)}
        ldr r1, [r0]
        movw r2, #{hexlo(MAGIC_VAL)}
        movt r2, #{hexhi(MAGIC_VAL)}
        cmp r1, r2
    """)
    r.b("skipinit", EQ)
    r.asm(f"""
        str r2, [r0]
        movw r0, #{hexlo(TA_EN)}
        movt r0, #{hexhi(TA_EN)}
        movs r3, #1
        strb r3, [r0]
        movw r0, #{hexlo(TA_VAL)}
        movt r0, #{hexhi(TA_VAL)}
        movs r3, #0
        strb r3, [r0]
    """)
    r.label("skipinit")
    # вольтметр — только на главном экране (screen==0)
    r.asm(f"""
        movw r4, #{hexlo(G_SCREEN)}
        movt r4, #{hexhi(G_SCREEN)}
        ldrb r4, [r4]
        cmp r4, #0
    """)
    r.b("no_batt", NE)
    r.asm(f"bl #{BATT_DRAW}")
    r.label("no_batt")
    # ДИАГНОСТИКА: позывной рисуем на ЛЮБОМ экране, если есть свежий TA
    # (условие en временно убрано — сначала убедимся, что приём работает)
    r.asm(f"""
        movw r0, #{hexlo(TA_VAL)}
        movt r0, #{hexhi(TA_VAL)}
        ldrb r0, [r0]
        cmp r0, #0
    """)
    r.b("end", EQ)
    r.asm(f"bl #{ta_draw_at | 1}")
    r.label("end")
    r.asm("pop {r4, pc}")
    tick = r.build()

    # ---- ta_hook ----
    hook_at = tick_at + len(tick)
    h = Asm(ks, hook_at)
    h.asm("""
        push {r4, r5, r6, lr}
        mov r4, r0
        ldrb r5, [r4, #1]
        cmp r5, #0x60
    """)
    h.b("check59", NE)
    # handle 0x60: len = frame[10], clamp 16
    h.asm("""
        ldrb r5, [r4, #10]
        cmp r5, #16
    """)
    h.b("len_ok", LS)
    h.asm("movs r5, #16")
    h.label("len_ok")
    h.asm("movs r6, #0")
    h.label("loop")
    h.asm("""
        cmp r6, r5
    """)
    h.b("afterloop", HS)
    h.asm(f"""
        adds r0, r4, #11
        ldrb r0, [r0, r6]
        movw r1, #{hexlo(TA_TXT)}
        movt r1, #{hexhi(TA_TXT)}
        strb r0, [r1, r6]
        adds r6, #1
    """)
    h.b("loop")
    h.label("afterloop")
    h.asm(f"""
        movw r1, #{hexlo(TA_LEN)}
        movt r1, #{hexhi(TA_LEN)}
        strb r5, [r1]
        movw r1, #{hexlo(TA_VAL)}
        movt r1, #{hexhi(TA_VAL)}
        movs r0, #1
        strb r0, [r1]
    """)
    h.b("pass")
    h.label("check59")
    # кадр 0x06 = начало (sub 0x60) или конец (sub 0x6f) вызова -> сброс позывного.
    # На начало сброс срабатывает ДО прихода 0x60 (TA), поэтому не мешает показу.
    h.asm("cmp r5, #0x06")
    h.b("pass", NE)
    h.asm(f"""
        movw r1, #{hexlo(TA_VAL)}
        movt r1, #{hexhi(TA_VAL)}
        movs r0, #0
        strb r0, [r1]
    """)
    h.label("pass")
    h.asm(f"""
        mov r0, r4
        bl #{ON_FRAME}
        pop {{r4, r5, r6, pc}}
    """)
    hook = h.build()

    code = ta_draw + tick + hook
    img += code
    while len(img) % 4:
        img.append(0xFF)
    print(f"код: ta_draw 0x{ta_draw_at:08X} ({len(ta_draw)}Б), "
          f"tick 0x{tick_at:08X} ({len(tick)}Б), hook 0x{hook_at:08X} ({len(hook)}Б)")

    # репоинты
    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)
    print("UI-хук -> new_tick_router; on_frame -> ta_hook")

    open(OUT, "wb").write(bytes(img))
    print(f"готово: {OUT}, {len(img)} Б")


if __name__ == "__main__":
    main()
