1557 lines
51 KiB
C
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);
|
|
}
|
|
}
|
|
}
|