Files
lucky-cat/components/platform/src/lcd_direct.c
T

3200 lines
132 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* ST7789P3 LCD 直接驱动后端。
*
* 本模块独占面板底层初始化和 SPI 访问,并通过串行渲染循环处理最小化的
* presentation 队列;调用方不得绕过该任务直接操作面板。
*/
#include "lcd_direct.h"
#include "lcd_direct_assets.h"
#include "platform_asset_bundle.h"
#include "lcd_status_assets.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "driver/spi_master.h"
#include "esp_attr.h"
#include "esp_check.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_heap_caps.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "soc/soc_caps.h"
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#include "gfx.h"
#include "lcd_emote_assets.h"
#include "lcd_emote_listening.h"
#include "lcd_emote_fortune.h"
#endif
#define LCD_DIRECT_SPI_HOST SPI2_HOST
#define LCD_DIRECT_NATIVE_WIDTH 240U
#define LCD_DIRECT_NATIVE_HEIGHT 320U
#if CONFIG_TQ_SCREEN_SWAP_XY
#define LCD_DIRECT_WIDTH LCD_DIRECT_NATIVE_HEIGHT
#define LCD_DIRECT_HEIGHT LCD_DIRECT_NATIVE_WIDTH
#else
#define LCD_DIRECT_WIDTH LCD_DIRECT_NATIVE_WIDTH
#define LCD_DIRECT_HEIGHT LCD_DIRECT_NATIVE_HEIGHT
#endif
/*
* 每次只合成并传输8行,用较小 DMA 缓冲换取更多 SPI 提交次数;100ms 是单批传输
* 故障边界。预热允许5秒是因为首次 PNG 文件读取和解码远慢于缓存命中。
*/
#define LCD_DIRECT_ROWS_PER_TRANSFER 8U
#define LCD_DIRECT_PIXELS_PER_TRANSFER (LCD_DIRECT_WIDTH * LCD_DIRECT_ROWS_PER_TRANSFER)
#define LCD_DIRECT_PRESENTATION_WAIT_POLL_MS 20U
#define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100U
#define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U
/*
* PNG 缓存固定6项并使用 LRU 淘汰,限制 PSRAM 峰值;边界测试恰好覆盖一次完整
* 缓存命中周期。名称上限同时约束缓存键和嵌入式素材索引。
*/
#define LCD_DIRECT_FRAME_CACHE_MAX 6U
#define LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES LCD_DIRECT_FRAME_CACHE_MAX
#define LCD_DIRECT_PATH_MAX 96U
/* 状态图标坐标基于逻辑屏幕尺寸计算,旋转屏幕时无需复制另一套绝对坐标。 */
#define LCD_DIRECT_CHARGING_ICON_WIDTH 50U
#define LCD_DIRECT_CHARGING_ICON_X (LCD_DIRECT_WIDTH - LCD_DIRECT_CHARGING_ICON_WIDTH - 8U)
#define LCD_DIRECT_CHARGING_ICON_Y 8U
#define LCD_DIRECT_LOW_BATTERY_ICON_WIDTH 232U
#define LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT 36U
#define LCD_DIRECT_LOW_BATTERY_ICON_X ((LCD_DIRECT_WIDTH - LCD_DIRECT_LOW_BATTERY_ICON_WIDTH) / 2U)
#define LCD_DIRECT_LOW_BATTERY_ICON_Y (LCD_DIRECT_HEIGHT - LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT - 8U)
/* 基础表情按100ms节奏播放,有限动画末帧额外停留200ms以避免视觉跳变过快。 */
#define LCD_DIRECT_LAST_FRAME_EXTRA_HOLD_MS 200U
#define LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS 100U
#define LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS 100U
/*
* 20kHz 背光 PWM 高于常见可听频率;10位占空比分辨率提供平滑亮度变化。
* 独立背光锁最多等待100ms,防止控制路径因并发更新无限阻塞。
*/
#define LCD_DIRECT_BACKLIGHT_PWM_FREQUENCY_HZ 20000U
#define LCD_DIRECT_BACKLIGHT_PWM_DUTY_RESOLUTION LEDC_TIMER_10_BIT
#define LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX ((1U << 10U) - 1U)
#define LCD_DIRECT_BACKLIGHT_PWM_TIMER LEDC_TIMER_0
#define LCD_DIRECT_BACKLIGHT_PWM_CHANNEL LEDC_CHANNEL_0
#define LCD_DIRECT_BACKLIGHT_LOCK_TIMEOUT_MS 100U
/* Montserrat 14 子集直接合成,无 label 字形缓存或新增动态缓冲。 */
#define LCD_DIRECT_CLOCK_X 12U
#define LCD_DIRECT_CLOCK_Y 14U
#define LCD_DIRECT_CLOCK_TEXT_LENGTH 5U
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U
#define LCD_DIRECT_CLOCK_COLOR 0xFFFFU
#define LCD_DIRECT_BATTERY_WIDTH 40U
#define LCD_DIRECT_BATTERY_HEIGHT 20U
#define LCD_DIRECT_BATTERY_X (LCD_DIRECT_WIDTH - LCD_DIRECT_BATTERY_WIDTH - 8U)
#define LCD_DIRECT_BATTERY_Y 12U
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#define LCD_DIRECT_HUD_ROWS 40U
#define LCD_DIRECT_EMOTE_FPS 30U
#define LCD_DIRECT_EMOTE_DMA_ROWS 16U
#define LCD_DIRECT_EMOTE_CLOSED_ROWS 14U
#define LCD_DIRECT_EMOTE_CLOSED_THRESHOLD 232U
/* 眼睛包围盒四周留 2px;缓存包含此区域内原有静态脸部像素。 */
#define LCD_DIRECT_EMOTE_CACHE_X 39
#define LCD_DIRECT_EMOTE_CACHE_Y 63
#define LCD_DIRECT_EMOTE_CACHE_WIDTH 242U
#define LCD_DIRECT_EMOTE_CACHE_HEIGHT 97U
#define LCD_DIRECT_EMOTE_CACHE_COUNT 9U
#define LCD_DIRECT_EMOTE_EYES_X 41
#define LCD_DIRECT_EMOTE_EYES_Y 65
#define LCD_DIRECT_EMOTE_OPEN_MS 2000U
#define LCD_DIRECT_EMOTE_STEP_MS 34U
#define LCD_DIRECT_EMOTE_MESH_COLS 16U
#define LCD_DIRECT_EMOTE_MESH_ROWS 2U
#define LCD_DIRECT_EMOTE_MESH_POINTS ((LCD_DIRECT_EMOTE_MESH_COLS + 1U) * (LCD_DIRECT_EMOTE_MESH_ROWS + 1U))
#endif
/* ST7789P3 的方向控制(MADCTL)和 RAM 数据格式控制(RAMCTRL)命令号。 */
#define LCD_CMD_MADCTL 0x36
#define LCD_CMD_RAMCTRL 0xB0
static const char *TAG = "lcd_direct";
unsigned lodepng_esp_decode32(unsigned char **out,
unsigned *w,
unsigned *h,
const unsigned char *in,
size_t insize);
const char *lodepng_esp_error_text(unsigned code);
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;
uint32_t duration_ms;
platform_lcd_presentation_step_t steps[PLATFORM_LCD_PRESENTATION_STEP_MAX];
size_t step_count;
} lcd_direct_request_t;
typedef struct lcd_direct_decoded_image_t {
/* 缓存项拥有 pixels/alpha;pinned 项不可被 LRU 淘汰。 */
bool valid;
bool alpha_valid;
char path[LCD_DIRECT_PATH_MAX];
uint16_t width;
uint16_t height;
uint16_t *pixels;
uint8_t *alpha;
uint32_t last_used;
bool pinned;
} lcd_direct_decoded_image_t;
typedef struct {
/* 图标优先引用缓存,缓存不足时 owns_transient 标记本次绘制临时所有权。 */
uint32_t completed_frames;
uint64_t total_draw_us;
uint32_t min_draw_us;
uint32_t max_draw_us;
} lcd_direct_sequence_perf_t;
typedef struct {
lcd_direct_icon_draw_t draw;
const lcd_direct_icon_t *icon;
const lcd_direct_decoded_image_t *decoded;
lcd_direct_decoded_image_t *cached;
lcd_direct_decoded_image_t transient;
bool owns_transient;
} lcd_direct_prepared_icon_t;
typedef struct {
uint8_t cmd;
uint8_t data[16];
uint8_t databytes;
} lcd_direct_init_cmd_t;
static const lcd_direct_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},
{0, {0}, 0xFF},
};
static esp_lcd_panel_io_handle_t s_panel_io;
static esp_lcd_panel_handle_t s_panel;
static SemaphoreHandle_t s_lock;
static SemaphoreHandle_t s_backlight_lock;
static SemaphoreHandle_t s_completion_sem;
static SemaphoreHandle_t s_preheat_done_sem;
static SemaphoreHandle_t s_transfer_done_sem;
static TaskHandle_t s_render_task;
static bool s_initialized;
static bool s_render_loop_running;
static bool s_stop_requested;
static bool s_pending_valid;
static lcd_direct_request_t s_pending;
static platform_lcd_status_overlay_t s_status_overlay;
static bool s_preheat_pending_valid;
static bool s_preheat_in_progress;
static bool s_preheat_waiter_active;
static lcd_direct_request_t s_preheat_pending;
static uint32_t s_preheat_generation;
static uint32_t s_preheat_completed_generation;
static esp_err_t s_preheat_result;
static uint32_t s_completion_count;
static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED;
static uint16_t s_line_buffer[LCD_DIRECT_PIXELS_PER_TRANSFER];
static lcd_direct_decoded_image_t s_frame_cache[LCD_DIRECT_FRAME_CACHE_MAX];
static uint32_t s_cache_use_counter;
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
/* 一次性解压的图像随 LCD 设备常驻;眨眼只修改网格,不重复解压。 */
static gfx_image_dsc_t s_emote_base_image;
static gfx_image_dsc_t s_emote_eye_image;
static uint8_t *s_emote_base_pixels;
static uint8_t *s_emote_eye_pixels;
static bool s_emote_ready;
static gfx_obj_t *s_emote_hud;
static gfx_image_dsc_t s_emote_hud_image;
static uint16_t *s_emote_hud_pixels;
static char s_emote_hud_clock[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U];
static platform_lcd_status_overlay_t s_emote_hud_status;
static bool s_emote_hud_valid;
static gfx_handle_t s_emote_handle;
static gfx_disp_t *s_emote_disp;
static gfx_obj_t *s_emote_eyes_open;
static gfx_obj_t *s_emote_background;
/* listening 复用引擎、合成缓冲、DMA 和 HUD;只有底图及局部眼型独立。 */
static gfx_image_dsc_t s_listening_base_image;
static uint8_t *s_listening_base_pixels;
static gfx_image_dsc_t s_listening_images[LCD_DIRECT_EMOTE_CACHE_COUNT];
static uint8_t *s_listening_pixels[LCD_DIRECT_EMOTE_CACHE_COUNT];
static gfx_obj_t *s_listening_eyes;
static bool s_listening_ready;
static int s_listening_current = -1;
static gfx_mesh_img_point_q8_t s_emote_eye_rest[LCD_DIRECT_EMOTE_MESH_POINTS];
static uint16_t *s_emote_dma_pixels;
static esp_err_t s_emote_flush_error;
static bool s_emote_transfer_fault;
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
/* 只在 GFX 渲染锁内访问;计数区域提交,步骤内有提交才算一次画面更新。 */
static uint32_t s_emote_completed_regions;
static uint64_t s_emote_copy_us;
static uint64_t s_emote_spi_us;
#endif
static gfx_obj_t *s_emote_closed_eyes;
static gfx_image_dsc_t s_emote_closed_image;
static uint8_t *s_emote_closed_pixels;
static bool s_emote_show_closed;
static const uint8_t s_emote_cache_closures[LCD_DIRECT_EMOTE_CACHE_COUNT] = {
0, 16, 24, 64, 80, 128, 160, 192, LCD_DIRECT_EMOTE_CLOSED_THRESHOLD,
};
static gfx_image_dsc_t s_emote_cached_images[LCD_DIRECT_EMOTE_CACHE_COUNT];
static uint8_t *s_emote_cached_pixels[LCD_DIRECT_EMOTE_CACHE_COUNT];
static uint16_t *s_emote_capture_pixels;
static gfx_obj_t *s_emote_cached_eyes;
static bool s_emote_cache_ready;
static int s_emote_cache_current = -1;
static bool s_emote_active;
#endif
/* 渲染状态由 s_state_lock 保护,面板和缓存访问则由 s_lock 串行化。 */
static void lcd_direct_signal_completion(void);
static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]);
static void lcd_direct_overlay_clock(uint16_t *pixel_buffer,
uint16_t row_start,
uint16_t row_count,
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]);
static void lcd_direct_overlay_battery(uint16_t *pixels, uint16_t row_start, uint16_t rows);
static void lcd_direct_queue_idle_if_available(void)
{
/* 自动恢复不能覆盖动画结束瞬间到达的新请求。 */
portENTER_CRITICAL(&s_state_lock);
if (!s_pending_valid && !s_stop_requested) {
s_pending = (lcd_direct_request_t) { .kind = PLATFORM_LCD_PRESENTATION_IDLE };
s_pending_valid = true;
}
portEXIT_CRITICAL(&s_state_lock);
}
static bool IRAM_ATTR lcd_direct_on_color_trans_done(esp_lcd_panel_io_handle_t panel_io,
esp_lcd_panel_io_event_data_t *event_data,
void *user_ctx)
{
/* SPI 完成回调位于中断上下文,只释放信号量并按需触发任务切换。 */
(void)panel_io;
(void)event_data;
(void)user_ctx;
BaseType_t higher_priority_task_woken = pdFALSE;
if (s_transfer_done_sem != NULL) {
(void)xSemaphoreGiveFromISR(s_transfer_done_sem, &higher_priority_task_woken);
}
return higher_priority_task_woken == pdTRUE;
}
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
/* 复制 GFX 输出与缓存窗口的交集。兼容整个屏幕或任意分块的 flush。 */
static void lcd_direct_emote_capture(uint16_t *dst, int x1, int y1, int x2, int y2,
const uint16_t *src)
{
int left = x1 > LCD_DIRECT_EMOTE_CACHE_X ? x1 : LCD_DIRECT_EMOTE_CACHE_X;
int top = y1 > LCD_DIRECT_EMOTE_CACHE_Y ? y1 : LCD_DIRECT_EMOTE_CACHE_Y;
int right_limit = LCD_DIRECT_EMOTE_CACHE_X + LCD_DIRECT_EMOTE_CACHE_WIDTH;
int bottom_limit = LCD_DIRECT_EMOTE_CACHE_Y + LCD_DIRECT_EMOTE_CACHE_HEIGHT;
int right = x2 < right_limit ? x2 : right_limit;
int bottom = y2 < bottom_limit ? y2 : bottom_limit;
if (left >= right || top >= bottom) {
return;
}
for (int y = top; y < bottom; ++y) {
memcpy(dst + (y - LCD_DIRECT_EMOTE_CACHE_Y) * LCD_DIRECT_EMOTE_CACHE_WIDTH +
left - LCD_DIRECT_EMOTE_CACHE_X,
src + (y - y1) * (x2 - x1) + left - x1,
(right - left) * sizeof(uint16_t));
}
}
static esp_err_t lcd_direct_emote_prepare_cache(void);
static esp_err_t lcd_direct_listening_prepare(void);
static void lcd_direct_emote_flush(gfx_disp_t *disp,
int x1,
int y1,
int x2,
int y2,
const void *data)
{
/* 离屏预热期间只捕获 GFX 实际合成结果,不写 LCD、不计播放帧率。 */
if (s_emote_capture_pixels != NULL) {
lcd_direct_emote_capture(s_emote_capture_pixels, x1, y1, x2, y2, data);
(void)gfx_disp_flush_ready(disp, false);
return;
}
portENTER_CRITICAL(&s_state_lock);
bool active = s_emote_active;
portEXIT_CRITICAL(&s_state_lock);
if (!active || s_emote_transfer_fault) {
(void)gfx_disp_flush_ready(disp, false);
return;
}
/* GFX 已在 PSRAM 中合成完整区域。这里仅复制/传输,不穿插网格计算。
* 单 DMA 暂存区必须等 ISR 确认完成才能复用;整区域发完再通知 GFX。 */
const uint16_t *source = data;
const size_t width = (size_t)(x2 - x1);
if (width == 0 || width > LCD_DIRECT_WIDTH || y2 <= y1 || s_emote_dma_pixels == NULL) {
s_emote_flush_error = ESP_ERR_INVALID_SIZE;
(void)gfx_disp_flush_ready(disp, false);
return;
}
const size_t capacity = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS;
for (int y = y1; y < y2;) {
size_t rows = capacity / width;
if (rows > (size_t)(y2 - y)) {
rows = (size_t)(y2 - y);
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const int64_t copy_start_us = esp_timer_get_time();
#endif
memcpy(s_emote_dma_pixels, source, width * rows * sizeof(uint16_t));
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
s_emote_copy_us += esp_timer_get_time() - copy_start_us;
const int64_t spi_start_us = esp_timer_get_time();
#endif
(void)xSemaphoreTake(s_transfer_done_sem, 0);
esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y, x2, y + rows,
s_emote_dma_pixels);
if (err == ESP_OK && xSemaphoreTake(s_transfer_done_sem,
pdMS_TO_TICKS(LCD_DIRECT_TRANSFER_TIMEOUT_MS)) != pdTRUE) {
err = ESP_ERR_TIMEOUT;
/* 超时后不再复用可能仍被 DMA 读取的缓冲,重启前禁止此路径传输。 */
s_emote_transfer_fault = true;
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
s_emote_spi_us += esp_timer_get_time() - spi_start_us;
#endif
if (err != ESP_OK) {
s_emote_flush_error = err;
ESP_LOGE(TAG, "composed emote transfer failed: %s", esp_err_to_name(err));
break;
}
source += width * rows;
y += rows;
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (s_emote_flush_error == ESP_OK) {
++s_emote_completed_regions;
}
#endif
(void)gfx_disp_flush_ready(disp, false);
}
static esp_err_t lcd_direct_emote_decode_asset(const lcd_emote_rle_asset_t *asset,
gfx_image_dsc_t *image, uint8_t **pixels)
{
size_t count = (size_t)asset->width * asset->height;
ESP_RETURN_ON_FALSE(count > 0 && count <= SIZE_MAX / 3, ESP_ERR_INVALID_SIZE,
TAG, "invalid compressed asset dimensions");
uint8_t *decoded = heap_caps_malloc(count * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
ESP_RETURN_ON_FALSE(decoded != NULL, ESP_ERR_NO_MEM, TAG, "allocate emote asset PSRAM failed");
if (!lcd_emote_decode_rle(asset->data, asset->compressed_size, decoded, count)) {
free(decoded);
return ESP_ERR_INVALID_SIZE;
}
*image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565A8,
.header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = asset->width,
.header.h = asset->height,
.data_size = count * 3,
.data = decoded,
};
*pixels = decoded;
return ESP_OK;
}
static esp_err_t lcd_direct_emote_prepare_assets(void)
{
if (s_emote_base_pixels != NULL && s_emote_eye_pixels != NULL) {
return ESP_OK;
}
int64_t start_us = esp_timer_get_time();
esp_err_t err = lcd_direct_emote_decode_asset(&idle_base_rle_asset,
&s_emote_base_image, &s_emote_base_pixels);
if (err == ESP_OK) {
err = lcd_direct_emote_decode_asset(&idle_eyes_open_rle_asset,
&s_emote_eye_image, &s_emote_eye_pixels);
}
if (err != ESP_OK) {
free(s_emote_base_pixels);
free(s_emote_eye_pixels);
s_emote_base_pixels = NULL;
s_emote_eye_pixels = NULL;
return err;
}
ESP_LOGI(TAG, "emote assets decoded once: compressed=%uB PSRAM=%uB decode_us=%lld",
(unsigned)(idle_base_rle_asset.compressed_size + idle_eyes_open_rle_asset.compressed_size),
(unsigned)(s_emote_base_image.data_size + s_emote_eye_image.data_size),
(long long)(esp_timer_get_time() - start_us));
return ESP_OK;
}
/* 一条带覆盖度的抛物线代替被压成碎像素的双层眼圈。无新图片资源。 */
static void lcd_direct_emote_draw_closed_eyes(uint8_t *dst, uint16_t width,
uint16_t height, uint16_t color)
{
const size_t count = (size_t)width * height;
memset(dst, 0, count * 3);
for (int x = 0; x < width; ++x) {
int dx = x - (x < 119 ? 49 : 189);
if (abs(dx) > 46) {
continue;
}
int y_q8 = 3 * 256 + dx * dx * (8 * 256) / (46 * 46);
for (int y = 0; y < height; ++y) {
int coverage = 2 * 256 - abs(y * 256 - y_q8);
if (coverage <= 0) {
continue;
}
if (coverage > 255) {
coverage = 255;
}
size_t index = (size_t)y * width + x;
dst[index * 2] = color >> 8;
dst[index * 2 + 1] = color & 0xFF;
dst[count * 2 + index] = coverage;
}
}
}
static esp_err_t lcd_direct_emote_refresh(void)
{
if (s_emote_transfer_fault) {
return ESP_ERR_TIMEOUT;
}
s_emote_flush_error = ESP_OK;
esp_err_t err = gfx_refr_now(s_emote_handle);
return err != ESP_OK ? err : s_emote_flush_error;
}
static esp_err_t lcd_direct_emote_init(void)
{
ESP_RETURN_ON_ERROR(lcd_direct_emote_prepare_assets(), TAG, "decode emote assets failed");
const gfx_core_config_t core_config = {
.fps = LCD_DIRECT_EMOTE_FPS,
.task = GFX_EMOTE_INIT_CONFIG(),
};
s_emote_handle = gfx_emote_init(&core_config);
if (s_emote_handle == NULL) {
free(s_emote_base_pixels);
free(s_emote_eye_pixels);
s_emote_base_pixels = NULL;
s_emote_eye_pixels = NULL;
return ESP_ERR_NO_MEM;
}
s_emote_dma_pixels = heap_caps_malloc(LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS *
sizeof(uint16_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
ESP_RETURN_ON_FALSE(s_emote_dma_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate emote DMA failed");
const gfx_disp_config_t display_config = {
.h_res = LCD_DIRECT_WIDTH,
.v_res = LCD_DIRECT_HEIGHT,
.flush_cb = lcd_direct_emote_flush,
.flags = {
.swap = true,
.buff_spiram = true,
.double_buffer = false,
},
.buffers = {
.buf_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_HEIGHT,
},
};
s_emote_disp = gfx_disp_add(s_emote_handle, &display_config);
ESP_RETURN_ON_FALSE(s_emote_disp != NULL, ESP_ERR_NO_MEM, TAG, "esp_emote_gfx display add failed");
s_emote_background = gfx_img_create(s_emote_disp);
gfx_obj_t *background = s_emote_background;
s_emote_eyes_open = gfx_mesh_img_create(s_emote_disp);
ESP_RETURN_ON_FALSE(background != NULL && s_emote_eyes_open != NULL,
ESP_ERR_NO_MEM, TAG, "esp_emote_gfx image create failed");
ESP_RETURN_ON_ERROR(gfx_img_set_src(background, (void *)&s_emote_base_image), TAG, "set emote background failed");
/* GFX 用黑色背景清除旧眼型,只绘制裁剪后的鼻子和胡须。 */
ESP_RETURN_ON_ERROR(gfx_disp_set_bg_color(s_emote_disp, (gfx_color_t) { .full = 0 }),
TAG, "set emote black background failed");
ESP_RETURN_ON_ERROR(gfx_disp_set_bg_enable(s_emote_disp, true), TAG, "enable emote background failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(background, idle_base_rle_asset.x, idle_base_rle_asset.y),
TAG, "position emote base failed");
ESP_RETURN_ON_ERROR(gfx_mesh_img_set_src(s_emote_eyes_open, (void *)&s_emote_eye_image),
TAG, "set emote eyes failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_eyes_open,
LCD_DIRECT_EMOTE_EYES_X, LCD_DIRECT_EMOTE_EYES_Y),
TAG, "position emote eyes failed");
ESP_RETURN_ON_ERROR(gfx_mesh_img_set_grid(s_emote_eyes_open,
LCD_DIRECT_EMOTE_MESH_COLS, LCD_DIRECT_EMOTE_MESH_ROWS),
TAG, "configure eye mesh failed");
ESP_RETURN_ON_FALSE(gfx_mesh_img_get_point_count(s_emote_eyes_open) == LCD_DIRECT_EMOTE_MESH_POINTS,
ESP_ERR_INVALID_SIZE, TAG, "unexpected eye mesh size");
for (size_t i = 0; i < LCD_DIRECT_EMOTE_MESH_POINTS; ++i) {
ESP_RETURN_ON_ERROR(gfx_mesh_img_get_point_q8(s_emote_eyes_open, i, &s_emote_eye_rest[i]),
TAG, "read eye mesh failed");
}
const size_t closed_count = idle_eyes_open_rle_asset.width * LCD_DIRECT_EMOTE_CLOSED_ROWS;
s_emote_closed_pixels = heap_caps_malloc(closed_count * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
ESP_RETURN_ON_FALSE(s_emote_closed_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate closed eyes failed");
uint16_t eye_color = 0xFFFF;
const size_t eye_count = idle_eyes_open_rle_asset.width * idle_eyes_open_rle_asset.height;
for (size_t i = 0; i < eye_count; ++i) {
if (s_emote_eye_pixels[eye_count * 2 + i] >= 200) {
eye_color = ((uint16_t)s_emote_eye_pixels[i * 2] << 8) | s_emote_eye_pixels[i * 2 + 1];
break;
}
}
lcd_direct_emote_draw_closed_eyes(s_emote_closed_pixels, idle_eyes_open_rle_asset.width,
LCD_DIRECT_EMOTE_CLOSED_ROWS, eye_color);
s_emote_closed_image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565A8,
.header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = idle_eyes_open_rle_asset.width,
.header.h = LCD_DIRECT_EMOTE_CLOSED_ROWS,
.data_size = closed_count * 3,
.data = s_emote_closed_pixels,
};
s_emote_closed_eyes = gfx_img_create(s_emote_disp);
ESP_RETURN_ON_FALSE(s_emote_closed_eyes != NULL, ESP_ERR_NO_MEM, TAG, "create closed eyes failed");
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_closed_eyes, &s_emote_closed_image), TAG, "set closed eyes failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_closed_eyes, LCD_DIRECT_EMOTE_EYES_X,
LCD_DIRECT_EMOTE_EYES_Y + 39), TAG, "position closed eyes failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, false), TAG, "hide closed eyes failed");
s_emote_hud_pixels = heap_caps_calloc(LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS,
sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
ESP_RETURN_ON_FALSE(s_emote_hud_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate HUD failed");
s_emote_hud_image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565,
.header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = LCD_DIRECT_WIDTH,
.header.h = LCD_DIRECT_HUD_ROWS,
.data_size = LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS * sizeof(uint16_t),
.data = (const uint8_t *)s_emote_hud_pixels,
};
s_emote_hud = gfx_img_create(s_emote_disp);
ESP_RETURN_ON_FALSE(s_emote_hud != NULL, ESP_ERR_NO_MEM, TAG, "create HUD failed");
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_hud, &s_emote_hud_image), TAG, "set HUD failed");
esp_err_t cache_err = lcd_direct_emote_prepare_cache();
if (cache_err != ESP_OK) {
ESP_LOGW(TAG, "Eye cache unavailable, retain live mesh: %s", esp_err_to_name(cache_err));
}
esp_err_t listening_err = lcd_direct_listening_prepare();
if (listening_err != ESP_OK) {
ESP_LOGW(TAG, "Listening GFX unavailable, retain PNG: %s", esp_err_to_name(listening_err));
}
s_emote_ready = true;
ESP_LOGI(TAG,
"esp_emote_gfx idle test ready (%u fps, PSRAM composed frame, %u-row DMA staging)",
(unsigned)LCD_DIRECT_EMOTE_FPS,
(unsigned)LCD_DIRECT_EMOTE_DMA_ROWS);
return ESP_OK;
}
static bool lcd_direct_should_stop_idle(void)
{
bool stop = false;
portENTER_CRITICAL(&s_state_lock);
stop = s_stop_requested || s_pending_valid;
portEXIT_CRITICAL(&s_state_lock);
return stop;
}
/* 调用方持有 GFX 锁;两种后端共用 Montserrat 子集和电池资产,无动态解码缓存。 */
static esp_err_t lcd_direct_emote_update_hud(bool force, bool *changed)
{
if (changed != NULL) {
*changed = false;
}
char clock[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U];
lcd_direct_clock_text(clock);
platform_lcd_status_overlay_t status;
portENTER_CRITICAL(&s_state_lock);
status = s_status_overlay;
portEXIT_CRITICAL(&s_state_lock);
if (!force && s_emote_hud_valid && strcmp(clock, s_emote_hud_clock) == 0 &&
status.battery_valid == s_emote_hud_status.battery_valid &&
status.battery_percent == s_emote_hud_status.battery_percent &&
status.low_battery_visible == s_emote_hud_status.low_battery_visible &&
status.charging_visible == s_emote_hud_status.charging_visible) {
return ESP_OK;
}
memset(s_emote_hud_pixels, 0, s_emote_hud_image.data_size);
lcd_direct_overlay_clock(s_emote_hud_pixels, 0, LCD_DIRECT_HUD_ROWS, clock);
lcd_direct_overlay_battery(s_emote_hud_pixels, 0, LCD_DIRECT_HUD_ROWS);
#if !CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES
/* GFX 图像使用高字节在前,PNG 行缓冲可能使用主机字节序。 */
for (size_t i = 0; i < LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS; ++i) {
uint16_t color = s_emote_hud_pixels[i];
s_emote_hud_pixels[i] = (color >> 8) | (color << 8);
}
#endif
esp_err_t err = gfx_img_set_src(s_emote_hud, &s_emote_hud_image);
if (err == ESP_OK) {
memcpy(s_emote_hud_clock, clock, sizeof(clock));
s_emote_hud_status = status;
s_emote_hud_valid = true;
if (changed != NULL) {
*changed = true;
}
}
return err;
}
static esp_err_t lcd_direct_emote_wait_until(int64_t deadline_us)
{
/* 即使已经迟到,也检查取消;追赶时不忙等、不跳过眼型。 */
if (lcd_direct_should_stop_idle()) {
return ESP_ERR_INVALID_STATE;
}
while (esp_timer_get_time() < deadline_us) {
if (lcd_direct_should_stop_idle()) {
return ESP_ERR_INVALID_STATE;
}
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock HUD failed");
bool changed = false;
esp_err_t hud_err = lcd_direct_emote_update_hud(false, &changed);
if (hud_err == ESP_OK && changed) {
hud_err = lcd_direct_emote_refresh();
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (hud_err != ESP_OK || unlock_err != ESP_OK) {
return hud_err != ESP_OK ? hud_err : unlock_err;
}
const int64_t remaining_us = deadline_us - esp_timer_get_time();
if (remaining_us <= 0) {
break;
}
const uint32_t remaining_ms = (remaining_us + 999) / 1000;
uint32_t slice_ms = remaining_ms > LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
: remaining_ms;
TickType_t ticks = pdMS_TO_TICKS(slice_ms);
/* 当前 tick 为 10ms;短余量也至少让出一个 tick,避免忙等。 */
vTaskDelay(ticks > 0 ? ticks : 1);
}
return ESP_OK;
}
static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
{
return lcd_direct_emote_wait_until(esp_timer_get_time() + (int64_t)duration_ms * 1000);
}
static int64_t lcd_direct_emote_step_deadline(int64_t blink_start_us, size_t step)
{
return blink_start_us + (int64_t)(step + 1U) * LCD_DIRECT_EMOTE_STEP_MS * 1000;
}
/* 同一张眼睛纹理连续收缩并弯曲;没有第二张眼型参与混合。
* closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */
static esp_err_t lcd_direct_emote_deform_eye_closure(uint8_t closure)
{
bool closed = closure >= LCD_DIRECT_EMOTE_CLOSED_THRESHOLD;
if (closed != s_emote_show_closed) {
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_eyes_open, !closed), TAG, "toggle mesh eyes failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, closed), TAG, "toggle closed eyes failed");
s_emote_show_closed = closed;
}
if (closed) {
return ESP_OK;
}
gfx_mesh_img_point_q8_t points[LCD_DIRECT_EMOTE_MESH_POINTS];
/* 上下眼睑对称收向原眼睛中线,包围盒位置补偿保持脸部锚点。 */
const int32_t center_y = 46 * 256;
int32_t min_y_q8 = INT32_MAX;
const int32_t scale = 256 - ((int32_t)closure * 248 / 255);
for (size_t i = 0; i < LCD_DIRECT_EMOTE_MESH_POINTS; ++i) {
points[i] = s_emote_eye_rest[i];
int32_t x = points[i].x_q8 / 256;
/* 238x93 纹理中,两只眼睛的中心分别约为 x=49 和 x=189。
* 抛物线把压扁的眼圈弯成浅浅的闭眼弧线。 */
int32_t dx = x - (x < 119 ? 49 : 189);
int32_t arch = 8 * 256 - dx * dx * (8 * 256) / (46 * 46);
if (arch < 0) {
arch = 0;
}
/* 弧线相对中线上下各约 4px,避免闭合时眼睛中心整体上移。 */
points[i].y_q8 = center_y + (points[i].y_q8 - center_y) * scale / 256
+ (4 * 256 - arch) * closure / 255;
if (points[i].y_q8 < min_y_q8) {
min_y_q8 = points[i].y_q8;
}
}
ESP_RETURN_ON_ERROR(gfx_mesh_img_set_points_q8(s_emote_eyes_open, points,
LCD_DIRECT_EMOTE_MESH_POINTS),
TAG, "deform eye mesh failed");
/* GFX 绘制原点会减掉网格 bounds_min;将该偏移加回对象位置,
* 才能保持纹理坐标相对于脸部的绝对位置,避免收缩时整体上移。 */
int32_t offset_y = min_y_q8 >= 0 ? min_y_q8 / 256
: -((-min_y_q8 + 255) / 256);
return gfx_obj_set_pos(s_emote_eyes_open, LCD_DIRECT_EMOTE_EYES_X,
LCD_DIRECT_EMOTE_EYES_Y + offset_y);
}
/* 同一任务获取/释放 GFX 锁。缓存只在 LCD 初始化时生成,失败完整回退。 */
static esp_err_t lcd_direct_emote_prepare_cache(void)
{
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock eye cache failed");
const int64_t start_us = esp_timer_get_time();
const size_t bytes = LCD_DIRECT_EMOTE_CACHE_WIDTH * LCD_DIRECT_EMOTE_CACHE_HEIGHT * 2U;
esp_err_t result = ESP_OK;
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT && result == ESP_OK; ++i) {
s_emote_cached_pixels[i] = heap_caps_calloc(1, bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (s_emote_cached_pixels[i] == NULL) {
result = ESP_ERR_NO_MEM;
break;
}
s_emote_cached_images[i] = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565,
.header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = LCD_DIRECT_EMOTE_CACHE_WIDTH,
.header.h = LCD_DIRECT_EMOTE_CACHE_HEIGHT,
.data_size = bytes,
.data = s_emote_cached_pixels[i],
};
result = lcd_direct_emote_deform_eye_closure(s_emote_cache_closures[i]);
if (result == ESP_OK) {
s_emote_capture_pixels = (uint16_t *)s_emote_cached_pixels[i];
gfx_disp_refresh_all(s_emote_disp);
result = gfx_refr_now(s_emote_handle);
s_emote_capture_pixels = NULL;
}
}
s_emote_capture_pixels = NULL;
esp_err_t restore_err = lcd_direct_emote_deform_eye_closure(0);
if (result == ESP_OK) {
result = restore_err;
}
if (result == ESP_OK) {
s_emote_cached_eyes = gfx_img_create(s_emote_disp);
result = s_emote_cached_eyes != NULL ? ESP_OK : ESP_ERR_NO_MEM;
}
if (result == ESP_OK) {
result = gfx_img_set_src(s_emote_cached_eyes, &s_emote_cached_images[0]);
}
if (result == ESP_OK) {
result = gfx_obj_set_pos(s_emote_cached_eyes, LCD_DIRECT_EMOTE_CACHE_X, LCD_DIRECT_EMOTE_CACHE_Y);
}
if (result == ESP_OK) {
result = gfx_obj_set_visible(s_emote_eyes_open, false);
}
if (result == ESP_OK) {
result = gfx_obj_set_visible(s_emote_closed_eyes, false);
}
if (result == ESP_OK) {
s_emote_cache_current = 0;
s_emote_cache_ready = true;
ESP_LOGI(TAG, "Eye cache ready frames=%u PSRAM=%uB prepare_us=%llu (no Flash images)",
(unsigned)LCD_DIRECT_EMOTE_CACHE_COUNT,
(unsigned)(bytes * LCD_DIRECT_EMOTE_CACHE_COUNT),
(unsigned long long)(esp_timer_get_time() - start_us));
} else {
if (s_emote_cached_eyes != NULL) {
(void)gfx_obj_delete(s_emote_cached_eyes);
s_emote_cached_eyes = NULL;
}
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT; ++i) {
free(s_emote_cached_pixels[i]);
s_emote_cached_pixels[i] = NULL;
}
(void)gfx_obj_set_visible(s_emote_eyes_open, true);
(void)gfx_obj_set_visible(s_emote_closed_eyes, false);
s_emote_show_closed = false;
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
return result != ESP_OK ? result : unlock_err;
}
static int lcd_direct_emote_cache_index(uint8_t closure)
{
if (closure >= LCD_DIRECT_EMOTE_CLOSED_THRESHOLD) {
closure = LCD_DIRECT_EMOTE_CLOSED_THRESHOLD;
}
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT; ++i) {
if (s_emote_cache_closures[i] == closure) {
return (int)i;
}
}
return -1;
}
static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure)
{
if (!s_emote_cache_ready) {
return lcd_direct_emote_deform_eye_closure(closure);
}
const int index = lcd_direct_emote_cache_index(closure);
ESP_RETURN_ON_FALSE(index >= 0, ESP_ERR_INVALID_ARG, TAG, "eye closure missing from cache");
if (index == s_emote_cache_current) {
return ESP_OK; /* 闭眼保持不失效、不传输。 */
}
esp_err_t err = gfx_img_set_src(s_emote_cached_eyes, &s_emote_cached_images[index]);
if (err == ESP_OK) {
s_emote_cache_current = index;
}
return err;
}
/* 初始化一次;关键图只在生成缓存时存在,完成后释放,不占运行期常驻内存。 */
static esp_err_t lcd_direct_listening_prepare(void)
{
const lcd_emote_rle_asset_t *assets[3] = {
&listening_eyes_open_rle_asset, &listening_eyes_half_rle_asset,
&listening_eyes_closed_rle_asset,
};
gfx_image_dsc_t keys[3] = {0};
uint8_t *key_pixels[3] = {0};
const unsigned width = assets[0]->width, height = assets[0]->height;
const size_t bytes = (size_t)width * height * 2;
const int64_t start_us = esp_timer_get_time();
esp_err_t result = lcd_direct_emote_decode_asset(&listening_base_rle_asset,
&s_listening_base_image, &s_listening_base_pixels);
for (size_t i = 0; i < 3 && result == ESP_OK; ++i) {
if (assets[i]->width != width || assets[i]->height != height ||
assets[i]->x != assets[0]->x || assets[i]->y != assets[0]->y ||
width == 0 || width > LCD_DIRECT_WIDTH || height == 0 || height > LCD_DIRECT_HEIGHT ||
assets[i]->x + width > LCD_DIRECT_WIDTH || assets[i]->y + height > LCD_DIRECT_HEIGHT) {
result = ESP_ERR_INVALID_SIZE;
break;
}
result = lcd_direct_emote_decode_asset(assets[i], &keys[i], &key_pixels[i]);
}
const uint8_t *sources[3] = {key_pixels[0], key_pixels[1], key_pixels[2]};
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT && result == ESP_OK; ++i) {
s_listening_pixels[i] = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (s_listening_pixels[i] == NULL) {
result = ESP_ERR_NO_MEM;
break;
}
if (!lcd_emote_listening_build_frame(s_listening_pixels[i], width, height,
sources, s_emote_cache_closures[i])) {
result = ESP_ERR_INVALID_SIZE;
break;
}
/* 缓存是黑背景不透明 RGB565。恢复窗口里的眉毛/胡须,避免黑块盖掉静态层。 */
const unsigned bw = listening_base_rle_asset.width;
const unsigned bh = listening_base_rle_asset.height;
for (unsigned y = 0; y < height; ++y) {
for (unsigned x = 0; x < width; ++x) {
size_t index = ((size_t)y * width + x) * 2;
int bx = (int)(assets[0]->x + x) - listening_base_rle_asset.x;
int by = (int)(assets[0]->y + y) - listening_base_rle_asset.y;
if (s_listening_pixels[i][index] == 0 && s_listening_pixels[i][index + 1] == 0 &&
bx >= 0 && by >= 0 && bx < bw && by < bh) {
size_t source = ((size_t)by * bw + bx) * 2;
s_listening_pixels[i][index] = s_listening_base_pixels[source];
s_listening_pixels[i][index + 1] = s_listening_base_pixels[source + 1];
}
}
}
s_listening_images[i] = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565, .header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = width, .header.h = height,
.data_size = bytes, .data = s_listening_pixels[i],
};
}
for (size_t i = 0; i < 3; ++i) free(key_pixels[i]);
if (result == ESP_OK) {
result = gfx_emote_lock(s_emote_handle);
if (result == ESP_OK) {
s_listening_eyes = gfx_img_create(s_emote_disp);
result = s_listening_eyes != NULL ? ESP_OK : ESP_ERR_NO_MEM;
if (result == ESP_OK) result = gfx_img_set_src(s_listening_eyes, &s_listening_images[0]);
if (result == ESP_OK) result = gfx_obj_set_pos(s_listening_eyes, assets[0]->x, assets[0]->y);
if (result == ESP_OK) result = gfx_obj_set_visible(s_listening_eyes, false);
if (result != ESP_OK && s_listening_eyes != NULL) {
(void)gfx_obj_delete(s_listening_eyes);
s_listening_eyes = NULL;
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) result = unlock_err;
}
}
if (result != ESP_OK) {
/* 若 unlock 异常,已经绑定的对象不能引用释放后的像素;保留到设备重启。 */
if (s_listening_eyes == NULL) {
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT; ++i) {
free(s_listening_pixels[i]);
s_listening_pixels[i] = NULL;
}
free(s_listening_base_pixels);
s_listening_base_pixels = NULL;
}
return result;
}
s_listening_ready = true;
ESP_LOGI(TAG, "Listening cache ready frames=%u window=%ux%u PSRAM=%uB "
"temporary_keys=%uB compressed=%uB prepare_us=%llu",
(unsigned)LCD_DIRECT_EMOTE_CACHE_COUNT, width, height,
(unsigned)(bytes * LCD_DIRECT_EMOTE_CACHE_COUNT + s_listening_base_image.data_size),
(unsigned)(width * height * 9),
(unsigned)(listening_base_rle_asset.compressed_size + assets[0]->compressed_size +
assets[1]->compressed_size + assets[2]->compressed_size),
(unsigned long long)(esp_timer_get_time() - start_us));
return ESP_OK;
}
/* 调用方持锁。进入状态时切换静态层;所有后端共用同一个显示与 HUD。 */
static esp_err_t lcd_direct_emote_select_face(bool listening)
{
const lcd_emote_rle_asset_t *base = listening ? &listening_base_rle_asset : &idle_base_rle_asset;
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_background, true), TAG, "show face base failed");
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_background,
listening ? &s_listening_base_image : &s_emote_base_image), TAG, "switch face base failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_background, base->x, base->y), TAG, "switch face position failed");
if (s_listening_eyes != NULL) {
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_listening_eyes, listening), TAG, "switch listening eyes failed");
}
if (s_emote_cached_eyes != NULL) {
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_cached_eyes, !listening), TAG, "switch idle cache failed");
}
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_eyes_open, !listening && !s_emote_cache_ready),
TAG, "switch live eyes failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, false), TAG, "hide closed eyes failed");
s_emote_show_closed = false;
return ESP_OK;
}
static esp_err_t lcd_direct_emote_set_face_closure(bool listening, uint8_t closure)
{
if (!listening) return lcd_direct_emote_set_eye_closure(closure);
const int index = lcd_direct_emote_cache_index(closure);
ESP_RETURN_ON_FALSE(index >= 0 && s_listening_ready, ESP_ERR_INVALID_STATE,
TAG, "listening eye cache unavailable");
if (index == s_listening_current) return ESP_OK;
esp_err_t err = gfx_img_set_src(s_listening_eyes, &s_listening_images[index]);
if (err == ESP_OK) s_listening_current = index;
return err;
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
static uint64_t lcd_direct_emote_fps_x100(uint32_t frames, uint64_t elapsed_us)
{
return elapsed_us > 0 ? (uint64_t)frames * 100000000ULL / elapsed_us : 0;
}
#endif
static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_request_t *request)
{
const bool listening = request != NULL && strcmp(request->asset_dir, "listening") == 0;
static const uint8_t listening_closure[] = {24, 80, 160, 232, 232, 192, 128, 64, 16, 0};
const uint32_t step_ms = listening ? 30U : LCD_DIRECT_EMOTE_STEP_MS;
ESP_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL,
ESP_ERR_INVALID_STATE,
TAG,
"esp_emote_gfx is not ready");
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock esp_emote_gfx failed");
portENTER_CRITICAL(&s_state_lock);
s_emote_active = true;
portEXIT_CRITICAL(&s_state_lock);
esp_err_t result = lcd_direct_emote_select_face(listening);
if (result == ESP_OK) result = lcd_direct_emote_update_hud(true, NULL);
if (result == ESP_OK) {
result = lcd_direct_emote_set_face_closure(listening, 0U);
}
if (result == ESP_OK) {
gfx_disp_refresh_all(s_emote_disp);
result = lcd_direct_emote_refresh();
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) {
result = unlock_err;
}
ESP_LOGI(TAG, "esp_emote_gfx %s playback started (cached=%d)",
listening ? "listening" : "idle", listening ? s_listening_ready : s_emote_cache_ready);
/* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */
static const uint8_t blink_closure[] = {
24U, 80U, 160U, 232U, 255U, 255U,
232U, 192U, 128U, 64U, 16U, 0U,
};
uint16_t cycles = 0;
const uint8_t *timeline = listening ? listening_closure : blink_closure;
const size_t step_count = listening ? sizeof(listening_closure) : sizeof(blink_closure);
const uint16_t repeats = request != NULL ? request->repeat_count : 0;
const uint32_t open_ms = repeats > 0 && request->frame_ms > 0
? request->frame_ms : listening ? 2400U : LCD_DIRECT_EMOTE_OPEN_MS;
while (result == ESP_OK && (repeats == 0 || cycles < repeats)) {
result = lcd_direct_emote_wait(open_ms);
if (result != ESP_OK) {
break;
}
const int64_t blink_start_us = esp_timer_get_time();
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
uint64_t draw_us_total = 0;
uint32_t draw_us_max = 0;
uint32_t late_frames = 0;
uint32_t eye_updates = 0;
/* lock / compose / PSRAM-copy / SPI-submit-and-wait,均为墙钟时间。 */
uint64_t cost_total_us[4] = {0};
uint32_t cost_max_us[4] = {0};
#endif
for (size_t i = 0; i < step_count && result == ESP_OK; ++i) {
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const int64_t frame_start_us = esp_timer_get_time();
#endif
result = gfx_emote_lock(s_emote_handle);
if (result != ESP_OK) {
break;
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const int64_t lock_acquired_us = esp_timer_get_time();
const uint64_t copy_before_us = s_emote_copy_us;
const uint64_t spi_before_us = s_emote_spi_us;
const uint32_t regions_before = s_emote_completed_regions;
#endif
result = lcd_direct_emote_set_face_closure(listening, timeline[i]);
if (result == ESP_OK) {
result = lcd_direct_emote_refresh();
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const uint64_t copy_us = s_emote_copy_us - copy_before_us;
const uint64_t spi_us = s_emote_spi_us - spi_before_us;
const uint64_t locked_us = esp_timer_get_time() - lock_acquired_us;
const uint64_t transfer_us = copy_us + spi_us;
const uint32_t costs[4] = {
(uint32_t)(lock_acquired_us - frame_start_us),
(uint32_t)(locked_us > transfer_us ? locked_us - transfer_us : 0),
(uint32_t)copy_us,
(uint32_t)spi_us,
};
for (size_t cost = 0; cost < 4; ++cost) {
cost_total_us[cost] += costs[cost];
if (costs[cost] > cost_max_us[cost]) {
cost_max_us[cost] = costs[cost];
}
}
if (result == ESP_OK && s_emote_completed_regions != regions_before) {
++eye_updates;
}
#endif
unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) {
result = unlock_err;
}
if (result == ESP_OK) {
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const uint32_t elapsed_us = (uint32_t)(esp_timer_get_time() - frame_start_us);
draw_us_total += elapsed_us;
if (elapsed_us > draw_us_max) {
draw_us_max = elapsed_us;
}
#endif
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (elapsed_us >= step_ms * 1000U) {
++late_frames;
}
#endif
/* 所有步骤锚定同一时间轴,tick/调度超时不累积到后续截止时间。
* 绘制确实过慢时不跳帧,也不增加固定等待;下一步继续检查取消。 */
result = lcd_direct_emote_wait_until(
listening ? blink_start_us + (int64_t)(i + 1U) * step_ms * 1000 :
lcd_direct_emote_step_deadline(blink_start_us, i));
}
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (result == ESP_OK) {
const uint64_t blink_us = esp_timer_get_time() - blink_start_us;
const uint32_t steps = step_count;
const uint64_t step_fps = lcd_direct_emote_fps_x100(steps, blink_us);
const uint64_t update_fps = lcd_direct_emote_fps_x100(eye_updates, blink_us);
/* 整轮结束再打日志,串口输出不落在被测眨眼窗口中。 */
ESP_LOGI(TAG, "emote_fps blink_us=%llu steps=%u step_fps=%llu.%02llu "
"eye_updates=%u update_fps=%llu.%02llu draw_avg_us=%llu "
"draw_max_us=%u late_steps=%u/%u face=%s",
(unsigned long long)blink_us, (unsigned)steps,
(unsigned long long)(step_fps / 100), (unsigned long long)(step_fps % 100),
(unsigned)eye_updates,
(unsigned long long)(update_fps / 100), (unsigned long long)(update_fps % 100),
(unsigned long long)(draw_us_total / steps), (unsigned)draw_us_max,
(unsigned)late_frames, (unsigned)steps, listening ? "listening" : "idle");
ESP_LOGI(TAG, "emote_cost lock_avg_us=%llu lock_max_us=%u "
"compose_avg_us=%llu compose_max_us=%u "
"copy_avg_us=%llu copy_max_us=%u spi_avg_us=%llu spi_max_us=%u",
(unsigned long long)(cost_total_us[0] / steps), (unsigned)cost_max_us[0],
(unsigned long long)(cost_total_us[1] / steps), (unsigned)cost_max_us[1],
(unsigned long long)(cost_total_us[2] / steps), (unsigned)cost_max_us[2],
(unsigned long long)(cost_total_us[3] / steps), (unsigned)cost_max_us[3]);
}
#endif
++cycles;
}
if (result == ESP_OK && repeats > 0) {
lcd_direct_signal_completion();
}
esp_err_t lock_err = gfx_emote_lock(s_emote_handle);
if (lock_err == ESP_OK) {
portENTER_CRITICAL(&s_state_lock);
s_emote_active = false;
portEXIT_CRITICAL(&s_state_lock);
lock_err = gfx_emote_unlock(s_emote_handle);
}
if (result == ESP_ERR_INVALID_STATE && lcd_direct_should_stop_idle()) {
result = ESP_OK;
}
return result != ESP_OK ? result : lock_err;
}
typedef struct {
lcd_fortune_texture_t shared[LCD_FORTUNE_SHARED_COUNT], text;
uint8_t *pixels[LCD_FORTUNE_SHARED_COUNT + 1], *frame;
gfx_image_dsc_t image, rod_image;
gfx_obj_t *object;
} lcd_direct_fortune_scene_t;
/* 极端锁/解绑故障保留已绑定资源到重启,不能释放给 GFX 的悬空指针。 */
static lcd_direct_fortune_scene_t *s_fortune_orphan;
static int lcd_direct_fortune_sequence_index(const lcd_direct_request_t *request)
{
if (request == NULL || request->step_count != 6) return -1;
for (size_t i = 0; i < 6; ++i) {
const platform_lcd_presentation_step_t *step = &request->steps[i];
if (step->asset_dir == NULL || step->frame_index != i % 3 ||
step->icon_id != PLATFORM_LCD_PRESENTATION_NO_ICON ||
step->delay_ms != (i == 5 ? 1500U : 500U)) return -1;
if (i < 3 && strcmp(step->asset_dir, "chouqian_qianzhi") != 0) return -1;
if (i >= 3 && strcmp(step->asset_dir, request->steps[3].asset_dir) != 0) return -1;
}
for (size_t i = 0; i < LCD_FORTUNE_RESULT_COUNT; ++i)
if (strcmp(request->steps[3].asset_dir, lcd_fortune_result_dirs[i]) == 0) return (int)i;
return -1; /* 不匹配的通用步骤仍使用原 PNG 播放,不偷换其他动作。 */
}
static void lcd_direct_fortune_free(lcd_direct_fortune_scene_t *scene)
{
if (scene == NULL) return;
for (size_t i = 0; i <= LCD_FORTUNE_SHARED_COUNT; ++i) free(scene->pixels[i]);
free(scene->frame);
free(scene);
}
static lcd_direct_fortune_scene_t *lcd_direct_fortune_prepare(size_t index)
{
if (index >= LCD_FORTUNE_RESULT_COUNT) return NULL;
lcd_direct_fortune_scene_t *scene = heap_caps_calloc(1, sizeof(*scene), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (scene == NULL) return NULL;
for (size_t i = 0; i <= LCD_FORTUNE_SHARED_COUNT; ++i) {
const lcd_fortune_asset_t *asset = i < LCD_FORTUNE_SHARED_COUNT ?
&lcd_fortune_shared[i] : &lcd_fortune_text[index];
size_t bytes = (size_t)asset->width * asset->height * 3;
scene->pixels[i] = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (scene->pixels[i] == NULL || !lcd_emote_fortune_decode(asset->data,
asset->compressed_size, scene->pixels[i], bytes)) {
lcd_direct_fortune_free(scene);
return NULL;
}
lcd_fortune_texture_t texture = {asset->width, asset->height, scene->pixels[i]};
if (i < LCD_FORTUNE_SHARED_COUNT) scene->shared[i] = texture;
else scene->text = texture;
}
const size_t bytes = LCD_FORTUNE_WIDTH * LCD_FORTUNE_HEIGHT * 2;
scene->frame = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (scene->frame == NULL) {lcd_direct_fortune_free(scene); return NULL;}
scene->image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565, .header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = LCD_FORTUNE_WIDTH, .header.h = LCD_FORTUNE_HEIGHT,
.data_size = bytes, .data = scene->frame,
};
const lcd_fortune_asset_t *rod = &lcd_fortune_shared[LCD_FORTUNE_ROD];
scene->rod_image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565A8, .header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = rod->width, .header.h = rod->height,
.data_size = (size_t)rod->width * rod->height * 3,
.data = scene->pixels[LCD_FORTUNE_ROD],
};
return scene;
}
/* 同一任务持锁退出并解绑对象,然后才能释放一次性 PSRAM 图层。 */
static esp_err_t lcd_direct_fortune_detach(lcd_direct_fortune_scene_t *scene)
{
esp_err_t err = gfx_emote_lock(s_emote_handle);
if (err != ESP_OK) return err;
portENTER_CRITICAL(&s_state_lock);
s_emote_active = false;
portEXIT_CRITICAL(&s_state_lock);
err = lcd_direct_emote_select_face(false);
if (scene->object != NULL) {
esp_err_t deleted = gfx_obj_delete(scene->object);
if (deleted == ESP_OK) scene->object = NULL;
if (err == ESP_OK) err = deleted;
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
return err != ESP_OK ? err : unlock_err;
}
static esp_err_t lcd_direct_play_fortune(size_t index)
{
if (s_fortune_orphan != NULL || s_emote_transfer_fault) return ESP_ERR_INVALID_STATE;
if (lcd_direct_should_stop_idle()) return ESP_ERR_INVALID_STATE;
const int64_t prepare_start_us = esp_timer_get_time();
lcd_direct_fortune_scene_t *scene = lcd_direct_fortune_prepare(index);
if (scene == NULL) return ESP_ERR_NOT_SUPPORTED;
esp_err_t result = gfx_emote_lock(s_emote_handle);
if (result != ESP_OK) {lcd_direct_fortune_free(scene); return result;}
bool started = false;
bool rod_hidden = false;
if (lcd_direct_should_stop_idle()) result = ESP_ERR_INVALID_STATE;
if (result == ESP_OK) {
scene->object = gfx_img_create(s_emote_disp);
result = scene->object != NULL ? ESP_OK : ESP_ERR_NO_MEM;
}
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_eyes_open, false);
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_closed_eyes, false);
if (result == ESP_OK && s_emote_cached_eyes != NULL) result = gfx_obj_set_visible(s_emote_cached_eyes, false);
if (result == ESP_OK && s_listening_eyes != NULL) result = gfx_obj_set_visible(s_listening_eyes, false);
if (result == ESP_OK) result = gfx_img_set_src(s_emote_background, &scene->rod_image);
if (result == ESP_OK) result = gfx_obj_set_pos(s_emote_background, 0, 40);
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_background, true);
if (result == ESP_OK) result = gfx_obj_set_pos(scene->object, LCD_FORTUNE_X, LCD_FORTUNE_Y);
if (result == ESP_OK) result = gfx_img_set_src(scene->object, &scene->image);
if (result == ESP_OK) result = lcd_direct_emote_update_hud(true, NULL);
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) result = unlock_err;
size_t pixel_bytes = scene->image.data_size;
for (size_t i = 0; i < LCD_FORTUNE_SHARED_COUNT; ++i)
pixel_bytes += (size_t)scene->shared[i].width * scene->shared[i].height * 3;
pixel_bytes += (size_t)scene->text.width * scene->text.height * 3;
ESP_LOGI(TAG, "Fortune layers asset=%s pixels_psram=%u scene_bytes=%u prepare_us=%llu",
lcd_fortune_result_dirs[index], (unsigned)pixel_bytes, (unsigned)sizeof(*scene),
(unsigned long long)(esp_timer_get_time() - prepare_start_us));
const int64_t start_us = esp_timer_get_time();
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
uint32_t frames = 0, draw_attempts = 0, late = 0, max_draw_us = 0;
uint64_t total_draw_us = 0;
#endif
for (uint32_t ms = 0; result == ESP_OK;) {
if (lcd_direct_should_stop_idle()) {result = ESP_ERR_INVALID_STATE; break;}
const int64_t draw_start_us = esp_timer_get_time();
result = gfx_emote_lock(s_emote_handle);
if (result != ESP_OK) break;
if (!lcd_emote_fortune_build_frame(scene->frame, scene->image.data_size,
scene->shared, &scene->text, ms)) result = ESP_ERR_INVALID_SIZE;
if (result == ESP_OK) result = gfx_img_set_src(scene->object, &scene->image);
if (result == ESP_OK && ms >= 600 && !rod_hidden) {
result = gfx_obj_set_visible(s_emote_background, false);
rod_hidden = result == ESP_OK;
}
if (result == ESP_OK) result = lcd_direct_emote_update_hud(false, NULL);
if (result == ESP_OK) {
portENTER_CRITICAL(&s_state_lock);
s_emote_active = true;
portEXIT_CRITICAL(&s_state_lock);
if (!started) gfx_disp_refresh_all(s_emote_disp);
started = true;
result = lcd_direct_emote_refresh();
}
unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) result = unlock_err;
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
uint32_t draw_us = (uint32_t)(esp_timer_get_time() - draw_start_us);
total_draw_us += draw_us;
++draw_attempts;
if (draw_us > max_draw_us) max_draw_us = draw_us;
if (draw_us >= LCD_FORTUNE_STEP_MS * 1000) ++late;
if (result == ESP_OK) ++frames;
#else
(void)draw_start_us;
#endif
if (result != ESP_OK) break;
if (ms == LCD_FORTUNE_MOTION_MS) break;
uint32_t next_ms = ms + LCD_FORTUNE_STEP_MS;
if (next_ms > LCD_FORTUNE_MOTION_MS) next_ms = LCD_FORTUNE_MOTION_MS;
result = lcd_direct_emote_wait_until(start_us + (int64_t)next_ms * 1000);
ms = next_ms;
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (started && frames > 0) {
uint64_t motion_us = esp_timer_get_time() - start_us;
uint64_t fps100 = lcd_direct_emote_fps_x100(frames, motion_us);
ESP_LOGI(TAG, "fortune_fps result=%s frames=%u motion_us=%llu update_fps=%llu.%02llu "
"draw_avg_us=%llu draw_max_us=%u late_frames=%u status=%s",
lcd_fortune_result_dirs[index], (unsigned)frames, (unsigned long long)motion_us,
(unsigned long long)(fps100 / 100), (unsigned long long)(fps100 % 100),
(unsigned long long)(total_draw_us / draw_attempts), (unsigned)max_draw_us, (unsigned)late,
esp_err_to_name(result));
}
#endif
/* 运动仍锚定同一起点;迟到不能吞掉用户阅读签文所需的末帧停留。 */
if (result == ESP_OK) result = lcd_direct_emote_wait_until(
esp_timer_get_time() + (int64_t)LCD_FORTUNE_HOLD_MS * 1000);
esp_err_t detach_err = lcd_direct_fortune_detach(scene);
if (detach_err == ESP_OK) lcd_direct_fortune_free(scene);
else {
s_fortune_orphan = scene;
s_emote_transfer_fault = true;
ESP_LOGE(TAG, "Fortune detach failed; bound pixels retained, restart required");
}
if (result == ESP_OK) result = detach_err;
if (result == ESP_OK && !lcd_direct_should_stop_idle()) lcd_direct_signal_completion();
if (!started && detach_err == ESP_OK && result != ESP_ERR_INVALID_STATE)
return ESP_ERR_NOT_SUPPORTED;
return result;
}
#endif
static uint16_t lcd_direct_swap_color(uint16_t color)
{
#if CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES
return (uint16_t)((color >> 8) | (color << 8));
#else
return color;
#endif
}
static esp_err_t lcd_direct_draw_rows(uint16_t row_start, uint16_t row_count)
{
/* 每批行发送后等待传输完成,确保静态行缓冲不会在 DMA 使用期间被覆盖。 */
ESP_RETURN_ON_FALSE(s_panel != NULL, ESP_ERR_INVALID_STATE, TAG, "panel is not ready");
ESP_RETURN_ON_FALSE(s_transfer_done_sem != NULL,
ESP_ERR_INVALID_STATE,
TAG,
"transfer semaphore is not ready");
(void)xSemaphoreTake(s_transfer_done_sem, 0);
ESP_RETURN_ON_ERROR(esp_lcd_panel_draw_bitmap(s_panel,
0,
row_start,
LCD_DIRECT_WIDTH,
row_start + row_count,
s_line_buffer),
TAG,
"panel draw failed");
ESP_RETURN_ON_FALSE(xSemaphoreTake(s_transfer_done_sem,
pdMS_TO_TICKS(LCD_DIRECT_TRANSFER_TIMEOUT_MS)) == pdTRUE,
ESP_ERR_TIMEOUT,
TAG,
"panel transfer timed out");
return ESP_OK;
}
static void *lcd_direct_alloc_psram(size_t size)
{
#if SOC_SPIRAM_SUPPORTED
void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr != NULL) {
return ptr;
}
#endif
return malloc(size);
}
static uint16_t lcd_direct_rgb888_to_rgb565(uint8_t red, uint8_t green, uint8_t blue)
{
return (uint16_t)(((red & 0xF8U) << 8) | ((green & 0xFCU) << 3) | (blue >> 3));
}
static uint16_t lcd_direct_blend_rgb565(uint16_t dst, uint16_t src, uint8_t alpha)
{
if (alpha == 0U) {
return dst;
}
if (alpha == 255U) {
return src;
}
const uint8_t src_r = (uint8_t)(((src >> 11) & 0x1FU) << 3);
const uint8_t src_g = (uint8_t)(((src >> 5) & 0x3FU) << 2);
const uint8_t src_b = (uint8_t)((src & 0x1FU) << 3);
const uint8_t dst_r = (uint8_t)(((dst >> 11) & 0x1FU) << 3);
const uint8_t dst_g = (uint8_t)(((dst >> 5) & 0x3FU) << 2);
const uint8_t dst_b = (uint8_t)((dst & 0x1FU) << 3);
const uint8_t out_r = (uint8_t)(((uint16_t)src_r * alpha + (uint16_t)dst_r * (255U - alpha)) / 255U);
const uint8_t out_g = (uint8_t)(((uint16_t)src_g * alpha + (uint16_t)dst_g * (255U - alpha)) / 255U);
const uint8_t out_b = (uint8_t)(((uint16_t)src_b * alpha + (uint16_t)dst_b * (255U - alpha)) / 255U);
return lcd_direct_rgb888_to_rgb565(out_r, out_g, out_b);
}
static void lcd_direct_release_decoded_image(lcd_direct_decoded_image_t *decoded)
{
if (decoded == NULL) {
return;
}
free(decoded->pixels);
free(decoded->alpha);
memset(decoded, 0, sizeof(*decoded));
}
static esp_err_t lcd_direct_decode_png(const char *path,
bool keep_alpha,
lcd_direct_decoded_image_t *out_decoded)
{
/* 解码后立即转换为面板原生 RGB565,仅在确有透明像素时保留 alpha 平面。 */
ESP_RETURN_ON_FALSE(path != NULL && out_decoded != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid PNG decode request");
memset(out_decoded, 0, sizeof(*out_decoded));
/* PNG 字节直接取自应用镜像的只读映射区;解码器只读取输入,不持有或释放该指针。 */
platform_asset_view_t asset = {0};
esp_err_t find_err = platform_asset_find(path, &asset);
if (find_err != ESP_OK) {
ESP_LOGE(TAG, "find PNG asset failed path=%s err=%s", path, esp_err_to_name(find_err));
return find_err;
}
unsigned char *rgba = NULL;
unsigned width = 0;
unsigned height = 0;
unsigned decode_err = lodepng_esp_decode32(&rgba, &width, &height,
asset.data, asset.size);
if (decode_err != 0U) {
ESP_LOGE(TAG, "decode PNG failed path=%s error=%u text=%s",
path,
decode_err,
lodepng_esp_error_text(decode_err));
return ESP_FAIL;
}
if (width == 0U || height == 0U || width > UINT16_MAX || height > UINT16_MAX) {
ESP_LOGE(TAG, "unsupported PNG dimensions path=%s width=%u height=%u", path, width, height);
free(rgba);
return ESP_ERR_INVALID_SIZE;
}
const size_t pixel_count = (size_t)width * (size_t)height;
uint16_t *pixels = (uint16_t *)lcd_direct_alloc_psram(pixel_count * sizeof(uint16_t));
if (pixels == NULL) {
ESP_LOGE(TAG, "allocate RGB565 PNG cache failed path=%s pixels=%u", path, (unsigned)pixel_count);
free(rgba);
return ESP_ERR_NO_MEM;
}
uint8_t *alpha = NULL;
bool alpha_needed = false;
if (keep_alpha) {
alpha = (uint8_t *)lcd_direct_alloc_psram(pixel_count);
if (alpha == NULL) {
ESP_LOGE(TAG, "allocate PNG alpha cache failed path=%s pixels=%u", path, (unsigned)pixel_count);
free(pixels);
free(rgba);
return ESP_ERR_NO_MEM;
}
}
for (size_t i = 0; i < pixel_count; ++i) {
const unsigned char *src = &rgba[i * 4U];
pixels[i] = lcd_direct_rgb888_to_rgb565(src[0], src[1], src[2]);
if (alpha != NULL) {
alpha[i] = src[3];
if (src[3] != 255U) {
alpha_needed = true;
}
}
}
free(rgba);
strlcpy(out_decoded->path, path, sizeof(out_decoded->path));
out_decoded->width = (uint16_t)width;
out_decoded->height = (uint16_t)height;
out_decoded->pixels = pixels;
out_decoded->alpha = alpha_needed ? alpha : NULL;
out_decoded->alpha_valid = alpha_needed;
out_decoded->valid = true;
if (!alpha_needed) {
free(alpha);
}
ESP_LOGI(TAG,
"PNG decoded path=%s size=%ux%u bytes=%u alpha=%d",
path,
width,
height,
(unsigned)(pixel_count * sizeof(uint16_t)),
alpha_needed);
return ESP_OK;
}
static lcd_direct_decoded_image_t *lcd_direct_find_cached_image(const char *path, bool require_alpha)
{
/* 命中时更新单调递增使用序号,供固定容量缓存执行 LRU 淘汰。 */
if (path == NULL) {
return NULL;
}
for (size_t i = 0; i < LCD_DIRECT_FRAME_CACHE_MAX; ++i) {
lcd_direct_decoded_image_t *entry = &s_frame_cache[i];
if (entry->valid && strcmp(entry->path, path) == 0) {
(void)require_alpha;
entry->last_used = ++s_cache_use_counter;
return entry;
}
}
return NULL;
}
static lcd_direct_decoded_image_t *lcd_direct_find_cache_slot(void)
{
/* 优先空槽,否则淘汰最久未使用且未 pinned 的资源。 */
lcd_direct_decoded_image_t *oldest = NULL;
for (size_t i = 0; i < LCD_DIRECT_FRAME_CACHE_MAX; ++i) {
if (!s_frame_cache[i].valid) {
return &s_frame_cache[i];
}
if (!s_frame_cache[i].pinned &&
(oldest == NULL || s_frame_cache[i].last_used < oldest->last_used)) {
oldest = &s_frame_cache[i];
}
}
if (oldest != NULL) {
lcd_direct_release_decoded_image(oldest);
}
return oldest;
}
static esp_err_t lcd_direct_get_png_image(const char *path,
bool keep_alpha,
bool cache,
lcd_direct_decoded_image_t **out_image,
lcd_direct_decoded_image_t *transient)
{
/* 缓存已满且全部固定时,允许调用方提供 transient 承接一次性解码结果。 */
ESP_RETURN_ON_FALSE(path != NULL && out_image != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid PNG image request");
*out_image = NULL;
lcd_direct_decoded_image_t *cached = lcd_direct_find_cached_image(path, keep_alpha);
if (cached != NULL) {
*out_image = cached;
return ESP_OK;
}
lcd_direct_decoded_image_t *target = NULL;
if (cache) {
target = lcd_direct_find_cache_slot();
}
if (target == NULL) {
ESP_RETURN_ON_FALSE(transient != NULL, ESP_ERR_NO_MEM, TAG, "PNG cache full and no transient target");
target = transient;
}
esp_err_t err = lcd_direct_decode_png(path, keep_alpha, target);
if (err != ESP_OK) {
return err;
}
if (target != transient) {
target->last_used = ++s_cache_use_counter;
}
*out_image = target;
return ESP_OK;
}
static uint32_t lcd_direct_backlight_duty(uint8_t brightness_percent)
{
uint32_t duty = ((uint32_t)brightness_percent * LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX + 50U) / 100U;
#if !CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH
duty = LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX - duty;
#endif
return duty;
}
static esp_err_t lcd_direct_backlight_init(uint8_t initial_brightness_percent)
{
const ledc_timer_config_t timer_config = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LCD_DIRECT_BACKLIGHT_PWM_DUTY_RESOLUTION,
.timer_num = LCD_DIRECT_BACKLIGHT_PWM_TIMER,
.freq_hz = LCD_DIRECT_BACKLIGHT_PWM_FREQUENCY_HZ,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_RETURN_ON_ERROR(ledc_timer_config(&timer_config), TAG, "backlight PWM timer config failed");
const ledc_channel_config_t channel_config = {
.gpio_num = CONFIG_TQ_SCREEN_BACKLIGHT_GPIO,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LCD_DIRECT_BACKLIGHT_PWM_CHANNEL,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LCD_DIRECT_BACKLIGHT_PWM_TIMER,
.duty = lcd_direct_backlight_duty(initial_brightness_percent),
.hpoint = 0,
};
ESP_RETURN_ON_ERROR(ledc_channel_config(&channel_config), TAG, "backlight PWM channel config failed");
ESP_LOGI(TAG,
"Backlight PWM initialized gpio=%d frequency=%uHz brightness=%d%% active_high=%d",
CONFIG_TQ_SCREEN_BACKLIGHT_GPIO,
(unsigned)LCD_DIRECT_BACKLIGHT_PWM_FREQUENCY_HZ,
initial_brightness_percent,
CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH ? 1 : 0);
return ESP_OK;
}
static esp_err_t lcd_direct_gpio_init(void)
{
uint64_t pin_mask = (1ULL << CONFIG_TQ_SCREEN_DC_GPIO);
if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) {
pin_mask |= (1ULL << CONFIG_TQ_SCREEN_RESET_PIN);
}
gpio_config_t gpio_config_data = {
.pin_bit_mask = pin_mask,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&gpio_config_data), TAG, "gpio config failed");
gpio_set_level(CONFIG_TQ_SCREEN_DC_GPIO, 0);
if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) {
gpio_set_level(CONFIG_TQ_SCREEN_RESET_PIN, 1);
}
return lcd_direct_backlight_init(0U);
}
static esp_err_t lcd_direct_panel_init(void)
{
/* 面板命令、方向和颜色格式在显示开启前一次性配置完成。 */
size_t max_transfer_pixels = LCD_DIRECT_PIXELS_PER_TRANSFER;
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
max_transfer_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS;
#endif
spi_bus_config_t bus_config = {
.mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO,
.miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO,
.sclk_io_num = CONFIG_TQ_SCREEN_SPI_CLK_GPIO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = max_transfer_pixels * sizeof(uint16_t),
};
ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_DIRECT_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO),
TAG,
"esp_lcd spi bus init failed");
esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = CONFIG_TQ_SCREEN_DC_GPIO,
.cs_gpio_num = CONFIG_TQ_SCREEN_SPI_CS_GPIO,
.pclk_hz = CONFIG_TQ_SCREEN_SPI_PIXEL_CLOCK_HZ,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.spi_mode = 0,
.trans_queue_depth = 1,
.on_color_trans_done = lcd_direct_on_color_trans_done,
};
ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_DIRECT_SPI_HOST,
&io_config,
&s_panel_io),
TAG,
"new panel io failed");
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = CONFIG_TQ_SCREEN_RESET_PIN,
#if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR,
#else
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
#endif
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
.bits_per_pixel = 16,
};
ESP_RETURN_ON_ERROR(esp_lcd_new_panel_st7789(s_panel_io, &panel_config, &s_panel),
TAG,
"new st7789 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");
for (size_t i = 0; s_st7789_init_cmds[i].databytes != 0xFFU; ++i) {
const lcd_direct_init_cmd_t *cmd = &s_st7789_init_cmds[i];
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io,
cmd->cmd,
cmd->databytes > 0U ? cmd->data : NULL,
cmd->databytes),
TAG,
"custom init cmd failed");
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_swap_xy(s_panel, CONFIG_TQ_SCREEN_SWAP_XY),
TAG,
"panel swap xy failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_mirror(s_panel, CONFIG_TQ_SCREEN_MIRROR_X, false),
TAG,
"panel mirror failed");
uint8_t madctl = 0;
if (CONFIG_TQ_SCREEN_MIRROR_X) {
madctl |= 0x40;
}
if (CONFIG_TQ_SCREEN_SWAP_XY) {
madctl |= 0x20;
}
#if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR
madctl |= 0x08;
#endif
uint8_t ramctrl[] = {0x00, 0xF0};
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, LCD_CMD_MADCTL, &madctl, 1),
TAG,
"panel MADCTL failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, LCD_CMD_RAMCTRL, ramctrl, sizeof(ramctrl)),
TAG,
"panel RAMCTRL failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_invert_color(s_panel, CONFIG_TQ_SCREEN_INVERT_COLOR),
TAG,
"panel invert 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_direct_clear_panel(void)
{
memset(s_line_buffer, 0, sizeof(s_line_buffer));
for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT;
row_start += LCD_DIRECT_ROWS_PER_TRANSFER) {
uint16_t row_count = LCD_DIRECT_HEIGHT - row_start;
if (row_count > LCD_DIRECT_ROWS_PER_TRANSFER) {
row_count = LCD_DIRECT_ROWS_PER_TRANSFER;
}
ESP_RETURN_ON_ERROR(lcd_direct_draw_rows(row_start, row_count),
TAG,
"initial panel clear failed");
}
return ESP_OK;
}
static uint16_t lcd_direct_color_for_row(uint16_t row, const lcd_direct_request_t *request)
{
uint16_t band = row / 40U;
if (request != NULL && request->kind == PLATFORM_LCD_PRESENTATION_SLOT_REELS) {
static const uint16_t slot_colors[] = {0x001F, 0xF800, 0xFFE0, 0x07E0, 0xF81F, 0xFFFF};
return slot_colors[band % (sizeof(slot_colors) / sizeof(slot_colors[0]))];
}
if (request != NULL && request->kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) {
uint32_t hash = 0;
for (size_t i = 0; request->asset_dir[i] != '\0'; ++i) {
hash = (hash * 33U) + (uint8_t)request->asset_dir[i];
}
static const uint16_t asset_colors[] = {0x0000, 0x07FF, 0xF800, 0x07E0, 0x001F, 0xFFFF};
return asset_colors[(hash + band) % (sizeof(asset_colors) / sizeof(asset_colors[0]))];
}
static const uint16_t idle_colors[] = {0x0000, 0x39E7, 0x7BEF, 0xFFFF, 0x7BEF, 0x39E7};
return idle_colors[band % (sizeof(idle_colors) / sizeof(idle_colors[0]))];
}
static esp_err_t lcd_direct_draw_test_pattern(const lcd_direct_request_t *request)
{
char clock_text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U] = {0};
lcd_direct_clock_text(clock_text);
for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT; row_start += LCD_DIRECT_ROWS_PER_TRANSFER) {
uint16_t row_count = LCD_DIRECT_HEIGHT - row_start;
if (row_count > LCD_DIRECT_ROWS_PER_TRANSFER) {
row_count = LCD_DIRECT_ROWS_PER_TRANSFER;
}
for (uint16_t y = 0; y < row_count; ++y) {
uint16_t color = lcd_direct_swap_color(lcd_direct_color_for_row(row_start + y, request));
for (uint16_t x = 0; x < LCD_DIRECT_WIDTH; ++x) {
s_line_buffer[(uint32_t)y * LCD_DIRECT_WIDTH + x] = color;
}
}
lcd_direct_overlay_clock(s_line_buffer, row_start, row_count, clock_text);
ESP_RETURN_ON_ERROR(lcd_direct_draw_rows(row_start, row_count),
TAG,
"write pixels failed");
}
return ESP_OK;
}
static size_t lcd_direct_prepare_icons(const lcd_direct_icon_draw_t *icons,
size_t icon_count,
lcd_direct_prepared_icon_t *prepared,
size_t prepared_capacity)
{
/* 绘制前集中解析全部图标,避免逐行合成时反复访问文件系统。 */
if (icons == NULL || icon_count == 0U || prepared == NULL || prepared_capacity == 0U) {
return 0;
}
size_t prepared_count = 0;
for (size_t i = 0; i < icon_count && prepared_count < prepared_capacity; ++i) {
const lcd_direct_icon_draw_t *draw = &icons[i];
const lcd_direct_icon_t *icon = lcd_direct_icon(draw->icon_id);
if (icon == NULL || icon->path == NULL) {
continue;
}
if (draw->x >= LCD_DIRECT_WIDTH || draw->y >= LCD_DIRECT_HEIGHT) {
continue;
}
lcd_direct_prepared_icon_t *slot = &prepared[prepared_count];
lcd_direct_decoded_image_t *decoded = NULL;
esp_err_t decode_err = lcd_direct_get_png_image(icon->path,
true,
true,
&decoded,
&slot->transient);
if (decode_err != ESP_OK || decoded == NULL) {
lcd_direct_release_decoded_image(&slot->transient);
ESP_LOGW(TAG, "prepare icon PNG failed path=%s err=%s", icon->path, esp_err_to_name(decode_err));
continue;
}
if (decoded == &slot->transient) {
slot->owns_transient = true;
} else {
decoded->pinned = true;
slot->cached = decoded;
}
slot->draw = *draw;
slot->icon = icon;
slot->decoded = decoded;
prepared_count++;
}
return prepared_count;
}
static void lcd_direct_release_prepared_icons(lcd_direct_prepared_icon_t *prepared, size_t prepared_count)
{
if (prepared == NULL) {
return;
}
for (size_t i = 0; i < prepared_count; ++i) {
if (prepared[i].cached != NULL) {
prepared[i].cached->pinned = false;
prepared[i].cached = NULL;
}
if (prepared[i].owns_transient) {
lcd_direct_release_decoded_image(&prepared[i].transient);
prepared[i].owns_transient = false;
}
prepared[i].decoded = NULL;
}
}
static size_t lcd_direct_append_status_icons(const lcd_direct_image_t *image,
lcd_direct_icon_draw_t *icons,
size_t icon_count,
size_t icon_capacity)
{
if (image == NULL || image->path == NULL || icons == NULL ||
icon_count >= icon_capacity || strstr(image->path, "/qrcode.png") != NULL ||
strstr(image->path, "/lcdtest") != NULL) {
return icon_count;
}
platform_lcd_status_overlay_t overlay = {0};
portENTER_CRITICAL(&s_state_lock);
overlay = s_status_overlay;
portEXIT_CRITICAL(&s_state_lock);
if (overlay.low_battery_visible && icon_count < icon_capacity) {
icons[icon_count++] = (lcd_direct_icon_draw_t) {
.icon_id = LCD_DIRECT_ICON_LOW_BATTERY,
.x = LCD_DIRECT_LOW_BATTERY_ICON_X,
.y = LCD_DIRECT_LOW_BATTERY_ICON_Y,
};
}
/* 充电状态由右上角新电池素材表示;保留底部低电量提醒。 */
return icon_count;
}
static void lcd_direct_overlay_icons(uint16_t *pixel_buffer,
uint16_t row_start,
uint16_t row_count,
const lcd_direct_prepared_icon_t *icons,
size_t icon_count)
{
/* 只处理当前传输行范围,并依据 alpha 平面与背景执行 RGB565 混合。 */
if (pixel_buffer == NULL || icons == NULL || icon_count == 0U) {
return;
}
const uint16_t row_end = row_start + row_count;
for (size_t icon_index = 0; icon_index < icon_count; ++icon_index) {
const lcd_direct_icon_draw_t *draw = &icons[icon_index].draw;
const lcd_direct_decoded_image_t *icon = icons[icon_index].decoded;
if (icon == NULL || !icon->valid || icon->pixels == NULL || icon->width == 0U || icon->height == 0U) {
continue;
}
if (draw->x >= LCD_DIRECT_WIDTH || draw->y >= LCD_DIRECT_HEIGHT) {
continue;
}
uint16_t draw_width = icon->width;
uint16_t draw_height = icon->height;
if (draw_width > LCD_DIRECT_WIDTH - draw->x) {
draw_width = LCD_DIRECT_WIDTH - draw->x;
}
if (draw_height > LCD_DIRECT_HEIGHT - draw->y) {
draw_height = LCD_DIRECT_HEIGHT - draw->y;
}
const uint16_t overlay_y0 = draw->y;
const uint16_t overlay_y1 = draw->y + draw_height;
if (overlay_y1 <= row_start || overlay_y0 >= row_end) {
continue;
}
const uint16_t copy_y0 = overlay_y0 > row_start ? overlay_y0 : row_start;
const uint16_t copy_y1 = overlay_y1 < row_end ? overlay_y1 : row_end;
for (uint16_t y = copy_y0; y < copy_y1; ++y) {
const uint16_t local_y = y - row_start;
const uint16_t image_y = y - draw->y;
const uint32_t icon_pixel_start = (uint32_t)image_y * icon->width;
const uint32_t icon_pixel_end = icon_pixel_start + draw_width;
for (uint32_t x = icon_pixel_start; x < icon_pixel_end; ++x) {
const uint32_t dst_index = (uint32_t)local_y * LCD_DIRECT_WIDTH + draw->x + (x - icon_pixel_start);
uint16_t color = icon->pixels[x];
if (icon->alpha_valid && icon->alpha != NULL) {
const uint16_t dst = lcd_direct_swap_color(pixel_buffer[dst_index]);
color = lcd_direct_blend_rgb565(dst, color, icon->alpha[x]);
}
pixel_buffer[dst_index] = lcd_direct_swap_color(color);
}
}
}
}
static size_t lcd_direct_clock_glyph_index(char character)
{
if (character >= '0' && character <= '9') {
return (size_t)(character - '0');
}
return character == ':' ? 10U : 11U;
}
static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U])
{
if (!platform_time_is_synced()) {
memcpy(text, "--:--", LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U);
return;
}
time_t now = time(NULL);
struct tm local_tm = {0};
if (localtime_r(&now, &local_tm) == NULL) {
memcpy(text, "--:--", LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U);
return;
}
text[0] = (char)('0' + local_tm.tm_hour / 10);
text[1] = (char)('0' + local_tm.tm_hour % 10);
text[2] = ':';
text[3] = (char)('0' + local_tm.tm_min / 10);
text[4] = (char)('0' + local_tm.tm_min % 10);
text[5] = '\0';
}
/* 输入/输出沿用 PNG 行缓冲字节序;GFX HUD 在提交前统一转换。 */
static void lcd_direct_hud_blend_pixel(uint16_t *pixels, uint16_t row_start,
uint16_t rows, int x, int y,
uint16_t color, uint8_t alpha)
{
if (pixels == NULL || alpha == 0 || x < 0 || x >= (int)LCD_DIRECT_WIDTH ||
y < (int)row_start || y >= (int)row_start + (int)rows) {
return;
}
const size_t index = (size_t)(y - row_start) * LCD_DIRECT_WIDTH + (size_t)x;
const uint16_t background = lcd_direct_swap_color(pixels[index]);
pixels[index] = lcd_direct_swap_color(lcd_direct_blend_rgb565(background, color, alpha));
}
static void lcd_direct_draw_clock_pass(uint16_t *pixels, uint16_t row_start,
uint16_t rows,
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U],
int x_offset, int y_offset, uint16_t color)
{
int cursor = LCD_DIRECT_CLOCK_X;
for (size_t i = 0; i < LCD_DIRECT_CLOCK_TEXT_LENGTH; ++i) {
const lcd_status_glyph_t *glyph = &lcd_status_font_glyphs[lcd_direct_clock_glyph_index(text[i])];
/* 原字体 line_height=16、base_line=3;不缩放,保留 4-bit 抗锯齿。 */
const int top = (int)LCD_DIRECT_CLOCK_Y + 16 - 3 - glyph->height - glyph->y;
for (unsigned y = 0; y < glyph->height; ++y) {
for (unsigned x = 0; x < glyph->width; ++x) {
const size_t pixel = y * glyph->width + x;
const uint8_t packed = lcd_status_font_bitmap[glyph->offset + pixel / 2];
const uint8_t alpha = ((pixel & 1U) ? packed & 15U : packed >> 4) * 17U;
lcd_direct_hud_blend_pixel(pixels, row_start, rows,
cursor + glyph->x + x_offset + (int)x,
top + y_offset + (int)y, color, alpha);
}
}
cursor += glyph->advance;
}
}
static void lcd_direct_overlay_clock(uint16_t *pixels, uint16_t row_start,
uint16_t rows,
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U])
{
if (pixels == NULL || text == NULL ||
(int)row_start >= (int)LCD_DIRECT_CLOCK_Y + 17 ||
(int)row_start + rows <= (int)LCD_DIRECT_CLOCK_Y) {
return;
}
lcd_direct_draw_clock_pass(pixels, row_start, rows, text, 1, 1, LCD_DIRECT_CLOCK_OUTLINE_COLOR);
lcd_direct_draw_clock_pass(pixels, row_start, rows, text, 0, 0, LCD_DIRECT_CLOCK_COLOR);
}
/* 档位向上分组:0..10→1,11..25→25,26..50→50,51..75→75,76..100→100。 */
static unsigned lcd_direct_battery_icon_index(uint8_t percent, bool charging)
{
unsigned level = percent <= 10 ? 0 : percent <= 25 ? 1 :
percent <= 50 ? 2 : percent <= 75 ? 3 : 4;
return level + (charging ? 5U : 0U);
}
static void lcd_direct_overlay_battery(uint16_t *pixels, uint16_t row_start, uint16_t rows)
{
platform_lcd_status_overlay_t status;
portENTER_CRITICAL(&s_state_lock);
status = s_status_overlay;
portEXIT_CRITICAL(&s_state_lock);
/* 素材没有未知电量图标;读取有效电量前不显示,避免暗示错误档位。 */
if (pixels == NULL || !status.battery_valid ||
(int)row_start >= (int)(LCD_DIRECT_BATTERY_Y + LCD_DIRECT_BATTERY_HEIGHT) ||
(int)row_start + rows <= (int)LCD_DIRECT_BATTERY_Y) {
return;
}
const lcd_status_icon_t *icon = &lcd_status_battery_icons[
lcd_direct_battery_icon_index(status.battery_percent, status.charging_visible)];
size_t pixel = 0;
/* 从 Flash 流式读 palette/RLE,保留源图 Alpha,不创建解码图缓存。 */
for (size_t run = 0; run + 1 < icon->run_bytes; run += 2) {
const uint8_t *value = icon->palette + (size_t)icon->runs[run + 1] * 3;
const uint16_t color = ((uint16_t)value[0] << 8) | value[1];
for (unsigned count = 0; count < icon->runs[run]; ++count, ++pixel) {
if (pixel >= LCD_DIRECT_BATTERY_WIDTH * LCD_DIRECT_BATTERY_HEIGHT) {
return;
}
lcd_direct_hud_blend_pixel(pixels, row_start, rows,
LCD_DIRECT_BATTERY_X + pixel % LCD_DIRECT_BATTERY_WIDTH,
LCD_DIRECT_BATTERY_Y + pixel / LCD_DIRECT_BATTERY_WIDTH,
color, value[2]);
}
}
}
static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image,
const lcd_direct_icon_draw_t *icons,
size_t icon_count)
{
/* 按固定行块合成主图、状态图标和时钟,限制单次 DMA 缓冲占用。 */
ESP_RETURN_ON_FALSE(image != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid image");
ESP_RETURN_ON_FALSE(image->path != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid image path");
lcd_direct_decoded_image_t transient = {0};
lcd_direct_decoded_image_t *decoded = NULL;
ESP_RETURN_ON_ERROR(lcd_direct_get_png_image(image->path, false, true, &decoded, &transient),
TAG,
"load PNG image failed");
ESP_RETURN_ON_FALSE(decoded != NULL && decoded->pixels != NULL,
ESP_FAIL,
TAG,
"decoded PNG image is invalid");
if (decoded != &transient) {
decoded->pinned = true;
}
lcd_direct_icon_draw_t combined_icons[LCD_DIRECT_DEFAULT_ICON_COUNT] = {0};
size_t combined_icon_count = icon_count;
if (combined_icon_count > LCD_DIRECT_DEFAULT_ICON_COUNT) {
combined_icon_count = LCD_DIRECT_DEFAULT_ICON_COUNT;
}
if (icons != NULL && combined_icon_count > 0U) {
memcpy(combined_icons, icons, combined_icon_count * sizeof(combined_icons[0]));
}
combined_icon_count = lcd_direct_append_status_icons(
image,
combined_icons,
combined_icon_count,
LCD_DIRECT_DEFAULT_ICON_COUNT);
lcd_direct_prepared_icon_t prepared_icons[LCD_DIRECT_DEFAULT_ICON_COUNT] = {0};
size_t prepared_icon_count = lcd_direct_prepare_icons(combined_icons,
combined_icon_count,
prepared_icons,
sizeof(prepared_icons) / sizeof(prepared_icons[0]));
const bool clock_visible = strstr(image->path, "/qrcode.png") == NULL &&
strstr(image->path, "/lcdtest") == NULL;
char clock_text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U] = {0};
if (clock_visible) {
lcd_direct_clock_text(clock_text);
}
esp_err_t err = ESP_OK;
for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT; row_start += LCD_DIRECT_ROWS_PER_TRANSFER) {
uint16_t row_count = LCD_DIRECT_HEIGHT - row_start;
if (row_count > LCD_DIRECT_ROWS_PER_TRANSFER) {
row_count = LCD_DIRECT_ROWS_PER_TRANSFER;
}
for (uint16_t y = 0; y < row_count; ++y) {
const uint32_t dst_row_offset = (uint32_t)y * LCD_DIRECT_WIDTH;
const uint32_t src_y = row_start + y;
for (uint16_t x = 0; x < LCD_DIRECT_WIDTH; ++x) {
uint16_t color = 0;
if (x < decoded->width && src_y < decoded->height) {
color = decoded->pixels[(uint32_t)src_y * decoded->width + x];
}
s_line_buffer[dst_row_offset + x] = lcd_direct_swap_color(color);
}
}
if (clock_visible) {
lcd_direct_overlay_clock(s_line_buffer, row_start, row_count, clock_text);
}
lcd_direct_overlay_icons(s_line_buffer, row_start, row_count, prepared_icons, prepared_icon_count);
if (clock_visible) {
lcd_direct_overlay_battery(s_line_buffer, row_start, row_count);
}
err = lcd_direct_draw_rows(row_start, row_count);
if (err != ESP_OK) {
ESP_LOGE(TAG, "write PNG image pixels failed path=%s", image->path);
break;
}
}
lcd_direct_release_prepared_icons(prepared_icons, prepared_icon_count);
if (decoded == &transient) {
lcd_direct_release_decoded_image(&transient);
} else {
decoded->pinned = false;
}
return err;
}
static esp_err_t lcd_direct_preheat_image(const lcd_direct_image_t *image)
{
ESP_RETURN_ON_FALSE(image != NULL && image->path != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid preheat image");
lcd_direct_decoded_image_t transient = {0};
lcd_direct_decoded_image_t *decoded = NULL;
esp_err_t err = lcd_direct_get_png_image(image->path, false, true, &decoded, &transient);
if (decoded == &transient) {
lcd_direct_release_decoded_image(&transient);
}
return err;
}
static esp_err_t lcd_direct_preheat_asset_sequence(const lcd_direct_request_t *request)
{
ESP_RETURN_ON_FALSE(request != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid preheat request");
const size_t frame_count = request->frame_order_count > 0U ? request->frame_order_count : 1U;
for (size_t i = 0; i < frame_count; ++i) {
const uint8_t frame_index = request->frame_order_count > 0U ? request->frame_order[i] : (uint8_t)i;
const lcd_direct_image_t *frame = lcd_direct_asset_frame(request->asset_dir, frame_index);
if (frame == NULL) {
return ESP_ERR_NOT_FOUND;
}
ESP_RETURN_ON_ERROR(lcd_direct_preheat_image(frame), TAG, "preheat PNG frame failed");
}
return ESP_OK;
}
static esp_err_t lcd_direct_preheat_basic_face(void)
{
const lcd_direct_request_t request = {
.kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE,
.asset_dir = "jichubiaoqing",
.frame_order = {0, 1, 2},
.frame_order_count = 3,
};
return lcd_direct_preheat_asset_sequence(&request);
}
static bool lcd_direct_preheat_is_pending(void)
{
bool pending;
portENTER_CRITICAL(&s_state_lock);
pending = s_preheat_pending_valid;
portEXIT_CRITICAL(&s_state_lock);
return pending;
}
static bool lcd_direct_take_preheat(lcd_direct_request_t *out_request,
uint32_t *out_generation)
{
/* 取出请求与标记 in-progress 在同一临界区完成,确保只有渲染任务消费。 */
bool has_request = false;
portENTER_CRITICAL(&s_state_lock);
if (s_preheat_pending_valid) {
*out_request = s_preheat_pending;
*out_generation = s_preheat_generation;
memset(&s_preheat_pending, 0, sizeof(s_preheat_pending));
s_preheat_pending_valid = false;
s_preheat_in_progress = true;
has_request = true;
}
portEXIT_CRITICAL(&s_state_lock);
return has_request;
}
static void lcd_direct_complete_preheat(uint32_t generation, esp_err_t result)
{
portENTER_CRITICAL(&s_state_lock);
s_preheat_result = result;
s_preheat_completed_generation = generation;
s_preheat_in_progress = false;
portEXIT_CRITICAL(&s_state_lock);
if (s_preheat_done_sem != NULL) {
(void)xSemaphoreGive(s_preheat_done_sem);
}
}
/* 仅由持有 s_lock 的 LCD 渲染任务调用,预热不会与实际绘制并发访问缓存。 */
static bool lcd_direct_service_preheat(void)
{
lcd_direct_request_t request = {0};
uint32_t generation = 0;
if (!lcd_direct_take_preheat(&request, &generation)) {
return false;
}
esp_err_t result = lcd_direct_preheat_asset_sequence(&request);
lcd_direct_complete_preheat(generation, result);
ESP_LOGI(TAG,
"LCD preheat completed asset=%s result=%s",
request.asset_dir,
esp_err_to_name(result));
return true;
}
static void lcd_direct_cancel_preheat(esp_err_t result)
{
bool signal_waiter = false;
uint32_t generation = 0;
portENTER_CRITICAL(&s_state_lock);
if (s_preheat_pending_valid || s_preheat_in_progress) {
generation = s_preheat_generation;
memset(&s_preheat_pending, 0, sizeof(s_preheat_pending));
s_preheat_pending_valid = false;
s_preheat_in_progress = false;
s_preheat_result = result;
s_preheat_completed_generation = generation;
signal_waiter = true;
}
portEXIT_CRITICAL(&s_state_lock);
if (signal_waiter && s_preheat_done_sem != NULL) {
(void)xSemaphoreGive(s_preheat_done_sem);
}
}
static uint32_t lcd_direct_asset_sequence_frame_ms(const lcd_direct_request_t *request)
{
return (request != NULL && request->frame_ms > 0U) ? request->frame_ms : 100U;
}
static uint32_t lcd_direct_asset_sequence_hold_ms(const lcd_direct_request_t *request,
size_t frame_index,
size_t frame_count)
{
const uint32_t frame_ms = lcd_direct_asset_sequence_frame_ms(request);
if (request != NULL && strcmp(request->asset_dir, "jichubiaoqing") == 0) {
return (frame_index + 1U == frame_count) ?
LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS :
LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS;
}
if (request != NULL && strcmp(request->asset_dir, "lcd_boundary_test") == 0) {
return frame_ms;
}
if (frame_index + 1U != frame_count) {
return frame_ms;
}
return frame_ms + LCD_DIRECT_LAST_FRAME_EXTRA_HOLD_MS;
}
static uint32_t lcd_direct_asset_sequence_duration_ms(const lcd_direct_request_t *request)
{
if (request == NULL || request->duration_ms == 0U) {
return 0U;
}
if (strcmp(request->asset_dir, "lcd_boundary_test") == 0) {
return request->duration_ms;
}
const size_t frame_count = request->frame_order_count > 0U ? request->frame_order_count : 1U;
const uint32_t frame_ms = lcd_direct_asset_sequence_frame_ms(request);
const uint64_t base_cycle_ms = (uint64_t)frame_count * (uint64_t)frame_ms;
if (base_cycle_ms == 0U) {
return request->duration_ms;
}
const uint64_t completed_cycles = (uint64_t)request->duration_ms / base_cycle_ms;
uint32_t last_frame_extra_ms = LCD_DIRECT_LAST_FRAME_EXTRA_HOLD_MS;
if (strcmp(request->asset_dir, "jichubiaoqing") == 0) {
const uint64_t base_cycle_ms = (uint64_t)frame_count * (uint64_t)frame_ms;
const uint64_t adjusted_cycle_ms = frame_count > 0U ?
((uint64_t)(frame_count - 1U) *
(uint64_t)LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS) +
(uint64_t)LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS :
base_cycle_ms;
last_frame_extra_ms = adjusted_cycle_ms > base_cycle_ms ?
(uint32_t)(adjusted_cycle_ms - base_cycle_ms) :
0U;
}
uint64_t adjusted_duration_ms = (uint64_t)request->duration_ms +
(completed_cycles * (uint64_t)last_frame_extra_ms);
if (adjusted_duration_ms > UINT32_MAX) {
adjusted_duration_ms = UINT32_MAX;
}
return (uint32_t)adjusted_duration_ms;
}
static esp_err_t lcd_direct_draw_asset_sequence_once(const lcd_direct_request_t *request)
{
ESP_RETURN_ON_FALSE(request != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid asset request");
const size_t frame_count = request->frame_order_count > 0U ? request->frame_order_count : 1U;
for (size_t i = 0; i < frame_count; ++i) {
(void)lcd_direct_service_preheat();
bool stop = false;
portENTER_CRITICAL(&s_state_lock);
stop = s_stop_requested || s_pending_valid;
portEXIT_CRITICAL(&s_state_lock);
if (stop) {
return ESP_ERR_INVALID_STATE;
}
const uint8_t frame_index = request->frame_order_count > 0U ? request->frame_order[i] : (uint8_t)i;
const lcd_direct_image_t *frame = lcd_direct_asset_frame(request->asset_dir, frame_index);
ESP_RETURN_ON_FALSE(frame != NULL,
ESP_ERR_NOT_FOUND,
TAG,
"LCD asset frame not found asset=%s frame=%u",
request->asset_dir,
(unsigned)frame_index);
ESP_RETURN_ON_ERROR(lcd_direct_draw_png_image(frame, NULL, 0),
TAG,
"draw asset frame failed");
const uint32_t hold_ms = lcd_direct_asset_sequence_hold_ms(request, i, frame_count);
vTaskDelay(pdMS_TO_TICKS(hold_ms));
}
static TickType_t s_last_draw_success_tick;
TickType_t now = xTaskGetTickCount();
if (s_last_draw_success_tick == 0 ||
(now - s_last_draw_success_tick) >= pdMS_TO_TICKS(5000)) {
s_last_draw_success_tick = now;
ESP_LOGI(TAG,
"LCD asset sequence drawn asset=%s frames=%u",
request->asset_dir,
(unsigned)frame_count);
}
return ESP_OK;
}
static esp_err_t lcd_direct_draw_asset_sequence_for_duration(const lcd_direct_request_t *request,
TickType_t start_tick,
TickType_t duration_ticks,
lcd_direct_sequence_perf_t *perf)
{
ESP_RETURN_ON_FALSE(request != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid timed asset request");
const size_t frame_count = request->frame_order_count > 0U ? request->frame_order_count : 1U;
while ((xTaskGetTickCount() - start_tick) < duration_ticks) {
for (size_t i = 0; i < frame_count; ++i) {
(void)lcd_direct_service_preheat();
bool stop = false;
portENTER_CRITICAL(&s_state_lock);
stop = s_stop_requested || s_pending_valid;
portEXIT_CRITICAL(&s_state_lock);
if (stop) {
return ESP_ERR_INVALID_STATE;
}
const uint8_t frame_index = request->frame_order_count > 0U ? request->frame_order[i] : (uint8_t)i;
const lcd_direct_image_t *frame = lcd_direct_asset_frame(request->asset_dir, frame_index);
ESP_RETURN_ON_FALSE(frame != NULL,
ESP_ERR_NOT_FOUND,
TAG,
"LCD asset frame not found asset=%s frame=%u",
request->asset_dir,
(unsigned)frame_index);
const int64_t draw_started_us = esp_timer_get_time();
ESP_RETURN_ON_ERROR(lcd_direct_draw_png_image(frame, NULL, 0),
TAG,
"draw timed asset frame failed");
const uint32_t draw_us = (uint32_t)(esp_timer_get_time() - draw_started_us);
if (perf != NULL) {
perf->completed_frames++;
perf->total_draw_us += draw_us;
if (draw_us < perf->min_draw_us) {
perf->min_draw_us = draw_us;
}
if (draw_us > perf->max_draw_us) {
perf->max_draw_us = draw_us;
}
}
const TickType_t now = xTaskGetTickCount();
const TickType_t elapsed_ticks = now - start_tick;
if (elapsed_ticks >= duration_ticks) {
return ESP_OK;
}
const uint32_t hold_ms = lcd_direct_asset_sequence_hold_ms(request, i, frame_count);
const TickType_t frame_ticks = pdMS_TO_TICKS(hold_ms);
const TickType_t remaining_ticks = duration_ticks - elapsed_ticks;
vTaskDelay(frame_ticks < remaining_ticks ? frame_ticks : remaining_ticks);
}
}
return ESP_OK;
}
static void lcd_direct_log_sequence_perf(const lcd_direct_request_t *request,
const char *mode,
const lcd_direct_sequence_perf_t *perf,
uint64_t elapsed_us)
{
if (request == NULL || perf == NULL || perf->completed_frames == 0U || elapsed_us == 0U) {
return;
}
const uint64_t fps_x100 =
((uint64_t)perf->completed_frames * 100000000ULL) / elapsed_us;
const uint32_t average_draw_us =
(uint32_t)(perf->total_draw_us / perf->completed_frames);
ESP_LOGI(TAG,
"LCD sequence perf asset=%s mode=%s working_set=%u cache_capacity=%u "
"frames=%u elapsed_ms=%llu fps=%llu.%02llu draw_us_avg=%u "
"draw_us_min=%u draw_us_max=%u hold_ms=%u",
request->asset_dir,
mode,
(unsigned)request->frame_order_count,
(unsigned)LCD_DIRECT_FRAME_CACHE_MAX,
(unsigned)perf->completed_frames,
(unsigned long long)(elapsed_us / 1000ULL),
(unsigned long long)(fps_x100 / 100ULL),
(unsigned long long)(fps_x100 % 100ULL),
(unsigned)average_draw_us,
(unsigned)perf->min_draw_us,
(unsigned)perf->max_draw_us,
(unsigned)lcd_direct_asset_sequence_frame_ms(request));
}
static esp_err_t lcd_direct_run_sequence_perf_stage(const lcd_direct_request_t *request,
uint32_t duration_ms,
const char *mode)
{
const TickType_t start_tick = xTaskGetTickCount();
const TickType_t duration_ticks = pdMS_TO_TICKS(duration_ms);
const int64_t started_us = esp_timer_get_time();
lcd_direct_sequence_perf_t perf = {
.min_draw_us = UINT32_MAX,
};
esp_err_t err = lcd_direct_draw_asset_sequence_for_duration(
request, start_tick, duration_ticks, &perf);
const uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
lcd_direct_log_sequence_perf(request, mode, &perf, elapsed_us);
return err;
}
static esp_err_t lcd_direct_run_boundary_test(const lcd_direct_request_t *request)
{
ESP_RETURN_ON_FALSE(request != NULL &&
request->frame_order_count > LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES,
ESP_ERR_INVALID_ARG,
TAG,
"invalid LCD boundary test request");
lcd_direct_request_t cache_hit_request = *request;
cache_hit_request.frame_order_count = LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES;
ESP_RETURN_ON_ERROR(lcd_direct_preheat_asset_sequence(&cache_hit_request),
TAG,
"preheat cache-hit frames failed");
const uint32_t cache_hit_duration_ms = request->duration_ms / 2U;
const uint32_t cache_thrash_duration_ms = request->duration_ms - cache_hit_duration_ms;
ESP_RETURN_ON_ERROR(lcd_direct_run_sequence_perf_stage(
&cache_hit_request, cache_hit_duration_ms, "cache_hit"),
TAG,
"cache-hit performance stage failed");
lcd_direct_request_t cache_thrash_preheat = *request;
cache_thrash_preheat.frame_order_count = LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES;
const size_t first_preheat_frame =
request->frame_order_count - LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES;
memcpy(cache_thrash_preheat.frame_order,
&request->frame_order[first_preheat_frame],
LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES * sizeof(cache_thrash_preheat.frame_order[0]));
ESP_RETURN_ON_ERROR(lcd_direct_preheat_asset_sequence(&cache_thrash_preheat),
TAG,
"preheat cache-thrash frames failed");
return lcd_direct_run_sequence_perf_stage(
request, cache_thrash_duration_ms, "cache_thrash");
}
static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *request)
{
/* 每帧间隙检查更高优先级请求和停止标志,使长动画可以被及时抢占。 */
ESP_RETURN_ON_FALSE(request != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid asset sequence");
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
/* 兼容历史基础表情请求(包括服务器错误的有限播放),统一到新眼睛。 */
if (s_emote_ready && request->duration_ms == 0U &&
strcmp(request->asset_dir, "jichubiaoqing") == 0) {
return lcd_direct_play_emote_face_until_pending(request);
}
if (s_emote_ready && s_listening_ready && request->duration_ms == 0U &&
strcmp(request->asset_dir, "listening") == 0) {
return lcd_direct_play_emote_face_until_pending(request);
}
#endif
if (request->asset_dir[0] == '\0' ||
lcd_direct_asset_frame(request->asset_dir,
request->frame_order_count > 0U ? request->frame_order[0] : 0U) == NULL) {
ESP_LOGW(TAG, "LCD asset unavailable, restore idle asset=%s", request->asset_dir);
lcd_direct_queue_idle_if_available();
if (request->repeat_count > 0U || request->duration_ms > 0U) {
lcd_direct_signal_completion();
}
return ESP_OK;
}
if (request->duration_ms > 0U) {
esp_err_t err;
if (strcmp(request->asset_dir, "lcd_boundary_test") == 0) {
err = lcd_direct_run_boundary_test(request);
} else {
err = lcd_direct_run_sequence_perf_stage(
request,
lcd_direct_asset_sequence_duration_ms(request),
"normal");
}
if (err != ESP_OK) {
return err;
}
lcd_direct_signal_completion();
return ESP_OK;
}
uint16_t remaining = request->repeat_count;
do {
esp_err_t err = lcd_direct_draw_asset_sequence_once(request);
if (err != ESP_OK) {
return err;
}
if (request->repeat_count > 0U) {
remaining--;
}
} while (request->repeat_count == 0U || remaining > 0U);
lcd_direct_signal_completion();
return ESP_OK;
}
static esp_err_t lcd_direct_play_step_sequence(const lcd_direct_request_t *request)
{
ESP_RETURN_ON_FALSE(request != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"invalid step sequence");
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
int fortune = lcd_direct_fortune_sequence_index(request);
if (fortune >= 0 && s_emote_ready && LCD_DIRECT_WIDTH == 320 && LCD_DIRECT_HEIGHT == 240) {
esp_err_t err = lcd_direct_play_fortune((size_t)fortune);
if (err != ESP_ERR_NOT_SUPPORTED) return err;
ESP_LOGW(TAG, "Fortune layers unavailable; using PNG fallback");
}
#endif
for (size_t i = 0; i < request->step_count; ++i) {
(void)lcd_direct_service_preheat();
bool stop = false;
portENTER_CRITICAL(&s_state_lock);
stop = s_stop_requested || s_pending_valid;
portEXIT_CRITICAL(&s_state_lock);
if (stop) {
return ESP_ERR_INVALID_STATE;
}
const platform_lcd_presentation_step_t *step = &request->steps[i];
const lcd_direct_image_t *frame = lcd_direct_asset_frame(step->asset_dir, step->frame_index);
ESP_RETURN_ON_FALSE(frame != NULL,
ESP_ERR_NOT_FOUND,
TAG,
"LCD step frame not found asset=%s frame=%u",
step->asset_dir != NULL ? step->asset_dir : "<null>",
(unsigned)step->frame_index);
lcd_direct_icon_draw_t icon = {0};
const lcd_direct_icon_draw_t *icons = NULL;
size_t icon_count = 0;
if (step->icon_id != PLATFORM_LCD_PRESENTATION_NO_ICON) {
icon = (lcd_direct_icon_draw_t) {
.icon_id = step->icon_id,
.x = step->icon_x,
.y = step->icon_y,
};
icons = &icon;
icon_count = 1;
}
ESP_RETURN_ON_ERROR(lcd_direct_draw_png_image(frame, icons, icon_count),
TAG,
"draw step frame failed");
uint32_t delay_ms = step->delay_ms > 0U ? step->delay_ms : 100U;
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
lcd_direct_signal_completion();
return ESP_OK;
}
static esp_err_t lcd_direct_play_basic_face_until_pending(void)
{
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
if (s_emote_ready) {
return lcd_direct_play_emote_face_until_pending(NULL);
}
#endif
const lcd_direct_request_t request = {
.kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE,
.asset_dir = "jichubiaoqing",
.frame_order = {0, 1, 2},
.frame_order_count = 3,
.repeat_count = 0,
.frame_ms = 100,
};
while (true) {
(void)lcd_direct_service_preheat();
bool stop = false;
portENTER_CRITICAL(&s_state_lock);
stop = s_stop_requested || s_pending_valid;
portEXIT_CRITICAL(&s_state_lock);
if (stop) {
return ESP_OK;
}
esp_err_t err = lcd_direct_draw_asset_sequence_once(&request);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
return err;
}
}
}
static void lcd_direct_signal_completion(void)
{
/* 完成计数是事实来源,二值信号量仅用于唤醒等待者,不承担事件计数。 */
portENTER_CRITICAL(&s_state_lock);
s_completion_count++;
portEXIT_CRITICAL(&s_state_lock);
if (s_completion_sem != NULL) {
(void)xSemaphoreGive(s_completion_sem);
}
}
static bool lcd_direct_take_pending(lcd_direct_request_t *out)
{
/* pending 槽采用移动语义,取出后立即清零,渲染任务独占本地副本。 */
bool has_pending = false;
portENTER_CRITICAL(&s_state_lock);
if (s_pending_valid) {
*out = s_pending;
s_pending_valid = false;
memset(&s_pending, 0, sizeof(s_pending));
has_pending = true;
}
portEXIT_CRITICAL(&s_state_lock);
return has_pending;
}
esp_err_t lcd_direct_init_device(void)
{
/* 同步原语先于 GPIO/SPI 创建,任意异步完成回调到达时所需对象均已存在。 */
if (s_initialized) {
return ESP_OK;
}
s_lock = xSemaphoreCreateMutex();
ESP_RETURN_ON_FALSE(s_lock != NULL, ESP_ERR_NO_MEM, TAG, "lock create failed");
s_backlight_lock = xSemaphoreCreateMutex();
ESP_RETURN_ON_FALSE(s_backlight_lock != NULL,
ESP_ERR_NO_MEM,
TAG,
"backlight lock create failed");
s_completion_sem = xSemaphoreCreateBinary();
ESP_RETURN_ON_FALSE(s_completion_sem != NULL, ESP_ERR_NO_MEM, TAG, "completion sem create failed");
s_preheat_done_sem = xSemaphoreCreateBinary();
ESP_RETURN_ON_FALSE(s_preheat_done_sem != NULL, ESP_ERR_NO_MEM, TAG, "preheat sem create failed");
s_transfer_done_sem = xSemaphoreCreateBinary();
ESP_RETURN_ON_FALSE(s_transfer_done_sem != NULL,
ESP_ERR_NO_MEM,
TAG,
"transfer sem create failed");
ESP_RETURN_ON_ERROR(lcd_direct_gpio_init(), TAG, "gpio init failed");
ESP_RETURN_ON_ERROR(lcd_direct_panel_init(), TAG, "esp_lcd panel init failed");
ESP_RETURN_ON_ERROR(lcd_direct_clear_panel(), TAG, "panel clear failed");
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
esp_err_t emote_err = lcd_direct_emote_init();
if (emote_err != ESP_OK) {
ESP_LOGW(TAG, "esp_emote_gfx idle test unavailable; using PNG fallback: %s",
esp_err_to_name(emote_err));
}
#endif
ESP_RETURN_ON_ERROR(ledc_set_duty(LEDC_LOW_SPEED_MODE,
LCD_DIRECT_BACKLIGHT_PWM_CHANNEL,
lcd_direct_backlight_duty(
CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT)),
TAG,
"backlight brightness set failed");
ESP_RETURN_ON_ERROR(ledc_update_duty(LEDC_LOW_SPEED_MODE,
LCD_DIRECT_BACKLIGHT_PWM_CHANNEL),
TAG,
"backlight brightness update failed");
s_initialized = true;
ESP_LOGI(TAG,
"Direct ST7789P3 LCD initialized (native=%ux%u logical=%ux%u MOSI=%d SCLK=%d CS=%d DC=%d RST=%d BL=%d clock=%d)",
(unsigned)LCD_DIRECT_NATIVE_WIDTH,
(unsigned)LCD_DIRECT_NATIVE_HEIGHT,
(unsigned)LCD_DIRECT_WIDTH,
(unsigned)LCD_DIRECT_HEIGHT,
CONFIG_TQ_SCREEN_SPI_MOSI_GPIO,
CONFIG_TQ_SCREEN_SPI_CLK_GPIO,
CONFIG_TQ_SCREEN_SPI_CS_GPIO,
CONFIG_TQ_SCREEN_DC_GPIO,
CONFIG_TQ_SCREEN_RESET_PIN,
CONFIG_TQ_SCREEN_BACKLIGHT_GPIO,
CONFIG_TQ_SCREEN_SPI_PIXEL_CLOCK_HZ);
return ESP_OK;
}
esp_err_t lcd_direct_set_backlight_brightness(uint8_t brightness_percent)
{
ESP_RETURN_ON_FALSE(brightness_percent <= 100U,
ESP_ERR_INVALID_ARG,
TAG,
"backlight brightness must be between 0 and 100 percent");
ESP_RETURN_ON_FALSE(s_initialized && s_backlight_lock != NULL,
ESP_ERR_INVALID_STATE,
TAG,
"LCD backlight is not initialized");
ESP_RETURN_ON_FALSE(xSemaphoreTake(s_backlight_lock,
pdMS_TO_TICKS(LCD_DIRECT_BACKLIGHT_LOCK_TIMEOUT_MS)) == pdTRUE,
ESP_ERR_TIMEOUT,
TAG,
"backlight lock timed out");
esp_err_t err = ledc_set_duty(LEDC_LOW_SPEED_MODE,
LCD_DIRECT_BACKLIGHT_PWM_CHANNEL,
lcd_direct_backlight_duty(brightness_percent));
if (err == ESP_OK) {
err = ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_DIRECT_BACKLIGHT_PWM_CHANNEL);
}
xSemaphoreGive(s_backlight_lock);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Backlight brightness set to %u%%", (unsigned)brightness_percent);
} else {
ESP_LOGE(TAG, "Backlight brightness update failed: %s", esp_err_to_name(err));
}
return err;
}
void lcd_direct_start_render_loop(void)
{
/* 当前调用任务本身成为渲染任务,因此此函数在收到停止请求前不会返回。 */
if (!s_initialized) {
ESP_LOGW(TAG, "Direct LCD is not initialized, skip render loop");
return;
}
portENTER_CRITICAL(&s_state_lock);
if (s_render_loop_running) {
portEXIT_CRITICAL(&s_state_lock);
ESP_LOGI(TAG, "Direct LCD render loop already running");
return;
}
s_render_loop_running = true;
s_stop_requested = false;
s_render_task = xTaskGetCurrentTaskHandle();
portEXIT_CRITICAL(&s_state_lock);
ESP_LOGI(TAG, "Direct LCD render loop started");
esp_err_t preheat_err = ESP_OK;
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
if (!s_emote_ready) {
preheat_err = lcd_direct_preheat_basic_face();
}
#else
preheat_err = lcd_direct_preheat_basic_face();
#endif
if (preheat_err != ESP_OK) {
ESP_LOGW(TAG, "Default LCD face PNG preheat failed: %s", esp_err_to_name(preheat_err));
}
while (true) {
bool stop = false;
portENTER_CRITICAL(&s_state_lock);
stop = s_stop_requested;
portEXIT_CRITICAL(&s_state_lock);
if (stop) {
break;
}
lcd_direct_request_t request = {0};
if (lcd_direct_preheat_is_pending()) {
if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) == pdTRUE) {
(void)lcd_direct_service_preheat();
xSemaphoreGive(s_lock);
}
} else if (lcd_direct_take_pending(&request)) {
if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) == pdTRUE) {
esp_err_t draw_err = ESP_OK;
if (request.kind == PLATFORM_LCD_PRESENTATION_IDLE) {
draw_err = lcd_direct_play_basic_face_until_pending();
} else if (request.kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) {
draw_err = lcd_direct_play_asset_sequence(&request);
} else if (request.kind == PLATFORM_LCD_PRESENTATION_STEP_SEQUENCE) {
draw_err = lcd_direct_play_step_sequence(&request);
} else {
draw_err = lcd_direct_draw_test_pattern(&request);
}
if ((draw_err == ESP_OK && request.return_to_idle) ||
(draw_err != ESP_OK && draw_err != ESP_ERR_INVALID_STATE)) {
lcd_direct_queue_idle_if_available();
}
xSemaphoreGive(s_lock);
if (draw_err != ESP_OK && draw_err != ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG, "LCD presentation draw failed: %s", esp_err_to_name(draw_err));
}
}
} else {
if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) == pdTRUE) {
esp_err_t idle_err = lcd_direct_play_basic_face_until_pending();
xSemaphoreGive(s_lock);
if (idle_err != ESP_OK && idle_err != ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG, "LCD idle draw failed: %s", esp_err_to_name(idle_err));
vTaskDelay(pdMS_TO_TICKS(100));
}
}
}
(void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(20));
}
lcd_direct_cancel_preheat(ESP_ERR_INVALID_STATE);
portENTER_CRITICAL(&s_state_lock);
s_render_task = NULL;
s_render_loop_running = false;
s_stop_requested = false;
portEXIT_CRITICAL(&s_state_lock);
ESP_LOGI(TAG, "Direct LCD render loop stopped");
}
esp_err_t lcd_direct_stop_render_loop(int timeout_ms)
{
/* 先协作停止;超过边界后删除已记录任务,并取消所有预热等待者。 */
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
TaskHandle_t task = NULL;
portENTER_CRITICAL(&s_state_lock);
task = s_render_task;
if (task != NULL) {
s_stop_requested = true;
}
portEXIT_CRITICAL(&s_state_lock);
if (task == NULL || timeout_ms == 0) {
return ESP_OK;
}
xTaskNotifyGive(task);
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
if (timeout_ticks == 0) {
timeout_ticks = 1;
}
TickType_t start = xTaskGetTickCount();
while ((xTaskGetTickCount() - start) < timeout_ticks) {
bool stopped = false;
portENTER_CRITICAL(&s_state_lock);
stopped = !s_render_loop_running && s_render_task == NULL;
portEXIT_CRITICAL(&s_state_lock);
if (stopped) {
return ESP_OK;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
if (task != xTaskGetCurrentTaskHandle()) {
ESP_LOGW(TAG, "Direct LCD render loop stop timed out after %d ms; force deleting", timeout_ms);
vTaskDelete(task);
portENTER_CRITICAL(&s_state_lock);
if (s_render_task == task) {
s_render_task = NULL;
}
s_render_loop_running = false;
s_stop_requested = false;
portEXIT_CRITICAL(&s_state_lock);
lcd_direct_cancel_preheat(ESP_ERR_INVALID_STATE);
}
return ESP_ERR_TIMEOUT;
}
esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *overlay)
{
if (overlay == NULL || overlay->battery_percent > 100U) {
return ESP_ERR_INVALID_ARG;
}
bool changed = false;
TaskHandle_t render_task = NULL;
portENTER_CRITICAL(&s_state_lock);
changed = s_status_overlay.low_battery_visible != overlay->low_battery_visible ||
s_status_overlay.charging_visible != overlay->charging_visible ||
s_status_overlay.battery_valid != overlay->battery_valid ||
s_status_overlay.battery_percent != overlay->battery_percent;
s_status_overlay = *overlay;
render_task = s_render_task;
portEXIT_CRITICAL(&s_state_lock);
if (changed && render_task != NULL) {
xTaskNotifyGive(render_task);
}
if (changed) {
ESP_LOGI(TAG,
"LCD battery overlays low=%d charging=%d",
overlay->low_battery_visible,
overlay->charging_visible);
}
return ESP_OK;
}
esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request)
{
/* 校验并深拷贝可变数组后再发布 pending,发布完成即不再引用调用方数据。 */
if (!s_initialized || request == NULL) {
return ESP_ERR_INVALID_STATE;
}
lcd_direct_request_t copied = {
.kind = request->kind,
.priority = request->priority,
.interrupt_current = request->interrupt_current,
.return_to_idle = request->return_to_idle,
.repeat_count = request->repeat_count,
.frame_ms = request->frame_ms,
.duration_ms = request->duration_ms,
};
if (request->asset_dir != NULL) {
strlcpy(copied.asset_dir, request->asset_dir, sizeof(copied.asset_dir));
}
if (request->frame_order != NULL && request->frame_order_count > 0U) {
copied.frame_order_count = request->frame_order_count;
if (copied.frame_order_count > PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX) {
copied.frame_order_count = PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX;
}
memcpy(copied.frame_order, request->frame_order, copied.frame_order_count);
}
if (request->steps != NULL && request->step_count > 0U) {
copied.step_count = request->step_count;
if (copied.step_count > PLATFORM_LCD_PRESENTATION_STEP_MAX) {
copied.step_count = PLATFORM_LCD_PRESENTATION_STEP_MAX;
}
memcpy(copied.steps, request->steps, copied.step_count * sizeof(copied.steps[0]));
}
TaskHandle_t task = NULL;
portENTER_CRITICAL(&s_state_lock);
s_pending = copied;
s_pending_valid = true;
task = s_render_task;
portEXIT_CRITICAL(&s_state_lock);
if (task != NULL) {
xTaskNotifyGive(task);
}
ESP_LOGI(TAG, "Direct LCD presentation queued kind=%d asset=%s", copied.kind, copied.asset_dir);
return ESP_OK;
}
esp_err_t lcd_direct_preheat_presentation(const platform_lcd_presentation_request_t *request)
{
/* 每次只允许一个同步预热等待者,以 generation 区分迟到的完成信号。 */
if (!s_initialized || request == NULL) {
return ESP_ERR_INVALID_STATE;
}
lcd_direct_request_t copied = {
.kind = request->kind,
.priority = request->priority,
.interrupt_current = request->interrupt_current,
.return_to_idle = request->return_to_idle,
.repeat_count = request->repeat_count,
.frame_ms = request->frame_ms,
.duration_ms = request->duration_ms,
};
if (request->asset_dir != NULL) {
strlcpy(copied.asset_dir, request->asset_dir, sizeof(copied.asset_dir));
}
if (request->frame_order != NULL && request->frame_order_count > 0U) {
copied.frame_order_count = request->frame_order_count;
if (copied.frame_order_count > PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX) {
copied.frame_order_count = PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX;
}
memcpy(copied.frame_order, request->frame_order, copied.frame_order_count);
}
if (copied.kind != PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) {
return ESP_ERR_INVALID_ARG;
}
TaskHandle_t render_task = NULL;
uint32_t generation = 0;
portENTER_CRITICAL(&s_state_lock);
if (!s_render_loop_running || s_stop_requested || s_render_task == NULL ||
s_preheat_pending_valid || s_preheat_in_progress || s_preheat_waiter_active) {
portEXIT_CRITICAL(&s_state_lock);
return ESP_ERR_INVALID_STATE;
}
/* Reserve the single request/wait slot before touching the completion
* semaphore so a concurrent caller cannot consume this caller's signal. */
s_preheat_waiter_active = true;
portEXIT_CRITICAL(&s_state_lock);
/* Discard a completion left by a caller that timed out after submission. */
(void)xSemaphoreTake(s_preheat_done_sem, 0);
portENTER_CRITICAL(&s_state_lock);
if (!s_render_loop_running || s_stop_requested || s_render_task == NULL) {
s_preheat_waiter_active = false;
portEXIT_CRITICAL(&s_state_lock);
return ESP_ERR_INVALID_STATE;
}
s_preheat_generation++;
if (s_preheat_generation == 0U) {
s_preheat_generation = 1U;
}
generation = s_preheat_generation;
s_preheat_pending = copied;
s_preheat_pending_valid = true;
render_task = s_render_task;
portEXIT_CRITICAL(&s_state_lock);
xTaskNotifyGive(render_task);
ESP_LOGI(TAG, "LCD preheat queued asset=%s", copied.asset_dir);
BaseType_t notified = xSemaphoreTake(s_preheat_done_sem,
pdMS_TO_TICKS(LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS));
esp_err_t result = ESP_ERR_TIMEOUT;
portENTER_CRITICAL(&s_state_lock);
if (s_preheat_completed_generation == generation) {
result = s_preheat_result;
} else if (notified == pdTRUE) {
result = ESP_ERR_INVALID_STATE;
}
s_preheat_waiter_active = false;
portEXIT_CRITICAL(&s_state_lock);
return result;
}
uint32_t lcd_direct_presentation_completion_count(void)
{
uint32_t count;
portENTER_CRITICAL(&s_state_lock);
count = s_completion_count;
portEXIT_CRITICAL(&s_state_lock);
return count;
}
esp_err_t lcd_direct_wait_presentation_completion(uint32_t since_count, uint32_t timeout_ms)
{
/* 循环检查单调计数而非相信信号量次数,可正确处理合并唤醒和先完成后等待。 */
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
TickType_t start_tick = xTaskGetTickCount();
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
for (;;) {
if (lcd_direct_presentation_completion_count() != since_count) {
return ESP_OK;
}
if (timeout_ms == 0 || (xTaskGetTickCount() - start_tick) >= timeout_ticks) {
return ESP_ERR_TIMEOUT;
}
TickType_t elapsed = xTaskGetTickCount() - start_tick;
TickType_t remaining = timeout_ticks > elapsed ? timeout_ticks - elapsed : 0;
if (s_completion_sem != NULL && remaining > 0) {
(void)xSemaphoreTake(s_completion_sem, remaining);
} else {
vTaskDelay(pdMS_TO_TICKS(LCD_DIRECT_PRESENTATION_WAIT_POLL_MS));
}
}
}