245 lines
5.7 KiB
C
245 lines
5.7 KiB
C
/**
|
|
* @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 <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
}
|