feat(lcd): unify idle restoration and add clock battery HUD
This commit is contained in:
@@ -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),
|
status->level_state == DOMAIN_BATTERY_LEVEL_CRITICAL),
|
||||||
.charging_visible =
|
.charging_visible =
|
||||||
status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING,
|
status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING,
|
||||||
|
.battery_valid = status->valid,
|
||||||
|
.battery_percent = status->percentage,
|
||||||
};
|
};
|
||||||
return platform_lcd_set_status_overlay(&overlay);
|
return platform_lcd_set_status_overlay(&overlay);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -683,6 +683,8 @@ typedef struct {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
bool low_battery_visible;
|
bool low_battery_visible;
|
||||||
bool charging_visible;
|
bool charging_visible;
|
||||||
|
bool battery_valid; /* false 时显示未知值,不伪造 0%。 */
|
||||||
|
uint8_t battery_percent; /* domain 提供的 SOC,0..100。 */
|
||||||
} platform_lcd_status_overlay_t;
|
} platform_lcd_status_overlay_t;
|
||||||
|
|
||||||
/* 状态覆盖层是持久状态,不会随一次 presentation 播放结束而自动清除。 */
|
/* 状态覆盖层是持久状态,不会随一次 presentation 播放结束而自动清除。 */
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@
|
|||||||
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U
|
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U
|
||||||
|
|
||||||
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
|
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
|
||||||
|
#define LCD_DIRECT_HUD_ROWS 40U
|
||||||
#define LCD_DIRECT_EMOTE_FPS 30U
|
#define LCD_DIRECT_EMOTE_FPS 30U
|
||||||
#define LCD_DIRECT_EMOTE_BUFFER_ROWS 4U
|
#define LCD_DIRECT_EMOTE_BUFFER_ROWS 4U
|
||||||
#define LCD_DIRECT_EMOTE_EYES_X 41
|
#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_base_pixels;
|
||||||
static uint8_t *s_emote_eye_pixels;
|
static uint8_t *s_emote_eye_pixels;
|
||||||
static bool s_emote_ready;
|
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_handle_t s_emote_handle;
|
||||||
static gfx_disp_t *s_emote_disp;
|
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,
|
uint16_t row_count,
|
||||||
const char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]);
|
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,
|
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,
|
esp_lcd_panel_io_event_data_t *event_data,
|
||||||
void *user_ctx)
|
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]),
|
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");
|
TAG, "read eye mesh failed");
|
||||||
}
|
}
|
||||||
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock esp_emote_gfx failed");
|
s_emote_hud_pixels = heap_caps_calloc(LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS,
|
||||||
ESP_RETURN_ON_ERROR(gfx_emote_unlock(s_emote_handle), TAG, "unlock esp_emote_gfx failed");
|
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;
|
s_emote_ready = true;
|
||||||
ESP_LOGI(TAG,
|
ESP_LOGI(TAG,
|
||||||
"esp_emote_gfx idle test ready (%u fps, 2 x %u-row DMA buffers)",
|
"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;
|
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)
|
static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
|
||||||
{
|
{
|
||||||
uint32_t remaining_ms = 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()) {
|
if (lcd_direct_should_stop_idle()) {
|
||||||
return ESP_ERR_INVALID_STATE;
|
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
|
uint32_t slice_ms = remaining_ms > LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
|
||||||
? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
|
? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
|
||||||
: remaining_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);
|
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_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL,
|
||||||
ESP_ERR_INVALID_STATE,
|
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);
|
portENTER_CRITICAL(&s_state_lock);
|
||||||
s_emote_active = true;
|
s_emote_active = true;
|
||||||
portEXIT_CRITICAL(&s_state_lock);
|
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) {
|
if (result == ESP_OK) {
|
||||||
gfx_disp_refresh_all(s_emote_disp);
|
gfx_disp_refresh_all(s_emote_disp);
|
||||||
result = gfx_refr_now(s_emote_handle);
|
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,
|
24U, 80U, 160U, 232U, 255U, 255U,
|
||||||
232U, 192U, 128U, 64U, 16U, 0U,
|
232U, 192U, 128U, 64U, 16U, 0U,
|
||||||
};
|
};
|
||||||
while (result == ESP_OK) {
|
uint16_t cycles = 0;
|
||||||
result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_OPEN_MS);
|
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) {
|
if (result != ESP_OK) {
|
||||||
break;
|
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);
|
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);
|
esp_err_t lock_err = gfx_emote_lock(s_emote_handle);
|
||||||
if (lock_err == ESP_OK) {
|
if (lock_err == ESP_OK) {
|
||||||
portENTER_CRITICAL(&s_state_lock);
|
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,
|
.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;
|
return icon_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1291,6 +1379,59 @@ static void lcd_direct_overlay_clock(uint16_t *pixel_buffer,
|
|||||||
LCD_DIRECT_CLOCK_COLOR);
|
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,
|
static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image,
|
||||||
const lcd_direct_icon_draw_t *icons,
|
const lcd_direct_icon_draw_t *icons,
|
||||||
size_t icon_count)
|
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_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);
|
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);
|
err = lcd_direct_draw_rows(row_start, row_count);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "write PNG image pixels failed path=%s", image->path);
|
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,
|
TAG,
|
||||||
"invalid asset sequence");
|
"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' ||
|
if (request->asset_dir[0] == '\0' ||
|
||||||
lcd_direct_asset_frame(request->asset_dir,
|
lcd_direct_asset_frame(request->asset_dir,
|
||||||
request->frame_order_count > 0U ? request->frame_order[0] : 0U) == NULL) {
|
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);
|
ESP_LOGW(TAG, "LCD asset unavailable, restore idle asset=%s", request->asset_dir);
|
||||||
const lcd_direct_request_t fallback = {
|
lcd_direct_queue_idle_if_available();
|
||||||
.kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE,
|
if (request->repeat_count > 0U || request->duration_ms > 0U) {
|
||||||
.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) {
|
|
||||||
lcd_direct_signal_completion();
|
lcd_direct_signal_completion();
|
||||||
}
|
}
|
||||||
return fallback_err;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request->duration_ms > 0U) {
|
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 CONFIG_TQ_LCD_EMOTE_GFX_TEST
|
||||||
if (s_emote_ready) {
|
if (s_emote_ready) {
|
||||||
return lcd_direct_play_emote_face_until_pending();
|
return lcd_direct_play_emote_face_until_pending(NULL);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
const lcd_direct_request_t request = {
|
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();
|
draw_err = lcd_direct_play_basic_face_until_pending();
|
||||||
} else if (request.kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) {
|
} else if (request.kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) {
|
||||||
draw_err = lcd_direct_play_asset_sequence(&request);
|
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) {
|
} else if (request.kind == PLATFORM_LCD_PRESENTATION_STEP_SEQUENCE) {
|
||||||
draw_err = lcd_direct_play_step_sequence(&request);
|
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 {
|
} else {
|
||||||
draw_err = lcd_direct_draw_test_pattern(&request);
|
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);
|
xSemaphoreGive(s_lock);
|
||||||
if (draw_err != ESP_OK && draw_err != ESP_ERR_INVALID_STATE) {
|
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));
|
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)
|
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;
|
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;
|
TaskHandle_t render_task = NULL;
|
||||||
portENTER_CRITICAL(&s_state_lock);
|
portENTER_CRITICAL(&s_state_lock);
|
||||||
changed = s_status_overlay.low_battery_visible != overlay->low_battery_visible ||
|
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;
|
s_status_overlay = *overlay;
|
||||||
render_task = s_render_task;
|
render_task = s_render_task;
|
||||||
portEXIT_CRITICAL(&s_state_lock);
|
portEXIT_CRITICAL(&s_state_lock);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 待机恢复与时间 / 电量状态栏
|
||||||
|
|
||||||
|
当前实现(2026-10-09)以本文为准,旧 lcd-expressions.md 中的测试图案、
|
||||||
|
优先级仲裁描述不代表当前运行行为。
|
||||||
|
|
||||||
|
## 返回待机
|
||||||
|
|
||||||
|
- 开机 / 默认空闲、显式 IDLE 请求、有限动画 return_to_idle、缺失素材回退和
|
||||||
|
非取消的播放错误都进入同一个待机后端。
|
||||||
|
- CONFIG_TQ_LCD_EMOTE_GFX_TEST 启用且初始化成功时使用单纹理网格眨眼;
|
||||||
|
禁用或初始化失败才使用历史三帧 PNG 安全后备。
|
||||||
|
- 历史 jichubiaoqing 请求(duration_ms=0)也适配到 GFX。
|
||||||
|
有限播放保留 repeat_count,每轮为 frame_ms 睁眼停留 + 12 个 34ms 眨眼步骤;
|
||||||
|
不承诺与旧 PNG 完全相同的持续时间。服务错误因此不再临时退回三帧。
|
||||||
|
- 自动恢复只在没有 pending 请求且未停止时排入 IDLE,不能覆盖新来的请求。
|
||||||
|
- 其他表情仍为 PNG / step sequence,未改动其图像或动画顺序。
|
||||||
|
pending 仍是单槽替换,不是完整优先级状态机。
|
||||||
|
|
||||||
|
## 状态栏
|
||||||
|
|
||||||
|
- 左上 HH:MM,沿用 SNTP 和系统时区;未同步显示 --:--。
|
||||||
|
- 右上百分比和电池;未知值显示 --%,充电绿色并带闪电缺口,低电量红色。
|
||||||
|
- SOC、有效性和充电状态来自现有 domain battery 服务,不从 ADC 重算。
|
||||||
|
- GFX 顶部 320x40 RGB565 图层,常驻 25,600 B PSRAM;只在分钟或电池状态
|
||||||
|
改变时使图层失效并刷新,眨眼仍只改变眼睛网格,不解码新 PNG。
|
||||||
|
- PNG 表情复用相同点阵 / 电池绘制器,在每帧合成时更新;二维码和 LCD
|
||||||
|
边界测试页面不显示状态栏。PNG 无限等待期间不会独立重绘状态栏。
|
||||||
|
- 不需要字体包或新图片。旧 PNG 低电量底部告警保留;旧充电 PNG 不再自动
|
||||||
|
叠加,避免与数字电量冲突。GFX 待机低电量以红色电池表示。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
`python3 tools/test_lcd_status_hud.py` 从实际源码提取绘制函数,用 Clang
|
||||||
|
AddressSanitizer / UndefinedBehaviorSanitizer 检查 808 组有效性、充电、
|
||||||
|
低电量、0..100% 组合,验证行块和整帧等价、边界及颜色。
|
||||||
|
|
||||||
|
使用 ESP-IDF v5.5.4 执行 idf.py build。实机需验证:
|
||||||
|
|
||||||
|
1. 联网后 --:-- 变成本地时间,跨分钟更新;未同步不显示伪造时间。
|
||||||
|
2. 插拔充电线、电量变化时状态更新,眼睛位置 / 眨眼无变化或残影。
|
||||||
|
3. 各有限动画、Wi-Fi 恢复、PTT 结束、OTA 退出及服务错误后回 GFX 待机。
|
||||||
|
4. 动画结束同时到达新请求时不被自动 IDLE 覆盖。
|
||||||
|
|
||||||
|
编译 / 主机测试不等同于上述实机验证。
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import subprocess, tempfile, re
|
||||||
|
src=(Path(__file__).resolve().parents[1]/'components/platform/src/lcd_direct.c').read_text()
|
||||||
|
def function(name):
|
||||||
|
# Locate definition rather than the forward declaration.
|
||||||
|
matches=list(re.finditer(r'static [^;{}]+\b'+name+r'\([^;{}]*\)\s*\{',src))
|
||||||
|
assert len(matches)==1,name
|
||||||
|
start=matches[0].start(); p=src.index('{',start); depth=1; end=p+1
|
||||||
|
while depth:
|
||||||
|
if src[end]=='{':depth+=1
|
||||||
|
if src[end]=='}':depth-=1
|
||||||
|
end+=1
|
||||||
|
return src[start:end]
|
||||||
|
table=re.search(r'static const uint8_t s_clock_glyph_rows.*?\n};',src,re.S).group(0)
|
||||||
|
prefix='''#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#define LCD_DIRECT_WIDTH 320
|
||||||
|
#define LCD_DIRECT_CLOCK_GLYPH_HEIGHT 7
|
||||||
|
#define CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES 1
|
||||||
|
#define portENTER_CRITICAL(x) ((void)0)
|
||||||
|
#define portEXIT_CRITICAL(x) ((void)0)
|
||||||
|
typedef struct {bool low_battery_visible, charging_visible, battery_valid; uint8_t battery_percent;} platform_lcd_status_overlay_t;
|
||||||
|
static platform_lcd_status_overlay_t s_status_overlay;
|
||||||
|
#define PLATFORM_LCD_PRESENTATION_IDLE 0
|
||||||
|
typedef struct { int kind; } lcd_direct_request_t;
|
||||||
|
static lcd_direct_request_t s_pending;
|
||||||
|
static bool s_pending_valid, s_stop_requested;
|
||||||
|
'''
|
||||||
|
test='''int main(void) {
|
||||||
|
uint16_t full[320*240+2], blocks[320*240+2];
|
||||||
|
for (unsigned valid=0;valid<2;++valid)
|
||||||
|
for (unsigned charge=0;charge<2;++charge)
|
||||||
|
for (unsigned low=0;low<2;++low)
|
||||||
|
for (unsigned percent=0;percent<=100;++percent) {
|
||||||
|
s_status_overlay=(platform_lcd_status_overlay_t){low,charge,valid,percent};
|
||||||
|
for (unsigned i=0;i<320*240+2;++i) full[i]=blocks[i]=0x1234;
|
||||||
|
lcd_direct_overlay_battery(full+1,0,240);
|
||||||
|
for(unsigned row=0;row<240;row+=8)
|
||||||
|
lcd_direct_overlay_battery(blocks+1+row*320,row,8);
|
||||||
|
assert(memcmp(full,blocks,sizeof(full))==0);
|
||||||
|
assert(full[0]==0x1234 && full[320*240+1]==0x1234);
|
||||||
|
for(unsigned y=0;y<240;++y) for(unsigned x=0;x<320;++x) {
|
||||||
|
if(x<212 || y<13 || y>=34) assert(full[1+y*320+x]==0x1234);
|
||||||
|
}
|
||||||
|
uint16_t expected=lcd_direct_swap_color(charge?0x07e0:low?0xf800:0xffff);
|
||||||
|
assert(full[1+14*320+288]==expected);
|
||||||
|
unsigned fill=percent*16/100;
|
||||||
|
if(valid && !charge && fill) assert(full[1+17*320+291]==expected);
|
||||||
|
if(!valid) assert(full[1+17*320+291]==0);
|
||||||
|
}
|
||||||
|
for (unsigned pending=0;pending<2;++pending)
|
||||||
|
for (unsigned stopping=0;stopping<2;++stopping) {
|
||||||
|
s_pending=(lcd_direct_request_t){99};
|
||||||
|
s_pending_valid=pending; s_stop_requested=stopping;
|
||||||
|
lcd_direct_queue_idle_if_available();
|
||||||
|
assert(s_pending.kind==((pending || stopping)?99:PLATFORM_LCD_PRESENTATION_IDLE));
|
||||||
|
assert(s_pending_valid==(pending || !stopping));
|
||||||
|
}
|
||||||
|
puts("PASS: idle queue preserves pending requests and stop; 808 battery combinations; full-frame/8-row equivalence, bounds, colors and fill");
|
||||||
|
}'''
|
||||||
|
with tempfile.TemporaryDirectory(prefix='lucky-hud-') as tmp:
|
||||||
|
c=Path(tmp)/'hud.c'; exe=Path(tmp)/'hud'
|
||||||
|
c.write_text(prefix+table+'\n'+'\n'.join(function(n) for n in ['lcd_direct_swap_color','lcd_direct_clock_glyph_index','lcd_direct_hud_pixel','lcd_direct_overlay_battery','lcd_direct_queue_idle_if_available'])+'\n'+test)
|
||||||
|
subprocess.run(['clang','-std=c11','-fsanitize=address,undefined','-g',str(c),'-o',str(exe)],check=True)
|
||||||
|
subprocess.run([str(exe)],check=True)
|
||||||
Reference in New Issue
Block a user