Files
ESP32_Serial_Swiss_Army_Knife/src/local_status_ui.c
T

434 lines
17 KiB
C

/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded read-only OLED status pages. */
#include "local_status_ui.h"
#include <inttypes.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include "board_pins.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_netif_ip_addr.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "local_display.h"
#include "serial_config.h"
#include "serial_service.h"
#include "session_broker.h"
#include "ssh_transport.h"
#include "usb_cdc_transport.h"
#include "web_serial_transport.h"
#include "web_server.h"
#include "wifi_manager.h"
#define LOCAL_STATUS_UI_REFRESH_MS 250U
#define LOCAL_STATUS_UI_BUTTON_POLL_MS 10U
#define LOCAL_STATUS_UI_DEBOUNCE_MS 30U
#define LOCAL_STATUS_UI_TASK_STACK_SIZE 6144U
#define LOCAL_STATUS_UI_TASK_PRIORITY (tskIDLE_PRIORITY + 1U)
#define LOCAL_STATUS_UI_TEXT_CAPACITY 22U
#define LOCAL_STATUS_UI_CLIENT_ROWS 4U
#define LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS 30000U
static const char *TAG = "local_status_ui";
typedef enum {
LOCAL_STATUS_PAGE_OVERVIEW = 0,
LOCAL_STATUS_PAGE_RS232,
LOCAL_STATUS_PAGE_BROKER,
LOCAL_STATUS_PAGE_NETWORK,
LOCAL_STATUS_PAGE_COUNT,
} local_status_page_t;
typedef enum {
LOCAL_STATUS_BUTTON_PREVIOUS = 0,
LOCAL_STATUS_BUTTON_SELECT,
LOCAL_STATUS_BUTTON_NEXT,
LOCAL_STATUS_BUTTON_COUNT,
} local_status_button_t;
typedef struct {
bool stable_pressed;
bool candidate_pressed;
TickType_t candidate_since;
} local_status_button_state_t;
typedef struct {
bool serial_config_available;
bool broker_available;
bool usb_available;
bool wifi_available;
bool web_available;
bool web_serial_available;
bool ssh_available;
bool serial_running;
serial_config_t serial_config;
serial_modem_state_t modem;
serial_service_counters_t serial_counters;
session_broker_global_snapshot_t broker;
session_broker_client_snapshot_t clients[SESSION_BROKER_MAX_CLIENTS];
size_t client_count;
usb_cdc_transport_snapshot_t usb;
wifi_manager_snapshot_t wifi;
web_server_snapshot_t web;
web_serial_transport_snapshot_t web_serial;
ssh_transport_snapshot_t ssh;
} local_status_snapshot_t;
static TaskHandle_t s_task;
static volatile TickType_t s_diagnostic_hold_until;
static const gpio_num_t s_button_gpios[LOCAL_STATUS_BUTTON_COUNT] = {
LOCAL_UI_BUTTON_PREVIOUS_GPIO,
LOCAL_UI_BUTTON_SELECT_GPIO,
LOCAL_UI_BUTTON_NEXT_GPIO,
};
static void format_text(char output[LOCAL_STATUS_UI_TEXT_CAPACITY], const char *format, ...)
{
va_list arguments;
va_start(arguments, format);
(void)vsnprintf(output, LOCAL_STATUS_UI_TEXT_CAPACITY, format, arguments);
va_end(arguments);
}
static const char *broker_type_to_string(session_broker_client_type_t type)
{
switch (type) {
case SESSION_BROKER_CLIENT_CONSOLE:
return "CON";
case SESSION_BROKER_CLIENT_USB:
return "USB";
case SESSION_BROKER_CLIENT_WEB:
return "WEB";
case SESSION_BROKER_CLIENT_SSH:
return "SSH";
case SESSION_BROKER_CLIENT_INTERNAL:
return "INT";
default:
return "?";
}
}
static void format_ipv4(uint32_t address, char output[16])
{
if (address == 0U) {
memcpy(output, "0.0.0.0", sizeof("0.0.0.0"));
return;
}
esp_ip4_addr_t ip = {.addr = address};
int written = snprintf(output, 16U, IPSTR, IP2STR(&ip));
if (written < 0 || written >= 16) {
memcpy(output, "0.0.0.0", sizeof("0.0.0.0"));
}
}
static void collect_snapshot(local_status_snapshot_t *snapshot)
{
memset(snapshot, 0, sizeof(*snapshot));
snapshot->serial_running = serial_service_is_running();
snapshot->serial_config_available =
serial_service_get_config(&snapshot->serial_config) == ESP_OK;
serial_service_get_modem_state(&snapshot->modem);
serial_service_get_counters(&snapshot->serial_counters);
snapshot->broker_available =
session_broker_get_global_snapshot(&snapshot->broker) == ESP_OK;
if (snapshot->broker_available) {
snapshot->client_count = session_broker_list_clients(
snapshot->clients, SESSION_BROKER_MAX_CLIENTS);
}
snapshot->usb_available = usb_cdc_transport_get_snapshot(&snapshot->usb) == ESP_OK;
snapshot->wifi_available = wifi_manager_get_snapshot(&snapshot->wifi) == ESP_OK;
snapshot->web_available = web_server_get_snapshot(&snapshot->web) == ESP_OK;
snapshot->web_serial_available =
web_serial_transport_get_snapshot(&snapshot->web_serial) == ESP_OK;
snapshot->ssh_available = ssh_transport_get_snapshot(&snapshot->ssh) == ESP_OK;
}
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->web_available && snapshot->web.last_error != ESP_OK) ||
(snapshot->ssh_available && snapshot->ssh.last_error != ESP_OK) ||
snapshot->serial_counters.rx_dropped_bytes != 0U ||
snapshot->serial_counters.tx_dropped_bytes != 0U ||
(snapshot->broker_available &&
snapshot->broker.counters.output_dropped_bytes != 0U);
}
static void render_status_panel(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
int rssi = snapshot->wifi_available ? snapshot->wifi.sta_rssi : 0;
unsigned int clients = snapshot->broker_available ? snapshot->broker.connected_clients : 0U;
unsigned int web_sessions =
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U;
unsigned int ssh_sessions = snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U;
format_text(line, "SER %s WF %d%s", snapshot->serial_running ? "ON" : "OFF", rssi,
snapshot_has_alert(snapshot) ? "!" : "");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, 0U, 0U, line);
format_text(line, "C%u U%s W%u S%u", clients,
snapshot->usb_available && snapshot->usb.dtr ? "1" : "0", web_sessions,
ssh_sessions);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, 0U, 8U, line);
}
static void render_overview_page(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "OVERVIEW");
if (snapshot->serial_config_available) {
format_text(line, "SER %s %" PRIu32 " %s%s%s", snapshot->serial_running ? "ON" : "OFF",
snapshot->serial_config.baud_rate,
serial_config_data_bits_to_string(snapshot->serial_config.data_bits),
serial_config_parity_to_string(snapshot->serial_config.parity),
serial_config_stop_bits_to_string(snapshot->serial_config.stop_bits));
} else {
format_text(line, "SER %s CONFIG N/A", snapshot->serial_running ? "ON" : "OFF");
}
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line);
format_text(line, "BROKER %u WR %s", snapshot->broker_available ? snapshot->broker.connected_clients : 0U,
snapshot->broker_available && snapshot->broker.writer_id != SESSION_BROKER_NO_CLIENT
? "SET"
: "NONE");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U, line);
format_text(line, "USB %s WEB %u", snapshot->usb_available && snapshot->usb.dtr ? "OPEN" : "OFF",
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U, line);
format_text(line, "HTTPS %s SSH %u", snapshot->web_available && snapshot->web.running ? "ON" : "OFF",
snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 32U, line);
format_text(line, "WIFI %s", snapshot->wifi_available
? wifi_manager_state_to_string(snapshot->wifi.state)
: "N/A");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line);
}
static void render_rs232_page(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
uint64_t faults = snapshot->serial_counters.frame_errors +
snapshot->serial_counters.parity_errors +
snapshot->serial_counters.fifo_overflows +
snapshot->serial_counters.buffer_full_events +
snapshot->serial_counters.breaks;
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "RS232 MODEM");
if (snapshot->serial_config_available) {
format_text(line, "%" PRIu32 " %s%s%s %s", snapshot->serial_config.baud_rate,
serial_config_data_bits_to_string(snapshot->serial_config.data_bits),
serial_config_parity_to_string(snapshot->serial_config.parity),
serial_config_stop_bits_to_string(snapshot->serial_config.stop_bits),
serial_config_flow_control_to_string(snapshot->serial_config.flow_control));
} else {
format_text(line, "CONFIG UNAVAILABLE");
}
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line);
format_text(line, "DCD%d DSR%d CTS%d RI%d", snapshot->modem.dcd, snapshot->modem.dsr,
snapshot->modem.cts, snapshot->modem.ri);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U, line);
format_text(line, "VALID %s RX %" PRIu64, snapshot->modem.valid ? "YES" : "NO",
snapshot->serial_counters.rx_bytes);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U, line);
format_text(line, "TX %" PRIu64 " Q%u", snapshot->serial_counters.tx_sent_to_uart_bytes,
(unsigned int)serial_service_tx_pending());
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 32U, line);
format_text(line, "DROP %" PRIu64 " ERR %" PRIu64,
snapshot->serial_counters.rx_dropped_bytes + snapshot->serial_counters.tx_dropped_bytes,
faults);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line);
}
static void render_broker_page(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
format_text(line, "BROKER C%u WR %s", snapshot->broker_available ? snapshot->broker.connected_clients : 0U,
snapshot->broker_available && snapshot->broker.writer_id != SESSION_BROKER_NO_CLIENT
? "SET"
: "NONE");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, line);
for (size_t i = 0U; i < LOCAL_STATUS_UI_CLIENT_ROWS; ++i) {
if (i >= snapshot->client_count) {
break;
}
const session_broker_client_snapshot_t *client = &snapshot->clients[i];
format_text(line, "%c%s %s Q%u", client->is_writer ? 'W' : 'O',
broker_type_to_string(client->type), client->name,
(unsigned int)client->output_bytes_pending);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U,
(uint8_t)((i + 1U) * 8U), line);
}
if (snapshot->broker_available) {
format_text(line, "DROP %" PRIu64 " EVT %" PRIu64,
snapshot->broker.counters.output_dropped_bytes,
snapshot->broker.counters.event_drops);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line);
}
}
static void render_network_page(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
char address[16];
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "NETWORK SERVICES");
if (snapshot->wifi_available) {
format_text(line, "WF %s %dDB", wifi_manager_state_to_string(snapshot->wifi.state),
snapshot->wifi.sta_rssi);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line);
format_ipv4(snapshot->wifi.ip, address);
format_text(line, "IP %s", address);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U, line);
format_text(line, "AP %s C%u", snapshot->wifi.ap_running ? "ON" : "OFF",
snapshot->wifi.ap_client_count);
} else {
format_text(line, "WIFI UNAVAILABLE");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line);
format_text(line, "AP N/A");
}
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U, line);
format_text(line, "HTTPS %s WEB %u", snapshot->web_available && snapshot->web.running ? "ON" : "OFF",
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 32U, line);
format_text(line, "SSH %s S%u", snapshot->ssh_available && snapshot->ssh.running ? "ON" : "OFF",
snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line);
}
static void render_page(local_status_page_t page, const local_status_snapshot_t *snapshot)
{
if (local_display_frame_begin() != ESP_OK) {
return;
}
local_display_frame_clear_all();
render_status_panel(snapshot);
switch (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;
}
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));
}
}
static bool poll_buttons(local_status_button_state_t states[LOCAL_STATUS_BUTTON_COUNT],
local_status_button_t *pressed_button)
{
TickType_t now = xTaskGetTickCount();
bool event = false;
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
bool pressed = gpio_get_level(s_button_gpios[i]) == 0;
if (pressed != states[i].candidate_pressed) {
states[i].candidate_pressed = pressed;
states[i].candidate_since = now;
}
if (states[i].stable_pressed != states[i].candidate_pressed &&
(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;
}
}
}
return event;
}
static void local_status_ui_task(void *context)
{
(void)context;
local_status_page_t page = LOCAL_STATUS_PAGE_OVERVIEW;
local_status_button_state_t buttons[LOCAL_STATUS_BUTTON_COUNT] = {0};
TickType_t last_render = 0U;
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 = xTaskGetTickCount();
}
for (;;) {
local_status_button_t button;
bool button_event = poll_buttons(buttons, &button);
if (button_event) {
if (button == LOCAL_STATUS_BUTTON_PREVIOUS) {
page = page == LOCAL_STATUS_PAGE_OVERVIEW
? LOCAL_STATUS_PAGE_COUNT - 1U
: (local_status_page_t)(page - 1U);
} else if (button == LOCAL_STATUS_BUTTON_NEXT) {
page = (local_status_page_t)((page + 1U) % LOCAL_STATUS_PAGE_COUNT);
}
}
TickType_t now = xTaskGetTickCount();
bool diagnostics_held = (int32_t)(now - s_diagnostic_hold_until) < 0;
if (!diagnostics_held &&
(button_event || (now - last_render) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_REFRESH_MS))) {
local_status_snapshot_t snapshot;
collect_snapshot(&snapshot);
render_page(page, &snapshot);
last_render = now;
}
vTaskDelay(pdMS_TO_TICKS(LOCAL_STATUS_UI_BUTTON_POLL_MS));
}
}
void local_status_ui_hold_for_diagnostics(void)
{
s_diagnostic_hold_until =
xTaskGetTickCount() + pdMS_TO_TICKS(LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS);
}
esp_err_t local_status_ui_start(void)
{
if (s_task != NULL) {
return ESP_ERR_INVALID_STATE;
}
BaseType_t created = xTaskCreate(local_status_ui_task, "local_status_ui",
LOCAL_STATUS_UI_TASK_STACK_SIZE, NULL,
LOCAL_STATUS_UI_TASK_PRIORITY, &s_task);
if (created != pdPASS) {
s_task = NULL;
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Read-only status UI started at %u Hz maximum",
1000U / LOCAL_STATUS_UI_REFRESH_MS);
return ESP_OK;
}