Files
thermostat-hw/main/ui/ui_manager.c
T
mattandClaude d4b2ab7f99 Work through the remaining review items in TODO.md
Keep the HTTP session alive between weather polls (TODO #1). Re-creating the
client each time forced a fresh TLS handshake inside the window where the UI
is frozen. Measured on hardware: the fetch went from ~1.3s to ~80ms, so the
pause is no longer long enough to matter once thermostat controls exist. The
timeout drops from 30s to 10s once a session is warm, since only a cold
handshake needs the longer budget. Any transport error or non-200 tears the
session down so a half-open connection cannot poison every later poll.

The response buffer is now reset per request rather than on
HTTP_EVENT_ON_CONNECTED -- a kept-alive connection does not re-fire that
event, so the buffer would have accumulated across polls.

Indicate stale readings (TODO #2). After two consecutive failures the
temperature dims and the status line shows the age. Age comes from tick
counts, not weather_data_t.last_update: there is no SNTP client here, so
time(NULL) starts at the epoch and any age derived from it is meaningless.

Gate the touch readout behind UI_DEBUG_TOUCH, default off (TODO #3). Kept the
note about LVGL not bubbling events to parents -- it documents a real trap.

Delete main/utils (TODO #4); it had no callers outside itself. Also refreshed
project_structure.txt, which was additionally missing i2c_bus and backlight.

Lay out two 4MB OTA slots (TODO #5) while no devices are fielded, and document
in the README that an OTA download cannot run while the panel is scanning --
the flash write faults the bounce-buffer ISR.

Log the truncation warning once per response instead of once per chunk, and
record the weather task's stack high-water mark (TODO #6): ~2400 bytes spare
of 6144.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 09:08:51 -07:00

193 lines
6.2 KiB
C

#include <stdio.h>
#include <string.h>
#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");
}