feat(lcd): animate fortune results with shared GFX layers
@@ -234,7 +234,7 @@ Use a clear conventional format:
|
||||
和预览,不把 tq-cat 的实现或其他目录里的旧文件当成当前工程。
|
||||
2. 优先查看 `lcd_direct.c`、领域表情目录、平台公共接口、构建列表和 Kconfig。
|
||||
图层制作还需阅读 `split_idle_expression.py`、`compress_idle_expression.py`。
|
||||
3. 当前是待机/listening GFX 与其他 PNG 混合后端;未接入 esp_emote_expression 状态机,
|
||||
3. 当前是待机/listening/抽签 GFX 与其他 PNG 混合后端;未接入 esp_emote_expression 状态机,
|
||||
也未整体迁移到 LVGL。不要凭依赖存在或请求优先级字段推断功能已经实现。
|
||||
|
||||
### 必须保持的正确性
|
||||
@@ -314,6 +314,15 @@ Use a clear conventional format:
|
||||
22. 实机验收包括待机/表情切换、跨分钟、充电、缓存启动、唤醒与网络并行、
|
||||
取消和超时。区分源码检查、主机测试、编译、设备日志与用户观感证据。
|
||||
没有 TE 同步,不能承诺完全无撕裂;用户满意也不等于已量化稳定帧率。
|
||||
23. 抽签还需阅读 `docs/抽签分层动画开发说明.zh-CN.md`。共享六图层与五份签文,
|
||||
一次只解压共享图层及所选签文;LZ4 与旧四字节 RLE 不是同一格式。
|
||||
`prepare_fortune_expression.py --check` 验证生成一致性/往返,
|
||||
`test_lcd_fortune_gfx.py` 验证实际 C 解码、几何、取消和失败释放。
|
||||
不能把所有结果都常驻或缓存全屏中间帧;退出先解绑 GFX 再释放 PSRAM。
|
||||
动画前准备失败才可 PNG 后备,取消/传输失败不得重播旧六帧。
|
||||
保留原领域结果选择和六步匹配;不得在平台重新随机抽签或改签文语义。
|
||||
保持 `(56,20,208,220)` 安全窗口,覆盖放大爪尖且避开时间/电池;
|
||||
图层尺寸或几何变化须同步生成器、预览与测试,不只替换 PNG。
|
||||
|
||||
回归命令(仓库根目录):
|
||||
|
||||
@@ -322,6 +331,7 @@ python3 tools/test_lcd_status_hud.py
|
||||
python3 tools/test_lcd_emote_transport.py
|
||||
python3 tools/test_lcd_emote_timeline.py
|
||||
python3 tools/test_lcd_listening_gfx.py
|
||||
python3 tools/test_lcd_fortune_gfx.py
|
||||
```
|
||||
|
||||
提交只包含任务相关文件,遵守主题分支与原有提交规则;不要提交临时补丁脚本、
|
||||
|
||||
@@ -659,6 +659,20 @@ static void handle_command_line(char *line)
|
||||
handle_prepare_rotate(key_version);
|
||||
} else if (strcmp(cmd, "lcd_test") == 0) {
|
||||
handle_lcd_test();
|
||||
} else if (strcmp(cmd, "lcd_fortune_test") == 0) {
|
||||
/* 有界、仅显示测试;不调用云抽签、不写 NVS、不改变语义选择映射。 */
|
||||
if (argc != 2 || argv[1][0] < '0' || argv[1][0] > '4' || argv[1][1] != '\0') {
|
||||
cli_write_error("invalid_fortune_index");
|
||||
return;
|
||||
}
|
||||
const domain_expression_request_t request = {
|
||||
.id = DOMAIN_EXPRESSION_CHOUQIAN,
|
||||
.trigger = DOMAIN_EXPRESSION_TRIGGER_FACTORY_LCD_TEST,
|
||||
.chouqian_result_index_valid = true,
|
||||
.chouqian_result_index = (size_t)(argv[1][0] - '0'),
|
||||
};
|
||||
if (domain_runtime_request_expression(&request) == ESP_OK) cli_write_ok();
|
||||
else cli_write_error("lcd_fortune_test_failed");
|
||||
} else {
|
||||
cli_write_error("unknown_command");
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ if(CONFIG_TQ_LCD_EMOTE_GFX_TEST)
|
||||
list(APPEND platform_srcs
|
||||
"src/lcd_emote_assets.c"
|
||||
"src/lcd_emote_listening.c"
|
||||
"src/lcd_emote_fortune.c"
|
||||
"src/generated/emote_gfx/fortune_assets.c"
|
||||
"src/generated/emote_gfx/idle_base.c"
|
||||
"src/generated/emote_gfx/idle_eyes_open.c"
|
||||
"src/generated/emote_gfx/listening_base.c"
|
||||
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"canvas": [
|
||||
320,
|
||||
240
|
||||
],
|
||||
"window": [
|
||||
56,
|
||||
20,
|
||||
208,
|
||||
220
|
||||
],
|
||||
"layers": [
|
||||
{
|
||||
"name": "pile",
|
||||
"width": 208,
|
||||
"height": 76,
|
||||
"x": 56,
|
||||
"y": 164,
|
||||
"raw_bytes": 47424,
|
||||
"lz4_bytes": 9417
|
||||
},
|
||||
{
|
||||
"name": "back",
|
||||
"width": 63,
|
||||
"height": 113,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 21357,
|
||||
"lz4_bytes": 4179
|
||||
},
|
||||
{
|
||||
"name": "edge",
|
||||
"width": 41,
|
||||
"height": 110,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 13530,
|
||||
"lz4_bytes": 4389
|
||||
},
|
||||
{
|
||||
"name": "front",
|
||||
"width": 100,
|
||||
"height": 190,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 57000,
|
||||
"lz4_bytes": 10774
|
||||
},
|
||||
{
|
||||
"name": "question",
|
||||
"width": 27,
|
||||
"height": 50,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 4050,
|
||||
"lz4_bytes": 817
|
||||
},
|
||||
{
|
||||
"name": "rod",
|
||||
"width": 169,
|
||||
"height": 56,
|
||||
"x": 0,
|
||||
"y": 40,
|
||||
"raw_bytes": 28392,
|
||||
"lz4_bytes": 2094
|
||||
},
|
||||
{
|
||||
"name": "text_buyun",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2638
|
||||
},
|
||||
{
|
||||
"name": "text_heping",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2851
|
||||
},
|
||||
{
|
||||
"name": "text_shangshang",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2069
|
||||
},
|
||||
{
|
||||
"name": "text_xiaoji",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2210
|
||||
},
|
||||
{
|
||||
"name": "text_zhongshang",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2109
|
||||
}
|
||||
],
|
||||
"note": "黑背景归零;共享爪子,签文字形来自原图;旧 PNG 保留"
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 730 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include "lcd_emote_assets.h"
|
||||
|
||||
/* 平台私有图层与几何;领域仍选择资产序列,不引入业务状态机。 */
|
||||
enum { LCD_FORTUNE_PILE, LCD_FORTUNE_BACK, LCD_FORTUNE_EDGE,
|
||||
LCD_FORTUNE_FRONT, LCD_FORTUNE_QUESTION, LCD_FORTUNE_ROD,
|
||||
LCD_FORTUNE_SHARED_COUNT, LCD_FORTUNE_RESULT_COUNT = 5 };
|
||||
#define LCD_FORTUNE_X 56
|
||||
#define LCD_FORTUNE_Y 20
|
||||
#define LCD_FORTUNE_WIDTH 208U
|
||||
#define LCD_FORTUNE_HEIGHT 220U
|
||||
#define LCD_FORTUNE_MOTION_MS 2500U
|
||||
#define LCD_FORTUNE_TOTAL_MS 4000U
|
||||
#define LCD_FORTUNE_HOLD_MS 1500U
|
||||
#define LCD_FORTUNE_STEP_MS 34U
|
||||
typedef struct {
|
||||
uint16_t width, height, x, y;
|
||||
size_t compressed_size;
|
||||
const uint8_t *data;
|
||||
} lcd_fortune_asset_t;
|
||||
extern const lcd_fortune_asset_t lcd_fortune_shared[LCD_FORTUNE_SHARED_COUNT];
|
||||
extern const lcd_fortune_asset_t lcd_fortune_text[LCD_FORTUNE_RESULT_COUNT];
|
||||
/* 有界 LZ4 block 解码,无动态内存;拒绝截断、无效回指及长度不符。 */
|
||||
bool lcd_emote_fortune_decode(const uint8_t *src, size_t size, uint8_t *dst, size_t capacity);
|
||||
extern const char *const lcd_fortune_result_dirs[LCD_FORTUNE_RESULT_COUNT];
|
||||
|
||||
/* RGB565 高字节在前 + 独立 A8;所有像素由调用方拥有。 */
|
||||
typedef struct { uint16_t width, height; const uint8_t *pixels; } lcd_fortune_texture_t;
|
||||
/* 输出不透明 RGB565,完整重画固定局部窗口,确保旧轮廓恢复而不残影。 */
|
||||
bool lcd_emote_fortune_build_frame(uint8_t *dst, size_t capacity,
|
||||
const lcd_fortune_texture_t shared[LCD_FORTUNE_SHARED_COUNT],
|
||||
const lcd_fortune_texture_t *text, uint32_t time_ms);
|
||||
@@ -38,6 +38,7 @@
|
||||
#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
|
||||
@@ -970,6 +971,7 @@ static esp_err_t lcd_direct_listening_prepare(void)
|
||||
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");
|
||||
@@ -1166,6 +1168,203 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
|
||||
}
|
||||
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)
|
||||
@@ -2457,6 +2656,14 @@ static esp_err_t lcd_direct_play_step_sequence(const lcd_direct_request_t *reque
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "lcd_emote_fortune.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct { float x, y, u, v; } vertex_t;
|
||||
static bool read_length(const uint8_t *src,size_t size,size_t *pos,size_t *length)
|
||||
{
|
||||
if (*length!=15) return true;
|
||||
unsigned value;
|
||||
do {
|
||||
if (*pos>=size || *length>SIZE_MAX-255) return false;
|
||||
value=src[(*pos)++]; *length+=value;
|
||||
} while (value==255);
|
||||
return true;
|
||||
}
|
||||
bool lcd_emote_fortune_decode(const uint8_t *src,size_t size,uint8_t *dst,size_t capacity)
|
||||
{
|
||||
if (!src || !size || !dst || !capacity) return false;
|
||||
size_t pos=0,written=0;
|
||||
while (pos<size) {
|
||||
unsigned token=src[pos++]; size_t literals=token>>4;
|
||||
if (!read_length(src,size,&pos,&literals) || literals>size-pos || literals>capacity-written) return false;
|
||||
memcpy(dst+written,src+pos,literals);written+=literals;pos+=literals;
|
||||
if (pos==size) return written==capacity && (token&15)==0;
|
||||
if (size-pos<2) return false;
|
||||
size_t offset=src[pos]|((size_t)src[pos+1]<<8);pos+=2;
|
||||
size_t match=token&15;
|
||||
if (!offset || offset>written || !read_length(src,size,&pos,&match) ||
|
||||
match>SIZE_MAX-4 || match+4>capacity-written) return false;
|
||||
match+=4;
|
||||
for (size_t i=0;i<match;++i) {dst[written]=dst[written-offset];++written;}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static float smooth(float v) { v = fmaxf(0, fminf(1, v)); return v*v*(3-2*v); }
|
||||
static uint16_t color_at(const lcd_fortune_texture_t *t, size_t i)
|
||||
{ return ((uint16_t)t->pixels[i*2]<<8) | t->pixels[i*2+1]; }
|
||||
static void blend(uint8_t *dst, int x, int y, uint16_t color, unsigned alpha)
|
||||
{
|
||||
x -= LCD_FORTUNE_X; y -= LCD_FORTUNE_Y;
|
||||
if (x < 0 || y < 0 || x >= LCD_FORTUNE_WIDTH || y >= LCD_FORTUNE_HEIGHT || !alpha) return;
|
||||
size_t i = ((size_t)y*LCD_FORTUNE_WIDTH+x)*2;
|
||||
uint16_t old = ((uint16_t)dst[i]<<8) | dst[i+1];
|
||||
unsigned r = (((color>>11)&31)*alpha + ((old>>11)&31)*(255-alpha)+127)/255;
|
||||
unsigned g = (((color>>5)&63)*alpha + ((old>>5)&63)*(255-alpha)+127)/255;
|
||||
unsigned b = ((color&31)*alpha + (old&31)*(255-alpha)+127)/255;
|
||||
uint16_t out = (r<<11)|(g<<5)|b;
|
||||
dst[i] = out>>8; dst[i+1] = out;
|
||||
}
|
||||
/* 双线性采样在预乘 Alpha 空间,避免黑色透明边缘造成轮廓暗边。 */
|
||||
static void sample(uint8_t *dst, int x, int y, const lcd_fortune_texture_t *t,
|
||||
float u, float v, unsigned opacity)
|
||||
{
|
||||
if (u < 0 || v < 0 || u > 1 || v > 1 || !opacity) return;
|
||||
float sx = u*(t->width-1), sy = v*(t->height-1);
|
||||
int x0 = (int)sx, y0 = (int)sy;
|
||||
int x1 = x0+1 < t->width ? x0+1 : x0, y1 = y0+1 < t->height ? y0+1 : y0;
|
||||
float fx = sx-x0, fy = sy-y0;
|
||||
const size_t indices[4] = {(size_t)y0*t->width+x0, (size_t)y0*t->width+x1,
|
||||
(size_t)y1*t->width+x0, (size_t)y1*t->width+x1};
|
||||
const float weights[4] = {(1-fx)*(1-fy), fx*(1-fy), (1-fx)*fy, fx*fy};
|
||||
float a=0, r=0, g=0, b=0;
|
||||
size_t count = (size_t)t->width*t->height;
|
||||
for (int i=0; i<4; ++i) {
|
||||
float aw = weights[i]*t->pixels[count*2+indices[i]];
|
||||
uint16_t c = color_at(t, indices[i]);
|
||||
a += aw; r += ((c>>11)&31)*aw; g += ((c>>5)&63)*aw; b += (c&31)*aw;
|
||||
}
|
||||
if (a < 0.5f) return;
|
||||
float inv_a = 1/a;
|
||||
uint16_t c = ((unsigned)(r*inv_a+0.5f)<<11)|((unsigned)(g*inv_a+0.5f)<<5)|(unsigned)(b*inv_a+0.5f);
|
||||
blend(dst,x,y,c,(unsigned)(a*opacity/255+0.5f));
|
||||
}
|
||||
static void blit(uint8_t *dst, const lcd_fortune_texture_t *t, int x, int y, unsigned opacity)
|
||||
{
|
||||
size_t count=(size_t)t->width*t->height;
|
||||
for (int sy=0; sy<t->height; ++sy) for (int sx=0; sx<t->width; ++sx) {
|
||||
size_t i=(size_t)sy*t->width+sx;
|
||||
unsigned a=t->pixels[count*2+i]*opacity/255;
|
||||
if (a) blend(dst,x+sx,y+sy,color_at(t,i),a);
|
||||
}
|
||||
}
|
||||
static void triangle(uint8_t *dst, const lcd_fortune_texture_t *t,
|
||||
vertex_t a, vertex_t b, vertex_t c, bool include_shared)
|
||||
{
|
||||
float det=(b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y);
|
||||
if (fabsf(det)<0.01f) return;
|
||||
int left=(int)floorf(fminf(a.x,fminf(b.x,c.x))), right=(int)ceilf(fmaxf(a.x,fmaxf(b.x,c.x)));
|
||||
int top=(int)floorf(fminf(a.y,fminf(b.y,c.y))), bottom=(int)ceilf(fmaxf(a.y,fmaxf(b.y,c.y)));
|
||||
if (left<LCD_FORTUNE_X) left=LCD_FORTUNE_X;
|
||||
if (top<LCD_FORTUNE_Y) top=LCD_FORTUNE_Y;
|
||||
if (right>LCD_FORTUNE_X+LCD_FORTUNE_WIDTH) right=LCD_FORTUNE_X+LCD_FORTUNE_WIDTH;
|
||||
if (bottom>LCD_FORTUNE_Y+LCD_FORTUNE_HEIGHT) bottom=LCD_FORTUNE_Y+LCD_FORTUNE_HEIGHT;
|
||||
float inv=1/det;
|
||||
for (int y=top; y<bottom; ++y) for (int x=left; x<right; ++x) {
|
||||
float px=x+0.5f-a.x, py=y+0.5f-a.y;
|
||||
float wb=(px*(c.y-a.y)-py*(c.x-a.x))*inv;
|
||||
float wc=((b.x-a.x)*py-(b.y-a.y)*px)*inv;
|
||||
/* 两个三角形共边只属于一方,避免 Alpha 轮廓重复混合。 */
|
||||
if (wb>=0 && wc>=0 && (include_shared ? wb+wc<=1 : wb+wc<1)) sample(dst,x,y,t,
|
||||
a.u+wb*(b.u-a.u)+wc*(c.u-a.u), a.v+wb*(b.v-a.v)+wc*(c.v-a.v),255);
|
||||
}
|
||||
}
|
||||
static vertex_t project(float u, float v, float width, float height, float theta,
|
||||
float cx, float cy, bool side, bool front)
|
||||
{
|
||||
float c=cosf(theta), s=sinf(theta), roll=0.025f*s;
|
||||
float x=side?0:(front?0.5f-u:u-0.5f)*width;
|
||||
float z=side?(u-0.5f)*10:0;
|
||||
float X=x*c-z*s, Z=x*s+z*c, k=320/(320+Z);
|
||||
float xx=X*k, yy=(v-0.5f)*height*k;
|
||||
return (vertex_t){cx+xx*cosf(roll)-yy*sinf(roll),cy+xx*sinf(roll)+yy*cosf(roll),u,v};
|
||||
}
|
||||
static void paw(uint8_t *dst, const lcd_fortune_texture_t *t, float width, float height,
|
||||
float theta, float cx, float cy, bool side, bool front)
|
||||
{
|
||||
/* 16 条纵向纹理带近似透视;每带仅两个三角形,非三维模型。 */
|
||||
for (int i=0; i<16; ++i) {
|
||||
float u0=i/16.0f,u1=(i+1)/16.0f;
|
||||
vertex_t a=project(u0,0,width,height,theta,cx,cy,side,front);
|
||||
vertex_t b=project(u1,0,width,height,theta,cx,cy,side,front);
|
||||
vertex_t c=project(u0,1,width,height,theta,cx,cy,side,front);
|
||||
vertex_t d=project(u1,1,width,height,theta,cx,cy,side,front);
|
||||
triangle(dst,t,a,b,c,true); triangle(dst,t,d,c,b,false);
|
||||
}
|
||||
}
|
||||
static void flip_question(uint8_t *dst,const lcd_fortune_texture_t *t,float theta,float cx,float cy)
|
||||
{
|
||||
/* 小问号与签的正面共用透视平面;正面展开时不突然贴出平面图案。 */
|
||||
vertex_t a=project(0.30f,0.43f,60,114,theta,cx,cy,false,true);
|
||||
vertex_t b=project(0.70f,0.43f,60,114,theta,cx,cy,false,true);
|
||||
vertex_t c=project(0.30f,0.816f,60,114,theta,cx,cy,false,true);
|
||||
vertex_t d=project(0.70f,0.816f,60,114,theta,cx,cy,false,true);
|
||||
/* 正面 local X 反向,因此按屏幕方向摆放 UV,签文不镜像。 */
|
||||
a.u=c.u=0; b.u=d.u=1; a.v=b.v=0; c.v=d.v=1;
|
||||
triangle(dst,t,a,b,c,true);triangle(dst,t,d,c,b,false);
|
||||
}
|
||||
static void overlay(uint8_t *dst,const lcd_fortune_texture_t *t,
|
||||
float left,float top,float width,float height,float reveal)
|
||||
{
|
||||
for (int y=(int)floorf(top); y<(int)ceilf(top+height*reveal); ++y)
|
||||
for (int x=(int)floorf(left); x<(int)ceilf(left+width); ++x)
|
||||
sample(dst,x,y,t,(x+0.5f-left)/width,(y+0.5f-top)/height,255);
|
||||
}
|
||||
static void stars(uint8_t *dst,float progress)
|
||||
{
|
||||
static const uint8_t points[][3]={{89,62,4},{232,91,6},{75,132,5},{251,145,4},
|
||||
{119,195,5},{229,211,6},{198,40,4},{79,191,4}};
|
||||
unsigned alpha=(unsigned)(255*progress);
|
||||
for (size_t i=0;i<sizeof(points)/sizeof(points[0]);++i) {
|
||||
int radius=points[i][2];
|
||||
for (int j=-radius;j<=radius;++j) {
|
||||
unsigned a=alpha*(radius+1-abs(j))/(radius+1);
|
||||
blend(dst,points[i][0]+j,points[i][1],0xffad,a);
|
||||
if (j) blend(dst,points[i][0],points[i][1]+j,0xffad,a);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool lcd_emote_fortune_build_frame(uint8_t *dst,size_t capacity,
|
||||
const lcd_fortune_texture_t shared[LCD_FORTUNE_SHARED_COUNT],
|
||||
const lcd_fortune_texture_t *text,uint32_t time_ms)
|
||||
{
|
||||
const size_t bytes=LCD_FORTUNE_WIDTH*LCD_FORTUNE_HEIGHT*2;
|
||||
if (!dst || capacity<bytes || !shared || !text || !text->pixels ||
|
||||
!text->width || !text->height || text->width>320 || text->height>240) return false;
|
||||
for (int i=0;i<LCD_FORTUNE_SHARED_COUNT;++i)
|
||||
if (!shared[i].pixels || !shared[i].width || !shared[i].height ||
|
||||
shared[i].width>320 || shared[i].height>240) return false;
|
||||
memset(dst,0,bytes);
|
||||
if (time_ms>LCD_FORTUNE_MOTION_MS) time_ms=LCD_FORTUNE_MOTION_MS;
|
||||
float zoom=smooth(((float)time_ms-1900)/600);
|
||||
if (time_ms<600) blit(dst,&shared[LCD_FORTUNE_ROD],lcd_fortune_shared[LCD_FORTUNE_ROD].x,
|
||||
lcd_fortune_shared[LCD_FORTUNE_ROD].y,255);
|
||||
float width=60+40*zoom,height=114+76*zoom,cx=160,cy=94+25*zoom;
|
||||
if (time_ms<600) {
|
||||
cy=212-118*smooth(time_ms/600.0f);
|
||||
paw(dst,&shared[LCD_FORTUNE_BACK],60,110,0,cx,cy,false,false);
|
||||
} else if (time_ms<1200) {
|
||||
float theta=3.14159265f*smooth((time_ms-600)/600.0f), c=cosf(theta), s=sinf(theta);
|
||||
bool side=fabsf(c)<0.17f,front=c<0;
|
||||
const lcd_fortune_texture_t *t=&shared[side?LCD_FORTUNE_EDGE:front?LCD_FORTUNE_FRONT:LCD_FORTUNE_BACK];
|
||||
paw(dst,t,60,side?110:114,theta,cx+4*s,cy-3*s,side,front);
|
||||
if (front && !side) flip_question(dst,&shared[LCD_FORTUNE_QUESTION],theta,cx+4*s,cy-3*s);
|
||||
} else {
|
||||
paw(dst,&shared[LCD_FORTUNE_FRONT],width,height,3.14159265f,cx,cy,false,true);
|
||||
}
|
||||
if (time_ms>=1200 && time_ms<1600)
|
||||
overlay(dst,&shared[LCD_FORTUNE_QUESTION],cx-12,cy-8,24,44,1);
|
||||
if (time_ms>=1600) {
|
||||
float left=cx-width/2,top=cy-height/2;
|
||||
overlay(dst,text,left+width*0.26f,top+height*70/190,width*0.48f,height*104/190,
|
||||
smooth((time_ms-1600)/300.0f));
|
||||
}
|
||||
/* 下方爪子遮挡抽出时的签底;放大阶段淡出,最终背景纯黑。 */
|
||||
blit(dst,&shared[LCD_FORTUNE_PILE],lcd_fortune_shared[LCD_FORTUNE_PILE].x,
|
||||
lcd_fortune_shared[LCD_FORTUNE_PILE].y,(unsigned)(255*(1-zoom)));
|
||||
if (zoom>0) stars(dst,zoom);
|
||||
return true;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# GFX 表情显示应用与开发指南
|
||||
|
||||
本文面向本项目的开发者,说明当前 `esp_emote_gfx` 的具体应用及后续开发方法。正文使用中文,代码、路径和日志字段保留原名以便检索。适用基线为 2026 年 10 月 9 日的工作区实现;资源和性能数字随代码变化而变化。
|
||||
本文面向本项目的开发者,说明当前 `esp_emote_gfx` 的具体应用及后续开发方法。正文使用中文,代码、路径和日志字段保留原名以便检索。适用基线包含 2026 年 10 月 10 日的抽签分层动画;资源和性能数字随代码变化而变化。
|
||||
|
||||
## 一、当前方案与边界
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
- 底层为 ESP32-S3、ST7789P3、单线 SPI,当前像素时钟为 40 MHz,逻辑画布为 320×240。
|
||||
- 当前使用 `esp_emote_gfx` 3.0.5,由 `platform` 私有依赖。没有接入 `esp_emote_expression` 状态管理组件,也没有把当前表情绘制建立在 LVGL 上。依赖目录中存在其他库不等于当前显示链路使用它们。
|
||||
- 待机基础表情与 listening 采用 GFX;其他表情仍走原 PNG 或步骤序列,不是所有资产都已经迁移。listening 缓存失败时单独回退旧 PNG,不影响待机。
|
||||
- 待机基础表情、listening 与五种抽签结果采用 GFX 图层;其他表情仍走原 PNG 或步骤序列,不是所有资产都已经迁移。listening 缓存失败时单独回退旧 PNG,不影响待机。抽签只在开始画面前准备失败时回退,播放途中取消/传输失败不从头重播 PNG。
|
||||
- 屏幕目前未使用 TE 同步。软件预合成和连续传输能改善撕裂观感,但不能保证面板扫描与写入完全同步。
|
||||
- 用户已确认待机缓存/固定时间轴以及新 listening 版本的观感满意。两者最终版本尚无新的配套量化日志,不能写成“稳定达到 30 FPS”或“全部实机验收完成”。
|
||||
- 用户已确认待机缓存/固定时间轴以及新 listening 版本的观感满意。2026-10-10 启动样本中的待机眨眼约 410 ms、步骤约 29.26/s、实际眼型更新约 21.95/s;不是 listening/抽签帧率或全负载验证,不能写成“全部实机验收完成”。
|
||||
|
||||
已安装组件提供图片、网格图片、运动、动画、标签、二维码和按钮等接口。但本项目仅实际使用图片、网格、显示刷新及相关对象控制;通用中文文本、按钮、触摸和复杂布局不属于当前已实现能力。
|
||||
|
||||
@@ -63,6 +63,11 @@ esp_err_t err = domain_runtime_request_expression(&request);
|
||||
| `components/platform/internal/lcd_emote_listening.h` | 平台私有插值接口,不向领域或控制层泄漏 GFX 类型 |
|
||||
| `tools/prepare_listening_expression.py` | 拆分旧 listening、编码往返校验、生成四个 C 资产及同算法动画预览 |
|
||||
| `tools/test_lcd_listening_gfx.py` | 实际资产、全闭合参数、缓存分配失败、GFX 对象失败、静态像素恢复与切换回归 |
|
||||
| `components/platform/src/lcd_emote_fortune.c` | 抽签 LZ4 有界解码、轻透视纹理光栅、签文展开、放大与星光 |
|
||||
| `components/platform/internal/lcd_emote_fortune.h` | 抽签私有图层/纹理/固定窗口契约 |
|
||||
| `components/platform/src/generated/emote_gfx/fortune_assets.c` | 六个共享图层、五种签文 LZ4 压缩载荷 |
|
||||
| `tools/prepare_fortune_expression.py` / `tools/preview_fortune_expression.py` | 拆分/压缩与实际 C 算法预览;均不进设备固件 |
|
||||
| `tools/test_lcd_fortune_gfx.py` | 五签、损坏输入、几何边界、时间轴、取消和释放回归 |
|
||||
|
||||
`/tmp` 下修改过程中生成的补丁脚本不是工程运行依赖,不应提交或作为正式资源流程使用。
|
||||
|
||||
@@ -178,6 +183,7 @@ RLE(游程长度编码)把连续相同像素写成“数量+像素值”
|
||||
| --- | --- | --- | --- |
|
||||
| 普通 PNG 与 GFX 失败后备 | `lcd_source/*.png` | `lcd_optimized/*.png` 经素材包链接入应用 | 原 PNG 解码/缓存链路 |
|
||||
| 新待机/listening GFX | 原 PNG 与拆分图层 PNG | 六个实际参与编译的 RLE C 资产 | 初始化解压,生成 PSRAM 眼型缓存,之后切换缓存 |
|
||||
| 五种抽签 GFX | 抽签源 PNG 与 `lcd_layers/chouqian` 拆分层 | 十一图层的 LZ4 block C 数组 | 动作开始解压六个共享层及所选签文,实时局部纹理光栅,结束/取消解绑后释放 |
|
||||
| 离线预览 | 图层与实际插值算法 | 不入固件 | 在电脑上检查造型,不能模拟 LCD 扫描 |
|
||||
|
||||
RLE 的编码往返对转换后的 RGB565+A8 数据无损,不等于原 PNG 的所有 RGB888
|
||||
@@ -195,6 +201,7 @@ components/platform/assets/
|
||||
lcd_layers/
|
||||
jichubiaoqing/ 待机可编辑拆分图层、布局及预览
|
||||
listening/ 聆听可编辑拆分图层、布局及预览
|
||||
chouqian/ 抽签共享图层、五种原签文及布局
|
||||
components/platform/src/generated/emote_gfx/
|
||||
idle_base.c 实际编译的底图压缩数据
|
||||
idle_eyes_open.c 实际编译的睁眼压缩数据
|
||||
@@ -202,6 +209,7 @@ components/platform/src/generated/emote_gfx/
|
||||
listening_eyes_open.c 实际编译的聆听睁眼关键图
|
||||
listening_eyes_half.c 实际编译的聆听半闭关键图
|
||||
listening_eyes_closed.c 实际编译的聆听闭眼关键图
|
||||
fortune_assets.c 实际编译的抽签 LZ4;不是 RLE
|
||||
```
|
||||
|
||||
半闭眼、闭眼源图及对应历史生成文件不代表当前参与编译;以构建列表为准,不把目录里的全部文件自动加入固件,也不擅自删除旧资源。
|
||||
@@ -290,7 +298,7 @@ PSRAM 外部内存充足当成内部 DMA 安全。当前 listening 的已接受
|
||||
|
||||
## 八、性能日志如何读
|
||||
|
||||
通过 `menuconfig` 搜索 `TQ_LCD_EMOTE_GFX_TEST` 和 `TQ_LCD_EMOTE_FPS_LOG`。前者控制 GFX 待机及 listening 路径;后者控制性能统计,默认关闭,当前本地测量配置开启。关闭统计不应改变动画节奏。
|
||||
通过 `menuconfig` 搜索 `TQ_LCD_EMOTE_GFX_TEST` 和 `TQ_LCD_EMOTE_FPS_LOG`。前者控制 GFX 待机、listening 及抽签路径;后者控制性能统计,默认关闭,当前本地测量配置开启。关闭统计不应改变动画节奏。抽签使用 `fortune_fps`,只计算运动段、不含最后停留,详见抽签开发说明。
|
||||
|
||||
每轮完整眨眼输出一次 `emote_fps` 和 `emote_cost`,不在逐帧传输中打印:
|
||||
|
||||
@@ -366,7 +374,25 @@ python3 tools/test_lcd_listening_gfx.py
|
||||
固定窗口/锚点变化需同步脚本与测试。需要手工保留眉毛等素材编辑时,不要直接重新
|
||||
拆分覆盖;当前生成脚本不是所有角色通用的热替换工具。
|
||||
|
||||
### 接入更多表情
|
||||
### 已接入的五种抽签结果(2026-10-10)
|
||||
|
||||
统一抽出、轻透视翻面、问号、签文展开、放大/闪光与回待机。领域仍选择旧六步
|
||||
序列和所选结果,平台只对完整匹配的资产序列优化;其他步骤序列不受影响。
|
||||
纹理光栅由平台私有 C 完成,GFX 使用普通图片承接局部合成及 HUD,不是完整
|
||||
三维引擎或 LVGL 动画。六图层共享,一次只载入一份签文,不建立五套缓存。
|
||||
旧十八张 PNG 完整保留。详见 [抽签开发说明](抽签分层动画开发说明.zh-CN.md)。
|
||||
|
||||
新载荷 43,547 B,应用实际净增 51,856 B(本次构建 3,157,728 B);运动期
|
||||
像素 PSRAM 278,249 B,加场景描述 148 B,正常结束/取消释放;临时图片对象
|
||||
普通堆请求 96 B(不含分配器开销);静态可写 RAM 增量 4 B,无新 DMA/任务。
|
||||
动画前分配失败后备 PNG;已开始后不因错误重播 PNG;解绑异常保留绑定资源
|
||||
直到重启,不能假装完整释放。不是新格式一定最省 Flash,比较详见资产台账。
|
||||
|
||||
本次主机测试、ESP-IDF 5.5.4、日志/GFX 关闭编译及应用烧录校验通过。
|
||||
启动日志确认待机/listening 缓存成功,短时显示日志无传输/锁/崩溃错误;
|
||||
尚未捕获实机抽签播放日志,不宣称已测得抽签 FPS 或五签观感已验收。
|
||||
|
||||
### 接入更多表情(逐个评估)
|
||||
|
||||
先逐个迁移,保留未迁移 PNG 路径。每个表情准备静态层、动态图层、锚点、时间线、缓存预算及结束行为。固定短动作适合预生成缓存;大量不同动作或连续交互需要考虑按需生成、共享缓存和生命周期,不应给每个表情无上限常驻一份整屏缓存。
|
||||
|
||||
@@ -397,6 +423,7 @@ python3 tools/test_lcd_status_hud.py
|
||||
python3 tools/test_lcd_emote_transport.py
|
||||
python3 tools/test_lcd_emote_timeline.py
|
||||
python3 tools/test_lcd_listening_gfx.py
|
||||
python3 tools/test_lcd_fortune_gfx.py
|
||||
```
|
||||
|
||||
这些脚本在电脑上从实际源码提取 C 函数,使用 Clang 与内存、未定义行为检查器。需要 Python 和 Clang;不是固件代码,不占设备 Flash 或 RAM。测试使用模拟接口,不能证明屏幕无撕裂、实时帧率或设备像素完全一致。
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# 抽签分层动画开发说明
|
||||
|
||||
## 当前实现
|
||||
|
||||
2026-10-10 将五种结果接入同一动作:卜运签(保留原素材中的“补运签”字形)、
|
||||
和平签、上上签、小吉签、中上签。领域结果目录及云端结果索引未改变,不在平台
|
||||
重新随机,不改抽签请求、语音或联网流程。
|
||||
|
||||
领域继续下发原六步:前三步 `chouqian_qianzhi` 的 0/1/2,后三步所选结果
|
||||
的 0/1/2,间隔 500/500/500/500/500/1500 ms。平台只有在完整匹配六步、
|
||||
无附加图标、原间隔及 320×240、GFX 就绪时使用新动画。修改步骤/间隔会回到
|
||||
普通 PNG,不静默覆盖通用步骤序列。
|
||||
|
||||
| 时间 | 动作 |
|
||||
| --- | --- |
|
||||
| 0~600 ms | 背面爪签从下方爪堆中抽出;爪堆遮挡签底,保留钓线/鱼 |
|
||||
| 600~1200 ms | 轻透视翻面;背面缩窄,近 90° 用侧面厚度关键层,正面展开 |
|
||||
| 1200~1600 ms | 正面问号保持;问号在翻面后半段已经随正面透视展开 |
|
||||
| 1600~1900 ms | 所选原签文自上往下展开,不使用完整图片交叉淡化 |
|
||||
| 1900~2500 ms | 爪签连续放大,爪堆淡出,程序金色星光出现 |
|
||||
| 运动末帧后 1500 ms | 保持结果,变化的时间/电池可刷新,最后正常完成并守卫回待机 |
|
||||
|
||||
名义总长约 4 秒;若运动因负载迟到,仍保留完整 1500 ms 末帧阅读时间。
|
||||
共同起点固定时间轴,目标运动步间隔 34 ms,75 张绘制(含起始与末端),不
|
||||
画完再等 34 ms、不跳动作、不忙等。实际调度可迟到;目标不是实测 FPS。
|
||||
取消以 20 ms 等待片轮询加当前绘制/传输时间为边界,不视为成功完成。
|
||||
保留统一待机 guard,旧动画完成不能覆盖新 pending 请求。
|
||||
|
||||
## 图层和渲染
|
||||
|
||||
六个共享层:下方爪堆、背面、侧面、空白正面、问号、钓线/鱼;另有五个原签文。
|
||||
保留源图,背景抖动杂点归零,清除背面裁剪内的钓线,将正面原签文区恢复签底,
|
||||
保留肉垫与外轮廓;程序星光代替分散的原星光。属于小幅性能适配,并非原 PNG
|
||||
逐像素不变。签文轮廓/颜色来自旧素材,不使用另加中文字体。
|
||||
|
||||
`lcd_emote_fortune.c` 在 PSRAM 的 `(56,20,208,220)` 局部不透明 RGB565 窗口
|
||||
完成光栅:16 条纹理带近似透视、前后近远缩放、微小姿态变化;仅近侧面时使用
|
||||
厚度层,避免半翻面时浮出一条不属于轮廓的金线。正面 UV 补偿反向,文字不镜像。
|
||||
双线性采样在预乘 Alpha 空间进行,边缘不会把透明黑混出暗边。
|
||||
|
||||
每步完整恢复同一局部窗口并画新轮廓,防止旧轮廓残影。图片位置/尺寸固定,
|
||||
无需以错误的“只更新新轮廓”留下旧像素。钓线在窗口外部分借用现有背景对象,
|
||||
翻面开始只隐藏一次;不逐帧让不变的钓线失效。窗口顶部覆盖放大爪尖但避开
|
||||
左上时间和右上电池;源图/几何变化需同步边界及预览。
|
||||
|
||||
GFX 负责普通图片图层、共享 HUD、区域合成及既有安全传输,不是使用 GFX
|
||||
三维引擎,也没有 LVGL GUI。复用 153,600 B GFX 合成缓冲、10,240 B DMA
|
||||
暂存区、ISR 完成等待和故障锁存;新增 91,520 B 是局部光栅缓冲,不是第二个
|
||||
整屏换帧缓冲。屏幕无 TE,不能承诺完全无撕裂。
|
||||
|
||||
## 制作与测试
|
||||
|
||||
```text
|
||||
components/platform/assets/lcd_source/chouqian_*.png 原十八张 PNG,保留
|
||||
components/platform/assets/lcd_optimized/chouqian_*.png 旧 PNG 后备,仍入包
|
||||
components/platform/assets/lcd_layers/chouqian/ 六共享图层、五签文、layout
|
||||
components/platform/src/generated/emote_gfx/fortune_assets.c 实际编译压缩资产
|
||||
components/platform/internal/lcd_emote_fortune.h 平台私有描述
|
||||
components/platform/src/lcd_emote_fortune.c 解码与几何
|
||||
components/platform/src/lcd_direct.c 匹配、准备、播放、解绑、测量
|
||||
```
|
||||
|
||||
生成文件不要手改。以下需要 Pillow 和 Clang,生成会覆盖本次拆分图层及 C,
|
||||
先保存手工美术修改;旧十八张源/优化 PNG 不改,不把拆分 PNG 重复入资源包。
|
||||
|
||||
```bash
|
||||
python3 tools/prepare_fortune_expression.py
|
||||
python3 tools/prepare_fortune_expression.py --check
|
||||
python3 tools/test_lcd_fortune_gfx.py
|
||||
python3 tools/preview_fortune_expression.py --output /tmp/lucky-fortune-preview --result 2
|
||||
```
|
||||
|
||||
新压缩编码为 LZ4 block:字面量加 16-bit 回指,用有界、自包含解码器直接恢复
|
||||
RGB565 高字节在前+独立 A8,不引入新解码任务/库,不在每帧解压。解码拒绝
|
||||
越界回指、截断和输出长度错误。与旧四字节像素 RLE 不能互换。往返相对转换后
|
||||
的 RGB565+A8 无损;转换/拆分并非 RGB888 原画无损。Alpha 全平面校验保留。
|
||||
|
||||
主机测试实际 C 解码和几何,五种结果共 1180 个采样时刻、空间不足/损坏输入、
|
||||
九次分配失败、75 个取消点、4 秒时间轴、解绑/传输失败;日志开/关均覆盖。
|
||||
预览也调用实际 C,另比较 C 解压与图层 PNG 转换后的像素及 Alpha。
|
||||
仍需执行原四个显示回归和 IDF 5.5.4,并对 LCD 源文件验证日志/GFX 关闭条件编译。
|
||||
|
||||
替换资源不是只改 PNG:当前拆分坐标、尺寸、问号、原签文区域、锚点、放大
|
||||
比例和窗口耦合。要调整生成器与几何,并用实际 C 预览检查翻面镜像、爪尖/签底
|
||||
裁剪、字形缺笔、Alpha 暗边、HUD 遮挡及返回待机。
|
||||
|
||||
## 资源与生命周期
|
||||
|
||||
新载荷 43,547 B,十一张优化图层 PNG 37,046 B,原十八 PNG 372,012 B、
|
||||
旧优化十八 PNG 136,226 B。不是相同图层数量的无损压缩比例对比;新方案
|
||||
只保留少量关键素材而实时计算过渡。新 LZ4 比其十一 PNG 大 6,501 B,选择
|
||||
它是为有界直接解压与减少 PNG 解码暂存,不是保证最小 Flash。
|
||||
|
||||
| 新增项 | 数值 |
|
||||
| --- | ---: |
|
||||
| 十一图层原始 RGB565+A8 | 246,633 B(含五签文,不全解压到运行内存) |
|
||||
| 编译资产对象只读段(含描述/目录名) | 43,831 B;data/BSS 0 |
|
||||
| 应用镜像 | 3,157,728 B,较上一提交 3,105,872 B 净增 51,856 B |
|
||||
| 单 OTA 槽余量 | 1,560,864 B |
|
||||
| 六共享图层解压像素 | 171,753 B |
|
||||
| 本次所选一份签文 | 14,976 B |
|
||||
| 局部光栅缓冲 | 91,520 B |
|
||||
| 播放期像素 PSRAM 峰值 | 278,249 B |
|
||||
| 场景状态/描述 | 148 B PSRAM;峰值明确请求合计 278,397 B |
|
||||
| 临时 GFX 图片对象/状态/链节点 | 80+8+8=96 B 普通堆,不含管理开销 |
|
||||
| 静态可写数据 | LCD 文件增加 4 B 指针 |
|
||||
| 新内部/DMA 暂存区、任务 | 0 |
|
||||
| 正常完成/取消后新增 PSRAM/对象常驻 | 0,解绑释放 |
|
||||
|
||||
数据来自目标编译器及本次构建,不包含分配器管理、局部栈和绘制临时开销。
|
||||
旧 PNG 后备保留,包仍是 820,125 B、75 PNG/32 Opus,所以完整固件不会按
|
||||
新旧素材差额减少。成本在本次提出的额外 Flash 80 KiB、像素 PSRAM 300 KiB
|
||||
预算内;不要将此预算自动授权给后续更大缓存或新内部/DMA 缓冲。
|
||||
|
||||
播放准备失败释放部分成果;创建/绑定失败且安全解绑后才回退 PNG;开始绘制
|
||||
后取消/失败不从头重播。每次动作只解压六共享层与一签文、申请一局部缓冲和
|
||||
一个图片对象,正常退出持同任务 GFX 锁,背景重新绑定待机,再删除临时对象,
|
||||
释放像素/场景。若锁/解绑异常,保留已绑定资源到重启,禁止再次创建该场景,
|
||||
不能宣称此异常下已完成释放;既有强制删任务异常路径也不作为正常释放保证。
|
||||
|
||||
## 设备测试与日志
|
||||
|
||||
工厂 UART0 增加仅显示的测试命令,不调用云端,不写 NVS:
|
||||
|
||||
```text
|
||||
lcd_fortune_test 0
|
||||
lcd_fortune_test 1
|
||||
lcd_fortune_test 2
|
||||
lcd_fortune_test 3
|
||||
lcd_fortune_test 4
|
||||
```
|
||||
|
||||
每条结束后等至少 4 秒再发下一条;新命令会中断当前画面,不能当排队命令。
|
||||
0~4 映射与上面顺序一致;仅接受单字符索引,缺失/越界/多参数拒绝。
|
||||
`OK` 仅代表请求入队,完成/显示仍以日志及屏幕为准。当前 USB Serial/JTAG
|
||||
是次级日志输出,CLI 读取 UART0,不能假设向 USB 日志口发送命令就会被执行。
|
||||
正常抽签仍可通过既有 SW2/云端流程触发,不需要该测试命令。
|
||||
|
||||
开启 `CONFIG_TQ_LCD_EMOTE_FPS_LOG` 时每次运动段输出 `fortune_fps`:
|
||||
`frames` 仅计成功绘制/传输;`motion_us` 为实际运动墙钟,排除最后保持;
|
||||
`update_fps` 使用相同时间分母;`draw_avg_us/max_us` 含锁、光栅、GFX、DMA
|
||||
复制及传输等待,平均分母是绘制尝试数(包括失败尝试);`late_frames` 是绘制
|
||||
达到 34 ms 的次数,不是面板丢帧。
|
||||
这些是任务墙钟,不能当纯 CPU,也不等于面板扫描率。开始日志另有解压/准备
|
||||
时间、像素 PSRAM 及场景描述字节。
|
||||
|
||||
本次五签主机/条件编译/IDF 5.5.4 构建通过;当前设备 ota_0 有效,经读取 OTA
|
||||
选择区确认后只用 app-flash 写 ota_0 应用,镜像校验成功,NVS/模型/选择区未改。
|
||||
启动采样 25 秒:最终应用 SHA 前缀 06e65e4a0、待机/listening 缓存成功,采样内
|
||||
无显示锁/传输/崩溃错误。未捕获抽签运动日志,未完成五签 LCD 观感及并行负载
|
||||
验收,不写成实测稳定 30 FPS。启动期间的内存最低值不能代表抽签运行期最低值。
|
||||
@@ -419,7 +419,57 @@ PNG 包仍为 820,125 B、75 PNG/32 Opus;只读资产已包含于整个应用
|
||||
内部/DMA 专用缓冲及静态可写数据均为 0;复用既有 HUD 和行缓冲。
|
||||
局部栈、CPU 耗时和设备最低内存尚未新增实机测量,不应写成整体资源零成本。
|
||||
|
||||
## 维护与后续清理
|
||||
## 新增抽签分层资产(2026-10-10)
|
||||
|
||||
以下新增项不合并进上面的历史目录汇总。旧抽签十八张源 PNG 共 372,012 B、
|
||||
优化 PNG 共 136,226 B,全部保留且后备仍入包。PNG 包仍 820,125 B,不重复
|
||||
加入本次拆分层。生成器使用优化 PNG 保存拆分层,但实际编译 LZ4,不是 RLE。
|
||||
|
||||
图层目录:`components/platform/assets/lcd_layers/chouqian/`。
|
||||
|
||||
| 图层 | 尺寸 | 优化 PNG B | 本机分配 B | RGB565+A8 B | LZ4 载荷 B |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| `pile.png` | 208×76 | 9,594 | 12,288 | 47,424 | 9,417 |
|
||||
| `back.png` | 63×113 | 2,999 | 4,096 | 21,357 | 4,179 |
|
||||
| `edge.png` | 41×110 | 2,957 | 4,096 | 13,530 | 4,389 |
|
||||
| `front.png` | 100×190 | 7,579 | 8,192 | 57,000 | 10,774 |
|
||||
| `question.png` | 27×50 | 730 | 4,096 | 4,050 | 817 |
|
||||
| `rod.png` | 169×56 | 1,584 | 4,096 | 28,392 | 2,094 |
|
||||
| `text_buyun.png` | 48×104 | 2,502 | 4,096 | 14,976 | 2,638 |
|
||||
| `text_heping.png` | 48×104 | 2,784 | 4,096 | 14,976 | 2,851 |
|
||||
| `text_shangshang.png` | 48×104 | 2,132 | 4,096 | 14,976 | 2,069 |
|
||||
| `text_xiaoji.png` | 48×104 | 2,091 | 4,096 | 14,976 | 2,210 |
|
||||
| `text_zhongshang.png` | 48×104 | 2,094 | 4,096 | 14,976 | 2,109 |
|
||||
| 合计 | 十一图层 | 37,046 | 57,344 | 246,633 | 43,547 |
|
||||
|
||||
`layout.json` 1,929 B、分配 4,096 B,不入固件;目录共十二文件 38,975 B、
|
||||
分配 61,440 B。预览工具默认输出 `/tmp/lucky-fortune-preview`,不是编译资产。
|
||||
源十八 PNG 与本次拆分层不是相同数量的无损比较;背景/几何做了小幅性能适配。
|
||||
LZ4 对转换后的 RGB565+A8 无损且完整保留 Alpha,但比拆分 PNG 多 6,501 B,
|
||||
不能称为压得最小。生成器及实际 C 解码均做往返校验,原旧 PNG 不覆盖。
|
||||
|
||||
生成 C:`components/platform/src/generated/emote_gfx/fortune_assets.c`,文本
|
||||
270,251 B、分配 270,336 B;目标对象只读段总计 43,831 B(载荷、176 B 资产
|
||||
描述、20 B 目录指针、88 B 目录名),可写 data/BSS 为 0。生成 C 不手改。
|
||||
|
||||
本次完整应用 3,157,728 B,较 HUD 提交 3,105,872 B 增加 51,856 B,包含
|
||||
资产、几何/解码、播放与 UART 显示测试命令的代码及对齐;单 OTA 槽余量
|
||||
1,560,864 B。所有只读资源已经包含在应用里,不能再相加。
|
||||
|
||||
六共享层解压 171,753 B、一次仅选一份签文 14,976 B、208×220 RGB565
|
||||
局部光栅缓冲 91,520 B,像素峰值合计 278,249 B PSRAM;目标编译器场景
|
||||
描述 148 B 也在 PSRAM,明确申请峰值 278,397 B,不含堆管理。
|
||||
正常结束/取消后释放,新增常驻像素为 0。临时 GFX 图片对象/状态/链节点
|
||||
普通堆请求合计 96 B,不含分配器;LCD 静态可写数据增加 4 B 指针,
|
||||
无新增内部/DMA 缓冲、合成整屏缓冲或任务;局部栈/CPU/实际最低余量另测。
|
||||
解绑故障时保留绑定资源到重启,不冒充完整释放。
|
||||
|
||||
五签主机回归、IDF 5.5.4、日志关闭/GFX 关闭编译与应用烧录校验通过。
|
||||
设备启动确认缓存成功、25 秒采样无显示锁/传输/崩溃错误;未捕获抽签运动
|
||||
日志,FPS/五签观感/并行负载最低内存仍待实机验收。详见
|
||||
[抽签开发说明](抽签分层动画开发说明.zh-CN.md)。
|
||||
|
||||
## 维护与后续清理(通用)
|
||||
|
||||
1. PNG 新增/替换按 [图片优化流程](lcd-image-asset-optimization.zh-CN.md) 生成并验证,
|
||||
不能编辑优化产物代替修改源图。GFX 新增/替换使用对应压缩脚本和编码往返测试。
|
||||
|
||||
@@ -639,7 +639,7 @@ menu "TQ Cat Configuration"
|
||||
Stack size in bytes for the control-plane LCD render worker.
|
||||
|
||||
config TQ_LCD_EMOTE_GFX_TEST
|
||||
bool "Use esp_emote_gfx for idle and listening expressions (experimental)"
|
||||
bool "Use esp_emote_gfx for idle, listening and fortune animations (experimental)"
|
||||
default n
|
||||
depends on TQ_SCREEN_ENABLE
|
||||
help
|
||||
@@ -647,18 +647,21 @@ menu "TQ Cat Configuration"
|
||||
RGB565A8 eye layer. Only the eye region is refreshed while
|
||||
blinking. Listening uses three compressed key shapes to generate
|
||||
a local eye-frame cache once. Both expressions share the display,
|
||||
DMA staging and clock/battery overlay. Other presentations use
|
||||
DMA staging and clock/battery overlay. Fortune sequences share
|
||||
layered paw textures and five original result texts, decoded
|
||||
only for one playback and freed afterward. Other presentations use
|
||||
the existing PNG renderer; listening falls back to PNG if its
|
||||
cache allocation fails.
|
||||
|
||||
config TQ_LCD_EMOTE_FPS_LOG
|
||||
bool "Log measured GFX blink FPS"
|
||||
bool "Log measured GFX animation FPS"
|
||||
depends on TQ_LCD_EMOTE_GFX_TEST
|
||||
default n
|
||||
help
|
||||
Log once per completed blink: measured step FPS, actual eye
|
||||
update FPS, draw time and deadline overruns. Excludes the idle
|
||||
open-eye pause. This does not measure panel scan rate.
|
||||
open-eye pause. Fortune logs once per motion segment, excluding
|
||||
the final hold. This does not measure panel scan rate.
|
||||
|
||||
config TQ_SCREEN_COLOR_ORDER_BGR
|
||||
bool "Use BGR color order"
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从保留的抽签源 PNG 拆分共享图层、五个签文,生成 LZ4;--check 不写文件。
|
||||
|
||||
会覆盖 lcd_layers/chouqian 和生成 C;先保存手工美术编辑。背景杂点归零、
|
||||
清除背面残留钓线、抽取并共享正面;RGB565+A8 编码往返无损不等于拆分原画无损。
|
||||
"""
|
||||
import argparse
|
||||
from collections import deque
|
||||
import json
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from compress_idle_expression import pixels565
|
||||
|
||||
def compress(raw):
|
||||
"""LZ4 block:字面量和 16-bit 回指;编码器自包含,不下载库。"""
|
||||
out=bytearray();table={};anchor=pos=0
|
||||
def length(value):
|
||||
while value>=255: out.append(255);value-=255
|
||||
out.append(value)
|
||||
while pos+12<=len(raw):
|
||||
key=raw[pos:pos+4];previous=table.get(key);table[key]=pos
|
||||
if previous is None or pos-previous>65535:
|
||||
pos+=1;continue
|
||||
size=4
|
||||
while pos+size<len(raw)-5 and raw[previous+size]==raw[pos+size]: size+=1
|
||||
literals=pos-anchor;match=size-4
|
||||
out.append((min(literals,15)<<4)|min(match,15))
|
||||
if literals>=15: length(literals-15)
|
||||
out.extend(raw[anchor:pos]);offset=pos-previous;out.extend((offset&255,offset>>8))
|
||||
if match>=15: length(match-15)
|
||||
end=pos+size
|
||||
for p in range(pos+1,end):
|
||||
if p+4<=len(raw):table[raw[p:p+4]]=p
|
||||
pos=anchor=end
|
||||
literals=len(raw)-anchor;out.append(min(literals,15)<<4)
|
||||
if literals>=15:length(literals-15)
|
||||
out.extend(raw[anchor:])
|
||||
assert decompress(out)==raw
|
||||
return out
|
||||
|
||||
def decompress(data):
|
||||
out=bytearray();pos=0
|
||||
def length(value):
|
||||
nonlocal pos
|
||||
if value==15:
|
||||
while True:
|
||||
v=data[pos];pos+=1;value+=v
|
||||
if v!=255:break
|
||||
return value
|
||||
while pos<len(data):
|
||||
token=data[pos];pos+=1;literals=length(token>>4)
|
||||
out.extend(data[pos:pos+literals]);pos+=literals
|
||||
if pos==len(data):break
|
||||
offset=data[pos]|data[pos+1]<<8;pos+=2;match=length(token&15)+4
|
||||
assert 0<offset<=len(out)
|
||||
for _ in range(match):out.append(out[-offset])
|
||||
return bytes(out)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "components/platform/assets/lcd_source"
|
||||
LAYERS = ROOT / "components/platform/assets/lcd_layers/chouqian"
|
||||
GENERATED = ROOT / "components/platform/src/generated/emote_gfx/fortune_assets.c"
|
||||
RESULTS = ("buyun", "heping", "shangshang", "xiaoji", "zhongshang")
|
||||
|
||||
def clean(name, box, largest=False):
|
||||
image = Image.open(SOURCE / name).convert("RGBA").crop(box)
|
||||
pixels = list(image.getdata())
|
||||
candidates = {(i % image.width, i // image.width) for i, (r,g,b,a) in enumerate(pixels)
|
||||
if max(r,g,b)>35 and a}
|
||||
components=[]
|
||||
while candidates:
|
||||
seed=candidates.pop(); queue=deque([seed]); group={seed}
|
||||
while queue:
|
||||
x,y=queue.popleft()
|
||||
for nx in range(max(0,x-1),min(image.width,x+2)):
|
||||
for ny in range(max(0,y-1),min(image.height,y+2)):
|
||||
if (nx,ny) in candidates:
|
||||
candidates.remove((nx,ny));group.add((nx,ny));queue.append((nx,ny))
|
||||
if len(group)>=8: components.append(group)
|
||||
selected=max(components,key=len) if largest else set().union(*components)
|
||||
image.putdata([(r,g,b,a) if (i%image.width,i//image.width) in selected else (0,0,0,0)
|
||||
for i,(r,g,b,a) in enumerate(pixels)])
|
||||
return image
|
||||
|
||||
def text_layer(name):
|
||||
image=Image.open(SOURCE/f"chouqian_{name}2.png").convert("RGBA").crop((136,94,184,198))
|
||||
# 只保留原签文橙红笔画及边缘,不把黄色签底一起叠加。
|
||||
image.putdata([(r,g,b,a) if r>150 and g<175 and b<100 and r>g*1.35 else (0,0,0,0)
|
||||
for r,g,b,a in image.getdata()])
|
||||
assert image.getbbox(), name
|
||||
return image
|
||||
|
||||
def layers():
|
||||
pile=clean("chouqian_qianzhi1.png",(56,164,264,240))
|
||||
back=clean("chouqian_qianzhi1.png",(130,37,193,150),True)
|
||||
for y in range(18,32):
|
||||
for x in range(8):
|
||||
r,g,b,a=back.getpixel((x,y))
|
||||
if r>g*1.3 and g>b*1.5: back.putpixel((x,y),(0,0,0,0))
|
||||
edge=clean("chouqian_qianzhi2.png",(139,38,180,148),True)
|
||||
front=clean("chouqian_shangshang2.png",(110,24,210,214),True)
|
||||
# 原签文所在内部签底做逐行双色插值;边框、肉垫、外轮廓保持原图。
|
||||
for y in range(70,174):
|
||||
l=front.getpixel((26,y));r=front.getpixel((73,y))
|
||||
assert l[3] and r[3], y
|
||||
for x in range(26,74):
|
||||
f=(x-26)/47
|
||||
front.putpixel((x,y),tuple(round(l[c]*(1-f)+r[c]*f) for c in range(3))+(255,))
|
||||
question=Image.open(SOURCE/"chouqian_shangshang0.png").convert("RGBA").crop((147,83,174,133))
|
||||
question.putdata([(r,g,b,a) if r>150 and g<215 and b<170 and r>g*1.10 else (0,0,0,0)
|
||||
for r,g,b,a in question.getdata()])
|
||||
assert question.getbbox()
|
||||
rod=clean("chouqian_qianzhi0.png",(0,40,169,96))
|
||||
return [("pile",pile,56,164),("back",back,0,0),("edge",edge,0,0),
|
||||
("front",front,0,0),("question",question,0,0),("rod",rod,0,40)] + [
|
||||
("text_"+name,text_layer(name),0,0) for name in RESULTS]
|
||||
|
||||
def main():
|
||||
args=argparse.ArgumentParser();args.add_argument("--check",action="store_true");args=args.parse_args()
|
||||
items=layers(); out=['/* Generated by tools/prepare_fortune_expression.py; do not edit. */',
|
||||
'#include "lcd_emote_fortune.h"','']
|
||||
entries=[]; report=[]
|
||||
if not args.check: LAYERS.mkdir(parents=True,exist_ok=True)
|
||||
for name,image,x,y in items:
|
||||
pixels=pixels565(image)
|
||||
raw=bytes(v for hi,lo,a in pixels for v in (hi,lo))+bytes(a for hi,lo,a in pixels)
|
||||
data=compress(raw) # 对 RGB565 高低字节与独立 A8 平面逐字节往返检查。
|
||||
out += [f'static const uint8_t fortune_{name}[] = {{']
|
||||
out += [' '+', '.join(f'0x{v:02x}' for v in data[i:i+24])+',' for i in range(0,len(data),24)]
|
||||
out += ['};','']
|
||||
entries.append(f' {{{image.width}, {image.height}, {x}, {y}, sizeof(fortune_{name}), fortune_{name}}},')
|
||||
report.append(dict(name=name,width=image.width,height=image.height,x=x,y=y,
|
||||
raw_bytes=image.width*image.height*3,lz4_bytes=len(data)))
|
||||
if args.check:
|
||||
with Image.open(LAYERS/f"{name}.png") as old:
|
||||
assert old.convert("RGBA").tobytes()==image.tobytes(), name
|
||||
else: image.save(LAYERS/f"{name}.png",optimize=True)
|
||||
out += ['const lcd_fortune_asset_t lcd_fortune_shared[LCD_FORTUNE_SHARED_COUNT] = {',*entries[:6],'};',
|
||||
'const lcd_fortune_asset_t lcd_fortune_text[LCD_FORTUNE_RESULT_COUNT] = {',*entries[6:],'};',
|
||||
'const char *const lcd_fortune_result_dirs[LCD_FORTUNE_RESULT_COUNT] = {',
|
||||
' '+', '.join('"chouqian_'+s+'"' for s in RESULTS)+',','};','']
|
||||
source='\n'.join(out)
|
||||
metadata=json.dumps(dict(canvas=[320,240],window=[56,20,208,220],layers=report,
|
||||
note="黑背景归零;共享爪子,签文字形来自原图;旧 PNG 保留"),ensure_ascii=False,indent=2)+'\n'
|
||||
if args.check:
|
||||
assert GENERATED.read_text()==source
|
||||
assert (LAYERS/"layout.json").read_text()==metadata
|
||||
else:
|
||||
GENERATED.write_text(source);(LAYERS/"layout.json").write_text(metadata)
|
||||
total=sum(s["lz4_bytes"] for s in report)
|
||||
peak=208*220*2+sum(s["raw_bytes"] for s in report[:6])+max(s["raw_bytes"] for s in report[6:])
|
||||
assert total<70*1024, ("为代码/描述保留10 KiB预算",total)
|
||||
assert peak<300*1024,peak
|
||||
print(json.dumps(dict(layers=report,payload_bytes=total,pixel_psram_peak=peak),ensure_ascii=False,indent=2))
|
||||
|
||||
if __name__=="__main__": main()
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""调用实际固件 C 解码与光栅函数生成 GIF/五签预览;不模拟 SPI 或扫描撕裂。"""
|
||||
import argparse
|
||||
import ctypes as C
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
from PIL import Image
|
||||
from compress_idle_expression import pixels565
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
class Asset(C.Structure):
|
||||
_fields_=[("width",C.c_uint16),("height",C.c_uint16),("x",C.c_uint16),("y",C.c_uint16),
|
||||
("size",C.c_size_t),("data",C.POINTER(C.c_uint8))]
|
||||
class Texture(C.Structure):
|
||||
_fields_=[("width",C.c_uint16),("height",C.c_uint16),("pixels",C.POINTER(C.c_uint8))]
|
||||
def image565(raw,width,height):
|
||||
pixels=[]
|
||||
for i in range(0,len(raw),2):
|
||||
c=raw[i]<<8|raw[i+1];r=(c>>11)&31;g=(c>>5)&63;b=c&31
|
||||
pixels.append(((r<<3)|(r>>2),(g<<2)|(g>>4),(b<<3)|(b>>2)))
|
||||
image=Image.new("RGB",(width,height));image.putdata(pixels);return image
|
||||
def main():
|
||||
parser=argparse.ArgumentParser();parser.add_argument("--output",type=Path,default=Path("/tmp/lucky-fortune-preview"))
|
||||
parser.add_argument("--result",type=int,choices=range(5),default=2);args=parser.parse_args()
|
||||
args.output.mkdir(parents=True,exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix="fortune-preview-") as tmp:
|
||||
library=Path(tmp)/"fortune.dylib"
|
||||
subprocess.run(["clang","-std=c11","-shared","-fPIC","-O2","-I",str(ROOT/"components/platform/internal"),
|
||||
str(ROOT/"components/platform/src/lcd_emote_fortune.c"),
|
||||
str(ROOT/"components/platform/src/generated/emote_gfx/fortune_assets.c"),"-o",str(library)],check=True)
|
||||
lib=C.CDLL(str(library));shared=(Asset*6).in_dll(lib,"lcd_fortune_shared");texts=(Asset*5).in_dll(lib,"lcd_fortune_text")
|
||||
decode=lib.lcd_emote_fortune_decode;decode.argtypes=[C.POINTER(C.c_uint8),C.c_size_t,C.POINTER(C.c_uint8),C.c_size_t];decode.restype=C.c_bool
|
||||
build=lib.lcd_emote_fortune_build_frame;build.argtypes=[C.POINTER(C.c_uint8),C.c_size_t,C.POINTER(Texture),C.POINTER(Texture),C.c_uint32];build.restype=C.c_bool
|
||||
buffers=[]
|
||||
def texture(a,name):
|
||||
count=a.width*a.height;buffer=(C.c_uint8*(count*3))();buffers.append(buffer)
|
||||
assert decode(a.data,a.size,buffer,len(buffer))
|
||||
pixels=pixels565(Image.open(ROOT/"components/platform/assets/lcd_layers/chouqian"/(name+".png")))
|
||||
expected=bytes(v for hi,lo,alpha in pixels for v in (hi,lo))+bytes(alpha for hi,lo,alpha in pixels)
|
||||
assert bytes(buffer)==expected,name # 验证 C 解压与图层完整 A8 一致。
|
||||
return Texture(a.width,a.height,buffer)
|
||||
names=("pile","back","edge","front","question","rod")
|
||||
textures=(Texture*6)(*(texture(a,n) for a,n in zip(shared,names)))
|
||||
results=("buyun","heping","shangshang","xiaoji","zhongshang")
|
||||
text_textures=[texture(a,"text_"+n) for a,n in zip(texts,results)]
|
||||
dst=(C.c_uint8*(208*220*2))()
|
||||
rod=Image.open(ROOT/"components/platform/assets/lcd_layers/chouqian/rod.png").convert("RGBA")
|
||||
def frame(ms,index):
|
||||
assert build(dst,len(dst),textures,C.byref(text_textures[index]),ms)
|
||||
image=Image.new("RGB",(320,240))
|
||||
if ms<600:image.paste(rod,(0,40),rod)
|
||||
image.paste(image565(bytes(dst),208,220),(56,20));return image
|
||||
frames=[frame(t,args.result) for t in range(0,2500,40)]+[frame(2500,args.result)]
|
||||
frames[0].save(args.output/"preview.gif",save_all=True,append_images=frames[1:],duration=[40]*(len(frames)-1)+[1500],loop=0)
|
||||
sheet=Image.new("RGB",(320*5,240))
|
||||
for i in range(5):
|
||||
f=frame(2500,i);sheet.paste(f,(320*i,0));f.save(args.output/(results[i]+".png"))
|
||||
sheet.save(args.output/"results.png")
|
||||
phases=(0,600,750,900,1050,1200,1800,2200,2500)
|
||||
stages=Image.new("RGB",(320*3,240*3))
|
||||
for i,t in enumerate(phases):stages.paste(frame(t,args.result),(i%3*320,i//3*240))
|
||||
stages.save(args.output/"stages.png")
|
||||
print("PASS: actual C decode == RGB565+A8 inputs; actual C GIF, five results and motion stages:",args.output)
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""抽签实际 C 解码/几何、五种资源、分配失败/取消/释放主机回归,非设备 FPS。"""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
SRC=(ROOT/"components/platform/src/lcd_direct.c").read_text()
|
||||
def extract(name):
|
||||
match=re.search(r"static [^;{}]+\b"+name+r"\([^;{}]*\)\s*\{",SRC)
|
||||
assert match,name
|
||||
start=match.start();end=SRC.index("{",start)+1;depth=1
|
||||
while depth:
|
||||
if SRC[end]=="{":depth+=1
|
||||
if SRC[end]=="}":depth-=1
|
||||
end+=1
|
||||
return SRC[start:end]
|
||||
PREFIX=r'''
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "lcd_emote_fortune.h"
|
||||
#define ESP_OK 0
|
||||
#define ESP_ERR_INVALID_STATE 1
|
||||
#define ESP_ERR_NO_MEM 2
|
||||
#define ESP_ERR_INVALID_SIZE 3
|
||||
#define ESP_ERR_NOT_SUPPORTED 4
|
||||
#define ESP_ERR_TIMEOUT 5
|
||||
#define PLATFORM_LCD_PRESENTATION_NO_ICON 255
|
||||
#define MALLOC_CAP_SPIRAM 1
|
||||
#define MALLOC_CAP_8BIT 2
|
||||
#define GFX_COLOR_FORMAT_RGB565 0
|
||||
#define GFX_COLOR_FORMAT_RGB565A8 1
|
||||
#define C_ARRAY_HEADER_MAGIC 0
|
||||
#define ESP_LOGI(...) ((void)0)
|
||||
#define ESP_LOGE(...) ((void)0)
|
||||
#define portENTER_CRITICAL(x) ((void)0)
|
||||
#define portEXIT_CRITICAL(x) ((void)0)
|
||||
typedef int esp_err_t;
|
||||
typedef struct {const char *asset_dir;unsigned frame_index,icon_id,delay_ms;} platform_lcd_presentation_step_t;
|
||||
typedef struct {size_t step_count;platform_lcd_presentation_step_t steps[6];} lcd_direct_request_t;
|
||||
typedef struct { struct {int cf,magic,w,h;} header;size_t data_size;const uint8_t *data;} gfx_image_dsc_t;
|
||||
typedef struct {bool visible;int x,y;const gfx_image_dsc_t *src;} gfx_obj_t;
|
||||
static void *s_emote_handle=(void*)1,*s_emote_disp=(void*)1;
|
||||
static gfx_obj_t mesh,closed,cache,listening,bg,scene_obj;
|
||||
static gfx_obj_t *s_emote_eyes_open=&mesh,*s_emote_closed_eyes=&closed,*s_emote_cached_eyes=&cache;
|
||||
static gfx_obj_t *s_listening_eyes=&listening,*s_emote_background=&bg;
|
||||
static bool s_emote_active,s_emote_transfer_fault,stop;
|
||||
static int lock_depth,allocs,live,fail_alloc,fail_obj,fail_lock,fail_unlock,fail_delete;
|
||||
static int completions,waits,cancel_wait,refreshes,fail_refresh;
|
||||
static int64_t now,deadlines[100];
|
||||
static int64_t esp_timer_get_time(void){return now;}
|
||||
static bool lcd_direct_should_stop_idle(void){return stop;}
|
||||
static void *heap_caps_malloc(size_t n,int cap){assert(cap==3);if(++allocs==fail_alloc)return NULL;void *p=malloc(n);if(p)++live;return p;}
|
||||
static void *heap_caps_calloc(size_t n,size_t s,int cap){void *p=heap_caps_malloc(n*s,cap);if(p)memset(p,0,n*s);return p;}
|
||||
static void test_free(void *p){if(p)--live;free(p);}
|
||||
#define free test_free
|
||||
static int gfx_emote_lock(void *h){if(fail_lock){--fail_lock;return 5;}assert(!lock_depth);++lock_depth;return 0;}
|
||||
static int gfx_emote_unlock(void *h){assert(lock_depth==1);--lock_depth;if(fail_unlock){--fail_unlock;return 5;}return 0;}
|
||||
static gfx_obj_t *gfx_img_create(void *d){assert(lock_depth==1);return fail_obj?NULL:&scene_obj;}
|
||||
static int gfx_obj_delete(gfx_obj_t *o){assert(lock_depth==1);if(fail_delete)return 5;o->src=NULL;return 0;}
|
||||
static int gfx_img_set_src(gfx_obj_t *o,void *src){assert(lock_depth==1);o->src=src;return 0;}
|
||||
static int gfx_obj_set_pos(gfx_obj_t *o,int x,int y){assert(lock_depth==1);o->x=x;o->y=y;return 0;}
|
||||
static int gfx_obj_set_visible(gfx_obj_t *o,bool v){assert(lock_depth==1);o->visible=v;return 0;}
|
||||
static int lcd_direct_emote_update_hud(bool force,bool *change){assert(lock_depth==1);return 0;}
|
||||
static int lcd_direct_emote_select_face(bool face){assert(lock_depth==1);bg.src=NULL;return 0;}
|
||||
static void gfx_disp_refresh_all(void *d){assert(lock_depth==1);}
|
||||
static int lcd_direct_emote_refresh(void){assert(lock_depth==1 && s_emote_active);++refreshes;now+=13000;return refreshes==fail_refresh?5:0;}
|
||||
static int lcd_direct_emote_wait_until(int64_t deadline){assert(!lock_depth && waits<100);deadlines[waits++]=deadline;
|
||||
if(waits==cancel_wait){stop=true;return 1;} if(now<deadline)now=deadline;return stop?1:0;}
|
||||
static void lcd_direct_signal_completion(void){++completions;}
|
||||
static uint64_t lcd_direct_emote_fps_x100(uint32_t n,uint64_t t){return t?n*100000000ULL/t:0;}
|
||||
static const char *esp_err_to_name(int e){return "mock";}
|
||||
'''
|
||||
TEST=r'''
|
||||
static void reset(void){assert(live==0 && !lock_depth && !s_fortune_orphan);
|
||||
allocs=fail_alloc=fail_obj=fail_lock=fail_unlock=fail_delete=waits=refreshes=cancel_wait=fail_refresh=completions=0;
|
||||
now=0;stop=s_emote_active=s_emote_transfer_fault=false;}
|
||||
int main(void){
|
||||
uint8_t out[64];
|
||||
assert(!lcd_emote_fortune_decode(NULL,1,out,1));
|
||||
const uint8_t invalid[][5]={{0x10},{0x00,0,0},{0x00,1,0},{0xf0,255,255},{0x11,5}};
|
||||
for(size_t i=0;i<5;++i)assert(!lcd_emote_fortune_decode(invalid[i],sizeof(invalid[i]),out,sizeof(out)));
|
||||
for(size_t r=0;r<5;++r){
|
||||
reset();lcd_direct_fortune_scene_t *s=lcd_direct_fortune_prepare(r);assert(s && live==9);
|
||||
const size_t bytes=LCD_FORTUNE_WIDTH*LCD_FORTUNE_HEIGHT*2;
|
||||
uint8_t *guard=malloc(bytes+2);guard[0]=guard[bytes+1]=0xa5;
|
||||
for(unsigned ms=0;ms<=4000;ms+=17){
|
||||
assert(lcd_emote_fortune_build_frame(guard+1,bytes,s->shared,&s->text,ms));
|
||||
assert(guard[0]==0xa5 && guard[bytes+1]==0xa5);
|
||||
}
|
||||
assert(!lcd_emote_fortune_build_frame(guard+1,bytes-1,s->shared,&s->text,0));
|
||||
assert(!lcd_emote_fortune_build_frame(NULL,bytes,s->shared,&s->text,0));
|
||||
assert(!lcd_emote_fortune_build_frame(guard+1,bytes,NULL,&s->text,0));
|
||||
lcd_emote_fortune_build_frame(guard+1,bytes,s->shared,&s->text,4000);
|
||||
lcd_emote_fortune_build_frame(s->frame,bytes,s->shared,&s->text,2500);
|
||||
assert(!memcmp(guard+1,s->frame,bytes));
|
||||
/* 最终签文输出供回归查看;五种结果不能全都变成同一张。 */
|
||||
char path[100];snprintf(path,sizeof(path),"/tmp/lucky-fortune-%zu.rgb565",r);
|
||||
FILE *f=fopen(path,"wb");assert(f);fwrite(s->frame,1,bytes,f);fclose(f);
|
||||
/* 截断输入和不足的解压空间不允许写越界。 */
|
||||
for(size_t i=0;i<6;++i){const lcd_fortune_asset_t *a=&lcd_fortune_shared[i];size_t n=(size_t)a->width*a->height*3;
|
||||
uint8_t *bad=malloc(n+2);bad[0]=bad[n+1]=0xa5;
|
||||
assert(!lcd_emote_fortune_decode(a->data,a->compressed_size-1,bad+1,n));
|
||||
assert(!lcd_emote_fortune_decode(a->data,a->compressed_size,bad+1,n-1));
|
||||
assert(bad[0]==0xa5 && bad[n+1]==0xa5);
|
||||
/* 普通测试内存不计入设备分配追踪。 */
|
||||
++live;free(bad);
|
||||
}
|
||||
++live;free(guard);lcd_direct_fortune_free(s);assert(live==0);
|
||||
lcd_direct_request_t req={.step_count=6};
|
||||
for(size_t i=0;i<6;++i)req.steps[i]=(platform_lcd_presentation_step_t){i<3?"chouqian_qianzhi":lcd_fortune_result_dirs[r],i%3,255,i==5?1500:500};
|
||||
assert(lcd_direct_fortune_sequence_index(&req)==r);
|
||||
req.steps[4].delay_ms=100;assert(lcd_direct_fortune_sequence_index(&req)==-1);
|
||||
req.steps[4].delay_ms=500;req.steps[3].asset_dir=NULL;assert(lcd_direct_fortune_sequence_index(&req)==-1);
|
||||
reset();assert(lcd_direct_play_fortune(r)==0 && live==0 && completions==1 && !s_emote_active);
|
||||
assert(waits==75 && refreshes==75 && now==4013000 && deadlines[74]==4013000);
|
||||
for(int i=0;i<73;++i)assert(deadlines[i]==(i+1)*34000);
|
||||
assert(deadlines[73]==2500000);
|
||||
}
|
||||
for(int n=1;n<=9;++n){reset();fail_alloc=n;assert(lcd_direct_play_fortune(0)==4 && !live && !completions);}
|
||||
reset();fail_obj=1;assert(lcd_direct_play_fortune(0)==4 && !live);
|
||||
reset();fail_lock=1;assert(lcd_direct_play_fortune(0)==5 && !live);
|
||||
for(int n=1;n<=75;++n){reset();cancel_wait=n;assert(lcd_direct_play_fortune(0)==1 && !live && !completions && !s_emote_active);}
|
||||
reset();fail_refresh=3;assert(lcd_direct_play_fortune(0)==5 && !live && !completions);
|
||||
reset();fail_delete=1;assert(lcd_direct_play_fortune(0)==5 && s_fortune_orphan && live==9 && !completions);
|
||||
assert(lcd_direct_play_fortune(0)==1);fail_delete=0;
|
||||
assert(!lcd_direct_fortune_detach(s_fortune_orphan));lcd_direct_fortune_free(s_fortune_orphan);s_fortune_orphan=NULL;
|
||||
reset();s_emote_transfer_fault=true;assert(lcd_direct_play_fortune(0)==1 && !live);
|
||||
puts("PASS: five assets/results, LZ4 corruption/bounds, 1180 geometry frames, nine allocation failures, 75 cancellation points, anchored 4s playback, cleanup/transfer fault");
|
||||
}
|
||||
'''
|
||||
start=SRC.index("typedef struct {\n lcd_fortune_texture_t shared")
|
||||
end=SRC.index("static int lcd_direct_fortune_sequence_index",start)
|
||||
names=["lcd_direct_fortune_sequence_index","lcd_direct_fortune_free","lcd_direct_fortune_prepare",
|
||||
"lcd_direct_fortune_detach","lcd_direct_play_fortune"]
|
||||
with tempfile.TemporaryDirectory(prefix="fortune-test-") as tmp:
|
||||
path=Path(tmp);c=path/"test.c";exe=path/"test"
|
||||
c.write_text(PREFIX+SRC[start:end]+"\n"+"\n".join(extract(n) for n in names)+TEST)
|
||||
for profile in (0,1):
|
||||
subprocess.run(["clang","-std=c11","-O1","-g","-fsanitize=address,undefined",
|
||||
f"-DCONFIG_TQ_LCD_EMOTE_FPS_LOG={profile}","-I",str(ROOT/"components/platform/internal"),
|
||||
str(c),str(ROOT/"components/platform/src/lcd_emote_fortune.c"),
|
||||
str(ROOT/"components/platform/src/generated/emote_gfx/fortune_assets.c"),"-o",str(exe)],check=True)
|
||||
subprocess.run([str(exe)],check=True)
|
||||
assert "DOMAIN_EXPRESSION_" not in (ROOT/"components/platform/src/lcd_emote_fortune.c").read_text()
|
||||
assert "if (err != ESP_ERR_NOT_SUPPORTED) return err;" in SRC
|
||||