# tft-app — fallback-рендер (safe-mode)
«Аварийный» режим индикации: минимум зависимостей, никаких ассетов и файловой системы.
Это же — первый экран walking-skeleton (Фаза 1) и гарантированная деградация, когда ассеты/layout
недоступны. Проектная основа — [ARCH.md §11](../../firmware/tft_app/ARCH.md); реализация —
[fallback.c](../../firmware/tft_app/src/ui/fallback/src/fallback.c).
---
## 1. Что такое fallback
`default`-layout **asset-free и FS-free**: не монтирует QSPI-FAT/SD, не трогает TLV-ридер ассетов.
Зависимости — только `bsp_display` + `gfx` (framebuffer, вкомпилированные шрифты, примитив стрелки)
+ домен. Рисует на чёрном фоне, без звука. Устройство никогда не «гаснет»: даже при полностью
битых ассетах есть чем показать этаж.
Принцип degradation, а не отказа (ARCH §3): сбой ассетов/layout/связи ведёт к безопасному
**упрощению** индикации, а не к отказу.
---
## 2. Что рисуется
Презентация решает по разрешённому режиму (`indication_task_t.mode`, см.
[MODE_PRIORITY.md](MODE_PRIORITY.md)) — **и по диспетчерскому входу** (§2.1), который
проверяется первым и безусловно перекрывает всё остальное:
```mermaid
flowchart TD
START([render mode, result, dispatcher]) --> CLR["gfx_clear() — AS прозрачен,
фон даёт чёрный PS"]
CLR --> DSP{"dispatcher != NONE?"}
DSP -->|да| DLBL["метка ВЫЗОВ/ОТВЕТ (SystemFont)"]
DLBL --> DONE([готово])
DSP -->|нет| Q{"mode == NORMAL?"}
Q -->|да| POS["этаж (FloorFontFallback), по центру"]
POS --> ARR{"direction UP/DOWN?"}
ARR -->|да| DRAWARR["примитив-стрелка"]
ARR -->|нет| DONE
DRAWARR --> DONE
Q -->|нет| LBL["метка режима (SystemFont)
ПОЖАР/ПЕРЕГРУЗ/СЕРВИС/…"]
LBL --> LAD{"погрузка и lading_secs>0?"}
LAD -->|да| CNT["обратный отсчёт (крупно)"]
LAD -->|нет| DONE
CNT --> DONE
```
- **Обычный режим** — номер этажа (`FloorFontFallback`) + стрелка вверх/вниз (примитив, без
спрайтов). Двойная стрелка (`SUL_DIR_DOUBLE`) в fallback не рисуется — это спрайтовая индикация
Фазы 4/5.
- **Спецрежим** — короткая текстовая метка системным шрифтом (`SystemFont`). Это **safe-mode**
индикация: богатая полноэкранная графика режимов (фон+спрайты) появится в Фазах 4–5. Для
временной погрузки дополнительно рисуется обратный отсчёт (`lading_secs`).
- **Диспетчерский вход** (§3.4) — та же safe-mode текстовая метка (переиспользует слот режимов:
`MODE_Y`/`SystemFont`), но проверяется ПЕРВЫМ, до `mode`. Слова временные, «ВЫЗОВ»/«ОТВЕТ».
Следующий этаж (`next`) в fallback **не** показывается — это элемент богатого layout (Фаза 5),
поэтому на `next_pending` перерисовки нет.
### 2.1 Диспетчерский вход — не данные СУЛ, отдельный вход в рендер
`opto` IN1 («вызов подан») / IN2 («вызов принят») — **локальный вход** (ARCH §8 п.3), не
`sul_result_t`: не идёт через `controller`/таблицу приоритетов режимов (эта таблица — только
для ортогональных сигналов СУЛ, см. [MODE_PRIORITY.md](MODE_PRIORITY.md)). Дизайн-диаграмма
ARCH §4 рисует его именно так — отдельным входом прямо в `ui.render(task, model, settings)`, не
через `controller`:
```mermaid
flowchart LR
OPTO["bsp_opto IN1/IN2
(app/dispatcher.c)"] -->|"g_dispatcher_indication"| RENDER["ui_fallback_render*()"]
CTRL["controller.process()
(только sul_result_t)"] -->|"indication_task_t"| RENDER
```
Приоритет — **высший из всех**, согласовано с пользователем: перекрывает обычную индикацию И
любой режим СУЛ (пожар/перегруз/…), работает даже без связи со станцией (dispatcher — независимое
оборудование, не станция). ОТВЕТ (IN2) перебивает ВЫЗОВ (IN1), если оба почему-то активны
одновременно. Референс поведения (не архитектуры) —
`OLD_PROJECT_TFT8_UKL/source/main_programm.c` (`tft_refresh_task`): там `icon_img_ptr`
перезаписывается диспетчерской проверкой ПОСЛЕДНЕЙ в каждом кадре, безусловно — тот же эффект,
что здесь достигается проверкой ПЕРВОЙ в `render()` с `return`.
---
## 3. По-символьный fallback шрифта
Второй уровень деградации — на уровне отдельного символа. Активный шрифт может не покрывать
кодпойнт, который выдал декодер (напр. `FloorFontFallback` знает только `0–9`, `-`, пробел, а
позиция пришла кириллицей). `gfx_draw_string()` подставляет `-` вместо отсутствующего глифа —
по-символьно, не обрывая всю строку на первом неизвестном символе. Если даже `-` нет в шрифте —
символ пропускается (нулевая ширина).
```mermaid
flowchart LR
CH["символ строки"] --> HAS{"есть глиф
в активном шрифте?"}
HAS -->|да| DRAW["нарисовать глиф"]
HAS -->|нет| SUB{"есть глиф '-'?"}
SUB -->|да| DASH["нарисовать '-'"]
SUB -->|нет| SKIP["пропустить (ширина 0)"]
```
Механизм есть с Фазы 1. Выделенная чистая функция проверки покрытия шрифта + её host-тест —
остаток Фазы 2 (см. [PLAN.md](../../firmware/tft_app/PLAN.md)).
---
## 4. Когда перерисовываем
`ui_fallback_render()` перерисовывает кадр целиком (без dirty-rect — `gfx` минимален) при
изменении того, что fallback реально показывает: **позиция, стрелка или режим**
(`pos_pending || direction_pending || mode_pending`). Иначе — no-op. И этот путь, и
безусловный старт (ниже) принимают диспетчерский вход (§2.1) отдельным параметром — сам он
НЕ входит в `pos_pending`/`direction_pending`/`mode_pending` (это diff `sul_result_t`, opto —
локальный вход, другая природа изменения).
Первую отрисовку презентация делает **безусловно** один раз при старте
(`ui_fallback_render_initial()`): кэш контроллера засеян дефолтом, и если первый реальный кадр
совпадёт с дефолтом, diff придёт «ничего не изменилось» — без безусловного старта экран остался бы
пустым. Тот же безусловный вызов переиспользуется как ТРЕТЬЯ причина полной перерисовки (кроме
старта и закрытия меню) — смена диспетчерского входа САМА ПО СЕБЕ, без нового кадра СУЛ в
очереди: `render_task` (см. [TASKS.md §3](TASKS.md)) сравнивает текущее
`g_dispatcher_indication` с прошлым и, если отличается, зовёт `ui_fallback_render_initial()`
из последнего известного `sul_result_t` + нового диспетчерского значения.
---
## 5. Как кадр попадает на экран (Фаза 3.2.4 — фундамент рендера)
Ограничение одного framebuffer'а Фазы 1 **снято**. `render()` рисует off-screen в
альфа-поверхность **AS** (ARGB8888); показ — отдельным вызовом `gfx_present()` у владельца
дисплея ([task_render.c](../../firmware/tft_app/src/app/task_render.c)): PXP компонует AS над
чёрным фоном PS и пишет **RGB565** в задний framebuffer, затем tear-free свап синхронно с
ELCDIF (`FRAME_DONE`). Гибрид bpp: рисование и AA — в полных 8 битах (AS), 565 — только на
самом выходе (сплошные цвета/текст квантуются незаметно; проверено на панели).
Индикация всегда рендерится **полным кадром** (в отличие от меню, где после открытия
перекомпоновывается только окно — см. [MENU.md](MENU.md) §5). Подробности компоновщика и
разбивка по задачам — [TASKS.md](TASKS.md).