# 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 1** → `mvns` 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 → (0xB7−x)`, 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) |