Files
rt-4d/docs/highlight-value-editor.md
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

230 строки
14 KiB
Markdown
Исходник Постоянная ссылка Ответственный История

Этот файл содержит неоднозначные символы Юникода
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 → (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) |