# tft_app: Phase 3.4 NKU Remote, Opto Inputs
This commit is contained in:
parent
958fd9d5e5
commit
8c72be8708
53 changed files with 8739 additions and 251 deletions
|
|
@ -172,7 +172,10 @@
|
||||||
"test_sul_nku_can",
|
"test_sul_nku_can",
|
||||||
"test_tft_app_controller",
|
"test_tft_app_controller",
|
||||||
"test_tft_app_settings_store",
|
"test_tft_app_settings_store",
|
||||||
"test_tft_app_menu"
|
"test_tft_app_menu",
|
||||||
|
"test_sul_registry",
|
||||||
|
"test_tft_app_menu_tree",
|
||||||
|
"test_sul_demo"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -200,7 +203,10 @@
|
||||||
"test_sul_nku_can",
|
"test_sul_nku_can",
|
||||||
"test_tft_app_controller",
|
"test_tft_app_controller",
|
||||||
"test_tft_app_settings_store",
|
"test_tft_app_settings_store",
|
||||||
"test_tft_app_menu"
|
"test_tft_app_menu",
|
||||||
|
"test_sul_registry",
|
||||||
|
"test_tft_app_menu_tree",
|
||||||
|
"test_sul_demo"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
341
docs/tft_app/ADDING_PROTOCOL.md
Normal file
341
docs/tft_app/ADDING_PROTOCOL.md
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
# tft-app — как добавить протокол СУЛ
|
||||||
|
|
||||||
|
Пошаговое руководство по добавлению нового протокола в реестр `sul` (ARCH.md §6, §8). Разбор —
|
||||||
|
на двух реальных драйверах: [nku_can](../../firmware/tft_app/src/domain/sul/nku_can/) (реальная
|
||||||
|
шина, CAN) и [demo](../../firmware/tft_app/src/domain/sul/demo/) (синтетический источник) —
|
||||||
|
ссылки на конкретные файлы вместо абстрактных описаний.
|
||||||
|
|
||||||
|
Проектная основа — [ARCH.md §6, §8](../../firmware/tft_app/ARCH.md); путь данных decode()→экран —
|
||||||
|
[DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md); меню/настройки — [MENU.md](MENU.md). Здесь — «что
|
||||||
|
конкретно создать и куда положить», по опыту Фазы 3.3 (первый протокол, добавленный ПОСЛЕ того,
|
||||||
|
как дескрипторный механизм и реестр перестали быть однопротокольными) и Фазы 3.5 (удалённая
|
||||||
|
адресация НКУ-CAN — первый случай, когда протокол сам инициирует запись в settings).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что строим — 5 частей (2 опциональны)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph NEW["новое, на каждый протокол"]
|
||||||
|
DEC["decode() + ctx<br/>чистый C, host-тест"]
|
||||||
|
DESC["sul_settings_desc_t<br/>параметры для меню (опц.)"]
|
||||||
|
PW["take_pending_write<br/>протокол сам пишет settings (опц.)"]
|
||||||
|
TR["transport<br/>HW-адаптер ИЛИ синтетика"]
|
||||||
|
end
|
||||||
|
subgraph SHARED["правится точечно"]
|
||||||
|
REG["запись в sul_registry.c<br/>id + decode + p_settings + p_ctx + take_pending_write"]
|
||||||
|
RX["ветка в task_sul_rx.c<br/>только если НОВЫЙ вид транспорта"]
|
||||||
|
end
|
||||||
|
DEC --> REG
|
||||||
|
DESC --> REG
|
||||||
|
PW --> REG
|
||||||
|
TR --> RX
|
||||||
|
REG --> RX
|
||||||
|
```
|
||||||
|
|
||||||
|
| Часть | Обязательна? | НКУ-CAN | Демо |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **decode() + ctx** | да, всегда | [nku_can.c](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c) | [demo.c](../../firmware/tft_app/src/domain/sul/demo/src/demo.c) |
|
||||||
|
| **connection_timeout_ms** | да, всегда (см. §5) — но может быть `DISABLED` | `3000` мс | `SUL_CONNECTION_TIMEOUT_DISABLED` |
|
||||||
|
| **sul_settings_desc_t** | нет — только если есть настраиваемый параметр в меню | адрес 0..15 (BYTE) | скорость (SELECT) |
|
||||||
|
| **take_pending_write** | нет — только если протокол сам инициирует запись (не через меню) | удалённая адресация (§3.5) | нет |
|
||||||
|
| **transport** | да, но может переиспользовать уже подключённый вид (см. §6) | `transport/can` — реальная шина | `transport/demo` — пустышка, кадр не несёт содержимого |
|
||||||
|
| **ветка в `task_sul_rx.c`** | только если transport — новый ВИД (не переиспользует уже подключённый) | уже была (Фаза 1) | добавлена в Фазе 3.3 |
|
||||||
|
|
||||||
|
**Важное разграничение** (легко перепутать): `sul_settings_desc_t` — это ПОЛЬЗОВАТЕЛЬ редактирует
|
||||||
|
значение в меню; `take_pending_write` — САМ ПРОТОКОЛ решает записать значение (по команде с шины,
|
||||||
|
без участия пользователя). Оба пишут в один и тот же `proto_slice[]` (§8), но с разных сторон.
|
||||||
|
|
||||||
|
**`connection_timeout_ms` — НЕ пользовательская настройка.** Реальный период отправки у станции
|
||||||
|
(раз в ~200 мс, раз в ~1 с — по семействам сильно разное) — знание протокола, оператор его не
|
||||||
|
знает и не должен настраивать; задаётся жёстко здесь, при регистрации. Если протокол шлёт кадры
|
||||||
|
ТОЛЬКО по изменению состояния на станции (event-driven, не периодически) — детекция обрыва по
|
||||||
|
тишине для него в принципе некорректна, ставить `SUL_CONNECTION_TIMEOUT_DISABLED` (0): таймаут в
|
||||||
|
`task_sul_rx.c` для этого протокола выключается целиком, «--» по тишине не появится никогда.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Шаг 1 — decode() + ctx (чистый C, домен)
|
||||||
|
|
||||||
|
Живёт в `domain/sul/<protocol>/`:
|
||||||
|
- `include/domain/sul/<protocol>.h` — публичный контракт: тип ctx, `<protocol>_init()`, decode(),
|
||||||
|
и любые `<protocol>_set_xxx()` для параметров, которые двигает app-слой (см. §4, §7).
|
||||||
|
- `src/<protocol>.c` — реализация. **Ни одного HAL-вызова** — весь ввод только через
|
||||||
|
`sul_frame_t*`, весь вывод только через `sul_result_t*`.
|
||||||
|
|
||||||
|
Контракт ([sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h)):
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef sul_status_t (*sul_decode_fn_t)(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `p_ctx` — состояние протокола между кадрами (напр. позиция, накапливаемая между пакетами у
|
||||||
|
НКУ-CAN). **Владеет caller** — в итоге `sul_registry.c` (см. §4), decode() его не создаёт и не
|
||||||
|
освобождает.
|
||||||
|
- Возврат: `SUL_STATUS_OK` (кадр распознан, `*p_out` = полная накопленная `sul_result_t`, НЕ
|
||||||
|
дельта) / `SUL_STATUS_IGNORED` (кадр не для этого протокола) / `SUL_STATUS_ERR` (ID совпал, но
|
||||||
|
кадр малформирован).
|
||||||
|
- Протокол может быть **decode-less** (нет настоящего кадра, как демо) — `p_frame` тогда можно
|
||||||
|
игнорировать целиком; сам факт вызова decode() трактуется как «тик» (см.
|
||||||
|
[demo.c](../../firmware/tft_app/src/domain/sul/demo/src/demo.c) — темп ведёт счётчик в ctx, а
|
||||||
|
не содержимое кадра).
|
||||||
|
- Если у протокола есть сколько-нибудь большая ТАБЛИЦА-ДАННЫЕ (карта символов, скриптованный
|
||||||
|
маршрут и т.п.) — выносить в свой файл рядом (напр. `demo_route.h/.c`), не смешивать с
|
||||||
|
decode()-логикой: правка данных не должна требовать вычитывать логику декодера.
|
||||||
|
|
||||||
|
**Host-тест обязателен** — golden-векторы кадров → ожидаемый `sul_result_t`
|
||||||
|
(`tests/host/tft_app_sul_<protocol>/`), по образцу `test_sul_nku_can.c`/`test_sul_demo.c`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Шаг 2 — sul_settings_desc_t (только если есть параметр в меню)
|
||||||
|
|
||||||
|
Нужен, если у протокола есть значение, редактируемое ПОЛЬЗОВАТЕЛЕМ в меню (адрес, скорость,
|
||||||
|
канал…). Живёт **в `sul_registry.c`**, не в модуле протокола — реестр уже знает про меню-факинг
|
||||||
|
метаданные (`p_name` там же):
|
||||||
|
|
||||||
|
```c
|
||||||
|
static const sul_settings_entry_t K_<PROTO>_SETTINGS_ENTRIES[] = {
|
||||||
|
{ .p_label = "...", .type = SUL_SETTINGS_BYTE /* | _SELECT | _BOOL */,
|
||||||
|
.slice_offset = 0U, .min = ..., .max = ..., .p_options = NULL /* или массив меток */ },
|
||||||
|
};
|
||||||
|
static const sul_settings_desc_t K_<PROTO>_SETTINGS = {
|
||||||
|
.p_entries = K_<PROTO>_SETTINGS_ENTRIES,
|
||||||
|
.count = sizeof(K_<PROTO>_SETTINGS_ENTRIES) / sizeof(K_<PROTO>_SETTINGS_ENTRIES[0]),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- `slice_offset` — смещение ВНУТРИ `settings_t.user.proto_slice[]` (0..`SETTINGS_PROTO_SLICE_LEN`-1
|
||||||
|
из `settings_store.h`), НЕ внутри всего `settings_t` — домен (реестр) не включает
|
||||||
|
`settings_store.h`; реальный `offsetof()` считает `menu/menu_tree.c` (единственный слой, знающий
|
||||||
|
оба типа).
|
||||||
|
- **Сейчас движок меню рассчитан ровно на 1 параметр на протокол** (`menu_tree.c`,
|
||||||
|
`T_PROTO_PARAM` — один зарезервированный слот). `count > 1` пока физически не отрисуется —
|
||||||
|
используется только `p_entries[0]`. Протоколу с несколькими параметрами понадобится сначала
|
||||||
|
расширить `menu_tree.c` (несколько слотов + скрытие неиспользуемых для протоколов с меньшим
|
||||||
|
числом параметров) — сознательно не сделано заранее (YAGNI, см. PLAN.md Фаза 3.3).
|
||||||
|
- Нет ни одного параметра — не создавать дескриптор, оставить `.p_settings = NULL` у драйвера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Шаг 3 — take_pending_write (только если протокол сам пишет settings)
|
||||||
|
|
||||||
|
Нужен, если протокол получает команду **с шины** (не от пользователя через меню), которая должна
|
||||||
|
записать значение в его же `proto_slice[]` — напр. удалённая установка адреса у НКУ-CAN (§3.5,
|
||||||
|
станция объявляет адрес кадром `0x4X1` + командой `0x5XB`, см.
|
||||||
|
[nku_can.c](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c)).
|
||||||
|
|
||||||
|
**Правило, ради которого этот механизм вообще существует:** decode() и логика распознавания
|
||||||
|
команды — чистые, **не трогают `settings_store` напрямую** (домен не пишет настройки, ARCH §1).
|
||||||
|
Вместо прямой записи — отдельный generic-канал:
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* sul.h */
|
||||||
|
typedef struct { uint8_t slice_offset; uint8_t value; } sul_slice_write_t;
|
||||||
|
typedef bool (*sul_take_pending_write_fn_t)(void *p_ctx, sul_slice_write_t *p_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
Реализация в модуле протокола — чистая функция, читает НАКОПЛЕННОЕ состояние ctx после последнего
|
||||||
|
decode(), настройки не трогает вообще:
|
||||||
|
|
||||||
|
```c
|
||||||
|
bool <protocol>_take_pending_write(void *p_ctx, sul_slice_write_t *p_out)
|
||||||
|
{
|
||||||
|
const <protocol>_ctx_t *p_state = (const <protocol>_ctx_t *) p_ctx;
|
||||||
|
if (/* нет запроса в этом ctx */) { return false; }
|
||||||
|
p_out->slice_offset = ...;
|
||||||
|
p_out->value = ...;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Регистрируется как поле `sul_driver_t.take_pending_write` (§5) — `NULL`, если протокол никогда
|
||||||
|
этого не делает (подавляющее большинство). **`task_sul_rx.c` полностью generic** — ветки по
|
||||||
|
id/протоколу для этого НЕТ, только `if (p_driver->take_pending_write != NULL)`. Идемпотентность
|
||||||
|
(писать только если значение реально отличается от сохранённого) — тоже generic, живёт в
|
||||||
|
`task_sul_rx.c`, а не в каждом протоколе отдельно: протокол просто говорит «вот что нужно
|
||||||
|
записать», сравнение с текущим — не его забота.
|
||||||
|
|
||||||
|
**Как отличить от `sul_settings_desc_t` (§3):** дескриптор — это ЧТО МОЖНО отредактировать в
|
||||||
|
меню; `take_pending_write` — это протокол САМ РЕШИЛ записать (пользователь не участвует). У
|
||||||
|
НКУ-CAN есть оба одновременно на **один и тот же** `proto_slice[0]` (адрес можно и вручную в
|
||||||
|
меню, и удалённо с шины) — это два независимых входа в одно и то же поле, не конфликт.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Шаг 4 — реестр (`sul_registry.c`)
|
||||||
|
|
||||||
|
Правки в одном файле:
|
||||||
|
|
||||||
|
1. `#include "domain/sul/<protocol>.h"`.
|
||||||
|
2. Добавить id в `enum` ([sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h)) —
|
||||||
|
**стабильный, не переиспользовать** уже выданные значения.
|
||||||
|
3. Статический ctx + запись в `s_registry[]`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static <protocol>_ctx_t s_<protocol>_ctx;
|
||||||
|
|
||||||
|
static const sul_driver_t s_registry[] = {
|
||||||
|
/* ...существующие... */
|
||||||
|
{
|
||||||
|
.id = SUL_PROTOCOL_<PROTO>,
|
||||||
|
.p_name = "<как в меню>",
|
||||||
|
.decode = <protocol>_decode,
|
||||||
|
.p_settings = &K_<PROTO>_SETTINGS, /* или не указывать = NULL */
|
||||||
|
.p_ctx = &s_<protocol>_ctx,
|
||||||
|
.take_pending_write = <protocol>_take_pending_write, /* или не указывать = NULL */
|
||||||
|
.connection_timeout_ms = <мс> /* или SUL_CONNECTION_TIMEOUT_DISABLED — ОБЯЗАТЕЛЬНО, не забыть! */,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`connection_timeout_ms` легко забыть — designated-initializer молча зануляет пропущенное поле в
|
||||||
|
`SUL_CONNECTION_TIMEOUT_DISABLED` (0), т.е. протокол ТИХО никогда не покажет «--» при обрыве
|
||||||
|
связи вместо явной ошибки сборки. Сверяйтесь с этим полем при код-ревью нового протокола.
|
||||||
|
|
||||||
|
4. `sul_registry_init()` — добавить `<protocol>_init(&s_<protocol>_ctx);`.
|
||||||
|
|
||||||
|
**Ctx живёт постоянно**, не пересоздаётся при переключении активного протокола (`sul_registry_
|
||||||
|
set_active()` только меняет, какая запись реестра активна — все ctx проинициализированы заранее).
|
||||||
|
|
||||||
|
Дальше **само меню не трогается**: `menu_tree_refresh_protocol_section()` подхватывает новый
|
||||||
|
протокол из реестра автоматически (имя в списке выбора, диапазон, параметр из дескриптора) —
|
||||||
|
см. [MENU.md §4](MENU.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Шаг 5 — transport (только если новый вид шины)
|
||||||
|
|
||||||
|
**Архитектурное решение, найденное на демо-протоколе (Фаза 3.3):** transport **не входит** в
|
||||||
|
`sul_driver_t`/реестр. Если бы входил, `tft_app_sul` пришлось бы линковать `bsp_can`/`bsp_uart` и
|
||||||
|
т.п., ломая host-тестируемость реестра без железа (реестр и декодеры — чистый C, транспорт —
|
||||||
|
HW/HIL). Transport остаётся **wiring'ом app-слоя** (`task_sul_rx.c`) — тонкий HW-адаптер
|
||||||
|
`sul_frame_t` живёт в `domain/sul/transport/<bus>/` (собственная CMake-библиотека, не
|
||||||
|
host-тестируется), но **вызывается только из `task_sul_rx.c`**, не из `sul_registry.c`.
|
||||||
|
|
||||||
|
Отсюда развилка:
|
||||||
|
- **Протокол на уже подключённом виде шины** (напр. второй CAN-протокол) — новый transport не
|
||||||
|
нужен; `task_sul_rx.c` может не тронуться вообще, если оба протокола используют один и тот же
|
||||||
|
`sul_transport_can_receive()`. Если конкретному протоколу всё же нужна другая
|
||||||
|
инициализация/фильтры — по образцу ветки НКУ-CAN (реаппликация параметров внутри существующей
|
||||||
|
ветки, см. §7).
|
||||||
|
- **Протокол на новом виде шины** (напр. первый UART-протокол Фазы 8 — УИМ/SD7/УЭЛ/УКЛ,
|
||||||
|
большинство из них не CAN, см. ARCH §14) — создать `domain/sul/transport/<bus>/` (по образцу
|
||||||
|
`transport/can`/`transport/demo`) **и** добавить ветку в `task_sul_rx.c` (§7). Второй и
|
||||||
|
последующие протоколы на ТОМ ЖЕ новом виде шины эту ветку уже не трогают.
|
||||||
|
|
||||||
|
**Готча: HW-фильтры — слепая зона host-тестов** (боевая, Фаза 3.5). Если decode() должен видеть
|
||||||
|
кадры ВНЕ основного набора ID протокола (широковещательные команды, кадры с чужим адресом — как
|
||||||
|
удалённая адресация НКУ-CAN, `0x4X1`/`0x5XB` с любым X), под них нужны СВОИ фильтры в транспорте
|
||||||
|
(wildcard-маска / отдельные MB — см. [can_transport.c](../../firmware/tft_app/src/domain/sul/transport/can/src/can_transport.c),
|
||||||
|
MB 5/6). Точные фильтры под «свои» ID отбрасывают такие кадры аппаратно, и **host-тесты декодера
|
||||||
|
этого не поймают** — они кормят decode() напрямую, мимо HW-фильтров: всё зелёное, на железе —
|
||||||
|
ноль реакции. Проверяйте соответствие «какие кадры decode() ОЖИДАЕТ увидеть» ↔ «какие кадры
|
||||||
|
фильтры транспорта ПРОПУСКАЮТ» глазами, при код-ревью транспорта.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Шаг 6 — диспетчеризация в `task_sul_rx.c`
|
||||||
|
|
||||||
|
Только если шаг 6 завёл новый вид транспорта. Это **единственное** оставшееся место, которое
|
||||||
|
знает про конкретные протоколы/транспорты — везде остальное протокол-агностично (в т.ч.
|
||||||
|
`take_pending_write`, §4 — тот генерик, ветки по id для него уже НЕТ):
|
||||||
|
|
||||||
|
```c
|
||||||
|
const sul_driver_t *p_driver = sul_registry_active();
|
||||||
|
|
||||||
|
sul_frame_t frame;
|
||||||
|
bsp_status_t rx_rc;
|
||||||
|
if (p_driver->id == SUL_PROTOCOL_NKU_CAN)
|
||||||
|
{
|
||||||
|
/* существующая ветка: адрес из настроек -> ctx И CAN-фильтры, sul_transport_can_receive() */
|
||||||
|
}
|
||||||
|
else if (p_driver->id == SUL_PROTOCOL_<НОВЫЙ>)
|
||||||
|
{
|
||||||
|
/* реаппликация параметров протокола (если есть, из proto_slice[0]) + приём с нового transport */
|
||||||
|
}
|
||||||
|
else /* демо и т.д. — оставшиеся протоколы без своей ветки */
|
||||||
|
{
|
||||||
|
rx_rc = sul_transport_demo_receive(CAN_RX_TIMEOUT_MS, &frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rx_rc == BSP_OK)
|
||||||
|
{
|
||||||
|
if (p_driver->decode(p_driver->p_ctx, &frame, &decoded) == SUL_STATUS_OK) { /* ... */ }
|
||||||
|
|
||||||
|
/* Generic, без ветки по id — см. §4. */
|
||||||
|
if (p_driver->take_pending_write != NULL)
|
||||||
|
{
|
||||||
|
sul_slice_write_t write;
|
||||||
|
if (p_driver->take_pending_write(p_driver->p_ctx, &write)) { /* ...идемпотентная запись... */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`p_driver->p_ctx` уже правильного типа для активного протокола — просто скастовать
|
||||||
|
(`(‹protocol›_ctx_t *) p_driver->p_ctx`), диспетчеризация по типу ctx отдельно писать не нужно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Шаг 7 — CMake
|
||||||
|
|
||||||
|
- `domain/sul/<protocol>/CMakeLists.txt` — `add_library(tft_app_sul_<protocol> STATIC src/<protocol>.c ...)`,
|
||||||
|
линкует `tft_app_elevator_model` + `tft_app_sul_headers` (**не** `tft_app_sul` — реестр сам
|
||||||
|
линкует протокол, обратная зависимость была бы циклической).
|
||||||
|
- `domain/sul/CMakeLists.txt` — `add_subdirectory(<protocol>)`, линкует `tft_app_sul_<protocol>` в
|
||||||
|
`tft_app_sul`.
|
||||||
|
- Если завели новый transport (§6) — свой `domain/sul/transport/<bus>/CMakeLists.txt`
|
||||||
|
(`tft_app_sul_transport_<bus>`, линкует `tft_app_sul_headers` + нужный `bsp_*`/`bsp_status`), и
|
||||||
|
явная зависимость `app` → `tft_app_sul_transport_<bus>` в `firmware/tft_app/CMakeLists.txt`
|
||||||
|
(транспорт НЕ идёт транзитивно через `tft_app_sul`, см. §6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Шаг 8 — тесты
|
||||||
|
|
||||||
|
- **Декодер** — `tests/host/tft_app_sul_<protocol>/test_sul_<protocol>.c`, golden-векторы (+ тесты
|
||||||
|
на `take_pending_write()`, если есть, §4). Зарегистрировать `add_host_test(...)` в
|
||||||
|
`tests/host/CMakeLists.txt` **и** добавить имя таргета в список `targets` ОБОИХ build-пресетов
|
||||||
|
`host-debug-build`/`host-release-build` в `CMakePresets.json` — иначе `ctest` не найдёт
|
||||||
|
исполняемый файл (`Not Run`, а не `FAIL`, легко пропустить); наступили на эти грабли в Фазе 3.3
|
||||||
|
(см. PLAN.md).
|
||||||
|
- **Реестр** — расширить `tests/host/tft_app_sul_registry/test_sul_registry.c`: новый протокол
|
||||||
|
находится по id, `sul_registry_count()` вырос, дескриптор (если есть) присутствует и содержит
|
||||||
|
ожидаемое, `take_pending_write` — `NULL`/не-`NULL` как задумано.
|
||||||
|
- **Меню** — обычно ничего менять не нужно в `test_tft_app_menu_tree.c` (он уже проверяет
|
||||||
|
протокол-агностичность механизма); добавить кейс только если новый протокол — интересный
|
||||||
|
крайний случай (напр. 0 параметров, ещё не встречалось ни у одного зарегистрированного
|
||||||
|
протокола).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Чего НЕ нужно трогать
|
||||||
|
|
||||||
|
Это и есть проверка того, что дескрипторный принцип (ARCH §8) реально работает, а не только на
|
||||||
|
бумаге:
|
||||||
|
|
||||||
|
- **Меню** (`menu_tree.c`, `menu.c`) — секция «Протокол» строится из дескриптора автоматически.
|
||||||
|
- **Controller / mode_priority** — работают с каноническим `sul_result_t`, протокол им не виден
|
||||||
|
вообще (см. [MODE_PRIORITY.md §5](MODE_PRIORITY.md)).
|
||||||
|
- **UI/fallback, render_task** — читают `indication_task_t`, о протоколах не знают.
|
||||||
|
- **Settings store** — `proto_slice[]` уже общий, под любой протокол (по одному активному за раз).
|
||||||
|
- **`task_sul_rx.c` для `take_pending_write`** (§4/§7) — generic, ветка по id нужна ТОЛЬКО для
|
||||||
|
транспорта (§6/§7), не для записи settings.
|
||||||
|
|
||||||
|
Если правка одного из этих слоёв кажется необходимой для нового протокола — вероятно, протокол
|
||||||
|
пытается пронести через `sul_result_t` что-то непротокольное (см. ARCH §6 — «провиженинг-концепты
|
||||||
|
типа `cop_mode`/`display_id` — это настройки устройства, не данные СУЛ»).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Референсы
|
||||||
|
|
||||||
|
| Хочу... | Смотреть |
|
||||||
|
| --- | --- |
|
||||||
|
| Протокол на реальной шине, с параметром | [nku_can/](../../firmware/tft_app/src/domain/sul/nku_can/), [transport/can/](../../firmware/tft_app/src/domain/sul/transport/can/) |
|
||||||
|
| Синтетический / decode-less протокол | [demo/](../../firmware/tft_app/src/domain/sul/demo/), [transport/demo/](../../firmware/tft_app/src/domain/sul/transport/demo/) |
|
||||||
|
| Данные декодера отдельно от логики | [demo_route.h/.c](../../firmware/tft_app/src/domain/sul/demo/src/) |
|
||||||
|
| Протокол сам пишет settings (без меню) | [nku_can_take_pending_write()](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c) — удалённая адресация, §4 выше |
|
||||||
|
| Полная связка настройки → меню → протокол | [MENU.md §4](MENU.md), [SETTINGS.md](SETTINGS.md) |
|
||||||
|
| Пошагово: как добавить НЕпротокольную настройку | [ADDING_SETTING.md](ADDING_SETTING.md) |
|
||||||
196
docs/tft_app/ADDING_SETTING.md
Normal file
196
docs/tft_app/ADDING_SETTING.md
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
# tft-app — как добавить настройку
|
||||||
|
|
||||||
|
Пошаговое руководство по добавлению нового поля в `settings_t` и пункта меню под него (ARCH.md
|
||||||
|
§8). Не про протоколы как таковые — это [ADDING_PROTOCOL.md](ADDING_PROTOCOL.md); не про то, что
|
||||||
|
такое `settings_t`/ярусы/QSPI — это [SETTINGS.md](SETTINGS.md); не про движок меню как таковой —
|
||||||
|
это [MENU.md](MENU.md). Здесь — практическая связка «хочу новый пункт в меню» → «что конкретно
|
||||||
|
трогать», с одной реальной сквозной сверкой (тумблер логов, §7).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Первая развилка — пользовательская или протокольная
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
START([Новая настройка]) --> Q{Имеет смысл ТОЛЬКО<br/>для одного протокола?<br/>(напр. адрес станции)}
|
||||||
|
Q -->|да| PROTO["Протокольная<br/>→ proto_slice + sul_settings_desc_t<br/>см. §5"]
|
||||||
|
Q -->|нет| USER["Пользовательская/device<br/>→ своё поле в settings_t<br/>см. §4"]
|
||||||
|
```
|
||||||
|
|
||||||
|
| | Пользовательская/device | Протокольная |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Где хранится | своё именованное поле `settings_t.user`/`.device` | общий `proto_slice[]`, трактует активный протокол |
|
||||||
|
| Кто описывает пункт меню | статическая строка в `menu_tree.c` (руками, один раз) | `sul_settings_desc_t` протокола (реестр строит пункт САМ при каждом переключении) |
|
||||||
|
| Видна в меню | всегда | только когда этот протокол активен |
|
||||||
|
| Пример | тумблер логов (§7), выбор протокола | адрес НКУ-CAN, скорость демо |
|
||||||
|
| Документ с деталями | этот, §4 | [ADDING_PROTOCOL.md §3](ADDING_PROTOCOL.md) |
|
||||||
|
|
||||||
|
Признак протокольной — значение теряет смысл, если сменить протокол (адрес станции НКУ-CAN не
|
||||||
|
значит ничего для демо-протокола). Если значение осмысленно всегда и для всех — пользовательская,
|
||||||
|
даже если физически лежит в `settings_device_t` (пример — `log_enabled`: «device» по имени поля,
|
||||||
|
но полноценный пункт меню уже сегодня).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Три слоя, которые всегда участвуют
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
F["Поле в settings_t<br/>(SETTINGS.md §2)"] --> T["Строка menu_item_desc_t<br/>offset/type/min/max/options<br/>(MENU.md §2)"]
|
||||||
|
T --> E["Редактор по типу<br/>menu_action() — generic,<br/>не меняется под новый пункт"]
|
||||||
|
T -.только протокольная.-> D["sul_settings_desc_t<br/>ADDING_PROTOCOL.md §3"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Пользовательская настройка трогает ТОЛЬКО первые два слоя (поле + строка дерева) — движок
|
||||||
|
редактирования (`menu.c`) и рендер (`menu_view.c`) уже generic, под конкретный пункт не меняются.
|
||||||
|
Протокольная добавляет третий слой (дескриптор), а строку дерева `T_PROTO_PARAM` строит
|
||||||
|
`menu_tree_refresh_protocol_section()` за вас — руками её не пишете.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Форматы значений — что есть сейчас, что заготовка
|
||||||
|
|
||||||
|
Движок хранит любое editable-значение как **один `uint8`** по `value_offset`, редактируется
|
||||||
|
ОДНИМ generic-механизмом — инкремент с заворотом `min→max→min`
|
||||||
|
([menu.c](../../firmware/tft_app/src/menu/src/menu.c), `cycle_value()`):
|
||||||
|
|
||||||
|
| `menu_item_type_t` | Реализован? | Чем отличается от соседей | Пример |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `MENU_BYTE` | ✅ | `options = NULL` → рендер числом | адрес НКУ-CAN 0..15 |
|
||||||
|
| `MENU_SELECT` | ✅ | `options[value]` → рендер меткой | протокол, скорость демо |
|
||||||
|
| `MENU_BOOL` | ✅ | `min=0, max=1`, `options` — 2 метки (обычно Вкл/Выкл) | тумблер логов |
|
||||||
|
| `MENU_SUBMENU` / `MENU_BACK` | ✅ | навигация, не значение (`value_offset` игнорируется) | «Настройки», «Выход» |
|
||||||
|
| `ARRAY` / `SERIAL` / `YEAR` / `PERCENT` / `BOOL_ARRAY` | ⬜ **нет** | под многобайтовые/составные значения (серийник, набор битов, год с иной раскладкой…) | — Фазы 5/6 |
|
||||||
|
|
||||||
|
**Важный вывод:** `MENU_BYTE`/`MENU_SELECT`/`MENU_BOOL` — механически ОДНО И ТО ЖЕ (один uint8,
|
||||||
|
инкремент с заворотом); разница только презентационная — есть ли `options[]` и какой смысл у
|
||||||
|
диапазона. Если ваша настройка укладывается в «одно число 0..N» — **новый код в движке не нужен
|
||||||
|
вообще**, только строка дерева (§4). Настоящая новая работа начинается, только если нужен ТИП из
|
||||||
|
нижней строки таблицы (⬜) — тогда это не «добавить настройку», а «добавить редактор»: новое
|
||||||
|
значение в `menu_item_type_t`, ветка в `menu_action()` (menu.c) и в рендере (`menu_view.c`) —
|
||||||
|
двигатель спроектирован под расширение (см. докстрок `menu.h`), но пока ни разу не расширялся.
|
||||||
|
|
||||||
|
`sul_settings_desc_t` (протокольные, [sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h))
|
||||||
|
использует СВОЙ `sul_settings_type_t` — подмножество ровно из трёх реализованных
|
||||||
|
(`SUL_SETTINGS_BYTE/_SELECT/_BOOL`), без `SUBMENU`/`BACK` (протокольный параметр никогда не
|
||||||
|
подменю) и без незаведённых типов. `menu_type_from_sul()` в `menu_tree.c` — единственное место
|
||||||
|
перевода между двумя enum'ами.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Шаг за шагом — пользовательская/device настройка
|
||||||
|
|
||||||
|
1. **Поле** — [settings_store.h](../../firmware/tft_app/src/services/settings_store/include/services/settings_store.h):
|
||||||
|
добавить `uint8_t`/`uint16_t` в `settings_user_t` (ярус A — общее для всех) или
|
||||||
|
`settings_device_t` (провиженинг/то, что не про конкретного пользователя-оператора). Прокомментировать
|
||||||
|
ярус и диапазон, как соседние поля. Значение ДОЛЖНО умещаться в `uint8_t`, если пойдёт в меню
|
||||||
|
через `value_offset` (движок читает/пишет ровно один байт, см. §3) — `max_load_kg` (`uint16_t`)
|
||||||
|
поэтому и не редактируется до появления типа `ARRAY`/составного редактора.
|
||||||
|
2. **Дефолт** — `K_DEFAULTS` в [settings_codec.c](../../firmware/tft_app/src/services/settings_store/src/settings_codec.c).
|
||||||
|
Паттерн «0 = не задано/скрыто» уже используется (`max_load_kg`, `year_production`) — держитесь
|
||||||
|
его для новых необязательных полей, чтобы UI мог единообразно решать «показывать ли».
|
||||||
|
3. **Пункт дерева** — [menu_tree.c](../../firmware/tft_app/src/menu/src/menu_tree.c):
|
||||||
|
- новое имя в `enum { T_ROOT, T_PROTO, ..., T_EXIT, T_COUNT }` — вставить ГДЕ УГОДНО между
|
||||||
|
текущими первым и последним пунктом уровня (порядок в enum = физическая позиция в плоском
|
||||||
|
массиве `s_tree[]` = порядок в списке на экране); `.first_child`/`.last_child` родителя
|
||||||
|
(`T_ROOT`) ссылаются на ИМЕНА констант, не на числа — перенумеровывать соседей руками не
|
||||||
|
нужно, если новый пункт не становится НОВЫМ первым/последним (обычный случай — вставить перед
|
||||||
|
`T_EXIT`, чтобы «Выход» остался последним, как сейчас);
|
||||||
|
- строка в `s_tree[]`:
|
||||||
|
```c
|
||||||
|
[T_NEW] = { .label = "...",
|
||||||
|
.type = MENU_BYTE /* | _SELECT | _BOOL */,
|
||||||
|
.value_offset = offsetof(settings_t, user.<поле> /* или device.<поле> */),
|
||||||
|
.min = ...,
|
||||||
|
.max = ...,
|
||||||
|
.parent = MENU_ROOT_INDEX,
|
||||||
|
.options = NULL /* или массив меток, как K_BOOL_LABELS */ },
|
||||||
|
```
|
||||||
|
- `options` — только для `SELECT`/`BOOL`, длиной ровно `max - min + 1`, иначе `menu_view.c`
|
||||||
|
прочитает `options[value]` за границей массива (та же готча, что клампится для протокольных
|
||||||
|
значений при смене протокола, см. [SETTINGS.md §4](SETTINGS.md) — здесь она НЕ актуальна:
|
||||||
|
диапазон статический, не переключается рантаймом, поэтому клампить нечего).
|
||||||
|
4. **Если настройка должна на что-то влиять** — это отдельный шаг, не автоматический, см. §6.
|
||||||
|
5. **Host-тест** — `tests/host/tft_app_menu/test_menu_tree.c`: по образцу
|
||||||
|
`test_protocol_param_edits_correct_settings_field` (init/open/next/action/assert) — открыть
|
||||||
|
меню, дойти до нового пункта (`menu_next()` до совпадения `label`, не хардкодить числовой
|
||||||
|
индекс — тот же приём, что `proto_index()` в этом файле), `menu_action()`, проверить, что
|
||||||
|
изменилось именно то поле `settings_t`, которое назвал `value_offset`.
|
||||||
|
6. **При необходимости** — `tests/host/tft_app_settings_store/test_settings_codec.c`:
|
||||||
|
строка в `test_defaults_sane` (если дефолт важен) и/или в `test_roundtrip_preserves_fields`
|
||||||
|
(сериализация копирует `settings_t` целиком, новое поле переживёт round-trip и без явной
|
||||||
|
проверки — строка в тесте нужна для регрессионной сигнализации, не потому что иначе сломается).
|
||||||
|
|
||||||
|
**Вложенные подменю.** Дерево сейчас **плоское** — все пункты (`T_PROTO`, `T_PROTO_PARAM`, `T_LOG`,
|
||||||
|
`T_EXIT`) на одном уровне, дети `MENU_ROOT_INDEX`. Движок поддерживает `MENU_SUBMENU`
|
||||||
|
(свой `first_child..last_child`, см. `menu.h`) — если пунктов станет много и захочется сгруппировать
|
||||||
|
(«Диагностика», «Звук»…), группировка возможна уже сегодня, просто пока не понадобилась.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Шаг за шагом — протокольная настройка
|
||||||
|
|
||||||
|
Не дублируется здесь — полностью [ADDING_PROTOCOL.md §3](ADDING_PROTOCOL.md) («Шаг 2 —
|
||||||
|
sul_settings_desc_t»). Коротко: дескриптор живёт в `sul_registry.c` (не в модуле протокола),
|
||||||
|
`slice_offset` считается ВНУТРИ `proto_slice[]`, реальный `offsetof(settings_t, ...)` считает
|
||||||
|
`menu_tree_refresh_protocol_section()` — руками строку `s_tree[T_PROTO_PARAM]` не пишете.
|
||||||
|
|
||||||
|
**Ограничение, актуальное сегодня** (SETTINGS.md §4): движок меню рассчитан РОВНО на один параметр
|
||||||
|
на протокол (`T_PROTO_PARAM` — один слот). Если новому протоколу нужно больше одного —
|
||||||
|
сначала расширяется `menu_tree.c` (несколько слотов + скрытие лишних у протоколов с меньшим
|
||||||
|
числом параметров), это отдельная, более крупная задача, не «добавить настройку».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Если настройка должна на что-то влиять — эффект (отдельно от хранения)
|
||||||
|
|
||||||
|
Поле в `settings_t` само по себе ничего не делает — это только хранение. Если нужен рантайм-эффект
|
||||||
|
(не просто «лежит и переживает перезагрузку»), пишете обработчик и вызываете его в ДВУХ местах —
|
||||||
|
паттерн уже дважды использован (`protocol_id`, `log_enabled`):
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* task_bringup.c — один раз при старте, из загруженных настроек */
|
||||||
|
log_set_enabled(settings_store_get()->device.log_enabled != 0U);
|
||||||
|
|
||||||
|
/* task_menu.c — после КАЖДОГО menu_action(), тем же вызовом */
|
||||||
|
log_set_enabled(g_menu.settings->device.log_enabled != 0U);
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему в обоих местах: `task_bringup.c` — эффект должен быть виден с самого старта (не только
|
||||||
|
после первого захода в меню); `task_menu.c` — эффект должен быть виден СРАЗУ в этом же сеансе
|
||||||
|
меню, не только после `settings_store_save()` (пользователь может листать значения долго, прежде
|
||||||
|
чем выйти-с-сохранением, или вообще не сохранить, а эффект уже должен сработать — см. TASKS.md про
|
||||||
|
`sul_registry_set_active()`, тот же приём).
|
||||||
|
|
||||||
|
Если эффекта не нужно (настройка просто хранится, как весь ярус A сейчас, см.
|
||||||
|
[SETTINGS.md §9](SETTINGS.md)) — это ОК, легитимное промежуточное состояние (Фазы 5/6 добавят
|
||||||
|
эффект отдельно, без переделки хранения/меню).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Пример целиком — тумблер логов (эталон пользовательской BOOL-настройки)
|
||||||
|
|
||||||
|
| Шаг | Файл | Что именно |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Поле | [settings_store.h](../../firmware/tft_app/src/services/settings_store/include/services/settings_store.h) | `settings_device_t.log_enabled` (`uint8_t`) |
|
||||||
|
| Дефолт | [settings_codec.c](../../firmware/tft_app/src/services/settings_store/src/settings_codec.c) | `.log_enabled = 1U` — логи включены из коробки |
|
||||||
|
| Пункт дерева | [menu_tree.c](../../firmware/tft_app/src/menu/src/menu_tree.c) | `T_LOG`: `MENU_BOOL`, `offsetof(settings_t, device.log_enabled)`, `min=0/max=1`, `options=K_BOOL_LABELS` |
|
||||||
|
| Эффект | [log.h](../../utils/log/log.h)/[log.c](../../utils/log/log.c) | `log_set_enabled()` — рантайм-гейт ПОВЕРХ компайл-тайм `LOG_LEVEL`, первая проверка в `log_write()` |
|
||||||
|
| Вызов эффекта | `task_bringup.c` + `task_menu.c` | оба места, см. §6 |
|
||||||
|
| Host-тест хранения | `tests/host/log/test_log.c` | вкл/выкл по умолчанию, гейт реально давит вывод, повторное включение восстанавливает |
|
||||||
|
| Host-тест меню | — | пункт `T_LOG` статический (не из дескриптора) — отдельного edit-теста нет, при желании — по образцу §4 п.5 |
|
||||||
|
|
||||||
|
Полная деталь и история (почему бит, а не пер-тег гейт) — PLAN.md, Фаза 3.6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Референсы
|
||||||
|
|
||||||
|
| Хочу... | Смотреть |
|
||||||
|
| --- | --- |
|
||||||
|
| Состав `settings_t`, ярусы, QSPI | [SETTINGS.md](SETTINGS.md) |
|
||||||
|
| Механика offset/движок меню/навигация | [MENU.md](MENU.md) |
|
||||||
|
| Протокольный параметр (адрес, скорость…) | [ADDING_PROTOCOL.md §3](ADDING_PROTOCOL.md) |
|
||||||
|
| Протокол сам пишет settings без меню | [ADDING_PROTOCOL.md §4](ADDING_PROTOCOL.md) (`take_pending_write`) |
|
||||||
|
| Готовый пример BOOL-настройки | §7 выше (тумблер логов) |
|
||||||
|
| Готовый пример SELECT/BYTE | адрес НКУ-CAN / скорость демо, [sul_registry.c](../../firmware/tft_app/src/domain/sul/src/sul_registry.c) |
|
||||||
|
|
@ -5,9 +5,11 @@
|
||||||
обоснование слоёв — в [ARCH.md](../../firmware/tft_app/ARCH.md); статус фаз —
|
обоснование слоёв — в [ARCH.md](../../firmware/tft_app/ARCH.md); статус фаз —
|
||||||
в [PLAN.md](../../firmware/tft_app/PLAN.md). Здесь — «как это работает в коде сейчас».
|
в [PLAN.md](../../firmware/tft_app/PLAN.md). Здесь — «как это работает в коде сейчас».
|
||||||
|
|
||||||
Сейчас реализован один протокол — **НКУ-CAN**. Архитектура рассчитана на много протоколов
|
Сейчас в реестре два протокола — **НКУ-CAN** (реальная шина) и **демо** (синтетический источник,
|
||||||
(УЭЛ/УКЛ/SD7/УИМ — Фаза 8): добавление протокола = новый декодер + запись в реестр, без правок
|
Фаза 3.3, витрина возможностей устройства без СУЛ на другом конце). Архитектура рассчитана на
|
||||||
остальных слоёв.
|
много протоколов (ещё УЭЛ/УКЛ/SD7/УИМ — Фаза 8): добавление протокола = новый декодер + запись
|
||||||
|
в реестр (+ transport, если новый вид шины) — см. [ADDING_PROTOCOL.md](ADDING_PROTOCOL.md) для
|
||||||
|
пошагового разбора.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -90,7 +92,7 @@ sequenceDiagram
|
||||||
|
|
||||||
```c
|
```c
|
||||||
typedef struct { uint32_t id; uint8_t bus; const uint8_t *p_data; uint16_t len; } sul_frame_t;
|
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);
|
typedef sul_status_t (*sul_decode_fn_t)(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Три исхода** `decode()` (`sul_status_t`):
|
**Три исхода** `decode()` (`sul_status_t`):
|
||||||
|
|
@ -152,8 +154,16 @@ flowchart LR
|
||||||
seismic — PACKET4), пакет-владелец распоряжается напрямую: сбрасывает в начале и выставляет по
|
seismic — PACKET4), пакет-владелец распоряжается напрямую: сбрасывает в начале и выставляет по
|
||||||
условию, гася устаревший режим.
|
условию, гася устаревший режим.
|
||||||
|
|
||||||
Удалённая установка адреса (кадры `0x4X1`/`0x5XB`) — **под-шаг 3.5** (см. PLAN.md). Сейчас эти
|
Удалённая установка адреса (кадры `0x4X1`/`0x5XB`, спецификация `REMOTE_ADDRES_SETUP.pdf`) —
|
||||||
ID игнорируются (wildcard-фильтры под них ещё не настраиваются).
|
**Фаза 3.5**: `nku_can_decode()` распознаёт анонс/команду независимо от классификации PACKET1..5
|
||||||
|
выше (один и тот же кадр может быть и своим PACKET4, и несущей команды) и выставляет запрос в
|
||||||
|
ctx; саму запись в `settings_store` делает `task_sul_rx.c` (decode() settings не пишет).
|
||||||
|
**Транспорт-сторона обязательна** (боевая находка, тот же класс, что адресные фильтры выше): кадры
|
||||||
|
`0x4X1`/`0x5XB` с любым X проходят HW-фильтры только благодаря двум **wildcard-фильтрам** (MB 5/6,
|
||||||
|
маска `0x70F` — адресный нибл `[7:4]` игнорируется) в `sul_transport_can_set_address()` — точные
|
||||||
|
фильтры PACKET1..5 их аппаратно отбрасывают, и decode() до них не доходит; host-тесты
|
||||||
|
этого не ловят (кормят decode() напрямую, мимо фильтров). Детали —
|
||||||
|
[PLAN.md §3.5](../../firmware/tft_app/PLAN.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,20 @@
|
||||||
## 2. Что рисуется
|
## 2. Что рисуется
|
||||||
|
|
||||||
Презентация решает по разрешённому режиму (`indication_task_t.mode`, см.
|
Презентация решает по разрешённому режиму (`indication_task_t.mode`, см.
|
||||||
[MODE_PRIORITY.md](MODE_PRIORITY.md)):
|
[MODE_PRIORITY.md](MODE_PRIORITY.md)) — **и по диспетчерскому входу** (§2.1), который
|
||||||
|
проверяется первым и безусловно перекрывает всё остальное:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
START([render mode, result]) --> CLR["gfx_clear() — AS прозрачен,<br/>фон даёт чёрный PS"]
|
START([render mode, result, dispatcher]) --> CLR["gfx_clear() — AS прозрачен,<br/>фон даёт чёрный PS"]
|
||||||
CLR --> Q{"mode == NORMAL?"}
|
CLR --> DSP{"dispatcher != NONE?"}
|
||||||
|
DSP -->|да| DLBL["метка ВЫЗОВ/ОТВЕТ (SystemFont)"]
|
||||||
|
DLBL --> DONE([готово])
|
||||||
|
DSP -->|нет| Q{"mode == NORMAL?"}
|
||||||
Q -->|да| POS["этаж (FloorFontFallback), по центру"]
|
Q -->|да| POS["этаж (FloorFontFallback), по центру"]
|
||||||
POS --> ARR{"direction UP/DOWN?"}
|
POS --> ARR{"direction UP/DOWN?"}
|
||||||
ARR -->|да| DRAWARR["примитив-стрелка"]
|
ARR -->|да| DRAWARR["примитив-стрелка"]
|
||||||
ARR -->|нет| DONE([готово])
|
ARR -->|нет| DONE
|
||||||
DRAWARR --> DONE
|
DRAWARR --> DONE
|
||||||
Q -->|нет| LBL["метка режима (SystemFont)<br/>ПОЖАР/ПЕРЕГРУЗ/СЕРВИС/…"]
|
Q -->|нет| LBL["метка режима (SystemFont)<br/>ПОЖАР/ПЕРЕГРУЗ/СЕРВИС/…"]
|
||||||
LBL --> LAD{"погрузка и lading_secs>0?"}
|
LBL --> LAD{"погрузка и lading_secs>0?"}
|
||||||
|
|
@ -46,10 +50,34 @@ flowchart TD
|
||||||
- **Спецрежим** — короткая текстовая метка системным шрифтом (`SystemFont`). Это **safe-mode**
|
- **Спецрежим** — короткая текстовая метка системным шрифтом (`SystemFont`). Это **safe-mode**
|
||||||
индикация: богатая полноэкранная графика режимов (фон+спрайты) появится в Фазах 4–5. Для
|
индикация: богатая полноэкранная графика режимов (фон+спрайты) появится в Фазах 4–5. Для
|
||||||
временной погрузки дополнительно рисуется обратный отсчёт (`lading_secs`).
|
временной погрузки дополнительно рисуется обратный отсчёт (`lading_secs`).
|
||||||
|
- **Диспетчерский вход** (§3.4) — та же safe-mode текстовая метка (переиспользует слот режимов:
|
||||||
|
`MODE_Y`/`SystemFont`), но проверяется ПЕРВЫМ, до `mode`. Слова временные, «ВЫЗОВ»/«ОТВЕТ».
|
||||||
|
|
||||||
Следующий этаж (`next`) в fallback **не** показывается — это элемент богатого layout (Фаза 5),
|
Следующий этаж (`next`) в fallback **не** показывается — это элемент богатого layout (Фаза 5),
|
||||||
поэтому на `next_pending` перерисовки нет.
|
поэтому на `next_pending` перерисовки нет.
|
||||||
|
|
||||||
|
### 2.1 Диспетчерский вход — не данные СУЛ, отдельный вход в рендер
|
||||||
|
|
||||||
|
`opto` IN1 («вызов подан») / IN2 («вызов принят») — **локальный вход** (ARCH §8 п.3), не
|
||||||
|
`sul_result_t`: не идёт через `controller`/таблицу приоритетов режимов (эта таблица — только
|
||||||
|
для ортогональных сигналов СУЛ, см. [MODE_PRIORITY.md](MODE_PRIORITY.md)). Дизайн-диаграмма
|
||||||
|
ARCH §4 рисует его именно так — отдельным входом прямо в `ui.render(task, model, settings)`, не
|
||||||
|
через `controller`:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
OPTO["bsp_opto IN1/IN2<br/>(app/dispatcher.c)"] -->|"g_dispatcher_indication"| RENDER["ui_fallback_render*()"]
|
||||||
|
CTRL["controller.process()<br/>(только sul_result_t)"] -->|"indication_task_t"| RENDER
|
||||||
|
```
|
||||||
|
|
||||||
|
Приоритет — **высший из всех**, согласовано с пользователем: перекрывает обычную индикацию И
|
||||||
|
любой режим СУЛ (пожар/перегруз/…), работает даже без связи со станцией (dispatcher — независимое
|
||||||
|
оборудование, не станция). ОТВЕТ (IN2) перебивает ВЫЗОВ (IN1), если оба почему-то активны
|
||||||
|
одновременно. Референс поведения (не архитектуры) —
|
||||||
|
`OLD_PROJECT_TFT8_UKL/source/main_programm.c` (`tft_refresh_task`): там `icon_img_ptr`
|
||||||
|
перезаписывается диспетчерской проверкой ПОСЛЕДНЕЙ в каждом кадре, безусловно — тот же эффект,
|
||||||
|
что здесь достигается проверкой ПЕРВОЙ в `render()` с `return`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. По-символьный fallback шрифта
|
## 3. По-символьный fallback шрифта
|
||||||
|
|
@ -78,12 +106,19 @@ flowchart LR
|
||||||
|
|
||||||
`ui_fallback_render()` перерисовывает кадр целиком (без dirty-rect — `gfx` минимален) при
|
`ui_fallback_render()` перерисовывает кадр целиком (без dirty-rect — `gfx` минимален) при
|
||||||
изменении того, что fallback реально показывает: **позиция, стрелка или режим**
|
изменении того, что fallback реально показывает: **позиция, стрелка или режим**
|
||||||
(`pos_pending || direction_pending || mode_pending`). Иначе — no-op.
|
(`pos_pending || direction_pending || mode_pending`). Иначе — no-op. И этот путь, и
|
||||||
|
безусловный старт (ниже) принимают диспетчерский вход (§2.1) отдельным параметром — сам он
|
||||||
|
НЕ входит в `pos_pending`/`direction_pending`/`mode_pending` (это diff `sul_result_t`, opto —
|
||||||
|
локальный вход, другая природа изменения).
|
||||||
|
|
||||||
Первую отрисовку презентация делает **безусловно** один раз при старте
|
Первую отрисовку презентация делает **безусловно** один раз при старте
|
||||||
(`ui_fallback_render_initial()`): кэш контроллера засеян дефолтом, и если первый реальный кадр
|
(`ui_fallback_render_initial()`): кэш контроллера засеян дефолтом, и если первый реальный кадр
|
||||||
совпадёт с дефолтом, diff придёт «ничего не изменилось» — без безусловного старта экран остался бы
|
совпадёт с дефолтом, diff придёт «ничего не изменилось» — без безусловного старта экран остался бы
|
||||||
пустым.
|
пустым. Тот же безусловный вызов переиспользуется как ТРЕТЬЯ причина полной перерисовки (кроме
|
||||||
|
старта и закрытия меню) — смена диспетчерского входа САМА ПО СЕБЕ, без нового кадра СУЛ в
|
||||||
|
очереди: `render_task` (см. [TASKS.md §3](TASKS.md)) сравнивает текущее
|
||||||
|
`g_dispatcher_indication` с прошлым и, если отличается, зовёт `ui_fallback_render_initial()`
|
||||||
|
из последнего известного `sul_result_t` + нового диспетчерского значения.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,14 @@ flowchart TD
|
||||||
| device/провиженинг | тумблер логов | `offsetof(settings_t, device.log_enabled)` |
|
| device/провиженинг | тумблер логов | `offsetof(settings_t, device.log_enabled)` |
|
||||||
| **B** протокольные | адрес НКУ | `offsetof(settings_t, user.proto_slice[0])` |
|
| **B** протокольные | адрес НКУ | `offsetof(settings_t, user.proto_slice[0])` |
|
||||||
|
|
||||||
Ярус B (протокольные параметры) в 3.2 привязан прямым offset к `proto_slice`; в 3.3 это обобщается
|
Ярус B (протокольные параметры) с Фазы 3.3 строится из дескриптора протокола
|
||||||
на дескриптор протокола `sul_settings_desc_t` (§8) — меню строит раздел «Настройки протокола» из
|
`sul_settings_desc_t` (§8), не хардкодом: `menu_tree_refresh_protocol_section()` читает
|
||||||
дескриптора активного протокола, не хардкодом.
|
`sul_registry_active()->p_settings` и заполняет раздел «Настройки протокола» (label/тип/
|
||||||
|
диапазон/offset/options) из него — при смене активного протокола (и при bringup) секция
|
||||||
|
перестраивается. Подробности механизма и как добавить протокол — [ADDING_PROTOCOL.md](ADDING_PROTOCOL.md).
|
||||||
|
Пошаговое «как добавить настройку» (любую — пользовательскую или протокольную, форматы значений)
|
||||||
|
— [ADDING_SETTING.md](ADDING_SETTING.md); состав/ярусы/хранение самого `settings_t` —
|
||||||
|
[SETTINGS.md](SETTINGS.md).
|
||||||
|
|
||||||
**Поток сохранения** (`menu_task` связывает модель и flash — см. §5):
|
**Поток сохранения** (`menu_task` связывает модель и flash — см. §5):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@ Dev-документация по **реализованному** слою `fir
|
||||||
| Документ | О чём |
|
| Документ | О чём |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md) | Путь данных: транспорт → декодер → контроллер → презентация. Слои, контракт `decode()`, карта пакетов НКУ-CAN → поля модели, адресация (decode + HW-фильтры), таймаут→default. |
|
| [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md) | Путь данных: транспорт → декодер → контроллер → презентация. Слои, контракт `decode()`, карта пакетов НКУ-CAN → поля модели, адресация (decode + HW-фильтры), таймаут→default. |
|
||||||
|
| [ADDING_PROTOCOL.md](ADDING_PROTOCOL.md) | Как добавить протокол в реестр `sul`: decode()+ctx, дескриптор настроек, transport, реестр, диспетчеризация в `task_sul_rx.c`, тесты. Разобрано на НКУ-CAN и демо-протоколе. |
|
||||||
|
| [ADDING_SETTING.md](ADDING_SETTING.md) | Как добавить настройку: пользовательская vs протокольная, форматы значений (BYTE/SELECT/BOOL — что реализовано, что заготовка), поле → пункт меню → эффект. Эталон — тумблер логов. |
|
||||||
| [MODE_PRIORITY.md](MODE_PRIORITY.md) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. |
|
| [MODE_PRIORITY.md](MODE_PRIORITY.md) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. |
|
||||||
| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, по-символьный fallback шрифта, триггеры перерисовки, путь кадра через компоновщик. |
|
| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, диспетчерский вход (высший приоритет, opto), по-символьный fallback шрифта, триггеры перерисовки, путь кадра через компоновщик. |
|
||||||
| [MENU.md](MENU.md) | Движок меню (Фаза 3.2): дерево-данные, редакторы по типу, чистая модель, связь с `settings_store`, оконный рендер, раскладка кнопок. |
|
| [MENU.md](MENU.md) | Движок меню (Фаза 3.2): дерево-данные, редакторы по типу, чистая модель, связь с `settings_store`, оконный рендер, раскладка кнопок. |
|
||||||
|
| [SETTINGS.md](SETTINGS.md) | Модуль настроек: состав `settings_t`, ярусы A/B/C и их связь, `proto_slice`/дескрипторы, гибридное хранение (ядро + клиентский TLV), карта QSPI, что осознанно не реализовано. |
|
||||||
| [TASKS.md](TASKS.md) | Задачи FreeRTOS: состав/приоритеты (и почему такие), старт системы, MPSC-взаимодействие, мягкая пауза, разделяемое состояние. |
|
| [TASKS.md](TASKS.md) | Задачи FreeRTOS: состав/приоритеты (и почему такие), старт системы, MPSC-взаимодействие, мягкая пауза, разделяемое состояние. |
|
||||||
|
|
||||||
## Связанные документы
|
## Связанные документы
|
||||||
|
|
@ -21,8 +24,10 @@ Dev-документация по **реализованному** слою `fir
|
||||||
|
|
||||||
## Границы
|
## Границы
|
||||||
|
|
||||||
Описаны домен, fallback-презентация, ядро настроек (`settings_store`), меню (модель + оконный
|
Описаны домен, fallback-презентация (+ диспетчерский вход, Фаза 3.4), ядро настроек
|
||||||
рендер + задачи) и фундамент рендера (double-buffer + PXP + гибрид bpp, Фаза 3.2.4). Остаток
|
(`settings_store` + состав/ярусы/QSPI-карта, [SETTINGS.md](SETTINGS.md)), меню (модель + оконный
|
||||||
Фазы 3: per-protocol дескрипторы (3.3), opto-входы (3.4), удалённая адресация НКУ-CAN (3.5),
|
рендер + задачи), фундамент рендера (double-buffer + PXP + гибрид bpp, Фаза 3.2.4), per-protocol
|
||||||
тумблер логов (3.6). Ещё не реализовано (см. PLAN.md): ассеты и layout-движок со спрайтами
|
дескрипторы + реестр из двух протоколов (Фаза 3.3), удалённая адресация НКУ-CAN (Фаза 3.5),
|
||||||
(Фазы 4–5), аудио (Фаза 6), мультипротокол (Фаза 8).
|
рантайм-тумблер логов (Фаза 3.6). **Фаза 3 полностью закрыта документацией.** Ещё не реализовано
|
||||||
|
(см. PLAN.md): ассеты и layout-движок со спрайтами (Фазы 4–5, включая ярус C настроек — клиентский
|
||||||
|
TLV, см. SETTINGS.md §3/§9), аудио (Фаза 6), остальные протоколы — УИМ/SD7/УЭЛ/УКЛ (Фаза 8).
|
||||||
|
|
|
||||||
275
docs/tft_app/SETTINGS.md
Normal file
275
docs/tft_app/SETTINGS.md
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
# tft-app — настройки: состав, ярусы, хранение
|
||||||
|
|
||||||
|
Документ описывает **реализованный** модуль `settings_store` — состав `settings_t`, ярусы A/B/C
|
||||||
|
и их взаимосвязь, гибридную модель хранения (ядро на QSPI + клиентский TLV) и карту QSPI. Проектная
|
||||||
|
основа — [ARCH.md §8](../../firmware/tft_app/ARCH.md) (настройки) и
|
||||||
|
[§10](../../firmware/tft_app/ARCH.md) (карта QSPI); реализация —
|
||||||
|
[settings_store.h](../../firmware/tft_app/src/services/settings_store/include/services/settings_store.h),
|
||||||
|
[settings_codec.c](../../firmware/tft_app/src/services/settings_store/src/settings_codec.c),
|
||||||
|
[partition.h](../../firmware/tft_app/src/services/partition/include/services/partition.h).
|
||||||
|
|
||||||
|
Не дублирует: механику дескрипторов протоколов ([ADDING_PROTOCOL.md](ADDING_PROTOCOL.md)) и связь
|
||||||
|
меню↔offset ([MENU.md](MENU.md) §4) — здесь только то, что уникально для самого модуля настроек:
|
||||||
|
что хранится, как устроены ярусы, как это лежит на флеше. Практическое «как добавить свою
|
||||||
|
настройку, шаг за шагом» — [ADDING_SETTING.md](ADDING_SETTING.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Три источника — settings сюда НЕ смешивает чужое
|
||||||
|
|
||||||
|
ARCH §8 разделяет три независимых источника данных приложения:
|
||||||
|
|
||||||
|
1. **`sul_result_t`** — только то, что прислала станция (см. [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md)).
|
||||||
|
2. **`settings_t`** (этот документ) — конфиг устройства/пользователя, живёт на QSPI.
|
||||||
|
3. **Локальные входы** — opto (диспетчерский вызов/ответ), кнопки/меню — свои
|
||||||
|
`volatile`-переменные в `app_tasks.h` (см. [TASKS.md](TASKS.md) §5), в `settings_t` не попадают
|
||||||
|
вообще (это не конфигурация, а рантайм-состояние оборудования).
|
||||||
|
|
||||||
|
`settings_t` — ЕДИНСТВЕННЫЙ персистентный источник из трёх; два остальных живут только в RAM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Состав `settings_t`
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
settings_device_t device; /* провиженинг, не в пользовательском меню */
|
||||||
|
settings_user_t user; /* ярусы A + B */
|
||||||
|
uint8_t _reserved[8]; /* фикс размера — offset'ы полей стабильны */
|
||||||
|
} settings_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Поле | Тип/размер | Ярус | Диапазон/дефолт | Редактируется в меню сейчас? |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `device.panel_type` | `uint8_t` | provisioning | `2` (TFT8), Фаза 9 | нет — provisioning-параметр (ARCH §9), не пользовательский |
|
||||||
|
| `device.protocol_id` | `uint8_t` | provisioning/B | `0` (NKU_CAN) | да — пункт «Протокол» (`T_PROTO`) |
|
||||||
|
| `device.log_enabled` | `uint8_t` | provisioning | `1` (вкл.) | да — пункт «Логи» (`T_LOG`, Фаза 3.6) |
|
||||||
|
| `user.max_load_kg` | `uint16_t` | A | `0` = скрыто | **нет** — хранится и сериализуется, редактор — Фаза 5/6 |
|
||||||
|
| `user.max_cap_persons` | `uint8_t` | A | `0` = скрыто | **нет** — Фаза 5/6 |
|
||||||
|
| `user.sound_volume_idx` | `uint8_t` | A | `2` из 0..4 | **нет** — эффект (звук) только с Фазы 6 |
|
||||||
|
| `user.music_volume_idx` | `uint8_t` | A | `1` из 0..4 | **нет** — Фаза 6 |
|
||||||
|
| `user.year_production` | `uint8_t` | A | `0` = скрыто, иначе `2000+N` | **нет** — Фаза 5/6 |
|
||||||
|
| `user.serial[10]` | ASCII+`\0` | A | `""` | **нет** — Фаза 5/6 |
|
||||||
|
| `user.proto_slice[8]` | `uint8_t[8]` | B | зависит от протокола (§4) | да — единственный параметр активного протокола (`T_PROTO_PARAM`) |
|
||||||
|
| `_reserved[8]` | padding | — | `0xFF` при сохранении | не поле, задел |
|
||||||
|
|
||||||
|
Ярус A хранится/сериализуется/проходит CRC уже сейчас (см. `K_DEFAULTS` в `settings_codec.c`) —
|
||||||
|
это НЕ означает наличие UI: полей ещё нет ни в одном пункте `menu_tree.c` (проверяемо — grep по
|
||||||
|
`max_load_kg`/`serial`/… в дереве не находит ничего). Раздел 9 держит это явно, чтобы не решить
|
||||||
|
по коду наоборот.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ярусы A/B/C — назначение и связь
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph ST["settings_t (QSPI 0x450000, ядро)"]
|
||||||
|
A["Ярус A — железобетонные<br/>вес/вместимость/громкости/серийник/год<br/>одинаковы для ЛЮБОГО клиента"]
|
||||||
|
B["Ярус B — протокольные<br/>proto_slice[8], трактует АКТИВНЫЙ протокол<br/>через sul_settings_desc_t"]
|
||||||
|
DEV["device — provisioning<br/>панель/протокол/лог-тумблер"]
|
||||||
|
end
|
||||||
|
C["Ярус C — клиентский UX-зоопарк<br/>лого, сдвиги/маска этажей, метки<br/>TLV в layout-регионе 0x440000<br/>⚠ ЕЩЁ НЕ РЕАЛИЗОВАНО (Фаза 4/5)"]
|
||||||
|
|
||||||
|
ST -.разные регионы, разный формат.-> C
|
||||||
|
```
|
||||||
|
|
||||||
|
- **A — железобетонные.** Одинаковый смысл для любого клиента/протокола: вес, вместимость,
|
||||||
|
громкости, серийник, год выпуска. Не зависят от того, какая станция подключена.
|
||||||
|
- **B — протокольные.** `proto_slice[8]` — ОБЩИЙ буфер под параметры активного протокола;
|
||||||
|
какой байт что значит, решает **дескриптор** протокола (`sul_settings_desc_t`, ARCH §8) —
|
||||||
|
см. §4 ниже. Слайс один на всех — при смене протокола он **переинтерпретируется**, не
|
||||||
|
расширяется (готча со staleness — тоже в §4).
|
||||||
|
- **device (provisioning).** Не входит ни в A, ни в B по смыслу ARCH §8, но физически хранится
|
||||||
|
рядом (структура `settings_device_t`): панель — provisioning (Фаза 9, пишет `service_tui`,
|
||||||
|
не пользователь), протокол и тумблер логов — фактически пользовательские пункты меню уже
|
||||||
|
сейчас, несмотря на «device» в имени поля.
|
||||||
|
- **C — клиентский UX-зоопарк.** Логотип, сдвиги/маска номеров этажей, произвольные текстовые
|
||||||
|
метки — то, что каждый клиент хочет по-своему. **НЕ входит в `settings_t` вообще** — ни
|
||||||
|
структурно, ни по QSPI-региону (§6). Живёт как TLV в layout-регионе (`0x440000`), пишется
|
||||||
|
`service_tui` (Model 2 «injected», ARCH §11), схема версионируется отдельно от ядра. На
|
||||||
|
сегодня это **только дизайн** (ARCH §11) — ни кода, ни формата TLV ещё нет (Фазы 4–5).
|
||||||
|
|
||||||
|
**Почему C не влезает в ядро, а не потому что «забыли»:** ядро (A+B) — маленькое, с фиксированным
|
||||||
|
`offsetof`, редактируется **прошивкой** через меню, должно быть всегда валидно (magic/CRC + дефолт
|
||||||
|
на любую порчу, §7). Зоопарк C — открытый по составу (у каждого клиента свой набор виджетов),
|
||||||
|
редактируется **внешним инструментом** без пересборки прошивки, требует версионируемой схемы —
|
||||||
|
принципиально другой формат и жизненный цикл, поэтому и другой регион (§6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `proto_slice` и дескрипторы протоколов (кратко — детали в ADDING_PROTOCOL.md)
|
||||||
|
|
||||||
|
Активный протокол описывает свои параметры дескриптором
|
||||||
|
([sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h)):
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
const char *p_label; sul_settings_type_t type; /* BYTE / SELECT / BOOL */
|
||||||
|
uint8_t slice_offset; /* смещение ВНУТРИ proto_slice[] */
|
||||||
|
uint8_t min, max;
|
||||||
|
const char *const *p_options; /* только для SELECT/BOOL */
|
||||||
|
} sul_settings_entry_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
Два зарегистрированных сейчас протокола ([sul_registry.c](../../firmware/tft_app/src/domain/sul/src/sul_registry.c)):
|
||||||
|
|
||||||
|
| Протокол | `slice_offset` | Тип | Диапазон | Смысл |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| НКУ-CAN | `proto_slice[0]` | BYTE | 0..15 | адрес станции |
|
||||||
|
| Демо | `proto_slice[0]` | SELECT | 0..2 | скорость скриптованного маршрута |
|
||||||
|
|
||||||
|
Домен не включает `settings_store.h` (ARCH §4, домен не знает презентацию/хранение) — `slice_offset`
|
||||||
|
считается ВНУТРИ `proto_slice[]`, а не внутри `settings_t`. Перевод в реальный
|
||||||
|
`offsetof(settings_t, user.proto_slice) + slice_offset` — единственная привилегия `menu/`
|
||||||
|
(`menu_tree_refresh_protocol_section()`, [MENU.md](MENU.md) §4) — только этот слой включает и
|
||||||
|
`domain/sul.h`, и `settings_store.h` одновременно.
|
||||||
|
|
||||||
|
**Готча (реальная, найдена и закрыта в Фазе 3.3, не гипотетическая).** Один и тот же байт
|
||||||
|
`proto_slice[0]` при переключении протокола в меню означает РАЗНОЕ (адрес 0..15 → скорость 0..2).
|
||||||
|
Без клампа «протухшее» значение (напр. адрес 15) читалось бы как `options[15]` за границей
|
||||||
|
2-элементного массива меток демо — мусорное чтение в рендере. `menu_tree_refresh_protocol_section()`
|
||||||
|
клампит значение под НОВЫЙ `max` в момент переключения (фикс в корне, не на каждом сайте чтения).
|
||||||
|
Практическое следствие: **у протокола с несколькими параметрами** второй/третий байт слайса будет
|
||||||
|
не инициализирован осмысленно при первом переключении на него — сейчас не актуально (у обоих
|
||||||
|
протоколов ровно один параметр), станет вопросом Фазы 8 (см. `ADDING_PROTOCOL.md` §3).
|
||||||
|
|
||||||
|
Как зарегистрировать параметр для НОВОГО протокола — пошагово в
|
||||||
|
[ADDING_PROTOCOL.md §3](ADDING_PROTOCOL.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Хранение — гибридная модель (ядро-struct + клиентский TLV)
|
||||||
|
|
||||||
|
| | Ядро (A + B + device) | Клиентский зоопарк (C) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Формат | фикс. `struct` + magic/version/CRC32 | TLV, версионируемая схема |
|
||||||
|
| Регион QSPI | `0x450000`, 8 КБ (§6) | `0x440000`, 64 КБ (layout-регион, §6) |
|
||||||
|
| Кто пишет | прошивка (меню, `settings_store_save()`) | `service_tui` (внешний инструмент) |
|
||||||
|
| Когда меняется | в поле, через экранное меню | при заказе/переконфигурации клиента, без пересборки |
|
||||||
|
| Валидация | magic+version+CRC32, авто-дефолт при любой невалидности | схема+версия+CRC, на host (golden-render) |
|
||||||
|
| Реализовано | ✅ да (этот документ) | ⬜ нет — только дизайн ARCH §11, Фазы 4–5 |
|
||||||
|
|
||||||
|
Ядро сознательно маленькое и жёсткое (offset-стабильность нужна декодерам/меню прямо сейчас);
|
||||||
|
зоопарк сознательно гибкий и внешний (открытый список клиентских причуд, ARCH §2.2). Один
|
||||||
|
`settings_t` на оба смысла означал бы либо раздувать ядро под гипотетические виджеты (нарушает
|
||||||
|
YAGNI и offset-стабильность), либо тащить версионируемый TLV-парсер в код, который должен быть
|
||||||
|
простым и всегда-валидным. Два формата, два региона, две скорости изменения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Карта QSPI (ARCH §10) — размер-независимая
|
||||||
|
|
||||||
|
| Регион | Смещение | Размер | Владелец |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| bootloader | `0x000000` | 256 КБ | bootloader |
|
||||||
|
| slot A (tft_app) | `0x040000` | 2 МБ | MCUboot |
|
||||||
|
| slot Б (tft_app) | `0x240000` | 2 МБ | MCUboot |
|
||||||
|
| **layout** (+ ярус C, TLV) | `0x440000` | 64 КБ | tft_app / `service_tui` |
|
||||||
|
| **settings** (ядро, этот документ) | `0x450000` | 8 КБ = 2 сектора × 4 КБ | tft_app |
|
||||||
|
| **assets** | `0x452000` | остаток до конца чипа | tft_app |
|
||||||
|
|
||||||
|
Все константы — [partition.h](../../firmware/tft_app/src/services/partition/include/services/partition.h),
|
||||||
|
общий источник для `app` и (в перспективе) генератора `service_tui`.
|
||||||
|
|
||||||
|
**Размер-независимость.** Прошивка работает на разных QSPI NOR (W25Q128/256/512 …). Все критичные
|
||||||
|
регионы — по ФИКСИРОВАННЫМ смещениям (компайл-тайм, без рантайм-детекта); размер-зависим ТОЛЬКО
|
||||||
|
регион ассетов — стартует с фикс-адреса и тянется до конца чипа
|
||||||
|
(`длина = bsp_qspi_flash_size() − 0x452000`). Больше флеш = больше места под ассеты, карта не
|
||||||
|
меняется.
|
||||||
|
|
||||||
|
**Settings — 2 сектора, сейчас используется ОДИН.** `TFT_APP_QSPI_SETTINGS_OFFSET` (сектор 0) —
|
||||||
|
рабочий, туда пишет `settings_store_save()`. Сектор 1 (`+0x1000`) — **зарезервирован под будущий
|
||||||
|
power-safe ping-pong** (erase+write не атомарен — обрыв питания посреди `save()` сейчас МОЖЕТ
|
||||||
|
испортить единственную копию; см. §9). Сектор 1 сегодня не читается и не пишется НИКЕМ — это
|
||||||
|
задел по карте, не реализованная защита.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Формат страницы (magic/version/CRC32)
|
||||||
|
|
||||||
|
Ровно один сектор QSPI (`BSP_QSPI_SECTOR_SIZE` = 4 КБ), проверяется статически
|
||||||
|
(`_Static_assert(sizeof(settings_page_t) == BSP_QSPI_SECTOR_SIZE)`):
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint32_t magic; /* 0x54465453 'STFT' */
|
||||||
|
uint8_t version; /* 1 */
|
||||||
|
uint8_t _pad[3];
|
||||||
|
settings_t data;
|
||||||
|
uint8_t _reserved[/* до конца сектора */];
|
||||||
|
uint32_t crc32; /* по всей странице КРОМЕ этого поля */
|
||||||
|
} settings_page_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **CRC32** — poly `0x04C11DB7`, MSB-first, init `0xFFFFFFFF`, без финального xor (как
|
||||||
|
`style_updater` в OLD_PROJECT) — [`settings_crc32()`](../../firmware/tft_app/src/services/settings_store/src/settings_codec.c).
|
||||||
|
- **`_pad`/`_reserved` = `0xFF`** при сериализации — паттерн стёртого флеша, не нули (те же
|
||||||
|
причины, что 0xFF-заполнение в других TLV этого репозитория).
|
||||||
|
- **Чтение — всегда успешно с точки зрения вызывающего.** `settings_deserialize()` возвращает
|
||||||
|
`false` на любое несовпадение magic/version/CRC — `settings_store_load()` в этом случае просто
|
||||||
|
подставляет `settings_defaults()`; настройки валидны ВСЕГДА после `load()`/`init_defaults()`,
|
||||||
|
ошибка не пробрасывается выше как fatal (аналог поведения `default`-layout, ARCH §11 — деградация,
|
||||||
|
не отказ).
|
||||||
|
- Сериализация/валидация — **чистые функции** в `settings_codec.c` (без единого обращения к QSPI),
|
||||||
|
host-тестируются без железа; `settings_store.c` — тонкий flash-адаптер поверх них.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Жизненный цикл (bringup → меню → save)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant B as bringup_task
|
||||||
|
participant Q as QSPI 0x450000
|
||||||
|
participant M as menu_task
|
||||||
|
participant S as sul_rx_task
|
||||||
|
|
||||||
|
B->>Q: settings_store_load()
|
||||||
|
alt magic/version/CRC валидны
|
||||||
|
Q-->>B: g_settings = данные с флеша
|
||||||
|
else невалидны/ошибка чтения
|
||||||
|
B->>B: settings_store_init_defaults()
|
||||||
|
end
|
||||||
|
B->>B: sul_registry_set_active(protocol_id)<br/>menu_tree_refresh_protocol_section()<br/>log_set_enabled(log_enabled)
|
||||||
|
|
||||||
|
Note over M,S: рантайм — RAM-мутации через settings_store_get_mutable()
|
||||||
|
M->>M: правка в меню (offset, dirty=true)
|
||||||
|
M->>Q: save() ТОЛЬКО если save_requested (=dirty на выходе)
|
||||||
|
S->>Q: save() — удалённый адрес НКУ-CAN (§3.5), свой путь записи
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Один экземпляр в RAM** (`static settings_t g_settings` в `settings_store.c`) — `get()`/
|
||||||
|
`get_mutable()` возвращают указатель на него же, копий нет.
|
||||||
|
- **Сохранение — не на каждую правку.** `menu.c` взводит `dirty` любой правкой, `save_requested`
|
||||||
|
выставляется РАВНЫМ `dirty` только на выходе из корня меню — если пользователь ничего не менял,
|
||||||
|
`settings_store_save()` не вызывается вообще (нет лишних erase-циклов флеша).
|
||||||
|
- **Два независимых писателя `save()`** — `menu_task` (выход из меню) и `sul_rx_task` (удалённый
|
||||||
|
адрес НКУ-CAN, §3.5) — защищены `g_save_mutex` (создаётся в `init_defaults()`/`load()`, оба
|
||||||
|
вызываются один раз из `bringup_task` ДО создания остальных задач, гонки при создании нет по
|
||||||
|
конструкции). Без мьютекса — два параллельных `erase+write` в один сектор, порча настроек
|
||||||
|
(найдено при проектировании 3.5, не на стенде — см. PLAN.md).
|
||||||
|
- **Реаппликация — не только после `save()`.** Активный протокол/тумблер логов переприменяются
|
||||||
|
СРАЗУ на каждое действие меню (`sul_registry_set_active()`, `menu_tree_refresh_protocol_section()`,
|
||||||
|
`log_set_enabled()`) — эффект виден в том же сеансе, независимо от того, дойдёт ли пользователь
|
||||||
|
до сохранения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Что осознанно НЕ реализовано (честно)
|
||||||
|
|
||||||
|
- **Power-safe ping-pong** — сектор 1 занят под задел (§6), не используется. `save()` сегодня:
|
||||||
|
erase → постраничная запись ОДНОГО сектора; обрыв питания между erase и завершением записи
|
||||||
|
оставит страницу с невалидным CRC → следующий `load()` откатится на дефолты (не hardfault, но
|
||||||
|
и не восстановит последнее сохранённое — пользовательские правки будут потеряны).
|
||||||
|
- **Ярус C / клиентский TLV** — ноль кода, только дизайн ARCH §11. Layout-регион (`0x440000`)
|
||||||
|
сегодня не читается и не пишется вообще (нет layout-движка, Фазы 4–5).
|
||||||
|
- **`settings_store_reset_user_defaults()`** — функция объявлена и реализована, но НЕ вызывается
|
||||||
|
ниоткуда в `app`-слое — нет пункта меню «сброс настроек». Задел под Фазу 5/6.
|
||||||
|
- **Ярус A без UI** — весь список из §2 («нет» в последней колонке) хранится/сериализуется/проходит
|
||||||
|
CRC уже сейчас, но нет ни одного пункта меню, который бы это редактировал — только `device.protocol_id`
|
||||||
|
и `device.log_enabled` (провиженинговые по имени поля) и `user.proto_slice` реально управляемы
|
||||||
|
оператором сегодня.
|
||||||
|
- **`device.panel_type`** — provisioning-параметр (ARCH §9), пишется `service_tui`, а не
|
||||||
|
пользовательским меню; сейчас хардкод-дефолт `BSP_DISPLAY_TFT8`, реальный provisioning-путь —
|
||||||
|
Фаза 9.
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
| `menu_task` | task_menu.c | `+3` | модель меню: кнопки, вход/навигация/правка/сохранение. **НЕ рисует** | `settings_store_save()` (flash) на выходе из меню |
|
| `menu_task` | task_menu.c | `+3` | модель меню: кнопки, вход/навигация/правка/сохранение. **НЕ рисует** | `settings_store_save()` (flash) на выходе из меню |
|
||||||
| `render_task` | task_render.c | `+2` | **единственный** владелец дисплея и вызывающий `gfx_present*()` | PXP busy-wait + ожидание FRAME_DONE |
|
| `render_task` | task_render.c | `+2` | **единственный** владелец дисплея и вызывающий `gfx_present*()` | PXP busy-wait + ожидание FRAME_DONE |
|
||||||
| `sul_rx_task` | task_sul_rx.c | `+1` (низший) | приём CAN → decode → controller; WDOG/heartbeat | busy-spin в `bsp_can_receive()` до 100 мс без трафика |
|
| `sul_rx_task` | task_sul_rx.c | `+1` (низший) | приём CAN → decode → controller; WDOG/heartbeat | busy-spin в `bsp_can_receive()` до 100 мс без трафика |
|
||||||
| — демон таймеров | (FreeRTOS) | `configTIMER_TASK_PRIORITY` (высший в системе) | `input_poll_cb` каждые 5 мс: `bsp_button_poll()` (debounce); Фаза 3.4 — сюда же opto | нет (колбэк короткий) |
|
| — демон таймеров | (FreeRTOS) | `configTIMER_TASK_PRIORITY` (высший в системе) | `input_poll_cb` каждые 5 мс: `bsp_button_poll()` (debounce) → `bsp_opto_process()` (debounce IN1/IN2) → `dispatcher_poll()` (§3.4, `app/dispatcher.c` — безусловный опрос `bsp_opto_read()`, НЕ колбэк, см. PLAN.md про баг реактивной версии; пишет `g_dispatcher_indication` и будит `render_task` прямо отсюда при изменении) | нет (все три коротких) |
|
||||||
|
|
||||||
`main()` создаёт только очередь, софт-таймер ввода и `bringup_task` — остальное wiring делает
|
`main()` создаёт только очередь, софт-таймер ввода и `bringup_task` — остальное wiring делает
|
||||||
сам `bringup_task`.
|
сам `bringup_task`.
|
||||||
|
|
@ -58,32 +58,65 @@ CPU, пока не создаст всех троих.
|
||||||
|
|
||||||
## 3. Взаимодействие (MPSC)
|
## 3. Взаимодействие (MPSC)
|
||||||
|
|
||||||
Два продюсера, один консюмер. **Данные** и **сигнал пробуждения** разделены:
|
Три продюсера, один консюмер (третий — dispatcher opto, §3.4). **Данные** и **сигнал
|
||||||
|
пробуждения** разделены:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
TMR["демон таймеров<br/>input_poll_cb 5 мс<br/>bsp_button_poll (debounce)"]
|
TMR["демон таймеров<br/>input_poll_cb 5 мс<br/>bsp_button_poll + bsp_opto_process"]
|
||||||
MT["menu_task (+3)<br/>модель меню g_menu"]
|
MT["menu_task (+3)<br/>модель меню g_menu"]
|
||||||
RX["sul_rx_task (+1)<br/>CAN→decode→controller<br/>WDOG безусловно"]
|
RX["sul_rx_task (+1)<br/>CAN→decode→controller<br/>WDOG безусловно"]
|
||||||
RT["render_task (+2)<br/>gfx_present*()"]
|
RT["render_task (+2)<br/>gfx_present*()"]
|
||||||
|
DSP["dispatcher_poll()<br/>(app/dispatcher.c)<br/>g_dispatcher_indication"]
|
||||||
|
|
||||||
TMR -."залатанные события кнопок".-> MT
|
TMR -."залатанные события кнопок".-> MT
|
||||||
|
TMR -."безусловный опрос, каждый тик".-> DSP
|
||||||
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
|
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
|
||||||
RX -->|"xQueueOverwrite (render_msg_t)<br/>+ xTaskNotifyGive"| RT
|
RX -->|"xQueueOverwrite (render_msg_t)<br/>+ xTaskNotifyGive"| RT
|
||||||
|
DSP -->|"xTaskNotifyGive<br/>(изменение вызов/ответ)"| RT
|
||||||
MT -."g_menu_active (мягкая пауза)".-> RX
|
MT -."g_menu_active (мягкая пауза)".-> RX
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Очередь `g_render_queue`** (глубина 1, `xQueueOverwrite`) — только плечо
|
- **Очередь `g_render_queue`** (глубина 1, `xQueueOverwrite`) — только плечо
|
||||||
`sul_rx→render`, несёт `render_msg_t` (diff + результат). Семантика «важно только
|
`sul_rx→render`, несёт `render_msg_t` (diff + результат). Семантика «важно только
|
||||||
последнее состояние»: рендер не обязан успевать за каждым кадром CAN.
|
последнее состояние»: рендер не обязан успевать за каждым кадром CAN. У dispatcher СВОЕЙ
|
||||||
|
очереди нет — состояние (`g_dispatcher_indication`) не история, читается напрямую (как
|
||||||
|
`g_menu`), очередь тут не нужна (сравнение с прошлым значением — внутри `render_task`).
|
||||||
- **`ulTaskNotifyTake(pdTRUE, portMAX_DELAY)`** в `render_task` — event-driven, без
|
- **`ulTaskNotifyTake(pdTRUE, portMAX_DELAY)`** в `render_task` — event-driven, без
|
||||||
поллинга; несколько notify от обоих продюсеров схлопываются в одно пробуждение (та же
|
поллинга; несколько notify от ЛЮБОГО из трёх продюсеров схлопываются в одно пробуждение
|
||||||
семантика «важно только последнее»).
|
(та же семантика «важно только последнее»). `render_task` не различает, КТО его разбудил —
|
||||||
|
каждую итерацию просто проверяет все три источника состояния заново.
|
||||||
- **Приоритет «меню важнее индикации» не кодируется в уведомлении**: проснувшись,
|
- **Приоритет «меню важнее индикации» не кодируется в уведомлении**: проснувшись,
|
||||||
`render_task` первым делом проверяет `menu_is_open(&g_menu)` — если меню открыто,
|
`render_task` первым делом проверяет `menu_is_open(&g_menu)` — если меню открыто,
|
||||||
очередь индикации даже не читается.
|
очередь индикации и dispatcher даже не читаются (см. §3.1 ниже — dispatcher копится, не
|
||||||
|
теряется).
|
||||||
- **Модель меню `g_menu`** мутирует только `menu_task`; `render_task` читает её для
|
- **Модель меню `g_menu`** мутирует только `menu_task`; `render_task` читает её для
|
||||||
отрисовки после notify (happens-before через нотификацию — как у очереди).
|
отрисовки после notify (happens-before через нотификацию — как у очереди).
|
||||||
|
- **`g_dispatcher_indication`** мутирует только `dispatcher_poll()` (`app/dispatcher.c`),
|
||||||
|
вызываемый БЕЗУСЛОВНО каждый тик из контекста демона таймеров — приоритет ВЫШЕ
|
||||||
|
`menu_task` (не реактивно на колбэк bsp_opto — см. PLAN.md §3.4 про баг первой,
|
||||||
|
реактивной версии). `render_task` снимает копию в
|
||||||
|
локальную переменную ОДИН раз за итерацию, а не перечитывает несколько раз (иначе
|
||||||
|
возможна гонка того же класса, что чинили для курсора меню при быстрой навигации, см.
|
||||||
|
PLAN.md, Фаза 3.2.4).
|
||||||
|
|
||||||
|
### 3.1 Диспетчерский вход и пауза меню (§3.4)
|
||||||
|
|
||||||
|
Приоритет диспетчерского сигнала («вызов»/«ответ») — **высший из всех режимов индикации**,
|
||||||
|
безусловно перекрывает и обычную позицию, и любой режим СУЛ; работает независимо от связи со
|
||||||
|
станцией (ARCH §8 п.3 — это «локальный вход», не данные СУЛ). Тем не менее, пока меню открыто,
|
||||||
|
`render_task` НЕ применяет его к экрану — как в `OLD_PROJECT_TFT8_UKL` (`tft_refresh_task`
|
||||||
|
целиком пропускает кадр, пока `in_menu_screen`), тот же принцип, что уже даёт мягкая пауза
|
||||||
|
`sul_rx_task`:
|
||||||
|
|
||||||
|
- **Debounce и опрос не паузятся** — `bsp_opto_process()` и `dispatcher_poll()` в
|
||||||
|
`input_poll_cb` работают независимо от `g_menu_active`, дёшево (как и button).
|
||||||
|
- **Применение к экрану — только на закрытии меню.** Пока меню открыто, оконный композит
|
||||||
|
перерисовывает ТОЛЬКО окно 480×272 (Фаза 3.2.4) — полный кадр индикации вне окна заморожен;
|
||||||
|
применить смену диспетчера немедленно значило бы либо сломать оконную оптимизацию (полный
|
||||||
|
редрав ради маленькой иконки), либо рисовать поверх окна меню. `g_dispatcher_indication`
|
||||||
|
тем временем просто держит ПОСЛЕДНЕЕ значение — ничего не теряется, отражается сразу по
|
||||||
|
закрытии меню тем же путём, что уже восстанавливает индикацию (`ui_fallback_render_initial()`).
|
||||||
|
|
||||||
### Мягкая пауза `sul_rx_task` на время меню
|
### Мягкая пауза `sul_rx_task` на время меню
|
||||||
|
|
||||||
|
|
@ -124,6 +157,7 @@ flowchart LR
|
||||||
| `g_display_ready` | bringup | render, sul_rx | `volatile bool`, одно-writer |
|
| `g_display_ready` | bringup | render, sul_rx | `volatile bool`, одно-writer |
|
||||||
| `g_menu_active` | menu | sul_rx | `volatile bool`, одно-writer |
|
| `g_menu_active` | menu | sul_rx | `volatile bool`, одно-writer |
|
||||||
| `g_menu` | menu | render | notify (happens-before) |
|
| `g_menu` | menu | render | notify (happens-before) |
|
||||||
|
| `g_dispatcher_indication` | dispatcher_poll() (демон таймеров, каждый тик) | render | `volatile enum`, один writer; render снимает копию один раз за итерацию (см. §3) |
|
||||||
| лог-буфер `utils/log` | все задачи | — | мьютекс `port/log/src/log_mutex.c` (FreeRTOS strong-override; включён в сборку `app` — до Фазы 3.2.4 был `#if 0`, гонка) |
|
| лог-буфер `utils/log` | все задачи | — | мьютекс `port/log/src/log_mutex.c` (FreeRTOS strong-override; включён в сборку `app` — до Фазы 3.2.4 был `#if 0`, гонка) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ add_executable(
|
||||||
src/app/task_sul_rx.c # приём CAN → decode → controller (WDOG/heartbeat безусловно, CAN-работа под !g_menu_active)
|
src/app/task_sul_rx.c # приём CAN → decode → controller (WDOG/heartbeat безусловно, CAN-работа под !g_menu_active)
|
||||||
src/app/task_menu.c # модель меню: кнопки/hold-to-enter/nav/edit/save — НЕ рисует
|
src/app/task_menu.c # модель меню: кнопки/hold-to-enter/nav/edit/save — НЕ рисует
|
||||||
src/app/task_render.c # презентация: единственный вызывающий gfx_present(), event-driven
|
src/app/task_render.c # презентация: единственный вызывающий gfx_present(), event-driven
|
||||||
|
src/app/dispatcher.c # диспетчерский вход opto IN1/IN2 (§3.4) — не задача, обвязка bsp_opto
|
||||||
${BSP_GENERATED}/clock_config.c # BOARD_BootClockRUN (зовётся board_hw_init)
|
${BSP_GENERATED}/clock_config.c # BOARD_BootClockRUN (зовётся board_hw_init)
|
||||||
${BSP_STARTUP_FILE}
|
${BSP_STARTUP_FILE}
|
||||||
${BSP_SYSCALLS_FILE}
|
${BSP_SYSCALLS_FILE}
|
||||||
|
|
@ -117,6 +118,7 @@ target_link_libraries(
|
||||||
PRIVATE bsp_board
|
PRIVATE bsp_board
|
||||||
bsp_led
|
bsp_led
|
||||||
bsp_button
|
bsp_button
|
||||||
|
bsp_opto # диспетчерский вход IN1/IN2 (§3.4), app/dispatcher.c
|
||||||
bsp_wdog
|
bsp_wdog
|
||||||
bsp_boot_state
|
bsp_boot_state
|
||||||
bsp_qspi_flash
|
bsp_qspi_flash
|
||||||
|
|
@ -128,6 +130,8 @@ target_link_libraries(
|
||||||
# Фаза 1: walking skeleton (CAN → декодер → контроллер → fallback-рендер)
|
# Фаза 1: walking skeleton (CAN → декодер → контроллер → fallback-рендер)
|
||||||
tft_app_sul
|
tft_app_sul
|
||||||
tft_app_sul_transport_can
|
tft_app_sul_transport_can
|
||||||
|
# Фаза 3.3: демо-протокол (транспорт — синтетический, без bsp)
|
||||||
|
tft_app_sul_transport_demo
|
||||||
tft_app_controller
|
tft_app_controller
|
||||||
tft_app_gfx
|
tft_app_gfx
|
||||||
tft_app_ui_fallback
|
tft_app_ui_fallback
|
||||||
|
|
|
||||||
|
|
@ -331,10 +331,10 @@ marks_both_pending`, аппаратно на реальном обрыве св
|
||||||
| **3.1 `services/settings_store`** ✅ | Персист ядра (magic/version/CRC, load/save/get/defaults) через `bsp_qspi_flash`, фикс-сектор `0x450000` (§10, размер-независимо). Сразу: `proto_slice[0]` → `nku_address` в декодер (замена хардкода `=0`). |
|
| **3.1 `services/settings_store`** ✅ | Персист ядра (magic/version/CRC, load/save/get/defaults) через `bsp_qspi_flash`, фикс-сектор `0x450000` (§10, размер-независимо). Сразу: `proto_slice[0]` → `nku_address` в декодер (замена хардкода `=0`). |
|
||||||
| **3.2 движок меню + рендер** ✅ (рендер — на одном буфере, см. 3.2.4) | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.5–2 с); BUTTON_1=следующий, короткое BUTTON_2=выбор/инкремент. Опрос ввода — **софт-таймер 5 мс** (не задача; масштабируется на opto). Модальный экран. |
|
| **3.2 движок меню + рендер** ✅ (рендер — на одном буфере, см. 3.2.4) | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.5–2 с); BUTTON_1=следующий, короткое BUTTON_2=выбор/инкремент. Опрос ввода — **софт-таймер 5 мс** (не задача; масштабируется на opto). Модальный экран. |
|
||||||
| **3.2.4 фундамент рендера (double-buffer + PXP)** ✅ | Перенос из Фазы 4. Убирает tearing/тормоза, снимает костыль частичной отрисовки. Гибрид bpp (AS=8888/PS+FB=565) + оконный композит меню — см. раздел ниже. Подтверждено на стенде (4 цикла) + реальной СУЛ. |
|
| **3.2.4 фундамент рендера (double-buffer + PXP)** ✅ | Перенос из Фазы 4. Убирает tearing/тормоза, снимает костыль частичной отрисовки. Гибрид bpp (AS=8888/PS+FB=565) + оконный композит меню — см. раздел ниже. Подтверждено на стенде (4 цикла) + реальной СУЛ. |
|
||||||
| **3.3 per-protocol дескрипторы** | `sul_settings_desc_t`: протокол регистрирует параметры (NKU-CAN: адрес 0..15); секция меню строится из дескриптора. |
|
| **3.3 per-protocol дескрипторы** 🟨 | `sul_settings_desc_t` (§8): протокол регистрирует параметры, секция меню строится из дескриптора (НКУ-CAN: адрес 0..15). Плюс демо-протокол (второй драйвер реестра) — тест протокол-агностичности механизма. Код-комплит, host зелёный; ждёт стенда (см. раздел ниже). |
|
||||||
| **3.4 opto-входы** | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). |
|
| **3.4 opto-входы** 🟨 | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). Код-комплит; ждёт стенда (см. раздел ниже). |
|
||||||
| **3.5 удалённая адресация NKU-CAN** | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). |
|
| **3.5 удалённая адресация NKU-CAN** ✅ | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). По спецификации `REMOTE_ADDRES_SETUP.pdf` + согласованному уточнению (X командного кадра сверяется с анонсом). Подтверждено на стенде 2026-07-23 (со второй попытки — wildcard-фильтры, см. раздел ниже). |
|
||||||
| **3.6 тумблер логов** | `log_enabled` в настройках + пункт меню + рантайм-гейт `log_set_enabled()` поверх компайл-тайм `LOG_LEVEL`. NB: продакшн-сборка — с `LOG_LEVEL >= INFO`, иначе гейтить нечего (макросы вырезаны). |
|
| **3.6 тумблер логов** ✅ | `log_enabled` в настройках + пункт меню + рантайм-гейт `log_set_enabled()` поверх компайл-тайм `LOG_LEVEL`. Подтверждено на стенде 2026-07-22. NB: продакшн-сборка — с `LOG_LEVEL >= INFO`, иначе гейтить нечего (макросы вырезаны). |
|
||||||
|
|
||||||
### 3.2.4 — Фундамент рендера: double-buffer + PXP (✅)
|
### 3.2.4 — Фундамент рендера: double-buffer + PXP (✅)
|
||||||
|
|
||||||
|
|
@ -513,6 +513,18 @@ marks_both_pending`, аппаратно на реальном обрыве св
|
||||||
> финальный образ.
|
> финальный образ.
|
||||||
>
|
>
|
||||||
> **Фаза 3.2.4 — ✅.**
|
> **Фаза 3.2.4 — ✅.**
|
||||||
|
>
|
||||||
|
> 🐛 **Найден и починен позже, при обычном использовании (не отдельная HW-сессия) — раздвоение
|
||||||
|
> курсора при быстрой навигации.** Root-cause: `menu_view_render(&g_menu)` читает `p_ctx->cur`
|
||||||
|
> заново на КАЖДОЙ итерации цикла отрисовки строк (до 6 раз за кадр), а не один раз в начале;
|
||||||
|
> `menu_task` выше по приоритету и может вытеснить `render_task` ПОСРЕДИ этого цикла на любое
|
||||||
|
> нажатие кнопки. Весь рендер занимает десятки мс (пункт выше — pxp≈30мс) против 5-мс каденции
|
||||||
|
> `menu_task` — при быстрой навигации окно гонки большое: `.cur` меняется между итерациями, и
|
||||||
|
> курсор оказывается нарисован сразу на двух строках одного кадра. **Фикс:** `menu_view_render()`
|
||||||
|
> снимает копию `*p_ctx` в локальную переменную на входе и работает только с ней — окно гонки
|
||||||
|
> схлопывается с длительности всего рендера до одного присваивания структуры (тот же класс фикса,
|
||||||
|
> что уже применялся для `proto_slice`/CAN-фильтров — снимок один раз, не перечитывать shared
|
||||||
|
> state в цикле). Не HIL-тестируется (рендер), проверяется на стенде.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -553,6 +565,332 @@ busy-wait Complete), затем свап: `xSemaphoreTake(frame_done)` (синх
|
||||||
(там же §PLAN Фаза 4 «Double buffering в gfx» — теперь закрывается здесь, в Фазе 4 остаётся только
|
(там же §PLAN Фаза 4 «Double buffering в gfx» — теперь закрывается здесь, в Фазе 4 остаётся только
|
||||||
PXP-блит спрайтов и layout).
|
PXP-блит спрайтов и layout).
|
||||||
|
|
||||||
|
### 3.3 — Протокол-дескрипторы + демо-протокол (🟨)
|
||||||
|
|
||||||
|
> **Статус: код-комплит, host-покрытие зелёное (24 host-теста tft_app_*, из них
|
||||||
|
> 23 новых/тронутых за этот под-шаг); Debug ARM-сборка линкуется чисто.
|
||||||
|
> Ожидает подтверждения на стенде** (по прецеденту Фаз 1/2/3.2.4 — статус ✅
|
||||||
|
> не раньше HW-проверки).
|
||||||
|
|
||||||
|
**Сделано:**
|
||||||
|
- **`sul_settings_desc_t`** (ARCH §8) — `domain/sul.h`: свой `sul_settings_type_t`
|
||||||
|
(BYTE/SELECT/BOOL) — НЕ переиспользует `menu_item_type_t` (домен не должен
|
||||||
|
знать презентацию, §4); `sul_settings_entry_t`/`sul_settings_desc_t`, поле
|
||||||
|
`.p_settings` в `sul_driver_t`. НКУ-CAN регистрирует адрес 0..15.
|
||||||
|
- **Реестр** (`sul_registry.c`) — `sul_registry_set_active(id)`/
|
||||||
|
`sul_registry_count()`; активный протокол — внутренний `s_active_id`,
|
||||||
|
app-слой толкает его из `settings_device_t.protocol_id` (реестр
|
||||||
|
settings_store НЕ читает — тот же паттерн, что `nku_can_set_address()`).
|
||||||
|
Новое поле `.p_ctx` в `sul_driver_t` — контекст decode() каждого драйвера
|
||||||
|
теперь статика, живёт постоянно в реестре (`sul_registry_init()`), не
|
||||||
|
пересоздаётся при смене протокола.
|
||||||
|
- **Меню** (`menu_tree.c`) — секция "Протокол" (`T_PROTO`/`T_PROTO_PARAM`)
|
||||||
|
строится из `sul_registry_active()->p_settings` через
|
||||||
|
`menu_tree_refresh_protocol_section(settings_t*)` (сигнатура с explicit
|
||||||
|
параметром — как у `menu_init()`, не через глобальный `settings_store_get_
|
||||||
|
mutable()`, чтобы не тянуть QSPI-адаптер в host-тест дерева); вызывается
|
||||||
|
при bringup и после каждого действия в меню. Дерево стало НЕ `const`
|
||||||
|
(было `static const K_TREE[]`) — секция протокола мутируется рантаймом.
|
||||||
|
- **Демо-протокол** (`domain/sul/demo/`, `domain/sul/transport/demo/`) —
|
||||||
|
второй драйвер реестра. Скриптованная "поездка" по кругу (25 шагов,
|
||||||
|
согласовано с пользователем): этаж 1↔11, промежуточные остановки на 7
|
||||||
|
(едет вверх) и 3 (едет вниз), гонг на остановках и на конечных этажах.
|
||||||
|
Транспорт тривиален (`sul_transport_demo_receive()` — всегда успешно и
|
||||||
|
мгновенно, без состояния); весь темп ведёт `demo_decode()` сам через
|
||||||
|
счётчик тиков в ctx (20/10/4 тика/шаг — медленно/норма/быстро) — decode()
|
||||||
|
остаётся ЧИСТОЙ функцией (host-тест по числу вызовов), содержимое кадра
|
||||||
|
игнорирует целиком (referenced-, но decode-less по сути, см.
|
||||||
|
HANDOFF_PHASE3_TAIL.md п.1). Даёт свой дескриптор — параметр "Скорость"
|
||||||
|
(SELECT), намеренно НЕ похож формой на "адрес" НКУ-CAN (BYTE) — тест того,
|
||||||
|
что дескрипторный механизм не завязан на адрес-подобный параметр.
|
||||||
|
- **`task_sul_rx.c` обобщена** — раньше жёстко держала `nku_can_ctx_t` на
|
||||||
|
стеке задачи и звала CAN-специфичные функции безусловно; теперь
|
||||||
|
диспетчеризация транспорта + реаппликации настроек — по
|
||||||
|
`sul_registry_active()->id`, ветка на 2 случая (НКУ-CAN/демо). decode()/
|
||||||
|
`.p_ctx`/меню/настройки остаются протокол-агностичными; только эта ветка
|
||||||
|
трогается Фазой 8 при протоколе на новом транспорте. **Осознанный
|
||||||
|
компромисс:** транспорт/apply-settings НЕ вынесены function pointer'ами в
|
||||||
|
`sul_driver_t` — это заставило бы `tft_app_sul` (реестр) линковать
|
||||||
|
`bsp_can`, ломая host-тестируемость реестра без bsp; транспорт остаётся
|
||||||
|
app-слоем (wiring, ARCH §4), домен/реестр — чистыми.
|
||||||
|
- **Найдена и закрыта реальная опасность** (не гипотетическая, вскрылась
|
||||||
|
при проектировании, не на стенде): `proto_slice[0]` — общий байт для
|
||||||
|
ЛЮБОГО активного протокола (адрес НКУ-CAN 0..15 vs скорость демо 0..2).
|
||||||
|
При переключении протокола в меню без клампа "протухшее" значение (напр.
|
||||||
|
адрес 15) читалось бы как `options[value]` за пределами массива меток
|
||||||
|
нового протокола — мусорное чтение памяти в рендере menu_view.c. Клампится
|
||||||
|
в `menu_tree_refresh_protocol_section()` в момент переключения — фикс в
|
||||||
|
корне (момент создания рассинхрона), не защита на каждом сайте чтения.
|
||||||
|
Host-тест `test_stale_value_clamped_on_protocol_switch` фиксирует.
|
||||||
|
|
||||||
|
**Host-тесты (новые за этот под-шаг):** `test_sul_registry` (9 на момент 3.3 —
|
||||||
|
выбор/защита от неизвестного id/дескрипторы обоих протоколов/раздельные ctx;
|
||||||
|
+1 позже в 3.5, см. её раздел),
|
||||||
|
`test_tft_app_menu_tree` (4 — секция строится из дескриптора для НКУ-CAN и
|
||||||
|
демо + кламп протухшего значения + edit end-to-end через реальный `menu.c`),
|
||||||
|
`test_sul_demo` (10 — маршрут/промежуточные остановки/цикл/скорость/игнор
|
||||||
|
содержимого кадра).
|
||||||
|
|
||||||
|
**Осознанно не тронуто в этом под-шаге (отдельный пункт хвоста):** 3.4 (опто)
|
||||||
|
— идёт следующим под-шагом, без пересечений по файлам с этим (3.5 закрыт
|
||||||
|
отдельно сразу вслед, см. ниже — тот же модуль `nku_can`, логично рядом).
|
||||||
|
|
||||||
|
### 3.4 — Опто-входы (диспетчерский вход) (🟨)
|
||||||
|
|
||||||
|
> **Статус: код-комплит (вторая итерация — первая словила баг на стенде, см.
|
||||||
|
> ниже), ARM Debug (111/111) линкуется и подписан
|
||||||
|
> (`build/Debug/signed/app_slot_a.bin`); host-покрытие без регрессий (24/24,
|
||||||
|
> новых тестов нет — см. ниже почему). Ждёт ПОВТОРНОГО подтверждения на
|
||||||
|
> стенде** (нужны реальные сигналы на IN1/IN2 — по прецеденту предыдущих
|
||||||
|
> под-шагов, ✅ не раньше HW-проверки).
|
||||||
|
|
||||||
|
**Дизайн (согласован с пользователем ДО кода):**
|
||||||
|
1. Диспетчерские вызов/ответ — сигналы с независимого оборудования (диспетчерский
|
||||||
|
пульт), не со станции управления лифтом — поэтому не идут через `controller`/
|
||||||
|
таблицу приоритетов режимов ([MODE_PRIORITY.md](../../docs/tft_app/MODE_PRIORITY.md),
|
||||||
|
та — только для ортогональных сигналов СУЛ); это отдельный вход прямо в
|
||||||
|
`ui.render()` (ARCH §4, §8 п.3 — «локальный вход», не `sul_result_t`).
|
||||||
|
2. Приоритет — высший из всех: безусловно перекрывает и обычную индикацию, и
|
||||||
|
любой режим СУЛ (пожар/перегруз/…), работает даже без связи со станцией
|
||||||
|
(dispatcher — независимое оборудование).
|
||||||
|
3. ОТВЕТ (IN2) перебивает ВЫЗОВ (IN1), если оба почему-то активны одновременно.
|
||||||
|
|
||||||
|
Референс поведения (не архитектуры) — `OLD_PROJECT_TFT8_UKL/source/main_programm.c`
|
||||||
|
(`tft_refresh_task`): там диспетчерская проверка идёт ПОСЛЕДНЕЙ в каждом кадре и
|
||||||
|
безусловно перезаписывает `icon_img_ptr`; здесь тот же эффект достигается проверкой
|
||||||
|
ПЕРВОЙ в `render()` с безусловным `return`.
|
||||||
|
|
||||||
|
**Реализация:**
|
||||||
|
- **`dispatcher_indication_t`** ([fallback.h](../../firmware/tft_app/src/ui/fallback/include/ui/fallback.h))
|
||||||
|
— `NONE`/`CALL`/`ANSWER`; `ui_fallback_render_initial()`/`ui_fallback_render()`
|
||||||
|
берут его ОТДЕЛЬНЫМ параметром — не часть `sul_result_t`/`indication_task_t`
|
||||||
|
(не данные СУЛ, другая природа изменения).
|
||||||
|
- **`app/dispatcher.c`** (новый файл; НЕ задача — нет своего `while(true)`) —
|
||||||
|
тонкая обвязка уже готового BSP-драйвера `bsp_opto` (level-mode + debounce на
|
||||||
|
IN1/IN2, реализован заранее). `dispatcher_init()` конфигурирует оба канала
|
||||||
|
(`BSP_OPTO_MODE_LEVEL`, debounce 10 мс, **без колбэков** —
|
||||||
|
`callbacks = {NULL, NULL, NULL}`) и сразу захватывает стартовое состояние
|
||||||
|
(`resolve_indication()`) — иначе уже висящий на старте вызов не отобразился
|
||||||
|
бы до первого `dispatcher_poll()`. `dispatcher_poll()` вызывается БЕЗУСЛОВНО
|
||||||
|
каждый тик `input_poll_cb` (не реактивно — см. находку ниже): заново читает
|
||||||
|
`bsp_opto_read()` для обоих каналов (`resolve_indication()` — ОТВЕТ
|
||||||
|
перебивает ВЫЗОВ без доп. состояния) и будит `render_task`, только если
|
||||||
|
итог реально изменился.
|
||||||
|
- **Найден и закрыт баг на стенде (первая итерация, реактивная схема).**
|
||||||
|
Первая версия `dispatcher.c` регистрировала колбэки bsp_opto
|
||||||
|
(`callbacks = {on_opto_change, on_opto_change, NULL}`) и пересчитывала
|
||||||
|
`g_dispatcher_indication` ТОЛЬКО изнутри колбэка, вызываемого
|
||||||
|
`bsp_opto_process()` на подтверждённую смену канала. На стенде: «ВЫЗОВ» не
|
||||||
|
сбрасывался при снятии сигнала IN1 — залипал; при этом ОТВЕТ на IN2
|
||||||
|
корректно перебивал (перекрытие по приоритету не сломано), но после снятия
|
||||||
|
IN2 индикация возвращалась не в NONE, а обратно в устаревший ВЫЗОВ; через
|
||||||
|
какое-то время само исправлялось. **Причина — не гонка в `bsp_opto`**
|
||||||
|
(пользователь подтвердил: тот же `bsp_opto` эксплуатируется в
|
||||||
|
`OLD_PROJECT_TFT8_UKL` без колбэков и без единой подобной проблемы), а
|
||||||
|
отсутствие самовосстановления в чисто реактивной схеме: у неё ровно один
|
||||||
|
шанс заметить каждое изменение (сам колбэк), и если он почему-то не
|
||||||
|
пересчитал итог заново с первой попытки — состояние виснет НАВСЕГДА, пока
|
||||||
|
не прилетит случайный следующий фронт на любом канале и не пересчитает всё
|
||||||
|
заново попутно. **Фикс** — как в `OLD_PROJECT_TFT8_UKL`
|
||||||
|
(`tft_refresh_task`/`bsp_opto_read()` каждую итерацию, `callbacks={NULL,...}`,
|
||||||
|
фронт детектирует сам потребитель): убрали колбэки, `dispatcher_poll()`
|
||||||
|
безусловно опрашивает `bsp_opto_read()` каждые 5 мс сам, без ожидания
|
||||||
|
уведомления от `bsp_opto` — тот же принцип, что и `bsp_button` (тоже без
|
||||||
|
колбэков, тоже опрашивается каждый тик, см. `bsp_button_poll()`). Единичный
|
||||||
|
сбой при этом не критичен — самовосстанавливается на следующем же тике.
|
||||||
|
- **`fallback.c`** — `render()` проверяет `dispatcher_label()` ПЕРВЫМ, до
|
||||||
|
`mode_label()`, с безусловным `return` при непустой метке (слова временные —
|
||||||
|
«ВЫЗОВ»/«ОТВЕТ», текст `SystemFont` в слоте режимов `MODE_Y`; полноэкранная
|
||||||
|
графика с иконками — Фаза 4/5).
|
||||||
|
- **`task_render.c`** — третий продюсер MPSC (были sul_rx_task + menu_task).
|
||||||
|
`g_dispatcher_indication` снимается в локальную переменную ОДИН раз за
|
||||||
|
итерацию (тот же приём, что чинил гонку курсора меню, Фаза 3.2.4) — без
|
||||||
|
снимка два обращения в теле цикла могли бы увидеть разные значения. Сравнение
|
||||||
|
с `last_dispatcher` — самостоятельная ТРЕТЬЯ причина полной перерисовки
|
||||||
|
(кроме старта и закрытия меню): opto может разбудить `render_task`, когда в
|
||||||
|
очереди СУЛ нет ни одного нового кадра.
|
||||||
|
- **Пауза на время меню** — как в `OLD_PROJECT_TFT8_UKL`: пока меню открыто,
|
||||||
|
`render_task` НЕ применяет смену диспетчера к экрану (оконный композит Фазы
|
||||||
|
3.2.4 перерисовывает только окно 480×272 — применить смену немедленно значило
|
||||||
|
бы либо сломать эту оптимизацию, либо рисовать поверх окна меню). Debounce и
|
||||||
|
опрос при этом НЕ паузятся — `g_dispatcher_indication` просто держит
|
||||||
|
последнее значение, ничего не теряется, отражается сразу по закрытии меню тем
|
||||||
|
же путём, что уже восстанавливает обычную индикацию.
|
||||||
|
- **`input_poll_cb`** ([task_menu.c](../../firmware/tft_app/src/app/task_menu.c))
|
||||||
|
— три вызова подряд, тем же софт-таймером (5 мс, демон программных таймеров
|
||||||
|
— высший приоритет в системе): `bsp_button_poll()` →
|
||||||
|
`bsp_opto_process()` (debounce IN1/IN2) → `dispatcher_poll()` (читает
|
||||||
|
результат debounce, безусловно, каждый тик).
|
||||||
|
- **Пины подтверждены заранее** (`bsp/generated/TFT_Board.mex`/`pin_mux.h`) —
|
||||||
|
IN1/IN2 уже размечены под opto на уровне платы, доп. переразводки не
|
||||||
|
потребовалось.
|
||||||
|
- **Логирование строго по фронтам** (`LOG_TAG "dispatcher"`) — `dispatcher_poll()`
|
||||||
|
зовёт `log_transition()` ТОЛЬКО когда индикация реально изменилась (не на
|
||||||
|
каждый тик): ВЫКЛ→ВКЛ — `"mode %s appeared"`, ВКЛ→ВЫКЛ — `"mode %s disabled"`
|
||||||
|
(`%s` — сырое имя enum, CALL/ANSWER/NONE, без Cyrillic-меток — по образцу
|
||||||
|
остального технического текста в LOG_I, см. `task_sul_rx.c`). Прямой переход
|
||||||
|
CALL↔ANSWER (оба не NONE — ОТВЕТ перебивает ВЫЗОВ или наоборот) печатает ОБЕ
|
||||||
|
строки (выключение старого + появление нового) — та же семантика edge-log,
|
||||||
|
что и одиночный переход в/из NONE, без частного случая. Полезно для HW-отладки
|
||||||
|
именно такого класса бага, что нашёлся выше (сырые переходы каналов видны в
|
||||||
|
логе независимо от того, что показывает экран).
|
||||||
|
|
||||||
|
**Осознанно без новых host-тестов.** `dispatcher.c`/изменения в `task_render.c`/
|
||||||
|
`fallback.c` — app-слой (wiring задач, порядок MPSC-пробуждений, порядок
|
||||||
|
отрисовки) и презентация поверх уже готового `bsp_opto` (BSP, HW-регистры —
|
||||||
|
host не тестируется в принципе), не новая доменная логика: `dispatcher_label()`
|
||||||
|
и приоритет ОТВЕТ>ВЫЗОВ — тривиальная чистая функция без ветвления состояния, не
|
||||||
|
отдельный модуль со своим контрактом (в отличие, например, от
|
||||||
|
`sul_settings_desc_t` в 3.3). Тот же паттерн, что и остальной app-слой
|
||||||
|
(`task_*.c` исторически без host-тестов — ARCH.md: host-тестируем только L2
|
||||||
|
domain). 24/24 host-тестов остаются зелёными без регрессий.
|
||||||
|
|
||||||
|
**Тесты не пройденные (нужен стенд):** **приоритетно — сценарий из репорта
|
||||||
|
бага выше** (снять IN1 после ВЫЗОВА → должен вернуться NONE, не залипать;
|
||||||
|
затем подать/снять IN2 несколько раз подряд, включая с активным IN1 — ОТВЕТ
|
||||||
|
должен корректно перебивать и корректно возвращать НЕ устаревшее состояние
|
||||||
|
при снятии); сигнал на IN1 → «ВЫЗОВ» на экране; сигнал на IN2 → «ОТВЕТ»;
|
||||||
|
диспетчерский вход поверх активного спецрежима СУЛ (пожар/перегруз/…) — должен
|
||||||
|
перекрывать; диспетчерский вход при полностью оборванной связи со станцией
|
||||||
|
(«--») — тоже должен отображаться; открыть меню при активном диспетчерском
|
||||||
|
входе, закрыть — индикация должна восстановиться корректно; быстрое
|
||||||
|
чередование IN1/IN2 — debounce не должен давать дребезг метки.
|
||||||
|
|
||||||
|
### 3.5 — Удалённая адресация НКУ-CAN (✅)
|
||||||
|
|
||||||
|
> **Статус: подтверждено на стенде (2026-07-23), с реальной станцией** — после
|
||||||
|
> фикса wildcard-фильтров (см. ниже; первая HW-проверка дала «ноль реакции»
|
||||||
|
> именно из-за них). Host-покрытие зелёное (45 тестов decode + 10 реестра,
|
||||||
|
> +9/+1 новых); ARM Debug линкуется, подписан.
|
||||||
|
|
||||||
|
**Спецификация — `REMOTE_ADDRES_SETUP.pdf`** (предоставлен пользователем,
|
||||||
|
`Документы/Протоколы связи СУЛ/НКУ МППЛ/`), пять пунктов:
|
||||||
|
1. Кадр `0x4X1` — X всегда = адрес станции управления; читать в ОЗУ, не в флеш.
|
||||||
|
2. Кадр `0x5XB` (X — любой по тексту документа) — команда в байте 4 (`data[3]`,
|
||||||
|
0-индексация; старший нибль).
|
||||||
|
3. Команда «2» — записать адрес из п.1 в энергонезависимую память.
|
||||||
|
4. Повторная запись — только если новый адрес отличается от сохранённого.
|
||||||
|
5. Контроллер станции держит команду «2» некоторое время, затем сбрасывает в «0».
|
||||||
|
|
||||||
|
**Согласованное уточнение (расходится с буквой PDF п.2):** пользователь также
|
||||||
|
прислал референсную C-реализацию (`msg_receiver_task`/`apply_remote_addr_filters`
|
||||||
|
из легаси), которая ДОПОЛНИТЕЛЬНО сверяет X командного кадра `0x5XB` с X из
|
||||||
|
последнего анонса `0x4X1` — PDF говорит «X может иметь любое значение».
|
||||||
|
Решение (пользователь): следовать референсному коду, X должен совпадать.
|
||||||
|
|
||||||
|
**Реализация:**
|
||||||
|
- **`nku_can_ctx_t`** ([nku_can.h](../../firmware/tft_app/src/domain/sul/nku_can/include/domain/sul/nku_can.h))
|
||||||
|
— два новых поля: `remote_addr_candidate` (липкий, последний X из 0x4X1) и
|
||||||
|
`pending_remote_write_addr` (транзитный — валиден ТОЛЬКО сразу после вызова
|
||||||
|
decode(), в котором распознана команда «2» с совпавшим X; сбрасывается в
|
||||||
|
`NKU_REMOTE_ADDR_NONE` в начале КАЖДОГО вызова).
|
||||||
|
- **`check_remote_address()`** ([nku_can.c](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c))
|
||||||
|
— независимая side-проверка на сыром ID/data, не влияет на классификацию
|
||||||
|
PACKET1..5. Важный нюанс: `0x5XB` при X = наш собственный адрес — это
|
||||||
|
БУКВАЛЬНО тот же ID, что `PACKET4_BASE`; кадр обрабатывается в обоих
|
||||||
|
качествах независимо (host-тест `test_remote_check_does_not_interfere_with_
|
||||||
|
own_packet4` это фиксирует). Кадр с `len < 4` — не падает, просто не
|
||||||
|
триггерит (свой гейт, общий `PROTO_DLC`-гейт на wildcard-ID не распространяется).
|
||||||
|
- **decode() НЕ пишет settings** (остаётся чистой функцией) — сигнал уходит
|
||||||
|
через generic-канал `sul_take_pending_write_fn_t`/`sul_slice_write_t`
|
||||||
|
(domain/sul.h) — **не** через ветку `if (id == SUL_PROTOCOL_NKU_CAN)` в
|
||||||
|
`task_sul_rx.c`, как было в первой версии. Пользователь верно заметил
|
||||||
|
архитектурную утечку: транспорт-диспетчеризация (§6/§7 ADDING_PROTOCOL.md)
|
||||||
|
ОБЯЗАНА знать протокол (это wiring), а вот "инициировать запись settings"
|
||||||
|
— чистая бизнес-логика протокола, ей в `task_sul_rx.c` не место. Рефакторинг:
|
||||||
|
`nku_can_take_pending_write()` — чистая функция-переводчик (ctx →
|
||||||
|
`{slice_offset, value}`), регистрируется в `sul_driver_t.take_pending_write`
|
||||||
|
(`NULL` у демо); `task_sul_rx.c` теперь проверяет только указатель на `NULL`,
|
||||||
|
БЕЗ единой protocol-specific строчки для этой фичи. Идемпотентность (п.4
|
||||||
|
спецификации) — общий гейт в `task_sul_rx.c`, одинаковый для любого
|
||||||
|
протокола, не дублируется в каждом. Подробности и как использовать для
|
||||||
|
будущих протоколов — [ADDING_PROTOCOL.md §4](../../docs/tft_app/ADDING_PROTOCOL.md).
|
||||||
|
- **Заодно — протокол-агностичное логирование**: `sul_rx_task` при `have_update`
|
||||||
|
логирует весь `sul_result_t` одной строкой (`LOG_I`, не `LOG_D` — тумблер §3.6
|
||||||
|
должен мочь включить это в поле без пересборки), с именем активного протокола
|
||||||
|
— не только НКУ-CAN, любой (включая демо/таймаут→default).
|
||||||
|
- **Найдена и закрыта гонка записи флеша**, не гипотетическая: `menu_task`
|
||||||
|
переключает `g_menu_active=false` ДО своего `settings_store_save()` (сделано
|
||||||
|
намеренно на 3.2.3, чтобы `sul_rx_task` не держал мягкую паузу лишние мс) —
|
||||||
|
значит `sul_rx_task` мог увидеть «меню закрыто» и попытаться сохранить СВОЙ
|
||||||
|
(удалённый) адрес, пока `save()` из `menu_task` ещё физически пишет QSPI:
|
||||||
|
два параллельных erase+write в один сектор. Закрыто мьютексом в
|
||||||
|
[settings_store.c](../../firmware/tft_app/src/services/settings_store/src/settings_store.c)
|
||||||
|
вокруг тела `save()` (создаётся в `init_defaults()`/`load()` — оба
|
||||||
|
гарантированно однопоточны, до `xTaskCreate()` в `bringup_task`).
|
||||||
|
|
||||||
|
**Тесты (host, `test_sul_nku_can.c`):** анонс обновляет кандидата и сам
|
||||||
|
классифицируется как `IGNORED`; команда «2» с совпавшим X триггерит; с
|
||||||
|
несовпавшим X / командой ≠«2» — не триггерит; pending транзитен (не залипает
|
||||||
|
на следующий, не относящийся к адресации, кадр); короткий кадр не падает и не
|
||||||
|
триггерит; пересечение с PACKET4 не портит ни то, ни другое.
|
||||||
|
|
||||||
|
**Найден и закрыт баг на стенде (первая HW-проверка, «ноль реакции»).**
|
||||||
|
Первая итерация не ставила **wildcard CAN-фильтры**: `sul_transport_can_set_
|
||||||
|
address()` настраивал только 5 точных фильтров PACKET1..5 (маска `0x7FF`) под
|
||||||
|
ТЕКУЩИЙ адрес — FlexCAN аппаратно отбрасывал `0x4X1` всегда (анонс не
|
||||||
|
совпадает ни с одним PACKET-ID) и `0x5XB` при X ≠ наш адрес; до
|
||||||
|
`check_remote_address()` кадры не доходили вовсе. Host-тесты (все зелёные!)
|
||||||
|
этого не ловят принципиально — они кормят `decode()` напрямую, мимо
|
||||||
|
HW-фильтров: **фильтры — transport-слой, слепая зона host-покрытия** (граница
|
||||||
|
host/HIL из ARCH §13 — тут её цена). В референсном коде OLD_PROJECT эти
|
||||||
|
фильтры есть (`apply_remote_addr_filters()`, MB 5/6, маска `0x70F` —
|
||||||
|
проверяются биты `[10:8]`+`[3:0]` ID, адресный нибл `[7:4]` игнорируется) —
|
||||||
|
при первом портировании остались за кадром, потому что жили в ДРУГОМ месте
|
||||||
|
референса (не в `msg_receiver_task`, откуда портировалась логика распознавания).
|
||||||
|
**Фикс** — [can_transport.c](../../firmware/tft_app/src/domain/sul/transport/can/src/can_transport.c):
|
||||||
|
MB 5 (`0x401`/`0x70F`) + MB 6 (`0x50B`/`0x70F`) добавлены в
|
||||||
|
`sul_transport_can_set_address()` после точных фильтров — от адреса не
|
||||||
|
зависят, но живут в той же функции (все фильтры в одном месте, нет второй
|
||||||
|
точки входа, которую можно забыть позвать).
|
||||||
|
|
||||||
|
**HW-проверка (2026-07-23, вторая, после фикса фильтров): пройдена** —
|
||||||
|
реальная станция, кадры `0x4X1`/`0x5XB`, удалённая запись адреса срабатывает.
|
||||||
|
|
||||||
|
### 3.6 — Тумблер логов (✅)
|
||||||
|
|
||||||
|
> **Статус: подтверждено на стенде (2026-07-22)** — переключение пункта «Логи»
|
||||||
|
> в меню включает/выключает UART-вывод на живом устройстве. Host-покрытие
|
||||||
|
> зелёное (28 тестов `test_log`, +5 новых); ARM Debug линкуется, подписан.
|
||||||
|
|
||||||
|
Общий бит (согласовано с пользователем — не гейт по тегам: `BOOL_ARRAY`-редактор
|
||||||
|
для тегового UI ещё не реализован, а `log_enabled` уже есть в `settings_t` и
|
||||||
|
пункте меню «Логи» без доп. объёма).
|
||||||
|
|
||||||
|
- **`log_set_enabled()`/`log_is_enabled()`** ([utils/log/log.h](../../utils/log/log.h),
|
||||||
|
[log.c](../../utils/log/log.c)) — рантайм-гейт ПОВЕРХ компайл-тайм `LOG_LEVEL`
|
||||||
|
(тот вырезает макросы физически — гейтить нечего ниже уровня). Гейт — первая
|
||||||
|
проверка в `log_write()`, до мьютекса/форматирования (дёшево выключенным).
|
||||||
|
По умолчанию (до первого вызова) — `true`: сообщения раннего bringup (до
|
||||||
|
загрузки настроек) не теряются молча.
|
||||||
|
- **Wiring** — [task_bringup.c](../../firmware/tft_app/src/app/task_bringup.c)
|
||||||
|
применяет из загруженных настроек один раз; [task_menu.c](../../firmware/tft_app/src/app/task_menu.c)
|
||||||
|
переприменяет после каждого действия в меню (тот же паттерн, что
|
||||||
|
протокол/адрес) — эффект сразу в этом сеансе меню, не только после `save()`.
|
||||||
|
- **Host-тесты (новые, `test_log.c`):** включено по умолчанию; `log_is_enabled()`
|
||||||
|
отражает состояние; выключенный тумблер гасит вывод и НЕ берёт мьютекс
|
||||||
|
(дешёвый путь); повторное включение восстанавливает вывод.
|
||||||
|
|
||||||
|
**HW-проверка (2026-07-22): пройдена** — переключение пункта «Логи» в меню
|
||||||
|
включает/выключает реальный UART-вывод на живом устройстве.
|
||||||
|
|
||||||
|
### Найдено при подготовке к 3.4 (опто) — таймаут связи был один на все протоколы
|
||||||
|
|
||||||
|
`CONNECTION_TIMEOUT_MS` (3000 мс) в `task_sul_rx.c` был единственной глобальной константой для
|
||||||
|
ЛЮБОГО активного протокола — не учитывало, что реальный период отправки разный по станциям
|
||||||
|
(где-то ~200 мс, где-то ~1 с), а некоторые протоколы шлют кадры ТОЛЬКО по изменению состояния
|
||||||
|
(event-driven на станции), для которых понятие «обрыва по тишине» в принципе некорректно.
|
||||||
|
|
||||||
|
**Фикс:** `connection_timeout_ms` — новое поле `sul_driver_t` ([sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h)),
|
||||||
|
задаётся протоколом при регистрации в `sul_registry.c` (**не** пользовательская настройка — оператор
|
||||||
|
не знает реальный период станции). НКУ-CAN — `3000` (как было, HW-подтверждено ранее). Демо —
|
||||||
|
`SUL_CONNECTION_TIMEOUT_DISABLED` (0) — синтетический источник, обрыва не бывает. `task_sul_rx.c`
|
||||||
|
проверяет `!= SUL_CONNECTION_TIMEOUT_DISABLED` перед сравнением с порогом — для протокола с
|
||||||
|
отключённым таймаутом «--» по тишине не появится никогда, целиком осознанно. Host-тест
|
||||||
|
`test_connection_timeout_set_per_protocol` фиксирует значения обоих протоколов. Подробности —
|
||||||
|
[ADDING_PROTOCOL.md §1](../../docs/tft_app/ADDING_PROTOCOL.md) (новое обязательное поле,
|
||||||
|
легко забыть — designated-initializer молча зануляет в DISABLED).
|
||||||
|
|
||||||
**Тесты (host).** Сериализация/дефолты/CRC ядра настроек; логика навигации меню; связывание
|
**Тесты (host).** Сериализация/дефолты/CRC ядра настроек; логика навигации меню; связывание
|
||||||
дескрипторов; распознавание команды удалённой адресации; рантайм-гейт логов.
|
дескрипторов; распознавание команды удалённой адресации; рантайм-гейт логов.
|
||||||
|
|
||||||
|
|
@ -560,9 +898,13 @@ PXP-блит спрайтов и layout).
|
||||||
влияет на декодер (пока протокол один); opto-вход отображается (примитив в fallback); тумблер логов
|
влияет на декодер (пока протокол один); opto-вход отображается (примитив в fallback); тумблер логов
|
||||||
работает; движок настроек/меню расширяется добавлением дескриптора/редактора без правок ядра.
|
работает; движок настроек/меню расширяется добавлением дескриптора/редактора без правок ядра.
|
||||||
|
|
||||||
**Документация (обязательный выход).** `docs/tft_app/SETTINGS.md` — без воды: состав `settings_t`,
|
**Документация (обязательный выход) — ✅ готово.** [`docs/tft_app/SETTINGS.md`](../../docs/tft_app/SETTINGS.md) —
|
||||||
ярусы A/B/C и их взаимосвязи, `proto_slice`/дескрипторы (§8), гибридное хранение (ядро-struct +
|
состав `settings_t` (таблица всех полей + что реально редактируется в меню сейчас), ярусы A/B/C и
|
||||||
клиентский TLV), карта QSPI (§10, размер-независимость). По паттерну «доки по реализованному».
|
их взаимосвязь, `proto_slice`/дескрипторы (§8, + готча со staleness из 3.3), гибридное хранение
|
||||||
|
(ядро-struct на QSPI + клиентский TLV — ярус C, пока только дизайн), карта QSPI (§10,
|
||||||
|
размер-независимость, честно про незанятый ping-pong-сектор). Раздел 9 отдельно перечисляет, что
|
||||||
|
осознанно не реализовано (ping-pong, ярус C, `reset_user_defaults()`, ярус A без UI) — по
|
||||||
|
конвенции «доки по реализованному», ничего не выдаётся за готовое раньше времени.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,17 @@
|
||||||
* - menu_task — модель меню: потребление кнопок, hold-to-enter,
|
* - menu_task — модель меню: потребление кнопок, hold-to-enter,
|
||||||
* навигация/edit/exit+save. НЕ рисует.
|
* навигация/edit/exit+save. НЕ рисует.
|
||||||
* - render_task — единственный владелец дисплея/вызывающий gfx_present().
|
* - render_task — единственный владелец дисплея/вызывающий gfx_present().
|
||||||
* Event-driven (будится xTaskNotifyGive от sul_rx_task И menu_task —
|
* Event-driven (будится xTaskNotifyGive от sul_rx_task, menu_task И
|
||||||
* MPSC), не поллит, не содержит кнопочной логики.
|
* dispatcher-колбэка opto, §3.4 — MPSC, три продюсера), не поллит, не
|
||||||
|
* содержит кнопочной логики.
|
||||||
|
*
|
||||||
|
* dispatcher (диспетчерские опто-входы, app/dispatcher.c) — НЕ задача (нет
|
||||||
|
* своего while(true)): dispatcher_poll() зовётся из input_poll_cb (тот же
|
||||||
|
* софт-таймер, что button, см. task_menu.c) на КАЖДОМ тике, безусловно —
|
||||||
|
* непрерывный опрос bsp_opto_read(), не реакция на колбэк (см. докстрок
|
||||||
|
* dispatcher.c про баг реактивной версии на стенде). Пишет
|
||||||
|
* g_dispatcher_indication и будит render_task напрямую из контекста демона
|
||||||
|
* программных таймеров, если итог изменился.
|
||||||
*
|
*
|
||||||
* main.c создаёt только очередь/софт-таймер ввода/bringup_task — сами задачи
|
* main.c создаёt только очередь/софт-таймер ввода/bringup_task — сами задачи
|
||||||
* друг друга создают/не создают по схеме выше, main.c про это не знает.
|
* друг друга создают/не создают по схеме выше, main.c про это не знает.
|
||||||
|
|
@ -29,6 +38,7 @@
|
||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
#include "task.h"
|
#include "task.h"
|
||||||
#include "timers.h"
|
#include "timers.h"
|
||||||
|
#include "ui/fallback.h" /* dispatcher_indication_t */
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
|
@ -98,11 +108,23 @@ extern volatile bool g_menu_active;
|
||||||
* как у g_render_queue). */
|
* как у g_render_queue). */
|
||||||
extern menu_ctx_t g_menu;
|
extern menu_ctx_t g_menu;
|
||||||
|
|
||||||
/** Хэндл render_task — sul_rx_task и menu_task (MPSC-продюсеры) будят его
|
/** Хэндл render_task — sul_rx_task, menu_task и dispatcher-колбэк opto
|
||||||
* xTaskNotifyGive() на любое изменение состояния. Устанавливается
|
* (MPSC-продюсеры, §3.4) будят его xTaskNotifyGive() на любое изменение
|
||||||
* bringup_task ДО создания sul_rx_task/menu_task. */
|
* состояния. Устанавливается bringup_task ДО создания sul_rx_task/menu_task. */
|
||||||
extern TaskHandle_t g_render_task_handle;
|
extern TaskHandle_t g_render_task_handle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Диспетчерский вход (opto IN1/IN2, §3.4) — «локальный вход» (ARCH §8
|
||||||
|
* п.3, не данные СУЛ), высший приоритет из всех режимов индикации.
|
||||||
|
*
|
||||||
|
* Единственный писатель — dispatcher_poll() (app/dispatcher.c), вызывается
|
||||||
|
* БЕЗУСЛОВНО на каждом тике input_poll_cb — контекст демона программных
|
||||||
|
* таймеров, наивысший приоритет в системе; единственный читатель —
|
||||||
|
* render_task (см. task_render.c — снимок в локальную переменную один раз за
|
||||||
|
* итерацию, тот же приём, что чинили для гонки курсора меню, см. PLAN.md).
|
||||||
|
*/
|
||||||
|
extern volatile dispatcher_indication_t g_dispatcher_indication;
|
||||||
|
|
||||||
/** Одноразовая инициализация (UART/QSPI/settings/confirm_self/SDRAM+gfx+CAN),
|
/** Одноразовая инициализация (UART/QSPI/settings/confirm_self/SDRAM+gfx+CAN),
|
||||||
* затем создаёт sul_rx_task/menu_task/render_task и удаляет себя. */
|
* затем создаёт sul_rx_task/menu_task/render_task и удаляет себя. */
|
||||||
void bringup_task(void *p_arg);
|
void bringup_task(void *p_arg);
|
||||||
|
|
@ -116,9 +138,33 @@ void menu_task(void *p_arg);
|
||||||
/** Презентация: владелец дисплея, единственный вызывающий gfx_present(). */
|
/** Презентация: владелец дисплея, единственный вызывающий gfx_present(). */
|
||||||
void render_task(void *p_arg);
|
void render_task(void *p_arg);
|
||||||
|
|
||||||
/** Колбэк софт-таймера debounce (bsp_button_poll) — main создаёт таймер, каденция в main. */
|
/** Колбэк софт-таймера debounce (bsp_button_poll + bsp_opto_process, §3.4) —
|
||||||
|
* main создаёт таймер, каденция в main. */
|
||||||
void input_poll_cb(TimerHandle_t x_timer);
|
void input_poll_cb(TimerHandle_t x_timer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Инициализировать диспетчерский вход (opto IN1/IN2, §3.4).
|
||||||
|
*
|
||||||
|
* bsp_opto_init() (без колбэков — см. dispatcher.c) + захват начального
|
||||||
|
* состояния пинов в g_dispatcher_indication (иначе если вызов уже активен на
|
||||||
|
* момент старта устройства — первый dispatcher_poll() ещё не случился, а до
|
||||||
|
* него экран не должен показывать NONE поверх уже висящего сигнала).
|
||||||
|
* Вызывать из main(), сразу после bsp_button_init() (пины уже настроены в
|
||||||
|
* BOARD_InitPins, как и у button) — до старта планировщика.
|
||||||
|
*/
|
||||||
|
void dispatcher_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Опросить диспетчерский вход (opto IN1/IN2, §3.4) и обновить
|
||||||
|
* g_dispatcher_indication.
|
||||||
|
*
|
||||||
|
* Вызывать БЕЗУСЛОВНО на каждом тике input_poll_cb, после bsp_opto_process()
|
||||||
|
* — непрерывный опрос bsp_opto_read(), не реакция на колбэк bsp_opto (см.
|
||||||
|
* докстрок dispatcher.c: реактивная версия залипала на стенде). Будит
|
||||||
|
* render_task, только если итоговая индикация реально изменилась.
|
||||||
|
*/
|
||||||
|
void dispatcher_poll(void);
|
||||||
|
|
||||||
/** Диагностика трейлера слота (read-only, безопасно звать многократно) —
|
/** Диагностика трейлера слота (read-only, безопасно звать многократно) —
|
||||||
* общая для bringup_task (before/after-confirm) и sul_rx_task (периодический
|
* общая для bringup_task (before/after-confirm) и sul_rx_task (периодический
|
||||||
* re-log). Определена в task_bringup.c. */
|
* re-log). Определена в task_bringup.c. */
|
||||||
|
|
|
||||||
124
firmware/tft_app/src/app/dispatcher.c
Normal file
124
firmware/tft_app/src/app/dispatcher.c
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
/**
|
||||||
|
* @file dispatcher.c
|
||||||
|
* @brief Диспетчерский вход (opto IN1/IN2, §3.4) — «Вызов подан» / «Вызов
|
||||||
|
* принят». НЕ задача (нет своего while(true)) — тонкая обвязка
|
||||||
|
* bsp_opto поверх уже готового BSP-драйвера (bsp/opto): каждый тик
|
||||||
|
* софт-таймера (dispatcher_poll(), см. input_poll_cb в task_menu.c)
|
||||||
|
* заново читает оба канала, пишет g_dispatcher_indication и будит
|
||||||
|
* render_task при изменении.
|
||||||
|
*
|
||||||
|
* ARCH §8 п.3: опто — «локальный вход», не данные СУЛ; вливается на уровне
|
||||||
|
* презентации (ARCH §4, dataflow — opto отдельным входом прямо в
|
||||||
|
* ui.render()), не через controller. Приоритет — высший из всех режимов
|
||||||
|
* индикации (согласовано с пользователем), безусловно перекрывает и обычную
|
||||||
|
* позицию, и любой режим СУЛ; работает независимо от связи со станцией.
|
||||||
|
*
|
||||||
|
* НЕПРЕРЫВНЫЙ ОПРОС, БЕЗ колбэков bsp_opto (callbacks = {NULL,...}) — как
|
||||||
|
* в OLD_PROJECT_TFT8_UKL (`tft_refresh_task`): там `bsp_opto_read()`
|
||||||
|
* читается каждую итерацию задачи, а фронт детектируется самим потребителем
|
||||||
|
* через пару state[0]/state[1], колбэк вообще не регистрируется. Первая
|
||||||
|
* версия этого файла была реактивной: пересчитывала состояние ТОЛЬКО изнутри
|
||||||
|
* колбэка `bsp_opto_process()`, вызываемого на подтверждённую смену канала.
|
||||||
|
* На стенде это дало баг (см. PLAN.md §3.4): «ВЫЗОВ» не сбрасывался при
|
||||||
|
* снятии физического сигнала — залипал до случайного следующего фронта на
|
||||||
|
* ЛЮБОМ канале, который наконец пересчитывал состояние с нуля. Причина —
|
||||||
|
* не гонка в bsp_opto (там её и не было, судя по истории OLD_PROJECT), а
|
||||||
|
* отсутствие самовосстановления: чисто реактивная схема имеет ровно один
|
||||||
|
* шанс заметить каждое изменение, и если он почему-то пропадает — состояние
|
||||||
|
* замирает НАВСЕГДА. Непрерывный опрос вместо этого переспрашивает
|
||||||
|
* `bsp_opto_read()` заново каждые 5 мс — единичный сбой (где бы он ни
|
||||||
|
* случился) чинится на следующем же тике, тот же принцип, что и у
|
||||||
|
* bsp_button (тоже без колбэков, тоже опрашивается каждый тик).
|
||||||
|
*
|
||||||
|
* RS_RX (bsp_opto третий канал, бинарный протокол) здесь не используется —
|
||||||
|
* остаётся под LPUART3 (rs_as_gpio=false), это для другого будущего сценария.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "app_tasks.h"
|
||||||
|
|
||||||
|
#include "bsp/opto.h"
|
||||||
|
#include "log/log.h"
|
||||||
|
|
||||||
|
#define LOG_TAG "dispatcher"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Вычислить индикацию из ТЕКУЩЕГО состояния обоих каналов. ОТВЕТ
|
||||||
|
* (IN2) перебивает ВЫЗОВ (IN1), если оба почему-то активны
|
||||||
|
* одновременно (согласовано с пользователем).
|
||||||
|
*/
|
||||||
|
static dispatcher_indication_t resolve_indication(void)
|
||||||
|
{
|
||||||
|
const bool ANSWER_ACTIVE = (bsp_opto_read(BSP_OPTO_CH_IN2) == BSP_OPTO_STATE_ACTIVE);
|
||||||
|
const bool CALL_ACTIVE = (bsp_opto_read(BSP_OPTO_CH_IN1) == BSP_OPTO_STATE_ACTIVE);
|
||||||
|
|
||||||
|
return ANSWER_ACTIVE ? DISPATCHER_INDICATION_ANSWER
|
||||||
|
: CALL_ACTIVE ? DISPATCHER_INDICATION_CALL
|
||||||
|
: DISPATCHER_INDICATION_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *indication_name(dispatcher_indication_t v)
|
||||||
|
{
|
||||||
|
switch (v)
|
||||||
|
{
|
||||||
|
case DISPATCHER_INDICATION_CALL: return "CALL";
|
||||||
|
case DISPATCHER_INDICATION_ANSWER: return "ANSWER";
|
||||||
|
case DISPATCHER_INDICATION_NONE:
|
||||||
|
default: return "NONE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Залогировать переход строго по фронтам (не по значению каждый
|
||||||
|
* тик — dispatcher_poll() зовёт это ТОЛЬКО когда индикация реально
|
||||||
|
* изменилась). ВЫКЛ->ВКЛ и ВКЛ->ВЫКЛ — раздельные строки, каждая по
|
||||||
|
* своему "режиму" (CALL/ANSWER); прямой переход CALL<->ANSWER (оба —
|
||||||
|
* не NONE) — это одновременно выключение старого И появление нового,
|
||||||
|
* печатаются обе строки.
|
||||||
|
*/
|
||||||
|
static void log_transition(dispatcher_indication_t old_state, dispatcher_indication_t new_state)
|
||||||
|
{
|
||||||
|
if (old_state != DISPATCHER_INDICATION_NONE)
|
||||||
|
{
|
||||||
|
LOG_I(LOG_TAG, "mode %s disabled", indication_name(old_state));
|
||||||
|
}
|
||||||
|
if (new_state != DISPATCHER_INDICATION_NONE)
|
||||||
|
{
|
||||||
|
LOG_I(LOG_TAG, "mode %s appeared", indication_name(new_state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const bsp_opto_config_t K_OPTO_CONFIG = {
|
||||||
|
.callbacks = { NULL, NULL, NULL }, /* непрерывный опрос — см. докстрок файла */
|
||||||
|
.modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL },
|
||||||
|
.edges = { BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING },
|
||||||
|
.rs_as_gpio = false, /* RS_RX остаётся под LPUART3 — не наш случай (см. докстрок файла) */
|
||||||
|
.debounce_ms = 10U, /* середина рекомендованного диапазона 5-10 мс, bsp/opto/opto.h */
|
||||||
|
};
|
||||||
|
|
||||||
|
void dispatcher_init(void)
|
||||||
|
{
|
||||||
|
(void) bsp_opto_init(&K_OPTO_CONFIG);
|
||||||
|
|
||||||
|
/* Захват состояния СРАЗУ — до первого dispatcher_poll() (следующий тик
|
||||||
|
* софт-таймера) экран не должен показывать NONE, если вызов уже висит на
|
||||||
|
* момент старта устройства (станция держит сигнал постоянно, не
|
||||||
|
* импульсом). */
|
||||||
|
g_dispatcher_indication = resolve_indication();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatcher_poll(void)
|
||||||
|
{
|
||||||
|
const dispatcher_indication_t NEW_STATE = resolve_indication();
|
||||||
|
|
||||||
|
if (NEW_STATE != g_dispatcher_indication)
|
||||||
|
{
|
||||||
|
log_transition(g_dispatcher_indication, NEW_STATE);
|
||||||
|
|
||||||
|
g_dispatcher_indication = NEW_STATE;
|
||||||
|
|
||||||
|
if (g_render_task_handle != NULL)
|
||||||
|
{
|
||||||
|
(void) xTaskNotifyGive(g_render_task_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,12 +31,14 @@ volatile bool g_display_ready = false;
|
||||||
volatile bool g_menu_active = false;
|
volatile bool g_menu_active = false;
|
||||||
QueueHandle_t g_render_queue = NULL;
|
QueueHandle_t g_render_queue = NULL;
|
||||||
TaskHandle_t g_render_task_handle = NULL;
|
TaskHandle_t g_render_task_handle = NULL;
|
||||||
|
volatile dispatcher_indication_t g_dispatcher_indication = DISPATCHER_INDICATION_NONE;
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */
|
board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
(void) bsp_button_init(); /* GPIO настроен в BOARD_InitPins; сброс debounce */
|
(void) bsp_button_init(); /* GPIO настроен в BOARD_InitPins; сброс debounce */
|
||||||
|
dispatcher_init(); /* opto IN1/IN2 (§3.4) — пины тоже уже в BOARD_InitPins */
|
||||||
|
|
||||||
g_render_queue = xQueueCreate(1, sizeof(render_msg_t));
|
g_render_queue = xQueueCreate(1, sizeof(render_msg_t));
|
||||||
configASSERT(g_render_queue != NULL);
|
configASSERT(g_render_queue != NULL);
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@
|
||||||
#include "bsp/qspi_flash.h"
|
#include "bsp/qspi_flash.h"
|
||||||
#include "bsp/sdram.h"
|
#include "bsp/sdram.h"
|
||||||
#include "bsp/uart_host.h"
|
#include "bsp/uart_host.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
#include "domain/sul/transport/can.h"
|
#include "domain/sul/transport/can.h"
|
||||||
#include "flash_map.h"
|
#include "flash_map.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
|
#include "menu/menu_tree.h"
|
||||||
#include "port/log_uart.h"
|
#include "port/log_uart.h"
|
||||||
#include "services/gfx.h"
|
#include "services/gfx.h"
|
||||||
#include "services/settings_store.h"
|
#include "services/settings_store.h"
|
||||||
|
|
@ -156,6 +158,16 @@ void bringup_task(void *p_arg)
|
||||||
settings_store_init_defaults();
|
settings_store_init_defaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Активный протокол + меню-секция "Протокол" — из загруженных/дефолтных
|
||||||
|
* настроек (§8), до первого возможного открытия меню в menu_task(). */
|
||||||
|
sul_registry_set_active(settings_store_get()->device.protocol_id);
|
||||||
|
menu_tree_refresh_protocol_section(settings_store_get_mutable());
|
||||||
|
|
||||||
|
/* Рантайм-тумблер логов (§3.6) — из настроек; ДО этой строки действует
|
||||||
|
* дефолт log.c (включено), чтобы сообщения выше (qspi/settings) не
|
||||||
|
* терялись молча, пока реальное значение ещё не загружено. */
|
||||||
|
log_set_enabled(settings_store_get()->device.log_enabled != 0U);
|
||||||
|
|
||||||
/* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки
|
/* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки
|
||||||
* (recovery загрузчика). SRC GPR, без flash. Безусловно, до потенциально
|
* (recovery загрузчика). SRC GPR, без flash. Безусловно, до потенциально
|
||||||
* рискованного bring-up дисплея/CAN ниже. */
|
* рискованного bring-up дисплея/CAN ниже. */
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
|
|
||||||
#include "FreeRTOS.h"
|
#include "FreeRTOS.h"
|
||||||
#include "bsp/button.h"
|
#include "bsp/button.h"
|
||||||
|
#include "bsp/opto.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
#include "menu/menu.h"
|
#include "menu/menu.h"
|
||||||
#include "menu/menu_tree.h"
|
#include "menu/menu_tree.h"
|
||||||
|
|
@ -44,7 +46,8 @@ void input_poll_cb(TimerHandle_t x_timer)
|
||||||
{
|
{
|
||||||
(void) x_timer;
|
(void) x_timer;
|
||||||
bsp_button_poll();
|
bsp_button_poll();
|
||||||
/* Фаза 3.4: bsp_opto_process(); (диспетчерские вход/ответ) */
|
bsp_opto_process(); /* debounce IN1/IN2 (§3.4) — короткая, как button */
|
||||||
|
dispatcher_poll(); /* непрерывный опрос bsp_opto_read(), не колбэк — см. dispatcher.c */
|
||||||
}
|
}
|
||||||
|
|
||||||
void menu_task(void *p_arg)
|
void menu_task(void *p_arg)
|
||||||
|
|
@ -70,6 +73,18 @@ void menu_task(void *p_arg)
|
||||||
{
|
{
|
||||||
menu_action(&g_menu);
|
menu_action(&g_menu);
|
||||||
|
|
||||||
|
/* Протокол мог смениться этим действием (T_PROTO) — дёшево
|
||||||
|
* переприменить оба (§8), тот же паттерн, что адрес/CAN-
|
||||||
|
* фильтры в sul_rx_task. Секция T_PROTO_PARAM должна отражать
|
||||||
|
* НОВЫЙ активный протокол уже в этом сеансе меню, не только
|
||||||
|
* после save(). */
|
||||||
|
sul_registry_set_active(g_menu.settings->device.protocol_id);
|
||||||
|
menu_tree_refresh_protocol_section(g_menu.settings);
|
||||||
|
|
||||||
|
/* Тумблер логов (§3.6) — тот же паттерн: эффект сразу в этом
|
||||||
|
* сеансе меню, не только после save(). */
|
||||||
|
log_set_enabled(g_menu.settings->device.log_enabled != 0U);
|
||||||
|
|
||||||
/* Обновить ДО settings_store_save() (флеш-запись, не
|
/* Обновить ДО settings_store_save() (флеш-запись, не
|
||||||
* мгновенная) — иначе sul_rx_task ещё несколько мс видел бы
|
* мгновенная) — иначе sul_rx_task ещё несколько мс видел бы
|
||||||
* устаревший g_menu_active=true и держал бы мягкую паузу
|
* устаревший g_menu_active=true и держал бы мягкую паузу
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
* @file task_render.c
|
* @file task_render.c
|
||||||
* @brief Презентация: единственный владелец дисплея и вызывающий gfx_present*().
|
* @brief Презентация: единственный владелец дисплея и вызывающий gfx_present*().
|
||||||
*
|
*
|
||||||
* Event-driven (xTaskNotifyGive от sul_rx_task И menu_task — MPSC, будят оба
|
* Event-driven (xTaskNotifyGive от sul_rx_task, menu_task И dispatcher-колбэка
|
||||||
* продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает несколько notify в одно
|
* opto §3.4 — MPSC, три продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает
|
||||||
* пробуждение — важно только «есть свежее состояние», не сколько раз оно
|
* несколько notify в одно пробуждение — важно только «есть свежее состояние»,
|
||||||
* менялось). НЕ содержит кнопочной логики — разделено от menu_task на Фазе
|
* не сколько раз оно менялось). НЕ содержит кнопочной логики — разделено от
|
||||||
* 3.2.4 (см. task_menu.c про причину).
|
* menu_task на Фазе 3.2.4 (см. task_menu.c про причину).
|
||||||
*
|
*
|
||||||
* Меню — оконный рендер (Фаза 3.2.4, ускорение навигации): на ОТКРЫТИИ — полная
|
* Меню — оконный рендер (Фаза 3.2.4, ускорение навигации): на ОТКРЫТИИ — полная
|
||||||
* очистка AS (стереть индикацию) + два полных gfx_present() подряд (double
|
* очистка AS (стереть индикацию) + два полных gfx_present() подряд (double
|
||||||
|
|
@ -14,6 +14,13 @@
|
||||||
* gfx_present_rect); дальше НАВИГАЦИЯ — перерисовка и композит только окна
|
* gfx_present_rect); дальше НАВИГАЦИЯ — перерисовка и композит только окна
|
||||||
* 480×272 (~27% кадра, пропорционально дешевле). Индикация — полные кадры,
|
* 480×272 (~27% кадра, пропорционально дешевле). Индикация — полные кадры,
|
||||||
* как и была.
|
* как и была.
|
||||||
|
*
|
||||||
|
* Диспетчерский вход (§3.4) — как в OLD_PROJECT_TFT8_UKL: пока меню открыто,
|
||||||
|
* индикация вообще не трогается (dispatcher копится в g_dispatcher_indication,
|
||||||
|
* применяется одним махом при закрытии — тем же путём, что уже восстанавливает
|
||||||
|
* индикацию после меню). Вне меню — САМОСТОЯТЕЛЬНАЯ третья причина перерисовки
|
||||||
|
* (не только новое сообщение в очереди/закрытие меню): opto может разбудить
|
||||||
|
* render_task без единого нового кадра СУЛ.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "app_tasks.h"
|
#include "app_tasks.h"
|
||||||
|
|
@ -40,10 +47,11 @@ void render_task(void *p_arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
sul_result_t last = sul_default_state();
|
sul_result_t last = sul_default_state();
|
||||||
ui_fallback_render_initial(&last);
|
ui_fallback_render_initial(&last, g_dispatcher_indication);
|
||||||
gfx_present();
|
gfx_present();
|
||||||
|
|
||||||
bool was_menu_open = false;
|
bool was_menu_open = false;
|
||||||
|
dispatcher_indication_t last_dispatcher = g_dispatcher_indication;
|
||||||
|
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
|
|
@ -74,13 +82,22 @@ void render_task(void *p_arg)
|
||||||
{
|
{
|
||||||
bool present_needed = false;
|
bool present_needed = false;
|
||||||
|
|
||||||
if (was_menu_open)
|
/* Снимок один раз за итерацию — g_dispatcher_indication пишет
|
||||||
|
* колбэк opto из СВОЕГО контекста (демон таймеров, выше по
|
||||||
|
* приоритету, чем render_task); без снимка два обращения ниже
|
||||||
|
* могли бы увидеть РАЗНЫЕ значения за одну итерацию (тот же
|
||||||
|
* класс гонки, что чинили для курсора меню, см. PLAN.md). */
|
||||||
|
const dispatcher_indication_t DISPATCHER_NOW = g_dispatcher_indication;
|
||||||
|
|
||||||
|
if (was_menu_open || (DISPATCHER_NOW != last_dispatcher))
|
||||||
{
|
{
|
||||||
/* Меню только что закрылось — восстановить индикацию
|
/* Меню только что закрылось, ИЛИ диспетчерский вход
|
||||||
* последним известным состоянием немедленно, не дожидаясь
|
* изменился без нового кадра СУЛ в очереди (свой продюсер,
|
||||||
* свежего сообщения (sul_rx_task мог простаивать под
|
* не sul_rx_task) — восстановить индикацию последним
|
||||||
* g_menu_active — очередь пока пуста). */
|
* известным состоянием СУЛ + ТЕКУЩИМ dispatcher немедленно,
|
||||||
ui_fallback_render_initial(&last);
|
* не дожидаясь свежего кадра (sul_rx_task мог простаивать
|
||||||
|
* под g_menu_active — очередь пока пуста). */
|
||||||
|
ui_fallback_render_initial(&last, DISPATCHER_NOW);
|
||||||
present_needed = true;
|
present_needed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +105,7 @@ void render_task(void *p_arg)
|
||||||
if (xQueueReceive(g_render_queue, &msg, 0) == pdTRUE)
|
if (xQueueReceive(g_render_queue, &msg, 0) == pdTRUE)
|
||||||
{
|
{
|
||||||
last = msg.result;
|
last = msg.result;
|
||||||
ui_fallback_render(&msg.task, &msg.result);
|
ui_fallback_render(&msg.task, &msg.result, DISPATCHER_NOW);
|
||||||
present_needed = true;
|
present_needed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,6 +113,8 @@ void render_task(void *p_arg)
|
||||||
{
|
{
|
||||||
gfx_present(); /* индикация — всегда полный кадр */
|
gfx_present(); /* индикация — всегда полный кадр */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
last_dispatcher = DISPATCHER_NOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
was_menu_open = MENU_OPEN_NOW;
|
was_menu_open = MENU_OPEN_NOW;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* @file task_sul_rx.c
|
* @file task_sul_rx.c
|
||||||
* @brief Приём CAN → decode → controller → уведомление render_task.
|
* @brief Приём активного протокола (реестр sul, §8) → decode → controller →
|
||||||
|
* уведомление render_task. Протокол-специфичный транспорт (CAN/демо)
|
||||||
|
* выбирается здесь по id активного драйвера — единственное место,
|
||||||
|
* которое трогает Фаза 8 при добавлении протокола на новом транспорте.
|
||||||
*
|
*
|
||||||
* WDOG/heartbeat/периодический re-log трейлера — БЕЗУСЛОВНО (housekeeping,
|
* WDOG/heartbeat/периодический re-log трейлера — БЕЗУСЛОВНО (housekeeping,
|
||||||
* не CAN-специфика). Сама CAN-работа (decode/controller/очередь) — под
|
* не протокол-специфика). Сам приём (decode/controller/очередь) — под
|
||||||
* `!g_menu_active`: мягкая пауза на время меню (см. app_tasks.h) — задача НЕ
|
* `!g_menu_active`: мягкая пауза на время меню (см. app_tasks.h) — задача НЕ
|
||||||
* suspend'ится, поэтому WDOG остаётся в безопасности по конструкции.
|
* suspend'ится, поэтому WDOG остаётся в безопасности по конструкции.
|
||||||
*
|
*
|
||||||
|
|
@ -12,43 +15,49 @@
|
||||||
* решает, делать ли CAN-работу вообще.
|
* решает, делать ли CAN-работу вообще.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "app_tasks.h"
|
|
||||||
|
|
||||||
#include "FreeRTOS.h"
|
#include "FreeRTOS.h"
|
||||||
|
#include "app_tasks.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
#include "bsp/wdog.h"
|
#include "bsp/wdog.h"
|
||||||
#include "domain/controller.h"
|
#include "domain/controller.h"
|
||||||
#include "domain/elevator_model.h"
|
#include "domain/elevator_model.h"
|
||||||
#include "domain/sul.h"
|
#include "domain/sul.h"
|
||||||
|
#include "domain/sul/demo.h"
|
||||||
#include "domain/sul/nku_can.h"
|
#include "domain/sul/nku_can.h"
|
||||||
#include "domain/sul/transport/can.h"
|
#include "domain/sul/transport/can.h"
|
||||||
|
#include "domain/sul/transport/demo.h"
|
||||||
|
#include "log/log.h"
|
||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
#include "services/settings_store.h"
|
#include "services/settings_store.h"
|
||||||
#include "task.h"
|
#include "task.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define LOG_TAG "sul_rx"
|
||||||
|
|
||||||
#define HEARTBEAT_PERIOD_MS 500U
|
#define HEARTBEAT_PERIOD_MS 500U
|
||||||
#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */
|
#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */
|
||||||
#define STATUS_LOG_PERIOD_MS \
|
#define STATUS_LOG_PERIOD_MS \
|
||||||
2000U /* периодический re-log трейлера — виден независимо
|
2000U /* периодический re-log трейлера — виден независимо
|
||||||
* от момента подключения терминала */
|
* от момента подключения терминала */
|
||||||
#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */
|
#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */
|
||||||
#define CONNECTION_TIMEOUT_MS \
|
/* «Пропадание трафика» — см. ARCH, поток данных: poll + timeout→default.
|
||||||
3000U /* «пропадание трафика» — см. ARCH, поток данных:
|
* Порог — СВОЙСТВО ПРОТОКОЛА (p_driver->connection_timeout_ms, domain/sul.h),
|
||||||
* poll + timeout→default. Порядок величины — как
|
* не константа здесь: разные станции шлют раз в ~200 мс или раз в ~1 с, а
|
||||||
* в OLD_PROJECT (там ~3 с на отметку потери связи).
|
* некоторые — только по изменению состояния (тогда таймаут вообще отключён,
|
||||||
* Меряется от last_frame_tick — пауза меню не портит
|
* SUL_CONNECTION_TIMEOUT_DISABLED) — не пользовательская настройка, задаётся
|
||||||
* логику: если трафик реально стоял, «--» появится
|
* протоколом при регистрации в sul_registry.c. Меряется от last_frame_tick —
|
||||||
* сразу по возврату из меню; если шёл — SINCE_FRAME_MS
|
* пауза меню не портит логику: если трафик реально стоял, «--» появится сразу
|
||||||
* обнулится первым же принятым кадром. */
|
* по возврату из меню; если шёл — SINCE_FRAME_MS обнулится первым же кадром. */
|
||||||
|
|
||||||
void sul_rx_task(void *p_arg)
|
void sul_rx_task(void *p_arg)
|
||||||
{
|
{
|
||||||
(void) p_arg;
|
(void) p_arg;
|
||||||
|
|
||||||
nku_can_ctx_t nku_ctx;
|
/* Ctx каждого зарегистрированного драйвера — постоянно, живёт в
|
||||||
nku_can_init(&nku_ctx);
|
* sul_registry.c (domain/sul.h, .p_ctx); переключение протокола не
|
||||||
|
* пересоздаёт его, только меняет, какой из них активен (§8). */
|
||||||
|
sul_registry_init();
|
||||||
|
|
||||||
controller_ctx_t ctrl_ctx;
|
controller_ctx_t ctrl_ctx;
|
||||||
controller_init(&ctrl_ctx);
|
controller_init(&ctrl_ctx);
|
||||||
|
|
@ -79,47 +88,112 @@ void sul_rx_task(void *p_arg)
|
||||||
|
|
||||||
if (g_display_ready && !g_menu_active)
|
if (g_display_ready && !g_menu_active)
|
||||||
{
|
{
|
||||||
/* Адрес станции из настроек (proto_slice[0]) — подхватывает правку из
|
const sul_driver_t *p_driver = sul_registry_active();
|
||||||
* меню без межзадачного сигнала (запись/чтение uint8 атомарны).
|
|
||||||
* Обе стороны: decode (nku_ctx) И HW RX-фильтры FlexCAN — вторая
|
/* Транспорт + реаппликация настроек протокола — ЗДЕСЬ (app-слой =
|
||||||
* без переприменения фильтров под реальный адрес станции кадры с
|
* wiring, ARCH §4), не в домене/реестре: держит sul_driver_t.p_ctx
|
||||||
* адресом != 0 отбрасывались бы на уровне CAN-контроллера, ещё до
|
* и sul_registry_* host-тестируемыми без bsp. Новый протокол на
|
||||||
|
* новом транспорте (Фаза 8: УИМ/SD7/УЭЛ/УКЛ, в основном UART) —
|
||||||
|
* новая ветка здесь; decode/меню/настройки не трогаются.
|
||||||
|
*
|
||||||
|
* НКУ-CAN: адрес из настроек (proto_slice[0]) — подхватывает
|
||||||
|
* правку из меню без межзадачного сигнала (запись/чтение uint8
|
||||||
|
* атомарны). Обе стороны: decode-ctx И HW RX-фильтры FlexCAN —
|
||||||
|
* вторая без переприменения фильтров под реальный адрес станции
|
||||||
|
* кадры с адресом != 0 отбрасывались бы CAN-контроллером ещё до
|
||||||
* decode (см. PLAN.md — найдено на реальной станции, адрес 1). */
|
* decode (см. PLAN.md — найдено на реальной станции, адрес 1). */
|
||||||
|
sul_frame_t frame;
|
||||||
|
bsp_status_t rx_rc;
|
||||||
|
if (p_driver->id == SUL_PROTOCOL_NKU_CAN)
|
||||||
|
{
|
||||||
const uint8_t NKU_ADDR = settings_store_get()->user.proto_slice[0];
|
const uint8_t NKU_ADDR = settings_store_get()->user.proto_slice[0];
|
||||||
nku_can_set_address(&nku_ctx, NKU_ADDR);
|
nku_can_set_address((nku_can_ctx_t *) p_driver->p_ctx, NKU_ADDR);
|
||||||
(void) sul_transport_can_set_address(NKU_ADDR); /* no-op, если адрес не менялся */
|
(void) sul_transport_can_set_address(NKU_ADDR); /* no-op, если адрес не менялся */
|
||||||
|
rx_rc = sul_transport_can_receive(CAN_RX_TIMEOUT_MS, &frame);
|
||||||
|
}
|
||||||
|
else /* SUL_PROTOCOL_DEMO — без реальной шины, всегда успешно */
|
||||||
|
{
|
||||||
|
const uint8_t SPEED_IDX = settings_store_get()->user.proto_slice[0];
|
||||||
|
demo_set_speed((demo_ctx_t *) p_driver->p_ctx, SPEED_IDX);
|
||||||
|
rx_rc = sul_transport_demo_receive(CAN_RX_TIMEOUT_MS, &frame);
|
||||||
|
}
|
||||||
|
|
||||||
sul_result_t decoded;
|
sul_result_t decoded;
|
||||||
bool have_update = false;
|
bool have_update = false;
|
||||||
|
|
||||||
sul_frame_t frame;
|
if (rx_rc == BSP_OK)
|
||||||
if (sul_transport_can_receive(CAN_RX_TIMEOUT_MS, &frame) == BSP_OK)
|
|
||||||
{
|
{
|
||||||
const sul_driver_t *p_driver = sul_registry_active();
|
if (p_driver->decode(p_driver->p_ctx, &frame, &decoded) == SUL_STATUS_OK)
|
||||||
if (p_driver->decode(&nku_ctx, &frame, &decoded) == SUL_STATUS_OK)
|
|
||||||
{
|
{
|
||||||
last_frame_tick = xTaskGetTickCount();
|
last_frame_tick = xTaskGetTickCount();
|
||||||
have_update = true;
|
have_update = true;
|
||||||
}
|
}
|
||||||
/* IGNORED/ERR — Фаза 1 их отдельно не обрабатывает, следующая итерация. */
|
/* IGNORED/ERR — Фаза 1 их отдельно не обрабатывает, следующая итерация. */
|
||||||
|
|
||||||
|
/* Протокол сам инициирует запись в свой proto_slice (§8) — напр.
|
||||||
|
* удалённая адресация НКУ-CAN (§3.5). Полностью generic: НЕТ
|
||||||
|
* ветки по id/протоколу — take_pending_write() либо NULL
|
||||||
|
* (протокол никогда этого не делает), либо сообщает готовый
|
||||||
|
* {offset, value}, домен settings не касается вообще. Вызывается
|
||||||
|
* НЕЗАВИСИМО от SUL_STATUS_OK/IGNORED/ERR выше — протокол мог
|
||||||
|
* распознать команду в кадре, который не подошёл ни под одну
|
||||||
|
* "индикационную" классификацию (напр. кадр от станции с чужим
|
||||||
|
* адресом, см. nku_can.c check_remote_address()). */
|
||||||
|
if (p_driver->take_pending_write != NULL)
|
||||||
|
{
|
||||||
|
sul_slice_write_t write;
|
||||||
|
if (p_driver->take_pending_write(p_driver->p_ctx, &write))
|
||||||
|
{
|
||||||
|
uint8_t *p_field =
|
||||||
|
&settings_store_get_mutable()->user.proto_slice[write.slice_offset];
|
||||||
|
/* Идемпотентность — ОБЩИЙ гейт для любого протокола, не
|
||||||
|
* его забота: без сравнения контроллер станции, держащий
|
||||||
|
* команду записи некоторое время (см. PDF НКУ-CAN п.5),
|
||||||
|
* писал бы флеш на КАЖДЫЙ повторный кадр. */
|
||||||
|
if (*p_field != write.value)
|
||||||
|
{
|
||||||
|
*p_field = write.value;
|
||||||
|
const bsp_status_t SAVE_RC = settings_store_save();
|
||||||
|
LOG_I(LOG_TAG, "%s: proto_slice[%u]=%u (rc=%d)", p_driver->p_name,
|
||||||
|
write.slice_offset, write.value, SAVE_RC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (p_driver->connection_timeout_ms != SUL_CONNECTION_TIMEOUT_DISABLED)
|
||||||
|
{
|
||||||
const uint32_t SINCE_FRAME_MS =
|
const uint32_t SINCE_FRAME_MS =
|
||||||
(uint32_t) (xTaskGetTickCount() - last_frame_tick) * portTICK_PERIOD_MS;
|
(uint32_t) (xTaskGetTickCount() - last_frame_tick) * portTICK_PERIOD_MS;
|
||||||
if (SINCE_FRAME_MS >= CONNECTION_TIMEOUT_MS)
|
if (SINCE_FRAME_MS >= p_driver->connection_timeout_ms)
|
||||||
{
|
{
|
||||||
/* poll + timeout→default (ARCH, поток данных) — controller сам
|
/* poll + timeout→default (ARCH, поток данных) — controller
|
||||||
* определит, реальное ли это изменение (не сработает повторно
|
* сам определит, реальное ли это изменение (не сработает
|
||||||
* на каждой итерации после первого перехода в default). */
|
* повторно на каждой итерации после перехода в default). */
|
||||||
decoded = sul_default_state();
|
decoded = sul_default_state();
|
||||||
have_update = true;
|
have_update = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/* иначе — протокол event-driven на станции (шлёт только по
|
||||||
|
* изменению), детекция обрыва по тишине для него некорректна —
|
||||||
|
* last_frame_tick тем не менее продолжает обновляться выше на
|
||||||
|
* каждый валидный кадр, просто здесь не используется. */
|
||||||
|
|
||||||
if (have_update)
|
if (have_update)
|
||||||
{
|
{
|
||||||
|
|
||||||
const indication_task_t DIFF = controller_process(&ctrl_ctx, &decoded);
|
const indication_task_t DIFF = controller_process(&ctrl_ctx, &decoded);
|
||||||
if (DIFF.pos_pending || DIFF.direction_pending || DIFF.mode_pending)
|
if (DIFF.pos_pending || DIFF.direction_pending || DIFF.mode_pending)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
LOG_I(LOG_TAG,
|
||||||
|
"update from %s: pos=%s next=%s dir=%u arrival=%d move=%d overload=%d "
|
||||||
|
"fire=%d lading=%d maint=%d fireman=%d seismic=%d err=%d floor=%u",
|
||||||
|
p_driver->p_name, decoded.pos, decoded.next, (unsigned) decoded.direction,
|
||||||
|
decoded.arrival, decoded.movement, decoded.overload, decoded.fire_alarm,
|
||||||
|
decoded.lading, decoded.maintenance, decoded.fireman, decoded.seismic,
|
||||||
|
decoded.error, decoded.floor_num);
|
||||||
|
|
||||||
const render_msg_t MSG = { .task = DIFF, .result = decoded };
|
const render_msg_t MSG = { .task = DIFF, .result = decoded };
|
||||||
(void) xQueueOverwrite(g_render_queue, &MSG);
|
(void) xQueueOverwrite(g_render_queue, &MSG);
|
||||||
if (g_render_task_handle != NULL)
|
if (g_render_task_handle != NULL)
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,14 @@ target_include_directories(tft_app_sul_headers INTERFACE include/)
|
||||||
target_link_libraries(tft_app_sul_headers INTERFACE tft_app_elevator_model)
|
target_link_libraries(tft_app_sul_headers INTERFACE tft_app_elevator_model)
|
||||||
|
|
||||||
add_subdirectory(nku_can)
|
add_subdirectory(nku_can)
|
||||||
|
add_subdirectory(demo)
|
||||||
add_subdirectory(transport/can)
|
add_subdirectory(transport/can)
|
||||||
|
add_subdirectory(transport/demo)
|
||||||
|
|
||||||
# tft_app_sul — реализация реестра. Явно знает обо всех драйверах (сейчас —
|
# tft_app_sul — реализация реестра. Явно знает обо всех драйверах (nku_can,
|
||||||
# один, nku_can); добавление протокола (Фаза 8) — новая запись в
|
# demo); добавление протокола (Фаза 8) — новая запись в sul_registry.c +
|
||||||
# sul_registry.c + новая зависимость здесь.
|
# новая зависимость здесь. Транспорты (transport/*) сюда НЕ линкуются —
|
||||||
|
# держит реестр host-тестируемым без bsp; wiring транспорт<->протокол —
|
||||||
|
# app-слой (task_sul_rx.c).
|
||||||
add_library(tft_app_sul STATIC src/sul_registry.c)
|
add_library(tft_app_sul STATIC src/sul_registry.c)
|
||||||
target_link_libraries(tft_app_sul PUBLIC tft_app_sul_headers tft_app_sul_nku_can)
|
target_link_libraries(tft_app_sul PUBLIC tft_app_sul_headers tft_app_sul_nku_can tft_app_sul_demo)
|
||||||
|
|
|
||||||
5
firmware/tft_app/src/domain/sul/demo/CMakeLists.txt
Normal file
5
firmware/tft_app/src/domain/sul/demo/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_sul_demo STATIC src/demo.c src/demo_route.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_sul_demo PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_sul_demo PUBLIC tft_app_elevator_model tft_app_sul_headers)
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* @file demo.h
|
||||||
|
* @brief Демо-протокол — синтетический источник данных, не связан с реальной
|
||||||
|
* шиной/станцией (ARCH §8, Фаза 3.3). Витрина возможностей устройства
|
||||||
|
* без СУЛ на другом конце; заодно тест того, что дескрипторный
|
||||||
|
* механизм (§8) и реестр `sul` протокол-агностичны не только для
|
||||||
|
* реального (НКУ-CAN), но и для синтетического источника.
|
||||||
|
*
|
||||||
|
* Маршрут — скриптованная "поездка" по кругу (согласовано с пользователем):
|
||||||
|
* этаж 1 → 11 с промежуточной остановкой на 7 (едет вверх), затем назад
|
||||||
|
* 11 → 1 с промежуточной остановкой на 3 (едет вниз), повтор.
|
||||||
|
*
|
||||||
|
* "Кадр" здесь — формальность, не несёт содержимого (см.
|
||||||
|
* sul_transport_demo_receive(), transport/demo.h): decode() САМ ведёт счёт
|
||||||
|
* тиков (вызовов) внутри ctx и решает, когда двигать маршрут дальше —
|
||||||
|
* остаётся чистой функцией (ctx, вызов) → (новый ctx, результат),
|
||||||
|
* host-тестируется как обычный декодер (число тиков → ожидаемый sul_result_t),
|
||||||
|
* без привязки к реальному времени (та живёт только в кадансе sul_rx_task).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_DEMO_H_
|
||||||
|
#define DOMAIN_SUL_DEMO_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
sul_result_t state;
|
||||||
|
uint8_t step; /**< индекс в маршруте, заворачивается по кругу */
|
||||||
|
uint8_t speed_idx; /**< 0..2 — медленно/норма/быстро (настройка) */
|
||||||
|
uint16_t ticks_since_step; /**< счётчик вызовов decode() до следующего шага */
|
||||||
|
} demo_ctx_t;
|
||||||
|
|
||||||
|
/** Сброс: маршрут на первом шаге, скорость — "Норма". */
|
||||||
|
void demo_init(demo_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Задать скорость прохождения маршрута.
|
||||||
|
*
|
||||||
|
* Вызывать из app-слоя, значение из настроек (proto_slice[0], §8).
|
||||||
|
* @param speed_idx 0..2 (медленно/норма/быстро); вне диапазона — клампится к 2.
|
||||||
|
*/
|
||||||
|
void demo_set_speed(demo_ctx_t *p_ctx, uint8_t speed_idx);
|
||||||
|
|
||||||
|
/** decode() для реестра sul — см. sul_decode_fn в domain/sul.h. Кадр игнорируется. */
|
||||||
|
sul_status_t demo_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_DEMO_H_ */
|
||||||
72
firmware/tft_app/src/domain/sul/demo/src/demo.c
Normal file
72
firmware/tft_app/src/domain/sul/demo/src/demo.c
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#include "domain/sul/demo.h"
|
||||||
|
|
||||||
|
#include "demo_route.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define DEMO_SPEED_MAX 2U
|
||||||
|
|
||||||
|
/* Тиков (вызовов decode()) на один шаг маршрута, по индексу скорости
|
||||||
|
* (0=медленно/1=норма/2=быстро). Тик = один опрос sul_rx_task (~100 мс,
|
||||||
|
* см. task_sul_rx.c) -> ~2с/~1с/~0.4с на шаг, полный круг ~50/25/10с. */
|
||||||
|
static const uint16_t K_TICKS_PER_STEP[DEMO_SPEED_MAX + 1U] = { 20U, 10U, 4U };
|
||||||
|
|
||||||
|
/* Спецрежим шага -> соответствующий булев сигнал sul_result_t (§6). Ровно
|
||||||
|
* один активен за раз (SUL_MODE_NORMAL -> все false) — этого достаточно для
|
||||||
|
* скриптованной демонстрации; настоящая одновременность нескольких сигналов
|
||||||
|
* (как теоретически возможно у живого НКУ-CAN, PACKET2+PACKET4 независимо)
|
||||||
|
* демо не воспроизводит — не тот масштаб задачи, при необходимости
|
||||||
|
* расширяется в demo_route.h (см. demo_step_t). */
|
||||||
|
static void apply_mode(sul_mode_t mode, sul_result_t *p_state)
|
||||||
|
{
|
||||||
|
p_state->overload = (mode == SUL_MODE_OVERLOAD);
|
||||||
|
p_state->fire_alarm = (mode == SUL_MODE_FIRE_ALARM);
|
||||||
|
p_state->lading = (mode == SUL_MODE_LADING);
|
||||||
|
p_state->maintenance = (mode == SUL_MODE_MAINTENANCE);
|
||||||
|
p_state->fireman = (mode == SUL_MODE_FIREMAN);
|
||||||
|
p_state->seismic = (mode == SUL_MODE_SEISMIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void apply_step(demo_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
const demo_step_t *p_step = &K_DEMO_ROUTE[p_ctx->step];
|
||||||
|
|
||||||
|
(void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s", p_step->p_pos);
|
||||||
|
p_ctx->state.next[0] = '\0'; /* демо не показывает "следующий этаж" */
|
||||||
|
p_ctx->state.direction = p_step->direction;
|
||||||
|
p_ctx->state.movement = (p_step->direction != SUL_DIR_NONE);
|
||||||
|
p_ctx->state.arrival = p_step->arrival;
|
||||||
|
p_ctx->state.floor_num = p_step->floor_num;
|
||||||
|
apply_mode(p_step->mode, &p_ctx->state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void demo_init(demo_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
p_ctx->state = sul_default_state();
|
||||||
|
p_ctx->step = 0U;
|
||||||
|
p_ctx->speed_idx = 1U; /* "Норма" по умолчанию */
|
||||||
|
p_ctx->ticks_since_step = 0U;
|
||||||
|
apply_step(p_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void demo_set_speed(demo_ctx_t *p_ctx, uint8_t speed_idx)
|
||||||
|
{
|
||||||
|
p_ctx->speed_idx = (speed_idx <= DEMO_SPEED_MAX) ? speed_idx : DEMO_SPEED_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
sul_status_t demo_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out)
|
||||||
|
{
|
||||||
|
(void) p_frame; /* демо не читает содержимое кадра, только факт вызова (тик) */
|
||||||
|
demo_ctx_t *p_state = (demo_ctx_t *) p_ctx;
|
||||||
|
|
||||||
|
p_state->ticks_since_step++;
|
||||||
|
if (p_state->ticks_since_step >= K_TICKS_PER_STEP[p_state->speed_idx])
|
||||||
|
{
|
||||||
|
p_state->ticks_since_step = 0U;
|
||||||
|
p_state->step = (uint8_t) ((p_state->step + 1U) % DEMO_ROUTE_LEN);
|
||||||
|
apply_step(p_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
*p_out = p_state->state;
|
||||||
|
return SUL_STATUS_OK;
|
||||||
|
}
|
||||||
34
firmware/tft_app/src/domain/sul/demo/src/demo_route.c
Normal file
34
firmware/tft_app/src/domain/sul/demo/src/demo_route.c
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#include "demo_route.h"
|
||||||
|
|
||||||
|
/* Скриптованный маршрут (согласовано с пользователем, Фаза 3.3): этаж 1 -> 11
|
||||||
|
* с промежуточной остановкой на 7 (едет вверх), затем 11 -> 1 с промежуточной
|
||||||
|
* остановкой на 3 (едет вниз), повтор. Остановки/концы — короткий гонг
|
||||||
|
* (arrival). mode = SUL_MODE_NORMAL везде — спецрежим (перегруз, пожар, ...)
|
||||||
|
* на конкретном шаге задаётся этим полем, см. demo_step_t в demo_route.h. */
|
||||||
|
const demo_step_t K_DEMO_ROUTE[DEMO_ROUTE_LEN] = {
|
||||||
|
{ "1", 1U, SUL_DIR_NONE, false, SUL_MODE_NORMAL },
|
||||||
|
{ "1", 1U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "2", 2U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "3", 3U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "4", 4U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "5", 5U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "6", 6U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "7", 7U, SUL_DIR_NONE, true, SUL_MODE_NORMAL }, /* промежуточная остановка вверх */
|
||||||
|
{ "7", 7U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "8", 8U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "9", 9U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "10", 10U, SUL_DIR_UP, false, SUL_MODE_NORMAL },
|
||||||
|
{ "11", 11U, SUL_DIR_NONE, true, SUL_MODE_NORMAL }, /* верхний этаж */
|
||||||
|
{ "11", 11U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "10", 10U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "9", 9U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "8", 8U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "7", 7U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "6", 6U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "5", 5U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "4", 4U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "3", 3U, SUL_DIR_NONE, true, SUL_MODE_NORMAL }, /* промежуточная остановка вниз */
|
||||||
|
{ "3", 3U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "2", 2U, SUL_DIR_DOWN, false, SUL_MODE_NORMAL },
|
||||||
|
{ "1", 1U, SUL_DIR_NONE, true, SUL_MODE_NORMAL }, /* нижний этаж, затем цикл сначала */
|
||||||
|
};
|
||||||
48
firmware/tft_app/src/domain/sul/demo/src/demo_route.h
Normal file
48
firmware/tft_app/src/domain/sul/demo/src/demo_route.h
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* @file demo_route.h
|
||||||
|
* @brief Скриптованный маршрут демо-протокола — ДАННЫЕ, не логика (ARCH §3,
|
||||||
|
* принцип 4: "данные, а не код"). Вынесены отдельно от decode()-логики
|
||||||
|
* (demo.c) для удобства правки — число остановок, диапазон/номера
|
||||||
|
* этажей, спецрежимы правятся здесь, не трогая demo.c.
|
||||||
|
*
|
||||||
|
* Приватный заголовок (src/, НЕ include/) — используется только demo.c
|
||||||
|
* внутри библиотеки tft_app_sul_demo, наружу (registry, app) не торчит.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_DEMO_ROUTE_H_
|
||||||
|
#define DOMAIN_SUL_DEMO_ROUTE_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Один шаг маршрута.
|
||||||
|
*
|
||||||
|
* `p_pos` и `floor_num` — РАЗНЫЕ вещи, легко перепутать:
|
||||||
|
* - `p_pos` — что РИСУЕТСЯ на экране (UTF-8 строка, как `sul_result_t.
|
||||||
|
* pos`). Может быть что угодно, что умеет активный шрифт:
|
||||||
|
* "7", "10", "П" (подвал), "-1" — не обязано быть числом.
|
||||||
|
* - `floor_num` — производный ЧИСЛОВОЙ этаж (как `sul_result_t.floor_num`),
|
||||||
|
* для БУДУЩЕЙ озвучки (Фаза 6, audio_policy) — рендера не
|
||||||
|
* касается вообще. У обычных этажей совпадает по смыслу с
|
||||||
|
* `p_pos` (напр. "7" и 7U), но не обязан: нечисловые позиции
|
||||||
|
* кодируются числом по-своему (см. `floor_number_parser()`
|
||||||
|
* в nku_can.c — тот же паттерн у реального протокола,
|
||||||
|
* подвал/минус получают свой числовой диапазон).
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const char *p_pos;
|
||||||
|
uint8_t floor_num;
|
||||||
|
sul_direction_t direction;
|
||||||
|
bool arrival; /**< гонг на этом шаге */
|
||||||
|
sul_mode_t mode; /**< спецрежим (SUL_MODE_NORMAL — нет режима) */
|
||||||
|
} demo_step_t;
|
||||||
|
|
||||||
|
#define DEMO_ROUTE_LEN 25U
|
||||||
|
|
||||||
|
extern const demo_step_t K_DEMO_ROUTE[DEMO_ROUTE_LEN];
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_DEMO_ROUTE_H_ */
|
||||||
|
|
@ -9,10 +9,10 @@
|
||||||
* decode(). Это позволяет декодеру оставаться описанным одной чистой функцией
|
* decode(). Это позволяет декодеру оставаться описанным одной чистой функцией
|
||||||
* и не тянуть за собой выделение памяти/жизненный цикл.
|
* и не тянуть за собой выделение памяти/жизненный цикл.
|
||||||
*
|
*
|
||||||
* Фаза 1 — одна запись реестра (nku_can), активный драйвер хардкожен.
|
* Активный протокол выбирается из настроек (`settings_device_t.protocol_id`,
|
||||||
* Фаза 3 добавит выбор активного протокола из настроек — сигнатура
|
* ARCH §8) через `sul_registry_set_active()` — вызывающий (app-слой) читает
|
||||||
* `sul_registry_active()` не изменится, изменится только то, что она
|
* настройки и толкает id сюда; сам registry настройки не читает (домен не
|
||||||
* возвращает.
|
* знает про settings_store, см. паттерн `nku_can_set_address()`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef DOMAIN_SUL_H_
|
#ifndef DOMAIN_SUL_H_
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
#include "domain/elevator_model.h"
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
|
@ -28,65 +29,197 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/** Результат decode() одного кадра/пакета. */
|
/** Результат decode() одного кадра/пакета. */
|
||||||
typedef enum
|
typedef enum
|
||||||
{
|
{
|
||||||
SUL_STATUS_OK = 0, /**< кадр распознан, ctx и *p_out обновлены */
|
SUL_STATUS_OK = 0, /**< кадр распознан, ctx и *p_out обновлены */
|
||||||
SUL_STATUS_IGNORED, /**< кадр не для этого драйвера — *p_out не тронут */
|
SUL_STATUS_IGNORED, /**< кадр не для этого драйвера — *p_out не тронут */
|
||||||
SUL_STATUS_ERR, /**< кадр совпал по ID, но малформирован (DLC и т.п.) */
|
SUL_STATUS_ERR, /**< кадр совпал по ID, но малформирован (DLC и т.п.) */
|
||||||
} sul_status_t;
|
} sul_status_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Кадр транспортного уровня, нейтральный к шине (CAN/UART/...).
|
* @brief Кадр транспортного уровня, нейтральный к шине (CAN/UART/...).
|
||||||
*
|
*
|
||||||
* Транспорт-адаптер (HW) заполняет её из своего протокола (для CAN — id и
|
* Транспорт-адаптер (HW) заполняет её из своего протокола (для CAN — id и
|
||||||
* data/len из bsp_can_frame_t); декодер (чистый C) её только читает.
|
* data/len из bsp_can_frame_t); декодер (чистый C) её только читает.
|
||||||
*/
|
*/
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
uint32_t id; /**< CAN ID либо адрес/маркер кадра другого транспорта */
|
uint32_t id; /**< CAN ID либо адрес/маркер кадра другого транспорта */
|
||||||
uint8_t bus; /**< на будущее — несколько шин одного типа (0 = единственная) */
|
uint8_t bus; /**< на будущее — несколько шин одного типа (0 = единственная) */
|
||||||
const uint8_t *p_data;
|
const uint8_t *p_data;
|
||||||
uint16_t len;
|
uint16_t len;
|
||||||
} sul_frame_t;
|
} sul_frame_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Чистая функция декодирования — БЕЗ единого HAL-вызова.
|
* @brief Чистая функция декодирования — БЕЗ единого HAL-вызова.
|
||||||
*
|
*
|
||||||
* @param p_ctx состояние драйвера (владеет caller, см. докстрок файла)
|
* @param p_ctx состояние драйвера (владеет caller, см. докстрок файла)
|
||||||
* @param p_frame один кадр транспортного уровня
|
* @param p_frame один кадр транспортного уровня
|
||||||
* @param p_out заполняется только при SUL_STATUS_OK
|
* @param p_out заполняется только при SUL_STATUS_OK
|
||||||
*/
|
*/
|
||||||
typedef sul_status_t (*sul_decode_fn)(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
typedef sul_status_t (*sul_decode_fn_t)(void *p_ctx, const sul_frame_t *p_frame,
|
||||||
|
sul_result_t *p_out);
|
||||||
|
|
||||||
typedef struct
|
/**
|
||||||
{
|
* @brief `sul_driver_t.connection_timeout_ms` — протокол не поддерживает
|
||||||
|
* детекцию обрыва связи по тишине (шлёт кадры ТОЛЬКО по изменению
|
||||||
|
* состояния на станции, не периодически) — таймаут-логика в
|
||||||
|
* task_sul_rx.c для него отключена целиком, «--» по тишине не
|
||||||
|
* покажется никогда для этого протокола.
|
||||||
|
*/
|
||||||
|
#define SUL_CONNECTION_TIMEOUT_DISABLED 0U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Запрос протокола на запись байта в СВОЙ proto_slice (ARCH §8) —
|
||||||
|
* напр. удалённая установка адреса у НКУ-CAN.
|
||||||
|
*
|
||||||
|
* НЕ путать с decode()/sul_result_t — это отдельный канал специально под
|
||||||
|
* settings, не под индикацию.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint8_t slice_offset; /**< куда в settings_t.user.proto_slice[] (§8) */
|
||||||
|
uint8_t value;
|
||||||
|
} sul_slice_write_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Забрать запрос протокола на самостоятельную запись в proto_slice.
|
||||||
|
*
|
||||||
|
* Чистая функция — читает НАКОПЛЕННОЕ состояние ctx после последнего
|
||||||
|
* decode(), settings_store не трогает вообще: домен не пишет настройки сам
|
||||||
|
* (см. докстрок файла), только сигнализирует через это; саму запись +
|
||||||
|
* идемпотентность (сравнение с текущим сохранённым значением — ОБЩИЙ гейт,
|
||||||
|
* одинаковый для любого протокола) делает app-слой (task_sul_rx.c).
|
||||||
|
*
|
||||||
|
* @param p_ctx ctx драйвера (см. sul_driver_t.p_ctx)
|
||||||
|
* @param p_out заполняется только при возврате true
|
||||||
|
* @return true — есть запрос, *p_out валиден; false — писать нечего.
|
||||||
|
*/
|
||||||
|
typedef bool (*sul_take_pending_write_fn_t)(void *p_ctx, sul_slice_write_t *p_out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Тип редактируемого параметра протокола (ARCH §8).
|
||||||
|
*
|
||||||
|
* Подмножество `menu_item_type_t` — без зависимости domain→menu (§4, домен не
|
||||||
|
* знает презентацию); трансляция в `menu_item_type_t` живёт на стороне menu/.
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SUL_SETTINGS_BYTE = 0U, /**< число min..max */
|
||||||
|
SUL_SETTINGS_SELECT, /**< выбор из p_options[0..max] */
|
||||||
|
SUL_SETTINGS_BOOL, /**< да/нет */
|
||||||
|
} sul_settings_type_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Один параметр протокола — строка данных (ARCH §8), не код.
|
||||||
|
*
|
||||||
|
* `slice_offset` — смещение ВНУТРИ `settings_t.user.proto_slice[]`
|
||||||
|
* (0..SETTINGS_PROTO_SLICE_LEN-1 из settings_store.h), не внутри всего
|
||||||
|
* `settings_t` — домен не включает settings_store.h, трансляцию в реальный
|
||||||
|
* offsetof() делает menu/ (единственный слой, знающий оба типа).
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const char *p_label;
|
||||||
|
sul_settings_type_t type;
|
||||||
|
uint8_t slice_offset;
|
||||||
|
uint8_t min; /**< для SELECT/BYTE/BOOL */
|
||||||
|
uint8_t max;
|
||||||
|
/** Метки для SELECT/BOOL (p_options[value]); NULL — рендер числом. */
|
||||||
|
const char *const *p_options;
|
||||||
|
} sul_settings_entry_t;
|
||||||
|
|
||||||
|
/** @brief Набор параметров протокола, регистрируется драйвером (ARCH §8). */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const sul_settings_entry_t *p_entries;
|
||||||
|
uint8_t count;
|
||||||
|
} sul_settings_desc_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
uint8_t id; /**< стабильный идентификатор протокола */
|
uint8_t id; /**< стабильный идентификатор протокола */
|
||||||
const char *p_name; /**< для меню (Фаза 3) */
|
const char *p_name; /**< для меню (выбор протокола) */
|
||||||
sul_decode_fn decode;
|
sul_decode_fn_t decode;
|
||||||
} sul_driver_t;
|
/** Параметры протокола для меню (§8); NULL — протокол без настроек. */
|
||||||
|
const sul_settings_desc_t *p_settings;
|
||||||
|
/**
|
||||||
|
* @brief Контекст decode() — статика, владеет драйвер (sul_registry.c),
|
||||||
|
* живёт постоянно (не пересоздаётся при смене активного
|
||||||
|
* протокола). Инициализируется `sul_registry_init()`.
|
||||||
|
*
|
||||||
|
* Транспорт (какую шину/источник опрашивать) сюда НЕ входит — это
|
||||||
|
* HW-специфика, остаётся wiring'ом app-слоя (task_sul_rx.c), чтобы
|
||||||
|
* реестр/декодеры оставались host-тестируемыми без bsp.
|
||||||
|
*/
|
||||||
|
void *p_ctx;
|
||||||
|
/**
|
||||||
|
* @brief Протокол сам инициирует запись в свой proto_slice (§8) — напр.
|
||||||
|
* удалённая адресация. NULL — протокол никогда этого не делает
|
||||||
|
* (большинство); app-слой (task_sul_rx.c) проверяет указатель на
|
||||||
|
* NULL перед вызовом, никакой протокол-специфичной ветки там нет.
|
||||||
|
*/
|
||||||
|
sul_take_pending_write_fn_t take_pending_write;
|
||||||
|
/**
|
||||||
|
* @brief Таймаут «потери связи» (мс) — сколько task_sul_rx.c ждёт без
|
||||||
|
* валидного кадра ЭТОГО протокола, прежде чем считать связь
|
||||||
|
* потерянной (→ sul_default_state(), «--» на экране).
|
||||||
|
*
|
||||||
|
* ОБЯЗАТЕЛЬНОЕ поле, не пользовательская настройка: реальный период
|
||||||
|
* отправки у станции — знание протокола (разные семейства/станции шлют
|
||||||
|
* раз в ~200 мс или раз в ~1 с — оператор этого не знает и не должен
|
||||||
|
* настраивать), жёстко задаётся здесь при регистрации в sul_registry.c.
|
||||||
|
* `SUL_CONNECTION_TIMEOUT_DISABLED` (0) — протокол event-driven на
|
||||||
|
* станции (шлёт только по изменению) — для него понятие «обрыва по
|
||||||
|
* тишине» в принципе некорректно, таймаут выключен целиком.
|
||||||
|
*/
|
||||||
|
uint32_t connection_timeout_ms;
|
||||||
|
} sul_driver_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Идентификаторы протоколов — стабильны, не переиспользовать.
|
* @brief Идентификаторы протоколов — стабильны, не переиспользовать.
|
||||||
*
|
*
|
||||||
* Список открыт (ARCH §2.2): УЭЛ, УКЛ, НКУ-SD7, УИМ добавляются в Фазе 8.
|
* Список открыт (ARCH §2.2): УЭЛ, УКЛ, НКУ-SD7, УИМ добавляются в Фазе 8.
|
||||||
*/
|
*/
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
SUL_PROTOCOL_NKU_CAN = 0U,
|
SUL_PROTOCOL_NKU_CAN = 0U,
|
||||||
};
|
SUL_PROTOCOL_DEMO =
|
||||||
|
1U, /**< синтетический источник, ARCH §8 — витрина/тест дескрипторного механизма */
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Активный драйвер. Фаза 1 — единственная запись, хардкод.
|
* @brief Инициализировать ctx ВСЕХ зарегистрированных драйверов (вызывать
|
||||||
|
* один раз при bringup, до первого sul_registry_active()->decode()).
|
||||||
|
*/
|
||||||
|
void sul_registry_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Активный драйвер — см. `sul_registry_set_active()`.
|
||||||
* @return указатель на статический дескриптор, никогда NULL.
|
* @return указатель на статический дескриптор, никогда NULL.
|
||||||
*/
|
*/
|
||||||
const sul_driver_t *sul_registry_active(void);
|
const sul_driver_t *sul_registry_active(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Найти драйвер по id (для будущего выбора по настройкам, Фаза 3).
|
* @brief Найти драйвер по id.
|
||||||
* @return NULL, если протокол не зарегистрирован.
|
* @return NULL, если протокол не зарегистрирован.
|
||||||
*/
|
*/
|
||||||
const sul_driver_t *sul_registry_find(uint8_t id);
|
const sul_driver_t *sul_registry_find(uint8_t id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Выбрать активный протокол (ARCH §8 — вызывается app-слоем из
|
||||||
|
* `settings_device_t.protocol_id`, дёшево при каждом вызове,
|
||||||
|
* см. паттерн `nku_can_set_address()`/CAN-фильтров).
|
||||||
|
*
|
||||||
|
* @param id id протокола. Неизвестный id — игнорируется, активный не меняется
|
||||||
|
* (защита от мусора в настройках; `sul_registry_active()` остаётся
|
||||||
|
* валидным).
|
||||||
|
*/
|
||||||
|
void sul_registry_set_active(uint8_t id);
|
||||||
|
|
||||||
|
/** @brief Число зарегистрированных протоколов (для построения меню). */
|
||||||
|
uint8_t sul_registry_count(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@
|
||||||
* Фаза 2 — полный разбор индикации: PACKET1 (направление, режимы, начало
|
* Фаза 2 — полный разбор индикации: PACKET1 (направление, режимы, начало
|
||||||
* движения), PACKET2 (перегруз), PACKET3 (позиция, гонг, временная погрузка),
|
* движения), PACKET2 (перегруз), PACKET3 (позиция, гонг, временная погрузка),
|
||||||
* PACKET4 (перегруз-вариант, сейсмо), PACKET5 (следующий этаж). Удалённая
|
* PACKET4 (перегруз-вариант, сейсмо), PACKET5 (следующий этаж). Удалённая
|
||||||
* установка адреса (0x4X1/0x5XB) — Фаза 3 (нужен settings_store для записи);
|
* установка адреса (0x4X1/0x5XB, Фаза 3.5, REMOTE_ADDRES_SETUP.pdf) —
|
||||||
* здесь эти кадры игнорируются как чужие ID.
|
* decode() только распознаёт и выставляет запрос в ctx; саму запись в
|
||||||
|
* settings_store делает app-слой через generic-канал sul_take_pending_
|
||||||
|
* write_fn_t (domain/sul.h) — nku_can_take_pending_write() ниже, decode()
|
||||||
|
* остаётся чистой функцией и не пишет настройки сам.
|
||||||
*
|
*
|
||||||
* Адрес станции (nku_address) захардкожен в 0 — фильтры/ID без сдвига группы.
|
* Адрес станции (nku_address) захардкожен в 0 — фильтры/ID без сдвига группы.
|
||||||
* Фаза 3 параметризует через настройки; сигнатура decode() не изменится.
|
* Фаза 3 параметризует через настройки; сигнатура decode() не изменится.
|
||||||
|
|
@ -26,6 +29,10 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/** Сентинел «анонса/запроса ещё не было» для полей удалённой адресации ниже —
|
||||||
|
* вне диапазона валидных X (4-битный нибл ID, 0..15). */
|
||||||
|
#define NKU_REMOTE_ADDR_NONE 0xFFU
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Состояние декодера — накопленный текущий sul_result_t + внутренние
|
* @brief Состояние декодера — накопленный текущий sul_result_t + внутренние
|
||||||
* латчи для полей с несколькими источниками.
|
* латчи для полей с несколькими источниками.
|
||||||
|
|
@ -52,9 +59,28 @@ typedef struct
|
||||||
uint8_t current_level; /**< PACKET1: data[3] & 0x3F — числовой уровень остановки,
|
uint8_t current_level; /**< PACKET1: data[3] & 0x3F — числовой уровень остановки,
|
||||||
для гейта «следующего этажа» в PACKET5 */
|
для гейта «следующего этажа» в PACKET5 */
|
||||||
uint8_t nku_address; /**< адрес станции 0..15 — сдвиг ID пакетов (из настроек) */
|
uint8_t nku_address; /**< адрес станции 0..15 — сдвиг ID пакетов (из настроек) */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Удалённая установка адреса (REMOTE_ADDRES_SETUP.pdf, §3.5).
|
||||||
|
*
|
||||||
|
* 1. Кадр 0x4X1 — X (биты [7:4] ID) объявляет адрес станции управления,
|
||||||
|
* копируется сюда безусловно (не во флеш — это делает app-слой).
|
||||||
|
* 2. Кадр 0x5XB — команда в старшем нибле data[3]; "2" запускает запись,
|
||||||
|
* но только если X ЭТОГО кадра совпадает с последним объявленным
|
||||||
|
* (согласовано — строже буквы PDF, которая номинально допускает
|
||||||
|
* любой X у командного кадра).
|
||||||
|
* decode() НЕ пишет settings — только выставляет pending_remote_write_
|
||||||
|
* addr; nku_can_take_pending_write() ниже транслирует его в generic
|
||||||
|
* sul_slice_write_t для app-слоя (идемпотентность — сравнение с текущим
|
||||||
|
* сохранённым значением — общий гейт для ЛЮБОГО протокола, живёт в
|
||||||
|
* task_sul_rx.c, не здесь).
|
||||||
|
*/
|
||||||
|
uint8_t remote_addr_candidate; /**< последний X из 0x4X1; NKU_REMOTE_ADDR_NONE — анонса не было */
|
||||||
|
uint8_t pending_remote_write_addr; /**< валиден ТОЛЬКО сразу после decode() этого вызова; NKU_REMOTE_ADDR_NONE — команды в этом кадре не было */
|
||||||
} nku_can_ctx_t;
|
} nku_can_ctx_t;
|
||||||
|
|
||||||
/** Сброс к состоянию по умолчанию (sul_default_state()); адрес станции = 0. */
|
/** Сброс к состоянию по умолчанию (sul_default_state()); адрес станции = 0;
|
||||||
|
* состояние удалённой адресации — «анонса/запроса не было». */
|
||||||
void nku_can_init(nku_can_ctx_t *p_ctx);
|
void nku_can_init(nku_can_ctx_t *p_ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -74,6 +100,20 @@ void nku_can_set_address(nku_can_ctx_t *p_ctx, uint8_t nku_address);
|
||||||
*/
|
*/
|
||||||
sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Реализация sul_take_pending_write_fn_t (domain/sul.h) для НКУ-CAN —
|
||||||
|
* удалённая установка адреса (§3.5).
|
||||||
|
*
|
||||||
|
* @param p_ctx nku_can_ctx_t* после последнего decode()
|
||||||
|
* @param p_out slice_offset=0 (proto_slice[0] = адрес), value = запрошенный
|
||||||
|
* адрес; заполняется только при возврате true
|
||||||
|
* @return true, если pending_remote_write_addr валиден в этом ctx (декодер
|
||||||
|
* только что распознал команду записи — см. nku_can_ctx_t выше).
|
||||||
|
* Идемпотентность НЕ его забота — сравнение с текущим сохранённым
|
||||||
|
* значением делает вызывающий (общий гейт для любого протокола).
|
||||||
|
*/
|
||||||
|
bool nku_can_take_pending_write(void *p_ctx, sul_slice_write_t *p_out);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,18 @@
|
||||||
|
|
||||||
#define NKU_ADDRESS_MAX 15U /* адрес 0..15; group4 = addr<<4 */
|
#define NKU_ADDRESS_MAX 15U /* адрес 0..15; group4 = addr<<4 */
|
||||||
|
|
||||||
|
/* ── Удалённая установка адреса (REMOTE_ADDRES_SETUP.pdf, §3.5) ──────────────
|
||||||
|
* Маска проверяет биты [10:8] и [3:0] ID, игнорирует адресный нибл X [7:4] —
|
||||||
|
* так распознаём кадр НЕЗАВИСИМО от X (адрес станции управления, не наш). */
|
||||||
|
#define REMOTE_ADDR_ID_MASK 0x70FU
|
||||||
|
#define REMOTE_ANNOUNCE_ID 0x401U /* 0x4X1 — анонс адреса станции управления */
|
||||||
|
#define REMOTE_CMD_ID 0x50BU /* 0x5XB — несущая команды (тот же ID, что PACKET4_BASE:
|
||||||
|
* наш собственный 0x50B|group4 тоже сюда попадает —
|
||||||
|
* не конфликт, команда/PACKET4 распознаются независимо */
|
||||||
|
#define REMOTE_CMD_WRITE_ADDR 0x2U /* команда "2" в старшем нибле data[3] — записать адрес */
|
||||||
|
#define REMOTE_CMD_DLC_MIN 4U /* нужен минимум data[3] — короче реального PROTO_DLC,
|
||||||
|
* но 0x5XB с чужим X не проходит общий DLC-гейт ниже */
|
||||||
|
|
||||||
#define ARROW_MASK 0x03U /* PACKET1 data[6][1:0] — стрелка */
|
#define ARROW_MASK 0x03U /* PACKET1 data[6][1:0] — стрелка */
|
||||||
#define MOVEMENT_MASK 0x0CU /* PACKET1 data[6][3:2] — начало движения */
|
#define MOVEMENT_MASK 0x0CU /* PACKET1 data[6][3:2] — начало движения */
|
||||||
#define ICON_MASK 0xF0U /* PACKET1 data[6][7:4] — код режима */
|
#define ICON_MASK 0xF0U /* PACKET1 data[6][7:4] — код режима */
|
||||||
|
|
@ -62,6 +74,8 @@ void nku_can_init(nku_can_ctx_t *p_ctx)
|
||||||
p_ctx->lading_instr = false;
|
p_ctx->lading_instr = false;
|
||||||
p_ctx->current_level = 0U;
|
p_ctx->current_level = 0U;
|
||||||
p_ctx->nku_address = 0U; /* Фаза 3.1: caller задаёт из настроек через set_address() */
|
p_ctx->nku_address = 0U; /* Фаза 3.1: caller задаёт из настроек через set_address() */
|
||||||
|
p_ctx->remote_addr_candidate = NKU_REMOTE_ADDR_NONE;
|
||||||
|
p_ctx->pending_remote_write_addr = NKU_REMOTE_ADDR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void nku_can_set_address(nku_can_ctx_t *p_ctx, uint8_t nku_address)
|
void nku_can_set_address(nku_can_ctx_t *p_ctx, uint8_t nku_address)
|
||||||
|
|
@ -233,10 +247,48 @@ static bool decode_packet5(nku_can_ctx_t *p_ctx, const uint8_t *p_data)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Удалённая установка адреса (REMOTE_ADDRES_SETUP.pdf) — независимая
|
||||||
|
* от nku_address side-проверка на СЫРОМ ID/data кадра, не влияет на
|
||||||
|
* классификацию PACKET1..5 ниже (один и тот же 0x5XB может быть и
|
||||||
|
* нашим PACKET4, и несущей команды одновременно — не конфликт).
|
||||||
|
*
|
||||||
|
* Транзитная (не латч): p_ctx->pending_remote_write_addr сбрасывается перед
|
||||||
|
* каждым вызовом в nku_can_decode() и выставляется заново только если ИМЕННО
|
||||||
|
* этот кадр — валидная команда записи.
|
||||||
|
*/
|
||||||
|
static void check_remote_address(nku_can_ctx_t *p_ctx, const sul_frame_t *p_frame)
|
||||||
|
{
|
||||||
|
const uint32_t ID = p_frame->id;
|
||||||
|
|
||||||
|
if ((ID & REMOTE_ADDR_ID_MASK) == REMOTE_ANNOUNCE_ID)
|
||||||
|
{
|
||||||
|
/* 0x4X1: X (биты [7:4]) — адрес станции управления. В ОЗУ, не во
|
||||||
|
* флеш (PDF п.1) — запись делает app-слой по команде ниже. */
|
||||||
|
p_ctx->remote_addr_candidate = (uint8_t) ((ID >> 4U) & 0x0FU);
|
||||||
|
}
|
||||||
|
else if (((ID & REMOTE_ADDR_ID_MASK) == REMOTE_CMD_ID) && (p_frame->len >= REMOTE_CMD_DLC_MIN))
|
||||||
|
{
|
||||||
|
/* 0x5XB: команда в старшем нибле data[3]. "2" запускает запись, но
|
||||||
|
* только если X ЭТОГО кадра совпадает с последним объявленным адресом
|
||||||
|
* (согласовано с пользователем — строже буквы PDF п.2, которая
|
||||||
|
* номинально допускает любой X у командного кадра). */
|
||||||
|
const uint8_t CMD = (uint8_t) ((p_frame->p_data[3] >> 4U) & 0x0FU);
|
||||||
|
const uint8_t CMD_X = (uint8_t) ((ID >> 4U) & 0x0FU);
|
||||||
|
if ((CMD == REMOTE_CMD_WRITE_ADDR) && (CMD_X == p_ctx->remote_addr_candidate))
|
||||||
|
{
|
||||||
|
p_ctx->pending_remote_write_addr = p_ctx->remote_addr_candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out)
|
sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out)
|
||||||
{
|
{
|
||||||
nku_can_ctx_t *p_state = (nku_can_ctx_t *) p_ctx;
|
nku_can_ctx_t *p_state = (nku_can_ctx_t *) p_ctx;
|
||||||
|
|
||||||
|
p_state->pending_remote_write_addr = NKU_REMOTE_ADDR_NONE; /* транзитно, см. докстрок выше */
|
||||||
|
check_remote_address(p_state, p_frame);
|
||||||
|
|
||||||
/* Сдвиг ID по адресу станции (id пакетов адресно-зависим). */
|
/* Сдвиг ID по адресу станции (id пакетов адресно-зависим). */
|
||||||
const uint32_t G4 = (uint32_t) p_state->nku_address << 4U;
|
const uint32_t G4 = (uint32_t) p_state->nku_address << 4U;
|
||||||
const uint32_t G6 = (uint32_t) p_state->nku_address << 6U;
|
const uint32_t G6 = (uint32_t) p_state->nku_address << 6U;
|
||||||
|
|
@ -288,3 +340,17 @@ sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_
|
||||||
*p_out = p_state->state;
|
*p_out = p_state->state;
|
||||||
return SUL_STATUS_OK;
|
return SUL_STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool nku_can_take_pending_write(void *p_ctx, sul_slice_write_t *p_out)
|
||||||
|
{
|
||||||
|
const nku_can_ctx_t *p_state = (const nku_can_ctx_t *) p_ctx;
|
||||||
|
|
||||||
|
if (p_state->pending_remote_write_addr == NKU_REMOTE_ADDR_NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
p_out->slice_offset = 0U; /* proto_slice[0] = адрес, см. K_NKU_CAN_SETTINGS в sul_registry.c */
|
||||||
|
p_out->value = p_state->pending_remote_write_addr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,90 @@
|
||||||
#include "domain/sul.h"
|
#include "domain/sul.h"
|
||||||
|
#include "domain/sul/demo.h"
|
||||||
#include "domain/sul/nku_can.h"
|
#include "domain/sul/nku_can.h"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* НКУ-CAN — один параметр: адрес станции 0..15 (proto_slice[0], §8). */
|
||||||
|
static const sul_settings_entry_t K_NKU_CAN_SETTINGS_ENTRIES[] = {
|
||||||
|
{
|
||||||
|
.p_label = "Адрес",
|
||||||
|
.type = SUL_SETTINGS_BYTE,
|
||||||
|
.slice_offset = 0U,
|
||||||
|
.min = 0U,
|
||||||
|
.max = 15U,
|
||||||
|
.p_options = NULL,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const sul_settings_desc_t K_NKU_CAN_SETTINGS = {
|
||||||
|
.p_entries = K_NKU_CAN_SETTINGS_ENTRIES,
|
||||||
|
.count = sizeof(K_NKU_CAN_SETTINGS_ENTRIES) / sizeof(K_NKU_CAN_SETTINGS_ENTRIES[0]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Демо — один параметр: скорость прохождения скриптованного маршрута (§8).
|
||||||
|
* Тест того, что дескрипторный механизм не завязан на "адрес"-подобную
|
||||||
|
* форму параметра (НКУ-CAN) — здесь SELECT с метками, не BYTE. */
|
||||||
|
static const char *const K_DEMO_SPEED_LABELS[] = { "Медленно", "Норма", "Быстро" };
|
||||||
|
|
||||||
|
static const sul_settings_entry_t K_DEMO_SETTINGS_ENTRIES[] = {
|
||||||
|
{
|
||||||
|
.p_label = "Скорость",
|
||||||
|
.type = SUL_SETTINGS_SELECT,
|
||||||
|
.slice_offset = 0U,
|
||||||
|
.min = 0U,
|
||||||
|
.max = 2U,
|
||||||
|
.p_options = K_DEMO_SPEED_LABELS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const sul_settings_desc_t K_DEMO_SETTINGS = {
|
||||||
|
.p_entries = K_DEMO_SETTINGS_ENTRIES,
|
||||||
|
.count = sizeof(K_DEMO_SETTINGS_ENTRIES) / sizeof(K_DEMO_SETTINGS_ENTRIES[0]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Ctx каждого драйвера — статика, живёт постоянно (см. domain/sul.h,
|
||||||
|
* .p_ctx). Инициализация — sul_registry_init(). */
|
||||||
|
static nku_can_ctx_t s_nku_can_ctx;
|
||||||
|
static demo_ctx_t s_demo_ctx;
|
||||||
|
|
||||||
static const sul_driver_t s_registry[] = {
|
static const sul_driver_t s_registry[] = {
|
||||||
{
|
{
|
||||||
.id = SUL_PROTOCOL_NKU_CAN,
|
.id = SUL_PROTOCOL_NKU_CAN,
|
||||||
.p_name = "НКУ-CAN",
|
.p_name = "НКУ-CAN",
|
||||||
.decode = nku_can_decode,
|
.decode = nku_can_decode,
|
||||||
|
.p_settings = &K_NKU_CAN_SETTINGS,
|
||||||
|
.p_ctx = &s_nku_can_ctx,
|
||||||
|
.take_pending_write = nku_can_take_pending_write, /* удалённая адресация, §3.5 */
|
||||||
|
.connection_timeout_ms = 3000U, /* как в OLD_PROJECT (~3с на отметку потери связи) */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.id = SUL_PROTOCOL_DEMO,
|
||||||
|
.p_name = "Демо",
|
||||||
|
.decode = demo_decode,
|
||||||
|
.p_settings = &K_DEMO_SETTINGS,
|
||||||
|
.p_ctx = &s_demo_ctx,
|
||||||
|
/* .take_pending_write не задан — демо никогда не пишет settings сам */
|
||||||
|
.connection_timeout_ms = SUL_CONNECTION_TIMEOUT_DISABLED, /* синтетический источник, обрыва не бывает */
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#define REGISTRY_COUNT (sizeof(s_registry) / sizeof(s_registry[0]))
|
#define REGISTRY_COUNT (sizeof(s_registry) / sizeof(s_registry[0]))
|
||||||
|
|
||||||
|
void sul_registry_init(void)
|
||||||
|
{
|
||||||
|
nku_can_init(&s_nku_can_ctx);
|
||||||
|
demo_init(&s_demo_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Активный id (§8) — толкает app-слой из settings_device_t.protocol_id через
|
||||||
|
* sul_registry_set_active(); дефолт — первая запись реестра (никогда NULL,
|
||||||
|
* даже до первого вызова set_active(), напр. на самых ранних этапах bringup). */
|
||||||
|
static uint8_t g_s_active_id = SUL_PROTOCOL_NKU_CAN;
|
||||||
|
|
||||||
const sul_driver_t *sul_registry_active(void)
|
const sul_driver_t *sul_registry_active(void)
|
||||||
{
|
{
|
||||||
/* Фаза 1: единственная запись, хардкод. Фаза 3 выберет по настройкам. */
|
const sul_driver_t *p_driver = sul_registry_find(g_s_active_id);
|
||||||
return &s_registry[0];
|
return (p_driver != NULL) ? p_driver : &s_registry[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sul_driver_t *sul_registry_find(uint8_t id)
|
const sul_driver_t *sul_registry_find(uint8_t id)
|
||||||
|
|
@ -31,3 +98,17 @@ const sul_driver_t *sul_registry_find(uint8_t id)
|
||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sul_registry_set_active(uint8_t id)
|
||||||
|
{
|
||||||
|
if (sul_registry_find(id) != NULL)
|
||||||
|
{
|
||||||
|
g_s_active_id = id;
|
||||||
|
}
|
||||||
|
/* неизвестный id — игнорируется, s_active_id не меняется (см. sul.h) */
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t sul_registry_count(void)
|
||||||
|
{
|
||||||
|
return (uint8_t) REGISTRY_COUNT;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,23 @@
|
||||||
|
|
||||||
#define NKU_ADDRESS_MAX 15U /* 4-битный адрес, group4 = addr<<4 */
|
#define NKU_ADDRESS_MAX 15U /* 4-битный адрес, group4 = addr<<4 */
|
||||||
|
|
||||||
/* MB index 0..4 — PACKET1..5 соответственно (см. sul_transport_can_set_address). */
|
/* Удалённая установка адреса (§3.5, REMOTE_ADDRES_SETUP): кадры 0x4X1
|
||||||
|
* (анонс адреса станции) и 0x5XB (несущая команды) должны приниматься с
|
||||||
|
* ЛЮБЫМ X — по определению фичи наш сохранённый адрес мог не совпадать с
|
||||||
|
* адресом станции. Точные фильтры PACKET1..5 (маска 0x7FF) такие кадры
|
||||||
|
* аппаратно отбрасывают: 0x4X1 не совпадает ни с одним из них вообще, а
|
||||||
|
* 0x5XB — только при X == наш адрес (это PACKET4). Поэтому два
|
||||||
|
* ДОПОЛНИТЕЛЬНЫХ wildcard-фильтра с маской, игнорирующей адресный нибл
|
||||||
|
* [7:4]: проверяются биты [10:8] и [3:0] ID. Порт apply_remote_addr_filters()
|
||||||
|
* из OLD_PROJECT (main_programm.c) — БЕЗ них удалённая адресация не работает
|
||||||
|
* вовсе (decode до кадров не доходит; найдено на стенде, host-тесты этого не
|
||||||
|
* ловят — они кормят decode() напрямую, мимо HW-фильтров). */
|
||||||
|
#define REMOTE_ADDR_ID_MASK 0x70FU
|
||||||
|
#define REMOTE_ANNOUNCE_ID 0x401U /* 0x4X1 — анонс адреса станции */
|
||||||
|
#define REMOTE_CMD_ID 0x50BU /* 0x5XB — несущая команды (X — любой) */
|
||||||
|
|
||||||
|
/* MB index 0..4 — PACKET1..5 (точные, зависят от адреса);
|
||||||
|
* MB index 5..6 — wildcard удалённой адресации (от адреса НЕ зависят). */
|
||||||
|
|
||||||
/* Хранилище последнего принятого кадра — см. предупреждение в can.h про
|
/* Хранилище последнего принятого кадра — см. предупреждение в can.h про
|
||||||
* время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */
|
* время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */
|
||||||
|
|
@ -76,6 +92,23 @@ bsp_status_t sul_transport_can_set_address(uint8_t nku_address)
|
||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wildcard-фильтры удалённой адресации (см. блок констант выше). От
|
||||||
|
* адреса не зависят — но живут здесь же, а не в init(): применение
|
||||||
|
* идемпотентно и дёшево (адрес меняется редко), зато ВСЕ фильтры
|
||||||
|
* настраиваются одной функцией в одном месте — нет второй точки входа,
|
||||||
|
* которую можно забыть позвать (init() фильтры не трогает намеренно,
|
||||||
|
* см. комментарий там). */
|
||||||
|
st = bsp_can_set_filter(5U, REMOTE_ANNOUNCE_ID, REMOTE_ADDR_ID_MASK, false);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
st = bsp_can_set_filter(6U, REMOTE_CMD_ID, REMOTE_ADDR_ID_MASK, false);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
s_last_applied_address = ADDR;
|
s_last_applied_address = ADDR;
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_sul_transport_demo STATIC src/demo_transport.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_sul_transport_demo PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_sul_transport_demo PUBLIC tft_app_sul_headers bsp_status)
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* @file demo.h
|
||||||
|
* @brief "Транспорт" демо-протокола (ARCH §8, Фаза 3.3) — нет реальной шины.
|
||||||
|
*
|
||||||
|
* У демо-протокола нет транспорта в привычном смысле: sul_transport_demo_
|
||||||
|
* receive() ничего не ждёт и не читает с шины — сразу возвращает пустой
|
||||||
|
* sul_frame_t. Кадр — формальность ради существующего контракта sul_decode_fn
|
||||||
|
* (см. demo.h, domain/sul/demo/) — demo_decode() его содержимое игнорирует,
|
||||||
|
* ведёт счёт тиков сам. Существует, чтобы sul_rx_task мог опрашивать демо
|
||||||
|
* тем же паттерном receive()->decode(), что и реальные протоколы.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_TRANSPORT_DEMO_H_
|
||||||
|
#define DOMAIN_SUL_TRANSPORT_DEMO_H_
|
||||||
|
|
||||||
|
#include "bsp/status.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Всегда успешно и мгновенно (не блокируется) — демо не ждёт шину.
|
||||||
|
*
|
||||||
|
* @param timeout_ms игнорируется (нечего ждать)
|
||||||
|
* @param p_out заполняется пустым кадром (id=0, len=0)
|
||||||
|
* @return BSP_OK всегда
|
||||||
|
*/
|
||||||
|
bsp_status_t sul_transport_demo_receive(uint32_t timeout_ms, sul_frame_t *p_out);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_TRANSPORT_DEMO_H_ */
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#include "domain/sul/transport/demo.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
bsp_status_t sul_transport_demo_receive(uint32_t timeout_ms, sul_frame_t *p_out)
|
||||||
|
{
|
||||||
|
(void) timeout_ms;
|
||||||
|
|
||||||
|
p_out->id = 0U;
|
||||||
|
p_out->bus = 0U;
|
||||||
|
p_out->p_data = NULL;
|
||||||
|
p_out->len = 0U;
|
||||||
|
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
@ -5,4 +5,6 @@ add_library(tft_app_menu STATIC src/menu.c src/menu_tree.c)
|
||||||
target_include_directories(tft_app_menu PUBLIC include/)
|
target_include_directories(tft_app_menu PUBLIC include/)
|
||||||
|
|
||||||
# settings_store — только типы settings_t (заголовок чистый, без QSPI-кода).
|
# settings_store — только типы settings_t (заголовок чистый, без QSPI-кода).
|
||||||
target_link_libraries(tft_app_menu PUBLIC tft_app_settings_store)
|
# tft_app_sul — menu_tree_refresh_protocol_section() читает sul_registry_*()
|
||||||
|
# (§8: секция "Протокол" строится из sul_settings_desc_t активного драйвера).
|
||||||
|
target_link_libraries(tft_app_menu PUBLIC tft_app_settings_store tft_app_sul)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,24 @@ const menu_item_desc_t *menu_tree_items(void);
|
||||||
/** Число пунктов в дереве. */
|
/** Число пунктов в дереве. */
|
||||||
uint8_t menu_tree_count(void);
|
uint8_t menu_tree_count(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Перестроить секцию "Протокол" дерева из активного sul_settings_desc_t
|
||||||
|
* (ARCH §8) — метки выбора протокола (из реестра) + единственный
|
||||||
|
* параметр активного протокола (label/тип/диапазон/offset/options);
|
||||||
|
* заодно клампит текущее значение параметра под новый диапазон
|
||||||
|
* (proto_slice[0] мог остаться от протокола с более широким
|
||||||
|
* диапазоном — иначе рендер читал бы options[] за границей).
|
||||||
|
*
|
||||||
|
* @param p_settings_rw активные настройки (то же, что передано в menu_init());
|
||||||
|
* функция не сохраняет — только клампит поле на месте.
|
||||||
|
*
|
||||||
|
* Вызывать: один раз при bringup (после settings_store_load(), до первого
|
||||||
|
* открытия меню) и после каждого menu_action() — дёшево (копия нескольких
|
||||||
|
* полей + короткий цикл по реестру), тот же паттерн, что переприменение
|
||||||
|
* адреса/CAN-фильтров в sul_rx_task.
|
||||||
|
*/
|
||||||
|
void menu_tree_refresh_protocol_section(settings_t *p_settings_rw);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,41 @@
|
||||||
#include "menu/menu_tree.h"
|
#include "menu/menu_tree.h"
|
||||||
|
|
||||||
|
#include "domain/sul.h"
|
||||||
#include "services/settings_store.h"
|
#include "services/settings_store.h"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
/* Метки значений (ярус B/устройство). */
|
/* Метки значений (ярус B/устройство). */
|
||||||
static const char *const K_BOOL_LABELS[] = { "Выкл", "Вкл" };
|
static const char *const K_BOOL_LABELS[] = { "Выкл", "Вкл" };
|
||||||
static const char *const K_PROTO_LABELS[] = { "НКУ-CAN" }; /* пока один протокол (§8) */
|
|
||||||
|
/* Верхняя граница на число протоколов в реестре — только размер буфера меток
|
||||||
|
* (menu_tree_refresh_protocol_section), не ограничение самого реестра.
|
||||||
|
* Сейчас 2 (НКУ-CAN, демо), с запасом под Фазу 8 (+ УИМ/SD7/УЭЛ/УКЛ — 6). */
|
||||||
|
#define MENU_TREE_MAX_PROTOCOLS 8U
|
||||||
|
static const char *s_proto_labels[MENU_TREE_MAX_PROTOCOLS] = { "НКУ-CAN" }; /* фолбэк до refresh() */
|
||||||
|
|
||||||
/* Индексы пунктов дерева. */
|
/* Индексы пунктов дерева. */
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
T_ROOT = 0,
|
T_ROOT = 0,
|
||||||
T_PROTO,
|
T_PROTO,
|
||||||
T_ADDR,
|
T_PROTO_PARAM, /* единственный параметр АКТИВНОГО протокола (§8) — популируется
|
||||||
|
* из sul_settings_desc_t, см. menu_tree_refresh_protocol_section() */
|
||||||
T_LOG,
|
T_LOG,
|
||||||
T_EXIT,
|
T_EXIT,
|
||||||
T_COUNT,
|
T_COUNT,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Боевое дерево Фазы 3. Действуют сейчас: протокол+адрес (→ декодер), логи
|
/* Боевое дерево Фазы 3. Действуют сейчас: протокол+параметр (→ декодер),
|
||||||
* (→ рантайм-гейт). Остальной каталог настроек — по мере своих фаз. */
|
* логи (→ рантайм-гейт). Остальной каталог настроек — по мере своих фаз.
|
||||||
static const menu_item_desc_t K_TREE[T_COUNT] = {
|
*
|
||||||
|
* НЕ const: секцию протокола (T_PROTO.max/.options, T_PROTO_PARAM целиком)
|
||||||
|
* популирует menu_tree_refresh_protocol_section() из активного
|
||||||
|
* sul_settings_desc_t — дескрипторный принцип (§8) применён и к выбору
|
||||||
|
* протокола, не только к его параметрам. Значения ниже — safe-фолбэк на
|
||||||
|
* случай, если refresh() почему-то не вызван (совпадает с тем, что было
|
||||||
|
* до Фазы 3.3, когда протокол был всего один). */
|
||||||
|
static menu_item_desc_t s_tree[T_COUNT] = {
|
||||||
[T_ROOT] = { .label = "Настройки",
|
[T_ROOT] = { .label = "Настройки",
|
||||||
.type = MENU_SUBMENU,
|
.type = MENU_SUBMENU,
|
||||||
.parent = MENU_ROOT_INDEX,
|
.parent = MENU_ROOT_INDEX,
|
||||||
|
|
@ -31,10 +45,10 @@ static const menu_item_desc_t K_TREE[T_COUNT] = {
|
||||||
.type = MENU_SELECT,
|
.type = MENU_SELECT,
|
||||||
.value_offset = offsetof(settings_t, device.protocol_id),
|
.value_offset = offsetof(settings_t, device.protocol_id),
|
||||||
.min = 0U,
|
.min = 0U,
|
||||||
.max = 0U, /* один протокол; диапазон вырастет в Фазе 8 */
|
.max = 0U,
|
||||||
.parent = MENU_ROOT_INDEX,
|
.parent = MENU_ROOT_INDEX,
|
||||||
.options = K_PROTO_LABELS },
|
.options = s_proto_labels },
|
||||||
[T_ADDR] = { .label = "Адрес",
|
[T_PROTO_PARAM] = { .label = "Адрес",
|
||||||
.type = MENU_BYTE,
|
.type = MENU_BYTE,
|
||||||
.value_offset = offsetof(settings_t, user.proto_slice[0]),
|
.value_offset = offsetof(settings_t, user.proto_slice[0]),
|
||||||
.min = 0U,
|
.min = 0U,
|
||||||
|
|
@ -50,9 +64,77 @@ static const menu_item_desc_t K_TREE[T_COUNT] = {
|
||||||
[T_EXIT] = { .label = "Выход", .type = MENU_BACK, .parent = MENU_ROOT_INDEX },
|
[T_EXIT] = { .label = "Выход", .type = MENU_BACK, .parent = MENU_ROOT_INDEX },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static menu_item_type_t menu_type_from_sul(sul_settings_type_t type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case SUL_SETTINGS_SELECT:
|
||||||
|
return MENU_SELECT;
|
||||||
|
case SUL_SETTINGS_BOOL:
|
||||||
|
return MENU_BOOL;
|
||||||
|
case SUL_SETTINGS_BYTE:
|
||||||
|
default:
|
||||||
|
return MENU_BYTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_tree_refresh_protocol_section(settings_t *p_settings_rw)
|
||||||
|
{
|
||||||
|
/* Метки выбора протокола — имена из реестра, не хардкод (§8). */
|
||||||
|
const uint8_t COUNT = sul_registry_count();
|
||||||
|
const uint8_t VISIBLE = (COUNT < MENU_TREE_MAX_PROTOCOLS) ? COUNT : MENU_TREE_MAX_PROTOCOLS;
|
||||||
|
for (uint8_t i = 0U; i < VISIBLE; i++)
|
||||||
|
{
|
||||||
|
const sul_driver_t *p_drv = sul_registry_find(i);
|
||||||
|
s_proto_labels[i] = (p_drv != NULL) ? p_drv->p_name : "?";
|
||||||
|
}
|
||||||
|
s_tree[T_PROTO].max = (uint8_t) (VISIBLE - 1U);
|
||||||
|
|
||||||
|
/* Единственный параметр активного протокола (§8). Сейчас у каждого
|
||||||
|
* зарегистрированного протокола ровно один (НКУ-CAN: адрес; демо:
|
||||||
|
* скорость) — N>1 на протокол и скрытие неиспользуемых слотов
|
||||||
|
* понадобится Фазе 8, не усложняем заранее (YAGNI). */
|
||||||
|
const sul_driver_t *p_active = sul_registry_active();
|
||||||
|
const sul_settings_desc_t *p_settings = p_active->p_settings;
|
||||||
|
|
||||||
|
if ((p_settings != NULL) && (p_settings->count > 0U))
|
||||||
|
{
|
||||||
|
const sul_settings_entry_t *p_entry = &p_settings->p_entries[0];
|
||||||
|
|
||||||
|
s_tree[T_PROTO_PARAM].label = p_entry->p_label;
|
||||||
|
s_tree[T_PROTO_PARAM].type = menu_type_from_sul(p_entry->type);
|
||||||
|
s_tree[T_PROTO_PARAM].value_offset =
|
||||||
|
(uint16_t) (offsetof(settings_t, user.proto_slice) + p_entry->slice_offset);
|
||||||
|
s_tree[T_PROTO_PARAM].min = p_entry->min;
|
||||||
|
s_tree[T_PROTO_PARAM].max = p_entry->max;
|
||||||
|
s_tree[T_PROTO_PARAM].options = p_entry->p_options;
|
||||||
|
|
||||||
|
/* Клампим ТЕКУЩЕЕ значение под новый диапазон — proto_slice[0] мог
|
||||||
|
* остаться от другого протокола с более широким диапазоном (напр.
|
||||||
|
* адрес НКУ-CAN 0..15 -> скорость демо 0..2); без этого рендер читал
|
||||||
|
* бы options[value] за пределами массива меток нового протокола. */
|
||||||
|
uint8_t *p_val = (uint8_t *) p_settings_rw + s_tree[T_PROTO_PARAM].value_offset;
|
||||||
|
if (*p_val > p_entry->max)
|
||||||
|
{
|
||||||
|
*p_val = p_entry->max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Протокол без параметров — инертный дефолт (не встречается пока
|
||||||
|
* ни у одного зарегистрированного протокола). */
|
||||||
|
s_tree[T_PROTO_PARAM].label = "—";
|
||||||
|
s_tree[T_PROTO_PARAM].type = MENU_BYTE;
|
||||||
|
s_tree[T_PROTO_PARAM].value_offset = offsetof(settings_t, user.proto_slice[0]);
|
||||||
|
s_tree[T_PROTO_PARAM].min = 0U;
|
||||||
|
s_tree[T_PROTO_PARAM].max = 0U;
|
||||||
|
s_tree[T_PROTO_PARAM].options = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const menu_item_desc_t *menu_tree_items(void)
|
const menu_item_desc_t *menu_tree_items(void)
|
||||||
{
|
{
|
||||||
return K_TREE;
|
return s_tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t menu_tree_count(void)
|
uint8_t menu_tree_count(void)
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -14,4 +14,5 @@ target_include_directories(
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
tft_app_settings_store
|
tft_app_settings_store
|
||||||
PUBLIC bsp_qspi_flash # bsp/qspi_flash.h (API + геометрия), bsp/status.h транзитивно
|
PUBLIC bsp_qspi_flash # bsp/qspi_flash.h (API + геометрия), bsp/status.h транзитивно
|
||||||
tft_app_partition) # смещения регионов (§10)
|
tft_app_partition # смещения регионов (§10)
|
||||||
|
freertos_kernel) # мьютекс save() — sul_rx_task (§3.5) и menu_task пишут конкурентно
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,51 @@
|
||||||
#include "services/settings_store.h"
|
#include "services/settings_store.h"
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
#include "bsp/qspi_flash.h"
|
#include "bsp/qspi_flash.h"
|
||||||
|
#include "semphr.h"
|
||||||
#include "services/partition.h"
|
#include "services/partition.h"
|
||||||
#include "settings_codec.h"
|
#include "settings_codec.h"
|
||||||
|
|
||||||
/* Активные настройки — единственный экземпляр модуля. */
|
/* Активные настройки — единственный экземпляр модуля. */
|
||||||
static settings_t g_settings;
|
static settings_t g_settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Защита save() от параллельного вызова из двух задач — сейчас
|
||||||
|
* menu_task (сохранение при выходе из меню) и sul_rx_task (запись
|
||||||
|
* удалённо заданного адреса НКУ-CAN, §3.5).
|
||||||
|
*
|
||||||
|
* menu_task переключает `g_menu_active=false` ДО своего save() (см.
|
||||||
|
* task_menu.c — иначе sul_rx_task лишние мс держал бы мягкую паузу) — то
|
||||||
|
* есть sul_rx_task может увидеть «меню уже закрыто» и попытаться сохранить
|
||||||
|
* СВОЙ адрес, пока save() из menu_task ещё физически пишет QSPI. Без
|
||||||
|
* мьютекса — два параллельных erase+write в один сектор, порча настроек.
|
||||||
|
*
|
||||||
|
* Создаётся в init_defaults()/load() — оба вызываются РОВНО один раз, из
|
||||||
|
* bringup_task, ДО xTaskCreate() menu_task/sul_rx_task (см. task_bringup.c)
|
||||||
|
* — на момент создания гонки нет по конструкции; лениво в save() создавать
|
||||||
|
* было бы уже НЕ безопасно (save() зовут два разных таска на протяжении
|
||||||
|
* всей жизни системы, а не один раз при старте).
|
||||||
|
*/
|
||||||
|
static SemaphoreHandle_t g_save_mutex;
|
||||||
|
|
||||||
|
static void ensure_save_mutex(void)
|
||||||
|
{
|
||||||
|
if (g_save_mutex == NULL)
|
||||||
|
{
|
||||||
|
g_save_mutex = xSemaphoreCreateMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void settings_store_init_defaults(void)
|
void settings_store_init_defaults(void)
|
||||||
{
|
{
|
||||||
|
ensure_save_mutex();
|
||||||
g_settings = settings_defaults();
|
g_settings = settings_defaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
bsp_status_t settings_store_load(void)
|
bsp_status_t settings_store_load(void)
|
||||||
{
|
{
|
||||||
|
ensure_save_mutex();
|
||||||
|
|
||||||
/* static: страница = сектор (4 КБ) — на стеке задачи (~3 КБ) переполнит его.
|
/* static: страница = сектор (4 КБ) — на стеке задачи (~3 КБ) переполнит его.
|
||||||
* Однократный вызов при старте, не реентерабельно (как s_page в save()). */
|
* Однократный вызов при старте, не реентерабельно (как s_page в save()). */
|
||||||
static settings_page_t s_page;
|
static settings_page_t s_page;
|
||||||
|
|
@ -35,15 +67,23 @@ bsp_status_t settings_store_load(void)
|
||||||
|
|
||||||
bsp_status_t settings_store_save(void)
|
bsp_status_t settings_store_save(void)
|
||||||
{
|
{
|
||||||
|
/* g_save_mutex гарантированно создан к этому моменту (init_defaults()/
|
||||||
|
* load() — единственные вызывающиеся раньше, см. докстрок выше). */
|
||||||
|
(void) xSemaphoreTake(g_save_mutex, portMAX_DELAY);
|
||||||
|
|
||||||
/* static: не 4 КБ на стеке + гарантия выравнивания под постраничную запись. */
|
/* static: не 4 КБ на стеке + гарантия выравнивания под постраничную запись. */
|
||||||
static settings_page_t s_page;
|
static settings_page_t s_page;
|
||||||
settings_serialize(&g_settings, &s_page);
|
settings_serialize(&g_settings, &s_page);
|
||||||
|
|
||||||
|
bsp_status_t rc = BSP_OK;
|
||||||
|
|
||||||
if (bsp_qspi_erase_sector(TFT_APP_QSPI_SETTINGS_OFFSET) != BSP_OK)
|
if (bsp_qspi_erase_sector(TFT_APP_QSPI_SETTINGS_OFFSET) != BSP_OK)
|
||||||
{
|
{
|
||||||
return BSP_ERR_HW;
|
rc = BSP_ERR_HW;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rc == BSP_OK)
|
||||||
|
{
|
||||||
const uint32_t PAGES = BSP_QSPI_SECTOR_SIZE / BSP_QSPI_PAGE_SIZE;
|
const uint32_t PAGES = BSP_QSPI_SECTOR_SIZE / BSP_QSPI_PAGE_SIZE;
|
||||||
for (uint32_t i = 0U; i < PAGES; ++i)
|
for (uint32_t i = 0U; i < PAGES; ++i)
|
||||||
{
|
{
|
||||||
|
|
@ -51,11 +91,14 @@ bsp_status_t settings_store_save(void)
|
||||||
const uint8_t *p_page_src = (const uint8_t *) &s_page + i * BSP_QSPI_PAGE_SIZE;
|
const uint8_t *p_page_src = (const uint8_t *) &s_page + i * BSP_QSPI_PAGE_SIZE;
|
||||||
if (bsp_qspi_write_page(ADDR, p_page_src) != BSP_OK)
|
if (bsp_qspi_write_page(ADDR, p_page_src) != BSP_OK)
|
||||||
{
|
{
|
||||||
return BSP_ERR_HW;
|
rc = BSP_ERR_HW;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return BSP_OK;
|
(void) xSemaphoreGive(g_save_mutex);
|
||||||
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings_t *settings_store_get(void)
|
const settings_t *settings_store_get(void)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,20 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Диспетчерский вход (opto IN1/IN2, §3.4) — «локальный вход», не
|
||||||
|
* данные СУЛ (ARCH §8 п.3). Самый высокий приоритет из всех режимов —
|
||||||
|
* безусловно перекрывает и обычную индикацию, и любой режим СУЛ
|
||||||
|
* (пожар/перегруз/…), работает даже без связи со станцией. ОТВЕТ
|
||||||
|
* перебивает ВЫЗОВ, если оба почему-то активны одновременно.
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
DISPATCHER_INDICATION_NONE = 0,
|
||||||
|
DISPATCHER_INDICATION_CALL, /**< «Вызов подан» — opto IN1 */
|
||||||
|
DISPATCHER_INDICATION_ANSWER, /**< «Вызов принят» — opto IN2, приоритет выше CALL */
|
||||||
|
} dispatcher_indication_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Безусловная первая отрисовка при старте.
|
* @brief Безусловная первая отрисовка при старте.
|
||||||
*
|
*
|
||||||
|
|
@ -23,17 +37,23 @@ extern "C"
|
||||||
* совпадёт с дефолтом, indication_task_t придёт «ничего не изменилось», и
|
* совпадёт с дефолтом, indication_task_t придёт «ничего не изменилось», и
|
||||||
* экран останется пустым, если полагаться только на ui_fallback_render().
|
* экран останется пустым, если полагаться только на ui_fallback_render().
|
||||||
* Вызвать один раз при старте до входа в цикл получения кадров.
|
* Вызвать один раз при старте до входа в цикл получения кадров.
|
||||||
|
*
|
||||||
|
* @param dispatcher текущее состояние диспетчерского входа (см. выше)
|
||||||
*/
|
*/
|
||||||
void ui_fallback_render_initial(const sul_result_t *p_result);
|
void ui_fallback_render_initial(const sul_result_t *p_result, dispatcher_indication_t dispatcher);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Инкрементальная перерисовка по diff.
|
* @brief Инкрементальная перерисовка по diff.
|
||||||
*
|
*
|
||||||
* Фаза 1: перерисовывает весь кадр целиком при ЛЮБОМ pending-поле (нет
|
* Фаза 1: перерисовывает весь кадр целиком при ЛЮБОМ pending-поле (нет
|
||||||
* partial-update — gfx минимален, без dirty-rect). No-op, если ничего не
|
* partial-update — gfx минимален, без dirty-rect). No-op, если ничего не
|
||||||
* помечено.
|
* помечено (dispatcher сюда не входит — его смену обрабатывает отдельный
|
||||||
|
* путь в task_render.c через ui_fallback_render_initial(), см. TASKS.md).
|
||||||
|
*
|
||||||
|
* @param dispatcher текущее состояние диспетчерского входа (см. выше)
|
||||||
*/
|
*/
|
||||||
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,
|
||||||
|
dispatcher_indication_t dispatcher);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,37 @@ static void render_normal(const sul_result_t *p_result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void render(sul_mode_t mode, const sul_result_t *p_result)
|
/**
|
||||||
|
* @brief Метка диспетчерского входа (§3.4) — короткая asset-free, тем же
|
||||||
|
* путём, что и метки режимов СУЛ (mode_label() выше). Слова временные
|
||||||
|
* (пользователь позже поправит формулировки для всех режимов разом).
|
||||||
|
*/
|
||||||
|
static const char *dispatcher_label(dispatcher_indication_t dispatcher)
|
||||||
|
{
|
||||||
|
switch (dispatcher)
|
||||||
|
{
|
||||||
|
case DISPATCHER_INDICATION_ANSWER: return "ОТВЕТ";
|
||||||
|
case DISPATCHER_INDICATION_CALL: return "ВЫЗОВ";
|
||||||
|
case DISPATCHER_INDICATION_NONE:
|
||||||
|
default: return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render(sul_mode_t mode, const sul_result_t *p_result, dispatcher_indication_t dispatcher)
|
||||||
{
|
{
|
||||||
gfx_clear();
|
gfx_clear();
|
||||||
|
|
||||||
|
/* Диспетчерский вход — локальный (ARCH §8 п.3, не данные СУЛ), приоритет
|
||||||
|
* ВЫШЕ любого mode: проверяем ПЕРВЫМ, безусловный return — так же
|
||||||
|
* работает без связи со станцией (mode/p_result мог быть sul_default_
|
||||||
|
* state() после таймаута, сюда это не попадёт вообще). */
|
||||||
|
const char *dispatcher_text = dispatcher_label(dispatcher);
|
||||||
|
if (dispatcher_text != NULL)
|
||||||
|
{
|
||||||
|
draw_centered(&SystemFont, dispatcher_text, MODE_Y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const char *label = mode_label(mode);
|
const char *label = mode_label(mode);
|
||||||
if (label == NULL)
|
if (label == NULL)
|
||||||
{
|
{
|
||||||
|
|
@ -82,20 +109,24 @@ static void render(sul_mode_t mode, const sul_result_t *p_result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ui_fallback_render_initial(const sul_result_t *p_result)
|
void ui_fallback_render_initial(const sul_result_t *p_result, dispatcher_indication_t dispatcher)
|
||||||
{
|
{
|
||||||
render(sul_resolve_mode(p_result), p_result);
|
render(sul_resolve_mode(p_result), p_result, dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
dispatcher_indication_t dispatcher)
|
||||||
{
|
{
|
||||||
/* Перерисовываем на изменение того, что fallback реально показывает:
|
/* Перерисовываем на изменение того, что fallback реально показывает:
|
||||||
* позиция, стрелка, режим (+ отсчёт погрузки идёт вместе с mode/pos).
|
* позиция, стрелка, режим (+ отсчёт погрузки идёт вместе с mode/pos).
|
||||||
* next-этаж в safe-mode не рисуется (богатый layout — Фаза 5), поэтому
|
* next-этаж в safe-mode не рисуется (богатый layout — Фаза 5), поэтому
|
||||||
* на next_pending не будим. */
|
* на next_pending не будим. Смена dispatcher сюда не входит — она не
|
||||||
|
* приходит с этим diff'ом вообще (свой путь пробуждения render_task,
|
||||||
|
* см. task_render.c) — только пока mode/pos/direction ТОЖЕ изменились
|
||||||
|
* в этом же кадре, dispatcher едет попутно через параметр. */
|
||||||
if (!p_task->pos_pending && !p_task->direction_pending && !p_task->mode_pending)
|
if (!p_task->pos_pending && !p_task->direction_pending && !p_task->mode_pending)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
render(p_task->mode, p_result);
|
render(p_task->mode, p_result, dispatcher);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,19 @@ static void draw_footer(const menu_ctx_t *p_ctx, uint8_t level_first, uint8_t le
|
||||||
|
|
||||||
void menu_view_render(const menu_ctx_t *p_ctx)
|
void menu_view_render(const menu_ctx_t *p_ctx)
|
||||||
{
|
{
|
||||||
|
/* Снимок на входе — p_ctx указывает на ЖИВОЙ g_menu (task_render.c зовёт
|
||||||
|
* menu_view_render(&g_menu)); menu_task ВЫШЕ по приоритету и может
|
||||||
|
* вытеснить ПОСРЕДИ этого рендера на любое нажатие кнопки. Без снимка
|
||||||
|
* .cur/.page читались бы заново на каждой итерации цикла ниже — успей
|
||||||
|
* menu_task сменить .cur между итерациями, курсор оказался бы
|
||||||
|
* нарисован сразу на двух строках ОДНОГО кадра ("раздвоение" курсора,
|
||||||
|
* особенно заметно при быстрой навигации — весь рендер занимает
|
||||||
|
* десятки мс против 5-мс каденции menu_task, окно гонки большое).
|
||||||
|
* Копия дешёвая (несколько полей структуры) — схлопывает окно гонки с
|
||||||
|
* длительности всего рендера до одного присваивания. */
|
||||||
|
const menu_ctx_t SNAPSHOT = *p_ctx;
|
||||||
|
p_ctx = &SNAPSHOT;
|
||||||
|
|
||||||
/* Обнулить и нарисовать ТОЛЬКО окно (дёшево — оконная перерисовка на
|
/* Обнулить и нарисовать ТОЛЬКО окно (дёшево — оконная перерисовка на
|
||||||
* навигации). Полную очистку AS от индикации вне окна делает владелец
|
* навигации). Полную очистку AS от индикации вне окна делает владелец
|
||||||
* дисплея (task_render) один раз на ОТКРЫТИИ меню — вместе с двумя
|
* дисплея (task_render) один раз на ОТКРЫТИИ меню — вместе с двумя
|
||||||
|
|
|
||||||
|
|
@ -379,3 +379,57 @@ add_host_test(
|
||||||
${PROJECT_SOURCE_DIR}/firmware/tft_app/src/menu/include
|
${PROJECT_SOURCE_DIR}/firmware/tft_app/src/menu/include
|
||||||
${TFT_APP_SETTINGS_DIR}/include
|
${TFT_APP_SETTINGS_DIR}/include
|
||||||
${PROJECT_SOURCE_DIR}/bsp/common/include)
|
${PROJECT_SOURCE_DIR}/bsp/common/include)
|
||||||
|
|
||||||
|
# tft_app — sul_registry (Фаза 3.3: активный протокол из настроек, §8)
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_sul_registry
|
||||||
|
SOURCES
|
||||||
|
tft_app_sul_registry/test_sul_registry.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/src/sul_registry.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/src/nku_can.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/src/demo.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/src/demo_route.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c
|
||||||
|
INCLUDES
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/include)
|
||||||
|
|
||||||
|
# tft_app — menu_tree (Фаза 3.3: секция "Протокол" строится из
|
||||||
|
# sul_settings_desc_t активного драйвера, не хардкодом, §8)
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_tft_app_menu_tree
|
||||||
|
SOURCES
|
||||||
|
tft_app_menu/test_menu_tree.c
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/tft_app/src/menu/src/menu_tree.c
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/tft_app/src/menu/src/menu.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/src/sul_registry.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/src/nku_can.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/src/demo.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/src/demo_route.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c
|
||||||
|
INCLUDES
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/tft_app/src/menu/include
|
||||||
|
${TFT_APP_SETTINGS_DIR}/include
|
||||||
|
${PROJECT_SOURCE_DIR}/bsp/common/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/include)
|
||||||
|
|
||||||
|
# tft_app — sul_demo (Фаза 3.3: чистый декодер демо-протокола, §8)
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_sul_demo
|
||||||
|
SOURCES
|
||||||
|
tft_app_sul_demo/test_sul_demo.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/src/demo.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/src/demo_route.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c
|
||||||
|
INCLUDES
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/demo/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/include)
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ void setUp(void)
|
||||||
|
|
||||||
/* Каждый тест начинает с чистого состояния логгера */
|
/* Каждый тест начинает с чистого состояния логгера */
|
||||||
log_init(capture_cb, NULL);
|
log_init(capture_cb, NULL);
|
||||||
|
log_set_enabled(true); /* тумблер §3.6 — не сбрасывается log_init(), сбросить явно */
|
||||||
}
|
}
|
||||||
|
|
||||||
void tearDown(void)
|
void tearDown(void)
|
||||||
|
|
@ -351,6 +352,52 @@ void test_ctx_pointer_passed_to_callback(void)
|
||||||
TEST_ASSERT_EQUAL_PTR(&dummy, g_s_capture.p_ctx);
|
TEST_ASSERT_EQUAL_PTR(&dummy, g_s_capture.p_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
* 7. Runtime toggle — рантайм-тумблер поверх компайл-тайм LOG_LEVEL (§3.6)
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
void test_enabled_by_default(void)
|
||||||
|
{
|
||||||
|
LOG_I("TAG", "message");
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, g_s_capture.call_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_is_enabled_reflects_state(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(log_is_enabled());
|
||||||
|
log_set_enabled(false);
|
||||||
|
TEST_ASSERT_FALSE(log_is_enabled());
|
||||||
|
log_set_enabled(true);
|
||||||
|
TEST_ASSERT_TRUE(log_is_enabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_disabled_suppresses_output(void)
|
||||||
|
{
|
||||||
|
log_set_enabled(false);
|
||||||
|
LOG_I("TAG", "message");
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, g_s_capture.call_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_reenabled_resumes_output(void)
|
||||||
|
{
|
||||||
|
log_set_enabled(false);
|
||||||
|
LOG_I("TAG", "swallowed");
|
||||||
|
log_set_enabled(true);
|
||||||
|
LOG_I("TAG", "visible");
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, g_s_capture.call_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_disabled_does_not_take_mutex(void)
|
||||||
|
{
|
||||||
|
log_set_enabled(false);
|
||||||
|
LOG_I("TAG", "message");
|
||||||
|
|
||||||
|
/* Гейт — раньше форматирования/мьютекса (дёшево при выключенном тумблере). */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, log_mutex_lock_fake.call_count);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, log_mutex_unlock_fake.call_count);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Runner ──────────────────────────────────────────────────────────────── */
|
/* ── Runner ──────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
|
|
@ -392,5 +439,12 @@ int main(void)
|
||||||
RUN_TEST(test_ctx_null_passed_to_callback);
|
RUN_TEST(test_ctx_null_passed_to_callback);
|
||||||
RUN_TEST(test_ctx_pointer_passed_to_callback);
|
RUN_TEST(test_ctx_pointer_passed_to_callback);
|
||||||
|
|
||||||
|
/* Runtime toggle */
|
||||||
|
RUN_TEST(test_enabled_by_default);
|
||||||
|
RUN_TEST(test_is_enabled_reflects_state);
|
||||||
|
RUN_TEST(test_disabled_suppresses_output);
|
||||||
|
RUN_TEST(test_reenabled_resumes_output);
|
||||||
|
RUN_TEST(test_disabled_does_not_take_mutex);
|
||||||
|
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ RAM), QSPI не трогает — моки не нужны. Деревья ме
|
||||||
## Что проверяется
|
## Что проверяется
|
||||||
|
|
||||||
**Плоское дерево** `[Протокол(SELECT), Адрес(BYTE), Логи(BOOL), Выход(BACK)]`:
|
**Плоское дерево** `[Протокол(SELECT), Адрес(BYTE), Логи(BOOL), Выход(BACK)]`:
|
||||||
|
|
||||||
- `open` ставит курсор на первый пункт верхнего уровня.
|
- `open` ставит курсор на первый пункт верхнего уровня.
|
||||||
- `menu_next` идёт по уровню с **заворотом** (последний → первый).
|
- `menu_next` идёт по уровню с **заворотом** (последний → первый).
|
||||||
- Инкремент `BYTE` (адрес 0..15) с заворотом `max→min`; `BOOL` — тумблер.
|
- Инкремент `BYTE` (адрес 0..15) с заворотом `max→min`; `BOOL` — тумблер.
|
||||||
|
|
@ -20,6 +21,7 @@ RAM), QSPI не трогает — моки не нужны. Деревья ме
|
||||||
- `menu_next`/`menu_action` — no-op при закрытом меню.
|
- `menu_next`/`menu_action` — no-op при закрытом меню.
|
||||||
|
|
||||||
**Дерево с подменю:**
|
**Дерево с подменю:**
|
||||||
|
|
||||||
- Вход в `SUBMENU` → первый ребёнок.
|
- Вход в `SUBMENU` → первый ребёнок.
|
||||||
- `BACK` в подменю → возврат к пункту-подменю (не закрытие).
|
- `BACK` в подменю → возврат к пункту-подменю (не закрытие).
|
||||||
- Правка в подменю сохраняется в `settings_t`; выход из корня → `save_requested`.
|
- Правка в подменю сохраняется в `settings_t`; выход из корня → `save_requested`.
|
||||||
|
|
@ -36,3 +38,37 @@ RAM), QSPI не трогает — моки не нужны. Деревья ме
|
||||||
```bash
|
```bash
|
||||||
ctest --preset host-debug-test -R test_tft_app_menu -V
|
ctest --preset host-debug-test -R test_tft_app_menu -V
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# test_tft_app_menu_tree
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/tft_app/src/menu/src/menu_tree.c` — боевое дерево меню Фазы 3:
|
||||||
|
секция "Протокол" строится из `sul_settings_desc_t` активного драйвера
|
||||||
|
(ARCH.md §8, Фаза 3.3), не хардкодится. Отдельно от теста выше (генерик-движок
|
||||||
|
на синтетических деревьях) — здесь проверяется КОНКРЕТНОЕ дерево и его связка
|
||||||
|
с реестром `sul`.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A — платформонезависимый. Линкует реальный `sul_registry.c` (не мок) —
|
||||||
|
проверяется настоящая связка дескриптор → дерево, не только форма данных.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- После `menu_tree_refresh_protocol_section()`: `T_PROTO.max`/`.options`
|
||||||
|
соответствуют реестру (сейчас — один протокол, НКУ-CAN); пункт-параметр
|
||||||
|
(`T_PROTO_PARAM`) получает `label`/`type`/`min`/`max`/`value_offset` из
|
||||||
|
дескриптора НКУ-CAN, `value_offset` корректно указывает на
|
||||||
|
`settings_t.user.proto_slice[0]`.
|
||||||
|
- Конец-в-конец: правка через реальный `menu.c` (`menu_open`/`menu_next`/
|
||||||
|
`menu_action`) пункта, построенного из дескриптора, действительно меняет
|
||||||
|
тот самый байт `settings_t`, который назвал дескриптор.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_tft_app_menu_tree -V
|
||||||
|
```
|
||||||
|
|
|
||||||
124
tests/host/tft_app_menu/test_menu_tree.c
Normal file
124
tests/host/tft_app_menu/test_menu_tree.c
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
/**
|
||||||
|
* @file test_menu_tree.c
|
||||||
|
* @brief Host-тест боевого дерева меню (menu_tree.c) — секция "Протокол"
|
||||||
|
* строится из sul_settings_desc_t активного протокола, не хардкодом
|
||||||
|
* (ARCH §8, Фаза 3.3).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "domain/sul.h"
|
||||||
|
#include "menu/menu_tree.h"
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void setUp(void) {}
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
/* T_PROTO — первый ребёнок корня (гарантия menu.h: items[MENU_ROOT_INDEX] —
|
||||||
|
* корневой SUBMENU). T_PROTO_PARAM — следующий по порядку в дереве
|
||||||
|
* (menu_tree.c, T_ROOT/T_PROTO/T_PROTO_PARAM/T_LOG/T_EXIT) — единственное,
|
||||||
|
* что этот тест знает о внутреннем порядке конкретного дерева. */
|
||||||
|
static uint8_t proto_index(void)
|
||||||
|
{
|
||||||
|
return menu_tree_items()[MENU_ROOT_INDEX].first_child;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_protocol_section_built_from_nku_can_descriptor(void)
|
||||||
|
{
|
||||||
|
settings_t s;
|
||||||
|
memset(&s, 0, sizeof(s));
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_NKU_CAN);
|
||||||
|
menu_tree_refresh_protocol_section(&s);
|
||||||
|
|
||||||
|
const menu_item_desc_t *p_items = menu_tree_items();
|
||||||
|
const uint8_t T_PROTO = proto_index();
|
||||||
|
const uint8_t T_PROTO_PARAM = (uint8_t) (T_PROTO + 1U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1U, p_items[T_PROTO].max); /* 2 протокола в реестре: НКУ-CAN + демо */
|
||||||
|
TEST_ASSERT_EQUAL_STRING("НКУ-CAN", p_items[T_PROTO].options[0]);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Демо", p_items[T_PROTO].options[1]);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Адрес", p_items[T_PROTO_PARAM].label);
|
||||||
|
TEST_ASSERT_EQUAL(MENU_BYTE, p_items[T_PROTO_PARAM].type);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0U, p_items[T_PROTO_PARAM].min);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(15U, p_items[T_PROTO_PARAM].max);
|
||||||
|
TEST_ASSERT_EQUAL_UINT16((uint16_t) offsetof(settings_t, user.proto_slice[0]),
|
||||||
|
p_items[T_PROTO_PARAM].value_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_protocol_section_switches_to_demo_descriptor(void)
|
||||||
|
{
|
||||||
|
settings_t s;
|
||||||
|
memset(&s, 0, sizeof(s));
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_DEMO);
|
||||||
|
menu_tree_refresh_protocol_section(&s);
|
||||||
|
|
||||||
|
const menu_item_desc_t *p_items = menu_tree_items();
|
||||||
|
const uint8_t T_PROTO_PARAM = (uint8_t) (proto_index() + 1U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Скорость", p_items[T_PROTO_PARAM].label);
|
||||||
|
TEST_ASSERT_EQUAL(MENU_SELECT, p_items[T_PROTO_PARAM].type);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0U, p_items[T_PROTO_PARAM].min);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(2U, p_items[T_PROTO_PARAM].max);
|
||||||
|
TEST_ASSERT_NOT_NULL(p_items[T_PROTO_PARAM].options);
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_NKU_CAN); /* не оставлять активный реестр на демо */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Значение proto_slice[0], "протухшее" от протокола с более широким диапазоном
|
||||||
|
* (адрес НКУ-CAN 0..15), должно клампиться под диапазон демо (0..2) при
|
||||||
|
* переключении — иначе рендер читал бы options[value] за пределами массива
|
||||||
|
* меток демо (см. комментарий в menu_tree_refresh_protocol_section()). */
|
||||||
|
static void test_stale_value_clamped_on_protocol_switch(void)
|
||||||
|
{
|
||||||
|
settings_t s;
|
||||||
|
memset(&s, 0, sizeof(s));
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_NKU_CAN);
|
||||||
|
menu_tree_refresh_protocol_section(&s);
|
||||||
|
s.user.proto_slice[0] = 15U; /* валидный адрес НКУ-CAN */
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_DEMO);
|
||||||
|
menu_tree_refresh_protocol_section(&s);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(2U, s.user.proto_slice[0]); /* клампится к max демо, не остаётся 15 */
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_NKU_CAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Конец-в-конец: правка пункта, построенного из дескриптора, действительно
|
||||||
|
* попадает в то самое поле settings_t, которое назвал дескриптор протокола —
|
||||||
|
* не только структура данных совпадает, но и реальный edit через menu.c. */
|
||||||
|
static void test_protocol_param_edits_correct_settings_field(void)
|
||||||
|
{
|
||||||
|
settings_t s;
|
||||||
|
memset(&s, 0, sizeof(s));
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_NKU_CAN);
|
||||||
|
menu_tree_refresh_protocol_section(&s);
|
||||||
|
|
||||||
|
menu_ctx_t ctx;
|
||||||
|
menu_init(&ctx, menu_tree_items(), menu_tree_count(), &s);
|
||||||
|
menu_open(&ctx);
|
||||||
|
menu_next(&ctx); /* T_PROTO -> T_PROTO_PARAM */
|
||||||
|
|
||||||
|
menu_action(&ctx); /* инкремент адреса 0 -> 1 */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1U, s.user.proto_slice[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_protocol_section_built_from_nku_can_descriptor);
|
||||||
|
RUN_TEST(test_protocol_section_switches_to_demo_descriptor);
|
||||||
|
RUN_TEST(test_stale_value_clamped_on_protocol_switch);
|
||||||
|
RUN_TEST(test_protocol_param_edits_correct_settings_field);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
33
tests/host/tft_app_sul_demo/README.md
Normal file
33
tests/host/tft_app_sul_demo/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# test_sul_demo
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/tft_app/src/domain/sul/demo/src/demo.c` — чистый декодер
|
||||||
|
демо-протокола (ARCH.md §8, Фаза 3.3): скриптованный маршрут "поездка" по
|
||||||
|
кругу, синтетический источник без реальной шины/станции.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A — платформонезависимый. `decode()` игнорирует содержимое кадра, ведёт счёт
|
||||||
|
тиков сам внутри `ctx` — детерминирован по числу вызовов, HAL/время не трогает.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- Дефолт после `demo_init()` — первый шаг маршрута (этаж "1", без движения).
|
||||||
|
- Маршрут НЕ двигается раньше порога тиков-на-шаг (без "перепрыгивания").
|
||||||
|
- Маршрут двигается ровно на пороге (норма — 10 тиков/шаг).
|
||||||
|
- Промежуточные остановки: этаж 7 (едет вверх), этаж 3 (едет вниз) — гонг
|
||||||
|
(`arrival`), направление `NONE` на остановке.
|
||||||
|
- Верхний этаж маршрута (11).
|
||||||
|
- Заворот маршрута по кругу — после последнего шага снова на первый, гонг
|
||||||
|
корректно снимается (не залипает `true`).
|
||||||
|
- `demo_set_speed()` — "Быстро" (4 тика/шаг) двигает маршрут за меньшее число
|
||||||
|
вызовов; вне диапазона (0..2) — клампится к максимуму.
|
||||||
|
- `decode()` не разыменовывает кадр (`NULL` — валидный вход, т.к. демо не
|
||||||
|
читает содержимое).
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_sul_demo -V
|
||||||
|
```
|
||||||
175
tests/host/tft_app_sul_demo/test_sul_demo.c
Normal file
175
tests/host/tft_app_sul_demo/test_sul_demo.c
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
/**
|
||||||
|
* @file test_sul_demo.c
|
||||||
|
* @brief Host-тесты чистого декодера демо-протокола (domain/sul/demo/src/demo.c)
|
||||||
|
* — скриптованный маршрут (ARCH §8, Фаза 3.3).
|
||||||
|
*
|
||||||
|
* Тики-на-шаг (20/10/4 для медленно/норма/быстро) — деталь реализации
|
||||||
|
* demo.c (k_ticks_per_step), продублирована здесь как в golden-тестах
|
||||||
|
* nku_can (там так же захардкожены PACKET-ID) — держать в синхроне при
|
||||||
|
* правке demo.c.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "domain/sul/demo.h"
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#define TICKS_SLOW 20U
|
||||||
|
#define TICKS_NORMAL 10U
|
||||||
|
#define TICKS_FAST 4U
|
||||||
|
|
||||||
|
#define ROUTE_LEN 25U /* DEMO_ROUTE_LEN в demo.c — держать в синхроне */
|
||||||
|
|
||||||
|
void setUp(void) {}
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
static sul_status_t advance(demo_ctx_t *p_ctx, sul_result_t *p_out, uint16_t ticks)
|
||||||
|
{
|
||||||
|
sul_status_t rc = SUL_STATUS_ERR;
|
||||||
|
for (uint16_t i = 0U; i < ticks; i++)
|
||||||
|
{
|
||||||
|
rc = demo_decode(p_ctx, NULL, p_out);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_init_shows_first_route_step(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", ctx.state.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, ctx.state.direction);
|
||||||
|
TEST_ASSERT_FALSE(ctx.state.arrival);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1U, ctx.state.floor_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_decode_ignores_frame_content(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
/* p_frame = NULL — decode() не должен разыменовывать его вовсе. */
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, demo_decode(&ctx, NULL, &out));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_route_does_not_advance_before_ticks_per_step(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
(void) advance(&ctx, &out, TICKS_NORMAL - 1U); /* на 1 тик меньше порога */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", out.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, out.direction); /* всё ещё шаг 0, не 1 (UP) */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_route_advances_to_second_step_at_normal_speed(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
(void) advance(&ctx, &out, TICKS_NORMAL); /* ровно порог -> шаг 1 */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", out.pos); /* пол не меняется... */
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); /* ...но кабина уже поехала вверх */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_route_reaches_intermediate_stop_at_floor_7_going_up(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
(void) advance(&ctx, &out, TICKS_NORMAL * 7U); /* шаг 7 — остановка на 7 вверх */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("7", out.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, out.direction);
|
||||||
|
TEST_ASSERT_TRUE(out.arrival);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(7U, out.floor_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_route_reaches_top_floor_11(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
(void) advance(&ctx, &out, TICKS_NORMAL * 12U); /* шаг 12 — верхний этаж */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("11", out.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, out.direction);
|
||||||
|
TEST_ASSERT_TRUE(out.arrival);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(11U, out.floor_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_route_reaches_intermediate_stop_at_floor_3_going_down(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
(void) advance(&ctx, &out, TICKS_NORMAL * 21U); /* шаг 21 — остановка на 3 вниз */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("3", out.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, out.direction);
|
||||||
|
TEST_ASSERT_TRUE(out.arrival);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(3U, out.floor_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_route_wraps_around_after_full_loop(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
/* Последний шаг (24) -> гонг на "1"; ровно ROUTE_LEN шагов дальше —
|
||||||
|
* назад на шаг 0 (тот же этаж "1", но arrival уже снят). */
|
||||||
|
(void) advance(&ctx, &out, TICKS_NORMAL * (ROUTE_LEN - 1U));
|
||||||
|
TEST_ASSERT_TRUE(out.arrival); /* последний шаг маршрута перед циклом */
|
||||||
|
|
||||||
|
(void) advance(&ctx, &out, TICKS_NORMAL);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1", out.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, out.direction);
|
||||||
|
TEST_ASSERT_FALSE(out.arrival); /* цикл замкнулся на шаг 0, не завис на 24 */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_set_speed_fast_advances_sooner(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
demo_set_speed(&ctx, 2U); /* "Быстро" */
|
||||||
|
|
||||||
|
sul_result_t out;
|
||||||
|
(void) advance(&ctx, &out, TICKS_FAST);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); /* уже шаг 1 при вчетверо меньшем числе тиков */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_set_speed_clamps_out_of_range(void)
|
||||||
|
{
|
||||||
|
demo_ctx_t ctx;
|
||||||
|
demo_init(&ctx);
|
||||||
|
demo_set_speed(&ctx, 99U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(2U, ctx.speed_idx); /* клампится к максимуму (Быстро) */
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_init_shows_first_route_step);
|
||||||
|
RUN_TEST(test_decode_ignores_frame_content);
|
||||||
|
RUN_TEST(test_route_does_not_advance_before_ticks_per_step);
|
||||||
|
RUN_TEST(test_route_advances_to_second_step_at_normal_speed);
|
||||||
|
RUN_TEST(test_route_reaches_intermediate_stop_at_floor_7_going_up);
|
||||||
|
RUN_TEST(test_route_reaches_top_floor_11);
|
||||||
|
RUN_TEST(test_route_reaches_intermediate_stop_at_floor_3_going_down);
|
||||||
|
RUN_TEST(test_route_wraps_around_after_full_loop);
|
||||||
|
RUN_TEST(test_set_speed_fast_advances_sooner);
|
||||||
|
RUN_TEST(test_set_speed_clamps_out_of_range);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
|
|
@ -86,6 +86,29 @@ static sul_frame_t make_packet5(uint8_t next_left, uint8_t next_right, uint8_t d
|
||||||
return (sul_frame_t){ .id = PACKET5_ID, .bus = 0, .p_data = s_data, .len = 8U };
|
return (sul_frame_t){ .id = PACKET5_ID, .bus = 0, .p_data = s_data, .len = 8U };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 0x4X1 — анонс адреса станции управления (X = addr_x, биты [7:4] ID). */
|
||||||
|
static sul_frame_t make_remote_announce(uint8_t addr_x)
|
||||||
|
{
|
||||||
|
static uint8_t s_data[8];
|
||||||
|
memset(s_data, 0, sizeof(s_data));
|
||||||
|
return (sul_frame_t){ .id = 0x401U | ((uint32_t) addr_x << 4U),
|
||||||
|
.bus = 0,
|
||||||
|
.p_data = s_data,
|
||||||
|
.len = 8U };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0x5XB — несущая команды; cmd в старшем нибле data[3], X = addr_x. */
|
||||||
|
static sul_frame_t make_remote_cmd(uint8_t addr_x, uint8_t cmd, uint8_t len)
|
||||||
|
{
|
||||||
|
static uint8_t s_data[8];
|
||||||
|
memset(s_data, 0, sizeof(s_data));
|
||||||
|
s_data[3] = (uint8_t) (cmd << 4U);
|
||||||
|
return (sul_frame_t){ .id = 0x50BU | ((uint32_t) addr_x << 4U),
|
||||||
|
.bus = 0,
|
||||||
|
.p_data = s_data,
|
||||||
|
.len = len };
|
||||||
|
}
|
||||||
|
|
||||||
/* ── PACKET1 — направление ───────────────────────────────────────────────── */
|
/* ── PACKET1 — направление ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
static void test_packet1_none(void)
|
static void test_packet1_none(void)
|
||||||
|
|
@ -587,6 +610,157 @@ static void test_address_clamped_to_max(void)
|
||||||
TEST_ASSERT_EQUAL(SUL_DIR_DOWN, out.direction);
|
TEST_ASSERT_EQUAL(SUL_DIR_DOWN, out.direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Удалённая установка адреса (REMOTE_ADDRES_SETUP.pdf, §3.5) ─────────────
|
||||||
|
* Кадры принадлежат чужому/произвольному X — вне классификации PACKET1..5
|
||||||
|
* этой станции, поэтому decode() почти всегда возвращает SUL_STATUS_IGNORED
|
||||||
|
* (кроме теста на пересечение с PACKET4 ниже) — проверяем ТОЛЬКО побочный
|
||||||
|
* эффект на ctx, а не *p_out. */
|
||||||
|
|
||||||
|
static void test_remote_announce_updates_candidate_and_is_ignored(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_remote_announce(5U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_IGNORED, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(5U, ctx.remote_addr_candidate);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(NKU_REMOTE_ADDR_NONE, ctx.pending_remote_write_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_remote_cmd_write_triggers_pending_when_x_matches(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t announce = make_remote_announce(5U);
|
||||||
|
(void) nku_can_decode(&ctx, &announce, &out);
|
||||||
|
|
||||||
|
const sul_frame_t cmd = make_remote_cmd(5U, 0x2U, 8U);
|
||||||
|
(void) nku_can_decode(&ctx, &cmd, &out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(5U, ctx.pending_remote_write_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_remote_cmd_ignored_when_x_mismatches_announce(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t announce = make_remote_announce(5U);
|
||||||
|
(void) nku_can_decode(&ctx, &announce, &out);
|
||||||
|
|
||||||
|
/* Согласовано с пользователем: строже буквы PDF (которая номинально
|
||||||
|
* допускает любой X у командного кадра) — X должен совпасть. */
|
||||||
|
const sul_frame_t cmd = make_remote_cmd(7U, 0x2U, 8U);
|
||||||
|
(void) nku_can_decode(&ctx, &cmd, &out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(NKU_REMOTE_ADDR_NONE, ctx.pending_remote_write_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_remote_cmd_ignored_when_command_is_not_write(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t announce = make_remote_announce(5U);
|
||||||
|
(void) nku_can_decode(&ctx, &announce, &out);
|
||||||
|
|
||||||
|
const sul_frame_t cmd = make_remote_cmd(5U, 0x0U, 8U); /* "0" = нет команды (PDF п.5) */
|
||||||
|
(void) nku_can_decode(&ctx, &cmd, &out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(NKU_REMOTE_ADDR_NONE, ctx.pending_remote_write_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_remote_pending_is_transient_not_latched(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t announce = make_remote_announce(5U);
|
||||||
|
(void) nku_can_decode(&ctx, &announce, &out);
|
||||||
|
const sul_frame_t cmd = make_remote_cmd(5U, 0x2U, 8U);
|
||||||
|
(void) nku_can_decode(&ctx, &cmd, &out);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(5U, ctx.pending_remote_write_addr); /* сработало */
|
||||||
|
|
||||||
|
/* Следующий кадр — обычный PACKET1, к удалённой адресации не относится:
|
||||||
|
* pending не должен остаться залипшим с прошлого вызова (app-слой читает
|
||||||
|
* его СРАЗУ после decode(), значение имеет смысл только для ЭТОГО кадра). */
|
||||||
|
const sul_frame_t normal = make_packet1(0U);
|
||||||
|
(void) nku_can_decode(&ctx, &normal, &out);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(NKU_REMOTE_ADDR_NONE, ctx.pending_remote_write_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_remote_short_cmd_frame_does_not_crash_or_trigger(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t announce = make_remote_announce(5U);
|
||||||
|
(void) nku_can_decode(&ctx, &announce, &out);
|
||||||
|
|
||||||
|
/* len < REMOTE_CMD_DLC_MIN (4) — data[3] недоступен, не должно ни упасть,
|
||||||
|
* ни ложно сработать (это НЕ проверка общего PROTO_DLC-гейта, тот на
|
||||||
|
* произвольный X 0x5XB не распространяется — своя защита). */
|
||||||
|
const sul_frame_t short_cmd = make_remote_cmd(5U, 0x2U, 2U);
|
||||||
|
(void) nku_can_decode(&ctx, &short_cmd, &out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(NKU_REMOTE_ADDR_NONE, ctx.pending_remote_write_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кадр 0x50B|G4 при адресе станции 0 — ОДНОВРЕМЕННО наш PACKET4 (X=0) И
|
||||||
|
* кандидат на "несущую команды" (X тоже 0). Обе классификации должны
|
||||||
|
* отработать независимо, без взаимной порчи. */
|
||||||
|
static void test_remote_check_does_not_interfere_with_own_packet4(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t announce = make_remote_announce(0U); /* X=0 — совпадёт с адресом станции ниже */
|
||||||
|
(void) nku_can_decode(&ctx, &announce, &out);
|
||||||
|
|
||||||
|
const sul_frame_t p4 = make_packet4(false, true); /* seismic=true, cmd-нибл data[3] остаётся 0 */
|
||||||
|
const sul_status_t RC = nku_can_decode(&ctx, &p4, &out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, RC);
|
||||||
|
TEST_ASSERT_TRUE(out.seismic); /* PACKET4-классификация не задета */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(NKU_REMOTE_ADDR_NONE, ctx.pending_remote_write_addr); /* cmd=0, не "2" */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── nku_can_take_pending_write() — generic-канал для task_sul_rx.c ────────── */
|
||||||
|
|
||||||
|
static void test_take_pending_write_false_when_none(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_slice_write_t write;
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(nku_can_take_pending_write(&ctx, &write));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_take_pending_write_true_and_fills_slice_offset_zero(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t announce = make_remote_announce(9U);
|
||||||
|
(void) nku_can_decode(&ctx, &announce, &out);
|
||||||
|
const sul_frame_t cmd = make_remote_cmd(9U, 0x2U, 8U);
|
||||||
|
(void) nku_can_decode(&ctx, &cmd, &out);
|
||||||
|
|
||||||
|
sul_slice_write_t write;
|
||||||
|
TEST_ASSERT_TRUE(nku_can_take_pending_write(&ctx, &write));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0U, write.slice_offset); /* proto_slice[0] = адрес */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(9U, write.value);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
|
|
@ -636,5 +810,16 @@ int main(void)
|
||||||
RUN_TEST(test_address_shifts_packet_ids);
|
RUN_TEST(test_address_shifts_packet_ids);
|
||||||
RUN_TEST(test_address_clamped_to_max);
|
RUN_TEST(test_address_clamped_to_max);
|
||||||
|
|
||||||
|
RUN_TEST(test_remote_announce_updates_candidate_and_is_ignored);
|
||||||
|
RUN_TEST(test_remote_cmd_write_triggers_pending_when_x_matches);
|
||||||
|
RUN_TEST(test_remote_cmd_ignored_when_x_mismatches_announce);
|
||||||
|
RUN_TEST(test_remote_cmd_ignored_when_command_is_not_write);
|
||||||
|
RUN_TEST(test_remote_pending_is_transient_not_latched);
|
||||||
|
RUN_TEST(test_remote_short_cmd_frame_does_not_crash_or_trigger);
|
||||||
|
RUN_TEST(test_remote_check_does_not_interfere_with_own_packet4);
|
||||||
|
|
||||||
|
RUN_TEST(test_take_pending_write_false_when_none);
|
||||||
|
RUN_TEST(test_take_pending_write_true_and_fills_slice_offset_zero);
|
||||||
|
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
tests/host/tft_app_sul_registry/README.md
Normal file
29
tests/host/tft_app_sul_registry/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# test_sul_registry
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/tft_app/src/domain/sul/src/sul_registry.c` — реестр драйверов СУЛ:
|
||||||
|
выбор активного протокола (ARCH.md §8, Фаза 3.3).
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A — платформонезависимый. Реестр — статические данные + чистые функции
|
||||||
|
поиска/выбора, HAL не трогает.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- `sul_registry_active()` — дефолт (до первого `set_active()`) и после явного
|
||||||
|
выбора известного id.
|
||||||
|
- `sul_registry_find()` — `NULL` на неизвестный id.
|
||||||
|
- `sul_registry_set_active()` — известный id переключает активный драйвер;
|
||||||
|
неизвестный id **игнорируется** (активный не меняется) — защита от мусора
|
||||||
|
в `settings_device_t.protocol_id`.
|
||||||
|
- `sul_registry_count()` — количество зарегистрированных протоколов.
|
||||||
|
- Дескриптор настроек НКУ-CAN (`sul_settings_desc_t`) присутствует и содержит
|
||||||
|
ожидаемый диапазон адреса (0..15).
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_sul_registry -V
|
||||||
|
```
|
||||||
130
tests/host/tft_app_sul_registry/test_sul_registry.c
Normal file
130
tests/host/tft_app_sul_registry/test_sul_registry.c
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
/**
|
||||||
|
* @file test_sul_registry.c
|
||||||
|
* @brief Host-тесты реестра драйверов СУЛ (sul_registry.c) — выбор активного
|
||||||
|
* протокола из настроек (ARCH §8, Фаза 3.3), 2 зарегистрированных
|
||||||
|
* драйвера (НКУ-CAN, демо).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "domain/sul.h"
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_NKU_CAN); /* известное состояние перед каждым тестом */
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
static void test_active_defaults_to_nku_can(void)
|
||||||
|
{
|
||||||
|
const sul_driver_t *p_active = sul_registry_active();
|
||||||
|
TEST_ASSERT_NOT_NULL(p_active);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SUL_PROTOCOL_NKU_CAN, p_active->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_find_returns_null_for_unknown_id(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_NULL(sul_registry_find(0xFFU));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_find_returns_demo(void)
|
||||||
|
{
|
||||||
|
const sul_driver_t *p_drv = sul_registry_find(SUL_PROTOCOL_DEMO);
|
||||||
|
TEST_ASSERT_NOT_NULL(p_drv);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Демо", p_drv->p_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_set_active_switches_between_protocols(void)
|
||||||
|
{
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_DEMO);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SUL_PROTOCOL_DEMO, sul_registry_active()->id);
|
||||||
|
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_NKU_CAN);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SUL_PROTOCOL_NKU_CAN, sul_registry_active()->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_set_active_ignores_unknown_id(void)
|
||||||
|
{
|
||||||
|
sul_registry_set_active(SUL_PROTOCOL_DEMO); /* известное состояние, не дефолт */
|
||||||
|
sul_registry_set_active(0xFFU); /* неизвестный id — игнорируется */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SUL_PROTOCOL_DEMO, sul_registry_active()->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_count_matches_registered_drivers(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(2U, sul_registry_count()); /* НКУ-CAN + демо */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_nku_can_settings_descriptor_present(void)
|
||||||
|
{
|
||||||
|
const sul_driver_t *p_drv = sul_registry_find(SUL_PROTOCOL_NKU_CAN);
|
||||||
|
TEST_ASSERT_NOT_NULL(p_drv->p_settings);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1U, p_drv->p_settings->count);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0U, p_drv->p_settings->p_entries[0].min);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(15U, p_drv->p_settings->p_entries[0].max);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_SETTINGS_BYTE, p_drv->p_settings->p_entries[0].type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Демо даёт СВОЙ дескриптор непохожей формы (SELECT с метками, не BYTE) —
|
||||||
|
* тест того, что дескрипторный механизм не завязан на "адрес"-подобный
|
||||||
|
* параметр НКУ-CAN (см. HANDOFF_PHASE3_TAIL.md, п.1). */
|
||||||
|
static void test_demo_settings_descriptor_present(void)
|
||||||
|
{
|
||||||
|
const sul_driver_t *p_drv = sul_registry_find(SUL_PROTOCOL_DEMO);
|
||||||
|
TEST_ASSERT_NOT_NULL(p_drv->p_settings);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1U, p_drv->p_settings->count);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_SETTINGS_SELECT, p_drv->p_settings->p_entries[0].type);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0U, p_drv->p_settings->p_entries[0].min);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(2U, p_drv->p_settings->p_entries[0].max);
|
||||||
|
TEST_ASSERT_NOT_NULL(p_drv->p_settings->p_entries[0].p_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic-канал протокол→settings (§3.5) — НКУ-CAN его использует (удалённая
|
||||||
|
* адресация), демо — нет. task_sul_rx.c проверяет только NULL/не-NULL, без
|
||||||
|
* ветки по id — вот что это гарантирует. */
|
||||||
|
static void test_take_pending_write_wired_only_for_nku_can(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_NOT_NULL(sul_registry_find(SUL_PROTOCOL_NKU_CAN)->take_pending_write);
|
||||||
|
TEST_ASSERT_NULL(sul_registry_find(SUL_PROTOCOL_DEMO)->take_pending_write);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таймаут "потери связи" — свойство протокола, не пользовательская настройка
|
||||||
|
* (жёстко задаётся при регистрации). НКУ-CAN шлёт периодически — таймаут
|
||||||
|
* активен; демо — синтетический источник, обрыва не бывает по определению. */
|
||||||
|
static void test_connection_timeout_set_per_protocol(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(3000U, sul_registry_find(SUL_PROTOCOL_NKU_CAN)->connection_timeout_ms);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(SUL_CONNECTION_TIMEOUT_DISABLED,
|
||||||
|
sul_registry_find(SUL_PROTOCOL_DEMO)->connection_timeout_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_each_driver_has_own_ctx(void)
|
||||||
|
{
|
||||||
|
sul_registry_init();
|
||||||
|
|
||||||
|
const sul_driver_t *p_nku = sul_registry_find(SUL_PROTOCOL_NKU_CAN);
|
||||||
|
const sul_driver_t *p_demo = sul_registry_find(SUL_PROTOCOL_DEMO);
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_NULL(p_nku->p_ctx);
|
||||||
|
TEST_ASSERT_NOT_NULL(p_demo->p_ctx);
|
||||||
|
TEST_ASSERT_NOT_EQUAL(p_nku->p_ctx, p_demo->p_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_active_defaults_to_nku_can);
|
||||||
|
RUN_TEST(test_find_returns_null_for_unknown_id);
|
||||||
|
RUN_TEST(test_find_returns_demo);
|
||||||
|
RUN_TEST(test_set_active_switches_between_protocols);
|
||||||
|
RUN_TEST(test_set_active_ignores_unknown_id);
|
||||||
|
RUN_TEST(test_count_matches_registered_drivers);
|
||||||
|
RUN_TEST(test_nku_can_settings_descriptor_present);
|
||||||
|
RUN_TEST(test_demo_settings_descriptor_present);
|
||||||
|
RUN_TEST(test_take_pending_write_wired_only_for_nku_can);
|
||||||
|
RUN_TEST(test_connection_timeout_set_per_protocol);
|
||||||
|
RUN_TEST(test_each_driver_has_own_ctx);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
@ -38,6 +39,10 @@
|
||||||
static log_write_cb_t g_s_write_cb = NULL;
|
static log_write_cb_t g_s_write_cb = NULL;
|
||||||
static void *g_s_p_ctx = NULL;
|
static void *g_s_p_ctx = NULL;
|
||||||
|
|
||||||
|
/* true по умолчанию — до первого log_set_enabled() (обычно из настроек,
|
||||||
|
* §3.6) ранние сообщения bringup не должны теряться молча. */
|
||||||
|
static bool g_s_log_enabled = true;
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Weak-хуки мьютекса — NOP для bare-metal */
|
/* Weak-хуки мьютекса — NOP для bare-metal */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
@ -89,10 +94,20 @@ void log_init(log_write_cb_t p_write_cb, void *p_ctx)
|
||||||
g_s_p_ctx = p_ctx;
|
g_s_p_ctx = p_ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void log_set_enabled(bool enabled)
|
||||||
|
{
|
||||||
|
g_s_log_enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool log_is_enabled(void)
|
||||||
|
{
|
||||||
|
return g_s_log_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
void log_write(int level, const char *p_tag, const char *p_fmt, // NOLINT(readability-function-size)
|
void log_write(int level, const char *p_tag, const char *p_fmt, // NOLINT(readability-function-size)
|
||||||
...)
|
...)
|
||||||
{
|
{
|
||||||
if (g_s_write_cb == NULL)
|
if (!g_s_log_enabled || (g_s_write_cb == NULL))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
#ifndef LOG_H
|
#ifndef LOG_H
|
||||||
#define LOG_H
|
#define LOG_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
|
@ -95,6 +96,32 @@ extern "C"
|
||||||
*/
|
*/
|
||||||
void log_init(log_write_cb_t p_write_cb, void *p_ctx);
|
void log_init(log_write_cb_t p_write_cb, void *p_ctx);
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Рантайм-тумблер (поверх компайл-тайм LOG_LEVEL) */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Включить/выключить логи в рантайме — гейт ПОВЕРХ компайл-тайм
|
||||||
|
* LOG_LEVEL (§3.6). LOG_LEVEL решается на этапе компиляции: макросы
|
||||||
|
* ниже уровня физически вырезаны (`((void)0)`) — этому тумблеру там
|
||||||
|
* нечего гейтить. Тумблер гейтит то, что ОСТАЛОСЬ скомпилированным
|
||||||
|
* (напр. продакшн с `LOG_LEVEL>=INFO` — включить/выключить INFO/WARN/
|
||||||
|
* ERROR в поле без пересборки).
|
||||||
|
*
|
||||||
|
* @param enabled false — log_write() перестаёт звать транспорт (дёшево,
|
||||||
|
* проверка раньше форматирования строки).
|
||||||
|
*/
|
||||||
|
void log_set_enabled(bool enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Текущее состояние тумблера.
|
||||||
|
*
|
||||||
|
* По умолчанию (до первого вызова log_set_enabled()) — true: ранние
|
||||||
|
* сообщения bringup (до загрузки настроек, откуда обычно приходит
|
||||||
|
* реальное значение) не теряются молча.
|
||||||
|
*/
|
||||||
|
bool log_is_enabled(void);
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Мьютекс (weak-хуки) */
|
/* Мьютекс (weak-хуки) */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue