Implement Phase 7C local status dashboard

Add fixed status icons, OLED dimming and wake behavior, lowercase
glyphs, and access-point metadata tracking. Update validation guidance,
roadmap status, and provide an icon layout mockup with licensing.
This commit is contained in:
2026-08-29 19:05:06 +02:00
parent c10e7d24a8
commit e291e29357
11 changed files with 652 additions and 126 deletions
+27 -4
View File
@@ -29,7 +29,7 @@ typedef struct {
uint8_t columns[5];
} glyph_t;
/* Compact 5x7 ASCII subset: lower-case input maps to upper-case glyphs. */
/* Compact project-owned 5x7 ASCII subset with distinct upper/lower-case glyphs. */
static const glyph_t s_glyphs[] = {
{' ', {0x00, 0x00, 0x00, 0x00, 0x00}},
{'!', {0x00, 0x00, 0x5f, 0x00, 0x00}},
@@ -75,6 +75,32 @@ static const glyph_t s_glyphs[] = {
{'X', {0x63, 0x14, 0x08, 0x14, 0x63}},
{'Y', {0x03, 0x04, 0x78, 0x04, 0x03}},
{'Z', {0x61, 0x51, 0x49, 0x45, 0x43}},
{'a', {0x20, 0x54, 0x54, 0x54, 0x78}},
{'b', {0x7f, 0x48, 0x44, 0x44, 0x38}},
{'c', {0x38, 0x44, 0x44, 0x44, 0x20}},
{'d', {0x38, 0x44, 0x44, 0x48, 0x7f}},
{'e', {0x38, 0x54, 0x54, 0x54, 0x18}},
{'f', {0x08, 0x7e, 0x09, 0x01, 0x02}},
{'g', {0x0c, 0x52, 0x52, 0x52, 0x3e}},
{'h', {0x7f, 0x08, 0x04, 0x04, 0x78}},
{'i', {0x00, 0x44, 0x7d, 0x40, 0x00}},
{'j', {0x20, 0x40, 0x44, 0x3d, 0x00}},
{'k', {0x7f, 0x10, 0x28, 0x44, 0x00}},
{'l', {0x00, 0x41, 0x7f, 0x40, 0x00}},
{'m', {0x7c, 0x04, 0x18, 0x04, 0x78}},
{'n', {0x7c, 0x08, 0x04, 0x04, 0x78}},
{'o', {0x38, 0x44, 0x44, 0x44, 0x38}},
{'p', {0x7c, 0x14, 0x14, 0x14, 0x08}},
{'q', {0x08, 0x14, 0x14, 0x18, 0x7c}},
{'r', {0x7c, 0x08, 0x04, 0x04, 0x08}},
{'s', {0x48, 0x54, 0x54, 0x54, 0x20}},
{'t', {0x04, 0x3f, 0x44, 0x40, 0x20}},
{'u', {0x3c, 0x40, 0x40, 0x20, 0x7c}},
{'v', {0x1c, 0x20, 0x40, 0x20, 0x1c}},
{'w', {0x3c, 0x40, 0x30, 0x40, 0x3c}},
{'x', {0x44, 0x28, 0x10, 0x28, 0x44}},
{'y', {0x0c, 0x50, 0x50, 0x50, 0x3c}},
{'z', {0x44, 0x64, 0x54, 0x4c, 0x44}},
};
static i2c_master_bus_handle_t s_bus;
@@ -212,9 +238,6 @@ static bool panel_geometry(local_display_panel_t panel, uint8_t *origin_y, uint8
static const glyph_t *find_glyph(char character)
{
if (character >= 'a' && character <= 'z') {
character = (char)(character - ('a' - 'A'));
}
for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) {
if (s_glyphs[index].character == character) {
return &s_glyphs[index];
+529 -104
View File
@@ -13,6 +13,8 @@
#include "esp_log.h"
#include "esp_netif_ip_addr.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "local_display.h"
#include "serial_config.h"
@@ -32,6 +34,12 @@
#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
#define LOCAL_STATUS_UI_STATUS_SLOT_WIDTH 18U
static const char *TAG = "local_status_ui";
@@ -50,6 +58,22 @@ typedef enum {
LOCAL_STATUS_BUTTON_COUNT,
} local_status_button_t;
typedef enum {
LOCAL_STATUS_POWER_ACTIVE = 0,
LOCAL_STATUS_POWER_DIMMED,
LOCAL_STATUS_POWER_OFF,
} local_status_power_t;
typedef enum {
LOCAL_STATUS_ICON_SERIAL = 0,
LOCAL_STATUS_ICON_WIFI,
LOCAL_STATUS_ICON_USB,
LOCAL_STATUS_ICON_WEB,
LOCAL_STATUS_ICON_SSH,
LOCAL_STATUS_ICON_CLIENTS,
LOCAL_STATUS_ICON_ALERT,
} local_status_icon_t;
typedef struct {
bool stable_pressed;
bool candidate_pressed;
@@ -78,8 +102,53 @@ typedef struct {
ssh_transport_snapshot_t ssh;
} local_status_snapshot_t;
/*
* The USB bitmap is a hand-rasterized 8x8 derivative of Pictogrammers
* Material Design Icons "usb". The Wi-Fi strength masks are compact segmented
* adaptations of "wifi-strength-4". Upstream SVGs and license are retained in
* third_party/material-design-icons/ (version 7.4.47, Apache-2.0 icons).
*/
static const uint8_t s_icon_serial[LOCAL_STATUS_UI_ICON_SIZE] = {
0x3cU, 0x42U, 0x7eU, 0x42U, 0x7eU, 0x42U, 0x3cU, 0x00U,
};
static const uint8_t s_icon_usb[LOCAL_STATUS_UI_ICON_SIZE] = {
0x18U, 0x3cU, 0x18U, 0x5aU, 0x3eU, 0x18U, 0x3cU, 0x18U,
};
static const uint8_t s_icon_web[LOCAL_STATUS_UI_ICON_SIZE] = {
0x3cU, 0x42U, 0xa5U, 0x99U, 0x99U, 0xa5U, 0x42U, 0x3cU,
};
static const uint8_t s_icon_ssh[LOCAL_STATUS_UI_ICON_SIZE] = {
0x7eU, 0x42U, 0x52U, 0x4aU, 0x52U, 0x42U, 0x7eU, 0x00U,
};
static const uint8_t s_icon_clients[LOCAL_STATUS_UI_ICON_SIZE] = {
0x24U, 0x5aU, 0x5aU, 0x24U, 0x00U, 0x7eU, 0xffU, 0x00U,
};
static const uint8_t s_icon_alert[LOCAL_STATUS_UI_ICON_SIZE] = {
0x18U, 0x18U, 0x3cU, 0x3cU, 0x7eU, 0x66U, 0xffU, 0x00U,
};
static const uint8_t s_icon_wifi_full[LOCAL_STATUS_UI_ICON_SIZE] = {
0x7eU, 0x81U, 0x3cU, 0x42U, 0x18U, 0x24U, 0x00U, 0x18U,
};
static const uint8_t s_icon_wifi_three[LOCAL_STATUS_UI_ICON_SIZE] = {
0x00U, 0x00U, 0x3cU, 0x42U, 0x18U, 0x24U, 0x00U, 0x18U,
};
static const uint8_t s_icon_wifi_two[LOCAL_STATUS_UI_ICON_SIZE] = {
0x00U, 0x00U, 0x00U, 0x00U, 0x18U, 0x24U, 0x00U, 0x18U,
};
static const uint8_t s_icon_wifi_one[LOCAL_STATUS_UI_ICON_SIZE] = {
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x18U,
};
static const uint8_t s_icon_access_point[LOCAL_STATUS_UI_ICON_SIZE] = {
0x18U, 0x18U, 0x3cU, 0x5aU, 0x99U, 0x18U, 0x18U, 0x18U,
};
static TaskHandle_t s_task;
static volatile TickType_t s_diagnostic_hold_until;
static StaticSemaphore_t s_diagnostic_gate_storage;
static SemaphoreHandle_t s_diagnostic_gate;
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 const gpio_num_t s_button_gpios[LOCAL_STATUS_BUTTON_COUNT] = {
LOCAL_UI_BUTTON_PREVIOUS_GPIO,
@@ -99,20 +168,49 @@ static const char *broker_type_to_string(session_broker_client_type_t type)
{
switch (type) {
case SESSION_BROKER_CLIENT_CONSOLE:
return "CON";
return "con";
case SESSION_BROKER_CLIENT_USB:
return "USB";
case SESSION_BROKER_CLIENT_WEB:
return "WEB";
return "web";
case SESSION_BROKER_CLIENT_SSH:
return "SSH";
case SESSION_BROKER_CLIENT_INTERNAL:
return "INT";
return "int";
default:
return "?";
}
}
static const char *writer_type_to_string(const local_status_snapshot_t *snapshot)
{
for (size_t i = 0U; i < snapshot->client_count; ++i) {
if (snapshot->clients[i].is_writer) {
return broker_type_to_string(snapshot->clients[i].type);
}
}
return "none";
}
static const char *on_off(bool value)
{
return value ? "On" : "Off";
}
static char parity_letter(serial_config_parity_t parity)
{
switch (parity) {
case SERIAL_CONFIG_PARITY_NONE:
return 'N';
case SERIAL_CONFIG_PARITY_EVEN:
return 'E';
case SERIAL_CONFIG_PARITY_ODD:
return 'O';
default:
return '?';
}
}
static void format_ipv4(uint32_t address, char output[16])
{
if (address == 0U) {
@@ -127,6 +225,116 @@ static void format_ipv4(uint32_t address, char output[16])
}
}
static const uint8_t *icon_bitmap(local_status_icon_t icon)
{
switch (icon) {
case LOCAL_STATUS_ICON_SERIAL:
return s_icon_serial;
case LOCAL_STATUS_ICON_USB:
return s_icon_usb;
case LOCAL_STATUS_ICON_WEB:
return s_icon_web;
case LOCAL_STATUS_ICON_SSH:
return s_icon_ssh;
case LOCAL_STATUS_ICON_CLIENTS:
return s_icon_clients;
case LOCAL_STATUS_ICON_ALERT:
return s_icon_alert;
case LOCAL_STATUS_ICON_WIFI:
default:
return s_icon_wifi_full;
}
}
static const uint8_t *wifi_bitmap(int8_t rssi)
{
if (rssi >= -55) {
return s_icon_wifi_full;
}
if (rssi >= -67) {
return s_icon_wifi_three;
}
if (rssi >= -75) {
return s_icon_wifi_two;
}
return s_icon_wifi_one;
}
static void format_compact_u64(uint64_t value, char output[8])
{
if (value < 1000U) {
(void)snprintf(output, 8U, "%u", (unsigned int)value);
} else if (value < 1000000U) {
(void)snprintf(output, 8U, "%uk", (unsigned int)(value / 1000U));
} else if (value < 1000000000U) {
(void)snprintf(output, 8U, "%uM", (unsigned int)(value / 1000000U));
} else if (value < 1000000000000ULL) {
(void)snprintf(output, 8U, "%uG", (unsigned int)(value / 1000000000U));
} else {
memcpy(output, "999G+", sizeof("999G+"));
}
}
static void format_ssid(const char *ssid, char output[9])
{
size_t length = strlen(ssid);
if (length <= 8U) {
(void)snprintf(output, 9U, "%s", ssid);
return;
}
memcpy(output, ssid, 7U);
output[7] = '~';
output[8] = '\0';
}
static bool diagnostic_hold_is_active(TickType_t now)
{
bool active;
portENTER_CRITICAL(&s_timing_mux);
if (s_diagnostic_hold_active &&
(now - s_diagnostic_hold_started) >=
pdMS_TO_TICKS(LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS)) {
s_diagnostic_hold_active = false;
}
active = s_diagnostic_hold_active;
portEXIT_CRITICAL(&s_timing_mux);
return active;
}
static bool consume_external_activity(uint32_t *observed_sequence)
{
uint32_t sequence;
portENTER_CRITICAL(&s_timing_mux);
sequence = s_external_activity_sequence;
portEXIT_CRITICAL(&s_timing_mux);
if (sequence == *observed_sequence) {
return false;
}
*observed_sequence = sequence;
return true;
}
static void draw_bitmap(local_display_panel_t panel,
uint8_t x,
uint8_t y,
const uint8_t bitmap[LOCAL_STATUS_UI_ICON_SIZE])
{
for (uint8_t row = 0U; row < LOCAL_STATUS_UI_ICON_SIZE; ++row) {
for (uint8_t column = 0U; column < LOCAL_STATUS_UI_ICON_SIZE; ++column) {
if ((bitmap[row] & (uint8_t)(0x80U >> column)) != 0U) {
local_display_frame_set_pixel(panel, (uint8_t)(x + column),
(uint8_t)(y + row), true);
}
}
}
}
static void draw_content_item(local_status_icon_t icon, uint8_t y, const char *text)
{
draw_bitmap(LOCAL_DISPLAY_PANEL_CONTENT, 0U, y, icon_bitmap(icon));
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, y, text);
}
static void collect_snapshot(local_status_snapshot_t *snapshot)
{
memset(snapshot, 0, sizeof(*snapshot));
@@ -166,119 +374,226 @@ static bool snapshot_has_alert(const local_status_snapshot_t *snapshot)
snapshot->broker.counters.output_dropped_bytes != 0U);
}
static bool wifi_is_active(const local_status_snapshot_t *snapshot)
{
if (!snapshot->wifi_available || !snapshot->wifi.started) {
return false;
}
return snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE ||
snapshot->wifi.state == WIFI_MANAGER_STATE_AP_ONLY ||
snapshot->wifi.ap_running;
}
static const uint8_t *wifi_snapshot_bitmap(const local_status_snapshot_t *snapshot)
{
if (snapshot->wifi_available && snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE) {
return wifi_bitmap(snapshot->wifi.sta_rssi);
}
if (snapshot->wifi_available && snapshot->wifi.ap_running) {
return s_icon_access_point;
}
return s_icon_wifi_full;
}
static void wifi_identity(const local_status_snapshot_t *snapshot,
const char **ssid,
uint8_t *channel)
{
if (snapshot->wifi_available && snapshot->wifi.sta_ssid_len > 0U &&
snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE) {
*ssid = snapshot->wifi.sta_ssid;
*channel = snapshot->wifi.sta_channel;
return;
}
if (snapshot->wifi_available && snapshot->wifi.ap_ssid_len > 0U &&
snapshot->wifi.ap_running) {
*ssid = snapshot->wifi.ap_ssid;
*channel = snapshot->wifi.ap_channel;
return;
}
*ssid = "none";
*channel = 0U;
}
static void render_status_slot(uint8_t slot,
const uint8_t bitmap[LOCAL_STATUS_UI_ICON_SIZE],
bool active,
const char *value)
{
if (!active) {
return;
}
uint8_t x = (uint8_t)(slot * LOCAL_STATUS_UI_STATUS_SLOT_WIDTH + 5U);
draw_bitmap(LOCAL_DISPLAY_PANEL_STATUS, x, 0U, bitmap);
if (value != NULL) {
size_t length = strlen(value);
uint8_t text_x = (uint8_t)(slot * LOCAL_STATUS_UI_STATUS_SLOT_WIDTH);
if (length < 3U) {
text_x = (uint8_t)(text_x + (3U - length) * 3U);
}
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, text_x, 8U, value);
}
}
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;
char value[4];
bool wifi_active = wifi_is_active(snapshot);
bool usb_active = snapshot->usb_available && snapshot->usb.attached && snapshot->usb.dtr;
bool web_active = snapshot->web_serial_available && snapshot->web_serial.active_sessions > 0U;
bool ssh_active = snapshot->ssh_available && snapshot->ssh.active_sessions > 0U;
bool clients_active = snapshot->broker_available && snapshot->broker.connected_clients > 0U;
bool alert_active = snapshot_has_alert(snapshot);
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);
render_status_slot(0U, s_icon_serial, snapshot->serial_running,
snapshot->serial_running ? "On" : NULL);
(void)snprintf(value, sizeof(value), "%u",
(unsigned int)(snapshot->wifi.sta_channel != 0U
? snapshot->wifi.sta_channel
: snapshot->wifi.ap_channel));
bool station_online = snapshot->wifi_available &&
snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE;
const uint8_t *wifi_icon = station_online
? wifi_bitmap(snapshot->wifi.sta_rssi)
: (snapshot->wifi.ap_running
? s_icon_access_point
: s_icon_wifi_full);
render_status_slot(1U, wifi_icon, wifi_active, wifi_active ? value : NULL);
render_status_slot(2U, s_icon_usb, usb_active,
usb_active ? (snapshot->usb.writer ? "Wr" : "Ob") : NULL);
(void)snprintf(value, sizeof(value), "%u",
(unsigned int)snapshot->web_serial.active_sessions);
render_status_slot(3U, s_icon_web, web_active, web_active ? value : NULL);
(void)snprintf(value, sizeof(value), "%u",
(unsigned int)snapshot->ssh.active_sessions);
render_status_slot(4U, s_icon_ssh, ssh_active, ssh_active ? value : NULL);
(void)snprintf(value, sizeof(value), "%u",
(unsigned int)snapshot->broker.connected_clients);
render_status_slot(5U, s_icon_clients, clients_active, clients_active ? value : NULL);
render_status_slot(6U, s_icon_alert, alert_active, alert_active ? "!" : NULL);
}
static void render_overview_page(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
char address[16];
char ssid_display[9];
const char *ssid;
uint8_t channel;
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "OVERVIEW");
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,
format_text(line, "Serial:%" PRIu32 " %s%c%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),
parity_letter(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");
format_text(line, "Serial:Unavailable");
}
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line);
draw_content_item(LOCAL_STATUS_ICON_SERIAL, 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);
format_text(line, "Clients:%u Wr:%s",
snapshot->broker_available ? snapshot->broker.connected_clients : 0U,
writer_type_to_string(snapshot));
draw_content_item(LOCAL_STATUS_ICON_CLIENTS, 16U, line);
wifi_identity(snapshot, &ssid, &channel);
format_ssid(ssid, ssid_display);
format_text(line, "WiFi:%s Ch:%u", ssid_display, channel);
draw_bitmap(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U,
wifi_snapshot_bitmap(snapshot));
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 24U, line);
format_ipv4(snapshot->wifi.ip, address);
format_text(line, "IP:%s", address);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 32U, line);
format_text(line, "Web:%u SSH:%u USB:%u",
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U,
snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U,
snapshot->usb_available && snapshot->usb.dtr ? 1U : 0U);
draw_content_item(LOCAL_STATUS_ICON_WEB, 40U, line);
}
static void render_rs232_page(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
char rx[8];
char tx[8];
char errors[8];
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");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "RS-232 / modem");
if (snapshot->serial_config_available) {
format_text(line, "%" PRIu32 " %s%s%s %s", snapshot->serial_config.baud_rate,
format_text(line, "Mode:%" PRIu32 " %s%c%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));
parity_letter(snapshot->serial_config.parity),
serial_config_stop_bits_to_string(snapshot->serial_config.stop_bits));
} else {
format_text(line, "CONFIG UNAVAILABLE");
format_text(line, "Mode:Unavailable");
}
draw_content_item(LOCAL_STATUS_ICON_SERIAL, 8U, line);
format_text(line, "Flow:%s", snapshot->serial_config_available
? serial_config_flow_control_to_string(
snapshot->serial_config.flow_control)
: "Unavailable");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 16U, line);
format_text(line, "DCD:%d DSR:%d CTS:%d", snapshot->modem.dcd, snapshot->modem.dsr,
snapshot->modem.cts);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 24U, line);
format_compact_u64(snapshot->serial_counters.rx_bytes, rx);
format_text(line, "Valid:%s RX:%s", snapshot->modem.valid ? "Yes" : "No", rx);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 32U, line);
format_compact_u64(snapshot->serial_counters.tx_sent_to_uart_bytes, tx);
format_compact_u64(faults + snapshot->serial_counters.rx_dropped_bytes +
snapshot->serial_counters.tx_dropped_bytes,
errors);
format_text(line, "TX:%s Err:%s", tx, errors);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 40U, line);
}
static local_status_icon_t client_icon(session_broker_client_type_t type)
{
switch (type) {
case SESSION_BROKER_CLIENT_USB:
return LOCAL_STATUS_ICON_USB;
case SESSION_BROKER_CLIENT_WEB:
return LOCAL_STATUS_ICON_WEB;
case SESSION_BROKER_CLIENT_SSH:
return LOCAL_STATUS_ICON_SSH;
default:
return LOCAL_STATUS_ICON_CLIENTS;
}
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");
format_text(line, "Broker clients:%u",
snapshot->broker_available ? snapshot->broker.connected_clients : 0U);
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;
}
for (size_t i = 0U; i < LOCAL_STATUS_UI_CLIENT_ROWS && i < snapshot->client_count; ++i) {
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,
format_text(line, "%s:%s Q:%u", client->is_writer ? "Wr" : "Ob",
broker_type_to_string(client->type),
(unsigned int)client->output_bytes_pending);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U,
(uint8_t)((i + 1U) * 8U), line);
draw_content_item(client_icon(client->type), (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);
char drops[8];
char events[8];
format_compact_u64(snapshot->broker.counters.output_dropped_bytes, drops);
format_compact_u64(snapshot->broker.counters.event_drops, events);
format_text(line, "Drop:%s Events:%s", drops, events);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 40U, line);
}
}
@@ -286,29 +601,31 @@ static void render_network_page(const local_status_snapshot_t *snapshot)
{
char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
char address[16];
char ssid_display[9];
const char *ssid;
uint8_t channel;
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",
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "Network / services");
wifi_identity(snapshot, &ssid, &channel);
format_ssid(ssid, ssid_display);
format_text(line, "WiFi:%s Ch:%u", ssid_display, channel);
draw_bitmap(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U,
wifi_snapshot_bitmap(snapshot));
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 8U, line);
format_ipv4(snapshot->wifi.ip, address);
format_text(line, "IP:%s", address);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 16U, line);
format_text(line, "AP:%s Clients:%u", on_off(snapshot->wifi.ap_running),
snapshot->wifi.ap_client_count);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 24U, line);
format_text(line, "HTTPS:%s Web:%u",
on_off(snapshot->web_available && snapshot->web.running),
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",
draw_content_item(LOCAL_STATUS_ICON_WEB, 32U, line);
format_text(line, "SSH:%s Sessions:%u",
on_off(snapshot->ssh_available && snapshot->ssh.running),
snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line);
draw_content_item(LOCAL_STATUS_ICON_SSH, 40U, line);
}
static void render_page(local_status_page_t page, const local_status_snapshot_t *snapshot)
@@ -316,6 +633,10 @@ static void render_page(local_status_page_t page, const local_status_snapshot_t
if (local_display_frame_begin() != ESP_OK) {
return;
}
if (diagnostic_hold_is_active(xTaskGetTickCount())) {
local_display_frame_cancel();
return;
}
local_display_frame_clear_all();
render_status_panel(snapshot);
@@ -367,37 +688,124 @@ static bool poll_buttons(local_status_button_state_t states[LOCAL_STATUS_BUTTON_
return event;
}
static void sync_display_power(local_status_power_t *power)
{
local_display_snapshot_t snapshot;
if (local_display_get_snapshot(&snapshot) != ESP_OK) {
return;
}
if (!snapshot.initialized) {
*power = LOCAL_STATUS_POWER_OFF;
} else if (snapshot.contrast <= LOCAL_STATUS_UI_DIM_CONTRAST) {
*power = LOCAL_STATUS_POWER_DIMMED;
} else {
*power = LOCAL_STATUS_POWER_ACTIVE;
}
}
static bool wake_display(local_status_power_t *power)
{
local_display_snapshot_t snapshot;
bool initialized = local_display_get_snapshot(&snapshot) == ESP_OK && snapshot.initialized;
esp_err_t error = ESP_OK;
if (*power == LOCAL_STATUS_POWER_OFF || !initialized) {
error = local_display_start();
} else if (*power == LOCAL_STATUS_POWER_DIMMED || snapshot.contrast != LOCAL_STATUS_UI_ACTIVE_CONTRAST) {
error = local_display_set_contrast(LOCAL_STATUS_UI_ACTIVE_CONTRAST);
}
if (error != ESP_OK) {
ESP_LOGW(TAG, "Display wake failed: %s", esp_err_to_name(error));
return false;
}
*power = LOCAL_STATUS_POWER_ACTIVE;
return true;
}
static void update_display_power(local_status_power_t *power,
TickType_t now,
TickType_t last_activity)
{
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);
if ((!should_stop && !should_dim) || s_diagnostic_gate == NULL ||
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(100U)) != pdTRUE) {
return;
}
/* A diagnostic publishes its hold before waiting on this gate. */
if (diagnostic_hold_is_active(xTaskGetTickCount())) {
(void)xSemaphoreGive(s_diagnostic_gate);
return;
}
if (should_stop) {
esp_err_t error = local_display_stop();
if (error == ESP_OK) {
*power = LOCAL_STATUS_POWER_OFF;
}
} else {
esp_err_t error = local_display_set_contrast(LOCAL_STATUS_UI_DIM_CONTRAST);
if (error == ESP_OK) {
*power = LOCAL_STATUS_POWER_DIMMED;
}
}
(void)xSemaphoreGive(s_diagnostic_gate);
}
static void local_status_ui_task(void *context)
{
(void)context;
local_status_page_t page = LOCAL_STATUS_PAGE_OVERVIEW;
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;
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();
buttons[i].candidate_since = last_activity;
}
for (;;) {
local_status_button_t button;
bool button_event = poll_buttons(buttons, &button);
bool render_requested = false;
TickType_t now = xTaskGetTickCount();
if (consume_external_activity(&observed_activity_sequence)) {
last_activity = now;
}
sync_display_power(&power);
if (button_event) {
if (button == LOCAL_STATUS_BUTTON_PREVIOUS) {
last_activity = now;
if (power != LOCAL_STATUS_POWER_ACTIVE) {
/* The first press wakes only, preventing accidental navigation. */
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;
}
}
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))) {
update_display_power(&power, now, last_activity);
bool diagnostics_held = diagnostic_hold_is_active(now);
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);
@@ -409,25 +817,42 @@ static void local_status_ui_task(void *context)
void local_status_ui_hold_for_diagnostics(void)
{
s_diagnostic_hold_until =
xTaskGetTickCount() + pdMS_TO_TICKS(LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS);
portENTER_CRITICAL(&s_timing_mux);
s_diagnostic_hold_started = xTaskGetTickCount();
s_diagnostic_hold_active = true;
++s_external_activity_sequence;
portEXIT_CRITICAL(&s_timing_mux);
/* Wait for any dim/off operation that began before the hold was published. */
if (s_diagnostic_gate != NULL &&
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(500U)) == pdTRUE) {
(void)xSemaphoreGive(s_diagnostic_gate);
}
}
esp_err_t local_status_ui_start(void)
{
if (s_task != NULL) {
if (s_task != NULL || s_diagnostic_gate != NULL) {
return ESP_ERR_INVALID_STATE;
}
s_diagnostic_gate = xSemaphoreCreateMutexStatic(&s_diagnostic_gate_storage);
if (s_diagnostic_gate == NULL) {
return ESP_ERR_NO_MEM;
}
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;
vSemaphoreDelete(s_diagnostic_gate);
s_diagnostic_gate = NULL;
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Read-only status UI started at %u Hz maximum",
ESP_LOGI(TAG,
"Read-only status UI started at %u Hz maximum; dim/off after 5/10 minutes",
1000U / LOCAL_STATUS_UI_REFRESH_MS);
return ESP_OK;
}
+1 -1
View File
@@ -38,7 +38,7 @@ static const char *TAG = "firmware";
void app_main(void)
{
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7B display driver started");
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7C local status UI started");
if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
+14 -7
View File
@@ -233,12 +233,17 @@ static uint8_t build_profile_order(uint8_t *order, const wifi_app_config_t *conf
return count;
}
static void note_ap_running(bool running, uint8_t channel)
static void note_ap_running(bool running, const wifi_app_config_t *config)
{
lock_shared();
bool changed = s_shared.snapshot.ap_running != running;
s_shared.snapshot.ap_running = running;
s_shared.snapshot.ap_channel = channel;
s_shared.snapshot.ap_channel = config->ap_channel;
if (running) {
s_shared.snapshot.ap_ssid_len = config->ap_ssid_len;
memset(s_shared.snapshot.ap_ssid, 0, sizeof(s_shared.snapshot.ap_ssid));
memcpy(s_shared.snapshot.ap_ssid, config->ap_ssid, config->ap_ssid_len);
}
if (!running) {
s_shared.snapshot.ap_client_count = 0U;
}
@@ -325,7 +330,7 @@ static esp_err_t set_runtime_ap_enabled(manager_runtime_t *runtime, bool enabled
}
if (error == ESP_OK) {
runtime->ap_enabled = true;
note_ap_running(true, config.ap_channel);
note_ap_running(true, &config);
} else {
/* Do not leave an untracked default/stale AP active after failure. */
esp_err_t rollback_error = esp_wifi_set_mode(WIFI_MODE_STA);
@@ -338,7 +343,7 @@ static esp_err_t set_runtime_ap_enabled(manager_runtime_t *runtime, bool enabled
error = esp_wifi_set_mode(WIFI_MODE_STA);
if (error == ESP_OK) {
runtime->ap_enabled = false;
note_ap_running(false, config.ap_channel);
note_ap_running(false, &config);
}
}
@@ -379,7 +384,7 @@ static void stop_radio(manager_runtime_t *runtime)
wifi_app_config_t config;
copy_working_config(&config);
if (runtime->ap_enabled) {
note_ap_running(false, config.ap_channel);
note_ap_running(false, &config);
}
wifi_config_secure_wipe(&config, sizeof(config));
@@ -551,7 +556,7 @@ static void start_radio_and_policy(manager_runtime_t *runtime)
runtime->radio_started = true;
runtime->ap_enabled = want_ap;
set_last_error(ESP_OK);
note_ap_running(want_ap, config.ap_channel);
note_ap_running(want_ap, &config);
if (runtime->profile_count == 0U) {
set_active_profile(-1, NULL);
@@ -1178,6 +1183,9 @@ esp_err_t wifi_manager_init(const wifi_app_config_t *config)
s_shared.snapshot.state = WIFI_MANAGER_STATE_STOPPED;
s_shared.snapshot.active_profile = -1;
s_shared.snapshot.ap_policy = config->ap_policy;
s_shared.snapshot.ap_ssid_len = config->ap_ssid_len;
memcpy(s_shared.snapshot.ap_ssid, config->ap_ssid, config->ap_ssid_len);
s_shared.snapshot.ap_ssid[config->ap_ssid_len] = '\0';
s_shared.snapshot.ap_channel = config->ap_channel;
s_shared.snapshot.sta_auth = WIFI_AUTH_OPEN;
s_shared.snapshot.last_error = ESP_OK;
@@ -1302,7 +1310,6 @@ esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config)
s_shared.snapshot.config_generation = 1U;
}
s_shared.snapshot.ap_policy = config->ap_policy;
s_shared.snapshot.ap_channel = config->ap_channel;
++s_shared.snapshot.counters.applies;
unlock_shared();
return ESP_OK;
+2
View File
@@ -60,6 +60,8 @@ typedef struct {
wifi_config_ap_policy_t ap_policy;
bool ap_running;
uint8_t ap_ssid_len;
char ap_ssid[WIFI_CONFIG_SSID_MAX_LEN + 1U];
uint8_t ap_client_count;
uint8_t ap_channel;