/** * @file ui_status.c * @brief Standalone LCD/button frontend for profile-driven operation. * * Architectural note: * This service owns all user-facing status formatting and button debouncing. * The application core pushes state into this module, while this module * returns high-level events such as "select next profile". */ #include "ui_status.h" #include #include #include #include "board_io.h" #include "lcd1602_display.h" #define UI_STATUS_LCD_COLUMNS 16u #define UI_STATUS_BUTTON_DEBOUNCE_TICKS_10MS 3u typedef struct ui_button_state_t { bool raw_pressed; bool stable_pressed; uint32_t last_change_tick_10ms; } ui_button_state_t; static char g_profile_name[APP_PROFILE_NAME_LENGTH]; static app_mode_t g_mode = APP_MODE_IDLE; static uint8_t g_error_flags = 0u; static uint8_t g_initialised = 0u; static uint8_t g_display_dirty = 0u; static ui_button_state_t g_button = {false, false, 0u}; static void ui_status_mark_dirty(void); static void ui_status_copy_padded(char *destination, const char *source); static const char *ui_status_mode_name(app_mode_t mode); static void ui_status_build_line_1(char *line_buffer); static void ui_status_build_line_2(char *line_buffer); static void ui_status_refresh_display(void); void ui_status_init(void) { bool raw_pressed; memset(g_profile_name, 0, sizeof(g_profile_name)); g_mode = APP_MODE_IDLE; g_error_flags = 0u; board_io_init_standalone_ui(); raw_pressed = board_io_is_standalone_ui_button_pressed(); /* * Synchronise the debounce state with the physical input level at boot. * This prevents a stuck-low or already-held button from generating a * synthetic "next profile" event immediately after startup. */ g_button.raw_pressed = raw_pressed; g_button.stable_pressed = raw_pressed; g_button.last_change_tick_10ms = 0u; lcd1602_display_init(); g_initialised = 1u; g_display_dirty = 1u; ui_status_refresh_display(); } void ui_status_set_profile_name(const char *profile_name) { char next_name[APP_PROFILE_NAME_LENGTH] = {0}; if (profile_name != NULL) { strncpy(next_name, profile_name, sizeof(next_name) - 1u); } if (strncmp(g_profile_name, next_name, sizeof(g_profile_name)) != 0) { memcpy(g_profile_name, next_name, sizeof(g_profile_name)); ui_status_mark_dirty(); } } void ui_status_set_mode(app_mode_t mode) { /* * TX_CURRENT is a transient transport state rather than a user-visible * operating mode. Ignoring it prevents needless LCD churn during polling. */ if (mode == APP_MODE_TX_CURRENT) { return; } if (g_mode != mode) { g_mode = mode; ui_status_mark_dirty(); } } void ui_status_set_error(uint8_t error_flags) { if (g_error_flags != error_flags) { g_error_flags = error_flags; ui_status_mark_dirty(); } } app_event_t ui_status_poll_event(uint32_t tick_10ms) { bool raw_pressed; app_event_t event = APP_EVENT_NONE; if (g_initialised == 0u) { return APP_EVENT_NONE; } raw_pressed = board_io_is_standalone_ui_button_pressed(); if (raw_pressed != g_button.raw_pressed) { g_button.raw_pressed = raw_pressed; g_button.last_change_tick_10ms = tick_10ms; } else if ((tick_10ms - g_button.last_change_tick_10ms) >= UI_STATUS_BUTTON_DEBOUNCE_TICKS_10MS) { if (g_button.stable_pressed != g_button.raw_pressed) { g_button.stable_pressed = g_button.raw_pressed; if (g_button.stable_pressed) { event = APP_EVENT_PROFILE_NEXT; } } } if (g_display_dirty != 0u) { ui_status_refresh_display(); } return event; } static void ui_status_mark_dirty(void) { g_display_dirty = 1u; } static void ui_status_copy_padded(char *destination, const char *source) { size_t index; for (index = 0u; index < UI_STATUS_LCD_COLUMNS; ++index) { destination[index] = ' '; } destination[UI_STATUS_LCD_COLUMNS] = '\0'; if (source == NULL) { return; } for (index = 0u; (index < UI_STATUS_LCD_COLUMNS) && (source[index] != '\0'); ++index) { destination[index] = source[index]; } } static const char *ui_status_mode_name(app_mode_t mode) { switch (mode) { case APP_MODE_WORK: return "WORK"; case APP_MODE_ERROR: return "ERROR"; case APP_MODE_IDLE: default: return "IDLE"; } } static void ui_status_build_line_1(char *line_buffer) { const char *label = g_profile_name; if (line_buffer == NULL) { return; } if (label[0] == '\0') { label = (g_mode == APP_MODE_WORK) ? "Manual control" : "No profile"; } ui_status_copy_padded(line_buffer, label); } static void ui_status_build_line_2(char *line_buffer) { char status_text[UI_STATUS_LCD_COLUMNS + 1u]; if (line_buffer == NULL) { return; } if (g_error_flags == 0u) { (void)snprintf(status_text, sizeof(status_text), "%s OK", ui_status_mode_name(g_mode)); } else { (void)snprintf(status_text, sizeof(status_text), "%s ERR %02X", ui_status_mode_name(g_mode), g_error_flags); } ui_status_copy_padded(line_buffer, status_text); } static void ui_status_refresh_display(void) { char line_1[UI_STATUS_LCD_COLUMNS + 1u]; char line_2[UI_STATUS_LCD_COLUMNS + 1u]; if (g_initialised == 0u) { return; } ui_status_build_line_1(line_1); ui_status_build_line_2(line_2); lcd1602_display_set_lines(line_1, line_2); g_display_dirty = 0u; }