/* * ST7789P3 LCD 直接驱动后端。 * * 本模块独占面板底层初始化和 SPI 访问,并通过串行渲染循环处理最小化的 * presentation 队列;调用方不得绕过该任务直接操作面板。 */ #include "lcd_direct.h" #include "lcd_direct_assets.h" #include "platform_asset_bundle.h" #include "lcd_status_assets.h" #include #include #include #include #include #include #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 : "", (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)); } } }