# tft_app: Phase 2
This commit is contained in:
parent
7f3170a7c0
commit
0482c08272
20 changed files with 1692 additions and 110 deletions
185
docs/tft_app/DOMAIN_DATAFLOW.md
Normal file
185
docs/tft_app/DOMAIN_DATAFLOW.md
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
# tft-app — путь данных домена (транспорт → экран)
|
||||||
|
|
||||||
|
Документ описывает **реализованный** доменный слой (Фазы 1–2): как кадр от СУЛ проходит
|
||||||
|
от шины до презентации, какие модули за что отвечают и каков контракт между ними. Проектное
|
||||||
|
обоснование слоёв — в [ARCH.md](../../firmware/tft_app/ARCH.md); статус фаз —
|
||||||
|
в [PLAN.md](../../firmware/tft_app/PLAN.md). Здесь — «как это работает в коде сейчас».
|
||||||
|
|
||||||
|
Сейчас реализован один протокол — **НКУ-CAN**. Архитектура рассчитана на много протоколов
|
||||||
|
(УЭЛ/УКЛ/SD7/УИМ — Фаза 8): добавление протокола = новый декодер + запись в реестр, без правок
|
||||||
|
остальных слоёв.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Обзор слоёв
|
||||||
|
|
||||||
|
Каждый слой — отдельная статическая библиотека CMake. **Домен — чистый C без единого
|
||||||
|
HAL-вызова** (host-тестируется), железо изолировано в `bsp/*` и тонком транспорт-адаптере.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph L0["L0 · железо (bsp, не трогаем)"]
|
||||||
|
CAN["bsp_can<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 со спрайтами и звук — Фазы 4–6 ([PLAN.md](../../firmware/tft_app/PLAN.md)).
|
||||||
98
docs/tft_app/FALLBACK.md
Normal file
98
docs/tft_app/FALLBACK.md
Normal 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**
|
||||||
|
индикация: богатая полноэкранная графика режимов (фон+спрайты) появится в Фазах 4–5. Для
|
||||||
|
временной погрузки дополнительно рисуется обратный отсчёт (`lading_secs`).
|
||||||
|
|
||||||
|
Следующий этаж (`next`) в fallback **не** показывается — это элемент богатого layout (Фаза 5),
|
||||||
|
поэтому на `next_pending` перерисовки нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. По-символьный fallback шрифта
|
||||||
|
|
||||||
|
Второй уровень деградации — на уровне отдельного символа. Активный шрифт может не покрывать
|
||||||
|
кодпойнт, который выдал декодер (напр. `FloorFontFallback` знает только `0–9`, `-`, пробел, а
|
||||||
|
позиция пришла кириллицей). `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).
|
||||||
103
docs/tft_app/MODE_PRIORITY.md
Normal file
103
docs/tft_app/MODE_PRIORITY.md
Normal 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
23
docs/tft_app/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# tft-app — документация домена
|
||||||
|
|
||||||
|
Dev-документация по **реализованному** доменному слою `firmware/tft_app` (Фазы 1–2): как данные
|
||||||
|
СУЛ проходят от протокола до экрана. Технические доки «как это работает в коде», в дополнение к
|
||||||
|
проектным ARCH/PLAN.
|
||||||
|
|
||||||
|
| Документ | О чём |
|
||||||
|
| --- | --- |
|
||||||
|
| [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md) | Путь данных: транспорт → декодер → контроллер → презентация. Слои, контракт `decode()`, карта пакетов НКУ-CAN → поля модели, таймаут→default. |
|
||||||
|
| [MODE_PRIORITY.md](MODE_PRIORITY.md) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. |
|
||||||
|
| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, по-символьный fallback шрифта, триггеры перерисовки, ограничение single-buffer. |
|
||||||
|
|
||||||
|
## Связанные документы
|
||||||
|
|
||||||
|
- [ARCH.md](../../firmware/tft_app/ARCH.md) — архитектура и проектное обоснование (источник истины по дизайну).
|
||||||
|
- [PLAN.md](../../firmware/tft_app/PLAN.md) — статус фаз разработки (источник истины по статусу).
|
||||||
|
- [DEV_ARCH.md](../DEV_ARCH.md) — устройство репозитория и сборки.
|
||||||
|
|
||||||
|
## Границы
|
||||||
|
|
||||||
|
Описан домен и fallback-презентация. Ещё не реализовано (см. PLAN.md): настройки/меню (Фаза 3),
|
||||||
|
ассеты и layout-движок со спрайтами (Фазы 4–5), аудио (Фаза 6), мультипротокол (Фаза 8). Удалённая
|
||||||
|
установка адреса НКУ-CAN отложена в Фазу 3.
|
||||||
|
|
@ -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 — низкий риск. | Без фикса |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Приложение. Заимствования из проектов
|
## Приложение. Заимствования из проектов
|
||||||
|
|
|
||||||
|
|
@ -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
189
firmware/tft_app/REPORT.md
Normal 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 — отчёт по Фазам 0–1
|
||||||
|
|
||||||
|
> Канонические источники — `firmware/tft_app/ARCH.md` (архитектура) и `firmware/tft_app/PLAN.md`
|
||||||
|
> (статус фаз, живой документ, обновляется каждой фазой). Этот файл — снимок на момент завершения
|
||||||
|
> Фазы 1, для быстрого ввода в контекст новой сессии. При расхождении — верить PLAN.md/ARCH.md, не
|
||||||
|
> этому файлу.
|
||||||
|
|
||||||
|
**Статус:** Фаза 0 ✅ и Фаза 1 ✅ — обе аппаратно подтверждены. Работа Фазы 1 **не закоммичена**
|
||||||
|
(в рабочем дереве поверх коммита `# tft_app: Phase 0`, `a9e6681`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 0 — Каркас (FreeRTOS + self-confirm слота MCUboot)
|
||||||
|
|
||||||
|
**Итог.** `firmware/tft_app` собирается как MCUboot-слот-образ (Direct-XIP), bootloader в него
|
||||||
|
прыгает, FreeRTOS стартует, образ подтверждает себя в рантайме (`boot_set_next`) и переживает
|
||||||
|
power cycle без отката — anti-brick сохранён. Подтверждено дважды на реальном железе (в т.ч.
|
||||||
|
через переустановку с microSD).
|
||||||
|
|
||||||
|
**Путь до рабочего состояния был не прямым — три независимых бага, все найдены и закрыты:**
|
||||||
|
|
||||||
|
1. **`bsp_qspi_init()` обязателен до любого `flash_area_*`.** Без него запись `image_ok` в трейлер
|
||||||
|
слота молча падала — `boot_set_next()` возвращал успех, но ничего не персистило.
|
||||||
|
2. **Корневая причина (самая коварная): забытый дефайн `__STARTUP_INITIALIZE_RAMFUNCTION`.**
|
||||||
|
Запись QSPI из app виснет в `qspi_write_fifo` — FlexSPI не дренирует IP TX FIFO. Причина:
|
||||||
|
startup копирует секцию `.ram_function` (код `bsp_qspi`, помеченный
|
||||||
|
`AT_QUICKACCESS_SECTION_CODE`) из flash в ITCM только под этим дефайном; `app` его не задавал
|
||||||
|
— в ITCM оставался leftover ramfunc **загрузчика** (тот исполнялся первым), поэтому чтения
|
||||||
|
«случайно работали», а запись — нет. Оба рабочих потребителя `bsp_qspi` (firmware_test,
|
||||||
|
bootloader) этот дефайн задают; `app` был первым, кто его пропустил. Диагностировано под
|
||||||
|
отладчиком (зависание конкретно в `qspi_write_fifo`), подтверждено изолированным
|
||||||
|
erase+write+readback self-test (частота мигания LED = индикатор результата) до восстановления
|
||||||
|
полной диагностики.
|
||||||
|
3. **UART, не USB CDC, для диагностики/логов.** USB CDC (target-side EHCI/PHY) под FreeRTOS не
|
||||||
|
заработал стабильно за несколько заходов — первый в репозитории FreeRTOS-потребитель target
|
||||||
|
USB device stack, класс неопределённости остался непроверенным. LPUART1/MCU-Link VCOM — самый
|
||||||
|
проверенный канал в репозитории (все `tests/target/*` HIL-образы), доступен сразу через уже
|
||||||
|
подключённый для SWD кабель, без enumeration/wait. Решение принято явно с пользователем.
|
||||||
|
|
||||||
|
**Побочные, но важные для будущих фаз находки:**
|
||||||
|
- Любая bsp-инициализация, зависящая от тика (`bsp_delay()` внутри `bsp_usb_cdc_init()` и т.п.),
|
||||||
|
нельзя звать **до** `vTaskStartScheduler()` — в FreeRTOS-режиме `bsp_tick` это
|
||||||
|
`xTaskGetTickCount()`, который не идёт до старта планировщика → вызов зависает навсегда молча.
|
||||||
|
`main()` в итоге — минимальный bare-metal (board bring-up + запуск планировщика), вся
|
||||||
|
инициализация — из задачи.
|
||||||
|
- Первый в репозитории потребитель `BSP_TICK_FREERTOS_MODE` вскрыл 2 латентных бага: (1) общий
|
||||||
|
`bsp_tick` — bare-metal STATIC-либа → `SysTick_Handler` конфликтует с FreeRTOS-портом (решение:
|
||||||
|
компилировать `tick.c` прямо в `app`, не через `libbsp_tick.a`); (2) `configTICK_RATE_HZ`
|
||||||
|
нельзя как `((TickType_t)1000)` — `#if` в `tick.c` не разбирает cast, только `1000`.
|
||||||
|
- `.vscode/launch.json`: у `🐛 Debug: tft_app` был `preLaunchTask` на `firmware_test`
|
||||||
|
(copy-paste) — исправлено на `build:app-debug`.
|
||||||
|
- Тонкий стек задачи (изначально x2=1 КБ) маскировался под тот же класс симптома, что и другие
|
||||||
|
баги (зависание/reset-loop) — увеличен с запасом (x4-x6, 2-3 КБ) везде, где заводятся новые
|
||||||
|
задачи.
|
||||||
|
|
||||||
|
**Осознанное отклонение от исходного плана:** `firmware/bootloader/test_stub` НЕ удалён (план
|
||||||
|
предполагал) — оставлен как независимая A/B-регрессия загрузчика, `app` пока не покрывает те же
|
||||||
|
сценарии (hang-классы, revert без self-confirm).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 1 — Walking skeleton (CAN → декодер → контроллер → fallback-рендер)
|
||||||
|
|
||||||
|
**Итог.** Полный вертикальный срез: реальный CAN-кадр НКУ-CAN → чистый декодер → контроллер
|
||||||
|
(diff) → fallback-рендер (позиция + стрелка) на экране. Подтверждено на TFT8 с реальным
|
||||||
|
CAN-трафиком станции.
|
||||||
|
|
||||||
|
### Архитектура (по слоям, каждый — своя CMake-либа, конвенция `bsp/<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), или оставляете как есть?
|
||||||
|
|
@ -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/)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,24 @@ typedef struct
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Что изменилось с прошлого вызова — presentation перерисовывает
|
* @brief Что изменилось с прошлого вызова — presentation перерисовывает
|
||||||
* только помеченные поля.
|
* только помеченные поля, audio_policy (Фаза 6) озвучивает изменившиеся
|
||||||
|
* события поверх этого diff.
|
||||||
|
*
|
||||||
|
* mode_pending — сменился РАЗРЕШЁННЫЙ экранный режим (свёртка сигналов по
|
||||||
|
* таблице приоритетов, §7), а не отдельный сырой сигнал. arrival/movement —
|
||||||
|
* уровни из декодера; их edge (false→true) — сигнал к озвучке (гонг, старт).
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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_ */
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
firmware/tft_app/src/domain/controller/src/mode_priority.c
Normal file
52
firmware/tft_app/src/domain/controller/src/mode_priority.c
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(). */
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
(void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s", S_SYMBOL_TABLE[RIGHT]);
|
|
||||||
}
|
/* Временная погрузка: остаток = минуты*60 + секунды. */
|
||||||
else
|
const uint8_t SECS = p_data[2] & LADING_SEC_MASK;
|
||||||
{
|
const uint8_t MINS = p_data[3] & LADING_MIN_MASK;
|
||||||
(void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s%s", S_SYMBOL_TABLE[LEFT],
|
p_ctx->state.lading_secs = (uint16_t) ((uint16_t) MINS * 60U + SECS);
|
||||||
S_SYMBOL_TABLE[RIGHT]);
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return compose_chars(p_data[3] & FLOOR_MASK, p_data[4] & FLOOR_MASK, p_ctx->state.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
p_ctx->state.next[0] = '\0';
|
||||||
return true;
|
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;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PACKET4_ID:
|
||||||
|
if (p_frame->len != PROTO_DLC)
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
p_state->overload_p4 = ((p_frame->p_data[5] & WEIGHT_MASK) == WEIGHT_MASK);
|
||||||
|
p_state->state.seismic = ((p_frame->p_data[0] & SEISMIC_MASK) == SEISMIC_MASK);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PACKET5_ID:
|
||||||
|
if (p_frame->len != PROTO_DLC)
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
if (!decode_packet5(p_state, p_frame->p_data))
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return SUL_STATUS_IGNORED;
|
||||||
|
}
|
||||||
|
|
||||||
|
recompute_multi_source(p_state);
|
||||||
*p_out = p_state->state;
|
*p_out = p_state->state;
|
||||||
return SUL_STATUS_OK;
|
return SUL_STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SUL_STATUS_IGNORED;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Вспомогательные конструкторы кадров ─────────────────────────────── */
|
/* ── Вспомогательные конструкторы кадров ─────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -38,6 +41,63 @@ static sul_frame_t make_packet3(uint8_t left, uint8_t 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 — направление ───────────────────────────────────────────── */
|
||||||
|
|
||||||
static void test_packet1_none(void)
|
static void test_packet1_none(void)
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue