# tft_app: plan & arch

This commit is contained in:
Dmitry Akimov 2026-07-20 18:40:40 +03:00
parent 6120e71416
commit e2e3f8aff8
3 changed files with 487 additions and 0 deletions

5
.gitignore vendored
View file

@ -78,3 +78,8 @@ tools/host/.venv-host-win/
.zed/
project_tree.txt
tools/service_tui/dist/
# Референсные дампы старых серийных проектов (не коммитим)
firmware/tft_app/OLD_PROJECT/
firmware/tft_app/OLD_PROJECT_*/
firmware/tft_app/special/

278
firmware/tft_app/ARCH.md Normal file
View file

@ -0,0 +1,278 @@
# tft-app — архитектура
> Прошивка лифтового индикатора для семейства **MIMXRT1052 TFT (4 / 7 / 8 / 10")**.
> Документ — живой источник истины по слоистой архитектуре. Статус фаз разработки —
> в [PLAN.md](PLAN.md).
---
## Часть I. Требования (бизнес-логика)
### 1. Основные функции
**1.1 Индикация в реальном времени.** Местоположение кабины, спецрежимы, музыкальное
сопровождение при движении, озвучка сигналов.
- Сигналы от СУЛ приходят по последовательному интерфейсу (+24 В UART, кастомный
бинарный протокол) либо по CAN.
- Отображение состояния двух диспетчерских оптовходов: «Вызов подан» / «Вызов принят» / пусто.
**1.2 Обновление ПО в поле** через microSD (сам образ прошивки — задача bootloader,
A/B Direct-XIP; ассеты/layout — задача tft-app, см. §12).
**1.3 Графический интерфейс настроек** в рантайме (меню, навигация двумя кнопками).
### 2. Частности
**2.1 Отображение.** 4 типа дисплея (TFT4/7/8/10); одна кодовая база, две сборки
(см. §9). Ассеты (иконки, картинки, звук) — на встроенной флеш, обновляются с microSD.
Шрифты — C-массивы ([LCD Image Converter](https://lcd-image-converter.riuson.com/)).
Макеты UI унифицированы и упрощены; по запросу клиента — кастом.
**2.2 Связь с СУЛ.** Общий программный интерфейс ко всем СУЛ; одна прошивка — множество
протоколов; выбор протокола через меню. Протоколы: УЭЛ, УКЛ, НКУ-CAN, НКУ-SD7, УИМ (список
открыт).
**2.3 Настройки.** Общий набор параметров + кастом под заказ. Настройки влияют на UI и на
работу протокола (напр. адрес индикатора: 0..15 у НКУ-CAN, 1..50 у УИМ) — все связи
предусмотрены (см. §6, §8).
---
## Часть II. Архитектура
### 3. Принципы
1. **Домен не знает железа.** Декодеры протоколов и доменная модель — чистый C без единого
HAL-вызова, тестируются на хосте (Unity + fff) тем же способом, что модули bootloader.
2. **Одна прошивка — много протоколов.** Протокол выбирается в рантайме из настроек через
реестр драйверов (§6). Новый протокол = реализовать декодер + зарегистрировать, без правок
в остальных слоях.
3. **Деградация, а не отказ.** Любой сбой ассетов/layout/связи ведёт к безопасному упрощению
индикации (§7, §11), устройство не «кирпичится» и не гаснет.
4. **Данные, а не код.** Макет, каталог ассетов, таблица приоритетов режимов, дескрипторы
настроек протоколов — данные (компилируемые или загружаемые), не разветвления в коде.
5. **Границы модулей = конвенции репозитория.** Каждый слой — статическая библиотека CMake,
публичные заголовки в `include/`, приватное в `src/`. Аппаратные модули — через `bsp/*`.
### 4. Слои
```
┌──────────────────────────────────────────────────────────────────────┐
│ app/ FreeRTOS-задачи, wiring, main (firmware/tft_app)│ L4
├──────────────────────────────────────────────────────────────────────┤
│ ui/ view-model → экран (layout-движок, темы, fallback-рендер) │
│ menu/ экран настроек, навигация 2 кнопки │ L3 презентация
│ audio_policy доменное событие → команды звука │
├──────────────────────────────────────────────────────────────────────┤
│ controller редьюсер: sul_result_t (+кэш) → indication_task (diff) │
│ sul/ реестр драйверов; драйвер = decoder(pure) + transport(hw) │ L2 ДОМЕН
│ elevator_model канонический sul_result_t, приоритеты режимов │ (чистый C, host-тесты)
├──────────────────────────────────────────────────────────────────────┤
│ gfx (compositor+PXP+fonts) audio_engine (WAV/playlist над bsp_mqs) │
│ assets (TLV-ридер, image cache) settings_store fs (FatFS SD/QSPI) │ L1 сервисы (адаптеры к железу)
├──────────────────────────────────────────────────────────────────────┤
│ bsp/* can uart_host opto button mqs sd qspi_flash sdram display led … │ L0 БОЕВОЙ, не трогаем
└──────────────────────────────────────────────────────────────────────┘
```
**Поток данных** (адаптирован из проекта `special`, поверх FreeRTOS):
```
транспорт (CAN/UART) ─► sul_driver.transport ─► sul_driver.decode(frame) ─► sul_result_t
│ poll + timeout→default
controller.process(sul_result_t) ─► indication_task (dirty-флаги)
┌────────────────────────────────────────────────┬─────────┘
▼ ▼
ui.render(task, model, settings) audio_policy(task, model, settings)
(+ локальные входы: opto диспетчер, меню) │
audio_engine (bsp_mqs)
```
### 5. Размещение и тестируемость
| Слой | Где живёт | Тип | Host-тест |
|---|---|---|---|
| `elevator_model`, `sul` (декодеры), `controller`, `audio_policy`, layout-солвер | `firmware/tft_app/src/domain/*`, `.../ui/layout/*` | чистый C | **да** (golden-векторы) |
| `sul` транспорт-адаптеры | `firmware/tft_app/src/domain/sul/transport/*` | HW | HIL |
| `gfx`, `audio_engine`, `assets`, `settings_store`, `fs` | `firmware/tft_app/src/services/*` | HW/адаптеры | частично (парсеры/TLV — да) |
| `ui`, `menu`, `app` | `firmware/tft_app/src/{ui,menu,app}/*` | app | вид/меню — HIL |
Host-тесты — `tests/host/tft_app_*` (Unity + fff, компилируем `.c` домена против моков bsp,
по образцу `tests/host/mcuboot_port`, `tests/host/protocol`).
### 6. Связь с СУЛ (`sul`)
**Канонический выход любого протокола** — `sul_result_t` (надмножество; простой протокол не
заполняет лишнее):
```c
#define SUL_POS_MAX 4 /* сейчас значимы 2; запас до 4 */
typedef struct {
char pos[SUL_POS_MAX * 4 + 1]; /* UTF-8, позиция кабины (напр. "12","-1","П") */
char next[SUL_POS_MAX * 4 + 1];/* UTF-8, следующий этаж (или пусто) */
uint8_t direction; /* none / up / down / double */
/* Ортогональные сигналы (могут сосуществовать; приоритет разрешает controller): */
bool arrival; /* гонг */
bool movement; /* начало движения */
bool overload; /* перегруз */
bool fire_alarm; /* пожарная тревога */
bool lading; /* погрузка */
bool maintenance; /* сервисный режим */
bool fireman; /* режим пожарного */
bool seismic; /* сейсмоопасность */
bool error; /* авария */
uint16_t lading_secs; /* обратный отсчёт погрузки, 0 = нет */
uint8_t floor_num; /* производный числовой этаж для озвучки (0 = н/д) */
} sul_result_t;
```
- **Позиция — UTF-8 строка** (у нас реальные шрифты ASCII + кириллица), а не число:
универсально для всех протоколов, host-тест сравнивает строки. Декодер не знает про шрифт.
- **Escape-hatch под кастом не закладываем** (YAGNI); совместимость обеспечивает версия схемы.
- **Валидация рендеримости** (§11): декодер может выдать кодпойнт вне покрытия активного
шрифта (кастомные коды протокола) → по-символьный fallback на этапе рендера.
**Драйвер = чистый декодер + транспорт-адаптер:**
```c
typedef struct { const uint8_t *data; uint16_t len; uint32_t id; uint8_t bus; } sul_frame_t;
/* Чистая функция — без железа, host-тестируется golden-векторами. */
typedef sul_status_t (*sul_decode_fn)(void *ctx, const sul_frame_t *frame, sul_result_t *out);
typedef struct {
uint8_t id; /* идентификатор протокола (стабильный) */
const char *name; /* для меню */
sul_decode_fn decode; /* pure */
const sul_settings_desc_t *settings; /* per-protocol параметры (§8) */
/* транспорт (CAN/UART) — отдельный тонкий адаптер, привязан к драйверу */
} sul_driver_t;
```
- **Реестр** `sul_registry[]` — таблица драйверов по `id`. Активный выбирается из настроек.
Добавление протокола = запись в таблицу.
- **Poll + timeout.** `sul` опрашивается периодически; при отсутствии кадров дольше таймаута
выдаётся `default` (потеря связи → сброс режимного состояния), как в `special`/OLD_PROJECT.
### 7. Контроллер и приоритеты режимов
`controller.process(sul_result_t*) → indication_task_t` (dirty-флаги: `pos_pending`,
`direction_pending`, `mode_pending`, `arrival_pending`, …) — презентация перерисовывает и
озвучивает только изменившееся (кэш прошлого состояния внутри контроллера).
- Ортогональные булевы сигналы `sul_result_t` сходятся в **один экранный режим** через
**таблицу приоритетов** (пожар > перегруз > сейсмо > сервис > … > норма).
- **Таблица приоритетов — данные, потенциально клиентские**, живёт рядом с клиентским
конфигом/layout, а не хардкодом в контроллере. Контроллер применяет активную таблицу.
### 8. Настройки
Три раздельных источника (не смешивать):
1. **`sul_result_t`** — только данные от СУЛ (§6).
2. **`settings`** — конфиг устройства/пользователя: громкости, лого, серийник, ёмкость, год,
выбранный протокол, панель (provisioning). Хранится на QSPI (сектор настроек), формат с
магиком/версией/CRC (развитие `settings_manager` из OLD_PROJECT).
3. **Локальные входы** — оптовходы (диспетчерский вызов/ответ) и кнопки/меню; вливаются на
уровне контроллера/презентации, не часть протокольных данных.
**Per-protocol настройки.** Каждый протокол регистрирует `sul_settings_desc_t` — какие у него
параметры, диапазоны, подписи для меню (напр. адрес 0..15 у НКУ-CAN, 1..50 у УИМ). `settings`
держит слайс под активный протокол; `driver` получает свой конфиг; меню строится из дескриптора
(развитие `settings_mgr_bind_menu`). Общие настройки — отдельно от протокольных.
### 9. Дисплеи и платы
Платы отличаются **только RGB-интерфейсом LCDIF** (TFT4 — 40pin без пинов ориентации; TFT7/8/10
— 50pin с U/D·L/R). SDRAM, SEMC, периферия — одинаковы. Панели TFT7/8/10 уже разведены рантаймом
в `bsp_display` (таблица `panel_config[]`: тайминги, клок, `has_orientation_pins`).
| Профиль сборки | LCDIF | Панель | Клок |
|---|---|---|---|
| `app-tft4` | 40pin, без ориентации | TFT4 (фикс) | Video PLL |
| `app-big` | 50pin, с U/D·L/R | 7 / 8 / 10 — **рантайм** из provisioning | PLL2 |
Различие изолировано в одном board-файле пин-мукса LCDIF. **Тип панели — provisioning-параметр**
(пишется service_tui), не пользовательская настройка. Матрица сборки — два buildPreset
(`app-tft4`, `app-big`), как `bootloader`/`firmware-test`.
### 10. Карта QSPI (W25Q128, 16 МБ)
| Регион | Смещение | Размер | Владелец |
|---|---|---|---|
| bootloader | `0x000000` | 256 КБ | bootloader |
| slot A (tft_app) | `0x040000` | 2 МБ | MCUboot |
| slot Б (tft_app) | `0x240000` | 2 МБ | MCUboot |
| **layout-регион** | `0x440000` | 64 КБ | tft_app |
| **assets-регион** | `0x450000` | ≈ 11 МБ | tft_app |
| settings | `0xFFE000` | 4 КБ | tft_app |
layout- и assets-регионы — **вне** flash-area загрузчика (bootutil про них не знает). Разметка
фиксируется в **едином partition-заголовке**, из которого читают app и генератор для service_tui.
### 11. Ассеты, layout и fallback
**Разделяем два артефакта** вместо монолитного `style.img`:
- **layout** — декларативная таблица виджетов (что рисуем: позиция/стрелка/режим/лого/…, якорь,
размер в %/единицах, шрифт, привязка к полю модели). Якорное позиционирование → один макет
раскладывается под 480×272 и под 1024×600. Формат — **TLV**, схема версионируется.
Layout-солвер (якорь→пиксели) — чистый C, host-тест (golden-render). Виджеты бывают
**примитивные** (text/rect/line/arrow) и **спрайтовые** (sprite-by-id) — единый движок.
- **assets** — упакованный индексированный **TLV-бандл** (host-утилита `tools/`): PNG → сырой
ARGB8888 на этапе пака (в рантайме нет lodepng и boot-time декодирования; XIP-mmap с FlexSPI,
блит без копии). Звук — WAV в том же бандле.
**Загрузка layout при старте:** сначала layout-регион QSPI (магик+версия схемы+CRC+совместимость);
если валиден — берём его; иначе — **встроенный default**.
- **Модель 1 (baked):** нужный layout вкомпилен как default в app-слот → цельный подписанный
образ, тестируется целиком, деплой через bootloader A/B. layout-регион пуст.
- **Модель 2 (injected):** базовый бинарь; service_tui пишет клиентский layout-блоб в регион
(вне слота → app-обновление его не трогает). Пересборка не нужна. Валидация блоба — на host
(схема + golden-render), поэтому проверка клиентского layout не требует сборки клиентского
бинаря.
**Fallback (safe-mode).** `default`-layout — **asset-free и FS-free**: не монтирует QSPI-FAT/SD,
не трогает TLV-ридер. Зависимости — только `bsp_display` + `bsp_can`/`sul` + домен +
один вкомпилированный шрифт + примитивы `gfx`. Рисует **только этаж + стрелки (вверх/вниз)**,
чёрный фон, без звука. Это же — первый экран walking-skeleton (§PLAN, Фаза 1).
**Деградация — по-виджетно.** layout битый → полный fallback. layout валиден, но ассет
отсутствует/битый → рисуем **примитивную форму этого виджета**, а не полный откат. Символ вне
покрытия шрифта → по-символьный fallback. Решение защёлкивается на старте (при XIP-mmap рантайм
не ревалидирует — иначе битое чтение = hardfault); валидация ассетов — CRC в индексе TLV.
### 12. Обновление в поле
- **Ассеты/layout — app-side updater** (доменные данные; app знает схему и валидацию;
переиспользуем `bsp_qspi` + FatFS-на-SD, как `update_style_task` в OLD_PROJECT). Запись —
потоковая, erase-before-write по 4 КБ, **заголовок-валидатор пишется последним** (торн-запись
→ регион невалиден → безопасный fallback, не кирпич).
- **Provisioning-заливка** (service_tui, заводской/сервисный контекст): канал — USB-CDC
загрузчика или USB-SDP ROM, **при условии приемлемой скорости**. Если заливка ассетов этим
путём окажется слишком медленной — отказываемся и делаем **только через microSD**. Решение —
по замеру на Фазе обновлений (§PLAN).
### 13. Тестирование
- **Host (Unity + fff):** декодеры всех протоколов (golden-векторы кадров → `sul_result_t`),
контроллер (diff + приоритеты), layout-солвер (golden-render), TLV-ридер/паковщик, парсеры
настроек, валидация рендеримости.
- **HIL:** gfx/PXP/ELCDIF, audio/MQS, SD/QSPI, реальные транспорты CAN/UART (по образцу
`tests/target/*` и `06_test_firmware_*`).
---
## Приложение. Заимствования из проектов
- `special` — эталон доменного дизайна: `UnifiedProtocolData`→`sul_result_t`, `proto_handler`
(poll+timeout)→реестр `sul`, `controller`+`IndicationTask`. Адаптирован под FreeRTOS и снейк-кейс.
- `OLD_PROJECT` (НКУ-CAN, TFT7/10) — боевые `bsp`, compositor (PXP), audio-движок,
`settings_manager`, декодер НКУ-CAN (PACKET1..5, адресация, удалённая установка адреса).
- `OLD_PROJECT_TFT8_UKL` — наш `bsp` (button/opto/sd/w25q/settings/file_loader), UART+CAN.
- `TFT10_UIM`, `TFT4_UIM`, `TFT4_SD7`, `TFT4_UEL` — декодеры УИМ / НКУ-SD7 / УЭЛ, матрица
«протокол × дисплей».

204
firmware/tft_app/PLAN.md Normal file
View file

@ -0,0 +1,204 @@
# tft-app — план разработки
> Пофазовый план. Стратегия — **walking skeleton**: сначала тонкий сквозной срез
> (CAN → модель → этаж на экране), затем наращивание слой за слоем. Архитектура —
> [ARCH.md](ARCH.md). Этот файл — **источник истины по статусу фаз**.
>
> Каждая фаза: цель · объём · артефакты · тесты · критерий выхода. Ранние фазы детальны,
> поздние укрупнены и детализируются по мере приближения.
>
> Легенда статуса: ⬜ не начата · 🟨 в работе · ✅ готова.
---
## Обзор фаз
| # | Фаза | Статус |
|---|---|---|
| 0 | Каркас проекта и сборка | ⬜ |
| 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ⬜ |
| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | ⬜ |
| 3 | Настройки и меню, локальные входы (opto) | ⬜ |
| 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ |
| 5 | Layout-движок: схема, солвер, темы, injected-регион | ⬜ |
| 6 | Аудио: движок над MQS, `audio_policy` | ⬜ |
| 7 | Обновление в поле (SD) + provisioning-канал | ⬜ |
| 8 | Мультипротокол: УИМ / SD7 / УЭЛ / УКЛ + per-client | ⬜ |
| 9 | Профиль `app-tft4` (40pin LCDIF, Video PLL) | ⬜ |
---
## Фаза 0 — Каркас проекта и сборка ⬜
**Цель.** `firmware/tft_app` собирается как ARM-таргет, грузится через bootloader, мигает
heartbeat; host-тест-харнесс на месте. Пустой, но живой каркас всех слоёв.
**Объём.**
- `firmware/tft_app/{CMakeLists.txt, src/, README.md}`; подкаталоги `src/{domain,services,ui,menu,app}`.
- `main.c`: `BOARD_ConfigMPU/InitPins/BootClock`, FreeRTOS, задача heartbeat (`bsp_led`, `bsp_tick`).
- Подключить в корневой `CMakeLists.txt` (заменить `firmware/bootloader/test_stub`).
- CMakePresets: buildPresets `app-big-debug/release` (профиль `app-big`). `app-tft4` — заглушка до Фазы 9.
- Линковка как MCUboot-слот-образа (Direct-XIP, `flexspi_nor.ld` + slot-offset), HAB-таргет.
- `tests/host/tft_app_smoke/` — пустой Unity-таргет, зелёный (проверка харнесса).
- `.vscode/launch.json` — конфигурация `🐛 Debug: tft_app (FreeRTOS)` (уже упомянута в DEV_ARCH).
**Тесты.** `just build::build-app-big-debug` собирает ELF; host smoke-тест зелёный.
**Критерий выхода.** ELF грузится bootloader'ом из слота, heartbeat мигает (проверка — пользователь
на железе). Удалён `test_stub` из корневого CMake.
---
## Фаза 1 — Walking skeleton ⬜
**Цель.** Сквозной срез: реальный CAN-кадр НКУ → чистый декодер → контроллер → **fallback-рендер
этажа и стрелки** на экране. Никаких ассетов, FS, TLV, меню.
**Объём.**
- `domain/elevator_model`: `sul_result_t`, `sul_status_t`, дефолт-состояние.
- `domain/sul`: тип `sul_driver_t`, реестр (одна запись); **чистый декодер `sul_nku_can`**
(подмножество: `pos`, `direction`) — портирован из OLD_PROJECT `msg_receiver_task` PACKET1/3.
- `domain/sul/transport/can`: тонкий адаптер `bsp_can``sul_frame_t`.
- `domain/controller`: `process()``indication_task_t` (diff), кэш, poll + timeout→default.
- `services/gfx`: минимум над `bsp_display` — один вкомпилированный шрифт + примитивы (стрелка).
- `ui/fallback`: рендер `pos` + стрелка (asset-free, FS-free) — §11 ARCH.
- `app`: задачи `sul_rx` (poll CAN) и `render`; wiring.
**Тесты (host).**
- `tests/host/tft_app_sul_nku`: golden-векторы CAN-кадров → ожидаемый `sul_result_t`.
- `tests/host/tft_app_controller`: последовательность `sul_result_t` → корректные dirty-флаги,
таймаут→default.
**Критерий выхода.** На стенде: подан НКУ-CAN трафик → на экране меняется номер этажа и стрелка
направления; при пропадании трафика — «--». Домен-тесты зелёные в CI.
---
## Фаза 2 — Домен: полный контракт ⬜
**Цель.** `sul_result_t` в полном объёме (§6 ARCH), таблица приоритетов режимов, устойчивый
таймаут/потеря связи.
**Объём.** Все поля модели (next, сигналы, `lading_secs`, `floor_num`); полный декодер НКУ-CAN
(PACKET1..5, режимы, удалённая установка адреса); `controller` разрешает приоритет режимов по
**таблице-данным** (§7); валидация рендеримости позиции (renderable-множество шрифта +
по-символьный fallback).
**Тесты (host).** Расширенные golden-векторы НКУ (все режимы, спецсостояния); тесты таблицы
приоритетов; тесты валидации/fallback символов.
**Критерий выхода.** Все режимы НКУ-CAN корректно доходят до fallback-рендера; полное host-покрытие
декодера и приоритетов.
---
## Фаза 3 — Настройки и меню ⬜
**Цель.** Персистентные настройки на QSPI, меню на двух кнопках, локальные входы (opto).
**Объём.** `services/settings_store` (магик/версия/CRC, сектор настроек §10); дескрипторы
per-protocol настроек (§8); `menu/` (навигация, редактирование, привязка к дескрипторам);
`bsp_button` + `bsp_opto` (диспетчерский вызов/ответ) → контроллер/презентация.
**Тесты (host).** Сериализация/дефолты/CRC настроек; логика навигации меню; связывание
дескрипторов.
**Критерий выхода.** Меню редактирует и сохраняет настройки; opto-иконки отображаются; выбор
протокола и его параметров работает (пока протокол один).
---
## Фаза 4 — Ассеты (TLV-бандл) ⬜
**Цель.** Ассеты в упакованном индексированном бандле на QSPI, XIP-блит без рантайм-декодирования.
**Объём.** Формат TLV (индекс, per-entry CRC, версия); host-утилита `tools/` (PNG→ARGB8888, WAV,
пак); `services/assets` (TLV-ридер, XIP-mmap, image cache); partition-заголовок (§10).
**Тесты (host).** Раунд-трип паковщик↔ридер; целостность индекса/CRC; кросс-валидация id.
**Критерий выхода.** Спрайты грузятся из assets-региона и блитятся; битый/отсутствующий ассет →
по-виджетная деградация.
---
## Фаза 5 — Layout-движок ⬜
**Цель.** Декларативные макеты: схема TLV, якорный солвер, темы/шаблоны, injected-регион +
baked default.
**Объём.** Схема layout (примитивные + спрайтовые виджеты, привязка к полям модели); солвер
якорь→пиксели (чистый C); загрузчик (регион QSPI → иначе baked default, §11); порт боевых макетов
НКУ (style_1/style_2) на новую схему; расширение host-утилиты (компиляция layout-исходника → TLV).
**Тесты (host).** Golden-render солвера на 480×272 и 1024×600; валидация схемы/версии; выбор
region/baked.
**Критерий выхода.** Богатый макет рисуется из данных; injected-layout валидируется на host без
сборки бинаря; клиентский кастом = новая таблица данных.
---
## Фаза 6 — Аудио ⬜
**Цель.** Озвучка и музыка через MQS, отделённая политика «событие → звук».
**Объём.** `services/audio_engine` (WAV-парсер, плейлист/приоритеты, воспроизведение над
`bsp_mqs` + eDMA); `audio_policy` (доменное событие/`indication_task` → команды звука: озвучка
этажа, гонг, тревоги, музыка); звук в TLV-бандле.
**Тесты (host).** WAV-парсер; логика плейлиста/приоритетов/вытеснения; маппинг `audio_policy`
(событие→очередь) на golden-сценариях.
**Критерий выхода.** Полная звуковая индикация НКУ (этажи, гонги, тревоги, музыка) с приоритетами.
---
## Фаза 7 — Обновление в поле ⬜
**Цель.** Обновление ассетов/layout с microSD; замер и решение по provisioning-каналу.
**Объём.** App-side updater (SD→QSPI-регион, потоковая запись, заголовок-валидатор последним,
§12); интеграция в service_tui; **замер скорости** заливки через CDC/SDP → решение SD-only или
CDC/SDP (§12).
**Тесты.** HIL: обновление региона с SD, устойчивость к торн-записи (→ fallback). Замер скорости.
**Критерий выхода.** Ассеты/layout обновляются с SD; провижининг-путь выбран по факту замера.
---
## Фаза 8 — Мультипротокол ⬜
**Цель.** Остальные протоколы + клиентская специфика.
**Объём.** Чистые декодеры УИМ, НКУ-SD7, УЭЛ, УКЛ (+ UART-транспорт-адаптеры) в реестр; их
per-protocol дескрипторы настроек; клиентские таблицы приоритетов и макеты.
**Тесты (host).** Golden-векторы по каждому протоколу; HIL по CAN и UART транспортам.
**Критерий выхода.** Любой протокол выбирается в меню и работает без правок остальных слоёв;
добавление нового протокола = декодер + запись в реестр.
---
## Фаза 9 — Профиль `app-tft4`
**Цель.** Вторая сборка под 40-pin плату TFT4.
**Объём.** Board-файл пин-мукса LCDIF (40pin, без ориентации); Video PLL клок (закрыть TODO в
`bsp_display`); buildPreset `app-tft4`; проверка размеров буферов/RAM; макеты под 480×272.
**Тесты.** HIL на плате TFT4.
**Критерий выхода.** `app-tft4` собирается и работает на плате TFT4; `app-big` — на TFT7/8/10 с
рантайм-выбором панели.
---
## Открытые вопросы (уточняются по мере приближения фаз)
- Точная скорость provisioning-заливки через CDC/SDP → SD-only? (Фаза 7)
- Формат исходника layout для host-утилиты (JSON/TOML) и позже GUI поверх компилятора. (Фаза 5)
- Renderable-множество символов на протокол vs общий шрифт; политика по-символьного fallback. (Фаза 2)
- Звук в бандле: WAV как есть или препарсенный PCM. (Фаза 6)