434 lines
17 KiB
C
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;
|
|
}
|