diff --git a/docs/tft_app/ADDING_SETTING.md b/docs/tft_app/ADDING_SETTING.md
index 3a340eb..ecb7d2b 100644
--- a/docs/tft_app/ADDING_SETTING.md
+++ b/docs/tft_app/ADDING_SETTING.md
@@ -12,7 +12,7 @@
```mermaid
flowchart TD
- START([Новая настройка]) --> Q{Имеет смысл ТОЛЬКО
для одного протокола?
(напр. адрес станции)}
+ START([Новая настройка]) --> Q{Имеет смысл ТОЛЬКО
для одного протокола?
}
Q -->|да| PROTO["Протокольная
→ proto_slice + sul_settings_desc_t
см. §5"]
Q -->|нет| USER["Пользовательская/device
→ своё поле в settings_t
см. §4"]
```
@@ -168,7 +168,7 @@ log_set_enabled(g_menu.settings->device.log_enabled != 0U);
---
-## 7. Пример целиком — тумблер логов (эталон пользовательской BOOL-настройки)
+## 7. Пример — тумблер логов (эталон пользовательской BOOL-настройки)
| Шаг | Файл | Что именно |
| --- | --- | --- |
diff --git a/firmware/tft_app/src/menu/src/menu_tree.c b/firmware/tft_app/src/menu/src/menu_tree.c
index c053161..3795de0 100644
--- a/firmware/tft_app/src/menu/src/menu_tree.c
+++ b/firmware/tft_app/src/menu/src/menu_tree.c
@@ -8,11 +8,14 @@
/* Метки значений (ярус 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 *s_proto_labels[MENU_TREE_MAX_PROTOCOLS] = { "НКУ-CAN" }; /* фолбэк до refresh() */
+static const char *g_s_proto_labels[MENU_TREE_MAX_PROTOCOLS] = {
+ "НКУ-CAN"
+}; /* фолбэк до refresh() */
/* Индексы пунктов дерева. */
enum
@@ -22,7 +25,9 @@ enum
T_PROTO_PARAM, /* единственный параметр АКТИВНОГО протокола (§8) — популируется
* из sul_settings_desc_t, см. menu_tree_refresh_protocol_section() */
T_LOG,
+ T_DUMMY,
T_EXIT,
+
T_COUNT,
};
@@ -35,32 +40,39 @@ enum
* протокола, не только к его параметрам. Значения ниже — safe-фолбэк на
* случай, если refresh() почему-то не вызван (совпадает с тем, что было
* до Фазы 3.3, когда протокол был всего один). */
-static menu_item_desc_t s_tree[T_COUNT] = {
+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 },
+ .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 = s_proto_labels },
+ .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 },
+ .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 },
+ .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 },
};
@@ -86,9 +98,9 @@ void menu_tree_refresh_protocol_section(settings_t *p_settings_rw)
for (uint8_t i = 0U; i < VISIBLE; i++)
{
const sul_driver_t *p_drv = sul_registry_find(i);
- s_proto_labels[i] = (p_drv != NULL) ? p_drv->p_name : "?";
+ g_s_proto_labels[i] = (p_drv != NULL) ? p_drv->p_name : "?";
}
- s_tree[T_PROTO].max = (uint8_t) (VISIBLE - 1U);
+ g_s_tree[T_PROTO].max = (uint8_t) (VISIBLE - 1U);
/* Единственный параметр активного протокола (§8). Сейчас у каждого
* зарегистрированного протокола ровно один (НКУ-CAN: адрес; демо:
@@ -101,19 +113,19 @@ void menu_tree_refresh_protocol_section(settings_t *p_settings_rw)
{
const sul_settings_entry_t *p_entry = &p_settings->p_entries[0];
- s_tree[T_PROTO_PARAM].label = p_entry->p_label;
- s_tree[T_PROTO_PARAM].type = menu_type_from_sul(p_entry->type);
- s_tree[T_PROTO_PARAM].value_offset =
+ 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);
- s_tree[T_PROTO_PARAM].min = p_entry->min;
- s_tree[T_PROTO_PARAM].max = p_entry->max;
- s_tree[T_PROTO_PARAM].options = p_entry->p_options;
+ 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 + s_tree[T_PROTO_PARAM].value_offset;
+ 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;
@@ -123,18 +135,18 @@ void menu_tree_refresh_protocol_section(settings_t *p_settings_rw)
{
/* Протокол без параметров — инертный дефолт (не встречается пока
* ни у одного зарегистрированного протокола). */
- s_tree[T_PROTO_PARAM].label = "—";
- s_tree[T_PROTO_PARAM].type = MENU_BYTE;
- s_tree[T_PROTO_PARAM].value_offset = offsetof(settings_t, user.proto_slice[0]);
- s_tree[T_PROTO_PARAM].min = 0U;
- s_tree[T_PROTO_PARAM].max = 0U;
- s_tree[T_PROTO_PARAM].options = NULL;
+ 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 s_tree;
+ return g_s_tree;
}
uint8_t menu_tree_count(void)
diff --git a/firmware/tft_app/src/services/settings_store/include/services/settings_store.h b/firmware/tft_app/src/services/settings_store/include/services/settings_store.h
index 201056e..4485bda 100644
--- a/firmware/tft_app/src/services/settings_store/include/services/settings_store.h
+++ b/firmware/tft_app/src/services/settings_store/include/services/settings_store.h
@@ -29,60 +29,60 @@ 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 */
+#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
+ {
+ 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'-терминирован */
-
- /* Ярус B — протокольные: активный протокол трактует slice через
+ 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;
+ 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;
+ typedef struct
+ {
+ settings_device_t device;
+ settings_user_t user;
+ uint8_t _reserved[SETTINGS_CORE_RESERVED]; /**< размер фикс — offset'ы полей стабильны */
+ } settings_t;
-/* ── API ─────────────────────────────────────────────────────────────────── */
+ /* ── API ─────────────────────────────────────────────────────────────────── */
-/**
+ /**
* @brief Инициализировать активные настройки значениями по умолчанию (без QSPI).
*
* Вызывать, когда QSPI недоступен (или до settings_store_load()) — get() после
* этого всегда валиден. settings_store_load() при валидном флеше перезапишет.
*/
-void settings_store_init_defaults(void);
+ void settings_store_init_defaults(void);
-/**
+ /**
* @brief Загрузить настройки из QSPI (сектор §10).
*
* Предусловие: bsp_qspi_init() уже вызван. Читает сектор, проверяет
@@ -93,9 +93,9 @@ void settings_store_init_defaults(void);
* @retval BSP_ERR_INVALID magic/version/CRC не совпал — загружены дефолты.
* @retval BSP_ERR_HW ошибка чтения флеша — загружены дефолты.
*/
-bsp_status_t settings_store_load(void);
+ bsp_status_t settings_store_load(void);
-/**
+ /**
* @brief Сохранить текущие настройки на QSPI.
*
* Стирает рабочий сектор, пересчитывает CRC32, пишет постранично. Вызывать по
@@ -103,16 +103,16 @@ bsp_status_t settings_store_load(void);
*
* @retval BSP_OK / BSP_ERR_HW.
*/
-bsp_status_t settings_store_save(void);
+ bsp_status_t settings_store_save(void);
-/** @brief Указатель на активные настройки (только чтение). Валиден после init/load. */
-const settings_t *settings_store_get(void);
+ /** @brief Указатель на активные настройки (только чтение). Валиден после init/load. */
+ const settings_t *settings_store_get(void);
-/** @brief Указатель на активные настройки для изменения (меню). save() — отдельно. */
-settings_t *settings_store_get_mutable(void);
+ /** @brief Указатель на активные настройки для изменения (меню). save() — отдельно. */
+ settings_t *settings_store_get_mutable(void);
-/** @brief Сбросить пользовательский ярус к дефолтам (device не трогает). save() — отдельно. */
-void settings_store_reset_user_defaults(void);
+ /** @brief Сбросить пользовательский ярус к дефолтам (device не трогает). save() — отдельно. */
+ void settings_store_reset_user_defaults(void);
#ifdef __cplusplus
}
diff --git a/firmware/tft_app/src/services/settings_store/src/settings_codec.c b/firmware/tft_app/src/services/settings_store/src/settings_codec.c
index 915b4df..c1338f9 100644
--- a/firmware/tft_app/src/services/settings_store/src/settings_codec.c
+++ b/firmware/tft_app/src/services/settings_store/src/settings_codec.c
@@ -27,6 +27,7 @@ static const settings_t K_DEFAULTS = {
.music_volume_idx = 1U,
.year_production = 0U, /* скрыть */
.serial = "",
+ .dummy_option = 0U, // Моя опция
.proto_slice = {0}, /* НКУ: адрес 0 */
},
};
@@ -70,7 +71,8 @@ bool settings_deserialize(const settings_page_t *p_page, settings_t *p_out)
return false;
}
- const uint32_t CRC = settings_crc32((const uint8_t *) p_page, sizeof(*p_page) - sizeof(uint32_t));
+ const uint32_t CRC =
+ settings_crc32((const uint8_t *) p_page, sizeof(*p_page) - sizeof(uint32_t));
if (CRC != p_page->crc32)
{
return false;
diff --git a/tests/host/tft_app_menu/test_menu_tree.c b/tests/host/tft_app_menu/test_menu_tree.c
index a43831d..8e12310 100644
--- a/tests/host/tft_app_menu/test_menu_tree.c
+++ b/tests/host/tft_app_menu/test_menu_tree.c
@@ -13,8 +13,12 @@
#include
#include
-void setUp(void) {}
-void tearDown(void) {}
+void setUp(void)
+{
+}
+void tearDown(void)
+{
+}
/* T_PROTO — первый ребёнок корня (гарантия menu.h: items[MENU_ROOT_INDEX] —
* корневой SUBMENU). T_PROTO_PARAM — следующий по порядку в дереве
@@ -46,7 +50,7 @@ static void test_protocol_section_built_from_nku_can_descriptor(void)
TEST_ASSERT_EQUAL_UINT8(0U, p_items[T_PROTO_PARAM].min);
TEST_ASSERT_EQUAL_UINT8(15U, p_items[T_PROTO_PARAM].max);
TEST_ASSERT_EQUAL_UINT16((uint16_t) offsetof(settings_t, user.proto_slice[0]),
- p_items[T_PROTO_PARAM].value_offset);
+ p_items[T_PROTO_PARAM].value_offset);
}
static void test_protocol_section_switches_to_demo_descriptor(void)
@@ -111,6 +115,33 @@ static void test_protocol_param_edits_correct_settings_field(void)
TEST_ASSERT_EQUAL_UINT8(1U, s.user.proto_slice[0]);
}
+/* Конец-в-конец: правка пункта, построенного из дескриптора, действительно
+ * попадает в то самое поле settings_t, которое назвал дескриптор протокола —
+ * не только структура данных совпадает, но и реальный edit через menu.c. */
+static void test_dummy_param_edits_correct_settings_field(void)
+{
+ settings_t s;
+ memset(&s, 0, sizeof(s));
+
+ sul_registry_set_active(SUL_PROTOCOL_NKU_CAN);
+ menu_tree_refresh_protocol_section(&s);
+
+ menu_ctx_t ctx;
+ menu_init(&ctx, menu_tree_items(), menu_tree_count(), &s);
+ menu_open(&ctx);
+ menu_next(&ctx); /* T_PROTO -> T_PROTO_PARAM */
+ menu_next(&ctx); /* T_PROTO_PARAM -> T_LOG */
+ menu_next(&ctx); /* T_LOG -> T_DUMMY */
+
+ menu_action(&ctx); /* инкремент значения 0 -> 1 */
+
+ TEST_ASSERT_EQUAL_UINT8(1U, s.user.dummy_option);
+
+ menu_action(&ctx); /* инкремент значения 1 -> 0 */
+
+ TEST_ASSERT_EQUAL_UINT8(0U, s.user.dummy_option);
+}
+
int main(void)
{
UNITY_BEGIN();
@@ -119,6 +150,7 @@ int main(void)
RUN_TEST(test_protocol_section_switches_to_demo_descriptor);
RUN_TEST(test_stale_value_clamped_on_protocol_switch);
RUN_TEST(test_protocol_param_edits_correct_settings_field);
+ RUN_TEST(test_dummy_param_edits_correct_settings_field);
return UNITY_END();
}