Compare commits
11 commits
main
...
tft-app-de
| Author | SHA1 | Date | |
|---|---|---|---|
| 00c2d43cbd | |||
| 8c72be8708 | |||
| 958fd9d5e5 | |||
| 2662111e1b | |||
| b9b46154cc | |||
| 4f84a173a5 | |||
| 50043bf46a | |||
| 0482c08272 | |||
| 7f3170a7c0 | |||
| a9e668196a | |||
| e2e3f8aff8 |
119 changed files with 33295 additions and 19 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -78,3 +78,8 @@ tools/host/.venv-host-win/
|
||||||
.zed/
|
.zed/
|
||||||
project_tree.txt
|
project_tree.txt
|
||||||
tools/service_tui/dist/
|
tools/service_tui/dist/
|
||||||
|
|
||||||
|
# Референсные дампы старых серийных проектов (не коммитим)
|
||||||
|
firmware/tft_app/OLD_PROJECT/
|
||||||
|
firmware/tft_app/OLD_PROJECT_*/
|
||||||
|
firmware/tft_app/special/
|
||||||
|
|
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -57,7 +57,7 @@
|
||||||
"loadFiles": [],
|
"loadFiles": [],
|
||||||
"runToEntryPoint": "main",
|
"runToEntryPoint": "main",
|
||||||
"rtos": "FreeRTOS",
|
"rtos": "FreeRTOS",
|
||||||
"preLaunchTask": "build:firmware-test-debug",
|
"preLaunchTask": "build:app-debug",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -47,8 +47,11 @@ add_subdirectory(lib)
|
||||||
if(NOT BUILD_TESTS_HOST)
|
if(NOT BUILD_TESTS_HOST)
|
||||||
add_subdirectory(firmware/test)
|
add_subdirectory(firmware/test)
|
||||||
add_subdirectory(firmware/bootloader)
|
add_subdirectory(firmware/bootloader)
|
||||||
# Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) — удалить,
|
add_subdirectory(firmware/tft_app)
|
||||||
# когда появится реальный firmware/tft_app. См. firmware/bootloader/PLAN.md.
|
# Заглушка tft_app для аппаратной верификации bootutil (Фаза 2). Пока
|
||||||
|
# оставляем рядом с реальным firmware/tft_app как независимую A/B-регрессию
|
||||||
|
# загрузчика — удалить, когда app сам сможет служить стендом. См.
|
||||||
|
# firmware/bootloader/PLAN.md и firmware/tft_app/PLAN.md.
|
||||||
add_subdirectory(firmware/bootloader/test_stub)
|
add_subdirectory(firmware/bootloader/test_stub)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,15 @@
|
||||||
"test_mcuboot_boot_select",
|
"test_mcuboot_boot_select",
|
||||||
"test_slot_version",
|
"test_slot_version",
|
||||||
"test_update_policy",
|
"test_update_policy",
|
||||||
"test_recovery"
|
"test_recovery",
|
||||||
|
"test_tft_app_smoke",
|
||||||
|
"test_sul_nku_can",
|
||||||
|
"test_tft_app_controller",
|
||||||
|
"test_tft_app_settings_store",
|
||||||
|
"test_tft_app_menu",
|
||||||
|
"test_sul_registry",
|
||||||
|
"test_tft_app_menu_tree",
|
||||||
|
"test_sul_demo"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -190,7 +198,15 @@
|
||||||
"test_mcuboot_boot_select",
|
"test_mcuboot_boot_select",
|
||||||
"test_slot_version",
|
"test_slot_version",
|
||||||
"test_update_policy",
|
"test_update_policy",
|
||||||
"test_recovery"
|
"test_recovery",
|
||||||
|
"test_tft_app_smoke",
|
||||||
|
"test_sul_nku_can",
|
||||||
|
"test_tft_app_controller",
|
||||||
|
"test_tft_app_settings_store",
|
||||||
|
"test_tft_app_menu",
|
||||||
|
"test_sul_registry",
|
||||||
|
"test_tft_app_menu_tree",
|
||||||
|
"test_sul_demo"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,13 @@ bsp_status_t bsp_display_init(bsp_display_type_t type,
|
||||||
uint32_t framebuffer_addr,
|
uint32_t framebuffer_addr,
|
||||||
bsp_display_frame_cb_t p_on_frame_done);
|
bsp_display_frame_cb_t p_on_frame_done);
|
||||||
|
|
||||||
|
/* То же + явный формат пикселя framebuffer'а (bsp_display_init — обёртка
|
||||||
|
* с BSP_DISPLAY_PIXEL_XRGB8888, совместимость со старыми потребителями). */
|
||||||
|
bsp_status_t bsp_display_init_ex(bsp_display_type_t type,
|
||||||
|
uint32_t framebuffer_addr,
|
||||||
|
bsp_display_frame_cb_t p_on_frame_done,
|
||||||
|
bsp_display_pixel_format_t format);
|
||||||
|
|
||||||
bsp_status_t bsp_display_deinit(void);
|
bsp_status_t bsp_display_deinit(void);
|
||||||
bsp_status_t bsp_display_set_rotation(bsp_display_rotation_t rotation);
|
bsp_status_t bsp_display_set_rotation(bsp_display_rotation_t rotation);
|
||||||
void bsp_display_set_next_buffer(uint32_t framebuffer_addr);
|
void bsp_display_set_next_buffer(uint32_t framebuffer_addr);
|
||||||
|
|
@ -60,6 +67,27 @@ const bsp_display_size_t *bsp_display_get_size(void);
|
||||||
bsp_display_type_t bsp_display_get_type(void);
|
bsp_display_type_t bsp_display_get_type(void);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Формат пикселя framebuffer'а** (что ELCDIF читает из памяти):
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
BSP_DISPLAY_PIXEL_XRGB8888 = 0U, /* 32 бита/пиксель — по умолчанию */
|
||||||
|
BSP_DISPLAY_PIXEL_RGB565, /* 16 бит/пиксель — ½ полосы сканаута */
|
||||||
|
} bsp_display_pixel_format_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
Формат задаёт ТОЛЬКО ширину слова в памяти (`LCDIF CTRL.WORD_LENGTH`) — то
|
||||||
|
есть нагрузку непрерывного DMA-сканаута на SDRAM. Ширина шины пинов панели —
|
||||||
|
всегда 24 бита и от формата не зависит: для RGB565 ELCDIF расширяет 565→24
|
||||||
|
на пинах (проверено на TFT8: белый — чистый белый, чистые R/G/B корректны).
|
||||||
|
Оба режима сосуществуют: старый потребитель (`firmware_test/test_display.c`)
|
||||||
|
работает через `bsp_display_init()` (XRGB8888), `tft_app` (`services/gfx`,
|
||||||
|
гибрид bpp) — через `bsp_display_init_ex(..., BSP_DISPLAY_PIXEL_RGB565)`.
|
||||||
|
|
||||||
|
Практика по полосе (TFT8 800×600 @ ~65 Гц, SDRAM 16 бит @ ~136 МГц ≈ 272 МБ/с):
|
||||||
|
сканаут XRGB8888 ≈ 126 МБ/с (46% всей полосы), RGB565 ≈ 63 МБ/с — вдвое
|
||||||
|
меньше давления на SDRAM для всех остальных мастеров (CPU/PXP).
|
||||||
|
|
||||||
**Типы дисплея:**
|
**Типы дисплея:**
|
||||||
|
|
||||||
```c
|
```c
|
||||||
|
|
@ -83,7 +111,7 @@ typedef enum {
|
||||||
} bsp_display_rotation_t;
|
} bsp_display_rotation_t;
|
||||||
```
|
```
|
||||||
|
|
||||||
**Коды возврата `bsp_display_init()`:**
|
**Коды возврата `bsp_display_init()` / `bsp_display_init_ex()`:**
|
||||||
|
|
||||||
| Код | Условие |
|
| Код | Условие |
|
||||||
| ----------------------- | ------------------------------ |
|
| ----------------------- | ------------------------------ |
|
||||||
|
|
@ -91,7 +119,7 @@ typedef enum {
|
||||||
| `BSP_ERR_PARAM` | `type >= BSP_DISPLAY_COUNT` |
|
| `BSP_ERR_PARAM` | `type >= BSP_DISPLAY_COUNT` |
|
||||||
| `BSP_ERR_NOT_SUPPORTED` | TFT4 — Video PLL не реализован |
|
| `BSP_ERR_NOT_SUPPORTED` | TFT4 — Video PLL не реализован |
|
||||||
|
|
||||||
`bsp_display_init()` идемпотентен: повторный вызов без `deinit` возвращает
|
Обе init-функции идемпотентны: повторный вызов без `deinit` возвращает
|
||||||
`BSP_OK` без побочных эффектов. Callback `p_on_frame_done` должен быть
|
`BSP_OK` без побочных эффектов. Callback `p_on_frame_done` должен быть
|
||||||
ISR-safe (`NULL` допускается).
|
ISR-safe (`NULL` допускается).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,22 @@ typedef enum bsp_display_rotation_e
|
||||||
BSP_DISPLAY_FLIP_HORIZONTAL, /**< LR=0 UD=0. Горизонтальный флип (SHLR). */
|
BSP_DISPLAY_FLIP_HORIZONTAL, /**< LR=0 UD=0. Горизонтальный флип (SHLR). */
|
||||||
} bsp_display_rotation_t;
|
} bsp_display_rotation_t;
|
||||||
|
|
||||||
|
/* ── Формат пикселя framebuffer'а (что ELCDIF читает из памяти) ───────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Формат пикселя в памяти framebuffer'а.
|
||||||
|
*
|
||||||
|
* Определяет ширину слова, которое ELCDIF читает по DMA из SDRAM (=нагрузку
|
||||||
|
* на полосу). Ширина шины пинов панели (24-бит) — отдельный параметр драйвера,
|
||||||
|
* от этого НЕ зависит: RGB565 (16 бит/пиксель в памяти) корректно выводится и
|
||||||
|
* на 24-битную панель (ELCDIF расширяет 565→24 на пинах).
|
||||||
|
*/
|
||||||
|
typedef enum bsp_display_pixel_format_e
|
||||||
|
{
|
||||||
|
BSP_DISPLAY_PIXEL_XRGB8888 = 0U, /**< 32 бита/пиксель (X игнорируется). По умолчанию. */
|
||||||
|
BSP_DISPLAY_PIXEL_RGB565, /**< 16 бит/пиксель — вдвое меньше полосы сканаута. */
|
||||||
|
} bsp_display_pixel_format_t;
|
||||||
|
|
||||||
/* ── Размер дисплея ──────────────────────────────────────────────────── */
|
/* ── Размер дисплея ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
typedef struct bsp_display_size_s
|
typedef struct bsp_display_size_s
|
||||||
|
|
@ -77,10 +93,26 @@ typedef void (*bsp_display_frame_cb_t)(void);
|
||||||
* Должен быть выровнен по 64 байт, в NonCacheable SDRAM.
|
* Должен быть выровнен по 64 байт, в NonCacheable SDRAM.
|
||||||
* @param on_frame_done ISR-safe callback по завершении кадра; NULL — без callback.
|
* @param on_frame_done ISR-safe callback по завершении кадра; NULL — без callback.
|
||||||
* @return BSP_OK | BSP_ERR_PARAM | BSP_ERR_NOT_SUPPORTED
|
* @return BSP_OK | BSP_ERR_PARAM | BSP_ERR_NOT_SUPPORTED
|
||||||
|
*
|
||||||
|
* @note Формат пикселя — XRGB8888 (обёртка над bsp_display_init_ex()).
|
||||||
*/
|
*/
|
||||||
bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr,
|
bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr,
|
||||||
bsp_display_frame_cb_t p_on_frame_done);
|
bsp_display_frame_cb_t p_on_frame_done);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief То же, что bsp_display_init(), но с явным форматом пикселя framebuffer'а.
|
||||||
|
*
|
||||||
|
* Позволяет потребителю выбрать RGB565 (вдвое меньше полосы сканаута) вместо
|
||||||
|
* XRGB8888, не меняя остальную настройку. bsp_display_init() — обёртка с
|
||||||
|
* форматом BSP_DISPLAY_PIXEL_XRGB8888 (совместимость со старыми потребителями).
|
||||||
|
*
|
||||||
|
* @param format Формат пикселя в памяти (см. bsp_display_pixel_format_t).
|
||||||
|
* @return BSP_OK | BSP_ERR_PARAM | BSP_ERR_NOT_SUPPORTED
|
||||||
|
*/
|
||||||
|
bsp_status_t bsp_display_init_ex(bsp_display_type_t type, uint32_t framebuffer_addr,
|
||||||
|
bsp_display_frame_cb_t p_on_frame_done,
|
||||||
|
bsp_display_pixel_format_t format);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Остановить ELCDIF, выключить подсветку, деинициализировать.
|
* @brief Остановить ELCDIF, выключить подсветку, деинициализировать.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -270,9 +270,19 @@ static void enable_lcd_interrupt(void)
|
||||||
ELCDIF_EnableInterrupts(LCDIF, kELCDIF_CurFrameDoneInterruptEnable);
|
ELCDIF_EnableInterrupts(LCDIF, kELCDIF_CurFrameDoneInterruptEnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Заполнить конфигурацию ELCDIF из таблицы + адрес буфера. */
|
/** @brief Смаппить BSP-формат пикселя на формат памяти ELCDIF. */
|
||||||
|
static elcdif_pixel_format_t map_pixel_format(bsp_display_pixel_format_t format)
|
||||||
|
{
|
||||||
|
/* dataBus остаётся 24-бит независимо: pixelFormat задаёт лишь ширину слова в
|
||||||
|
* памяти (WORD_LENGTH), ELCDIF расширяет 565→24 на пинах (см. fsl_elcdif.c
|
||||||
|
* s_pixelFormatReg). */
|
||||||
|
return (format == BSP_DISPLAY_PIXEL_RGB565) ? kELCDIF_PixelFormatRGB565
|
||||||
|
: kELCDIF_PixelFormatXRGB8888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Заполнить конфигурацию ELCDIF из таблицы + адрес буфера + формат. */
|
||||||
static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer_addr,
|
static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer_addr,
|
||||||
elcdif_rgb_mode_config_t *p_out)
|
bsp_display_pixel_format_t format, elcdif_rgb_mode_config_t *p_out)
|
||||||
{
|
{
|
||||||
p_out->panelWidth = p_cfg->width;
|
p_out->panelWidth = p_cfg->width;
|
||||||
p_out->panelHeight = p_cfg->height;
|
p_out->panelHeight = p_cfg->height;
|
||||||
|
|
@ -284,7 +294,7 @@ static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer
|
||||||
p_out->vbp = p_cfg->vbp;
|
p_out->vbp = p_cfg->vbp;
|
||||||
p_out->polarityFlags = p_cfg->pol_flags;
|
p_out->polarityFlags = p_cfg->pol_flags;
|
||||||
p_out->bufferAddr = framebuffer_addr;
|
p_out->bufferAddr = framebuffer_addr;
|
||||||
p_out->pixelFormat = kELCDIF_PixelFormatXRGB8888;
|
p_out->pixelFormat = map_pixel_format(format);
|
||||||
p_out->dataBus = kELCDIF_DataBus24Bit;
|
p_out->dataBus = kELCDIF_DataBus24Bit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,6 +302,15 @@ static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer
|
||||||
|
|
||||||
bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr,
|
bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr,
|
||||||
bsp_display_frame_cb_t p_on_frame_done)
|
bsp_display_frame_cb_t p_on_frame_done)
|
||||||
|
{
|
||||||
|
/* Обёртка совместимости — формат по умолчанию XRGB8888 (старые потребители). */
|
||||||
|
return bsp_display_init_ex(type, framebuffer_addr, p_on_frame_done,
|
||||||
|
BSP_DISPLAY_PIXEL_XRGB8888);
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_status_t bsp_display_init_ex(bsp_display_type_t type, uint32_t framebuffer_addr,
|
||||||
|
bsp_display_frame_cb_t p_on_frame_done,
|
||||||
|
bsp_display_pixel_format_t format)
|
||||||
{
|
{
|
||||||
if ((uint32_t) type >= (uint32_t) BSP_DISPLAY_COUNT)
|
if ((uint32_t) type >= (uint32_t) BSP_DISPLAY_COUNT)
|
||||||
{
|
{
|
||||||
|
|
@ -324,7 +343,7 @@ bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr
|
||||||
g_s_display.frame_cb = p_on_frame_done;
|
g_s_display.frame_cb = p_on_frame_done;
|
||||||
|
|
||||||
elcdif_rgb_mode_config_t elcdif_cfg;
|
elcdif_rgb_mode_config_t elcdif_cfg;
|
||||||
build_elcdif_cfg(p_cfg, framebuffer_addr, &elcdif_cfg);
|
build_elcdif_cfg(p_cfg, framebuffer_addr, format, &elcdif_cfg);
|
||||||
ELCDIF_RgbModeInit(LCDIF, &elcdif_cfg);
|
ELCDIF_RgbModeInit(LCDIF, &elcdif_cfg);
|
||||||
enable_lcd_interrupt();
|
enable_lcd_interrupt();
|
||||||
ELCDIF_RgbModeStart(LCDIF);
|
ELCDIF_RgbModeStart(LCDIF);
|
||||||
|
|
|
||||||
253
cmake/linker/MIMXRT1052xxxxx_app_slot.ld
Normal file
253
cmake/linker/MIMXRT1052xxxxx_app_slot.ld
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
/*
|
||||||
|
** ###################################################################
|
||||||
|
** Linker file — tft_app как MCUboot-слот-образ (Direct-XIP).
|
||||||
|
**
|
||||||
|
** Полный набор секций как в MIMXRT1052xxxxx_bootloader_flexspi_nor.ld
|
||||||
|
** (включая .ram_function → ITCM для AT_QUICKACCESS_SECTION_CODE, .ncache,
|
||||||
|
** DataQuickAccess), но адаптирован под слот:
|
||||||
|
** - НЕТ .flash_config / .ivt (образ грузит не BootROM, а bootloader
|
||||||
|
** software-jump'ом — FCB/IVT/DCD не нужны).
|
||||||
|
** - Перед vector table зарезервирован imgtool-заголовок (0x200):
|
||||||
|
** boot_select.c прыгает по flash_base + fa_off + ih_hdr_size.
|
||||||
|
** - m_text — на весь слот (2 МБ − header − vectors).
|
||||||
|
**
|
||||||
|
** КРИТИЧНО: .ram_function размещает код bsp_qspi_flash
|
||||||
|
** (AT_QUICKACCESS_SECTION_CODE, входная секция CodeQuickAccess) в ITCM и
|
||||||
|
** копируется startup'ом. Без неё этот код исполняется из flash, а любая
|
||||||
|
** IP-команда FlexSPI (bsp_qspi_init/erase/write) блокирует AHB-путь →
|
||||||
|
** HardFault на fetch следующей инструкции из той же flash (XIP). См.
|
||||||
|
** bsp/qspi_flash/README.md "XIP-безопасность" и
|
||||||
|
** docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md §5.
|
||||||
|
**
|
||||||
|
** Базовый адрес слота — через -Wl,--defsym=__slot_base__=0x...
|
||||||
|
** Slot A: 0x60040000 Slot Б: 0x60240000
|
||||||
|
**
|
||||||
|
** Copyright 2016 Freescale Semiconductor, Inc.
|
||||||
|
** Copyright 2016-2024 NXP
|
||||||
|
** SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
** ###################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
ENTRY(Reset_Handler)
|
||||||
|
|
||||||
|
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x0400; /* newlib sbrk-heap; FreeRTOS heap_4 — свой массив в .bss */
|
||||||
|
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x1000;
|
||||||
|
VECTOR_RAM_SIZE = DEFINED(__ram_vector_table__) ? 0x00000400 : 0;
|
||||||
|
|
||||||
|
SLOT_BASE = DEFINED(__slot_base__) ? __slot_base__ : 0x60040000;
|
||||||
|
SLOT_SIZE = 0x00200000; /* 2 МБ — = imgtool -S 0x200000 и flash_map */
|
||||||
|
IMGTOOL_HDR_SZ = 0x200; /* imgtool sign -H 0x200 */
|
||||||
|
VECTOR_SIZE = 0x400;
|
||||||
|
|
||||||
|
MEMORY
|
||||||
|
{
|
||||||
|
m_interrupts (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ, LENGTH = VECTOR_SIZE
|
||||||
|
m_text (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ + VECTOR_SIZE, LENGTH = SLOT_SIZE - IMGTOOL_HDR_SZ - VECTOR_SIZE
|
||||||
|
m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128 КБ — ramfunc */
|
||||||
|
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128 КБ — .data/.bss/heap/stack */
|
||||||
|
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ */
|
||||||
|
/* SDRAM 32 МБ, разбита на 2: некэшируемые DMA-буферы компоновщика (ELCDIF
|
||||||
|
* читает framebuffer, PXP — AS/PS напрямую; MPU Region 9 в board_mpu_init()
|
||||||
|
* параметризован ЭТИМИ __NCACHE_REGION_* символами, C-код не трогаем) +
|
||||||
|
* остаток под кэшируемые буферы будущих фаз (ассеты, аудио).
|
||||||
|
*
|
||||||
|
* Фаза 3.2.4 (double-buffer + PXP): 4 буфера TFT8 800×600×4 ≈ 1.92 МБ каждый
|
||||||
|
* — AS (alpha_buffer) + PS (processing_buffer) + framebuffer[2] = 7.68 МБ
|
||||||
|
* → 8 МБ (степень двойки; MPU-регион обязан быть степенью двойки и выровнен
|
||||||
|
* по своему размеру — 8 МБ @ 0x80000000 удовлетворяет). См. bsp/sdram/
|
||||||
|
* README.md про NXP-рекомендацию некэшируемых DMA-буферов в SDRAM. */
|
||||||
|
m_sdram_ncache (RW) : ORIGIN = 0x80000000, LENGTH = 0x00800000 /* 8 МБ — AS+PS+2×FB (non-cacheable) */
|
||||||
|
m_sdram (RW) : ORIGIN = 0x80800000, LENGTH = 0x01800000 /* 24 МБ — кэшируемый остаток, пока не используется */
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
__NCACHE_REGION_START = ORIGIN(m_sdram_ncache);
|
||||||
|
__NCACHE_REGION_SIZE = LENGTH(m_sdram_ncache); /* 8 МБ non-cacheable — AS+PS+2×FB */
|
||||||
|
|
||||||
|
/* Vector table (imgtool header зарезервирован до неё в MEMORY). */
|
||||||
|
.interrupts :
|
||||||
|
{
|
||||||
|
__VECTOR_TABLE = .;
|
||||||
|
__Vectors = .;
|
||||||
|
. = ALIGN(4);
|
||||||
|
KEEP(*(.isr_vector))
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > m_interrupts
|
||||||
|
|
||||||
|
.text :
|
||||||
|
{
|
||||||
|
. = ALIGN(4);
|
||||||
|
*(.text)
|
||||||
|
*(.text*)
|
||||||
|
*(.rodata)
|
||||||
|
*(.rodata*)
|
||||||
|
*(.glue_7)
|
||||||
|
*(.glue_7t)
|
||||||
|
*(.eh_frame)
|
||||||
|
KEEP (*(.init))
|
||||||
|
KEEP (*(.fini))
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } > m_text
|
||||||
|
|
||||||
|
.ARM :
|
||||||
|
{
|
||||||
|
__exidx_start = .;
|
||||||
|
*(.ARM.exidx*)
|
||||||
|
__exidx_end = .;
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.ctors :
|
||||||
|
{
|
||||||
|
__CTOR_LIST__ = .;
|
||||||
|
KEEP (*crtbegin.o(.ctors))
|
||||||
|
KEEP (*crtbegin?.o(.ctors))
|
||||||
|
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors))
|
||||||
|
KEEP (*(SORT(.ctors.*)))
|
||||||
|
KEEP (*(.ctors))
|
||||||
|
__CTOR_END__ = .;
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.dtors :
|
||||||
|
{
|
||||||
|
__DTOR_LIST__ = .;
|
||||||
|
KEEP (*crtbegin.o(.dtors))
|
||||||
|
KEEP (*crtbegin?.o(.dtors))
|
||||||
|
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors))
|
||||||
|
KEEP (*(SORT(.dtors.*)))
|
||||||
|
KEEP (*(.dtors))
|
||||||
|
__DTOR_END__ = .;
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.preinit_array :
|
||||||
|
{
|
||||||
|
PROVIDE_HIDDEN (__preinit_array_start = .);
|
||||||
|
KEEP (*(.preinit_array*))
|
||||||
|
PROVIDE_HIDDEN (__preinit_array_end = .);
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.init_array :
|
||||||
|
{
|
||||||
|
PROVIDE_HIDDEN (__init_array_start = .);
|
||||||
|
KEEP (*(SORT(.init_array.*)))
|
||||||
|
KEEP (*(.init_array*))
|
||||||
|
PROVIDE_HIDDEN (__init_array_end = .);
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.fini_array :
|
||||||
|
{
|
||||||
|
PROVIDE_HIDDEN (__fini_array_start = .);
|
||||||
|
KEEP (*(SORT(.fini_array.*)))
|
||||||
|
KEEP (*(.fini_array*))
|
||||||
|
PROVIDE_HIDDEN (__fini_array_end = .);
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
__etext = .;
|
||||||
|
__DATA_ROM = .;
|
||||||
|
|
||||||
|
.interrupts_ram :
|
||||||
|
{
|
||||||
|
. = ALIGN(4);
|
||||||
|
__VECTOR_RAM__ = .;
|
||||||
|
__interrupts_ram_start__ = .;
|
||||||
|
*(.m_interrupts_ram)
|
||||||
|
. += VECTOR_RAM_SIZE;
|
||||||
|
. = ALIGN(4);
|
||||||
|
__interrupts_ram_end__ = .;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
__VECTOR_RAM = DEFINED(__ram_vector_table__) ? __VECTOR_RAM__ : ORIGIN(m_interrupts);
|
||||||
|
__RAM_VECTOR_TABLE_SIZE_BYTES = DEFINED(__ram_vector_table__) ? (__interrupts_ram_end__ - __interrupts_ram_start__) : 0x0;
|
||||||
|
|
||||||
|
.data : AT(__DATA_ROM)
|
||||||
|
{
|
||||||
|
. = ALIGN(4);
|
||||||
|
__DATA_RAM = .;
|
||||||
|
__data_start__ = .;
|
||||||
|
*(.data)
|
||||||
|
*(.data*)
|
||||||
|
*(DataQuickAccess) /* quick access data section */
|
||||||
|
KEEP(*(.jcr*))
|
||||||
|
. = ALIGN(4);
|
||||||
|
__data_end__ = .;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
__ram_function_flash_start = __DATA_ROM + (__data_end__ - __data_start__); /* startup копирует ramfunc из flash в ITCM */
|
||||||
|
|
||||||
|
.ram_function : AT(__ram_function_flash_start)
|
||||||
|
{
|
||||||
|
. = ALIGN(32);
|
||||||
|
__ram_function_start__ = .;
|
||||||
|
*(CodeQuickAccess) /* AT_QUICKACCESS_SECTION_CODE — bsp_qspi FlexSPI IP-код */
|
||||||
|
. = ALIGN(128);
|
||||||
|
__ram_function_end__ = .;
|
||||||
|
} > m_qacode
|
||||||
|
|
||||||
|
__NDATA_ROM = __ram_function_flash_start + (__ram_function_end__ - __ram_function_start__);
|
||||||
|
/* framebuffer (gfx, AT_NONCACHEABLE_SECTION_ALIGN, без инициализатора) —
|
||||||
|
* в .ncache (bare/zero-init), НЕ .ncache.init (copy-from-flash, сейчас
|
||||||
|
* ничего не использует, оставлена под будущие initialized non-cacheable
|
||||||
|
* буферы). Обе — в m_sdram_ncache, см. MEMORY выше. */
|
||||||
|
.ncache.init :
|
||||||
|
{
|
||||||
|
. = ALIGN(32);
|
||||||
|
__noncachedata_start__ = .;
|
||||||
|
*(NonCacheable.init)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__noncachedata_init_end__ = .;
|
||||||
|
} > m_sdram_ncache
|
||||||
|
. = __noncachedata_init_end__;
|
||||||
|
.ncache :
|
||||||
|
{
|
||||||
|
*(NonCacheable)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__noncachedata_end__ = .;
|
||||||
|
} > m_sdram_ncache
|
||||||
|
|
||||||
|
__DATA_END = __NDATA_ROM;
|
||||||
|
text_end = ORIGIN(m_text) + LENGTH(m_text);
|
||||||
|
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
|
||||||
|
ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)")
|
||||||
|
ASSERT((__noncachedata_end__ - ORIGIN(m_sdram_ncache)) <= LENGTH(m_sdram_ncache),
|
||||||
|
"m_sdram_ncache overflow — AS+PS+2×FB не влезли в 8 МБ")
|
||||||
|
|
||||||
|
.bss :
|
||||||
|
{
|
||||||
|
. = ALIGN(4);
|
||||||
|
__START_BSS = .;
|
||||||
|
__bss_start__ = .;
|
||||||
|
*(.bss)
|
||||||
|
*(.bss*)
|
||||||
|
*(COMMON)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__bss_end__ = .;
|
||||||
|
__END_BSS = .;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
.heap :
|
||||||
|
{
|
||||||
|
. = ALIGN(8);
|
||||||
|
__end__ = .;
|
||||||
|
PROVIDE(end = .);
|
||||||
|
__HeapBase = .;
|
||||||
|
. += HEAP_SIZE;
|
||||||
|
__HeapLimit = .;
|
||||||
|
__heap_limit = .;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
.stack :
|
||||||
|
{
|
||||||
|
. = ALIGN(8);
|
||||||
|
. += STACK_SIZE;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
__StackTop = ORIGIN(m_data) + LENGTH(m_data);
|
||||||
|
__StackLimit = __StackTop - STACK_SIZE;
|
||||||
|
PROVIDE(__stack = __StackTop);
|
||||||
|
|
||||||
|
.ARM.attributes 0 : { *(.ARM.attributes) }
|
||||||
|
|
||||||
|
ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
|
||||||
|
}
|
||||||
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) |
|
||||||
209
docs/tft_app/DOMAIN_DATAFLOW.md
Normal file
209
docs/tft_app/DOMAIN_DATAFLOW.md
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
# tft-app — путь данных домена (транспорт → экран)
|
||||||
|
|
||||||
|
Документ описывает **реализованный** доменный слой (Фазы 1–2): как кадр от СУЛ проходит
|
||||||
|
от шины до презентации, какие модули за что отвечают и каков контракт между ними. Проектное
|
||||||
|
обоснование слоёв — в [ARCH.md](../../firmware/tft_app/ARCH.md); статус фаз —
|
||||||
|
в [PLAN.md](../../firmware/tft_app/PLAN.md). Здесь — «как это работает в коде сейчас».
|
||||||
|
|
||||||
|
Сейчас в реестре два протокола — **НКУ-CAN** (реальная шина) и **демо** (синтетический источник,
|
||||||
|
Фаза 3.3, витрина возможностей устройства без СУЛ на другом конце). Архитектура рассчитана на
|
||||||
|
много протоколов (ещё УЭЛ/УКЛ/SD7/УИМ — Фаза 8): добавление протокола = новый декодер + запись
|
||||||
|
в реестр (+ transport, если новый вид шины) — см. [ADDING_PROTOCOL.md](ADDING_PROTOCOL.md) для
|
||||||
|
пошагового разбора.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Обзор слоёв
|
||||||
|
|
||||||
|
Каждый слой — отдельная статическая библиотека CMake. **Домен — чистый C без единого
|
||||||
|
HAL-вызова** (host-тестируется), железо изолировано в `bsp/*` и тонком транспорт-адаптере.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph L0["L0 · железо (bsp, не трогаем)"]
|
||||||
|
CAN["bsp_can<br/>приём CAN-кадров"]
|
||||||
|
end
|
||||||
|
subgraph L1["L1 · транспорт (HW-адаптер)"]
|
||||||
|
TR["sul/transport/can<br/>bsp_can_frame_t → sul_frame_t"]
|
||||||
|
end
|
||||||
|
subgraph L2["L2 · домен (чистый C, host-тесты)"]
|
||||||
|
REG["sul (реестр)<br/>sul_registry_active()"]
|
||||||
|
DEC["sul/nku_can<br/>чистый декодер PACKET1..5"]
|
||||||
|
MODEL["elevator_model<br/>sul_result_t, sul_mode_t"]
|
||||||
|
CTRL["controller<br/>diff + свёртка режима"]
|
||||||
|
PRIO["mode_priority<br/>таблица приоритетов"]
|
||||||
|
end
|
||||||
|
subgraph L3["L3 · презентация"]
|
||||||
|
UI["ui/fallback<br/>этаж/стрелка/метка режима"]
|
||||||
|
AUD["audio_policy<br/>(Фаза 6)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
CAN --> TR --> REG --> DEC
|
||||||
|
DEC -->|"sul_result_t"| CTRL
|
||||||
|
MODEL -.->|"тип"| DEC
|
||||||
|
MODEL -.->|"тип"| CTRL
|
||||||
|
PRIO -.->|"свёртка"| CTRL
|
||||||
|
CTRL -->|"indication_task_t"| UI
|
||||||
|
CTRL -->|"indication_task_t"| AUD
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключевой инвариант: **декодер не знает про шрифт/экран, презентация не знает про биты
|
||||||
|
протокола**. Единственная валюта между ними — канонический `sul_result_t`
|
||||||
|
([elevator_model.h](../../firmware/tft_app/src/domain/elevator_model/include/domain/elevator_model.h)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Путь одного кадра
|
||||||
|
|
||||||
|
`sul_rx_task` опрашивает CAN, прогоняет кадр через активный декодер и передаёт результат
|
||||||
|
контроллеру; `render_task` применяет diff. **Данные** между ними — очередь глубины 1
|
||||||
|
(`xQueueOverwrite`: важно только последнее состояние, не история); **пробуждение** —
|
||||||
|
`xTaskNotifyGive` (render_task — event-driven, не поллит). Пока открыто меню, `sul_rx_task`
|
||||||
|
находится в «мягкой паузе» (decode/controller/очередь пропускаются, WDOG кормится). Полная
|
||||||
|
картина задач — [TASKS.md](TASKS.md).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant CAN as bsp_can
|
||||||
|
participant TR as transport/can
|
||||||
|
participant REG as sul_registry
|
||||||
|
participant DEC as nku_can_decode()
|
||||||
|
participant CTX as nku_can_ctx_t
|
||||||
|
participant CTRL as controller_process()
|
||||||
|
participant UI as ui_fallback_render()
|
||||||
|
|
||||||
|
CAN->>TR: bsp_can_frame_t (id, data, dlc)
|
||||||
|
TR->>REG: sul_frame_t
|
||||||
|
REG->>DEC: active().decode(ctx, frame, out)
|
||||||
|
DEC->>CTX: обновить поле(я) пришедшего пакета
|
||||||
|
CTX-->>DEC: полная накопленная state
|
||||||
|
DEC-->>CTRL: sul_result_t (при SUL_STATUS_OK)
|
||||||
|
CTRL->>CTRL: diff с кэшем + sul_resolve_mode()
|
||||||
|
CTRL-->>UI: indication_task_t (pending-флаги + mode)
|
||||||
|
UI->>UI: перерисовать изменившееся
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Контракт декодера
|
||||||
|
|
||||||
|
**Кадр транспортного уровня** — нейтрален к шине (CAN/UART/…); транспорт заполняет, декодер
|
||||||
|
только читает ([sul.h](../../firmware/tft_app/src/domain/sul/include/domain/sul.h)):
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct { uint32_t id; uint8_t bus; const uint8_t *p_data; uint16_t len; } sul_frame_t;
|
||||||
|
typedef sul_status_t (*sul_decode_fn_t)(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Три исхода** `decode()` (`sul_status_t`):
|
||||||
|
|
||||||
|
| Статус | Когда | Что с `*p_out` |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SUL_STATUS_OK` | кадр распознан | заполнен полной накопленной `state` |
|
||||||
|
| `SUL_STATUS_IGNORED` | ID не этого протокола | **не тронут** |
|
||||||
|
| `SUL_STATUS_ERR` | ID совпал, но кадр малформирован (неверный DLC / код символа вне таблицы) | не тронут |
|
||||||
|
|
||||||
|
**Декодер stateful через ctx, но остаётся чистой функцией.** Разные пакеты несут разные поля
|
||||||
|
в разных кадрах — `decode()` обновляет только пришедшее и отдаёт наружу **полную** копию
|
||||||
|
накопленного `state`, а не дельту. Состояние держит caller в `nku_can_ctx_t` и передаёт
|
||||||
|
указатель на каждый вызов — декодер не владеет памятью/жизненным циклом.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
P1["PACKET1<br/>направление, режим"] --> ST["nku_can_ctx_t.state<br/>(полный sul_result_t)"]
|
||||||
|
P3["PACKET3<br/>позиция, гонг, погрузка"] --> ST
|
||||||
|
P4["PACKET4<br/>перегруз, сейсмо"] --> ST
|
||||||
|
ST -->|"копия целиком"| OUT["*p_out"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. НКУ-CAN: пакет → поля модели
|
||||||
|
|
||||||
|
Порт боевого декодера `OLD_PROJECT/source/main_programm.c` (`msg_receiver_task`) в чистую
|
||||||
|
функцию. Реализация — [nku_can.c](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c).
|
||||||
|
|
||||||
|
**Адрес станции — из настроек (Фаза 3), применяется на ОБА конца одновременно** (боевая
|
||||||
|
находка: правка только декодера бесполезна — HW-фильтры FlexCAN отбрасывают кадры чужого
|
||||||
|
адреса до всякого софта):
|
||||||
|
|
||||||
|
- декодер: `nku_can_set_address()` — ID пакетов сдвигаются на `group4 = addr<<4` (PACKET1..4,
|
||||||
|
биты [7:4]) и `group6 = addr<<6` (PACKET5, биты [8:6], протокол отводит 3 бита);
|
||||||
|
- транспорт: `sul_transport_can_set_address()` — переконфигурация RX-фильтров Message
|
||||||
|
Buffer'ов под те же ID (diff-защита: реальная переконфигурация только при смене адреса).
|
||||||
|
|
||||||
|
`sul_rx_task` вызывает оба на каждой итерации (дёшево) — правка адреса в меню подхватывается
|
||||||
|
без межзадачного сигнала. В таблице ниже ID приведены для адреса 0 (базовые).
|
||||||
|
|
||||||
|
| Пакет | ID (адрес 0) | Байты/маски → поля `sul_result_t` |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **PACKET1** | `0x506` | `d6[1:0]` → `direction` · `d6[3:2]` → `movement` · `d6[7:4]` код режима → `fire_alarm`/`maintenance`/`fireman`/погрузка-инстр. · `d3[5:0]` → уровень остановки (внутр., гейт PACKET5) |
|
||||||
|
| **PACKET2** | `0x408` | `d7&0x40` → перегруз (источник 1) |
|
||||||
|
| **PACKET3** | `0x508` | `d5/d6 &0x3F` → `pos`, `floor_num` · `d3&0x40==0` → `arrival` (гонг) · `d2&0x3F` сек + `d3&0x0F` мин → `lading_secs` |
|
||||||
|
| **PACKET4** | `0x50B` | `d5&0x40` → перегруз (источник 2) · `d0&0x80` → `seismic` |
|
||||||
|
| **PACKET5** | `0x606` | `d3/d4 &0x3F` → `next` (только пока едет ↑/↓ и назначение ≠ уровень; иначе `next=""`) |
|
||||||
|
|
||||||
|
**Мультиисточниковые поля.** Некоторые выходные поля кормятся несколькими пакетами. Они держатся
|
||||||
|
в `ctx` раздельными латчами и на выход идут как **OR** — иначе пакет-без-сигнала сбросил бы флаг,
|
||||||
|
выставленный другим пакетом (боевой баг-класс):
|
||||||
|
|
||||||
|
- `overload = overload_p2 || overload_p4` (PACKET2 и PACKET4 независимы);
|
||||||
|
- `lading = lading_instr || (lading_secs > 0)` (инструментальная из PACKET1, временная из PACKET3).
|
||||||
|
|
||||||
|
Режимными флагами, которыми владеет **один** пакет (fire/maintenance/fireman — PACKET1;
|
||||||
|
seismic — PACKET4), пакет-владелец распоряжается напрямую: сбрасывает в начале и выставляет по
|
||||||
|
условию, гася устаревший режим.
|
||||||
|
|
||||||
|
Удалённая установка адреса (кадры `0x4X1`/`0x5XB`, спецификация `REMOTE_ADDRES_SETUP.pdf`) —
|
||||||
|
**Фаза 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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Контроллер: diff и таймаут
|
||||||
|
|
||||||
|
`controller_process()` — редьюсер: сравнивает новый `sul_result_t` с кэшем, выдаёт
|
||||||
|
`indication_task_t` (что перерисовать/озвучить) и обновляет кэш
|
||||||
|
([controller.c](../../firmware/tft_app/src/domain/controller/src/controller.c)).
|
||||||
|
|
||||||
|
| Поле `indication_task_t` | Условие | Потребитель |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pos_pending` | изменилась строка позиции | презентация |
|
||||||
|
| `next_pending` | изменился следующий этаж | презентация (Фаза 5) |
|
||||||
|
| `direction_pending` | изменилось направление | презентация |
|
||||||
|
| `mode_pending` | сменился **разрешённый** режим (см. [MODE_PRIORITY.md](MODE_PRIORITY.md)) | презентация |
|
||||||
|
| `arrival_pending` | фронт `false→true` гонга | аудио (Фаза 6) |
|
||||||
|
| `movement_pending` | фронт `false→true` начала движения | аудио (Фаза 6) |
|
||||||
|
| `mode` | разрешённый режим нового результата | презентация читает при `mode_pending` |
|
||||||
|
|
||||||
|
**Таймаут связи — не отдельный API.** При пропадании кадров дольше таймаута `sul_rx`-задача
|
||||||
|
просто вызывает `controller_process()` с `sul_default_state()` — тем же путём, что и обычный кадр
|
||||||
|
(`pos="--"`, режимы сброшены). Diff корректно пометит изменения, презентация покажет «--».
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
POLL["sul_rx: опрос CAN"] --> GOT{"кадр за таймаут?"}
|
||||||
|
GOT -->|да| DEC["decode → sul_result_t"]
|
||||||
|
GOT -->|нет| DEF["sul_default_state()"]
|
||||||
|
DEC --> CTRL["controller_process()"]
|
||||||
|
DEF --> CTRL
|
||||||
|
CTRL --> Q["очередь глубины 1"]
|
||||||
|
Q --> RENDER["render-задача"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Дальше — презентация
|
||||||
|
|
||||||
|
Как `indication_task_t` превращается в пиксели и звук:
|
||||||
|
|
||||||
|
- Свёртка сигналов в один экранный режим — [MODE_PRIORITY.md](MODE_PRIORITY.md).
|
||||||
|
- Безопасная (asset-free) отрисовка этажа/стрелки/режима — [FALLBACK.md](FALLBACK.md).
|
||||||
|
- Богатый layout со спрайтами и звук — Фазы 4–6 ([PLAN.md](../../firmware/tft_app/PLAN.md)).
|
||||||
136
docs/tft_app/FALLBACK.md
Normal file
136
docs/tft_app/FALLBACK.md
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
# tft-app — fallback-рендер (safe-mode)
|
||||||
|
|
||||||
|
«Аварийный» режим индикации: минимум зависимостей, никаких ассетов и файловой системы.
|
||||||
|
Это же — первый экран walking-skeleton (Фаза 1) и гарантированная деградация, когда ассеты/layout
|
||||||
|
недоступны. Проектная основа — [ARCH.md §11](../../firmware/tft_app/ARCH.md); реализация —
|
||||||
|
[fallback.c](../../firmware/tft_app/src/ui/fallback/src/fallback.c).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что такое fallback
|
||||||
|
|
||||||
|
`default`-layout **asset-free и FS-free**: не монтирует QSPI-FAT/SD, не трогает TLV-ридер ассетов.
|
||||||
|
Зависимости — только `bsp_display` + `gfx` (framebuffer, вкомпилированные шрифты, примитив стрелки)
|
||||||
|
+ домен. Рисует на чёрном фоне, без звука. Устройство никогда не «гаснет»: даже при полностью
|
||||||
|
битых ассетах есть чем показать этаж.
|
||||||
|
|
||||||
|
Принцип degradation, а не отказа (ARCH §3): сбой ассетов/layout/связи ведёт к безопасному
|
||||||
|
**упрощению** индикации, а не к отказу.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Что рисуется
|
||||||
|
|
||||||
|
Презентация решает по разрешённому режиму (`indication_task_t.mode`, см.
|
||||||
|
[MODE_PRIORITY.md](MODE_PRIORITY.md)) — **и по диспетчерскому входу** (§2.1), который
|
||||||
|
проверяется первым и безусловно перекрывает всё остальное:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
START([render mode, result, dispatcher]) --> CLR["gfx_clear() — AS прозрачен,<br/>фон даёт чёрный PS"]
|
||||||
|
CLR --> DSP{"dispatcher != NONE?"}
|
||||||
|
DSP -->|да| DLBL["метка ВЫЗОВ/ОТВЕТ (SystemFont)"]
|
||||||
|
DLBL --> DONE([готово])
|
||||||
|
DSP -->|нет| Q{"mode == NORMAL?"}
|
||||||
|
Q -->|да| POS["этаж (FloorFontFallback), по центру"]
|
||||||
|
POS --> ARR{"direction UP/DOWN?"}
|
||||||
|
ARR -->|да| DRAWARR["примитив-стрелка"]
|
||||||
|
ARR -->|нет| DONE
|
||||||
|
DRAWARR --> DONE
|
||||||
|
Q -->|нет| LBL["метка режима (SystemFont)<br/>ПОЖАР/ПЕРЕГРУЗ/СЕРВИС/…"]
|
||||||
|
LBL --> LAD{"погрузка и lading_secs>0?"}
|
||||||
|
LAD -->|да| CNT["обратный отсчёт (крупно)"]
|
||||||
|
LAD -->|нет| DONE
|
||||||
|
CNT --> DONE
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Обычный режим** — номер этажа (`FloorFontFallback`) + стрелка вверх/вниз (примитив, без
|
||||||
|
спрайтов). Двойная стрелка (`SUL_DIR_DOUBLE`) в fallback не рисуется — это спрайтовая индикация
|
||||||
|
Фазы 4/5.
|
||||||
|
- **Спецрежим** — короткая текстовая метка системным шрифтом (`SystemFont`). Это **safe-mode**
|
||||||
|
индикация: богатая полноэкранная графика режимов (фон+спрайты) появится в Фазах 4–5. Для
|
||||||
|
временной погрузки дополнительно рисуется обратный отсчёт (`lading_secs`).
|
||||||
|
- **Диспетчерский вход** (§3.4) — та же safe-mode текстовая метка (переиспользует слот режимов:
|
||||||
|
`MODE_Y`/`SystemFont`), но проверяется ПЕРВЫМ, до `mode`. Слова временные, «ВЫЗОВ»/«ОТВЕТ».
|
||||||
|
|
||||||
|
Следующий этаж (`next`) в fallback **не** показывается — это элемент богатого layout (Фаза 5),
|
||||||
|
поэтому на `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 шрифта
|
||||||
|
|
||||||
|
Второй уровень деградации — на уровне отдельного символа. Активный шрифт может не покрывать
|
||||||
|
кодпойнт, который выдал декодер (напр. `FloorFontFallback` знает только `0–9`, `-`, пробел, а
|
||||||
|
позиция пришла кириллицей). `gfx_draw_string()` подставляет `-` вместо отсутствующего глифа —
|
||||||
|
по-символьно, не обрывая всю строку на первом неизвестном символе. Если даже `-` нет в шрифте —
|
||||||
|
символ пропускается (нулевая ширина).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
CH["символ строки"] --> HAS{"есть глиф<br/>в активном шрифте?"}
|
||||||
|
HAS -->|да| DRAW["нарисовать глиф"]
|
||||||
|
HAS -->|нет| SUB{"есть глиф '-'?"}
|
||||||
|
SUB -->|да| DASH["нарисовать '-'"]
|
||||||
|
SUB -->|нет| SKIP["пропустить (ширина 0)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Механизм есть с Фазы 1. Выделенная чистая функция проверки покрытия шрифта + её host-тест —
|
||||||
|
остаток Фазы 2 (см. [PLAN.md](../../firmware/tft_app/PLAN.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Когда перерисовываем
|
||||||
|
|
||||||
|
`ui_fallback_render()` перерисовывает кадр целиком (без dirty-rect — `gfx` минимален) при
|
||||||
|
изменении того, что fallback реально показывает: **позиция, стрелка или режим**
|
||||||
|
(`pos_pending || direction_pending || mode_pending`). Иначе — no-op. И этот путь, и
|
||||||
|
безусловный старт (ниже) принимают диспетчерский вход (§2.1) отдельным параметром — сам он
|
||||||
|
НЕ входит в `pos_pending`/`direction_pending`/`mode_pending` (это diff `sul_result_t`, opto —
|
||||||
|
локальный вход, другая природа изменения).
|
||||||
|
|
||||||
|
Первую отрисовку презентация делает **безусловно** один раз при старте
|
||||||
|
(`ui_fallback_render_initial()`): кэш контроллера засеян дефолтом, и если первый реальный кадр
|
||||||
|
совпадёт с дефолтом, diff придёт «ничего не изменилось» — без безусловного старта экран остался бы
|
||||||
|
пустым. Тот же безусловный вызов переиспользуется как ТРЕТЬЯ причина полной перерисовки (кроме
|
||||||
|
старта и закрытия меню) — смена диспетчерского входа САМА ПО СЕБЕ, без нового кадра СУЛ в
|
||||||
|
очереди: `render_task` (см. [TASKS.md §3](TASKS.md)) сравнивает текущее
|
||||||
|
`g_dispatcher_indication` с прошлым и, если отличается, зовёт `ui_fallback_render_initial()`
|
||||||
|
из последнего известного `sul_result_t` + нового диспетчерского значения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Как кадр попадает на экран (Фаза 3.2.4 — фундамент рендера)
|
||||||
|
|
||||||
|
Ограничение одного framebuffer'а Фазы 1 **снято**. `render()` рисует off-screen в
|
||||||
|
альфа-поверхность **AS** (ARGB8888); показ — отдельным вызовом `gfx_present()` у владельца
|
||||||
|
дисплея ([task_render.c](../../firmware/tft_app/src/app/task_render.c)): PXP компонует AS над
|
||||||
|
чёрным фоном PS и пишет **RGB565** в задний framebuffer, затем tear-free свап синхронно с
|
||||||
|
ELCDIF (`FRAME_DONE`). Гибрид bpp: рисование и AA — в полных 8 битах (AS), 565 — только на
|
||||||
|
самом выходе (сплошные цвета/текст квантуются незаметно; проверено на панели).
|
||||||
|
|
||||||
|
Индикация всегда рендерится **полным кадром** (в отличие от меню, где после открытия
|
||||||
|
перекомпоновывается только окно — см. [MENU.md](MENU.md) §5). Подробности компоновщика и
|
||||||
|
разбивка по задачам — [TASKS.md](TASKS.md).
|
||||||
216
docs/tft_app/MENU.md
Normal file
216
docs/tft_app/MENU.md
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
# tft-app — движок меню и связь с настройками
|
||||||
|
|
||||||
|
Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель; 3.2.2–3.2.4: рендер,
|
||||||
|
wiring, разделение на задачи) и его связь с модулем настроек `settings_store`. Проектная основа —
|
||||||
|
[ARCH.md §8](../../firmware/tft_app/ARCH.md); реализация —
|
||||||
|
[menu.c](../../firmware/tft_app/src/menu/src/menu.c),
|
||||||
|
[settings_store](../../firmware/tft_app/src/services/settings_store/).
|
||||||
|
|
||||||
|
Движущее требование (§8): клиент приносит уникальные настройки, и добавление их **не должно
|
||||||
|
требовать переписывания** движка. Отсюда два принципа: меню — **данные**, редакторы —
|
||||||
|
**подключаемые по типу**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Разделение слоёв
|
||||||
|
|
||||||
|
Модель меню — чистый C (host-тест), отделена от рендера (HIL) и от записи на флеш.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
BTN["bsp_button<br/>короткое BUTTON_1 (вход/next) / BUTTON_2 (action)"]
|
||||||
|
subgraph MODEL["menu (чистая модель, host-тест)"]
|
||||||
|
TREE["дерево-данные<br/>menu_item_desc_t[]"]
|
||||||
|
NAV["навигация + редактор<br/>menu_next / menu_action"]
|
||||||
|
end
|
||||||
|
SET["settings_t<br/>(RAM, get_mutable)"]
|
||||||
|
APP["app-слой<br/>(проверяет save_requested)"]
|
||||||
|
STORE["settings_store<br/>save() → QSPI 0x450000"]
|
||||||
|
REND["рендер меню<br/>окно 480×272 @ (0,0)"]
|
||||||
|
|
||||||
|
BTN --> NAV
|
||||||
|
TREE --> NAV
|
||||||
|
NAV -->|"мутация по offset"| SET
|
||||||
|
NAV -->|"save_requested"| APP
|
||||||
|
APP --> STORE
|
||||||
|
NAV -.запросы.-> REND
|
||||||
|
SET -.значения.-> REND
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключ: **модель сама не сохраняет и не рисует**. Она мутирует переданный `settings_t*` и на
|
||||||
|
выходе-с-сохранением выставляет `save_requested`; фактический `settings_store_save()` вызывает
|
||||||
|
app-слой. Это держит навигацию/редактирование host-тестируемыми без QSPI и без рендера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Меню — данные
|
||||||
|
|
||||||
|
Пункт меню — строка-дескриптор ([menu.h](../../firmware/tft_app/src/menu/include/menu/menu.h)):
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
const char *label;
|
||||||
|
menu_item_type_t type; /* редактор: SUBMENU/BACK/SELECT/BYTE/BOOL */
|
||||||
|
uint16_t value_offset; /* offsetof(settings_t, <uint8-поле>) */
|
||||||
|
uint8_t min, max; /* диапазон для SELECT/BYTE/BOOL */
|
||||||
|
uint8_t parent, first_child, last_child; /* дерево (плоский массив+индексы) */
|
||||||
|
} menu_item_desc_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
Дерево — плоский массив; уровень = непрерывный диапазон детей `[first_child..last_child]` одного
|
||||||
|
родителя. `items[0]` — корневое `SUBMENU`, его дети — верхний уровень.
|
||||||
|
|
||||||
|
**Добавить пункт = добавить строку** массива (тот же принцип, что `k_mode_priority[]`). Добавить
|
||||||
|
причудливый редактор = добавить значение в `menu_item_type_t` + ветку в `menu_action` — движок
|
||||||
|
навигации не меняется. Фаза 3 использует `SELECT/BYTE/BOOL`; `ARRAY/SERIAL/YEAR/PERCENT/BOOL_ARRAY`
|
||||||
|
придут со своими фазами (5/6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Навигация
|
||||||
|
|
||||||
|
Две кнопки, обе коротким нажатием (без удержания — раскладка `OLD_PROJECT_TFT8_UKL`): BUTTON_1 →
|
||||||
|
`menu_next` (следующий пункт уровня, с заворотом) когда меню открыто, **вход в меню** (app-слой,
|
||||||
|
`menu_open`) когда закрыто; BUTTON_2 → `menu_action` (по типу пункта) когда открыто, намеренный
|
||||||
|
no-op когда закрыто.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
START([menu_action на текущем пункте]) --> T{тип пункта}
|
||||||
|
T -->|SUBMENU| ENTER["курсор → first_child (вход)"]
|
||||||
|
T -->|"BACK, parent=корень"| EXIT["save_requested = dirty<br/>меню закрыто"]
|
||||||
|
T -->|"BACK, в подменю"| UP["курсор → пункт-подменю (возврат)"]
|
||||||
|
T -->|SELECT/BYTE/BOOL| EDIT["значение min..max с заворотом<br/>dirty = true"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`dirty` взводится любой правкой; при выходе из корня `save_requested = dirty` (сохраняем только
|
||||||
|
если что-то менялось). Отдельного «отменить» нет — правки живут в RAM, на флеш попадают лишь при
|
||||||
|
выходе-с-сохранением.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Связь с настройками
|
||||||
|
|
||||||
|
Модель привязана к `settings_t` (ядро настроек, [SETTINGS](../../firmware/tft_app/src/services/settings_store/include/services/settings_store.h))
|
||||||
|
через **байтовый offset** — читает/пишет `uint8`-поле по `value_offset`:
|
||||||
|
|
||||||
|
| Ярус настройки | Пример пункта | Привязка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **A** железобетонные | громкость, год | `offsetof(settings_t, user.<поле>)` |
|
||||||
|
| device/провиженинг | тумблер логов | `offsetof(settings_t, device.log_enabled)` |
|
||||||
|
| **B** протокольные | адрес НКУ | `offsetof(settings_t, user.proto_slice[0])` |
|
||||||
|
|
||||||
|
Ярус B (протокольные параметры) с Фазы 3.3 строится из дескриптора протокола
|
||||||
|
`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):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as Пользователь (кнопки)
|
||||||
|
participant M as menu (модель)
|
||||||
|
participant S as settings_t (RAM)
|
||||||
|
participant MT as menu_task
|
||||||
|
participant St as settings_store
|
||||||
|
|
||||||
|
U->>M: menu_action (правка / выход)
|
||||||
|
M->>S: запись поля по offset (dirty=true)
|
||||||
|
M-->>MT: open=false, save_requested=true
|
||||||
|
MT->>St: settings_store_save()
|
||||||
|
St->>St: serialize + CRC → QSPI 0x450000
|
||||||
|
```
|
||||||
|
|
||||||
|
Адрес станции (`nku_can_set_address()`) `sul_rx_task` пере-применяет из настроек сам, на следующей
|
||||||
|
итерации — не требует отдельного сигнала (декодер читает `settings_store_get()` каждую итерацию).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Рендер и wiring (Фаза 3.2.2–3.2.4)
|
||||||
|
|
||||||
|
Рендер ([menu_view.c](../../firmware/tft_app/src/ui/menu/src/menu_view.c)) — отдельный слой, читает
|
||||||
|
модель запросами (`menu_current`, `menu_level_range`, `menu_read_value`) и рисует в **фиксированном
|
||||||
|
окне 480×272 @ логич.(0,0)** — одинаково на всех панелях (на больших — левый-верхний угол, остальное
|
||||||
|
чёрное). Логики навигации не содержит.
|
||||||
|
|
||||||
|
**Раскладка** (под реальные шрифты: `SystemFont`/JBMono24 h=31, `SystemFontSmall`/JBMono12 h=16):
|
||||||
|
заголовок 36 + 6 строк × 36 + футер 20 = **272**, обрамление — тонкая серая рамка 1 px по периметру.
|
||||||
|
Заголовок = подпись текущего уровня; строки «подпись слева / значение справа»; **курсор — сплошная
|
||||||
|
полоса-заливка** (`gfx_fill_rect`) + белый текст; футер — легенда кнопок + «N/M».
|
||||||
|
|
||||||
|
**Цвет — тинтингом** (`gfx_draw_string(..., color)` с альфа-блендингом): один белый шрифт рисуется
|
||||||
|
любым цветом со сглаживанием, и оно корректно ложится на полосу-курсор. Значения SELECT/BOOL — из
|
||||||
|
`options[]` дескриптора, BYTE — числом.
|
||||||
|
|
||||||
|
**Оконный рендер** (double-buffer + PXP + гибрид bpp, Фаза 3.2.4): `menu_view_render()` рисует
|
||||||
|
off-screen в альфа-поверхность AS (ARGB8888) **только окно** `MENU_VIEW_WIN_W×H` (windowed clear +
|
||||||
|
отрисовка); показ — у владельца дисплея ([task_render.c](../../firmware/tft_app/src/app/task_render.c)):
|
||||||
|
|
||||||
|
- **открытие меню** — полная очистка AS (стереть индикацию вне окна) + **два** полных
|
||||||
|
`gfx_present()` подряд: из-за double buffering ОБА framebuffer'а обязаны получить корректный
|
||||||
|
кадр вне окна (контракт `gfx_present_rect`, см. gfx.h);
|
||||||
|
- **навигация/правка** — `gfx_present_rect(0,0,окно)`: PXP перекомпоновывает только 480×272
|
||||||
|
(~27% кадра) — пропорционально дешевле полного кадра.
|
||||||
|
|
||||||
|
Рисуем вне экрана, показываем атомарным свапом → tear-free. Компоновщик — `services/gfx`
|
||||||
|
(эталон `OLD_PROJECT_TFT8_UKL/source/display/`).
|
||||||
|
|
||||||
|
**Меню и рендер — РАЗНЫЕ задачи** ([task_menu.c](../../firmware/tft_app/src/app/task_menu.c) /
|
||||||
|
[task_render.c](../../firmware/tft_app/src/app/task_render.c)). Найдено на HW-верификации Фазы
|
||||||
|
3.2.4: в объединённой задаче (Фазы 3.2.1–3.2.3, один framebuffer, без ожиданий) блокировок не было,
|
||||||
|
разделение было безвредным упущением — но `gfx_present()` (double-buffer + PXP) внёс блокирующее
|
||||||
|
ожидание кадра, и в объединённой задаче это ожидание попутно блокировало вход в меню (ноль реакции
|
||||||
|
на кнопки). Эталон разделения — `OLD_PROJECT_TFT8_UKL`: `BUTTONS_TASK`/`menu_task` отдельно от
|
||||||
|
`REFRESH_TASK`/`tft_refresh_task`. Полная картина задач/приоритетов/взаимодействия —
|
||||||
|
[TASKS.md](TASKS.md).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
BTN["bsp_button<br/>софт-таймер 5 мс<br/>(debounce, независимо от обеих задач)"]
|
||||||
|
MT["menu_task<br/>модель + мгновенный вход/навигация + save<br/>НЕ рисует"]
|
||||||
|
RX["sul_rx_task<br/>CAN → decode → controller<br/>WDOG безусловно"]
|
||||||
|
RT["render_task<br/>ЕДИНСТВЕННЫЙ вызывающий gfx_present()<br/>event-driven"]
|
||||||
|
|
||||||
|
BTN --> MT
|
||||||
|
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
|
||||||
|
RX -->|"xQueueOverwrite (данные)<br/>+ xTaskNotifyGive (сигнал)"| RT
|
||||||
|
MT -.g_menu_active.-> RX
|
||||||
|
```
|
||||||
|
|
||||||
|
**Связь — MPSC.** Два продюсера (`sul_rx_task`, `menu_task`), один консюмер (`render_task`).
|
||||||
|
Данные (какой этаж/диф) идут только по плечу `sul_rx_task`→`render_task` — однослотовая
|
||||||
|
`xQueueOverwrite`-очередь (важно только последнее). Пробуждение — `xTaskNotifyGive()`/
|
||||||
|
`ulTaskNotifyTake(pdTRUE, portMAX_DELAY)` от ОБОИХ продюсеров: `render_task` не поллит, спит между
|
||||||
|
изменениями; несколько notify схлопываются в одно пробуждение (та же семантика «важно только
|
||||||
|
последнее»). Приоритет «меню важнее индикации» не кодируется в уведомлении — `render_task`,
|
||||||
|
проснувшись, всегда СНАЧАЛА проверяет `menu_is_open()`.
|
||||||
|
|
||||||
|
**Мягкая пауза `sul_rx_task` на время меню.** Пока меню открыто, `menu_task` держит
|
||||||
|
`g_menu_active=true`; `sul_rx_task` под этим флагом пропускает decode/controller/запись в очередь —
|
||||||
|
но WDOG/heartbeat кормятся БЕЗУСЛОВНО (вне флага), задача не suspend'ится. При выходе из меню
|
||||||
|
`render_task` (по признаку «меню только что закрылось») сразу перерисовывает последнее известное
|
||||||
|
состояние индикации, не дожидаясь свежего CAN-кадра.
|
||||||
|
|
||||||
|
**Ввод.** Опрос кнопок — **софт-таймер** (`input_poll_cb`, 5 мс; демон таймеров на высшем приоритете
|
||||||
|
в системе → нажатия не теряются, пока заняты остальные задачи; в Фазе 3.4 туда же
|
||||||
|
`bsp_opto_process()`). Раскладка — как в `OLD_PROJECT_TFT8_UKL`, оба нажатия короткие, без
|
||||||
|
удержания: BUTTON_1 = вход в меню (закрыто) / следующий пункт (открыто); BUTTON_2 = выбор/инкремент
|
||||||
|
(открыто), намеренный no-op (закрыто).
|
||||||
|
|
||||||
|
**Приоритеты задач** (`app_tasks.h` — единая точка правды, `tskIDLE_PRIORITY`-относительно):
|
||||||
|
`bringup_task` (одноразовая, самый высокий из четырёх) → `menu_task` → `render_task` → `sul_rx_task`
|
||||||
|
(самый низкий). **Важно:** `render_task` НАМЕРЕННО выше `sul_rx_task`, не наоборот — `bsp_can_receive()`
|
||||||
|
busy-spin без yield (`bsp/can/src/can.c`) занимает CPU весь `CAN_RX_TIMEOUT_MS` (100 мс) при
|
||||||
|
отсутствии трафика, и `xTaskDelayUntil()` в этом случае не блокирует вовсе (дедлайн уже в прошлом —
|
||||||
|
см. `sdk/rtos/freertos/freertos-kernel/tasks.c`), т.е. `sul_rx_task` не отдаёт CPU добровольно.
|
||||||
|
Если `sul_rx_task` окажется выше `render_task`, последняя будет голодать всё время отсутствия
|
||||||
|
CAN-трафика (найдено на HW-верификации — экран не обновлялся при старте без связи и при обрыве
|
||||||
|
связи; см. PLAN.md, Фаза 3.2.4). `menu_task` по-прежнему выше `render_task` — её PXP busy-wait
|
||||||
|
(~60-100 мс) не должен придерживать ввод. Логгер (`utils/log`) под FreeRTOS — с мьютексом
|
||||||
|
(`port/log/src/log_mutex.c`, до Фазы 3.2.4 был `#if 0` и не собирался — гонка на общем static-буфере
|
||||||
|
логгера между несколькими пишущими задачами).
|
||||||
103
docs/tft_app/MODE_PRIORITY.md
Normal file
103
docs/tft_app/MODE_PRIORITY.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# tft-app — приоритеты режимов
|
||||||
|
|
||||||
|
Как множество одновременных сигналов от СУЛ сворачивается в **один** экранный режим.
|
||||||
|
Проектная основа — [ARCH.md §7](../../firmware/tft_app/ARCH.md); реализация —
|
||||||
|
[mode_priority.c](../../firmware/tft_app/src/domain/controller/src/mode_priority.c).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Задача
|
||||||
|
|
||||||
|
Сигналы в `sul_result_t` **ортогональны** — могут быть активны одновременно (перегруз во время
|
||||||
|
сервиса, пожар при погрузке и т.п.). Но экран показывает **один** режим. Нужно правило: какой
|
||||||
|
режим побеждает, когда активны несколько.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph SIG["сигналы sul_result_t (булевы, независимы)"]
|
||||||
|
F["fireman"]
|
||||||
|
FA["fire_alarm"]
|
||||||
|
OV["overload"]
|
||||||
|
SE["seismic"]
|
||||||
|
MA["maintenance"]
|
||||||
|
LA["lading"]
|
||||||
|
end
|
||||||
|
SIG --> R["sul_resolve_mode()"]
|
||||||
|
R --> M["sul_mode_t<br/>(ровно один режим)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Таблица — это данные, а не код
|
||||||
|
|
||||||
|
Приоритет задан **порядком строк** таблицы-массива, а не значениями enum. Резолвер таблицу не
|
||||||
|
знает «в лицо» — читает булев сигнал по смещению поля (`offsetof`), что делает каждую строку
|
||||||
|
голыми данными:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct { size_t flag_offset; sul_mode_t mode; } mode_rule_t;
|
||||||
|
|
||||||
|
static const mode_rule_t k_mode_priority[] = {
|
||||||
|
{offsetof(sul_result_t, fireman), SUL_MODE_FIREMAN}, // высший
|
||||||
|
{offsetof(sul_result_t, fire_alarm), SUL_MODE_FIRE_ALARM},
|
||||||
|
{offsetof(sul_result_t, overload), SUL_MODE_OVERLOAD},
|
||||||
|
{offsetof(sul_result_t, seismic), SUL_MODE_SEISMIC},
|
||||||
|
{offsetof(sul_result_t, maintenance), SUL_MODE_MAINTENANCE},
|
||||||
|
{offsetof(sul_result_t, lading), SUL_MODE_LADING}, // низший спецрежим
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Согласованный порядок: **пожарный › пожар › перегруз › сейсмо › сервис › погрузка**. Если ни одна
|
||||||
|
строка не сработала — `SUL_MODE_NORMAL` (обычная индикация этажа/стрелки).
|
||||||
|
|
||||||
|
> ⚠️ Числовые значения `sul_mode_t` — **просто идентификаторы**, они не задают приоритет. Приоритет
|
||||||
|
> живёт только в порядке строк `k_mode_priority[]`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Резолвер
|
||||||
|
|
||||||
|
Проход таблицы сверху вниз; первая строка с активным сигналом выигрывает.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
START([sul_resolve_mode]) --> I["i = 0"]
|
||||||
|
I --> CHK{"строка i:<br/>сигнал по offset активен?"}
|
||||||
|
CHK -->|да| RET["вернуть mode строки i"]
|
||||||
|
CHK -->|нет| NEXT["i++"]
|
||||||
|
NEXT --> MORE{"строки ещё есть?"}
|
||||||
|
MORE -->|да| CHK
|
||||||
|
MORE -->|нет| NORM["вернуть SUL_MODE_NORMAL"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Резолвер вызывается из `controller_process()` дважды за кадр (для прошлого и нового состояния) —
|
||||||
|
их сравнение даёт `mode_pending`. Смена **сырого** сигнала, не меняющая разрешённый режим (напр.
|
||||||
|
добавился перегруз при уже активном пожаре), `mode_pending` не поднимает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Как менять
|
||||||
|
|
||||||
|
Всё — правки одного массива, резолвер не трогается:
|
||||||
|
|
||||||
|
- **Изменить приоритет** — переставить строки.
|
||||||
|
- **Добавить режим** — дописать строку: `offsetof` нового булева сигнала + его `sul_mode_t`
|
||||||
|
(сам сигнал добавляется в `sul_result_t` и заполняется декодером).
|
||||||
|
- **Убрать режим** — удалить строку (сигнал в модели может остаться, просто не влияет на экран).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Развязка от протокола и клиентская кастомизация
|
||||||
|
|
||||||
|
Таблица оперирует **каноническими** сигналами `sul_result_t`, а не битами НКУ-CAN. Обзор боевых
|
||||||
|
декодеров (УИМ/УКЛ/УЭЛ/SD7) подтвердил: набор сигналов один на все протоколы — различаются лишь
|
||||||
|
битовые кодировки (живут в каждом декодере). Поэтому одна таблица работает для любого протокола.
|
||||||
|
|
||||||
|
Приоритеты «потенциально клиентские» (§7): клиентская кастомизация = подмена массива
|
||||||
|
`k_mode_priority[]` (в перспективе — загружаемая таблица рядом с layout/конфигом), **без правок
|
||||||
|
кода** резолвера или декодеров.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Куда встраивается результат: `mode` едет в `indication_task_t` и читается презентацией при
|
||||||
|
`mode_pending` — см. [DOMAIN_DATAFLOW.md §5](DOMAIN_DATAFLOW.md) и [FALLBACK.md](FALLBACK.md).
|
||||||
BIN
docs/tft_app/MenuPromo.png
Normal file
BIN
docs/tft_app/MenuPromo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
33
docs/tft_app/README.md
Normal file
33
docs/tft_app/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# tft-app — документация домена
|
||||||
|
|
||||||
|
Dev-документация по **реализованному** слою `firmware/tft_app` (Фазы 1–3.2): как данные
|
||||||
|
СУЛ проходят от протокола до экрана, и как устроено приложение поверх FreeRTOS. Технические
|
||||||
|
доки «как это работает в коде», в дополнение к проектным ARCH/PLAN.
|
||||||
|
|
||||||
|
| Документ | О чём |
|
||||||
|
| --- | --- |
|
||||||
|
| [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) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. |
|
||||||
|
| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, диспетчерский вход (высший приоритет, opto), по-символьный fallback шрифта, триггеры перерисовки, путь кадра через компоновщик. |
|
||||||
|
| [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-взаимодействие, мягкая пауза, разделяемое состояние. |
|
||||||
|
|
||||||
|
## Связанные документы
|
||||||
|
|
||||||
|
- [ARCH.md](../../firmware/tft_app/ARCH.md) — архитектура и проектное обоснование (источник истины по дизайну).
|
||||||
|
- [PLAN.md](../../firmware/tft_app/PLAN.md) — статус фаз разработки (источник истины по статусу).
|
||||||
|
- [DEV_ARCH.md](../DEV_ARCH.md) — устройство репозитория и сборки.
|
||||||
|
- [bsp/display/README.md](../../bsp/display/README.md) — драйвер ELCDIF (два формата пикселя: XRGB8888/RGB565).
|
||||||
|
|
||||||
|
## Границы
|
||||||
|
|
||||||
|
Описаны домен, fallback-презентация (+ диспетчерский вход, Фаза 3.4), ядро настроек
|
||||||
|
(`settings_store` + состав/ярусы/QSPI-карта, [SETTINGS.md](SETTINGS.md)), меню (модель + оконный
|
||||||
|
рендер + задачи), фундамент рендера (double-buffer + PXP + гибрид bpp, Фаза 3.2.4), per-protocol
|
||||||
|
дескрипторы + реестр из двух протоколов (Фаза 3.3), удалённая адресация НКУ-CAN (Фаза 3.5),
|
||||||
|
рантайм-тумблер логов (Фаза 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.
|
||||||
171
docs/tft_app/TASKS.md
Normal file
171
docs/tft_app/TASKS.md
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
# tft-app — задачи FreeRTOS и их взаимодействие
|
||||||
|
|
||||||
|
Документ описывает **реализованную** (Фаза 3.2.4) многозадачную структуру `tft_app`: какие
|
||||||
|
задачи существуют, кто кого создаёт, как они обмениваются данными и почему приоритеты именно
|
||||||
|
такие. Контракт между задачами — [app_tasks.h](../../firmware/tft_app/src/app/app_tasks.h)
|
||||||
|
(единственный источник истины по приоритетам/разделяемому состоянию); тела задач —
|
||||||
|
`firmware/tft_app/src/app/task_*.c`.
|
||||||
|
|
||||||
|
Структура выстрадана на HW-верификации (два боевых регресса — см. PLAN.md, Фаза 3.2.4):
|
||||||
|
объединение меню и рендера в одну задачу блокировало ввод, а неверный относительный приоритет
|
||||||
|
`sul_rx`/`render` морозил экран при отсутствии CAN-трафика. Эталон разделения —
|
||||||
|
`OLD_PROJECT_TFT8_UKL` (`BUTTONS_TASK` отдельно от `REFRESH_TASK`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Состав
|
||||||
|
|
||||||
|
| Задача | Файл | Приоритет (`app_tasks.h`) | Роль | Блокировки |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `bringup_task` | task_bringup.c | `+4` (высший, недолгоживущая) | одноразовая инициализация → создаёт три остальные → `vTaskDelete(NULL)` | QSPI/flash-операции |
|
||||||
|
| `menu_task` | task_menu.c | `+3` | модель меню: кнопки, вход/навигация/правка/сохранение. **НЕ рисует** | `settings_store_save()` (flash) на выходе из меню |
|
||||||
|
| `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 мс без трафика |
|
||||||
|
| — демон таймеров | (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 делает
|
||||||
|
сам `bringup_task`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Старт системы
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant M as main()
|
||||||
|
participant B as bringup_task (+4)
|
||||||
|
participant R as render_task (+2)
|
||||||
|
participant S as sul_rx_task (+1)
|
||||||
|
participant U as menu_task (+3)
|
||||||
|
|
||||||
|
M->>M: board_hw_init, очередь, таймер ввода
|
||||||
|
M->>B: xTaskCreate + vTaskStartScheduler
|
||||||
|
B->>B: log_mutex → UART/лог → QSPI → settings → self-confirm
|
||||||
|
B->>B: SDRAM → gfx (PXP+ELCDIF) → CAN
|
||||||
|
B->>B: g_display_ready = true
|
||||||
|
B->>R: xTaskCreate (хэндл → g_render_task_handle)
|
||||||
|
B->>S: xTaskCreate
|
||||||
|
B->>U: xTaskCreate
|
||||||
|
B->>B: vTaskDelete(NULL)
|
||||||
|
R->>R: первый кадр («--») + gfx_present()
|
||||||
|
```
|
||||||
|
|
||||||
|
Порядок создания (render первым) документирует зависимость: его хэндл нужен продюсерам для
|
||||||
|
`xTaskNotifyGive`. Формально гонки нет — `bringup_task` выше всех по приоритету и монополизирует
|
||||||
|
CPU, пока не создаст всех троих.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Взаимодействие (MPSC)
|
||||||
|
|
||||||
|
Три продюсера, один консюмер (третий — dispatcher opto, §3.4). **Данные** и **сигнал
|
||||||
|
пробуждения** разделены:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
TMR["демон таймеров<br/>input_poll_cb 5 мс<br/>bsp_button_poll + bsp_opto_process"]
|
||||||
|
MT["menu_task (+3)<br/>модель меню g_menu"]
|
||||||
|
RX["sul_rx_task (+1)<br/>CAN→decode→controller<br/>WDOG безусловно"]
|
||||||
|
RT["render_task (+2)<br/>gfx_present*()"]
|
||||||
|
DSP["dispatcher_poll()<br/>(app/dispatcher.c)<br/>g_dispatcher_indication"]
|
||||||
|
|
||||||
|
TMR -."залатанные события кнопок".-> MT
|
||||||
|
TMR -."безусловный опрос, каждый тик".-> DSP
|
||||||
|
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
|
||||||
|
RX -->|"xQueueOverwrite (render_msg_t)<br/>+ xTaskNotifyGive"| RT
|
||||||
|
DSP -->|"xTaskNotifyGive<br/>(изменение вызов/ответ)"| RT
|
||||||
|
MT -."g_menu_active (мягкая пауза)".-> RX
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Очередь `g_render_queue`** (глубина 1, `xQueueOverwrite`) — только плечо
|
||||||
|
`sul_rx→render`, несёт `render_msg_t` (diff + результат). Семантика «важно только
|
||||||
|
последнее состояние»: рендер не обязан успевать за каждым кадром CAN. У dispatcher СВОЕЙ
|
||||||
|
очереди нет — состояние (`g_dispatcher_indication`) не история, читается напрямую (как
|
||||||
|
`g_menu`), очередь тут не нужна (сравнение с прошлым значением — внутри `render_task`).
|
||||||
|
- **`ulTaskNotifyTake(pdTRUE, portMAX_DELAY)`** в `render_task` — event-driven, без
|
||||||
|
поллинга; несколько notify от ЛЮБОГО из трёх продюсеров схлопываются в одно пробуждение
|
||||||
|
(та же семантика «важно только последнее»). `render_task` не различает, КТО его разбудил —
|
||||||
|
каждую итерацию просто проверяет все три источника состояния заново.
|
||||||
|
- **Приоритет «меню важнее индикации» не кодируется в уведомлении**: проснувшись,
|
||||||
|
`render_task` первым делом проверяет `menu_is_open(&g_menu)` — если меню открыто,
|
||||||
|
очередь индикации и dispatcher даже не читаются (см. §3.1 ниже — dispatcher копится, не
|
||||||
|
теряется).
|
||||||
|
- **Модель меню `g_menu`** мутирует только `menu_task`; `render_task` читает её для
|
||||||
|
отрисовки после 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` на время меню
|
||||||
|
|
||||||
|
`menu_task` держит `g_menu_active=true`, пока меню открыто; `sul_rx_task` под этим флагом
|
||||||
|
пропускает CAN-работу (decode/controller/очередь), но **WDOG/heartbeat кормит безусловно** —
|
||||||
|
задача не suspend'ится, поэтому сторожевой таймер в безопасности по конструкции, что бы ни
|
||||||
|
происходило с меню. Флаг обновляется **до** `settings_store_save()` (flash-запись небыстрая —
|
||||||
|
иначе пауза держалась бы дольше нужного). На закрытие меню `render_task` немедленно
|
||||||
|
восстанавливает индикацию из последнего известного состояния, не дожидаясь свежего CAN-кадра.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Почему приоритеты именно такие
|
||||||
|
|
||||||
|
`menu (+3) > render (+2) > sul_rx (+1)` — оба соотношения выстраданы на железе:
|
||||||
|
|
||||||
|
1. **`render` ВЫШЕ `sul_rx`.** `bsp_can_receive()` — busy-spin без единого блокирующего
|
||||||
|
FreeRTOS-вызова: без CAN-трафика `sul_rx_task` занимает CPU весь таймаут (100 мс) каждую
|
||||||
|
итерацию, и `xTaskDelayUntil()` при просроченном дедлайне не блокирует вовсе — задача
|
||||||
|
непрерывно READY. Более низкоприоритетный `render_task` в этой ситуации голодал:
|
||||||
|
пустой экран при старте без связи, залипание индикации при обрыве. Обратный порядок
|
||||||
|
приоритетов чинит это, не трогая общий bare-metal модуль `bsp_can`.
|
||||||
|
2. **`menu` ВЫШЕ `render`.** Блокировки рендера (PXP busy-wait + ожидание кадра) не должны
|
||||||
|
придерживать обработку кнопок — то самое свойство, ради которого меню и рендер разведены
|
||||||
|
по задачам (в объединённой задаче ввод был мёртв).
|
||||||
|
3. **`bringup` выше всех** — монополизирует CPU на время одноразовой инициализации.
|
||||||
|
4. **Демон таймеров — наивысший в системе** (`configTIMER_TASK_PRIORITY`): debounce-сэмплы
|
||||||
|
не теряются, чем бы ни были заняты остальные.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Разделяемое состояние (`app_tasks.h`)
|
||||||
|
|
||||||
|
| Объект | Пишет | Читает | Синхронизация |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `g_render_queue` | sul_rx | render | FreeRTOS queue |
|
||||||
|
| `g_render_task_handle` | bringup (до создания продюсеров) | sul_rx, menu | создание-до-использования |
|
||||||
|
| `g_display_ready` | bringup | render, sul_rx | `volatile bool`, одно-writer |
|
||||||
|
| `g_menu_active` | menu | sul_rx | `volatile bool`, одно-writer |
|
||||||
|
| `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`, гонка) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Кадр на экран (сводка)
|
||||||
|
|
||||||
|
Рендер-пайплайн (детали — [FALLBACK.md](FALLBACK.md) §5, [MENU.md](MENU.md) §5): CPU рисует в
|
||||||
|
AS (ARGB8888) → `gfx_present*()` в `render_task` = PXP-композит AS над чёрным PS → задний
|
||||||
|
framebuffer **RGB565** → tear-free свап по FRAME_DONE. Гибрид bpp держит сканаут ELCDIF на
|
||||||
|
половинной полосе SDRAM (63 МБ/с вместо 126). Индикация — всегда полный кадр (~31 мс PXP);
|
||||||
|
меню после открытия — только окно 480×272 (~8 мс расчётно).
|
||||||
308
firmware/tft_app/ARCH.md
Normal file
308
firmware/tft_app/ARCH.md
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
# tft-app — архитектура
|
||||||
|
|
||||||
|
> Прошивка лифтового индикатора для семейства **MIMXRT1052 TFT (4 / 7 / 8 / 10")**.
|
||||||
|
> Документ — живой источник истины по слоистой архитектуре. Статус фаз разработки —
|
||||||
|
> в [PLAN.md](PLAN.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть I. Требования (бизнес-логика)
|
||||||
|
|
||||||
|
### 1. Основные функции
|
||||||
|
|
||||||
|
**1.1 Индикация в реальном времени.** Местоположение кабины, спецрежимы, музыкальное
|
||||||
|
сопровождение при движении, озвучка сигналов.
|
||||||
|
|
||||||
|
- Сигналы от СУЛ приходят по последовательному интерфейсу (+24 В UART, кастомный
|
||||||
|
бинарный протокол) либо по CAN.
|
||||||
|
- Отображение состояния двух диспетчерских оптовходов: «Вызов подан» / «Вызов принят» / пусто.
|
||||||
|
|
||||||
|
**1.2 Обновление ПО в поле** через microSD (сам образ прошивки — задача bootloader,
|
||||||
|
A/B Direct-XIP; ассеты/layout — задача tft-app, см. §12).
|
||||||
|
|
||||||
|
**1.3 Графический интерфейс настроек** в рантайме (меню, навигация двумя кнопками).
|
||||||
|
|
||||||
|
### 2. Частности
|
||||||
|
|
||||||
|
**2.1 Отображение.** 4 типа дисплея (TFT4/7/8/10); одна кодовая база, две сборки
|
||||||
|
(см. §9). Ассеты (иконки, картинки, звук) — на встроенной флеш, обновляются с microSD.
|
||||||
|
Шрифты — C-массивы ([LCD Image Converter](https://lcd-image-converter.riuson.com/)).
|
||||||
|
Макеты UI унифицированы и упрощены; по запросу клиента — кастом.
|
||||||
|
|
||||||
|
**2.2 Связь с СУЛ.** Общий программный интерфейс ко всем СУЛ; одна прошивка — множество
|
||||||
|
протоколов; выбор протокола через меню. Протоколы: УЭЛ, УКЛ, НКУ-CAN, НКУ-SD7, УИМ (список
|
||||||
|
открыт).
|
||||||
|
|
||||||
|
**2.3 Настройки.** Общий набор параметров + кастом под заказ. Настройки влияют на UI и на
|
||||||
|
работу протокола (напр. адрес индикатора: 0..15 у НКУ-CAN, 1..50 у УИМ) — все связи
|
||||||
|
предусмотрены (см. §6, §8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть II. Архитектура
|
||||||
|
|
||||||
|
### 3. Принципы
|
||||||
|
|
||||||
|
1. **Домен не знает железа.** Декодеры протоколов и доменная модель — чистый C без единого
|
||||||
|
HAL-вызова, тестируются на хосте (Unity + fff) тем же способом, что модули bootloader.
|
||||||
|
2. **Одна прошивка — много протоколов.** Протокол выбирается в рантайме из настроек через
|
||||||
|
реестр драйверов (§6). Новый протокол = реализовать декодер + зарегистрировать, без правок
|
||||||
|
в остальных слоях.
|
||||||
|
3. **Деградация, а не отказ.** Любой сбой ассетов/layout/связи ведёт к безопасному упрощению
|
||||||
|
индикации (§7, §11), устройство не «кирпичится» и не гаснет.
|
||||||
|
4. **Данные, а не код.** Макет, каталог ассетов, таблица приоритетов режимов, дескрипторы
|
||||||
|
настроек протоколов — данные (компилируемые или загружаемые), не разветвления в коде.
|
||||||
|
5. **Границы модулей = конвенции репозитория.** Каждый слой — статическая библиотека CMake,
|
||||||
|
публичные заголовки в `include/`, приватное в `src/`. Аппаратные модули — через `bsp/*`.
|
||||||
|
|
||||||
|
### 4. Слои
|
||||||
|
|
||||||
|
```bash
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ app/ FreeRTOS-задачи, wiring, main (firmware/tft_app)│ L4
|
||||||
|
├──────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ui/ view-model → экран (layout-движок, темы, fallback-рендер) │
|
||||||
|
│ menu/ экран настроек, навигация 2 кнопки │ L3 презентация
|
||||||
|
│ audio_policy доменное событие → команды звука │
|
||||||
|
├──────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ controller редьюсер: sul_result_t (+кэш) → indication_task (diff) │
|
||||||
|
│ sul/ реестр драйверов; драйвер = decoder(pure) + transport(hw) │ L2 ДОМЕН
|
||||||
|
│ elevator_model канонический sul_result_t, приоритеты режимов │ (чистый C, host-тесты)
|
||||||
|
├──────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ gfx (compositor+PXP+fonts) audio_engine (WAV/playlist над bsp_mqs) │
|
||||||
|
│ assets (TLV-ридер, image cache) settings_store fs (FatFS SD/QSPI) │ L1 сервисы (адаптеры к железу)
|
||||||
|
├──────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ bsp/* can uart_host opto button mqs sd qspi_flash sdram display led … │ L0 БОЕВОЙ, не трогаем
|
||||||
|
└──────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Поток данных** (адаптирован из проекта `special`, поверх FreeRTOS):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
транспорт (CAN/UART) ─► sul_driver.transport ─► sul_driver.decode(frame) ─► sul_result_t
|
||||||
|
│ poll + timeout→default
|
||||||
|
▼
|
||||||
|
controller.process(sul_result_t) ─► indication_task (dirty-флаги)
|
||||||
|
│
|
||||||
|
┌────────────────────────────────────────────────┬─────────┘
|
||||||
|
▼ ▼
|
||||||
|
ui.render(task, model, settings) audio_policy(task, model, settings)
|
||||||
|
(+ локальные входы: opto диспетчер, меню) │
|
||||||
|
audio_engine (bsp_mqs)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Размещение и тестируемость
|
||||||
|
|
||||||
|
| Слой | Где живёт | Тип | Host-тест |
|
||||||
|
| ------------------------------------------------------------------------------- | -------------------------------------------------- | ----------- | --------------------------- |
|
||||||
|
| `elevator_model`, `sul` (декодеры), `controller`, `audio_policy`, layout-солвер | `firmware/tft_app/src/domain/*`, `.../ui/layout/*` | чистый C | **да** (golden-векторы) |
|
||||||
|
| `sul` транспорт-адаптеры | `firmware/tft_app/src/domain/sul/transport/*` | HW | HIL |
|
||||||
|
| `gfx`, `audio_engine`, `assets`, `settings_store`, `fs` | `firmware/tft_app/src/services/*` | HW/адаптеры | частично (парсеры/TLV — да) |
|
||||||
|
| `ui`, `menu`, `app` | `firmware/tft_app/src/{ui,menu,app}/*` | app | вид/меню — HIL |
|
||||||
|
|
||||||
|
Host-тесты — `tests/host/tft_app_*` (Unity + fff, компилируем `.c` домена против моков bsp,
|
||||||
|
по образцу `tests/host/mcuboot_port`, `tests/host/protocol`).
|
||||||
|
|
||||||
|
### 6. Связь с СУЛ (`sul`)
|
||||||
|
|
||||||
|
**Канонический выход любого протокола** — `sul_result_t` (надмножество; простой протокол не
|
||||||
|
заполняет лишнее):
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define SUL_POS_MAX 4 /* сейчас значимы 2; запас до 4 */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char pos[SUL_POS_MAX * 4 + 1]; /* UTF-8, позиция кабины (напр. "12","-1","П") */
|
||||||
|
char next[SUL_POS_MAX * 4 + 1];/* UTF-8, следующий этаж (или пусто) */
|
||||||
|
uint8_t direction; /* none / up / down / double */
|
||||||
|
/* Ортогональные сигналы (могут сосуществовать; приоритет разрешает controller): */
|
||||||
|
bool arrival; /* гонг */
|
||||||
|
bool movement; /* начало движения */
|
||||||
|
bool overload; /* перегруз */
|
||||||
|
bool fire_alarm; /* пожарная тревога */
|
||||||
|
bool lading; /* погрузка */
|
||||||
|
bool maintenance; /* сервисный режим */
|
||||||
|
bool fireman; /* режим пожарного */
|
||||||
|
bool seismic; /* сейсмоопасность */
|
||||||
|
bool error; /* авария */
|
||||||
|
uint16_t lading_secs; /* обратный отсчёт погрузки, 0 = нет */
|
||||||
|
uint8_t floor_num; /* производный числовой этаж для озвучки (0 = н/д) */
|
||||||
|
} sul_result_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Позиция — UTF-8 строка** (у нас реальные шрифты ASCII + кириллица), а не число:
|
||||||
|
универсально для всех протоколов, host-тест сравнивает строки. Декодер не знает про шрифт.
|
||||||
|
- **Escape-hatch под кастом не закладываем** (YAGNI); совместимость обеспечивает версия схемы.
|
||||||
|
- **Валидация рендеримости** (§11): декодер может выдать кодпойнт вне покрытия активного
|
||||||
|
шрифта (кастомные коды протокола) → по-символьный fallback на этапе рендера.
|
||||||
|
|
||||||
|
**Драйвер = чистый декодер + транспорт-адаптер:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct { const uint8_t *data; uint16_t len; uint32_t id; uint8_t bus; } sul_frame_t;
|
||||||
|
|
||||||
|
/* Чистая функция — без железа, host-тестируется golden-векторами. */
|
||||||
|
typedef sul_status_t (*sul_decode_fn)(void *ctx, const sul_frame_t *frame, sul_result_t *out);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t id; /* идентификатор протокола (стабильный) */
|
||||||
|
const char *name; /* для меню */
|
||||||
|
sul_decode_fn decode; /* pure */
|
||||||
|
const sul_settings_desc_t *settings; /* per-protocol параметры (§8) */
|
||||||
|
/* транспорт (CAN/UART) — отдельный тонкий адаптер, привязан к драйверу */
|
||||||
|
} sul_driver_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Реестр** `sul_registry[]` — таблица драйверов по `id`. Активный выбирается из настроек.
|
||||||
|
Добавление протокола = запись в таблицу.
|
||||||
|
- **Poll + timeout.** `sul` опрашивается периодически; при отсутствии кадров дольше таймаута
|
||||||
|
выдаётся `default` (потеря связи → сброс режимного состояния), как в `special`/OLD_PROJECT.
|
||||||
|
|
||||||
|
### 7. Контроллер и приоритеты режимов
|
||||||
|
|
||||||
|
`controller.process(sul_result_t*) → indication_task_t` (dirty-флаги: `pos_pending`,
|
||||||
|
`direction_pending`, `mode_pending`, `arrival_pending`, …) — презентация перерисовывает и
|
||||||
|
озвучивает только изменившееся (кэш прошлого состояния внутри контроллера).
|
||||||
|
|
||||||
|
- Ортогональные булевы сигналы `sul_result_t` сходятся в **один экранный режим** через
|
||||||
|
**таблицу приоритетов** (пожар > перегруз > сейсмо > сервис > … > норма).
|
||||||
|
- **Таблица приоритетов — данные, потенциально клиентские**, живёт рядом с клиентским
|
||||||
|
конфигом/layout, а не хардкодом в контроллере. Контроллер применяет активную таблицу.
|
||||||
|
|
||||||
|
### 8. Настройки
|
||||||
|
|
||||||
|
Три раздельных источника (не смешивать):
|
||||||
|
|
||||||
|
1. **`sul_result_t`** — только данные от СУЛ (§6).
|
||||||
|
2. **`settings`** — конфиг устройства/пользователя: громкости, лого, серийник, ёмкость, год,
|
||||||
|
выбранный протокол, панель (provisioning). Хранится на QSPI (сектор настроек), формат с
|
||||||
|
магиком/версией/CRC (развитие `settings_manager` из OLD_PROJECT).
|
||||||
|
3. **Локальные входы** — оптовходы (диспетчерский вызов/ответ) и кнопки/меню; вливаются на
|
||||||
|
уровне контроллера/презентации, не часть протокольных данных.
|
||||||
|
|
||||||
|
**Per-protocol настройки.** Каждый протокол регистрирует `sul_settings_desc_t` — какие у него
|
||||||
|
параметры, диапазоны, подписи для меню (напр. адрес 0..15 у НКУ-CAN, 1..50 у УИМ). `settings`
|
||||||
|
держит слайс под активный протокол; `driver` получает свой конфиг; меню строится из дескриптора
|
||||||
|
(развитие `settings_mgr_bind_menu`). Общие настройки — отдельно от протокольных.
|
||||||
|
|
||||||
|
### 9. Дисплеи и платы
|
||||||
|
|
||||||
|
Платы отличаются **только RGB-интерфейсом LCDIF** (TFT4 — 40pin без пинов ориентации; TFT7/8/10
|
||||||
|
— 50pin с U/D·L/R). SDRAM, SEMC, периферия — одинаковы. Панели TFT7/8/10 уже разведены рантаймом
|
||||||
|
в `bsp_display` (таблица `panel_config[]`: тайминги, клок, `has_orientation_pins`).
|
||||||
|
|
||||||
|
| Профиль сборки | LCDIF | Панель | Клок |
|
||||||
|
| -------------- | --------------------- | ---------------------------------------- | --------- |
|
||||||
|
| `app-tft4` | 40pin, без ориентации | TFT4 (фикс) | Video PLL |
|
||||||
|
| `app-big` | 50pin, с U/D·L/R | 7 / 8 / 10 — **рантайм** из provisioning | PLL2 |
|
||||||
|
|
||||||
|
Различие изолировано в одном board-файле пин-мукса LCDIF. **Тип панели — provisioning-параметр**
|
||||||
|
(пишется service_tui), не пользовательская настройка. Матрица сборки — два buildPreset
|
||||||
|
(`app-tft4`, `app-big`), как `bootloader`/`firmware-test`.
|
||||||
|
|
||||||
|
### 10. Карта QSPI (размер-независимая)
|
||||||
|
|
||||||
|
Прошивка работает на разных QSPI NOR (W25Q128 16 МБ / 256 / 512 …). Поэтому **все
|
||||||
|
критичные регионы — по фиксированным смещениям** (компайл-тайм, без рантайм-детекта), а
|
||||||
|
**размер-зависим только регион ассетов**: он стартует с фиксированного адреса и тянется до
|
||||||
|
конца чипа (`длина = ёмкость − assets_start`; ёмкость — по JEDEC-ID).
|
||||||
|
|
||||||
|
| Регион | Смещение | Размер | Владелец |
|
||||||
|
| ----------------- | ---------- | ----------------- | ---------- |
|
||||||
|
| bootloader | `0x000000` | 256 КБ | bootloader |
|
||||||
|
| slot A (tft_app) | `0x040000` | 2 МБ | MCUboot |
|
||||||
|
| slot Б (tft_app) | `0x240000` | 2 МБ | MCUboot |
|
||||||
|
| **layout-регион** | `0x440000` | 64 КБ | tft_app |
|
||||||
|
| **settings** | `0x450000` | 8 КБ (2 сектора) | tft_app |
|
||||||
|
| **assets-регион** | `0x452000` | остаток до конца | tft_app |
|
||||||
|
|
||||||
|
- **settings — фиксированный `0x450000`, размер-независимо** (общий адрес для app и
|
||||||
|
service_tui, без вычислений из ёмкости). Отведено 2 сектора: первый рабочий, второй — задел
|
||||||
|
под power-safe ping-pong (erase+write настроек не атомарен; ping-pong защищает от обрыва
|
||||||
|
питания). Клиентский UX-зоопарк (ярус C настроек, §8) хранится в TLV в **layout-регионе**,
|
||||||
|
не здесь.
|
||||||
|
- **assets — остаток до конца чипа**: единственный размер-зависимый регион; больше флеш =
|
||||||
|
больше места под ассеты, без правок карты.
|
||||||
|
|
||||||
|
layout-, settings- и assets-регионы — **вне** flash-area загрузчика (bootutil про них не знает).
|
||||||
|
Разметка фиксируется в **едином partition-заголовке**, из которого читают app и генератор для
|
||||||
|
service_tui.
|
||||||
|
|
||||||
|
### 11. Ассеты, layout и fallback
|
||||||
|
|
||||||
|
**Разделяем два артефакта** вместо монолитного `style.img`:
|
||||||
|
|
||||||
|
- **layout** — декларативная таблица виджетов (что рисуем: позиция/стрелка/режим/лого/…, якорь,
|
||||||
|
размер в %/единицах, шрифт, привязка к полю модели). Якорное позиционирование → один макет
|
||||||
|
раскладывается под 480×272 и под 1024×600. Формат — **TLV**, схема версионируется.
|
||||||
|
Layout-солвер (якорь→пиксели) — чистый C, host-тест (golden-render). Виджеты бывают
|
||||||
|
**примитивные** (text/rect/line/arrow) и **спрайтовые** (sprite-by-id) — единый движок.
|
||||||
|
- **assets** — упакованный индексированный **TLV-бандл** (host-утилита `tools/`): PNG → сырой
|
||||||
|
ARGB8888 на этапе пака (в рантайме нет lodepng и boot-time декодирования; XIP-mmap с FlexSPI,
|
||||||
|
блит без копии). Звук — WAV в том же бандле.
|
||||||
|
|
||||||
|
**Загрузка layout при старте:** сначала layout-регион QSPI (магик+версия схемы+CRC+совместимость);
|
||||||
|
если валиден — берём его; иначе — **встроенный default**.
|
||||||
|
|
||||||
|
- **Модель 1 (baked):** нужный layout вкомпилен как default в app-слот → цельный подписанный
|
||||||
|
образ, тестируется целиком, деплой через bootloader A/B. layout-регион пуст.
|
||||||
|
- **Модель 2 (injected):** базовый бинарь; service_tui пишет клиентский layout-блоб в регион
|
||||||
|
(вне слота → app-обновление его не трогает). Пересборка не нужна. Валидация блоба — на host
|
||||||
|
(схема + golden-render), поэтому проверка клиентского layout не требует сборки клиентского
|
||||||
|
бинаря.
|
||||||
|
|
||||||
|
**Fallback (safe-mode).** `default`-layout — **asset-free и FS-free**: не монтирует QSPI-FAT/SD,
|
||||||
|
не трогает TLV-ридер. Зависимости — только `bsp_display` + `bsp_can`/`sul` + домен +
|
||||||
|
один вкомпилированный шрифт + примитивы `gfx`. Рисует **только этаж + стрелки (вверх/вниз)**,
|
||||||
|
чёрный фон, без звука. Это же — первый экран walking-skeleton (§PLAN, Фаза 1).
|
||||||
|
|
||||||
|
**Деградация — по-виджетно.** layout битый → полный fallback. layout валиден, но ассет
|
||||||
|
отсутствует/битый → рисуем **примитивную форму этого виджета**, а не полный откат. Символ вне
|
||||||
|
покрытия шрифта → по-символьный fallback. Решение защёлкивается на старте (при XIP-mmap рантайм
|
||||||
|
не ревалидирует — иначе битое чтение = hardfault); валидация ассетов — CRC в индексе TLV.
|
||||||
|
|
||||||
|
### 12. Обновление в поле
|
||||||
|
|
||||||
|
- **Ассеты/layout — app-side updater** (доменные данные; app знает схему и валидацию;
|
||||||
|
переиспользуем `bsp_qspi` + FatFS-на-SD, как `update_style_task` в OLD_PROJECT). Запись —
|
||||||
|
потоковая, erase-before-write по 4 КБ, **заголовок-валидатор пишется последним** (торн-запись
|
||||||
|
→ регион невалиден → безопасный fallback, не кирпич).
|
||||||
|
- **Provisioning-заливка** (service_tui, заводской/сервисный контекст): канал — USB-CDC
|
||||||
|
загрузчика или USB-SDP ROM, **при условии приемлемой скорости**. Если заливка ассетов этим
|
||||||
|
путём окажется слишком медленной — отказываемся и делаем **только через microSD**. Решение —
|
||||||
|
по замеру на Фазе обновлений (§PLAN).
|
||||||
|
|
||||||
|
### 13. Тестирование
|
||||||
|
|
||||||
|
- **Host (Unity + fff):** декодеры всех протоколов (golden-векторы кадров → `sul_result_t`),
|
||||||
|
контроллер (diff + приоритеты), layout-солвер (golden-render), TLV-ридер/паковщик, парсеры
|
||||||
|
настроек, валидация рендеримости.
|
||||||
|
- **HIL:** gfx/PXP/ELCDIF, audio/MQS, SD/QSPI, реальные транспорты CAN/UART (по образцу
|
||||||
|
`tests/target/*` и `06_test_firmware_*`).
|
||||||
|
|
||||||
|
### 14. Errata-вотчлист (IMXRT1050 Chip Errata Rev. 2.1)
|
||||||
|
|
||||||
|
> Риски кремния, привязанные к фазам. Многие пункты — **только для ревизии A0**
|
||||||
|
> (исправлены в A1) → на A1-кремнии неактуальны. **Открытый вопрос: уточнить ревизию
|
||||||
|
> кремния стендовых плат (A0/A1)** — определяет применимость половины списка. Источник —
|
||||||
|
> `DOCS/MANUALS/ERRATA.pdf` (IMXRT1050CE, Rev. 2.1, 08/2019).
|
||||||
|
|
||||||
|
| Errata | Суть | Фаза | Ревизия |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **ERR011138** | LCDIF: последовательное программирование LUT может сбоить, если две записи идут близко. | 4/9 — актуально **только если** используем LUT ELCDIF для Index8BPP-фреймбуфера (экономия RAM ~4× vs ARGB8888). | A0, исправлено в A1 |
|
||||||
|
| **ERR011207** | FlexSPI: при `FLEXSPI_AHBCR[PREFETCHEN]=1` в редком случае возвращаются неверные данные. | 4 — XIP-mmap блит ассетов из QSPI. Проверить конфиг prefetch в `bsp` FlexSPI. | A0, исправлено в A1 |
|
||||||
|
| **ERR011377** | FlexSPI: бит статуса блокировки DLL неточен из-за тайминга. | 4/7 — запись/чтение QSPI-ассетов. | Без фикса |
|
||||||
|
| **ERR011572** | Cortex-M7: write-through чтения/записи могут вернуть неверные данные. | Все — наш фреймбуфер `AT_NONCACHEABLE` обходит проблему; но любой write-through-регион под подозрением. | Без фикса |
|
||||||
|
| **ERR006032 / ERR009527 / ERR009595** | FlexCAN: баги TX-пути (кадр с неверным ID при abort/deactivate в bus-idle; сбой abort; порча кадра при freeze/low-power в bus-off). | 2 — **если** «удалённая установка адреса» требует CAN **TX**; иначе 8. Пока только RX — низкий риск. | Без фикса |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приложение. Заимствования из проектов
|
||||||
|
|
||||||
|
- `special` — эталон доменного дизайна: `UnifiedProtocolData`→`sul_result_t`, `proto_handler`
|
||||||
|
(poll+timeout)→реестр `sul`, `controller`+`IndicationTask`. Адаптирован под FreeRTOS и снейк-кейс.
|
||||||
|
- `OLD_PROJECT` (НКУ-CAN, TFT7/10) — боевые `bsp`, compositor (PXP), audio-движок,
|
||||||
|
`settings_manager`, декодер НКУ-CAN (PACKET1..5, адресация, удалённая установка адреса).
|
||||||
|
- `OLD_PROJECT_TFT8_UKL` — наш `bsp` (button/opto/sd/w25q/settings/file_loader), UART+CAN.
|
||||||
|
- `TFT10_UIM`, `TFT4_UIM`, `TFT4_SD7`, `TFT4_UEL` — декодеры УИМ / НКУ-SD7 / УЭЛ, матрица
|
||||||
|
«протокол × дисплей».
|
||||||
162
firmware/tft_app/CMakeLists.txt
Normal file
162
firmware/tft_app/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
# firmware/tft_app/CMakeLists.txt
|
||||||
|
#
|
||||||
|
# tft_app — прошивка лифтового индикатора. Собирается как MCUboot-слот-образ
|
||||||
|
# (Direct-XIP), подписывается imgtool (см. just build::sign-app). Архитектура —
|
||||||
|
# ARCH.md, план фаз — PLAN.md.
|
||||||
|
#
|
||||||
|
# Фаза 0 (каркас): FreeRTOS-мигалка + кормление WDOG + рантайм self-confirm
|
||||||
|
# слота + UART диагностика (LPUART1/MCU-Link VCOM — см. src/app/main.c про
|
||||||
|
# отказ от USB CDC под FreeRTOS для этой цели).
|
||||||
|
|
||||||
|
if(BUILD_TESTS_HOST)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# FreeRTOS ядро — первый RTOS-потребитель в репозитории. Собираем свой STATIC
|
||||||
|
# таргет из вендоренных исходников SDK (порт GCC/ARM_CM4F пригоден для cm7f,
|
||||||
|
# heap_4). Конфиг — src/app/FreeRTOSConfig.h.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
set(FREERTOS_DIR ${CMAKE_SOURCE_DIR}/sdk/rtos/freertos/freertos-kernel)
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
freertos_kernel STATIC
|
||||||
|
${FREERTOS_DIR}/tasks.c
|
||||||
|
${FREERTOS_DIR}/list.c
|
||||||
|
${FREERTOS_DIR}/queue.c
|
||||||
|
${FREERTOS_DIR}/timers.c
|
||||||
|
${FREERTOS_DIR}/event_groups.c
|
||||||
|
${FREERTOS_DIR}/stream_buffer.c
|
||||||
|
${FREERTOS_DIR}/portable/GCC/ARM_CM4F/port.c
|
||||||
|
${FREERTOS_DIR}/portable/MemMang/heap_4.c)
|
||||||
|
|
||||||
|
target_include_directories(
|
||||||
|
freertos_kernel
|
||||||
|
PUBLIC ${FREERTOS_DIR}/include
|
||||||
|
${FREERTOS_DIR}/portable/GCC/ARM_CM4F
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/app) # FreeRTOSConfig.h
|
||||||
|
|
||||||
|
# core_cm7.h, MIMXRT1052.h, SystemCoreClock
|
||||||
|
target_link_libraries(freertos_kernel PUBLIC sdk_device)
|
||||||
|
|
||||||
|
# Вендоренное ядро — не наш стиль/warnings.
|
||||||
|
target_compile_options(freertos_kernel PRIVATE -w)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Доменные/сервисные/презентационные слои — каждый своя статическая либа
|
||||||
|
# (ARCH.md §3, конвенция bsp/<module>). tft_app_* префикс — коллизии имён.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
add_subdirectory(src/domain)
|
||||||
|
add_subdirectory(src/services)
|
||||||
|
add_subdirectory(src/ui)
|
||||||
|
add_subdirectory(src/menu)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# app — исполняемый образ слота
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
app
|
||||||
|
src/app/main.c # main() + wiring + FreeRTOS-хуки
|
||||||
|
src/app/task_bringup.c # одноразовый bring-up (QSPI/settings/self-confirm/SDRAM+gfx+CAN) → создаёт остальные задачи
|
||||||
|
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_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_STARTUP_FILE}
|
||||||
|
${BSP_SYSCALLS_FILE}
|
||||||
|
# confirm_self(): flash_area_* + boot_set_next. Переиспользуем backend и
|
||||||
|
# bootutil_public (общий с bootloader/test_stub). flash_map_backend.c зовёт
|
||||||
|
# led_status_tick_install() — даём локальную no-op заглушку (boot_shims.c).
|
||||||
|
src/app/boot_shims.c
|
||||||
|
# bsp_tick, скомпилирован ПРЯМО в app (не через libbsp_tick.a) — режим задан
|
||||||
|
# ниже (BSP_TICK_FREERTOS_MODE), чтобы не тянуть SysTick_Handler,
|
||||||
|
# конфликтующий с FreeRTOS-портом. Приходит транзитивно через bsp_uart_host.
|
||||||
|
${CMAKE_SOURCE_DIR}/bsp/tick/src/tick.c
|
||||||
|
# log_mutex.c — FreeRTOS strong-override весов log_mutex_{init,lock,unlock}
|
||||||
|
# (weak NOP в utils/log/log.c). Компилируется ПРЯМО в app (не отдельной
|
||||||
|
# либой) — как tick.c: FreeRTOS-специфика только этой цели, нужна во ВСЕХ
|
||||||
|
# сборках tft_app (без неё — гонка на общем static-буфере логгера между
|
||||||
|
# bringup_task/sul_rx_task/menu_task/render_task, см. PLAN.md).
|
||||||
|
${CMAKE_SOURCE_DIR}/port/log/src/log_mutex.c
|
||||||
|
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c)
|
||||||
|
|
||||||
|
target_include_directories(
|
||||||
|
app PRIVATE src/app ${MCUBOOT_BOOTUTIL_INCLUDES}
|
||||||
|
${CMAKE_SOURCE_DIR}/firmware/bootloader/src) # led_status.h
|
||||||
|
|
||||||
|
# __STARTUP_INITIALIZE_RAMFUNCTION — startup копирует .ram_function из flash в
|
||||||
|
# ITCM. БЕЗ него ramfunc-код bsp_qspi (AT_QUICKACCESS_SECTION_CODE) в ITCM не
|
||||||
|
# попадает → запись QSPI виснет (см. bsp/qspi_flash/README.md).
|
||||||
|
# __STARTUP_INITIALIZE_NONCACHEDATA — аналогично для .ncache/.ncache.init:
|
||||||
|
# framebuffer (services/gfx, AT_NONCACHEABLE_SECTION_ALIGN) размещён в SDRAM
|
||||||
|
# non-cacheable регионе (cmake/linker/..._app_slot.ld, m_sdram_ncache) — по
|
||||||
|
# рекомендации NXP для буферов, которые ELCDIF читает по DMA (см.
|
||||||
|
# bsp/sdram/README.md и OLD_PROJECT/source/display/image_cache.c).
|
||||||
|
target_compile_definitions(
|
||||||
|
app PRIVATE APP_OWN_SLOT_ID=0 __STARTUP_CLEAR_BSS __STARTUP_INITIALIZE_RAMFUNCTION
|
||||||
|
__STARTUP_INITIALIZE_NONCACHEDATA)
|
||||||
|
|
||||||
|
# Вендоренный bootutil_public.c — под своими warnings (как в bootloader/stub).
|
||||||
|
set_source_files_properties(
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c
|
||||||
|
PROPERTIES COMPILE_OPTIONS "${MCUBOOT_VENDORED_COMPILE_OPTIONS}")
|
||||||
|
|
||||||
|
# FreeRTOS-режим bsp_tick: без него tick.c определяет SysTick_Handler
|
||||||
|
# (bare-metal), конфликтующий с обработчиком FreeRTOS-порта. Компилируется прямо
|
||||||
|
# в app (link-line раньше архивов) → bare-metal member из libbsp_tick.a не
|
||||||
|
# извлекается.
|
||||||
|
set_source_files_properties(
|
||||||
|
${CMAKE_SOURCE_DIR}/bsp/tick/src/tick.c PROPERTIES COMPILE_DEFINITIONS
|
||||||
|
BSP_TICK_FREERTOS_MODE)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
app
|
||||||
|
PRIVATE bsp_board
|
||||||
|
bsp_led
|
||||||
|
bsp_button
|
||||||
|
bsp_opto # диспетчерский вход IN1/IN2 (§3.4), app/dispatcher.c
|
||||||
|
bsp_wdog
|
||||||
|
bsp_boot_state
|
||||||
|
bsp_qspi_flash
|
||||||
|
bsp_uart_host
|
||||||
|
bsp_sdram
|
||||||
|
bsp_boot_xip_no_dcd
|
||||||
|
freertos_kernel
|
||||||
|
port_log_uart # UART диагностика (utils_log + bsp_uart_host, LPUART1/MCU-Link VCOM)
|
||||||
|
# Фаза 1: walking skeleton (CAN → декодер → контроллер → fallback-рендер)
|
||||||
|
tft_app_sul
|
||||||
|
tft_app_sul_transport_can
|
||||||
|
# Фаза 3.3: демо-протокол (транспорт — синтетический, без bsp)
|
||||||
|
tft_app_sul_transport_demo
|
||||||
|
tft_app_controller
|
||||||
|
tft_app_gfx
|
||||||
|
tft_app_ui_fallback
|
||||||
|
# Фаза 3.1: персист ядра настроек (QSPI 0x450000)
|
||||||
|
tft_app_settings_store
|
||||||
|
# Фаза 3.2.1/3.2.2: движок меню (модель) + рендер
|
||||||
|
tft_app_menu
|
||||||
|
tft_app_ui_menu)
|
||||||
|
|
||||||
|
target_link_options(
|
||||||
|
app
|
||||||
|
PRIVATE
|
||||||
|
-Wl,--gc-sections
|
||||||
|
-Wl,--print-memory-usage
|
||||||
|
-Wl,-Map=${CMAKE_BINARY_DIR}/app.map
|
||||||
|
-Wl,--defsym=__slot_base__=0x60040000 # Slot A; Slot Б — на фазе обновления
|
||||||
|
-Wl,--defsym=__stack_size__=0x1000
|
||||||
|
-Wl,--defsym=__heap_size__=0x400
|
||||||
|
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_app_slot.ld)
|
||||||
|
|
||||||
|
set_target_properties(app PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET app
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:app> ${CMAKE_BINARY_DIR}/app.bin
|
||||||
|
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:app>
|
||||||
|
COMMENT "Generating app.bin")
|
||||||
155
firmware/tft_app/HANDOFF_PHASE3_TAIL.md
Normal file
155
firmware/tft_app/HANDOFF_PHASE3_TAIL.md
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
# Хвост Фазы 3 — бриф для постановки задачи (перед Фазой 4)
|
||||||
|
|
||||||
|
Рабочий документ для **отдельного треда**. Фаза 3.2.4 (фундамент рендера: double-buffer + PXP +
|
||||||
|
гибрид bpp + оконный композит меню) закрыта ✅ — см. [PLAN.md](PLAN.md). Ниже — пять пунктов,
|
||||||
|
которые по плану остаются перед Фазой 4 (ассеты), с фактическим состоянием кода (проверено на
|
||||||
|
момент написания, не по памяти) и открытыми вопросами, которые нужно решать **совместно**, не
|
||||||
|
молча.
|
||||||
|
|
||||||
|
**Прочитать сначала** (источники истины, не пересказывать здесь): [ARCH.md](ARCH.md) §6-8
|
||||||
|
(домен/контроллер/настройки), [PLAN.md](PLAN.md) Фаза 3 (декомпозиция 3.1-3.6),
|
||||||
|
[docs/tft_app/TASKS.md](../../docs/tft_app/TASKS.md) (задачи FreeRTOS, приоритеты, MPSC-паттерн —
|
||||||
|
пункт 2 ниже в него встраивается), [docs/tft_app/DOMAIN_DATAFLOW.md](../../docs/tft_app/DOMAIN_DATAFLOW.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Выбор протокола + демо-протокол (под-шаг 3.3)
|
||||||
|
|
||||||
|
**Замысел уже зафиксирован в ARCH.md §8** (не изобретать заново): каждый протокол регистрирует
|
||||||
|
`sul_settings_desc_t` — свои параметры/диапазоны/подписи для меню; `sul_driver_t` несёт
|
||||||
|
`const sul_settings_desc_t *settings`; активный протокол выбирается из `settings`.
|
||||||
|
|
||||||
|
**Фактическое состояние (пробел):**
|
||||||
|
- `sul_driver_t` (`domain/sul/include/domain/sul.h`) поля `.settings` **ещё не имеет** —
|
||||||
|
только `id`/`p_name`/`decode`. Тип `sul_settings_desc_t` **нигде не определён**, встречается
|
||||||
|
только в комментариях (`settings_store.h`, ARCH.md §8).
|
||||||
|
- `sul_registry_active()` (`domain/sul/src/sul_registry.c`) **захардкожена на индекс 0**:
|
||||||
|
`/* Фаза 1: единственная запись, хардкод. Фаза 3 выберет по настройкам. */`.
|
||||||
|
`sul_registry_find(id)` — уже generic (ищет по id в таблице), лишь `active()` не читает
|
||||||
|
`settings_device_t.protocol_id`.
|
||||||
|
- В меню пункт **«Протокол» уже существует** (`menu_tree.c`, `T_PROTO`, `MENU_SELECT`,
|
||||||
|
`value_offset = offsetof(settings_t, device.protocol_id)`), но `K_PROTO_LABELS = {"НКУ-CAN"}`
|
||||||
|
и `max = 0` — выбирать физически не из чего.
|
||||||
|
- `settings_device_t.protocol_id` (`settings_store.h`) — поле уже есть и персистится, просто
|
||||||
|
ничего его не читает на стороне `sul`.
|
||||||
|
|
||||||
|
**Демо-протокол — предложение пользователя, не в исходном PLAN.md.** Идея: второй, полностью
|
||||||
|
синтетический декодер/источник данных для демонстрации возможностей устройства (не требует
|
||||||
|
реальной шины/станции). Открытые вопросы, требующие совместного решения:
|
||||||
|
- Что именно демонстрирует демо-протокол? (перебор всех режимов/этажей по таймеру? фиксированный
|
||||||
|
сценарий по кругу? управляемый через меню сценарий?)
|
||||||
|
- Нужен ли ему «транспорт» вообще, или это decode-less источник (генерирует `sul_result_t`
|
||||||
|
напрямую, минуя `sul_frame_t`/`sul_decode_fn`)? Если он не вписывается в существующий контракт
|
||||||
|
`sul_decode_fn` — это сигнал, что контракт реестра надо аккуratно обобщить, не подгонять под
|
||||||
|
него демо-протокол.
|
||||||
|
- Даёт ли демо-протокол `.settings` (свой `sul_settings_desc_t`) — если да, это ХОРОШИЙ тест на
|
||||||
|
то, что дескрипторный механизм действительно протокол-агностичен (не NKU-CAN-specific), а не
|
||||||
|
только на бумаге.
|
||||||
|
- Как переключение протокола в меню взаимодействует с `sul_rx_task` (сейчас она пишет адрес в
|
||||||
|
`nku_can_ctx_t` каждую итерацию безусловно — при реальном переключении протокола нужно решить,
|
||||||
|
пересоздаётся ли decoder-context или живёт постоянно для каждого зарегистрированного протокола).
|
||||||
|
|
||||||
|
## 2. Задача обработки оптовходов → уведомление render_task (под-шаг 3.4)
|
||||||
|
|
||||||
|
**BSP-драйвер уже полностью реализован** (`bsp/opto/include/bsp/opto.h` + `src/opto.c`) — не
|
||||||
|
писать заново. Два канала (`BSP_OPTO_CH_IN1/IN2`, уровневый режим с дебаунсом) + опциональный
|
||||||
|
`BSP_OPTO_CH_RS` (протокольный режим, коллбэк из ISR). `bsp_opto_process()` — обязана вызываться
|
||||||
|
из некой периодической точки (аналог `bsp_button_poll()`); это уже **предвиделось** в
|
||||||
|
`task_menu.c`: `input_poll_cb` содержит закомментированный `/* Фаза 3.4: bsp_opto_process(); */`
|
||||||
|
ровно на месте `bsp_button_poll()`.
|
||||||
|
|
||||||
|
**Референс поведения (не архитектуры) — `OLD_PROJECT_TFT8_UKL/source/main_programm.c`**,
|
||||||
|
`tft_refresh_task`: IN1/IN2 → `show_call_icon`/`show_answer_icon` → выбор иконки
|
||||||
|
(`icon_img_ptr`) поверх/вместо этажа. У нас в fallback (asset-free) это будет текст/примитив —
|
||||||
|
спрайты-иконки только с Фазы 4/5 (см. [FALLBACK.md](../../docs/tft_app/FALLBACK.md)).
|
||||||
|
|
||||||
|
**Открытые вопросы (обсудить совместно, есть развилка с реальными последствиями для
|
||||||
|
[TASKS.md](../../docs/tft_app/TASKS.md)):**
|
||||||
|
- **Где живёт вызов `bsp_opto_process()`** — в существующем софт-таймере `input_poll_cb`
|
||||||
|
(5 мс, вместе с debounce кнопок — симметрично, минимальный дифф) или в отдельной задаче?
|
||||||
|
Софт-таймер — самый высокий приоритет в системе, колбэк должен оставаться коротким;
|
||||||
|
`bsp_opto_process()` для LEVEL-режима — судя по докстроке API, короткая (просто дебаунс+
|
||||||
|
колбэк), похоже на `bsp_button_poll()` по характеру.
|
||||||
|
- **Кто ВЛАДЕЕТ состоянием «звонок»/«ответ»** и решает, что показывать — новый доменный слой
|
||||||
|
(аналог `controller`, т.к. это не протокольные данные СУЛ — ARCH §8 п.3 явно относит опто к
|
||||||
|
«локальным входам», не к `sul_result_t`) или это чисто presentation-слой стейт внутри
|
||||||
|
`fallback.c` (как `icon_img_ptr` в старом проекте)? От этого зависит, нужен ли новый
|
||||||
|
`indication_task_t`-подобный diff или простой уровневый флаг.
|
||||||
|
- **Как будить `render_task`.** Сейчас MPSC — два продюсера (`sul_rx_task`, `menu_task`) →
|
||||||
|
один консюмер через `xTaskNotifyGive`/`ulTaskNotifyTake(pdTRUE,...)` (см. TASKS.md §3). Опто
|
||||||
|
становится **третьим продюсером** — сам механизм (notify) масштабируется без переделки, но
|
||||||
|
нужно решить: пишет ли опто-обработчик прямо в `render_task`'овские структуры (как
|
||||||
|
`g_menu`/очередь) или заводит свою пару «состояние + notify»; и как это соотносится с
|
||||||
|
«мягкой паузой» на время меню (опто-события во время открытого меню — игнорировать, копить,
|
||||||
|
или отрабатывать сразу?).
|
||||||
|
- Debounce у `bsp_opto` уже встроен (`debounce_ms` в конфиге) — доп. дебаунс на уровне задачи не
|
||||||
|
нужен, в отличие от кнопок (там `BUTTON_DEBOUNCE_SAMPLES` в `bsp_button.c` — разные модули,
|
||||||
|
не путать подходы).
|
||||||
|
|
||||||
|
## 3. Тумблер логов (под-шаг 3.6)
|
||||||
|
|
||||||
|
**Фактическое состояние:**
|
||||||
|
- `settings_device_t.log_enabled` — персистится, есть пункт меню «Логи» (`menu_tree.c`, `T_LOG`,
|
||||||
|
`MENU_BOOL`) — **уже работает как UI и хранение**, но ни на что не влияет: `log_set_enabled()`/
|
||||||
|
`log_is_enabled()` **не существуют** в `utils/log/log.h` — рантайм-гейта поверх компайл-тайм
|
||||||
|
`LOG_LEVEL` нет вообще. Сейчас переключение пункта «Логи» — чистый no-op по факту.
|
||||||
|
- PLAN.md уже фиксирует важный нюанс: продакшн-сборка должна собираться с `LOG_LEVEL >= INFO`
|
||||||
|
(иначе `LOG_I`/`LOG_E` разворачиваются в `((void)0)` на этапе компиляции — рантайм-гейт
|
||||||
|
гейтить нечего, макросы физически вырезаны).
|
||||||
|
- **Найдено и починено в эту сессию** (контекст для обсуждения «слоёв»): FreeRTOS strong-override
|
||||||
|
мьютекса логгера (`port/log/src/log_mutex.c`) был под `#if 0` и не собирался ни в один таргет —
|
||||||
|
общий `static`-буфер логгера (`utils/log/log.c`) писался из нескольких задач без синхронизации.
|
||||||
|
Уже включено в сборку `app`. Держать в уме при обсуждении «слоёв» логов — это фундамент
|
||||||
|
(потокобезопасность), тумблер (3.6) — уровень выше него.
|
||||||
|
|
||||||
|
**Обсудить:** глобальный вкл/выкл (один бит, как сейчас в `settings_device_t`) или гейт по тегам
|
||||||
|
(`LOG_TAG` — сейчас `"bringup"`/`"menu"`/`"sul_rx"` и т.д.) — второе гибче для будущей диагностики
|
||||||
|
в поле, но за пределами однобитового поля потребует более сложного хранения. Также: временная
|
||||||
|
диагностика этой сессии (тайминги `gfx_present()`) была снесена целиком (H4) — стоит ли новый
|
||||||
|
рантайм-тумблер логов проектировать так, чтобы **подобную точечную диагностику** можно было
|
||||||
|
включать в поле без пересборки (актуально для будущих HW-расследований), а не только глушить/
|
||||||
|
пускать весь поток.
|
||||||
|
|
||||||
|
## 4. Ревизия движка настроек по плану
|
||||||
|
|
||||||
|
Не новая реализация — **аудит текущего `services/settings_store` против замысла ARCH §8 /
|
||||||
|
PLAN.md Фаза 3** перед тем, как Фаза 4 добавит ярус C (клиентский TLV). Что уже проверено в этой
|
||||||
|
сессии:
|
||||||
|
|
||||||
|
| Ярус | Замысел (ARCH §8) | Факт сейчас |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **A** железобетонные | вес/вместимость/громкости/серийник/год | Поля есть в `settings_user_t`, персистятся; реальный **эффект** (рендер/звук) — Фазы 5/6, ещё не подключён |
|
||||||
|
| **B** протокольные | `sul_settings_desc_t` на протокол, меню строится из дескриптора | `proto_slice[0]` = адрес НКУ-CAN — единственное поле с реальным эффектом (decode + HW-фильтры CAN, оба конца). Дескрипторного механизма (см. п.1) физически нет — меню сейчас **хардкодит** привязку по `offsetof`, не строится из данных протокола |
|
||||||
|
| **C** клиентский TLV | лого/шаблон/сдвиги этажей, рядом с layout | Не реализовано — Фаза 4/5, ожидаемо |
|
||||||
|
|
||||||
|
Ключевой пробел для ревизии: **дескрипторный принцип «настройка = строка данных»** пока
|
||||||
|
реализован только для меню-дерева (`menu_item_desc_t[]`), но НЕ для протокольного яруса B —
|
||||||
|
секция «Протокол» в дереве прописана руками (`T_PROTO`/`T_ADDR` как отдельные статичные записи),
|
||||||
|
а не сгенерирована из `sul_settings_desc_t` активного протокола. Это ровно то, что должен закрыть
|
||||||
|
п.1 (3.3) — после чего эта ревизия по факту и должна показать честную картину.
|
||||||
|
|
||||||
|
## 5. `docs/tft_app/SETTINGS.md` (обязательный выход Фазы 3)
|
||||||
|
|
||||||
|
Дословно из PLAN.md: «без воды: состав `settings_t`, ярусы A/B/C и их взаимосвязи,
|
||||||
|
`proto_slice`/дескрипторы (§8), гибридное хранение (ядро-struct + клиентский TLV), карта QSPI
|
||||||
|
(§10, размер-независимость). По паттерну "доки по реализованному"».
|
||||||
|
|
||||||
|
**Писать ПОСЛЕДНИМ в этом хвосте** — репозиторий последовательно держит паттерн «документация
|
||||||
|
по факту реализованного» (см. существующие `docs/tft_app/*.md` — каждый описывает код как он
|
||||||
|
есть, не архитектурные намерения). Если написать `SETTINGS.md` до п.1 (дескрипторы), придётся
|
||||||
|
переписывать сразу же после — `proto_slice`/дескрипторы буквально в required-списке документа.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Предлагаемый порядок (не более чем предложение — решать в целевом треде)
|
||||||
|
|
||||||
|
1. **П.4** (ревизия) — короткий, даёт общую картину, ничего не ломает.
|
||||||
|
2. **П.1** (протокол-дескрипторы + демо) — самый большой архитектурный кусок, разблокирует
|
||||||
|
честную секцию «B» для ревизии и для будущего SETTINGS.md.
|
||||||
|
3. **П.2** (опто) — независим от 1/3, можно параллельно.
|
||||||
|
4. **П.3** (тумблер логов) — маленький, изолированный.
|
||||||
|
5. **П.5** (SETTINGS.md) — после того как 1 реально закрыт (не раньше).
|
||||||
|
|
||||||
|
Все пять пунктов явно помечены пользователем как «согласуем/обсудим совместно» — начинать
|
||||||
|
реализацию любого из них без обсуждения конкретной развилки (см. «Открытые вопросы» в каждом
|
||||||
|
пункте) не нужно.
|
||||||
1013
firmware/tft_app/PLAN.md
Normal file
1013
firmware/tft_app/PLAN.md
Normal file
File diff suppressed because it is too large
Load diff
65
firmware/tft_app/README.md
Normal file
65
firmware/tft_app/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# tft_app
|
||||||
|
|
||||||
|
Прошивка лифтового индикатора (MIMXRT1052, TFT 4/7/8/10). Собирается как
|
||||||
|
MCUboot-слот-образ (Direct-XIP), которым управляет [bootloader](../bootloader/).
|
||||||
|
|
||||||
|
- **Архитектура** — [ARCH.md](ARCH.md).
|
||||||
|
- **План разработки (статус фаз)** — [PLAN.md](PLAN.md).
|
||||||
|
|
||||||
|
> `OLD_PROJECT*` и `special/` — референсные дампы старых серийных проектов
|
||||||
|
> (в `.gitignore`), источник бизнес-логики и доменного дизайна. Не собираются.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Статус: Фаза 0 (каркас)
|
||||||
|
|
||||||
|
Минимальный живой каркас: FreeRTOS + heartbeat + кормление унаследованного от
|
||||||
|
загрузчика WDOG + само-подтверждение слота MCUboot. Доменные/презентационные
|
||||||
|
слои — со следующих фаз.
|
||||||
|
|
||||||
|
## Сборка и подпись
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# devcontainer:
|
||||||
|
just build::build-app-debug # app.elf/.bin (Slot A, XIP из слота)
|
||||||
|
just build::sign-app-debug # + imgtool EC256 → build/Debug/signed/app_slot_a.bin
|
||||||
|
just build::test-host # host-тесты (вкл. test_tft_app_smoke) зелёные
|
||||||
|
```
|
||||||
|
|
||||||
|
Подпись — imgtool ECDSA P-256 (как stub загрузчика), **без** `--confirm`: образ
|
||||||
|
подтверждает свой слот в рантайме (`boot_set_next`). HAB для app не нужен — его
|
||||||
|
грузит не BootROM, а загрузчик после проверки imgtool-подписи.
|
||||||
|
|
||||||
|
## Прошивка (хост)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# итеративно — SWD подписанного слот-образа в Slot A (0x60040000), power cycle после:
|
||||||
|
just host::flash-swd-app-slot-debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Диагностика (Фаза 0)
|
||||||
|
|
||||||
|
USB CDC ACM (VCOM), 115200. Логирует boot-последовательность и **периодически
|
||||||
|
(раз в 2 с) состояние трейлера слота** (magic/copy_done/image_ok) — видно
|
||||||
|
независимо от момента подключения терминала:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
screen /dev/cu.usbmodemXXXX 115200 # macOS; порт свой на каждое подключение
|
||||||
|
```
|
||||||
|
|
||||||
|
Транспорт — `port_log_cdc` (переиспользуемый адаптер `utils/log` над
|
||||||
|
`bsp_usb_cdc`, симметричный `port_log_uart`), best-effort (строки без хоста
|
||||||
|
молча теряются).
|
||||||
|
|
||||||
|
## Раскладка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
src/
|
||||||
|
├── app/ точка входа, FreeRTOS-задачи, wiring, FreeRTOSConfig.h
|
||||||
|
├── domain/ чистый C, host-тесты: elevator_model, sul (декодеры), controller
|
||||||
|
├── services/ адаптеры к железу: gfx, audio_engine, assets, settings, fs
|
||||||
|
├── ui/ layout-движок, темы, fallback-рендер
|
||||||
|
└── menu/ экран настроек, навигация двумя кнопками
|
||||||
|
```
|
||||||
|
|
||||||
|
Слот-линкер — [cmake/linker/MIMXRT1052xxxxx_app_slot.ld](../../cmake/linker/MIMXRT1052xxxxx_app_slot.ld).
|
||||||
189
firmware/tft_app/REPORT.md
Normal file
189
firmware/tft_app/REPORT.md
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
Готово. Полная повторная верификация:
|
||||||
|
|
||||||
|
| Проверка | Результат |
|
||||||
|
| ---------------------- | ------------------------------------------------------ |
|
||||||
|
| Host-тесты | **19/19** зелёные |
|
||||||
|
| Debug сборка+подпись | чисто, `image_ok` UNSET |
|
||||||
|
| Release сборка+подпись | чисто, `image_ok` UNSET |
|
||||||
|
| Оба бинарника | 2 097 152 байт (ровно размер слота) |
|
||||||
|
| Аппаратно (ваш лог) | self-confirm пережил перезагрузку, CAN→рендер работает |
|
||||||
|
|
||||||
|
PLAN.md обновлён: Фаза 1 → ✅, известное ограничение (single-buffer) зафиксировано с честной формулировкой механизма, явно вписано в объём Фазы 4 вместе с PXP. Также отметил, что критерий «timeout→--» проверен только логически (host-тестом), не на реальном обрыве связи — не стал завышать степень подтверждённости.
|
||||||
|
|
||||||
|
Отчёт — [tft_app_phase_0_1_report.md](../../../private/tmp/claude-501/-Users-von-akimow-Desktop-TFT-ENV-tft-manufacture-test/93af6316-b6c7-4b2a-9063-169ca896d1ff/scratchpad/tft_app_phase_0_1_report.md). Полный текст ниже — можно скопировать целиком в новый тред:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# tft_app — отчёт по Фазам 0–1
|
||||||
|
|
||||||
|
> Канонические источники — `firmware/tft_app/ARCH.md` (архитектура) и `firmware/tft_app/PLAN.md`
|
||||||
|
> (статус фаз, живой документ, обновляется каждой фазой). Этот файл — снимок на момент завершения
|
||||||
|
> Фазы 1, для быстрого ввода в контекст новой сессии. При расхождении — верить PLAN.md/ARCH.md, не
|
||||||
|
> этому файлу.
|
||||||
|
|
||||||
|
**Статус:** Фаза 0 ✅ и Фаза 1 ✅ — обе аппаратно подтверждены. Работа Фазы 1 **не закоммичена**
|
||||||
|
(в рабочем дереве поверх коммита `# tft_app: Phase 0`, `a9e6681`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 0 — Каркас (FreeRTOS + self-confirm слота MCUboot)
|
||||||
|
|
||||||
|
**Итог.** `firmware/tft_app` собирается как MCUboot-слот-образ (Direct-XIP), bootloader в него
|
||||||
|
прыгает, FreeRTOS стартует, образ подтверждает себя в рантайме (`boot_set_next`) и переживает
|
||||||
|
power cycle без отката — anti-brick сохранён. Подтверждено дважды на реальном железе (в т.ч.
|
||||||
|
через переустановку с microSD).
|
||||||
|
|
||||||
|
**Путь до рабочего состояния был не прямым — три независимых бага, все найдены и закрыты:**
|
||||||
|
|
||||||
|
1. **`bsp_qspi_init()` обязателен до любого `flash_area_*`.** Без него запись `image_ok` в трейлер
|
||||||
|
слота молча падала — `boot_set_next()` возвращал успех, но ничего не персистило.
|
||||||
|
2. **Корневая причина (самая коварная): забытый дефайн `__STARTUP_INITIALIZE_RAMFUNCTION`.**
|
||||||
|
Запись QSPI из app виснет в `qspi_write_fifo` — FlexSPI не дренирует IP TX FIFO. Причина:
|
||||||
|
startup копирует секцию `.ram_function` (код `bsp_qspi`, помеченный
|
||||||
|
`AT_QUICKACCESS_SECTION_CODE`) из flash в ITCM только под этим дефайном; `app` его не задавал
|
||||||
|
— в ITCM оставался leftover ramfunc **загрузчика** (тот исполнялся первым), поэтому чтения
|
||||||
|
«случайно работали», а запись — нет. Оба рабочих потребителя `bsp_qspi` (firmware_test,
|
||||||
|
bootloader) этот дефайн задают; `app` был первым, кто его пропустил. Диагностировано под
|
||||||
|
отладчиком (зависание конкретно в `qspi_write_fifo`), подтверждено изолированным
|
||||||
|
erase+write+readback self-test (частота мигания LED = индикатор результата) до восстановления
|
||||||
|
полной диагностики.
|
||||||
|
3. **UART, не USB CDC, для диагностики/логов.** USB CDC (target-side EHCI/PHY) под FreeRTOS не
|
||||||
|
заработал стабильно за несколько заходов — первый в репозитории FreeRTOS-потребитель target
|
||||||
|
USB device stack, класс неопределённости остался непроверенным. LPUART1/MCU-Link VCOM — самый
|
||||||
|
проверенный канал в репозитории (все `tests/target/*` HIL-образы), доступен сразу через уже
|
||||||
|
подключённый для SWD кабель, без enumeration/wait. Решение принято явно с пользователем.
|
||||||
|
|
||||||
|
**Побочные, но важные для будущих фаз находки:**
|
||||||
|
- Любая bsp-инициализация, зависящая от тика (`bsp_delay()` внутри `bsp_usb_cdc_init()` и т.п.),
|
||||||
|
нельзя звать **до** `vTaskStartScheduler()` — в FreeRTOS-режиме `bsp_tick` это
|
||||||
|
`xTaskGetTickCount()`, который не идёт до старта планировщика → вызов зависает навсегда молча.
|
||||||
|
`main()` в итоге — минимальный bare-metal (board bring-up + запуск планировщика), вся
|
||||||
|
инициализация — из задачи.
|
||||||
|
- Первый в репозитории потребитель `BSP_TICK_FREERTOS_MODE` вскрыл 2 латентных бага: (1) общий
|
||||||
|
`bsp_tick` — bare-metal STATIC-либа → `SysTick_Handler` конфликтует с FreeRTOS-портом (решение:
|
||||||
|
компилировать `tick.c` прямо в `app`, не через `libbsp_tick.a`); (2) `configTICK_RATE_HZ`
|
||||||
|
нельзя как `((TickType_t)1000)` — `#if` в `tick.c` не разбирает cast, только `1000`.
|
||||||
|
- `.vscode/launch.json`: у `🐛 Debug: tft_app` был `preLaunchTask` на `firmware_test`
|
||||||
|
(copy-paste) — исправлено на `build:app-debug`.
|
||||||
|
- Тонкий стек задачи (изначально x2=1 КБ) маскировался под тот же класс симптома, что и другие
|
||||||
|
баги (зависание/reset-loop) — увеличен с запасом (x4-x6, 2-3 КБ) везде, где заводятся новые
|
||||||
|
задачи.
|
||||||
|
|
||||||
|
**Осознанное отклонение от исходного плана:** `firmware/bootloader/test_stub` НЕ удалён (план
|
||||||
|
предполагал) — оставлен как независимая A/B-регрессия загрузчика, `app` пока не покрывает те же
|
||||||
|
сценарии (hang-классы, revert без self-confirm).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 1 — Walking skeleton (CAN → декодер → контроллер → fallback-рендер)
|
||||||
|
|
||||||
|
**Итог.** Полный вертикальный срез: реальный CAN-кадр НКУ-CAN → чистый декодер → контроллер
|
||||||
|
(diff) → fallback-рендер (позиция + стрелка) на экране. Подтверждено на TFT8 с реальным
|
||||||
|
CAN-трафиком станции.
|
||||||
|
|
||||||
|
### Архитектура (по слоям, каждый — своя CMake-либа, конвенция `bsp/<module>`)
|
||||||
|
|
||||||
|
```
|
||||||
|
domain/elevator_model sul_result_t (pos: UTF-8 строка, direction: none/up/down/double),
|
||||||
|
sul_default_state()
|
||||||
|
domain/sul sul_frame_t, sul_driver_t, sul_status_t, реестр драйверов
|
||||||
|
(заголовок и реализация реестра — РАЗНЫЕ CMake-таргеты,
|
||||||
|
иначе цикл: декодеру нужны только типы sul.h, реестру — decode() декодера)
|
||||||
|
domain/sul/nku_can чистый декодер PACKET1 (направление) / PACKET3 (позиция) —
|
||||||
|
порт OLD_PROJECT msg_receiver_task, БЕЗ единого HAL-вызова
|
||||||
|
domain/sul/transport/can тонкий bsp_can → sul_frame_t адаптер (HW, не host-тестируется)
|
||||||
|
domain/controller process(sul_result_t) → indication_task_t (diff), кэш
|
||||||
|
services/gfx framebuffer (SDRAM non-cacheable) + RLE-декодер шрифтов
|
||||||
|
(lcd-image-converter формат) + примитив стрелки
|
||||||
|
ui/fallback рендер pos+стрелка, asset-free/FS-free (ARCH §11)
|
||||||
|
app задачи sul_rx (WDOG+CAN+decode+controller) и render,
|
||||||
|
связаны очередью глубины 1 (xQueueOverwrite — важно
|
||||||
|
только последнее состояние, не история)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Host-тесты (все зелёные, домен полностью протестирован до касания железа):**
|
||||||
|
- `tests/host/tft_app_sul_nku` — 16 тестов, golden-векторы CAN-кадров (все direction, позиции,
|
||||||
|
легаси-квирк `left==0`≈пробел, кириллица, IGNORED/ERR на чужой ID/DLC/вне таблицы символов,
|
||||||
|
накопление состояния между PACKET1 и PACKET3).
|
||||||
|
- `tests/host/tft_app_controller` — 6 тестов (diff-логика, timeout→default тем же путём, что и
|
||||||
|
обычный кадр).
|
||||||
|
- Полный пакет: **19/19** (включая существующие тесты bootloader/bsp).
|
||||||
|
|
||||||
|
### Ключевые находки/решения по ходу
|
||||||
|
|
||||||
|
1. **Framebuffer в SDRAM обязан идти через `AT_NONCACHEABLE_SECTION_ALIGN`** (прямая рекомендация
|
||||||
|
NXP, порт паттерна из `OLD_PROJECT/source/display/image_cache.c`). MPU держит SDRAM как
|
||||||
|
`Normal Write-Back Cacheable` (`board_mpu_init()`, Region 8); ELCDIF читает framebuffer
|
||||||
|
напрямую по DMA — без non-cacheable региона CPU писал бы через кэш, DMA видел бы устаревшие
|
||||||
|
данные (мусор/частичные кадры на экране, выглядело бы как загадочный баг железа). Механизм уже
|
||||||
|
был параметризован в `board_mpu_init()` через линкер-символы `__NCACHE_REGION_START/SIZE`
|
||||||
|
(Region 9, унаследовано из bootloader) — переопределены в `app_slot.ld` на новый
|
||||||
|
`m_sdram_ncache` (2 МБ в начале SDRAM) вместо OCRAM (256 КБ, для framebuffer ~1.83 МБ не
|
||||||
|
влезло бы). На первой попытке framebuffer был сырым указателем на константный адрес (работало
|
||||||
|
случайно, линкер честно репортил `0 B` в секции) — исправлено на настоящее объявление
|
||||||
|
переменной через макрос.
|
||||||
|
2. **RLE-декодер шрифтов — порт проверенного в проде алгоритма** из `OLD_PROJECT/source/fonts/
|
||||||
|
fonts.c` (UNIQUE/REPEATABLE блоки, бинарный поиск по отсортированному коду символа), не
|
||||||
|
переизобретён с нуля. Формат — то, что реально экспортирует lcd-image-converter (`tImage`/
|
||||||
|
`tChar`/`tFont`, ARGB8888, "Color A8R8G8B8"). Пользователь сгенерировал `FloorFontFallback.c`
|
||||||
|
(Inter 215pt, символы 0-9 и "-") и `SystemFont.c` (JetBrains Mono 24pt, ASCII+кириллица)
|
||||||
|
инструментом напрямую.
|
||||||
|
3. **По-символьный fallback** (ARCH §11): символ вне таблицы шрифта → подстановка `'-'`
|
||||||
|
(согласованная политика — "-" общий заменитель во `FloorFontFallback`).
|
||||||
|
4. Сгенерированные шрифтовые файлы ожидают типы из bare-name `fonts.h` по-разному
|
||||||
|
(`SystemFont.c` — явный `#include`, `FloorFontFallback.c` — вообще без include, разные версии
|
||||||
|
конвертера) — закрыто через `-include`/`-I` в CMake; сами сгенерированные файлы не
|
||||||
|
редактировались (конвертер их перезапишет при регенерации).
|
||||||
|
5. Файлы шрифтов перенесены из `domain/` (где их положил пользователь) в `services/gfx/fonts/` —
|
||||||
|
по ARCH §4 шрифты относятся к gfx, не к домену (домен — чистый C без рендеринга).
|
||||||
|
|
||||||
|
### ⚠️ Известное ограничение (зафиксировано, чинится в Фазе 4)
|
||||||
|
|
||||||
|
Один framebuffer, без double buffering — `ui_fallback_render()` пишет напрямую в буфер, который
|
||||||
|
ELCDIF в этот момент сканирует по DMA. Если перерисовка (`gfx_clear` + RLE-decode глифов +
|
||||||
|
стрелка) не укладывается в один кадр развёртки, видно сам процесс закраски (глиф «набирается» по
|
||||||
|
частям за несколько кадров) — **не tearing и не порча пикселей** (каждая отдельная запись всегда
|
||||||
|
валидна), просто заметен сам процесс. Подтверждено на железе пользователем ("рвано, но без
|
||||||
|
артефактов" — совпадает с механизмом).
|
||||||
|
|
||||||
|
**Фикс — double buffering**, НЕ требует PXP отдельно (это вопрос буферизации, не композиции):
|
||||||
|
второй framebuffer (`m_sdram_ncache` 2→4 МБ), swap через `bsp_display_set_next_buffer()` +
|
||||||
|
семафор на `FRAME_DONE` (сейчас `gfx_init()` передаёт `NULL` вместо колбэка). Явно вписано в
|
||||||
|
объём Фазы 4 (PLAN.md) вместе с PXP-компоновщиком (для многослойной композиции реальных
|
||||||
|
ассетов — фон+спрайты+текст, та же фаза, естественно смежная работа).
|
||||||
|
|
||||||
|
### Прочее
|
||||||
|
|
||||||
|
- Критерий выхода выполнен на TFT8: реальный CAN-трафик → корректный номер этажа и стрелка.
|
||||||
|
Таймаут→"--" логически проверен host-тестом, аппаратно на реальном обрыве связи отдельно не
|
||||||
|
гонялся (низкий риск — тот же код-путь, что у обычного кадра).
|
||||||
|
- Debug и Release оба собираются и подписываются чисто (2 097 152 байта — ровно размер слота,
|
||||||
|
`image_ok` UNSET в обоих — рантайм self-confirm, не sign-time `--confirm`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Состояние репозитория на конец Фазы 1
|
||||||
|
|
||||||
|
- Ветка: `tft-app-dev`.
|
||||||
|
- Коммит `a9e6681` (# tft_app: Phase 0) — последний закоммиченный чекпоинт.
|
||||||
|
- Вся Фаза 1 — в рабочем дереве, не закоммичена: `firmware/tft_app/src/{domain,services/gfx,
|
||||||
|
ui/fallback}/`, `firmware/tft_app/src/app/main.c` (переписан), `tests/host/tft_app_sul_nku/`,
|
||||||
|
`tests/host/tft_app_controller/`, изменения в `CMakePresets.json`,
|
||||||
|
`cmake/linker/MIMXRT1052xxxxx_app_slot.ld`, `firmware/tft_app/CMakeLists.txt`,
|
||||||
|
`tests/host/CMakeLists.txt`, `firmware/tft_app/PLAN.md`.
|
||||||
|
- Стоит рассмотреть коммит перед длительным перерывом/переключением контекста — на усмотрение
|
||||||
|
пользователя (не коммичу без явной просьбы).
|
||||||
|
|
||||||
|
## Дальше — Фаза 2
|
||||||
|
|
||||||
|
Полный контракт `sul_result_t` (next, сигналы, `lading_secs`, `floor_num`), полный декодер
|
||||||
|
НКУ-CAN (PACKET1..5, режимы, удалённая установка адреса), таблица приоритетов режимов
|
||||||
|
(данные, потенциально клиентские), валидация рендеримости позиции (renderable-множество шрифта +
|
||||||
|
по-символьный fallback — механизм уже есть в `gfx`, п.3 выше). Подробности — PLAN.md, раздел
|
||||||
|
«Фаза 2».
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Одно практическое замечание, раз вы переключаетесь в новый тред: вся Фаза 1 сейчас **не закоммичена** — сидит в рабочем дереве поверх коммита Фазы 0. Хотите, чтобы я закоммитил перед переключением (стандартный `git add` + сообщение по Фазе 1), или оставляете как есть?
|
||||||
122
firmware/tft_app/src/app/FreeRTOSConfig.h
Normal file
122
firmware/tft_app/src/app/FreeRTOSConfig.h
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* FreeRTOSConfig.h — конфигурация ядра для tft_app (MIMXRT1052, Cortex-M7F).
|
||||||
|
*
|
||||||
|
* Порт: GCC/ARM_CM4F (используется и для cm7f, см.
|
||||||
|
* sdk/rtos/freertos/freertos-kernel/CMakeLists.txt). Схема кучи — heap_4.
|
||||||
|
*
|
||||||
|
* Фаза 0: configTOTAL_HEAP_SIZE держим скромным во внутренней RAM (heap_4
|
||||||
|
* размещает свой массив в .bss → DTCM). На фазе дисплея куча (и фреймбуферы)
|
||||||
|
* переедут в SDRAM — тогда пересмотреть размер и размещение.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FREERTOS_CONFIG_H
|
||||||
|
#define FREERTOS_CONFIG_H
|
||||||
|
|
||||||
|
#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__)
|
||||||
|
#include <stdint.h>
|
||||||
|
extern uint32_t SystemCoreClock; /* задаётся менеджером тактирования */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ── Планировщик ─────────────────────────────────────────────────────────── */
|
||||||
|
#define configUSE_PREEMPTION 1
|
||||||
|
#define configUSE_TICKLESS_IDLE 0
|
||||||
|
#define configCPU_CLOCK_HZ (SystemCoreClock)
|
||||||
|
/* Без cast — bsp/tick.c (BSP_TICK_FREERTOS_MODE) сравнивает это значение в
|
||||||
|
* `#if configTICK_RATE_HZ != 1000`; препроцессор не умеет разбирать cast
|
||||||
|
* внутри #if ("missing binary operator"). Для C-кода бит-в-бит то же самое —
|
||||||
|
* TickType_t получается неявным преобразованием на месте использования. */
|
||||||
|
#define configTICK_RATE_HZ 1000
|
||||||
|
#define configMAX_PRIORITIES 8
|
||||||
|
#define configMINIMAL_STACK_SIZE ((unsigned short)128)
|
||||||
|
#define configMAX_TASK_NAME_LEN 20
|
||||||
|
#define configUSE_16_BIT_TICKS 0
|
||||||
|
#define configIDLE_SHOULD_YIELD 1
|
||||||
|
#define configUSE_TASK_NOTIFICATIONS 1
|
||||||
|
#define configUSE_MUTEXES 1
|
||||||
|
#define configUSE_RECURSIVE_MUTEXES 1
|
||||||
|
#define configUSE_COUNTING_SEMAPHORES 1
|
||||||
|
#define configQUEUE_REGISTRY_SIZE 8
|
||||||
|
#define configUSE_QUEUE_SETS 0
|
||||||
|
#define configUSE_TIME_SLICING 1
|
||||||
|
#define configUSE_NEWLIB_REENTRANT 0
|
||||||
|
#define configENABLE_BACKWARD_COMPATIBILITY 0
|
||||||
|
#define configSTACK_DEPTH_TYPE uint32_t
|
||||||
|
|
||||||
|
/* ── Память ──────────────────────────────────────────────────────────────── */
|
||||||
|
#define configFRTOS_MEMORY_SCHEME 4 /* heap_4.c */
|
||||||
|
#define configSUPPORT_STATIC_ALLOCATION 0
|
||||||
|
#define configSUPPORT_DYNAMIC_ALLOCATION 1
|
||||||
|
#define configTOTAL_HEAP_SIZE ((size_t)(0x8000)) /* 32 КБ (Фаза 0, DTCM) */
|
||||||
|
#define configAPPLICATION_ALLOCATED_HEAP 0
|
||||||
|
|
||||||
|
/* ── Hooks (строгая диагностика на этапе разработки) ─────────────────────── */
|
||||||
|
#define configUSE_IDLE_HOOK 0
|
||||||
|
#define configUSE_TICK_HOOK 0
|
||||||
|
#define configCHECK_FOR_STACK_OVERFLOW 2 /* → vApplicationStackOverflowHook */
|
||||||
|
#define configUSE_MALLOC_FAILED_HOOK 1 /* → vApplicationMallocFailedHook */
|
||||||
|
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
|
||||||
|
|
||||||
|
/* ── Статистика/трассировка ──────────────────────────────────────────────── */
|
||||||
|
#define configGENERATE_RUN_TIME_STATS 0
|
||||||
|
#define configUSE_TRACE_FACILITY 1
|
||||||
|
#define configUSE_STATS_FORMATTING_FUNCTIONS 0
|
||||||
|
|
||||||
|
/* ── Со-рутины (не используем) ───────────────────────────────────────────── */
|
||||||
|
#define configUSE_CO_ROUTINES 0
|
||||||
|
#define configMAX_CO_ROUTINE_PRIORITIES 2
|
||||||
|
|
||||||
|
/* ── Программные таймеры ─────────────────────────────────────────────────── */
|
||||||
|
#define configUSE_TIMERS 1
|
||||||
|
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)
|
||||||
|
#define configTIMER_QUEUE_LENGTH 10
|
||||||
|
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2)
|
||||||
|
|
||||||
|
/* ── Ловим ошибки на разработке ──────────────────────────────────────────── */
|
||||||
|
#define configASSERT(x) \
|
||||||
|
if ((x) == 0) \
|
||||||
|
{ \
|
||||||
|
taskDISABLE_INTERRUPTS(); \
|
||||||
|
for (;;) \
|
||||||
|
{ \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Опциональные API ────────────────────────────────────────────────────── */
|
||||||
|
#define INCLUDE_vTaskPrioritySet 1
|
||||||
|
#define INCLUDE_uxTaskPriorityGet 1
|
||||||
|
#define INCLUDE_vTaskDelete 1
|
||||||
|
#define INCLUDE_vTaskSuspend 1
|
||||||
|
#define INCLUDE_xResumeFromISR 1
|
||||||
|
#define INCLUDE_vTaskDelayUntil 1
|
||||||
|
#define INCLUDE_vTaskDelay 1
|
||||||
|
#define INCLUDE_xTaskGetSchedulerState 1
|
||||||
|
#define INCLUDE_xTaskGetCurrentTaskHandle 1
|
||||||
|
#define INCLUDE_uxTaskGetStackHighWaterMark 1
|
||||||
|
#define INCLUDE_xTaskGetIdleTaskHandle 0
|
||||||
|
#define INCLUDE_eTaskGetState 0
|
||||||
|
#define INCLUDE_xTimerPendFunctionCall 1
|
||||||
|
#define INCLUDE_xTaskAbortDelay 0
|
||||||
|
#define INCLUDE_xTaskGetHandle 0
|
||||||
|
#define INCLUDE_xTaskResumeFromISR 1
|
||||||
|
|
||||||
|
/* ── Прерывания Cortex-M (RT1052 NVIC = 4 бита приоритета) ───────────────── */
|
||||||
|
#ifdef __NVIC_PRIO_BITS
|
||||||
|
#define configPRIO_BITS __NVIC_PRIO_BITS
|
||||||
|
#else
|
||||||
|
#define configPRIO_BITS 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY ((1U << configPRIO_BITS) - 1)
|
||||||
|
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2
|
||||||
|
|
||||||
|
#define configKERNEL_INTERRUPT_PRIORITY \
|
||||||
|
(configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
|
||||||
|
#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
|
||||||
|
(configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
|
||||||
|
|
||||||
|
/* ── Маппинг обработчиков порта на CMSIS-имена (vector table) ────────────── */
|
||||||
|
#define vPortSVCHandler SVC_Handler
|
||||||
|
#define xPortPendSVHandler PendSV_Handler
|
||||||
|
#define xPortSysTickHandler SysTick_Handler
|
||||||
|
|
||||||
|
#endif /* FREERTOS_CONFIG_H */
|
||||||
173
firmware/tft_app/src/app/app_tasks.h
Normal file
173
firmware/tft_app/src/app/app_tasks.h
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
/**
|
||||||
|
* @file app_tasks.h
|
||||||
|
* @brief Контракт между задачами app-слоя (wiring, ARCH §4).
|
||||||
|
*
|
||||||
|
* Четыре задачи (найдено на HW-верификации Фазы 3.2.4 — меню и render в одной
|
||||||
|
* задаче оказались взаимно неотзывчивы, см. PLAN.md):
|
||||||
|
* - bringup_task — одноразовая инициализация (UART/QSPI/settings/self-confirm/
|
||||||
|
* SDRAM+gfx+CAN), затем создаёт три нижеследующие задачи и удаляет себя.
|
||||||
|
* - sul_rx_task — приём CAN → decode → controller. WDOG/heartbeat кормятся
|
||||||
|
* БЕЗУСЛОВНО; сама CAN-работа — под !g_menu_active (мягкая пауза на время
|
||||||
|
* меню — WDOG остаётся в безопасности по конструкции).
|
||||||
|
* - menu_task — модель меню: потребление кнопок, hold-to-enter,
|
||||||
|
* навигация/edit/exit+save. НЕ рисует.
|
||||||
|
* - render_task — единственный владелец дисплея/вызывающий gfx_present().
|
||||||
|
* Event-driven (будится xTaskNotifyGive от sul_rx_task, menu_task И
|
||||||
|
* 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 про это не знает.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef APP_TASKS_H_
|
||||||
|
#define APP_TASKS_H_
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "domain/controller.h" /* indication_task_t */
|
||||||
|
#include "domain/elevator_model.h" /* sul_result_t */
|
||||||
|
#include "menu/menu.h" /* menu_ctx_t */
|
||||||
|
#include "queue.h"
|
||||||
|
#include "task.h"
|
||||||
|
#include "timers.h"
|
||||||
|
#include "ui/fallback.h" /* dispatcher_indication_t */
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* ── Приоритеты (единая точка правды, tskIDLE_PRIORITY-относительно) ────────
|
||||||
|
*
|
||||||
|
* menu_task > render_task > sul_rx_task. Найдено на HW-верификации (см.
|
||||||
|
* PLAN.md): `bsp_can_receive()` — busy-spin БЕЗ yield (bsp/can/src/can.c,
|
||||||
|
* никаких блокирующих FreeRTOS-вызовов внутри `for(;;){poll;...}`); пока нет
|
||||||
|
* CAN-трафика, sul_rx_task занимает весь CAN_RX_TIMEOUT_MS каждую итерацию, и
|
||||||
|
* xTaskDelayUntil() в этом случае НЕ блокирует вовсе (дедлайн уже в прошлом —
|
||||||
|
* см. tasks.c, xShouldDelay остаётся false). Более низкоприоритетная задача
|
||||||
|
* никогда не получает CPU, пока такая задача продолжает быть READY —
|
||||||
|
* render_task, будучи ниже sul_rx_task, банально не мог выполниться (ни
|
||||||
|
* первый рендер при старте без связи, ни обработка «--» при обрыве связи).
|
||||||
|
* При живом трафике это было невидимо (receive почти всегда быстрый,
|
||||||
|
* sul_rx_task реально блокируется), поэтому не всплывало раньше.
|
||||||
|
*
|
||||||
|
* menu_task остаётся ВЫШЕ render_task: её собственный busy-wait внутри PXP
|
||||||
|
* (gfx_pxp_run, ~60-100 мс по замерам) не должен придерживать ввод — то же
|
||||||
|
* свойство, ради которого меню и рендер разведены по разным задачам.
|
||||||
|
* bringup_task — выше всех троих (монополизирует CPU на время одноразовой
|
||||||
|
* инициализации, пока остальные задачи ещё не созданы). Демон программных
|
||||||
|
* таймеров (bsp_button debounce, input_poll_cb) — отдельно, на
|
||||||
|
* configTIMER_TASK_PRIORITY (см. FreeRTOSConfig.h — наивысший в системе), не
|
||||||
|
* отсюда.
|
||||||
|
*
|
||||||
|
* Корень (busy-spin в bsp_can_receive без yield) НЕ тронут — это отдельный,
|
||||||
|
* более рискованный шаг (bsp_can — общий модуль, используется и bare-metal
|
||||||
|
* таргетами; добавить FreeRTOS-yield внутрь потребует условной компиляции,
|
||||||
|
* как bsp_tick/log_mutex). Обсудить отдельно, если приоритетов недостаточно. */
|
||||||
|
#define APP_PRIORITY_BRINGUP (tskIDLE_PRIORITY + 4U)
|
||||||
|
#define APP_PRIORITY_MENU (tskIDLE_PRIORITY + 3U)
|
||||||
|
#define APP_PRIORITY_RENDER (tskIDLE_PRIORITY + 2U)
|
||||||
|
#define APP_PRIORITY_SUL_RX (tskIDLE_PRIORITY + 1U)
|
||||||
|
|
||||||
|
/** Общий стек-бюджет app-тасков (см. PLAN.md, Фаза 0 — тонкий стек уже
|
||||||
|
* маскировался под похожий на зависание симптом; x8 — с запасом,
|
||||||
|
* проверено на всех четырёх ролях). */
|
||||||
|
#define APP_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 8U)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Самое свежее состояние индикации (не история): sul_rx_task → render_task.
|
||||||
|
*
|
||||||
|
* Очередь глубины 1 с xQueueOverwrite() — важно только ПОСЛЕДНЕЕ состояние,
|
||||||
|
* не промежуточные кадры (рендер не обязан успевать за каждым). Пробуждение
|
||||||
|
* render_task — отдельно, через xTaskNotifyGive (см. g_render_task_handle).
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
indication_task_t task;
|
||||||
|
sul_result_t result;
|
||||||
|
} render_msg_t;
|
||||||
|
|
||||||
|
/** Очередь глубины 1 (создаётся в main). Продюсер — sul_rx_task, консюмер — render_task. */
|
||||||
|
extern QueueHandle_t g_render_queue;
|
||||||
|
|
||||||
|
/** Дисплей+CAN подняты (bring-up в bringup_task) — sul_rx_task/render_task ждут этого. */
|
||||||
|
extern volatile bool g_display_ready;
|
||||||
|
|
||||||
|
/** true, пока меню открыто. Мягкая пауза sul_rx_task (CAN-decode/controller/
|
||||||
|
* очередь пропускаются, WDOG/heartbeat — нет, см. task_sul_rx.c). Единственный
|
||||||
|
* писатель — menu_task, единственный читатель — sul_rx_task. */
|
||||||
|
extern volatile bool g_menu_active;
|
||||||
|
|
||||||
|
/** Модель меню. Владеет и мутирует ТОЛЬКО menu_task; render_task только читает
|
||||||
|
* для отрисовки (после xTaskNotifyGive — happens-before через нотификацию,
|
||||||
|
* как у g_render_queue). */
|
||||||
|
extern menu_ctx_t g_menu;
|
||||||
|
|
||||||
|
/** Хэндл render_task — sul_rx_task, menu_task и dispatcher-колбэк opto
|
||||||
|
* (MPSC-продюсеры, §3.4) будят его xTaskNotifyGive() на любое изменение
|
||||||
|
* состояния. Устанавливается bringup_task ДО создания sul_rx_task/menu_task. */
|
||||||
|
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),
|
||||||
|
* затем создаёт sul_rx_task/menu_task/render_task и удаляет себя. */
|
||||||
|
void bringup_task(void *p_arg);
|
||||||
|
|
||||||
|
/** Приём CAN + WDOG/heartbeat (безусловно) + decode/controller (под !g_menu_active). */
|
||||||
|
void sul_rx_task(void *p_arg);
|
||||||
|
|
||||||
|
/** Модель меню: потребление кнопок, hold-to-enter, навигация/edit/exit+save. */
|
||||||
|
void menu_task(void *p_arg);
|
||||||
|
|
||||||
|
/** Презентация: владелец дисплея, единственный вызывающий gfx_present(). */
|
||||||
|
void render_task(void *p_arg);
|
||||||
|
|
||||||
|
/** Колбэк софт-таймера debounce (bsp_button_poll + bsp_opto_process, §3.4) —
|
||||||
|
* main создаёт таймер, каденция в main. */
|
||||||
|
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, безопасно звать многократно) —
|
||||||
|
* общая для bringup_task (before/after-confirm) и sul_rx_task (периодический
|
||||||
|
* re-log). Определена в task_bringup.c. */
|
||||||
|
void log_slot_status(const char *p_when);
|
||||||
|
|
||||||
|
#endif /* APP_TASKS_H_ */
|
||||||
15
firmware/tft_app/src/app/boot_shims.c
Normal file
15
firmware/tft_app/src/app/boot_shims.c
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* boot_shims.c — заглушки символов, которые дёргает общий с bootloader
|
||||||
|
* flash_map_backend.c, но которые не имеют смысла в контексте app.
|
||||||
|
*
|
||||||
|
* flash_map_backend.c зовёт led_status_tick_install() из цикла поблочного
|
||||||
|
* erase, чтобы двигать LED-анимацию прогресса в bootloader. В app окно
|
||||||
|
* установки не открыто (LED-индикацией управляет прикладная логика), поэтому
|
||||||
|
* здесь это no-op — ровно как у test_stub, но без утаскивания led_status.c
|
||||||
|
* (и его зависимости от bare-metal bsp_tick, конфликтующего с FreeRTOS-SysTick).
|
||||||
|
*/
|
||||||
|
|
||||||
|
void led_status_tick_install(void)
|
||||||
|
{
|
||||||
|
/* no-op: у app нет окна LED-установки */
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
firmware/tft_app/src/app/main.c
Normal file
88
firmware/tft_app/src/app/main.c
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* @file main.c
|
||||||
|
* @brief tft_app — точка входа: board init, объекты, bringup_task, планировщик, хуки.
|
||||||
|
*
|
||||||
|
* ARCH §4: app-слой = задачи + wiring + main. main() создаёт только очередь,
|
||||||
|
* софт-таймер ввода и ОДНУ задачу (bringup_task) — она сама создаёт
|
||||||
|
* sul_rx_task/menu_task/render_task после одноразовой инициализации и
|
||||||
|
* удаляет себя (см. app_tasks.h — полный контракт между задачами).
|
||||||
|
*
|
||||||
|
* Фаза 0 (каркас, подтверждено на железе): образ линкуется как валидный
|
||||||
|
* MCUboot-слот (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует,
|
||||||
|
* WDOG кормится, образ подтверждает себя в рантайме — см. PLAN.md, Фаза 0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "app_tasks.h"
|
||||||
|
#include "board.h"
|
||||||
|
#include "bsp/button.h"
|
||||||
|
#include "bsp/led.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include "task.h"
|
||||||
|
#include "timers.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define INPUT_POLL_PERIOD_MS 5U /* софт-таймер опроса ввода (bsp_button_poll; opto — Фаза 3.4) */
|
||||||
|
|
||||||
|
/* ── Разделяемое состояние задач (объявления — app_tasks.h) ──────────────── */
|
||||||
|
|
||||||
|
volatile bool g_display_ready = false;
|
||||||
|
volatile bool g_menu_active = false;
|
||||||
|
QueueHandle_t g_render_queue = NULL;
|
||||||
|
TaskHandle_t g_render_task_handle = NULL;
|
||||||
|
volatile dispatcher_indication_t g_dispatcher_indication = DISPATCHER_INDICATION_NONE;
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */
|
||||||
|
bsp_led_init();
|
||||||
|
(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));
|
||||||
|
configASSERT(g_render_queue != NULL);
|
||||||
|
|
||||||
|
/* Опрос ввода — софт-таймер (демон на высшем приоритете в системе вытесняет
|
||||||
|
* всё остальное — нажатия не теряются). Масштабируется на opto (Фаза 3.4)
|
||||||
|
* тем же колбэком. */
|
||||||
|
TimerHandle_t input_timer =
|
||||||
|
xTimerCreate("input", pdMS_TO_TICKS(INPUT_POLL_PERIOD_MS), pdTRUE, NULL, input_poll_cb);
|
||||||
|
configASSERT(input_timer != NULL);
|
||||||
|
(void) xTimerStart(input_timer, 0);
|
||||||
|
|
||||||
|
/* Единственная задача, которую создаёт main — bringup_task сама создаст
|
||||||
|
* sul_rx_task/menu_task/render_task после инициализации и удалит себя
|
||||||
|
* (см. app_tasks.h про приоритеты/порядок). */
|
||||||
|
(void) xTaskCreate(bringup_task, "bringup", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_BRINGUP,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
vTaskStartScheduler();
|
||||||
|
|
||||||
|
/* Сюда планировщик не возвращается. Если вернулся — не хватило кучи под
|
||||||
|
* idle/timer задачу. WDOG сбросит плату. */
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── FreeRTOS hooks (строгая диагностика) ────────────────────────────────── */
|
||||||
|
|
||||||
|
void vApplicationStackOverflowHook(TaskHandle_t task, char *name)
|
||||||
|
{
|
||||||
|
(void) task;
|
||||||
|
(void) name;
|
||||||
|
taskDISABLE_INTERRUPTS();
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
/* WDOG сбросит плату — детерминированный отказ вместо тихой порчи. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void vApplicationMallocFailedHook(void)
|
||||||
|
{
|
||||||
|
taskDISABLE_INTERRUPTS();
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
198
firmware/tft_app/src/app/task_bringup.c
Normal file
198
firmware/tft_app/src/app/task_bringup.c
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
/**
|
||||||
|
* @file task_bringup.c
|
||||||
|
* @brief Одноразовое bring-up: лог-мьютекс, UART/лог, QSPI+settings+self-confirm,
|
||||||
|
* SDRAM+gfx+CAN. Затем создаёт steady-state задачи и удаляет себя.
|
||||||
|
*
|
||||||
|
* Слот образа ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной загрузке —
|
||||||
|
* anti-brick). Контракт с bootloader и обоснование self-confirm/QSPI-ramfunc/
|
||||||
|
* UART-диагностики — см. PLAN.md, Фаза 0.
|
||||||
|
*
|
||||||
|
* Выделено в отдельную задачу на Фазе 3.2.4 (было частью sul_rx_task) — по
|
||||||
|
* итогам HW-верификации: одноразовая init-последовательность архитектурно не
|
||||||
|
* то же самое, что вечный CAN-цикл, и разделение делает обе задачи проще
|
||||||
|
* контролировать по отдельности.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "app_tasks.h"
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "bootutil/bootutil_public.h"
|
||||||
|
#include "bsp/boot_state.h"
|
||||||
|
#include "bsp/display.h"
|
||||||
|
#include "bsp/qspi_flash.h"
|
||||||
|
#include "bsp/sdram.h"
|
||||||
|
#include "bsp/uart_host.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
#include "domain/sul/transport/can.h"
|
||||||
|
#include "flash_map.h"
|
||||||
|
#include "log/log.h"
|
||||||
|
#include "menu/menu_tree.h"
|
||||||
|
#include "port/log_uart.h"
|
||||||
|
#include "services/gfx.h"
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
#include "task.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define LOG_TAG "bringup"
|
||||||
|
|
||||||
|
/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */
|
||||||
|
#ifndef APP_OWN_SLOT_ID
|
||||||
|
#define APP_OWN_SLOT_ID 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Панель текущего стенда — Фаза 1 хардкод (Фаза 9: provisioning выбирает из
|
||||||
|
* settings, см. ARCH §9 — TFT7/8/10 рантайм-выбор внутри профиля app-big). */
|
||||||
|
#define PANEL_TYPE BSP_DISPLAY_TFT8
|
||||||
|
|
||||||
|
/* ── Диагностика трейлера слота (read-only, безопасно звать многократно).
|
||||||
|
* Общая для bringup_task (before/after-confirm) и sul_rx_task (periodic) —
|
||||||
|
* прототип в app_tasks.h. ──────────────────────────────────────────────── */
|
||||||
|
void log_slot_status(const char *p_when)
|
||||||
|
{
|
||||||
|
const struct flash_area *p_fap;
|
||||||
|
const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap);
|
||||||
|
if (RC_OPEN != 0)
|
||||||
|
{
|
||||||
|
LOG_E(LOG_TAG, "%s: flash_area_open(slot%d) rc=%d", p_when, APP_OWN_SLOT_ID, RC_OPEN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct boot_swap_state st = { 0 };
|
||||||
|
const int RC_RD = boot_read_swap_state(p_fap, &st);
|
||||||
|
LOG_I(LOG_TAG, "%s: slot%d magic=%d copy_done=%d image_ok=%d (rd=%d)", p_when, APP_OWN_SLOT_ID,
|
||||||
|
st.magic, st.copy_done, st.image_ok, RC_RD);
|
||||||
|
|
||||||
|
flash_area_close(p_fap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Подтвердить СОБСТВЕННЫЙ слот (APP_OWN_SLOT_ID).
|
||||||
|
*
|
||||||
|
* boot_set_next(fap, active=true, confirm=true), НЕ boot_set_confirmed(): та
|
||||||
|
* жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от исполняемого
|
||||||
|
* слота — для Direct-XIP с двумя слотами это подтвердило бы не тот при
|
||||||
|
* исполнении из Slot Б.
|
||||||
|
*/
|
||||||
|
static void confirm_self(void)
|
||||||
|
{
|
||||||
|
log_slot_status("before-confirm"); /* ожидаем magic=1(GOOD) image_ok=3(UNSET) */
|
||||||
|
|
||||||
|
const struct flash_area *p_fap;
|
||||||
|
const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap);
|
||||||
|
if (RC_OPEN != 0)
|
||||||
|
{
|
||||||
|
LOG_E(LOG_TAG, "confirm: flash_area_open(slot%d) rc=%d", APP_OWN_SLOT_ID, RC_OPEN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int RC_SET = boot_set_next(p_fap, true, true);
|
||||||
|
LOG_I(LOG_TAG, "confirm: boot_set_next rc=%d", RC_SET);
|
||||||
|
|
||||||
|
flash_area_close(p_fap);
|
||||||
|
|
||||||
|
log_slot_status("after-confirm"); /* ожидаем image_ok=1(SET) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief SDRAM (SEMC, без DCD) + компоновщик gfx + CAN — перед steady-state задачами.
|
||||||
|
*
|
||||||
|
* bsp_sdram_configure()+init() — та же документированная в bsp/sdram.h связка
|
||||||
|
* для «прошивок без DCD», которую использует и bootloader-smoke-test.
|
||||||
|
*/
|
||||||
|
static bool bring_up_display_and_can(void)
|
||||||
|
{
|
||||||
|
if (bsp_sdram_configure() != BSP_OK)
|
||||||
|
{
|
||||||
|
LOG_E(LOG_TAG, "bsp_sdram_configure FAILED");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bsp_sdram_init() != BSP_OK)
|
||||||
|
{
|
||||||
|
LOG_E(LOG_TAG, "bsp_sdram_init FAILED");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (gfx_init(PANEL_TYPE) != BSP_OK)
|
||||||
|
{
|
||||||
|
LOG_E(LOG_TAG, "gfx_init FAILED");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sul_transport_can_init() != BSP_OK)
|
||||||
|
{
|
||||||
|
LOG_E(LOG_TAG, "sul_transport_can_init FAILED");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_I(LOG_TAG, "display+CAN bring-up OK");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bringup_task(void *p_arg)
|
||||||
|
{
|
||||||
|
(void) p_arg;
|
||||||
|
|
||||||
|
/* ДО первого LOG_* где-либо в системе — log_write() берёт мьютекс
|
||||||
|
* безусловно (port/log/src/log_mutex.c, strong-override weak-NOP).
|
||||||
|
* bringup_task — первая и пока единственная запущенная задача, так что
|
||||||
|
* порядок гарантирован конструкцией (main создаёт только её). */
|
||||||
|
log_mutex_init();
|
||||||
|
|
||||||
|
/* LPUART1/MCU-Link VCOM — доступен сразу, без enumeration/wait (в отличие
|
||||||
|
* от target-side USB CDC). */
|
||||||
|
(void) bsp_uart_host_init(115200U);
|
||||||
|
log_uart_init();
|
||||||
|
|
||||||
|
/* flash_map_backend требует bsp_qspi_init() ДО любой flash_area_*. */
|
||||||
|
const bool QSPI_OK = (bsp_qspi_init() == BSP_OK);
|
||||||
|
LOG_I(LOG_TAG, "tft_app boot: qspi=%s", QSPI_OK ? "OK" : "FAIL");
|
||||||
|
|
||||||
|
/* Настройки ядра (§8, §10): с флеша если QSPI поднялся, иначе дефолты. */
|
||||||
|
if (QSPI_OK)
|
||||||
|
{
|
||||||
|
const bsp_status_t S_RC = settings_store_load();
|
||||||
|
LOG_I(LOG_TAG, "settings: load rc=%d proto_addr=%u", S_RC,
|
||||||
|
settings_store_get()->user.proto_slice[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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. Безусловно, до потенциально
|
||||||
|
* рискованного bring-up дисплея/CAN ниже. */
|
||||||
|
bsp_boot_health_mark();
|
||||||
|
|
||||||
|
if (QSPI_OK)
|
||||||
|
{
|
||||||
|
confirm_self();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_E(LOG_TAG, "qspi_init FAILED — self-confirm skipped, slot will revert");
|
||||||
|
}
|
||||||
|
|
||||||
|
g_display_ready = bring_up_display_and_can(); /* sul_rx_task/render_task ждут этого */
|
||||||
|
|
||||||
|
/* render_task — первым: его хэндл нужен sul_rx_task/menu_task для
|
||||||
|
* xTaskNotifyGive. Порядок формально некритичен (bringup_task —
|
||||||
|
* наивысший приоритет из четырёх и монополизирует CPU до своего
|
||||||
|
* удаления — никто из троих не может выполниться раньше, чем все три
|
||||||
|
* xTaskCreate() ниже отработают), но документирует зависимость явно. */
|
||||||
|
(void) xTaskCreate(render_task, "render", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_RENDER,
|
||||||
|
&g_render_task_handle);
|
||||||
|
(void) xTaskCreate(sul_rx_task, "sul_rx", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_SUL_RX, NULL);
|
||||||
|
(void) xTaskCreate(menu_task, "menu", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_MENU, NULL);
|
||||||
|
|
||||||
|
vTaskDelete(NULL); /* одноразовая задача — дальше нечего делать */
|
||||||
|
}
|
||||||
129
firmware/tft_app/src/app/task_menu.c
Normal file
129
firmware/tft_app/src/app/task_menu.c
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/**
|
||||||
|
* @file task_menu.c
|
||||||
|
* @brief Модель меню: потребление кнопок (debounce — bsp_button/софт-таймер,
|
||||||
|
* независимо от этой задачи), мгновенные вход/навигация/edit/exit+save.
|
||||||
|
*
|
||||||
|
* Раскладка — как в OLD_PROJECT_TFT8_UKL (без удержания, короткие нажатия):
|
||||||
|
* короткое BUTTON_1 = вход в меню (когда закрыто) / следующий пункт (когда
|
||||||
|
* открыто); короткое BUTTON_2 = выбор/действие (когда открыто), намеренный
|
||||||
|
* no-op вне меню.
|
||||||
|
*
|
||||||
|
* НЕ рисует — мутирует `g_menu` и будит render_task (xTaskNotifyGive) на любое
|
||||||
|
* изменение состояния. Разделено от render_task на Фазе 3.2.4 (HW-находка):
|
||||||
|
* раньше (3.2.1–3.2.3, один framebuffer, без ожиданий) потребление кнопок и
|
||||||
|
* рендер жили в одной задаче безвредно — блокировок не было. gfx_present()
|
||||||
|
* (double-buffer + PXP) внёс блокирующее ожидание кадра; в объединённой
|
||||||
|
* задаче это ожидание попутно блокировало вход в меню (ноль реакции на
|
||||||
|
* кнопки — см. PLAN.md). Эталон разделения — OLD_PROJECT_TFT8_UKL:
|
||||||
|
* BUTTONS_TASK/menu_task отдельно от REFRESH_TASK/tft_refresh_task.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "app_tasks.h"
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "bsp/button.h"
|
||||||
|
#include "bsp/opto.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
#include "log/log.h"
|
||||||
|
#include "menu/menu.h"
|
||||||
|
#include "menu/menu_tree.h"
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
#include "task.h"
|
||||||
|
#include "timers.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define LOG_TAG "menu"
|
||||||
|
|
||||||
|
#define MENU_TICK_MS 5U /* каденция потребления кнопок */
|
||||||
|
|
||||||
|
menu_ctx_t g_menu;
|
||||||
|
|
||||||
|
/* Софт-таймер (высший приоритет демона) опрашивает debounce независимо от
|
||||||
|
* menu_task/render_task — нажатия не теряются, пока кто-то из них занят.
|
||||||
|
* Колбэк короткий, без блокировок. */
|
||||||
|
void input_poll_cb(TimerHandle_t x_timer)
|
||||||
|
{
|
||||||
|
(void) x_timer;
|
||||||
|
bsp_button_poll();
|
||||||
|
bsp_opto_process(); /* debounce IN1/IN2 (§3.4) — короткая, как button */
|
||||||
|
dispatcher_poll(); /* непрерывный опрос bsp_opto_read(), не колбэк — см. dispatcher.c */
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_task(void *p_arg)
|
||||||
|
{
|
||||||
|
(void) p_arg;
|
||||||
|
|
||||||
|
menu_init(&g_menu, menu_tree_items(), menu_tree_count(), settings_store_get_mutable());
|
||||||
|
|
||||||
|
TickType_t last_wake = xTaskGetTickCount();
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
/* Порядок и структура — как в OLD_PROJECT_TFT8_UKL menu_task():
|
||||||
|
* BUTTON_2 читается и обрабатывается только "внутри", BUTTON_1 —
|
||||||
|
* читается всегда (вход ИЛИ навигация в зависимости от open). Оба
|
||||||
|
* события дренируются безусловно (один get_event_pressed на кнопку
|
||||||
|
* за итерацию) — не копится устаревшее состояние. */
|
||||||
|
if (bsp_button_get_event_pressed(BSP_BUTTON_2))
|
||||||
|
{
|
||||||
|
if (menu_is_open(&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() (флеш-запись, не
|
||||||
|
* мгновенная) — иначе sul_rx_task ещё несколько мс видел бы
|
||||||
|
* устаревший g_menu_active=true и держал бы мягкую паузу
|
||||||
|
* дольше нужного. */
|
||||||
|
g_menu_active = menu_is_open(&g_menu);
|
||||||
|
|
||||||
|
if (!g_menu_active && g_menu.save_requested)
|
||||||
|
{
|
||||||
|
/* Выход: сохранить (если менялось). Адрес подхватит
|
||||||
|
* sul_rx_task из настроек на следующей итерации. */
|
||||||
|
const bsp_status_t RC = settings_store_save();
|
||||||
|
LOG_I(LOG_TAG, "settings saved rc=%d", RC);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
/* иначе — намеренный no-op вне меню (как в референсе) */
|
||||||
|
}
|
||||||
|
else if (bsp_button_get_event_pressed(BSP_BUTTON_1))
|
||||||
|
{
|
||||||
|
if (menu_is_open(&g_menu))
|
||||||
|
{
|
||||||
|
menu_next(&g_menu);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
menu_open(&g_menu); /* мгновенный вход, без удержания */
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Финальная синхронизация флага мягкой паузы с моделью — покрывает
|
||||||
|
* вход/навигацию (выход уже обновил его выше, до save()). */
|
||||||
|
g_menu_active = menu_is_open(&g_menu);
|
||||||
|
|
||||||
|
if (changed && (g_render_task_handle != NULL))
|
||||||
|
{
|
||||||
|
(void) xTaskNotifyGive(g_render_task_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(MENU_TICK_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
122
firmware/tft_app/src/app/task_render.c
Normal file
122
firmware/tft_app/src/app/task_render.c
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
/**
|
||||||
|
* @file task_render.c
|
||||||
|
* @brief Презентация: единственный владелец дисплея и вызывающий gfx_present*().
|
||||||
|
*
|
||||||
|
* Event-driven (xTaskNotifyGive от sul_rx_task, menu_task И dispatcher-колбэка
|
||||||
|
* opto §3.4 — MPSC, три продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает
|
||||||
|
* несколько notify в одно пробуждение — важно только «есть свежее состояние»,
|
||||||
|
* не сколько раз оно менялось). НЕ содержит кнопочной логики — разделено от
|
||||||
|
* menu_task на Фазе 3.2.4 (см. task_menu.c про причину).
|
||||||
|
*
|
||||||
|
* Меню — оконный рендер (Фаза 3.2.4, ускорение навигации): на ОТКРЫТИИ — полная
|
||||||
|
* очистка AS (стереть индикацию) + два полных gfx_present() подряд (double
|
||||||
|
* buffering: оба FB обязаны получить корректный кадр вне окна — контракт
|
||||||
|
* gfx_present_rect); дальше НАВИГАЦИЯ — перерисовка и композит только окна
|
||||||
|
* 480×272 (~27% кадра, пропорционально дешевле). Индикация — полные кадры,
|
||||||
|
* как и была.
|
||||||
|
*
|
||||||
|
* Диспетчерский вход (§3.4) — как в OLD_PROJECT_TFT8_UKL: пока меню открыто,
|
||||||
|
* индикация вообще не трогается (dispatcher копится в g_dispatcher_indication,
|
||||||
|
* применяется одним махом при закрытии — тем же путём, что уже восстанавливает
|
||||||
|
* индикацию после меню). Вне меню — САМОСТОЯТЕЛЬНАЯ третья причина перерисовки
|
||||||
|
* (не только новое сообщение в очереди/закрытие меню): opto может разбудить
|
||||||
|
* render_task без единого нового кадра СУЛ.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "app_tasks.h"
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include "services/gfx.h"
|
||||||
|
#include "task.h"
|
||||||
|
#include "ui/fallback.h"
|
||||||
|
#include "ui/menu_view.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define DISPLAY_WAIT_POLL_MS 5U /* пока g_display_ready не выставлен bringup_task'ом */
|
||||||
|
|
||||||
|
void render_task(void *p_arg)
|
||||||
|
{
|
||||||
|
(void) p_arg;
|
||||||
|
|
||||||
|
while (!g_display_ready)
|
||||||
|
{
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(DISPLAY_WAIT_POLL_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
sul_result_t last = sul_default_state();
|
||||||
|
ui_fallback_render_initial(&last, g_dispatcher_indication);
|
||||||
|
gfx_present();
|
||||||
|
|
||||||
|
bool was_menu_open = false;
|
||||||
|
dispatcher_indication_t last_dispatcher = g_dispatcher_indication;
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
(void) ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
|
||||||
|
const bool MENU_OPEN_NOW = menu_is_open(&g_menu);
|
||||||
|
|
||||||
|
if (MENU_OPEN_NOW)
|
||||||
|
{
|
||||||
|
if (!was_menu_open)
|
||||||
|
{
|
||||||
|
/* Открытие: стереть индикацию из ВСЕГО AS и прогнать полный
|
||||||
|
* кадр в ОБА FB — после этого вне окна оба буфера корректны
|
||||||
|
* (чёрные), и навигация может обновлять только окно. */
|
||||||
|
gfx_clear();
|
||||||
|
menu_view_render(&g_menu);
|
||||||
|
gfx_present();
|
||||||
|
gfx_present(); /* тот же AS — во второй FB (double buffering) */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Навигация/правка: только окно (~27% кадра). */
|
||||||
|
menu_view_render(&g_menu);
|
||||||
|
gfx_present_rect(0U, 0U, MENU_VIEW_WIN_W, MENU_VIEW_WIN_H);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool present_needed = false;
|
||||||
|
|
||||||
|
/* Снимок один раз за итерацию — 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) — восстановить индикацию последним
|
||||||
|
* известным состоянием СУЛ + ТЕКУЩИМ dispatcher немедленно,
|
||||||
|
* не дожидаясь свежего кадра (sul_rx_task мог простаивать
|
||||||
|
* под g_menu_active — очередь пока пуста). */
|
||||||
|
ui_fallback_render_initial(&last, DISPATCHER_NOW);
|
||||||
|
present_needed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_msg_t msg;
|
||||||
|
if (xQueueReceive(g_render_queue, &msg, 0) == pdTRUE)
|
||||||
|
{
|
||||||
|
last = msg.result;
|
||||||
|
ui_fallback_render(&msg.task, &msg.result, DISPATCHER_NOW);
|
||||||
|
present_needed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (present_needed)
|
||||||
|
{
|
||||||
|
gfx_present(); /* индикация — всегда полный кадр */
|
||||||
|
}
|
||||||
|
|
||||||
|
last_dispatcher = DISPATCHER_NOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
was_menu_open = MENU_OPEN_NOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
209
firmware/tft_app/src/app/task_sul_rx.c
Normal file
209
firmware/tft_app/src/app/task_sul_rx.c
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
/**
|
||||||
|
* @file task_sul_rx.c
|
||||||
|
* @brief Приём активного протокола (реестр sul, §8) → decode → controller →
|
||||||
|
* уведомление render_task. Протокол-специфичный транспорт (CAN/демо)
|
||||||
|
* выбирается здесь по id активного драйвера — единственное место,
|
||||||
|
* которое трогает Фаза 8 при добавлении протокола на новом транспорте.
|
||||||
|
*
|
||||||
|
* WDOG/heartbeat/периодический re-log трейлера — БЕЗУСЛОВНО (housekeeping,
|
||||||
|
* не протокол-специфика). Сам приём (decode/controller/очередь) — под
|
||||||
|
* `!g_menu_active`: мягкая пауза на время меню (см. app_tasks.h) — задача НЕ
|
||||||
|
* suspend'ится, поэтому WDOG остаётся в безопасности по конструкции.
|
||||||
|
*
|
||||||
|
* Bring-up (QSPI/settings/self-confirm/SDRAM+gfx+CAN) — bringup_task
|
||||||
|
* (task_bringup.c); эта задача стартует уже после него, `g_display_ready`
|
||||||
|
* решает, делать ли CAN-работу вообще.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "app_tasks.h"
|
||||||
|
#include "bsp/led.h"
|
||||||
|
#include "bsp/wdog.h"
|
||||||
|
#include "domain/controller.h"
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
#include "domain/sul/demo.h"
|
||||||
|
#include "domain/sul/nku_can.h"
|
||||||
|
#include "domain/sul/transport/can.h"
|
||||||
|
#include "domain/sul/transport/demo.h"
|
||||||
|
#include "log/log.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
#include "task.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define LOG_TAG "sul_rx"
|
||||||
|
|
||||||
|
#define HEARTBEAT_PERIOD_MS 500U
|
||||||
|
#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */
|
||||||
|
#define STATUS_LOG_PERIOD_MS \
|
||||||
|
2000U /* периодический re-log трейлера — виден независимо
|
||||||
|
* от момента подключения терминала */
|
||||||
|
#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */
|
||||||
|
/* «Пропадание трафика» — см. ARCH, поток данных: poll + timeout→default.
|
||||||
|
* Порог — СВОЙСТВО ПРОТОКОЛА (p_driver->connection_timeout_ms, domain/sul.h),
|
||||||
|
* не константа здесь: разные станции шлют раз в ~200 мс или раз в ~1 с, а
|
||||||
|
* некоторые — только по изменению состояния (тогда таймаут вообще отключён,
|
||||||
|
* SUL_CONNECTION_TIMEOUT_DISABLED) — не пользовательская настройка, задаётся
|
||||||
|
* протоколом при регистрации в sul_registry.c. Меряется от last_frame_tick —
|
||||||
|
* пауза меню не портит логику: если трафик реально стоял, «--» появится сразу
|
||||||
|
* по возврату из меню; если шёл — SINCE_FRAME_MS обнулится первым же кадром. */
|
||||||
|
|
||||||
|
void sul_rx_task(void *p_arg)
|
||||||
|
{
|
||||||
|
(void) p_arg;
|
||||||
|
|
||||||
|
/* Ctx каждого зарегистрированного драйвера — постоянно, живёт в
|
||||||
|
* sul_registry.c (domain/sul.h, .p_ctx); переключение протокола не
|
||||||
|
* пересоздаёт его, только меняет, какой из них активен (§8). */
|
||||||
|
sul_registry_init();
|
||||||
|
|
||||||
|
controller_ctx_t ctrl_ctx;
|
||||||
|
controller_init(&ctrl_ctx);
|
||||||
|
|
||||||
|
const TickType_t FEED_PERIOD = pdMS_TO_TICKS(WDOG_FEED_PERIOD_MS);
|
||||||
|
uint32_t elapsed_ms = 0U;
|
||||||
|
uint32_t since_status_ms = 0U;
|
||||||
|
TickType_t last_wake = xTaskGetTickCount();
|
||||||
|
TickType_t last_frame_tick = xTaskGetTickCount();
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
bsp_wdog_refresh();
|
||||||
|
|
||||||
|
elapsed_ms += WDOG_FEED_PERIOD_MS;
|
||||||
|
if (elapsed_ms >= HEARTBEAT_PERIOD_MS)
|
||||||
|
{
|
||||||
|
elapsed_ms = 0U;
|
||||||
|
bsp_led_toggle(LED_APP);
|
||||||
|
}
|
||||||
|
|
||||||
|
since_status_ms += WDOG_FEED_PERIOD_MS;
|
||||||
|
if (since_status_ms >= STATUS_LOG_PERIOD_MS)
|
||||||
|
{
|
||||||
|
since_status_ms = 0U;
|
||||||
|
log_slot_status("periodic");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_display_ready && !g_menu_active)
|
||||||
|
{
|
||||||
|
const sul_driver_t *p_driver = sul_registry_active();
|
||||||
|
|
||||||
|
/* Транспорт + реаппликация настроек протокола — ЗДЕСЬ (app-слой =
|
||||||
|
* wiring, ARCH §4), не в домене/реестре: держит sul_driver_t.p_ctx
|
||||||
|
* и sul_registry_* host-тестируемыми без bsp. Новый протокол на
|
||||||
|
* новом транспорте (Фаза 8: УИМ/SD7/УЭЛ/УКЛ, в основном UART) —
|
||||||
|
* новая ветка здесь; decode/меню/настройки не трогаются.
|
||||||
|
*
|
||||||
|
* НКУ-CAN: адрес из настроек (proto_slice[0]) — подхватывает
|
||||||
|
* правку из меню без межзадачного сигнала (запись/чтение uint8
|
||||||
|
* атомарны). Обе стороны: decode-ctx И HW RX-фильтры FlexCAN —
|
||||||
|
* вторая без переприменения фильтров под реальный адрес станции
|
||||||
|
* кадры с адресом != 0 отбрасывались бы CAN-контроллером ещё до
|
||||||
|
* 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];
|
||||||
|
nku_can_set_address((nku_can_ctx_t *) p_driver->p_ctx, NKU_ADDR);
|
||||||
|
(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;
|
||||||
|
bool have_update = false;
|
||||||
|
|
||||||
|
if (rx_rc == BSP_OK)
|
||||||
|
{
|
||||||
|
if (p_driver->decode(p_driver->p_ctx, &frame, &decoded) == SUL_STATUS_OK)
|
||||||
|
{
|
||||||
|
last_frame_tick = xTaskGetTickCount();
|
||||||
|
have_update = true;
|
||||||
|
}
|
||||||
|
/* 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 =
|
||||||
|
(uint32_t) (xTaskGetTickCount() - last_frame_tick) * portTICK_PERIOD_MS;
|
||||||
|
if (SINCE_FRAME_MS >= p_driver->connection_timeout_ms)
|
||||||
|
{
|
||||||
|
/* poll + timeout→default (ARCH, поток данных) — controller
|
||||||
|
* сам определит, реальное ли это изменение (не сработает
|
||||||
|
* повторно на каждой итерации после перехода в default). */
|
||||||
|
decoded = sul_default_state();
|
||||||
|
have_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* иначе — протокол event-driven на станции (шлёт только по
|
||||||
|
* изменению), детекция обрыва по тишине для него некорректна —
|
||||||
|
* last_frame_tick тем не менее продолжает обновляться выше на
|
||||||
|
* каждый валидный кадр, просто здесь не используется. */
|
||||||
|
|
||||||
|
if (have_update)
|
||||||
|
{
|
||||||
|
|
||||||
|
const indication_task_t DIFF = controller_process(&ctrl_ctx, &decoded);
|
||||||
|
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 };
|
||||||
|
(void) xQueueOverwrite(g_render_queue, &MSG);
|
||||||
|
if (g_render_task_handle != NULL)
|
||||||
|
{
|
||||||
|
(void) xTaskNotifyGive(g_render_task_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelayUntil(&last_wake, FEED_PERIOD);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
firmware/tft_app/src/domain/CMakeLists.txt
Normal file
3
firmware/tft_app/src/domain/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
add_subdirectory(elevator_model)
|
||||||
|
add_subdirectory(sul)
|
||||||
|
add_subdirectory(controller)
|
||||||
3
firmware/tft_app/src/domain/README.md
Normal file
3
firmware/tft_app/src/domain/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# domain
|
||||||
|
|
||||||
|
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.
|
||||||
5
firmware/tft_app/src/domain/controller/CMakeLists.txt
Normal file
5
firmware/tft_app/src/domain/controller/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_controller STATIC src/controller.c src/mode_priority.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_controller PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_controller PUBLIC tft_app_elevator_model)
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* @file controller.h
|
||||||
|
* @brief Редьюсер: sul_result_t (+кэш) → indication_task_t (diff).
|
||||||
|
*
|
||||||
|
* Чистый C, без единого HAL-вызова. НЕ владеет временем/таймаутами сам —
|
||||||
|
* caller (app-level sul_rx задача, у неё есть доступ к тику) при обнаружении
|
||||||
|
* таймаута связи просто вызывает controller_process() с sul_default_state(),
|
||||||
|
* тем же путём, что и любой обычный кадр — специального API для timeout не
|
||||||
|
* нужно (см. ARCH.md, поток данных: «poll + timeout→default» — это
|
||||||
|
* обязанность вызывающей задачи, не контроллера).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_CONTROLLER_H_
|
||||||
|
#define DOMAIN_CONTROLLER_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Состояние редьюсера — кэш последнего примененного результата. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
sul_result_t cache;
|
||||||
|
} controller_ctx_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Что изменилось с прошлого вызова — presentation перерисовывает
|
||||||
|
* только помеченные поля, audio_policy (Фаза 6) озвучивает изменившиеся
|
||||||
|
* события поверх этого diff.
|
||||||
|
*
|
||||||
|
* mode_pending — сменился РАЗРЕШЁННЫЙ экранный режим (свёртка сигналов по
|
||||||
|
* таблице приоритетов, §7), а не отдельный сырой сигнал. arrival/movement —
|
||||||
|
* уровни из декодера; их edge (false→true) — сигнал к озвучке (гонг, старт).
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
bool pos_pending;
|
||||||
|
bool next_pending;
|
||||||
|
bool direction_pending;
|
||||||
|
bool mode_pending;
|
||||||
|
bool arrival_pending;
|
||||||
|
bool movement_pending;
|
||||||
|
|
||||||
|
sul_mode_t mode; /**< разрешённый режим нового результата — presentation
|
||||||
|
читает его при mode_pending (не нужен отдельный getter) */
|
||||||
|
} indication_task_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сброс кэша к sul_default_state().
|
||||||
|
*
|
||||||
|
* Кэш засеивается дефолтом ДО первого реального кадра — если первый кадр от
|
||||||
|
* СУЛ совпадёт с дефолтом (напр. пришёл со всё ещё пустой позицией), diff
|
||||||
|
* будет "ничего не изменилось". Первую отрисовку (пустой экран → что-то на
|
||||||
|
* экране) presentation обязана сделать безусловно один раз при старте, не
|
||||||
|
* дожидаясь pending-флагов (см. app: render-задача).
|
||||||
|
*/
|
||||||
|
void controller_init(controller_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Применить новый результат, вернуть diff относительно кэша.
|
||||||
|
*
|
||||||
|
* Обновляет кэш на p_result безусловно (следующий вызов будет сравнивать
|
||||||
|
* именно с этим состоянием).
|
||||||
|
*/
|
||||||
|
indication_task_t controller_process(controller_ctx_t *p_ctx, const sul_result_t *p_result);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_CONTROLLER_H_ */
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* @file mode_priority.h
|
||||||
|
* @brief Свёртка ортогональных сигналов sul_result_t в один экранный режим
|
||||||
|
* по таблице приоритетов (ARCH.md §7).
|
||||||
|
*
|
||||||
|
* Таблица — ЧИСТЫЕ ДАННЫЕ (offset-массив в mode_priority.c), «потенциально
|
||||||
|
* клиентские» (§7): приоритет задаётся ПОРЯДКОМ строк, не значениями enum.
|
||||||
|
* Менять приоритет = переставить строки; добавить режим = дописать строку;
|
||||||
|
* резолвер при этом не трогается. Контроллер применяет активную таблицу.
|
||||||
|
*
|
||||||
|
* Чистый C, host-тестируется (tests/host/tft_app_controller).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_MODE_PRIORITY_H_
|
||||||
|
#define DOMAIN_MODE_PRIORITY_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Разрешить экранный режим по активной таблице приоритетов.
|
||||||
|
*
|
||||||
|
* Проходит таблицу сверху вниз, возвращает режим первой строки, чей булев
|
||||||
|
* сигнал в @p p_result выставлен. Если ни один спецрежим не активен —
|
||||||
|
* SUL_MODE_NORMAL.
|
||||||
|
*/
|
||||||
|
sul_mode_t sul_resolve_mode(const sul_result_t *p_result);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_MODE_PRIORITY_H_ */
|
||||||
31
firmware/tft_app/src/domain/controller/src/controller.c
Normal file
31
firmware/tft_app/src/domain/controller/src/controller.c
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#include "domain/controller.h"
|
||||||
|
|
||||||
|
#include "domain/mode_priority.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void controller_init(controller_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
p_ctx->cache = sul_default_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
indication_task_t controller_process(controller_ctx_t *p_ctx, const sul_result_t *p_result)
|
||||||
|
{
|
||||||
|
const sul_mode_t PREV_MODE = sul_resolve_mode(&p_ctx->cache);
|
||||||
|
const sul_mode_t NEW_MODE = sul_resolve_mode(p_result);
|
||||||
|
|
||||||
|
const indication_task_t TASK = {
|
||||||
|
.pos_pending = (strcmp(p_ctx->cache.pos, p_result->pos) != 0),
|
||||||
|
.next_pending = (strcmp(p_ctx->cache.next, p_result->next) != 0),
|
||||||
|
.direction_pending = (p_ctx->cache.direction != p_result->direction),
|
||||||
|
.mode_pending = (PREV_MODE != NEW_MODE),
|
||||||
|
/* Событийные уровни — presentation/audio реагируют на фронт false→true. */
|
||||||
|
.arrival_pending = (!p_ctx->cache.arrival && p_result->arrival),
|
||||||
|
.movement_pending = (!p_ctx->cache.movement && p_result->movement),
|
||||||
|
.mode = NEW_MODE,
|
||||||
|
};
|
||||||
|
|
||||||
|
p_ctx->cache = *p_result;
|
||||||
|
|
||||||
|
return TASK;
|
||||||
|
}
|
||||||
52
firmware/tft_app/src/domain/controller/src/mode_priority.c
Normal file
52
firmware/tft_app/src/domain/controller/src/mode_priority.c
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#include "domain/mode_priority.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Строка таблицы приоритетов: булев сигнал (по смещению в sul_result_t)
|
||||||
|
* → экранный режим.
|
||||||
|
*
|
||||||
|
* Смещение вместо указателя-на-член (в C его нет) — читаем bool по
|
||||||
|
* offsetof, что делает таблицу голыми данными без кода на строку.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
size_t flag_offset; /**< offsetof(sul_result_t, <bool-поле>) */
|
||||||
|
sul_mode_t mode;
|
||||||
|
} mode_rule_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ТАБЛИЦА ПРИОРИТЕТОВ (ARCH §7). Порядок = приоритет, сверху вниз.
|
||||||
|
* Согласовано: fireman (высший) > пожар > перегруз > сейсмо > сервис >
|
||||||
|
* погрузка > (норма — когда ни одна строка не сработала).
|
||||||
|
*
|
||||||
|
* Изменить приоритет — переставить строки. Добавить режим — дописать строку
|
||||||
|
* (offset нового bool-сигнала + его sul_mode_t). Резолвер ниже не меняется.
|
||||||
|
* Клиентская кастомизация (§7) — подмена этого массива, без правок кода.
|
||||||
|
*/
|
||||||
|
static const mode_rule_t k_mode_priority[] = {
|
||||||
|
{ offsetof(sul_result_t, fireman), SUL_MODE_FIREMAN },
|
||||||
|
{ offsetof(sul_result_t, fire_alarm), SUL_MODE_FIRE_ALARM },
|
||||||
|
{ offsetof(sul_result_t, overload), SUL_MODE_OVERLOAD },
|
||||||
|
{ offsetof(sul_result_t, seismic), SUL_MODE_SEISMIC },
|
||||||
|
{ offsetof(sul_result_t, maintenance), SUL_MODE_MAINTENANCE },
|
||||||
|
{ offsetof(sul_result_t, lading), SUL_MODE_LADING },
|
||||||
|
};
|
||||||
|
|
||||||
|
#define MODE_PRIORITY_COUNT (sizeof(k_mode_priority) / sizeof(k_mode_priority[0]))
|
||||||
|
|
||||||
|
sul_mode_t sul_resolve_mode(const sul_result_t *p_result)
|
||||||
|
{
|
||||||
|
const uint8_t *p_base = (const uint8_t *) p_result;
|
||||||
|
|
||||||
|
for (size_t i = 0U; i < MODE_PRIORITY_COUNT; ++i)
|
||||||
|
{
|
||||||
|
const bool ACTIVE = *(const bool *) (p_base + k_mode_priority[i].flag_offset);
|
||||||
|
if (ACTIVE)
|
||||||
|
{
|
||||||
|
return k_mode_priority[i].mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SUL_MODE_NORMAL;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
add_library(tft_app_elevator_model STATIC src/elevator_model.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_elevator_model PUBLIC include/)
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* @file elevator_model.h
|
||||||
|
* @brief Канонический результат декодирования СУЛ (Часть II §6 ARCH.md).
|
||||||
|
*
|
||||||
|
* Чистый C, без единого HAL-вызова — host-тестируется тем же способом, что
|
||||||
|
* модули bootloader. `sul_result_t` — надмножество полей для ЛЮБОГО протокола
|
||||||
|
* (НКУ-CAN, УИМ, SD7, УЭЛ, УКЛ, …); конкретный декодер заполняет то, что несёт
|
||||||
|
* его протокол, остальное остаётся дефолтным.
|
||||||
|
*
|
||||||
|
* Обзор боевых декодеров (NKU-CAN, UIM, UKL, UEL, SD7) подтвердил: набор
|
||||||
|
* сигналов ниже покрывает все протоколы — различаются лишь битовые кодировки
|
||||||
|
* (живут в каждом декодере), не сам словарь сигналов. Провиженинг-концепты
|
||||||
|
* (напр. `cop_mode`/`display_id`) — это настройки устройства (§8), НЕ данные
|
||||||
|
* СУЛ, и в этой структуре им не место.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_ELEVATOR_MODEL_H_
|
||||||
|
#define DOMAIN_ELEVATOR_MODEL_H_
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Максимум значащих символов позиции сейчас — 2; запас до 4 (см. ARCH §6). */
|
||||||
|
#define SUL_POS_MAX 4U
|
||||||
|
|
||||||
|
/** UTF-8, худший случай — 4 байта/кодпойнт + '\0'. */
|
||||||
|
#define SUL_POS_BUF_LEN (SUL_POS_MAX * 4U + 1U)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Направление движения кабины.
|
||||||
|
*
|
||||||
|
* Канонический словарь — НЕ протокол-специфичный набор битов. Декодер
|
||||||
|
* конкретного СУЛ транслирует свои сырые коды в эти значения (для НКУ-CAN
|
||||||
|
* PACKET1 ARROW_MASK совпадает по значению случайно — не полагаться на это
|
||||||
|
* в других протоколах).
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SUL_DIR_NONE = 0, /**< нет движения / нет данных */
|
||||||
|
SUL_DIR_UP = 1,
|
||||||
|
SUL_DIR_DOWN = 2,
|
||||||
|
SUL_DIR_DOUBLE = 3, /**< двойная стрелка (спецрежим индикации) */
|
||||||
|
} sul_direction_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Канонический экранный режим — результат свёртки ортогональных булевых
|
||||||
|
* сигналов `sul_result_t` через таблицу приоритетов (ARCH §7).
|
||||||
|
*
|
||||||
|
* ВАЖНО: числовые значения enum — просто идентификаторы, они НЕ задают
|
||||||
|
* приоритет. Приоритет определяется ТОЛЬКО порядком строк в таблице
|
||||||
|
* (domain/controller, `k_mode_priority[]`). Менять приоритет = переставлять
|
||||||
|
* строки таблицы, не трогая ни этот enum, ни резолвер.
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SUL_MODE_NORMAL = 0, /**< обычная индикация этажа/стрелки */
|
||||||
|
SUL_MODE_LADING, /**< погрузка (инструментальная или временная) */
|
||||||
|
SUL_MODE_MAINTENANCE,/**< сервис / ревизия / МП1 / МП2 */
|
||||||
|
SUL_MODE_SEISMIC, /**< сейсмоопасность */
|
||||||
|
SUL_MODE_OVERLOAD, /**< перегруз */
|
||||||
|
SUL_MODE_FIRE_ALARM, /**< пожарная тревога */
|
||||||
|
SUL_MODE_FIREMAN, /**< режим пожарного */
|
||||||
|
} sul_mode_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Канонический результат декодирования кадра/пакета СУЛ (ARCH §6).
|
||||||
|
*
|
||||||
|
* Позиция — UTF-8 строка (реальные шрифты ASCII + кириллица), не число:
|
||||||
|
* универсально для всех протоколов, host-тест сравнивает строки напрямую.
|
||||||
|
* Декодер ничего не знает про шрифт/рендер — валидация рендеримости
|
||||||
|
* (покрытие активным шрифтом) — задача UI-слоя (ARCH §11), не decode().
|
||||||
|
*
|
||||||
|
* Сигналы ОРТОГОНАЛЬНЫ (могут сосуществовать) — в один экранный режим их
|
||||||
|
* сводит controller по таблице приоритетов (§7), не декодер. Декодер выдаёт
|
||||||
|
* УРОВНИ (сигнал активен сейчас), а не события: edge-детекцию (гонг→звук,
|
||||||
|
* начало движения→звук) делает presentation/audio_policy поверх diff
|
||||||
|
* контроллера (ARCH §4, Фаза 6).
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char pos[SUL_POS_BUF_LEN]; /**< позиция кабины, напр. "12", "-1", "П" */
|
||||||
|
char next[SUL_POS_BUF_LEN]; /**< следующий этаж (пусто "" = нет данных) */
|
||||||
|
sul_direction_t direction;
|
||||||
|
|
||||||
|
/* Ортогональные сигналы (приоритет разрешает controller, §7): */
|
||||||
|
bool arrival; /**< гонг (прибытие) */
|
||||||
|
bool movement; /**< начало движения */
|
||||||
|
bool overload; /**< перегруз */
|
||||||
|
bool fire_alarm; /**< пожарная тревога */
|
||||||
|
bool lading; /**< погрузка */
|
||||||
|
bool maintenance; /**< сервисный режим */
|
||||||
|
bool fireman; /**< режим пожарного */
|
||||||
|
bool seismic; /**< сейсмоопасность */
|
||||||
|
bool error; /**< авария */
|
||||||
|
|
||||||
|
uint16_t lading_secs; /**< обратный отсчёт временной погрузки, сек; 0 = нет */
|
||||||
|
uint8_t floor_num; /**< производный числовой этаж для озвучки (0 = н/д) */
|
||||||
|
} sul_result_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние по умолчанию / при потере связи.
|
||||||
|
*
|
||||||
|
* pos="--" (нет данных), next="", direction=SUL_DIR_NONE, все сигналы false,
|
||||||
|
* lading_secs=0, floor_num=0. Вызывающий (контроллер через sul_rx-задачу при
|
||||||
|
* таймауте, либо init перед первым кадром) использует это как отправную точку —
|
||||||
|
* не HAL-зависимо, чистая функция.
|
||||||
|
*/
|
||||||
|
sul_result_t sul_default_state(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_ELEVATOR_MODEL_H_ */
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
sul_result_t sul_default_state(void)
|
||||||
|
{
|
||||||
|
return (sul_result_t){.pos = "--", .direction = SUL_DIR_NONE};
|
||||||
|
}
|
||||||
20
firmware/tft_app/src/domain/sul/CMakeLists.txt
Normal file
20
firmware/tft_app/src/domain/sul/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# tft_app_sul_headers — INTERFACE, только domain/sul.h (типы sul_frame_t/
|
||||||
|
# sul_status_t/sul_driver_t). Разделено от реализации реестра (ниже), чтобы
|
||||||
|
# tft_app_sul_nku_can мог использовать типы без циклической зависимости:
|
||||||
|
# nku_can нужны только типы sul.h, а sul_registry.c сам зовёт nku_can_decode.
|
||||||
|
add_library(tft_app_sul_headers INTERFACE)
|
||||||
|
target_include_directories(tft_app_sul_headers INTERFACE include/)
|
||||||
|
target_link_libraries(tft_app_sul_headers INTERFACE tft_app_elevator_model)
|
||||||
|
|
||||||
|
add_subdirectory(nku_can)
|
||||||
|
add_subdirectory(demo)
|
||||||
|
add_subdirectory(transport/can)
|
||||||
|
add_subdirectory(transport/demo)
|
||||||
|
|
||||||
|
# tft_app_sul — реализация реестра. Явно знает обо всех драйверах (nku_can,
|
||||||
|
# demo); добавление протокола (Фаза 8) — новая запись в sul_registry.c +
|
||||||
|
# новая зависимость здесь. Транспорты (transport/*) сюда НЕ линкуются —
|
||||||
|
# держит реестр host-тестируемым без bsp; wiring транспорт<->протокол —
|
||||||
|
# app-слой (task_sul_rx.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 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_ */
|
||||||
228
firmware/tft_app/src/domain/sul/include/domain/sul.h
Normal file
228
firmware/tft_app/src/domain/sul/include/domain/sul.h
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
/**
|
||||||
|
* @file sul.h
|
||||||
|
* @brief Реестр драйверов СУЛ (ARCH.md §6) — «одна прошивка — много протоколов».
|
||||||
|
*
|
||||||
|
* Драйвер = чистый декодер (host-тестируемый, без HAL) + отдельный тонкий
|
||||||
|
* транспорт-адаптер (HW, живёт в sul/transport/<bus>). Декодер НЕ владеет
|
||||||
|
* состоянием сам — состояние (например, накопленная позиция между PACKET1 и
|
||||||
|
* PACKET3 у НКУ-CAN) держит caller в ctx и передаёт указатель на каждый вызов
|
||||||
|
* decode(). Это позволяет декодеру оставаться описанным одной чистой функцией
|
||||||
|
* и не тянуть за собой выделение памяти/жизненный цикл.
|
||||||
|
*
|
||||||
|
* Активный протокол выбирается из настроек (`settings_device_t.protocol_id`,
|
||||||
|
* ARCH §8) через `sul_registry_set_active()` — вызывающий (app-слой) читает
|
||||||
|
* настройки и толкает id сюда; сам registry настройки не читает (домен не
|
||||||
|
* знает про settings_store, см. паттерн `nku_can_set_address()`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_H_
|
||||||
|
#define DOMAIN_SUL_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Результат decode() одного кадра/пакета. */
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SUL_STATUS_OK = 0, /**< кадр распознан, ctx и *p_out обновлены */
|
||||||
|
SUL_STATUS_IGNORED, /**< кадр не для этого драйвера — *p_out не тронут */
|
||||||
|
SUL_STATUS_ERR, /**< кадр совпал по ID, но малформирован (DLC и т.п.) */
|
||||||
|
} sul_status_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Кадр транспортного уровня, нейтральный к шине (CAN/UART/...).
|
||||||
|
*
|
||||||
|
* Транспорт-адаптер (HW) заполняет её из своего протокола (для CAN — id и
|
||||||
|
* data/len из bsp_can_frame_t); декодер (чистый C) её только читает.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint32_t id; /**< CAN ID либо адрес/маркер кадра другого транспорта */
|
||||||
|
uint8_t bus; /**< на будущее — несколько шин одного типа (0 = единственная) */
|
||||||
|
const uint8_t *p_data;
|
||||||
|
uint16_t len;
|
||||||
|
} sul_frame_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Чистая функция декодирования — БЕЗ единого HAL-вызова.
|
||||||
|
*
|
||||||
|
* @param p_ctx состояние драйвера (владеет caller, см. докстрок файла)
|
||||||
|
* @param p_frame один кадр транспортного уровня
|
||||||
|
* @param p_out заполняется только при SUL_STATUS_OK
|
||||||
|
*/
|
||||||
|
typedef sul_status_t (*sul_decode_fn_t)(void *p_ctx, const sul_frame_t *p_frame,
|
||||||
|
sul_result_t *p_out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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; /**< стабильный идентификатор протокола */
|
||||||
|
const char *p_name; /**< для меню (выбор протокола) */
|
||||||
|
sul_decode_fn_t decode;
|
||||||
|
/** Параметры протокола для меню (§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 Идентификаторы протоколов — стабильны, не переиспользовать.
|
||||||
|
*
|
||||||
|
* Список открыт (ARCH §2.2): УЭЛ, УКЛ, НКУ-SD7, УИМ добавляются в Фазе 8.
|
||||||
|
*/
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
SUL_PROTOCOL_NKU_CAN = 0U,
|
||||||
|
SUL_PROTOCOL_DEMO =
|
||||||
|
1U, /**< синтетический источник, ARCH §8 — витрина/тест дескрипторного механизма */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Инициализировать ctx ВСЕХ зарегистрированных драйверов (вызывать
|
||||||
|
* один раз при bringup, до первого sul_registry_active()->decode()).
|
||||||
|
*/
|
||||||
|
void sul_registry_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Активный драйвер — см. `sul_registry_set_active()`.
|
||||||
|
* @return указатель на статический дескриптор, никогда NULL.
|
||||||
|
*/
|
||||||
|
const sul_driver_t *sul_registry_active(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Найти драйвер по id.
|
||||||
|
* @return NULL, если протокол не зарегистрирован.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_H_ */
|
||||||
5
firmware/tft_app/src/domain/sul/nku_can/CMakeLists.txt
Normal file
5
firmware/tft_app/src/domain/sul/nku_can/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_sul_nku_can STATIC src/nku_can.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_sul_nku_can PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_sul_nku_can PUBLIC tft_app_elevator_model tft_app_sul_headers)
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* @file nku_can.h
|
||||||
|
* @brief Чистый декодер протокола НКУ-CAN (порт из OLD_PROJECT msg_receiver_task).
|
||||||
|
*
|
||||||
|
* Фаза 2 — полный разбор индикации: PACKET1 (направление, режимы, начало
|
||||||
|
* движения), PACKET2 (перегруз), PACKET3 (позиция, гонг, временная погрузка),
|
||||||
|
* PACKET4 (перегруз-вариант, сейсмо), PACKET5 (следующий этаж). Удалённая
|
||||||
|
* установка адреса (0x4X1/0x5XB, Фаза 3.5, REMOTE_ADDRES_SETUP.pdf) —
|
||||||
|
* 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 без сдвига группы.
|
||||||
|
* Фаза 3 параметризует через настройки; сигнатура decode() не изменится.
|
||||||
|
*
|
||||||
|
* Чистый C, без единого HAL-вызова — host-тестируется golden-векторами
|
||||||
|
* CAN-кадров (tests/host/tft_app_sul_nku).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_NKU_CAN_H_
|
||||||
|
#define DOMAIN_SUL_NKU_CAN_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Сентинел «анонса/запроса ещё не было» для полей удалённой адресации ниже —
|
||||||
|
* вне диапазона валидных X (4-битный нибл ID, 0..15). */
|
||||||
|
#define NKU_REMOTE_ADDR_NONE 0xFFU
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние декодера — накопленный текущий sul_result_t + внутренние
|
||||||
|
* латчи для полей с несколькими источниками.
|
||||||
|
*
|
||||||
|
* Разные пакеты несут РАЗНЫЕ поля в разных кадрах — decode() обновляет только
|
||||||
|
* пришедшее и отдаёт наружу ПОЛНУЮ накопленную копию state, а не дельту.
|
||||||
|
*
|
||||||
|
* Некоторые ВЫХОДНЫЕ поля кормятся НЕСКОЛЬКИМИ пакетами и должны быть их OR,
|
||||||
|
* иначе пакет-без-сигнала сбросил бы флаг, выставленный другим пакетом (боевой
|
||||||
|
* баг-класс из legacy — там держались parallel-флаги overload_flag/_new,
|
||||||
|
* lading_flag/time_lading). Держим эти под-источники раздельно в ctx и
|
||||||
|
* пересчитываем выход как OR после каждого пакета:
|
||||||
|
* state.overload = overload_p2 || overload_p4;
|
||||||
|
* state.lading = lading_instr || (lading_secs > 0).
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
sul_result_t state;
|
||||||
|
|
||||||
|
/* Раздельные под-источники (см. докстроку выше). */
|
||||||
|
bool overload_p2; /**< PACKET2: data[7] & 0x40 */
|
||||||
|
bool overload_p4; /**< PACKET4: data[5] & 0x40 */
|
||||||
|
bool lading_instr; /**< PACKET1: код режима 0x10 (инструментальная) */
|
||||||
|
uint8_t current_level; /**< PACKET1: data[3] & 0x3F — числовой уровень остановки,
|
||||||
|
для гейта «следующего этажа» в PACKET5 */
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** Сброс к состоянию по умолчанию (sul_default_state()); адрес станции = 0;
|
||||||
|
* состояние удалённой адресации — «анонса/запроса не было». */
|
||||||
|
void nku_can_init(nku_can_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Задать адрес станции (0..15) — сдвиг ID пакетов (group4/group6).
|
||||||
|
*
|
||||||
|
* Вызывать после nku_can_init(), значение из настроек (proto_slice[0], Фаза 3.1).
|
||||||
|
* Значения > 15 клампятся к 15. Для адреса 0 поведение как в Фазах 1/2.
|
||||||
|
*/
|
||||||
|
void nku_can_set_address(nku_can_ctx_t *p_ctx, uint8_t nku_address);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief decode() для реестра sul (см. sul_decode_fn в domain/sul.h).
|
||||||
|
*
|
||||||
|
* @param p_ctx nku_can_ctx_t*, инициализированный nku_can_init()
|
||||||
|
* @param p_frame сырой CAN-кадр (id + до 8 байт data)
|
||||||
|
* @param p_out заполняется полной накопленной state только при SUL_STATUS_OK
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_NKU_CAN_H_ */
|
||||||
356
firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c
Normal file
356
firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
#include "domain/sul/nku_can.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Базовые ID (для адреса станции 0). Реальный ID = base | сдвиг группы адреса:
|
||||||
|
* PACKET1..4 — group4 = nku_address<<4 (биты [7:4]); PACKET5 — group6 =
|
||||||
|
* nku_address<<6 (биты [8:6]). Адрес приходит из настроек (Фаза 3.1,
|
||||||
|
* proto_slice[0]); nku_can_set_address(). Для адреса 0 сдвиг нулевой — поведение
|
||||||
|
* идентично Фазам 1/2. */
|
||||||
|
#define PACKET1_BASE 0x506U /* направление, режимы, начало движения, двери */
|
||||||
|
#define PACKET2_BASE 0x408U /* перегруз (вариант 1) */
|
||||||
|
#define PACKET3_BASE 0x508U /* позиция кабины, гонг, временная погрузка */
|
||||||
|
#define PACKET4_BASE 0x50BU /* перегруз (вариант 2), сейсмоопасность */
|
||||||
|
#define PACKET5_BASE 0x606U /* следующий этаж */
|
||||||
|
#define PROTO_DLC 8U
|
||||||
|
|
||||||
|
#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 MOVEMENT_MASK 0x0CU /* PACKET1 data[6][3:2] — начало движения */
|
||||||
|
#define ICON_MASK 0xF0U /* PACKET1 data[6][7:4] — код режима */
|
||||||
|
#define FLOOR_MASK 0x3FU /* символ этажа / числовой уровень */
|
||||||
|
/* Двери (PACKET1 data[4]: откр. 0x10 / закр. 0x20) — только озвучка, не поле
|
||||||
|
* §6; декодируются в Фазе 6 (audio_policy). Здесь намеренно не разбираются. */
|
||||||
|
|
||||||
|
/* Коды режима в нибле data[6] & ICON_MASK (PACKET1). Точные значения нибла. */
|
||||||
|
#define ICON_LADING 0x10U /* инструментальная погрузка */
|
||||||
|
#define ICON_MP1 0x30U /* сервис / МП1 */
|
||||||
|
#define ICON_REVISION 0x40U /* ревизия */
|
||||||
|
#define ICON_MP2 0x50U /* сервис / МП2 */
|
||||||
|
#define ICON_FIRE 0x70U /* пожарная тревога */
|
||||||
|
#define ICON_FIREMAN 0xF0U /* режим пожарного */
|
||||||
|
|
||||||
|
#define WEIGHT_MASK 0x40U /* PACKET2 data[7] / PACKET4 data[5] — перегруз */
|
||||||
|
#define GONG_MASK 0x40U /* PACKET3 data[3] — гонг активен при БИТЕ == 0 */
|
||||||
|
#define LADING_SEC_MASK 0x3FU /* PACKET3 data[2] — секунды погрузки */
|
||||||
|
#define LADING_MIN_MASK 0x0FU /* PACKET3 data[3] — минуты погрузки */
|
||||||
|
#define SEISMIC_MASK 0x80U /* PACKET4 data[0] — сейсмоопасность */
|
||||||
|
|
||||||
|
#define SYMBOL_SPACE 16U
|
||||||
|
#define SYMBOL_A 10U
|
||||||
|
#define SYMBOL_P 17U /* "П" */
|
||||||
|
#define SYMBOL_p 19U /* "п" */
|
||||||
|
#define SYMBOL_HYPHEN 22U /* "-" */
|
||||||
|
#define SYMBOL_TOTAL 38U
|
||||||
|
|
||||||
|
#define FLOOR_NUM_UNKNOWN 60U /* «н/д» для озвучки — как в legacy floor_number_parser */
|
||||||
|
|
||||||
|
/* Таблица символов НКУ-CAN — порт из OLD_PROJECT floor_string_composer()
|
||||||
|
* (source/main_programm.c). Индекс — код символа с шины (байт & FLOOR_MASK). */
|
||||||
|
static const char *const S_SYMBOL_TABLE[SYMBOL_TOTAL] = {
|
||||||
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "b", "C", "d", "E", "F", " ", "П", "Р",
|
||||||
|
"п", "H", "U", "-", "_", "u", "L", "У", "Б", "Г", "R", "V", "N", "S", "K", "Y", "G", "B", "T",
|
||||||
|
};
|
||||||
|
|
||||||
|
void nku_can_init(nku_can_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
p_ctx->state = sul_default_state();
|
||||||
|
p_ctx->overload_p2 = false;
|
||||||
|
p_ctx->overload_p4 = false;
|
||||||
|
p_ctx->lading_instr = false;
|
||||||
|
p_ctx->current_level = 0U;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
p_ctx->nku_address = (nku_address <= NKU_ADDRESS_MAX) ? nku_address : NKU_ADDRESS_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Пересчёт выходных полей, кормящихся несколькими пакетами (см. nku_can.h). */
|
||||||
|
static void recompute_multi_source(nku_can_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
p_ctx->state.overload = p_ctx->overload_p2 || p_ctx->overload_p4;
|
||||||
|
p_ctx->state.lading = p_ctx->lading_instr || (p_ctx->state.lading_secs > 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Составить UTF-8 строку этажа из кодов левого/правого символа.
|
||||||
|
*
|
||||||
|
* Порт floor_string_composer(): left==SPACE или left==0 → однозначный этаж
|
||||||
|
* (только правый символ; станция шлёт 0x00 как «нет левого символа», трактуем
|
||||||
|
* как пробел). Возврат false — код вне таблицы (малформированный кадр).
|
||||||
|
*/
|
||||||
|
static bool compose_chars(uint8_t left, uint8_t right, char *p_out)
|
||||||
|
{
|
||||||
|
if ((left >= SYMBOL_TOTAL) || (right >= SYMBOL_TOTAL))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((left == SYMBOL_SPACE) || (left == 0U))
|
||||||
|
{
|
||||||
|
(void) snprintf(p_out, SUL_POS_BUF_LEN, "%s", S_SYMBOL_TABLE[right]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(void) snprintf(p_out, SUL_POS_BUF_LEN, "%s%s", S_SYMBOL_TABLE[left],
|
||||||
|
S_SYMBOL_TABLE[right]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Производный числовой этаж для озвучки — порт floor_number_parser().
|
||||||
|
*
|
||||||
|
* Стандартные (0..40): left*10+right (или только right при пробеле).
|
||||||
|
* Отрицательные ("-" слева): 40+right. Подвальные ("П"/"п" слева): 50+right.
|
||||||
|
* Всё нераспознанное / вне диапазона → FLOOR_NUM_UNKNOWN (60).
|
||||||
|
*/
|
||||||
|
static uint8_t floor_number_parser(uint8_t left, uint8_t right)
|
||||||
|
{
|
||||||
|
if (((left < SYMBOL_A) || (left == SYMBOL_SPACE)) && (right < SYMBOL_A))
|
||||||
|
{
|
||||||
|
const uint8_t FLOOR = (left == SYMBOL_SPACE) ? right : (uint8_t) (left * 10U + right);
|
||||||
|
return (FLOOR < 41U) ? FLOOR : FLOOR_NUM_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((left == SYMBOL_HYPHEN) && (right < SYMBOL_A))
|
||||||
|
{
|
||||||
|
return (right > 0U) ? (uint8_t) (40U + right) : FLOOR_NUM_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((left == SYMBOL_P) || (left == SYMBOL_p)) && (right < SYMBOL_A))
|
||||||
|
{
|
||||||
|
return (uint8_t) (50U + right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FLOOR_NUM_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PACKET1 (0x506) — направление, режим, начало движения, уровень остановки.
|
||||||
|
*
|
||||||
|
* data[6]: [1:0] стрелка · [3:2] начало движения · [7:4] код режима.
|
||||||
|
* data[3][5:0]: числовой уровень остановки (гейт «следующего этажа», PACKET5).
|
||||||
|
*
|
||||||
|
* Режимные флаги, которыми владеет ТОЛЬКО PACKET1 (fire/maintenance/fireman и
|
||||||
|
* инструментальная погрузка), сбрасываются в начале и выставляются по нибле —
|
||||||
|
* так пакет-без-режима гасит устаревший режим (как в legacy). overload и
|
||||||
|
* seismic PACKET1 НЕ трогает (ими владеют PACKET2/4).
|
||||||
|
*/
|
||||||
|
static void decode_packet1(nku_can_ctx_t *p_ctx, const uint8_t *p_data)
|
||||||
|
{
|
||||||
|
p_ctx->state.direction = (sul_direction_t) (p_data[6] & ARROW_MASK);
|
||||||
|
p_ctx->state.movement = ((p_data[6] & MOVEMENT_MASK) != 0U);
|
||||||
|
p_ctx->current_level = p_data[3] & FLOOR_MASK;
|
||||||
|
|
||||||
|
/* Стрелка «нет движения» → сбрасываем следующий этаж (появляется только
|
||||||
|
* пока кабина едет, гаснет на прибытии — см. PACKET5). */
|
||||||
|
if (p_ctx->state.direction == SUL_DIR_NONE)
|
||||||
|
{
|
||||||
|
p_ctx->state.next[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
p_ctx->state.fire_alarm = false;
|
||||||
|
p_ctx->state.maintenance = false;
|
||||||
|
p_ctx->state.fireman = false;
|
||||||
|
p_ctx->lading_instr = false;
|
||||||
|
|
||||||
|
switch (p_data[6] & ICON_MASK)
|
||||||
|
{
|
||||||
|
case ICON_FIRE:
|
||||||
|
p_ctx->state.fire_alarm = true;
|
||||||
|
break;
|
||||||
|
case ICON_MP1:
|
||||||
|
case ICON_MP2:
|
||||||
|
case ICON_REVISION:
|
||||||
|
p_ctx->state.maintenance = true;
|
||||||
|
break;
|
||||||
|
case ICON_LADING:
|
||||||
|
p_ctx->lading_instr = true;
|
||||||
|
break;
|
||||||
|
case ICON_FIREMAN:
|
||||||
|
p_ctx->state.fireman = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PACKET3 (0x508) — позиция кабины, гонг, временная погрузка.
|
||||||
|
*
|
||||||
|
* @return false — код символа позиции вне таблицы (малформированный кадр).
|
||||||
|
*/
|
||||||
|
static bool decode_packet3(nku_can_ctx_t *p_ctx, const uint8_t *p_data)
|
||||||
|
{
|
||||||
|
const uint8_t LEFT = p_data[5] & FLOOR_MASK;
|
||||||
|
const uint8_t RIGHT = p_data[6] & FLOOR_MASK;
|
||||||
|
|
||||||
|
if (!compose_chars(LEFT, RIGHT, p_ctx->state.pos))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p_ctx->state.floor_num = floor_number_parser(LEFT, RIGHT);
|
||||||
|
|
||||||
|
/* Гонг активен, когда БИТ 0x40 в data[3] СБРОШЕН (инверсная кодировка). */
|
||||||
|
p_ctx->state.arrival = ((p_data[3] & GONG_MASK) == 0U);
|
||||||
|
|
||||||
|
/* Временная погрузка: остаток = минуты*60 + секунды. */
|
||||||
|
const uint8_t SECS = p_data[2] & LADING_SEC_MASK;
|
||||||
|
const uint8_t MINS = p_data[3] & LADING_MIN_MASK;
|
||||||
|
p_ctx->state.lading_secs = (uint16_t) ((uint16_t) MINS * 60U + SECS);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PACKET5 (0x606) — следующий этаж.
|
||||||
|
*
|
||||||
|
* Доверяем «следующему этажу» только пока кабина реально едет (↑/↓) и байт
|
||||||
|
* назначения (data[0]) отличается от текущего уровня остановки — 0x606 и 0x506
|
||||||
|
* приходят асинхронно, устаревший байт назначения на стоянке порождал бы
|
||||||
|
* ложную индикацию (боевая заметка legacy). Иначе — очищаем next.
|
||||||
|
*
|
||||||
|
* @return false — код символа вне таблицы (малформированный кадр); при этом
|
||||||
|
* поле next не трогаем.
|
||||||
|
*/
|
||||||
|
static bool decode_packet5(nku_can_ctx_t *p_ctx, const uint8_t *p_data)
|
||||||
|
{
|
||||||
|
const bool MOVING =
|
||||||
|
(p_ctx->state.direction == SUL_DIR_UP) || (p_ctx->state.direction == SUL_DIR_DOWN);
|
||||||
|
|
||||||
|
if (MOVING && (p_data[0] != p_ctx->current_level))
|
||||||
|
{
|
||||||
|
return compose_chars(p_data[3] & FLOOR_MASK, p_data[4] & FLOOR_MASK, p_ctx->state.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
p_ctx->state.next[0] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
|
{
|
||||||
|
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 пакетов адресно-зависим). */
|
||||||
|
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 ID = p_frame->id;
|
||||||
|
|
||||||
|
/* ID известного пакета совпал, но DLC не тот — малформированный кадр. */
|
||||||
|
if ((ID == (PACKET1_BASE | G4)) || (ID == (PACKET2_BASE | G4)) || (ID == (PACKET3_BASE | G4)) ||
|
||||||
|
(ID == (PACKET4_BASE | G4)) || (ID == (PACKET5_BASE | G6)))
|
||||||
|
{
|
||||||
|
if (p_frame->len != PROTO_DLC)
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ID == (PACKET1_BASE | G4))
|
||||||
|
{
|
||||||
|
decode_packet1(p_state, p_frame->p_data);
|
||||||
|
}
|
||||||
|
else if (ID == (PACKET2_BASE | G4))
|
||||||
|
{
|
||||||
|
p_state->overload_p2 = ((p_frame->p_data[7] & WEIGHT_MASK) == WEIGHT_MASK);
|
||||||
|
}
|
||||||
|
else if (ID == (PACKET3_BASE | G4))
|
||||||
|
{
|
||||||
|
if (!decode_packet3(p_state, p_frame->p_data))
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ID == (PACKET4_BASE | G4))
|
||||||
|
{
|
||||||
|
p_state->overload_p4 = ((p_frame->p_data[5] & WEIGHT_MASK) == WEIGHT_MASK);
|
||||||
|
p_state->state.seismic = ((p_frame->p_data[0] & SEISMIC_MASK) == SEISMIC_MASK);
|
||||||
|
}
|
||||||
|
else if (ID == (PACKET5_BASE | G6))
|
||||||
|
{
|
||||||
|
if (!decode_packet5(p_state, p_frame->p_data))
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return SUL_STATUS_IGNORED;
|
||||||
|
}
|
||||||
|
|
||||||
|
recompute_multi_source(p_state);
|
||||||
|
*p_out = p_state->state;
|
||||||
|
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;
|
||||||
|
}
|
||||||
114
firmware/tft_app/src/domain/sul/src/sul_registry.c
Normal file
114
firmware/tft_app/src/domain/sul/src/sul_registry.c
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
#include "domain/sul.h"
|
||||||
|
#include "domain/sul/demo.h"
|
||||||
|
#include "domain/sul/nku_can.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[] = {
|
||||||
|
{
|
||||||
|
.id = SUL_PROTOCOL_NKU_CAN,
|
||||||
|
.p_name = "НКУ-CAN",
|
||||||
|
.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]))
|
||||||
|
|
||||||
|
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 *p_driver = sul_registry_find(g_s_active_id);
|
||||||
|
return (p_driver != NULL) ? p_driver : &s_registry[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sul_driver_t *sul_registry_find(uint8_t id)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < REGISTRY_COUNT; i++)
|
||||||
|
{
|
||||||
|
if (s_registry[i].id == id)
|
||||||
|
{
|
||||||
|
return &s_registry[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_sul_transport_can STATIC src/can_transport.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_sul_transport_can PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_sul_transport_can PUBLIC tft_app_sul_headers bsp_can)
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* @file can.h
|
||||||
|
* @brief Тонкий транспорт-адаптер bsp_can → sul_frame_t (HW, не host-тестируется).
|
||||||
|
*
|
||||||
|
* Не декодирует протокол — только инициализация CAN + фильтры и перевод
|
||||||
|
* bsp_can_frame_t в нейтральный sul_frame_t. Декодирование — domain/sul/nku_can.h.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_TRANSPORT_CAN_H_
|
||||||
|
#define DOMAIN_SUL_TRANSPORT_CAN_H_
|
||||||
|
|
||||||
|
#include "bsp/status.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief bsp_can_init(). RX-фильтры НЕ настраивает.
|
||||||
|
*
|
||||||
|
* Вызывающий обязан сразу после этого позвать sul_transport_can_set_address()
|
||||||
|
* — внутренний сентинел форсирует первое применение фильтров независимо от
|
||||||
|
* переданного адреса (как в OLD_PROJECT msg_receiver_task).
|
||||||
|
*/
|
||||||
|
bsp_status_t sul_transport_can_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief (Пере)настроить RX-фильтры (PACKET1..5) под адрес станции.
|
||||||
|
*
|
||||||
|
* НКУ-CAN кодирует адрес станции в ID: PACKET1..4 — биты [7:4] (group4 =
|
||||||
|
* addr<<4), PACKET5 — биты [8:6] (group6 = addr<<6, протокол отводит под
|
||||||
|
* него только 3 бита). Дёшево звать на каждой итерации приёма — реальная
|
||||||
|
* переконфигурация Message Buffer'ов FlexCAN происходит только при
|
||||||
|
* фактическом изменении адреса (внутренний diff, сентинел на первый вызов).
|
||||||
|
* Эталон — OLD_PROJECT msg_receiver_task/apply_nku_can_filters().
|
||||||
|
*
|
||||||
|
* @param nku_address 0..15; вне диапазона — приводится к 15.
|
||||||
|
*/
|
||||||
|
bsp_status_t sul_transport_can_set_address(uint8_t nku_address);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Принять один кадр и перевести в sul_frame_t.
|
||||||
|
*
|
||||||
|
* @warning p_out->p_data валиден ТОЛЬКО до следующего вызова этой функции
|
||||||
|
* (внутренний статический буфер, единственный ожидаемый вызыватель — задача
|
||||||
|
* sul_rx с последовательным receive→decode). Не сохранять sul_frame_t между
|
||||||
|
* итерациями цикла приёма.
|
||||||
|
*
|
||||||
|
* @param timeout_ms таймаут ожидания кадра, 0 = не блокироваться
|
||||||
|
* @param p_out заполняется только при возврате BSP_OK
|
||||||
|
* @return BSP_OK, BSP_ERR_TIMEOUT
|
||||||
|
*/
|
||||||
|
bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_TRANSPORT_CAN_H_ */
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
#include "domain/sul/transport/can.h"
|
||||||
|
|
||||||
|
#include "bsp/can.h"
|
||||||
|
|
||||||
|
/* Базовые ID НКУ-CAN (адрес станции 0) — та же протокольная раскладка, что и
|
||||||
|
* decode-сторона (domain/sul/nku_can/src/nku_can.c): PACKET1..4 кодируют
|
||||||
|
* адрес в битах [7:4] ID (group4 = addr<<4), PACKET5 — в битах [8:6]
|
||||||
|
* (group6 = addr<<6, протокол отводит под него только 3 бита). Decode и
|
||||||
|
* transport намеренно НЕ шарят общий заголовок с этими константами — каждый
|
||||||
|
* владеет своей копией протокольного факта (декодер ничего не знает про
|
||||||
|
* транспорт и наоборот, ARCH). */
|
||||||
|
#define PACKET1_BASE 0x506U
|
||||||
|
#define PACKET2_BASE 0x408U
|
||||||
|
#define PACKET3_BASE 0x508U
|
||||||
|
#define PACKET4_BASE 0x50BU
|
||||||
|
#define PACKET5_BASE 0x606U
|
||||||
|
#define STD_ID_MASK 0x7FFU /* 11-bit STD — проверять все биты */
|
||||||
|
|
||||||
|
#define NKU_ADDRESS_MAX 15U /* 4-битный адрес, group4 = addr<<4 */
|
||||||
|
|
||||||
|
/* Удалённая установка адреса (§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 про
|
||||||
|
* время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */
|
||||||
|
static bsp_can_frame_t s_last_frame;
|
||||||
|
|
||||||
|
/* Сентинел вне диапазона 0..15 — форсирует применение фильтров на первый
|
||||||
|
* вызов sul_transport_can_set_address(), независимо от переданного адреса
|
||||||
|
* (порт OLD_PROJECT msg_receiver_task: last_nku_address = 0xFFU). */
|
||||||
|
static uint8_t s_last_applied_address = 0xFFU;
|
||||||
|
|
||||||
|
bsp_status_t sul_transport_can_init(void)
|
||||||
|
{
|
||||||
|
const bsp_can_config_t cfg = {.bitrate = 125000U}; /* см. OLD_PROJECT msg_receiver_task */
|
||||||
|
|
||||||
|
return bsp_can_init(&cfg);
|
||||||
|
/* Фильтры не настраиваем здесь — вызывающий (sul_rx_task) обязан сразу
|
||||||
|
* позвать sul_transport_can_set_address(), см. can.h. */
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_status_t sul_transport_can_set_address(uint8_t nku_address)
|
||||||
|
{
|
||||||
|
const uint8_t ADDR = (nku_address <= NKU_ADDRESS_MAX) ? nku_address : NKU_ADDRESS_MAX;
|
||||||
|
|
||||||
|
if (ADDR == s_last_applied_address)
|
||||||
|
{
|
||||||
|
return BSP_OK; /* не менялось — переконфигурация MB не нужна */
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t GROUP4 = (uint32_t) ADDR << 4U;
|
||||||
|
const uint32_t GROUP6 = (uint32_t) ADDR << 6U;
|
||||||
|
|
||||||
|
bsp_status_t st = bsp_can_set_filter(0U, PACKET1_BASE | GROUP4, STD_ID_MASK, false);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
st = bsp_can_set_filter(1U, PACKET2_BASE | GROUP4, STD_ID_MASK, false);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
st = bsp_can_set_filter(2U, PACKET3_BASE | GROUP4, STD_ID_MASK, false);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
st = bsp_can_set_filter(3U, PACKET4_BASE | GROUP4, STD_ID_MASK, false);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
st = bsp_can_set_filter(4U, PACKET5_BASE | GROUP6, STD_ID_MASK, false);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out)
|
||||||
|
{
|
||||||
|
const bsp_status_t st = bsp_can_receive(&s_last_frame, timeout_ms);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
p_out->id = s_last_frame.id;
|
||||||
|
p_out->bus = 0U;
|
||||||
|
p_out->p_data = s_last_frame.data;
|
||||||
|
p_out->len = s_last_frame.dlc;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
10
firmware/tft_app/src/menu/CMakeLists.txt
Normal file
10
firmware/tft_app/src/menu/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# menu — движок меню настроек (§8). Модель (menu.c, чистая) + боевое дерево
|
||||||
|
# (menu_tree.c, данные). Рендер — отдельно в ui/menu.
|
||||||
|
add_library(tft_app_menu STATIC src/menu.c src/menu_tree.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_menu PUBLIC include/)
|
||||||
|
|
||||||
|
# settings_store — только типы settings_t (заголовок чистый, без QSPI-кода).
|
||||||
|
# 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)
|
||||||
3
firmware/tft_app/src/menu/README.md
Normal file
3
firmware/tft_app/src/menu/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# menu
|
||||||
|
|
||||||
|
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.
|
||||||
113
firmware/tft_app/src/menu/include/menu/menu.h
Normal file
113
firmware/tft_app/src/menu/include/menu/menu.h
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
/**
|
||||||
|
* @file menu.h
|
||||||
|
* @brief Движок меню настроек (ARCH.md §8) — ЧИСТАЯ модель, без рендера и QSPI.
|
||||||
|
*
|
||||||
|
* Дерево пунктов — ДАННЫЕ (плоский массив + индексы parent/child). Редакторы
|
||||||
|
* подключаются по типу (menu_item_type_t) — добавить причудливый параметр =
|
||||||
|
* добавить тип + строку дерева, движок не меняется (§8, требование
|
||||||
|
* расширяемости под клиента).
|
||||||
|
*
|
||||||
|
* Модель ОПЕРИРУЕТ ПЕРЕДАННЫМ `settings_t*` (мутирует RAM на месте) и **сама не
|
||||||
|
* сохраняет** — на выходе-с-сохранением выставляет `save_requested`, а
|
||||||
|
* `settings_store_save()` вызывает app-слой. Это держит модель host-тестируемой
|
||||||
|
* (без QSPI) и разделяет логику/side-effect по слоям.
|
||||||
|
*
|
||||||
|
* Навигация: BUTTON_1 → menu_next() (следующий пункт уровня, с заворотом);
|
||||||
|
* короткое BUTTON_2 → menu_action() (вход в подменю / инкремент значения /
|
||||||
|
* выход-с-сохранением). Вход в меню (короткое BUTTON_1, вне меню) и модальность — уровень app.
|
||||||
|
*
|
||||||
|
* Соглашение: items[MENU_ROOT_INDEX] — корневое SUBMENU; его дети — верхний
|
||||||
|
* уровень (их parent == MENU_ROOT_INDEX). Дети одного уровня — непрерывный
|
||||||
|
* диапазон [first_child..last_child].
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MENU_MENU_H_
|
||||||
|
#define MENU_MENU_H_
|
||||||
|
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define MENU_ITEMS_PER_PAGE 6U /* строк на экране (окно 480×272, §PLAN 3.2) */
|
||||||
|
#define MENU_ROOT_INDEX 0U /* items[0] — корневое SUBMENU */
|
||||||
|
|
||||||
|
/** Тип пункта = редактор. Расширяется добавлением значения (Фазы 5/6:
|
||||||
|
* ARRAY/SERIAL/YEAR/PERCENT/BOOL_ARRAY) — движок не меняется. */
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
MENU_SUBMENU, /**< вход в подменю [first_child..last_child] */
|
||||||
|
MENU_BACK, /**< возврат уровнем выше; в корне — выход (+save) */
|
||||||
|
MENU_SELECT, /**< выбор из списка 0..max (напр. протокол) */
|
||||||
|
MENU_BYTE, /**< число min..max (напр. адрес) */
|
||||||
|
MENU_BOOL, /**< да/нет */
|
||||||
|
} menu_item_type_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Пункт меню — данные. Привязка к настройке — байтовый offset uint8-поля
|
||||||
|
* в settings_t (для SELECT/BYTE/BOOL). Для SUBMENU/BACK offset игнорируется.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const char *label;
|
||||||
|
menu_item_type_t type;
|
||||||
|
uint16_t value_offset; /**< offsetof(settings_t, <uint8-поле>) */
|
||||||
|
uint8_t min; /**< для SELECT/BYTE/BOOL */
|
||||||
|
uint8_t max;
|
||||||
|
uint8_t parent; /**< индекс родителя (MENU_ROOT_INDEX — верхний уровень) */
|
||||||
|
uint8_t first_child; /**< для SUBMENU — диапазон детей */
|
||||||
|
uint8_t last_child;
|
||||||
|
/** Метки значений для SELECT/BOOL (options[value]); NULL → рендер числом.
|
||||||
|
* Только для презентации — модель (menu.c) это поле не использует. */
|
||||||
|
const char *const *options;
|
||||||
|
} menu_item_desc_t;
|
||||||
|
|
||||||
|
/** Состояние навигации — чистое. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const menu_item_desc_t *items;
|
||||||
|
uint8_t count;
|
||||||
|
settings_t *settings; /**< мутируется на месте */
|
||||||
|
uint8_t cur; /**< индекс выделенного пункта */
|
||||||
|
uint8_t page; /**< страница для рендера */
|
||||||
|
bool open;
|
||||||
|
bool dirty; /**< значение менялось */
|
||||||
|
bool save_requested; /**< выход-с-сохранением: app зовёт save() */
|
||||||
|
} menu_ctx_t;
|
||||||
|
|
||||||
|
/** Инициализировать (меню закрыто). @p items[MENU_ROOT_INDEX] — корневое SUBMENU. */
|
||||||
|
void menu_init(menu_ctx_t *p_ctx, const menu_item_desc_t *p_items, uint8_t count,
|
||||||
|
settings_t *p_settings);
|
||||||
|
|
||||||
|
/** Открыть меню: курсор на первый пункт верхнего уровня, флаги сброшены. */
|
||||||
|
void menu_open(menu_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/** BUTTON_1: следующий пункт текущего уровня (заворот с последнего на первый). */
|
||||||
|
void menu_next(menu_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/** Короткое BUTTON_2: вход в подменю / инкремент значения / выход-с-сохранением. */
|
||||||
|
void menu_action(menu_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
bool menu_is_open(const menu_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/* ── Запросы для рендера ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/** Индекс выделенного пункта. */
|
||||||
|
uint8_t menu_current(const menu_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/** Диапазон пунктов текущего уровня (соседи выделенного) — для отрисовки списка. */
|
||||||
|
void menu_level_range(const menu_ctx_t *p_ctx, uint8_t *p_first, uint8_t *p_last);
|
||||||
|
|
||||||
|
/** Текущее uint8-значение editable-пункта (SELECT/BYTE/BOOL) для отрисовки. */
|
||||||
|
uint8_t menu_read_value(const menu_ctx_t *p_ctx, uint8_t idx);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* MENU_MENU_H_ */
|
||||||
50
firmware/tft_app/src/menu/include/menu/menu_tree.h
Normal file
50
firmware/tft_app/src/menu/include/menu/menu_tree.h
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* @file menu_tree.h
|
||||||
|
* @brief Боевое дерево меню Фазы 3 (данные) — Протокол / Адрес / Логи / Выход.
|
||||||
|
*
|
||||||
|
* Отдельно от движка (menu.c): движок generic, дерево — конкретная конфигурация,
|
||||||
|
* привязанная к полям settings_t по offset (§8). Добавить пункт = строка в
|
||||||
|
* menu_tree.c, движок не меняется.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MENU_MENU_TREE_H_
|
||||||
|
#define MENU_MENU_TREE_H_
|
||||||
|
|
||||||
|
#include "menu/menu.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Дерево пунктов (items[0] — корневое SUBMENU). */
|
||||||
|
const menu_item_desc_t *menu_tree_items(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
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* MENU_MENU_TREE_H_ */
|
||||||
123
firmware/tft_app/src/menu/src/menu.c
Normal file
123
firmware/tft_app/src/menu/src/menu.c
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
#include "menu/menu.h"
|
||||||
|
|
||||||
|
/* Указатель на uint8-поле настройки, к которому привязан пункт idx. */
|
||||||
|
static uint8_t *field_ptr(const menu_ctx_t *p_ctx, uint8_t idx)
|
||||||
|
{
|
||||||
|
return (uint8_t *) p_ctx->settings + p_ctx->items[idx].value_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t page_of(const menu_ctx_t *p_ctx, uint8_t idx, uint8_t first)
|
||||||
|
{
|
||||||
|
return (uint8_t) ((idx - first) / MENU_ITEMS_PER_PAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_init(menu_ctx_t *p_ctx, const menu_item_desc_t *p_items, uint8_t count,
|
||||||
|
settings_t *p_settings)
|
||||||
|
{
|
||||||
|
p_ctx->items = p_items;
|
||||||
|
p_ctx->count = count;
|
||||||
|
p_ctx->settings = p_settings;
|
||||||
|
p_ctx->cur = MENU_ROOT_INDEX;
|
||||||
|
p_ctx->page = 0U;
|
||||||
|
p_ctx->open = false;
|
||||||
|
p_ctx->dirty = false;
|
||||||
|
p_ctx->save_requested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_open(menu_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
p_ctx->open = true;
|
||||||
|
p_ctx->dirty = false;
|
||||||
|
p_ctx->save_requested = false;
|
||||||
|
p_ctx->cur = p_ctx->items[MENU_ROOT_INDEX].first_child; /* первый пункт верхнего уровня */
|
||||||
|
p_ctx->page = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool menu_is_open(const menu_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
return p_ctx->open;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t menu_current(const menu_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
return p_ctx->cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_level_range(const menu_ctx_t *p_ctx, uint8_t *p_first, uint8_t *p_last)
|
||||||
|
{
|
||||||
|
const uint8_t PARENT = p_ctx->items[p_ctx->cur].parent;
|
||||||
|
*p_first = p_ctx->items[PARENT].first_child;
|
||||||
|
*p_last = p_ctx->items[PARENT].last_child;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t menu_read_value(const menu_ctx_t *p_ctx, uint8_t idx)
|
||||||
|
{
|
||||||
|
return *field_ptr(p_ctx, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_next(menu_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
if (!p_ctx->open)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t first;
|
||||||
|
uint8_t last;
|
||||||
|
menu_level_range(p_ctx, &first, &last);
|
||||||
|
|
||||||
|
p_ctx->cur = (p_ctx->cur >= last) ? first : (uint8_t) (p_ctx->cur + 1U); /* заворот */
|
||||||
|
p_ctx->page = page_of(p_ctx, p_ctx->cur, first);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Инкремент editable-значения с заворотом min→max→min. */
|
||||||
|
static void cycle_value(menu_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
const menu_item_desc_t *p_it = &p_ctx->items[p_ctx->cur];
|
||||||
|
uint8_t *p_v = field_ptr(p_ctx, p_ctx->cur);
|
||||||
|
|
||||||
|
*p_v = (*p_v >= p_it->max) ? p_it->min : (uint8_t) (*p_v + 1U);
|
||||||
|
p_ctx->dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_action(menu_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
if (!p_ctx->open)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu_item_desc_t *p_it = &p_ctx->items[p_ctx->cur];
|
||||||
|
|
||||||
|
switch (p_it->type)
|
||||||
|
{
|
||||||
|
case MENU_SUBMENU:
|
||||||
|
p_ctx->cur = p_it->first_child;
|
||||||
|
p_ctx->page = 0U;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_BACK:
|
||||||
|
if (p_it->parent == MENU_ROOT_INDEX)
|
||||||
|
{
|
||||||
|
/* Корневой выход — сохранить, если что-то менялось. */
|
||||||
|
p_ctx->save_requested = p_ctx->dirty;
|
||||||
|
p_ctx->open = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Вернуться к пункту-подменю, из которого вошли. */
|
||||||
|
p_ctx->cur = p_it->parent;
|
||||||
|
uint8_t first;
|
||||||
|
uint8_t last;
|
||||||
|
menu_level_range(p_ctx, &first, &last);
|
||||||
|
p_ctx->page = page_of(p_ctx, p_ctx->cur, first);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_SELECT:
|
||||||
|
case MENU_BYTE:
|
||||||
|
case MENU_BOOL:
|
||||||
|
cycle_value(p_ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
155
firmware/tft_app/src/menu/src/menu_tree.c
Normal file
155
firmware/tft_app/src/menu/src/menu_tree.c
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
#include "menu/menu_tree.h"
|
||||||
|
|
||||||
|
#include "domain/sul.h"
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Метки значений (ярус B/устройство). */
|
||||||
|
static const char *const K_BOOL_LABELS[] = { "Выкл", "Вкл" };
|
||||||
|
|
||||||
|
static const char *const K_DUMMY_LABELS[] = { "Гойда!", "Зрада!" };
|
||||||
|
/* Верхняя граница на число протоколов в реестре — только размер буфера меток
|
||||||
|
* (menu_tree_refresh_protocol_section), не ограничение самого реестра.
|
||||||
|
* Сейчас 2 (НКУ-CAN, демо), с запасом под Фазу 8 (+ УИМ/SD7/УЭЛ/УКЛ — 6). */
|
||||||
|
#define MENU_TREE_MAX_PROTOCOLS 8U
|
||||||
|
static const char *g_s_proto_labels[MENU_TREE_MAX_PROTOCOLS] = {
|
||||||
|
"НКУ-CAN"
|
||||||
|
}; /* фолбэк до refresh() */
|
||||||
|
|
||||||
|
/* Индексы пунктов дерева. */
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
T_ROOT = 0,
|
||||||
|
T_PROTO,
|
||||||
|
T_PROTO_PARAM, /* единственный параметр АКТИВНОГО протокола (§8) — популируется
|
||||||
|
* из sul_settings_desc_t, см. menu_tree_refresh_protocol_section() */
|
||||||
|
T_LOG,
|
||||||
|
T_DUMMY,
|
||||||
|
T_EXIT,
|
||||||
|
|
||||||
|
T_COUNT,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Боевое дерево Фазы 3. Действуют сейчас: протокол+параметр (→ декодер),
|
||||||
|
* логи (→ рантайм-гейт). Остальной каталог настроек — по мере своих фаз.
|
||||||
|
*
|
||||||
|
* НЕ 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 g_s_tree[T_COUNT] = {
|
||||||
|
[T_ROOT] = { .label = "Настройки",
|
||||||
|
.type = MENU_SUBMENU,
|
||||||
|
.parent = MENU_ROOT_INDEX,
|
||||||
|
.first_child = T_PROTO,
|
||||||
|
.last_child = T_EXIT },
|
||||||
|
[T_PROTO] = { .label = "Протокол",
|
||||||
|
.type = MENU_SELECT,
|
||||||
|
.value_offset = offsetof(settings_t, device.protocol_id),
|
||||||
|
.min = 0U,
|
||||||
|
.max = 0U,
|
||||||
|
.parent = MENU_ROOT_INDEX,
|
||||||
|
.options = g_s_proto_labels },
|
||||||
|
[T_PROTO_PARAM] = { .label = "Адрес",
|
||||||
|
.type = MENU_BYTE,
|
||||||
|
.value_offset = offsetof(settings_t, user.proto_slice[0]),
|
||||||
|
.min = 0U,
|
||||||
|
.max = 15U,
|
||||||
|
.parent = MENU_ROOT_INDEX },
|
||||||
|
[T_LOG] = { .label = "Логи",
|
||||||
|
.type = MENU_BOOL,
|
||||||
|
.value_offset = offsetof(settings_t, device.log_enabled),
|
||||||
|
.min = 0U,
|
||||||
|
.max = 1U,
|
||||||
|
.parent = MENU_ROOT_INDEX,
|
||||||
|
.options = K_BOOL_LABELS },
|
||||||
|
[T_DUMMY] = { .label = "Общий?",
|
||||||
|
.type = MENU_BOOL,
|
||||||
|
.value_offset = offsetof(settings_t, user.dummy_option),
|
||||||
|
.min = 0U,
|
||||||
|
.max = 1U,
|
||||||
|
.parent = MENU_ROOT_INDEX,
|
||||||
|
.options = K_DUMMY_LABELS },
|
||||||
|
[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);
|
||||||
|
g_s_proto_labels[i] = (p_drv != NULL) ? p_drv->p_name : "?";
|
||||||
|
}
|
||||||
|
g_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];
|
||||||
|
|
||||||
|
g_s_tree[T_PROTO_PARAM].label = p_entry->p_label;
|
||||||
|
g_s_tree[T_PROTO_PARAM].type = menu_type_from_sul(p_entry->type);
|
||||||
|
g_s_tree[T_PROTO_PARAM].value_offset =
|
||||||
|
(uint16_t) (offsetof(settings_t, user.proto_slice) + p_entry->slice_offset);
|
||||||
|
g_s_tree[T_PROTO_PARAM].min = p_entry->min;
|
||||||
|
g_s_tree[T_PROTO_PARAM].max = p_entry->max;
|
||||||
|
g_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 + g_s_tree[T_PROTO_PARAM].value_offset;
|
||||||
|
if (*p_val > p_entry->max)
|
||||||
|
{
|
||||||
|
*p_val = p_entry->max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Протокол без параметров — инертный дефолт (не встречается пока
|
||||||
|
* ни у одного зарегистрированного протокола). */
|
||||||
|
g_s_tree[T_PROTO_PARAM].label = "—";
|
||||||
|
g_s_tree[T_PROTO_PARAM].type = MENU_BYTE;
|
||||||
|
g_s_tree[T_PROTO_PARAM].value_offset = offsetof(settings_t, user.proto_slice[0]);
|
||||||
|
g_s_tree[T_PROTO_PARAM].min = 0U;
|
||||||
|
g_s_tree[T_PROTO_PARAM].max = 0U;
|
||||||
|
g_s_tree[T_PROTO_PARAM].options = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu_item_desc_t *menu_tree_items(void)
|
||||||
|
{
|
||||||
|
return g_s_tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t menu_tree_count(void)
|
||||||
|
{
|
||||||
|
return (uint8_t) T_COUNT;
|
||||||
|
}
|
||||||
3
firmware/tft_app/src/services/CMakeLists.txt
Normal file
3
firmware/tft_app/src/services/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
add_subdirectory(gfx)
|
||||||
|
add_subdirectory(partition)
|
||||||
|
add_subdirectory(settings_store)
|
||||||
3
firmware/tft_app/src/services/README.md
Normal file
3
firmware/tft_app/src/services/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# services
|
||||||
|
|
||||||
|
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.
|
||||||
29
firmware/tft_app/src/services/gfx/CMakeLists.txt
Normal file
29
firmware/tft_app/src/services/gfx/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# services/gfx — Фаза 1 минимальный gfx: framebuffer (SDRAM non-cacheable) +
|
||||||
|
# RLE-шрифты (lcd-image-converter) + примитивы. См. include/services/gfx.h.
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
tft_app_gfx STATIC
|
||||||
|
src/gfx.c
|
||||||
|
fonts/FloorFontFallback.c
|
||||||
|
fonts/SystemFont.c
|
||||||
|
fonts/JBMono12.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_gfx PUBLIC include/)
|
||||||
|
|
||||||
|
# Сгенерированные lcd-image-converter файлы ожидают tImage/tChar/tFont из
|
||||||
|
# bare-name "fonts.h": SystemFont.c явным #include <fonts.h> (резолвится
|
||||||
|
# через -I ниже), FloorFontFallback.c вообще без include-строки (более новая
|
||||||
|
# версия конвертера, полагается на внешний force-include). Один и тот же
|
||||||
|
# заголовок (fonts/include/fonts.h) закрывает оба случая. НЕ редактируем
|
||||||
|
# сгенерированные файлы напрямую — конвертер их перегенерирует при
|
||||||
|
# обновлении шрифта, ручная правка потеряется.
|
||||||
|
target_include_directories(tft_app_gfx PRIVATE fonts/include)
|
||||||
|
set_source_files_properties(
|
||||||
|
fonts/FloorFontFallback.c fonts/SystemFont.c fonts/JBMono12.c
|
||||||
|
PROPERTIES
|
||||||
|
COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/fonts/include/fonts.h;-w")
|
||||||
|
|
||||||
|
# sdk_common — fsl_common.h (AT_NONCACHEABLE_SECTION_ALIGN); sdk_pxp — PXP-компоновщик;
|
||||||
|
# freertos_kernel — семафор FRAME_DONE в gfx_present/ISR-колбэке (Фаза 3.2.4).
|
||||||
|
target_link_libraries(tft_app_gfx PUBLIC bsp_display bsp_sdram sdk_common PRIVATE sdk_pxp
|
||||||
|
freertos_kernel)
|
||||||
7352
firmware/tft_app/src/services/gfx/fonts/FloorFontFallback.c
Normal file
7352
firmware/tft_app/src/services/gfx/fonts/FloorFontFallback.c
Normal file
File diff suppressed because one or more lines are too long
132
firmware/tft_app/src/services/gfx/fonts/FloorFontFallback.xml
Normal file
132
firmware/tft_app/src/services/gfx/fonts/FloorFontFallback.xml
Normal file
File diff suppressed because one or more lines are too long
6204
firmware/tft_app/src/services/gfx/fonts/JBMono12.c
Normal file
6204
firmware/tft_app/src/services/gfx/fonts/JBMono12.c
Normal file
File diff suppressed because it is too large
Load diff
501
firmware/tft_app/src/services/gfx/fonts/JBMono12.xml
Normal file
501
firmware/tft_app/src/services/gfx/fonts/JBMono12.xml
Normal file
|
|
@ -0,0 +1,501 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<data type="font" name="JBMono12">
|
||||||
|
<family>JetBrainsMono Nerd Font</family>
|
||||||
|
<size>12</size>
|
||||||
|
<ascent>12</ascent>
|
||||||
|
<descent>4</descent>
|
||||||
|
<style>Regular</style>
|
||||||
|
<widthType>monospaced</widthType>
|
||||||
|
<antialiasing>true</antialiasing>
|
||||||
|
<foreground>ffffffff</foreground>
|
||||||
|
<background>00ffffff</background>
|
||||||
|
<multiplicityWidth>1</multiplicityWidth>
|
||||||
|
<multiplicityHeight>1</multiplicityHeight>
|
||||||
|
<string>!"#$%&'()*+,-./0123456789:;<=>?\x0040ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ абвгдежзийклмнопрстуфхцчшщъыьэюяё</string>
|
||||||
|
<converted>/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/firmware/tft_app/src/services/gfx/fonts/JBMono12.c</converted>
|
||||||
|
<chars>
|
||||||
|
<char code="0021" character="!">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAb0lEQVQYlbWQMQ7AIAhFSbsxOzi4cRQmz+MxvQTHICqhk4mNju1fIP+F5H8A/lWMMbq755zz9K659N47AICZ2QZba22dH0F39zHGWOG9JhYRqbVWVdXXZQghEBGllNLWk5nZzKyUUo6PQEQ8gm/1AE4gQ4DHRCD/AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0022" character=""">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAV0lEQVQYldWPsQ0AMQjErCzJShkpE6RiASZgBhBEX+U3+OJdneRrDN8hIlJVBaCqOuecAAMgM/OccwAiIrq7XxkRkZl5j3cPAHf3tdYC2HtvM7MPK/7FAxDiNu/Jp3qhAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0023" character="#">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA40lEQVQYlbXQoQqEQBQF0EEdEcFgsD0MFstgtBmsBkE/w2b1J6x+jkZREHz4AYJ9yoNZQWHLrnnLnnTDLfcy9hf6N7Rt24ZhGI7jOM7zPE/TND2tvu/7NE1TwzAMIiLLsiwdAKBpmibP8/y+7zvLskwIIZRSSruu63p9ICI6juNs27Ydx3HoRES2bdsAAHVd13Ecx+u6rl3XdXpRFEVVVZXneR4AQFmWpWma5r7vu6aUUr7v+4iIUkoZBEGwLMtynufJGGMMETGKosh1XVdKKZ8JnHNORMQ550mSJMMwDP+59DdvpYRv8Y8SthsAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0024" character="$">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAABCElEQVQYlY2RIaqFQBiFf0REEGQmKRbBfKO7ECwGwQVYxCgWu12MrsAF2GwOqKDZDZjGMFhmkvPa5V1ueO/UL3yHcwD+m6qqKtM0zS+gKIqyLMviOI7zAXzf94/jOKSU8jzPM03T9A2HYRi6ruu2bdvyPM855xxjjBUAgNfr9VrXdRVCiL7v+yiKIlVVVQAAaNu2va7ropRSz/O8D6emaVpd17UQQjDGWJIkyVdjQghpmqZ5nuexbdsGhBAqiqIwDMMghBDLsixKKY3jOFbv+76zLMsQQkjXdT0MwxBjjKdpmgAAIAiCYN/3XUopOee8LMvyyznP8+y6rvte7Tccx3FkjLG/Pvg7PyBjec0gjKDmAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0025" character="%">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA/ElEQVQYlaWQPY5FUABGv/f8hFsoaO8CKEWr1FPoVaxAbEEiIrEFyV2EDehEUNBgARQSiUoxzcxUL9PMab8vOckB/gXP8/zHIQzDcN/3fZqmSdd1HQAcx3GGYRgwz/Ns27adZVmWpmmqKIqyLMtiWZb1fp7neX0DAEVRFIwx1rZtiyiKovM8z3Vd1ziO467rOkEQhF+vKIqiqqrqtm2baZompZQCwOvnUFVVdRzH4fu+TwghdV3XbwBwXdc1DMPo+74fx3GklFLP87y3pmlaWZZlEATBdV2XJEkSIYQAABhjLEmSBABkWZabpmnu+77zPM8/huE4jvtf8z/5AgS1ZEDJV5Q8AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0026" character="&">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+ElEQVQYla2QIa9GYACFD9cmECSmmSaySWyCKtl0VWJT/Ab/gT+iURXbK79T2WwECntv+7a7G265TzzhPGcH+BcURVF+hY7jOIQQwhhj4ziOmqZpAMADQFVVFSGEqKqqchzH5XmeA4AAAPM8z0EQBNu2bUmSJKIoip9ay7IsxhjLsiz74fM8zzuO45imabrv+7Zt29Z1XW+apuHatm15nufTNE3ruq7jOI7XdV2XZVnQ931flmUJAIIgCJRSuu/7LkmS9GWaphlFUTQMw1AUReH7vv88z3Oe5wnDMIyu67r3fV9KKXVd1w3DMLyu6/oMk2VZ/p+f/+QbjTJtH0aFURkAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0027" character="'">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAQUlEQVQYldWOsQ0AMAjDop7AS3mapzgAFIlOTFXHDvVkxUuAt5iZdXeT5GxrJDMTACTpGquqjihJ7u4REe/Of8gGWM4fe5J46CwAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0028" character="(">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsklEQVQYlZXQMQrEIBAFUBGDprYKptUrWKQXD+YRcgw7ryFpbKZLF4i9EAjIVgm7a4rdqYb/hoEZhP4tpZRqQimljDHGdV3XD+Cc85xz9t77YRiGD3TOOQAAxhi7Mnw1WmsdQgjHcRwNjuM4AgC8b7sRY4zP8zwfsdZau67rHnHbtu37vhuXZVmstZYQQpoHCCFEKaXM8zz3fd83A9M0Tfu+7yml1CBCCFFKqTHGPOLP9QIKokAx/OVPFwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0029" character=")">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsUlEQVQYlZ3PMQrEIBQE0I9gYxrPYGFtZZculukt7TyJ9/AASe0xcoacIKQIggH/VluIwsJO+2CYAfgVIYQghJAhbtu2ned5LsuydDhN0xRCCKWUorXWw4Z93/eUUhqitdZe13UNUSmlEBEZYwwAoFn4vu8LAPBd3iCllAIAICJ2KKWU933fz/M8Ha7ruh7HcTRDCCHEe+9rrdUYYxqMMcacc3bOue7CPM8z55wP//2VD1ZNQtEZxK1FAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="002a" character="*">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQYlcWQoQ2FMBRFLx2ABUjoAjXdoKMgKjBMUNMdugCeKlSHqMA0wTxJE1R1g+Ar8N/8f9TNMSe5wJ9ZlmXp+75/BWOMPbvWWoUQ4vHMGGNijFFrra/rupRSKoQQvPcenHNurbUppXTf951zzs45J6WUAICmaZp1XVcion3f97Zt27c7TdNUSild13Xbtm3zPM8AwADgPM9zHMfxOI5jGIaBiOgX73zNB1/dTKkq9csNAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="002b" character="+">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAaElEQVQYlcWPsQ3AIAwE7YgJPIo38T6egTU8iWsGoEKiNjUVqeiiKGmS6/5f+tcD/AwRESLiZbjWWszMWx93TYmZWVV1GznnHBFhZpbGGMPdHQBARKSUUnrvvbXWnm/WWuucc755+CknVhou2T+twoIAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="002c" character=",">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAUklEQVQYlWNgGAX4ATOMwcTExPTgwYMH//79+3fy5MmTKKpUVFRU/v///9/R0dERw4igoKCg379//+bm5uaGmwZjKCgoKBw+fPjw169fv1LuWgCBERz6sepnJwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="002d" character="-">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAPElEQVQYld2OMREAMAjE0rs6Qw6W0IGD98GKi05IaIdmTJbANywAMzN395GZmRERG6C7W5ImVlU937zLAVJ5Ej5MadxOAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="002e" character=".">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAP0lEQVQYlWNgGAX4ATOMYWVlZXXmzJkz79+/f3/+/PnzDAwMDEwwSWFhYWERERERGRkZGazG8PLy8tLcrQwMAGFZDFYICXB2AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="002f" character="/">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAgUlEQVQYlb3PwQ2EERQE4D95R4mELlAJN2XQCZ3QCYXQwnPyFzCbPexh5zjfYTLP8yX0qTTGmHPOAbDW2nvvdc45wFJKmXNO3CCitdbKOWdA771nZtZaa8DWWuu9dwCllGJmDiEEwJRS2ntvIsJ7Y4xRa63/+CaEEDHGKKWUgD/nBdJIQIBOesLvAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0030" character="0">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA3ElEQVQYla2QMQqEMBREf9JECwlobAU9Ri7wz5DW1iansfAUlhZi4wW8gR4gqQLBxmQrhWWbhd1XzoOBGYC/kGVZ9hFKKeWyLMt1Xde2bZtSSj1yXde17/s+z/O867rOWmsZYwzKsixjjLGu6xoAgFJKjTEGEZEyxhgAgHPOISKmaZo651ySJAm9q6uqqqZpmhAR7+yRx3EcTdM04ziOj/Te+xBCKIqi2Pd9J4QQzjk/z/MEAIB5nudhGAYhhNBa6xBCEEKIt50xxmittW3bth9ncM45IYT8cPI3vABt0V6Va7TWdgAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0031" character="1">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeUlEQVQYlbWPsQ3FIBBD3wVKdrg9GI0ZGICenh3oWYElECUSqZBSfKX4Ulza0rMN38k553LOee+9U0rp+BagtdbmnLPWWo0x5oQXQCmleO99770/iRYghBB+1V1vW/4PLYCq6rkkIqKqOsYYArDWWs8LADHG+Eb9Sjf1Eyda/P4+TwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0032" character="2">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAvUlEQVQYla2QsQnEIABFTQrBBCWdhBQ3g604RaoMkCnEESxdxMpWCISUQoYQXSEQrzkCR666u1f+Vzz4APwFjDG+jUII4b3353meIYQwTdN0yW3bNmOM6fu+l1LKnHOGEEKAMcZaa00ppQAAQAghpZTCOee3xDiO43Ecx60/DMOQUkpKKfUmuq7r9n3fnXOuruv6EgghtCzLsq7r2rZte4mqqiprrY0xRsYYe7xomqYBCCFUPjDP8/zllz/xBA4sVkkNLblvAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0033" character="3">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuUlEQVQYlbWNIRKEIABFv7CVGUxoJRNM3oHmLbwBNyDavIXRbMeAzRkTlRvYGDa5s47ZV/+894H3SCml/Md5nicAfABASikBoCzLcp7neVmW5WYzxti6rus0TROllAIAuUZjjGnbtj2O4+Cc85uplFJ93/fbtm2P7EXTNE1KKQkhBKmqqhrHcWSMMQCIMUZCCOGccxRFUYQQgrXW1nVdD8Mw7Pu+/1Jd13Xee59zzs45p7XWj78r/TJfSCVTaamqv4gAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0034" character="4">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAArElEQVQYlbWQMQrDIBhGP5Li2CxZnLyCSyBzIOCayRwmx/AIXkdcvUF2FSejnRIaurX0rY/3/fAD/2dd1zWllBhj7Ca6ruv2fd+3bds+KqWUcs45Qgi5iWEYhpxznqZp6vu+F0IIAEDbtq0xxmitNQDM8zyHEAIAYBzHsdZaOeecUkqllPKUj6ZpGgCw1trzTIwxXrPPN5ZlWa7yOI7De+/PynvvSynl1599ywu7JEq1CEDV3gAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0035" character="5">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAzklEQVQYlbWQMQqEMAAE9w5LI1aCVjb+wR/4gDRWFv7EL0iekFekSG1j4QcCFga0sQsWIbniEDxsDo6bdpfdZYH/oZRS/sK+7zsABKehbdtWSikBwFprP8Rt27Z5nudr4uOMNcaYdV1XIYTgnPNlWZYHANR1XUdRFIVhGDZN0xzHcZRlWd7GFUVROOdcnuf5M47jmDHGkiRJAEBrrZ1zLsuyDAAwTdPU932fpmnadV2ntdZBELzHVlVVDcMweO/9OI4jpZTe+ggh5Jczv+QFdORnRQN/QWoAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0036" character="6">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA1klEQVQYlbWQoQ2DQBhGv7aQQIK5AcDgOdgAiWOCswgEO7AEA5DcHMxx4hAkCECQEHLn+GvbNKlqn/yeeh/wf4ZhGKSU8kOUZVkaY0wURdGbcF3XVUqptm3b1/0BAHVd10IIsSzLwjnn0zRN+77vN8dxHK21ttbavu/7oiiKOI7jLMsypGmaEhElSZIAgO/7/nEchxBC3IMgCIiI5nmeAcBaa7dt2xhjDJ7need5nl3XdWEYhk3TNNd1XZxzDgDI8zwfx3EkIlrXda2qqvpoZYyx39z5lSc26GE3uT7EwQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0037" character="7">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAkUlEQVQYlbXQIQqEIRCGYVmbyaLBNHgQsdi8js3kQSw2m9GjCEaLVxBlN21yYdP/1mfgg0HomRhj7P2jGGN8fY+UUgoAQEop55yz994RIYR47z2llCKEkDHG7L23EEJcMznnXEopF3DO+VprWWvthc45N8YYGGN8YWuthRDCBVprfc45AAAXppRSrbX+ecvjfQBl5EgvAXAo4QAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0038" character="8">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA6ElEQVQYla2QIQqEQBiF37hF1KImg2DyAKYRvIHVYhYEcapzCYNn8FwGEcGmiGAQZP5NLmzbsF98r3zvAX/BcRyHMca+wiRJkmmaJiKibdu2oiiKTzkMw9D3fe95niellPd937Zt29A0Tdv3fc+yLAOAIAgCIqIwDMMXEZFlWZYQQui6rksp5bIsS9u2LQAgz/P8OI5jXddVKaXquq4BAIZhGOd5nlVVVYwxxjnnSikVRVGEOI5jIiLXdd1HcBzHUQghYJqmeV3X1XVd5/u+X5ZlSUTEOecAgDRN03me52dn0zTNbw/9nzdXE3IIlKTINwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0039" character="9">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAyUlEQVQYla2QLQ6EMBhEh3U0aQgG0wug8aRXQTXU1uOQGAiX4SKYJkj+EgwVuH6rWIPb8OQkbzIZ4BXiOI4foZRSTtM0ERHt+75rrTUAIAzD0Dnn2rZthRCiLMuSiCjLsgx5nufeex9FUXQ3WWutMcZ81nVdgyAI0jRNAYBzzoUQ4jiOAwAwDMOwLMvSdV03juN4XdeVJEkCAGCMsaIoiqZpGiKivu/7x2qllDrP8/xZN5xzvm3bVlVV9bDquq7neZ4ZY+zvF1/gCz+JWccDwSd3AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="003a" character=":">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAYUlEQVQYlc3OsQnAMAwEwCfpPYUWUKPSA3hSL+JWrbZQI1QKUrkIpE3IVw8PzwE/y7nLGGOoqlZVrbUWABx7zMx0d4+I+ArEzDznnBERZmY3EBFR772LiDzetNbay1IAuABl4iPvhmdIwgAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="003b" character=";">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAdUlEQVQYlcWOuw3AIAxETVIxBRtQwBqMTMcCeAdKt9gSEnIqokQpoyhX3a94AD/LGGOueV8mpZQQEVVVSykFAGBb4xhjMDOLiHwMeAJZa21rrRER1Vrr7RVjjKqqIYSwupPWe+977x0R8TE651zOOc8553vaA32NNo2dwQBFAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="003c" character="<">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAoElEQVQYlb2QMQoDIRREf6KbBc+g2C0ewMrKwnNYWXghwQNs55G00UOIhUKaFFlIEwh55TDMMAPwf26fRM45H2OMi8gYYzHG2HvvUkoJAACUUhpCCL33fp7neRzHAQAAhBAy55wppSSEEO9J97XWyjlnpZQyxph93/dLF0IIWWttKaW01pr33m/btl1MGGPsnHO11qq11h+nPF58dcqPeAJ/Vjb9ASw5hwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="003d" character="=">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAATElEQVQYlc3OoQ0AQQhE0R0s9IGmCCT9B4PBUsR5xCan7r59yWTO+VkgIlJV3VBVBRGRmZmN7u73WQAwM9uQmQlm5u7ujRERL/9/3ANF/xItIfjKFAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="003e" character=">">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnUlEQVQYlcWQsQkDIRhGf7TzRHABKwsrRRsXuKHsbg23EG4G4Qa5ThC00dK0CUmXQF774HvwAfwBjDF2zrmPkhBC5pzzPM/TWmufHRpjDCmlvO/7vq7rSiklY4x5WxFCiBhjXGst771Hz1IppbTWupRSeu8dAAD2fd9zzrnWWkMIYdu2DQAAKKW0tdaO4zgYY+ylgxBCnHP+xRs/5QHwjTe1DGoXdQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="003f" character="?">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAn0lEQVQYlbWOsQ3DIBBFjyjpqBAFlUsbMQk9u9CbMVwhMQSbYCkwAGIDaEhjW7FEmbzynt7XAfwPY4zpvfdSSnHOOYwxviSllHLOuVJKxRjjtm3bcEVrrfd93wEAHt+CEEKklDKllG7Fuq5rrbXmnLMQQtxKxhjz3vtpmqYQQgAAeJ7SWmsRQqi11s7bVc7zPC/Lsgy/fB8M5etgKH/LB/BxPmNkMhNwAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0040" character="\x0040">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAABKElEQVQYla2RPYqDYBiEX10FMSII8uEPWAiCpaU29oKgXfQSwSsInsAreI3U2ltY2OgHQlKEqIWoC8I22SKk2GafcmaKYQbgXxAEQSAIgngTPc/z+r7vt23buq7rsizLaJqmQdM0bVmWJUmShOM4zjAMo67rOgzD8CuKosiyLCuO4/j7BUVRlO/7PqVpmna9Xq8AAEVRFEEQBPM8z7fb7UaSJEmu67pKkiSdz+ezrut6nuc5AAD5W0qSJOn5fD4fj8ejbdv2zRzHceR5nmcYhkEIofv9fqeO4zhYlmUxxriqqqosy1JRFOVyuVwgiqJoGIaBeOE4jqOqqgoAALIsy9M0TWmapggh9DGbbdt20zTNvu87xhi7rut+hERRFE3TNE+n0+nPI34A6vt7FtO2dO4AAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0041" character="A">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAzElEQVQYlbWQoQ6DMABEr9tEMVUkoDAY/qK0GgP8CYqfq4FvIJhWLSUIkmqShs4MzMzE9tQll7xLDvgL9zPUdV3P8zxTSqlSSgHA7SyllNI556SU8kMxTdPU933vvfeMMXYVSZIkx3EcaZqm1lpbVVV1aYUQQmut13Vdh2EYhBACAB7n3r7ve9d1HWOMFUVRAAABAK21fr6Joihq27aN4zhGlmVZCCHkeZ4DACGEbNu2NU3T3MqyLJdlWYwxBgBCCGEcx5Fzzn976Xe8AHbfWmPCJlRGAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0042" character="B">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAwElEQVQYlbWQPQqEMBCFh2VzA9HKLiltQxByBDuxDXgFj5H72KSf3EHBVmyNnTCzlcJut7D72o/3wwP4n1JKiZn5PM8zhBCapmlueBzH0bZtq5RS3nu/rusqhBA3tNZaAICyLEtmZqWUel5uY4zJ8zzv+76PMcZ5nufbuSzLMk3TREQ0DMPw1nnF1nVdExFprfXjczki4rZtW1VV1d1ZFEUhpZRd13VZlmXjOI4AALDv+87MTESEiOiccz898gu9AJ8CZptGWhdkAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0043" character="C">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAp0lEQVQYla1QuwnEIAB9WgriBpE0NjpEBnAMCzdwDDdIa28hrpKMYQSbFFdd4O4g1+SV78H7AY+Ac85/SGOMqbXW8zzPbds255y7xJRSyjnnaZom55zrvffL5TiOw1prAYAQQkIIYZ7nGQAwxhjLsizfcfSu4H9xjDGEEAIAKKU0xhiVUgoAsK7rWkopUkrpvfettcYYYwAArbV+79z3ff/YefvQ83gBmn1DU+spXHkAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0044" character="D">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAkklEQVQYlbWOIQ4DIRQFH1gkDsMBSCBBITkFl9oTof8psBjQCJC/btO0SZOKHTtiBniOvfdmZt57byKiEEIAAAkAQghRSikppdR777XWKqWUAIBzzsk5ZwAwxhhmZueck5+JMcYAAKWU+pLv/CettRYA1lrrllprHWOM13Vdc87ZWmv3LTPzOecQEXnv/a/ck7wAFT5D5YgYffIAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0045" character="E">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAh0lEQVQYlbVQuwkEIRScd2wPhtZhDQpmVmF5gtUoGCkaGJoIe5FyGNxGO9kwjzcf4D3EGON9QCmlrnVgrbXOObd4rbVusbXWUkrp9+MWhRBizjkBYIwxvPeelicRUe+9A0DOOUsp5Q5kjDFn0M+/FtuTMcY453zxUkoBAIQQwtlTa62f9nkDXz3kWXgV7pA8AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0046" character="F">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAdUlEQVQYlbWQsQrAIAxEzyJ0c3X1k/yCfKx/4iQIZtfBdFKsQ+ni28Ild1yAc5RSimwQEWkAUEopIqIQQhgHOeesx5BSSjHGuDpO0VprnXMOAERE5iIz855pjDFT9N77e+Fl21prtda6Zl5fFafYe++/H3OQB0zTUcnBbitKAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0047" character="G">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAx0lEQVQYla2QMQqEMBREhxi0Ch4gniJaCl4kZxBSew9PoCDB0tLcYpNTxAOkCNlmXdxd2MrXzZ+BzwxwC4wx9nOs67re932PMUZrrZVSyre5bdu2LMtSVVXV931/HMdRFEUBQgiJMcau6zoAoJTSYRgGzjlHlmVZSik1TdN8vyNXMY7j+HjRtm1Lr+Y8z7MxxkzTNDHGGE0ppRBCKMuyNMaYPM9zAAghBACA1lqv67pyzrlSSnnv/RmCEEKcPZ1z7qPn34Xu5wnke12L5n1j1gAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0048" character="H">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAb0lEQVQYlb3OsQ0DIQyF4d9HWiR6DwA9I8D+ywAS1TkNSFF0l1TJ19jyky3D77TWWimlAIQQgplZjDEeALK8Lx2fLj52k3POIiLee79nAtB773POOcYYzjmnqppSSuyw1lovH7rzPTwXADOz1/pvT3k8MQi8BRYhAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0049" character="I">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAVUlEQVQYlb2PsQnAQAwDhcFDGGPceAiv8ZN7JKX6IimS6nOVkBBCwDlmZvhARAQAYGZWVUWSa60VEXFrq6qSZHf39uRt7mRoZpaZCQDu7lsD+Pj5MxecwDPTE8r/dwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="004a" character="J">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAcUlEQVQYlcWOuw3AIAxEwTUtNZULD8AWiIWYwiN4BBrmYQoKS8ipEilNqih55f10zn1DzjmbmQEAwFPwF7PWWpmZTyHGGNday8zMISLuvXcppaSU0hhjiIhc9dZam3NOVdXeeyciuu17730IITx9eIkDC5cpCcSkSXkAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="004b" character="K">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuUlEQVQYlbWQMQqEMBRE57NLRMgpxAPkBtrbCLlGSq9ik8Yb5BbpA9aCVUDSWSRFLLZSEKuF3WkfvBkG+EteABBjjNZau67ryhhjy7IsRVEUAICUUmrbtgUApZQKIQTOOb/BsixL770fhmEAgPfpF0KIrus6IqJxHMerOKWUtm3b9n3fjTHmturU9n3fH8dx1HVdPyARkXPOTdM0PSAASCllzjlXVVVdsGmaBgCIiOZ5nrXW+kdffpUP6glflAFEpnMAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="004c" character="L">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAQklEQVQYlWNgoB1oampq2rp161Z0cSZ8mgadJAuMwcnJySkvLy8P43/+/PkzXNLR0dHxwYMHD2D8NWvWrMFnKq0AADWrEJHoqFaMAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="004d" character="M">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAm0lEQVQYlcXLsQ3DIBQE0PvfCPcUnoQB2ICaATyFd2ATS9afxLUbCkZggEuVKHHqKK873R3wGzHGSJL7vu8AsG3bRpLruq4KAGOMkVJK3nufc869966qqgBAkud5nqWUsizLcl3XBQAKAM45Z2ZWa61mZvM8z19lCCEcx3FM0zQBgAMAEZHWWlNVJUkRkdfziSTf80d5p/f1vz0Ag9ZCg71BUkQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="004e" character="N">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAj0lEQVQYlbWQMQoDIRBFh20sPIN4ARGvM3o/wcbaUvAIXkaLmUmTXRI2TSB51Twe/GIA/gcR0RhjnM7M7L33l4iIWGvt6SGEcAAAiIjMOSci4uvicR611hpjjB9ja60ZY4xzzt3i3nvXWmtKKTEzv0UAgJxzRkRca61b7L13pZTSWusryhMiolJK+epJP+YBCZhPa79YyKIAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="004f" character="O">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQYla2QsQ2FMBBDnVRECCZAbJCGBbIC7EORaa7IDuxwLUWWiHSku/zq/wYJ/QI3lvwa28ArGoZhuIXLsizHcRyqqszM27ZtP5hSSkRE0zRN+77vOecMALDW2lprDSEEAOj7vm+tNe+9t8YY03VdJyICANd1XQDgnHP2qeAzVFUVERnHcQSAr5dSCgCAiCillOZ5nmOM8TzP87aztdaYmdd1Xf976H19AFl8TMWcK0SbAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0050" character="P">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnElEQVQYlbWQoQ0EIRREP5dTNICiCwogOCqAQmiIItYjCP4TJMHRAiQYztyu2Dtz4p6dzLxkAP7HGGPsN4iIzjlHCCEAADDnnMYYwznn1lo7xhhKKUXOUGutQwgBAOA4jqP33h93BWOMCSFESilds621lnPOa60VY4yUUvo8G957X0opiIi11npNzTmnlFLeFR/Or+Hee/90zp94AWrRVqh2QYAFAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0051" character="Q">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA1ElEQVQYla2PsQmFMABEjyAmhaYQbCRgGxzBBZwhveAC2jiMjWtIGhfIImmCBAtJfvP5fPBj9a+8O7h3wF+U53l+M9u2bbXWOoQQjDFGKaU+4b7v+7IsS1VV1TiOo7XWUkopyrIsY4yxaZoGAJIkSZxzruu6jlBKKQA45xwAXNd1ee89Y4yRJ8Dn0HvvQwihKIoCANI0TbMsy87zPAEA27Zt67quQggxz/P8Xf78jG/1fd/fNuq6rq21dpqm6SfEMAzDcRyHEELcCQkhUkrJOedPT/ACtzxgeRj1rY8AAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0052" character="R">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsUlEQVQYlbWQsQ3DIBBFz/QegAJ5DypKb0CPhJBsF2zBHMjjMAAFHQtQQEXhSxMjJakiJa999/W/DuB/1ForPkkppX3f9yFba01KKZdlWY7jOBAROed8SCGEuI9zztlaa8l7xbquK2OMhRDCSLbW2t3tvfeEEDLktm2bMcb03jullL4MEkKIaZqmGGN0zrkPCQCgtdallDLP80wAAK7ruhARAQDO8zx7710ppX71x294AOk/bdmDgaR9AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0053" character="S">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA3UlEQVQYla1QqwqEQAAcOMEtltWgcf0J/2C/QNZstNs2+Qti92OsBoNhq4gimAWRZffSWe7KHTdxZmAewF9AKaVvZJIkyTRNk7XW7vu+p2ma3uIwDEPbtm0YhmFVVdVxHIfrui4AYNu2TQghAIAQQjjnnBBCHgAQx3FclmVpjDHLsix93/daaw0AcBzHkVLKeZ7n67ouKaX82FgIIay1ljHGEARB0DRN4/u+/zKs67pmWZYBAMZxHOu6rqMoivI8z8/zPO/NnHPedV1njDFKKVUURfGW53me98uzX+IJBGBjGGI4/8sAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0054" character="T">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAASElEQVQYlc3NsQ3AQAgDQERNxwAWQ7AGc7AS0zodVZTu9bnSlmyRM7q7+WJmRszMACAigiSrqgDA3X0XVFVJMjNzs6/L35U3PP5oJAEjEsTtAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0055" character="U">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAbElEQVQYlbWNoRHAIBAEd96g6IAaUKjvg4YoJ03QAwXQBhggJsRkIllz4mbv4BxjjKGqCmCMMWut5b338ieIiPyWACfL1lqz1lqAnb33DsD14JxzKaVUa62vHkIIOec855yllBJjjJ+PPXmYGxKTKbk1k2hXAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0056" character="V">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsUlEQVQYlcWOIQ7EIAAENzwAUYtAVfOCNn0Aig+g+hR8ZR9SDRqJrME1dRB8A6d6ueb05VZtZsUs8JtorfVxHMcn27ZtW5ZlAeect9Za3/c9ABBCSM45K6UUACDGGOd5ngFACCFqrbXruo4AgHPOTdM0AcA4jmMIIaSU0tc4DMNgrbXvA4wxdnvP8zyllPLxet/33Rhjruu6KKX0Ma7rupZSivfe34zcxTnnKKX04ftDXvn3VPuFwfXPAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0057" character="W">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA00lEQVQYlbWQrcqFQBRFN95bbFPFAX+QAdEH8MV9AZPRaD1nMKhw8IA4mPyS9Wt3pQUbVtjAT/iM4ziGEMK6rut5nuc8z7Oq6rZtW+S992mapmVZlgCQZVlmrbXLsiyfrus6Y4y5rutyzjlVVRER55z71HVdN03T3Pd9i4jkeZ7v+77HcRxHRERvdhiG4c0yM0fMzO84TdNkjDHWWktEFDEzJ0mSFEVREBEdx3G0bdsSEQEAQghBVRUA+r7vn+d5qqqqvgDAzCwi8joAeO/9b679nz/OVYJSbhIF3AAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0058" character="X">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA10lEQVQYlbWQoQqEQBiEB3/YIDaDD7Btn8LmC8j2fY4tdrPFJ7C7RbNPYLApatGoGCyLXDg8OC4d3E0aZuBjGOAvojAMw2EYBmOMWZZlcV3XHcdxvK7rAgAYY0xd1zUAaK113/c9Y4wBAIQQwlprpZRy3/c9juP4jZ/neW6ttU3TNHfm3KYoioKIqCzL8mNZVVXVPM/zuq6r53neq4iiKDrP8+Sc82mapiRJkifXcZy2bds0TVMAUEqp4ziOIAgCKKXUtm2b7/s+ABARdV3XZVmW/eTJL/UAox1jUu92BO0AAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0059" character="Y">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnUlEQVQYlcWOoQ2DIRSEH2UAFBLDCnhmwP8DsAIL4JEMgGEDPCOgkfjfkJCQQNXftGlT1/Qz93KX3DuA32CMMbXWijHGlxdCCDHGCIQQcp7neRzHAQDAOedzzimEEAAAYK21pZSCEELee59SSo9qSintvXettR5jDCmlfPntnHNrrZVzzm/DGGNs772VUurybtfRWmvP+hJ+4mv4D+7qlUCfDs+MDQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="005a" character="Z">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlklEQVQYlbWQoQ2DIRBGDwgKBB6BZgUMGzAEq7AAjgVYgh0YgQFIsCgS0qo2P/krKtqnvuTl7rscwF9AAAB7740xxlehlFLv8CKEEHrvnTHGjjVCCDHGGN57f+uIMcZaa0UIoUNorfVaaxljzG2qlFJyzvkmnHNuzjmllPIQhBDSWmsppXS9mlJKgXPOHx+w1tqvPvVjnn90Q4JkZY9dAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="005b" character="[">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAcUlEQVQYld1QoRHAIBBLe13gNYoNfhFGwOFYCc0qv8A7BgCBfl1FD8UAjUouueQuwAHXIs45R0QEAGOMMeecX6qUUszMeu8955wB4Nlraq01pZSWvk+bfzVDCEFEJMYYge1bZmbvvQcAVdXWWju1nvECpngllXTNCmQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="005c" character="\">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAi0lEQVQYla3PoQ3AIBQE0CZNgDACggnwGPbAsAeGPTDsgWEIHAb7NQmakPr2p6no2Xcn7jjeQimlSilFCCEPZIyxMcZwzjl0HWOMpZSCotZa7723lFKihdZaCyEEFL33vvfeURRCiLXWMsYYtJBzzimlhKK11s45J+ecP/C/z+cdAQBqrRUAAF1/ygUoz0WQmhpSkwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="005d" character="]">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAaUlEQVQYld2QIQ7AIBAEh2o0mj+cJOE9vBJ7kh+AREOCr2sQDfVdO5vZZOEQA2Cttd57D7DWWq219jRijLH33uecs5RSXjUppbTD67T5IwiAiIiqaq217icYAOecCyEEgDHGyDnnT+MxNwlKKyOcrIjIAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="005e" character="^">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAf0lEQVQYlc2PsQkAIRAE93lQDK3AzEtMLMUKbMnIEuzE4OD6EIysQD7yO3j4iRYGFgb4hPuMGGMcY4w55xQRAYDryNZaM8aYEEIgItp7bwCAc86ttZa11oqIpJTS+11KKbXWCgA559x77wAApZRiZiYiAgCttWZm9t77bzL/xwPkHC0Jm9n1ugAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="005f" character="_">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAL0lEQVQYlWNgGAW0Aoze3t7e3d3d3egSCxYsWMAoICAgoKysrIwu+fz58+fk2wkAcOAKQwcLfHEAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0060" character="`">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAQElEQVQYlWNgIBcwMzAwMLCxsbFNmTJlChMTE9OtW7duYajy8fHx+fjx40dtbW1trMYoKioqioiIiJDtjlGADAD/0wxQ+/iB8QAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0061" character="a">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QIQrEMBBFJ8NSqKgKFCLqajKmIvcoOU18XA4RV5UL5AC1tT1AbhBTURGYFcsu7AEW9tn/xOMD/CPGGJNzztd1Xfu+7+u6rp/xOI4jxhiJiEIIoZRSEBHFW5imaVJKKSmlzDlnrbV+AACklJK11p7neQohBAAAIiIQETEzL8uyAACM4zgyMxMRYa21ttbaPM/zMAyDc8591Xrvfa213vd9b9u2MTNrrfVH6Lqu6/u+/+lnL55FK0k0AK3doAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0062" character="b">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAv0lEQVQYlbWQMQqEMBBFf5YEJI2pvIadde4g5hpewAvkPl4idU4gAYtAQBFMESHb7AZ22cZiXzn/z4MZ4H+c53lKKeX3/AEA5MXPEACGYRicc85aa/u+70sjxhiNMabruk5rrdd1XYspxhiVUgoAhBAi55zbtm2L1nvvAWDbti3nnDnnvIR1XdcAwDnnhBCSUkpFO8/z3DRNM47jGEIIlFJK35vHcRzLsiz7vu/TNE3XdV0fd1VVVTHG2K0P3ucJsWlTtQdXcj0AAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0063" character="c">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAt0lEQVQYlc2QIQ6DMABFf1tbEnRBNOEAGCQ3wKCqOEMTbA9TU4FEVyI4R4/Q4Aoh6dQmtsmJPfm/ePkf+EeapmmstTbGGPd935VS6lU659y2bVvf970xxhzHcXDOOSilNKWUhmEYAIAQQuZ5nqWUEowxlnPOXdd17zqac87neZ5lWZbPcBzHUQghAADLsizruq5VVVXTNE3XdV11XdcAgLZtW++9v+/7DiEErbX+mFQURfHrm77wABzjRgvyH9dnAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0064" character="d">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAs0lEQVQYlb1Qqw3EIAB9HAJRikFU4JAdoA7ThDk6QgcgwXario4DSUldRQvnSGouuRP37PvmAf9B27ZtKaX0fd+/PglfADAMw7Cu67rv++6ccw/Se++P4zistbbruq5aKaX0PM9zHMcRALTWunZSSiljjKWUEgDEGGONzTnn+77vpmkaABBCiEpe13Vt27ZN0zRJKeU8z/Nj0LIsizHGhBCCUkoBQCml1GGEEMI5578c9yXe7TJFV6+ryrkAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0065" character="e">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QscmEMBiG37vGD4sEm4ClRQaQgOAAqaziHpkho+gCgvbZIiM4QEQIiBC46m/+u/bgngHe5+UBfhEppZzneT7P8wwhBK21BoAnADjnXFmWZdd13bZt27quKxERiqIoUkppGIZBCCGapmnu+777vu8fnHN+HMcRY4w55/ynGsdxBBHRdV1X27btx0PLsizTNE1VVVXGGBNCCHVd1wAApZTy3vucc973fbfW2rcFxhj7Qqn/vADOUEfsc/tSWwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0066" character="f">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAjElEQVQYlbWQIQ7EIBREfzd1KLAIToHhPAQEcD2OwAHgCggwTbAI/pq2WbWp6bOTeckMwLtQSmmMMc45JyJiCCHcoffet9aaUkoJIQQhhOy/zZxzTimlu2GMMZdqrbXmiXPObYwxxjnn1lorpZRaaw0AUGut+3HSe+9jjFFKKZf182/B8xAR8ekvb/EF2NNPnB8+fbQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0067" character="g">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA30lEQVQYlc2QsQmFMBRF7wspBFsbWxW0sUsmCI5i6QoOEHALt7ATtxDBykYR06iIkl9Z/Oq3/w5wzuEC/zghhGiaptm2bauqqpqmaQrDMGQAUJZleV3XlWVZ5nme5/u+zznnYIyx4zgOpZQCgCAIAmutjeM4ZkREjuM4xhgDAMuyLK+OPc/zjOM4pmmavv6vIK21Xtd1reu67vu+f7EEAERESiklpZRt27Zd13VJkiRfBCKiPM9zY4xxXdfFW7jv+36e5znP81wURQEABACccx5FUWSttcMwDPd93z+f+wD3zmhtZ/lJrAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0068" character="h">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAe0lEQVQYlb2OsRHAIAwDhY8hGIKGDVgCCrZiJkZgBzcu6KB0qlDkcnSJKp9e1gn4TnPOGWOMT58AwBhj3p7oPkophZm5995zznkn1lqrtdZCCKHWWpmZiYg2TCklAHDOOVVV773ftSIiADDGGABgrbUbHgcdoarqKfiXLlsjMc0mRHA5AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0069" character="i">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlklEQVQYlbWPMQoDIRRE5yfbiAp6AT2X93A7T+JJxDN4CUFIZSnYmSYLWVi2CORVn5nhwQd+5XkcRETee7/WWq21dlpprfWcc8YY46WGc86JiE5hSim9Puz7vh/5AwBCCME552qtVUopj3IDgFJKAYDee/82Pu5euS03IiJjjAEAxhhTSilrrR1jDAghxLog55zvrP/iDRWHRj4/lDppAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="006a" character="j">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAoUlEQVQYla2OsQkEIRBFh0MYEDMLEEwsxC6MxAoswKqswDoswAtNTBTFi/bYY4+N9kUzvPn8AXgEzjkPIQQhhLhIrbXee2/nnPubZoyx70IppaWU8j6htdYAAKT33o0x5jiOMUZERAAAstZaKaV0yDnnPObX3fcPSUIIQUQ89wIAACKi997XWiullH5Fa62NMUbOOVtr7U9KKaWklPKu/8IHHz5D3Td/AScAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="006b" character="k">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAm0lEQVQYlbWQIQ4DIRBFJ0sxOMJdUHgsqB6BK+A4AwLJXbgFSQV2roCAULPbdLeiap/8789kMgC3QAAAEBFfO99y+7QIIdfJ7RoIIUSttTrn3I+MMcbee88558cRcs75c0dKKccYA46D1k4ppZxWISJaa61SSs05p9Zan6QxxgAApJRSa60xxtgGALDWWkfRe+8ppTSEEP496Q7esRtMWLQyMsEAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="006c" character="l">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAcElEQVQYlbWNsQ3AIAwErcAMtO5YgAW9CjU1Q8AIHgEhGtM7TRIlKei46qV7/QPsIeec9aKUUj7SOecQEYmIaq317WxrrQEAjDHGf/VYXW6S9g7MzN57H0IIvfcuIvK0jDEmxhjnnFNVNaWUVqu7OAG6SDSt5aSUHQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="006d" character="m">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAe0lEQVQYlc3PoQ3AIBAF0AsOT5iAGVjhLIJxmIoxUDhQIEgwGCa4q2ltk6YVffJ/8fMB/sY553LOea21YowxpZRqrdUYY4SUUmqttbXWIiKGEMIYY3jvvQAAmHPO3nvfe+9SSmmtNaWUEneTL0pmZiKiK6ATM/OT1185ALDcQS1HF1HTAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="006e" character="n">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAbElEQVQYlc3PoQ3AMAxE0ZOVJbxDSIaJ94qUjTxCaLBJJNNAF7WwaqWCfvp04IA/NuecvfduZjbGGCIiF+69t6pqKaW01pqZGRHRhbXWCgDMzBEROedM53qttQDA3R0AUkqJcNMzjIh4//P7DpwdMUWLINh9AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="006f" character="o">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuUlEQVQYlc3QIaqFQBhA4aOg8IOgzTZYbCb7gDuwuBzBfchkm2BzD27CqMGgmAb+cMtrL1+43w7OgV9UlmXpnHPXdV3LsizWWgsQAgzDMBhjTNd13XEcxziOIwBxHMfv+75N0zQAaZqmqqpVVVWhiEiSJMl5nifA8zyP997neZ6H/k+WZRmAiEgURdF93zcA8zzP0zRNRVEUfd/3+77vQRAEANR1Xa/ruqqqbtu2tW3b/ksSEfnyNYAPXSJNRzxK77wAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0070" character="p">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAxUlEQVQYlc3Qoa2FMBiG4a8nDYGaolgBWYdmiKZrdIEugGSWLsEASAxpgiAhgZo2gaRHUXPVdee3b55ffMAv3rIsyziOo3POzfM8SylljiGEME3T1HVdNwzDsG3bRgghOSqlFADUdV2nlJIQQnxeve/7DgDneZ4ppcQYYzlyzjkAMMYYIYTc933nt9Za2zRNo7XWx3EclFJKX+m99+u6rtd1XcYY8zzPk2Xf931ZlmVRFMULsgSAGGP8M0Lbtm1VVdW/lvsCSNNcscFzBHQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0071" character="q">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAyUlEQVQYlc2QKw6EMABEhxZCKhZRQ0AimoYD1ILpOXoEblBJOBWCw2CakBIMAuiaBbPJ6h37JpkP8I9SSqlxHMdlWZa+7/t5nmchhCAAYK213nuvtdZlWZZFURRxHMeglNJ93/e2bVsAqKqqCiGEuq5rQimlaZqm67quAOCcc3ccua7rOs/zZIwxAMiyLHvgcRzHNE2TMcZwznnXdd0DAWAYhqFpmsY55/I8z7/mRFEUvT56Ct0whBC2bdt+HkIIIVJKmSRJ8tP4BgM8Tq2elpx2AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0072" character="r">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAdUlEQVQYlc2PsQ3AIAwErSireBHEFCzgQbwBLTVDsAYFG4Ao3FI7FVaqSJFS5KqX74s3wB/JOeeUUqq11jnnZGY2WUopvffunHMhhKCqiohoMsYYd7m11oiIzn0QEdnZe+/XWsvknTHGAAA4noaaVFV9/+f3XJKUNcEowcICAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0073" character="s">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtklEQVQYlc2QOwrEIBRFH6+KBMtUySZchIV7kNTWVllBwNa1ZAfpUopYGEh6VyCYz3QDQ6acYk574Ry4AP8I55zP8zyXUopzzkkp5Xtc13W11tqu6zqllIoxRkIIAQAA772fpmlijLG6rusPrRBChBDCdV1XzjkbY8yjXVVVNQzDcN/33bZti4iI27ZtWmt9HMexLMsCAEAppQAA0Pd9v+/7fp7nmVJK4ziOD23TNA0i4u9O+soLn6hPnzInEQIAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0074" character="t">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAiUlEQVQYlbWOsQ3DIBBFf+KwBrihhYaekmnoYApWyC4MQMsGFLdBSkgDlqVIVhq/6uv+3dMBt7Ct4L33xhhTSilr9lpBa61/TkMIgYjoM6GJc8499kmMMQJASikBQK21Hob35Gx9Xn37X9laa0opJaWUQgjBGGPHFuec55xz772PMYa11l5Z7+ILqL465/pTmCoAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0075" character="u">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAg0lEQVQYlc2PoQ0DIBRELyQnAc1fAYNhDNZiFBwjMAMaDzMQDKmradI2VX32cu9ywD8y55wxxggAJLnWWt57rwDAOedIEgCUUkpEhCTVO+PncO+9jTEGAKy1FgDOOQcAUEoptdYqIpJzzmOM8ayHEEJrrd17b++9p5TSy4bWWn/9/3cenkopcTvCwUwAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0076" character="v">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnUlEQVQYlc2QsQkEIRREPwcb/RKM7EFbsAARi7ABe7ANQ7UALUaxAiMNDOSiXbjw4IJ70TADwzAA/4YQQvTeOyLi7eWcs7XWAiLinHNKKSUAACGEnHMOY4wBAEAIIXjvPQCAMcbUWutTrbXWY4xxXddVSinOOfeEiIhrraWUUnvvzTnnH8NSSqnWWltr7fZetwghBEopjTHGXz/yDW+dY0Ivukb51wAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0077" character="w">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAxUlEQVQYlc3QMQ5FMBgA4B8vdNCuLDZr0/QKkh6m13EPhzDpIrZKiIEVw7+UgfQd473vCB/An4nquq6LoijWdV2XZVmGYRjSNE2bpmk+53menHM+TdOU53kupZTbtm3HcRxRlmVZVVUVIiKllMZxHAMAICJGYRiGWmvtnHNt27ZKKfU8z2OMMZAkSXJd12WMMWVZlvu+713XdUIIAQAA8zzP7/u+QRAEfd/33ntPCCEfAIBxHEdERO+9t9Zaxhi77/v+Rd8Xv9RqYqVP+rgAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0078" character="x">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAs0lEQVQYlc2QMQrEIBQFf1iLgEX6gGCXM9h7iVzBM3iF4GGCbVKnEMRCIYWV2AZiLW4X2Ga7hZ12eFM8gD/jRQghzjmXUkreez9N02Stted5ngAAIKWUOec8DMOwbdu2ruv6zBFCyBhjnHPuuq5rHMfxoz/P89xaa0op9SH6vu9DCOE4jqOUUiil9JHLsiwxxogxxlprve/73nVdB4wxVmutnHMOAEAIIfd930II8auLvvEGonZSJHFUDYgAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0079" character="y">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtklEQVQYlc2QMQrEIBREf2MrQsgRPsRr5AT26WxS2Np6gpS5h7nAbxU8QC5gF8gFrP5WuxDc7XfKeTAzDMC/aZ7nudZalVLq7RERee89CCHEdV3Xuq4rAAAiIjPzNE0TAABs27blnDMAQAghlFLKJ1prrZmZERHP8zydc+7RnVJKx3EcrbU2DMPwgNZay8wcY4zd6nEcR2ZmY4zp4LIsy33ftxBCdJCIaN/3/esZiIhSSvnrrE4vT6hLqa7LHfoAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="007a" character="z">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAiUlEQVQYlc2QsQkEIQAE9yITQzExFxPBSLAOsQQrMbIrC7ALLUEwEe/S9z/7T37SgWVY4M+4AKDWWoUQ4lVIKeUFAM45RwghAJBzzowxppRSx0yMMY4xhtZaH8Jaa+ec03vvD8E55621llJKH0GllEIppSGEsPfeANB77wCAtda63zDGmF+v+IYHniM7AGsjpawAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="007b" character="{">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA0ElEQVQYlZWQMQqEMBBFw2QrQ6yUYGOjVeIlLFN5h5wrx/AInkAsgl06CWnsRDC7jYqyFruvm3l/hmEQ+peyLEuMMb4167qurbV2nuc5z/P8Jtu2bbXWGgAAIYTgKjHGeJqmKYQQvqRzzjHG2FEDAACllBZFUQghxDiO45kWQoj3Ttd1XRzH8TlpjDFpmqZVVVWEEKKUUod8bdu2+Z2+73vOOT8nrwclSZJ47/2jXNd1JYSQx7c1TdMsy7IMwzBkWZZ9BSilVEopoyiKHjf8xAdEblLpzqASjQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="007c" character="|">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAANklEQVQYlWNgIBbo6OjosLGxsWFIMDExMf3///+/qampKVwMn0mjkuggMDAwUFBQUJBoDTgBADDVBz1puKD+AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="007d" character="}">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAv0lEQVQYlZ3QMQrEIBAF0LCkE4vY2dgImkbwHJ7AyiN4gxzEu3iJtEGwSSAQSBNIIBB3m92woLCwvxrmDcwwVfUrnHMOAABFDCGE67quruu64oC11i7Lsnz3Hp9iXdd13/e9iIwxNgzDUMR5nmdCCGnbtoUQwrqu63sKIYTGcRyf7xhjzK1aa30cxyGllNM0Tdu2bTdijHGMMfZ932c7QwiBUkqLB6WUUtM0TfYA770/z/N0zrkMlVJKCCEy+Dsvju9VO5oT3xUAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="007e" character="~">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAZElEQVQYldXPsQlFIQwF0AuvNBO4htMIgYBzhIyRQdzEyj6NG1ileEt8+LyzwQG+5AEAEREzs8zMvfdWVS2lFNRaa2amu/u9944xxjnn9N47WmttrbUAgJk5ImLOOYmI/pv6qRdqGCnwWcsDxAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0401" character="Ё">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAxElEQVQYlbWPMQqEMBREJ8sqNqlTpEjjJaxTGgg2NrmCXU4WELyEV1BIpWhhaWPYrSJLWLbb182fz5/5AADOObfW2jzPcymlbJqmQcQYY0IIoSzL0jnnxnEc8QmllAJAlmVZURTFbczzPL8SlFLqGRe6ruv6vu+j3rZtu81933fvvf+Mus2qqqrrui4AOM/zHIZhIDGTEEKO4zgAYFmWpa7r+i7Utm2LhEc6+JrJGGNCCBH1uq4rAGCapin9U2utf139F2935mpkysr/rAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0410" character="А">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAzElEQVQYlbWQoQ6DMABEr9tEMVUkoDAY/qK0GgP8CYqfq4FvIJhWLSUIkmqShs4MzMzE9tQll7xLDvgL9zPUdV3P8zxTSqlSSgHA7SyllNI556SU8kMxTdPU933vvfeMMXYVSZIkx3EcaZqm1lpbVVV1aYUQQmut13Vdh2EYhBACAB7n3r7ve9d1HWOMFUVRAAABAK21fr6Joihq27aN4zhGlmVZCCHkeZ4DACGEbNu2NU3T3MqyLJdlWYwxBgBCCGEcx5Fzzn976Xe8AHbfWmPCJlRGAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0411" character="Б">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsklEQVQYlbWQsQ6DIABEj5YPYHRkhNGvcOMTSPwMP8uwsLg48gtOrMQEYTMh0snGOrbp2y6Xu1wO+B/btm31BmOMUQAghBCttZ7neT4DKaVETxFCCN57f218AsAwDAMAcM55zjmv67riWuGcc9M0Tfu+7+M4jh9m13UdAAghxHEch5RSPu7Ll2VZcs65bdv2PahpmkYIIfq+7yml1BhjAAAxxlhrraWUYq21Sin143Vf8wKDE2UOZSMfywAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0412" character="В">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAwElEQVQYlbWQPQqEMBCFh2VzA9HKLiltQxByBDuxDXgFj5H72KSf3EHBVmyNnTCzlcJut7D72o/3wwP4n1JKiZn5PM8zhBCapmlueBzH0bZtq5RS3nu/rusqhBA3tNZaAICyLEtmZqWUel5uY4zJ8zzv+76PMcZ5nufbuSzLMk3TREQ0DMPw1nnF1nVdExFprfXjczki4rZtW1VV1d1ZFEUhpZRd13VZlmXjOI4AALDv+87MTESEiOiccz898gu9AJ8CZptGWhdkAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0413" character="Г">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAARElEQVQYlWNgoB1oampq+o8GduzYsYMFpuDQoUOH4uLi4mD8Hz9+/IBLfvny5cvDhw8fIpvIhM+6AZX8////f3wK6QUAEGconZ31ULwAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0414" character="Д">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtElEQVQYlbWQMQ6EIBREZwUswdoCEmJPy0U4EHfyCpaWngASL2BISDR/Kzeuu80W+9qXmcn/wP+IMUa6obXWDQBwzvk4jmPf971zzgEAY4zxU27btq3ruh7HcZyNr+S+7/t9rgEAIYS4Jt6klFKWUspXOQzDkFJKH1Jrrb33flmWpeu6TimlAEAppTBN03S/kYio1lobAAghhMcFa60lImLWWjvP85xzzudW27atMcb88uV3nqKGYEJfxLFlAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0415" character="Е">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAh0lEQVQYlbVQuwkEIRScd2wPhtZhDQpmVmF5gtUoGCkaGJoIe5FyGNxGO9kwjzcf4D3EGON9QCmlrnVgrbXOObd4rbVusbXWUkrp9+MWhRBizjkBYIwxvPeelicRUe+9A0DOOUsp5Q5kjDFn0M+/FtuTMcY453zxUkoBAIQQwtlTa62f9nkDXz3kWXgV7pA8AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0416" character="Ж">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA5klEQVQYlbWQK66EQBQFDzOeBIMkgCMQFKYFu2gBbZAEyx6wbAPTFotgETgUjr6aT3ITnnojx035SioF/IZpmqa+73sAICLyfd9XSikiotcwDIOUUlqWZf0LUkqptdZwHMe57/sWQggiojiO4+M4jjzP8/d1XVeWZVkURVGSJAkzcxiGYdu2LQCg67rOGGOYmY0xZhzH8RO1LMtS13VNRKSUUud5nrZt20jTNGVmdl3XJSIKgiDYtm2rqqp6FUVRzPM87/u+A8DzPI/WWpdlWWJd17VpmgYAjDHG8zxPCCGYmX+09it/M/B7pCMz4lQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0417" character="З">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAy0lEQVQYla2QsQqDMABEb3LTJYgO/kFAXMUfyD/EHxCi4B7I5uQ/ueRPRBd1yuR03UpLO3ToG4/j7jjgLwghxIeolFLbtm0keV3X1bZtCwCIoigKIYR5nuc8z3PnnLvv+47jOEaapqm11iZJkgCAlFKSZFEUxTNaSim11tp775dlWd56nXNuXdeVJI0x5utipZQiyaqqKjRN00zTNL0a9n3fjTEGZVmWJDkMw5BlWdb3fU+SdV3XAICu67rjOA6SPM/zHMdx/O2h//MAavtgJaTXop4AAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0418" character="И">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAApUlEQVQYlbWQsQ3DIBBFv3AkxmAASwiXLulc0yAxFVPAKizgAW4DV1D5LpWjWKkiJb/79+7r9A/4n87zPJdlWQAg55xFRKy1Vr0vTdM0xRgjACil1A167/0YY/TeOwDcYEop1Vqr1lrfbq7ruh7HcczzPDMzO+fcK7lt20ZEtO/7fs1eMIQQSinlowozs4iIMcZc3jnnHpdprTUiIgAQEfn2V7/SE7vRT5d1v3weAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0419" character="Й">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA1ElEQVQYlb2PsQ2EMBAEV/ZLILkICjAyhswhGQkBJEiUQDVUARl10AARCe6ACCJ8Hxk9//lPdKu929UBAOI4jruu6wCgLMuyqqoKHiGEWNd1naZp2rZty7IswyeMMZYkSRKGYfgwruu60jRNAaDv+56ISCml2OcS55w3TdP4pIeZ53l+nud5HMcBAA+zbdt2HMcxCILg0WmMMfu+71JK6ZxzWmt9XxZFUVhr7bIsy/2BH+q6rodhGPCNc84REUVRFHmttdYvL+Z5nq21FgCIiH4S/sQb65hf4yQm9CIAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="041a" character="К">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuUlEQVQYlbWQMQqEMBRE57NLRMgpxAPkBtrbCLlGSq9ik8Yb5BbpA9aCVUDSWSRFLLZSEKuF3WkfvBkG+EteABBjjNZau67ryhhjy7IsRVEUAICUUmrbtgUApZQKIQTOOb/BsixL770fhmEAgPfpF0KIrus6IqJxHMerOKWUtm3b9n3fjTHmturU9n3fH8dx1HVdPyARkXPOTdM0PSAASCllzjlXVVVdsGmaBgCIiOZ5nrXW+kdffpUP6glflAFEpnMAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="041b" character="Л">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAj0lEQVQYlbWQOwoDIRRFL8FPZWNtIegG1C3OLlyKe7C1dgcidgqmmikyCWmS0x4Ol/eA/3Ecx7FfsNbaBwAQQkhKKWmttffenxEBAEop7b33WmsdY4xTXuWcc77OXXKttd5Kxhj7WCqlVGut3aRzzoUQQiml3G7MOecYYxRCCACQUsq99zbGGHDO+dcv/ZYn0j5B9WBXoTQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="041c" character="М">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAm0lEQVQYlcXLsQ3DIBQE0PvfCPcUnoQB2ICaATyFd2ATS9afxLUbCkZggEuVKHHqKK873R3wGzHGSJL7vu8AsG3bRpLruq4KAGOMkVJK3nufc869966qqgBAkud5nqWUsizLcl3XBQAKAM45Z2ZWa61mZvM8z19lCCEcx3FM0zQBgAMAEZHWWlNVJUkRkdfziSTf80d5p/f1vz0Ag9ZCg71BUkQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="041d" character="Н">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAb0lEQVQYlb3OsQ0DIQyF4d9HWiR6DwA9I8D+ywAS1TkNSFF0l1TJ19jyky3D77TWWimlAIQQgplZjDEeALK8Lx2fLj52k3POIiLee79nAtB773POOcYYzjmnqppSSuyw1lovH7rzPTwXADOz1/pvT3k8MQi8BRYhAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="041e" character="О">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQYla2QsQ2FMBBDnVRECCZAbJCGBbIC7EORaa7IDuxwLUWWiHSku/zq/wYJ/QI3lvwa28ArGoZhuIXLsizHcRyqqszM27ZtP5hSSkRE0zRN+77vOecMALDW2lprDSEEAOj7vm+tNe+9t8YY03VdJyICANd1XQDgnHP2qeAzVFUVERnHcQSAr5dSCgCAiCillOZ5nmOM8TzP87aztdaYmdd1Xf976H19AFl8TMWcK0SbAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="041f" character="П">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAUUlEQVQYlcWNIQ7AUAhD4WcHQXABLsTdsSgEGqZIlom5//dMmzZNAfYREdEvmJkXAAAioqoqEZGIyIyuMe7uZmaZmZOtr7vfyqqqCbq7n3qaG1i5Lf9EjqipAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0420" character="Р">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnElEQVQYlbWQoQ0EIRREP5dTNICiCwogOCqAQmiIItYjCP4TJMHRAiQYztyu2Dtz4p6dzLxkAP7HGGPsN4iIzjlHCCEAADDnnMYYwznn1lo7xhhKKUXOUGutQwgBAOA4jqP33h93BWOMCSFESilds621lnPOa60VY4yUUvo8G957X0opiIi11npNzTmnlFLeFR/Or+Hee/90zp94AWrRVqh2QYAFAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0421" character="С">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAp0lEQVQYla1QuwnEIAB9WgriBpE0NjpEBnAMCzdwDDdIa28hrpKMYQSbFFdd4O4g1+SV78H7AY+Ac85/SGOMqbXW8zzPbds255y7xJRSyjnnaZom55zrvffL5TiOw1prAYAQQkIIYZ7nGQAwxhjLsizfcfSu4H9xjDGEEAIAKKU0xhiVUgoAsK7rWkopUkrpvfettcYYYwAArbV+79z3ff/YefvQ83gBmn1DU+spXHkAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0422" character="Т">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAASElEQVQYlc3NsQ3AQAgDQERNxwAWQ7AGc7AS0zodVZTu9bnSlmyRM7q7+WJmRszMACAigiSrqgDA3X0XVFVJMjNzs6/L35U3PP5oJAEjEsTtAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0423" character="У">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtUlEQVQYlbWQsQmFMBiEr7GxCWSBoIWtK2QG67TBAWycwCKlG2QER4iCpVWWyAQh8JO86j0Qq1d45X1wdxzwjoZhGGKMkTHGvt51XdeyLAuqqqpCCEFrrQGgaZqmlFL6vu8BAMYYcxzHAQDTNE3ee/+L7rquyznntm3b8zzPeZ7nW7dzzq3ruhIRCSHEDSqlVCml7Pu+P1bXdV0TEY3jOD6glFKmlBLnnD+gtdZu27b9cdEr+gCEqEvt4xiDugAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0424" character="Ф">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA2ElEQVQYla2QIQqEUABEBxERPoJVPoJGs80kWMVriAfQq1hsnkE8gelX9QA2qx+MzqYFl4UFYSc+BuYxwF9S13XteZ73AYMgCJqmaUiybds2juMYAOD7vr/v+z4Mw3Bd1zWO46i11mmapqiqqlrXdQWA8zzPKIqivu/7rus6Q0oplVLqPqOUUlJKafwSNEjStm37Di3LskjSmOd5zrIscxzHAQDTNM2iKIplWRYIIcQ0TdNxHAdJaq31tm1bGIYh3u0kSRKSLMuyFEKIL4E8z3PXdd2nzz7MC03hYFFqmbeZAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0425" character="Х">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA10lEQVQYlbWQoQqEQBiEB3/YIDaDD7Btn8LmC8j2fY4tdrPFJ7C7RbNPYLApatGoGCyLXDg8OC4d3E0aZuBjGOAvojAMw2EYBmOMWZZlcV3XHcdxvK7rAgAYY0xd1zUAaK113/c9Y4wBAIQQwlprpZRy3/c9juP4jZ/neW6ttU3TNHfm3KYoioKIqCzL8mNZVVXVPM/zuq6r53neq4iiKDrP8+Sc82mapiRJkifXcZy2bds0TVMAUEqp4ziOIAgCKKXUtm2b7/s+ABARdV3XZVmW/eTJL/UAox1jUu92BO0AAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0426" character="Ц">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAbElEQVQYlcXOMQoDMRBD0a+d0tUMuPBtXMz9e5/CxhdIqoRlSbYLeZVAhQS/s9ZamZkAmZl77w1wAEiSmdkrS9K7/OavZUREa61FREhSrbUCMOecj4sxxjgA3N110nvvAPZpq5RS3N3v/tx7AttTI/AfDEtdAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0427" character="Ч">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAdUlEQVQYlbWOsQkAIRRDg/8awVqwEaydwTV0Rgu3cBM3cIFcdVcI2pkyj0cC3EmMMZKkMcYAgLXWkqT33qudJCKyhQCg5pwTAJxzboXPGGP03nuttbbWmojIDwEg55xLKSWllLTWervzvQ0hhPOhtSDJk3A7LwAvJo8DYRwWAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0428" character="Ш">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAYklEQVQYlbWNsQ3AIAwEH2yJGrEAO7nyTHgAb8UWrOFUaSJEE3Ht6+6BO6iqzjknAKy1Vu+9u7ubmeWIiK+QUkoAsB1f8unybP7LEhG11hozc621llIKAEBEJDaMMcapeosH0X05cx9mb9kAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0429" character="Щ">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAaUlEQVQYlcWOsQ0EIQwE116EhFNXQUe4DLqiDlohp4X76EPITjfRSiONFniHiIg55wSAvfcupZQxxui9dwUAEREAUFX9b5LUW/UzmVJK7u4k6e6ec85mZmittefAMbnWWjzJWmu9/bnzA+SxK0Zt/sr3AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="042a" character="Ъ">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsElEQVQYlb2PsQ3DIBREP06QPAANlWEJCksuEBswAkzAEIjB7A1gAeTGDXJDQ2ElTZzCSZu89p3udAC/4fFiWZbl6jrGGPPee4QQ+pA557zv+/6t9QYAMI7jKKWUx3EclFK6bdvWWmv3M0UIIVprLYQQpZQyTdPUnTKllJRSilJKMcbYGGO6606ttcYY4zAMw1v2fd9zzrm11kop5TzPMwAAOOfc+Xdd1zWEEL5d+wdPAYRNboE68LcAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="042b" character="Ы">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAqUlEQVQYlb2NsQnEIBiF/+QICIKlA6SQgGghDhOdJEM4gAM4RCytbV3CFSz0qhw5Dq67e917H7wP4Dcxxpicc75v3nvvnHMzAMA0TdMdXn3+9joDAFBK6b7vO8YYf0BCCDmO46i1Vs45H2MMAICHEEJs27YppZSUUq7ruiKEUGutvTlLKYUxxt5ul2VZtNbaWmtjjPEFxxhDSinP8zxTSimEEHrv/fL+O0/ivTsaOLjyiQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="042c" character="Ь">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVQYlbXQva3DIBSG4e+ElgFMZbGEOwpLlhiEKahcMhgjeAAKhOQGARINVdLcmyI/UorkKc9bfNIBfqfWWrdt2x7vFwAgImKMsZfxHQYA1lo7xhhSSimEEOd5nmOMQQDQWmshhJBzzsuyLDnnrJRS+I9aaw0AnHOeUkr7vu9Pm733fhzHMc/zfI/TNE1SSmmMMeu6rt57DwAopZTrnxhjdM45IqLPP/U9Ny2GTa0vVLndAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="042d" character="Э">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAyUlEQVQYla2QsQ2DMBBFf6w0yGBXFpZMSQWdazpo2IYBmIACUXgLBgHTUZhhaC4VaRJFkZJX3tP/ujvgLyRJkrwMm6ZplmVZiIi2bdvatm2fct/3fZqmyRhjhmEYjuM4AABxHMd93/dKKQUAWmtNRJTneX670tZam6Zpyjnn8zzPZVmW90t2XddVVVUxxtjHjYmIiqIoWBRFkXPOGWMMAEgpJQCc53kCALz33jnnsizLxnEcQwjhWVXXdb2u6/r2zgshhPjymb/wALRxStgk+bZaAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="042e" character="Ю">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAyUlEQVQYlbWQKw6EMABEH+0qUL1ANUEhuEGPgOoB8JgKNAfgDFwEicCRkGAwBMEB6mHNLsl+5O5zk0kmLwM/I4qiKAiCAABrre37vgeo67r23vtlWRZjjBEAUkqplFJlWZZJkiRt27bOOSeec3Ecx+u6rtu2bV3XdWmapgJAKaWyLMvO8zwBjuM4AASA1lpXVVW9ywmAcRzHPM/zr+UVhLgEX8ppmib9wBhjhmEYbk8J771vmqaZ53ne930viqL4eCgMw/B66D/cAV7ATMMCQg6KAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="042f" character="Я">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuklEQVQYlb2OIQrDMBhGP5ILlEJqCoWa2qqKuqhAXI9Q3QPlCNE9QlRsq3OJmIoQAv/MBuumt2c/Hu8DfofWWp/neZZSCj3puq5jVVVV+77vzjk3DMMwjuMIAJxzDimlJCKq67oGgKZpGiKivu97dhzHUUop67qunHP+1dy2bUsppeu6rhhjfJkAAMYYCyEEY4xRSqnbuCzLknPObdu2700AgPfeW2vt5yHM8zwTEU3TNAGAEELczD/zABNrZ21n8NN3AAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0020" character=" ">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAADklEQVQYlWNgGAXDCQAAAdAAAYH02hIAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0430" character="а">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QIQrEMBBFJ8NSqKgKFCLqajKmIvcoOU18XA4RV5UL5AC1tT1AbhBTURGYFcsu7AEW9tn/xOMD/CPGGJNzztd1Xfu+7+u6rp/xOI4jxhiJiEIIoZRSEBHFW5imaVJKKSmlzDlnrbV+AACklJK11p7neQohBAAAIiIQETEzL8uyAACM4zgyMxMRYa21ttbaPM/zMAyDc8591Xrvfa213vd9b9u2MTNrrfVH6Lqu6/u+/+lnL55FK0k0AK3doAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0431" character="б">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA5UlEQVQYlbWQscmFMBhFrzGIitpYiktErAQ3sHEMu6yQWbQRHMHGCexFrMwGCoHI96rf5m/fO+U9cIoLfA3Xdd00TdN3CMMwVEoprbV+nufZ931/Zd/3/bZtW9d1XVmWZZZlGQBwAGiappFSSs/zPM45P8/zBAD4vu8TEWmt9bIsy3Vd1zAMAwAgCIKAiEhKKQGgruvaGGOSJEleKYQQAMAYY9ZaW1VVxay11hhj4jiOASCKoogxxu77vgEA0zRN4ziOeZ7nSil1HMfhOI4DACiKopjneSYiWtd1bdu2/ffOX/rHfAB9AGb3FNwK8wAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0432" character="в">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAk0lEQVQYlc3QMQoDIRAFUIlprQS18Qx2Cl5HvIM3sPdQ3kGwEJtcQNBp4ja7CylSBvLa+f8Xg9A/qrXW1ynnnJ1z7j7OOaf33htjTAghAABwzvnzCvTeeymlSCnlWmuNMcbdfJ/23jullD5mrbUWIYSUUgoAQGutH1eAUkoZY0wIITDGmBBC7uY+tdZajDH++kPfHIbGWc/DIXgrAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0433" character="г">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAASUlEQVQYlWNgGIwgIyMj4zkaSE5OTmZhYGBg4Obm5n7//v37rKysLJiGmzdv3mSBcd6/f//+wIEDB5BNZMJnHYWS/6AAn0J6AQAy+SlNkz/OOQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0434" character="д">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAApklEQVQYlc3QMQrDIACF4V/rkmxCwLgEPEKu4O4JPIi3ywkEs7pmaSA5gUs7BUrbqVO/9cF78OAfpZTS/Y2UUkoArbVe13UNIYSUUhrHcQRQAEopdRzHkXPOQghxNUqAvu/71lp7n5MA1lp7nuf5EU7TNHnvfa21WmvtMAwDgDHGsCzL8vhi3/ddAsQYo3gxz/MMcHPOuVJK2bZtu7a6ruuMMeb3557RM1DqYyMo+gAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0435" character="е">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QscmEMBiG37vGD4sEm4ClRQaQgOAAqaziHpkho+gCgvbZIiM4QEQIiBC46m/+u/bgngHe5+UBfhEppZzneT7P8wwhBK21BoAnADjnXFmWZdd13bZt27quKxERiqIoUkppGIZBCCGapmnu+777vu8fnHN+HMcRY4w55/ynGsdxBBHRdV1X27btx0PLsizTNE1VVVXGGBNCCHVd1wAApZTy3vucc973fbfW2rcFxhj7Qqn/vADOUEfsc/tSWwAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0436" character="ж">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtElEQVQYlc3QMQqEMBRF0QciWFtGspr0EtKkdBVuQNOmdDNaBcE6XVo7eYXtbwSZatYwczdw4AL/1rIsyzRNEwDknLNSSjnn3HEcB4wx5r7vu2mahiS11nrf930cxxEAUEopwzAMJNn3fS8i0rZtCwCIMUYRkfd9XxGRlFICAFRVVZ3neXrvPUkaY8zzPI/WWsNaa6/ruuq6rr/mtm1bCCFgXdd1nucZAEiy67rOOedI8hf3PkfnYt04HWpgAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0437" character="з">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAs0lEQVQYlc2OoRHDIBRAP71iSBzcER2RGTJBNojJDHgYAskUjILMAHEwADkEEfyq9q4XW9E3wHsP4B/Ztm0LIYTruq5933ellAIAgK7ruuM4DmOMmaZpMsaYWmsdhmF4llLKOI4j55z3fd8jIpZSSs45f9Tee99aa4iIzjlHCCFfbUIIWZZlQUSc53l+SCnleZ7nuq4rY4xRSun7BQAAtNY6xhgREVNKyVprb0ohhLi1fs8LpmFSfwYhTBUAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0438" character="и">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAk0lEQVQYlc2QIQ5DIRBEd3sBbkECBoNAwWlw/wicDhSKC+ARCOQGw1a1adOkrklHTvJeJgPwj+m9d2OMAQBIKaUxxpBSSgAAICKy1lohhFhrLWZmrbW+vRqu67pKKYWZ+VkSEYUQwpxzOufcBxljjK21Vmutb4P23puZ2XvvEREf5FObc84AAIiI55yjlFK/+eZ77rq+T1Xz2tpaAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0439" character="й">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAxUlEQVQYlaWQIQ6EMBRE528wKG7RBEJCQipQ9BZwAhwew9UwrULVVBIsAoFsMP2rlizB7Y6cybxMBvhLZVmWwzAMANC2bds0TXOFcRzHzjk3TdO0ruua53n+IAghRBRF0c1clmUpiqIAgHEcx23bNiGEAAB4772UUiZJkhzHcTAzZ1mWvb4Jfd/3xhjDzHyZ3nuvlFL7vu9VVVWPZtd1nbXWzvM83wad53kyM9d1XRMRfZoXVmutAYCIKIQQ0jRNf/jzb70BsD5f5VfLuEAAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="043a" character="к">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnUlEQVQYlc2QqxEDIQBEd5KYQzCHuiYogAIogQrQ0AQzGCQFnUdhoAYsCoFJTLiJjIjIU2v2Mwv8GXcAOM/znHPOUkohhJCUUmKMsRsAHMdxbNu2AYD33lNKaYwxPlbEvu+7lFJaa62UUo4xBgCg1lqfb3LOeRluSxhjjBBCcM65Ukpdq2qtVWutV2drrV2DPnHOud57DyGEX1/zDS98/kCmTdNVMQAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="043b" character="л">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAiklEQVQYlc2QoQ0EIRREJxsQhHyDIFTwExqgNQw9YSiA0AZB47EITq3ZNSdO3LMvmZkM8I/EGON8wMwsAMAYY8YYI6WUtNa6lFKEEEIAgJRSzjlnrbUSEd2J1y333vtZdwGAc86ttdZLMjOHEELvvb+WttZazjlbay0AEBGdc4733kMppX70yLd8AJA/OKcId/ZhAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="043c" character="м">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAoElEQVQYlc3PsQ3DIBQE0Ps/fBeU1OzAMmxAYdnbuEeIERjCco/o3HsAWpqkiaNskLz27ooD/szDOedaa00ppfZ93733/jiO47qui0VEjDFmnueZmXlZlmWMMbTWmgGg997P8zzXdV2ttbbWWgGAAWCapinGGLdt21JKSUTkEzIzl1JKCCHEGCMREQAoACAiGmOMnHO+ywDAz7fvC/fyF17xckACCUlV5wAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="043d" character="н">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeklEQVQYlc2MIQ7FIBBEJ2WDQIDBbEAjOAJn4P4KzQFICHKraNqm+uc/Ny8zA/wjrbVWSikAYIwxvfeeUkoHAIQQgtZaA4BSSjEzExHRXnvvPTOztdY+btdaS17knPOxC7XW6pxzMca43XU755xjjCEist21vMuv/CtOPO40jxWVJmIAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="043e" character="о">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuUlEQVQYlc3QIaqFQBhA4aOg8IOgzTZYbCb7gDuwuBzBfchkm2BzD27CqMGgmAb+cMtrL1+43w7OgV9UlmXpnHPXdV3LsizWWgsQAgzDMBhjTNd13XEcxziOIwBxHMfv+75N0zQAaZqmqqpVVVWhiEiSJMl5nifA8zyP997neZ6H/k+WZRmAiEgURdF93zcA8zzP0zRNRVEUfd/3+77vQRAEANR1Xa/ruqqqbtu2tW3b/ksSEfnyNYAPXSJNRzxK77wAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="043f" character="п">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAWklEQVQYlc2NsQnAMAwEP0GVhSbQMF7Ma7nXAr+BdlEqg0mRKoRc938PD/yMAwAyM1VVd9F77wIA7u5jjEGSrbU255wiIrKWJBkRYWa2uvPp8wVZVbWLe/6KC/hVG6btSRffAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0440" character="р">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAxUlEQVQYlc3Qoa2FMBiG4a8nDYGaolgBWYdmiKZrdIEugGSWLsEASAxpgiAhgZo2gaRHUXPVdee3b55ffMAv3rIsyziOo3POzfM8SylljiGEME3T1HVdNwzDsG3bRgghOSqlFADUdV2nlJIQQnxeve/7DgDneZ4ppcQYYzlyzjkAMMYYIYTc933nt9Za2zRNo7XWx3EclFJKX+m99+u6rtd1XcYY8zzPk2Xf931ZlmVRFMULsgSAGGP8M0Lbtm1VVdW/lvsCSNNcscFzBHQAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0441" character="с">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAt0lEQVQYlc2QIQ6DMABFf1tbEnRBNOEAGCQ3wKCqOEMTbA9TU4FEVyI4R4/Q4Aoh6dQmtsmJPfm/ePkf+EeapmmstTbGGPd935VS6lU659y2bVvf970xxhzHcXDOOSilNKWUhmEYAIAQQuZ5nqWUEowxlnPOXdd17zqac87neZ5lWZbPcBzHUQghAADLsizruq5VVVXTNE3XdV11XdcAgLZtW++9v+/7DiEErbX+mFQURfHrm77wABzjRgvyH9dnAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0442" character="т">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAUUlEQVQYlWNgGGSAMS4uLq6zs7MTXWLZsmXLGKWkpKTU1NTUmJiYmPbu3bs3MzMz88aNGzeeP3/+HK6SiYmJ6f////9NTU1N4WL47KSR5EAAAGWUF7OsG5RSAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0443" character="у">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtklEQVQYlc2QMQrEIBREf2MrQsgRPsRr5AT26WxS2Np6gpS5h7nAbxU8QC5gF8gFrP5WuxDc7XfKeTAzDMC/aZ7nudZalVLq7RERee89CCHEdV3Xuq4rAAAiIjPzNE0TAABs27blnDMAQAghlFLKJ1prrZmZERHP8zydc+7RnVJKx3EcrbU2DMPwgNZay8wcY4zd6nEcR2ZmY4zp4LIsy33ftxBCdJCIaN/3/esZiIhSSvnrrE4vT6hLqa7LHfoAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0444" character="ф">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA0UlEQVQYlbWQMQqEMBREh1RWIUewtLIRIRewt7G1swqCnbcQPENOYukJ7FIbCIE0KdL8rXZRWGGbnXLmwTAD/FeMMUZEVFVV9fG+QTej67pu3/ediOg4jmOe5xkAUJZlmVJKwzAMIYQwjuN4nufZ932PaZqmbds2AHDOuTzP82VZFq21Zpxzbq211xprreWccxZjjEIIcQ2FECLGGCGllCml1DRN4733bdu2xhijlFIAAKWUMsYYIqIQQljXdb1Nep9Q13X9eMKjiqIosizLfoJfmjZlsQpDnVsAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0445" character="х">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAs0lEQVQYlc2QMQrEIBQFf1iLgEX6gGCXM9h7iVzBM3iF4GGCbVKnEMRCIYWV2AZiLW4X2Ga7hZ12eFM8gD/jRQghzjmXUkreez9N02Stted5ngAAIKWUOec8DMOwbdu2ruv6zBFCyBhjnHPuuq5rHMfxoz/P89xaa0op9SH6vu9DCOE4jqOUUiil9JHLsiwxxogxxlprve/73nVdB4wxVmutnHMOAEAIIfd930II8auLvvEGonZSJHFUDYgAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0446" character="ц">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAc0lEQVQYlc3OPQoDIRiE4TffomKt4N8ltPX+x7DXG1hm2yUEqxR52oGZgX80xhi99w5Qa61zzikiIgCllKK11gBKKRVjjAByavxB6JxzKaXkvfcAIYQAwN57vz+stZYAWGvt66G11gCub1vGGJNzzqc/ZzeP5yQEHn1mKgAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0447" character="ч">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAh0lEQVQYlc2QoQ1EIRAFH4QggQ1mBQpBA9SCoj06oAoKIVsDinMnfnLqmxs7ycvkAf9GSimJiMQYIwDknLOISAghaGutZWbWWmsAMMYYZmallNLnnAMA3nv/XDV7773WWmOMMeecRERfCQCttdZ777XW6pxzPwNLKeXee4mI9FPee+/rC17wARs+Js8tO3YXAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0448" character="ш">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAaklEQVQYlc3QoRHAIBBE0Z0lGBgGgzhDSxgKwh8l0QMtXRpITDB59s+sWOBvWmttrbUAYO+9a61VVXWMMa4QQiilFAAQEXHOuZxzJkmamb2tHkaSTCklkowxRu+9BwD03rs9mHPOzxccuAEZaTUBPteqPAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0449" character="щ">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAbElEQVQYlc2OsQ0AIQzETkkaIqWCIiuwDSUNuzECE6VhkP8JoPnm3VqyDPyN1lpbay0AiIgQEZlzzjHGoJRSyjlnAHB3B4BSSlFVpVv1o2RmNjMjIjIzExFRVUXvvT8Hjsm99+aTrLXW28+dF6zQKv7telSjAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="044a" character="ъ">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnElEQVQYlc3QsQkEIRCF4YG72EyzVUy1ALuwhm1iK7AfA5swEds4ZECTZRODuWQPdpPLDu5Lf5gHA/BvUkrpdZJSyls0xhjvvSci0lrra3sgIo4xxrZtW++9K6VUa63t+74DAIAQQhAR5ZxzrbXOOee6rustfs6GEEIppTyvG8uyLIwx5pxziIgAAMA553Q6juOIMUZrrf3Rg756A4uIVKjOSL7bAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="044b" character="ы">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAn0lEQVQYlc2MMQoDIRQFf0xAhAVh0UNYWHkBt1pLW/ECnkXwioKVBxCbFP402ZAThEz5eDMAf8bNe+9TSsk5564x55znnPPBGGNCCPFtcM45IYQQAAAppYwxxn3f9+8TAQDYtm0LIYTWWjuO40BEBAC4a621UkpZay2llJ7neT7fkCshpZTGGNN7758sIqIxxtRa6xhjlFLKWmtd6V/zAlJhPTWZrkvDAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="044c" character="ь">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAkElEQVQYlc2QIQ6DIRSDOzb13J+QYJZgOAQJittwIRwHAS5BuASCZ/Gb+VmWqYmJfapp04oCf8YVAHLOWQgheu/9PRQAoJRSRESfzdsW3nsPAMzMpZSy1loXAGitNSKiMca4n1hrLXYYQgh7pdZaY4xRbENKKbXW2jnnjDFmzjlfzccJM3NKKR3HcfzulO95AgYZPHl3TOqCAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="044d" character="э">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAtklEQVQYlc3QIQrDMBjF8cdaE1NCzWdraqLqSkRcKbG9RE9SXVlyjtwjJicItZGxgW9mjMH0YL8D/OE94B/N8zx7730ppXjvvTHGAMADAJxzrpRSlmVZcs75uq4LACCEEPu+70REUko5juPIzKyUUu/0NE0Tf9Ba60fbtq211qaUUt/3/TAMAwDUWisAIMYYz/M8iYiO4zju+76bpmkAAOu6riGEwMwcQgjbtm1fk7qu63552csTqXJPeRp+6oUAAAAASUVORK5CYII=</picture>
|
||||||
|
</char>
|
||||||
|
<char code="044e" character="ю">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QoQ3EIABFP+QMwRImQGFJO0J1F2AEREfoDl0FhcFjSE2TGlRNDUkHKKcud8npS+6Z737e/8C/MY7j6L33ADBN03Se5xljjFprTRljTAghGGNsnud5GIahlFKcc46+GpRSqtZac845hBCMMYYCQN/3/bqu633fNwC8kgJASil1XdcRQsinDwWA1lrb930XQggppTTGmG3btkdrrQHAdV3XsixLKaUcx3FYa+3XNM45/8lnb54oTFBrn6oV8wAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
<char code="044f" character="я">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAoklEQVQYlc2QMQoDIRREvyFgtaUgiK2we4dvYe9VxCtZWuwFtvEQXmGXbC1WNj9NNiQXCHntwLxhAP4RRMScc973fX+8MMYY4Jzz4ziOlFJCRHTOOSKiZVkWkFJKIiJrrQUAmKZpusL7eZ7ntm3buq5rKaUwxtiluwEAeO/9GGMIIUSttX4NCiGE1lpTSqnPWtBa6957jzHGT+c8z/MPD3vzBK5CUJOtN8zGAAAAAElFTkSuQmCC</picture>
|
||||||
|
</char>
|
||||||
|
<char code="0451" character="ё">
|
||||||
|
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA60lEQVQYlaWQMQqDQAAEN0G44wTFRrC0sLpKBMEHWFlp5yOsbOx8ij5AQXvfYHNP8AGKIIpwkMpASJok2y47yy7wlzjnPM/zHADSNE3DMAyfZlEUxb7vu6qq6jiOY9M0zUuaMcYAgBBCFEVRnobjOE5d1/W6rqsQQlzYOwCUZVkyxpjv+37f933XdR2llIIQQrZt26IoikzTNG3bts/zPIMgCG66ruvLsizzPM9SSnlVJUmSgFJKj+M4XNd1P+5s27atqqoyDMOI4zgWQgjLsiwAgOd53jAMg5RSTtM0ZVmWvRE0TdN++/grPQB+ZVkAx8URhAAAAABJRU5ErkJggg==</picture>
|
||||||
|
</char>
|
||||||
|
</chars>
|
||||||
|
</data>
|
||||||
7624
firmware/tft_app/src/services/gfx/fonts/SystemFont.c
Normal file
7624
firmware/tft_app/src/services/gfx/fonts/SystemFont.c
Normal file
File diff suppressed because it is too large
Load diff
41
firmware/tft_app/src/services/gfx/fonts/include/fonts.h
Normal file
41
firmware/tft_app/src/services/gfx/fonts/include/fonts.h
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @file fonts.h
|
||||||
|
* @brief Layout типов, ожидаемый файлами lcd-image-converter (../FloorFontFallback.c,
|
||||||
|
* ../SystemFont.c) — SystemFont.c явно делает #include <fonts.h> (bare name,
|
||||||
|
* так сгенерировал конвертер), FloorFontFallback.c рассчитывает на те же
|
||||||
|
* типы через force-include (см. CMakeLists.txt этой библиотеки).
|
||||||
|
*
|
||||||
|
* Порядок/типы полей — НЕ менять без пересборки обоих шрифтов в конвертере:
|
||||||
|
* они определяют бинарную раскладку структур в сгенерированных .c.
|
||||||
|
*
|
||||||
|
* Публичный API поверх этого формата — services/gfx.h (там те же типы
|
||||||
|
* переобъявлены под неймспейс services/, этот файл — только для компиляции
|
||||||
|
* сгенерированных файлов, наружу библиотеки gfx не торчит).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FONTS_H_
|
||||||
|
#define FONTS_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const uint32_t *data;
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
uint8_t dataSize;
|
||||||
|
} tImage;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
long int code;
|
||||||
|
const tImage *image;
|
||||||
|
} tChar;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int length;
|
||||||
|
const tChar *chars;
|
||||||
|
} tFont;
|
||||||
|
|
||||||
|
#endif /* FONTS_H_ */
|
||||||
173
firmware/tft_app/src/services/gfx/include/services/gfx.h
Normal file
173
firmware/tft_app/src/services/gfx/include/services/gfx.h
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
/**
|
||||||
|
* @file gfx.h
|
||||||
|
* @brief Фаза 3.2.4 — gfx как компоновщик: double-buffer + PXP.
|
||||||
|
*
|
||||||
|
* Модель (эталон OLD_PROJECT_TFT8_UKL/source/display/): CPU рисует ВЕСЬ кадр в
|
||||||
|
* альфа-поверхность **AS** (`alpha_buffer`, ARGB8888; примитивы пишут alpha
|
||||||
|
* 0xFF, `gfx_clear` обнуляет → прозрачно). PXP блендит AS над фоновой
|
||||||
|
* поверхностью **PS** (`processing_buffer`, сейчас сплошной чёрный) в один из
|
||||||
|
* двух задних framebuffer'ов, затем свап синхронно с ELCDIF (семафор FRAME_DONE).
|
||||||
|
* Рисуем off-screen, показываем атомарным свапом → tear-free. Стиль-картинка в
|
||||||
|
* PS и спрайты в AS — Фаза 4/5; сейчас PS чёрный, композиция = чёрный фон +
|
||||||
|
* нарисованное в AS.
|
||||||
|
*
|
||||||
|
* Формат tImage/tChar/tFont и RLE-декодирование — порт проверенного в проде
|
||||||
|
* алгоритма из OLD_PROJECT (source/fonts/fonts.c), тот же формат данных, что
|
||||||
|
* реально экспортирует lcd-image-converter (см. services/gfx/fonts/).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SERVICES_GFX_H_
|
||||||
|
#define SERVICES_GFX_H_
|
||||||
|
|
||||||
|
#include "bsp/display.h"
|
||||||
|
#include "bsp/status.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Точный layout, экспортируемый lcd-image-converter — копия
|
||||||
|
* services/gfx/fonts/include/fonts.h (тот форсированно инклудится в
|
||||||
|
* сгенерированные .c, здесь — то же самое для остального приложения; два
|
||||||
|
* файла держать в синхроне, формат внешний и стабилен). */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const uint32_t *data;
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
uint8_t dataSize;
|
||||||
|
} tImage;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
long int code;
|
||||||
|
const tImage *image;
|
||||||
|
} tChar;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int length;
|
||||||
|
const tChar *chars;
|
||||||
|
} tFont;
|
||||||
|
|
||||||
|
/* Компилируемые в прошивку шрифты (services/gfx/fonts/, сгенерированы
|
||||||
|
* lcd-image-converter пользователем). */
|
||||||
|
extern const tFont FloorFontFallback; /* 0-9, "-", пробел — fallback (§11 ARCH) */
|
||||||
|
|
||||||
|
/* SystemFont — ASCII + кириллица (логи/меню, метки режимов в fallback).
|
||||||
|
* Сгенерированный SystemFont.c экспортирует tFont под именем из .xml
|
||||||
|
* конвертера — JBMono24 (JetBrains Mono 24pt). Публичное имя API — SystemFont;
|
||||||
|
* связываем алиасом-макросом, не редактируя сгенерированный файл (он
|
||||||
|
* перезапишется при регенерации шрифта). Использовать как объект: &SystemFont —
|
||||||
|
* симметрично FloorFontFallback. */
|
||||||
|
extern const tFont JBMono24;
|
||||||
|
#define SystemFont JBMono24
|
||||||
|
|
||||||
|
/* SystemFontSmall — мелкий моно (JetBrains Mono 12pt, ASCII+кириллица) для
|
||||||
|
* футера меню/подсказок. Тот же приём алиаса, что SystemFont (сгенерированный
|
||||||
|
* файл экспортирует tFont под именем JBMono12 из .xml конвертера). */
|
||||||
|
extern const tFont JBMono12;
|
||||||
|
#define SystemFontSmall JBMono12
|
||||||
|
|
||||||
|
/** XRGB8888 (X игнорируется ELCDIF) — X-байт значения не имеет. */
|
||||||
|
typedef uint32_t gfx_color_t;
|
||||||
|
|
||||||
|
#define GFX_COLOR_BLACK 0x00000000U
|
||||||
|
#define GFX_COLOR_WHITE 0x00FFFFFFU
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Поднять компоновщик: AS/PS/2×FB (SDRAM non-cacheable) + PXP + ELCDIF.
|
||||||
|
*
|
||||||
|
* Создаёт семафор FRAME_DONE и регистрирует ISR-колбэк ELCDIF (даёт семафор),
|
||||||
|
* заливает PS сплошным чёрным, инициализирует PXP (AS над PS → выходной FB) и
|
||||||
|
* стартует ELCDIF на FB[0]. До первого gfx_present() экран чёрный.
|
||||||
|
*
|
||||||
|
* SDRAM (SEMC) должна быть уже поднята вызывающим (bsp_sdram_configure() +
|
||||||
|
* bsp_sdram_init()) — gfx не владеет SEMC-инициализацией, только буферами
|
||||||
|
* внутри уже готовой SDRAM.
|
||||||
|
*
|
||||||
|
* @param type тип панели (Фаза 1 — хардкод из app; Фаза 9 — provisioning)
|
||||||
|
*/
|
||||||
|
bsp_status_t gfx_init(bsp_display_type_t type);
|
||||||
|
|
||||||
|
/** Обнулить AS (весь кадр становится прозрачным). Вызывать перед отрисовкой
|
||||||
|
* нового полного кадра; непрорисованные области покажут фон PS (чёрный). */
|
||||||
|
void gfx_clear(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Показать нарисованный в AS кадр: PXP-композит AS над PS → задний FB,
|
||||||
|
* затем свап синхронно с ELCDIF (tear-free).
|
||||||
|
*
|
||||||
|
* Блокирующий (busy-wait завершения PXP + ожидание FRAME_DONE) — звать из
|
||||||
|
* задачи-владельца дисплея после того, как полный кадр нарисован в AS.
|
||||||
|
*/
|
||||||
|
void gfx_present(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Показать ТОЛЬКО прямоугольник (x,y,w,h): PXP-композит региона AS над
|
||||||
|
* PS → тот же регион заднего FB, затем tear-free свап.
|
||||||
|
*
|
||||||
|
* Дешевле полного gfx_present() пропорционально площади (окно меню 480×272 —
|
||||||
|
* ~27% кадра). Клампится по экрану.
|
||||||
|
*
|
||||||
|
* @warning КОНТРАКТ ВЫЗЫВАЮЩЕГО: вне прямоугольника задний FB не
|
||||||
|
* перекомпоновывается — из-за double buffering там содержимое ДВУХ present'ов
|
||||||
|
* назад. Использовать только когда ОБА FB уже содержат корректный кадр вне
|
||||||
|
* прямоугольника (модальное меню: на открытии — два полных gfx_present()
|
||||||
|
* подряд с одним AS, затем навигация — только окно; см. task_render.c).
|
||||||
|
*/
|
||||||
|
void gfx_present_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||||
|
|
||||||
|
/** Обнулить (сделать прозрачным) только прямоугольник AS — дешёвая замена
|
||||||
|
* полного gfx_clear() для оконной перерисовки (окно меню). Клампится. */
|
||||||
|
void gfx_clear_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием).
|
||||||
|
*
|
||||||
|
* Глифы шрифта — белые grayscale-покрытием (α = яркость пикселя); отрисовка
|
||||||
|
* блендит целевой @p color по этой α с фоном framebuffer'а — так один белый
|
||||||
|
* шрифт рисуется любым цветом, а сглаживание корректно ложится и на цветной
|
||||||
|
* фон (напр. полосу-курсор меню). Для белого цвета на чёрном фоне результат
|
||||||
|
* идентичен «прямой» отрисовке.
|
||||||
|
*
|
||||||
|
* Символ вне таблицы шрифта (отсутствует в font->chars[]) → подстановка '-'
|
||||||
|
* ("-" — согласованный fallback-глиф, см. FloorFontFallback) — по-символьный
|
||||||
|
* fallback (ARCH §11), не обрыв/пропуск на первом неизвестном символе. Если
|
||||||
|
* даже '-' не найден в переданном шрифте — символ пропускается (ширина 0).
|
||||||
|
*
|
||||||
|
* Понимает 2-байтовые UTF-8 последовательности (ведущий байт 0xD0/0xD1 —
|
||||||
|
* кириллица) как ОДИН символ для подстановки/позиционирования — как и
|
||||||
|
* lcd-image-converter кодирует code в tChar для таких шрифтов.
|
||||||
|
*
|
||||||
|
* @return суммарная ширина отрисованной строки, пиксели.
|
||||||
|
*/
|
||||||
|
uint16_t gfx_draw_string(const tFont *p_font, const char *p_str, uint16_t x, uint16_t y,
|
||||||
|
gfx_color_t color);
|
||||||
|
|
||||||
|
/** Ширина строки без отрисовки (для центрирования и т.п.). */
|
||||||
|
uint16_t gfx_string_width(const tFont *p_font, const char *p_str);
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
GFX_ARROW_UP,
|
||||||
|
GFX_ARROW_DOWN,
|
||||||
|
} gfx_arrow_dir_t;
|
||||||
|
|
||||||
|
/** Простая треугольная стрелка — примитив, без спрайтов (ARCH §11: fallback asset-free). */
|
||||||
|
void gfx_draw_arrow(gfx_arrow_dir_t dir, uint16_t x, uint16_t y, uint16_t size, gfx_color_t color);
|
||||||
|
|
||||||
|
/** Залить прямоугольник сплошным цветом (фон, полоса-курсор меню). Обрезается по экрану. */
|
||||||
|
void gfx_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, gfx_color_t color);
|
||||||
|
|
||||||
|
/** Контур прямоугольника толщиной 1 px (рамки/разделители). Обрезается по экрану. */
|
||||||
|
void gfx_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, gfx_color_t color);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* SERVICES_GFX_H_ */
|
||||||
537
firmware/tft_app/src/services/gfx/src/gfx.c
Normal file
537
firmware/tft_app/src/services/gfx/src/gfx.c
Normal file
|
|
@ -0,0 +1,537 @@
|
||||||
|
#include "services/gfx.h"
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "fsl_common.h" /* AT_NONCACHEABLE_SECTION_ALIGN */
|
||||||
|
#include "fsl_pxp.h"
|
||||||
|
#include "semphr.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ── Поверхности компоновщика (SDRAM, non-cacheable) ────────────────────────
|
||||||
|
*
|
||||||
|
* AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции
|
||||||
|
* NonCacheable/.ncache → cmake/linker/..._app_slot.ld отображает её на
|
||||||
|
* m_sdram_ncache (8 МБ в начале SDRAM), а board_mpu_init() (bsp/generated/
|
||||||
|
* board.c, Region 9) конфигурирует ЭТОТ ЖЕ диапазон как non-cacheable через
|
||||||
|
* линкер-символы __NCACHE_REGION_START/SIZE — рекомендация NXP для буферов,
|
||||||
|
* которые ELCDIF/PXP читают по DMA (см. OLD_PROJECT source/display/image_cache.c,
|
||||||
|
* тот же макрос, framebuffer/alpha_buffer/processing_buffer). Без этого CPU
|
||||||
|
* писал бы через Write-Back D-Cache (Region 8: SDRAM WB Cacheable), и DMA
|
||||||
|
* читал бы устаревшие данные, пока кэш-линия не вытеснится сама.
|
||||||
|
*
|
||||||
|
* Четыре поверхности (эталон TFT8_UKL): AS (рисует CPU), PS (фон, чёрный),
|
||||||
|
* FB[2] (выход PXP = вход ELCDIF, double buffer). Размер — под ТЕКУЩУЮ панель
|
||||||
|
* стенда (TFT8, 800×600), не под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9)
|
||||||
|
* станет рантайм-выбирать между TFT7/8/10 в одном бинарнике, размер поверхностей
|
||||||
|
* и m_sdram_ncache придётся поднять до максимума панели.
|
||||||
|
*
|
||||||
|
* ГИБРИД bpp (Фаза 3.2.4): AS — ARGB8888 (8-бит альфа → полноценный AA, примитивы
|
||||||
|
* пишут сюда без изменений); PS + оба FB — RGB565 (вдвое меньше полосы: ELCDIF
|
||||||
|
* сканирует FB непрерывно, это доминирующая нагрузка на 16-бит SDRAM). PXP читает
|
||||||
|
* AS(8888)/PS(565), блендит внутри в ≥8 бит, пишет выход в 565. Форматы поверхностей
|
||||||
|
* PXP независимы, поэтому 565-выход НЕ ломает альфа-смешивание. */
|
||||||
|
#define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/PXP/AXI выравнивание */
|
||||||
|
#define SURFACE_PIXELS (800U * 600U)
|
||||||
|
#define AS_BYTES_PER_PIXEL 4U /* AS: ARGB8888 */
|
||||||
|
#define FB_BYTES_PER_PIXEL 2U /* PS + выходные FB: RGB565 */
|
||||||
|
#define ALPHA_OPAQUE 0xFF000000U /* AS: alpha=0xFF → пиксель непрозрачен для PXP-блендинга */
|
||||||
|
#define FALLBACK_CHAR '-'
|
||||||
|
|
||||||
|
/* AS — CPU рисует сюда (ARGB8888, alpha значим). Гибрид: остаётся 32-бит. */
|
||||||
|
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_alpha_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
|
||||||
|
/* PS — фон под AS (сейчас сплошной чёрный, заливается однократно в gfx_init). RGB565. */
|
||||||
|
AT_NONCACHEABLE_SECTION_ALIGN(static uint16_t s_processing_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
|
||||||
|
/* Выходные буферы PXP = сканируемые ELCDIF (double buffer, свап в gfx_present). RGB565. */
|
||||||
|
AT_NONCACHEABLE_SECTION_ALIGN(static uint16_t s_framebuffer[2][SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
|
||||||
|
|
||||||
|
static uint16_t s_fb_width;
|
||||||
|
static uint16_t s_fb_height;
|
||||||
|
|
||||||
|
static uint8_t s_back_index; /* индекс FB, в который PXP компонует следующий кадр */
|
||||||
|
static SemaphoreHandle_t s_frame_done; /* даётся из ELCDIF ISR по завершении кадра */
|
||||||
|
static pxp_output_buffer_config_t s_output_cfg; /* хранится: gfx_present меняет buffer0Addr */
|
||||||
|
|
||||||
|
static inline void set_pixel(uint16_t x, uint16_t y, gfx_color_t color)
|
||||||
|
{
|
||||||
|
if ((x >= s_fb_width) || (y >= s_fb_height))
|
||||||
|
{
|
||||||
|
return; /* примитивы могут частично выходить за экран — не UB, просто обрезка */
|
||||||
|
}
|
||||||
|
/* AS: пишем непрозрачно (alpha 0xFF), RGB из color (X-байт игнорируем). */
|
||||||
|
s_alpha_buffer[(uint32_t) y * s_fb_width + x] = (color & 0x00FFFFFFU) | ALPHA_OPAQUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Альфа-блендинг: наложить @p color с покрытием @p a (0..255) на текущее
|
||||||
|
* содержимое AS. a=0 — пиксель нетронут (прозрачный край глифа), a=255 —
|
||||||
|
* полная замена. Так сглаживание глифа корректно ложится на уже нарисованный
|
||||||
|
* фон (в т.ч. полосу-курсор), а не штампует чёрный бокс. Результат всегда
|
||||||
|
* непрозрачен (alpha 0xFF): PXP покажет его поверх PS. */
|
||||||
|
static inline void blend_pixel(uint16_t x, uint16_t y, gfx_color_t color, uint8_t a)
|
||||||
|
{
|
||||||
|
if ((x >= s_fb_width) || (y >= s_fb_height) || (a == 0U))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t idx = (uint32_t) y * s_fb_width + x;
|
||||||
|
if (a == 0xFFU)
|
||||||
|
{
|
||||||
|
s_alpha_buffer[idx] = (color & 0x00FFFFFFU) | ALPHA_OPAQUE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t bg = s_alpha_buffer[idx];
|
||||||
|
const uint32_t inv = 255U - a;
|
||||||
|
const uint32_t r = (((color >> 16) & 0xFFU) * a + ((bg >> 16) & 0xFFU) * inv) / 255U;
|
||||||
|
const uint32_t g = (((color >> 8) & 0xFFU) * a + ((bg >> 8) & 0xFFU) * inv) / 255U;
|
||||||
|
const uint32_t b = (((color) & 0xFFU) * a + ((bg) & 0xFFU) * inv) / 255U;
|
||||||
|
s_alpha_buffer[idx] = (r << 16) | (g << 8) | b | ALPHA_OPAQUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Поиск глифа (бинарный — chars[] отсортирован по code, гарантия формата
|
||||||
|
* lcd-image-converter) — порт draw_char()+get_character_width() из
|
||||||
|
* OLD_PROJECT source/fonts/fonts.c, унифицировано в одну функцию (там был
|
||||||
|
* бинарный поиск в draw_char() и отдельный линейный в get_character_width()
|
||||||
|
* — один и тот же инвариант сортировки, лишнее дублирование). ─────────── */
|
||||||
|
static const tImage *find_glyph(const tFont *p_font, long code)
|
||||||
|
{
|
||||||
|
int low = 0;
|
||||||
|
int high = p_font->length - 1;
|
||||||
|
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
const int mid = low + (high - low) / 2;
|
||||||
|
if (p_font->chars[mid].code == code)
|
||||||
|
{
|
||||||
|
return p_font->chars[mid].image;
|
||||||
|
}
|
||||||
|
if (p_font->chars[mid].code < code)
|
||||||
|
{
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── RLE-декодирование глифа — порт draw_char() из OLD_PROJECT
|
||||||
|
* source/fonts/fonts.c (проверенный в проде алгоритм, формат — как
|
||||||
|
* реально экспортирует lcd-image-converter, "RLE compression enabled").
|
||||||
|
*
|
||||||
|
* Поток uint32_t, каждый блок начинается с заголовка:
|
||||||
|
* (header & 0xFFFFFF00) == 0xFFFFFF00 → UNIQUE: len = 0x100-(header&0xFF)
|
||||||
|
* уникальных пикселей подряд следуют в потоке (по одному слову каждый).
|
||||||
|
* иначе → REPEATABLE: len = header&0xFFFF
|
||||||
|
* повторений ОДНОГО пикселя (следующее слово потока, читается один раз).
|
||||||
|
* Пиксели — ARGB8888, порядок row-major, перенос строки на границе width
|
||||||
|
* (advance_pixel в оригинале). ──────────────────────────────────────────── */
|
||||||
|
#define UNIQUE_BLOCK_MASK 0xFFFFFF00U
|
||||||
|
|
||||||
|
/* Покрытие (α) пикселя глифа — белый глиф запечён grayscale'ом (0xVVVVVVVV),
|
||||||
|
* V одинаков во всех байтах; берём младший. */
|
||||||
|
#define GLYPH_COVERAGE(pixel) ((uint8_t) ((pixel) & 0xFFU))
|
||||||
|
|
||||||
|
static void draw_glyph(const tImage *p_image, uint16_t x_pos, uint16_t y_pos, gfx_color_t color)
|
||||||
|
{
|
||||||
|
const uint32_t total = (uint32_t) p_image->width * p_image->height;
|
||||||
|
|
||||||
|
uint32_t in_idx = 0U;
|
||||||
|
uint32_t out_n = 0U;
|
||||||
|
uint32_t col = 0U;
|
||||||
|
uint32_t row = 0U;
|
||||||
|
|
||||||
|
while (out_n < total)
|
||||||
|
{
|
||||||
|
const uint32_t header = p_image->data[in_idx++];
|
||||||
|
|
||||||
|
if ((header & UNIQUE_BLOCK_MASK) == UNIQUE_BLOCK_MASK)
|
||||||
|
{
|
||||||
|
const uint32_t len = 0x100U - (header & 0xFFU);
|
||||||
|
for (uint32_t i = 0U; (i < len) && (out_n < total); i++)
|
||||||
|
{
|
||||||
|
blend_pixel((uint16_t) (x_pos + col), (uint16_t) (y_pos + row), color,
|
||||||
|
GLYPH_COVERAGE(p_image->data[in_idx]));
|
||||||
|
col++;
|
||||||
|
if (col >= p_image->width)
|
||||||
|
{
|
||||||
|
col = 0U;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
out_n++;
|
||||||
|
in_idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const uint32_t len = header & 0xFFFFU;
|
||||||
|
const uint8_t a = GLYPH_COVERAGE(p_image->data[in_idx]);
|
||||||
|
for (uint32_t i = 0U; (i < len) && (out_n < total); i++)
|
||||||
|
{
|
||||||
|
blend_pixel((uint16_t) (x_pos + col), (uint16_t) (y_pos + row), color, a);
|
||||||
|
col++;
|
||||||
|
if (col >= p_image->width)
|
||||||
|
{
|
||||||
|
col = 0U;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
out_n++;
|
||||||
|
}
|
||||||
|
in_idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Декодирование одного символа строки, включая 2-байтовый UTF-8
|
||||||
|
* (кириллица) — порт логики из OLD_PROJECT draw_string(): ведущие байты
|
||||||
|
* 0xD0/0xD1 комбинируются со следующим байтом в один code, как их кодирует
|
||||||
|
* lcd-image-converter в tChar.code для таких шрифтов. ──────────────────── */
|
||||||
|
static long next_codepoint(const char *p_str, uint8_t *p_consumed)
|
||||||
|
{
|
||||||
|
const uint8_t lead = (uint8_t) p_str[0];
|
||||||
|
|
||||||
|
if (((lead == 0xD0U) || (lead == 0xD1U)) && (p_str[1] != '\0'))
|
||||||
|
{
|
||||||
|
*p_consumed = 2U;
|
||||||
|
return ((long) lead << 8) | (uint8_t) p_str[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
*p_consumed = 1U;
|
||||||
|
return (long) lead;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* find_glyph() с fallback-подстановкой '-' (ARCH §11: по-символьный fallback
|
||||||
|
* на отсутствующий в шрифте символ — политика подтверждена: '-' — общий
|
||||||
|
* заменитель во FloorFontFallback). Общая для draw_string/string_width. */
|
||||||
|
static const tImage *find_glyph_with_fallback(const tFont *p_font, long code)
|
||||||
|
{
|
||||||
|
const tImage *p_glyph = find_glyph(p_font, code);
|
||||||
|
if (p_glyph == NULL)
|
||||||
|
{
|
||||||
|
p_glyph = find_glyph(p_font, (long) FALLBACK_CHAR);
|
||||||
|
}
|
||||||
|
return p_glyph; /* NULL, если даже '-' нет в этом шрифте */
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t gfx_draw_string(const tFont *p_font, const char *p_str, uint16_t x, uint16_t y,
|
||||||
|
gfx_color_t color)
|
||||||
|
{
|
||||||
|
if ((p_font == NULL) || (p_str == NULL))
|
||||||
|
{
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t offset = 0U;
|
||||||
|
|
||||||
|
while (*p_str != '\0')
|
||||||
|
{
|
||||||
|
uint8_t consumed = 1U;
|
||||||
|
const long code = next_codepoint(p_str, &consumed);
|
||||||
|
p_str += consumed;
|
||||||
|
|
||||||
|
const tImage *p_glyph = find_glyph_with_fallback(p_font, code);
|
||||||
|
if (p_glyph == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_glyph(p_glyph, (uint16_t) (x + offset), y, color);
|
||||||
|
offset = (uint16_t) (offset + p_glyph->width);
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t gfx_string_width(const tFont *p_font, const char *p_str)
|
||||||
|
{
|
||||||
|
if ((p_font == NULL) || (p_str == NULL))
|
||||||
|
{
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t width = 0U;
|
||||||
|
|
||||||
|
while (*p_str != '\0')
|
||||||
|
{
|
||||||
|
uint8_t consumed = 1U;
|
||||||
|
const long code = next_codepoint(p_str, &consumed);
|
||||||
|
p_str += consumed;
|
||||||
|
|
||||||
|
const tImage *p_glyph = find_glyph_with_fallback(p_font, code);
|
||||||
|
if (p_glyph != NULL)
|
||||||
|
{
|
||||||
|
width = (uint16_t) (width + p_glyph->width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Стрелка — примитив (без спрайтов, ARCH §11 fallback asset-free) ────── */
|
||||||
|
|
||||||
|
void gfx_draw_arrow(gfx_arrow_dir_t dir, uint16_t x, uint16_t y, uint16_t size, gfx_color_t color)
|
||||||
|
{
|
||||||
|
if (size < 2U)
|
||||||
|
{
|
||||||
|
return; /* вырожденный размер — не рисуем, не делим на (size-1)=0 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Равнобедренный треугольник построчной заливкой: half_width растёт
|
||||||
|
* линейно от 0 (вершина) до size/2 (основание). */
|
||||||
|
for (uint16_t row = 0U; row < size; row++)
|
||||||
|
{
|
||||||
|
const uint16_t half_width = (uint16_t) (((uint32_t) row * (size / 2U)) / (size - 1U));
|
||||||
|
const uint16_t center = (uint16_t) (x + size / 2U);
|
||||||
|
const uint16_t py = (dir == GFX_ARROW_UP) ? (uint16_t) (y + row) : (uint16_t) (y + (size - 1U) - row);
|
||||||
|
|
||||||
|
for (uint16_t dx = 0U; dx <= half_width; dx++)
|
||||||
|
{
|
||||||
|
set_pixel((uint16_t) (center - dx), py, color);
|
||||||
|
set_pixel((uint16_t) (center + dx), py, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Прямоугольники — примитивы (фон/полоса-курсор/разделители меню) ─────── */
|
||||||
|
|
||||||
|
void gfx_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, gfx_color_t color)
|
||||||
|
{
|
||||||
|
for (uint16_t j = 0U; j < h; j++)
|
||||||
|
{
|
||||||
|
for (uint16_t i = 0U; i < w; i++)
|
||||||
|
{
|
||||||
|
set_pixel((uint16_t) (x + i), (uint16_t) (y + j), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void gfx_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, gfx_color_t color)
|
||||||
|
{
|
||||||
|
if ((w == 0U) || (h == 0U))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint16_t i = 0U; i < w; i++)
|
||||||
|
{
|
||||||
|
set_pixel((uint16_t) (x + i), y, color);
|
||||||
|
set_pixel((uint16_t) (x + i), (uint16_t) (y + h - 1U), color);
|
||||||
|
}
|
||||||
|
for (uint16_t j = 0U; j < h; j++)
|
||||||
|
{
|
||||||
|
set_pixel(x, (uint16_t) (y + j), color);
|
||||||
|
set_pixel((uint16_t) (x + w - 1U), (uint16_t) (y + j), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── PXP-компоновщик (порт OLD_PROJECT_TFT8_UKL/source/display/pxp_config.c) ── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Настроить поверхности PXP под композит прямоугольника (x,y,w,h).
|
||||||
|
*
|
||||||
|
* Адреса PS/AS/выхода смещаются к (x,y); pitch остаётся полной строкой
|
||||||
|
* поверхности (страйд не меняется) — PXP пишет w×h с шагом полного ряда,
|
||||||
|
* попадая ровно в прямоугольник кадра. Позиции PS/AS — (0,0,w,h) относительно
|
||||||
|
* выходного кадра w×h (та же конвенция, что была для полного кадра — эталон
|
||||||
|
* TFT8_UKL, проверено на железе).
|
||||||
|
*
|
||||||
|
* Полная конфигурация на КАЖДЫЙ композит (без скрытого состояния между
|
||||||
|
* полным и оконным present'ом) — записи регистров PXP дёшевы.
|
||||||
|
*
|
||||||
|
* @note Текущий потребитель оконного композита — окно меню @ (0,0): смещения
|
||||||
|
* по y кратны полной строке (800×4=3200 Б AS / 800×2=1600 Б FB — кратны
|
||||||
|
* 64), выравнивание сохраняется. Для произвольного x следить за
|
||||||
|
* выравниванием адресов под требования PXP.
|
||||||
|
*/
|
||||||
|
static void pxp_configure_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint32_t out_addr)
|
||||||
|
{
|
||||||
|
const uint32_t OFFSET = (uint32_t) y * s_fb_width + x;
|
||||||
|
|
||||||
|
/* PS — фон RGB565 (гибрид). Альфа PS в блендинге не участвует (значима альфа AS). */
|
||||||
|
const pxp_ps_buffer_config_t ps_cfg = {
|
||||||
|
.pixelFormat = kPXP_PsPixelFormatRGB565,
|
||||||
|
.swapByte = false,
|
||||||
|
.bufferAddr = (uint32_t) &s_processing_buffer[OFFSET],
|
||||||
|
.bufferAddrU = 0U,
|
||||||
|
.bufferAddrV = 0U,
|
||||||
|
.pitchBytes = (uint16_t) (s_fb_width * FB_BYTES_PER_PIXEL),
|
||||||
|
};
|
||||||
|
PXP_SetProcessSurfaceBufferConfig(PXP, &ps_cfg);
|
||||||
|
|
||||||
|
/* AS — ARGB8888 (гибрид: остаётся 32-бит ради полной 8-бит альфы/AA). */
|
||||||
|
const pxp_as_buffer_config_t as_cfg = {
|
||||||
|
.pixelFormat = kPXP_AsPixelFormatARGB8888,
|
||||||
|
.bufferAddr = (uint32_t) &s_alpha_buffer[OFFSET],
|
||||||
|
.pitchBytes = (uint16_t) (s_fb_width * AS_BYTES_PER_PIXEL),
|
||||||
|
};
|
||||||
|
PXP_SetAlphaSurfaceBufferConfig(PXP, &as_cfg);
|
||||||
|
|
||||||
|
/* Выход RGB565 (гибрид): PXP квантует результат бленда в 565 на записи. */
|
||||||
|
s_output_cfg.pixelFormat = kPXP_OutputPixelFormatRGB565;
|
||||||
|
s_output_cfg.interlacedMode = kPXP_OutputProgressive;
|
||||||
|
s_output_cfg.buffer0Addr = out_addr;
|
||||||
|
s_output_cfg.buffer1Addr = 0U;
|
||||||
|
s_output_cfg.pitchBytes = (uint16_t) (s_fb_width * FB_BYTES_PER_PIXEL);
|
||||||
|
s_output_cfg.width = w;
|
||||||
|
s_output_cfg.height = h;
|
||||||
|
PXP_SetOutputBufferConfig(PXP, &s_output_cfg);
|
||||||
|
|
||||||
|
PXP_SetProcessSurfacePosition(PXP, 0U, 0U, w, h);
|
||||||
|
PXP_SetAlphaSurfacePosition(PXP, 0U, 0U, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void gfx_pxp_init(void)
|
||||||
|
{
|
||||||
|
PXP_Init(PXP);
|
||||||
|
|
||||||
|
/* Embedded alpha: доля AS-пикселя над PS берётся из его альфа-байта.
|
||||||
|
* Одноразовая часть конфигурации (per-композит — pxp_configure_rect). */
|
||||||
|
const pxp_as_blend_config_t blend_cfg = {
|
||||||
|
.alpha = 0xFFU,
|
||||||
|
.invertAlpha = false,
|
||||||
|
.alphaMode = kPXP_AlphaEmbedded,
|
||||||
|
.ropMode = kPXP_RopMaskAs,
|
||||||
|
};
|
||||||
|
PXP_SetAlphaSurfaceBlendConfig(PXP, &blend_cfg);
|
||||||
|
|
||||||
|
PXP_EnableCsc1(PXP, false); /* включён по умолчанию — фон RGB, конверсия не нужна */
|
||||||
|
|
||||||
|
pxp_configure_rect(0U, 0U, s_fb_width, s_fb_height, (uint32_t) s_framebuffer[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Запустить PXP и дождаться завершения композиции (busy-wait, как в эталоне). */
|
||||||
|
static void gfx_pxp_run(void)
|
||||||
|
{
|
||||||
|
PXP_ClearStatusFlags(PXP, kPXP_CommandLoadFlag);
|
||||||
|
PXP_ClearStatusFlags(PXP, kPXP_Axi0ReadErrorFlag);
|
||||||
|
PXP_ClearStatusFlags(PXP, kPXP_Axi0WriteErrorFlag);
|
||||||
|
PXP_ClearStatusFlags(PXP, kPXP_CompleteFlag);
|
||||||
|
|
||||||
|
PXP_Start(PXP);
|
||||||
|
while ((kPXP_CompleteFlag & PXP_GetStatusFlags(PXP)) == 0U)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ISR-safe: конец кадра ELCDIF → отпустить семафор (синхронизация свапа). */
|
||||||
|
static void on_frame_done(void)
|
||||||
|
{
|
||||||
|
BaseType_t hp_task_woken = pdFALSE;
|
||||||
|
(void) xSemaphoreGiveFromISR(s_frame_done, &hp_task_woken);
|
||||||
|
portYIELD_FROM_ISR(hp_task_woken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Компоновщик / init ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
bsp_status_t gfx_init(bsp_display_type_t type)
|
||||||
|
{
|
||||||
|
/* SDRAM (SEMC) — забота вызывающего (bsp_sdram_configure()+init()), gfx
|
||||||
|
* владеет только поверхностями внутри уже готовой SDRAM. */
|
||||||
|
s_frame_done = xSemaphoreCreateBinary();
|
||||||
|
if (s_frame_done == NULL)
|
||||||
|
{
|
||||||
|
return BSP_ERR_INIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PS — чёрный фон; AS — прозрачно; оба выходных FB — чёрные (ELCDIF стартует
|
||||||
|
* на FB[0] ещё до первого gfx_present, иначе на экране был бы мусор). */
|
||||||
|
(void) memset(s_processing_buffer, 0, sizeof(s_processing_buffer));
|
||||||
|
(void) memset(s_alpha_buffer, 0, sizeof(s_alpha_buffer));
|
||||||
|
(void) memset(s_framebuffer, 0, sizeof(s_framebuffer));
|
||||||
|
|
||||||
|
/* Семафор создан и колбэк готов ДО включения IRQ внутри bsp_display_init_ex.
|
||||||
|
* RGB565 (гибрид): выходные FB 16-бит → ELCDIF-сканаут вдвое дешевле. */
|
||||||
|
const bsp_status_t st = bsp_display_init_ex(type, (uint32_t) s_framebuffer[0], on_frame_done,
|
||||||
|
BSP_DISPLAY_PIXEL_RGB565);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bsp_display_size_t *p_size = bsp_display_get_size();
|
||||||
|
s_fb_width = p_size->width;
|
||||||
|
s_fb_height = p_size->height;
|
||||||
|
s_back_index = 0U; /* FB[0] показывается; первый present уйдёт в FB[1] */
|
||||||
|
|
||||||
|
gfx_pxp_init();
|
||||||
|
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gfx_clear(void)
|
||||||
|
{
|
||||||
|
/* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный).
|
||||||
|
* Чистим AS (ARGB8888) — 4 байта/пиксель. */
|
||||||
|
(void) memset(s_alpha_buffer, 0, (size_t) s_fb_width * s_fb_height * AS_BYTES_PER_PIXEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Общий путь полного и оконного present'а: композит прямоугольника в задний
|
||||||
|
* FB + tear-free свап. Прямоугольник должен быть предварительно заклампен. */
|
||||||
|
static void present_rect_internal(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
|
||||||
|
{
|
||||||
|
/* Компонуем в НЕ показываемый сейчас буфер, показываем атомарным свапом. */
|
||||||
|
s_back_index ^= 1U;
|
||||||
|
|
||||||
|
const uint32_t OUT_ADDR =
|
||||||
|
(uint32_t) &s_framebuffer[s_back_index][(uint32_t) y * s_fb_width + x];
|
||||||
|
pxp_configure_rect(x, y, w, h, OUT_ADDR);
|
||||||
|
|
||||||
|
gfx_pxp_run(); /* AS над PS → прямоугольник заднего FB */
|
||||||
|
|
||||||
|
/* Синхронизация с развёрткой: дождаться конца кадра, затем отдать ELCDIF
|
||||||
|
* новый буфер — он переключится аппаратно на границе кадра (tear-free). */
|
||||||
|
(void) xSemaphoreTake(s_frame_done, portMAX_DELAY);
|
||||||
|
|
||||||
|
/* ELCDIF всегда получает БАЗОВЫЙ адрес кадра (сканирует весь FB). */
|
||||||
|
bsp_display_set_next_buffer((uint32_t) s_framebuffer[s_back_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gfx_present(void)
|
||||||
|
{
|
||||||
|
present_rect_internal(0U, 0U, s_fb_width, s_fb_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gfx_present_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
|
||||||
|
{
|
||||||
|
if ((x >= s_fb_width) || (y >= s_fb_height) || (w == 0U) || (h == 0U))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Клампинг по экрану — как у примитивов. */
|
||||||
|
if ((uint32_t) x + w > s_fb_width)
|
||||||
|
{
|
||||||
|
w = (uint16_t) (s_fb_width - x);
|
||||||
|
}
|
||||||
|
if ((uint32_t) y + h > s_fb_height)
|
||||||
|
{
|
||||||
|
h = (uint16_t) (s_fb_height - y);
|
||||||
|
}
|
||||||
|
|
||||||
|
present_rect_internal(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gfx_clear_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
|
||||||
|
{
|
||||||
|
if ((x >= s_fb_width) || (y >= s_fb_height) || (w == 0U) || (h == 0U))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((uint32_t) x + w > s_fb_width)
|
||||||
|
{
|
||||||
|
w = (uint16_t) (s_fb_width - x);
|
||||||
|
}
|
||||||
|
if ((uint32_t) y + h > s_fb_height)
|
||||||
|
{
|
||||||
|
h = (uint16_t) (s_fb_height - y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Построчный memset региона AS (страйд — полная строка поверхности). */
|
||||||
|
for (uint16_t row = 0U; row < h; row++)
|
||||||
|
{
|
||||||
|
(void) memset(&s_alpha_buffer[(uint32_t) (y + row) * s_fb_width + x], 0,
|
||||||
|
(size_t) w * AS_BYTES_PER_PIXEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
firmware/tft_app/src/services/partition/CMakeLists.txt
Normal file
3
firmware/tft_app/src/services/partition/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Заголовочный модуль карты разделов QSPI (§10) — только константы, без кода.
|
||||||
|
add_library(tft_app_partition INTERFACE)
|
||||||
|
target_include_directories(tft_app_partition INTERFACE include/)
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* @file partition.h
|
||||||
|
* @brief Карта разделов QSPI NOR для tft_app (ARCH.md §10) — единый источник
|
||||||
|
* смещений для app и (в перспективе) генератора service_tui.
|
||||||
|
*
|
||||||
|
* Все смещения — БАЙТОВЫЕ, flash-относительные (0-based), как принимает
|
||||||
|
* `bsp_qspi_*`. Размер-независимость: критичные регионы (загрузчик, слоты,
|
||||||
|
* layout, settings) — по фиксированным смещениям; размер-зависим ТОЛЬКО регион
|
||||||
|
* ассетов — стартует с фикс-адреса и тянется до конца чипа
|
||||||
|
* (`длина = bsp_qspi_flash_size() - TFT_APP_QSPI_ASSETS_OFFSET`).
|
||||||
|
*
|
||||||
|
* Работает на W25Q128/256/512 без пересчёта: меняется лишь длина ассетов.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SERVICES_PARTITION_H_
|
||||||
|
#define SERVICES_PARTITION_H_
|
||||||
|
|
||||||
|
/* ── Фиксированные регионы (размер-независимо) ───────────────────────────── */
|
||||||
|
|
||||||
|
#define TFT_APP_QSPI_BOOTLOADER_OFFSET 0x000000U
|
||||||
|
#define TFT_APP_QSPI_BOOTLOADER_SIZE 0x040000U /* 256 КБ */
|
||||||
|
|
||||||
|
#define TFT_APP_QSPI_SLOT_A_OFFSET 0x040000U
|
||||||
|
#define TFT_APP_QSPI_SLOT_B_OFFSET 0x240000U
|
||||||
|
#define TFT_APP_QSPI_SLOT_SIZE 0x200000U /* 2 МБ на слот */
|
||||||
|
|
||||||
|
#define TFT_APP_QSPI_LAYOUT_OFFSET 0x440000U
|
||||||
|
#define TFT_APP_QSPI_LAYOUT_SIZE 0x010000U /* 64 КБ (+ client-UX TLV, ярус C) */
|
||||||
|
|
||||||
|
#define TFT_APP_QSPI_SETTINGS_OFFSET 0x450000U
|
||||||
|
#define TFT_APP_QSPI_SETTINGS_SIZE 0x002000U /* 8 КБ = 2 сектора:
|
||||||
|
сектор 0 — рабочий,
|
||||||
|
сектор 1 — задел под ping-pong */
|
||||||
|
|
||||||
|
/* ── Размер-зависимый регион ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#define TFT_APP_QSPI_ASSETS_OFFSET 0x452000U
|
||||||
|
/* Длина ассетов — рантайм: bsp_qspi_flash_size() - TFT_APP_QSPI_ASSETS_OFFSET. */
|
||||||
|
|
||||||
|
#endif /* SERVICES_PARTITION_H_ */
|
||||||
18
firmware/tft_app/src/services/settings_store/CMakeLists.txt
Normal file
18
firmware/tft_app/src/services/settings_store/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# services/settings_store — персист ядра настроек (§8, §10).
|
||||||
|
# settings_codec.c — чистая сериализация/CRC/дефолты (host-тест);
|
||||||
|
# settings_store.c — тонкий flash-адаптер поверх bsp_qspi_flash.
|
||||||
|
add_library(
|
||||||
|
tft_app_settings_store STATIC
|
||||||
|
src/settings_store.c
|
||||||
|
src/settings_codec.c)
|
||||||
|
|
||||||
|
target_include_directories(
|
||||||
|
tft_app_settings_store
|
||||||
|
PUBLIC include/
|
||||||
|
PRIVATE src/)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
tft_app_settings_store
|
||||||
|
PUBLIC bsp_qspi_flash # bsp/qspi_flash.h (API + геометрия), bsp/status.h транзитивно
|
||||||
|
tft_app_partition # смещения регионов (§10)
|
||||||
|
freertos_kernel) # мьютекс save() — sul_rx_task (§3.5) и menu_task пишут конкурентно
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* @file settings_store.h
|
||||||
|
* @brief Персистентные настройки индикатора (ARCH.md §8, §10) — ЯДРО.
|
||||||
|
*
|
||||||
|
* Хранит **ядро** настроек (ярусы A «железобетонные» + B «протокольные») в
|
||||||
|
* фиксированном секторе QSPI `0x450000` (§10) с magic/version/CRC32. Клиентский
|
||||||
|
* UX-«зоопарк» (ярус C: лого, сдвиги/маска этажей, метки) сюда НЕ входит — он в
|
||||||
|
* TLV рядом с layout-конфигом (§11), добавляется без правок этого ядра.
|
||||||
|
*
|
||||||
|
* Ярусы:
|
||||||
|
* A. Железобетонные (все клиенты): вес/вместимость, громкости, серийник, год.
|
||||||
|
* B. Протокольные (§8): активный протокол трактует `proto_slice` через
|
||||||
|
* свой дескриптор `sul_settings_desc_t` (НКУ-CAN: proto_slice[0] = адрес).
|
||||||
|
*
|
||||||
|
* Формат/сериализация (magic/version/CRC, дефолты) — чистые функции в
|
||||||
|
* settings_codec.c, host-тестируются без QSPI. Этот модуль — тонкий flash-адаптер
|
||||||
|
* поверх них (`bsp_qspi_flash`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SERVICES_SETTINGS_STORE_H_
|
||||||
|
#define SERVICES_SETTINGS_STORE_H_
|
||||||
|
|
||||||
|
#include "bsp/status.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ── Константы ───────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#define SETTINGS_SERIAL_LEN 10U /* серийный номер, ASCII + '\0' */
|
||||||
|
#define SETTINGS_PROTO_SLICE_LEN 8U /* запас под протокольные параметры (§8) */
|
||||||
|
#define SETTINGS_CORE_RESERVED 8U /* задел под будущие ядровые поля A/B */
|
||||||
|
#define SETTINGS_VOLUME_LEVELS 5U /* индексы громкости: OFF/25/50/75/100 */
|
||||||
|
|
||||||
|
/* ── Устройство/провиженинг (меняется редко, не в пользовательском меню) ──── */
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint8_t panel_type; /**< bsp_display_type_t; хардкод TFT8, provisioning — Фаза 9 */
|
||||||
|
uint8_t protocol_id; /**< активный протокол реестра sul (NKU_CAN = 0) */
|
||||||
|
uint8_t log_enabled; /**< рантайм-тумблер логов (под-шаг 3.6) */
|
||||||
|
uint8_t _pad;
|
||||||
|
} settings_device_t;
|
||||||
|
|
||||||
|
/* ── Пользовательские (редактируются в меню на объекте) ──────────────────── */
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
/* Ярус A — железобетонные (хранятся сейчас; редактор/эффект — Фазы 5/6). */
|
||||||
|
uint16_t max_load_kg; /**< грузоподъёмность, кг 0..9999 */
|
||||||
|
uint8_t max_cap_persons; /**< вместимость, чел 0..99 */
|
||||||
|
uint8_t sound_volume_idx; /**< 0..SETTINGS_VOLUME_LEVELS-1 */
|
||||||
|
uint8_t music_volume_idx;
|
||||||
|
uint8_t year_production; /**< 0 = скрыть, иначе 2000+N (25 = 2025) */
|
||||||
|
char serial[SETTINGS_SERIAL_LEN]; /**< ASCII, '\0'-терминирован */
|
||||||
|
uint8_t dummy_option;
|
||||||
|
/* Ярус B — протокольные: активный протокол трактует slice через
|
||||||
|
sul_settings_desc_t (§8). НКУ-CAN: proto_slice[0] = адрес 0..15. */
|
||||||
|
uint8_t proto_slice[SETTINGS_PROTO_SLICE_LEN];
|
||||||
|
} settings_user_t;
|
||||||
|
|
||||||
|
/* ── Ядро настроек ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
settings_device_t device;
|
||||||
|
settings_user_t user;
|
||||||
|
uint8_t _reserved[SETTINGS_CORE_RESERVED]; /**< размер фикс — offset'ы полей стабильны */
|
||||||
|
} settings_t;
|
||||||
|
|
||||||
|
/* ── API ─────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Инициализировать активные настройки значениями по умолчанию (без QSPI).
|
||||||
|
*
|
||||||
|
* Вызывать, когда QSPI недоступен (или до settings_store_load()) — get() после
|
||||||
|
* этого всегда валиден. settings_store_load() при валидном флеше перезапишет.
|
||||||
|
*/
|
||||||
|
void settings_store_init_defaults(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Загрузить настройки из QSPI (сектор §10).
|
||||||
|
*
|
||||||
|
* Предусловие: bsp_qspi_init() уже вызван. Читает сектор, проверяет
|
||||||
|
* magic/version/CRC32; при любой невалидности — загружает дефолты (настройки
|
||||||
|
* всё равно валидны).
|
||||||
|
*
|
||||||
|
* @retval BSP_OK загружено с флеша.
|
||||||
|
* @retval BSP_ERR_INVALID magic/version/CRC не совпал — загружены дефолты.
|
||||||
|
* @retval BSP_ERR_HW ошибка чтения флеша — загружены дефолты.
|
||||||
|
*/
|
||||||
|
bsp_status_t settings_store_load(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сохранить текущие настройки на QSPI.
|
||||||
|
*
|
||||||
|
* Стирает рабочий сектор, пересчитывает CRC32, пишет постранично. Вызывать по
|
||||||
|
* подтверждению в меню (3.2). Предусловие: bsp_qspi_init().
|
||||||
|
*
|
||||||
|
* @retval BSP_OK / BSP_ERR_HW.
|
||||||
|
*/
|
||||||
|
bsp_status_t settings_store_save(void);
|
||||||
|
|
||||||
|
/** @brief Указатель на активные настройки (только чтение). Валиден после init/load. */
|
||||||
|
const settings_t *settings_store_get(void);
|
||||||
|
|
||||||
|
/** @brief Указатель на активные настройки для изменения (меню). save() — отдельно. */
|
||||||
|
settings_t *settings_store_get_mutable(void);
|
||||||
|
|
||||||
|
/** @brief Сбросить пользовательский ярус к дефолтам (device не трогает). save() — отдельно. */
|
||||||
|
void settings_store_reset_user_defaults(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* SERVICES_SETTINGS_STORE_H_ */
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
#include "settings_codec.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
_Static_assert(sizeof(settings_page_t) == BSP_QSPI_SECTOR_SIZE,
|
||||||
|
"settings_page_t must be exactly one QSPI sector");
|
||||||
|
|
||||||
|
/* ── Дефолты ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* panel_type = BSP_DISPLAY_TFT8. В bsp_display_type_t это значение 2
|
||||||
|
* (TFT4=0, TFT7=1, TFT8=2); здесь не тянем bsp/display.h в чистый модуль,
|
||||||
|
* значение фиксируем числом (см. ARCH §9, панель — provisioning Фазы 9). */
|
||||||
|
#define SETTINGS_DEFAULT_PANEL 2U /* BSP_DISPLAY_TFT8 */
|
||||||
|
|
||||||
|
static const settings_t K_DEFAULTS = {
|
||||||
|
.device =
|
||||||
|
{
|
||||||
|
.panel_type = SETTINGS_DEFAULT_PANEL,
|
||||||
|
.protocol_id = 0U, /* NKU_CAN */
|
||||||
|
.log_enabled = 1U, /* bring-up логи из коробки; меню выключит */
|
||||||
|
},
|
||||||
|
.user =
|
||||||
|
{
|
||||||
|
.max_load_kg = 0U, /* «не задано» → скрыто в UI */
|
||||||
|
.max_cap_persons = 0U,
|
||||||
|
.sound_volume_idx = 2U,
|
||||||
|
.music_volume_idx = 1U,
|
||||||
|
.year_production = 0U, /* скрыть */
|
||||||
|
.serial = "",
|
||||||
|
.dummy_option = 0U, // Моя опция
|
||||||
|
.proto_slice = {0}, /* НКУ: адрес 0 */
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
settings_t settings_defaults(void)
|
||||||
|
{
|
||||||
|
return K_DEFAULTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── CRC-32 (MSB-first, poly 0x04C11DB7, init 0xFFFFFFFF, без финального xor) ─ */
|
||||||
|
|
||||||
|
uint32_t settings_crc32(const uint8_t *p_data, size_t len)
|
||||||
|
{
|
||||||
|
uint32_t crc = 0xFFFFFFFFU;
|
||||||
|
for (size_t i = 0U; i < len; ++i)
|
||||||
|
{
|
||||||
|
crc ^= (uint32_t) p_data[i] << 24U;
|
||||||
|
for (uint8_t b = 0U; b < 8U; ++b)
|
||||||
|
{
|
||||||
|
crc = (crc & 0x80000000U) ? ((crc << 1U) ^ 0x04C11DB7U) : (crc << 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Сериализация ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void settings_serialize(const settings_t *p_in, settings_page_t *p_out)
|
||||||
|
{
|
||||||
|
memset(p_out, 0xFFU, sizeof(*p_out)); /* _pad/_reserved = 0xFF (стёртый флеш) */
|
||||||
|
p_out->magic = SETTINGS_MAGIC;
|
||||||
|
p_out->version = SETTINGS_VERSION;
|
||||||
|
p_out->data = *p_in;
|
||||||
|
p_out->crc32 = settings_crc32((const uint8_t *) p_out, sizeof(*p_out) - sizeof(uint32_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool settings_deserialize(const settings_page_t *p_page, settings_t *p_out)
|
||||||
|
{
|
||||||
|
if ((p_page->magic != SETTINGS_MAGIC) || (p_page->version != SETTINGS_VERSION))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t CRC =
|
||||||
|
settings_crc32((const uint8_t *) p_page, sizeof(*p_page) - sizeof(uint32_t));
|
||||||
|
if (CRC != p_page->crc32)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*p_out = p_page->data;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* @file settings_codec.h
|
||||||
|
* @brief Чистая сериализация/валидация ядра настроек (без QSPI) — host-тест.
|
||||||
|
*
|
||||||
|
* Раскладка страницы на флеше, magic/version/CRC32 и дефолты. Отделено от
|
||||||
|
* flash-адаптера (settings_store.c), чтобы тестировать формат на хосте без
|
||||||
|
* железа (по образцу доменных чистых модулей).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SETTINGS_CODEC_H_
|
||||||
|
#define SETTINGS_CODEC_H_
|
||||||
|
|
||||||
|
#include "bsp/qspi_flash.h" /* BSP_QSPI_SECTOR_SIZE */
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define SETTINGS_MAGIC 0x54465453U /* 'STFT' (little-endian) */
|
||||||
|
#define SETTINGS_VERSION 1U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Страница настроек — ровно один сектор QSPI.
|
||||||
|
*
|
||||||
|
* CRC32 считается по всем байтам страницы КРОМЕ самого поля crc32.
|
||||||
|
* _pad и _reserved при сохранении = 0xFF (стёртый флеш).
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint32_t magic;
|
||||||
|
uint8_t version;
|
||||||
|
uint8_t _pad[3];
|
||||||
|
settings_t data;
|
||||||
|
uint8_t _reserved[BSP_QSPI_SECTOR_SIZE - sizeof(uint32_t) /* magic */
|
||||||
|
- sizeof(uint8_t) /* version */
|
||||||
|
- 3U /* _pad */
|
||||||
|
- sizeof(settings_t) - sizeof(uint32_t)]; /* crc32 */
|
||||||
|
uint32_t crc32;
|
||||||
|
} settings_page_t;
|
||||||
|
|
||||||
|
/** @brief Настройки по умолчанию (при пустом/битом флеше). */
|
||||||
|
settings_t settings_defaults(void);
|
||||||
|
|
||||||
|
/** @brief Собрать страницу из настроек: magic/version/data/reserved(0xFF)/crc32. */
|
||||||
|
void settings_serialize(const settings_t *p_in, settings_page_t *p_out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Проверить страницу и извлечь настройки.
|
||||||
|
* @return true — magic/version/CRC валидны, *p_out заполнен; false — иначе
|
||||||
|
* (*p_out не тронут; caller подставляет дефолты).
|
||||||
|
*/
|
||||||
|
bool settings_deserialize(const settings_page_t *p_page, settings_t *p_out);
|
||||||
|
|
||||||
|
/** @brief CRC-32 (poly 0x04C11DB7, MSB-first, init 0xFFFFFFFF) — как в style_updater. */
|
||||||
|
uint32_t settings_crc32(const uint8_t *p_data, size_t len);
|
||||||
|
|
||||||
|
#endif /* SETTINGS_CODEC_H_ */
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
#include "services/settings_store.h"
|
||||||
|
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "bsp/qspi_flash.h"
|
||||||
|
#include "semphr.h"
|
||||||
|
#include "services/partition.h"
|
||||||
|
#include "settings_codec.h"
|
||||||
|
|
||||||
|
/* Активные настройки — единственный экземпляр модуля. */
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
ensure_save_mutex();
|
||||||
|
g_settings = settings_defaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_status_t settings_store_load(void)
|
||||||
|
{
|
||||||
|
ensure_save_mutex();
|
||||||
|
|
||||||
|
/* static: страница = сектор (4 КБ) — на стеке задачи (~3 КБ) переполнит его.
|
||||||
|
* Однократный вызов при старте, не реентерабельно (как s_page в save()). */
|
||||||
|
static settings_page_t s_page;
|
||||||
|
|
||||||
|
if (bsp_qspi_read(TFT_APP_QSPI_SETTINGS_OFFSET, (uint8_t *) &s_page, sizeof(s_page)) != BSP_OK)
|
||||||
|
{
|
||||||
|
g_settings = settings_defaults();
|
||||||
|
return BSP_ERR_HW;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings_deserialize(&s_page, &g_settings))
|
||||||
|
{
|
||||||
|
g_settings = settings_defaults();
|
||||||
|
return BSP_ERR_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_status_t settings_store_save(void)
|
||||||
|
{
|
||||||
|
/* g_save_mutex гарантированно создан к этому моменту (init_defaults()/
|
||||||
|
* load() — единственные вызывающиеся раньше, см. докстрок выше). */
|
||||||
|
(void) xSemaphoreTake(g_save_mutex, portMAX_DELAY);
|
||||||
|
|
||||||
|
/* static: не 4 КБ на стеке + гарантия выравнивания под постраничную запись. */
|
||||||
|
static settings_page_t 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)
|
||||||
|
{
|
||||||
|
rc = BSP_ERR_HW;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc == BSP_OK)
|
||||||
|
{
|
||||||
|
const uint32_t PAGES = BSP_QSPI_SECTOR_SIZE / BSP_QSPI_PAGE_SIZE;
|
||||||
|
for (uint32_t i = 0U; i < PAGES; ++i)
|
||||||
|
{
|
||||||
|
const uint32_t ADDR = TFT_APP_QSPI_SETTINGS_OFFSET + 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)
|
||||||
|
{
|
||||||
|
rc = BSP_ERR_HW;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(void) xSemaphoreGive(g_save_mutex);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings_t *settings_store_get(void)
|
||||||
|
{
|
||||||
|
return &g_settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings_t *settings_store_get_mutable(void)
|
||||||
|
{
|
||||||
|
return &g_settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
void settings_store_reset_user_defaults(void)
|
||||||
|
{
|
||||||
|
const settings_t DEFAULTS = settings_defaults();
|
||||||
|
g_settings.user = DEFAULTS.user;
|
||||||
|
}
|
||||||
2
firmware/tft_app/src/ui/CMakeLists.txt
Normal file
2
firmware/tft_app/src/ui/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
add_subdirectory(fallback)
|
||||||
|
add_subdirectory(menu)
|
||||||
3
firmware/tft_app/src/ui/README.md
Normal file
3
firmware/tft_app/src/ui/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# ui
|
||||||
|
|
||||||
|
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.
|
||||||
8
firmware/tft_app/src/ui/fallback/CMakeLists.txt
Normal file
8
firmware/tft_app/src/ui/fallback/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
add_library(tft_app_ui_fallback STATIC src/fallback.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_ui_fallback PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
tft_app_ui_fallback
|
||||||
|
PUBLIC tft_app_elevator_model tft_app_controller
|
||||||
|
PRIVATE tft_app_gfx)
|
||||||
62
firmware/tft_app/src/ui/fallback/include/ui/fallback.h
Normal file
62
firmware/tft_app/src/ui/fallback/include/ui/fallback.h
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* @file fallback.h
|
||||||
|
* @brief Fallback-рендер (ARCH §11): позиция кабины + стрелка направления,
|
||||||
|
* asset-free, FS-free. Один вкомпилированный шрифт (FloorFontFallback),
|
||||||
|
* примитив стрелки — без TLV/layout-движка (те — Фаза 4/5).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef UI_FALLBACK_H_
|
||||||
|
#define UI_FALLBACK_H_
|
||||||
|
|
||||||
|
#include "domain/controller.h"
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#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 Безусловная первая отрисовка при старте.
|
||||||
|
*
|
||||||
|
* controller_init() засеивает кэш дефолтом — если первый реальный кадр
|
||||||
|
* совпадёт с дефолтом, indication_task_t придёт «ничего не изменилось», и
|
||||||
|
* экран останется пустым, если полагаться только на ui_fallback_render().
|
||||||
|
* Вызвать один раз при старте до входа в цикл получения кадров.
|
||||||
|
*
|
||||||
|
* @param dispatcher текущее состояние диспетчерского входа (см. выше)
|
||||||
|
*/
|
||||||
|
void ui_fallback_render_initial(const sul_result_t *p_result, dispatcher_indication_t dispatcher);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Инкрементальная перерисовка по diff.
|
||||||
|
*
|
||||||
|
* Фаза 1: перерисовывает весь кадр целиком при ЛЮБОМ pending-поле (нет
|
||||||
|
* 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,
|
||||||
|
dispatcher_indication_t dispatcher);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* UI_FALLBACK_H_ */
|
||||||
132
firmware/tft_app/src/ui/fallback/src/fallback.c
Normal file
132
firmware/tft_app/src/ui/fallback/src/fallback.c
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
#include "ui/fallback.h"
|
||||||
|
|
||||||
|
#include "domain/mode_priority.h"
|
||||||
|
#include "services/gfx.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Фаза 1/2: позиции захардкожены под TFT8 (800×600, panel текущего стенда).
|
||||||
|
* Цифра '0' FloorFontFallback — 132×162 px (проверено по сгенерированным
|
||||||
|
* данным шрифта). Фаза 5 (layout-движок) заменит на якорное позиционирование,
|
||||||
|
* независимое от разрешения панели. */
|
||||||
|
#define PANEL_WIDTH 800U
|
||||||
|
#define POS_Y 150U
|
||||||
|
#define ARROW_Y 380U
|
||||||
|
#define ARROW_SIZE 80U
|
||||||
|
#define MODE_Y 260U /* строка режима (SystemFont), safe-mode индикация */
|
||||||
|
#define LADING_Y 330U /* обратный отсчёт погрузки (крупным шрифтом) */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Короткая asset-free метка спецрежима (ARCH §11: fallback без
|
||||||
|
* спрайтов/FS). Богатая полноэкранная графика режимов — Фаза 4/5;
|
||||||
|
* здесь — только текст системным шрифтом, чтобы режим был виден и в
|
||||||
|
* safe-mode (без ассетов). NORMAL метки не имеет.
|
||||||
|
*/
|
||||||
|
static const char *mode_label(sul_mode_t mode)
|
||||||
|
{
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case SUL_MODE_FIREMAN: return "ПОЖАРНЫЙ";
|
||||||
|
case SUL_MODE_FIRE_ALARM: return "ПОЖАР";
|
||||||
|
case SUL_MODE_OVERLOAD: return "ПЕРЕГРУЗ";
|
||||||
|
case SUL_MODE_SEISMIC: return "СЕЙСМО";
|
||||||
|
case SUL_MODE_MAINTENANCE: return "СЕРВИС";
|
||||||
|
case SUL_MODE_LADING: return "ПОГРУЗКА";
|
||||||
|
case SUL_MODE_NORMAL:
|
||||||
|
default: return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_centered(const tFont *p_font, const char *p_str, uint16_t y)
|
||||||
|
{
|
||||||
|
const uint16_t width = gfx_string_width(p_font, p_str);
|
||||||
|
const uint16_t x = (uint16_t) ((PANEL_WIDTH - width) / 2U);
|
||||||
|
(void) gfx_draw_string(p_font, p_str, x, y, GFX_COLOR_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render_normal(const sul_result_t *p_result)
|
||||||
|
{
|
||||||
|
draw_centered(&FloorFontFallback, p_result->pos, POS_Y);
|
||||||
|
|
||||||
|
/* SUL_DIR_NONE — без стрелки. SUL_DIR_DOUBLE — тоже (двойная стрелка —
|
||||||
|
* отдельная спрайтовая индикация Фазы 4/5, в fallback не рисуем). */
|
||||||
|
if ((p_result->direction == SUL_DIR_UP) || (p_result->direction == SUL_DIR_DOWN))
|
||||||
|
{
|
||||||
|
const gfx_arrow_dir_t dir = (p_result->direction == SUL_DIR_UP) ? GFX_ARROW_UP : GFX_ARROW_DOWN;
|
||||||
|
const uint16_t arrow_x = (uint16_t) ((PANEL_WIDTH - ARROW_SIZE) / 2U);
|
||||||
|
gfx_draw_arrow(dir, arrow_x, ARROW_Y, ARROW_SIZE, GFX_COLOR_WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
/* Диспетчерский вход — локальный (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);
|
||||||
|
if (label == NULL)
|
||||||
|
{
|
||||||
|
render_normal(p_result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Спецрежим: safe-mode текст. Для временной погрузки — ещё и обратный
|
||||||
|
* отсчёт крупным шрифтом (lading_secs>0 отличает временную от
|
||||||
|
* инструментальной). */
|
||||||
|
draw_centered(&SystemFont, label, MODE_Y);
|
||||||
|
|
||||||
|
if ((mode == SUL_MODE_LADING) && (p_result->lading_secs > 0U))
|
||||||
|
{
|
||||||
|
char secs[6];
|
||||||
|
(void) snprintf(secs, sizeof(secs), "%u", (unsigned) p_result->lading_secs);
|
||||||
|
draw_centered(&FloorFontFallback, secs, LADING_Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_fallback_render_initial(const sul_result_t *p_result, dispatcher_indication_t dispatcher)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
dispatcher_indication_t dispatcher)
|
||||||
|
{
|
||||||
|
/* Перерисовываем на изменение того, что fallback реально показывает:
|
||||||
|
* позиция, стрелка, режим (+ отсчёт погрузки идёт вместе с mode/pos).
|
||||||
|
* next-этаж в safe-mode не рисуется (богатый layout — Фаза 5), поэтому
|
||||||
|
* на 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)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
render(p_task->mode, p_result, dispatcher);
|
||||||
|
}
|
||||||
9
firmware/tft_app/src/ui/menu/CMakeLists.txt
Normal file
9
firmware/tft_app/src/ui/menu/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# ui/menu — рендер меню настроек (презентация, HIL). Фаза 3.2.2.
|
||||||
|
add_library(tft_app_ui_menu STATIC src/menu_view.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_ui_menu PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
tft_app_ui_menu
|
||||||
|
PUBLIC tft_app_menu # menu_ctx_t / menu_item_desc_t + запросы модели
|
||||||
|
PRIVATE tft_app_gfx) # примитивы + шрифты
|
||||||
41
firmware/tft_app/src/ui/menu/include/ui/menu_view.h
Normal file
41
firmware/tft_app/src/ui/menu/include/ui/menu_view.h
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @file menu_view.h
|
||||||
|
* @brief Рендер меню настроек (презентация) — отделён от чистой модели (menu.c).
|
||||||
|
*
|
||||||
|
* Рисует окно 480×272 в логических (0,0) — одинаково на всех панелях (§PLAN 3.2):
|
||||||
|
* заголовок уровня, список пунктов с полосой-курсором, значения, футер-подсказка.
|
||||||
|
* Читает только модель (menu_ctx_t) через её запросы; логики навигации не содержит.
|
||||||
|
* HIL — проверяется на железе.
|
||||||
|
*
|
||||||
|
* Рисует ПОЛНЫЙ кадр в AS (double-buffer + PXP, Фаза 3.2.4) — инкрементальной
|
||||||
|
* построчной отрисовки больше нет: off-screen перерисовка полного кадра +
|
||||||
|
* атомарный свап дёшевы и tear-free. Показ — gfx_present() у владельца дисплея.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef UI_MENU_VIEW_H_
|
||||||
|
#define UI_MENU_VIEW_H_
|
||||||
|
|
||||||
|
#include "menu/menu.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Габариты окна меню @ логич.(0,0) — публичны: владелец дисплея (task_render)
|
||||||
|
* использует их для оконного present'а (gfx_present_rect) при навигации. */
|
||||||
|
#define MENU_VIEW_WIN_W 480U
|
||||||
|
#define MENU_VIEW_WIN_H 272U
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Отрисовать полный кадр меню в AS (очистка + заголовок + строки + футер).
|
||||||
|
*
|
||||||
|
* Рисует off-screen (AS); на экран попадёт после gfx_present() владельца дисплея.
|
||||||
|
*/
|
||||||
|
void menu_view_render(const menu_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* UI_MENU_VIEW_H_ */
|
||||||
162
firmware/tft_app/src/ui/menu/src/menu_view.c
Normal file
162
firmware/tft_app/src/ui/menu/src/menu_view.c
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
#include "ui/menu_view.h"
|
||||||
|
|
||||||
|
#include "services/gfx.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Окно @ (0,0) — габариты публичны (menu_view.h: MENU_VIEW_WIN_W/H, нужны
|
||||||
|
* task_render для оконного present'а). Метрики под реальные шрифты: JBMono24
|
||||||
|
* h=31 (заголовок/строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */
|
||||||
|
#define WIN_W MENU_VIEW_WIN_W
|
||||||
|
#define WIN_H MENU_VIEW_WIN_H
|
||||||
|
#define TITLE_H 36U
|
||||||
|
#define ROW_H 36U
|
||||||
|
#define FOOTER_H 20U
|
||||||
|
#define ROWS_Y0 TITLE_H
|
||||||
|
#define FOOTER_Y (WIN_H - FOOTER_H) /* 252 */
|
||||||
|
#define TEXT_DY24 2U /* центрирование JBMono24 (h31) в полосе 36 */
|
||||||
|
#define TEXT_DY12 2U /* JBMono12 (h16) в футере 20 */
|
||||||
|
#define LEFT_MARGIN 16U
|
||||||
|
#define RIGHT_MARGIN 16U
|
||||||
|
#define EDGE_MARGIN 8U
|
||||||
|
|
||||||
|
/* Рамка 1 px по периметру — строки/футер вписаны внутрь неё (не затирают). */
|
||||||
|
#define BORDER 1U
|
||||||
|
#define CONTENT_X BORDER
|
||||||
|
#define CONTENT_W (WIN_W - 2U * BORDER)
|
||||||
|
|
||||||
|
/* Палитра (XRGB). Собирается тинтингом из белых шрифтов. */
|
||||||
|
#define COL_BG 0x000000U
|
||||||
|
#define COL_TITLE 0x00CFE0FFU
|
||||||
|
#define COL_LABEL 0x00FFFFFFU
|
||||||
|
#define COL_VALUE 0x00BFBFBFU
|
||||||
|
#define COL_SEL_BG 0x0017406BU
|
||||||
|
#define COL_SEL_TEXT 0x00FFFFFFU
|
||||||
|
#define COL_FOOTER 0x006F6F6FU
|
||||||
|
#define COL_SEP 0x00333333U /* разделители + рамка */
|
||||||
|
|
||||||
|
/* Строка значения пункта. @return false — у пункта нет значения (BACK). */
|
||||||
|
static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size_t buf_len)
|
||||||
|
{
|
||||||
|
const menu_item_desc_t *p_it = &p_ctx->items[idx];
|
||||||
|
|
||||||
|
switch (p_it->type)
|
||||||
|
{
|
||||||
|
case MENU_SUBMENU:
|
||||||
|
(void) snprintf(p_buf, buf_len, ">");
|
||||||
|
return true;
|
||||||
|
case MENU_BYTE:
|
||||||
|
(void) snprintf(p_buf, buf_len, "%u", (unsigned) menu_read_value(p_ctx, idx));
|
||||||
|
return true;
|
||||||
|
case MENU_SELECT:
|
||||||
|
case MENU_BOOL:
|
||||||
|
{
|
||||||
|
const uint8_t V = menu_read_value(p_ctx, idx);
|
||||||
|
if (p_it->options != NULL)
|
||||||
|
{
|
||||||
|
(void) snprintf(p_buf, buf_len, "%s", p_it->options[V]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(void) snprintf(p_buf, buf_len, "%u", (unsigned) V);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MENU_BACK:
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Отрисовать одну строку по её экранному y (fill фона + подпись + значение).
|
||||||
|
* Заливает CONTENT_X..CONTENT_W — рамку (x=0, x=WIN_W-1) не трогает. */
|
||||||
|
static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool selected)
|
||||||
|
{
|
||||||
|
const gfx_color_t BG_COL = selected ? COL_SEL_BG : COL_BG;
|
||||||
|
const gfx_color_t TEXT_COL =
|
||||||
|
selected
|
||||||
|
? COL_SEL_TEXT
|
||||||
|
: COL_LABEL; // FIXME: Conditional operator with identical true and false expressions
|
||||||
|
gfx_fill_rect(CONTENT_X, row_y, CONTENT_W, ROW_H, BG_COL);
|
||||||
|
|
||||||
|
(void) gfx_draw_string(&SystemFont, p_ctx->items[idx].label, LEFT_MARGIN,
|
||||||
|
(uint16_t) (row_y + TEXT_DY24), TEXT_COL);
|
||||||
|
|
||||||
|
char value[24];
|
||||||
|
if (format_value(p_ctx, idx, value, sizeof(value)))
|
||||||
|
{
|
||||||
|
const uint16_t VW = gfx_string_width(&SystemFont, value);
|
||||||
|
const uint16_t VX = (uint16_t) (WIN_W - RIGHT_MARGIN - VW);
|
||||||
|
const gfx_color_t VC = selected ? COL_SEL_TEXT : COL_VALUE;
|
||||||
|
(void) gfx_draw_string(&SystemFont, value, VX, (uint16_t) (row_y + TEXT_DY24), VC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_footer(const menu_ctx_t *p_ctx, uint8_t level_first, uint8_t level_last)
|
||||||
|
{
|
||||||
|
gfx_fill_rect(CONTENT_X, (uint16_t) (FOOTER_Y - 1U), CONTENT_W, 1U, COL_SEP);
|
||||||
|
|
||||||
|
(void) gfx_draw_string(&SystemFontSmall, "IN - далее, SEL - выбор", LEFT_MARGIN,
|
||||||
|
(uint16_t) (FOOTER_Y + TEXT_DY12), COL_FOOTER);
|
||||||
|
|
||||||
|
const uint8_t TOTAL = (uint8_t) (level_last - level_first + 1U);
|
||||||
|
const uint8_t PAGES = (uint8_t) ((TOTAL + MENU_ITEMS_PER_PAGE - 1U) / MENU_ITEMS_PER_PAGE);
|
||||||
|
char page[8];
|
||||||
|
(void) snprintf(page, sizeof(page), "%u/%u", (unsigned) (p_ctx->page + 1U), (unsigned) PAGES);
|
||||||
|
const uint16_t PW = gfx_string_width(&SystemFontSmall, page);
|
||||||
|
(void) gfx_draw_string(&SystemFontSmall, page, (uint16_t) (WIN_W - EDGE_MARGIN - PW),
|
||||||
|
(uint16_t) (FOOTER_Y + TEXT_DY12), COL_FOOTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 от индикации вне окна делает владелец
|
||||||
|
* дисплея (task_render) один раз на ОТКРЫТИИ меню — вместе с двумя
|
||||||
|
* полными present'ами (контракт gfx_present_rect). */
|
||||||
|
gfx_clear_rect(0U, 0U, WIN_W, WIN_H);
|
||||||
|
|
||||||
|
if (!p_ctx->open)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Заголовок = подпись текущего уровня (родитель выделенного пункта). */
|
||||||
|
const char *p_title = p_ctx->items[p_ctx->items[p_ctx->cur].parent].label;
|
||||||
|
const uint16_t TW = gfx_string_width(&SystemFont, p_title);
|
||||||
|
(void) gfx_draw_string(&SystemFont, p_title, (uint16_t) ((WIN_W - TW) / 2U), TEXT_DY24,
|
||||||
|
COL_TITLE);
|
||||||
|
gfx_fill_rect(CONTENT_X, (uint16_t) (TITLE_H - 1U), CONTENT_W, 1U, COL_SEP);
|
||||||
|
|
||||||
|
uint8_t first;
|
||||||
|
uint8_t last;
|
||||||
|
menu_level_range(p_ctx, &first, &last);
|
||||||
|
|
||||||
|
const uint8_t PAGE_START = (uint8_t) (first + p_ctx->page * MENU_ITEMS_PER_PAGE);
|
||||||
|
for (uint8_t slot = 0U; slot < MENU_ITEMS_PER_PAGE; slot++)
|
||||||
|
{
|
||||||
|
const uint8_t IDX = (uint8_t) (PAGE_START + slot);
|
||||||
|
if (IDX > last)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
draw_row(p_ctx, IDX, (uint16_t) (ROWS_Y0 + slot * ROW_H), (IDX == p_ctx->cur));
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_footer(p_ctx, first, last);
|
||||||
|
|
||||||
|
gfx_draw_rect(0U, 0U, WIN_W, WIN_H, COL_SEP);
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,39 @@ build-app-debug: _configure-debug
|
||||||
build-app-release: _configure-release
|
build-app-release: _configure-release
|
||||||
cmake --build --preset app-release
|
cmake --build --preset app-release
|
||||||
|
|
||||||
|
# ── подпись app (imgtool EC256, как stub; БЕЗ --confirm — self-confirm в рантайме) ──
|
||||||
|
# Загрузчик валидирует подпись слота (MCUBOOT_VALIDATE_PRIMARY_SLOT +
|
||||||
|
# MCUBOOT_SIGN_EC256), поэтому образ ОБЯЗАН быть подписан тестовым ключом
|
||||||
|
# root-ec-p256.pem. Прошивка результата — через SWD в слот A (0x60040000).
|
||||||
|
# HAB-контейнер (hab-app-*/SDP-путь) на Фазе 0 не используется, см. PLAN.md.
|
||||||
|
#
|
||||||
|
# БЕЗ --confirm: image_ok в трейлере НЕ выставлен на этапе подписи — образ
|
||||||
|
# подтверждает себя рантаймом (boot_set_next в confirm_self, src/app/main.c).
|
||||||
|
# Так сохраняется anti-brick revert: образ, который загрузился, но не смог
|
||||||
|
# подтвердиться (завис до confirm), MCUboot откатит на следующей загрузке.
|
||||||
|
|
||||||
|
[doc('Собрать и подписать tft_app для Slot A (Debug)')]
|
||||||
|
[group('build')]
|
||||||
|
sign-app-debug: build-app-debug
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "{{ BUILD_DIR }}/Debug/signed"
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 0.1.0 --align 1 --pad-header --pad \
|
||||||
|
"{{ BUILD_DIR }}/Debug/app.bin" "{{ BUILD_DIR }}/Debug/signed/app_slot_a.bin"
|
||||||
|
echo " ✅ build/Debug/signed/app_slot_a.bin (Slot A 0x60040000) — прошивка через just host::flash-swd-app-slot-debug"
|
||||||
|
|
||||||
|
[doc('Собрать и подписать tft_app для Slot A (Release)')]
|
||||||
|
[group('build')]
|
||||||
|
sign-app-release: build-app-release
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "{{ BUILD_DIR }}/Release/signed"
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 0.1.0 --align 1 --pad-header --pad \
|
||||||
|
"{{ BUILD_DIR }}/Release/app.bin" "{{ BUILD_DIR }}/Release/signed/app_slot_a.bin"
|
||||||
|
echo " ✅ build/Release/signed/app_slot_a.bin (Slot A 0x60040000)"
|
||||||
|
|
||||||
# ── все проекты сразу ─────────────────────────────────────────────────────────
|
# ── все проекты сразу ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[doc('Собрать все проекты (Debug)')]
|
[doc('Собрать все проекты (Debug)')]
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,30 @@ flash-swd-app-release:
|
||||||
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
||||||
--firmware app --build-type Release
|
--firmware app --build-type Release
|
||||||
|
|
||||||
|
# ── слот-образ (подписанный imgtool, без HAB) — пишем сырой .bin по адресу ──
|
||||||
|
# слота A напрямую через pyOCD (не через flash_swd.py: тот собирает FCB+IVT+HAB
|
||||||
|
# под 0x60000000; слот — это данные, которые читает boot_go(), не boot-образ
|
||||||
|
# BootROM). Тот же приём, что для stub'ов загрузчика. Требует собранного и
|
||||||
|
# подписанного образа: just build::sign-app-<debug|release>.
|
||||||
|
|
||||||
|
[doc('Прошить подписанный tft_app в Slot A (0x60040000) через SWD (Debug). Power cycle после')]
|
||||||
|
[group('flash-swd')]
|
||||||
|
flash-swd-app-slot-debug:
|
||||||
|
uv run --directory "{{ HIL_DIR }}" pyocd flash \
|
||||||
|
--target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \
|
||||||
|
--frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \
|
||||||
|
--base-address 0x60040000 --erase sector \
|
||||||
|
"{{ BUILD_DIR }}/Debug/signed/app_slot_a.bin"
|
||||||
|
|
||||||
|
[doc('Прошить подписанный tft_app в Slot A (0x60040000) через SWD (Release). Power cycle после')]
|
||||||
|
[group('flash-swd')]
|
||||||
|
flash-swd-app-slot-release:
|
||||||
|
uv run --directory "{{ HIL_DIR }}" pyocd flash \
|
||||||
|
--target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \
|
||||||
|
--frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \
|
||||||
|
--base-address 0x60040000 --erase sector \
|
||||||
|
"{{ BUILD_DIR }}/Release/signed/app_slot_a.bin"
|
||||||
|
|
||||||
[confirm("Стереть всю Flash (W25Q128)? Все прошивки будут удалены.")]
|
[confirm("Стереть всю Flash (W25Q128)? Все прошивки будут удалены.")]
|
||||||
[doc('Стереть всю Flash (W25Q128) через SWD (chip erase). Нужен power cycle после')]
|
[doc('Стереть всю Flash (W25Q128) через SWD (chip erase). Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,25 @@ target_link_libraries(
|
||||||
# Явный include на случай если utils обрабатывается после port в корневом CMake.
|
# Явный include на случай если utils обрабатывается после port в корневом CMake.
|
||||||
# Дублирует транзитивный путь из utils — безвредно.
|
# Дублирует транзитивный путь из utils — безвредно.
|
||||||
target_include_directories(port_log_uart PUBLIC ${CMAKE_SOURCE_DIR}/utils)
|
target_include_directories(port_log_uart PUBLIC ${CMAKE_SOURCE_DIR}/utils)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# port_log_cdc — USB CDC ACM адаптер для utils_log.
|
||||||
|
#
|
||||||
|
# Связывает utils_log с bsp_usb_cdc: - регистрирует bsp_usb_cdc_write() как
|
||||||
|
# write callback (best-effort — см. port/log_cdc.h) - предоставляет
|
||||||
|
# strong-реализацию log_get_timestamp_ms() → bsp_tick_get_ms()
|
||||||
|
#
|
||||||
|
# ВАЖНО: bsp_usb_cdc_init() вызывает bsp_delay() внутри себя — под FreeRTOS
|
||||||
|
# звать ТОЛЬКО после vTaskStartScheduler(), из задачи (см. port/log_cdc.h).
|
||||||
|
#
|
||||||
|
# Не компилируется для host-тестов (bsp_usb_cdc там недоступен).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
add_library(port_log_cdc STATIC src/log_cdc.c)
|
||||||
|
|
||||||
|
target_include_directories(port_log_cdc PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
port_log_cdc PUBLIC utils # несёт log.h и LOG_LEVEL транзитивно
|
||||||
|
bsp_usb_cdc bsp_tick)
|
||||||
|
target_include_directories(port_log_cdc PUBLIC ${CMAKE_SOURCE_DIR}/utils)
|
||||||
|
|
|
||||||
46
port/log/include/port/log_cdc.h
Normal file
46
port/log/include/port/log_cdc.h
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* @file port/log_cdc.h
|
||||||
|
* @brief USB CDC ACM адаптер логгера: регистрирует bsp_usb_cdc как транспорт.
|
||||||
|
*
|
||||||
|
* @warning Инициализация — ТОЛЬКО после vTaskStartScheduler() (из задачи
|
||||||
|
* FreeRTOS). bsp_usb_cdc_init() внутри себя вызывает bsp_delay(), которая в
|
||||||
|
* FreeRTOS-режиме bsp_tick реализована через xTaskGetTickCount() — до старта
|
||||||
|
* планировщика тик не идёт (счётчик стоит на 0), вызов до этого момента
|
||||||
|
* зависает навсегда.
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* // внутри задачи, ПОСЛЕ vTaskStartScheduler():
|
||||||
|
* bsp_usb_cdc_init();
|
||||||
|
* log_cdc_init(); // регистрирует транспорт + timestamp
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Транспорт best-effort: пока хост не открыл VCOM (или предыдущая передача
|
||||||
|
* ещё не завершена), записи молча отбрасываются — см. bsp_usb_cdc_write().
|
||||||
|
* Не подходит для сообщений, которые обязаны быть доставлены — для этого
|
||||||
|
* нужен ack-протокол поверх (как JSON-протокол bootloader'а).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PORT_LOG_CDC_H
|
||||||
|
#define PORT_LOG_CDC_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Зарегистрировать USB CDC как транспорт логгера.
|
||||||
|
*
|
||||||
|
* Вызывает log_init() с cdc-callback и регистрирует
|
||||||
|
* log_get_timestamp_ms() → bsp_tick_get_ms().
|
||||||
|
*
|
||||||
|
* Предусловие: bsp_usb_cdc_init() уже вызван — после старта планировщика,
|
||||||
|
* см. предупреждение в докстроке файла.
|
||||||
|
*/
|
||||||
|
void log_cdc_init(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PORT_LOG_CDC_H */
|
||||||
48
port/log/src/log_cdc.c
Normal file
48
port/log/src/log_cdc.c
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* @file log_cdc.c
|
||||||
|
* @brief USB CDC ACM адаптер логгера.
|
||||||
|
*
|
||||||
|
* Подключает utils_log к bsp_usb_cdc:
|
||||||
|
* - write callback → bsp_usb_cdc_write() (best-effort, см. port/log_cdc.h)
|
||||||
|
* - timestamp hook → bsp_tick_get_ms()
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "port/log_cdc.h"
|
||||||
|
|
||||||
|
#include "bsp/tick.h"
|
||||||
|
#include "bsp/usb_cdc.h"
|
||||||
|
#include "log/log.h"
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Strong-реализация weak-хука timestamp */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
uint32_t log_get_timestamp_ms(void)
|
||||||
|
{
|
||||||
|
return bsp_tick_get_ms();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Write callback */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void cdc_write(const char *p_buf, size_t len, void *p_ctx)
|
||||||
|
{
|
||||||
|
(void) p_ctx;
|
||||||
|
if (!bsp_usb_cdc_is_ready())
|
||||||
|
{
|
||||||
|
return; /* хост не подключён — молча отбрасываем (best-effort) */
|
||||||
|
}
|
||||||
|
/* Не блокируемся на BSP_ERR_BUSY (предыдущая передача не завершена) —
|
||||||
|
* это диагностический канал, потерянная строка не критична. */
|
||||||
|
(void) bsp_usb_cdc_write((const uint8_t *) p_buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Публичный API */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
void log_cdc_init(void)
|
||||||
|
{
|
||||||
|
log_init(cdc_write, NULL);
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
* @note Вызов LOG_* из ISR запрещён: xSemaphoreTake с portMAX_DELAY
|
* @note Вызов LOG_* из ISR запрещён: xSemaphoreTake с portMAX_DELAY
|
||||||
* недопустим в ISR-контексте.
|
* недопустим в ISR-контексте.
|
||||||
*/
|
*/
|
||||||
#if 0
|
|
||||||
#include "FreeRTOS.h"
|
#include "FreeRTOS.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
#include "semphr.h"
|
#include "semphr.h"
|
||||||
|
|
@ -37,4 +36,3 @@ void log_mutex_unlock(void)
|
||||||
{
|
{
|
||||||
xSemaphoreGive(s_log_mutex);
|
xSemaphoreGive(s_log_mutex);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
@ -56,6 +56,7 @@ add_sdk_driver(lpi2c fsl_lpi2c.c)
|
||||||
add_sdk_driver(pwm fsl_pwm.c)
|
add_sdk_driver(pwm fsl_pwm.c)
|
||||||
add_sdk_driver(adc fsl_adc.c)
|
add_sdk_driver(adc fsl_adc.c)
|
||||||
add_sdk_driver(elcdif fsl_elcdif.c)
|
add_sdk_driver(elcdif fsl_elcdif.c)
|
||||||
|
add_sdk_driver(pxp fsl_pxp.c)
|
||||||
add_sdk_driver(cache fsl_cache.c)
|
add_sdk_driver(cache fsl_cache.c)
|
||||||
add_sdk_driver(sai fsl_sai.c)
|
add_sdk_driver(sai fsl_sai.c)
|
||||||
add_sdk_driver(sai_edma fsl_sai_edma.c)
|
add_sdk_driver(sai_edma fsl_sai_edma.c)
|
||||||
|
|
@ -73,6 +74,7 @@ target_link_libraries(sdk_lpi2c PUBLIC sdk_clock)
|
||||||
target_link_libraries(sdk_pwm PUBLIC sdk_clock)
|
target_link_libraries(sdk_pwm PUBLIC sdk_clock)
|
||||||
target_link_libraries(sdk_adc PUBLIC sdk_clock)
|
target_link_libraries(sdk_adc PUBLIC sdk_clock)
|
||||||
target_link_libraries(sdk_elcdif PUBLIC sdk_clock)
|
target_link_libraries(sdk_elcdif PUBLIC sdk_clock)
|
||||||
|
target_link_libraries(sdk_pxp PUBLIC sdk_clock) # PXP_Init → CLOCK_EnableClock(kCLOCK_Pxp)
|
||||||
target_link_libraries(sdk_sai PUBLIC sdk_clock)
|
target_link_libraries(sdk_sai PUBLIC sdk_clock)
|
||||||
target_link_libraries(sdk_sai_edma PUBLIC sdk_sai sdk_edma)
|
target_link_libraries(sdk_sai_edma PUBLIC sdk_sai sdk_edma)
|
||||||
target_link_libraries(sdk_edma PUBLIC sdk_common)
|
target_link_libraries(sdk_edma PUBLIC sdk_common)
|
||||||
|
|
|
||||||
|
|
@ -314,3 +314,122 @@ if(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||||
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/slot_version.c
|
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/slot_version.c
|
||||||
PROPERTIES COMPILE_OPTIONS "-fno-sanitize=address,undefined")
|
PROPERTIES COMPILE_OPTIONS "-fno-sanitize=address,undefined")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# tft_app — smoke (Фаза 0: проверка харнесса; доменные тесты — Фаза 1)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_tft_app_smoke
|
||||||
|
SOURCES
|
||||||
|
tft_app_smoke/test_tft_app_smoke.c)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# tft_app — domain (Фаза 1/2: чистый декодер НКУ-CAN + контроллер + приоритеты)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
set(TFT_APP_DOMAIN_DIR ${PROJECT_SOURCE_DIR}/firmware/tft_app/src/domain)
|
||||||
|
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_sul_nku_can
|
||||||
|
SOURCES
|
||||||
|
tft_app_sul_nku/test_sul_nku_can.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/src/nku_can.c
|
||||||
|
INCLUDES
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/include)
|
||||||
|
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_tft_app_controller
|
||||||
|
SOURCES
|
||||||
|
tft_app_controller/test_controller.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/controller/src/controller.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/controller/src/mode_priority.c
|
||||||
|
INCLUDES
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/controller/include)
|
||||||
|
|
||||||
|
# tft_app — settings_store (Фаза 3.1: чистый кодек настроек magic/version/CRC)
|
||||||
|
set(TFT_APP_SETTINGS_DIR ${PROJECT_SOURCE_DIR}/firmware/tft_app/src/services/settings_store)
|
||||||
|
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_tft_app_settings_store
|
||||||
|
SOURCES
|
||||||
|
tft_app_settings_store/test_settings_codec.c
|
||||||
|
${TFT_APP_SETTINGS_DIR}/src/settings_codec.c
|
||||||
|
INCLUDES
|
||||||
|
${TFT_APP_SETTINGS_DIR}/include
|
||||||
|
${TFT_APP_SETTINGS_DIR}/src
|
||||||
|
${PROJECT_SOURCE_DIR}/bsp/qspi_flash/include
|
||||||
|
${PROJECT_SOURCE_DIR}/bsp/common/include)
|
||||||
|
|
||||||
|
# tft_app — menu (Фаза 3.2.1: чистая модель навигации/редактирования, без QSPI)
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_tft_app_menu
|
||||||
|
SOURCES
|
||||||
|
tft_app_menu/test_menu.c
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/tft_app/src/menu/src/menu.c
|
||||||
|
INCLUDES
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/tft_app/src/menu/include
|
||||||
|
${TFT_APP_SETTINGS_DIR}/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)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue