## RT-4D Menu Russification — Feasibility & Plan

**Verdict: YES (in-place) — every Russian letter already has a shipped glyph and the stock GBK double-byte render path draws it with zero firmware modification; the only real work is rewriting labels in place and abbreviating any that exceed 7 Cyrillic chars.**

## Why (the three gating facts)

1. **Cyrillic glyphs present? YES.** All 66 modern-Russian letters (33+33, incl. Ё/ё) exist in the shipped SPI font ROM, addressed as GB2312 row A7 (`0xA7A1..0xA7F1`). Confirmed two ways: the Unicode codepoint index at SPI `0x3F0000` lists `U+0410..U+044F` + `U+0401/U+0451` contiguously (slots 9825–9905), and feeding the raw GBK bytes through the firmware's own glyph-address math lands on populated bitmaps (`А`=`A7A7`→`0x1D00D8`, 20 nonzero bytes).
2. **Render supports double-byte? YES.** `draw_string @0x08008A50` classifies any byte `≥0x80` as a GBK lead byte, consumes the trail byte, forms `(lead<<8)|trail`, and blits a 14-px cell — this is the same classic GBK path the stock Chinese mode uses. No modification needed.
3. **Fields fit? PARTIALLY — but workably.** Records are a hard 16 bytes with the last 2 reserved for the item id, leaving **14 usable bytes = 7 Cyrillic chars** (2 bytes each). Short labels (`Зоны`, `Меню`, `Каналы`, `Имя`) fit natively; labels needing ≥8 Cyrillic chars must be abbreviated (`Настр.`, `Основ.`) — standard practice for Russian ham-radio UIs and not a blocker.

The single most important constraint: records are reached by `base + 16·index` and via 80 hard literal-pool pointers, so **every patch must stay exactly 16 bytes — no resize, no reorder.**

---

## A. Menu / UI string tables & the language system

**The `0x080253ED` table is the entire on-screen UI text pool**, a contiguous array of fixed 16-byte records spanning `0x080253ED → 0x0802723D` (7,760 bytes, 480 records). It starts cleanly after code/literal-pool at `0x080252CD..0x080253EC` and ends where the country/MCC table begins (`0x0802724A`). Verified head:

```
0x80253ed  42 61 73 69 63 20 53 65 74 20 20 20 20 20 30 31  |Basic Set     01|
0x80253fd  4b 65 79 20 44 65 66 69 6e 65 20 20 20 20 30 32  |Key Define    02|
0x802544d  4d 65 73 73 61 67 65 20 20 20 20 20 20 20 30 37  |Message       07|
0x802545d  52 61 64 69 6f 20 4e 61 6d 65 20 20 20 20 30 31  |Radio Name    01|  <- submenu restarts at 01
```

It interleaves two 16-byte record kinds, both translatable:

| Kind | Count | Unique | Examples | Suffix |
|---|---|---|---|---|
| Menu items (label + `NN` ordinal) | 181 | 157 | `Basic Set␠␠␠␠␠01`, `Scan Direction15`, `Personal ID␠␠␠01` | 2 ASCII digits |
| Option / enum-value / field labels | 299 | 282 | `Off`, `High`, `Slot 1`, `FM`/`AM`/`SSB`, `Show Caller Info`, `Callsign :`, `Unicode`/`GBK`/`None` | none |

**String model — pointer-indexed, not a packed blob.** A menu-descriptor table at `0x08015214`+ holds 80 literal-pool pointers, each targeting a 16-byte record at `base + 16·index` (all 16-aligned): `lit@0x0801521c → 0x080253ED` (rec #0), `lit@0x08015238 → 0x0802545D` (rec #7), etc. Each descriptor is `[RAM state word 0x2000xxxx][pointer to fixed-16 title record][NUL-terminated inline ASCII copy of the parent label]`. Consequences:
- A record's text = its 16 bytes; its identity = its index `(vaddr − 0x080253ED)/16`. **Records must stay exactly 16 bytes** or every pointer/index breaks.
- The trailing `NN` is a parsed ordinal (display ordering/bounds, restarts per submenu) — **not drawn** (renderer emits only the 14-col label) and **must be preserved byte-exact**.
- A full translation must patch **both** the fixed-16 record **and** the inline descriptor copy, or the breadcrumb parent-title stays English.

**No runtime language switch, no parallel Chinese table.** The image contains zero occurrences of `Language`/`English`/`Chinese`/`中文`/`语言` (ASCII or GBK). A full-app scan for coherent GBK CJK runs found exactly one — `0x08019920 = "正在进行数字调试"` (a service/debug status line), not a menu. The SPI dump below the font region has only a sequential codepoint index at `0x14C000`, not menu text. `language=Chinese` lives in the **PC upgrade tool's** `data.ini`, configuring that Windows app, not the radio. **Conclusion: Chinese mode renders CJK by pulling glyphs from the SPI font by codepoint/GBK index at draw time — there is no Chinese table to overwrite. Russification = rewriting the English strings in place.**

Excluded from russification (not UI language): country/MCC table `0x0802724A..0x0802833E` (~256 entries), pinyin IME syllable table `0x0802219A..0x080240D8`.

**Scope:** ~157 menu labels + ~282 option/field labels (both in the fixed-16 table) + ~80 inline descriptor copies (mostly duplicates) + ~150–250 NUL-terminated rodata prompt/status strings (`Please Wait...` @`0x08014964`, `Update DMR Chip` @`0x08014950`, `Unread SMS :` @`0x0801FB44`, `FM RX Standby` @`0x080159F4`, `PC Programing`, `Draft Full!`, `Calibration OK!`, …). **Total ~600–700 distinct strings**, all edited in place at ≤ original byte width.

## B. Font glyph coverage — Cyrillic exists (make-or-break: GO)

**All 66 Russian letters are present in the shipped font ROM.** The glyphs are served from external SPI data-flash, not MCU flash — the app image contains no standalone ASCII font table, and the firmware references SPI font-band literals (`0x00200000` @`0x0801469E`).

**Wide-font Unicode index at SPI `0x3F0000`:** a sorted table of 32,256 big-endian `u16` codepoints (`4E 02 4E 04 …`), `0xFFFF`-terminated at `0x3FFC00`. Slots 0–8288 are CJK; slots 8289+ are the GB2312 A1–A9 symbol/letter rows in Unicode order (Greek `U+0391` at slot 9569, then Cyrillic). ASCII proper returns no slot (served by the separate narrow bank).

**The key test — Cyrillic in the index (all 66, GB2312 A7-row order):**
```
slot 9825:0410(А) 9826:0411(Б) ... 9831:0401(Ё) ... 9857:042f(Я)   <- 33 upper incl. Ё
slot 9873:0430(а) 9874:0431(б) ... 9879:0451(ё) ... 9905:044f(я)   <- 33 lower incl. ё
```
Note Ё=U+0401 inserted right after Е and ё right after е — GB2312 collation, confirming this is the A7 row (`А=0xA7A1 … Я=0xA7C1, а=0xA7D1 … я=0xA7F1, Ё=0xA7A7, ё=0xA7D7`).

**Bitmap ROM format confirmed:** glyphs live in the dense SPI band `0x150000–0x240000`. Cell format is 16×16, 2 bytes/row, little-endian, bit 0 = leftmost pixel (32 B/glyph for the wide bank). Proof — rendering `U+4E00 一` (single horizontal stroke) yields a clean 14-px bar (`FF 3F` = 8+6 bits, LSB-first), nailing bit order and cell size. Cyrillic-block windows (near `0x1B9880`) show recognizable "А" and "Ж" letterforms.

**We do NOT need to add glyphs.** Russification requires **no font editing** — labels just need to be authored as GB2312 A7-row double-byte codes, a path the stock Chinese mode already exercises. One honest caveat: the exact slot→byte-address arithmetic across the *full* 32k table is banked/sparse (not a clean single-base `FONT_BASE + slot·32`), but this is irrelevant to russification because Section C proves the *menu* path uses a direct GBK plane computation with a known base, and that lands on populated Cyrillic cells.

## C. String rendering / draw path — GBK double-byte, zero firmware change

**Primitives.** `SPI_flash_read(dst,src,len)` @`0x08021826` (thunk `0x08021828`, ~120 callers) issues opcode `0x03`, clocks the address big-endian, streams bytes. Two blitters compute `font_base + glyph_index × cell_size`:

- **ASCII blitter `0x08007FB8`** — 7 px wide, 14-byte cell, base **`0x19C000`**: `index = char − 0x20`, `addr = base + index*14`.
- **CJK/GBK blitter `0x08008454`** — 14 px wide, 28-byte cell (`0x1C`), base **`0x19E000`**:
```
row = lead − 0x81 ; col = trail − 0x40
index = col + row*190      (− 1 when trail > 0x7F, skipping the 0x7F gap)
addr = 0x19E000 + index*28
```

**The decisive routine — `draw_string(x,y,str,len) @0x08008A50`** (~110 callers, incl. menu renderers `0x08013818–0x08014680`) iterates byte-by-byte and dispatches on a lead-byte test:
- byte `0x01–0x7F` → ASCII blitter, cursor +7 px.
- byte `0x80–0xFE` → **lead byte**: consumes the next byte, forms `(lead<<8)|trail`, calls CJK blitter, cursor +14 px, `i += 2`.
- `0xFF`/`0x00` → whitespace/terminator.

This is a **classic GBK renderer**: any high-bit byte is a double-byte lead. A sibling width-measurer at `0x0800553C` uses the identical `0x80` classification, confirming the convention project-wide.

**Crucially, the menu path uses direct GBK plane math, NOT the `0x3F0000` Unicode index.** The Unicode table is a separate path (SMS/contact rendering that stores UTF-16). Menu labels reach glyphs by their raw GBK bytes. Feeding GB2312 A7 Cyrillic bytes through the exact firmware math lands on populated cells:

| Char | GBK bytes | computed SPI offset | nonzero bytes in cell |
|---|---|---|---|
| `А` | `A7 A7` | `0x1D00D8` | 20 — legible Cyrillic-A |
| `Я` | `A7 C0` | `0x1D0394` | 21 |
| `а` | `A7 D1` | `0x1D0570` | 14 |

**Exact recipe: to draw `А`, the string must contain raw bytes `0xA7 0xA7`** (`0xA7 0xC0` for `Я`, `0xA7 0xD1` for `а`, …). `draw_string` sees `0xA7 ≥ 0x80`, takes the double-byte branch, and blitter `0x08008454` fetches the already-present Cyrillic cell — **zero firmware modification**. Cost: 2 bytes / 14 px per Cyrillic char.

(Note: the A7-row byte values differ slightly between the two analyses — Section B/D derive `А=0xA7A7` from the Unicode-order slot position, while Section D also cites `А=0xA7A1` from a direct `gb18030` round-trip. This exact byte mapping must be pinned on-target during the PoC — see the PoC verify step — but both agree Cyrillic lives in row A7 and the cells are populated.)

## D. Practical encoding, field-width constraints & effort

**Field-width math.** 16-byte records, last 2 bytes = item id ⇒ **14 usable bytes = max 7 Cyrillic chars** at 2 bytes each. Mixing 1-byte ASCII punctuation (`.`) is legal GBK and saves a byte.

| English | Russian | GBK bytes | Fits 14? |
|---|---|---|---|
| Zone Set | Зоны | 8 | Yes |
| Menu | Меню | 8 | Yes |
| Channel Set | Каналы | 12 | Yes |
| Radio Name | Имя | 6 | Yes |
| Basic Set | Настр. | 11 | Yes |
| Basic Set | Настройки | 18 | **No → abbreviate** |
| Basic Set | Основные | 16 | **No → Основ. (11B)** |

Every label ≥8 Cyrillic chars must be abbreviated (Настр., Каналы, Зоны, Сообщ., Скан, Аналог, Цифра) — normal for Russian ham UIs.

**Approach comparison.** (a) Overwrite English table in place, keep `language=English` — smallest change, firmware-only, ids preserved. (b) Find and overwrite a Chinese GBK table, set `language=Chinese` — but per Section A **no such table exists**, so this is not available. (c) Font-mod: overwrite ASCII cells `0x19C000` with 1-byte Cyrillic — gives 14 chars/label but touches font flash and sacrifices Latin. **Winner: (a).** Section C proves the menu `draw_string` is codepage-agnostic (dispatches on the `0x80` bit regardless of `language` setting) and already resolves A7 Cyrillic to populated glyphs — so overwriting the English table in place with GBK A7 bytes works directly, no config change, no font flash.

## Recommended approach

**Strategy (a): overwrite the English fixed-16 UI table in place with GB2312 A7-row (GBK double-byte) Russian, no config or font-flash changes.** Justification: the menu renderer `draw_string @0x08008A50` classifies bytes purely by the high bit and routes `0x80+` through the GBK blitter `0x08008454`, which computes a direct plane address landing on the Cyrillic cells that already ship in the SPI font ROM. This is a **single-artifact change** (patch the app `.bin`, reflash MCU app) — no dependency on a nonexistent Chinese table, no SPI font rewrite, no `data.ini` edit. Length-preserving patches mean no pointer/index/relocation fix-ups.

## Proof-of-concept — one label

Target the shortest high-value item: **"Zone Set" → "Зоны"** at vaddr `0x0802543D` (file offset `0x0802543D − 0x08002800 = 0x22C3D`).

Original 16 bytes:
```
5A 6F 6E 65 20 53 65 74 20 20 20 20 20 20 30 36   "Zone Set      06"
```
Replacement (GBK "Зоны" + space-pad + preserve id `06`, stays exactly 16 bytes):
```
A7 A9 A7 E0 A7 DF A7 ED 20 20 20 20 20 20 30 36
└─── Зоны ────────────┘ └── pad ───┘ └"06"┘
```

**Write & flash:** this is a **firmware (MCU app) patch**, not an SPI codeplug write. At file offset `0x22C3D` in `rt4d_stock_v3.25_abs_0x08002800.bin` overwrite bytes `[0..7]` = `A7 A9 A7 E0 A7 DF A7 ED`, `[8..13]` = `0x20`, leave `[14..15]` = `30 36`. Re-wrap the app at load base `0x08002800` (flash to `0x08000000`+`0x2800`), recompute any top-level image CRC if the loader checks one, and flash the MCU app via the normal firmware-upgrade path.

**Verify on target:** open the menu and confirm the "Zone Set" slot now reads **Зоны** in Cyrillic (not tofu/`?`). This single test simultaneously resolves the two residual unknowns: (1) that the menu draw path renders A7 double-byte bytes as glyphs, and (2) the **exact A7 byte→glyph mapping** — if "Зоны" renders but with wrong letters, re-derive the A7 trail bytes on-target (try both the `gb18030` round-trip values `А=0xA7A1…` and the Unicode-slot-order values `А=0xA7A7…`) and re-flash. If it renders correctly, the encoding is pinned and full russification is mechanical.

## Full-russification plan

1. **Pin the A7 encoding on-target** via the PoC above. Lock the definitive Cyrillic-char → GBK-byte table before batch work.
2. **Build the patch-table generator** (~40–60 lines Python): read the `0x080253ED..0x0802723D` table in 16-byte strides; for each record parse `label`+`id`; look up a translation dict; encode Russian to the pinned GBK bytes; space-pad to 14 bytes; re-append the 2-byte id; **assert `len == 16`**. Emit `(file_offset, old16, new16)` tuples. Because every patch is length-preserving, no address fix-ups.
3. **Author the translation dictionary** (~450 fixed-width labels + abbreviations that fit the 7-char ceiling and stay unambiguous). Reuse across duplicate labels.
4. **Patch inline descriptor copies** at `0x08015214`+ (NUL-terminated, ≤ original length) so breadcrumb parent-titles are Russian too.
5. **Patch the loose rodata prompt/status strings** (~150–250) — NUL-terminated, each edited at ≤ its original allocation (do not overrun into the next string). These are one-byte-encoding GBK too; abbreviate where Russian is longer.
6. **Recompute image CRC** if present; re-wrap at base `0x08002800`; flash MCU app.
7. **On-radio verification pass** — walk the full menu tree, checking abbreviations render, cursor advances correctly on mixed Russian(2B)+id(1B) fields, and no field overflows/overlaps.

**Tooling to build:** GBK encoder (trivial, `label.encode('gb18030')`), the length-preserving patch generator, an optional CRC recomputer, and a menu-tree walker checklist. **Effort:** ~half-day for tooling + PoC flash; ~1–2 days for the full translation table + on-radio verification.

**Risks:**
- **A7 byte mapping ambiguity** — the two analyses disagree on exact A7 trail bytes; the PoC resolves this before any batch work. Do not skip step 1.
- **A7 glyph slots pruned?** Section C shows the computed A7 offsets hit populated cells (`А`→20 nonzero bytes), so this appears fine, but confirm on-screen during the PoC — if any A7 slot is blank the letter renders as a gap.
- **Cursor/mixed-width** — Russian(14 px) + id digits(7 px) must not overlap; the mixed PoC field is the acid test.
- **Fixed-16 geometry** — never resize/reorder a record or change the trailing `NN`; pointer and index arithmetic will break menu dispatch.
- **Image integrity** — a top-level firmware CRC will reject naive patches; confirm/recompute before flashing.

**Safety note (calibration / app recovery):** all patches target the **MCU application region only** (`0x08002800`+). Do **not** touch the SPI data-flash — it holds the font ROM *and* the radio's calibration/codeplug; a bad SPI write can destroy factory RF calibration. Before flashing, dump and archive the current MCU app and the full 4 MB SPI (`radio-spi-dump.bin` already serves as the SPI baseline) so you can restore. Keep the stock app image on hand to reflash via the normal upgrade path if a patched app fails to boot. Since strategy (a) never rewrites the font or codeplug, the blast radius is limited to the app image, which is recoverable through the standard firmware-upgrade flow.