This commit is contained in:
aZholtikov committed 2026-10-27 12:07:05 +03:00
1 parent ecb64fa256
commit 1f319b33f8
10 files changed
+4608

No files matched your search

+1
View File
@@ -0,0 +1 @@
idf_component_register(SRCS "zh_sim_race_button_box.c" INCLUDE_DIRS "" REQUIRES zh_160x_i2c zh_encoder zh_ota_server zh_menu esp_wifi nvs_flash)
+16
View File
@@ -0,0 +1,16 @@
description: In progress
url: https://github.com/aZholtikov/zh_sim_race_button_box
version: "1.0.0"
license: "Apache-2.0"
maintainers:
- "Alexey Zholtikov <alexey.zholtikov@yandex.ru>"
dependencies:
idf: ">=5.0"
zh_160x_i2c:
git: "https://github.com/aZholtikov/zh_160x_i2c.git"
zh_ota_server:
git: "https://github.com/aZholtikov/zh_ota_server.git"
zh_encoder:
git: "https://github.com/aZholtikov/zh_encoder.git"
zh_menu:
git: "https://github.com/aZholtikov/zh_menu.git"
+682
View File
@@ -0,0 +1,682 @@
#include "zh_sim_race_button_box.h"
#include "freertos/semphr.h"
#include "nvs.h" /* NEW */
#define ENCODER_NUMBER 0x01
#define LCD_VIEWPORT_ROWS 3
#define LCD_COLS 16
#define LCD_TOTAL_ROWS 4
#define BOOT_SPLASH_MS 5000
#define MAIN_SCREEN_COUNT 5
#define LOAD_SCREEN_COUNT 5
#define NVS_NAMESPACE "sim_race_box" /* NEW */
#define NVS_KEY_SETTINGS "settings" /* NEW */
static const char *TAG = "sim_race_box";
/* ============================================================
* Данные main-экранов (idle)
* ============================================================ */
int16_t main_race_data[VIEW_CELLS] = {185, 7500, 4, 234, 0};
int16_t main_engine_data[VIEW_CELLS] = {78, 82, 12, 45, 0};
int16_t main_times_data[VIEW_CELLS] = {1234, 1201, 1250, -49, 0};
int16_t main_debug_data[VIEW_CELLS] = {45, -42, 152, 12, 0};
int16_t main_blank_data[VIEW_CELLS] = {0, 0, 0, 0, 0};
/* ---------- Дескрипторы экранов ---------- */
typedef struct
{
const char *name;
const char *const labels[VIEW_ROWS];
int16_t *data;
} main_screen_desc_t;
typedef struct
{
const char *name;
const char *const lines[VIEW_ROWS];
} load_screen_desc_t;
static const main_screen_desc_t main_screens[MAIN_SCREEN_COUNT] = {
{"Race", {"SPD", "RPM", "GER", "LAP"}, main_race_data},
{"Engine", {"OIL", "WTR", "BAT", "FUL"}, main_engine_data},
{"Times", {"LAP", "BST", "LST", "DLT"}, main_times_data},
{"Debug", {"HEP", "SIG", "UPT", "CPU"}, main_debug_data},
{"Blank", {"---", "---", "---", "---"}, main_blank_data},
};
static const load_screen_desc_t load_screens[LOAD_SCREEN_COUNT] = {
{"Boot", {"Booting...", "RAM: OK", "I2C: OK", "Starting..."}},
{"Version", {"FW v1.0", "Build 2025-10", "ESP-IDF v5.4", "zh_menu v1.2"}},
{"Diag", {"I2C: OK", "LCD: OK", "ENC: OK", "All: OK"}},
{"Ready", {"System ready", "Ready for race", "Let's go", ""}},
{"Blank", {"", "", "", ""}},
};
/* ---------- Пользовательский выбор ---------- */
static int _boot_screen = 0;
static int _main_screen = 0;
static bool _boot_done = false;
static bool _prev_selected = false; /* NEW: для детекции выхода из edit */
/* ---------- Динамические буферы меток ---------- */
char lbl_boot_screen[MENU_LABEL_LEN] = "B:Boot";
char lbl_main_screen[MENU_LABEL_LEN] = "M:Race";
char lbl_temp_on[MENU_LABEL_LEN] = "Temp ON 15C";
char lbl_temp_off[MENU_LABEL_LEN] = "Temp OFF 35C";
char lbl_pw_ch_1[MENU_LABEL_LEN] = "PW CH 1 100%";
char lbl_pw_ch_2[MENU_LABEL_LEN] = "PW CH 2 100%";
char lbl_pw_ch_3[MENU_LABEL_LEN] = "PW CH 3 100%";
char lbl_pw_ch_4[MENU_LABEL_LEN] = "PW CH 4 100%";
int16_t g_temp_on = 15;
int16_t g_temp_off = 35;
uint8_t g_pw_ch[4] = {100, 100, 100, 100};
/* ---------- Handle'ы ---------- */
zh_encoder_handle_t *encoder_handle = NULL;
zh_menu_handle_t *menu_handle = NULL;
zh_pcf8574_handle_t *pcf8574_handle = NULL;
zh_160x_i2c_handle_t *lcd_handle = NULL;
/* ---------- Состояние меню ---------- */
static zh_menu_node_t *_last_node = NULL;
static uint8_t _last_index = 0;
static zh_menu_node_t *_cached_node = NULL;
/* ---------- Кэш строк LCD ---------- */
static char _row_cache[LCD_TOTAL_ROWS][LCD_COLS];
static bool _row_valid[LCD_TOTAL_ROWS] = {false, false, false, false};
/* ---------- Мьютекс LCD ---------- */
static SemaphoreHandle_t _lcd_mutex = NULL;
void zh_encoder_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data);
/* ============================================================
* NEW: NVS blob — все сохраняемые поля в одной структуре
* ============================================================ */
typedef struct
{
int16_t temp_on;
int16_t temp_off;
uint8_t pw_ch[4];
uint8_t boot_screen;
uint8_t main_screen;
} settings_blob_t;
/* ============================================================
* NEW: загрузка настроек из NVS.
* Безопасно к отсутствию записи, к несовпадению размера blob
* и к устаревшим индексам экранов (выходящим за границы).
* ============================================================ */
static void _load_settings(void)
{
nvs_handle_t nvs;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "No saved settings (%s), using defaults", esp_err_to_name(err));
return;
}
settings_blob_t blob;
size_t size = sizeof(blob);
err = nvs_get_blob(nvs, NVS_KEY_SETTINGS, &blob, &size);
nvs_close(nvs);
if (err != ESP_OK || size != sizeof(blob))
{
ESP_LOGW(TAG, "Settings blob missing or size mismatch (%s)", esp_err_to_name(err));
return;
}
/* Защита от устаревших индексов после обновления прошивки */
if (blob.temp_on >= 0 && blob.temp_on <= 100)
g_temp_on = blob.temp_on;
if (blob.temp_off >= 0 && blob.temp_off <= 100)
g_temp_off = blob.temp_off;
memcpy(g_pw_ch, blob.pw_ch, sizeof(g_pw_ch));
if (blob.boot_screen < LOAD_SCREEN_COUNT)
_boot_screen = blob.boot_screen;
if (blob.main_screen < MAIN_SCREEN_COUNT)
_main_screen = blob.main_screen;
ESP_LOGI(TAG, "Settings loaded: temp_on=%d temp_off=%d pw=[%u,%u,%u,%u] boot=%d main=%d",
g_temp_on, g_temp_off, g_pw_ch[0], g_pw_ch[1], g_pw_ch[2], g_pw_ch[3],
_boot_screen, _main_screen);
}
/* ============================================================
* NEW: сохранение настроек в NVS (единый blob, атомарно).
* ============================================================ */
static void _save_settings(void)
{
nvs_handle_t nvs;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_open RW failed: %s", esp_err_to_name(err));
return;
}
settings_blob_t blob = {
.temp_on = g_temp_on,
.temp_off = g_temp_off,
.boot_screen = (uint8_t)_boot_screen,
.main_screen = (uint8_t)_main_screen,
};
memcpy(blob.pw_ch, g_pw_ch, sizeof(g_pw_ch));
err = nvs_set_blob(nvs, NVS_KEY_SETTINGS, &blob, sizeof(blob));
if (err == ESP_OK)
err = nvs_commit(nvs);
nvs_close(nvs);
if (err != ESP_OK)
ESP_LOGE(TAG, "Failed to save settings: %s", esp_err_to_name(err));
else
ESP_LOGI(TAG, "Settings saved");
}
/* ---------- Утилиты ---------- */
static inline int16_t _clamp_i16(int16_t v, int16_t lo, int16_t hi)
{
return v < lo ? lo : (v > hi ? hi : v);
}
static inline uint8_t _clamp_u8(int16_t v, uint8_t lo, uint8_t hi)
{
return v < lo ? lo : (v > hi ? hi : v);
}
static void _invalidate_row_cache(void)
{
memset(_row_valid, 0, sizeof(_row_valid));
}
/* ---------- Форматирование ---------- */
static void _format_row(char *buf, const char *label, bool selected, bool edit,
bool arrow_up, bool arrow_down)
{
memset(buf, ' ', LCD_COLS);
buf[LCD_COLS] = '\0';
buf[0] = edit ? '*' : (selected ? '>' : ' ');
uint8_t text_cols = (arrow_up || arrow_down) ? (LCD_COLS - 2) : (LCD_COLS - 1);
size_t len = strlen(label);
if (len > text_cols)
len = text_cols;
memcpy(&buf[1], label, len);
if (arrow_up)
{
buf[LCD_COLS - 1] = '^';
}
else if (arrow_down)
{
buf[LCD_COLS - 1] = 'v';
}
}
static void _format_main_row(char *buf, const char *label, int16_t value)
{
memset(buf, ' ', LCD_COLS);
buf[LCD_COLS] = '\0';
snprintf(buf, LCD_COLS + 1, "%3s:%5d", label, (int)value);
}
static void _format_load_row(char *buf, const char *line)
{
memset(buf, ' ', LCD_COLS);
buf[LCD_COLS] = '\0';
size_t len = strlen(line);
if (len > LCD_COLS)
len = LCD_COLS;
memcpy(buf, line, len);
}
static void _lcd_put_row(uint8_t row, const char *text)
{
if (_row_valid[row] && memcmp(_row_cache[row], text, LCD_COLS) == 0)
return;
zh_160x_set_cursor(&lcd_handle, row, 0);
zh_160x_print_char(&lcd_handle, text);
memcpy(_row_cache[row], text, LCD_COLS);
_row_valid[row] = true;
}
/* ---------- Точечное обновление меток ---------- */
static void _refresh_dynamic_labels(zh_menu_node_t *node)
{
if (node == NULL || node->submenu == NULL)
return;
for (uint8_t i = 0; i < node->item_numbers; ++i)
{
const char *lbl = node->submenu[i].label;
if (lbl == lbl_temp_on)
{
snprintf(lbl_temp_on, sizeof(lbl_temp_on), "Temp ON %2dC", (int)g_temp_on);
}
else if (lbl == lbl_temp_off)
{
snprintf(lbl_temp_off, sizeof(lbl_temp_off), "Temp OFF %2dC", (int)g_temp_off);
}
else if (lbl == lbl_pw_ch_1)
{
snprintf(lbl_pw_ch_1, sizeof(lbl_pw_ch_1), "PW CH 1 %3d%%", (int)g_pw_ch[0]);
}
else if (lbl == lbl_pw_ch_2)
{
snprintf(lbl_pw_ch_2, sizeof(lbl_pw_ch_2), "PW CH 2 %3d%%", (int)g_pw_ch[1]);
}
else if (lbl == lbl_pw_ch_3)
{
snprintf(lbl_pw_ch_3, sizeof(lbl_pw_ch_3), "PW CH 3 %3d%%", (int)g_pw_ch[2]);
}
else if (lbl == lbl_pw_ch_4)
{
snprintf(lbl_pw_ch_4, sizeof(lbl_pw_ch_4), "PW CH 4 %3d%%", (int)g_pw_ch[3]);
}
else if (lbl == lbl_boot_screen)
{
snprintf(lbl_boot_screen, sizeof(lbl_boot_screen), "B:%s", load_screens[_boot_screen].name);
}
else if (lbl == lbl_main_screen)
{
snprintf(lbl_main_screen, sizeof(lbl_main_screen), "M:%s", main_screens[_main_screen].name);
}
}
}
/* ---------- Отрисовка экранов ---------- */
static void _draw_main_screen(void)
{
const main_screen_desc_t *s = &main_screens[_main_screen];
for (uint8_t i = 0; i < VIEW_ROWS; ++i)
{
char buf[LCD_COLS + 1];
_format_main_row(buf, s->labels[i], s->data[i]);
_lcd_put_row(i, buf);
}
}
static void _draw_load_screen(void)
{
const load_screen_desc_t *s = &load_screens[_boot_screen];
for (uint8_t i = 0; i < VIEW_ROWS; ++i)
{
char buf[LCD_COLS + 1];
_format_load_row(buf, s->lines[i]);
_lcd_put_row(i, buf);
}
}
/* ---------- Отрисовка меню ---------- */
static void _draw_menu(zh_menu_node_t *current_node, uint8_t current_index,
const zh_menu_status_t *status)
{
if (current_node != _cached_node)
{
_invalidate_row_cache();
_cached_node = current_node;
}
_refresh_dynamic_labels(current_node);
zh_menu_view_t view = {0};
zh_menu_get_view(&menu_handle, &view);
char header[LCD_COLS + 1];
memset(header, ' ', LCD_COLS);
header[LCD_COLS] = '\0';
size_t hlen = strlen(current_node->label);
if (hlen > LCD_COLS)
hlen = LCD_COLS;
memcpy(header, current_node->label, hlen);
_lcd_put_row(0, header);
for (uint8_t i = 0; i < LCD_VIEWPORT_ROWS; ++i)
{
char buf[LCD_COLS + 1];
if (i < view.visible_count)
{
uint8_t idx = view.first_visible + i;
bool is_cursor = (idx == current_index);
bool is_edit = is_cursor && status->is_selected &&
current_node->submenu[idx].selectable;
bool arrow_up = (i == 0) && (view.first_visible > 0);
bool arrow_down = (i == (view.visible_count - 1)) &&
((uint8_t)(view.first_visible + view.visible_count) < current_node->item_numbers);
_format_row(buf, current_node->submenu[idx].label,
is_cursor, is_edit, arrow_up, arrow_down);
}
else
{
memset(buf, ' ', LCD_COLS);
buf[LCD_COLS] = '\0';
}
_lcd_put_row((uint8_t)(i + 1), buf);
}
}
/* ---------- Boot: 5 сек splash → main ---------- */
static void _boot_task(void *pv)
{
(void)pv;
vTaskDelay(pdMS_TO_TICKS(BOOT_SPLASH_MS));
_boot_done = true;
zh_menu_status_t s = {0};
zh_menu_get(&menu_handle, &s);
if (!s.is_active)
{
xSemaphoreTake(_lcd_mutex, portMAX_DELAY);
zh_160x_lcd_clear(&lcd_handle); /* NEW: очистка перед первым main view */
_invalidate_row_cache();
_draw_main_screen();
xSemaphoreGive(_lcd_mutex);
}
vTaskDelete(NULL);
}
/* ---------- app_main ---------- */
void app_main(void)
{
esp_log_level_set("zh_menu", ESP_LOG_ERROR);
esp_log_level_set("zh_encoder", ESP_LOG_ERROR);
esp_log_level_set("zh_160x_i2c", ESP_LOG_ERROR);
esp_log_level_set("zh_pcf8574", ESP_LOG_ERROR);
esp_log_level_set("zh_vector", ESP_LOG_ERROR);
/* ---------- NEW: NVS init + загрузка настроек ---------- */
esp_err_t nvs_err = nvs_flash_init();
if (nvs_err == ESP_ERR_NVS_NO_FREE_PAGES || nvs_err == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
nvs_err = nvs_flash_init();
}
ESP_ERROR_CHECK(nvs_err);
_load_settings(); /* до отрисовки splash и до чтения _boot_screen */
esp_event_loop_create_default();
esp_event_handler_instance_register(ZH_ENCODER, ESP_EVENT_ANY_ID,
&zh_encoder_event_handler, NULL, NULL);
/* ---------- Encoder ---------- */
zh_encoder_init_config_t encoder_config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
encoder_config.task_priority = 5;
encoder_config.stack_size = configMINIMAL_STACK_SIZE;
encoder_config.queue_size = 5;
encoder_config.a_gpio_number = GPIO_NUM_4;
encoder_config.b_gpio_number = GPIO_NUM_16;
encoder_config.s_gpio_number = GPIO_NUM_15;
encoder_config.encoder_min_value = -1000000;
encoder_config.encoder_max_value = 1000000;
encoder_config.encoder_step = 1;
encoder_config.encoder_number = ENCODER_NUMBER;
zh_encoder_init(&encoder_config, &encoder_handle);
/* ---------- I2C + PCF8574 + LCD ---------- */
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_NUM_MAX - 1,
.scl_io_num = GPIO_NUM_26,
.sda_io_num = GPIO_NUM_25,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t i2c_bus_handle = NULL;
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle));
zh_pcf8574_init_config_t pcf_config = ZH_PCF8574_INIT_CONFIG_DEFAULT();
pcf_config.i2c_handle = i2c_bus_handle;
pcf_config.i2c_address = 0x27;
ESP_ERROR_CHECK(zh_pcf8574_init(&pcf_config, &pcf8574_handle));
ESP_ERROR_CHECK(zh_160x_init(&pcf8574_handle, &lcd_handle, ZH_LCD_16X4));
zh_160x_lcd_clear(&lcd_handle);
zh_160x_off_cursor(&lcd_handle);
_lcd_mutex = xSemaphoreCreateMutex();
configASSERT(_lcd_mutex != NULL);
/* ---------- Menu ---------- */
zh_menu_init_config_t menu_config = ZH_MENU_INIT_CONFIG_DEFAULT();
menu_config.task_priority = 5;
menu_config.stack_size = 4096;
menu_config.queue_size = 5;
menu_config.root = &main_menu;
menu_config.loop = false;
menu_config.viewport_rows = LCD_VIEWPORT_ROWS;
menu_config.enter_cb = &zh_menu_enter_main_menu_callback;
menu_config.exit_cb = &zh_menu_exit_main_menu_callback;
menu_config.change_cb = &zh_menu_change_menu_callback;
zh_menu_init(&menu_config, &menu_handle);
/* ---------- Splash: сохранённый load-экран ---------- */
_boot_done = false;
_prev_selected = false;
_invalidate_row_cache();
_draw_load_screen();
xTaskCreate(_boot_task, "boot", 3072, NULL, 3, NULL);
}
/* ---------- Encoder event handler ---------- */
void zh_encoder_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
(void)arg;
zh_menu_status_t menu_status = {0};
zh_menu_get(&menu_handle, &menu_status);
switch (event_id)
{
case ZH_BUTTON_EVENT:
{
zh_encoder_button_event_on_isr_t *button_event = event_data;
if (button_event->button_status == true)
{
menu_status.is_active == false ? zh_menu_set(&menu_handle, ZH_MENU_ENTER, NULL, 0)
: menu_status.is_selected == false ? zh_menu_set(&menu_handle, ZH_MENU_SELECT, NULL, 0)
: zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL, 0);
}
break;
}
case ZH_ENCODER_EVENT:
{
zh_encoder_event_on_isr_t *encoder_event = event_data;
if (menu_status.is_active == true)
{
if (menu_status.is_selected == true)
{
zh_menu_set(&menu_handle, ZH_MENU_SELECT, encoder_event,
sizeof(zh_encoder_event_on_isr_t));
}
else
{
encoder_event->encoder_status == true
? zh_menu_set(&menu_handle, ZH_MENU_UP, NULL, 0)
: zh_menu_set(&menu_handle, ZH_MENU_DOWN, NULL, 0);
}
}
break;
}
default:
break;
}
}
/* ---------- Menu callbacks ---------- */
void zh_menu_enter_main_menu_callback(void *arg)
{
(void)arg;
xSemaphoreTake(_lcd_mutex, portMAX_DELAY);
_cached_node = NULL;
_invalidate_row_cache();
xSemaphoreGive(_lcd_mutex);
}
void zh_menu_exit_main_menu_callback(void *arg)
{
(void)arg;
/* NEW: страховочное сохранение при выходе из меню */
_save_settings();
_prev_selected = false;
xSemaphoreTake(_lcd_mutex, portMAX_DELAY);
zh_160x_lcd_clear(&lcd_handle);
_invalidate_row_cache();
if (_boot_done)
_draw_main_screen();
else
_draw_load_screen();
_cached_node = NULL;
xSemaphoreGive(_lcd_mutex);
}
void zh_menu_change_menu_callback(zh_menu_node_t *current_node, uint8_t current_index)
{
_last_node = current_node;
_last_index = current_index;
zh_menu_status_t status = {0};
zh_menu_get(&menu_handle, &status);
/* NEW: сохраняем при выходе из режима редактирования (BACK) */
if (_prev_selected && !status.is_selected)
{
_save_settings();
}
_prev_selected = status.is_selected;
xSemaphoreTake(_lcd_mutex, portMAX_DELAY);
_draw_menu(current_node, current_index, &status);
xSemaphoreGive(_lcd_mutex);
}
/* ---------- Selectable: Temp / PW ---------- */
void zh_menu_selectable_item_callback(void *arg)
{
if (_last_node == NULL || _last_node->submenu == NULL)
return;
if (_last_index >= _last_node->item_numbers)
return;
zh_menu_node_t *item = &_last_node->submenu[_last_index];
if (arg == NULL)
{
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
return;
}
zh_encoder_event_on_isr_t *enc = arg;
int8_t dir = enc->encoder_status ? +1 : -1;
bool changed = false;
if (item->label == lbl_temp_on)
{
g_temp_on = _clamp_i16(g_temp_on + dir, 10, 50);
changed = true;
}
else if (item->label == lbl_temp_off)
{
g_temp_off = _clamp_i16(g_temp_off + dir, 10, 50);
changed = true;
}
else if (item->label == lbl_pw_ch_1)
{
g_pw_ch[0] = _clamp_u8(g_pw_ch[0] + dir * 5, 0, 100);
changed = true;
}
else if (item->label == lbl_pw_ch_2)
{
g_pw_ch[1] = _clamp_u8(g_pw_ch[1] + dir * 5, 0, 100);
changed = true;
}
else if (item->label == lbl_pw_ch_3)
{
g_pw_ch[2] = _clamp_u8(g_pw_ch[2] + dir * 5, 0, 100);
changed = true;
}
else if (item->label == lbl_pw_ch_4)
{
g_pw_ch[3] = _clamp_u8(g_pw_ch[3] + dir * 5, 0, 100);
changed = true;
}
if (changed)
{
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
}
}
/* ---------- Selectable: выбор load-экрана ---------- */
void zh_menu_boot_screen_callback(void *arg)
{
if (arg == NULL)
{
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
return;
}
zh_encoder_event_on_isr_t *enc = arg;
int8_t dir = enc->encoder_status ? +1 : -1;
int next = _boot_screen + dir;
if (next < 0)
next = LOAD_SCREEN_COUNT - 1;
if (next >= LOAD_SCREEN_COUNT)
next = 0;
if (next != _boot_screen)
{
_boot_screen = next;
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
}
}
/* ---------- Selectable: выбор main-экрана ---------- */
void zh_menu_main_screen_callback(void *arg)
{
if (arg == NULL)
{
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
return;
}
zh_encoder_event_on_isr_t *enc = arg;
int8_t dir = enc->encoder_status ? +1 : -1;
int next = _main_screen + dir;
if (next < 0)
next = MAIN_SCREEN_COUNT - 1;
if (next >= MAIN_SCREEN_COUNT)
next = 0;
if (next != _main_screen)
{
_main_screen = next;
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
}
}
void zh_menu_simple_item_callback(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "Simple item callback performed");
}
void zh_menu_back_command_callback(void *arg)
{
(void)arg;
zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL, 0);
}
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "zh_menu.h"
#include "zh_160x_i2c.h"
#include "zh_encoder.h"
#include "zh_ota_server.h"
#define ENCODER_NUMBER 0x01
#define MENU_VIEWPORT_ROWS 3
#define MENU_LABEL_LEN 17
/* ---------- Размеры view ---------- */
#define VIEW_CELLS 5 /* ячеек в массиве данных (main) */
#define VIEW_ROWS 4 /* строк отрисовывается на LCD */
/* ---------- Пул основных экранов (idle) — 5 массивов ---------- */
extern int16_t main_race_data[VIEW_CELLS];
extern int16_t main_engine_data[VIEW_CELLS];
extern int16_t main_times_data[VIEW_CELLS];
extern int16_t main_debug_data[VIEW_CELLS];
extern int16_t main_blank_data[VIEW_CELLS];
/* ---------- Динамические буферы меток ---------- */
extern char lbl_temp_on[MENU_LABEL_LEN];
extern char lbl_temp_off[MENU_LABEL_LEN];
extern char lbl_pw_ch_1[MENU_LABEL_LEN];
extern char lbl_pw_ch_2[MENU_LABEL_LEN];
extern char lbl_pw_ch_3[MENU_LABEL_LEN];
extern char lbl_pw_ch_4[MENU_LABEL_LEN];
extern char lbl_boot_screen[MENU_LABEL_LEN]; /* "B: <имя load-экрана>" */
extern char lbl_main_screen[MENU_LABEL_LEN]; /* "M: <имя main-экрана>" */
/* ---------- Переменные значений ---------- */
extern int16_t g_temp_on;
extern int16_t g_temp_off;
extern uint8_t g_pw_ch[4];
/* ---------- Callbacks ---------- */
void zh_menu_enter_main_menu_callback(void *arg);
void zh_menu_exit_main_menu_callback(void *arg);
void zh_menu_change_menu_callback(zh_menu_node_t *current_node, uint8_t current_index);
void zh_menu_selectable_item_callback(void *arg);
void zh_menu_simple_item_callback(void *args);
void zh_menu_back_command_callback(void *arg);
void zh_menu_boot_screen_callback(void *arg);
void zh_menu_main_screen_callback(void *arg);
#ifdef __cplusplus
extern "C"
{
#endif
zh_menu_node_t main_menu = {
.label = "Settings",
.submenu = (zh_menu_node_t[]){
{.label = "Screens", .submenu = (zh_menu_node_t[]){
{.label = lbl_boot_screen, .item_cb = &zh_menu_boot_screen_callback, .selectable = true},
{.label = lbl_main_screen, .item_cb = &zh_menu_main_screen_callback, .selectable = true},
{.label = "Back", .item_cb = &zh_menu_back_command_callback},
},
.item_numbers = 3},
{.label = "Air condition", .submenu = (zh_menu_node_t[]){
{.label = lbl_temp_on, .item_cb = &zh_menu_selectable_item_callback, .selectable = true},
{.label = lbl_temp_off, .item_cb = &zh_menu_selectable_item_callback, .selectable = true},
{.label = "Back", .item_cb = &zh_menu_back_command_callback},
},
.item_numbers = 3},
{.label = "Lights", .submenu = (zh_menu_node_t[]){
{.label = lbl_pw_ch_1, .item_cb = &zh_menu_selectable_item_callback, .selectable = true},
{.label = lbl_pw_ch_2, .item_cb = &zh_menu_selectable_item_callback, .selectable = true},
{.label = lbl_pw_ch_3, .item_cb = &zh_menu_selectable_item_callback, .selectable = true},
{.label = lbl_pw_ch_4, .item_cb = &zh_menu_selectable_item_callback, .selectable = true},
{.label = "Back", .item_cb = &zh_menu_back_command_callback},
},
.item_numbers = 5},
{.label = "Exit", .item_cb = &zh_menu_back_command_callback},
},
.item_numbers = 4,
};
#ifdef __cplusplus
}
#endif