This commit is contained in:
aZholtikov committed 2026-10-28 15:47:41 +03:00
1 parent b57a9eac15
commit c884a61f6e
2 files changed
+115 -124

No files matched your search

+78 -97
View File
@@ -1,6 +1,5 @@
#include "zh_sim_race_button_box.h" #include "zh_sim_race_button_box.h"
// Хэндлы - функции - переменные // Хэндлы - функции - переменные
// #define ENCODER_NUMBER 0x01 // #define ENCODER_NUMBER 0x01
// #define LCD_VIEWPORT_ROWS 3 // #define LCD_VIEWPORT_ROWS 3
@@ -8,13 +7,13 @@
// #define LCD_TOTAL_ROWS 4 // #define LCD_TOTAL_ROWS 4
#define BOOT_SPLASH_MS 5000 #define BOOT_SPLASH_MS 5000
#define MAIN_SCREEN_COUNT 5 // #define MAIN_SCREEN_COUNT 5
#define LOAD_SCREEN_COUNT 5 // #define LOAD_SCREEN_COUNT 5
// #define NVS_NAMESPACE "sim_race_box" /* NEW */ // #define NVS_NAMESPACE "sim_race_box" /* NEW */
// #define NVS_KEY_SETTINGS "settings" /* NEW */ // #define NVS_KEY_SETTINGS "settings" /* NEW */
static const char *TAG = "sim_race_box"; // static const char *TAG = "sim_race_box";
/* ============================================================ /* ============================================================
* Данные main-экранов (idle) * Данные main-экранов (idle)
@@ -25,21 +24,7 @@ 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_debug_data[VIEW_CELLS] = {45, -42, 152, 12, 0};
int16_t main_blank_data[VIEW_CELLS] = {0, 0, 0, 0, 0}; int16_t main_blank_data[VIEW_CELLS] = {0, 0, 0, 0, 0};
/* ---------- Дескрипторы экранов ---------- */ static const zh_main_screen_description_t main_screens[] = {
// 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 zh_main_screen_description_t main_screens[MAIN_SCREEN_COUNT] = {
{"Race", {"SPD", "RPM", "GER", "LAP"}, main_race_data}, {"Race", {"SPD", "RPM", "GER", "LAP"}, main_race_data},
{"Engine", {"OIL", "WTR", "BAT", "FUL"}, main_engine_data}, {"Engine", {"OIL", "WTR", "BAT", "FUL"}, main_engine_data},
{"Times", {"LAP", "BST", "LST", "DLT"}, main_times_data}, {"Times", {"LAP", "BST", "LST", "DLT"}, main_times_data},
@@ -47,29 +32,18 @@ static const zh_main_screen_description_t main_screens[MAIN_SCREEN_COUNT] = {
{"Blank", {"---", "---", "---", "---"}, main_blank_data}, {"Blank", {"---", "---", "---", "---"}, main_blank_data},
}; };
static const zh_boot_screen_description_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 _boot_done = false;
static bool _prev_selected = false; /* NEW: для детекции выхода из edit */ static bool _prev_selected = false; /* NEW: для детекции выхода из edit */
/* ---------- Динамические буферы меток ---------- */ /* ---------- Динамические буферы меток ---------- */
char lbl_boot_screen[MENU_LABEL_LEN] = "B:Boot"; // char lbl_boot_screen[LCD_TOTAL_COLS + 1] = "B:Boot";
char lbl_main_screen[MENU_LABEL_LEN] = "M:Race"; // char lbl_main_screen[LCD_TOTAL_COLS + 1] = "M:Race";
char lbl_temp_on[MENU_LABEL_LEN] = "Temp ON 15C"; // char lbl_temp_on[LCD_TOTAL_COLS + 1] = "Temp ON 15C";
char lbl_temp_off[MENU_LABEL_LEN] = "Temp OFF 35C"; // char lbl_temp_off[LCD_TOTAL_COLS + 1] = "Temp OFF 35C";
char lbl_pw_ch_1[MENU_LABEL_LEN] = "PW CH 1 100%"; // char lbl_pw_ch_1[LCD_TOTAL_COLS + 1] = "PW CH 1 100%";
char lbl_pw_ch_2[MENU_LABEL_LEN] = "PW CH 2 100%"; // char lbl_pw_ch_2[LCD_TOTAL_COLS + 1] = "PW CH 2 100%";
char lbl_pw_ch_3[MENU_LABEL_LEN] = "PW CH 3 100%"; // char lbl_pw_ch_3[LCD_TOTAL_COLS + 1] = "PW CH 3 100%";
char lbl_pw_ch_4[MENU_LABEL_LEN] = "PW CH 4 100%"; // char lbl_pw_ch_4[LCD_TOTAL_COLS + 1] = "PW CH 4 100%";
/* ---------- Handle'ы ---------- */ /* ---------- Handle'ы ---------- */
zh_encoder_handle_t *encoder_handle = NULL; zh_encoder_handle_t *encoder_handle = NULL;
@@ -85,6 +59,14 @@ static void zh_save_settings(void);
void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
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_back_command_callback(void *arg);
void zh_menu_boot_screen_callback(void *arg);
void zh_menu_main_screen_callback(void *arg);
/* ---------- Состояние меню ---------- */ /* ---------- Состояние меню ---------- */
static zh_menu_node_t *last_node = NULL; static zh_menu_node_t *last_node = NULL;
static uint8_t last_index = 0; static uint8_t last_index = 0;
@@ -150,15 +132,15 @@ static void zh_save_settings(void)
} }
/* ---------- Утилиты ---------- */ /* ---------- Утилиты ---------- */
static inline int16_t _clamp_i16(int16_t v, int16_t lo, int16_t hi) // static inline int16_t _clamp_i16(int16_t v, int16_t lo, int16_t hi)
{ // {
return v < lo ? lo : (v > hi ? hi : v); // return v < lo ? lo : (v > hi ? hi : v);
} // }
static inline uint8_t _clamp_u8(int16_t v, uint8_t lo, uint8_t hi) // static inline uint8_t _clamp_u8(int16_t v, uint8_t lo, uint8_t hi)
{ // {
return v < lo ? lo : (v > hi ? hi : v); // return v < lo ? lo : (v > hi ? hi : v);
} // }
static void _invalidaterow_cache(void) static void _invalidaterow_cache(void)
{ {
@@ -166,8 +148,7 @@ static void _invalidaterow_cache(void)
} }
/* ---------- Форматирование ---------- */ /* ---------- Форматирование ---------- */
static void _format_row(char *buf, const char *label, bool selected, bool edit, static void _format_row(char *buf, const char *label, bool selected, bool edit, bool arrow_up, bool arrow_down)
bool arrow_up, bool arrow_down)
{ {
memset(buf, ' ', LCD_TOTAL_COLS); memset(buf, ' ', LCD_TOTAL_COLS);
buf[LCD_TOTAL_COLS] = '\0'; buf[LCD_TOTAL_COLS] = '\0';
@@ -211,8 +192,9 @@ static void _format_load_row(char *buf, const char *line)
static void _lcd_put_row(uint8_t row, const char *text) static void _lcd_put_row(uint8_t row, const char *text)
{ {
if (row_valid[row] && memcmp(row_cache[row], text, LCD_TOTAL_COLS) == 0) if (row_valid[row] && memcmp(row_cache[row], text, LCD_TOTAL_COLS) == 0)
{
return; return;
}
zh_160x_set_cursor(&lcd_handle, row, 0); zh_160x_set_cursor(&lcd_handle, row, 0);
zh_160x_print_char(&lcd_handle, text); zh_160x_print_char(&lcd_handle, text);
memcpy(row_cache[row], text, LCD_TOTAL_COLS); memcpy(row_cache[row], text, LCD_TOTAL_COLS);
@@ -255,11 +237,11 @@ static void _refresh_dynamic_labels(zh_menu_node_t *node)
} }
else if (lbl == lbl_boot_screen) else if (lbl == lbl_boot_screen)
{ {
snprintf(lbl_boot_screen, sizeof(lbl_boot_screen), "B:%s", load_screens[_boot_screen].name); snprintf(lbl_boot_screen, sizeof(lbl_boot_screen), "B:%s", boot_screens[boot_screen_number].name);
} }
else if (lbl == lbl_main_screen) else if (lbl == lbl_main_screen)
{ {
snprintf(lbl_main_screen, sizeof(lbl_main_screen), "M:%s", main_screens[_main_screen].name); snprintf(lbl_main_screen, sizeof(lbl_main_screen), "M:%s", main_screens[main_screen_number].name);
} }
} }
} }
@@ -267,7 +249,7 @@ static void _refresh_dynamic_labels(zh_menu_node_t *node)
/* ---------- Отрисовка экранов ---------- */ /* ---------- Отрисовка экранов ---------- */
static void _draw_main_screen(void) static void _draw_main_screen(void)
{ {
const zh_main_screen_description_t *s = &main_screens[_main_screen]; const zh_main_screen_description_t *s = &main_screens[main_screen_number];
for (uint8_t i = 0; i < LCD_TOTAL_ROWS; ++i) for (uint8_t i = 0; i < LCD_TOTAL_ROWS; ++i)
{ {
char buf[LCD_TOTAL_COLS + 1]; char buf[LCD_TOTAL_COLS + 1];
@@ -278,7 +260,7 @@ static void _draw_main_screen(void)
static void _draw_load_screen(void) static void _draw_load_screen(void)
{ {
const zh_boot_screen_description_t *s = &load_screens[_boot_screen]; const zh_boot_screen_description_t *s = &boot_screens[boot_screen_number];
for (uint8_t i = 0; i < LCD_TOTAL_ROWS; ++i) for (uint8_t i = 0; i < LCD_TOTAL_ROWS; ++i)
{ {
char buf[LCD_TOTAL_COLS + 1]; char buf[LCD_TOTAL_COLS + 1];
@@ -451,7 +433,7 @@ void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t ev
case ZH_BUTTON_EVENT: case ZH_BUTTON_EVENT:
{ {
zh_encoder_button_event_on_isr_t *button_event = event_data; zh_encoder_button_event_on_isr_t *button_event = event_data;
if (button_event->button_status == true) if (button_event->button_status == ENCODER_BUTTON_RELEASED)
{ {
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) 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); : zh_menu_set(&menu_handle, ZH_MENU_BACK, NULL, 0);
@@ -531,60 +513,58 @@ void zh_menu_change_menu_callback(zh_menu_node_t *current_node, uint8_t current_
void zh_menu_selectable_item_callback(void *arg) void zh_menu_selectable_item_callback(void *arg)
{ {
if (last_node == NULL || last_node->submenu == NULL) if (last_node == NULL || last_node->submenu == NULL)
{
return; return;
}
if (last_index >= last_node->item_numbers) if (last_index >= last_node->item_numbers)
{
return; return;
}
zh_menu_node_t *item = &last_node->submenu[last_index]; zh_menu_node_t *item = &last_node->submenu[last_index];
if (arg == NULL) if (arg == NULL)
{ {
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0); zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
return; return;
} }
zh_encoder_event_on_isr_t *encoder = arg;
zh_encoder_event_on_isr_t *enc = arg; int8_t dir = encoder->encoder_status ? +1 : -1;
int8_t dir = enc->encoder_status ? +1 : -1;
bool changed = false; bool changed = false;
if (item->label == lbl_temp_on) if (item->label == lbl_temp_on)
{ {
air_condition_temp_on = _clamp_i16(air_condition_temp_on + dir, 15, 30); air_condition_temp_on = VALUE_CLAMP(air_condition_temp_on + dir, 15, 30);
changed = true; changed = true;
} }
else if (item->label == lbl_temp_off) else if (item->label == lbl_temp_off)
{ {
air_condition_temp_off = _clamp_i16(air_condition_temp_off + dir, 15, 30); air_condition_temp_off = VALUE_CLAMP(air_condition_temp_off + dir, 15, 30);
changed = true; changed = true;
} }
else if (item->label == lbl_pw_ch_1) else if (item->label == lbl_pw_ch_1)
{ {
exit_channel_power[0] = _clamp_u8(exit_channel_power[0] + dir * 5, 0, 100); exit_channel_power[0] = VALUE_CLAMP(exit_channel_power[0] + dir * 5, 0, 100);
changed = true; changed = true;
} }
else if (item->label == lbl_pw_ch_2) else if (item->label == lbl_pw_ch_2)
{ {
exit_channel_power[1] = _clamp_u8(exit_channel_power[1] + dir * 5, 0, 100); exit_channel_power[1] = VALUE_CLAMP(exit_channel_power[1] + dir * 5, 0, 100);
changed = true; changed = true;
} }
else if (item->label == lbl_pw_ch_3) else if (item->label == lbl_pw_ch_3)
{ {
exit_channel_power[2] = _clamp_u8(exit_channel_power[2] + dir * 5, 0, 100); exit_channel_power[2] = VALUE_CLAMP(exit_channel_power[2] + dir * 5, 0, 100);
changed = true; changed = true;
} }
else if (item->label == lbl_pw_ch_4) else if (item->label == lbl_pw_ch_4)
{ {
exit_channel_power[3] = _clamp_u8(exit_channel_power[3] + dir * 5, 0, 100); exit_channel_power[3] = VALUE_CLAMP(exit_channel_power[3] + dir * 5, 0, 100);
changed = true; changed = true;
} }
if (changed == true)
if (changed)
{ {
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0); zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
} }
} }
/* ---------- Selectable: выбор load-экрана ---------- */
void zh_menu_boot_screen_callback(void *arg) void zh_menu_boot_screen_callback(void *arg)
{ {
if (arg == NULL) if (arg == NULL)
@@ -592,24 +572,24 @@ void zh_menu_boot_screen_callback(void *arg)
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0); zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
return; return;
} }
zh_encoder_event_on_isr_t *encoder = arg;
zh_encoder_event_on_isr_t *enc = arg; int8_t dir = encoder->encoder_status == true ? +1 : -1;
int8_t dir = enc->encoder_status ? +1 : -1; int next = boot_screen_number + dir;
int next = _boot_screen + dir;
if (next < 0) if (next < 0)
next = LOAD_SCREEN_COUNT - 1;
if (next >= LOAD_SCREEN_COUNT)
next = 0;
if (next != _boot_screen)
{ {
_boot_screen = next; next = ARRAY_SIZE(boot_screens) - 1;
}
if (next >= ARRAY_SIZE(boot_screens))
{
next = 0;
}
if (next != boot_screen_number)
{
boot_screen_number = next;
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0); zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
} }
} }
/* ---------- Selectable: выбор main-экрана ---------- */
void zh_menu_main_screen_callback(void *arg) void zh_menu_main_screen_callback(void *arg)
{ {
if (arg == NULL) if (arg == NULL)
@@ -617,28 +597,29 @@ void zh_menu_main_screen_callback(void *arg)
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0); zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
return; return;
} }
zh_encoder_event_on_isr_t *encoder = arg;
zh_encoder_event_on_isr_t *enc = arg; int8_t dir = encoder->encoder_status == true ? +1 : -1;
int8_t dir = enc->encoder_status ? +1 : -1; int next = main_screen_number + dir;
int next = _main_screen + dir;
if (next < 0) if (next < 0)
next = MAIN_SCREEN_COUNT - 1;
if (next >= MAIN_SCREEN_COUNT)
next = 0;
if (next != _main_screen)
{ {
_main_screen = next; next = ARRAY_SIZE(main_screens) - 1;
}
if (next >= ARRAY_SIZE(main_screens))
{
next = 0;
}
if (next != main_screen_number)
{
main_screen_number = next;
zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0); zh_menu_set(&menu_handle, ZH_MENU_SHOW, NULL, 0);
} }
} }
void zh_menu_simple_item_callback(void *arg) // void zh_menu_simple_item_callback(void *arg)
{ // {
(void)arg; // (void)arg;
ESP_LOGI(TAG, "Simple item callback performed"); // ESP_LOGI(TAG, "Simple item callback performed");
} // }
void zh_menu_back_command_callback(void *arg) void zh_menu_back_command_callback(void *arg)
{ {
+37 -27
View File
@@ -7,17 +7,30 @@
#include "zh_encoder.h" #include "zh_encoder.h"
#include "zh_ota_server.h" #include "zh_ota_server.h"
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#define VALUE_CLAMP(v, lo, hi) ((v) < (lo) ? (lo) : ((v) > (hi) ? (hi) : (v)))
#define ENCODER_NUMBER 1 #define ENCODER_NUMBER 1
#define LCD_TOTAL_ROWS 4 #define LCD_TOTAL_ROWS 4
#define LCD_TOTAL_COLS 16 #define LCD_TOTAL_COLS 16
#define STATS_TASK_PRIORITY tskIDLE_PRIORITY /*!< Priority of the statistics/monitoring task */
#define STATS_TASK_STACK_SIZE configMINIMAL_STACK_SIZE + 1280 /*!< Stack size of the statistics/monitoring task */
#define EXPANDER_TASK_PRIORITY tskIDLE_PRIORITY + 5 /*!< Priority of the PCF8574 expander polling task */
#define EXPANDER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE /*!< Stack size of the expander polling task */
#define ENCODER_TASK_PRIORITY tskIDLE_PRIORITY + 4 /*!< Priority of the encoder reading task */
#define ENCODER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE /*!< Stack size of the encoder task */
// #define LCD_VIEWPORT_ROWS (LCD_TOTAL_ROWS - 1) // #define LCD_VIEWPORT_ROWS (LCD_TOTAL_ROWS - 1)
#define MENU_LABEL_LEN 17 #define ENCODER_BUTTON_RELEASED true /*!< State: encoder button released */
#define ENCODER_BUTTON_PRESSED false /*!< State: encoder button pressed */
// #define LCD_TOTAL_COLS+1 17
/* ---------- Размеры view ---------- */ /* ---------- Размеры view ---------- */
#define VIEW_CELLS 5 /* ячеек в массиве данных (main) */ #define VIEW_CELLS 5 /* ячеек в массиве данных (main) */
#define VIEW_ROWS 4 /* строк отрисовывается на LCD */ // #define VIEW_ROWS 4 /* строк отрисовывается на LCD */
#define NVS_NAMESPACE "sim_race_box" /* NEW */ #define NVS_NAMESPACE "sim_race_box" /* NEW */
#define NVS_KEY_SETTINGS "settings" /* NEW */ #define NVS_KEY_SETTINGS "settings" /* NEW */
@@ -29,36 +42,25 @@ extern int16_t main_times_data[VIEW_CELLS];
extern int16_t main_debug_data[VIEW_CELLS]; extern int16_t main_debug_data[VIEW_CELLS];
extern int16_t main_blank_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 #ifdef __cplusplus
extern "C" extern "C"
{ {
#endif #endif
extern void zh_menu_selectable_item_callback(void *arg);
extern void zh_menu_back_command_callback(void *arg);
extern void zh_menu_boot_screen_callback(void *arg);
extern void zh_menu_main_screen_callback(void *arg);
static char lbl_boot_screen[LCD_TOTAL_COLS + 1] = "B:Boot";
static char lbl_main_screen[LCD_TOTAL_COLS + 1] = "M:Race";
static char lbl_temp_on[LCD_TOTAL_COLS + 1] = "Temp ON 15C";
static char lbl_temp_off[LCD_TOTAL_COLS + 1] = "Temp OFF 35C";
static char lbl_pw_ch_1[LCD_TOTAL_COLS + 1] = "PW CH 1 100%";
static char lbl_pw_ch_2[LCD_TOTAL_COLS + 1] = "PW CH 2 100%";
static char lbl_pw_ch_3[LCD_TOTAL_COLS + 1] = "PW CH 3 100%";
static char lbl_pw_ch_4[LCD_TOTAL_COLS + 1] = "PW CH 4 100%";
typedef struct typedef struct
{ {
uint8_t air_condition_temp_on; uint8_t air_condition_temp_on;
@@ -82,6 +84,14 @@ extern "C"
const char *const lines[LCD_TOTAL_ROWS]; const char *const lines[LCD_TOTAL_ROWS];
} zh_boot_screen_description_t; } zh_boot_screen_description_t;
static const zh_boot_screen_description_t boot_screens[] = {
{"Boot", {"Booting...", "RAM: OK", "I2C: OK", "Starting..."}},
{"Version", {"FW v1.0", "Build 2025-10", "ESP-IDF v5.4", ""}},
{"Diag", {"I2C: OK", "LCD: OK", "ENC: OK", "All: OK"}},
{"Ready", {"System ready", "Ready for race", "Let's go", ""}},
{"Blank", {"---", "---", "---", "---"}},
};
zh_menu_node_t main_menu = { zh_menu_node_t main_menu = {
.label = "Settings", .label = "Settings",
.submenu = (zh_menu_node_t[]){ .submenu = (zh_menu_node_t[]){