#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 this does not copy — * the RGB driver just switches which buffer is being scanned out and waits * for the switch to take effect. */ esp_lcd_panel_handle_t panel = display_panel(); esp_lcd_panel_draw_bitmap(panel, 0, 0, DISPLAY_H_RES, DISPLAY_V_RES, 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; }