# tft_app: Phase 2

This commit is contained in:
Dmitry Akimov 2026-07-21 15:38:59 +03:00
parent 7f3170a7c0
commit 0482c08272
20 changed files with 1692 additions and 110 deletions

View file

@ -0,0 +1,185 @@
# tft-app — путь данных домена (транспорт → экран)
Документ описывает **реализованный** доменный слой (Фазы 12): как кадр от СУЛ проходит
от шины до презентации, какие модули за что отвечают и каков контракт между ними. Проектное
обоснование слоёв — в [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<br/>приём CAN-кадров"]
end
subgraph L1["L1 · транспорт (HW-адаптер)"]
TR["sul/transport/can<br/>bsp_can_frame_t → sul_frame_t"]
end
subgraph L2["L2 · домен (чистый C, host-тесты)"]
REG["sul (реестр)<br/>sul_registry_active()"]
DEC["sul/nku_can<br/>чистый декодер PACKET1..5"]
MODEL["elevator_model<br/>sul_result_t, sul_mode_t"]
CTRL["controller<br/>diff + свёртка режима"]
PRIO["mode_priority<br/>таблица приоритетов"]
end
subgraph L3["L3 · презентация"]
UI["ui/fallback<br/>этаж/стрелка/метка режима"]
AUD["audio_policy<br/>(Фаза 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<br/>направление, режим"] --> ST["nku_can_ctx_t.state<br/>(полный sul_result_t)"]
P3["PACKET3<br/>позиция, гонг, погрузка"] --> ST
P4["PACKET4<br/>перегруз, сейсмо"] --> 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 со спрайтами и звук — Фазы 46 ([PLAN.md](../../firmware/tft_app/PLAN.md)).

98
docs/tft_app/FALLBACK.md Normal file
View file

@ -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)<br/>ПОЖАР/ПЕРЕГРУЗ/СЕРВИС/…"]
LBL --> LAD{"погрузка и lading_secs>0?"}
LAD -->|да| CNT["обратный отсчёт (крупно)"]
LAD -->|нет| DONE
CNT --> DONE
```
- **Обычный режим** — номер этажа (`FloorFontFallback`) + стрелка вверх/вниз (примитив, без
спрайтов). Двойная стрелка (`SUL_DIR_DOUBLE`) в fallback не рисуется — это спрайтовая индикация
Фазы 4/5.
- **Спецрежим** — короткая текстовая метка системным шрифтом (`SystemFont`). Это **safe-mode**
индикация: богатая полноэкранная графика режимов (фон+спрайты) появится в Фазах 45. Для
временной погрузки дополнительно рисуется обратный отсчёт (`lading_secs`).
Следующий этаж (`next`) в fallback **не** показывается — это элемент богатого layout (Фаза 5),
поэтому на `next_pending` перерисовки нет.
---
## 3. По-символьный fallback шрифта
Второй уровень деградации — на уровне отдельного символа. Активный шрифт может не покрывать
кодпойнт, который выдал декодер (напр. `FloorFontFallback` знает только `09`, `-`, пробел, а
позиция пришла кириллицей). `gfx_draw_string()` подставляет `-` вместо отсутствующего глифа —
по-символьно, не обрывая всю строку на первом неизвестном символе. Если даже `-` нет в шрифте —
символ пропускается (нулевая ширина).
```mermaid
flowchart LR
CH["символ строки"] --> HAS{"есть глиф<br/>в активном шрифте?"}
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).

View file

@ -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<br/>(ровно один режим)"]
```
---
## 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:<br/>сигнал по 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).

23
docs/tft_app/README.md Normal file
View file

@ -0,0 +1,23 @@
# tft-app — документация домена
Dev-документация по **реализованному** доменному слою `firmware/tft_app` (Фазы 12): как данные
СУЛ проходят от протокола до экрана. Технические доки «как это работает в коде», в дополнение к
проектным 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-движок со спрайтами (Фазы 45), аудио (Фаза 6), мультипротокол (Фаза 8). Удалённая
установка адреса НКУ-CAN отложена в Фазу 3.

View file

@ -266,6 +266,21 @@ layout- и assets-регионы — **вне** flash-area загрузчика
- **HIL:** gfx/PXP/ELCDIF, audio/MQS, SD/QSPI, реальные транспорты CAN/UART (по образцу - **HIL:** gfx/PXP/ELCDIF, audio/MQS, SD/QSPI, реальные транспорты CAN/UART (по образцу
`tests/target/*` и `06_test_firmware_*`). `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 — низкий риск. | Без фикса |
--- ---
## Приложение. Заимствования из проектов ## Приложение. Заимствования из проектов

View file

@ -17,7 +17,7 @@
|---|---|---| |---|---|---|
| 0 | Каркас проекта и сборка | ✅ | | 0 | Каркас проекта и сборка | ✅ |
| 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ✅ | | 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ✅ |
| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | | | 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | 🟨 |
| 3 | Настройки и меню, локальные входы (opto) | ⬜ | | 3 | Настройки и меню, локальные входы (opto) | ⬜ |
| 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ | | 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ |
| 5 | Layout-движок: схема, солвер, темы, injected-регион | ⬜ | | 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), таблица приоритетов режимов, устойчивый **Цель.** `sul_result_t` в полном объёме (§6 ARCH), таблица приоритетов режимов, устойчивый
таймаут/потеря связи. таймаут/потеря связи.
@ -203,6 +238,11 @@ marks_both_pending`, аппаратно на реальном обрыве св
**Объём.** `services/settings_store` (магик/версия/CRC, сектор настроек §10); дескрипторы **Объём.** `services/settings_store` (магик/версия/CRC, сектор настроек §10); дескрипторы
per-protocol настроек (§8); `menu/` (навигация, редактирование, привязка к дескрипторам); per-protocol настроек (§8); `menu/` (навигация, редактирование, привязка к дескрипторам);
`bsp_button` + `bsp_opto` (диспетчерский вызов/ответ) → контроллер/презентация. `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 настроек; логика навигации меню; связывание **Тесты (host).** Сериализация/дефолты/CRC настроек; логика навигации меню; связывание
дескрипторов. дескрипторов.
@ -311,3 +351,7 @@ per-protocol дескрипторы настроек; клиентские та
- Формат исходника layout для host-утилиты (JSON/TOML) и позже GUI поверх компилятора. (Фаза 5) - Формат исходника layout для host-утилиты (JSON/TOML) и позже GUI поверх компилятора. (Фаза 5)
- Renderable-множество символов на протокол vs общий шрифт; политика по-символьного fallback. (Фаза 2) - Renderable-множество символов на протокол vs общий шрифт; политика по-символьного fallback. (Фаза 2)
- Звук в бандле: WAV как есть или препарсенный PCM. (Фаза 6) - Звук в бандле: WAV как есть или препарсенный PCM. (Фаза 6)
- **Каталог доменных точек логгирования** (что / где / уровень: декодер, контроллер, транспорт,
смена режима, таймаут связи, …) — согласовать **совместно**, затем расставить **одним
выделенным проходом** по готовому домену. Инфраструктура готова (`utils/log` + `port/log_uart`,
компайл-тайм `LOG_LEVEL`). Рантайм-тумблер в меню — Фаза 3 (см. её объём).

189
firmware/tft_app/REPORT.md Normal file
View file

@ -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 — отчёт по Фазам 01
> Канонические источники — `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/<module>`)
```
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), или оставляете как есть?

View file

@ -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/) target_include_directories(tft_app_controller PUBLIC include/)

View file

@ -30,12 +30,24 @@ typedef struct
/** /**
* @brief Что изменилось с прошлого вызова presentation перерисовывает * @brief Что изменилось с прошлого вызова presentation перерисовывает
* только помеченные поля. * только помеченные поля, audio_policy (Фаза 6) озвучивает изменившиеся
* события поверх этого diff.
*
* mode_pending сменился РАЗРЕШЁННЫЙ экранный режим (свёртка сигналов по
* таблице приоритетов, §7), а не отдельный сырой сигнал. arrival/movement
* уровни из декодера; их edge (falsetrue) сигнал к озвучке (гонг, старт).
*/ */
typedef struct typedef struct
{ {
bool pos_pending; bool pos_pending;
bool next_pending;
bool direction_pending; bool direction_pending;
bool mode_pending;
bool arrival_pending;
bool movement_pending;
sul_mode_t mode; /**< разрешённый режим нового результата — presentation
читает его при mode_pending (не нужен отдельный getter) */
} indication_task_t; } indication_task_t;
/** /**

View file

@ -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_ */

View file

@ -1,5 +1,7 @@
#include "domain/controller.h" #include "domain/controller.h"
#include "domain/mode_priority.h"
#include <string.h> #include <string.h>
void controller_init(controller_ctx_t *p_ctx) 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) 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), .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), .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; p_ctx->cache = *p_result;
return task; return TASK;
} }

View file

@ -0,0 +1,52 @@
#include "domain/mode_priority.h"
#include <stddef.h>
/**
* @brief Строка таблицы приоритетов: булев сигнал (по смещению в sul_result_t)
* экранный режим.
*
* Смещение вместо указателя-на-член (в C его нет) читаем bool по
* offsetof, что делает таблицу голыми данными без кода на строку.
*/
typedef struct
{
size_t flag_offset; /**< offsetof(sul_result_t, <bool-поле>) */
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;
}

View file

@ -7,14 +7,17 @@
* (НКУ-CAN, УИМ, SD7, УЭЛ, УКЛ, ); конкретный декодер заполняет то, что несёт * (НКУ-CAN, УИМ, SD7, УЭЛ, УКЛ, ); конкретный декодер заполняет то, что несёт
* его протокол, остальное остаётся дефолтным. * его протокол, остальное остаётся дефолтным.
* *
* Фаза 1 подмножество: `pos` + `direction` (см. PLAN.md). Остальные поля * Обзор боевых декодеров (NKU-CAN, UIM, UKL, UEL, SD7) подтвердил: набор
* ARCH §6 (next, сигналы, lading_secs, floor_num) добавляются в Фазе 2 вместе * сигналов ниже покрывает все протоколы различаются лишь битовые кодировки
* с полным декодером НКУ-CAN и таблицей приоритетов режимов. * (живут в каждом декодере), не сам словарь сигналов. Провиженинг-концепты
* (напр. `cop_mode`/`display_id`) это настройки устройства (§8), НЕ данные
* СУЛ, и в этой структуре им не место.
*/ */
#ifndef DOMAIN_ELEVATOR_MODEL_H_ #ifndef DOMAIN_ELEVATOR_MODEL_H_
#define DOMAIN_ELEVATOR_MODEL_H_ #define DOMAIN_ELEVATOR_MODEL_H_
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
@ -45,25 +48,67 @@ typedef enum
} sul_direction_t; } 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 + кириллица), не число: * Позиция UTF-8 строка (реальные шрифты ASCII + кириллица), не число:
* универсально для всех протоколов, host-тест сравнивает строки напрямую. * универсально для всех протоколов, host-тест сравнивает строки напрямую.
* Декодер ничего не знает про шрифт/рендер валидация рендеримости * Декодер ничего не знает про шрифт/рендер валидация рендеримости
* (покрытие активным шрифтом) задача UI-слоя (ARCH §11), не decode(). * (покрытие активным шрифтом) задача UI-слоя (ARCH §11), не decode().
*
* Сигналы ОРТОГОНАЛЬНЫ (могут сосуществовать) в один экранный режим их
* сводит controller по таблице приоритетов (§7), не декодер. Декодер выдаёт
* УРОВНИ (сигнал активен сейчас), а не события: edge-детекцию (гонгзвук,
* начало движениязвук) делает presentation/audio_policy поверх diff
* контроллера (ARCH §4, Фаза 6).
*/ */
typedef struct 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; 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; } sul_result_t;
/** /**
* @brief Состояние по умолчанию / при потере связи. * @brief Состояние по умолчанию / при потере связи.
* *
* pos="--" (нет данных), direction=SUL_DIR_NONE. Вызывающий (контроллер через * pos="--" (нет данных), next="", direction=SUL_DIR_NONE, все сигналы false,
* sul_rx-задачу при таймауте, либо init перед первым кадром) использует это * lading_secs=0, floor_num=0. Вызывающий (контроллер через sul_rx-задачу при
* как отправную точку не HAL-зависимо, чистая функция. * таймауте, либо init перед первым кадром) использует это как отправную точку
* не HAL-зависимо, чистая функция.
*/ */
sul_result_t sul_default_state(void); sul_result_t sul_default_state(void);

View file

@ -2,9 +2,11 @@
* @file nku_can.h * @file nku_can.h
* @brief Чистый декодер протокола НКУ-CAN (порт из OLD_PROJECT msg_receiver_task). * @brief Чистый декодер протокола НКУ-CAN (порт из OLD_PROJECT msg_receiver_task).
* *
* Фаза 1 подмножество: PACKET1 (направление) + PACKET3 (позиция кабины). * Фаза 2 полный разбор индикации: PACKET1 (направление, режимы, начало
* Остальные пакеты (PACKET2/4/5, режимы, гонг, погрузка, удалённая установка * движения), PACKET2 (перегруз), PACKET3 (позиция, гонг, временная погрузка),
* адреса) Фаза 2, см. PLAN.md. * PACKET4 (перегруз-вариант, сейсмо), PACKET5 (следующий этаж). Удалённая
* установка адреса (0x4X1/0x5XB) Фаза 3 (нужен settings_store для записи);
* здесь эти кадры игнорируются как чужие ID.
* *
* Адрес станции (nku_address) захардкожен в 0 фильтры/ID без сдвига группы. * Адрес станции (nku_address) захардкожен в 0 фильтры/ID без сдвига группы.
* Фаза 3 параметризует через настройки; сигнатура decode() не изменится. * Фаза 3 параметризует через настройки; сигнатура decode() не изменится.
@ -25,19 +27,30 @@ extern "C"
#endif #endif
/** /**
* @brief Состояние декодера накопленный текущий sul_result_t. * @brief Состояние декодера накопленный текущий sul_result_t + внутренние
* латчи для полей с несколькими источниками.
* *
* PACKET1 и PACKET3 несут РАЗНЫЕ поля (направление / позицию) в разных * Разные пакеты несут РАЗНЫЕ поля в разных кадрах decode() обновляет только
* кадрах decode() обновляет только своё поле в state и отдаёт наружу * пришедшее и отдаёт наружу ПОЛНУЮ накопленную копию state, а не дельту.
* ПОЛНУЮ накопленную копию, а не только то, что пришло в этом кадре.
* *
* Фаза 2 добавит сюда edge-detection состояние для гонга/погрузки/режимов * Некоторые ВЫХОДНЫЕ поля кормятся НЕСКОЛЬКИМИ пакетами и должны быть их OR,
* (аналог event_edge_t в legacy) структура специально не голый sul_result_t, * иначе пакет-без-сигнала сбросил бы флаг, выставленный другим пакетом (боевой
* чтобы не менять сигнатуру при расширении. * баг-класс из 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 typedef struct
{ {
sul_result_t state; 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; } nku_can_ctx_t;
/** Сброс к состоянию по умолчанию (sul_default_state()). Вызвать перед первым decode(). */ /** Сброс к состоянию по умолчанию (sul_default_state()). Вызвать перед первым decode(). */

View file

@ -3,19 +3,46 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
/* Фаза 1: адрес станции захардкожен в 0 — базовые ID без сдвига группы /* Адрес станции захардкожен в 0 (Фаза 1/2) — базовые ID без сдвига группы
* (легаси: 0x506|group4, group4 = nku_address<<4; для address=0 group4=0). * (легаси: 0x506|group4, group4 = nku_address<<4; для address=0 group4=0;
* Фаза 3 параметризует через настройки. */ * PACKET5 group6 = nku_address<<6). Фаза 3 параметризует через настройки. */
#define PACKET1_ID 0x506U #define PACKET1_ID 0x506U /* направление, режимы, начало движения, двери */
#define PACKET3_ID 0x508U #define PACKET2_ID 0x408U /* перегруз (вариант 1) */
#define PACKET3_ID 0x508U /* позиция кабины, гонг, временная погрузка */
#define PACKET4_ID 0x50BU /* перегруз (вариант 2), сейсмоопасность */
#define PACKET5_ID 0x606U /* следующий этаж */
#define PROTO_DLC 8U #define PROTO_DLC 8U
#define ARROW_MASK 0x03U #define ARROW_MASK 0x03U /* PACKET1 data[6][1:0] — стрелка */
#define FLOOR_MASK 0x3FU #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). Здесь намеренно не разбираются. */
/* Коды режима в нибле 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_SPACE 16U
#define SYMBOL_A 10U
#define SYMBOL_P 17U /* "П" */
#define SYMBOL_p 19U /* "п" */
#define SYMBOL_HYPHEN 22U /* "-" */
#define SYMBOL_TOTAL 38U #define SYMBOL_TOTAL 38U
#define FLOOR_NUM_UNKNOWN 60U /* «н/д» для озвучки — как в legacy floor_number_parser */
/* Таблица символов НКУ-CAN — порт из OLD_PROJECT floor_string_composer() /* Таблица символов НКУ-CAN — порт из OLD_PROJECT floor_string_composer()
* (source/main_programm.c). Индекс код символа с шины (байт & FLOOR_MASK). */ * (source/main_programm.c). Индекс код символа с шины (байт & FLOOR_MASK). */
static const char *const S_SYMBOL_TABLE[SYMBOL_TOTAL] = { static const char *const S_SYMBOL_TABLE[SYMBOL_TOTAL] = {
@ -26,59 +53,173 @@ static const char *const S_SYMBOL_TABLE[SYMBOL_TOTAL] = {
void nku_can_init(nku_can_ctx_t *p_ctx) 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=двойная стрелка. * Порт floor_string_composer(): left==SPACE или left==0 однозначный этаж
* Легаси (msg_receiver_task, PACKET1) обрабатывал только 0/1/2 case 3 * (только правый символ; станция шлёт 0x00 как «нет левого символа», трактуем
* отсутствовал (направление молча не менялось). 3 = «двойная стрелка» * как пробел). Возврат false код вне таблицы (малформированный кадр).
* (спецрежим индикации, словарь special/DisplayArrowIcon) уточнено отдельно. */
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, прямое * Стандартные (0..40): left*10+right (или только right при пробеле).
* приведение типа корректно без switch/default. * Отрицательные ("-" слева): 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) 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.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) порт * @return false код символа позиции вне таблицы (малформированный кадр).
* floor_string_composer() из OLD_PROJECT. left==SPACE или left==0
* однозначный этаж, выводится только правый символ (легаси трактует байт
* 0x00 так же, как пробел станция может слать нулевой байт вместо явного
* кода пробела для «нет левого символа»; порт без изменений поведения).
*
* В отличие от легаси (молча оставляет буфер как есть при выходе за
* SYMBOL_TOTAL маска даёт до 63 сырых значений при 38 валидных символах),
* здесь это ошибка (false) не полагаемся на предыдущее содержимое буфера.
*
* @return false left/right вне таблицы символов (малформированный кадр).
*/ */
static bool decode_packet3(nku_can_ctx_t *p_ctx, const uint8_t *p_data) 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 LEFT = p_data[5] & FLOOR_MASK;
const uint8_t RIGHT = p_data[6] & 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; 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]); return compose_chars(p_data[3] & FLOOR_MASK, p_data[4] & FLOOR_MASK, p_ctx->state.next);
}
else
{
(void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s%s", S_SYMBOL_TABLE[LEFT],
S_SYMBOL_TABLE[RIGHT]);
} }
p_ctx->state.next[0] = '\0';
return true; 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; 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) if (p_frame->len != PROTO_DLC)
{ {
return SUL_STATUS_ERR; return SUL_STATUS_ERR;
} }
decode_packet1(p_state, p_frame->p_data); decode_packet1(p_state, p_frame->p_data);
*p_out = p_state->state; break;
return SUL_STATUS_OK;
}
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) if (p_frame->len != PROTO_DLC)
{ {
return SUL_STATUS_ERR; 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; return SUL_STATUS_ERR;
} }
*p_out = p_state->state; break;
return SUL_STATUS_OK;
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;
} }

View file

@ -50,7 +50,15 @@ typedef struct
/* Компилируемые в прошивку шрифты (services/gfx/fonts/, сгенерированы /* Компилируемые в прошивку шрифты (services/gfx/fonts/, сгенерированы
* lcd-image-converter пользователем). */ * lcd-image-converter пользователем). */
extern const tFont FloorFontFallback; /* 0-9, "-", пробел — fallback (§11 ARCH) */ 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-байт значения не имеет. */ /** XRGB8888 (X игнорируется ELCDIF) — X-байт значения не имеет. */
typedef uint32_t gfx_color_t; typedef uint32_t gfx_color_t;

View file

@ -1,8 +1,11 @@
#include "ui/fallback.h" #include "ui/fallback.h"
#include "domain/mode_priority.h"
#include "services/gfx.h" #include "services/gfx.h"
/* Фаза 1: позиции захардкожены под TFT8 (800×600, panel текущего стенда). #include <stdio.h>
/* Фаза 1/2: позиции захардкожены под TFT8 (800×600, panel текущего стенда).
* Цифра '0' FloorFontFallback 132×162 px (проверено по сгенерированным * Цифра '0' FloorFontFallback 132×162 px (проверено по сгенерированным
* данным шрифта). Фаза 5 (layout-движок) заменит на якорное позиционирование, * данным шрифта). Фаза 5 (layout-движок) заменит на якорное позиционирование,
* независимое от разрешения панели. */ * независимое от разрешения панели. */
@ -10,17 +13,43 @@
#define POS_Y 150U #define POS_Y 150U
#define ARROW_Y 380U #define ARROW_Y 380U
#define ARROW_SIZE 80U #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); static void draw_centered(const tFont *p_font, const char *p_str, uint16_t y)
const uint16_t pos_x = (uint16_t) ((PANEL_WIDTH - str_width) / 2U); {
(void) gfx_draw_string(&FloorFontFallback, p_result->pos, pos_x, POS_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 — тоже (не путать с static void render_normal(const sul_result_t *p_result)
* up/down одной стрелкой; отдельная индикация Фаза 2). */ {
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)) 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; 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) 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) 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; return;
} }
render(p_result); render(p_task->mode, p_result);
} }

View file

@ -325,7 +325,7 @@ add_host_test(
tft_app_smoke/test_tft_app_smoke.c) 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) 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_controller/test_controller.c
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.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/controller.c
${TFT_APP_DOMAIN_DIR}/controller/src/mode_priority.c
INCLUDES INCLUDES
${TFT_APP_DOMAIN_DIR}/elevator_model/include ${TFT_APP_DOMAIN_DIR}/elevator_model/include
${TFT_APP_DOMAIN_DIR}/controller/include) ${TFT_APP_DOMAIN_DIR}/controller/include)

View file

@ -8,6 +8,7 @@
#include "unity.h" #include "unity.h"
#include "domain/controller.h" #include "domain/controller.h"
#include "domain/mode_priority.h"
#include <stdio.h> #include <stdio.h>
@ -109,6 +110,149 @@ static void test_feeding_default_state_after_real_data_marks_both_pending(void)
TEST_ASSERT_TRUE(task.direction_pending); 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) int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@ -120,5 +264,13 @@ int main(void)
RUN_TEST(test_only_direction_change_marks_only_direction_pending); 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_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(); return UNITY_END();
} }

View file

@ -6,14 +6,17 @@
* golden-векторы CAN-кадров строятся прямо в тесте. * golden-векторы CAN-кадров строятся прямо в тесте.
*/ */
#include "unity.h"
#include "domain/sul/nku_can.h" #include "domain/sul/nku_can.h"
#include "unity.h"
#include <string.h> #include <string.h>
void setUp(void) {} void setUp(void)
void tearDown(void) {} {
}
void tearDown(void)
{
}
/* ── Вспомогательные конструкторы кадров ─────────────────────────────── */ /* ── Вспомогательные конструкторы кадров ─────────────────────────────── */
@ -26,7 +29,7 @@ static sul_frame_t make_packet1(uint8_t arrow_bits)
static uint8_t s_data[8]; static uint8_t s_data[8];
memset(s_data, 0, sizeof(s_data)); memset(s_data, 0, sizeof(s_data));
s_data[6] = arrow_bits; /* ARROW_MASK=0x03 на data[6] (DATA7) */ 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) 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)); memset(s_data, 0, sizeof(s_data));
s_data[5] = left; /* FLOOR_MASK=0x3F на data[5] (left) */ s_data[5] = left; /* FLOOR_MASK=0x3F на data[5] (left) */
s_data[6] = right; /* FLOOR_MASK=0x3F на data[6] (right) */ 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 — направление ───────────────────────────────────────────── */ /* ── PACKET1 — направление ───────────────────────────────────────────── */
@ -45,9 +105,9 @@ static void test_packet1_none(void)
nku_can_ctx_t ctx; nku_can_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; 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); 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; 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); 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; 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); 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; 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); 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; 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); 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; 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); 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; 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); TEST_ASSERT_EQUAL_STRING("5", out.pos);
} }
@ -174,8 +234,8 @@ static void test_unknown_id_is_ignored_and_out_untouched(void)
out.direction = SUL_DIR_UP; /* сентинел, decode() не должен его тронуть */ out.direction = SUL_DIR_UP; /* сентинел, decode() не должен его тронуть */
(void) memset(out.pos, 'X', sizeof(out.pos)); (void) memset(out.pos, 'X', sizeof(out.pos));
uint8_t data[8] = {0}; uint8_t data[8] = { 0 };
const sul_frame_t frame = {.id = UNKNOWN_ID, .bus = 0, .p_data = data, .len = 8U}; 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_STATUS_IGNORED, nku_can_decode(&ctx, &frame, &out));
TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); /* не тронут */ 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; sul_result_t out;
uint8_t data[8] = {0}; uint8_t data[8] = { 0 };
const sul_frame_t frame = {.id = PACKET1_ID, .bus = 0, .p_data = data, .len = 6U}; 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)); 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_ctx_t ctx;
nku_can_init(&ctx); nku_can_init(&ctx);
sul_result_t out; sul_result_t out;
uint8_t data[8] = {0}; uint8_t data[8] = { 0 };
const sul_frame_t frame = {.id = PACKET3_ID, .bus = 0, .p_data = data, .len = 3U}; 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)); 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); 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) int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@ -258,5 +601,28 @@ int main(void)
RUN_TEST(test_state_accumulates_across_packet_types); RUN_TEST(test_state_accumulates_across_packet_types);
RUN_TEST(test_init_resets_to_default); 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(); return UNITY_END();
} }