# tft-app — настройки: состав, ярусы, хранение Документ описывает **реализованный** модуль `settings_store` — состав `settings_t`, ярусы A/B/C и их взаимосвязь, гибридную модель хранения (ядро на QSPI + клиентский TLV) и карту QSPI. Проектная основа — [ARCH.md §8](../../firmware/tft_app/ARCH.md) (настройки) и [§10](../../firmware/tft_app/ARCH.md) (карта QSPI); реализация — [settings_store.h](../../firmware/tft_app/src/services/settings_store/include/services/settings_store.h), [settings_codec.c](../../firmware/tft_app/src/services/settings_store/src/settings_codec.c), [partition.h](../../firmware/tft_app/src/services/partition/include/services/partition.h). Не дублирует: механику дескрипторов протоколов ([ADDING_PROTOCOL.md](ADDING_PROTOCOL.md)) и связь меню↔offset ([MENU.md](MENU.md) §4) — здесь только то, что уникально для самого модуля настроек: что хранится, как устроены ярусы, как это лежит на флеше. Практическое «как добавить свою настройку, шаг за шагом» — [ADDING_SETTING.md](ADDING_SETTING.md). --- ## 1. Три источника — settings сюда НЕ смешивает чужое ARCH §8 разделяет три независимых источника данных приложения: 1. **`sul_result_t`** — только то, что прислала станция (см. [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md)). 2. **`settings_t`** (этот документ) — конфиг устройства/пользователя, живёт на QSPI. 3. **Локальные входы** — opto (диспетчерский вызов/ответ), кнопки/меню — свои `volatile`-переменные в `app_tasks.h` (см. [TASKS.md](TASKS.md) §5), в `settings_t` не попадают вообще (это не конфигурация, а рантайм-состояние оборудования). `settings_t` — ЕДИНСТВЕННЫЙ персистентный источник из трёх; два остальных живут только в RAM. --- ## 2. Состав `settings_t` ```c typedef struct { settings_device_t device; /* провиженинг, не в пользовательском меню */ settings_user_t user; /* ярусы A + B */ uint8_t _reserved[8]; /* фикс размера — offset'ы полей стабильны */ } settings_t; ``` | Поле | Тип/размер | Ярус | Диапазон/дефолт | Редактируется в меню сейчас? | | --- | --- | --- | --- | --- | | `device.panel_type` | `uint8_t` | provisioning | `2` (TFT8), Фаза 9 | нет — provisioning-параметр (ARCH §9), не пользовательский | | `device.protocol_id` | `uint8_t` | provisioning/B | `0` (NKU_CAN) | да — пункт «Протокол» (`T_PROTO`) | | `device.log_enabled` | `uint8_t` | provisioning | `1` (вкл.) | да — пункт «Логи» (`T_LOG`, Фаза 3.6) | | `user.max_load_kg` | `uint16_t` | A | `0` = скрыто | **нет** — хранится и сериализуется, редактор — Фаза 5/6 | | `user.max_cap_persons` | `uint8_t` | A | `0` = скрыто | **нет** — Фаза 5/6 | | `user.sound_volume_idx` | `uint8_t` | A | `2` из 0..4 | **нет** — эффект (звук) только с Фазы 6 | | `user.music_volume_idx` | `uint8_t` | A | `1` из 0..4 | **нет** — Фаза 6 | | `user.year_production` | `uint8_t` | A | `0` = скрыто, иначе `2000+N` | **нет** — Фаза 5/6 | | `user.serial[10]` | ASCII+`\0` | A | `""` | **нет** — Фаза 5/6 | | `user.proto_slice[8]` | `uint8_t[8]` | B | зависит от протокола (§4) | да — единственный параметр активного протокола (`T_PROTO_PARAM`) | | `_reserved[8]` | padding | — | `0xFF` при сохранении | не поле, задел | Ярус A хранится/сериализуется/проходит CRC уже сейчас (см. `K_DEFAULTS` в `settings_codec.c`) — это НЕ означает наличие UI: полей ещё нет ни в одном пункте `menu_tree.c` (проверяемо — grep по `max_load_kg`/`serial`/… в дереве не находит ничего). Раздел 9 держит это явно, чтобы не решить по коду наоборот. --- ## 3. Ярусы A/B/C — назначение и связь ```mermaid flowchart TB subgraph ST["settings_t (QSPI 0x450000, ядро)"] A["Ярус A — железобетонные
вес/вместимость/громкости/серийник/год
одинаковы для ЛЮБОГО клиента"] B["Ярус B — протокольные
proto_slice[8], трактует АКТИВНЫЙ протокол
через sul_settings_desc_t"] DEV["device — provisioning
панель/протокол/лог-тумблер"] end C["Ярус C — клиентский UX-зоопарк
лого, сдвиги/маска этажей, метки
TLV в layout-регионе 0x440000
⚠ ЕЩЁ НЕ РЕАЛИЗОВАНО (Фаза 4/5)"] ST -.разные регионы, разный формат.-> C ``` - **A — железобетонные.** Одинаковый смысл для любого клиента/протокола: вес, вместимость, громкости, серийник, год выпуска. Не зависят от того, какая станция подключена. - **B — протокольные.** `proto_slice[8]` — ОБЩИЙ буфер под параметры активного протокола; какой байт что значит, решает **дескриптор** протокола (`sul_settings_desc_t`, ARCH §8) — см. §4 ниже. Слайс один на всех — при смене протокола он **переинтерпретируется**, не расширяется (готча со staleness — тоже в §4). - **device (provisioning).** Не входит ни в A, ни в B по смыслу ARCH §8, но физически хранится рядом (структура `settings_device_t`): панель — provisioning (Фаза 9, пишет `service_tui`, не пользователь), протокол и тумблер логов — фактически пользовательские пункты меню уже сейчас, несмотря на «device» в имени поля. - **C — клиентский UX-зоопарк.** Логотип, сдвиги/маска номеров этажей, произвольные текстовые метки — то, что каждый клиент хочет по-своему. **НЕ входит в `settings_t` вообще** — ни структурно, ни по QSPI-региону (§6). Живёт как TLV в layout-регионе (`0x440000`), пишется `service_tui` (Model 2 «injected», ARCH §11), схема версионируется отдельно от ядра. На сегодня это **только дизайн** (ARCH §11) — ни кода, ни формата TLV ещё нет (Фазы 4–5). **Почему C не влезает в ядро, а не потому что «забыли»:** ядро (A+B) — маленькое, с фиксированным `offsetof`, редактируется **прошивкой** через меню, должно быть всегда валидно (magic/CRC + дефолт на любую порчу, §7). Зоопарк C — открытый по составу (у каждого клиента свой набор виджетов), редактируется **внешним инструментом** без пересборки прошивки, требует версионируемой схемы — принципиально другой формат и жизненный цикл, поэтому и другой регион (§6). --- ## 4. `proto_slice` и дескрипторы протоколов (кратко — детали в ADDING_PROTOCOL.md) Активный протокол описывает свои параметры дескриптором ([sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h)): ```c typedef struct { const char *p_label; sul_settings_type_t type; /* BYTE / SELECT / BOOL */ uint8_t slice_offset; /* смещение ВНУТРИ proto_slice[] */ uint8_t min, max; const char *const *p_options; /* только для SELECT/BOOL */ } sul_settings_entry_t; ``` Два зарегистрированных сейчас протокола ([sul_registry.c](../../firmware/tft_app/src/domain/sul/src/sul_registry.c)): | Протокол | `slice_offset` | Тип | Диапазон | Смысл | | --- | --- | --- | --- | --- | | НКУ-CAN | `proto_slice[0]` | BYTE | 0..15 | адрес станции | | Демо | `proto_slice[0]` | SELECT | 0..2 | скорость скриптованного маршрута | Домен не включает `settings_store.h` (ARCH §4, домен не знает презентацию/хранение) — `slice_offset` считается ВНУТРИ `proto_slice[]`, а не внутри `settings_t`. Перевод в реальный `offsetof(settings_t, user.proto_slice) + slice_offset` — единственная привилегия `menu/` (`menu_tree_refresh_protocol_section()`, [MENU.md](MENU.md) §4) — только этот слой включает и `domain/sul.h`, и `settings_store.h` одновременно. **Готча (реальная, найдена и закрыта в Фазе 3.3, не гипотетическая).** Один и тот же байт `proto_slice[0]` при переключении протокола в меню означает РАЗНОЕ (адрес 0..15 → скорость 0..2). Без клампа «протухшее» значение (напр. адрес 15) читалось бы как `options[15]` за границей 2-элементного массива меток демо — мусорное чтение в рендере. `menu_tree_refresh_protocol_section()` клампит значение под НОВЫЙ `max` в момент переключения (фикс в корне, не на каждом сайте чтения). Практическое следствие: **у протокола с несколькими параметрами** второй/третий байт слайса будет не инициализирован осмысленно при первом переключении на него — сейчас не актуально (у обоих протоколов ровно один параметр), станет вопросом Фазы 8 (см. `ADDING_PROTOCOL.md` §3). Как зарегистрировать параметр для НОВОГО протокола — пошагово в [ADDING_PROTOCOL.md §3](ADDING_PROTOCOL.md). --- ## 5. Хранение — гибридная модель (ядро-struct + клиентский TLV) | | Ядро (A + B + device) | Клиентский зоопарк (C) | | --- | --- | --- | | Формат | фикс. `struct` + magic/version/CRC32 | TLV, версионируемая схема | | Регион QSPI | `0x450000`, 8 КБ (§6) | `0x440000`, 64 КБ (layout-регион, §6) | | Кто пишет | прошивка (меню, `settings_store_save()`) | `service_tui` (внешний инструмент) | | Когда меняется | в поле, через экранное меню | при заказе/переконфигурации клиента, без пересборки | | Валидация | magic+version+CRC32, авто-дефолт при любой невалидности | схема+версия+CRC, на host (golden-render) | | Реализовано | ✅ да (этот документ) | ⬜ нет — только дизайн ARCH §11, Фазы 4–5 | Ядро сознательно маленькое и жёсткое (offset-стабильность нужна декодерам/меню прямо сейчас); зоопарк сознательно гибкий и внешний (открытый список клиентских причуд, ARCH §2.2). Один `settings_t` на оба смысла означал бы либо раздувать ядро под гипотетические виджеты (нарушает YAGNI и offset-стабильность), либо тащить версионируемый TLV-парсер в код, который должен быть простым и всегда-валидным. Два формата, два региона, две скорости изменения. --- ## 6. Карта QSPI (ARCH §10) — размер-независимая | Регион | Смещение | Размер | Владелец | | --- | --- | --- | --- | | bootloader | `0x000000` | 256 КБ | bootloader | | slot A (tft_app) | `0x040000` | 2 МБ | MCUboot | | slot Б (tft_app) | `0x240000` | 2 МБ | MCUboot | | **layout** (+ ярус C, TLV) | `0x440000` | 64 КБ | tft_app / `service_tui` | | **settings** (ядро, этот документ) | `0x450000` | 8 КБ = 2 сектора × 4 КБ | tft_app | | **assets** | `0x452000` | остаток до конца чипа | tft_app | Все константы — [partition.h](../../firmware/tft_app/src/services/partition/include/services/partition.h), общий источник для `app` и (в перспективе) генератора `service_tui`. **Размер-независимость.** Прошивка работает на разных QSPI NOR (W25Q128/256/512 …). Все критичные регионы — по ФИКСИРОВАННЫМ смещениям (компайл-тайм, без рантайм-детекта); размер-зависим ТОЛЬКО регион ассетов — стартует с фикс-адреса и тянется до конца чипа (`длина = bsp_qspi_flash_size() − 0x452000`). Больше флеш = больше места под ассеты, карта не меняется. **Settings — 2 сектора, сейчас используется ОДИН.** `TFT_APP_QSPI_SETTINGS_OFFSET` (сектор 0) — рабочий, туда пишет `settings_store_save()`. Сектор 1 (`+0x1000`) — **зарезервирован под будущий power-safe ping-pong** (erase+write не атомарен — обрыв питания посреди `save()` сейчас МОЖЕТ испортить единственную копию; см. §9). Сектор 1 сегодня не читается и не пишется НИКЕМ — это задел по карте, не реализованная защита. --- ## 7. Формат страницы (magic/version/CRC32) Ровно один сектор QSPI (`BSP_QSPI_SECTOR_SIZE` = 4 КБ), проверяется статически (`_Static_assert(sizeof(settings_page_t) == BSP_QSPI_SECTOR_SIZE)`): ```c typedef struct { uint32_t magic; /* 0x54465453 'STFT' */ uint8_t version; /* 1 */ uint8_t _pad[3]; settings_t data; uint8_t _reserved[/* до конца сектора */]; uint32_t crc32; /* по всей странице КРОМЕ этого поля */ } settings_page_t; ``` - **CRC32** — poly `0x04C11DB7`, MSB-first, init `0xFFFFFFFF`, без финального xor (как `style_updater` в OLD_PROJECT) — [`settings_crc32()`](../../firmware/tft_app/src/services/settings_store/src/settings_codec.c). - **`_pad`/`_reserved` = `0xFF`** при сериализации — паттерн стёртого флеша, не нули (те же причины, что 0xFF-заполнение в других TLV этого репозитория). - **Чтение — всегда успешно с точки зрения вызывающего.** `settings_deserialize()` возвращает `false` на любое несовпадение magic/version/CRC — `settings_store_load()` в этом случае просто подставляет `settings_defaults()`; настройки валидны ВСЕГДА после `load()`/`init_defaults()`, ошибка не пробрасывается выше как fatal (аналог поведения `default`-layout, ARCH §11 — деградация, не отказ). - Сериализация/валидация — **чистые функции** в `settings_codec.c` (без единого обращения к QSPI), host-тестируются без железа; `settings_store.c` — тонкий flash-адаптер поверх них. --- ## 8. Жизненный цикл (bringup → меню → save) ```mermaid sequenceDiagram participant B as bringup_task participant Q as QSPI 0x450000 participant M as menu_task participant S as sul_rx_task B->>Q: settings_store_load() alt magic/version/CRC валидны Q-->>B: g_settings = данные с флеша else невалидны/ошибка чтения B->>B: settings_store_init_defaults() end B->>B: sul_registry_set_active(protocol_id)
menu_tree_refresh_protocol_section()
log_set_enabled(log_enabled) Note over M,S: рантайм — RAM-мутации через settings_store_get_mutable() M->>M: правка в меню (offset, dirty=true) M->>Q: save() ТОЛЬКО если save_requested (=dirty на выходе) S->>Q: save() — удалённый адрес НКУ-CAN (§3.5), свой путь записи ``` - **Один экземпляр в RAM** (`static settings_t g_settings` в `settings_store.c`) — `get()`/ `get_mutable()` возвращают указатель на него же, копий нет. - **Сохранение — не на каждую правку.** `menu.c` взводит `dirty` любой правкой, `save_requested` выставляется РАВНЫМ `dirty` только на выходе из корня меню — если пользователь ничего не менял, `settings_store_save()` не вызывается вообще (нет лишних erase-циклов флеша). - **Два независимых писателя `save()`** — `menu_task` (выход из меню) и `sul_rx_task` (удалённый адрес НКУ-CAN, §3.5) — защищены `g_save_mutex` (создаётся в `init_defaults()`/`load()`, оба вызываются один раз из `bringup_task` ДО создания остальных задач, гонки при создании нет по конструкции). Без мьютекса — два параллельных `erase+write` в один сектор, порча настроек (найдено при проектировании 3.5, не на стенде — см. PLAN.md). - **Реаппликация — не только после `save()`.** Активный протокол/тумблер логов переприменяются СРАЗУ на каждое действие меню (`sul_registry_set_active()`, `menu_tree_refresh_protocol_section()`, `log_set_enabled()`) — эффект виден в том же сеансе, независимо от того, дойдёт ли пользователь до сохранения. --- ## 9. Что осознанно НЕ реализовано (честно) - **Power-safe ping-pong** — сектор 1 занят под задел (§6), не используется. `save()` сегодня: erase → постраничная запись ОДНОГО сектора; обрыв питания между erase и завершением записи оставит страницу с невалидным CRC → следующий `load()` откатится на дефолты (не hardfault, но и не восстановит последнее сохранённое — пользовательские правки будут потеряны). - **Ярус C / клиентский TLV** — ноль кода, только дизайн ARCH §11. Layout-регион (`0x440000`) сегодня не читается и не пишется вообще (нет layout-движка, Фазы 4–5). - **`settings_store_reset_user_defaults()`** — функция объявлена и реализована, но НЕ вызывается ниоткуда в `app`-слое — нет пункта меню «сброс настроек». Задел под Фазу 5/6. - **Ярус A без UI** — весь список из §2 («нет» в последней колонке) хранится/сериализуется/проходит CRC уже сейчас, но нет ни одного пункта меню, который бы это редактировал — только `device.protocol_id` и `device.log_enabled` (провиженинговые по имени поля) и `user.proto_slice` реально управляемы оператором сегодня. - **`device.panel_type`** — provisioning-параметр (ARCH §9), пишется `service_tui`, а не пользовательским меню; сейчас хардкод-дефолт `BSP_DISPLAY_TFT8`, реальный provisioning-путь — Фаза 9.