Compare commits

...

11 commits

119 changed files with 33295 additions and 19 deletions

5
.gitignore vendored
View file

@ -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
View file

@ -57,7 +57,7 @@
"loadFiles": [], "loadFiles": [],
"runToEntryPoint": "main", "runToEntryPoint": "main",
"rtos": "FreeRTOS", "rtos": "FreeRTOS",
"preLaunchTask": "build:firmware-test-debug", "preLaunchTask": "build:app-debug",
} }
] ]
} }

View file

@ -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()

View file

@ -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"
] ]
}, },
{ {

View file

@ -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` допускается).

View file

@ -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 расширяет 56524 на пинах).
*/
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, выключить подсветку, деинициализировать.
* *

View file

@ -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 расширяет 56524 на пинах (см. 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);

View 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")
}

View 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) |

View 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) |

View file

@ -0,0 +1,209 @@
# tft-app — путь данных домена (транспорт → экран)
Документ описывает **реализованный** доменный слой (Фазы 12): как кадр от СУЛ проходит
от шины до презентации, какие модули за что отвечают и каков контракт между ними. Проектное
обоснование слоёв — в [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 со спрайтами и звук — Фазы 46 ([PLAN.md](../../firmware/tft_app/PLAN.md)).

136
docs/tft_app/FALLBACK.md Normal file
View 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**
индикация: богатая полноэкранная графика режимов (фон+спрайты) появится в Фазах 45. Для
временной погрузки дополнительно рисуется обратный отсчёт (`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` знает только `09`, `-`, пробел, а
позиция пришла кириллицей). `gfx_draw_string()` подставляет `-` вместо отсутствующего глифа —
по-символьно, не обрывая всю строку на первом неизвестном символе. Если даже `-` нет в шрифте —
символ пропускается (нулевая ширина).
```mermaid
flowchart LR
CH["символ строки"] --> HAS{"есть глиф<br/>в активном шрифте?"}
HAS -->|да| DRAW["нарисовать глиф"]
HAS -->|нет| SUB{"есть глиф '-'?"}
SUB -->|да| DASH["нарисовать '-'"]
SUB -->|нет| SKIP["пропустить (ширина 0)"]
```
Механизм есть с Фазы 1. Выделенная чистая функция проверки покрытия шрифта + её host-тест —
остаток Фазы 2 (см. [PLAN.md](../../firmware/tft_app/PLAN.md)).
---
## 4. Когда перерисовываем
`ui_fallback_render()` перерисовывает кадр целиком (без dirty-rect — `gfx` минимален) при
изменении того, что fallback реально показывает: **позиция, стрелка или режим**
(`pos_pending || direction_pending || mode_pending`). Иначе — no-op. И этот путь, и
безусловный старт (ниже) принимают диспетчерский вход (§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
View file

@ -0,0 +1,216 @@
# tft-app — движок меню и связь с настройками
Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель; 3.2.23.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.23.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.13.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-буфере
логгера между несколькими пишущими задачами).

View file

@ -0,0 +1,103 @@
# tft-app — приоритеты режимов
Как множество одновременных сигналов от СУЛ сворачивается в **один** экранный режим.
Проектная основа — [ARCH.md §7](../../firmware/tft_app/ARCH.md); реализация —
[mode_priority.c](../../firmware/tft_app/src/domain/controller/src/mode_priority.c).
---
## 1. Задача
Сигналы в `sul_result_t` **ортогональны** — могут быть активны одновременно (перегруз во время
сервиса, пожар при погрузке и т.п.). Но экран показывает **один** режим. Нужно правило: какой
режим побеждает, когда активны несколько.
```mermaid
flowchart LR
subgraph SIG["сигналы sul_result_t (булевы, независимы)"]
F["fireman"]
FA["fire_alarm"]
OV["overload"]
SE["seismic"]
MA["maintenance"]
LA["lading"]
end
SIG --> R["sul_resolve_mode()"]
R --> M["sul_mode_t<br/>(ровно один режим)"]
```
---
## 2. Таблица — это данные, а не код
Приоритет задан **порядком строк** таблицы-массива, а не значениями enum. Резолвер таблицу не
знает «в лицо» — читает булев сигнал по смещению поля (`offsetof`), что делает каждую строку
голыми данными:
```c
typedef struct { size_t flag_offset; sul_mode_t mode; } mode_rule_t;
static const mode_rule_t k_mode_priority[] = {
{offsetof(sul_result_t, fireman), SUL_MODE_FIREMAN}, // высший
{offsetof(sul_result_t, fire_alarm), SUL_MODE_FIRE_ALARM},
{offsetof(sul_result_t, overload), SUL_MODE_OVERLOAD},
{offsetof(sul_result_t, seismic), SUL_MODE_SEISMIC},
{offsetof(sul_result_t, maintenance), SUL_MODE_MAINTENANCE},
{offsetof(sul_result_t, lading), SUL_MODE_LADING}, // низший спецрежим
};
```
Согласованный порядок: **пожарный пожар перегруз сейсмо сервис погрузка**. Если ни одна
строка не сработала — `SUL_MODE_NORMAL` (обычная индикация этажа/стрелки).
> ⚠️ Числовые значения `sul_mode_t`**просто идентификаторы**, они не задают приоритет. Приоритет
> живёт только в порядке строк `k_mode_priority[]`.
---
## 3. Резолвер
Проход таблицы сверху вниз; первая строка с активным сигналом выигрывает.
```mermaid
flowchart TD
START([sul_resolve_mode]) --> I["i = 0"]
I --> CHK{"строка i:<br/>сигнал по offset активен?"}
CHK -->|да| RET["вернуть mode строки i"]
CHK -->|нет| NEXT["i++"]
NEXT --> MORE{"строки ещё есть?"}
MORE -->|да| CHK
MORE -->|нет| NORM["вернуть SUL_MODE_NORMAL"]
```
Резолвер вызывается из `controller_process()` дважды за кадр (для прошлого и нового состояния) —
их сравнение даёт `mode_pending`. Смена **сырого** сигнала, не меняющая разрешённый режим (напр.
добавился перегруз при уже активном пожаре), `mode_pending` не поднимает.
---
## 4. Как менять
Всё — правки одного массива, резолвер не трогается:
- **Изменить приоритет** — переставить строки.
- **Добавить режим** — дописать строку: `offsetof` нового булева сигнала + его `sul_mode_t`
(сам сигнал добавляется в `sul_result_t` и заполняется декодером).
- **Убрать режим** — удалить строку (сигнал в модели может остаться, просто не влияет на экран).
---
## 5. Развязка от протокола и клиентская кастомизация
Таблица оперирует **каноническими** сигналами `sul_result_t`, а не битами НКУ-CAN. Обзор боевых
декодеров (УИМ/УКЛ/УЭЛ/SD7) подтвердил: набор сигналов один на все протоколы — различаются лишь
битовые кодировки (живут в каждом декодере). Поэтому одна таблица работает для любого протокола.
Приоритеты «потенциально клиентские» (§7): клиентская кастомизация = подмена массива
`k_mode_priority[]` (в перспективе — загружаемая таблица рядом с layout/конфигом), **без правок
кода** резолвера или декодеров.
---
Куда встраивается результат: `mode` едет в `indication_task_t` и читается презентацией при
`mode_pending` — см. [DOMAIN_DATAFLOW.md §5](DOMAIN_DATAFLOW.md) и [FALLBACK.md](FALLBACK.md).

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
View file

@ -0,0 +1,33 @@
# tft-app — документация домена
Dev-документация по **реализованному** слою `firmware/tft_app` (Фазы 13.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-движок со спрайтами (Фазы 45, включая ярус C настроек — клиентский
TLV, см. SETTINGS.md §3/§9), аудио (Фаза 6), остальные протоколы — УИМ/SD7/УЭЛ/УКЛ (Фаза 8).

275
docs/tft_app/SETTINGS.md Normal file
View 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 ещё нет (Фазы 45).
**Почему 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, Фазы 45 |
Ядро сознательно маленькое и жёсткое (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-движка, Фазы 45).
- **`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
View 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
View 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 / УЭЛ, матрица
«протокол × дисплей».

View 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")

View 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

File diff suppressed because it is too large Load diff

View 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
View file

@ -0,0 +1,189 @@
Готово. Полная повторная верификация:
| Проверка | Результат |
| ---------------------- | ------------------------------------------------------ |
| Host-тесты | **19/19** зелёные |
| Debug сборка+подпись | чисто, `image_ok` UNSET |
| Release сборка+подпись | чисто, `image_ok` UNSET |
| Оба бинарника | 2 097 152 байт (ровно размер слота) |
| Аппаратно (ваш лог) | self-confirm пережил перезагрузку, CAN→рендер работает |
PLAN.md обновлён: Фаза 1 → ✅, известное ограничение (single-buffer) зафиксировано с честной формулировкой механизма, явно вписано в объём Фазы 4 вместе с PXP. Также отметил, что критерий «timeout→--» проверен только логически (host-тестом), не на реальном обрыве связи — не стал завышать степень подтверждённости.
Отчёт — [tft_app_phase_0_1_report.md](../../../private/tmp/claude-501/-Users-von-akimow-Desktop-TFT-ENV-tft-manufacture-test/93af6316-b6c7-4b2a-9063-169ca896d1ff/scratchpad/tft_app_phase_0_1_report.md). Полный текст ниже — можно скопировать целиком в новый тред:
---
```markdown
# tft_app — отчёт по Фазам 01
> Канонические источники — `firmware/tft_app/ARCH.md` (архитектура) и `firmware/tft_app/PLAN.md`
> (статус фаз, живой документ, обновляется каждой фазой). Этот файл — снимок на момент завершения
> Фазы 1, для быстрого ввода в контекст новой сессии. При расхождении — верить PLAN.md/ARCH.md, не
> этому файлу.
**Статус:** Фаза 0 ✅ и Фаза 1 ✅ — обе аппаратно подтверждены. Работа Фазы 1 **не закоммичена**
(в рабочем дереве поверх коммита `# tft_app: Phase 0`, `a9e6681`).
---
## Фаза 0 — Каркас (FreeRTOS + self-confirm слота MCUboot)
**Итог.** `firmware/tft_app` собирается как MCUboot-слот-образ (Direct-XIP), bootloader в него
прыгает, FreeRTOS стартует, образ подтверждает себя в рантайме (`boot_set_next`) и переживает
power cycle без отката — anti-brick сохранён. Подтверждено дважды на реальном железе (в т.ч.
через переустановку с microSD).
**Путь до рабочего состояния был не прямым — три независимых бага, все найдены и закрыты:**
1. **`bsp_qspi_init()` обязателен до любого `flash_area_*`.** Без него запись `image_ok` в трейлер
слота молча падала — `boot_set_next()` возвращал успех, но ничего не персистило.
2. **Корневая причина (самая коварная): забытый дефайн `__STARTUP_INITIALIZE_RAMFUNCTION`.**
Запись QSPI из app виснет в `qspi_write_fifo` — FlexSPI не дренирует IP TX FIFO. Причина:
startup копирует секцию `.ram_function` (код `bsp_qspi`, помеченный
`AT_QUICKACCESS_SECTION_CODE`) из flash в ITCM только под этим дефайном; `app` его не задавал
— в ITCM оставался leftover ramfunc **загрузчика** (тот исполнялся первым), поэтому чтения
«случайно работали», а запись — нет. Оба рабочих потребителя `bsp_qspi` (firmware_test,
bootloader) этот дефайн задают; `app` был первым, кто его пропустил. Диагностировано под
отладчиком (зависание конкретно в `qspi_write_fifo`), подтверждено изолированным
erase+write+readback self-test (частота мигания LED = индикатор результата) до восстановления
полной диагностики.
3. **UART, не USB CDC, для диагностики/логов.** USB CDC (target-side EHCI/PHY) под FreeRTOS не
заработал стабильно за несколько заходов — первый в репозитории FreeRTOS-потребитель target
USB device stack, класс неопределённости остался непроверенным. LPUART1/MCU-Link VCOM — самый
проверенный канал в репозитории (все `tests/target/*` HIL-образы), доступен сразу через уже
подключённый для SWD кабель, без enumeration/wait. Решение принято явно с пользователем.
**Побочные, но важные для будущих фаз находки:**
- Любая bsp-инициализация, зависящая от тика (`bsp_delay()` внутри `bsp_usb_cdc_init()` и т.п.),
нельзя звать **до** `vTaskStartScheduler()` — в FreeRTOS-режиме `bsp_tick` это
`xTaskGetTickCount()`, который не идёт до старта планировщика → вызов зависает навсегда молча.
`main()` в итоге — минимальный bare-metal (board bring-up + запуск планировщика), вся
инициализация — из задачи.
- Первый в репозитории потребитель `BSP_TICK_FREERTOS_MODE` вскрыл 2 латентных бага: (1) общий
`bsp_tick` — bare-metal STATIC-либа → `SysTick_Handler` конфликтует с FreeRTOS-портом (решение:
компилировать `tick.c` прямо в `app`, не через `libbsp_tick.a`); (2) `configTICK_RATE_HZ`
нельзя как `((TickType_t)1000)``#if` в `tick.c` не разбирает cast, только `1000`.
- `.vscode/launch.json`: у `🐛 Debug: tft_app` был `preLaunchTask` на `firmware_test`
(copy-paste) — исправлено на `build:app-debug`.
- Тонкий стек задачи (изначально x2=1 КБ) маскировался под тот же класс симптома, что и другие
баги (зависание/reset-loop) — увеличен с запасом (x4-x6, 2-3 КБ) везде, где заводятся новые
задачи.
**Осознанное отклонение от исходного плана:** `firmware/bootloader/test_stub` НЕ удалён (план
предполагал) — оставлен как независимая A/B-регрессия загрузчика, `app` пока не покрывает те же
сценарии (hang-классы, revert без self-confirm).
---
## Фаза 1 — Walking skeleton (CAN → декодер → контроллер → fallback-рендер)
**Итог.** Полный вертикальный срез: реальный CAN-кадр НКУ-CAN → чистый декодер → контроллер
(diff) → fallback-рендер (позиция + стрелка) на экране. Подтверждено на TFT8 с реальным
CAN-трафиком станции.
### Архитектура (по слоям, каждый — своя CMake-либа, конвенция `bsp/<module>`)
```
domain/elevator_model sul_result_t (pos: UTF-8 строка, direction: none/up/down/double),
sul_default_state()
domain/sul sul_frame_t, sul_driver_t, sul_status_t, реестр драйверов
(заголовок и реализация реестра — РАЗНЫЕ CMake-таргеты,
иначе цикл: декодеру нужны только типы sul.h, реестру — decode() декодера)
domain/sul/nku_can чистый декодер PACKET1 (направление) / PACKET3 (позиция) —
порт OLD_PROJECT msg_receiver_task, БЕЗ единого HAL-вызова
domain/sul/transport/can тонкий bsp_can → sul_frame_t адаптер (HW, не host-тестируется)
domain/controller process(sul_result_t) → indication_task_t (diff), кэш
services/gfx framebuffer (SDRAM non-cacheable) + RLE-декодер шрифтов
(lcd-image-converter формат) + примитив стрелки
ui/fallback рендер pos+стрелка, asset-free/FS-free (ARCH §11)
app задачи sul_rx (WDOG+CAN+decode+controller) и render,
связаны очередью глубины 1 (xQueueOverwrite — важно
только последнее состояние, не история)
```
**Host-тесты (все зелёные, домен полностью протестирован до касания железа):**
- `tests/host/tft_app_sul_nku` — 16 тестов, golden-векторы CAN-кадров (все direction, позиции,
легаси-квирк `left==0`≈пробел, кириллица, IGNORED/ERR на чужой ID/DLC/вне таблицы символов,
накопление состояния между PACKET1 и PACKET3).
- `tests/host/tft_app_controller` — 6 тестов (diff-логика, timeout→default тем же путём, что и
обычный кадр).
- Полный пакет: **19/19** (включая существующие тесты bootloader/bsp).
### Ключевые находки/решения по ходу
1. **Framebuffer в SDRAM обязан идти через `AT_NONCACHEABLE_SECTION_ALIGN`** (прямая рекомендация
NXP, порт паттерна из `OLD_PROJECT/source/display/image_cache.c`). MPU держит SDRAM как
`Normal Write-Back Cacheable` (`board_mpu_init()`, Region 8); ELCDIF читает framebuffer
напрямую по DMA — без non-cacheable региона CPU писал бы через кэш, DMA видел бы устаревшие
данные (мусор/частичные кадры на экране, выглядело бы как загадочный баг железа). Механизм уже
был параметризован в `board_mpu_init()` через линкер-символы `__NCACHE_REGION_START/SIZE`
(Region 9, унаследовано из bootloader) — переопределены в `app_slot.ld` на новый
`m_sdram_ncache` (2 МБ в начале SDRAM) вместо OCRAM (256 КБ, для framebuffer ~1.83 МБ не
влезло бы). На первой попытке framebuffer был сырым указателем на константный адрес (работало
случайно, линкер честно репортил `0 B` в секции) — исправлено на настоящее объявление
переменной через макрос.
2. **RLE-декодер шрифтов — порт проверенного в проде алгоритма** из `OLD_PROJECT/source/fonts/
fonts.c` (UNIQUE/REPEATABLE блоки, бинарный поиск по отсортированному коду символа), не
переизобретён с нуля. Формат — то, что реально экспортирует lcd-image-converter (`tImage`/
`tChar`/`tFont`, ARGB8888, "Color A8R8G8B8"). Пользователь сгенерировал `FloorFontFallback.c`
(Inter 215pt, символы 0-9 и "-") и `SystemFont.c` (JetBrains Mono 24pt, ASCII+кириллица)
инструментом напрямую.
3. **По-символьный fallback** (ARCH §11): символ вне таблицы шрифта → подстановка `'-'`
(согласованная политика — "-" общий заменитель во `FloorFontFallback`).
4. Сгенерированные шрифтовые файлы ожидают типы из bare-name `fonts.h` по-разному
(`SystemFont.c` — явный `#include`, `FloorFontFallback.c` — вообще без include, разные версии
конвертера) — закрыто через `-include`/`-I` в CMake; сами сгенерированные файлы не
редактировались (конвертер их перезапишет при регенерации).
5. Файлы шрифтов перенесены из `domain/` (где их положил пользователь) в `services/gfx/fonts/`
по ARCH §4 шрифты относятся к gfx, не к домену (домен — чистый C без рендеринга).
### ⚠️ Известное ограничение (зафиксировано, чинится в Фазе 4)
Один framebuffer, без double buffering — `ui_fallback_render()` пишет напрямую в буфер, который
ELCDIF в этот момент сканирует по DMA. Если перерисовка (`gfx_clear` + RLE-decode глифов +
стрелка) не укладывается в один кадр развёртки, видно сам процесс закраски (глиф «набирается» по
частям за несколько кадров) — **не tearing и не порча пикселей** (каждая отдельная запись всегда
валидна), просто заметен сам процесс. Подтверждено на железе пользователем ("рвано, но без
артефактов" — совпадает с механизмом).
**Фикс — double buffering**, НЕ требует PXP отдельно (это вопрос буферизации, не композиции):
второй framebuffer (`m_sdram_ncache` 2→4 МБ), swap через `bsp_display_set_next_buffer()` +
семафор на `FRAME_DONE` (сейчас `gfx_init()` передаёт `NULL` вместо колбэка). Явно вписано в
объём Фазы 4 (PLAN.md) вместе с PXP-компоновщиком (для многослойной композиции реальных
ассетов — фон+спрайты+текст, та же фаза, естественно смежная работа).
### Прочее
- Критерий выхода выполнен на TFT8: реальный CAN-трафик → корректный номер этажа и стрелка.
Таймаут→"--" логически проверен host-тестом, аппаратно на реальном обрыве связи отдельно не
гонялся (низкий риск — тот же код-путь, что у обычного кадра).
- Debug и Release оба собираются и подписываются чисто (2 097 152 байта — ровно размер слота,
`image_ok` UNSET в обоих — рантайм self-confirm, не sign-time `--confirm`).
---
## Состояние репозитория на конец Фазы 1
- Ветка: `tft-app-dev`.
- Коммит `a9e6681` (# tft_app: Phase 0) — последний закоммиченный чекпоинт.
- Вся Фаза 1 — в рабочем дереве, не закоммичена: `firmware/tft_app/src/{domain,services/gfx,
ui/fallback}/`, `firmware/tft_app/src/app/main.c` (переписан), `tests/host/tft_app_sul_nku/`,
`tests/host/tft_app_controller/`, изменения в `CMakePresets.json`,
`cmake/linker/MIMXRT1052xxxxx_app_slot.ld`, `firmware/tft_app/CMakeLists.txt`,
`tests/host/CMakeLists.txt`, `firmware/tft_app/PLAN.md`.
- Стоит рассмотреть коммит перед длительным перерывом/переключением контекста — на усмотрение
пользователя (не коммичу без явной просьбы).
## Дальше — Фаза 2
Полный контракт `sul_result_t` (next, сигналы, `lading_secs`, `floor_num`), полный декодер
НКУ-CAN (PACKET1..5, режимы, удалённая установка адреса), таблица приоритетов режимов
(данные, потенциально клиентские), валидация рендеримости позиции (renderable-множество шрифта +
по-символьный fallback — механизм уже есть в `gfx`, п.3 выше). Подробности — PLAN.md, раздел
«Фаза 2».
```
---
Одно практическое замечание, раз вы переключаетесь в новый тред: вся Фаза 1 сейчас **не закоммичена** — сидит в рабочем дереве поверх коммита Фазы 0. Хотите, чтобы я закоммитил перед переключением (стандартный `git add` + сообщение по Фазе 1), или оставляете как есть?

View file

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

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

View 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-установки */
}

View 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);
}
}
}

View 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 (;;)
{
}
}

View 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); /* одноразовая задача — дальше нечего делать */
}

View 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.13.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));
}
}

View 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;
}
}

View 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);
}
}

View file

@ -0,0 +1,3 @@
add_subdirectory(elevator_model)
add_subdirectory(sul)
add_subdirectory(controller)

View file

@ -0,0 +1,3 @@
# domain
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

View 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)

View file

@ -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 + timeoutdefault» это
* обязанность вызывающей задачи, не контроллера).
*/
#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 (falsetrue) сигнал к озвучке (гонг, старт).
*/
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_ */

View file

@ -0,0 +1,37 @@
/**
* @file mode_priority.h
* @brief Свёртка ортогональных сигналов sul_result_t в один экранный режим
* по таблице приоритетов (ARCH.md §7).
*
* Таблица ЧИСТЫЕ ДАННЫЕ (offset-массив в mode_priority.c), «потенциально
* клиентские» (§7): приоритет задаётся ПОРЯДКОМ строк, не значениями enum.
* Менять приоритет = переставить строки; добавить режим = дописать строку;
* резолвер при этом не трогается. Контроллер применяет активную таблицу.
*
* Чистый C, host-тестируется (tests/host/tft_app_controller).
*/
#ifndef DOMAIN_MODE_PRIORITY_H_
#define DOMAIN_MODE_PRIORITY_H_
#include "domain/elevator_model.h"
#ifdef __cplusplus
extern "C"
{
#endif
/**
* @brief Разрешить экранный режим по активной таблице приоритетов.
*
* Проходит таблицу сверху вниз, возвращает режим первой строки, чей булев
* сигнал в @p p_result выставлен. Если ни один спецрежим не активен
* SUL_MODE_NORMAL.
*/
sul_mode_t sul_resolve_mode(const sul_result_t *p_result);
#ifdef __cplusplus
}
#endif
#endif /* DOMAIN_MODE_PRIORITY_H_ */

View file

@ -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;
}

View file

@ -0,0 +1,52 @@
#include "domain/mode_priority.h"
#include <stddef.h>
/**
* @brief Строка таблицы приоритетов: булев сигнал (по смещению в sul_result_t)
* экранный режим.
*
* Смещение вместо указателя-на-член (в C его нет) читаем bool по
* offsetof, что делает таблицу голыми данными без кода на строку.
*/
typedef struct
{
size_t flag_offset; /**< offsetof(sul_result_t, <bool-поле>) */
sul_mode_t mode;
} mode_rule_t;
/**
* ТАБЛИЦА ПРИОРИТЕТОВ (ARCH §7). Порядок = приоритет, сверху вниз.
* Согласовано: fireman (высший) > пожар > перегруз > сейсмо > сервис >
* погрузка > (норма когда ни одна строка не сработала).
*
* Изменить приоритет переставить строки. Добавить режим дописать строку
* (offset нового bool-сигнала + его sul_mode_t). Резолвер ниже не меняется.
* Клиентская кастомизация (§7) подмена этого массива, без правок кода.
*/
static const mode_rule_t k_mode_priority[] = {
{ offsetof(sul_result_t, fireman), SUL_MODE_FIREMAN },
{ offsetof(sul_result_t, fire_alarm), SUL_MODE_FIRE_ALARM },
{ offsetof(sul_result_t, overload), SUL_MODE_OVERLOAD },
{ offsetof(sul_result_t, seismic), SUL_MODE_SEISMIC },
{ offsetof(sul_result_t, maintenance), SUL_MODE_MAINTENANCE },
{ offsetof(sul_result_t, lading), SUL_MODE_LADING },
};
#define MODE_PRIORITY_COUNT (sizeof(k_mode_priority) / sizeof(k_mode_priority[0]))
sul_mode_t sul_resolve_mode(const sul_result_t *p_result)
{
const uint8_t *p_base = (const uint8_t *) p_result;
for (size_t i = 0U; i < MODE_PRIORITY_COUNT; ++i)
{
const bool ACTIVE = *(const bool *) (p_base + k_mode_priority[i].flag_offset);
if (ACTIVE)
{
return k_mode_priority[i].mode;
}
}
return SUL_MODE_NORMAL;
}

View file

@ -0,0 +1,3 @@
add_library(tft_app_elevator_model STATIC src/elevator_model.c)
target_include_directories(tft_app_elevator_model PUBLIC include/)

View file

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

View file

@ -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};
}

View 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)

View 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)

View file

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

View 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;
}

View 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 }, /* нижний этаж, затем цикл сначала */
};

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

View 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` без зависимости domainmenu (§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_ */

View 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)

View file

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

View 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;
}

View 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;
}

View file

@ -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)

View file

@ -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 с последовательным receivedecode). Не сохранять 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_ */

View file

@ -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;
}

View file

@ -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)

View file

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

View file

@ -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;
}

View 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)

View file

@ -0,0 +1,3 @@
# menu
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

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

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

View 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;
}
}

View 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;
}

View file

@ -0,0 +1,3 @@
add_subdirectory(gfx)
add_subdirectory(partition)
add_subdirectory(settings_store)

View file

@ -0,0 +1,3 @@
# services
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

View 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)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View 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>!"#$%&amp;'()*+,-./0123456789:;&lt;=>?\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="&quot;">
<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="&amp;">
<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="&lt;">
<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>

File diff suppressed because it is too large Load diff

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

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

View 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);
}
}

View file

@ -0,0 +1,3 @@
# Заголовочный модуль карты разделов QSPI (§10) — только константы, без кода.
add_library(tft_app_partition INTERFACE)
target_include_directories(tft_app_partition INTERFACE include/)

View file

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

View 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 пишут конкурентно

View file

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

View file

@ -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;
}

View file

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

View file

@ -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;
}

View file

@ -0,0 +1,2 @@
add_subdirectory(fallback)
add_subdirectory(menu)

View file

@ -0,0 +1,3 @@
# ui
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

View 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)

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

View 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);
}

View 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) # примитивы + шрифты

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

View 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);
}

View file

@ -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)')]

View file

@ -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')]

View file

@ -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)

View 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
View 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);
}

View file

@ -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

View file

@ -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)

View file

@ -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