diff --git a/docs/tft_app/DOMAIN_DATAFLOW.md b/docs/tft_app/DOMAIN_DATAFLOW.md new file mode 100644 index 0000000..b8b056a --- /dev/null +++ b/docs/tft_app/DOMAIN_DATAFLOW.md @@ -0,0 +1,185 @@ +# tft-app — путь данных домена (транспорт → экран) + +Документ описывает **реализованный** доменный слой (Фазы 1–2): как кадр от СУЛ проходит +от шины до презентации, какие модули за что отвечают и каков контракт между ними. Проектное +обоснование слоёв — в [ARCH.md](../../firmware/tft_app/ARCH.md); статус фаз — +в [PLAN.md](../../firmware/tft_app/PLAN.md). Здесь — «как это работает в коде сейчас». + +Сейчас реализован один протокол — **НКУ-CAN**. Архитектура рассчитана на много протоколов +(УЭЛ/УКЛ/SD7/УИМ — Фаза 8): добавление протокола = новый декодер + запись в реестр, без правок +остальных слоёв. + +--- + +## 1. Обзор слоёв + +Каждый слой — отдельная статическая библиотека CMake. **Домен — чистый C без единого +HAL-вызова** (host-тестируется), железо изолировано в `bsp/*` и тонком транспорт-адаптере. + +```mermaid +flowchart TB + subgraph L0["L0 · железо (bsp, не трогаем)"] + CAN["bsp_can
приём CAN-кадров"] + end + subgraph L1["L1 · транспорт (HW-адаптер)"] + TR["sul/transport/can
bsp_can_frame_t → sul_frame_t"] + end + subgraph L2["L2 · домен (чистый C, host-тесты)"] + REG["sul (реестр)
sul_registry_active()"] + DEC["sul/nku_can
чистый декодер PACKET1..5"] + MODEL["elevator_model
sul_result_t, sul_mode_t"] + CTRL["controller
diff + свёртка режима"] + PRIO["mode_priority
таблица приоритетов"] + end + subgraph L3["L3 · презентация"] + UI["ui/fallback
этаж/стрелка/метка режима"] + AUD["audio_policy
(Фаза 6)"] + end + + CAN --> TR --> REG --> DEC + DEC -->|"sul_result_t"| CTRL + MODEL -.->|"тип"| DEC + MODEL -.->|"тип"| CTRL + PRIO -.->|"свёртка"| CTRL + CTRL -->|"indication_task_t"| UI + CTRL -->|"indication_task_t"| AUD +``` + +Ключевой инвариант: **декодер не знает про шрифт/экран, презентация не знает про биты +протокола**. Единственная валюта между ними — канонический `sul_result_t` +([elevator_model.h](../../firmware/tft_app/src/domain/elevator_model/include/domain/elevator_model.h)). + +--- + +## 2. Путь одного кадра + +`sul_rx`-задача приложения опрашивает CAN, прогоняет кадр через активный декодер и передаёт +результат контроллеру; `render`-задача применяет diff. Очередь между ними — глубины 1 +(`xQueueOverwrite`: важно только последнее состояние, не история). + +```mermaid +sequenceDiagram + participant CAN as bsp_can + participant TR as transport/can + participant REG as sul_registry + participant DEC as nku_can_decode() + participant CTX as nku_can_ctx_t + participant CTRL as controller_process() + participant UI as ui_fallback_render() + + CAN->>TR: bsp_can_frame_t (id, data, dlc) + TR->>REG: sul_frame_t + REG->>DEC: active().decode(ctx, frame, out) + DEC->>CTX: обновить поле(я) пришедшего пакета + CTX-->>DEC: полная накопленная state + DEC-->>CTRL: sul_result_t (при SUL_STATUS_OK) + CTRL->>CTRL: diff с кэшем + sul_resolve_mode() + CTRL-->>UI: indication_task_t (pending-флаги + mode) + UI->>UI: перерисовать изменившееся +``` + +--- + +## 3. Контракт декодера + +**Кадр транспортного уровня** — нейтрален к шине (CAN/UART/…); транспорт заполняет, декодер +только читает ([sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h)): + +```c +typedef struct { uint32_t id; uint8_t bus; const uint8_t *p_data; uint16_t len; } sul_frame_t; +typedef sul_status_t (*sul_decode_fn)(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out); +``` + +**Три исхода** `decode()` (`sul_status_t`): + +| Статус | Когда | Что с `*p_out` | +| --- | --- | --- | +| `SUL_STATUS_OK` | кадр распознан | заполнен полной накопленной `state` | +| `SUL_STATUS_IGNORED` | ID не этого протокола | **не тронут** | +| `SUL_STATUS_ERR` | ID совпал, но кадр малформирован (неверный DLC / код символа вне таблицы) | не тронут | + +**Декодер stateful через ctx, но остаётся чистой функцией.** Разные пакеты несут разные поля +в разных кадрах — `decode()` обновляет только пришедшее и отдаёт наружу **полную** копию +накопленного `state`, а не дельту. Состояние держит caller в `nku_can_ctx_t` и передаёт +указатель на каждый вызов — декодер не владеет памятью/жизненным циклом. + +```mermaid +flowchart LR + P1["PACKET1
направление, режим"] --> ST["nku_can_ctx_t.state
(полный sul_result_t)"] + P3["PACKET3
позиция, гонг, погрузка"] --> ST + P4["PACKET4
перегруз, сейсмо"] --> ST + ST -->|"копия целиком"| OUT["*p_out"] +``` + +--- + +## 4. НКУ-CAN: пакет → поля модели + +Порт боевого декодера `OLD_PROJECT/source/main_programm.c` (`msg_receiver_task`) в чистую +функцию. Адрес станции = 0 (базовые ID без сдвига группы; параметризация из настроек — Фаза 3). +Реализация — [nku_can.c](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c). + +| Пакет | ID | Байты/маски → поля `sul_result_t` | +| --- | --- | --- | +| **PACKET1** | `0x506` | `d6[1:0]` → `direction` · `d6[3:2]` → `movement` · `d6[7:4]` код режима → `fire_alarm`/`maintenance`/`fireman`/погрузка-инстр. · `d3[5:0]` → уровень остановки (внутр., гейт PACKET5) | +| **PACKET2** | `0x408` | `d7&0x40` → перегруз (источник 1) | +| **PACKET3** | `0x508` | `d5/d6 &0x3F` → `pos`, `floor_num` · `d3&0x40==0` → `arrival` (гонг) · `d2&0x3F` сек + `d3&0x0F` мин → `lading_secs` | +| **PACKET4** | `0x50B` | `d5&0x40` → перегруз (источник 2) · `d0&0x80` → `seismic` | +| **PACKET5** | `0x606` | `d3/d4 &0x3F` → `next` (только пока едет ↑/↓ и назначение ≠ уровень; иначе `next=""`) | + +**Мультиисточниковые поля.** Некоторые выходные поля кормятся несколькими пакетами. Они держатся +в `ctx` раздельными латчами и на выход идут как **OR** — иначе пакет-без-сигнала сбросил бы флаг, +выставленный другим пакетом (боевой баг-класс): + +- `overload = overload_p2 || overload_p4` (PACKET2 и PACKET4 независимы); +- `lading = lading_instr || (lading_secs > 0)` (инструментальная из PACKET1, временная из PACKET3). + +Режимными флагами, которыми владеет **один** пакет (fire/maintenance/fireman — PACKET1; +seismic — PACKET4), пакет-владелец распоряжается напрямую: сбрасывает в начале и выставляет по +условию, гася устаревший режим. + +Удалённая установка адреса (кадры `0x4X1`/`0x5XB`) — **отложена в Фазу 3** (нужен `settings_store` +для записи `nku_address`; чистый декодер не пишет настройки). Сейчас эти ID игнорируются. + +--- + +## 5. Контроллер: diff и таймаут + +`controller_process()` — редьюсер: сравнивает новый `sul_result_t` с кэшем, выдаёт +`indication_task_t` (что перерисовать/озвучить) и обновляет кэш +([controller.c](../../firmware/tft_app/src/domain/controller/src/controller.c)). + +| Поле `indication_task_t` | Условие | Потребитель | +| --- | --- | --- | +| `pos_pending` | изменилась строка позиции | презентация | +| `next_pending` | изменился следующий этаж | презентация (Фаза 5) | +| `direction_pending` | изменилось направление | презентация | +| `mode_pending` | сменился **разрешённый** режим (см. [MODE_PRIORITY.md](MODE_PRIORITY.md)) | презентация | +| `arrival_pending` | фронт `false→true` гонга | аудио (Фаза 6) | +| `movement_pending` | фронт `false→true` начала движения | аудио (Фаза 6) | +| `mode` | разрешённый режим нового результата | презентация читает при `mode_pending` | + +**Таймаут связи — не отдельный API.** При пропадании кадров дольше таймаута `sul_rx`-задача +просто вызывает `controller_process()` с `sul_default_state()` — тем же путём, что и обычный кадр +(`pos="--"`, режимы сброшены). Diff корректно пометит изменения, презентация покажет «--». + +```mermaid +flowchart TD + POLL["sul_rx: опрос CAN"] --> GOT{"кадр за таймаут?"} + GOT -->|да| DEC["decode → sul_result_t"] + GOT -->|нет| DEF["sul_default_state()"] + DEC --> CTRL["controller_process()"] + DEF --> CTRL + CTRL --> Q["очередь глубины 1"] + Q --> RENDER["render-задача"] +``` + +--- + +## 6. Дальше — презентация + +Как `indication_task_t` превращается в пиксели и звук: + +- Свёртка сигналов в один экранный режим — [MODE_PRIORITY.md](MODE_PRIORITY.md). +- Безопасная (asset-free) отрисовка этажа/стрелки/режима — [FALLBACK.md](FALLBACK.md). +- Богатый layout со спрайтами и звук — Фазы 4–6 ([PLAN.md](../../firmware/tft_app/PLAN.md)). diff --git a/docs/tft_app/FALLBACK.md b/docs/tft_app/FALLBACK.md new file mode 100644 index 0000000..6918a04 --- /dev/null +++ b/docs/tft_app/FALLBACK.md @@ -0,0 +1,98 @@ +# 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)): + +```mermaid +flowchart TD + START([render mode, result]) --> CLR["gfx_clear(BLACK)"] + CLR --> 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`). + +Следующий этаж (`next`) в fallback **не** показывается — это элемент богатого layout (Фаза 5), +поэтому на `next_pending` перерисовки нет. + +--- + +## 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. + +Первую отрисовку презентация делает **безусловно** один раз при старте +(`ui_fallback_render_initial()`): кэш контроллера засеян дефолтом, и если первый реальный кадр +совпадёт с дефолтом, diff придёт «ничего не изменилось» — без безусловного старта экран остался бы +пустым. + +--- + +## 5. Известное ограничение — один framebuffer + +Сейчас один буфер, без double buffering: `render()` пишет напрямую в буфер, который ELCDIF в этот +момент сканирует по DMA. Если перерисовка не укладывается в кадр развёртки, виден сам процесс +закраски (глиф «набирается» за несколько кадров) — **не** tearing и **не** порча пикселей (каждая +запись валидна), просто заметен процесс. + +Фикс — double buffering (второй буфер + swap по `FRAME_DONE`), запланирован в **Фазе 4** вместе с +PXP-компоновщиком. Подробнее — [PLAN.md, Фаза 4](../../firmware/tft_app/PLAN.md). diff --git a/docs/tft_app/MODE_PRIORITY.md b/docs/tft_app/MODE_PRIORITY.md new file mode 100644 index 0000000..10081e4 --- /dev/null +++ b/docs/tft_app/MODE_PRIORITY.md @@ -0,0 +1,103 @@ +# tft-app — приоритеты режимов + +Как множество одновременных сигналов от СУЛ сворачивается в **один** экранный режим. +Проектная основа — [ARCH.md §7](../../firmware/tft_app/ARCH.md); реализация — +[mode_priority.c](../../firmware/tft_app/src/domain/controller/src/mode_priority.c). + +--- + +## 1. Задача + +Сигналы в `sul_result_t` **ортогональны** — могут быть активны одновременно (перегруз во время +сервиса, пожар при погрузке и т.п.). Но экран показывает **один** режим. Нужно правило: какой +режим побеждает, когда активны несколько. + +```mermaid +flowchart LR + subgraph SIG["сигналы sul_result_t (булевы, независимы)"] + F["fireman"] + FA["fire_alarm"] + OV["overload"] + SE["seismic"] + MA["maintenance"] + LA["lading"] + end + SIG --> R["sul_resolve_mode()"] + R --> M["sul_mode_t
(ровно один режим)"] +``` + +--- + +## 2. Таблица — это данные, а не код + +Приоритет задан **порядком строк** таблицы-массива, а не значениями enum. Резолвер таблицу не +знает «в лицо» — читает булев сигнал по смещению поля (`offsetof`), что делает каждую строку +голыми данными: + +```c +typedef struct { size_t flag_offset; sul_mode_t mode; } mode_rule_t; + +static const mode_rule_t k_mode_priority[] = { + {offsetof(sul_result_t, fireman), SUL_MODE_FIREMAN}, // высший + {offsetof(sul_result_t, fire_alarm), SUL_MODE_FIRE_ALARM}, + {offsetof(sul_result_t, overload), SUL_MODE_OVERLOAD}, + {offsetof(sul_result_t, seismic), SUL_MODE_SEISMIC}, + {offsetof(sul_result_t, maintenance), SUL_MODE_MAINTENANCE}, + {offsetof(sul_result_t, lading), SUL_MODE_LADING}, // низший спецрежим +}; +``` + +Согласованный порядок: **пожарный › пожар › перегруз › сейсмо › сервис › погрузка**. Если ни одна +строка не сработала — `SUL_MODE_NORMAL` (обычная индикация этажа/стрелки). + +> ⚠️ Числовые значения `sul_mode_t` — **просто идентификаторы**, они не задают приоритет. Приоритет +> живёт только в порядке строк `k_mode_priority[]`. + +--- + +## 3. Резолвер + +Проход таблицы сверху вниз; первая строка с активным сигналом выигрывает. + +```mermaid +flowchart TD + START([sul_resolve_mode]) --> I["i = 0"] + I --> CHK{"строка i:
сигнал по offset активен?"} + CHK -->|да| RET["вернуть mode строки i"] + CHK -->|нет| NEXT["i++"] + NEXT --> MORE{"строки ещё есть?"} + MORE -->|да| CHK + MORE -->|нет| NORM["вернуть SUL_MODE_NORMAL"] +``` + +Резолвер вызывается из `controller_process()` дважды за кадр (для прошлого и нового состояния) — +их сравнение даёт `mode_pending`. Смена **сырого** сигнала, не меняющая разрешённый режим (напр. +добавился перегруз при уже активном пожаре), `mode_pending` не поднимает. + +--- + +## 4. Как менять + +Всё — правки одного массива, резолвер не трогается: + +- **Изменить приоритет** — переставить строки. +- **Добавить режим** — дописать строку: `offsetof` нового булева сигнала + его `sul_mode_t` + (сам сигнал добавляется в `sul_result_t` и заполняется декодером). +- **Убрать режим** — удалить строку (сигнал в модели может остаться, просто не влияет на экран). + +--- + +## 5. Развязка от протокола и клиентская кастомизация + +Таблица оперирует **каноническими** сигналами `sul_result_t`, а не битами НКУ-CAN. Обзор боевых +декодеров (УИМ/УКЛ/УЭЛ/SD7) подтвердил: набор сигналов один на все протоколы — различаются лишь +битовые кодировки (живут в каждом декодере). Поэтому одна таблица работает для любого протокола. + +Приоритеты «потенциально клиентские» (§7): клиентская кастомизация = подмена массива +`k_mode_priority[]` (в перспективе — загружаемая таблица рядом с layout/конфигом), **без правок +кода** резолвера или декодеров. + +--- + +Куда встраивается результат: `mode` едет в `indication_task_t` и читается презентацией при +`mode_pending` — см. [DOMAIN_DATAFLOW.md §5](DOMAIN_DATAFLOW.md) и [FALLBACK.md](FALLBACK.md). diff --git a/docs/tft_app/README.md b/docs/tft_app/README.md new file mode 100644 index 0000000..0beab30 --- /dev/null +++ b/docs/tft_app/README.md @@ -0,0 +1,23 @@ +# tft-app — документация домена + +Dev-документация по **реализованному** доменному слою `firmware/tft_app` (Фазы 1–2): как данные +СУЛ проходят от протокола до экрана. Технические доки «как это работает в коде», в дополнение к +проектным ARCH/PLAN. + +| Документ | О чём | +| --- | --- | +| [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md) | Путь данных: транспорт → декодер → контроллер → презентация. Слои, контракт `decode()`, карта пакетов НКУ-CAN → поля модели, таймаут→default. | +| [MODE_PRIORITY.md](MODE_PRIORITY.md) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. | +| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, по-символьный fallback шрифта, триггеры перерисовки, ограничение single-buffer. | + +## Связанные документы + +- [ARCH.md](../../firmware/tft_app/ARCH.md) — архитектура и проектное обоснование (источник истины по дизайну). +- [PLAN.md](../../firmware/tft_app/PLAN.md) — статус фаз разработки (источник истины по статусу). +- [DEV_ARCH.md](../DEV_ARCH.md) — устройство репозитория и сборки. + +## Границы + +Описан домен и fallback-презентация. Ещё не реализовано (см. PLAN.md): настройки/меню (Фаза 3), +ассеты и layout-движок со спрайтами (Фазы 4–5), аудио (Фаза 6), мультипротокол (Фаза 8). Удалённая +установка адреса НКУ-CAN отложена в Фазу 3. diff --git a/firmware/tft_app/ARCH.md b/firmware/tft_app/ARCH.md index 437d89e..6a39370 100644 --- a/firmware/tft_app/ARCH.md +++ b/firmware/tft_app/ARCH.md @@ -266,6 +266,21 @@ layout- и assets-регионы — **вне** flash-area загрузчика - **HIL:** gfx/PXP/ELCDIF, audio/MQS, SD/QSPI, реальные транспорты CAN/UART (по образцу `tests/target/*` и `06_test_firmware_*`). +### 14. Errata-вотчлист (IMXRT1050 Chip Errata Rev. 2.1) + +> Риски кремния, привязанные к фазам. Многие пункты — **только для ревизии A0** +> (исправлены в A1) → на A1-кремнии неактуальны. **Открытый вопрос: уточнить ревизию +> кремния стендовых плат (A0/A1)** — определяет применимость половины списка. Источник — +> `DOCS/MANUALS/ERRATA.pdf` (IMXRT1050CE, Rev. 2.1, 08/2019). + +| Errata | Суть | Фаза | Ревизия | +| --- | --- | --- | --- | +| **ERR011138** | LCDIF: последовательное программирование LUT может сбоить, если две записи идут близко. | 4/9 — актуально **только если** используем LUT ELCDIF для Index8BPP-фреймбуфера (экономия RAM ~4× vs ARGB8888). | A0, исправлено в A1 | +| **ERR011207** | FlexSPI: при `FLEXSPI_AHBCR[PREFETCHEN]=1` в редком случае возвращаются неверные данные. | 4 — XIP-mmap блит ассетов из QSPI. Проверить конфиг prefetch в `bsp` FlexSPI. | A0, исправлено в A1 | +| **ERR011377** | FlexSPI: бит статуса блокировки DLL неточен из-за тайминга. | 4/7 — запись/чтение QSPI-ассетов. | Без фикса | +| **ERR011572** | Cortex-M7: write-through чтения/записи могут вернуть неверные данные. | Все — наш фреймбуфер `AT_NONCACHEABLE` обходит проблему; но любой write-through-регион под подозрением. | Без фикса | +| **ERR006032 / ERR009527 / ERR009595** | FlexCAN: баги TX-пути (кадр с неверным ID при abort/deactivate в bus-idle; сбой abort; порча кадра при freeze/low-power в bus-off). | 2 — **если** «удалённая установка адреса» требует CAN **TX**; иначе 8. Пока только RX — низкий риск. | Без фикса | + --- ## Приложение. Заимствования из проектов diff --git a/firmware/tft_app/PLAN.md b/firmware/tft_app/PLAN.md index 2a46064..e09894c 100644 --- a/firmware/tft_app/PLAN.md +++ b/firmware/tft_app/PLAN.md @@ -17,7 +17,7 @@ |---|---|---| | 0 | Каркас проекта и сборка | ✅ | | 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ✅ | -| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | ⬜ | +| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | 🟨 | | 3 | Настройки и меню, локальные входы (opto) | ⬜ | | 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ | | 5 | Layout-движок: схема, солвер, темы, injected-регион | ⬜ | @@ -178,7 +178,42 @@ marks_both_pending`, аппаратно на реальном обрыве св --- -## Фаза 2 — Домен: полный контракт ⬜ +## Фаза 2 — Домен: полный контракт 🟨 + +> **Статус: код-комплит, host-покрытие зелёное; ожидает подтверждения на стенде.** +> Домен (модель/декодер/приоритеты) и fallback-презентация написаны, host-тесты +> **47** (34 декодер + 13 контроллер/приоритеты) зелёные, ARM Debug и Release +> собираются/линкуются чисто. По прецеденту Фазы 1 статус ✅ ставится только после +> проверки на железе (реальный НКУ-CAN трафик → режимы/следующий этаж на экране). +> +> Решения, принятые с пользователем по ходу: +> - **Приоритет режимов** (таблица `k_mode_priority[]`, чистые данные, offset-массив): +> fireman > пожар > перегруз > сейсмо > сервис > погрузка > норма. Менять приоритет = +> переставить строки; добавить режим = дописать строку; резолвер не трогается (§7). +> Таблица намеренно развязана от НКУ-CAN — обзор UIM/UKL/UEL/SD7 подтвердил, что +> набор булевых сигналов `sul_result_t` §6 покрывает все протоколы (различаются лишь +> битовые кодировки, живут в декодерах), а `cop_mode`/`display_id` — это провиженинг, +> не данные СУЛ. +> - **Удалённая установка адреса — отложена целиком в Фазу 3** (нужен `settings_store` +> для записи `nku_address`; чистый декодер не должен писать настройки). Кадры +> 0x4X1/0x5XB пока игнорируются как чужие ID. Адрес станции = 0 (хардкод, как в Фазе 1). +> - **Декодер выдаёт УРОВНИ, не события.** Гонг/двери/начало движения — булевы уровни; +> edge-детекция (→ звук) — Фаза 6 (audio_policy поверх diff контроллера). Двери в §6 +> не входят (только звук) — пересмотр в Фазе 6. +> - **Мультиисточниковые поля** (overload из PACKET2+PACKET4; lading инструментальный +> PACKET1 + временной PACKET3) — внутренние латчи в `nku_can_ctx_t`, на выход их OR: +> пакет-без-сигнала не сбрасывает флаг, выставленный другим пакетом (боевой баг-класс). +> +> ⚠️ **Остаток Фазы 2 (не блокирует HW-проверку домена):** явный host-тест валидации +> рендеримости позиции (покрытие активного шрифта + по-символьный fallback). Сам +> МЕХАНИЗМ per-char fallback уже есть в `gfx_draw_string` с Фазы 1 (неизвестный глиф → +> подстановка «-»); недостаёт выделенной чистой функции проверки покрытия и её теста — +> требует небольшого выноса из `gfx.c` (зависит от `bsp_display`). +> +> Побочная находка: `SystemFont.c` (сгенерирован lcd-image-converter) экспортирует tFont +> под именем `JBMono24` (из .xml), а `gfx.h` обещал `SystemFont` — рассинхрон был +> латентным (Фаза 1 линковала только `FloorFontFallback`), вскрылся при первом +> потребителе. Закрыт алиасом-макросом в `gfx.h`, сам сгенерированный файл не тронут. **Цель.** `sul_result_t` в полном объёме (§6 ARCH), таблица приоритетов режимов, устойчивый таймаут/потеря связи. @@ -203,6 +238,11 @@ marks_both_pending`, аппаратно на реальном обрыве св **Объём.** `services/settings_store` (магик/версия/CRC, сектор настроек §10); дескрипторы per-protocol настроек (§8); `menu/` (навигация, редактирование, привязка к дескрипторам); `bsp_button` + `bsp_opto` (диспетчерский вызов/ответ) → контроллер/презентация. +**Рантайм-тумблер логгирования:** флаг `log_enabled` в настройках + пункт меню «включить +логгирование» + рантайм-гейт в `utils/log` (`log_set_enabled()`, проверка в начале `log_write()`) +поверх компайл-тайм `LOG_LEVEL`. Инфраструктура (`utils/log`, `port/log_uart`, `log_mutex`) уже +готова — здесь только рантайм-гейт и его привязка к настройке/меню. NB: продакшн-сборка должна +идти с `LOG_LEVEL >= INFO` (не `OFF`), иначе тумблеру нечего гейтить — макросы вырезаны компилятором. **Тесты (host).** Сериализация/дефолты/CRC настроек; логика навигации меню; связывание дескрипторов. @@ -311,3 +351,7 @@ per-protocol дескрипторы настроек; клиентские та - Формат исходника layout для host-утилиты (JSON/TOML) и позже GUI поверх компилятора. (Фаза 5) - Renderable-множество символов на протокол vs общий шрифт; политика по-символьного fallback. (Фаза 2) - Звук в бандле: WAV как есть или препарсенный PCM. (Фаза 6) +- **Каталог доменных точек логгирования** (что / где / уровень: декодер, контроллер, транспорт, + смена режима, таймаут связи, …) — согласовать **совместно**, затем расставить **одним + выделенным проходом** по готовому домену. Инфраструктура готова (`utils/log` + `port/log_uart`, + компайл-тайм `LOG_LEVEL`). Рантайм-тумблер в меню — Фаза 3 (см. её объём). diff --git a/firmware/tft_app/REPORT.md b/firmware/tft_app/REPORT.md new file mode 100644 index 0000000..18dbd16 --- /dev/null +++ b/firmware/tft_app/REPORT.md @@ -0,0 +1,189 @@ +Готово. Полная повторная верификация: + +| Проверка | Результат | +| ---------------------- | ------------------------------------------------------ | +| Host-тесты | **19/19** зелёные | +| Debug сборка+подпись | чисто, `image_ok` UNSET | +| Release сборка+подпись | чисто, `image_ok` UNSET | +| Оба бинарника | 2 097 152 байт (ровно размер слота) | +| Аппаратно (ваш лог) | self-confirm пережил перезагрузку, CAN→рендер работает | + +PLAN.md обновлён: Фаза 1 → ✅, известное ограничение (single-buffer) зафиксировано с честной формулировкой механизма, явно вписано в объём Фазы 4 вместе с PXP. Также отметил, что критерий «timeout→--» проверен только логически (host-тестом), не на реальном обрыве связи — не стал завышать степень подтверждённости. + +Отчёт — [tft_app_phase_0_1_report.md](../../../private/tmp/claude-501/-Users-von-akimow-Desktop-TFT-ENV-tft-manufacture-test/93af6316-b6c7-4b2a-9063-169ca896d1ff/scratchpad/tft_app_phase_0_1_report.md). Полный текст ниже — можно скопировать целиком в новый тред: + +--- + +```markdown +# tft_app — отчёт по Фазам 0–1 + +> Канонические источники — `firmware/tft_app/ARCH.md` (архитектура) и `firmware/tft_app/PLAN.md` +> (статус фаз, живой документ, обновляется каждой фазой). Этот файл — снимок на момент завершения +> Фазы 1, для быстрого ввода в контекст новой сессии. При расхождении — верить PLAN.md/ARCH.md, не +> этому файлу. + +**Статус:** Фаза 0 ✅ и Фаза 1 ✅ — обе аппаратно подтверждены. Работа Фазы 1 **не закоммичена** +(в рабочем дереве поверх коммита `# tft_app: Phase 0`, `a9e6681`). + +--- + +## Фаза 0 — Каркас (FreeRTOS + self-confirm слота MCUboot) + +**Итог.** `firmware/tft_app` собирается как MCUboot-слот-образ (Direct-XIP), bootloader в него +прыгает, FreeRTOS стартует, образ подтверждает себя в рантайме (`boot_set_next`) и переживает +power cycle без отката — anti-brick сохранён. Подтверждено дважды на реальном железе (в т.ч. +через переустановку с microSD). + +**Путь до рабочего состояния был не прямым — три независимых бага, все найдены и закрыты:** + +1. **`bsp_qspi_init()` обязателен до любого `flash_area_*`.** Без него запись `image_ok` в трейлер + слота молча падала — `boot_set_next()` возвращал успех, но ничего не персистило. +2. **Корневая причина (самая коварная): забытый дефайн `__STARTUP_INITIALIZE_RAMFUNCTION`.** + Запись QSPI из app виснет в `qspi_write_fifo` — FlexSPI не дренирует IP TX FIFO. Причина: + startup копирует секцию `.ram_function` (код `bsp_qspi`, помеченный + `AT_QUICKACCESS_SECTION_CODE`) из flash в ITCM только под этим дефайном; `app` его не задавал + — в ITCM оставался leftover ramfunc **загрузчика** (тот исполнялся первым), поэтому чтения + «случайно работали», а запись — нет. Оба рабочих потребителя `bsp_qspi` (firmware_test, + bootloader) этот дефайн задают; `app` был первым, кто его пропустил. Диагностировано под + отладчиком (зависание конкретно в `qspi_write_fifo`), подтверждено изолированным + erase+write+readback self-test (частота мигания LED = индикатор результата) до восстановления + полной диагностики. +3. **UART, не USB CDC, для диагностики/логов.** USB CDC (target-side EHCI/PHY) под FreeRTOS не + заработал стабильно за несколько заходов — первый в репозитории FreeRTOS-потребитель target + USB device stack, класс неопределённости остался непроверенным. LPUART1/MCU-Link VCOM — самый + проверенный канал в репозитории (все `tests/target/*` HIL-образы), доступен сразу через уже + подключённый для SWD кабель, без enumeration/wait. Решение принято явно с пользователем. + +**Побочные, но важные для будущих фаз находки:** +- Любая bsp-инициализация, зависящая от тика (`bsp_delay()` внутри `bsp_usb_cdc_init()` и т.п.), + нельзя звать **до** `vTaskStartScheduler()` — в FreeRTOS-режиме `bsp_tick` это + `xTaskGetTickCount()`, который не идёт до старта планировщика → вызов зависает навсегда молча. + `main()` в итоге — минимальный bare-metal (board bring-up + запуск планировщика), вся + инициализация — из задачи. +- Первый в репозитории потребитель `BSP_TICK_FREERTOS_MODE` вскрыл 2 латентных бага: (1) общий + `bsp_tick` — bare-metal STATIC-либа → `SysTick_Handler` конфликтует с FreeRTOS-портом (решение: + компилировать `tick.c` прямо в `app`, не через `libbsp_tick.a`); (2) `configTICK_RATE_HZ` + нельзя как `((TickType_t)1000)` — `#if` в `tick.c` не разбирает cast, только `1000`. +- `.vscode/launch.json`: у `🐛 Debug: tft_app` был `preLaunchTask` на `firmware_test` + (copy-paste) — исправлено на `build:app-debug`. +- Тонкий стек задачи (изначально x2=1 КБ) маскировался под тот же класс симптома, что и другие + баги (зависание/reset-loop) — увеличен с запасом (x4-x6, 2-3 КБ) везде, где заводятся новые + задачи. + +**Осознанное отклонение от исходного плана:** `firmware/bootloader/test_stub` НЕ удалён (план +предполагал) — оставлен как независимая A/B-регрессия загрузчика, `app` пока не покрывает те же +сценарии (hang-классы, revert без self-confirm). + +--- + +## Фаза 1 — Walking skeleton (CAN → декодер → контроллер → fallback-рендер) + +**Итог.** Полный вертикальный срез: реальный CAN-кадр НКУ-CAN → чистый декодер → контроллер +(diff) → fallback-рендер (позиция + стрелка) на экране. Подтверждено на TFT8 с реальным +CAN-трафиком станции. + +### Архитектура (по слоям, каждый — своя CMake-либа, конвенция `bsp/`) + +``` +domain/elevator_model sul_result_t (pos: UTF-8 строка, direction: none/up/down/double), + sul_default_state() +domain/sul sul_frame_t, sul_driver_t, sul_status_t, реестр драйверов + (заголовок и реализация реестра — РАЗНЫЕ CMake-таргеты, + иначе цикл: декодеру нужны только типы sul.h, реестру — decode() декодера) +domain/sul/nku_can чистый декодер PACKET1 (направление) / PACKET3 (позиция) — + порт OLD_PROJECT msg_receiver_task, БЕЗ единого HAL-вызова +domain/sul/transport/can тонкий bsp_can → sul_frame_t адаптер (HW, не host-тестируется) +domain/controller process(sul_result_t) → indication_task_t (diff), кэш +services/gfx framebuffer (SDRAM non-cacheable) + RLE-декодер шрифтов + (lcd-image-converter формат) + примитив стрелки +ui/fallback рендер pos+стрелка, asset-free/FS-free (ARCH §11) +app задачи sul_rx (WDOG+CAN+decode+controller) и render, + связаны очередью глубины 1 (xQueueOverwrite — важно + только последнее состояние, не история) +``` + +**Host-тесты (все зелёные, домен полностью протестирован до касания железа):** +- `tests/host/tft_app_sul_nku` — 16 тестов, golden-векторы CAN-кадров (все direction, позиции, + легаси-квирк `left==0`≈пробел, кириллица, IGNORED/ERR на чужой ID/DLC/вне таблицы символов, + накопление состояния между PACKET1 и PACKET3). +- `tests/host/tft_app_controller` — 6 тестов (diff-логика, timeout→default тем же путём, что и + обычный кадр). +- Полный пакет: **19/19** (включая существующие тесты bootloader/bsp). + +### Ключевые находки/решения по ходу + +1. **Framebuffer в SDRAM обязан идти через `AT_NONCACHEABLE_SECTION_ALIGN`** (прямая рекомендация + NXP, порт паттерна из `OLD_PROJECT/source/display/image_cache.c`). MPU держит SDRAM как + `Normal Write-Back Cacheable` (`board_mpu_init()`, Region 8); ELCDIF читает framebuffer + напрямую по DMA — без non-cacheable региона CPU писал бы через кэш, DMA видел бы устаревшие + данные (мусор/частичные кадры на экране, выглядело бы как загадочный баг железа). Механизм уже + был параметризован в `board_mpu_init()` через линкер-символы `__NCACHE_REGION_START/SIZE` + (Region 9, унаследовано из bootloader) — переопределены в `app_slot.ld` на новый + `m_sdram_ncache` (2 МБ в начале SDRAM) вместо OCRAM (256 КБ, для framebuffer ~1.83 МБ не + влезло бы). На первой попытке framebuffer был сырым указателем на константный адрес (работало + случайно, линкер честно репортил `0 B` в секции) — исправлено на настоящее объявление + переменной через макрос. +2. **RLE-декодер шрифтов — порт проверенного в проде алгоритма** из `OLD_PROJECT/source/fonts/ + fonts.c` (UNIQUE/REPEATABLE блоки, бинарный поиск по отсортированному коду символа), не + переизобретён с нуля. Формат — то, что реально экспортирует lcd-image-converter (`tImage`/ + `tChar`/`tFont`, ARGB8888, "Color A8R8G8B8"). Пользователь сгенерировал `FloorFontFallback.c` + (Inter 215pt, символы 0-9 и "-") и `SystemFont.c` (JetBrains Mono 24pt, ASCII+кириллица) + инструментом напрямую. +3. **По-символьный fallback** (ARCH §11): символ вне таблицы шрифта → подстановка `'-'` + (согласованная политика — "-" общий заменитель во `FloorFontFallback`). +4. Сгенерированные шрифтовые файлы ожидают типы из bare-name `fonts.h` по-разному + (`SystemFont.c` — явный `#include`, `FloorFontFallback.c` — вообще без include, разные версии + конвертера) — закрыто через `-include`/`-I` в CMake; сами сгенерированные файлы не + редактировались (конвертер их перезапишет при регенерации). +5. Файлы шрифтов перенесены из `domain/` (где их положил пользователь) в `services/gfx/fonts/` — + по ARCH §4 шрифты относятся к gfx, не к домену (домен — чистый C без рендеринга). + +### ⚠️ Известное ограничение (зафиксировано, чинится в Фазе 4) + +Один framebuffer, без double buffering — `ui_fallback_render()` пишет напрямую в буфер, который +ELCDIF в этот момент сканирует по DMA. Если перерисовка (`gfx_clear` + RLE-decode глифов + +стрелка) не укладывается в один кадр развёртки, видно сам процесс закраски (глиф «набирается» по +частям за несколько кадров) — **не tearing и не порча пикселей** (каждая отдельная запись всегда +валидна), просто заметен сам процесс. Подтверждено на железе пользователем ("рвано, но без +артефактов" — совпадает с механизмом). + +**Фикс — double buffering**, НЕ требует PXP отдельно (это вопрос буферизации, не композиции): +второй framebuffer (`m_sdram_ncache` 2→4 МБ), swap через `bsp_display_set_next_buffer()` + +семафор на `FRAME_DONE` (сейчас `gfx_init()` передаёт `NULL` вместо колбэка). Явно вписано в +объём Фазы 4 (PLAN.md) вместе с PXP-компоновщиком (для многослойной композиции реальных +ассетов — фон+спрайты+текст, та же фаза, естественно смежная работа). + +### Прочее + +- Критерий выхода выполнен на TFT8: реальный CAN-трафик → корректный номер этажа и стрелка. + Таймаут→"--" логически проверен host-тестом, аппаратно на реальном обрыве связи отдельно не + гонялся (низкий риск — тот же код-путь, что у обычного кадра). +- Debug и Release оба собираются и подписываются чисто (2 097 152 байта — ровно размер слота, + `image_ok` UNSET в обоих — рантайм self-confirm, не sign-time `--confirm`). + +--- + +## Состояние репозитория на конец Фазы 1 + +- Ветка: `tft-app-dev`. +- Коммит `a9e6681` (# tft_app: Phase 0) — последний закоммиченный чекпоинт. +- Вся Фаза 1 — в рабочем дереве, не закоммичена: `firmware/tft_app/src/{domain,services/gfx, + ui/fallback}/`, `firmware/tft_app/src/app/main.c` (переписан), `tests/host/tft_app_sul_nku/`, + `tests/host/tft_app_controller/`, изменения в `CMakePresets.json`, + `cmake/linker/MIMXRT1052xxxxx_app_slot.ld`, `firmware/tft_app/CMakeLists.txt`, + `tests/host/CMakeLists.txt`, `firmware/tft_app/PLAN.md`. +- Стоит рассмотреть коммит перед длительным перерывом/переключением контекста — на усмотрение + пользователя (не коммичу без явной просьбы). + +## Дальше — Фаза 2 + +Полный контракт `sul_result_t` (next, сигналы, `lading_secs`, `floor_num`), полный декодер +НКУ-CAN (PACKET1..5, режимы, удалённая установка адреса), таблица приоритетов режимов +(данные, потенциально клиентские), валидация рендеримости позиции (renderable-множество шрифта + +по-символьный fallback — механизм уже есть в `gfx`, п.3 выше). Подробности — PLAN.md, раздел +«Фаза 2». +``` + +--- + +Одно практическое замечание, раз вы переключаетесь в новый тред: вся Фаза 1 сейчас **не закоммичена** — сидит в рабочем дереве поверх коммита Фазы 0. Хотите, чтобы я закоммитил перед переключением (стандартный `git add` + сообщение по Фазе 1), или оставляете как есть? \ No newline at end of file diff --git a/firmware/tft_app/src/domain/controller/CMakeLists.txt b/firmware/tft_app/src/domain/controller/CMakeLists.txt index 871029f..f9d94af 100644 --- a/firmware/tft_app/src/domain/controller/CMakeLists.txt +++ b/firmware/tft_app/src/domain/controller/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(tft_app_controller STATIC src/controller.c) +add_library(tft_app_controller STATIC src/controller.c src/mode_priority.c) target_include_directories(tft_app_controller PUBLIC include/) diff --git a/firmware/tft_app/src/domain/controller/include/domain/controller.h b/firmware/tft_app/src/domain/controller/include/domain/controller.h index 6686031..4958e7a 100644 --- a/firmware/tft_app/src/domain/controller/include/domain/controller.h +++ b/firmware/tft_app/src/domain/controller/include/domain/controller.h @@ -30,12 +30,24 @@ typedef struct /** * @brief Что изменилось с прошлого вызова — presentation перерисовывает - * только помеченные поля. + * только помеченные поля, audio_policy (Фаза 6) озвучивает изменившиеся + * события поверх этого diff. + * + * mode_pending — сменился РАЗРЕШЁННЫЙ экранный режим (свёртка сигналов по + * таблице приоритетов, §7), а не отдельный сырой сигнал. arrival/movement — + * уровни из декодера; их edge (false→true) — сигнал к озвучке (гонг, старт). */ typedef struct { bool pos_pending; + bool next_pending; bool direction_pending; + bool mode_pending; + bool arrival_pending; + bool movement_pending; + + sul_mode_t mode; /**< разрешённый режим нового результата — presentation + читает его при mode_pending (не нужен отдельный getter) */ } indication_task_t; /** diff --git a/firmware/tft_app/src/domain/controller/include/domain/mode_priority.h b/firmware/tft_app/src/domain/controller/include/domain/mode_priority.h new file mode 100644 index 0000000..f5d53f8 --- /dev/null +++ b/firmware/tft_app/src/domain/controller/include/domain/mode_priority.h @@ -0,0 +1,37 @@ +/** + * @file mode_priority.h + * @brief Свёртка ортогональных сигналов sul_result_t в один экранный режим + * по таблице приоритетов (ARCH.md §7). + * + * Таблица — ЧИСТЫЕ ДАННЫЕ (offset-массив в mode_priority.c), «потенциально + * клиентские» (§7): приоритет задаётся ПОРЯДКОМ строк, не значениями enum. + * Менять приоритет = переставить строки; добавить режим = дописать строку; + * резолвер при этом не трогается. Контроллер применяет активную таблицу. + * + * Чистый C, host-тестируется (tests/host/tft_app_controller). + */ + +#ifndef DOMAIN_MODE_PRIORITY_H_ +#define DOMAIN_MODE_PRIORITY_H_ + +#include "domain/elevator_model.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * @brief Разрешить экранный режим по активной таблице приоритетов. + * + * Проходит таблицу сверху вниз, возвращает режим первой строки, чей булев + * сигнал в @p p_result выставлен. Если ни один спецрежим не активен — + * SUL_MODE_NORMAL. + */ +sul_mode_t sul_resolve_mode(const sul_result_t *p_result); + +#ifdef __cplusplus +} +#endif + +#endif /* DOMAIN_MODE_PRIORITY_H_ */ diff --git a/firmware/tft_app/src/domain/controller/src/controller.c b/firmware/tft_app/src/domain/controller/src/controller.c index d0d5f40..79c1ccb 100644 --- a/firmware/tft_app/src/domain/controller/src/controller.c +++ b/firmware/tft_app/src/domain/controller/src/controller.c @@ -1,5 +1,7 @@ #include "domain/controller.h" +#include "domain/mode_priority.h" + #include void controller_init(controller_ctx_t *p_ctx) @@ -9,12 +11,21 @@ void controller_init(controller_ctx_t *p_ctx) indication_task_t controller_process(controller_ctx_t *p_ctx, const sul_result_t *p_result) { - const indication_task_t task = { + const sul_mode_t PREV_MODE = sul_resolve_mode(&p_ctx->cache); + const sul_mode_t NEW_MODE = sul_resolve_mode(p_result); + + const indication_task_t TASK = { .pos_pending = (strcmp(p_ctx->cache.pos, p_result->pos) != 0), + .next_pending = (strcmp(p_ctx->cache.next, p_result->next) != 0), .direction_pending = (p_ctx->cache.direction != p_result->direction), + .mode_pending = (PREV_MODE != NEW_MODE), + /* Событийные уровни — presentation/audio реагируют на фронт false→true. */ + .arrival_pending = (!p_ctx->cache.arrival && p_result->arrival), + .movement_pending = (!p_ctx->cache.movement && p_result->movement), + .mode = NEW_MODE, }; p_ctx->cache = *p_result; - return task; + return TASK; } diff --git a/firmware/tft_app/src/domain/controller/src/mode_priority.c b/firmware/tft_app/src/domain/controller/src/mode_priority.c new file mode 100644 index 0000000..53fdde5 --- /dev/null +++ b/firmware/tft_app/src/domain/controller/src/mode_priority.c @@ -0,0 +1,52 @@ +#include "domain/mode_priority.h" + +#include + +/** + * @brief Строка таблицы приоритетов: булев сигнал (по смещению в sul_result_t) + * → экранный режим. + * + * Смещение вместо указателя-на-член (в C его нет) — читаем bool по + * offsetof, что делает таблицу голыми данными без кода на строку. + */ +typedef struct +{ + size_t flag_offset; /**< offsetof(sul_result_t, ) */ + sul_mode_t mode; +} mode_rule_t; + +/** + * ТАБЛИЦА ПРИОРИТЕТОВ (ARCH §7). Порядок = приоритет, сверху вниз. + * Согласовано: fireman (высший) > пожар > перегруз > сейсмо > сервис > + * погрузка > (норма — когда ни одна строка не сработала). + * + * Изменить приоритет — переставить строки. Добавить режим — дописать строку + * (offset нового bool-сигнала + его sul_mode_t). Резолвер ниже не меняется. + * Клиентская кастомизация (§7) — подмена этого массива, без правок кода. + */ +static const mode_rule_t k_mode_priority[] = { + { offsetof(sul_result_t, fireman), SUL_MODE_FIREMAN }, + { offsetof(sul_result_t, fire_alarm), SUL_MODE_FIRE_ALARM }, + { offsetof(sul_result_t, overload), SUL_MODE_OVERLOAD }, + { offsetof(sul_result_t, seismic), SUL_MODE_SEISMIC }, + { offsetof(sul_result_t, maintenance), SUL_MODE_MAINTENANCE }, + { offsetof(sul_result_t, lading), SUL_MODE_LADING }, +}; + +#define MODE_PRIORITY_COUNT (sizeof(k_mode_priority) / sizeof(k_mode_priority[0])) + +sul_mode_t sul_resolve_mode(const sul_result_t *p_result) +{ + const uint8_t *p_base = (const uint8_t *) p_result; + + for (size_t i = 0U; i < MODE_PRIORITY_COUNT; ++i) + { + const bool ACTIVE = *(const bool *) (p_base + k_mode_priority[i].flag_offset); + if (ACTIVE) + { + return k_mode_priority[i].mode; + } + } + + return SUL_MODE_NORMAL; +} diff --git a/firmware/tft_app/src/domain/elevator_model/include/domain/elevator_model.h b/firmware/tft_app/src/domain/elevator_model/include/domain/elevator_model.h index 7ef9d20..21bd96d 100644 --- a/firmware/tft_app/src/domain/elevator_model/include/domain/elevator_model.h +++ b/firmware/tft_app/src/domain/elevator_model/include/domain/elevator_model.h @@ -7,14 +7,17 @@ * (НКУ-CAN, УИМ, SD7, УЭЛ, УКЛ, …); конкретный декодер заполняет то, что несёт * его протокол, остальное остаётся дефолтным. * - * Фаза 1 — подмножество: `pos` + `direction` (см. PLAN.md). Остальные поля - * ARCH §6 (next, сигналы, lading_secs, floor_num) добавляются в Фазе 2 вместе - * с полным декодером НКУ-CAN и таблицей приоритетов режимов. + * Обзор боевых декодеров (NKU-CAN, UIM, UKL, UEL, SD7) подтвердил: набор + * сигналов ниже покрывает все протоколы — различаются лишь битовые кодировки + * (живут в каждом декодере), не сам словарь сигналов. Провиженинг-концепты + * (напр. `cop_mode`/`display_id`) — это настройки устройства (§8), НЕ данные + * СУЛ, и в этой структуре им не место. */ #ifndef DOMAIN_ELEVATOR_MODEL_H_ #define DOMAIN_ELEVATOR_MODEL_H_ +#include #include #ifdef __cplusplus @@ -45,25 +48,67 @@ typedef enum } sul_direction_t; /** - * @brief Канонический результат декодирования кадра/пакета СУЛ. + * @brief Канонический экранный режим — результат свёртки ортогональных булевых + * сигналов `sul_result_t` через таблицу приоритетов (ARCH §7). + * + * ВАЖНО: числовые значения enum — просто идентификаторы, они НЕ задают + * приоритет. Приоритет определяется ТОЛЬКО порядком строк в таблице + * (domain/controller, `k_mode_priority[]`). Менять приоритет = переставлять + * строки таблицы, не трогая ни этот enum, ни резолвер. + */ +typedef enum +{ + SUL_MODE_NORMAL = 0, /**< обычная индикация этажа/стрелки */ + SUL_MODE_LADING, /**< погрузка (инструментальная или временная) */ + SUL_MODE_MAINTENANCE,/**< сервис / ревизия / МП1 / МП2 */ + SUL_MODE_SEISMIC, /**< сейсмоопасность */ + SUL_MODE_OVERLOAD, /**< перегруз */ + SUL_MODE_FIRE_ALARM, /**< пожарная тревога */ + SUL_MODE_FIREMAN, /**< режим пожарного */ +} sul_mode_t; + +/** + * @brief Канонический результат декодирования кадра/пакета СУЛ (ARCH §6). * * Позиция — UTF-8 строка (реальные шрифты ASCII + кириллица), не число: * универсально для всех протоколов, host-тест сравнивает строки напрямую. * Декодер ничего не знает про шрифт/рендер — валидация рендеримости * (покрытие активным шрифтом) — задача UI-слоя (ARCH §11), не decode(). + * + * Сигналы ОРТОГОНАЛЬНЫ (могут сосуществовать) — в один экранный режим их + * сводит controller по таблице приоритетов (§7), не декодер. Декодер выдаёт + * УРОВНИ (сигнал активен сейчас), а не события: edge-детекцию (гонг→звук, + * начало движения→звук) делает presentation/audio_policy поверх diff + * контроллера (ARCH §4, Фаза 6). */ typedef struct { - char pos[SUL_POS_BUF_LEN]; /**< позиция кабины, напр. "12", "-1", "П" */ + char pos[SUL_POS_BUF_LEN]; /**< позиция кабины, напр. "12", "-1", "П" */ + char next[SUL_POS_BUF_LEN]; /**< следующий этаж (пусто "" = нет данных) */ sul_direction_t direction; + + /* Ортогональные сигналы (приоритет разрешает controller, §7): */ + 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; /** * @brief Состояние по умолчанию / при потере связи. * - * pos="--" (нет данных), direction=SUL_DIR_NONE. Вызывающий (контроллер через - * sul_rx-задачу при таймауте, либо init перед первым кадром) использует это - * как отправную точку — не HAL-зависимо, чистая функция. + * pos="--" (нет данных), next="", direction=SUL_DIR_NONE, все сигналы false, + * lading_secs=0, floor_num=0. Вызывающий (контроллер через sul_rx-задачу при + * таймауте, либо init перед первым кадром) использует это как отправную точку — + * не HAL-зависимо, чистая функция. */ sul_result_t sul_default_state(void); diff --git a/firmware/tft_app/src/domain/sul/nku_can/include/domain/sul/nku_can.h b/firmware/tft_app/src/domain/sul/nku_can/include/domain/sul/nku_can.h index 93ab7aa..7447c9e 100644 --- a/firmware/tft_app/src/domain/sul/nku_can/include/domain/sul/nku_can.h +++ b/firmware/tft_app/src/domain/sul/nku_can/include/domain/sul/nku_can.h @@ -2,9 +2,11 @@ * @file nku_can.h * @brief Чистый декодер протокола НКУ-CAN (порт из OLD_PROJECT msg_receiver_task). * - * Фаза 1 — подмножество: PACKET1 (направление) + PACKET3 (позиция кабины). - * Остальные пакеты (PACKET2/4/5, режимы, гонг, погрузка, удалённая установка - * адреса) — Фаза 2, см. PLAN.md. + * Фаза 2 — полный разбор индикации: PACKET1 (направление, режимы, начало + * движения), PACKET2 (перегруз), PACKET3 (позиция, гонг, временная погрузка), + * PACKET4 (перегруз-вариант, сейсмо), PACKET5 (следующий этаж). Удалённая + * установка адреса (0x4X1/0x5XB) — Фаза 3 (нужен settings_store для записи); + * здесь эти кадры игнорируются как чужие ID. * * Адрес станции (nku_address) захардкожен в 0 — фильтры/ID без сдвига группы. * Фаза 3 параметризует через настройки; сигнатура decode() не изменится. @@ -25,19 +27,30 @@ extern "C" #endif /** - * @brief Состояние декодера — накопленный текущий sul_result_t. + * @brief Состояние декодера — накопленный текущий sul_result_t + внутренние + * латчи для полей с несколькими источниками. * - * PACKET1 и PACKET3 несут РАЗНЫЕ поля (направление / позицию) в разных - * кадрах — decode() обновляет только своё поле в state и отдаёт наружу - * ПОЛНУЮ накопленную копию, а не только то, что пришло в этом кадре. + * Разные пакеты несут РАЗНЫЕ поля в разных кадрах — decode() обновляет только + * пришедшее и отдаёт наружу ПОЛНУЮ накопленную копию state, а не дельту. * - * Фаза 2 добавит сюда edge-detection состояние для гонга/погрузки/режимов - * (аналог event_edge_t в legacy) — структура специально не голый sul_result_t, - * чтобы не менять сигнатуру при расширении. + * Некоторые ВЫХОДНЫЕ поля кормятся НЕСКОЛЬКИМИ пакетами и должны быть их OR, + * иначе пакет-без-сигнала сбросил бы флаг, выставленный другим пакетом (боевой + * баг-класс из legacy — там держались parallel-флаги overload_flag/_new, + * lading_flag/time_lading). Держим эти под-источники раздельно в ctx и + * пересчитываем выход как OR после каждого пакета: + * state.overload = overload_p2 || overload_p4; + * state.lading = lading_instr || (lading_secs > 0). */ typedef struct { sul_result_t state; + + /* Раздельные под-источники (см. докстроку выше). */ + bool overload_p2; /**< PACKET2: data[7] & 0x40 */ + bool overload_p4; /**< PACKET4: data[5] & 0x40 */ + bool lading_instr; /**< PACKET1: код режима 0x10 (инструментальная) */ + uint8_t current_level; /**< PACKET1: data[3] & 0x3F — числовой уровень остановки, + для гейта «следующего этажа» в PACKET5 */ } nku_can_ctx_t; /** Сброс к состоянию по умолчанию (sul_default_state()). Вызвать перед первым decode(). */ diff --git a/firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c b/firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c index b2ca756..63b2c6e 100644 --- a/firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c +++ b/firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c @@ -3,18 +3,45 @@ #include #include -/* Фаза 1: адрес станции захардкожен в 0 — базовые ID без сдвига группы - * (легаси: 0x506|group4, group4 = nku_address<<4; для address=0 group4=0). - * Фаза 3 параметризует через настройки. */ -#define PACKET1_ID 0x506U -#define PACKET3_ID 0x508U +/* Адрес станции захардкожен в 0 (Фаза 1/2) — базовые ID без сдвига группы + * (легаси: 0x506|group4, group4 = nku_address<<4; для address=0 group4=0; + * PACKET5 — group6 = nku_address<<6). Фаза 3 параметризует через настройки. */ +#define PACKET1_ID 0x506U /* направление, режимы, начало движения, двери */ +#define PACKET2_ID 0x408U /* перегруз (вариант 1) */ +#define PACKET3_ID 0x508U /* позиция кабины, гонг, временная погрузка */ +#define PACKET4_ID 0x50BU /* перегруз (вариант 2), сейсмоопасность */ +#define PACKET5_ID 0x606U /* следующий этаж */ #define PROTO_DLC 8U -#define ARROW_MASK 0x03U -#define FLOOR_MASK 0x3FU +#define ARROW_MASK 0x03U /* PACKET1 data[6][1:0] — стрелка */ +#define MOVEMENT_MASK 0x0CU /* PACKET1 data[6][3:2] — начало движения */ +#define ICON_MASK 0xF0U /* PACKET1 data[6][7:4] — код режима */ +#define FLOOR_MASK 0x3FU /* символ этажа / числовой уровень */ +/* Двери (PACKET1 data[4]: откр. 0x10 / закр. 0x20) — только озвучка, не поле + * §6; декодируются в Фазе 6 (audio_policy). Здесь намеренно не разбираются. */ -#define SYMBOL_SPACE 16U -#define SYMBOL_TOTAL 38U +/* Коды режима в нибле data[6] & ICON_MASK (PACKET1). Точные значения нибла. */ +#define ICON_LADING 0x10U /* инструментальная погрузка */ +#define ICON_MP1 0x30U /* сервис / МП1 */ +#define ICON_REVISION 0x40U /* ревизия */ +#define ICON_MP2 0x50U /* сервис / МП2 */ +#define ICON_FIRE 0x70U /* пожарная тревога */ +#define ICON_FIREMAN 0xF0U /* режим пожарного */ + +#define WEIGHT_MASK 0x40U /* PACKET2 data[7] / PACKET4 data[5] — перегруз */ +#define GONG_MASK 0x40U /* PACKET3 data[3] — гонг активен при БИТЕ == 0 */ +#define LADING_SEC_MASK 0x3FU /* PACKET3 data[2] — секунды погрузки */ +#define LADING_MIN_MASK 0x0FU /* PACKET3 data[3] — минуты погрузки */ +#define SEISMIC_MASK 0x80U /* PACKET4 data[0] — сейсмоопасность */ + +#define SYMBOL_SPACE 16U +#define SYMBOL_A 10U +#define SYMBOL_P 17U /* "П" */ +#define SYMBOL_p 19U /* "п" */ +#define SYMBOL_HYPHEN 22U /* "-" */ +#define SYMBOL_TOTAL 38U + +#define FLOOR_NUM_UNKNOWN 60U /* «н/д» для озвучки — как в legacy floor_number_parser */ /* Таблица символов НКУ-CAN — порт из OLD_PROJECT floor_string_composer() * (source/main_programm.c). Индекс — код символа с шины (байт & FLOOR_MASK). */ @@ -25,60 +52,174 @@ static const char *const S_SYMBOL_TABLE[SYMBOL_TOTAL] = { void nku_can_init(nku_can_ctx_t *p_ctx) { - p_ctx->state = sul_default_state(); + p_ctx->state = sul_default_state(); + p_ctx->overload_p2 = false; + p_ctx->overload_p4 = false; + p_ctx->lading_instr = false; + p_ctx->current_level = 0U; +} + +/* Пересчёт выходных полей, кормящихся несколькими пакетами (см. nku_can.h). */ +static void recompute_multi_source(nku_can_ctx_t *p_ctx) +{ + p_ctx->state.overload = p_ctx->overload_p2 || p_ctx->overload_p4; + p_ctx->state.lading = p_ctx->lading_instr || (p_ctx->state.lading_secs > 0U); } /** - * @brief PACKET1 (0x506) — направление движения. + * @brief Составить UTF-8 строку этажа из кодов левого/правого символа. * - * ARROW_MASK=0x03 на data[6] (DATA7): 0=нет/1=вверх/2=вниз/3=двойная стрелка. - * Легаси (msg_receiver_task, PACKET1) обрабатывал только 0/1/2 — case 3 - * отсутствовал (направление молча не менялось). 3 = «двойная стрелка» - * (спецрежим индикации, словарь special/DisplayArrowIcon) — уточнено отдельно. + * Порт floor_string_composer(): left==SPACE или left==0 → однозначный этаж + * (только правый символ; станция шлёт 0x00 как «нет левого символа», трактуем + * как пробел). Возврат false — код вне таблицы (малформированный кадр). + */ +static bool compose_chars(uint8_t left, uint8_t right, char *p_out) +{ + if ((left >= SYMBOL_TOTAL) || (right >= SYMBOL_TOTAL)) + { + return false; + } + + if ((left == SYMBOL_SPACE) || (left == 0U)) + { + (void) snprintf(p_out, SUL_POS_BUF_LEN, "%s", S_SYMBOL_TABLE[right]); + } + else + { + (void) snprintf(p_out, SUL_POS_BUF_LEN, "%s%s", S_SYMBOL_TABLE[left], + S_SYMBOL_TABLE[right]); + } + + return true; +} + +/** + * @brief Производный числовой этаж для озвучки — порт floor_number_parser(). * - * Маска покрывает ровно 0..3 — весь диапазон sul_direction_t, прямое - * приведение типа корректно без switch/default. + * Стандартные (0..40): left*10+right (или только right при пробеле). + * Отрицательные ("-" слева): 40+right. Подвальные ("П"/"п" слева): 50+right. + * Всё нераспознанное / вне диапазона → FLOOR_NUM_UNKNOWN (60). + */ +static uint8_t floor_number_parser(uint8_t left, uint8_t right) +{ + if (((left < SYMBOL_A) || (left == SYMBOL_SPACE)) && (right < SYMBOL_A)) + { + const uint8_t FLOOR = (left == SYMBOL_SPACE) ? right : (uint8_t) (left * 10U + right); + return (FLOOR < 41U) ? FLOOR : FLOOR_NUM_UNKNOWN; + } + + if ((left == SYMBOL_HYPHEN) && (right < SYMBOL_A)) + { + return (right > 0U) ? (uint8_t) (40U + right) : FLOOR_NUM_UNKNOWN; + } + + if (((left == SYMBOL_P) || (left == SYMBOL_p)) && (right < SYMBOL_A)) + { + return (uint8_t) (50U + right); + } + + return FLOOR_NUM_UNKNOWN; +} + +/** + * @brief PACKET1 (0x506) — направление, режим, начало движения, уровень остановки. + * + * data[6]: [1:0] стрелка · [3:2] начало движения · [7:4] код режима. + * data[3][5:0]: числовой уровень остановки (гейт «следующего этажа», PACKET5). + * + * Режимные флаги, которыми владеет ТОЛЬКО PACKET1 (fire/maintenance/fireman и + * инструментальная погрузка), сбрасываются в начале и выставляются по нибле — + * так пакет-без-режима гасит устаревший режим (как в legacy). overload и + * seismic PACKET1 НЕ трогает (ими владеют PACKET2/4). */ static void decode_packet1(nku_can_ctx_t *p_ctx, const uint8_t *p_data) { p_ctx->state.direction = (sul_direction_t) (p_data[6] & ARROW_MASK); + p_ctx->state.movement = ((p_data[6] & MOVEMENT_MASK) != 0U); + p_ctx->current_level = p_data[3] & FLOOR_MASK; + + /* Стрелка «нет движения» → сбрасываем следующий этаж (появляется только + * пока кабина едет, гаснет на прибытии — см. PACKET5). */ + if (p_ctx->state.direction == SUL_DIR_NONE) + { + p_ctx->state.next[0] = '\0'; + } + + p_ctx->state.fire_alarm = false; + p_ctx->state.maintenance = false; + p_ctx->state.fireman = false; + p_ctx->lading_instr = false; + + switch (p_data[6] & ICON_MASK) + { + case ICON_FIRE: + p_ctx->state.fire_alarm = true; + break; + case ICON_MP1: + case ICON_MP2: + case ICON_REVISION: + p_ctx->state.maintenance = true; + break; + case ICON_LADING: + p_ctx->lading_instr = true; + break; + case ICON_FIREMAN: + p_ctx->state.fireman = true; + break; + default: + break; + } } /** - * @brief PACKET3 (0x508) — позиция кабины (left/right символы). + * @brief PACKET3 (0x508) — позиция кабины, гонг, временная погрузка. * - * FLOOR_MASK=0x3F на data[5] (left) и data[6] (right) — порт - * floor_string_composer() из OLD_PROJECT. left==SPACE или left==0 → - * однозначный этаж, выводится только правый символ (легаси трактует байт - * 0x00 так же, как пробел — станция может слать нулевой байт вместо явного - * кода пробела для «нет левого символа»; порт без изменений поведения). - * - * В отличие от легаси (молча оставляет буфер как есть при выходе за - * SYMBOL_TOTAL — маска даёт до 63 сырых значений при 38 валидных символах), - * здесь это ошибка (false) — не полагаемся на предыдущее содержимое буфера. - * - * @return false — left/right вне таблицы символов (малформированный кадр). + * @return false — код символа позиции вне таблицы (малформированный кадр). */ static bool decode_packet3(nku_can_ctx_t *p_ctx, const uint8_t *p_data) { const uint8_t LEFT = p_data[5] & FLOOR_MASK; const uint8_t RIGHT = p_data[6] & FLOOR_MASK; - if ((LEFT >= SYMBOL_TOTAL) || (RIGHT >= SYMBOL_TOTAL)) + if (!compose_chars(LEFT, RIGHT, p_ctx->state.pos)) { return false; } + p_ctx->state.floor_num = floor_number_parser(LEFT, RIGHT); - if ((LEFT == SYMBOL_SPACE) || (LEFT == 0U)) + /* Гонг активен, когда БИТ 0x40 в data[3] СБРОШЕН (инверсная кодировка). */ + p_ctx->state.arrival = ((p_data[3] & GONG_MASK) == 0U); + + /* Временная погрузка: остаток = минуты*60 + секунды. */ + const uint8_t SECS = p_data[2] & LADING_SEC_MASK; + const uint8_t MINS = p_data[3] & LADING_MIN_MASK; + p_ctx->state.lading_secs = (uint16_t) ((uint16_t) MINS * 60U + SECS); + + return true; +} + +/** + * @brief PACKET5 (0x606) — следующий этаж. + * + * Доверяем «следующему этажу» только пока кабина реально едет (↑/↓) и байт + * назначения (data[0]) отличается от текущего уровня остановки — 0x606 и 0x506 + * приходят асинхронно, устаревший байт назначения на стоянке порождал бы + * ложную индикацию (боевая заметка legacy). Иначе — очищаем next. + * + * @return false — код символа вне таблицы (малформированный кадр); при этом + * поле next не трогаем. + */ +static bool decode_packet5(nku_can_ctx_t *p_ctx, const uint8_t *p_data) +{ + const bool MOVING = + (p_ctx->state.direction == SUL_DIR_UP) || (p_ctx->state.direction == SUL_DIR_DOWN); + + if (MOVING && (p_data[0] != p_ctx->current_level)) { - (void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s", S_SYMBOL_TABLE[RIGHT]); - } - else - { - (void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s%s", S_SYMBOL_TABLE[LEFT], - S_SYMBOL_TABLE[RIGHT]); + return compose_chars(p_data[3] & FLOOR_MASK, p_data[4] & FLOOR_MASK, p_ctx->state.next); } + p_ctx->state.next[0] = '\0'; return true; } @@ -86,19 +227,25 @@ sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_ { nku_can_ctx_t *p_state = (nku_can_ctx_t *) p_ctx; - if (p_frame->id == PACKET1_ID) + switch (p_frame->id) { + case PACKET1_ID: if (p_frame->len != PROTO_DLC) { return SUL_STATUS_ERR; } decode_packet1(p_state, p_frame->p_data); - *p_out = p_state->state; - return SUL_STATUS_OK; - } + break; - if (p_frame->id == PACKET3_ID) - { + case PACKET2_ID: + if (p_frame->len != PROTO_DLC) + { + return SUL_STATUS_ERR; + } + p_state->overload_p2 = ((p_frame->p_data[7] & WEIGHT_MASK) == WEIGHT_MASK); + break; + + case PACKET3_ID: if (p_frame->len != PROTO_DLC) { return SUL_STATUS_ERR; @@ -107,9 +254,33 @@ sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_ { return SUL_STATUS_ERR; } - *p_out = p_state->state; - return SUL_STATUS_OK; + break; + + case PACKET4_ID: + if (p_frame->len != PROTO_DLC) + { + return SUL_STATUS_ERR; + } + p_state->overload_p4 = ((p_frame->p_data[5] & WEIGHT_MASK) == WEIGHT_MASK); + p_state->state.seismic = ((p_frame->p_data[0] & SEISMIC_MASK) == SEISMIC_MASK); + break; + + case PACKET5_ID: + if (p_frame->len != PROTO_DLC) + { + return SUL_STATUS_ERR; + } + if (!decode_packet5(p_state, p_frame->p_data)) + { + return SUL_STATUS_ERR; + } + break; + + default: + return SUL_STATUS_IGNORED; } - return SUL_STATUS_IGNORED; + recompute_multi_source(p_state); + *p_out = p_state->state; + return SUL_STATUS_OK; } diff --git a/firmware/tft_app/src/services/gfx/include/services/gfx.h b/firmware/tft_app/src/services/gfx/include/services/gfx.h index 1ce4a19..e5738c0 100644 --- a/firmware/tft_app/src/services/gfx/include/services/gfx.h +++ b/firmware/tft_app/src/services/gfx/include/services/gfx.h @@ -50,7 +50,15 @@ typedef struct /* Компилируемые в прошивку шрифты (services/gfx/fonts/, сгенерированы * lcd-image-converter пользователем). */ extern const tFont FloorFontFallback; /* 0-9, "-", пробел — fallback (§11 ARCH) */ -extern const tFont SystemFont; /* ASCII + кириллица — логи/меню */ + +/* SystemFont — ASCII + кириллица (логи/меню, метки режимов в fallback). + * Сгенерированный SystemFont.c экспортирует tFont под именем из .xml + * конвертера — JBMono24 (JetBrains Mono 24pt). Публичное имя API — SystemFont; + * связываем алиасом-макросом, не редактируя сгенерированный файл (он + * перезапишется при регенерации шрифта). Использовать как объект: &SystemFont — + * симметрично FloorFontFallback. */ +extern const tFont JBMono24; +#define SystemFont JBMono24 /** XRGB8888 (X игнорируется ELCDIF) — X-байт значения не имеет. */ typedef uint32_t gfx_color_t; diff --git a/firmware/tft_app/src/ui/fallback/src/fallback.c b/firmware/tft_app/src/ui/fallback/src/fallback.c index 391bfd9..c958357 100644 --- a/firmware/tft_app/src/ui/fallback/src/fallback.c +++ b/firmware/tft_app/src/ui/fallback/src/fallback.c @@ -1,8 +1,11 @@ #include "ui/fallback.h" +#include "domain/mode_priority.h" #include "services/gfx.h" -/* Фаза 1: позиции захардкожены под TFT8 (800×600, panel текущего стенда). +#include + +/* Фаза 1/2: позиции захардкожены под TFT8 (800×600, panel текущего стенда). * Цифра '0' FloorFontFallback — 132×162 px (проверено по сгенерированным * данным шрифта). Фаза 5 (layout-движок) заменит на якорное позиционирование, * независимое от разрешения панели. */ @@ -10,17 +13,43 @@ #define POS_Y 150U #define ARROW_Y 380U #define ARROW_SIZE 80U +#define MODE_Y 260U /* строка режима (SystemFont), safe-mode индикация */ +#define LADING_Y 330U /* обратный отсчёт погрузки (крупным шрифтом) */ -static void render(const sul_result_t *p_result) +/** + * @brief Короткая asset-free метка спецрежима (ARCH §11: fallback без + * спрайтов/FS). Богатая полноэкранная графика режимов — Фаза 4/5; + * здесь — только текст системным шрифтом, чтобы режим был виден и в + * safe-mode (без ассетов). NORMAL метки не имеет. + */ +static const char *mode_label(sul_mode_t mode) { - gfx_clear(GFX_COLOR_BLACK); + switch (mode) + { + case SUL_MODE_FIREMAN: return "ПОЖАРНЫЙ"; + case SUL_MODE_FIRE_ALARM: return "ПОЖАР"; + case SUL_MODE_OVERLOAD: return "ПЕРЕГРУЗ"; + case SUL_MODE_SEISMIC: return "СЕЙСМО"; + case SUL_MODE_MAINTENANCE: return "СЕРВИС"; + case SUL_MODE_LADING: return "ПОГРУЗКА"; + case SUL_MODE_NORMAL: + default: return NULL; + } +} - const uint16_t str_width = gfx_string_width(&FloorFontFallback, p_result->pos); - const uint16_t pos_x = (uint16_t) ((PANEL_WIDTH - str_width) / 2U); - (void) gfx_draw_string(&FloorFontFallback, p_result->pos, pos_x, POS_Y); +static void draw_centered(const tFont *p_font, const char *p_str, uint16_t y) +{ + const uint16_t width = gfx_string_width(p_font, p_str); + const uint16_t x = (uint16_t) ((PANEL_WIDTH - width) / 2U); + (void) gfx_draw_string(p_font, p_str, x, y); +} - /* SUL_DIR_NONE — без стрелки. SUL_DIR_DOUBLE — тоже (не путать с - * up/down одной стрелкой; отдельная индикация — Фаза 2). */ +static void render_normal(const sul_result_t *p_result) +{ + draw_centered(&FloorFontFallback, p_result->pos, POS_Y); + + /* SUL_DIR_NONE — без стрелки. SUL_DIR_DOUBLE — тоже (двойная стрелка — + * отдельная спрайтовая индикация Фазы 4/5, в fallback не рисуем). */ if ((p_result->direction == SUL_DIR_UP) || (p_result->direction == SUL_DIR_DOWN)) { const gfx_arrow_dir_t dir = (p_result->direction == SUL_DIR_UP) ? GFX_ARROW_UP : GFX_ARROW_DOWN; @@ -29,16 +58,44 @@ static void render(const sul_result_t *p_result) } } +static void render(sul_mode_t mode, const sul_result_t *p_result) +{ + gfx_clear(GFX_COLOR_BLACK); + + const char *label = mode_label(mode); + if (label == NULL) + { + render_normal(p_result); + return; + } + + /* Спецрежим: safe-mode текст. Для временной погрузки — ещё и обратный + * отсчёт крупным шрифтом (lading_secs>0 отличает временную от + * инструментальной). */ + draw_centered(&SystemFont, label, MODE_Y); + + if ((mode == SUL_MODE_LADING) && (p_result->lading_secs > 0U)) + { + char secs[6]; + (void) snprintf(secs, sizeof(secs), "%u", (unsigned) p_result->lading_secs); + draw_centered(&FloorFontFallback, secs, LADING_Y); + } +} + void ui_fallback_render_initial(const sul_result_t *p_result) { - render(p_result); + render(sul_resolve_mode(p_result), p_result); } void ui_fallback_render(const indication_task_t *p_task, const sul_result_t *p_result) { - if (!p_task->pos_pending && !p_task->direction_pending) + /* Перерисовываем на изменение того, что fallback реально показывает: + * позиция, стрелка, режим (+ отсчёт погрузки идёт вместе с mode/pos). + * next-этаж в safe-mode не рисуется (богатый layout — Фаза 5), поэтому + * на next_pending не будим. */ + if (!p_task->pos_pending && !p_task->direction_pending && !p_task->mode_pending) { return; } - render(p_result); + render(p_task->mode, p_result); } diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 8acc72e..db97424 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -325,7 +325,7 @@ add_host_test( tft_app_smoke/test_tft_app_smoke.c) # ----------------------------------------------------------------------------- -# tft_app — domain (Фаза 1: чистый декодер НКУ-CAN + контроллер) +# tft_app — domain (Фаза 1/2: чистый декодер НКУ-CAN + контроллер + приоритеты) # ----------------------------------------------------------------------------- set(TFT_APP_DOMAIN_DIR ${PROJECT_SOURCE_DIR}/firmware/tft_app/src/domain) @@ -348,6 +348,7 @@ add_host_test( tft_app_controller/test_controller.c ${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c ${TFT_APP_DOMAIN_DIR}/controller/src/controller.c + ${TFT_APP_DOMAIN_DIR}/controller/src/mode_priority.c INCLUDES ${TFT_APP_DOMAIN_DIR}/elevator_model/include ${TFT_APP_DOMAIN_DIR}/controller/include) diff --git a/tests/host/tft_app_controller/test_controller.c b/tests/host/tft_app_controller/test_controller.c index da9e087..fc28696 100644 --- a/tests/host/tft_app_controller/test_controller.c +++ b/tests/host/tft_app_controller/test_controller.c @@ -8,6 +8,7 @@ #include "unity.h" #include "domain/controller.h" +#include "domain/mode_priority.h" #include @@ -109,6 +110,149 @@ static void test_feeding_default_state_after_real_data_marks_both_pending(void) TEST_ASSERT_TRUE(task.direction_pending); } +/* ── Таблица приоритетов режимов (sul_resolve_mode) ──────────────────── */ + +static void test_resolve_mode_normal_when_no_signals(void) +{ + const sul_result_t r = sul_default_state(); + TEST_ASSERT_EQUAL(SUL_MODE_NORMAL, sul_resolve_mode(&r)); +} + +static void test_resolve_mode_single_signals(void) +{ + sul_result_t r; + + r = sul_default_state(); + r.fireman = true; + TEST_ASSERT_EQUAL(SUL_MODE_FIREMAN, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.fire_alarm = true; + TEST_ASSERT_EQUAL(SUL_MODE_FIRE_ALARM, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.overload = true; + TEST_ASSERT_EQUAL(SUL_MODE_OVERLOAD, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.seismic = true; + TEST_ASSERT_EQUAL(SUL_MODE_SEISMIC, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.maintenance = true; + TEST_ASSERT_EQUAL(SUL_MODE_MAINTENANCE, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.lading = true; + TEST_ASSERT_EQUAL(SUL_MODE_LADING, sul_resolve_mode(&r)); +} + +/* Согласованный порядок: fireman > пожар > перегруз > сейсмо > сервис > + * погрузка. Проверяем каждую соседнюю пару при одновременной активности. */ +static void test_resolve_mode_priority_ordering(void) +{ + sul_result_t r; + + r = sul_default_state(); + r.fireman = true; + r.fire_alarm = true; /* fireman выигрывает у пожара */ + TEST_ASSERT_EQUAL(SUL_MODE_FIREMAN, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.fire_alarm = true; + r.overload = true; /* пожар выигрывает у перегруза */ + TEST_ASSERT_EQUAL(SUL_MODE_FIRE_ALARM, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.overload = true; + r.seismic = true; /* перегруз выигрывает у сейсмо */ + TEST_ASSERT_EQUAL(SUL_MODE_OVERLOAD, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.seismic = true; + r.maintenance = true; /* сейсмо выигрывает у сервиса */ + TEST_ASSERT_EQUAL(SUL_MODE_SEISMIC, sul_resolve_mode(&r)); + + r = sul_default_state(); + r.maintenance = true; + r.lading = true; /* сервис выигрывает у погрузки */ + TEST_ASSERT_EQUAL(SUL_MODE_MAINTENANCE, sul_resolve_mode(&r)); +} + +/* ── Diff новых полей контроллера ────────────────────────────────────── */ + +static void test_mode_pending_on_mode_change(void) +{ + controller_ctx_t ctx; + controller_init(&ctx); + + sul_result_t normal = sul_default_state(); + (void) snprintf(normal.pos, SUL_POS_BUF_LEN, "5"); + (void) controller_process(&ctx, &normal); + + sul_result_t fire = normal; + fire.fire_alarm = true; + const indication_task_t task = controller_process(&ctx, &fire); + + TEST_ASSERT_TRUE(task.mode_pending); + TEST_ASSERT_EQUAL(SUL_MODE_FIRE_ALARM, task.mode); + TEST_ASSERT_FALSE(task.pos_pending); /* позиция не менялась */ +} + +/* Смена сырого сигнала, не меняющая РАЗРЕШЁННЫЙ режим (перегруз при активном + * пожаре), не должна поднимать mode_pending. */ +static void test_mode_pending_stable_when_resolved_mode_unchanged(void) +{ + controller_ctx_t ctx; + controller_init(&ctx); + + sul_result_t fire = sul_default_state(); + fire.fire_alarm = true; + (void) controller_process(&ctx, &fire); + + sul_result_t fire_plus_overload = fire; + fire_plus_overload.overload = true; /* режим остаётся FIRE_ALARM */ + const indication_task_t task = controller_process(&ctx, &fire_plus_overload); + + TEST_ASSERT_FALSE(task.mode_pending); + TEST_ASSERT_EQUAL(SUL_MODE_FIRE_ALARM, task.mode); +} + +static void test_arrival_and_movement_pending_on_rising_edge(void) +{ + controller_ctx_t ctx; + controller_init(&ctx); + + sul_result_t quiet = sul_default_state(); + (void) controller_process(&ctx, &quiet); + + sul_result_t event = sul_default_state(); + event.arrival = true; + event.movement = true; + indication_task_t task = controller_process(&ctx, &event); + TEST_ASSERT_TRUE(task.arrival_pending); + TEST_ASSERT_TRUE(task.movement_pending); + + /* Уровень держится — фронта нет, pending не выставляется повторно. */ + task = controller_process(&ctx, &event); + TEST_ASSERT_FALSE(task.arrival_pending); + TEST_ASSERT_FALSE(task.movement_pending); +} + +static void test_next_pending_on_next_floor_change(void) +{ + controller_ctx_t ctx; + controller_init(&ctx); + + sul_result_t r = sul_default_state(); + (void) controller_process(&ctx, &r); + + (void) snprintf(r.next, SUL_POS_BUF_LEN, "16"); + const indication_task_t task = controller_process(&ctx, &r); + TEST_ASSERT_TRUE(task.next_pending); + TEST_ASSERT_FALSE(task.pos_pending); +} + int main(void) { UNITY_BEGIN(); @@ -120,5 +264,13 @@ int main(void) RUN_TEST(test_only_direction_change_marks_only_direction_pending); RUN_TEST(test_feeding_default_state_after_real_data_marks_both_pending); + RUN_TEST(test_resolve_mode_normal_when_no_signals); + RUN_TEST(test_resolve_mode_single_signals); + RUN_TEST(test_resolve_mode_priority_ordering); + RUN_TEST(test_mode_pending_on_mode_change); + RUN_TEST(test_mode_pending_stable_when_resolved_mode_unchanged); + RUN_TEST(test_arrival_and_movement_pending_on_rising_edge); + RUN_TEST(test_next_pending_on_next_floor_change); + return UNITY_END(); } diff --git a/tests/host/tft_app_sul_nku/test_sul_nku_can.c b/tests/host/tft_app_sul_nku/test_sul_nku_can.c index f420cab..4c305e7 100644 --- a/tests/host/tft_app_sul_nku/test_sul_nku_can.c +++ b/tests/host/tft_app_sul_nku/test_sul_nku_can.c @@ -6,14 +6,17 @@ * golden-векторы CAN-кадров строятся прямо в тесте. */ -#include "unity.h" - #include "domain/sul/nku_can.h" +#include "unity.h" #include -void setUp(void) {} -void tearDown(void) {} +void setUp(void) +{ +} +void tearDown(void) +{ +} /* ── Вспомогательные конструкторы кадров ─────────────────────────────── */ @@ -26,7 +29,7 @@ static sul_frame_t make_packet1(uint8_t arrow_bits) static uint8_t s_data[8]; memset(s_data, 0, sizeof(s_data)); s_data[6] = arrow_bits; /* ARROW_MASK=0x03 на data[6] (DATA7) */ - return (sul_frame_t){.id = PACKET1_ID, .bus = 0, .p_data = s_data, .len = 8U}; + return (sul_frame_t){ .id = PACKET1_ID, .bus = 0, .p_data = s_data, .len = 8U }; } static sul_frame_t make_packet3(uint8_t left, uint8_t right) @@ -35,7 +38,64 @@ static sul_frame_t make_packet3(uint8_t left, uint8_t right) memset(s_data, 0, sizeof(s_data)); s_data[5] = left; /* FLOOR_MASK=0x3F на data[5] (left) */ s_data[6] = right; /* FLOOR_MASK=0x3F на data[6] (right) */ - return (sul_frame_t){.id = PACKET3_ID, .bus = 0, .p_data = s_data, .len = 8U}; + return (sul_frame_t){ .id = PACKET3_ID, .bus = 0, .p_data = s_data, .len = 8U }; +} + +#define PACKET2_ID 0x408U +#define PACKET4_ID 0x50BU +#define PACKET5_ID 0x606U + +/* PACKET1 полный: стрелка [1:0], начало движения [3:2], код режима [7:4] в + * data[6]; числовой уровень остановки — data[3]&0x3F. */ +static sul_frame_t make_packet1_full(uint8_t arrow, uint8_t movement, uint8_t icon_nibble, + uint8_t level) +{ + static uint8_t s_data[8]; + memset(s_data, 0, sizeof(s_data)); + s_data[6] = (uint8_t) ((icon_nibble & 0xF0U) | ((movement & 0x03U) << 2) | (arrow & 0x03U)); + s_data[3] = level & 0x3FU; + return (sul_frame_t){ .id = PACKET1_ID, .bus = 0, .p_data = s_data, .len = 8U }; +} + +static sul_frame_t make_packet2(bool overload) +{ + static uint8_t s_data[8]; + memset(s_data, 0, sizeof(s_data)); + s_data[7] = overload ? 0x40U : 0x00U; /* WEIGHT_MASK */ + return (sul_frame_t){ .id = PACKET2_ID, .bus = 0, .p_data = s_data, .len = 8U }; +} + +/* PACKET3 полный: символы этажа + гонг (активен при СБРОШЕННОМ бите 0x40 в + * data[3]) + временная погрузка (data[2] сек, data[3] низкий нибл — мин). */ +static sul_frame_t make_packet3_full(uint8_t left, uint8_t right, bool gong, uint8_t secs, + uint8_t mins) +{ + static uint8_t s_data[8]; + memset(s_data, 0, sizeof(s_data)); + s_data[5] = left; + s_data[6] = right; + s_data[2] = secs & 0x3FU; + s_data[3] = (uint8_t) ((mins & 0x0FU) | (gong ? 0x00U : 0x40U)); + return (sul_frame_t){ .id = PACKET3_ID, .bus = 0, .p_data = s_data, .len = 8U }; +} + +static sul_frame_t make_packet4(bool overload, bool seismic) +{ + static uint8_t s_data[8]; + memset(s_data, 0, sizeof(s_data)); + s_data[5] = overload ? 0x40U : 0x00U; /* WEIGHT_MASK */ + s_data[0] = seismic ? 0x80U : 0x00U; /* SEISMIC_MASK */ + return (sul_frame_t){ .id = PACKET4_ID, .bus = 0, .p_data = s_data, .len = 8U }; +} + +static sul_frame_t make_packet5(uint8_t next_left, uint8_t next_right, uint8_t dest_level) +{ + static uint8_t s_data[8]; + memset(s_data, 0, sizeof(s_data)); + s_data[0] = dest_level; + s_data[3] = next_left; + s_data[4] = next_right; + return (sul_frame_t){ .id = PACKET5_ID, .bus = 0, .p_data = s_data, .len = 8U }; } /* ── PACKET1 — направление ───────────────────────────────────────────── */ @@ -45,9 +105,9 @@ static void test_packet1_none(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - const sul_frame_t frame = make_packet1(0U); + const sul_frame_t FRAME = make_packet1(0U); - TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &FRAME, &out)); TEST_ASSERT_EQUAL(SUL_DIR_NONE, out.direction); } @@ -56,9 +116,9 @@ static void test_packet1_up(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - const sul_frame_t frame = make_packet1(1U); + const sul_frame_t FRAME = make_packet1(1U); - TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &FRAME, &out)); TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); } @@ -67,9 +127,9 @@ static void test_packet1_down(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - const sul_frame_t frame = make_packet1(2U); + const sul_frame_t FRAME = make_packet1(2U); - TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &FRAME, &out)); TEST_ASSERT_EQUAL(SUL_DIR_DOWN, out.direction); } @@ -78,9 +138,9 @@ static void test_packet1_double(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - const sul_frame_t frame = make_packet1(3U); + const sul_frame_t FRAME = make_packet1(3U); - TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &FRAME, &out)); TEST_ASSERT_EQUAL(SUL_DIR_DOUBLE, out.direction); } @@ -90,9 +150,9 @@ static void test_packet1_ignores_bits_outside_mask(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - const sul_frame_t frame = make_packet1(0xFCU | 1U); /* мусор в старших битах + arrow=1 */ + const sul_frame_t FRAME = make_packet1(0xFCU | 1U); /* мусор в старших битах + arrow=1 */ - TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &FRAME, &out)); TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); } @@ -103,9 +163,9 @@ static void test_packet3_two_digit_floor(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - const sul_frame_t frame = make_packet3(1U, 2U); /* "1","2" -> "12" */ + const sul_frame_t FRAME = make_packet3(1U, 2U); /* "1","2" -> "12" */ - TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &FRAME, &out)); TEST_ASSERT_EQUAL_STRING("12", out.pos); } @@ -114,9 +174,9 @@ static void test_packet3_single_digit_via_space(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - const sul_frame_t frame = make_packet3(16U, 5U); /* left=SPACE, right="5" -> "5" */ + const sul_frame_t FRAME = make_packet3(16U, 5U); /* left=SPACE, right="5" -> "5" */ - TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &FRAME, &out)); TEST_ASSERT_EQUAL_STRING("5", out.pos); } @@ -171,11 +231,11 @@ static void test_unknown_id_is_ignored_and_out_untouched(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - out.direction = SUL_DIR_UP; /* сентинел, decode() не должен его тронуть */ + out.direction = SUL_DIR_UP; /* сентинел, decode() не должен его тронуть */ (void) memset(out.pos, 'X', sizeof(out.pos)); - uint8_t data[8] = {0}; - const sul_frame_t frame = {.id = UNKNOWN_ID, .bus = 0, .p_data = data, .len = 8U}; + uint8_t data[8] = { 0 }; + const sul_frame_t frame = { .id = UNKNOWN_ID, .bus = 0, .p_data = data, .len = 8U }; TEST_ASSERT_EQUAL(SUL_STATUS_IGNORED, nku_can_decode(&ctx, &frame, &out)); TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); /* не тронут */ @@ -187,8 +247,8 @@ static void test_wrong_dlc_on_packet1_id_is_error(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - uint8_t data[8] = {0}; - const sul_frame_t frame = {.id = PACKET1_ID, .bus = 0, .p_data = data, .len = 6U}; + uint8_t data[8] = { 0 }; + const sul_frame_t frame = { .id = PACKET1_ID, .bus = 0, .p_data = data, .len = 6U }; TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &frame, &out)); } @@ -198,8 +258,8 @@ static void test_wrong_dlc_on_packet3_id_is_error(void) nku_can_ctx_t ctx; nku_can_init(&ctx); sul_result_t out; - uint8_t data[8] = {0}; - const sul_frame_t frame = {.id = PACKET3_ID, .bus = 0, .p_data = data, .len = 3U}; + uint8_t data[8] = { 0 }; + const sul_frame_t frame = { .id = PACKET3_ID, .bus = 0, .p_data = data, .len = 3U }; TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &frame, &out)); } @@ -234,6 +294,289 @@ static void test_init_resets_to_default(void) TEST_ASSERT_EQUAL(SUL_DIR_NONE, ctx.state.direction); } +/* ── PACKET1 — режимы (код нибла data[6][7:4]) и начало движения ──────── */ + +static void test_packet1_mode_fire(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + const sul_frame_t frame = make_packet1_full(0U, 0U, 0x70U, 0U); + + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_TRUE(out.fire_alarm); +} + +static void test_packet1_mode_maintenance_variants(void) +{ + const uint8_t icons[] = { 0x30U, 0x50U, 0x40U }; /* МП1 / МП2 / ревизия */ + for (size_t i = 0U; i < sizeof(icons); ++i) + { + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + const sul_frame_t frame = make_packet1_full(0U, 0U, icons[i], 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_TRUE(out.maintenance); + } +} + +static void test_packet1_mode_lading_instrument(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + const sul_frame_t frame = make_packet1_full(0U, 0U, 0x10U, 0U); + + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_TRUE(out.lading); +} + +static void test_packet1_mode_fireman(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + const sul_frame_t frame = make_packet1_full(0U, 0U, 0xF0U, 0U); + + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_TRUE(out.fireman); +} + +/* Пакет без кода режима гасит устаревший режим (PACKET1-владелец). */ +static void test_packet1_normal_clears_previous_mode(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t fire = make_packet1_full(0U, 0U, 0x70U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &fire, &out)); + TEST_ASSERT_TRUE(out.fire_alarm); + + const sul_frame_t normal = make_packet1_full(1U, 0U, 0x00U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &normal, &out)); + TEST_ASSERT_FALSE(out.fire_alarm); +} + +static void test_packet1_movement_bits(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t moving = make_packet1_full(1U, 1U, 0x00U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &moving, &out)); + TEST_ASSERT_TRUE(out.movement); + + const sul_frame_t still = make_packet1_full(1U, 0U, 0x00U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &still, &out)); + TEST_ASSERT_FALSE(out.movement); +} + +/* ── PACKET2 / PACKET4 — перегруз (мультиисточник) и сейсмо ───────────── */ + +static void test_packet2_overload_set_and_clear(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t on = make_packet2(true); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &on, &out)); + TEST_ASSERT_TRUE(out.overload); + + const sul_frame_t off = make_packet2(false); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &off, &out)); + TEST_ASSERT_FALSE(out.overload); +} + +/* Ключевой мультиисточник: перегруз из PACKET4 не должен сбрасываться + * пакетом PACKET2 без перегруза (и наоборот) — выход = OR под-источников. */ +static void test_overload_multisource_independence(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t p4_on = make_packet4(true, false); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &p4_on, &out)); + TEST_ASSERT_TRUE(out.overload); + + const sul_frame_t p2_off = make_packet2(false); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &p2_off, &out)); + TEST_ASSERT_TRUE(out.overload); /* PACKET4-перегруз пережил PACKET2-без-перегруза */ + + const sul_frame_t p4_off = make_packet4(false, false); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &p4_off, &out)); + TEST_ASSERT_FALSE(out.overload); /* оба источника сняты */ +} + +static void test_packet4_seismic_set_and_clear(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t on = make_packet4(false, true); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &on, &out)); + TEST_ASSERT_TRUE(out.seismic); + + const sul_frame_t off = make_packet4(false, false); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &off, &out)); + TEST_ASSERT_FALSE(out.seismic); +} + +/* ── PACKET3 — гонг, временная погрузка, floor_num ───────────────────── */ + +static void test_packet3_gong_active_when_bit_clear(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t gong = make_packet3_full(1U, 2U, true, 0U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &gong, &out)); + TEST_ASSERT_TRUE(out.arrival); + + const sul_frame_t no_gong = make_packet3_full(1U, 2U, false, 0U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &no_gong, &out)); + TEST_ASSERT_FALSE(out.arrival); +} + +static void test_packet3_lading_time_seconds(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t frame = make_packet3_full(1U, 2U, false, 30U, 1U); /* 1*60+30 */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out)); + TEST_ASSERT_EQUAL_UINT16(90U, out.lading_secs); + TEST_ASSERT_TRUE(out.lading); /* временная погрузка тоже поднимает lading */ +} + +/* Мультиисточник lading: временная (PACKET3) держит режим, даже когда PACKET1 + * не несёт инструментальной; и наоборот. */ +static void test_lading_multisource_independence(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + /* Временная погрузка идёт, PACKET1 без режима — lading остаётся. */ + const sul_frame_t timed = make_packet3_full(1U, 2U, false, 10U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &timed, &out)); + TEST_ASSERT_TRUE(out.lading); + + const sul_frame_t p1_normal = make_packet1_full(0U, 0U, 0x00U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &p1_normal, &out)); + TEST_ASSERT_TRUE(out.lading); /* временная погрузка пережила «нормальный» PACKET1 */ + + /* Отсчёт истёк — lading гаснет. */ + const sul_frame_t timed_zero = make_packet3_full(1U, 2U, false, 0U, 0U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &timed_zero, &out)); + TEST_ASSERT_FALSE(out.lading); +} + +static void test_packet3_floor_num_derivation(void) +{ + nku_can_ctx_t ctx; + sul_result_t out; + + nku_can_init(&ctx); + const sul_frame_t two_digit = make_packet3_full(1U, 2U, false, 0U, 0U); /* "12" */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &two_digit, &out)); + TEST_ASSERT_EQUAL_UINT8(12U, out.floor_num); + + nku_can_init(&ctx); + const sul_frame_t negative = make_packet3_full(22U, 1U, false, 0U, 0U); /* "-1" -> 41 */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &negative, &out)); + TEST_ASSERT_EQUAL_UINT8(41U, out.floor_num); + + nku_can_init(&ctx); + const sul_frame_t basement = make_packet3_full(17U, 1U, false, 0U, 0U); /* "П1" -> 51 */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &basement, &out)); + TEST_ASSERT_EQUAL_UINT8(51U, out.floor_num); +} + +/* ── PACKET5 — следующий этаж (гейт: только пока едет и назначение ≠ уровень) */ + +static void test_packet5_next_shown_while_moving(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t moving = make_packet1_full(1U, 1U, 0x00U, 5U); /* UP, уровень 5 */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &moving, &out)); + + const sul_frame_t next = make_packet5(1U, 6U, 16U); /* "16", назначение 16 ≠ 5 */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &next, &out)); + TEST_ASSERT_EQUAL_STRING("16", out.next); +} + +static void test_packet5_next_suppressed_when_not_moving(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t still = make_packet1_full(0U, 0U, 0x00U, 5U); /* нет движения */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &still, &out)); + + const sul_frame_t next = make_packet5(1U, 6U, 16U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &next, &out)); + TEST_ASSERT_EQUAL_STRING("", out.next); +} + +static void test_packet5_next_suppressed_when_dest_equals_level(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t moving = make_packet1_full(1U, 1U, 0x00U, 5U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &moving, &out)); + + const sul_frame_t next = make_packet5(1U, 6U, 5U); /* назначение == уровень */ + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &next, &out)); + TEST_ASSERT_EQUAL_STRING("", out.next); +} + +/* Следующий этаж гаснет на прибытии (PACKET1 сообщает «нет движения»). */ +static void test_packet5_next_cleared_on_arrival(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + + const sul_frame_t moving = make_packet1_full(1U, 1U, 0x00U, 5U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &moving, &out)); + const sul_frame_t next = make_packet5(1U, 6U, 16U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &next, &out)); + TEST_ASSERT_EQUAL_STRING("16", out.next); + + const sul_frame_t arrived = make_packet1_full(0U, 0U, 0x00U, 16U); + TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &arrived, &out)); + TEST_ASSERT_EQUAL_STRING("", out.next); +} + +static void test_wrong_dlc_on_packet2_4_5_is_error(void) +{ + nku_can_ctx_t ctx; + nku_can_init(&ctx); + sul_result_t out; + uint8_t data[8] = { 0 }; + + const sul_frame_t f2 = { .id = PACKET2_ID, .bus = 0, .p_data = data, .len = 6U }; + TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &f2, &out)); + const sul_frame_t f4 = { .id = PACKET4_ID, .bus = 0, .p_data = data, .len = 7U }; + TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &f4, &out)); + const sul_frame_t f5 = { .id = PACKET5_ID, .bus = 0, .p_data = data, .len = 4U }; + TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &f5, &out)); +} + int main(void) { UNITY_BEGIN(); @@ -258,5 +601,28 @@ int main(void) RUN_TEST(test_state_accumulates_across_packet_types); RUN_TEST(test_init_resets_to_default); + RUN_TEST(test_packet1_mode_fire); + RUN_TEST(test_packet1_mode_maintenance_variants); + RUN_TEST(test_packet1_mode_lading_instrument); + RUN_TEST(test_packet1_mode_fireman); + RUN_TEST(test_packet1_normal_clears_previous_mode); + RUN_TEST(test_packet1_movement_bits); + + RUN_TEST(test_packet2_overload_set_and_clear); + RUN_TEST(test_overload_multisource_independence); + RUN_TEST(test_packet4_seismic_set_and_clear); + + RUN_TEST(test_packet3_gong_active_when_bit_clear); + RUN_TEST(test_packet3_lading_time_seconds); + RUN_TEST(test_lading_multisource_independence); + RUN_TEST(test_packet3_floor_num_derivation); + + RUN_TEST(test_packet5_next_shown_while_moving); + RUN_TEST(test_packet5_next_suppressed_when_not_moving); + RUN_TEST(test_packet5_next_suppressed_when_dest_equals_level); + RUN_TEST(test_packet5_next_cleared_on_arrival); + + RUN_TEST(test_wrong_dlc_on_packet2_4_5_is_error); + return UNITY_END(); }