Files
rt-4d/docs/highlight-value-editor.md
T
viktorиClaude Opus 4.8 ae36c3b729 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>
2026-07-08 15:47:22 +09:00

14 KiB
Исходник Ответственный История

RT-4D Menu Selection → Inverse-Video Highlight — RE Findings & Patch Spec

Firmware: rt4d_stock_v3.25_abs_0x08002800.bin (ARM Cortex-M4F Thumb, load base 0x08002800, size 155740 = 0x2605C). All addresses are virtual (vaddr). File offset = vaddr 0x08002800.


1. Render pipeline (as reverse-engineered)

1.1 Text / glyph layer (confirmed, trusted)

  • draw_string @0x08008A50. Signature: (r0=y_page, r1=x_pixel, r2=char*, r3=len, [sp,#0x28]=mode). The mode word at [sp,#0x28] is loaded (ldr r3,[sp,#0x28]) and passed as r3/r7 to the glyph blitters. Advance is +7px per ASCII char (adds r0,r6,#7).
  • ASCII glyph blitter @0x08007FB8, CJK blitter @0x08008454. Both copy a 14-byte (7 cols × 2 pages, column-major, 8 vertical px/byte) glyph into a stack buffer via 0x08021828, then transform by mode (r7) before blitting:
    • mode 0 → glyph copied as-is → normal (black text on clear background).
    • mode 1mvns every byte (full invert) + edge masks: even byte &=0xFE, odd byte &=0x7F. → INVERSE VIDEO: the whole 7×16 cell becomes a white bar with the glyph punched black, leaving a 1px gap top+bottom for clean row separation. Verified by simulation: mode1(empty cell)=FE 7F FE 7F… (solid bar w/ 1px gaps); mode1(solid px)=00 (black). This is exactly a modern "highlight bar with readable text".
    • mode 2 → odd bytes |=0x80 → sets the bottom pixel of the top page → a thin underline / bottom rule (used for title bars / section headers), NOT a full highlight.
    • mode ≥3 → falls through to normal (mode 3/4 seen in the home screen = plain text).
  • LCD framebuffer primitives (128×64 mono, ST7565/UC1701 class, page-addressed over SPI):
    • lcd_set_addr(r0=x, r1=y) @0x08014A7C — maps x → (0xB7x), sets page/column.
    • lcd_write_col(r0=byte) @0x08014B28 — writes one 8-px vertical column, auto-advances.
    • lcd_flush() @0x08014CB8 — DMA/SPI blit of the composed frame (calls SPI 0x08004CE0).
    • Line geometry: 7px pitch → 18 chars per 128px line; y is a page index 0..7 (menu text is drawn at page/y=4).

1.2 Menu system architecture (retained-mode, staged buffers)

The settings menu (Basic Set, Key Define, Analog Set, Digital Set, Channel Set, Zone Set, Message, Device Name, … full descriptor/label table at 0x0801521C–0x08015440, [submenu/handler ptr][16-byte fixed label] records) is not drawn by a single visible row loop. Instead it is retained-mode:

  • Widget-setup helpers stage a menu descriptor into RAM state struct @0x20000CE8 (fields: +1=widget type, +4/5=item count, +2=selected index, +6=sel, +7=flags) and label/value buffers @0x20000A83 (+0x15=current-item text, +0x27=next-item text, +0x17=inline-edit buffer, +0x2a=split/cursor position). Setup entry points: 0x08009C58 (generic selectable list), 0x0800B0B4 (numeric value), 0x0800B100, 0x0800B8D8, 0x08009D3C.
  • List refresh 0x0801CB10 computes sel and (sel+1)%count, copies the selected item into the "current" slot (+0x15) and the following item into the "next" slot (+0x27) — i.e. a 2-line window with the selected item always in the top ("current") slot.
  • Screen paint / blit happens in the home/menu render dispatcher 0x080142C0, which tbb-dispatches (@0x08014348, on mode byte [struct-1 +0x16], cases 0–6) to 7 small widget painters, all drawing at y=4:
    • case 0 → 0x08014074 (inline field / list-item painter) — see §2.
    • cases 1–6 → 0x080143A0 / 0x080143E6 / 0x08014442 / 0x080144DE / 0x08014514 / 0x0801435A (inline value editors: split a value into segments, draw the edited segment with mode 1 and the rest with mode 0).
  • The big per-item value screen 0x08014E20 (called from 0x0800ABD0 / 0x0800B054) is the submenu value/edit dispatch; it feeds text through 0x08009C58 (×8) rather than drawing directly.

Menu-list RENDER routine answer (task item 1): the visible menu row/field is drawn by 0x08014074 (dispatched from the render loop 0x080142C0 via the tbb @0x08014348). 0x08014074 is the only function in the image that both references the menu text buffer (0x20000A83) and calls a glyph blitter — it is the concrete draw site to patch.


2. How the current selection is drawn (task item 2)

0x08014074 (reads state struct 0x20000A83; L = [+0x2a] = split/caret position 0..0x10; buf = +0x17 = 17-char item text buffer). All draws at y=4. Decoded:

if [+0x2a] >= 0x11:                      ; buffer full — no caret
    draw_string(y=4, x=1, buf, len=0x11, mode=0)          ; whole line, normal
else:
    draw_string(y=4, x=1,        buf,       len=L,        mode=0)  ; text BEFORE the cursor  (normal)
    draw_string(y=4, x=L*7+1,    buf+L,     len=1,        mode=1)  ; the SELECTED char       (INVERSE)  ← cursor
    draw_string(y=4, x=L*7+8,    buf+L+1,   len=0x10-L,   mode=0)  ; text AFTER the cursor   (normal)

So today the "cursor / selection indicator" is a single character rendered in mode-1 inverse video (a 1-char-wide highlight caret), positioned at column L. The special treatment of the selected index is the middle draw_string call with len=1, mode=1 (instruction sequence: movs r0,#1 ; str r0,[sp] sets mode=1; movs r3,#1 sets len=1). The inline value-editor widgets (cases 1–6 of 0x080142C0) work the same way, inverting the segment currently being edited.

There is no >/triangle glyph and no separate arrow bitmap — the "arrow/left-cursor" the UI shows is this inverse caret block. (The only bitmap-cursor-like helper, 0x08008224, is the battery/RSSI icon drawer, unrelated.) So "remove the arrow" = "stop drawing the 1-char inverse caret and instead inverse the entire selected line".


3. Cleanest way to a full-width inverse highlight (task item 3)

Two mechanisms exist; mode-1 is the right one (mode 2 is only an underline):

  • (a) Draw the selected row's full text with mode 1 and pad the string to the full 18-col line width so the highlight bar spans edge-to-edge. Because mode-1 inverts each cell (including the space glyph → solid bar with 1px gaps), a right-padded string already yields a full-width readable highlight bar — no separate rectangle-fill routine is required.
  • (b) Fill/invert-rect helper: the image has no general "invert rectangle" routine; the only rect-ish primitive is the icon column-writer 0x08008300/0x08008224 (fixed 14-col templates). Re-purposing it is more invasive than (a). So approach (a) is chosen.

For the settings-list specifically the item text is staged into buffers padded with spaces already (buffers are memset-filled to 0x10 with 0x20/blanks by 0x080062EC before the label copy), so a mode-1 draw of the current-slot buffer paints the whole row as a bar.


4. Concrete patch (task item 4)

Approach: minimal, in-place, length-safe — switch the selected row's whole draw to mode 1

The selected line is the top / "current" slot of 0x08014074. Replace the 3-segment (normal | inverse-caret | normal) draw with one full-width mode-1 draw of the whole buffer. This makes the entire selected line an inverse highlight bar and eliminates the 1-char caret.

0x08014074 prologue+body bytes (for reference, from offset 0x11874):

0x08014074: 38 b5 22 48 90 f8 2a 00 11 28 09 db 00 20 11 23   ; push; ldr r0,=struct; ldrb r0,[r0,#0x2a]; cmp #0x11; blt; movs r0,#0; movs r3,#0x11
0x08014084: 1e 4a 17 32 01 21 00 90 04 20 f4 f7 df fc 34 e0   ; ldr r2,=struct; adds r2,#0x17; movs r1,#1; str r0,[sp]; movs r0,#4; bl draw_string; b .+
0x08014094: 00 20 00 90 19 48 90 f8 2a 30 00 f1 17 02 01 21   ; movs r0,#0; str r0,[sp](mode=0); ...; movs r1,#1
0x080140a4: 04 20 f4 f7 d3 fc 01 20 00 90 14 48 90 f8 2a 30   ; movs r0,#4; bl draw_string; movs r0,#1; str r0,[sp](mode=1)  ← caret
...

The blt at 0x0801407E (11 28 09 db: cmp r0,#0x11 / blt) already selects between the "full buffer" branch (0x08014080, draws the whole 17-char buffer at x=1,y=4) and the "3-segment caret" branch (0x08014094). The simplest robust change is: make the whole-buffer branch use mode 1, and force execution down that branch always (skip the caret path). That gives a full-line inverse highlight for the item and removes the caret entirely.

Patch — 2 sites, 4 bytes total, no code cave, no length change.

The full-buffer branch at 0x08014080 draws draw_string(y=4, x=1, buf, len=0x11, mode=[sp]). We (P1) force that branch to always run and (P2) make its mode = 1.

P1 — force the full-buffer / highlight branch. Remove the blt that would otherwise divert to the 3-segment caret path, so the full-buffer draw at 0x08014080 always executes:

  • vaddr 0x0801407E: original 09 DB (blt #0x08014094) → new 00 BF (nop). (2 bytes)

P2 — make that draw inverse. The branch sets its mode via movs r0,#0 ; str r0,[sp]:

  • vaddr 0x08014080: original 00 20 (movs r0,#0) → new 01 20 (movs r0,#1). (2 bytes)

Verified patched disassembly: cmp r0,#0x11 ; nop ; movs r0,#1 ; movs r3,#0x11 ; ldr r2,=buf ; adds r2,#0x17 ; movs r1,#1 ; str r0,[sp] ; movs r0,#4 ; bl draw_string → draws the full 17-char (space-padded) buffer at x=1,y=4 in mode 1; the caret path at 0x08014094 is now dead code.

Result: whenever this widget paints, it draws the full 17-char (space-padded to 18-col line) buffer at x=1, y=4 in mode 1 = full-width inverse-video highlight bar, and the old single-char inverse caret path (0x08014094…) is never reached → arrow/caret removed.

Final patch list — (vaddr, file_offset, original_bytes, new_bytes):

0x0801407E  (off 0x1187E)  : 09 DB  ->  00 BF     ; blt 0x8014094  ->  nop
0x08014080  (off 0x11880)  : 00 20  ->  01 20     ; movs r0,#0     ->  movs r0,#1  (mode 0 -> 1)

Bytes are shown in stored (file) order. Total change: 4 bytes, in place, no length change.

Optional wider fix (cases 1–6 / other menus)

The same 1-char-inverse→full-line-inverse idea applies to the inline value-editor widgets 0x080143A0…0x08014514 and the list refresh 0x0801CB10. Those are out of scope for a minimal, low-risk patch (each edits distinct value fields where a per-segment caret is actually desirable). Recommend shipping only the 0x08014074 change first, verify on-radio, then decide whether the value-editors should also flip.

Alternative (code-cave) approach, if per-row control is wanted

If you later want the highlight on a scrolling multi-row list (rather than the single current-item slot), a code cave is available:

  • 0xFF-erased cave: 0x08024AD2, 320 bytes free.
  • 0x00 cave: 0x08024778, 336 bytes free. A small Thumb helper could loop rows, calling draw_string(y=row_page, x=1, row_text, 18, mode = (row==sel)?1:0), then hook it in place of the bl 0x08014074. Not needed for the minimal fix above.

5. Residual risks & on-radio verification

Risks

  1. Buffer padding: the full-line branch draws len = the buffer count. If the item text isn't space-padded to the full 18 columns in every menu that reaches 0x08014074, the highlight bar will only span the text, not the whole line. Mitigation: the setup helpers memset the buffers to blanks (0x20) to width 0x10 before copying the label, so padding is generally present; confirm visually. If a bar is short, extend the draw len to 18 and ensure trailing spaces.
  2. Shared painter: 0x08014074 (case 0) may also render non-list inline fields (e.g. a name/DTMF entry field) where the single-char caret was intentional. Forcing full-line inverse there removes the per-char caret — acceptable for a "selected line" look but check text-entry screens remain usable (you lose the char-position caret). If that regresses a text-entry screen, gate the change on the widget-type byte instead of nop-ing the blt.
  3. 1px row gaps: mode-1 edge masks leave 1px clear at top and bottom of the cell — this is desirable (separates rows) and matches modern radios; no action needed.
  4. Checksum/signature: if the loader validates a firmware CRC/signature, patched bytes must be re-CRC'd. v3.25 is an absolute image at 0x08002800 — verify whether the bootloader checks an appended checksum before flashing.

Verification on-radio

  1. Flash patched image. Enter Menu. The currently-highlighted item should show as a solid inverse bar (white background, black text) spanning the line width; the old 1-char arrow/inverse caret should be gone.
  2. Scroll up/down: the highlight bar must follow the selection (top "current" slot) and text stays readable at every position.
  3. Enter a submenu with a numeric value (e.g. Backlight / Light Timer) — confirm value screens still render (those go through 0x08014E20, unaffected).
  4. Open a text-entry screen (Device Name / Message) — confirm it's still operable (risk #2). If the editing caret is needed there, switch to the type-gated variant.
  5. Watch for any garbled top line at boot/home screen (shared render dispatcher 0x080142C0) — the patch only alters case-0 widget, home layout should be unchanged.

Address quick-reference

what vaddr
draw_string 0x08008A50
ASCII glyph blitter (mode in r7) 0x08007FB8
CJK glyph blitter 0x08008454
lcd_set_addr / lcd_write_col / lcd_flush 0x08014A7C / 0x08014B28 / 0x08014CB8
menu descriptor/label table 0x0801521C–0x08015440
list-widget setup (generic) 0x08009C58
list refresh (current/next slot) 0x0801CB10
screen render dispatcher (tbb) 0x080142C0 (tbb @0x08014348)
selected-row painter (PATCH SITE) 0x08014074
menu state struct / text buffers (RAM) 0x20000CE8 / 0x20000A83
code caves 0x08024AD2 (320B, 0xFF) / 0x08024778 (336B, 0x00)