Files
rotaryThermo/main/ui/lvgl_port.c
T
mattandClaude Opus 5 4c7b120473 Fix TWDT starvation, vsync race, and UI responsiveness from review
Three bugs and two UX defects found on review, plus hardening:

- The control loop fed the task watchdog then slept exactly the watchdog
  period (both 10s), tripping the TWDT fractionally late on every single
  iteration. Sleep is now sliced 1s with a reset per slice. Also set
  CONFIG_ESP_TASK_WDT_PANIC=y - the sdkconfig comment claimed a hung task
  panics the chip, but nothing actually made that true.

- The LVGL flush called draw_bitmap (which only queues the framebuffer
  switch) and released LVGL immediately, letting it render the next frame
  into the buffer still being scanned out - the exact PRD 8.2 race the
  double-buffer design exists to prevent. display_present_frame() now
  blocks on a vsync handshake until the swap has latched.

- WiFi reconnect slept up to 30s inside the system event handler, stalling
  all queued WiFi/IP events. Moved to an esp_timer.

- The setpoint label, arc, and mode/room lines rendered the control loop's
  copy of state, which refreshes every 10s - every knob detent looked dead
  for up to 10 seconds. The UI now reads settings live; when the fallback
  ladder clamps the setpoint it shows both values. Room-select focus now
  shows which room is being selected.

- z2m ISO_8601_local timestamps carry a UTC offset the parser ignored,
  which would shift apparent reading age by the local offset (5-6h in the
  US) and permanently trip the fallback ladder. Offset is now honoured.

- Suppressed the cosmetic "control loop stopped" fault in the window
  between relay_init and the control task's first call-in at boot.

- Explicit pullups on the encoder A/B lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:05:51 -07:00

188 lines
4.9 KiB
C

#include "lvgl_port.h"
#include "cst826.h"
#include "display.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
static const char *TAG = "lvgl";
#define TICK_PERIOD_MS 2
#define TASK_STACK 8192
#define TASK_PRIO 3
#define TASK_MAX_DELAY_MS 20
#define TASK_MIN_DELAY_MS 2
static SemaphoreHandle_t s_lock;
static lv_display_t *s_disp;
static lv_indev_t *s_touch_indev;
/* ---------------------------------------------------------------- tick -- */
static void tick_cb(void *arg)
{
(void)arg;
lv_tick_inc(TICK_PERIOD_MS);
}
/* --------------------------------------------------------------- flush -- */
static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
(void)area;
/*
* px_map is one of the panel's own framebuffers, so no copy happens — the
* RGB driver switches which buffer is scanned out. present_frame blocks
* until that switch has latched at vsync; releasing LVGL any earlier would
* let it render the next frame into the buffer still on screen.
*/
display_present_frame(px_map);
lv_display_flush_ready(disp);
}
/* --------------------------------------------------------------- input -- */
static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
{
(void)indev;
cst826_touch_t t;
cst826_read(&t);
if (t.pressed) {
data->point.x = t.x;
data->point.y = t.y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
/* ---------------------------------------------------------------- task -- */
static void lvgl_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "service task running");
while (1) {
uint32_t next = TASK_MAX_DELAY_MS;
if (lvgl_port_lock(-1)) {
next = lv_timer_handler();
lvgl_port_unlock();
}
if (next > TASK_MAX_DELAY_MS) {
next = TASK_MAX_DELAY_MS;
} else if (next < TASK_MIN_DELAY_MS) {
next = TASK_MIN_DELAY_MS;
}
vTaskDelay(pdMS_TO_TICKS(next));
}
}
/* ---------------------------------------------------------------- init -- */
esp_err_t lvgl_port_init(void)
{
if (s_disp) {
return ESP_OK;
}
void *fb0 = NULL, *fb1 = NULL;
esp_err_t err = display_get_framebuffers(&fb0, &fb1);
if (err != ESP_OK || !fb0 || !fb1) {
ESP_LOGE(TAG, "no framebuffers — display_init() must run first");
return ESP_ERR_INVALID_STATE;
}
s_lock = xSemaphoreCreateRecursiveMutex();
if (!s_lock) {
return ESP_ERR_NO_MEM;
}
/*
* PRD 8.2: lv_init() before creating any LVGL object. Calling
* lv_display_create() first faults with LoadProhibited inside the TLSF
* allocator, which is a confusing way to discover an ordering bug.
*/
if (!lv_is_initialized()) {
lv_init();
}
s_disp = lv_display_create(DISPLAY_H_RES, DISPLAY_V_RES);
if (!s_disp) {
return ESP_ERR_NO_MEM;
}
lv_display_set_color_format(s_disp, LV_COLOR_FORMAT_RGB565);
lv_display_set_flush_cb(s_disp, flush_cb);
size_t fb_bytes = (size_t)DISPLAY_H_RES * DISPLAY_V_RES * 2;
lv_display_set_buffers(s_disp, fb0, fb1, fb_bytes,
LV_DISPLAY_RENDER_MODE_FULL);
/* Touch, if the controller answered. The thermostat is fully usable from
* the encoder alone, so a missing touch panel is not fatal. */
if (cst826_is_ready()) {
s_touch_indev = lv_indev_create();
lv_indev_set_type(s_touch_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(s_touch_indev, touch_read_cb);
lv_indev_set_display(s_touch_indev, s_disp);
} else {
ESP_LOGW(TAG, "touch unavailable — encoder only");
}
const esp_timer_create_args_t tick_args = {
.callback = tick_cb,
.name = "lv_tick",
};
esp_timer_handle_t tick_timer;
ESP_RETURN_ON_ERROR(esp_timer_create(&tick_args, &tick_timer),
TAG, "tick timer");
ESP_RETURN_ON_ERROR(
esp_timer_start_periodic(tick_timer, TICK_PERIOD_MS * 1000),
TAG, "tick start");
if (xTaskCreate(lvgl_task, "lvgl", TASK_STACK, NULL, TASK_PRIO, NULL)
!= pdPASS) {
ESP_LOGE(TAG, "task create failed");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "up: %dx%d, full-refresh double buffered",
DISPLAY_H_RES, DISPLAY_V_RES);
return ESP_OK;
}
bool lvgl_port_lock(int timeout_ms)
{
if (!s_lock) {
return false;
}
TickType_t wait = (timeout_ms < 0) ? portMAX_DELAY
: pdMS_TO_TICKS(timeout_ms);
return xSemaphoreTakeRecursive(s_lock, wait) == pdTRUE;
}
void lvgl_port_unlock(void)
{
if (s_lock) {
xSemaphoreGiveRecursive(s_lock);
}
}
lv_display_t *lvgl_port_display(void)
{
return s_disp;
}