#include #include #include "esp_log.h" #include "lvgl.h" #include "lvgl_port.h" #include "ui_manager.h" static const char *TAG = "ui_manager"; #define UI_LOCK_TIMEOUT_MS 1000 /* * Bring-up diagnostic: a live readout of the last touch coordinate. Off by * default -- define UI_DEBUG_TOUCH to put it back on screen when working on * the input path. */ /* #define UI_DEBUG_TOUCH */ static lv_obj_t *temp_label = NULL; static lv_obj_t *cond_label = NULL; static lv_obj_t *status_label = NULL; #ifdef UI_DEBUG_TOUCH static lv_obj_t *touch_label = NULL; #endif /* * lv_label_set_text() invalidates the label unconditionally, even when the * new text is identical -- which would repaint the temperature every poll * whether or not it moved. Comparing first means an unchanged value costs * nothing: no invalidation, so no redraw and no flush. */ static void label_set_if_changed(lv_obj_t *label, const char *text) { if (label == NULL || text == NULL) { return; } const char *current = lv_label_get_text(label); if (current != NULL && strcmp(current, text) == 0) { return; } lv_label_set_text(label, text); } /* * NOTE, even with the readout compiled out: LVGL does not bubble events to * parents by default. An event handler on the screen object never fires for * taps landing on a child widget -- the header or the temperature card * consume them -- which presents as whole regions of the panel being dead to * touch while the hardware reports them perfectly well. Anything that needs * to observe touches screen-wide must poll the input device, as below, rather * than hang an event handler on the screen. */ #ifdef UI_DEBUG_TOUCH static void touch_readout_timer_cb(lv_timer_t *timer) { lv_indev_t *indev = lv_indev_get_next(NULL); if (indev == NULL || touch_label == NULL) { return; } lv_point_t p; lv_indev_get_point(indev, &p); char buf[48]; if (lv_indev_get_state(indev) == LV_INDEV_STATE_PRESSED) { snprintf(buf, sizeof(buf), "touch %d, %d", (int)p.x, (int)p.y); } else { snprintf(buf, sizeof(buf), "touch %d, %d (released)", (int)p.x, (int)p.y); } label_set_if_changed(touch_label, buf); } #endif void ui_update_weather(int16_t temp_f, const char *condition) { if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) { ESP_LOGW(TAG, "Dropped weather update: LVGL lock timed out"); return; } char buf[32]; snprintf(buf, sizeof(buf), "%d°F", temp_f); label_set_if_changed(temp_label, buf); label_set_if_changed(cond_label, condition); lvgl_port_unlock(); } void ui_set_weather_stale(bool stale) { if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) { ESP_LOGW(TAG, "Dropped stale-state update: LVGL lock timed out"); return; } if (temp_label != NULL) { /* Unlike lv_label_set_text(), setting an identical style value does * not invalidate, so this is safe to call on every poll. */ lv_obj_set_style_text_color(temp_label, stale ? lv_color_hex(0x5A6672) : lv_color_white(), LV_PART_MAIN); } lvgl_port_unlock(); } void ui_set_status(const char *status) { if (status == NULL) { return; } if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) { ESP_LOGW(TAG, "Dropped status update: LVGL lock timed out"); return; } label_set_if_changed(status_label, status); lvgl_port_unlock(); } void ui_init(void) { /* ui_init() runs from app_main, not the LVGL task, so it needs the lock * like any other outside caller. */ if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) { ESP_LOGE(TAG, "Could not acquire LVGL lock for UI init"); return; } lv_obj_t *scr = lv_screen_active(); lv_obj_set_style_bg_color(scr, lv_color_hex(0x1A2332), LV_PART_MAIN); lv_obj_set_style_pad_all(scr, 0, LV_PART_MAIN); lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(scr, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_t *header = lv_obj_create(scr); lv_obj_set_size(header, LV_PCT(100), 64); lv_obj_set_style_radius(header, 0, LV_PART_MAIN); lv_obj_set_style_bg_color(header, lv_color_hex(0x0D1620), LV_PART_MAIN); lv_obj_set_style_border_width(header, 0, LV_PART_MAIN); lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *title = lv_label_create(header); lv_label_set_text(title, "Thermostat"); lv_obj_set_style_text_color(title, lv_color_white(), LV_PART_MAIN); lv_obj_set_style_text_font(title, &lv_font_montserrat_28, LV_PART_MAIN); lv_obj_center(title); lv_obj_t *card = lv_obj_create(scr); lv_obj_set_size(card, LV_PCT(80), LV_SIZE_CONTENT); lv_obj_set_style_bg_color(card, lv_color_hex(0x2C3E50), LV_PART_MAIN); lv_obj_set_style_border_width(card, 2, LV_PART_MAIN); lv_obj_set_style_border_color(card, lv_color_hex(0x3498DB), LV_PART_MAIN); lv_obj_set_style_radius(card, 12, LV_PART_MAIN); lv_obj_set_style_pad_all(card, 24, LV_PART_MAIN); lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); temp_label = lv_label_create(card); lv_label_set_text(temp_label, "--°F"); lv_obj_set_style_text_color(temp_label, lv_color_white(), LV_PART_MAIN); lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_48, LV_PART_MAIN); cond_label = lv_label_create(card); lv_label_set_text(cond_label, "Waiting for data..."); lv_obj_set_style_text_color(cond_label, lv_color_hex(0xBDC3C7), LV_PART_MAIN); status_label = lv_label_create(scr); lv_label_set_text(status_label, "Starting..."); lv_obj_set_style_text_color(status_label, lv_color_hex(0x7F8C8D), LV_PART_MAIN); #ifdef UI_DEBUG_TOUCH touch_label = lv_label_create(scr); lv_label_set_text(touch_label, "touch --, --"); lv_obj_set_style_text_color(touch_label, lv_color_hex(0x3498DB), LV_PART_MAIN); lv_timer_create(touch_readout_timer_cb, 100, NULL); #endif lvgl_port_unlock(); ESP_LOGI(TAG, "UI initialized"); }