feat(lcd): unify idle restoration and add clock battery HUD

This commit is contained in:
chenjintao
2026-10-09 14:38:17 +08:00
parent f6408be05b
commit e75846ea15
5 changed files with 294 additions and 46 deletions
@@ -1416,6 +1416,8 @@ esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *s
status->level_state == DOMAIN_BATTERY_LEVEL_CRITICAL),
.charging_visible =
status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING,
.battery_valid = status->valid,
.battery_percent = status->percentage,
};
return platform_lcd_set_status_overlay(&overlay);
}
+2
View File
@@ -683,6 +683,8 @@ typedef struct {
typedef struct {
bool low_battery_visible;
bool charging_visible;
bool battery_valid; /* false 时显示未知值,不伪造 0%。 */
uint8_t battery_percent; /* domain 提供的 SOC,0..100。 */
} platform_lcd_status_overlay_t;
/* 状态覆盖层是持久状态,不会随一次 presentation 播放结束而自动清除。 */
+178 -46
View File
@@ -12,6 +12,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
@@ -98,6 +99,7 @@
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#define LCD_DIRECT_HUD_ROWS 40U
#define LCD_DIRECT_EMOTE_FPS 30U
#define LCD_DIRECT_EMOTE_BUFFER_ROWS 4U
#define LCD_DIRECT_EMOTE_EYES_X 41
@@ -241,6 +243,12 @@ 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;
@@ -258,6 +266,18 @@ static void lcd_direct_overlay_clock(uint16_t *pixel_buffer,
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)
@@ -423,8 +443,20 @@ static esp_err_t lcd_direct_emote_init(void)
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");
}
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock esp_emote_gfx failed");
ESP_RETURN_ON_ERROR(gfx_emote_unlock(s_emote_handle), TAG, "unlock esp_emote_gfx 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");
s_emote_ready = true;
ESP_LOGI(TAG,
"esp_emote_gfx idle test ready (%u fps, 2 x %u-row DMA buffers)",
@@ -442,6 +474,47 @@ static bool lcd_direct_should_stop_idle(void)
return stop;
}
/* 调用方持有 GFX 锁;字形复用 PNG 路径,无字体包和额外 PNG 解码。 */
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(uint32_t duration_ms)
{
uint32_t remaining_ms = duration_ms;
@@ -449,6 +522,16 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
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 = gfx_refr_now(s_emote_handle);
}
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;
}
uint32_t slice_ms = remaining_ms > LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
: remaining_ms;
@@ -495,7 +578,7 @@ static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure)
LCD_DIRECT_EMOTE_EYES_Y + offset_y);
}
static esp_err_t lcd_direct_play_emote_face_until_pending(void)
static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_request_t *request)
{
ESP_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL,
ESP_ERR_INVALID_STATE,
@@ -505,7 +588,10 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(void)
portENTER_CRITICAL(&s_state_lock);
s_emote_active = true;
portEXIT_CRITICAL(&s_state_lock);
esp_err_t result = lcd_direct_emote_set_eye_closure(0U);
esp_err_t result = lcd_direct_emote_update_hud(true, NULL);
if (result == ESP_OK) {
result = lcd_direct_emote_set_eye_closure(0U);
}
if (result == ESP_OK) {
gfx_disp_refresh_all(s_emote_disp);
result = gfx_refr_now(s_emote_handle);
@@ -521,8 +607,12 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(void)
24U, 80U, 160U, 232U, 255U, 255U,
232U, 192U, 128U, 64U, 16U, 0U,
};
while (result == ESP_OK) {
result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_OPEN_MS);
uint16_t cycles = 0;
const uint16_t repeats = request != NULL ? request->repeat_count : 0;
const uint32_t open_ms = repeats > 0 && request->frame_ms > 0
? request->frame_ms : 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;
}
@@ -543,8 +633,12 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(void)
result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_STEP_MS);
}
}
++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);
@@ -1117,13 +1211,7 @@ static size_t lcd_direct_append_status_icons(const lcd_direct_image_t *image,
.y = LCD_DIRECT_LOW_BATTERY_ICON_Y,
};
}
if (overlay.charging_visible && icon_count < icon_capacity) {
icons[icon_count++] = (lcd_direct_icon_draw_t) {
.icon_id = LCD_DIRECT_ICON_CHARGING,
.x = LCD_DIRECT_CHARGING_ICON_X,
.y = LCD_DIRECT_CHARGING_ICON_Y,
};
}
/* 充电状态改由右上角程序绘制的电池图标表示,避免和百分比重叠。 */
return icon_count;
}
@@ -1291,6 +1379,59 @@ static void lcd_direct_overlay_clock(uint16_t *pixel_buffer,
LCD_DIRECT_CLOCK_COLOR);
}
/* 小型电池 + 数字百分比,沿用 5x7 点阵;所有写入都裁剪到当前行块。 */
static void lcd_direct_hud_pixel(uint16_t *pixels, uint16_t row_start,
uint16_t rows, int x, int y, uint16_t color)
{
if (x >= 0 && x < LCD_DIRECT_WIDTH && y >= row_start && y < row_start + rows) {
pixels[(y - row_start) * LCD_DIRECT_WIDTH + x] = lcd_direct_swap_color(color);
}
}
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);
const uint16_t color = status.charging_visible ? 0x07E0U :
status.low_battery_visible ? 0xF800U : 0xFFFFU;
const int left = LCD_DIRECT_WIDTH - 108;
char text[5];
if (status.battery_valid) {
snprintf(text, sizeof(text), "%u%%", (unsigned)status.battery_percent);
} else {
memcpy(text, "--%", 4);
}
static const uint8_t percent[7] = {0x19, 0x1A, 0x02, 0x04, 0x08, 0x0B, 0x13};
for (size_t i = 0; text[i] != '\0'; ++i) {
const uint8_t *glyph = text[i] == '%' ? percent :
s_clock_glyph_rows[lcd_direct_clock_glyph_index(text[i])];
for (int y = 0; y < 21; ++y) {
for (int x = 0; x < 15; ++x) {
if (glyph[y / 3] & (1U << (4 - x / 3))) {
lcd_direct_hud_pixel(pixels, row_start, rows, left + i * 18 + x, 13 + y, color);
}
}
}
}
const int icon_x = LCD_DIRECT_WIDTH - 32;
const int fill = status.battery_valid ? status.battery_percent * 16 / 100 : 0;
for (int y = 0; y < 20; ++y) {
for (int x = 0; x < 24; ++x) {
bool on = x < 22 && (x < 2 || x >= 20 || y < 2 || y >= 18);
on |= x >= 22 && y >= 6 && y < 14;
on |= status.battery_valid && x >= 3 && x < 3 + fill && y >= 3 && y < 17;
/* 充电时在电池中挖出黑色闪电,绿色外框仍可辨识。 */
if (status.charging_visible && y >= 3 && y < 17 &&
x >= 11 - (y - 3) / 3 && x <= 14 - (y - 3) / 3) {
on = false;
}
lcd_direct_hud_pixel(pixels, row_start, rows, icon_x + x, 14 + y, on ? color : 0);
}
}
}
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)
@@ -1367,6 +1508,9 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image,
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);
@@ -1772,23 +1916,23 @@ static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *requ
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);
}
#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 sequence unavailable, fallback to basic face asset=%s", request->asset_dir);
const lcd_direct_request_t fallback = {
.kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE,
.asset_dir = "jichubiaoqing",
.frame_order = {0, 1, 2},
.frame_order_count = 3,
.repeat_count = 1,
.frame_ms = 100,
};
esp_err_t fallback_err = lcd_direct_draw_asset_sequence_once(&fallback);
if (fallback_err == ESP_OK && request->repeat_count > 0U) {
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 fallback_err;
return ESP_OK;
}
if (request->duration_ms > 0U) {
@@ -1878,7 +2022,7 @@ 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();
return lcd_direct_play_emote_face_until_pending(NULL);
}
#endif
const lcd_direct_request_t request = {
@@ -2083,29 +2227,15 @@ void lcd_direct_start_render_loop(void)
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);
if (draw_err == ESP_OK && request.return_to_idle) {
lcd_direct_request_t idle = {
.kind = PLATFORM_LCD_PRESENTATION_IDLE,
};
portENTER_CRITICAL(&s_state_lock);
s_pending = idle;
s_pending_valid = true;
portEXIT_CRITICAL(&s_state_lock);
}
} else if (request.kind == PLATFORM_LCD_PRESENTATION_STEP_SEQUENCE) {
draw_err = lcd_direct_play_step_sequence(&request);
if (draw_err == ESP_OK && request.return_to_idle) {
lcd_direct_request_t idle = {
.kind = PLATFORM_LCD_PRESENTATION_IDLE,
};
portENTER_CRITICAL(&s_state_lock);
s_pending = idle;
s_pending_valid = true;
portEXIT_CRITICAL(&s_state_lock);
}
} 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));
@@ -2187,7 +2317,7 @@ esp_err_t lcd_direct_stop_render_loop(int timeout_ms)
esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *overlay)
{
if (overlay == NULL) {
if (overlay == NULL || overlay->battery_percent > 100U) {
return ESP_ERR_INVALID_ARG;
}
@@ -2195,7 +2325,9 @@ esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *ove
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.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);