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
co-authored by Claude Opus 4.8
Коммит ae36c3b729
72 изменённых файлов: 24124 добавлений и 0 удалений
+242
Просмотреть файл
@@ -0,0 +1,242 @@
# RT-4D Standby / Main (Home) Screen — Render Map & Rewrite Recipe
**Key:** `main-screen` · **Target:** `stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin` (ARM CortexM4F Thumb, load/vaddr base `0x08002800`). All disassembly via capstone `CS_ARCH_ARM + CS_MODE_THUMB`.
This document reverseengineers the **standby / operating (VFO or channel) home screen** — the first UI redesign target. It ties into the three prior docs: geometry & draw primitives in **`display.md`**, the state machine & hook points in **`main-loop.md`**, and keys in **`input.md`**. Read those for the lowlevel draw API and the superloop hook; this doc covers *what the home screen paints, from which data, at which coordinates, and how to replace it.*
---
## 0. TL;DR
- The home screen is **screen id 0** (`g_screen @0x200008B3 == 0`). Its **draw handler is `home_draw @0x08013BD8`** (dispatched from `ui_draw_dispatch @0x0801E1BC`'s `tbb` table). An **alternate home / dualdisplay variant is id 7 → `0x08013804`**.
- Home render is **twostage and datadriven**, not hardcoded:
1. **`format_area_display(area, hi, shift) @0x08011414`** reads the live **channel cache** at `0x20002DEA` (48byte records) + the perarea **mode table** at `0x20002DCF`, formats frequency / channelname / modetag ASCII into the **display struct `g_disp @0x200009C3`**, and sets **dirty flags** in `g_dirty @0x200024EB`.
2. **`home_draw @0x08013BD8`** consumes `g_disp` + `g_dirty` and calls the `display.md` primitives (`draw_string`, `draw_number_row`, `draw_str_spaced`, `draw_marker5`) to paint each *changed* element.
- **Redraw is incremental / dirtyflag driven.** An element repaints only when its byte in `g_dirty @0x200024EB` is nonzero; setting a flag (e.g. after a channel/area change, RX event, or key) triggers repaint on the next UI tick. No fullframe clear.
- **The single struct you must understand is `g_disp @0x200009C3`.** Every onscreen string/flag is a field of it (see §3). To replace the home screen, either (a) keep `format_area_display` and relayout in your own `home_draw`, or (b) ignore both and render straight from the cache `0x20002DEA` + radio getters. Both keep the SPI codeplug format and CPS serial protocol **untouched** (they only *read* cache RAM and call display primitives).
---
## 1. Where the home screen is dispatched from
Chain (from `main-loop.md`, confirmed): superloop `app_main @0x0802136C` → normal UI tick `ui_tick_normal @0x080207DC`**`bl ui_draw_dispatch @0x0801E1BC` at callsite `0x080207E6`**. `ui_draw_dispatch` `tbb`jumps on `g_screen @0x200008B3`; **index 0 → `home_draw @0x08013BD8`**, index 7 → `0x08013804` (alt/dual home). The mirror input `tbb` (`ui_process_key @0x0801E6EC`) routes id 0 → key handler `0x08017F60`.
| screen id | draw handler | content struct | dirtyflag array | role |
|---:|---|---|---|---|
| **0** | **`home_draw 0x08013BD8`** | **`g_disp 0x200009C3`** | **`g_dirty 0x200024EB`** | **primary standby (VFO/channel) home** |
| 7 | `0x08013804` | `0x2000096A` | `0x200009BD` | alt home / dualdisplay variant |
| 2 | `0x08013980` | `0x200008D0` | `0x200008F5` | standbykey / list overlay |
> **Hook (recommended, from `main-loop.md` Option A):** redirect the `bl` at `0x080207E6` to `my_draw_router`; render your own home there. Everything else (RF, DMR RX, scan, battery, and the entire serial/CPS + SPI regionwrite path) is preserved. 2instruction cut.
---
## 2. The render pipeline (data → struct → pixels)
```
SPI codeplug channels (0x004000, 48B recs) ← format on flash, DO NOT CHANGE
│ loaded/cached by channel-load code (writes 0x20002DEA)
g_chcache @0x20002DEA : live channel record cache, 48-byte (0x30) stride, [area]
g_chmode @0x20002DCF : per-area display-mode bytes (= g_chcache 0x1B)
▼ format_area_display(area, hi, shift) @0x08011414 (4 call sites)
│ reads freq/tones/power/mode from cache, formats ASCII
g_disp @0x200009C3 : DISPLAY STRUCT (freq/name strings, mode flags, TG, markers)
│ + sets element dirty flags in g_dirty @0x200024EB
▼ home_draw @0x08013BD8 (dispatched every UI tick for screen 0)
│ paints only elements whose g_dirty byte != 0
draw_string / draw_number_row / draw_str_spaced / draw_marker5 (display.md prims)
▼ lcd_set_pos + lcd_write_col → direct-to-GDDRAM (no framebuffer)
```
### 2.1 `format_area_display(u8 area, u8 hi, u8 shift) @0x08011414` — the formatter
Prototype (AAPCS): `void format_area_display(u8 area /*r0*/, u8 hi /*r1→r5*/, u8 shift /*r2→r8*/)`.
- `area` (r4) = which VFO/area (0 = A, 1 = B) — indexes the cache `0x20002DEA + area*0x30`.
- `hi` (r5) = highlight/selection state (0,1,2,3) → stored to `g_disp+0x1D`; `2` selects an alt copy path.
- `shift` (r8) = TXshift display mode: `0`=simplex/RX shown, `1`=+shift, `2`=shift/reverse (Talkaround/Reverse). Selects which of RX(`+5`)/TX(`+9`) freq words is shown.
Reads (per 48byte cache record at `0x20002DEA + area*0x30`):
| cache off | field | used for |
|---:|---|---|
| `[+0]` | flags; **`>>6` = mode** (1 ⇒ DMR/digital, else analog) | "DMR"/"ANA" tag, "A-"/"D-" prefix |
| `[+5]` | u32 **RX frequency** (`MHz×100000`, `FREQ_MULTIPLIER`) | main freq digits |
| `[+9]` | u32 **TX frequency** | shown when `shift`≠0 / reverse |
| `[+0xD]` | u12 (`ubfx #0,#0xC`) **RX tone** (CTCSS/DCS) | tone field |
| `[+0xE]>>4` | nibble | power/flag |
| `[+0xF]` | u12 **TX tone** | tone field |
| `[+0x10]>>4` | nibble | flag |
| `[+0x20]` | 16byte **channel name** (ASCII) | name display (mode==2) |
Also reads globals: `g_power @0x20000B76``g_disp+0x1C` (the "HD"/status flag byte); `g_chmode @0x20002DCF + area``g_disp+0x1E` (element mode), `[+2+area]``g_disp+0x1F` (submode).
Writes into `g_disp @0x200009C3` (see §3) and builds ASCII via helpers:
- **`num_to_ascii(uint val, int ndigits) @0x08018530`** — rightaligns `ndigits` decimal ASCII into scratch **`0x200024D0`**. Used to render the frequency (`freq`, 8 digits) and channel number.
- **`str_insert_char(buf, ch, pos, len) @0x080135A8`** — inserts `ch` at `pos` (shifting right). Used to splice the **"." decimal point** into the frequency string (`ch=0x2E` at pos 3) → `"438.80000"`.
- **`memcpy_off(dst, src, dstoff, len) @0x080062EC`** — `dst[dstoff+i]=src[i]`. The workhorse for copying templates / name / formatted number into `g_disp` fields.
Inline ASCII templates embedded in the formatter (decoded): `"CH MODE"`, `"VFO MODE"`, `"VFO MODE-A"`, `"VFO MODE-B"`, `"CH-"`, `"A-"` (analog), `"D-"` (digital), `"ANA"`, `"DMR"`. These are the literal strings the stock home screen shows in the mode/status line.
Called from 4 sites (`0x080033D2`, `0x08004F60`, `0x08008F28`, `0x0800951C`) — always after a state change (area switch, channel change, mode toggle). Example call: `0x0800951C``format_area_display(area=[0x20000B5F], hi=2, shift=[0x200029BB+0x67])`.
### 2.2 `home_draw @0x08013BD8` — the painter
Consumes `g_disp @0x200009C3` and perelement dirty flags `g_dirty @0x200024EB`. For each element: `if (g_dirty[k]) draw…`. Ends `pop {…pc}` at `0x08013E50`. Row constants used: `r4=1` (top status page pair), `r5=2` (Area A page pair), `r6=5` (Area B page pair) — but these are rederived from `g_disp+0x42` (active area) at entry so the *selected* area gets the larger/highlighted rows. Uses `mode` arg to `draw_string` = normal(0)/inverse(1) for highlight.
---
## 3. `g_disp @0x200009C3` — the display struct (field map)
Two parallel area subrecords of stride **0x43** (Area A at `+0x00`, Area B at `+0x43`), plus shared/status fields. Offsets below are from `0x200009C3` (Area A); add `0x43` for Area B. All confirmed from `format_area_display` writes + `home_draw` reads.
| off | type | name | meaning / source | drawn by (element) |
|---:|---|---|---|---|
| `+0x00` | char[16] | `area_line0` | Area A top text (name/tag line) | `draw_str_spaced` (flag+0) |
| `+0x15` | char[7] | `line_prefix` | `"CH-"`+num or `"A-"/"D-"` mode tag | `draw_str_spaced`, x=0x68 (flag+6) |
| `+0x1C` | u8 | `status_flag` | ← `g_power @0x20000B76`; drives "HD"/" " topright indicator | `draw_str_spaced` x=0x74, template `"HD"`/`" "` (flag+2) |
| `+0x1D` | u8 | `hiliteA` | highlight state (r5) for Area A frame | `draw_frame_corners` (0x08008224) |
| `+0x1E` | u8 | `modeA` | **element mode**: 0 = blank, 1 = **frequency**, 2 = **channel name** | selects freq vs name path |
| `+0x1F` | u8 | `submodeA` | submode: 0/1 name width, 2 = bigdigit freq | picks big_char vs string |
| `+0x20` | char[16] | `contentA` | **the main Area A string** (formatted freq `"438.80000"` OR channel name OR `"CH MODE"`/`"VFO MODE"`) | `draw_number_row`(big digits) / `draw_string` on row r5 (flag+7) |
| `+0x30` | u8 | `modeB` | Area B element mode (0/1/2) | (flag+5) |
| `+0x31` | u8 | `submodeB` | Area B submode | |
| `+0x32` | char[16] | `contentB` | Area B main string | `draw_string` row r6 (flag+5) |
| `+0x42` | u8 | `active_area` | 0 = A selected, else B — chooses which rows get big/highlight | branches at handler entry |
| `+0x43…` | — | Area B record | mirror of `+0x00…` at stride 0x43 | (rendered by sibling fn `0x08013E64`) |
| `+0x63` | u8 | `ab_marker` | 0/1/2 A/B rightedge marker state ← `g_active_area 0x20000B5F` | `draw_marker5` pages 2 & 5 (flag+7 preamble) |
| `+0x64` | u8 | `batt_marker` | 0/1 → battery/dual marker layout | `draw_marker5` pages 1/3/6 (flag+8) |
Additional companion buffer **`g_disp2 @0x20000A28`** holds the **modetag / secondary line** ("ANA"/"DMR", "A-"/"D-") built alongside `contentA` — drawn on the status row.
> **`draw_marker5(page, on) @0x0800870C`** (aka the "batt/arrow icon" in `display.md`): draws a 5column glyph at columns **123–127** (`0x7B+i`). Pattern bytes `30 78 FC 30 30` = a rightpointing arrowhead → the **A/B activearea selector arrows** on the far right edge. `draw_icon_batt @0x08008874` is the structurallyidentical sibling used for the battery/dual markers.
---
## 4. Onscreen element → data source → draw call (the master table)
Coordinates: `page` = 8px row band (0=top…7), `x` = pixel column (0…127). Rows: top status ≈ page 1, Area A ≈ pages 2–3, Area B ≈ pages 5–6 (exact page passed in `r4/r5/r6`, reordered by `active_area`).
| # | Element (what you see) | Data source | Draw call (page,x) | Dirty flag |
|---|---|---|---|---|
| 1 | **Area A main line** — frequency `"438.80000"` (big digits) *or* channel name *or* `"CH MODE"/"VFO MODE"` | `g_disp+0x20``format_area_display` from cache `0x20002DEA[area]` (`+5` freq / `+0x20` name) & `modeA=g_disp+0x1E` | `draw_number_row(r5,0x14,…,8|7)` big digits **or** `draw_string(r5,0x64,…,2)` | `g_dirty[7]` |
| 2 | **Area B main line** (dualwatch second area) | `g_disp+0x32` ← same formatter, `area=1` | `draw_string(r6,0x0A/0x27/0x1C,…)` per `modeB=g_disp+0x30` | `g_dirty[5]` |
| 3 | **Top name/tag line** (radio name / area label) | `g_disp+0x00` | `draw_str_spaced(r4,0x00,…,16)` | `g_dirty[0]` |
| 4 | **Channel # / A/D prefix** (`"CH-001"`, `"A-"`, `"D-"`) | `g_disp+0x15` ← templates `"CH-"/"A-"/"D-"` + `num_to_ascii` | `draw_str_spaced(r4,0x68,…,4)` | `g_dirty[6]` |
| 5 | **Mode tag** `"ANA"` / `"DMR"` (FM/AM/SSB vs digital) | `g_disp2 0x20000A28``cache[+0]>>6` (mode bits) | `memcpy_off`+`draw_string` on status row | (with #4) |
| 6 | **HD / status indicator** (topright) | `g_disp+0x1C``g_power @0x20000B76`; template `"HD"`/`" "` | `draw_str_spaced(r4,0x74,…,2)` | `g_dirty[2]` |
| 7 | **A/B selector arrows** (right edge, col 123) | `g_disp+0x63``g_active_area 0x20000B5F`; `g_disp+0x64` | `draw_marker5(2,on)` & `draw_marker5(5,on)`; batt markers pages 1/3/6 | `g_dirty[7]`,`[8]` |
| 8 | **Area A highlight frame** | `g_disp+0x1D` (hilite) & `+0x42` | `draw_frame_corners(page,x) 0x08008224` | `g_dirty[3]` |
| 9 | **CTCSS/DCS tone, power nibble** | cache `[+0xD]`/`[+0xF]` (12bit tone), `[+0xE]`/`[+0x10]` (nibble) | folded into `contentA`/`g_disp2` via formatter | (with #1) |
| 10 | **DMR TG / Contact / ColorCode (RX)** | DMR call ctx `g_call @0x2000A6C5` (`+1`=call type, `+2`=ID, `+0x38`=name), fed from cache & FM100B | built by the callinfo path near `0x08006754`; painted on Area line as name/ID | RXevent driven |
| 11 | **Battery / signal icons** | see `display.md` §draw_icon_batt/signal (cols 21 & 123); battery volts from ADC1 `0x08010960` | `draw_icon_batt`/`draw_icon_signal` | periodgated task |
Notes:
- **FM / AM / SSB vs DMR**: the coarse analog/digital split is `cache[+0]>>6``"ANA"`/`"DMR"`. The fine analog demod (FM/AM/SSB) is a perchannel field (`RX Demod (A)` menu) stored in the channel record and shown via the same statusline path; it is *not* a separate draw primitive.
- **RX/TX arrow**: TX vs RX display is driven by the `shift` arg (Talkaround/Reverse) selecting cache `[+5]` (RX) vs `[+9]` (TX); the onair TX indicator is a statusflag element, not a distinct routine.
- **Dualwatch A/B**: `active_area (g_disp+0x42)` and `ab_marker (+0x63)` decide which area is highlighted and where the arrows point; `g_active_area @0x20000B5F` is the master.
---
## 5. Key RAM state variables (home screen)
| addr | width | name | role |
|---|---|---|---|
| `0x200009C3` | struct | `g_disp` | **home display struct** — all home strings/flags (§3). Area A `+0`, Area B `+0x43`. |
| `0x200024EB` | u8[9+] | `g_dirty` | perelement **dirty flags**; element k paints iff `g_dirty[k]!=0`. Indices used: 0,2,3,4,5,6,7,8. |
| `0x20002DEA` | rec[N] | `g_chcache` | **live channel cache**, 48byte (`0x30`) stride; `[+5]`RXfreq `[+9]`TXfreq `[+0xD/0xF]`tones `[+0x20]`name. Mirrors codeplug channel record — **readonly for UI**. |
| `0x20002DCF` | u8[] | `g_chmode` | perarea display mode/submode bytes (`= g_chcache 0x1B`). |
| `0x20000B5F` | u8 | `g_active_area` | selected VFO/area index (0=A). |
| `0x20000B76` | u8 | `g_power`/status | drives `g_disp+0x1C` (HD indicator). |
| `0x200024D0` | u8[16] | `g_numbuf` | scratch for `num_to_ascii` (freq/number ASCII build). |
| `0x2000A6C5` | struct | `g_call` | DMR RX call ctx: `+1` call type, `+2` ID (BCDdecoded via `0x080112B8`), `+0x38` name. |
| `0x200029BB` | struct | `g_uictx2` | secondary UI context (`+0x63`,`+0x67`,`+0x197` read by formatter callers). |
| `0x200008B3` | u8 | `g_screen` | screen id; 0 = this home screen. |
---
## 6. Redraw cadence & triggers
- **Cadence:** `home_draw` is called **every UI tick** (unthrottled) from `ui_tick_normal @0x080207DC` via `ui_draw_dispatch @0x0801E1BC` at `0x080207E6`, but each element is gated by its `g_dirty` byte, so a steady screen costs ~nothing.
- **Triggers (what sets `g_dirty`):**
- **Key / channel / area change** — the standbykey handler path (e.g. `0x080094F8`) calls `format_area_display`, then sets `g_dirty[7]=1` (`0x080095DA`) and `g_disp+0x63 = g_active_area`, forcing Area A repaint next tick. Other setters at `0x08005DA0`, `0x0800975E`.
- **Screen entry** — `ui_draw_dispatch` preamble `clear_dirty_flags @0x080136E4` walks a companion dirty row `0x20002512` and forcemarks fields dirty on entry (full repaint on screen switch).
- **RX / DMR event** — updates `g_call @0x2000A6C5` and reruns the formatter → dirties the affected area.
- **Periodic** — battery/signal icons and RSSI are repainted by the slower periodgated tasks in `ui_tick_normal` (counters `0x20000BF5/BF6/BF8`), independent of `g_dirty`.
- **Model:** directtoGDDRAM (no framebuffer, `display.md` §1). Partial repaint is safe because elements occupy fixed, nonoverlapping regions.
---
## 7. Recipe to REPLACE the home screen
### 7.1 Hook point
Redirect the draw dispatch (`main-loop.md` Option A): patch `bl 0x0801E1BC` at **`0x080207E6`** → `bl my_draw_router`. In `my_draw_router`, `if (g_screen==0) my_home(); else <call stock 0x0801E1BC for other screens>`. (Symmetrically patch key dispatch `0x08020816` if you want custom home navigation — see `input.md`.)
### 7.2 Two build strategies
**Strategy A — reuse stock formatter, relayout only (least work).**
Keep calling `format_area_display(area, hi, shift) @0x08011414` (it does the codeplug→ASCII math for you), then read the readymade strings from `g_disp @0x200009C3` and paint them wherever you like with `draw_string`. You get frequency/name/mode formatting for free and never touch the cache or codeplug.
```c
// stock entry points (thumb; set bit0 when taking a function pointer)
void format_area_display(u8 area,u8 hi,u8 shift); // 0x08011414 — fills g_disp
void draw_string(u8 pg,u8 x,const char*s,u8 n,u8 mode); // 0x08008A50
void draw_number_row(u8 pg,u8 x,const u8*d,u16 n,u8 mode);// 0x08008B90 (big freq digits)
#define G_DISP ((volatile u8*)0x200009C3)
void my_home(void){
format_area_display(g_active_area,2,0); // stock builds strings into g_disp
for(u8 pg=0;pg<8;pg++) blit_cols(pg,128,0,0); // clear (display.md §2.1)
draw_string(0, 2, (char*)&G_DISP[0x00], 16, 0); // top line
draw_string(2, 4, (char*)&G_DISP[0x20], 16, 0); // Area A main (freq/name)
draw_string(5, 4, (char*)&G_DISP[0x32], 16, 0); // Area B main
// add your own layout, icons, TG/CC from g_call@0x2000A6C5, battery, etc.
}
```
**Strategy B — full custom, read straight from the cache (max control).**
Skip `g_disp` entirely. Read the live channel cache and radio getters yourself:
```c
#define CH(area) ((volatile u8*)(0x20002DEA + (area)*0x30))
u32 rx = *(u32*)(CH(area)+5); // RX freq, MHz*100000
u32 tx = *(u32*)(CH(area)+9); // TX freq
u16 rtone = *(u16*)(CH(area)+0xD) & 0x0FFF; // RX CTCSS/DCS
u8 isDMR = CH(area)[0] >> 6; // 1 => digital
char*name = (char*)(CH(area)+0x20); // 16-byte channel name
// DMR RX overlay:
#define CALL ((volatile u8*)0x2000A6C5) // +1 type, +2 id, +0x38 name
// battery volts: call 0x08010960 (raw*4/0x42 -> tenths of a volt)
```
Format freq with `num_to_ascii(rx,8) @0x08018530` (→ `0x200024D0`) then `str_insert_char(buf,'.',3,len) @0x080135A8`, or roll your own. Paint with the `display.md` primitives.
### 7.3 Data getters + draw primitives your home screen will call
| purpose | vaddr | prototype |
|---|---|---|
| build all home strings (opt.) | `0x08011414` | `format_area_display(u8 area,u8 hi,u8 shift)` |
| decimal ASCII | `0x08018530` | `num_to_ascii(uint val,int ndigits)``0x200024D0` |
| insert char (decimal pt) | `0x080135A8` | `str_insert_char(char*buf,char ch,int pos,int len)` |
| byte copy w/ offset | `0x080062EC` | `memcpy_off(void*dst,const void*src,int dstoff,int len)` |
| text | `0x08008A50` | `draw_string(pg,x,s,n,mode)` |
| big freq digits | `0x08008B90` | `draw_number_row(pg,x,digits,n,mode)` |
| spaced small text | `0x08008BC6` | `draw_str_spaced(pg,x,s,n,mode)` |
| A/B arrow / batt marker | `0x0800870C` / `0x08008874` | `draw_marker5(page,on)` (col 123) |
| highlight frame corners | `0x08008224` | `draw_frame_corners(x,page)` |
| battery/signal icons | `0x08008874`/`0x08008A04` | (see `display.md`) |
| battery volts (ADC) | `0x08010960` | `u8 batt_read(void)` (tenths V) |
| clear region | `0x08008D24` | `blit_cols(pg,count,NULL,0)` |
### 7.4 Boundary compliance (hard constraint)
Everything above **reads** RAM caches (`0x20002DEA`, `0x2000A6C5`, `0x200009C3`) and calls **display + ADC** primitives. None of it writes SPI codeplug regions or touches the USART6 CPS framer / SPI regionwrite engine (`main-loop.md` §5). The channel cache `0x20002DEA` mirrors the onflash 48byte channel record but is a *separate RAM copy* — reading it cannot change the codeplug format. **CPS + codeplug compatibility is unaffected.**
---
## 8. Confidence
- **HIGH** — home screen = id 0, draw handler `0x08013BD8`; the formatter `format_area_display @0x08011414`; the display struct `g_disp @0x200009C3` field map; the channel cache `0x20002DEA` (48byte stride, RX@+5/TX@+9/tones@+0xD,0xF/name@+0x20); helpers `num_to_ascii 0x08018530`, `str_insert_char 0x080135A8`, `memcpy_off 0x080062EC`; the dirtyflag model (`g_dirty 0x200024EB`) and the `0x080095DA` trigger. All directly disassembled, literal pools resolved, inline templates decoded (`"CH MODE"/"VFO MODE"/"CH-"/"A-"/"D-"/"ANA"/"DMR"`).
- **MEDIUMHIGH** — exact page (row) numbers per element: the `r4/r5/r6` row registers are reordered by `active_area` at handler entry, so the absolute page of Area A vs B swaps with selection; the column xvalues are exact (from the draw calls). Battery/signal icon columns per `display.md`.
- **MEDIUM** — the DMR TG/ColorCode overlay path (`g_call 0x2000A6C5`, near `0x08006754`) is identified and its fields typed, but the full CC/slot rendering sequence on the standby line is only partially traced (it interleaves with FM100B RX events); the analog FM/AM/SSB subtag is inferred from the `RX Demod (A)` channel field rather than a distinct draw routine. Confirm ondevice when redesigning the DMR overlay.