3200 lines
132 KiB
C
3200 lines
132 KiB
C
/*
|
||
* 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));
|
||
}
|
||
}
|
||
}
|