diff --git a/components/control_plane/src/runtime/control_plane_factory_cli.c b/components/control_plane/src/runtime/control_plane_factory_cli.c index 83416e5..1e8f805 100644 --- a/components/control_plane/src/runtime/control_plane_factory_cli.c +++ b/components/control_plane/src/runtime/control_plane_factory_cli.c @@ -577,6 +577,20 @@ static void handle_log_command(size_t argc, char *argv[]) } } +static void handle_lcd_test(void) +{ + const domain_expression_request_t request = { + .id = DOMAIN_EXPRESSION_LCD_BOUNDARY_TEST, + .trigger = DOMAIN_EXPRESSION_TRIGGER_FACTORY_LCD_TEST, + }; + esp_err_t err = domain_runtime_request_expression(&request); + if (err != ESP_OK) { + cli_write_error("lcd_test_failed"); + return; + } + cli_write_ok(); +} + static void handle_command_line(char *line) { trim_text(line); @@ -661,6 +675,8 @@ static void handle_command_line(char *line) handle_prepare_rotate(key_version); } else if (strcmp(cmd, "log") == 0) { handle_log_command(argc, argv); + } else if (strcmp(cmd, "lcd_test") == 0) { + handle_lcd_test(); } else { cli_write_error("unknown_command"); } diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index a3c4b6b..fca8f92 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -170,6 +170,7 @@ typedef enum { DOMAIN_EXPRESSION_WUXING, DOMAIN_EXPRESSION_LISTENING, DOMAIN_EXPRESSION_OTA_UPDATE, + DOMAIN_EXPRESSION_LCD_BOUNDARY_TEST, } domain_expression_id_t; typedef enum { @@ -198,6 +199,7 @@ typedef enum { DOMAIN_EXPRESSION_TRIGGER_OTA_UPDATE, DOMAIN_EXPRESSION_TRIGGER_MODEL_LUCKY_COLOR, DOMAIN_EXPRESSION_TRIGGER_PTT_BUTTON, + DOMAIN_EXPRESSION_TRIGGER_FACTORY_LCD_TEST, } domain_expression_trigger_t; typedef struct { diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index afd3f93..b40a5ee 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -81,6 +81,10 @@ static const uint8_t s_basic_face_frame_order[] = {0, 1, 2}; static const uint8_t s_muyu_frame_order[] = {0, 1, 2, 1}; static const uint8_t s_listening_frame_order[] = {0, 1, 2}; static const uint8_t s_ota_update_frame_order[] = {0, 1, 2}; +static const uint8_t s_lcd_boundary_test_frame_order[] = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, +}; static const char *const s_chouqian_result_asset_dirs[] = { "chouqian_buyun", "chouqian_heping", @@ -280,6 +284,22 @@ static const domain_expression_catalog_entry_t s_expression_catalog[] = { .frame_ms = 200, }, }, + { + .id = DOMAIN_EXPRESSION_LCD_BOUNDARY_TEST, + .platform_request = { + .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, + .priority = 250, + .interrupt_current = true, + .return_to_idle = true, + .asset_dir = "lcd_boundary_test", + .frame_order = s_lcd_boundary_test_frame_order, + .frame_order_count = sizeof(s_lcd_boundary_test_frame_order) / + sizeof(s_lcd_boundary_test_frame_order[0]), + .repeat_count = 0, + .frame_ms = 1, + .duration_ms = 10000, + }, + }, }; static void snapshot_domain_wifi_behavior_hooks(domain_wifi_behavior_hooks_t *out_hooks) diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index bd13e81..3479036 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -156,6 +156,22 @@ set(platform_lcd_png_assets charging lowbat qrcode + lcdtest00 + lcdtest01 + lcdtest02 + lcdtest03 + lcdtest04 + lcdtest05 + lcdtest06 + lcdtest07 + lcdtest08 + lcdtest09 + lcdtest10 + lcdtest11 + lcdtest12 + lcdtest13 + lcdtest14 + lcdtest15 ) set(platform_lcd_png_asset_files) foreach(lcd_png_asset IN LISTS platform_lcd_png_assets) diff --git a/components/platform/assets/lcd_optimized/lcdtest00.png b/components/platform/assets/lcd_optimized/lcdtest00.png new file mode 100644 index 0000000..cb11aac Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest00.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest01.png b/components/platform/assets/lcd_optimized/lcdtest01.png new file mode 100644 index 0000000..bc2feaf Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest01.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest02.png b/components/platform/assets/lcd_optimized/lcdtest02.png new file mode 100644 index 0000000..df813d2 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest02.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest03.png b/components/platform/assets/lcd_optimized/lcdtest03.png new file mode 100644 index 0000000..d336577 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest03.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest04.png b/components/platform/assets/lcd_optimized/lcdtest04.png new file mode 100644 index 0000000..4b0e4c0 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest04.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest05.png b/components/platform/assets/lcd_optimized/lcdtest05.png new file mode 100644 index 0000000..9cc4fe4 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest05.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest06.png b/components/platform/assets/lcd_optimized/lcdtest06.png new file mode 100644 index 0000000..bf4cbad Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest06.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest07.png b/components/platform/assets/lcd_optimized/lcdtest07.png new file mode 100644 index 0000000..df5b7da Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest07.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest08.png b/components/platform/assets/lcd_optimized/lcdtest08.png new file mode 100644 index 0000000..1151b60 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest08.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest09.png b/components/platform/assets/lcd_optimized/lcdtest09.png new file mode 100644 index 0000000..a143a70 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest09.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest10.png b/components/platform/assets/lcd_optimized/lcdtest10.png new file mode 100644 index 0000000..34f24b8 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest10.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest11.png b/components/platform/assets/lcd_optimized/lcdtest11.png new file mode 100644 index 0000000..bfee899 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest11.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest12.png b/components/platform/assets/lcd_optimized/lcdtest12.png new file mode 100644 index 0000000..10979b3 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest12.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest13.png b/components/platform/assets/lcd_optimized/lcdtest13.png new file mode 100644 index 0000000..899ed82 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest13.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest14.png b/components/platform/assets/lcd_optimized/lcdtest14.png new file mode 100644 index 0000000..56e8250 Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest14.png differ diff --git a/components/platform/assets/lcd_optimized/lcdtest15.png b/components/platform/assets/lcd_optimized/lcdtest15.png new file mode 100644 index 0000000..d161f5d Binary files /dev/null and b/components/platform/assets/lcd_optimized/lcdtest15.png differ diff --git a/components/platform/assets/lcd_optimized/manifest.json b/components/platform/assets/lcd_optimized/manifest.json index 8e7f20c..b346c17 100644 --- a/components/platform/assets/lcd_optimized/manifest.json +++ b/components/platform/assets/lcd_optimized/manifest.json @@ -495,6 +495,246 @@ "source_sha256": "a048e4e9a74ad59d8835a4a4288cee5598b0f4b96de801bdfd3a38dde4c70b72", "width": 320 }, + "lcdtest00.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 2885, + "optimized_sha256": "3ef8a1298e87bf95d438e7632c74289aff003c715daf761e28c39123321146de", + "output_mode": "indexed", + "rgb565_colors": 34, + "source_bytes": 3321, + "source_mode": "RGB", + "source_sha256": "1e9644092b2de9edb12d03bfeaef5b5fd3ce702b5c4ef0feda2e6feed88ee92c", + "width": 320 + }, + "lcdtest01.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 2965, + "optimized_sha256": "635099f9f20e5debab027b6a445255aa942e785fecf8a8901c9ff9ca7e280f1a", + "output_mode": "indexed", + "rgb565_colors": 40, + "source_bytes": 3574, + "source_mode": "RGB", + "source_sha256": "02f3915f405ce676241ff7e3cda707d01baabec1b7c5416dd48e4c3cb899f2ee", + "width": 320 + }, + "lcdtest02.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 2982, + "optimized_sha256": "f7db16b21b7662fcab878793658b7836887453c1ea142faebf7d54206b31b119", + "output_mode": "indexed", + "rgb565_colors": 50, + "source_bytes": 3767, + "source_mode": "RGB", + "source_sha256": "d6deabbeebf55acb43c71d93a99fea65be1b866f0b7d63c628a86777b63f75b3", + "width": 320 + }, + "lcdtest03.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3043, + "optimized_sha256": "7f8d0c54698558220925d4d8903f83b5e4572c28906b3cf7ea25983897010f02", + "output_mode": "indexed", + "rgb565_colors": 54, + "source_bytes": 3903, + "source_mode": "RGB", + "source_sha256": "9b5fdb2ab9acf148b516596e39a3cafc7abd1cd7cb257e2ed3b03d16e0701fe6", + "width": 320 + }, + "lcdtest04.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 2949, + "optimized_sha256": "03d7113fdb653277b0e384905e641001ae04c5dc0047058f29765a52e8aaa576", + "output_mode": "indexed", + "rgb565_colors": 43, + "source_bytes": 3579, + "source_mode": "RGB", + "source_sha256": "84e51f7ba6c608de7e9864e2765c0e21649a576b33db3a57e781824ae966dd7d", + "width": 320 + }, + "lcdtest05.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3225, + "optimized_sha256": "c76b3e5271d17354f145a211f001f8609e592181101308fcefa553171d1d215f", + "output_mode": "indexed", + "rgb565_colors": 59, + "source_bytes": 3996, + "source_mode": "RGB", + "source_sha256": "4ab57523735c7627330751dfae4031905bed66d34296003ca0628126eaaee11b", + "width": 320 + }, + "lcdtest06.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3431, + "optimized_sha256": "148789a0e47478f59b67d55242c26df112b617fba99b9f4bd1777af31ea8e8be", + "output_mode": "indexed", + "rgb565_colors": 60, + "source_bytes": 4089, + "source_mode": "RGB", + "source_sha256": "fc9e01e9b5f6140a3a1904ac3b31b3ed659df475c171e0edde5af0de8215bb3a", + "width": 320 + }, + "lcdtest07.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3369, + "optimized_sha256": "0845948af9c1bbb2a176ca139d15b0b130df401f2c32a488781b8fdbfbd6c479", + "output_mode": "indexed", + "rgb565_colors": 52, + "source_bytes": 3753, + "source_mode": "RGB", + "source_sha256": "c3bd1a40466269b2778f8edd84174881d9de773e15a2222974fa4ac8caf498cb", + "width": 320 + }, + "lcdtest08.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3299, + "optimized_sha256": "19f355a63a6d437e2b171644a4c297ea78e9da05abbc236f14f7c94d6e7898cc", + "output_mode": "indexed", + "rgb565_colors": 60, + "source_bytes": 3860, + "source_mode": "RGB", + "source_sha256": "a4382986103c92344afd3bf3686585e9750f7286f1c508cf42abbc4e56e9fad8", + "width": 320 + }, + "lcdtest09.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3458, + "optimized_sha256": "9fbcbdca9b17fbbf116c0f0f5c7968099253543664cab4c0049f42f9cb99dc07", + "output_mode": "indexed", + "rgb565_colors": 60, + "source_bytes": 4008, + "source_mode": "RGB", + "source_sha256": "b4b811390bc77db0f4e28ff45b102c24f77aebc50aa57ecbb4378bf440ff5008", + "width": 320 + }, + "lcdtest10.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3229, + "optimized_sha256": "e37daec397eea565e1e46479ba96cdbbdb22736470d2893aadf894f857cb1265", + "output_mode": "indexed", + "rgb565_colors": 40, + "source_bytes": 3545, + "source_mode": "RGB", + "source_sha256": "e050346ca4d6a15504ea4276a28a6bd23cb6eeab031ec4ada4dc8d65405bbd6e", + "width": 320 + }, + "lcdtest11.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 2966, + "optimized_sha256": "57c958dac45e46376c7419ea5c28acb8e072140d8848b49a3a9cd723da58517e", + "output_mode": "indexed", + "rgb565_colors": 28, + "source_bytes": 3169, + "source_mode": "RGB", + "source_sha256": "bd373fbf6e41318c7767c8b877699027cb282569506262d59072c366e11fd9d6", + "width": 320 + }, + "lcdtest12.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 2921, + "optimized_sha256": "705d5aca100df51c8bfd13ceb6f42ef9a4305416755ea4247e5781cb12dfe4e1", + "output_mode": "indexed", + "rgb565_colors": 47, + "source_bytes": 3453, + "source_mode": "RGB", + "source_sha256": "bce48a61a6a55d3c69e998a4907652f712a9b76d5c7a99c28a87afaad9777d5c", + "width": 320 + }, + "lcdtest13.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3020, + "optimized_sha256": "fdc3b0aea5740bc4322157af16b1cc3d52fd723fa0ac7598bd3a97da9baa2741", + "output_mode": "indexed", + "rgb565_colors": 54, + "source_bytes": 3710, + "source_mode": "RGB", + "source_sha256": "b66b4113787fd763c2b01f0213bc14bbb1d72ebc27e643126e765a70d2fe1ab0", + "width": 320 + }, + "lcdtest14.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 2986, + "optimized_sha256": "f8df29b876d32f2e744f5e5cb3dd386358eab42346419760b0d276d31224e36f", + "output_mode": "indexed", + "rgb565_colors": 35, + "source_bytes": 3443, + "source_mode": "RGB", + "source_sha256": "18619fa00094c38abd445acbe99980e0b85eebad67bb559f7d63799c3594d18e", + "width": 320 + }, + "lcdtest15.png": { + "alpha_range": [ + 255, + 255 + ], + "height": 240, + "optimized_bytes": 3033, + "optimized_sha256": "9fa7245972c22418d8aaca38ea057c06a9991a9f617fb65cb2b1323579c4ee03", + "output_mode": "indexed", + "rgb565_colors": 58, + "source_bytes": 3646, + "source_mode": "RGB", + "source_sha256": "3dad7138d4f1409767d29fefa7e02e948ae02c0624ebc5a9347ed30350955b4e", + "width": 320 + }, "listening0.png": { "alpha_range": [ 255, @@ -889,9 +1129,9 @@ "generator": "tools/optimize_lcd_assets.py", "pillow_version": "12.3.0", "totals": { - "asset_count": 59, - "optimized_bytes": 381384, - "source_bytes": 1069465 + "asset_count": 75, + "optimized_bytes": 431145, + "source_bytes": 1128281 }, "version": 1 } diff --git a/components/platform/assets/lcd_source/lcdtest00.png b/components/platform/assets/lcd_source/lcdtest00.png new file mode 100644 index 0000000..74a1c6d Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest00.png differ diff --git a/components/platform/assets/lcd_source/lcdtest01.png b/components/platform/assets/lcd_source/lcdtest01.png new file mode 100644 index 0000000..ae3e9db Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest01.png differ diff --git a/components/platform/assets/lcd_source/lcdtest02.png b/components/platform/assets/lcd_source/lcdtest02.png new file mode 100644 index 0000000..5d6b29f Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest02.png differ diff --git a/components/platform/assets/lcd_source/lcdtest03.png b/components/platform/assets/lcd_source/lcdtest03.png new file mode 100644 index 0000000..03da715 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest03.png differ diff --git a/components/platform/assets/lcd_source/lcdtest04.png b/components/platform/assets/lcd_source/lcdtest04.png new file mode 100644 index 0000000..a89ca1c Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest04.png differ diff --git a/components/platform/assets/lcd_source/lcdtest05.png b/components/platform/assets/lcd_source/lcdtest05.png new file mode 100644 index 0000000..2f83df9 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest05.png differ diff --git a/components/platform/assets/lcd_source/lcdtest06.png b/components/platform/assets/lcd_source/lcdtest06.png new file mode 100644 index 0000000..5f23210 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest06.png differ diff --git a/components/platform/assets/lcd_source/lcdtest07.png b/components/platform/assets/lcd_source/lcdtest07.png new file mode 100644 index 0000000..1834ed4 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest07.png differ diff --git a/components/platform/assets/lcd_source/lcdtest08.png b/components/platform/assets/lcd_source/lcdtest08.png new file mode 100644 index 0000000..d5f6284 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest08.png differ diff --git a/components/platform/assets/lcd_source/lcdtest09.png b/components/platform/assets/lcd_source/lcdtest09.png new file mode 100644 index 0000000..1072ede Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest09.png differ diff --git a/components/platform/assets/lcd_source/lcdtest10.png b/components/platform/assets/lcd_source/lcdtest10.png new file mode 100644 index 0000000..340073f Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest10.png differ diff --git a/components/platform/assets/lcd_source/lcdtest11.png b/components/platform/assets/lcd_source/lcdtest11.png new file mode 100644 index 0000000..cb459a8 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest11.png differ diff --git a/components/platform/assets/lcd_source/lcdtest12.png b/components/platform/assets/lcd_source/lcdtest12.png new file mode 100644 index 0000000..75f1417 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest12.png differ diff --git a/components/platform/assets/lcd_source/lcdtest13.png b/components/platform/assets/lcd_source/lcdtest13.png new file mode 100644 index 0000000..f985907 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest13.png differ diff --git a/components/platform/assets/lcd_source/lcdtest14.png b/components/platform/assets/lcd_source/lcdtest14.png new file mode 100644 index 0000000..3266713 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest14.png differ diff --git a/components/platform/assets/lcd_source/lcdtest15.png b/components/platform/assets/lcd_source/lcdtest15.png new file mode 100644 index 0000000..141abd7 Binary files /dev/null and b/components/platform/assets/lcd_source/lcdtest15.png differ diff --git a/components/platform/src/lcd_direct.c b/components/platform/src/lcd_direct.c index f4dc29f..74bce11 100644 --- a/components/platform/src/lcd_direct.c +++ b/components/platform/src/lcd_direct.c @@ -27,6 +27,7 @@ #include "esp_lcd_panel_vendor.h" #include "esp_log.h" #include "esp_heap_caps.h" +#include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" @@ -49,6 +50,7 @@ #define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100U #define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U #define LCD_DIRECT_FRAME_CACHE_MAX 6U +#define LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES LCD_DIRECT_FRAME_CACHE_MAX #define LCD_DIRECT_PATH_MAX 96U #define LCD_DIRECT_CHARGING_ICON_WIDTH 50U #define LCD_DIRECT_CHARGING_ICON_X (LCD_DIRECT_WIDTH - LCD_DIRECT_CHARGING_ICON_WIDTH - 8U) @@ -116,6 +118,13 @@ typedef struct lcd_direct_decoded_image_t { bool pinned; } lcd_direct_decoded_image_t; +typedef struct { + uint32_t completed_frames; + uint64_t total_draw_us; + uint32_t min_draw_us; + uint32_t max_draw_us; +} lcd_direct_sequence_perf_t; + typedef struct { lcd_direct_icon_draw_t draw; const lcd_direct_icon_t *icon; @@ -778,7 +787,8 @@ static size_t lcd_direct_append_status_icons(const lcd_direct_image_t *image, size_t icon_capacity) { if (image == NULL || image->path == NULL || icons == NULL || - icon_count >= icon_capacity || strstr(image->path, "/qrcode.png") != NULL) { + icon_count >= icon_capacity || strstr(image->path, "/qrcode.png") != NULL || + strstr(image->path, "/lcdtest") != NULL) { return icon_count; } @@ -1011,7 +1021,8 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image, combined_icon_count, prepared_icons, sizeof(prepared_icons) / sizeof(prepared_icons[0])); - const bool clock_visible = strstr(image->path, "/qrcode.png") == NULL; + const bool clock_visible = strstr(image->path, "/qrcode.png") == NULL && + strstr(image->path, "/lcdtest") == NULL; char clock_text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U] = {0}; if (clock_visible) { lcd_direct_clock_text(clock_text); @@ -1193,6 +1204,9 @@ static uint32_t lcd_direct_asset_sequence_hold_ms(const lcd_direct_request_t *re LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS : LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS; } + if (request != NULL && strcmp(request->asset_dir, "lcd_boundary_test") == 0) { + return frame_ms; + } if (frame_index + 1U != frame_count) { return frame_ms; } @@ -1204,6 +1218,9 @@ static uint32_t lcd_direct_asset_sequence_duration_ms(const lcd_direct_request_t if (request == NULL || request->duration_ms == 0U) { return 0U; } + if (strcmp(request->asset_dir, "lcd_boundary_test") == 0) { + return request->duration_ms; + } const size_t frame_count = request->frame_order_count > 0U ? request->frame_order_count : 1U; const uint32_t frame_ms = lcd_direct_asset_sequence_frame_ms(request); @@ -1284,7 +1301,8 @@ static esp_err_t lcd_direct_draw_asset_sequence_once(const lcd_direct_request_t static esp_err_t lcd_direct_draw_asset_sequence_for_duration(const lcd_direct_request_t *request, TickType_t start_tick, - TickType_t duration_ticks) + TickType_t duration_ticks, + lcd_direct_sequence_perf_t *perf) { ESP_RETURN_ON_FALSE(request != NULL, ESP_ERR_INVALID_ARG, @@ -1312,9 +1330,21 @@ static esp_err_t lcd_direct_draw_asset_sequence_for_duration(const lcd_direct_re request->asset_dir, (unsigned)frame_index); + const int64_t draw_started_us = esp_timer_get_time(); ESP_RETURN_ON_ERROR(lcd_direct_draw_png_image(frame, NULL, 0), TAG, "draw timed asset frame failed"); + const uint32_t draw_us = (uint32_t)(esp_timer_get_time() - draw_started_us); + if (perf != NULL) { + perf->completed_frames++; + perf->total_draw_us += draw_us; + if (draw_us < perf->min_draw_us) { + perf->min_draw_us = draw_us; + } + if (draw_us > perf->max_draw_us) { + perf->max_draw_us = draw_us; + } + } const TickType_t now = xTaskGetTickCount(); const TickType_t elapsed_ticks = now - start_tick; @@ -1332,6 +1362,91 @@ static esp_err_t lcd_direct_draw_asset_sequence_for_duration(const lcd_direct_re return ESP_OK; } +static void lcd_direct_log_sequence_perf(const lcd_direct_request_t *request, + const char *mode, + const lcd_direct_sequence_perf_t *perf, + uint64_t elapsed_us) +{ + if (request == NULL || perf == NULL || perf->completed_frames == 0U || elapsed_us == 0U) { + return; + } + + const uint64_t fps_x100 = + ((uint64_t)perf->completed_frames * 100000000ULL) / elapsed_us; + const uint32_t average_draw_us = + (uint32_t)(perf->total_draw_us / perf->completed_frames); + ESP_LOGI(TAG, + "LCD sequence perf asset=%s mode=%s working_set=%u cache_capacity=%u " + "frames=%u elapsed_ms=%llu fps=%llu.%02llu draw_us_avg=%u " + "draw_us_min=%u draw_us_max=%u hold_ms=%u", + request->asset_dir, + mode, + (unsigned)request->frame_order_count, + (unsigned)LCD_DIRECT_FRAME_CACHE_MAX, + (unsigned)perf->completed_frames, + (unsigned long long)(elapsed_us / 1000ULL), + (unsigned long long)(fps_x100 / 100ULL), + (unsigned long long)(fps_x100 % 100ULL), + (unsigned)average_draw_us, + (unsigned)perf->min_draw_us, + (unsigned)perf->max_draw_us, + (unsigned)lcd_direct_asset_sequence_frame_ms(request)); +} + +static esp_err_t lcd_direct_run_sequence_perf_stage(const lcd_direct_request_t *request, + uint32_t duration_ms, + const char *mode) +{ + const TickType_t start_tick = xTaskGetTickCount(); + const TickType_t duration_ticks = pdMS_TO_TICKS(duration_ms); + const int64_t started_us = esp_timer_get_time(); + lcd_direct_sequence_perf_t perf = { + .min_draw_us = UINT32_MAX, + }; + esp_err_t err = lcd_direct_draw_asset_sequence_for_duration( + request, start_tick, duration_ticks, &perf); + const uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us); + lcd_direct_log_sequence_perf(request, mode, &perf, elapsed_us); + return err; +} + +static esp_err_t lcd_direct_run_boundary_test(const lcd_direct_request_t *request) +{ + ESP_RETURN_ON_FALSE(request != NULL && + request->frame_order_count > LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES, + ESP_ERR_INVALID_ARG, + TAG, + "invalid LCD boundary test request"); + + lcd_direct_request_t cache_hit_request = *request; + cache_hit_request.frame_order_count = LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES; + + ESP_RETURN_ON_ERROR(lcd_direct_preheat_asset_sequence(&cache_hit_request), + TAG, + "preheat cache-hit frames failed"); + + const uint32_t cache_hit_duration_ms = request->duration_ms / 2U; + const uint32_t cache_thrash_duration_ms = request->duration_ms - cache_hit_duration_ms; + ESP_RETURN_ON_ERROR(lcd_direct_run_sequence_perf_stage( + &cache_hit_request, cache_hit_duration_ms, "cache_hit"), + TAG, + "cache-hit performance stage failed"); + + lcd_direct_request_t cache_thrash_preheat = *request; + cache_thrash_preheat.frame_order_count = LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES; + const size_t first_preheat_frame = + request->frame_order_count - LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES; + memcpy(cache_thrash_preheat.frame_order, + &request->frame_order[first_preheat_frame], + LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES * sizeof(cache_thrash_preheat.frame_order[0])); + ESP_RETURN_ON_ERROR(lcd_direct_preheat_asset_sequence(&cache_thrash_preheat), + TAG, + "preheat cache-thrash frames failed"); + + return lcd_direct_run_sequence_perf_stage( + request, cache_thrash_duration_ms, "cache_thrash"); +} + static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *request) { ESP_RETURN_ON_FALSE(request != NULL, @@ -1359,9 +1474,15 @@ static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *requ } if (request->duration_ms > 0U) { - const TickType_t start_tick = xTaskGetTickCount(); - const TickType_t duration_ticks = pdMS_TO_TICKS(lcd_direct_asset_sequence_duration_ms(request)); - esp_err_t err = lcd_direct_draw_asset_sequence_for_duration(request, start_tick, duration_ticks); + esp_err_t err; + if (strcmp(request->asset_dir, "lcd_boundary_test") == 0) { + err = lcd_direct_run_boundary_test(request); + } else { + err = lcd_direct_run_sequence_perf_stage( + request, + lcd_direct_asset_sequence_duration_ms(request), + "normal"); + } if (err != ESP_OK) { return err; } diff --git a/components/platform/src/lcd_direct_assets.c b/components/platform/src/lcd_direct_assets.c index e61be78..25f9497 100644 --- a/components/platform/src/lcd_direct_assets.c +++ b/components/platform/src/lcd_direct_assets.c @@ -32,6 +32,25 @@ static const lcd_direct_image_t s_ota_frames[] = { {.path = LCD_ASSET_BASE_PATH "ota3.png"}, }; +static const lcd_direct_image_t s_lcd_boundary_test_frames[] = { + {.path = LCD_ASSET_BASE_PATH "lcdtest00.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest01.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest02.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest03.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest04.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest05.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest06.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest07.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest08.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest09.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest10.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest11.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest12.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest13.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest14.png"}, + {.path = LCD_ASSET_BASE_PATH "lcdtest15.png"}, +}; + static const lcd_direct_image_t s_wifi_connecting_qr_frames[] = { {.path = LCD_ASSET_BASE_PATH "qrcode.png"}, }; @@ -270,6 +289,14 @@ const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t frame_index); } + if (strcmp(asset_dir, "lcd_boundary_test") == 0) { + return lcd_direct_frame_from_array( + s_lcd_boundary_test_frames, + sizeof(s_lcd_boundary_test_frames) / + sizeof(s_lcd_boundary_test_frames[0]), + frame_index); + } + if (strcmp(asset_dir, "wifi_connecting_qr") == 0) { return lcd_direct_frame_from_array( s_wifi_connecting_qr_frames, diff --git a/docs/lcd-boundary-test-preview.gif b/docs/lcd-boundary-test-preview.gif new file mode 100644 index 0000000..66090f6 Binary files /dev/null and b/docs/lcd-boundary-test-preview.gif differ diff --git a/docs/lcd-boundary-test.zh-CN.md b/docs/lcd-boundary-test.zh-CN.md new file mode 100644 index 0000000..0162744 --- /dev/null +++ b/docs/lcd-boundary-test.zh-CN.md @@ -0,0 +1,110 @@ +# LCD 边界测试动画 + +## 测试资源 + +测试动画包含 16 张 320×240 PNG,对应当前单次播放请求允许的最大帧序列长度。 + +文件名为: + +```text +lcdtest00.png +lcdtest01.png +... +lcdtest15.png +``` + +每帧包含以下测试元素: + +- 1 像素外边框和四色角标,用于检查裁切、偏移和方向; +- RGB565 基础色条,用于检查 RGB/BGR 色序和颜色异常; +- 连续移动的竖直白线和水平黄线,用于观察撕裂和跳帧; +- 旋转红色指针和移动方块,用于观察运动连续性; +- `F00`~`F15` 帧号,用于识别乱序和重复帧; +- 16 格进度条,用于识别丢帧; +- 4 位红绿二进制标记,用于辅助确认帧编号。 + +第 15 帧播放完后可以平滑回到第 0 帧。 + +## 真机触发 + +固件需要启用 Factory CLI。通过 UART0 发送: + +```text +lcd_test +``` + +设备返回: + +```text +OK +``` + +测试动画会: + +- 立即打断当前普通动画; +- 先循环播放 `00 → 05`,再按 `00 → 15` 顺序循环; +- 每次全屏绘制后只等待 1 ms; +- 两个性能阶段各运行约 5 秒,总计约 10 秒(不含预热时间); +- 完成后自动返回待机动画。 + +1 ms 是额外等待时间,不代表 1000 FPS。实际帧率由 PNG 读取和解码、RGB565 转换、缓存命中情况以及 40 MHz SPI 全屏传输共同决定。 + +## 性能日志 + +测试会输出两条性能汇总日志,不会逐帧打印性能日志并干扰测试: + +```text +LCD sequence perf asset=lcd_boundary_test mode=cache_hit working_set=6 cache_capacity=6 frames=120 elapsed_ms=5010 fps=23.95 draw_us_avg=40680 draw_us_min=39520 draw_us_max=48760 hold_ms=1 +LCD sequence perf asset=lcd_boundary_test mode=cache_thrash working_set=16 cache_capacity=6 frames=38 elapsed_ms=5020 fps=7.56 draw_us_avg=131960 draw_us_min=108305 draw_us_max=343547 hold_ms=1 +``` + +`cache_hit` 阶段开始计时前会把第 0~5 帧解码并装入 6 帧缓存,计时期间直接从缓存读取 RGB565 数据。`cache_thrash` 阶段开始计时前会把第 10~15 帧装入缓存,使第 0 帧开始的 16 帧顺序播放持续发生缓存淘汰和重新解码。预热耗时不计入两个阶段的 FPS。 + +为避免时钟、充电和低电量图标占用同一组图片缓存槽位,边界测试帧不绘制这些运行时叠加层。普通动画的叠加层行为不受影响。 + +字段含义: + +| 字段 | 含义 | +| --- | --- | +| `mode` | `cache_hit` 表示缓存命中;`cache_thrash` 表示工作集超过缓存容量 | +| `working_set` | 当前阶段循环使用的不同图片数量 | +| `cache_capacity` | 编译时配置的图片缓存槽位数量 | +| `frames` | 已经完成 SPI 传输的全屏帧数 | +| `elapsed_ms` | 整段测试的实际墙钟时间 | +| `fps` | `frames ÷ elapsed_ms` 得到的真实平均帧率 | +| `draw_us_avg` | 平均每帧读取、解码、转换和 SPI 传输耗时 | +| `draw_us_min` | 最快一帧的绘制耗时 | +| `draw_us_max` | 最慢一帧的绘制耗时 | +| `hold_ms` | 每帧绘制完成后的配置等待时间 | + +示例数值仅用于说明日志格式,必须以真机输出为准。 + +两组结果的用途不同: + +- `cache_hit` 更接近 RGB565 帧已经在内存中时,LCD 合成与 SPI 全屏传输的速度上限; +- `cache_thrash` 代表长动画超过缓存容量后,Flash 读取、PNG 解码、RGB565 转换和 SPI 传输叠加的实际速度。 + +## 观察方法 + +重点观察: + +1. 帧号是否严格按 `F00` 到 `F15` 循环。 +2. 白色竖线和黄色横线是否连续移动。 +3. 指针是否匀速旋转,是否出现明显停顿或倒退。 +4. 屏幕边框和四角标记是否完整可见。 +5. 顶部色条颜色是否正确。 +6. 快速运动过程中是否出现上下区域属于不同帧的撕裂。 +7. 串口日志中是否出现 PNG 解码失败、资源缺失或 LCD 传输错误。 + +## 重新生成测试帧 + +测试帧由确定性脚本生成: + +```bash +python tools/generate_lcd_boundary_frames.py +python tools/optimize_lcd_assets.py generate +python tools/optimize_lcd_assets.py verify +idf.py build +``` + +生成脚本需要 Pillow。普通固件构建仍然不需要 Pillow。 diff --git a/docs/lcd-expressions.md b/docs/lcd-expressions.md index 021063f..3d5d84c 100644 --- a/docs/lcd-expressions.md +++ b/docs/lcd-expressions.md @@ -49,6 +49,7 @@ and legacy animation assets have been removed from the build. | `DOMAIN_EXPRESSION_WEALTH_SPIN` | Model-command direction worker | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Draws a direct backend pattern selected from `spin`. | | `DOMAIN_EXPRESSION_WEALTH_DIRECTION` | Model-command direction worker | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Draws a direct backend pattern selected from the mapped direction asset name. | | `DOMAIN_EXPRESSION_OTA_UPDATE` | OTA update worker | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Loops the three-frame `ota` sequence during firmware download and installation. | +| `DOMAIN_EXPRESSION_LCD_BOUNDARY_TEST` | Factory CLI `lcd_test` | `PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE` | Measures six preheated cache-resident frames for about 5 seconds, then measures all 16 frames under cache churn for about 5 seconds, using a 1 ms hold between full-screen draws before returning to idle. | Server error currently triggers only from: diff --git a/tools/generate_lcd_boundary_frames.py b/tools/generate_lcd_boundary_frames.py new file mode 100644 index 0000000..fc12148 --- /dev/null +++ b/tools/generate_lcd_boundary_frames.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Generate deterministic 320x240 frames for LCD boundary testing.""" + +from __future__ import annotations + +import math +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +SOURCE_DIR = PROJECT_ROOT / "components" / "platform" / "assets" / "lcd_source" +PREVIEW_PATH = PROJECT_ROOT / "docs" / "lcd-boundary-test-preview.gif" + +WIDTH = 320 +HEIGHT = 240 +FRAME_COUNT = 16 +FRAME_PREFIX = "lcdtest" + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +RED = (255, 0, 0) +GREEN = (0, 255, 0) +BLUE = (0, 0, 255) +CYAN = (0, 255, 255) +MAGENTA = (255, 0, 255) +YELLOW = (255, 255, 0) +GRAY = (132, 130, 132) +DARK_GRAY = (33, 32, 33) + + +def draw_corner_targets(draw: ImageDraw.ImageDraw) -> None: + targets = ( + (0, 0, RED), + (WIDTH - 12, 0, GREEN), + (0, HEIGHT - 12, BLUE), + (WIDTH - 12, HEIGHT - 12, YELLOW), + ) + for x, y, color in targets: + draw.rectangle((x, y, x + 11, y + 11), fill=color, outline=WHITE) + + +def draw_color_bars(draw: ImageDraw.ImageDraw) -> None: + colors = (WHITE, YELLOW, CYAN, GREEN, MAGENTA, RED, BLUE, BLACK) + for index, color in enumerate(colors): + x0 = index * (WIDTH // len(colors)) + x1 = (index + 1) * (WIDTH // len(colors)) - 1 + draw.rectangle((x0, 1, x1, 30), fill=color) + + +def draw_motion_field(draw: ImageDraw.ImageDraw, frame_index: int) -> None: + field_top = 32 + field_bottom = 207 + stripe_width = 16 + phase = frame_index * 4 + + draw.rectangle((1, field_top, WIDTH - 2, field_bottom), fill=DARK_GRAY) + for x in range(-HEIGHT, WIDTH + HEIGHT, stripe_width * 2): + shifted = x + phase + draw.polygon( + ( + (shifted, field_top), + (shifted + stripe_width, field_top), + (shifted - 160 + stripe_width, field_bottom), + (shifted - 160, field_bottom), + ), + fill=GRAY, + ) + + vertical_x = frame_index * (WIDTH // FRAME_COUNT) + horizontal_y = field_top + frame_index * ((field_bottom - field_top + 1) // FRAME_COUNT) + draw.rectangle((vertical_x, field_top, vertical_x + 2, field_bottom), fill=WHITE) + draw.rectangle((1, horizontal_y, WIDTH - 2, horizontal_y + 2), fill=YELLOW) + + center_x = WIDTH // 2 + center_y = (field_top + field_bottom) // 2 + angle = (2.0 * math.pi * frame_index / FRAME_COUNT) - (math.pi / 2.0) + pointer_x = round(center_x + math.cos(angle) * 62) + pointer_y = round(center_y + math.sin(angle) * 62) + draw.ellipse((center_x - 65, center_y - 65, center_x + 65, center_y + 65), outline=CYAN, width=2) + draw.line((center_x, center_y, pointer_x, pointer_y), fill=RED, width=4) + draw.ellipse((center_x - 6, center_y - 6, center_x + 6, center_y + 6), fill=WHITE, outline=BLACK) + + moving_x = round(center_x + math.cos(angle) * 92) + moving_y = round(center_y + math.sin(angle) * 50) + draw.rectangle((moving_x - 7, moving_y - 7, moving_x + 7, moving_y + 7), fill=MAGENTA, outline=WHITE) + + +def draw_frame_footer(draw: ImageDraw.ImageDraw, frame_index: int, font: ImageFont.ImageFont) -> None: + footer_top = 209 + draw.rectangle((1, footer_top, WIDTH - 2, HEIGHT - 2), fill=BLACK) + draw.text((16, 211), f"F{frame_index:02d}", font=font, fill=WHITE) + + cell_width = 12 + cell_gap = 2 + strip_x = 102 + strip_y = 214 + for index in range(FRAME_COUNT): + x0 = strip_x + index * (cell_width + cell_gap) + color = WHITE if index == frame_index else DARK_GRAY + draw.rectangle((x0, strip_y, x0 + cell_width - 1, strip_y + 10), fill=color, outline=GRAY) + + for bit in range(4): + x0 = strip_x + bit * 20 + color = GREEN if frame_index & (1 << bit) else RED + draw.rectangle((x0, 229, x0 + 15, 237), fill=color) + + +def generate_frame(frame_index: int, font: ImageFont.ImageFont) -> Image.Image: + image = Image.new("RGB", (WIDTH, HEIGHT), BLACK) + draw = ImageDraw.Draw(image) + draw_color_bars(draw) + draw_motion_field(draw, frame_index) + draw_frame_footer(draw, frame_index, font) + draw.rectangle((0, 0, WIDTH - 1, HEIGHT - 1), outline=WHITE) + draw.rectangle((1, 1, WIDTH - 2, HEIGHT - 2), outline=RED) + draw_corner_targets(draw) + return image + + +def main() -> None: + SOURCE_DIR.mkdir(parents=True, exist_ok=True) + PREVIEW_PATH.parent.mkdir(parents=True, exist_ok=True) + font = ImageFont.load_default(size=22) + frames = [generate_frame(index, font) for index in range(FRAME_COUNT)] + + for index, frame in enumerate(frames): + output_path = SOURCE_DIR / f"{FRAME_PREFIX}{index:02d}.png" + frame.save(output_path, format="PNG", optimize=True, compress_level=9) + + frames[0].save( + PREVIEW_PATH, + format="GIF", + save_all=True, + append_images=frames[1:], + duration=80, + loop=0, + disposal=2, + ) + print(f"Generated {FRAME_COUNT} LCD boundary frames and {PREVIEW_PATH}") + + +if __name__ == "__main__": + main()