Add persistent OLED UI settings and recovery

This commit is contained in:
2026-08-29 20:18:08 +02:00
parent 16c0c02389
commit 276559536b
14 changed files with 538 additions and 38 deletions
+82 -24
View File
@@ -35,8 +35,6 @@
#define LOCAL_STATUS_UI_TEXT_CAPACITY 22U
#define LOCAL_STATUS_UI_CLIENT_ROWS 4U
#define LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS 30000U
#define LOCAL_STATUS_UI_DIM_DELAY_MS (5U * 60U * 1000U)
#define LOCAL_STATUS_UI_OFF_DELAY_MS (10U * 60U * 1000U)
#define LOCAL_STATUS_UI_DIM_CONTRAST 1U
#define LOCAL_STATUS_UI_ACTIVE_CONTRAST 127U
#define LOCAL_STATUS_UI_ICON_SIZE 8U
@@ -45,6 +43,7 @@
#define LOCAL_STATUS_UI_MENU_TIMEOUT_MS 30000U
#define LOCAL_STATUS_UI_RESULT_TIMEOUT_MS 2500U
#define LOCAL_STATUS_UI_CONFIRM_HOLD_MS 2000U
#define LOCAL_STATUS_UI_STUCK_BUTTON_MS 10000U
static const char *TAG = "local_status_ui";
@@ -117,6 +116,7 @@ typedef struct {
TickType_t pressed_since;
bool long_emitted;
bool suppress_gesture;
bool quarantined;
} local_status_button_state_t;
typedef struct {
@@ -201,6 +201,8 @@ static portMUX_TYPE s_timing_mux = portMUX_INITIALIZER_UNLOCKED;
static bool s_diagnostic_hold_active;
static TickType_t s_diagnostic_hold_started;
static uint32_t s_external_activity_sequence;
static local_ui_config_t s_config;
static bool s_config_available;
static const gpio_num_t s_button_gpios[LOCAL_STATUS_BUTTON_COUNT] = {
LOCAL_UI_BUTTON_PREVIOUS_GPIO,
@@ -417,7 +419,9 @@ static bool snapshot_has_alert(const local_status_snapshot_t *snapshot)
return !snapshot->broker_available || !snapshot->usb_available ||
!snapshot->wifi_available || !snapshot->web_available ||
!snapshot->web_serial_available || !snapshot->ssh_available ||
(snapshot->wifi_available && snapshot->wifi.last_error != ESP_OK) ||
(snapshot->wifi_available &&
(snapshot->wifi.last_error != ESP_OK ||
snapshot->wifi.counters.queue_drops != 0U)) ||
(snapshot->web_available && snapshot->web.last_error != ESP_OK) ||
(snapshot->ssh_available && snapshot->ssh.last_error != ESP_OK) ||
snapshot->serial_counters.rx_dropped_bytes != 0U ||
@@ -927,9 +931,16 @@ static bool poll_button_event(local_status_button_state_t states[LOCAL_STATUS_BU
}
if (!states[i].stable_pressed) {
states[i].suppress_gesture = false;
states[i].quarantined = false;
}
}
if (states[i].stable_pressed) {
if (states[i].stable_pressed && !states[i].quarantined &&
(now - states[i].pressed_since) >=
pdMS_TO_TICKS(LOCAL_STATUS_UI_STUCK_BUTTON_MS)) {
states[i].quarantined = true;
states[i].suppress_gesture = true;
}
if (states[i].stable_pressed && !states[i].quarantined) {
++pressed_count;
}
}
@@ -945,7 +956,7 @@ static bool poll_button_event(local_status_button_state_t states[LOCAL_STATUS_BU
return false;
}
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
if (pressed_edge[i]) {
if (pressed_edge[i] && !states[i].quarantined) {
*event = (local_status_button_event_t){
.button = (local_status_button_t)i,
.type = LOCAL_STATUS_BUTTON_EVENT_PRESS,
@@ -954,7 +965,8 @@ static bool poll_button_event(local_status_button_state_t states[LOCAL_STATUS_BU
}
}
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
if (states[i].stable_pressed && !states[i].long_emitted &&
if (states[i].stable_pressed && !states[i].quarantined &&
!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;
@@ -1196,13 +1208,16 @@ static void handle_button_event(local_status_ui_state_t *state,
static void update_display_power(local_status_power_t *power,
TickType_t now,
TickType_t last_activity)
TickType_t last_activity,
const local_ui_config_t *config)
{
TickType_t inactive = now - last_activity;
bool should_stop = *power != LOCAL_STATUS_POWER_OFF &&
inactive >= pdMS_TO_TICKS(LOCAL_STATUS_UI_OFF_DELAY_MS);
bool should_dim = *power == LOCAL_STATUS_POWER_ACTIVE &&
inactive >= pdMS_TO_TICKS(LOCAL_STATUS_UI_DIM_DELAY_MS);
bool should_stop = config->off_timeout_seconds != 0U &&
*power != LOCAL_STATUS_POWER_OFF &&
inactive >= pdMS_TO_TICKS(config->off_timeout_seconds * 1000U);
bool should_dim = config->dim_timeout_seconds != 0U &&
*power == LOCAL_STATUS_POWER_ACTIVE &&
inactive >= pdMS_TO_TICKS(config->dim_timeout_seconds * 1000U);
if ((!should_stop && !should_dim) || s_diagnostic_gate == NULL ||
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(100U)) != pdTRUE) {
return;
@@ -1267,21 +1282,14 @@ static void local_status_ui_task(void *context)
last_activity = now;
}
sync_display_power(&power);
bool wake_gesture = false;
if (power != LOCAL_STATUS_POWER_ACTIVE) {
if (diagnostics_held) {
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);
}
}
if (have_event && !wake_gesture) {
if (have_event) {
last_activity = now;
if (!diagnostics_held) {
local_status_snapshot_t snapshot;
@@ -1309,7 +1317,11 @@ static void local_status_ui_task(void *context)
render_requested = true;
}
update_display_power(&power, now, last_activity);
local_ui_config_t config;
portENTER_CRITICAL(&s_timing_mux);
config = s_config;
portEXIT_CRITICAL(&s_timing_mux);
update_display_power(&power, now, last_activity, &config);
diagnostics_held = diagnostic_hold_is_active(now);
bool rendered = false;
if (power != LOCAL_STATUS_POWER_OFF && !diagnostics_held &&
@@ -1344,14 +1356,55 @@ void local_status_ui_hold_for_diagnostics(void)
}
}
esp_err_t local_status_ui_start(void)
esp_err_t local_status_ui_get_config(local_ui_config_t *config)
{
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
portENTER_CRITICAL(&s_timing_mux);
bool available = s_config_available;
*config = s_config;
portEXIT_CRITICAL(&s_timing_mux);
return available ? ESP_OK : ESP_ERR_INVALID_STATE;
}
esp_err_t local_status_ui_apply_config(const local_ui_config_t *config)
{
esp_err_t error = local_ui_config_validate(config);
if (error != ESP_OK) {
return error;
}
portENTER_CRITICAL(&s_timing_mux);
if (!s_config_available) {
portEXIT_CRITICAL(&s_timing_mux);
return ESP_ERR_INVALID_STATE;
}
s_config = *config;
++s_external_activity_sequence;
portEXIT_CRITICAL(&s_timing_mux);
return ESP_OK;
}
esp_err_t local_status_ui_start(const local_ui_config_t *config)
{
esp_err_t error = local_ui_config_validate(config);
if (error != ESP_OK) {
return error;
}
if (s_task != NULL || s_diagnostic_gate != NULL) {
return ESP_ERR_INVALID_STATE;
}
portENTER_CRITICAL(&s_timing_mux);
s_config = *config;
s_config_available = true;
portEXIT_CRITICAL(&s_timing_mux);
s_diagnostic_gate = xSemaphoreCreateMutexStatic(&s_diagnostic_gate_storage);
if (s_diagnostic_gate == NULL) {
portENTER_CRITICAL(&s_timing_mux);
s_config_available = false;
portEXIT_CRITICAL(&s_timing_mux);
return ESP_ERR_NO_MEM;
}
@@ -1362,11 +1415,16 @@ esp_err_t local_status_ui_start(void)
s_task = NULL;
vSemaphoreDelete(s_diagnostic_gate);
s_diagnostic_gate = NULL;
portENTER_CRITICAL(&s_timing_mux);
s_config_available = false;
portEXIT_CRITICAL(&s_timing_mux);
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG,
"Read-only status UI started at %u Hz maximum; dim/off after 5/10 minutes",
1000U / LOCAL_STATUS_UI_REFRESH_MS);
"Local status/control UI started at %u Hz maximum; dim/off after %u/%u seconds",
1000U / LOCAL_STATUS_UI_REFRESH_MS,
(unsigned int)config->dim_timeout_seconds,
(unsigned int)config->off_timeout_seconds);
return ESP_OK;
}