Files
RadioPhotonic_PCB_software/App/Services/ui_status.c
2026-04-26 18:39:55 +03:00

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;
}