Add local OLED recovery controls
This commit is contained in:
+541
-39
@@ -12,6 +12,7 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif_ip_addr.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/semphr.h"
|
||||
@@ -40,6 +41,10 @@
|
||||
#define LOCAL_STATUS_UI_ACTIVE_CONTRAST 127U
|
||||
#define LOCAL_STATUS_UI_ICON_SIZE 8U
|
||||
#define LOCAL_STATUS_UI_STATUS_SLOT_WIDTH 18U
|
||||
#define LOCAL_STATUS_UI_MENU_VISIBLE_ROWS 5U
|
||||
#define LOCAL_STATUS_UI_MENU_TIMEOUT_MS 30000U
|
||||
#define LOCAL_STATUS_UI_RESULT_TIMEOUT_MS 2500U
|
||||
#define LOCAL_STATUS_UI_CONFIRM_HOLD_MS 2000U
|
||||
|
||||
static const char *TAG = "local_status_ui";
|
||||
|
||||
@@ -64,6 +69,36 @@ typedef enum {
|
||||
LOCAL_STATUS_POWER_OFF,
|
||||
} local_status_power_t;
|
||||
|
||||
typedef enum {
|
||||
LOCAL_STATUS_MODE_STATUS = 0,
|
||||
LOCAL_STATUS_MODE_MENU,
|
||||
LOCAL_STATUS_MODE_CONFIRM,
|
||||
LOCAL_STATUS_MODE_RESULT,
|
||||
} local_status_mode_t;
|
||||
|
||||
typedef enum {
|
||||
LOCAL_STATUS_ACTION_SERIAL = 0,
|
||||
LOCAL_STATUS_ACTION_WIFI_POWER,
|
||||
LOCAL_STATUS_ACTION_WIFI_RECONNECT,
|
||||
LOCAL_STATUS_ACTION_WEB,
|
||||
LOCAL_STATUS_ACTION_SSH,
|
||||
LOCAL_STATUS_ACTION_REVOKE_WRITER,
|
||||
LOCAL_STATUS_ACTION_DISPLAY_OFF,
|
||||
LOCAL_STATUS_ACTION_REBOOT,
|
||||
LOCAL_STATUS_ACTION_COUNT,
|
||||
} local_status_action_t;
|
||||
|
||||
typedef enum {
|
||||
LOCAL_STATUS_BUTTON_EVENT_PRESS = 0,
|
||||
LOCAL_STATUS_BUTTON_EVENT_SHORT_RELEASE,
|
||||
LOCAL_STATUS_BUTTON_EVENT_LONG_REACHED,
|
||||
} local_status_button_event_type_t;
|
||||
|
||||
typedef struct {
|
||||
local_status_button_t button;
|
||||
local_status_button_event_type_t type;
|
||||
} local_status_button_event_t;
|
||||
|
||||
typedef enum {
|
||||
LOCAL_STATUS_ICON_SERIAL = 0,
|
||||
LOCAL_STATUS_ICON_WIFI,
|
||||
@@ -78,8 +113,24 @@ typedef struct {
|
||||
bool stable_pressed;
|
||||
bool candidate_pressed;
|
||||
TickType_t candidate_since;
|
||||
TickType_t pressed_since;
|
||||
bool long_emitted;
|
||||
bool suppress_gesture;
|
||||
} local_status_button_state_t;
|
||||
|
||||
typedef struct {
|
||||
local_status_mode_t mode;
|
||||
local_status_page_t page;
|
||||
local_status_action_t menu_action;
|
||||
local_status_action_t confirmed_action;
|
||||
bool confirmed_stop;
|
||||
session_broker_client_id_t expected_writer_id;
|
||||
TickType_t mode_since;
|
||||
esp_err_t result_error;
|
||||
bool result_requested;
|
||||
bool restart_pending;
|
||||
} local_status_ui_state_t;
|
||||
|
||||
typedef struct {
|
||||
bool serial_config_available;
|
||||
bool broker_available;
|
||||
@@ -628,47 +679,229 @@ static void render_network_page(const local_status_snapshot_t *snapshot)
|
||||
draw_content_item(LOCAL_STATUS_ICON_SSH, 40U, line);
|
||||
}
|
||||
|
||||
static void render_page(local_status_page_t page, const local_status_snapshot_t *snapshot)
|
||||
static bool network_available(const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
return snapshot->wifi_available &&
|
||||
(snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE || snapshot->wifi.ap_running);
|
||||
}
|
||||
|
||||
static const char *action_label(local_status_action_t action,
|
||||
const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
switch (action) {
|
||||
case LOCAL_STATUS_ACTION_SERIAL:
|
||||
return snapshot->serial_running ? "Serial:stop" : "Serial:start";
|
||||
case LOCAL_STATUS_ACTION_WIFI_POWER:
|
||||
return snapshot->wifi_available && snapshot->wifi.started ? "WiFi:stop" : "WiFi:start";
|
||||
case LOCAL_STATUS_ACTION_WIFI_RECONNECT:
|
||||
return "WiFi:reconnect";
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
return snapshot->web_available && snapshot->web.running ? "HTTPS:stop" : "HTTPS:start";
|
||||
case LOCAL_STATUS_ACTION_SSH:
|
||||
return snapshot->ssh_available && snapshot->ssh.running ? "SSH:stop" : "SSH:start";
|
||||
case LOCAL_STATUS_ACTION_REVOKE_WRITER:
|
||||
return "Writer:revoke";
|
||||
case LOCAL_STATUS_ACTION_DISPLAY_OFF:
|
||||
return "Display:off";
|
||||
case LOCAL_STATUS_ACTION_REBOOT:
|
||||
return "Device:reboot";
|
||||
default:
|
||||
return "Unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
static bool action_is_stop(local_status_action_t action,
|
||||
const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
switch (action) {
|
||||
case LOCAL_STATUS_ACTION_SERIAL:
|
||||
return snapshot->serial_running;
|
||||
case LOCAL_STATUS_ACTION_WIFI_POWER:
|
||||
return snapshot->wifi_available && snapshot->wifi.started;
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
return snapshot->web_available && snapshot->web.running;
|
||||
case LOCAL_STATUS_ACTION_SSH:
|
||||
return snapshot->ssh_available && snapshot->ssh.running;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static const char *confirmed_action_label(const local_status_ui_state_t *state)
|
||||
{
|
||||
switch (state->confirmed_action) {
|
||||
case LOCAL_STATUS_ACTION_SERIAL:
|
||||
return state->confirmed_stop ? "Serial:stop" : "Serial:start";
|
||||
case LOCAL_STATUS_ACTION_WIFI_POWER:
|
||||
return state->confirmed_stop ? "WiFi:stop" : "WiFi:start";
|
||||
case LOCAL_STATUS_ACTION_WIFI_RECONNECT:
|
||||
return "WiFi:reconnect";
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
return state->confirmed_stop ? "HTTPS:stop" : "HTTPS:start";
|
||||
case LOCAL_STATUS_ACTION_SSH:
|
||||
return state->confirmed_stop ? "SSH:stop" : "SSH:start";
|
||||
case LOCAL_STATUS_ACTION_REVOKE_WRITER:
|
||||
return "Writer:revoke";
|
||||
case LOCAL_STATUS_ACTION_DISPLAY_OFF:
|
||||
return "Display:off";
|
||||
case LOCAL_STATUS_ACTION_REBOOT:
|
||||
return "Device:reboot";
|
||||
default:
|
||||
return "Unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
static bool action_requires_confirmation(local_status_action_t action,
|
||||
const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
switch (action) {
|
||||
case LOCAL_STATUS_ACTION_SERIAL:
|
||||
return snapshot->serial_running;
|
||||
case LOCAL_STATUS_ACTION_WIFI_POWER:
|
||||
return snapshot->wifi_available && snapshot->wifi.started;
|
||||
case LOCAL_STATUS_ACTION_WIFI_RECONNECT:
|
||||
return true;
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
return snapshot->web_available && snapshot->web.running;
|
||||
case LOCAL_STATUS_ACTION_SSH:
|
||||
return snapshot->ssh_available && snapshot->ssh.running;
|
||||
case LOCAL_STATUS_ACTION_REVOKE_WRITER:
|
||||
case LOCAL_STATUS_ACTION_REBOOT:
|
||||
return true;
|
||||
case LOCAL_STATUS_ACTION_DISPLAY_OFF:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void render_menu_page(const local_status_ui_state_t *state,
|
||||
const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "Controls");
|
||||
for (uint8_t row = 0U; row < LOCAL_STATUS_UI_MENU_VISIBLE_ROWS; ++row) {
|
||||
uint8_t index = (uint8_t)((state->menu_action + row) % LOCAL_STATUS_ACTION_COUNT);
|
||||
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
|
||||
format_text(line, "%c%s", row == 0U ? '>' : ' ',
|
||||
action_label((local_status_action_t)index, snapshot));
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U,
|
||||
(uint8_t)((row + 1U) * 8U), line);
|
||||
}
|
||||
}
|
||||
|
||||
static void render_confirm_page(const local_status_ui_state_t *state,
|
||||
const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
|
||||
(void)snapshot;
|
||||
format_text(line, "Confirm:%s", confirmed_action_label(state));
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, line);
|
||||
switch (state->confirmed_action) {
|
||||
case LOCAL_STATUS_ACTION_REVOKE_WRITER:
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U,
|
||||
"Writer loses control");
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_REBOOT:
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U,
|
||||
"Unsaved RAM is lost");
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_WIFI_POWER:
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U,
|
||||
"Network access ends");
|
||||
break;
|
||||
default:
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U,
|
||||
"Active service changes");
|
||||
break;
|
||||
}
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U,
|
||||
"Hold select 2s");
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U,
|
||||
"Prev:cancel");
|
||||
}
|
||||
|
||||
static void render_result_page(const local_status_ui_state_t *state,
|
||||
const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
|
||||
(void)snapshot;
|
||||
format_text(line, "Action:%s", confirmed_action_label(state));
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, line);
|
||||
if (state->restart_pending) {
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U,
|
||||
"Restarting...");
|
||||
} else if (state->result_error == ESP_OK) {
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U,
|
||||
state->result_requested ? "Requested" : "Completed");
|
||||
} else {
|
||||
format_text(line, "Error:%s", esp_err_to_name(state->result_error));
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U, line);
|
||||
}
|
||||
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U,
|
||||
"Select:menu");
|
||||
}
|
||||
|
||||
static bool render_ui(const local_status_ui_state_t *state,
|
||||
const local_status_snapshot_t *snapshot)
|
||||
{
|
||||
if (local_display_frame_begin() != ESP_OK) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (diagnostic_hold_is_active(xTaskGetTickCount())) {
|
||||
local_display_frame_cancel();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
local_display_frame_clear_all();
|
||||
render_status_panel(snapshot);
|
||||
switch (page) {
|
||||
case LOCAL_STATUS_PAGE_OVERVIEW:
|
||||
render_overview_page(snapshot);
|
||||
switch (state->mode) {
|
||||
case LOCAL_STATUS_MODE_STATUS:
|
||||
switch (state->page) {
|
||||
case LOCAL_STATUS_PAGE_OVERVIEW:
|
||||
render_overview_page(snapshot);
|
||||
break;
|
||||
case LOCAL_STATUS_PAGE_RS232:
|
||||
render_rs232_page(snapshot);
|
||||
break;
|
||||
case LOCAL_STATUS_PAGE_BROKER:
|
||||
render_broker_page(snapshot);
|
||||
break;
|
||||
case LOCAL_STATUS_PAGE_NETWORK:
|
||||
render_network_page(snapshot);
|
||||
break;
|
||||
default:
|
||||
local_display_frame_cancel();
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case LOCAL_STATUS_PAGE_RS232:
|
||||
render_rs232_page(snapshot);
|
||||
case LOCAL_STATUS_MODE_MENU:
|
||||
render_menu_page(state, snapshot);
|
||||
break;
|
||||
case LOCAL_STATUS_PAGE_BROKER:
|
||||
render_broker_page(snapshot);
|
||||
case LOCAL_STATUS_MODE_CONFIRM:
|
||||
render_confirm_page(state, snapshot);
|
||||
break;
|
||||
case LOCAL_STATUS_PAGE_NETWORK:
|
||||
render_network_page(snapshot);
|
||||
case LOCAL_STATUS_MODE_RESULT:
|
||||
render_result_page(state, snapshot);
|
||||
break;
|
||||
default:
|
||||
local_display_frame_cancel();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t error = local_display_frame_end();
|
||||
if (error != ESP_OK && error != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGW(TAG, "Display refresh failed: %s", esp_err_to_name(error));
|
||||
}
|
||||
return error == ESP_OK;
|
||||
}
|
||||
|
||||
static bool poll_buttons(local_status_button_state_t states[LOCAL_STATUS_BUTTON_COUNT],
|
||||
local_status_button_t *pressed_button)
|
||||
static bool poll_button_event(local_status_button_state_t states[LOCAL_STATUS_BUTTON_COUNT],
|
||||
bool *chord_latched,
|
||||
local_status_button_event_t *event)
|
||||
{
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
bool event = false;
|
||||
bool pressed_edge[LOCAL_STATUS_BUTTON_COUNT] = {0};
|
||||
bool short_release[LOCAL_STATUS_BUTTON_COUNT] = {0};
|
||||
uint8_t pressed_count = 0U;
|
||||
|
||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||
bool pressed = gpio_get_level(s_button_gpios[i]) == 0;
|
||||
@@ -680,12 +913,62 @@ static bool poll_buttons(local_status_button_state_t states[LOCAL_STATUS_BUTTON_
|
||||
(now - states[i].candidate_since) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_DEBOUNCE_MS)) {
|
||||
states[i].stable_pressed = states[i].candidate_pressed;
|
||||
if (states[i].stable_pressed) {
|
||||
*pressed_button = (local_status_button_t)i;
|
||||
event = true;
|
||||
states[i].pressed_since = now;
|
||||
states[i].long_emitted = false;
|
||||
pressed_edge[i] = true;
|
||||
} else if (!states[i].long_emitted && !states[i].suppress_gesture) {
|
||||
short_release[i] = true;
|
||||
}
|
||||
if (!states[i].stable_pressed) {
|
||||
states[i].suppress_gesture = false;
|
||||
}
|
||||
}
|
||||
if (states[i].stable_pressed) {
|
||||
++pressed_count;
|
||||
}
|
||||
}
|
||||
return event;
|
||||
|
||||
if (pressed_count > 1U) {
|
||||
*chord_latched = true;
|
||||
return false;
|
||||
}
|
||||
if (*chord_latched) {
|
||||
if (pressed_count == 0U) {
|
||||
*chord_latched = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||
if (pressed_edge[i]) {
|
||||
*event = (local_status_button_event_t){
|
||||
.button = (local_status_button_t)i,
|
||||
.type = LOCAL_STATUS_BUTTON_EVENT_PRESS,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||
if (states[i].stable_pressed && !states[i].long_emitted &&
|
||||
!states[i].suppress_gesture &&
|
||||
(now - states[i].pressed_since) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_CONFIRM_HOLD_MS)) {
|
||||
states[i].long_emitted = true;
|
||||
*event = (local_status_button_event_t){
|
||||
.button = (local_status_button_t)i,
|
||||
.type = LOCAL_STATUS_BUTTON_EVENT_LONG_REACHED,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||
if (short_release[i]) {
|
||||
*event = (local_status_button_event_t){
|
||||
.button = (local_status_button_t)i,
|
||||
.type = LOCAL_STATUS_BUTTON_EVENT_SHORT_RELEASE,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void sync_display_power(local_status_power_t *power)
|
||||
@@ -721,6 +1004,184 @@ static bool wake_display(local_status_power_t *power)
|
||||
return true;
|
||||
}
|
||||
|
||||
static esp_err_t stop_display_from_control(void)
|
||||
{
|
||||
if (s_diagnostic_gate == NULL ||
|
||||
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(100U)) != pdTRUE) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t error = diagnostic_hold_is_active(xTaskGetTickCount())
|
||||
? ESP_ERR_INVALID_STATE
|
||||
: local_display_stop();
|
||||
(void)xSemaphoreGive(s_diagnostic_gate);
|
||||
return error;
|
||||
}
|
||||
|
||||
static void execute_action(local_status_ui_state_t *state,
|
||||
const local_status_snapshot_t *snapshot,
|
||||
local_status_power_t *power)
|
||||
{
|
||||
local_status_action_t action = state->confirmed_action;
|
||||
esp_err_t error = ESP_OK;
|
||||
bool requested = false;
|
||||
|
||||
switch (action) {
|
||||
case LOCAL_STATUS_ACTION_SERIAL:
|
||||
if (snapshot->serial_running != state->confirmed_stop) {
|
||||
error = ESP_ERR_INVALID_STATE;
|
||||
} else {
|
||||
error = state->confirmed_stop ? serial_service_stop() : serial_service_start();
|
||||
}
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_WIFI_POWER:
|
||||
if (!snapshot->wifi_available || snapshot->wifi.started != state->confirmed_stop) {
|
||||
error = ESP_ERR_INVALID_STATE;
|
||||
} else {
|
||||
error = state->confirmed_stop ? wifi_manager_stop() : wifi_manager_start();
|
||||
requested = error == ESP_OK;
|
||||
}
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_WIFI_RECONNECT:
|
||||
error = snapshot->wifi_available && snapshot->wifi.started
|
||||
? wifi_manager_reconnect()
|
||||
: ESP_ERR_INVALID_STATE;
|
||||
requested = error == ESP_OK;
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
if (!snapshot->web_available || snapshot->web.running != state->confirmed_stop) {
|
||||
error = ESP_ERR_INVALID_STATE;
|
||||
} else {
|
||||
error = state->confirmed_stop
|
||||
? web_server_stop()
|
||||
: (network_available(snapshot) ? web_server_start() : ESP_ERR_INVALID_STATE);
|
||||
}
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_SSH:
|
||||
if (!snapshot->ssh_available || snapshot->ssh.running != state->confirmed_stop) {
|
||||
error = ESP_ERR_INVALID_STATE;
|
||||
} else {
|
||||
error = state->confirmed_stop
|
||||
? ssh_transport_stop()
|
||||
: (network_available(snapshot) ? ssh_transport_start() : ESP_ERR_INVALID_STATE);
|
||||
}
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_REVOKE_WRITER:
|
||||
error = session_broker_force_release_writer(state->expected_writer_id);
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_DISPLAY_OFF:
|
||||
error = stop_display_from_control();
|
||||
if (error == ESP_OK) {
|
||||
*power = LOCAL_STATUS_POWER_OFF;
|
||||
}
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_REBOOT:
|
||||
state->restart_pending = true;
|
||||
break;
|
||||
default:
|
||||
error = ESP_ERR_INVALID_ARG;
|
||||
break;
|
||||
}
|
||||
|
||||
state->result_error = error;
|
||||
state->result_requested = requested;
|
||||
state->mode = LOCAL_STATUS_MODE_RESULT;
|
||||
state->mode_since = xTaskGetTickCount();
|
||||
}
|
||||
|
||||
static void handle_button_event(local_status_ui_state_t *state,
|
||||
const local_status_snapshot_t *snapshot,
|
||||
const local_status_button_event_t *event,
|
||||
local_status_button_state_t buttons[LOCAL_STATUS_BUTTON_COUNT],
|
||||
local_status_power_t *power,
|
||||
bool *render_requested)
|
||||
{
|
||||
if (event->type == LOCAL_STATUS_BUTTON_EVENT_PRESS &&
|
||||
*power != LOCAL_STATUS_POWER_ACTIVE) {
|
||||
buttons[event->button].suppress_gesture = true;
|
||||
*render_requested = wake_display(power);
|
||||
return;
|
||||
}
|
||||
if (event->type == LOCAL_STATUS_BUTTON_EVENT_PRESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->type == LOCAL_STATUS_BUTTON_EVENT_LONG_REACHED) {
|
||||
if (event->button == LOCAL_STATUS_BUTTON_PREVIOUS) {
|
||||
state->mode = LOCAL_STATUS_MODE_STATUS;
|
||||
state->page = LOCAL_STATUS_PAGE_OVERVIEW;
|
||||
state->mode_since = xTaskGetTickCount();
|
||||
*render_requested = true;
|
||||
} else if (event->button == LOCAL_STATUS_BUTTON_SELECT &&
|
||||
state->mode == LOCAL_STATUS_MODE_CONFIRM) {
|
||||
execute_action(state, snapshot, power);
|
||||
*render_requested = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->type != LOCAL_STATUS_BUTTON_EVENT_SHORT_RELEASE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state->mode) {
|
||||
case LOCAL_STATUS_MODE_STATUS:
|
||||
if (event->button == LOCAL_STATUS_BUTTON_PREVIOUS) {
|
||||
state->page = state->page == LOCAL_STATUS_PAGE_OVERVIEW
|
||||
? LOCAL_STATUS_PAGE_COUNT - 1U
|
||||
: (local_status_page_t)(state->page - 1U);
|
||||
} else if (event->button == LOCAL_STATUS_BUTTON_NEXT) {
|
||||
state->page = (local_status_page_t)((state->page + 1U) % LOCAL_STATUS_PAGE_COUNT);
|
||||
} else if (event->button == LOCAL_STATUS_BUTTON_SELECT) {
|
||||
state->mode = LOCAL_STATUS_MODE_MENU;
|
||||
state->mode_since = xTaskGetTickCount();
|
||||
}
|
||||
*render_requested = true;
|
||||
break;
|
||||
case LOCAL_STATUS_MODE_MENU:
|
||||
if (event->button == LOCAL_STATUS_BUTTON_PREVIOUS) {
|
||||
state->menu_action = state->menu_action == LOCAL_STATUS_ACTION_SERIAL
|
||||
? LOCAL_STATUS_ACTION_COUNT - 1U
|
||||
: (local_status_action_t)(state->menu_action - 1U);
|
||||
} else if (event->button == LOCAL_STATUS_BUTTON_NEXT) {
|
||||
state->menu_action =
|
||||
(local_status_action_t)((state->menu_action + 1U) % LOCAL_STATUS_ACTION_COUNT);
|
||||
} else if (event->button == LOCAL_STATUS_BUTTON_SELECT) {
|
||||
state->confirmed_action = state->menu_action;
|
||||
state->confirmed_stop = action_is_stop(state->menu_action, snapshot);
|
||||
state->expected_writer_id = snapshot->broker_available
|
||||
? snapshot->broker.writer_id
|
||||
: SESSION_BROKER_NO_CLIENT;
|
||||
if (action_requires_confirmation(state->menu_action, snapshot)) {
|
||||
state->mode = LOCAL_STATUS_MODE_CONFIRM;
|
||||
state->mode_since = xTaskGetTickCount();
|
||||
} else {
|
||||
execute_action(state, snapshot, power);
|
||||
}
|
||||
}
|
||||
*render_requested = true;
|
||||
break;
|
||||
case LOCAL_STATUS_MODE_CONFIRM:
|
||||
if (event->button == LOCAL_STATUS_BUTTON_PREVIOUS) {
|
||||
state->mode = LOCAL_STATUS_MODE_MENU;
|
||||
state->mode_since = xTaskGetTickCount();
|
||||
*render_requested = true;
|
||||
}
|
||||
break;
|
||||
case LOCAL_STATUS_MODE_RESULT:
|
||||
if (event->button == LOCAL_STATUS_BUTTON_PREVIOUS) {
|
||||
state->mode = LOCAL_STATUS_MODE_STATUS;
|
||||
state->page = LOCAL_STATUS_PAGE_OVERVIEW;
|
||||
} else if (event->button == LOCAL_STATUS_BUTTON_SELECT) {
|
||||
state->mode = LOCAL_STATUS_MODE_MENU;
|
||||
}
|
||||
state->mode_since = xTaskGetTickCount();
|
||||
*render_requested = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_display_power(local_status_power_t *power,
|
||||
TickType_t now,
|
||||
TickType_t last_activity)
|
||||
@@ -758,59 +1219,100 @@ static void update_display_power(local_status_power_t *power,
|
||||
static void local_status_ui_task(void *context)
|
||||
{
|
||||
(void)context;
|
||||
local_status_page_t page = LOCAL_STATUS_PAGE_OVERVIEW;
|
||||
local_status_ui_state_t state = {
|
||||
.mode = LOCAL_STATUS_MODE_STATUS,
|
||||
.page = LOCAL_STATUS_PAGE_OVERVIEW,
|
||||
.menu_action = LOCAL_STATUS_ACTION_SERIAL,
|
||||
.confirmed_action = LOCAL_STATUS_ACTION_SERIAL,
|
||||
};
|
||||
local_status_power_t power = LOCAL_STATUS_POWER_ACTIVE;
|
||||
local_status_button_state_t buttons[LOCAL_STATUS_BUTTON_COUNT] = {0};
|
||||
TickType_t last_render = 0U;
|
||||
TickType_t last_activity = xTaskGetTickCount();
|
||||
uint32_t observed_activity_sequence = 0U;
|
||||
bool chord_latched = false;
|
||||
state.mode_since = last_activity;
|
||||
|
||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||
bool pressed = gpio_get_level(s_button_gpios[i]) == 0;
|
||||
buttons[i].stable_pressed = pressed;
|
||||
buttons[i].candidate_pressed = pressed;
|
||||
buttons[i].candidate_since = last_activity;
|
||||
buttons[i].pressed_since = last_activity;
|
||||
/* A button held during boot cannot become a confirmation gesture. */
|
||||
buttons[i].long_emitted = pressed;
|
||||
buttons[i].suppress_gesture = pressed;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
local_status_button_t button;
|
||||
bool button_event = poll_buttons(buttons, &button);
|
||||
local_status_button_event_t event;
|
||||
bool have_event = poll_button_event(buttons, &chord_latched, &event);
|
||||
bool render_requested = false;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
bool diagnostics_held = diagnostic_hold_is_active(now);
|
||||
|
||||
if (consume_external_activity(&observed_activity_sequence)) {
|
||||
last_activity = now;
|
||||
}
|
||||
sync_display_power(&power);
|
||||
|
||||
if (button_event) {
|
||||
last_activity = now;
|
||||
if (power != LOCAL_STATUS_POWER_ACTIVE) {
|
||||
/* The first press wakes only, preventing accidental navigation. */
|
||||
bool wake_gesture = false;
|
||||
if (power != LOCAL_STATUS_POWER_ACTIVE) {
|
||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||
if (buttons[i].stable_pressed) {
|
||||
buttons[i].suppress_gesture = true;
|
||||
wake_gesture = true;
|
||||
}
|
||||
}
|
||||
if (wake_gesture && !diagnostics_held) {
|
||||
last_activity = now;
|
||||
render_requested = wake_display(&power);
|
||||
} else if (button == LOCAL_STATUS_BUTTON_PREVIOUS) {
|
||||
page = page == LOCAL_STATUS_PAGE_OVERVIEW
|
||||
? LOCAL_STATUS_PAGE_COUNT - 1U
|
||||
: (local_status_page_t)(page - 1U);
|
||||
render_requested = true;
|
||||
} else if (button == LOCAL_STATUS_BUTTON_NEXT) {
|
||||
page = (local_status_page_t)((page + 1U) % LOCAL_STATUS_PAGE_COUNT);
|
||||
render_requested = true;
|
||||
} else {
|
||||
render_requested = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (have_event && !wake_gesture) {
|
||||
last_activity = now;
|
||||
if (!diagnostics_held) {
|
||||
local_status_snapshot_t snapshot;
|
||||
collect_snapshot(&snapshot);
|
||||
handle_button_event(&state, &snapshot, &event, buttons, &power,
|
||||
&render_requested);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.mode == LOCAL_STATUS_MODE_CONFIRM &&
|
||||
(now - state.mode_since) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_MENU_TIMEOUT_MS)) {
|
||||
state.mode = LOCAL_STATUS_MODE_MENU;
|
||||
state.mode_since = now;
|
||||
render_requested = true;
|
||||
} else if (state.mode == LOCAL_STATUS_MODE_MENU &&
|
||||
(now - last_activity) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_MENU_TIMEOUT_MS)) {
|
||||
state.mode = LOCAL_STATUS_MODE_STATUS;
|
||||
state.page = LOCAL_STATUS_PAGE_OVERVIEW;
|
||||
state.mode_since = now;
|
||||
render_requested = true;
|
||||
} else if (state.mode == LOCAL_STATUS_MODE_RESULT && !state.restart_pending &&
|
||||
(now - state.mode_since) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_RESULT_TIMEOUT_MS)) {
|
||||
state.mode = LOCAL_STATUS_MODE_MENU;
|
||||
state.mode_since = now;
|
||||
render_requested = true;
|
||||
}
|
||||
|
||||
update_display_power(&power, now, last_activity);
|
||||
bool diagnostics_held = diagnostic_hold_is_active(now);
|
||||
diagnostics_held = diagnostic_hold_is_active(now);
|
||||
bool rendered = false;
|
||||
if (power != LOCAL_STATUS_POWER_OFF && !diagnostics_held &&
|
||||
(render_requested ||
|
||||
(now - last_render) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_REFRESH_MS))) {
|
||||
local_status_snapshot_t snapshot;
|
||||
collect_snapshot(&snapshot);
|
||||
render_page(page, &snapshot);
|
||||
rendered = render_ui(&state, &snapshot);
|
||||
last_render = now;
|
||||
}
|
||||
if (state.restart_pending &&
|
||||
(rendered || (now - state.mode_since) >= pdMS_TO_TICKS(2000U))) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500U));
|
||||
esp_restart();
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(LOCAL_STATUS_UI_BUTTON_POLL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user