merged changes
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"WebSearch",
|
"WebSearch",
|
||||||
"WebFetch(domain:docs.waveshare.com)",
|
"WebFetch(domain:docs.waveshare.com)",
|
||||||
"WebFetch(domain:www.waveshare.com)",
|
"WebFetch(domain:www.waveshare.com)",
|
||||||
|
"WebFetch(domain:raw.githubusercontent.com)",
|
||||||
"Bash(npm view:*)",
|
"Bash(npm view:*)",
|
||||||
"WebFetch(domain:raw.githubusercontent.com)",
|
"WebFetch(domain:raw.githubusercontent.com)",
|
||||||
"Bash(docker ps:*)",
|
"Bash(docker ps:*)",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ stats_server.py --WS/JSON--> ws_client --> dashboard_ui (LVGL)
|
|||||||
|
|
||||||
The Pi runs a WebSocket server that pushes system stats (CPU, memory, disk, temperature, network, services) as JSON every 2 seconds. The ESP32 parses the JSON and updates LVGL widgets. A data staleness watchdog forces reconnection if the server goes silent.
|
The Pi runs a WebSocket server that pushes system stats (CPU, memory, disk, temperature, network, services) as JSON every 2 seconds. The ESP32 parses the JSON and updates LVGL widgets. A data staleness watchdog forces reconnection if the server goes silent.
|
||||||
|
|
||||||
The display uses a two-column layout: left half shows Pi stats (CPU/RAM/DISK bars, CPU temp) and a services table; right half shows a large HH:MM:SS clock (montserrat_36), date with day-of-week, and local sensor readings (room temp, humidity). The services table auto-scrolls when more than 4 services are present. The clock updates every second from the on-board RTC, which syncs from the Pi's time when drift exceeds 60 seconds.
|
The display uses a two-column layout: left half shows Pi stats (CPU/RAM/DISK bars, CPU temp) and a services table; right half shows a large HH:MM:SS clock (montserrat_36), date with day-of-week, and local sensor readings (room temp, humidity). The services table auto-scrolls when services exceed the visible area; row height and visible row count are measured from LVGL at runtime, so the scroll loop adapts automatically to font, padding, or border changes. The clock updates every second from the on-board RTC, which syncs from the Pi's time when drift exceeds 60 seconds.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ static lv_obj_t *lbl_cpu_temp;
|
|||||||
/* Services table */
|
/* Services table */
|
||||||
static lv_obj_t *tbl_services;
|
static lv_obj_t *tbl_services;
|
||||||
static int s_service_count;
|
static int s_service_count;
|
||||||
|
static int s_dup_rows; /* duplicate rows appended for looping */
|
||||||
|
static lv_coord_t s_row_h; /* measured row height in px */
|
||||||
|
|
||||||
/* Local sensors (bottom bar) */
|
/* Local sensors (bottom bar) */
|
||||||
static lv_obj_t *lbl_local;
|
static lv_obj_t *lbl_local;
|
||||||
@@ -116,21 +118,52 @@ static lv_obj_t *create_label(lv_obj_t *parent, int x, int y, const lv_font_t *f
|
|||||||
static void scroll_timer_cb(lv_timer_t *timer)
|
static void scroll_timer_cb(lv_timer_t *timer)
|
||||||
{
|
{
|
||||||
(void)timer;
|
(void)timer;
|
||||||
if (s_service_count <= 4) {
|
if (s_dup_rows <= 0 || s_row_h <= 0) {
|
||||||
|
/* Too few services or not yet measured — no scrolling */
|
||||||
lv_obj_scroll_to_y(tbl_services, 0, LV_ANIM_OFF);
|
lv_obj_scroll_to_y(tbl_services, 0, LV_ANIM_OFF);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_coord_t cur_y = lv_obj_get_scroll_y(tbl_services);
|
lv_coord_t cur_y = lv_obj_get_scroll_y(tbl_services);
|
||||||
/* Each row is ~16px (12px font + 2px pad top + 2px pad bot) */
|
lv_coord_t wrap_y = s_service_count * s_row_h;
|
||||||
lv_coord_t row_h = 16;
|
|
||||||
lv_coord_t max_scroll = (s_service_count - 4) * row_h;
|
|
||||||
|
|
||||||
if (cur_y >= max_scroll) {
|
if (cur_y >= wrap_y) {
|
||||||
lv_obj_scroll_to_y(tbl_services, 0, LV_ANIM_ON);
|
lv_obj_scroll_to_y(tbl_services, 0, LV_ANIM_OFF);
|
||||||
} else {
|
cur_y = 0;
|
||||||
lv_obj_scroll_to_y(tbl_services, cur_y + row_h, LV_ANIM_ON);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lv_obj_scroll_to_y(tbl_services, cur_y + s_row_h, LV_ANIM_OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Measure row height, compute visible rows, append duplicates for seamless loop */
|
||||||
|
static void fill_duplicate_rows(int count)
|
||||||
|
{
|
||||||
|
s_dup_rows = 0;
|
||||||
|
s_row_h = 0;
|
||||||
|
if (count <= 0) return;
|
||||||
|
|
||||||
|
/* Trim table to exactly count rows so measurement is clean */
|
||||||
|
lv_table_set_row_cnt(tbl_services, count);
|
||||||
|
lv_obj_update_layout(tbl_services);
|
||||||
|
|
||||||
|
lv_coord_t content_h = lv_obj_get_self_height(tbl_services);
|
||||||
|
lv_coord_t row_h = content_h / count;
|
||||||
|
if (row_h <= 0) return;
|
||||||
|
|
||||||
|
lv_coord_t visible_h = lv_obj_get_content_height(tbl_services);
|
||||||
|
int visible_rows = (visible_h + row_h - 1) / row_h; /* ceil */
|
||||||
|
|
||||||
|
if (count <= visible_rows) return; /* everything fits — no scrolling */
|
||||||
|
|
||||||
|
for (int i = 0; i < visible_rows; i++) {
|
||||||
|
lv_table_set_cell_value(tbl_services, count + i, 0,
|
||||||
|
lv_table_get_cell_value(tbl_services, i, 0));
|
||||||
|
lv_table_set_cell_value(tbl_services, count + i, 1,
|
||||||
|
lv_table_get_cell_value(tbl_services, i, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
s_dup_rows = visible_rows;
|
||||||
|
s_row_h = row_h;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Create UI sections ---------- */
|
/* ---------- Create UI sections ---------- */
|
||||||
@@ -221,7 +254,7 @@ static void create_time_bar(lv_obj_t *parent)
|
|||||||
lbl_date = lv_label_create(bar_cont);
|
lbl_date = lv_label_create(bar_cont);
|
||||||
lv_obj_set_style_text_font(lbl_date, &InziuIosevka_Slab_CC_20px, 0);
|
lv_obj_set_style_text_font(lbl_date, &InziuIosevka_Slab_CC_20px, 0);
|
||||||
lv_obj_set_style_text_color(lbl_date, lv_color_black(), 0);
|
lv_obj_set_style_text_color(lbl_date, lv_color_black(), 0);
|
||||||
lv_obj_align(lbl_date, LV_ALIGN_RIGHT_MID, -10, 0);
|
lv_obj_align(lbl_date, LV_ALIGN_RIGHT_MID, -20, 0);
|
||||||
lv_label_set_text(lbl_date, "----/--/-- ---");
|
lv_label_set_text(lbl_date, "----/--/-- ---");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +265,7 @@ static void create_main_section(lv_obj_t *parent)
|
|||||||
|
|
||||||
tbl_services = lv_table_create(parent);
|
tbl_services = lv_table_create(parent);
|
||||||
lv_obj_set_pos(tbl_services, 4, MAIN_Y + 16);
|
lv_obj_set_pos(tbl_services, 4, MAIN_Y + 16);
|
||||||
lv_obj_set_size(tbl_services, 190, 68);
|
lv_obj_set_size(tbl_services, 190, 82);
|
||||||
lv_table_set_col_cnt(tbl_services, 2);
|
lv_table_set_col_cnt(tbl_services, 2);
|
||||||
lv_table_set_col_width(tbl_services, 0, 110);
|
lv_table_set_col_width(tbl_services, 0, 110);
|
||||||
lv_table_set_col_width(tbl_services, 1, 65);
|
lv_table_set_col_width(tbl_services, 1, 65);
|
||||||
@@ -248,8 +281,8 @@ static void create_main_section(lv_obj_t *parent)
|
|||||||
lv_table_set_cell_value(tbl_services, i, 1, "---");
|
lv_table_set_cell_value(tbl_services, i, 1, "---");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auto-scroll timer: 3 second period */
|
/* Auto-scroll timer: 1 second period */
|
||||||
s_scroll_timer = lv_timer_create(scroll_timer_cb, 3000, NULL);
|
s_scroll_timer = lv_timer_create(scroll_timer_cb, 1000, NULL);
|
||||||
|
|
||||||
/* === Left column: Pi Vitals (below services) === */
|
/* === Left column: Pi Vitals (below services) === */
|
||||||
int rx = 0;
|
int rx = 0;
|
||||||
@@ -258,12 +291,12 @@ static void create_main_section(lv_obj_t *parent)
|
|||||||
int bar_w = 82;
|
int bar_w = 82;
|
||||||
int bar_h = 12;
|
int bar_h = 12;
|
||||||
int val_x = rx + 4 + lbl_w + bar_w + 4; /* value label after bar */
|
int val_x = rx + 4 + lbl_w + bar_w + 4; /* value label after bar */
|
||||||
int temp_x = rx + 160; /* TEMP column, right of value labels */
|
int temp_x = rx + 155; /* TEMP column, right of value labels */
|
||||||
|
|
||||||
/* Pi Vitals header — Y=162 */
|
/* Pi Vitals header */
|
||||||
create_label(parent, rx + 4, 162, &InziuIosevka_Slab_CC_12px, "PI VITALS");
|
create_label(parent, rx + 4, 177, &InziuIosevka_Slab_CC_12px, "PI VITALS");
|
||||||
|
|
||||||
int ry = 176;
|
int ry = 192;
|
||||||
|
|
||||||
/* CPU [========] 12% TEMP */
|
/* CPU [========] 12% TEMP */
|
||||||
create_label(parent, rx + 4, ry, &InziuIosevka_Slab_CC_12px, "CPU");
|
create_label(parent, rx + 4, ry, &InziuIosevka_Slab_CC_12px, "CPU");
|
||||||
@@ -290,7 +323,7 @@ static void create_main_section(lv_obj_t *parent)
|
|||||||
|
|
||||||
/* === Right column: Status image (200x200) === */
|
/* === Right column: Status image (200x200) === */
|
||||||
img_status = lv_img_create(parent);
|
img_status = lv_img_create(parent);
|
||||||
lv_obj_set_pos(img_status, 200, MAIN_Y + 2);
|
lv_obj_set_pos(img_status, 200, MAIN_Y + 1);
|
||||||
lv_obj_set_size(img_status, 200, 200);
|
lv_obj_set_size(img_status, 200, 200);
|
||||||
lv_obj_set_style_bg_color(img_status, lv_color_white(), 0);
|
lv_obj_set_style_bg_color(img_status, lv_color_white(), 0);
|
||||||
lv_obj_set_style_bg_opa(img_status, LV_OPA_COVER, 0);
|
lv_obj_set_style_bg_opa(img_status, LV_OPA_COVER, 0);
|
||||||
@@ -316,7 +349,7 @@ static void create_bottom_bar(lv_obj_t *parent)
|
|||||||
lv_obj_set_style_text_font(lbl_net, &InziuIosevka_Slab_CC_12px, 0);
|
lv_obj_set_style_text_font(lbl_net, &InziuIosevka_Slab_CC_12px, 0);
|
||||||
lv_obj_set_style_text_color(lbl_net, lv_color_black(), 0);
|
lv_obj_set_style_text_color(lbl_net, lv_color_black(), 0);
|
||||||
lv_obj_align(lbl_net, LV_ALIGN_LEFT_MID, 0, 0);
|
lv_obj_align(lbl_net, LV_ALIGN_LEFT_MID, 0, 0);
|
||||||
lv_label_set_text(lbl_net, "NETWORK RX: ---- kbps TX: ---- kbps");
|
lv_label_set_text(lbl_net, "NETWORK DOWN: ---- kBps / UP: ---- kBps");
|
||||||
|
|
||||||
/* Local sensor readings — right-aligned */
|
/* Local sensor readings — right-aligned */
|
||||||
lbl_local = lv_label_create(bot_cont);
|
lbl_local = lv_label_create(bot_cont);
|
||||||
@@ -378,11 +411,8 @@ void dashboard_ui_update_stats(const pi_stats_t *stats)
|
|||||||
lv_table_set_cell_value(tbl_services, i, 0, stats->services[i].name);
|
lv_table_set_cell_value(tbl_services, i, 0, stats->services[i].name);
|
||||||
lv_table_set_cell_value(tbl_services, i, 1, tag);
|
lv_table_set_cell_value(tbl_services, i, 1, tag);
|
||||||
}
|
}
|
||||||
/* Clear unused rows */
|
/* Measure row height, compute visible rows, append duplicates */
|
||||||
for (int i = stats->service_count; i < WS_MAX_SERVICES; i++) {
|
fill_duplicate_rows(stats->service_count);
|
||||||
lv_table_set_cell_value(tbl_services, i, 0, "");
|
|
||||||
lv_table_set_cell_value(tbl_services, i, 1, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Uptime */
|
/* Uptime */
|
||||||
snprintf(buf, sizeof(buf), "Uptime: %.0fh", stats->uptime_hrs);
|
snprintf(buf, sizeof(buf), "Uptime: %.0fh", stats->uptime_hrs);
|
||||||
@@ -390,7 +420,7 @@ void dashboard_ui_update_stats(const pi_stats_t *stats)
|
|||||||
|
|
||||||
/* Network */
|
/* Network */
|
||||||
char net_buf[64];
|
char net_buf[64];
|
||||||
snprintf(net_buf, sizeof(net_buf), "NETWORK RX: %.0f kbps TX: %.0f kbps",
|
snprintf(net_buf, sizeof(net_buf), "NETWORK DOWN: %.0f kBps / UP: %.0f kBps",
|
||||||
stats->net_rx_kbps, stats->net_tx_kbps);
|
stats->net_rx_kbps, stats->net_tx_kbps);
|
||||||
lv_label_set_text(lbl_net, net_buf);
|
lv_label_set_text(lbl_net, net_buf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,10 +245,10 @@ static void sensor_task(void *arg)
|
|||||||
static void button_task(void *arg)
|
static void button_task(void *arg)
|
||||||
{
|
{
|
||||||
for (;;) {
|
for (;;) {
|
||||||
/* Wait for GP18 button event (single click = bit 0) */
|
/* Wait for GP18 button event (single click = bit 0, long press = bit 2) */
|
||||||
EventBits_t bits = xEventGroupWaitBits(
|
EventBits_t bits = xEventGroupWaitBits(
|
||||||
GP18ButtonGroups,
|
GP18ButtonGroups,
|
||||||
set_bit_button(0), /* single press bit */
|
set_bit_button(0) | set_bit_button(2),
|
||||||
pdTRUE, /* clear on exit */
|
pdTRUE, /* clear on exit */
|
||||||
pdFALSE, /* any bit */
|
pdFALSE, /* any bit */
|
||||||
pdMS_TO_TICKS(500)
|
pdMS_TO_TICKS(500)
|
||||||
@@ -257,7 +257,12 @@ static void button_task(void *arg)
|
|||||||
if (bits & set_bit_button(0)) {
|
if (bits & set_bit_button(0)) {
|
||||||
bool muted = !alert_is_muted();
|
bool muted = !alert_is_muted();
|
||||||
alert_mute(muted);
|
alert_mute(muted);
|
||||||
ESP_LOGI(TAG, "GP18 pressed: alerts %s", muted ? "muted" : "unmuted");
|
ESP_LOGI(TAG, "GP18 single press: alerts %s", muted ? "muted" : "unmuted");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bits & set_bit_button(2)) {
|
||||||
|
ESP_LOGI(TAG, "GP18 long press: forcing WS reconnect");
|
||||||
|
ws_client_reconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,14 @@ void ws_client_stop(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ws_client_reconnect(void)
|
||||||
|
{
|
||||||
|
if (!s_client) return;
|
||||||
|
ESP_LOGI(TAG, "Manual WS reconnect triggered");
|
||||||
|
esp_websocket_client_close(s_client, pdMS_TO_TICKS(2000));
|
||||||
|
s_last_data_tick = xTaskGetTickCount();
|
||||||
|
}
|
||||||
|
|
||||||
ws_state_t ws_client_get_state(void)
|
ws_state_t ws_client_get_state(void)
|
||||||
{
|
{
|
||||||
return s_state;
|
return s_state;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ typedef void (*ws_state_callback_t)(ws_state_t state);
|
|||||||
void ws_client_init(const char *uri);
|
void ws_client_init(const char *uri);
|
||||||
void ws_client_start(void);
|
void ws_client_start(void);
|
||||||
void ws_client_stop(void);
|
void ws_client_stop(void);
|
||||||
|
void ws_client_reconnect(void);
|
||||||
ws_state_t ws_client_get_state(void);
|
ws_state_t ws_client_get_state(void);
|
||||||
void ws_client_get_stats(pi_stats_t *out);
|
void ws_client_get_stats(pi_stats_t *out);
|
||||||
void ws_client_set_data_callback(ws_data_callback_t cb);
|
void ws_client_set_data_callback(ws_data_callback_t cb);
|
||||||
|
|||||||
Reference in New Issue
Block a user