/* * LCD component: single ST7789V panel driven by esp_lcd + LVGL. */ #include "lcd.h" #include #include #include #include "driver/gpio.h" #include "driver/spi_master.h" #include "esp_check.h" #include "esp_err.h" #include "esp_heap_caps.h" #include "esp_lcd_panel_commands.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_vendor.h" #include "esp_log.h" #include "esp_random.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "lvgl.h" #include "lcd_face.h" #include "sdkconfig.h" #include "soc/soc_caps.h" #define LCD_HOST SPI2_HOST #define LCD_PIXEL_CLOCK_HZ CONFIG_TQ_SCREEN_SPI_PIXEL_CLOCK_HZ #define LCD_SPI_MODE 0 #ifndef CONFIG_TQ_SCREEN_SPI_MISO_GPIO #error "CONFIG_TQ_SCREEN_SPI_MISO_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SCREEN_SPI_MOSI_GPIO #error "CONFIG_TQ_SCREEN_SPI_MOSI_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SCREEN_SPI_CLK_GPIO #error "CONFIG_TQ_SCREEN_SPI_CLK_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SCREEN_SPI_CS_GPIO #error "CONFIG_TQ_SCREEN_SPI_CS_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SCREEN_DC_GPIO #error "CONFIG_TQ_SCREEN_DC_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SCREEN_RESET_PIN #error "CONFIG_TQ_SCREEN_RESET_PIN must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SCREEN_BACKLIGHT_GPIO #error "CONFIG_TQ_SCREEN_BACKLIGHT_GPIO must be defined in Kconfig" #endif #define PIN_NUM_MISO CONFIG_TQ_SCREEN_SPI_MISO_GPIO #define PIN_NUM_MOSI CONFIG_TQ_SCREEN_SPI_MOSI_GPIO #define PIN_NUM_CLK CONFIG_TQ_SCREEN_SPI_CLK_GPIO #define PIN_NUM_CS CONFIG_TQ_SCREEN_SPI_CS_GPIO #define PIN_NUM_DC CONFIG_TQ_SCREEN_DC_GPIO #define PIN_NUM_RST CONFIG_TQ_SCREEN_RESET_PIN #define PIN_NUM_BCKL CONFIG_TQ_SCREEN_BACKLIGHT_GPIO #if CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH #define LCD_BK_LIGHT_ON_LEVEL 1 #else #define LCD_BK_LIGHT_ON_LEVEL 0 #endif #if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR #define LCD_PANEL_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR #else #define LCD_PANEL_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB #endif #if CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES #define LCD_PANEL_DATA_ENDIAN LCD_RGB_DATA_ENDIAN_LITTLE #define LCD_BOOT_SWAP_COLOR_BYTES true #else #define LCD_PANEL_DATA_ENDIAN LCD_RGB_DATA_ENDIAN_BIG #define LCD_BOOT_SWAP_COLOR_BYTES false #endif #if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR #define LCD_BOOT_COLOR_ORDER_BGR true #else #define LCD_BOOT_COLOR_ORDER_BGR false #endif #if CONFIG_TQ_SCREEN_INVERT_COLOR #define LCD_BOOT_INVERT_COLOR true #else #define LCD_BOOT_INVERT_COLOR false #endif #if CONFIG_TQ_SCREEN_MIRROR_X #define LCD_PANEL_MIRROR_X true #else #define LCD_PANEL_MIRROR_X false #endif #if CONFIG_TQ_SCREEN_SWAP_XY #define LCD_PANEL_SWAP_XY true #else #define LCD_PANEL_SWAP_XY false #endif #define LCD_H_RES 320 #define LCD_V_RES 240 #define LCD_DRAW_BUF_LINES CONFIG_TQ_SCREEN_LVGL_DRAW_BUF_LINES #define LCD_TICK_PERIOD_MS 2 #define LCD_RENDER_TASK_DELAY_MAXMS 20 #define ST7789_CMD_RAMCTRL 0xB0 #define ST7789_RAMCTRL_PARAM1 0x00 #define ST7789_RAMCTRL_PARAM2_BASE 0xF0 #define ST7789_RAMCTRL_LE_BIT (1 << 3) #define LCD_SLOT_REEL_COUNT 3 #define LCD_SLOT_SYMBOL_COUNT 5 #define LCD_SLOT_RESULT_HOLD_MS 600 #define LCD_SLOT_STATUS_SPIN_PHASES 4 #define LCD_PRESENTATION_DEFAULT_FRAME_MS 150 #define LCD_PRESENTATION_WAIT_POLL_MS 20 #define LCD_PREHEAT_DECODE_FRAMES_PER_LOOP 1 #define LCD_BOOT_PREHEAT_ASSET_DIR "spin" typedef struct { platform_lcd_presentation_kind_t kind; uint8_t priority; bool interrupt_current; bool return_to_idle; char asset_dir[PLATFORM_LCD_PRESENTATION_ASSET_DIR_MAX]; uint8_t frame_order[PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX]; size_t frame_order_count; uint16_t repeat_count; uint32_t frame_ms; } lcd_presentation_request_t; static const char *TAG = "lcd"; static SemaphoreHandle_t s_lvgl_mutex; static esp_lcd_panel_io_handle_t s_panel_io; static esp_lcd_panel_handle_t s_panel; static lv_display_t *s_display; static esp_timer_handle_t s_lvgl_tick_timer; static bool s_initialized; static bool s_default_screen_ready; static bool s_render_loop_running; static bool s_render_loop_stop_requested; static bool s_presentation_pending; static bool s_presentation_active; static bool s_preheat_pending; static bool s_preheat_active; static lcd_presentation_request_t s_pending_presentation; static lcd_presentation_request_t s_active_presentation; static lcd_presentation_request_t s_pending_preheat; static lcd_presentation_request_t s_active_preheat; static uint32_t s_presentation_completion_count; static portMUX_TYPE s_presentation_lock = portMUX_INITIALIZER_UNLOCKED; static SemaphoreHandle_t s_presentation_completion_sem; static TaskHandle_t s_render_loop_task; #if CONFIG_TQ_SCREEN_COLOR_AUTOTEST static lv_obj_t *s_color_profile_label; #endif typedef struct { uint8_t cmd; uint8_t data[16]; uint8_t databytes; // Number of bytes in data; bit7 means delay after command; 0xFF marks end. } lcd_init_cmd_t; typedef struct { bool color_order_bgr; bool swap_color_bytes; bool invert_color; } lcd_color_profile_t; static void lcd_log_heap_snapshot(const char *phase) { size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); size_t largest_internal = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); size_t largest_dma = heap_caps_get_largest_free_block(MALLOC_CAP_DMA); #if SOC_SPIRAM_SUPPORTED size_t free_spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); size_t largest_spiram = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); ESP_LOGI(TAG, "Heap %s: free_internal=%u largest_internal=%u largest_dma=%u free_spiram=%u largest_spiram=%u", phase != NULL ? phase : "", (unsigned)free_internal, (unsigned)largest_internal, (unsigned)largest_dma, (unsigned)free_spiram, (unsigned)largest_spiram); #else ESP_LOGI(TAG, "Heap %s: free_internal=%u largest_internal=%u largest_dma=%u", phase != NULL ? phase : "", (unsigned)free_internal, (unsigned)largest_internal, (unsigned)largest_dma); #endif } typedef struct { lv_obj_t *panel; lv_obj_t *top_label; lv_obj_t *center_label; lv_obj_t *bottom_label; uint32_t start_index; uint32_t current_index; uint32_t final_index; uint32_t stop_ms; uint32_t step_interval_ms; bool stopped; } lcd_slot_reel_t; typedef struct { lv_obj_t *overlay; lv_obj_t *status_label; int64_t start_ms; int64_t hold_deadline_ms; lcd_slot_reel_t reels[LCD_SLOT_REEL_COUNT]; } lcd_slot_animation_state_t; static const lcd_color_profile_t s_boot_color_profile = { .color_order_bgr = LCD_BOOT_COLOR_ORDER_BGR, .swap_color_bytes = LCD_BOOT_SWAP_COLOR_BYTES, .invert_color = LCD_BOOT_INVERT_COLOR, }; #if CONFIG_TQ_SCREEN_COLOR_AUTOTEST static const lcd_color_profile_t s_color_profiles[] = { {.color_order_bgr = false, .swap_color_bytes = false, .invert_color = false}, {.color_order_bgr = false, .swap_color_bytes = true, .invert_color = false}, {.color_order_bgr = true, .swap_color_bytes = false, .invert_color = false}, {.color_order_bgr = true, .swap_color_bytes = true, .invert_color = false}, {.color_order_bgr = false, .swap_color_bytes = false, .invert_color = true}, {.color_order_bgr = false, .swap_color_bytes = true, .invert_color = true}, {.color_order_bgr = true, .swap_color_bytes = false, .invert_color = true}, {.color_order_bgr = true, .swap_color_bytes = true, .invert_color = true}, }; #endif static const lcd_init_cmd_t s_st7789_init_cmds[] = { {0x36, {(1 << 5) | (1 << 6)}, 1}, {0x3A, {0x55}, 1}, {0xB2, {0x0C, 0x0C, 0x00, 0x33, 0x33}, 5}, {0xB7, {0x45}, 1}, {0xBB, {0x2B}, 1}, {0xC0, {0x2C}, 1}, {0xC2, {0x01, 0xFF}, 2}, {0xC3, {0x11}, 1}, {0xC4, {0x20}, 1}, {0xC6, {0x0F}, 1}, {0xD0, {0xA4, 0xA1}, 2}, {0xE0, {0xD0, 0x00, 0x05, 0x0E, 0x15, 0x0D, 0x37, 0x43, 0x47, 0x09, 0x15, 0x12, 0x16, 0x19}, 14}, {0xE1, {0xD0, 0x00, 0x05, 0x0D, 0x0C, 0x06, 0x2D, 0x44, 0x40, 0x0E, 0x1C, 0x18, 0x16, 0x19}, 14}, {0x11, {0}, 0x80}, {0x29, {0}, 0x80}, {0, {0}, 0xFF} }; static const char *s_slot_symbols[LCD_SLOT_SYMBOL_COUNT] = { "7", "BAR", "CAT", "GEM", "WIN", }; static const uint32_t s_slot_reel_stop_ms[LCD_SLOT_REEL_COUNT] = {1200, 1500, 1800}; static const uint32_t s_slot_reel_step_interval_ms[LCD_SLOT_REEL_COUNT] = {70, 85, 100}; static const lv_coord_t s_slot_reel_x_offsets[LCD_SLOT_REEL_COUNT] = {-100, 0, 100}; static lcd_slot_animation_state_t s_slot_animation; static const char *lcd_presentation_kind_name(platform_lcd_presentation_kind_t kind) { switch (kind) { case PLATFORM_LCD_PRESENTATION_IDLE: return "idle"; case PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE: return "asset_sequence"; case PLATFORM_LCD_PRESENTATION_SLOT_REELS: return "slot_reels"; default: return "unknown"; } } static bool lcd_copy_presentation_request(lcd_presentation_request_t *out, const platform_lcd_presentation_request_t *request) { if (out == NULL || request == NULL) { return false; } memset(out, 0, sizeof(*out)); out->kind = request->kind; out->priority = request->priority; out->interrupt_current = request->interrupt_current; out->return_to_idle = request->return_to_idle; out->repeat_count = request->repeat_count; out->frame_ms = request->frame_ms > 0U ? request->frame_ms : LCD_PRESENTATION_DEFAULT_FRAME_MS; if (request->frame_order_count > PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX) { return false; } out->frame_order_count = request->frame_order_count; if (request->frame_order_count > 0U) { if (request->frame_order == NULL) { return false; } memcpy(out->frame_order, request->frame_order, request->frame_order_count); } if (request->kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { if (request->asset_dir == NULL || request->asset_dir[0] == '\0') { return false; } size_t asset_dir_len = strlen(request->asset_dir); if (asset_dir_len >= sizeof(out->asset_dir)) { return false; } memcpy(out->asset_dir, request->asset_dir, asset_dir_len + 1U); } return true; } static bool lcd_presentation_requests_match(const lcd_presentation_request_t *left, const lcd_presentation_request_t *right) { if (left == NULL || right == NULL || left->kind != right->kind) { return false; } if (left->kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { return strcmp(left->asset_dir, right->asset_dir) == 0 && left->frame_order_count == right->frame_order_count && memcmp(left->frame_order, right->frame_order, left->frame_order_count) == 0 && left->repeat_count == right->repeat_count && left->frame_ms == right->frame_ms; } return true; } static bool lcd_presentation_should_queue_locked(const lcd_presentation_request_t *request) { if (s_presentation_pending) { if (lcd_presentation_requests_match(request, &s_pending_presentation)) { return false; } if (request->priority < s_pending_presentation.priority) { return false; } if (request->priority == s_pending_presentation.priority && !request->interrupt_current) { return false; } } if (s_presentation_active) { if (lcd_presentation_requests_match(request, &s_active_presentation)) { return false; } if (request->priority < s_active_presentation.priority) { return false; } if (request->priority == s_active_presentation.priority && !request->interrupt_current) { return false; } } return true; } static void lcd_presentation_set_active(const lcd_presentation_request_t *request) { portENTER_CRITICAL(&s_presentation_lock); if (request != NULL && request->kind != PLATFORM_LCD_PRESENTATION_IDLE) { s_active_presentation = *request; s_presentation_active = true; } else { memset(&s_active_presentation, 0, sizeof(s_active_presentation)); s_presentation_active = false; } portEXIT_CRITICAL(&s_presentation_lock); } static bool lcd_presentation_take_pending(lcd_presentation_request_t *out) { bool has_pending = false; portENTER_CRITICAL(&s_presentation_lock); if (s_presentation_pending) { *out = s_pending_presentation; memset(&s_pending_presentation, 0, sizeof(s_pending_presentation)); s_presentation_pending = false; has_pending = true; } portEXIT_CRITICAL(&s_presentation_lock); return has_pending; } static bool lcd_preheat_take_pending(lcd_presentation_request_t *out) { bool has_pending = false; portENTER_CRITICAL(&s_presentation_lock); if (s_preheat_pending) { *out = s_pending_preheat; memset(&s_pending_preheat, 0, sizeof(s_pending_preheat)); s_preheat_pending = false; has_pending = true; } portEXIT_CRITICAL(&s_presentation_lock); return has_pending; } static bool lcd_presentation_get_active(lcd_presentation_request_t *out) { bool is_active = false; portENTER_CRITICAL(&s_presentation_lock); if (s_presentation_active) { if (out != NULL) { *out = s_active_presentation; } is_active = true; } portEXIT_CRITICAL(&s_presentation_lock); return is_active; } static TaskHandle_t lcd_get_render_loop_task_handle(void) { TaskHandle_t task = NULL; portENTER_CRITICAL(&s_presentation_lock); task = s_render_loop_task; portEXIT_CRITICAL(&s_presentation_lock); return task; } static bool lcd_render_loop_mark_started(TaskHandle_t task) { bool started = false; portENTER_CRITICAL(&s_presentation_lock); if (!s_render_loop_running && s_render_loop_task == NULL) { s_render_loop_task = task; s_render_loop_running = true; s_render_loop_stop_requested = false; started = true; } portEXIT_CRITICAL(&s_presentation_lock); return started; } static bool lcd_render_loop_stop_requested(void) { bool stop_requested; portENTER_CRITICAL(&s_presentation_lock); stop_requested = s_render_loop_stop_requested; portEXIT_CRITICAL(&s_presentation_lock); return stop_requested; } static bool lcd_render_loop_stopped(void) { bool stopped; portENTER_CRITICAL(&s_presentation_lock); stopped = !s_render_loop_running && s_render_loop_task == NULL; portEXIT_CRITICAL(&s_presentation_lock); return stopped; } static TaskHandle_t lcd_render_loop_request_stop(void) { TaskHandle_t task; portENTER_CRITICAL(&s_presentation_lock); if (s_render_loop_running || s_render_loop_task != NULL) { s_render_loop_stop_requested = true; } else { s_render_loop_stop_requested = false; } task = s_render_loop_task; portEXIT_CRITICAL(&s_presentation_lock); return task; } static void lcd_render_loop_mark_stopped(TaskHandle_t task) { portENTER_CRITICAL(&s_presentation_lock); if (s_render_loop_task == task) { s_render_loop_task = NULL; } s_render_loop_running = false; s_render_loop_stop_requested = false; portEXIT_CRITICAL(&s_presentation_lock); } static void lcd_render_loop_force_stopped(TaskHandle_t task) { portENTER_CRITICAL(&s_presentation_lock); if (s_render_loop_task == task) { s_render_loop_task = NULL; } s_render_loop_running = false; s_render_loop_stop_requested = false; portEXIT_CRITICAL(&s_presentation_lock); } static void lcd_notify_render_task(void) { TaskHandle_t task = lcd_get_render_loop_task_handle(); if (task != NULL) { xTaskNotifyGive(task); } } static uint32_t lcd_slot_wrap_index(int32_t index) { int32_t wrapped = index % LCD_SLOT_SYMBOL_COUNT; if (wrapped < 0) { wrapped += LCD_SLOT_SYMBOL_COUNT; } return (uint32_t)wrapped; } static void lcd_slot_set_reel_style(lcd_slot_reel_t *reel) { if (reel == NULL || reel->panel == NULL || reel->center_label == NULL) { return; } lv_color_t border_color = reel->stopped ? lv_color_hex(0xD9A441) : lv_color_hex(0x4A4A4A); lv_color_t center_color = reel->stopped ? lv_color_hex(0xFFE6A3) : lv_color_hex(0xFFFFFF); lv_obj_set_style_border_color(reel->panel, border_color, LV_PART_MAIN); lv_obj_set_style_text_color(reel->center_label, center_color, LV_PART_MAIN); } static void lcd_slot_update_reel_labels(lcd_slot_reel_t *reel, uint32_t center_index) { if (reel == NULL || reel->top_label == NULL || reel->center_label == NULL || reel->bottom_label == NULL) { return; } if (reel->current_index == center_index) { return; } reel->current_index = center_index; lv_label_set_text(reel->top_label, s_slot_symbols[lcd_slot_wrap_index((int32_t)center_index - 1)]); lv_label_set_text(reel->center_label, s_slot_symbols[lcd_slot_wrap_index((int32_t)center_index)]); lv_label_set_text(reel->bottom_label, s_slot_symbols[lcd_slot_wrap_index((int32_t)center_index + 1)]); } static void lcd_slot_update_status_label(bool all_stopped, int64_t elapsed_ms) { if (s_slot_animation.status_label == NULL) { return; } if (!all_stopped) { static const char *const spin_status[LCD_SLOT_STATUS_SPIN_PHASES] = { "SPIN", "SPIN.", "SPIN..", "SPIN..." }; size_t phase = (size_t)((elapsed_ms / 180) % LCD_SLOT_STATUS_SPIN_PHASES); lv_label_set_text(s_slot_animation.status_label, spin_status[phase]); return; } uint32_t left = s_slot_animation.reels[0].final_index; uint32_t mid = s_slot_animation.reels[1].final_index; uint32_t right = s_slot_animation.reels[2].final_index; if (left == mid && mid == right) { lv_label_set_text(s_slot_animation.status_label, "JACKPOT"); } else if (left == mid || mid == right || left == right) { lv_label_set_text(s_slot_animation.status_label, "DOUBLE"); } else { lv_label_set_text(s_slot_animation.status_label, "SPIN COMPLETE"); } } static void lcd_slot_delete_overlay(void) { if (s_slot_animation.overlay != NULL) { lv_obj_delete(s_slot_animation.overlay); } memset(&s_slot_animation, 0, sizeof(s_slot_animation)); } static void lcd_slot_create_reel(size_t index) { lcd_slot_reel_t *reel = &s_slot_animation.reels[index]; reel->panel = lv_obj_create(s_slot_animation.overlay); if (reel->panel == NULL) { return; } lv_obj_set_size(reel->panel, 82, 124); lv_obj_align(reel->panel, LV_ALIGN_CENTER, s_slot_reel_x_offsets[index], 6); lv_obj_set_style_bg_color(reel->panel, lv_color_hex(0x151515), LV_PART_MAIN); lv_obj_set_style_border_width(reel->panel, 2, LV_PART_MAIN); lv_obj_set_style_radius(reel->panel, 10, LV_PART_MAIN); lv_obj_set_style_pad_all(reel->panel, 0, LV_PART_MAIN); lv_obj_clear_flag(reel->panel, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *marker = lv_obj_create(reel->panel); if (marker != NULL) { lv_obj_set_size(marker, 70, 34); lv_obj_align(marker, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_bg_color(marker, lv_color_hex(0x24374D), LV_PART_MAIN); lv_obj_set_style_border_width(marker, 1, LV_PART_MAIN); lv_obj_set_style_border_color(marker, lv_color_hex(0xD9A441), LV_PART_MAIN); lv_obj_set_style_radius(marker, 6, LV_PART_MAIN); lv_obj_clear_flag(marker, LV_OBJ_FLAG_SCROLLABLE); reel->center_label = lv_label_create(marker); if (reel->center_label != NULL) { lv_obj_center(reel->center_label); } } reel->top_label = lv_label_create(reel->panel); if (reel->top_label != NULL) { lv_obj_align(reel->top_label, LV_ALIGN_TOP_MID, 0, 14); lv_obj_set_style_text_color(reel->top_label, lv_color_hex(0x6F6F6F), LV_PART_MAIN); } reel->bottom_label = lv_label_create(reel->panel); if (reel->bottom_label != NULL) { lv_obj_align(reel->bottom_label, LV_ALIGN_BOTTOM_MID, 0, -14); lv_obj_set_style_text_color(reel->bottom_label, lv_color_hex(0x6F6F6F), LV_PART_MAIN); } reel->start_index = esp_random() % LCD_SLOT_SYMBOL_COUNT; reel->current_index = UINT32_MAX; reel->final_index = esp_random() % LCD_SLOT_SYMBOL_COUNT; reel->stop_ms = s_slot_reel_stop_ms[index]; reel->step_interval_ms = s_slot_reel_step_interval_ms[index]; reel->stopped = false; lcd_slot_set_reel_style(reel); lcd_slot_update_reel_labels(reel, reel->start_index); } static esp_err_t lcd_slot_begin_animation(void) { lcd_slot_delete_overlay(); s_slot_animation.overlay = lv_obj_create(lv_screen_active()); if (s_slot_animation.overlay == NULL) { ESP_LOGE(TAG, "slot overlay create failed"); return ESP_ERR_NO_MEM; } lv_obj_set_size(s_slot_animation.overlay, LCD_H_RES, LCD_V_RES); lv_obj_align(s_slot_animation.overlay, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_bg_color(s_slot_animation.overlay, lv_color_hex(0x050505), LV_PART_MAIN); lv_obj_set_style_border_width(s_slot_animation.overlay, 0, LV_PART_MAIN); lv_obj_set_style_radius(s_slot_animation.overlay, 0, LV_PART_MAIN); lv_obj_set_style_pad_all(s_slot_animation.overlay, 0, LV_PART_MAIN); lv_obj_clear_flag(s_slot_animation.overlay, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *title = lv_label_create(s_slot_animation.overlay); if (title != NULL) { lv_label_set_text(title, "SLOT MACHINE"); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20); lv_obj_set_style_text_color(title, lv_color_hex(0xF6F1D3), LV_PART_MAIN); } lv_obj_t *frame = lv_obj_create(s_slot_animation.overlay); if (frame != NULL) { lv_obj_set_size(frame, 292, 142); lv_obj_align(frame, LV_ALIGN_CENTER, 0, 6); lv_obj_set_style_bg_opa(frame, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_style_border_width(frame, 2, LV_PART_MAIN); lv_obj_set_style_border_color(frame, lv_color_hex(0x8E6A25), LV_PART_MAIN); lv_obj_set_style_radius(frame, 12, LV_PART_MAIN); lv_obj_clear_flag(frame, LV_OBJ_FLAG_SCROLLABLE); } for (size_t i = 0; i < LCD_SLOT_REEL_COUNT; ++i) { lcd_slot_create_reel(i); } s_slot_animation.status_label = lv_label_create(s_slot_animation.overlay); if (s_slot_animation.status_label != NULL) { lv_label_set_text(s_slot_animation.status_label, "SPIN"); lv_obj_align(s_slot_animation.status_label, LV_ALIGN_BOTTOM_MID, 0, -18); lv_obj_set_style_text_color(s_slot_animation.status_label, lv_color_hex(0xF6F1D3), LV_PART_MAIN); } s_slot_animation.start_ms = esp_timer_get_time() / 1000; s_slot_animation.hold_deadline_ms = 0; ESP_LOGI(TAG, "Slot animation started"); return ESP_OK; } static bool lcd_slot_update_animation(void) { if (s_slot_animation.overlay == NULL) { return true; } int64_t now_ms = esp_timer_get_time() / 1000; int64_t elapsed_ms = now_ms - s_slot_animation.start_ms; bool all_stopped = true; for (size_t i = 0; i < LCD_SLOT_REEL_COUNT; ++i) { lcd_slot_reel_t *reel = &s_slot_animation.reels[i]; bool stopped = (uint32_t)elapsed_ms >= reel->stop_ms; uint32_t center_index = reel->final_index; if (!stopped) { uint32_t steps = (uint32_t)elapsed_ms / reel->step_interval_ms; center_index = lcd_slot_wrap_index((int32_t)reel->start_index + (int32_t)steps); all_stopped = false; } if (reel->stopped != stopped) { reel->stopped = stopped; lcd_slot_set_reel_style(reel); } lcd_slot_update_reel_labels(reel, center_index); } lcd_slot_update_status_label(all_stopped, elapsed_ms); if (!all_stopped) { return false; } if (s_slot_animation.hold_deadline_ms == 0) { s_slot_animation.hold_deadline_ms = now_ms + LCD_SLOT_RESULT_HOLD_MS; ESP_LOGI(TAG, "Slot animation finished: %s | %s | %s", s_slot_symbols[s_slot_animation.reels[0].final_index], s_slot_symbols[s_slot_animation.reels[1].final_index], s_slot_symbols[s_slot_animation.reels[2].final_index]); } if (now_ms >= s_slot_animation.hold_deadline_ms) { lcd_slot_delete_overlay(); return true; } return false; } static void lcd_presentation_cancel_active(void) { lcd_presentation_request_t active = {0}; if (!lcd_presentation_get_active(&active)) { return; } if (active.kind == PLATFORM_LCD_PRESENTATION_SLOT_REELS) { lcd_slot_delete_overlay(); } else if (active.kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { (void)lcd_face_show_idle(lv_screen_active()); } lcd_presentation_set_active(NULL); } static esp_err_t lcd_presentation_start(const lcd_presentation_request_t *request) { if (request == NULL) { return ESP_ERR_INVALID_ARG; } switch (request->kind) { case PLATFORM_LCD_PRESENTATION_IDLE: return lcd_face_show_idle(lv_screen_active()); case PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE: { const lcd_face_asset_sequence_t sequence = { .asset_dir = request->asset_dir, .frame_order = request->frame_order, .frame_order_count = request->frame_order_count, .repeat_count = request->repeat_count, .frame_ms = request->frame_ms, .return_to_idle = request->return_to_idle, }; return lcd_face_play_asset_sequence(lv_screen_active(), &sequence); } case PLATFORM_LCD_PRESENTATION_SLOT_REELS: return lcd_slot_begin_animation(); default: return ESP_ERR_INVALID_ARG; } } static void lcd_presentation_process_pending(void) { lcd_presentation_request_t request = {0}; if (!lcd_presentation_take_pending(&request)) { return; } lcd_presentation_cancel_active(); esp_err_t err = lcd_presentation_start(&request); if (err == ESP_OK) { ESP_LOGI(TAG, "LCD presentation started: kind=%s priority=%u", lcd_presentation_kind_name(request.kind), (unsigned int)request.priority); lcd_presentation_set_active(&request); return; } ESP_LOGW(TAG, "LCD presentation start failed: kind=%s err=%s", lcd_presentation_kind_name(request.kind), esp_err_to_name(err)); (void)lcd_face_show_idle(lv_screen_active()); lcd_presentation_set_active(NULL); } static void lcd_preheat_process_pending(void) { lcd_presentation_request_t pending = {0}; if (lcd_preheat_take_pending(&pending)) { s_active_preheat = pending; s_preheat_active = true; } if (!s_preheat_active) { return; } if (s_active_preheat.kind != PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { memset(&s_active_preheat, 0, sizeof(s_active_preheat)); s_preheat_active = false; return; } const lcd_face_asset_sequence_t sequence = { .asset_dir = s_active_preheat.asset_dir, .frame_order = s_active_preheat.frame_order, .frame_order_count = s_active_preheat.frame_order_count, .repeat_count = s_active_preheat.repeat_count, .frame_ms = s_active_preheat.frame_ms, .return_to_idle = s_active_preheat.return_to_idle, }; bool complete = false; esp_err_t err = lcd_face_preheat_asset_sequence(&sequence, LCD_PREHEAT_DECODE_FRAMES_PER_LOOP, &complete); if (err != ESP_OK) { ESP_LOGW(TAG, "LCD preheat failed: asset=%s err=%s", s_active_preheat.asset_dir, esp_err_to_name(err)); memset(&s_active_preheat, 0, sizeof(s_active_preheat)); s_preheat_active = false; return; } if (complete) { ESP_LOGI(TAG, "LCD preheat complete: asset=%s", s_active_preheat.asset_dir); memset(&s_active_preheat, 0, sizeof(s_active_preheat)); s_preheat_active = false; } } static void lcd_schedule_boot_preheat(void) { memset(&s_active_preheat, 0, sizeof(s_active_preheat)); s_active_preheat.kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE; s_active_preheat.asset_dir[0] = '\0'; strlcpy(s_active_preheat.asset_dir, LCD_BOOT_PREHEAT_ASSET_DIR, sizeof(s_active_preheat.asset_dir)); s_active_preheat.frame_ms = LCD_PRESENTATION_DEFAULT_FRAME_MS; s_preheat_active = true; ESP_LOGI(TAG, "LCD boot preheat scheduled: asset=%s", s_active_preheat.asset_dir); } static void lcd_signal_presentation_completion(void) { if (s_presentation_completion_sem != NULL) { (void)xSemaphoreGive(s_presentation_completion_sem); } } static void lcd_clear_preheat_state(void) { portENTER_CRITICAL(&s_presentation_lock); s_preheat_pending = false; memset(&s_pending_preheat, 0, sizeof(s_pending_preheat)); portEXIT_CRITICAL(&s_presentation_lock); s_preheat_active = false; memset(&s_active_preheat, 0, sizeof(s_active_preheat)); } static void lcd_presentation_update_active(void) { lcd_presentation_request_t active = {0}; if (!lcd_presentation_get_active(&active)) { return; } if (active.kind == PLATFORM_LCD_PRESENTATION_SLOT_REELS) { if (lcd_slot_update_animation()) { lcd_presentation_set_active(NULL); } return; } if (active.kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { bool return_to_idle = false; if (lcd_face_take_completed(&return_to_idle)) { if (return_to_idle) { (void)lcd_face_show_idle(lv_screen_active()); } lcd_presentation_set_active(NULL); /* Publish completion so blocking callers (e.g. the wealth spin * intro) can gate on a finite asset sequence finishing its cycles. */ portENTER_CRITICAL(&s_presentation_lock); s_presentation_completion_count++; portEXIT_CRITICAL(&s_presentation_lock); lcd_signal_presentation_completion(); } } } static bool lcd_lock(int timeout_ms) { if (s_lvgl_mutex == NULL) { return false; } return xSemaphoreTakeRecursive(s_lvgl_mutex, pdMS_TO_TICKS(timeout_ms)) == pdTRUE; } static void lcd_unlock(void) { if (s_lvgl_mutex != NULL) { (void)xSemaphoreGiveRecursive(s_lvgl_mutex); } } static void lvgl_tick_cb(void *arg) { (void)arg; lv_tick_inc(LCD_TICK_PERIOD_MS); } static esp_err_t lcd_apply_color_profile(const lcd_color_profile_t *profile) { ESP_RETURN_ON_FALSE(profile != NULL, ESP_ERR_INVALID_ARG, TAG, "color profile is null"); uint8_t madctl = 0; if (LCD_PANEL_MIRROR_X) { madctl |= LCD_CMD_MX_BIT; } if (LCD_PANEL_SWAP_XY) { madctl |= LCD_CMD_MV_BIT; } if (profile->color_order_bgr) { madctl |= LCD_CMD_BGR_BIT; } uint8_t ramctrl[] = {ST7789_RAMCTRL_PARAM1, ST7789_RAMCTRL_PARAM2_BASE}; if (profile->swap_color_bytes) { ramctrl[1] |= ST7789_RAMCTRL_LE_BIT; } ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, LCD_CMD_MADCTL, &madctl, 1), TAG, "panel set MADCTL failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, ST7789_CMD_RAMCTRL, ramctrl, sizeof(ramctrl)), TAG, "panel set RAMCTRL failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_invert_color(s_panel, profile->invert_color), TAG, "panel set invert failed"); return ESP_OK; } #if CONFIG_TQ_SCREEN_COLOR_AUTOTEST static size_t lcd_find_matching_profile(const lcd_color_profile_t *target) { for (size_t i = 0; i < sizeof(s_color_profiles) / sizeof(s_color_profiles[0]); i++) { if (s_color_profiles[i].color_order_bgr == target->color_order_bgr && s_color_profiles[i].swap_color_bytes == target->swap_color_bytes && s_color_profiles[i].invert_color == target->invert_color) { return i; } } return 0; } static void lcd_log_color_profile(size_t index, const lcd_color_profile_t *profile) { ESP_LOGI(TAG, "Color profile %u: bgr=%d swap_bytes=%d invert=%d mirror_x=%d swap_xy=%d x_gap=%d y_gap=%d", (unsigned int)index, profile->color_order_bgr, profile->swap_color_bytes, profile->invert_color, LCD_PANEL_MIRROR_X, LCD_PANEL_SWAP_XY, CONFIG_TQ_SCREEN_X_GAP, CONFIG_TQ_SCREEN_Y_GAP); } static void lcd_update_color_profile_label(size_t index, const lcd_color_profile_t *profile) { if (s_color_profile_label == NULL || profile == NULL) { return; } lv_label_set_text_fmt(s_color_profile_label, "profile=%u bgr=%d swap_bytes=%d invert=%d", (unsigned int)index, profile->color_order_bgr, profile->swap_color_bytes, profile->invert_color); } static lv_obj_t *lcd_create_color_patch(lv_obj_t *parent, const char *name, uint32_t color_hex) { lv_obj_t *patch = lv_obj_create(parent); lv_obj_set_size(patch, 72, 56); lv_obj_set_style_bg_color(patch, lv_color_hex(color_hex), LV_PART_MAIN); lv_obj_set_style_border_width(patch, 1, LV_PART_MAIN); lv_obj_set_style_border_color(patch, lv_color_hex(0x2A2A2A), LV_PART_MAIN); lv_obj_set_style_radius(patch, 4, LV_PART_MAIN); lv_obj_clear_flag(patch, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *label = lv_label_create(patch); lv_label_set_text(label, name); lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), LV_PART_MAIN); lv_obj_center(label); return patch; } static void lcd_create_color_autotest_screen(void) { lv_obj_t *screen = lv_screen_active(); lv_obj_set_style_bg_color(screen, lv_color_hex(0x101010), LV_PART_MAIN); lv_obj_set_style_pad_all(screen, 10, LV_PART_MAIN); lv_obj_t *title = lv_label_create(screen); lv_label_set_text(title, "ST7789 Color Auto-Test"); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), LV_PART_MAIN); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0); lv_obj_t *grid = lv_obj_create(screen); lv_obj_set_size(grid, 312, 140); lv_obj_align(grid, LV_ALIGN_TOP_MID, 0, 24); lv_obj_set_style_bg_opa(grid, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_style_border_width(grid, 0, LV_PART_MAIN); lv_obj_set_style_pad_all(grid, 0, LV_PART_MAIN); lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP); lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE); (void)lcd_create_color_patch(grid, "RED", 0xFF0000); (void)lcd_create_color_patch(grid, "GREEN", 0x00FF00); (void)lcd_create_color_patch(grid, "BLUE", 0x0000FF); (void)lcd_create_color_patch(grid, "WHITE", 0xFFFFFF); (void)lcd_create_color_patch(grid, "YELLOW", 0xFFFF00); (void)lcd_create_color_patch(grid, "CYAN", 0x00FFFF); (void)lcd_create_color_patch(grid, "MAGENTA", 0xFF00FF); (void)lcd_create_color_patch(grid, "BLACK", 0x000000); s_color_profile_label = lv_label_create(screen); lv_label_set_text(s_color_profile_label, "profile=NA bgr=0 swap_bytes=0 invert=0"); lv_obj_set_style_text_color(s_color_profile_label, lv_color_hex(0xFFFFFF), LV_PART_MAIN); lv_obj_align(s_color_profile_label, LV_ALIGN_BOTTOM_MID, 0, -8); } #endif static esp_err_t lcd_apply_st7789_init_cmds(void) { for (size_t i = 0; s_st7789_init_cmds[i].databytes != 0xFF; i++) { const lcd_init_cmd_t *cmd = &s_st7789_init_cmds[i]; size_t payload_len = cmd->databytes & 0x1F; const uint8_t *payload = (payload_len > 0) ? cmd->data : NULL; ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, cmd->cmd, payload, payload_len), TAG, "panel custom init cmd 0x%02X failed", cmd->cmd); if (cmd->databytes & 0x80) { vTaskDelay(pdMS_TO_TICKS(120)); } } return ESP_OK; } static bool on_color_trans_done(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *event_data, void *user_ctx) { (void)panel_io; (void)event_data; lv_display_t *display = (lv_display_t *)user_ctx; if (display != NULL) { lv_display_flush_ready(display); } return false; } static void lvgl_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *color_map) { esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)lv_display_get_user_data(display); esp_err_t err = esp_lcd_panel_draw_bitmap(panel, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_map); if (err != ESP_OK) { ESP_LOGE(TAG, "panel draw failed: %s", esp_err_to_name(err)); lv_display_flush_ready(display); } } static esp_err_t lcd_backlight_gpio_init(void) { gpio_config_t io_conf = { .pin_bit_mask = (1ULL << PIN_NUM_BCKL), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; return gpio_config(&io_conf); } static esp_err_t lcd_panel_init(void) { spi_bus_config_t buscfg = { .sclk_io_num = PIN_NUM_CLK, .mosi_io_num = PIN_NUM_MOSI, .miso_io_num = PIN_NUM_MISO, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = LCD_H_RES * LCD_DRAW_BUF_LINES * sizeof(uint16_t), }; ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO), TAG, "spi bus init failed"); esp_lcd_panel_io_spi_config_t io_config = { .dc_gpio_num = PIN_NUM_DC, .cs_gpio_num = PIN_NUM_CS, .pclk_hz = LCD_PIXEL_CLOCK_HZ, .lcd_cmd_bits = 8, .lcd_param_bits = 8, .spi_mode = LCD_SPI_MODE, .trans_queue_depth = 10, .on_color_trans_done = NULL, .user_ctx = NULL, }; ESP_RETURN_ON_ERROR( esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &s_panel_io), TAG, "new panel io failed"); esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = PIN_NUM_RST, .rgb_ele_order = LCD_PANEL_RGB_ORDER, .data_endian = LCD_PANEL_DATA_ENDIAN, .bits_per_pixel = 16, }; ESP_RETURN_ON_ERROR(esp_lcd_new_panel_st7789(s_panel_io, &panel_config, &s_panel), TAG, "new panel failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(s_panel), TAG, "panel reset failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel init failed"); ESP_RETURN_ON_ERROR(lcd_apply_st7789_init_cmds(), TAG, "apply custom panel init failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_swap_xy(s_panel, LCD_PANEL_SWAP_XY), TAG, "panel swap_xy failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_mirror(s_panel, LCD_PANEL_MIRROR_X, false), TAG, "panel mirror failed"); ESP_RETURN_ON_ERROR(lcd_apply_color_profile(&s_boot_color_profile), TAG, "apply boot color profile failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_set_gap(s_panel, CONFIG_TQ_SCREEN_X_GAP, CONFIG_TQ_SCREEN_Y_GAP), TAG, "panel set gap failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_disp_on_off(s_panel, true), TAG, "panel display on failed"); return ESP_OK; } static esp_err_t lcd_lvgl_init(void) { lv_init(); size_t draw_buf_pixels = LCD_H_RES * LCD_DRAW_BUF_LINES; size_t draw_buf_bytes = draw_buf_pixels * sizeof(uint16_t); lcd_log_heap_snapshot("before_lvgl_draw_buffers"); void *buf1 = heap_caps_malloc(draw_buf_bytes, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); void *buf2 = heap_caps_malloc(draw_buf_bytes, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (buf1 == NULL || buf2 == NULL) { ESP_LOGE(TAG, "LVGL draw buffer alloc failed (bytes_each=%u buf1=%p buf2=%p)", (unsigned)draw_buf_bytes, buf1, buf2); heap_caps_free(buf1); heap_caps_free(buf2); lcd_log_heap_snapshot("after_lvgl_draw_buffer_alloc_failed"); return ESP_ERR_NO_MEM; } lcd_log_heap_snapshot("after_lvgl_draw_buffers"); s_display = lv_display_create(LCD_H_RES, LCD_V_RES); if (s_display == NULL) { heap_caps_free(buf1); heap_caps_free(buf2); ESP_LOGE(TAG, "lv display create failed"); return ESP_FAIL; } lv_display_set_color_format(s_display, LV_COLOR_FORMAT_RGB565); lv_display_set_buffers(s_display, buf1, buf2, draw_buf_bytes, LV_DISPLAY_RENDER_MODE_PARTIAL); lv_display_set_flush_cb(s_display, lvgl_flush_cb); lv_display_set_user_data(s_display, s_panel); esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = on_color_trans_done, }; ESP_RETURN_ON_ERROR(esp_lcd_panel_io_register_event_callbacks(s_panel_io, &cbs, s_display), TAG, "register io callbacks failed"); const esp_timer_create_args_t lvgl_tick_timer_args = { .callback = lvgl_tick_cb, .arg = NULL, .dispatch_method = ESP_TIMER_TASK, .name = "lvgl_tick", .skip_unhandled_events = true, }; ESP_RETURN_ON_ERROR(esp_timer_create(&lvgl_tick_timer_args, &s_lvgl_tick_timer), TAG, "tick timer create failed"); ESP_RETURN_ON_ERROR(esp_timer_start_periodic(s_lvgl_tick_timer, LCD_TICK_PERIOD_MS * 1000), TAG, "tick timer start failed"); return ESP_OK; } static void lcd_create_default_screen(void) { if (s_default_screen_ready) { return; } #if CONFIG_TQ_SCREEN_COLOR_AUTOTEST lcd_create_color_autotest_screen(); ESP_LOGI(TAG, "LVGL color auto-test screen started"); #else lv_obj_t *screen = lv_screen_active(); esp_err_t face_err = lcd_face_create_idle_screen(screen); if (face_err == ESP_OK) { ESP_LOGI(TAG, "LVGL cat face idle screen started"); } else { lv_obj_set_style_bg_color(screen, lv_color_hex(0x000000), LV_PART_MAIN); lv_obj_t *title = lv_label_create(screen); lv_label_set_text(title, "Cat face init failed"); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), LV_PART_MAIN); lv_obj_center(title); ESP_LOGE(TAG, "Failed to create cat face screen: %s", esp_err_to_name(face_err)); } #endif s_default_screen_ready = true; } esp_err_t lcd_init_device(void) { if (s_initialized) { return ESP_OK; } s_lvgl_mutex = xSemaphoreCreateRecursiveMutex(); ESP_RETURN_ON_FALSE(s_lvgl_mutex != NULL, ESP_ERR_NO_MEM, TAG, "lvgl mutex create failed"); s_presentation_completion_sem = xSemaphoreCreateBinary(); ESP_RETURN_ON_FALSE(s_presentation_completion_sem != NULL, ESP_ERR_NO_MEM, TAG, "presentation completion semaphore create failed"); ESP_RETURN_ON_ERROR(lcd_backlight_gpio_init(), TAG, "backlight gpio init failed"); ESP_RETURN_ON_ERROR(lcd_panel_init(), TAG, "panel init failed"); ESP_RETURN_ON_ERROR(lcd_lvgl_init(), TAG, "lvgl init failed"); lcd_backlight_on(); s_initialized = true; ESP_LOGI(TAG, "LCD initialized with LVGL (single ST7789V, MOSI=%d, SCLK=%d, CS=%d, DC=%d, RST=%d, BL=%d)", PIN_NUM_MOSI, PIN_NUM_CLK, PIN_NUM_CS, PIN_NUM_DC, PIN_NUM_RST, PIN_NUM_BCKL); return ESP_OK; } void lcd_backlight_on(void) { gpio_set_level(PIN_NUM_BCKL, LCD_BK_LIGHT_ON_LEVEL); } void lcd_start_render_loop(void) { #if CONFIG_TQ_SCREEN_COLOR_AUTOTEST size_t profile_index = lcd_find_matching_profile(&s_boot_color_profile); int64_t next_switch_ms = 0; #endif if (!s_initialized || s_display == NULL) { ESP_LOGW(TAG, "LCD is not initialized, skip LVGL loop"); return; } if (!lcd_render_loop_mark_started(xTaskGetCurrentTaskHandle())) { ESP_LOGI(TAG, "LCD render loop already running"); return; } if (lcd_lock(1000)) { lcd_create_default_screen(); lcd_schedule_boot_preheat(); #if CONFIG_TQ_SCREEN_COLOR_AUTOTEST ESP_LOGI(TAG, "Color auto-test enabled, interval=%dms", CONFIG_TQ_SCREEN_COLOR_AUTOTEST_INTERVAL_MS); lcd_log_color_profile(profile_index, &s_color_profiles[profile_index]); lcd_update_color_profile_label(profile_index, &s_color_profiles[profile_index]); next_switch_ms = (esp_timer_get_time() / 1000) + CONFIG_TQ_SCREEN_COLOR_AUTOTEST_INTERVAL_MS; #endif lcd_unlock(); } while (!lcd_render_loop_stop_requested()) { uint32_t wait_ms = 5; if (lcd_lock(50)) { lcd_presentation_process_pending(); lcd_presentation_update_active(); wait_ms = lv_timer_handler(); lcd_preheat_process_pending(); #if CONFIG_TQ_SCREEN_COLOR_AUTOTEST int64_t now_ms = esp_timer_get_time() / 1000; if (now_ms >= next_switch_ms) { profile_index = (profile_index + 1) % (sizeof(s_color_profiles) / sizeof(s_color_profiles[0])); esp_err_t err = lcd_apply_color_profile(&s_color_profiles[profile_index]); if (err == ESP_OK) { lcd_log_color_profile(profile_index, &s_color_profiles[profile_index]); lcd_update_color_profile_label(profile_index, &s_color_profiles[profile_index]); } else { ESP_LOGE(TAG, "Failed to apply color profile %u: %s", (unsigned int)profile_index, esp_err_to_name(err)); } next_switch_ms = now_ms + CONFIG_TQ_SCREEN_COLOR_AUTOTEST_INTERVAL_MS; } #endif lcd_unlock(); } if (wait_ms == 0 || wait_ms > LCD_RENDER_TASK_DELAY_MAXMS) { wait_ms = LCD_RENDER_TASK_DELAY_MAXMS; } TickType_t wait_ticks = pdMS_TO_TICKS(wait_ms); if (wait_ticks == 0) { wait_ticks = 1; } (void)ulTaskNotifyTake(pdTRUE, wait_ticks); } if (lcd_lock(1000)) { lcd_presentation_cancel_active(); lcd_clear_preheat_state(); lcd_unlock(); } lcd_render_loop_mark_stopped(xTaskGetCurrentTaskHandle()); } esp_err_t lcd_stop_render_loop(int timeout_ms) { if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } TaskHandle_t task = lcd_render_loop_request_stop(); lcd_notify_render_task(); if (task == NULL || timeout_ms == 0) { return ESP_OK; } TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms); if (timeout_ticks == 0) { timeout_ticks = 1; } const TickType_t start_tick = xTaskGetTickCount(); while (!lcd_render_loop_stopped() && (xTaskGetTickCount() - start_tick) < timeout_ticks) { TickType_t wait_ticks = pdMS_TO_TICKS(10); vTaskDelay(wait_ticks > 0 ? wait_ticks : 1); } if (lcd_render_loop_stopped()) { return ESP_OK; } if (task == xTaskGetCurrentTaskHandle()) { return ESP_ERR_TIMEOUT; } ESP_LOGW(TAG, "LCD render task stop timed out after %d ms; force deleting", timeout_ms); vTaskDelete(task); lcd_render_loop_force_stopped(task); return ESP_ERR_TIMEOUT; } esp_err_t lcd_request_presentation(const platform_lcd_presentation_request_t *request) { if (!s_initialized || s_display == NULL || lcd_render_loop_stop_requested()) { return ESP_ERR_INVALID_STATE; } lcd_presentation_request_t copied = {0}; if (!lcd_copy_presentation_request(&copied, request)) { return ESP_ERR_INVALID_ARG; } portENTER_CRITICAL(&s_presentation_lock); bool should_queue = lcd_presentation_should_queue_locked(&copied); if (should_queue) { s_pending_presentation = copied; s_presentation_pending = true; } portEXIT_CRITICAL(&s_presentation_lock); if (should_queue) { ESP_LOGI(TAG, "LCD presentation queued: kind=%s priority=%u interrupt=%d", lcd_presentation_kind_name(copied.kind), (unsigned int)copied.priority, copied.interrupt_current); lcd_notify_render_task(); } return ESP_OK; } esp_err_t lcd_preheat_presentation(const platform_lcd_presentation_request_t *request) { if (!s_initialized || s_display == NULL || lcd_render_loop_stop_requested()) { return ESP_ERR_INVALID_STATE; } lcd_presentation_request_t copied = {0}; if (!lcd_copy_presentation_request(&copied, request) || copied.kind != PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { return ESP_ERR_INVALID_ARG; } bool should_queue = true; portENTER_CRITICAL(&s_presentation_lock); if (s_preheat_pending && lcd_presentation_requests_match(&copied, &s_pending_preheat)) { should_queue = false; } if (should_queue) { s_pending_preheat = copied; s_preheat_pending = true; } portEXIT_CRITICAL(&s_presentation_lock); if (should_queue) { ESP_LOGI(TAG, "LCD preheat queued: asset=%s", copied.asset_dir); lcd_notify_render_task(); } return ESP_OK; } uint32_t lcd_presentation_completion_count(void) { uint32_t count; portENTER_CRITICAL(&s_presentation_lock); count = s_presentation_completion_count; portEXIT_CRITICAL(&s_presentation_lock); return count; } esp_err_t lcd_wait_presentation_completion(uint32_t since_count, uint32_t timeout_ms) { if (!s_initialized || s_display == NULL || lcd_render_loop_stop_requested()) { return ESP_ERR_INVALID_STATE; } const TickType_t start_tick = xTaskGetTickCount(); const TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms); for (;;) { uint32_t count; portENTER_CRITICAL(&s_presentation_lock); count = s_presentation_completion_count; portEXIT_CRITICAL(&s_presentation_lock); if (count != since_count) { return ESP_OK; } if ((xTaskGetTickCount() - start_tick) >= timeout_ticks) { return ESP_ERR_TIMEOUT; } if (lcd_render_loop_stop_requested()) { return ESP_ERR_INVALID_STATE; } TickType_t elapsed_ticks = xTaskGetTickCount() - start_tick; TickType_t remaining_ticks = timeout_ticks > elapsed_ticks ? timeout_ticks - elapsed_ticks : 0; if (s_presentation_completion_sem != NULL && remaining_ticks > 0) { (void)xSemaphoreTake(s_presentation_completion_sem, remaining_ticks); } else { TickType_t wait_ticks = pdMS_TO_TICKS(LCD_PRESENTATION_WAIT_POLL_MS); if (wait_ticks == 0) { wait_ticks = 1; } vTaskDelay(wait_ticks); } } }