Files
lucky-cat/components/platform/src/lcd.c
T
2026-06-25 00:34:34 +08:00

1557 lines
51 KiB
C

/*
* LCD component: single ST7789V panel driven by esp_lcd + LVGL.
*/
#include "lcd.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#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);
}
}
}