#include "dashboard_ui.h" #include "user_fonts.h" #include #include /* ---------- Layout constants ---------- */ #define SCREEN_W 400 #define SCREEN_H 300 #define TOP_BAR_H 24 #define TIME_BAR_H 50 #define BOT_H 24 #define MAIN_Y (TOP_BAR_H + TIME_BAR_H) /* 74 */ #define MAIN_H (SCREEN_H - MAIN_Y - BOT_H) /* 202 */ #define LEFT_COL_W 200 #define RIGHT_COL_X 200 /* ---------- Static widget handles ---------- */ /* Top bar */ static lv_obj_t *lbl_ip; static lv_obj_t *lbl_batt; static lv_obj_t *lbl_batt_v; static lv_obj_t *bar_batt_top; static lv_obj_t *lbl_ws; /* Time bar */ static lv_obj_t *lbl_clock; static lv_obj_t *lbl_date; /* Pi stats bars + labels */ static lv_obj_t *bar_cpu; static lv_obj_t *lbl_cpu_val; static lv_obj_t *bar_ram; static lv_obj_t *lbl_ram_val; static lv_obj_t *bar_disk; static lv_obj_t *lbl_disk_val; static lv_obj_t *lbl_cpu_temp; /* Services table */ static lv_obj_t *tbl_services; static int s_service_count; /* Local sensors (bottom bar) */ static lv_obj_t *lbl_local; static lv_obj_t *lbl_uptime; /* Status image placeholder */ static lv_obj_t *img_status; /* Network */ static lv_obj_t *lbl_net; /* Auto-scroll timer */ static lv_timer_t *s_scroll_timer; /* ---------- Styles ---------- */ static lv_style_t style_bar_bg; static lv_style_t style_bar_ind; static lv_style_t style_bar_batt_bg; static lv_style_t style_bar_batt_ind; static void init_styles(void) { /* Bar background: white with 1px black border */ lv_style_init(&style_bar_bg); lv_style_set_bg_color(&style_bar_bg, lv_color_white()); lv_style_set_bg_opa(&style_bar_bg, LV_OPA_COVER); lv_style_set_border_color(&style_bar_bg, lv_color_black()); lv_style_set_border_width(&style_bar_bg, 1); lv_style_set_radius(&style_bar_bg, 0); /* Bar indicator: solid black fill */ lv_style_init(&style_bar_ind); lv_style_set_bg_color(&style_bar_ind, lv_color_black()); lv_style_set_bg_opa(&style_bar_ind, LV_OPA_COVER); lv_style_set_radius(&style_bar_ind, 0); /* Battery bar on black top bar: inverted colors */ lv_style_init(&style_bar_batt_bg); lv_style_set_bg_color(&style_bar_batt_bg, lv_color_black()); lv_style_set_bg_opa(&style_bar_batt_bg, LV_OPA_COVER); lv_style_set_border_color(&style_bar_batt_bg, lv_color_white()); lv_style_set_border_width(&style_bar_batt_bg, 1); lv_style_set_radius(&style_bar_batt_bg, 0); lv_style_init(&style_bar_batt_ind); lv_style_set_bg_color(&style_bar_batt_ind, lv_color_white()); lv_style_set_bg_opa(&style_bar_batt_ind, LV_OPA_COVER); lv_style_set_radius(&style_bar_batt_ind, 0); } static lv_obj_t *create_bar(lv_obj_t *parent, int x, int y, int w, int h) { lv_obj_t *bar = lv_bar_create(parent); lv_obj_set_pos(bar, x, y); lv_obj_set_size(bar, w, h); lv_bar_set_range(bar, 0, 100); lv_bar_set_value(bar, 0, LV_ANIM_OFF); lv_obj_add_style(bar, &style_bar_bg, LV_PART_MAIN); lv_obj_add_style(bar, &style_bar_ind, LV_PART_INDICATOR); return bar; } static lv_obj_t *create_label(lv_obj_t *parent, int x, int y, const lv_font_t *font, const char *text) { lv_obj_t *lbl = lv_label_create(parent); lv_obj_set_pos(lbl, x, y); lv_obj_set_style_text_font(lbl, font, 0); lv_obj_set_style_text_color(lbl, lv_color_black(), 0); lv_label_set_text(lbl, text); return lbl; } /* ---------- Auto-scroll callback ---------- */ static void scroll_timer_cb(lv_timer_t *timer) { (void)timer; if (s_service_count <= 4) { lv_obj_scroll_to_y(tbl_services, 0, LV_ANIM_OFF); return; } 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 row_h = 16; lv_coord_t max_scroll = (s_service_count - 4) * row_h; if (cur_y >= max_scroll) { lv_obj_scroll_to_y(tbl_services, 0, LV_ANIM_ON); } else { lv_obj_scroll_to_y(tbl_services, cur_y + row_h, LV_ANIM_ON); } } /* ---------- Create UI sections ---------- */ static void create_top_bar(lv_obj_t *parent) { lv_obj_t *bar_cont = lv_obj_create(parent); lv_obj_set_pos(bar_cont, 0, 0); lv_obj_set_size(bar_cont, SCREEN_W, TOP_BAR_H); lv_obj_set_style_bg_color(bar_cont, lv_color_black(), 0); lv_obj_set_style_bg_opa(bar_cont, LV_OPA_COVER, 0); lv_obj_set_style_border_width(bar_cont, 0, 0); lv_obj_set_style_radius(bar_cont, 0, 0); lv_obj_set_style_pad_all(bar_cont, 2, 0); lv_obj_clear_flag(bar_cont, LV_OBJ_FLAG_SCROLLABLE); /* IP address — left */ lbl_ip = lv_label_create(bar_cont); lv_obj_set_style_text_color(lbl_ip, lv_color_white(), 0); lv_obj_set_style_text_font(lbl_ip, &InziuIosevka_Slab_CC_12px, 0); lv_obj_align(lbl_ip, LV_ALIGN_LEFT_MID, 2, 0); lv_label_set_text(lbl_ip, "N/A"); /* WS status — center-left */ lbl_ws = lv_label_create(bar_cont); lv_obj_set_style_text_color(lbl_ws, lv_color_white(), 0); lv_obj_set_style_text_font(lbl_ws, &InziuIosevka_Slab_CC_12px, 0); lv_obj_align(lbl_ws, LV_ALIGN_LEFT_MID, 140, 0); lv_label_set_text(lbl_ws, "WS:---"); /* Battery voltage — left of bar */ lbl_batt_v = lv_label_create(bar_cont); lv_obj_set_style_text_color(lbl_batt_v, lv_color_white(), 0); lv_obj_set_style_text_font(lbl_batt_v, &InziuIosevka_Slab_CC_12px, 0); lv_obj_align(lbl_batt_v, LV_ALIGN_RIGHT_MID, -72, 0); lv_label_set_text(lbl_batt_v, "-.--V"); /* Battery bar (24x10) — right */ bar_batt_top = lv_bar_create(bar_cont); lv_obj_set_size(bar_batt_top, 24, 10); lv_obj_align(bar_batt_top, LV_ALIGN_RIGHT_MID, -40, 0); lv_bar_set_range(bar_batt_top, 0, 100); lv_bar_set_value(bar_batt_top, 0, LV_ANIM_OFF); lv_obj_add_style(bar_batt_top, &style_bar_batt_bg, LV_PART_MAIN); lv_obj_add_style(bar_batt_top, &style_bar_batt_ind, LV_PART_INDICATOR); /* Battery positive terminal nub */ lv_obj_t *batt_nub = lv_obj_create(bar_cont); lv_obj_set_size(batt_nub, 2, 4); lv_obj_align(batt_nub, LV_ALIGN_RIGHT_MID, -37, 0); lv_obj_set_style_bg_color(batt_nub, lv_color_white(), 0); lv_obj_set_style_bg_opa(batt_nub, LV_OPA_COVER, 0); lv_obj_set_style_border_width(batt_nub, 0, 0); lv_obj_set_style_radius(batt_nub, 0, 0); lv_obj_set_style_pad_all(batt_nub, 0, 0); lv_obj_clear_flag(batt_nub, LV_OBJ_FLAG_SCROLLABLE); /* Battery text — right of bar */ lbl_batt = lv_label_create(bar_cont); lv_obj_set_style_text_color(lbl_batt, lv_color_white(), 0); lv_obj_set_style_text_font(lbl_batt, &InziuIosevka_Slab_CC_12px, 0); lv_obj_align(lbl_batt, LV_ALIGN_RIGHT_MID, -2, 0); lv_label_set_text(lbl_batt, "--%"); } static void create_time_bar(lv_obj_t *parent) { lv_obj_t *bar_cont = lv_obj_create(parent); lv_obj_set_pos(bar_cont, 0, TOP_BAR_H); lv_obj_set_size(bar_cont, SCREEN_W, TIME_BAR_H); lv_obj_set_style_bg_color(bar_cont, lv_color_white(), 0); lv_obj_set_style_bg_opa(bar_cont, LV_OPA_COVER, 0); lv_obj_set_style_border_color(bar_cont, lv_color_black(), 0); lv_obj_set_style_border_width(bar_cont, 1, 0); lv_obj_set_style_border_side(bar_cont, LV_BORDER_SIDE_BOTTOM, 0); lv_obj_set_style_radius(bar_cont, 0, 0); lv_obj_set_style_pad_all(bar_cont, 0, 0); lv_obj_clear_flag(bar_cont, LV_OBJ_FLAG_SCROLLABLE); /* Clock HH:MM:SS — left */ lbl_clock = lv_label_create(bar_cont); lv_obj_set_style_text_font(lbl_clock, &DSEG14C_BI_32px, 0); lv_obj_set_style_text_color(lbl_clock, lv_color_black(), 0); lv_obj_align(lbl_clock, LV_ALIGN_LEFT_MID, 10, 0); lv_label_set_text(lbl_clock, "--:--:--"); /* Date YYYY/MM/DD DAY — right */ 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_color(lbl_date, lv_color_black(), 0); lv_obj_align(lbl_date, LV_ALIGN_RIGHT_MID, -10, 0); lv_label_set_text(lbl_date, "----/--/-- ---"); } static void create_main_section(lv_obj_t *parent) { /* === Left column: Services + Pi Vitals === */ create_label(parent, 4, MAIN_Y + 2, &InziuIosevka_Slab_CC_12px, "SERVICES"); tbl_services = lv_table_create(parent); lv_obj_set_pos(tbl_services, 4, MAIN_Y + 16); lv_obj_set_size(tbl_services, 190, 68); lv_table_set_col_cnt(tbl_services, 2); lv_table_set_col_width(tbl_services, 0, 110); lv_table_set_col_width(tbl_services, 1, 65); lv_obj_set_style_text_font(tbl_services, &InziuIosevka_Slab_CC_12px, 0); lv_obj_set_style_border_color(tbl_services, lv_color_black(), 0); lv_obj_set_style_border_width(tbl_services, 1, 0); lv_obj_set_style_pad_ver(tbl_services, 2, LV_PART_ITEMS); lv_obj_set_style_pad_hor(tbl_services, 4, LV_PART_ITEMS); lv_obj_add_flag(tbl_services, LV_OBJ_FLAG_SCROLLABLE); for (int i = 0; i < WS_MAX_SERVICES; i++) { lv_table_set_cell_value(tbl_services, i, 0, "---"); lv_table_set_cell_value(tbl_services, i, 1, "---"); } /* Auto-scroll timer: 3 second period */ s_scroll_timer = lv_timer_create(scroll_timer_cb, 3000, NULL); /* === Left column: Pi Vitals (below services) === */ int rx = 0; int row_h = 18; /* vertical spacing per stat row */ int lbl_w = 36; /* width reserved for "CPU " etc */ int bar_w = 82; int bar_h = 12; 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 */ /* Pi Vitals header — Y=162 */ create_label(parent, rx + 4, 162, &InziuIosevka_Slab_CC_12px, "PI VITALS"); int ry = 176; /* CPU [========] 12% TEMP */ create_label(parent, rx + 4, ry, &InziuIosevka_Slab_CC_12px, "CPU"); bar_cpu = create_bar(parent, rx + 4 + lbl_w, ry, bar_w, bar_h); lbl_cpu_val = create_label(parent, val_x, ry, &InziuIosevka_Slab_CC_12px, "--%"); create_label(parent, temp_x, ry, &InziuIosevka_Slab_CC_12px, "TEMP"); /* RAM [========] 58% 45 C */ ry += row_h; create_label(parent, rx + 4, ry, &InziuIosevka_Slab_CC_12px, "RAM"); bar_ram = create_bar(parent, rx + 4 + lbl_w, ry, bar_w, bar_h); lbl_ram_val = create_label(parent, val_x, ry, &InziuIosevka_Slab_CC_12px, "--%"); lbl_cpu_temp = create_label(parent, temp_x, ry, &InziuIosevka_Slab_CC_20px, "-- C"); /* DISK [========] 44% */ ry += row_h; create_label(parent, rx + 4, ry, &InziuIosevka_Slab_CC_12px, "DISK"); bar_disk = create_bar(parent, rx + 4 + lbl_w, ry, bar_w, bar_h); lbl_disk_val = create_label(parent, val_x, ry, &InziuIosevka_Slab_CC_12px, "--%"); /* uptime */ ry += row_h; lbl_uptime = create_label(parent, rx + 4, ry, &InziuIosevka_Slab_CC_12px, "Uptime: --h"); /* === Right column: Status image (200x200) === */ img_status = lv_img_create(parent); lv_obj_set_pos(img_status, 200, MAIN_Y + 2); 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_opa(img_status, LV_OPA_COVER, 0); } static void create_bottom_bar(lv_obj_t *parent) { int y0 = SCREEN_H - BOT_H; lv_obj_t *bot_cont = lv_obj_create(parent); lv_obj_set_pos(bot_cont, 0, y0); lv_obj_set_size(bot_cont, SCREEN_W, BOT_H); lv_obj_set_style_bg_color(bot_cont, lv_color_white(), 0); lv_obj_set_style_bg_opa(bot_cont, LV_OPA_COVER, 0); lv_obj_set_style_border_color(bot_cont, lv_color_black(), 0); lv_obj_set_style_border_width(bot_cont, 1, 0); lv_obj_set_style_border_side(bot_cont, LV_BORDER_SIDE_TOP, 0); lv_obj_set_style_radius(bot_cont, 0, 0); lv_obj_set_style_pad_all(bot_cont, 4, 0); lv_obj_clear_flag(bot_cont, LV_OBJ_FLAG_SCROLLABLE); lbl_net = lv_label_create(bot_cont); 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_align(lbl_net, LV_ALIGN_LEFT_MID, 0, 0); lv_label_set_text(lbl_net, "NETWORK RX: ---- kbps TX: ---- kbps"); /* Local sensor readings — right-aligned */ lbl_local = lv_label_create(bot_cont); lv_obj_set_style_text_font(lbl_local, &InziuIosevka_Slab_CC_12px, 0); lv_obj_set_style_text_color(lbl_local, lv_color_black(), 0); lv_obj_align(lbl_local, LV_ALIGN_RIGHT_MID, 0, 0); lv_label_set_text(lbl_local, "T: --.- H: --%"); } void dashboard_ui_create(void) { init_styles(); lv_obj_t *scr = lv_scr_act(); lv_obj_set_style_bg_color(scr, lv_color_white(), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); create_top_bar(scr); create_time_bar(scr); create_main_section(scr); create_bottom_bar(scr); } /* ---------- Update functions ---------- */ void dashboard_ui_update_stats(const pi_stats_t *stats) { if (!stats || !stats->valid) return; char buf[32]; /* Bars */ lv_bar_set_value(bar_cpu, (int)stats->cpu_pct, LV_ANIM_OFF); snprintf(buf, sizeof(buf), "%d%%", (int)stats->cpu_pct); lv_label_set_text(lbl_cpu_val, buf); lv_bar_set_value(bar_ram, (int)stats->mem_pct, LV_ANIM_OFF); snprintf(buf, sizeof(buf), "%d%%", (int)stats->mem_pct); lv_label_set_text(lbl_ram_val, buf); lv_bar_set_value(bar_disk, (int)stats->disk_pct, LV_ANIM_OFF); snprintf(buf, sizeof(buf), "%d%%", (int)stats->disk_pct); lv_label_set_text(lbl_disk_val, buf); /* CPU temp */ snprintf(buf, sizeof(buf), "%.0f C", stats->cpu_temp); lv_label_set_text(lbl_cpu_temp, buf); /* Services table */ s_service_count = stats->service_count; for (int i = 0; i < stats->service_count && i < WS_MAX_SERVICES; i++) { const char *tag; switch (stats->services[i].status) { case SVC_RUNNING: tag = "[RUN]"; break; case SVC_WARNING: tag = "[WARN]"; break; default: tag = "[STOP]"; break; } lv_table_set_cell_value(tbl_services, i, 0, stats->services[i].name); lv_table_set_cell_value(tbl_services, i, 1, tag); } /* Clear unused rows */ for (int i = stats->service_count; i < WS_MAX_SERVICES; i++) { lv_table_set_cell_value(tbl_services, i, 0, ""); lv_table_set_cell_value(tbl_services, i, 1, ""); } /* Uptime */ snprintf(buf, sizeof(buf), "Uptime: %.0fh", stats->uptime_hrs); lv_label_set_text(lbl_uptime, buf); /* Network */ char net_buf[64]; snprintf(net_buf, sizeof(net_buf), "NETWORK RX: %.0f kbps TX: %.0f kbps", stats->net_rx_kbps, stats->net_tx_kbps); lv_label_set_text(lbl_net, net_buf); } void dashboard_ui_update_local(float temp, float humidity, uint8_t battery, bool charging, float voltage) { char buf[32]; snprintf(buf, sizeof(buf), "T: %.1f H: %.0f%%", temp, humidity); lv_label_set_text(lbl_local, buf); /* Update top bar battery voltage */ snprintf(buf, sizeof(buf), "%.2fV", voltage); lv_label_set_text(lbl_batt_v, buf); /* Update top bar battery text + bar */ snprintf(buf, sizeof(buf), charging ? "%d%%C" : "%d%%", battery); lv_label_set_text(lbl_batt, buf); lv_bar_set_value(bar_batt_top, battery, LV_ANIM_OFF); } void dashboard_ui_update_time(int h, int m, int s, int year, int month, int day, int weekday) { static const char *day_names[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; char buf[32]; snprintf(buf, sizeof(buf), "%02d:%02d:%02d", h, m, s); lv_label_set_text(lbl_clock, buf); const char *wd = (weekday >= 0 && weekday <= 6) ? day_names[weekday] : "---"; snprintf(buf, sizeof(buf), "%04d/%02d/%02d %s", year, month, day, wd); lv_label_set_text(lbl_date, buf); } void dashboard_ui_update_connection(ws_state_t ws_state, bool wifi_connected, const char *ip_str) { /* IP / WiFi status */ lv_label_set_text(lbl_ip, ip_str ? ip_str : "N/A"); /* WS status */ const char *ws_str; switch (ws_state) { case WS_STATE_CONNECTED: ws_str = "WS:LIVE"; break; case WS_STATE_CONNECTING: ws_str = "WS:..."; break; case WS_STATE_DISCONNECTED: ws_str = "WS:OFF"; break; case WS_STATE_ERROR: ws_str = "WS:ERR"; break; default: ws_str = "WS:---"; break; } lv_label_set_text(lbl_ws, ws_str); } void dashboard_ui_update_status_image(const lv_img_dsc_t *dsc) { if (dsc) { lv_img_set_src(img_status, dsc); } }