From 8d58bdb21b7389d810681aa793c6314e52443bcf Mon Sep 17 00:00:00 2001 From: espressif2022 <111102666+espressif2022@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:14:43 +0800 Subject: [PATCH] feat: add emote style for v2 (#1217) * feat: add emote style for v2 * feat: delete asset probe apply --- main/CMakeLists.txt | 7 +- main/Kconfig.projbuild | 19 +- main/assets.cc | 121 ++++- main/boards/echoear/EchoEar.cc | 28 +- main/boards/echoear/config.json | 9 +- main/boards/echoear/emote.json | 22 + main/boards/echoear/emote_display.cc | 454 ----------------- main/boards/echoear/emote_display.h | 64 --- main/boards/echoear/layout.json | 37 ++ main/boards/esp-box-3/config.json | 7 +- main/boards/esp-box-3/emote.json | 22 + main/boards/esp-box-3/esp_box3_board.cc | 13 +- main/boards/esp-box-3/layout.json | 37 ++ main/display/display.h | 2 +- main/display/emote_display.cc | 652 ++++++++++++++++++++++++ main/display/emote_display.h | 102 ++++ main/idf_component.yml | 2 +- scripts/spiffs_assets/build.py | 185 ++++++- scripts/spiffs_assets/build_all.py | 61 ++- 19 files changed, 1275 insertions(+), 569 deletions(-) create mode 100644 main/boards/echoear/emote.json delete mode 100644 main/boards/echoear/emote_display.cc delete mode 100644 main/boards/echoear/emote_display.h create mode 100644 main/boards/echoear/layout.json create mode 100644 main/boards/esp-box-3/emote.json create mode 100644 main/boards/esp-box-3/layout.json create mode 100644 main/display/emote_display.cc create mode 100644 main/display/emote_display.h mode change 100755 => 100644 scripts/spiffs_assets/build.py diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 746fa607..f9867e18 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -16,6 +16,7 @@ set(SOURCES "audio/audio_codec.cc" "display/lcd_display.cc" "display/oled_display.cc" "display/lvgl_display/lvgl_display.cc" + "display/emote_display.cc" "display/lvgl_display/emoji_collection.cc" "display/lvgl_display/lvgl_theme.cc" "display/lvgl_display/lvgl_font.cc" @@ -212,11 +213,7 @@ elseif(CONFIG_BOARD_TYPE_ECHOEAR) set(BOARD_TYPE "echoear") set(BUILTIN_TEXT_FONT font_puhui_20_4) set(BUILTIN_ICON_FONT font_awesome_20_4) - # Find esp_emote_gfx component for ECHOEAR extra files - find_component_by_pattern("esp_emote_gfx" EMOTE_GFX_COMPONENT EMOTE_GFX_COMPONENT_PATH) - if(EMOTE_GFX_COMPONENT_PATH) - set(DEFAULT_ASSETS_EXTRA_FILES "${EMOTE_GFX_COMPONENT_PATH}/emoji_normal") - endif() + set(DEFAULT_EMOJI_COLLECTION twemoji_64) elseif(CONFIG_BOARD_TYPE_ESP32S3_AUDIO_BOARD) set(BOARD_TYPE "waveshare-s3-audio-board") set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index da80df9a..2e1875ae 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -473,11 +473,22 @@ choice DISPLAY_ESP32S3_AUDIO_BOARD bool "ST7789, 分辨率240*320" endchoice -config USE_WECHAT_MESSAGE_STYLE - bool "Enable WeChat Message Style" - default n +choice DISPLAY_STYLE + prompt "Select display style" + default USE_DEFAULT_MESSAGE_STYLE help - 使用微信聊天界面风格 + Select display style for Xiaozhi device + + config USE_DEFAULT_MESSAGE_STYLE + bool "Enable default message style" + + config USE_WECHAT_MESSAGE_STYLE + bool "Enable WeChat Message Style" + + config USE_EMOTE_MESSAGE_STYLE + bool "Emote animation style" + depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR +endchoice config USE_ESP_WAKE_WORD bool "Enable Wake Word Detection (without AFE)" diff --git a/main/assets.cc b/main/assets.cc index 9d799005..e72ab5a2 100644 --- a/main/assets.cc +++ b/main/assets.cc @@ -3,6 +3,7 @@ #include "display.h" #include "application.h" #include "lvgl_theme.h" +#include "emote_display.h" #include #include @@ -107,6 +108,7 @@ bool Assets::Apply() { ESP_LOGE(TAG, "The index.json file is not found"); return false; } + cJSON* root = cJSON_ParseWithLength(static_cast(ptr), size); if (root == nullptr) { ESP_LOGE(TAG, "The index.json file is not valid"); @@ -175,7 +177,8 @@ bool Assets::Apply() { if (cJSON_IsObject(emoji)) { cJSON* name = cJSON_GetObjectItem(emoji, "name"); cJSON* file = cJSON_GetObjectItem(emoji, "file"); - if (cJSON_IsString(name) && cJSON_IsString(file)) { + cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf"); + if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) { if (!GetAssetData(file->valuestring, ptr, size)) { ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring); continue; @@ -237,7 +240,6 @@ bool Assets::Apply() { } } } -#endif auto display = Board::GetInstance().GetDisplay(); ESP_LOGI(TAG, "Refreshing display theme..."); @@ -246,6 +248,121 @@ bool Assets::Apply() { if (current_theme != nullptr) { display->SetTheme(current_theme); } +#elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE) + auto &board = Board::GetInstance(); + auto display = board.GetDisplay(); + auto emote_display = dynamic_cast(display); + + cJSON* font = cJSON_GetObjectItem(root, "text_font"); + if (cJSON_IsString(font)) { + std::string fonts_text_file = font->valuestring; + if (GetAssetData(fonts_text_file, ptr, size)) { + auto text_font = std::make_shared(ptr); + if (text_font->font() == nullptr) { + ESP_LOGE(TAG, "Failed to load fonts.bin"); + return false; + } + + if (emote_display) { + emote_display->AddTextFont(text_font); + } + } else { + ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str()); + } + } + + cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection"); + if (cJSON_IsArray(emoji_collection)) { + int emoji_count = cJSON_GetArraySize(emoji_collection); + if (emote_display) { + for (int i = 0; i < emoji_count; i++) { + cJSON* icon = cJSON_GetArrayItem(emoji_collection, i); + if (cJSON_IsObject(icon)) { + cJSON* name = cJSON_GetObjectItem(icon, "name"); + cJSON* file = cJSON_GetObjectItem(icon, "file"); + + if (cJSON_IsString(name) && cJSON_IsString(file)) { + if (GetAssetData(file->valuestring, ptr, size)) { + cJSON* eaf = cJSON_GetObjectItem(icon, "eaf"); + bool lack_value = false; + bool loop_value = false; + int fps_value = 0; + + if (cJSON_IsObject(eaf)) { + cJSON* lack = cJSON_GetObjectItem(eaf, "lack"); + cJSON* loop = cJSON_GetObjectItem(eaf, "loop"); + cJSON* fps = cJSON_GetObjectItem(eaf, "fps"); + + lack_value = lack ? cJSON_IsTrue(lack) : false; + loop_value = loop ? cJSON_IsTrue(loop) : false; + fps_value = fps ? fps->valueint : 0; + + emote_display->AddEmojiData(name->valuestring, ptr, size, + static_cast(fps_value), + loop_value, lack_value); + } + + } else { + ESP_LOGE(TAG, "Emoji \"%10s\" image file %s is not found", name->valuestring, file->valuestring); + } + } + } + } + } + } + + cJSON* icon_collection = cJSON_GetObjectItem(root, "icon_collection"); + if (cJSON_IsArray(icon_collection)) { + if (emote_display) { + int icon_count = cJSON_GetArraySize(icon_collection); + for (int i = 0; i < icon_count; i++) { + cJSON* icon = cJSON_GetArrayItem(icon_collection, i); + if (cJSON_IsObject(icon)) { + cJSON* name = cJSON_GetObjectItem(icon, "name"); + cJSON* file = cJSON_GetObjectItem(icon, "file"); + + if (cJSON_IsString(name) && cJSON_IsString(file)) { + if (GetAssetData(file->valuestring, ptr, size)) { + emote_display->AddIconData(name->valuestring, ptr, size); + } else { + ESP_LOGE(TAG, "Icon \"%10s\" image file %s is not found", name->valuestring, file->valuestring); + } + } + } + } + } + } + + cJSON* layout_json = cJSON_GetObjectItem(root, "layout"); + if (cJSON_IsArray(layout_json)) { + int layout_count = cJSON_GetArraySize(layout_json); + + for (int i = 0; i < layout_count; i++) { + cJSON* layout_item = cJSON_GetArrayItem(layout_json, i); + if (cJSON_IsObject(layout_item)) { + cJSON* name = cJSON_GetObjectItem(layout_item, "name"); + cJSON* align = cJSON_GetObjectItem(layout_item, "align"); + cJSON* x = cJSON_GetObjectItem(layout_item, "x"); + cJSON* y = cJSON_GetObjectItem(layout_item, "y"); + cJSON* width = cJSON_GetObjectItem(layout_item, "width"); + cJSON* height = cJSON_GetObjectItem(layout_item, "height"); + + if (cJSON_IsString(name) && cJSON_IsString(align) && cJSON_IsNumber(x) && cJSON_IsNumber(y)) { + int width_val = cJSON_IsNumber(width) ? width->valueint : 0; + int height_val = cJSON_IsNumber(height) ? height->valueint : 0; + + if (emote_display) { + emote_display->AddLayoutData(name->valuestring, align->valuestring, + x->valueint, y->valueint, width_val, height_val); + } + } else { + ESP_LOGW(TAG, "Invalid layout item %d: missing required fields", i); + } + } + } + } +#endif + cJSON_Delete(root); return true; } diff --git a/main/boards/echoear/EchoEar.cc b/main/boards/echoear/EchoEar.cc index 1da379f2..9e5f5c7e 100644 --- a/main/boards/echoear/EchoEar.cc +++ b/main/boards/echoear/EchoEar.cc @@ -1,11 +1,11 @@ #include "wifi_board.h" #include "codecs/box_audio_codec.h" #include "display/lcd_display.h" +#include "display/emote_display.h" #include "application.h" #include "button.h" #include "config.h" #include "backlight.h" -#include "emote_display.h" #include #include @@ -26,7 +26,6 @@ #define TAG "EchoEar" -#define USE_LVGL_DEFAULT 0 temperature_sensor_handle_t temp_sensor = NULL; static const st77916_lcd_init_cmd_t vendor_specific_init_yysj[] = { @@ -387,11 +386,7 @@ private: Cst816s* cst816s_; Charge* charge_; Button boot_button_; -#if USE_LVGL_DEFAULT - LcdDisplay* display_; -#else - anim::EmoteDisplay* display_ = nullptr; -#endif + Display* display_ = nullptr; PwmBacklight* backlight_ = nullptr; esp_timer_handle_t touchpad_timer_; esp_lcd_touch_handle_t tp; // LCD touch handle @@ -517,13 +512,12 @@ private: void InitializeSpi() { - spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK, - QSPI_PIN_NUM_LCD_DATA0, - QSPI_PIN_NUM_LCD_DATA1, - QSPI_PIN_NUM_LCD_DATA2, - QSPI_PIN_NUM_LCD_DATA3, - QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); - // bus_config.isr_cpu_id = ESP_INTR_CPU_AFFINITY_1; + const spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK, + QSPI_PIN_NUM_LCD_DATA0, + QSPI_PIN_NUM_LCD_DATA1, + QSPI_PIN_NUM_LCD_DATA2, + QSPI_PIN_NUM_LCD_DATA3, + QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO)); } @@ -559,11 +553,11 @@ private: esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); -#if USE_LVGL_DEFAULT +#if CONFIG_USE_EMOTE_MESSAGE_STYLE + display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT); +#else display_ = new SpiLcdDisplay(panel_io, panel, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); -#else - display_ = new anim::EmoteDisplay(panel, panel_io); #endif backlight_ = new PwmBacklight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); backlight_->RestoreBrightness(); diff --git a/main/boards/echoear/config.json b/main/boards/echoear/config.json index 0f0d5dfa..0ed1a265 100644 --- a/main/boards/echoear/config.json +++ b/main/boards/echoear/config.json @@ -3,8 +3,13 @@ "builds": [ { "name": "echoear", - "sdkconfig_append": [ - ] + "sdkconfig_append": [ + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"", + "CONFIG_USE_EMOTE_MESSAGE_STYLE=y", + "CONFIG_BOARD_TYPE_ECHOEAR=y", + "CONFIG_FLASH_CUSTOM_ASSETS=y", + "CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-echoear.bin\"" + ] } ] } \ No newline at end of file diff --git a/main/boards/echoear/emote.json b/main/boards/echoear/emote.json new file mode 100644 index 00000000..abc11c8f --- /dev/null +++ b/main/boards/echoear/emote.json @@ -0,0 +1,22 @@ +[ + {"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20}, + {"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20}, + {"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20}, + {"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20}, + {"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20}, + {"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20}, + {"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20}, + {"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20}, + {"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20}, + {"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20} +] diff --git a/main/boards/echoear/emote_display.cc b/main/boards/echoear/emote_display.cc deleted file mode 100644 index 095850a0..00000000 --- a/main/boards/echoear/emote_display.cc +++ /dev/null @@ -1,454 +0,0 @@ -#include "emote_display.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "display/lcd_display.h" -#include "config.h" -#include "gfx.h" -#include "application.h" - -namespace anim { - -static const char* TAG = "emoji"; - -// Asset name mapping from the old constants to file names -static const std::unordered_map asset_name_map = { - {"angry_one", "angry_one.aaf"}, - {"dizzy_one", "dizzy_one.aaf"}, - {"enjoy_one", "enjoy_one.aaf"}, - {"happy_one", "happy_one.aaf"}, - {"idle_one", "idle_one.aaf"}, - {"listen", "listen.aaf"}, - {"sad_one", "sad_one.aaf"}, - {"shocked_one", "shocked_one.aaf"}, - {"thinking_one", "thinking_one.aaf"}, - {"icon_battery", "icon_Battery.bin"}, - {"icon_wifi_failed", "icon_WiFi_failed.bin"}, - {"icon_mic", "icon_mic.bin"}, - {"icon_speaker_zzz", "icon_speaker_zzz.bin"}, - {"icon_wifi", "icon_wifi.bin"}, - {"srmodels", "srmodels.bin"}, - {"kaiti", "KaiTi.ttf"} -}; - -// UI element management -static gfx_obj_t* obj_label_tips = nullptr; -static gfx_obj_t* obj_label_time = nullptr; -static gfx_obj_t* obj_anim_eye = nullptr; -static gfx_obj_t* obj_anim_mic = nullptr; -static gfx_obj_t* obj_img_icon = nullptr; -static gfx_image_dsc_t icon_img_dsc; - -// Track current icon to determine when to show time -static std::string current_icon_type = "icon_battery"; - -enum class UIDisplayMode : uint8_t { - SHOW_ANIM_TOP = 1, // Show obj_anim_mic - SHOW_TIME = 2, // Show obj_label_time - SHOW_TIPS = 3 // Show obj_label_tips -}; - -static void SetUIDisplayMode(UIDisplayMode mode) -{ - gfx_obj_set_visible(obj_anim_mic, false); - gfx_obj_set_visible(obj_label_time, false); - gfx_obj_set_visible(obj_label_tips, false); - - // Show the selected control - switch (mode) { - case UIDisplayMode::SHOW_ANIM_TOP: - gfx_obj_set_visible(obj_anim_mic, true); - break; - case UIDisplayMode::SHOW_TIME: - gfx_obj_set_visible(obj_label_time, true); - break; - case UIDisplayMode::SHOW_TIPS: - gfx_obj_set_visible(obj_label_tips, true); - break; - } -} - -static void clock_tm_callback(void* user_data) -{ - // Only display time when battery icon is shown - if (current_icon_type == "icon_battery") { - time_t now; - struct tm timeinfo; - time(&now); - - setenv("TZ", "GMT+0", 1); - tzset(); - localtime_r(&now, &timeinfo); - - char time_str[6]; - snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min); - - gfx_label_set_text(obj_label_time, time_str); - SetUIDisplayMode(UIDisplayMode::SHOW_TIME); - } -} - - -static void InitializeGraphics(esp_lcd_panel_handle_t panel, gfx_handle_t* engine_handle) -{ - gfx_core_config_t gfx_cfg = { - .flush_cb = EmoteEngine::OnFlush, - .user_data = panel, - .flags = { - .swap = true, - .double_buffer = true, - .buff_dma = true, - }, - .h_res = DISPLAY_WIDTH, - .v_res = DISPLAY_HEIGHT, - .fps = 30, - .buffers = { - .buf1 = nullptr, - .buf2 = nullptr, - .buf_pixels = DISPLAY_WIDTH * 16, - }, - .task = GFX_EMOTE_INIT_CONFIG() - }; - - gfx_cfg.task.task_stack_caps = MALLOC_CAP_DEFAULT; - gfx_cfg.task.task_affinity = 1; - gfx_cfg.task.task_priority = 1; - gfx_cfg.task.task_stack = 20 * 1024; - - *engine_handle = gfx_emote_init(&gfx_cfg); -} - -static void InitializeEyeAnimation(gfx_handle_t engine_handle) -{ - obj_anim_eye = gfx_anim_create(engine_handle); - - void* anim_data = nullptr; - size_t anim_size = 0; - auto& assets = Assets::GetInstance(); - if (!assets.GetAssetData(asset_name_map.at("idle_one"), anim_data, anim_size)) { - ESP_LOGE(TAG, "Failed to get idle_one animation data"); - return; - } - - gfx_anim_set_src(obj_anim_eye, anim_data, anim_size); - - gfx_obj_align(obj_anim_eye, GFX_ALIGN_LEFT_MID, 10, -20); - gfx_anim_set_mirror(obj_anim_eye, true, (DISPLAY_WIDTH - (173 + 10) * 2)); - gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, 20, false); - gfx_anim_start(obj_anim_eye); -} - -static void InitializeFont(gfx_handle_t engine_handle) -{ - gfx_font_t font; - void* font_data = nullptr; - size_t font_size = 0; - auto& assets = Assets::GetInstance(); - if (!assets.GetAssetData(asset_name_map.at("kaiti"), font_data, font_size)) { - ESP_LOGE(TAG, "Failed to get kaiti font data"); - return; - } - - gfx_label_cfg_t font_cfg = { - .name = "DejaVuSans.ttf", - .mem = font_data, - .mem_size = font_size, - }; - gfx_label_new_font(engine_handle, &font_cfg, &font); - - ESP_LOGI(TAG, "stack: %d", uxTaskGetStackHighWaterMark(nullptr)); -} - -static void InitializeLabels(gfx_handle_t engine_handle) -{ - // Initialize tips label - obj_label_tips = gfx_label_create(engine_handle); - gfx_obj_align(obj_label_tips, GFX_ALIGN_TOP_MID, 0, 45); - gfx_obj_set_size(obj_label_tips, 160, 40); - gfx_label_set_text(obj_label_tips, "启动中..."); - gfx_label_set_font_size(obj_label_tips, 20); - gfx_label_set_color(obj_label_tips, GFX_COLOR_HEX(0xFFFFFF)); - gfx_label_set_text_align(obj_label_tips, GFX_TEXT_ALIGN_LEFT); - gfx_label_set_long_mode(obj_label_tips, GFX_LABEL_LONG_SCROLL); - gfx_label_set_scroll_speed(obj_label_tips, 20); - gfx_label_set_scroll_loop(obj_label_tips, true); - - // Initialize time label - obj_label_time = gfx_label_create(engine_handle); - gfx_obj_align(obj_label_time, GFX_ALIGN_TOP_MID, 0, 30); - gfx_obj_set_size(obj_label_time, 160, 50); - gfx_label_set_text(obj_label_time, "--:--"); - gfx_label_set_font_size(obj_label_time, 40); - gfx_label_set_color(obj_label_time, GFX_COLOR_HEX(0xFFFFFF)); - gfx_label_set_text_align(obj_label_time, GFX_TEXT_ALIGN_CENTER); -} - -static void InitializeMicAnimation(gfx_handle_t engine_handle) -{ - obj_anim_mic = gfx_anim_create(engine_handle); - gfx_obj_align(obj_anim_mic, GFX_ALIGN_TOP_MID, 0, 25); - - void* anim_data = nullptr; - size_t anim_size = 0; - auto& assets = Assets::GetInstance(); - if (!assets.GetAssetData(asset_name_map.at("listen"), anim_data, anim_size)) { - ESP_LOGE(TAG, "Failed to get listen animation data"); - return; - } - - gfx_anim_set_src(obj_anim_mic, anim_data, anim_size); - gfx_anim_start(obj_anim_mic); - gfx_obj_set_visible(obj_anim_mic, false); -} - -static void InitializeIcon(gfx_handle_t engine_handle) -{ - obj_img_icon = gfx_img_create(engine_handle); - gfx_obj_align(obj_img_icon, GFX_ALIGN_TOP_MID, -100, 38); - - SetupImageDescriptor(&icon_img_dsc, "icon_wifi_failed"); - gfx_img_set_src(obj_img_icon, static_cast(&icon_img_dsc)); -} - -static void RegisterCallbacks(esp_lcd_panel_io_handle_t panel_io, gfx_handle_t engine_handle) -{ - const esp_lcd_panel_io_callbacks_t cbs = { - .on_color_trans_done = EmoteEngine::OnFlushIoReady, - }; - esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, engine_handle); -} - -void SetupImageDescriptor(gfx_image_dsc_t* img_dsc, const std::string& asset_name) -{ - auto& assets = Assets::GetInstance(); - std::string filename = asset_name_map.at(asset_name); - - void* img_data = nullptr; - size_t img_size = 0; - if (!assets.GetAssetData(filename, img_data, img_size)) { - ESP_LOGE(TAG, "Failed to get asset data for %s", asset_name.c_str()); - return; - } - - std::memcpy(&img_dsc->header, img_data, sizeof(gfx_image_header_t)); - img_dsc->data = static_cast(img_data) + sizeof(gfx_image_header_t); - img_dsc->data_size = img_size - sizeof(gfx_image_header_t); -} - -EmoteEngine::EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io) -{ - ESP_LOGI(TAG, "Create EmoteEngine, panel: %p, panel_io: %p", panel, panel_io); - - InitializeGraphics(panel, &engine_handle_); - - gfx_emote_lock(engine_handle_); - gfx_emote_set_bg_color(engine_handle_, GFX_COLOR_HEX(0x000000)); - - // Initialize all UI components - InitializeEyeAnimation(engine_handle_); - InitializeFont(engine_handle_); - InitializeLabels(engine_handle_); - InitializeMicAnimation(engine_handle_); - InitializeIcon(engine_handle_); - - current_icon_type = "icon_wifi_failed"; - SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); - - gfx_timer_create(engine_handle_, clock_tm_callback, 1000, obj_label_tips); - - gfx_emote_unlock(engine_handle_); - - RegisterCallbacks(panel_io, engine_handle_); -} - -EmoteEngine::~EmoteEngine() -{ - if (engine_handle_) { - gfx_emote_deinit(engine_handle_); - engine_handle_ = nullptr; - } -} - -void EmoteEngine::setEyes(const std::string& asset_name, bool repeat, int fps) -{ - if (!engine_handle_) { - return; - } - - auto& assets = Assets::GetInstance(); - std::string filename = asset_name_map.at(asset_name); - - void* src_data = nullptr; - size_t src_len = 0; - if (!assets.GetAssetData(filename, src_data, src_len)) { - ESP_LOGE(TAG, "Failed to get asset data for %s", asset_name.c_str()); - return; - } - - Lock(); - gfx_anim_set_src(obj_anim_eye, src_data, src_len); - gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, fps, repeat); - gfx_anim_start(obj_anim_eye); - Unlock(); -} - -void EmoteEngine::stopEyes() -{ - // Implementation if needed -} - -void EmoteEngine::Lock() -{ - if (engine_handle_) { - gfx_emote_lock(engine_handle_); - } -} - -void EmoteEngine::Unlock() -{ - if (engine_handle_) { - gfx_emote_unlock(engine_handle_); - } -} - -void EmoteEngine::SetIcon(const std::string& asset_name) -{ - if (!engine_handle_) { - return; - } - - Lock(); - SetupImageDescriptor(&icon_img_dsc, asset_name); - gfx_img_set_src(obj_img_icon, static_cast(&icon_img_dsc)); - current_icon_type = asset_name; - Unlock(); -} - -bool EmoteEngine::OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, - esp_lcd_panel_io_event_data_t* edata, - void* user_ctx) -{ - gfx_emote_flush_ready((gfx_handle_t)user_ctx, true); - return true; -} - -void EmoteEngine::OnFlush(gfx_handle_t handle, int x_start, int y_start, - int x_end, int y_end, const void* color_data) -{ - auto* panel = static_cast(gfx_emote_get_user_data(handle)); - if (panel) { - esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data); - } -} - -// EmoteDisplay implementation -EmoteDisplay::EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io) -{ - InitializeEngine(panel, panel_io); -} - -EmoteDisplay::~EmoteDisplay() = default; - -void EmoteDisplay::SetEmotion(const char* emotion) -{ - if (!engine_) { - return; - } - - using EmotionParam = std::tuple; - static const std::unordered_map emotion_map = { - {"happy", {"happy_one", true, 20}}, - {"laughing", {"enjoy_one", true, 20}}, - {"funny", {"happy_one", true, 20}}, - {"loving", {"happy_one", true, 20}}, - {"embarrassed", {"happy_one", true, 20}}, - {"confident", {"happy_one", true, 20}}, - {"delicious", {"happy_one", true, 20}}, - {"sad", {"sad_one", true, 20}}, - {"crying", {"happy_one", true, 20}}, - {"sleepy", {"happy_one", true, 20}}, - {"silly", {"happy_one", true, 20}}, - {"angry", {"angry_one", true, 20}}, - {"surprised", {"happy_one", true, 20}}, - {"shocked", {"shocked_one", true, 20}}, - {"thinking", {"thinking_one", true, 20}}, - {"winking", {"happy_one", true, 20}}, - {"relaxed", {"happy_one", true, 20}}, - {"confused", {"dizzy_one", true, 20}}, - {"neutral", {"idle_one", false, 20}}, - {"idle", {"idle_one", false, 20}}, - }; - - auto it = emotion_map.find(emotion); - if (it != emotion_map.end()) { - std::string asset_name = std::get<0>(it->second); - bool repeat = std::get<1>(it->second); - int fps = std::get<2>(it->second); - engine_->setEyes(asset_name, repeat, fps); - } -} - -void EmoteDisplay::SetChatMessage(const char* role, const char* content) -{ - engine_->Lock(); - if (content && strlen(content) > 0) { - gfx_label_set_text(obj_label_tips, content); - SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); - } - engine_->Unlock(); -} - -void EmoteDisplay::SetStatus(const char* status) -{ - if (!engine_) { - return; - } - - if (std::strcmp(status, "聆听中...") == 0) { - SetUIDisplayMode(UIDisplayMode::SHOW_ANIM_TOP); - engine_->setEyes("happy_one", true, 20); - engine_->SetIcon("icon_mic"); - } else if (std::strcmp(status, "待命") == 0) { - SetUIDisplayMode(UIDisplayMode::SHOW_TIME); - engine_->SetIcon("icon_battery"); - } else if (std::strcmp(status, "说话中...") == 0) { - SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); - engine_->SetIcon("icon_speaker_zzz"); - } else if (std::strcmp(status, "错误") == 0) { - SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); - engine_->SetIcon("icon_wifi_failed"); - } - - engine_->Lock(); - if (std::strcmp(status, "连接中...") != 0) { - gfx_label_set_text(obj_label_tips, status); - } - engine_->Unlock(); -} - -void EmoteDisplay::InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io) -{ - engine_ = std::make_unique(panel, panel_io); -} - -bool EmoteDisplay::Lock(int timeout_ms) -{ - return true; -} - -void EmoteDisplay::Unlock() -{ - // Implementation if needed -} - -} // namespace anim diff --git a/main/boards/echoear/emote_display.h b/main/boards/echoear/emote_display.h deleted file mode 100644 index d3020a25..00000000 --- a/main/boards/echoear/emote_display.h +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include "display/lcd_display.h" -#include -#include -#include -#include -#include "gfx.h" -#include "assets.h" - -namespace anim { - -// Helper function for setting up image descriptors -void SetupImageDescriptor(gfx_image_dsc_t* img_dsc, const std::string& asset_name); - -class EmoteEngine; - -using FlushIoReadyCallback = std::function; -using FlushCallback = std::function; - -class EmoteEngine { -public: - EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io); - ~EmoteEngine(); - - void setEyes(const std::string& asset_name, bool repeat, int fps); - void stopEyes(); - - void Lock(); - void Unlock(); - - void SetIcon(const std::string& asset_name); - - // Callback functions (public to be accessible from static helper functions) - static bool OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx); - static void OnFlush(gfx_handle_t handle, int x_start, int y_start, int x_end, int y_end, const void *color_data); - -private: - gfx_handle_t engine_handle_; -}; - -class EmoteDisplay : public Display { -public: - EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io); - virtual ~EmoteDisplay(); - - virtual void SetEmotion(const char* emotion) override; - virtual void SetStatus(const char* status) override; - virtual void SetChatMessage(const char* role, const char* content) override; - - anim::EmoteEngine* GetEngine() - { - return engine_.get(); - } - -private: - void InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io); - virtual bool Lock(int timeout_ms = 0) override; - virtual void Unlock() override; - - std::unique_ptr engine_; -}; - -} // namespace anim diff --git a/main/boards/echoear/layout.json b/main/boards/echoear/layout.json new file mode 100644 index 00000000..f9169f6e --- /dev/null +++ b/main/boards/echoear/layout.json @@ -0,0 +1,37 @@ +[ + { + "name": "eye_anim", + "align": "GFX_ALIGN_LEFT_MID", + "x": 10, + "y": 10 + }, + { + "name": "status_icon", + "align": "GFX_ALIGN_TOP_MID", + "x": -100, + "y": 38 + }, + { + "name": "toast_label", + "align": "GFX_ALIGN_TOP_MID", + "x": 0, + "y": 40, + "width": 160, + "height": 40 + }, + { + "name": "clock_label", + "align": "GFX_ALIGN_TOP_MID", + "x": 0, + "y": 40, + "width": 60, + "height": 50 + }, + { + "name": "listen_anim", + "align": "GFX_ALIGN_TOP_MID", + "x": 0, + "y": 25 + } +] + \ No newline at end of file diff --git a/main/boards/esp-box-3/config.json b/main/boards/esp-box-3/config.json index 67ead9b9..5c1c53c8 100644 --- a/main/boards/esp-box-3/config.json +++ b/main/boards/esp-box-3/config.json @@ -4,7 +4,12 @@ { "name": "esp-box-3", "sdkconfig_append": [ - "CONFIG_USE_DEVICE_AEC=y" + "CONFIG_USE_DEVICE_AEC=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"", + "CONFIG_USE_EMOTE_MESSAGE_STYLE=y", + "CONFIG_BOARD_TYPE_ESP_BOX_3=y", + "CONFIG_FLASH_CUSTOM_ASSETS=y", + "CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-esp-box-3.bin\"" ] } ] diff --git a/main/boards/esp-box-3/emote.json b/main/boards/esp-box-3/emote.json new file mode 100644 index 00000000..abc11c8f --- /dev/null +++ b/main/boards/esp-box-3/emote.json @@ -0,0 +1,22 @@ +[ + {"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20}, + {"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20}, + {"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20}, + {"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20}, + {"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20}, + {"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20}, + {"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20}, + {"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20}, + {"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20}, + {"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20}, + {"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20} +] diff --git a/main/boards/esp-box-3/esp_box3_board.cc b/main/boards/esp-box-3/esp_box3_board.cc index 47f964f2..3f499597 100644 --- a/main/boards/esp-box-3/esp_box3_board.cc +++ b/main/boards/esp-box-3/esp_box3_board.cc @@ -1,5 +1,7 @@ #include "wifi_board.h" #include "codecs/box_audio_codec.h" +#include "display/display.h" +#include "display/emote_display.h" #include "display/lcd_display.h" #include "esp_lcd_ili9341.h" #include "application.h" @@ -39,7 +41,7 @@ class EspBox3Board : public WifiBoard { private: i2c_master_bus_handle_t i2c_bus_; Button boot_button_; - LcdDisplay* display_; + Display* display_; void InitializeI2c() { // Initialize I2C peripheral @@ -125,8 +127,13 @@ private: esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); esp_lcd_panel_disp_on_off(panel, true); - display_ = new SpiLcdDisplay(panel_io, panel, - DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + +#if CONFIG_USE_EMOTE_MESSAGE_STYLE + display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT); +#else + display_ = new SpiLcdDisplay(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); +#endif } public: diff --git a/main/boards/esp-box-3/layout.json b/main/boards/esp-box-3/layout.json new file mode 100644 index 00000000..8a1dfbcc --- /dev/null +++ b/main/boards/esp-box-3/layout.json @@ -0,0 +1,37 @@ +[ + { + "name": "eye_anim", + "align": "GFX_ALIGN_LEFT_MID", + "x": 10, + "y": 30 + }, + { + "name": "status_icon", + "align": "GFX_ALIGN_TOP_MID", + "x": -120, + "y": 18 + }, + { + "name": "toast_label", + "align": "GFX_ALIGN_TOP_MID", + "x": 0, + "y": 20, + "width": 200, + "height": 40 + }, + { + "name": "clock_label", + "align": "GFX_ALIGN_TOP_MID", + "x": 0, + "y": 20, + "width": 200, + "height": 50 + }, + { + "name": "listen_anim", + "align": "GFX_ALIGN_TOP_MID", + "x": 0, + "y": 5 + } +] + \ No newline at end of file diff --git a/main/display/display.h b/main/display/display.h index f577b267..b24da4eb 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -3,7 +3,7 @@ #include "emoji_collection.h" -#ifdef LVGL_VERSION_MAJOR +#ifndef CONFIG_USE_EMOTE_MESSAGE_STYLE #define HAVE_LVGL 1 #include #endif diff --git a/main/display/emote_display.cc b/main/display/emote_display.cc new file mode 100644 index 00000000..5bed5fae --- /dev/null +++ b/main/display/emote_display.cc @@ -0,0 +1,652 @@ +#include "emote_display.h" + +// Standard C++ headers +#include +#include +#include +#include + +// Standard C headers +#include +#include + +// ESP-IDF headers +#include +#include +#include + +// FreeRTOS headers +#include +#include + +// Project headers +#include "assets.h" +#include "assets/lang_config.h" +#include "board.h" +#include "gfx.h" + +LV_FONT_DECLARE(BUILTIN_TEXT_FONT); + +namespace emote { + +// ============================================================================ +// Constants and Type Definitions +// ============================================================================ + +static const char* TAG = "EmoteDisplay"; + +// UI Element Names - Centralized Management +#define UI_ELEMENT_EYE_ANIM "eye_anim" +#define UI_ELEMENT_TOAST_LABEL "toast_label" +#define UI_ELEMENT_CLOCK_LABEL "clock_label" +#define UI_ELEMENT_LISTEN_ANIM "listen_anim" +#define UI_ELEMENT_STATUS_ICON "status_icon" + +// Icon Names - Centralized Management +#define ICON_MIC "icon_mic" +#define ICON_BATTERY "icon_Battery" +#define ICON_SPEAKER_ZZZ "icon_speaker_zzz" +#define ICON_WIFI_FAILED "icon_WiFi_failed" +#define ICON_WIFI_OK "icon_wifi" +#define ICON_LISTEN "listen" + +using FlushIoReadyCallback = std::function; +using FlushCallback = std::function; + +// ============================================================================ +// Global Variables +// ============================================================================ + +// UI element management +static gfx_obj_t* g_obj_label_toast = nullptr; +static gfx_obj_t* g_obj_label_clock = nullptr; +static gfx_obj_t* g_obj_anim_eye = nullptr; +static gfx_obj_t* g_obj_anim_listen = nullptr; +static gfx_obj_t* g_obj_img_status = nullptr; + +// Track current icon to determine when to show time +static std::string g_current_icon_type = ICON_WIFI_FAILED; +static gfx_image_dsc_t g_icon_img_dsc; + + +// ============================================================================ +// Forward Declarations +// ============================================================================ + +class EmoteDisplay; +class EmoteEngine; + +enum class UIDisplayMode : uint8_t { + SHOW_LISTENING = 1, // Show g_obj_anim_listen + SHOW_TIME = 2, // Show g_obj_label_clock + SHOW_TIPS = 3 // Show g_obj_label_toast +}; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +// Function to convert align string to GFX_ALIGN enum value +char StringToGfxAlign(const std::string &align_str) +{ + static const std::unordered_map align_map = { + {"GFX_ALIGN_DEFAULT", GFX_ALIGN_DEFAULT}, + {"GFX_ALIGN_TOP_LEFT", GFX_ALIGN_TOP_LEFT}, + {"GFX_ALIGN_TOP_MID", GFX_ALIGN_TOP_MID}, + {"GFX_ALIGN_TOP_RIGHT", GFX_ALIGN_TOP_RIGHT}, + {"GFX_ALIGN_LEFT_MID", GFX_ALIGN_LEFT_MID}, + {"GFX_ALIGN_CENTER", GFX_ALIGN_CENTER}, + {"GFX_ALIGN_RIGHT_MID", GFX_ALIGN_RIGHT_MID}, + {"GFX_ALIGN_BOTTOM_LEFT", GFX_ALIGN_BOTTOM_LEFT}, + {"GFX_ALIGN_BOTTOM_MID", GFX_ALIGN_BOTTOM_MID}, + {"GFX_ALIGN_BOTTOM_RIGHT", GFX_ALIGN_BOTTOM_RIGHT}, + {"GFX_ALIGN_OUT_TOP_LEFT", GFX_ALIGN_OUT_TOP_LEFT}, + {"GFX_ALIGN_OUT_TOP_MID", GFX_ALIGN_OUT_TOP_MID}, + {"GFX_ALIGN_OUT_TOP_RIGHT", GFX_ALIGN_OUT_TOP_RIGHT}, + {"GFX_ALIGN_OUT_LEFT_TOP", GFX_ALIGN_OUT_LEFT_TOP}, + {"GFX_ALIGN_OUT_LEFT_MID", GFX_ALIGN_OUT_LEFT_MID}, + {"GFX_ALIGN_OUT_LEFT_BOTTOM", GFX_ALIGN_OUT_LEFT_BOTTOM}, + {"GFX_ALIGN_OUT_RIGHT_TOP", GFX_ALIGN_OUT_RIGHT_TOP}, + {"GFX_ALIGN_OUT_RIGHT_MID", GFX_ALIGN_OUT_RIGHT_MID}, + {"GFX_ALIGN_OUT_RIGHT_BOTTOM", GFX_ALIGN_OUT_RIGHT_BOTTOM}, + {"GFX_ALIGN_OUT_BOTTOM_LEFT", GFX_ALIGN_OUT_BOTTOM_LEFT}, + {"GFX_ALIGN_OUT_BOTTOM_MID", GFX_ALIGN_OUT_BOTTOM_MID}, + {"GFX_ALIGN_OUT_BOTTOM_RIGHT", GFX_ALIGN_OUT_BOTTOM_RIGHT} + }; + + const auto it = align_map.find(align_str); + if (it != align_map.cend()) { + return it->second; + } + + ESP_LOGW(TAG, "Unknown align string: %s, using GFX_ALIGN_DEFAULT", align_str.c_str()); + return GFX_ALIGN_DEFAULT; +} + +// ============================================================================ +// EmoteEngine Class Declaration +// ============================================================================ + +class EmoteEngine { +public: + EmoteEngine(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io, + const int width, const int height, EmoteDisplay* const display); + ~EmoteEngine(); + + void SetEyes(const std::string &emoji_name, const bool repeat, const int fps, EmoteDisplay* const display); + void SetIcon(const std::string &icon_name, EmoteDisplay* const display); + + void* GetEngineHandle() const + { + return engine_handle_; + } + + // Callback functions (public to be accessible from static helper functions) + static bool OnFlushIoReady(const esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t* const edata, void* const user_ctx); + static void OnFlush(const gfx_handle_t handle, const int x_start, const int y_start, const int x_end, const int y_end, const void* const color_data); + +private: + gfx_handle_t engine_handle_; +}; + +// ============================================================================ +// UI Management Functions +// ============================================================================ + +static void SetUIDisplayMode(const UIDisplayMode mode, EmoteDisplay* const display) +{ + if (!display) { + ESP_LOGE(TAG, "SetUIDisplayMode: display is nullptr"); + return; + } + + gfx_obj_set_visible(g_obj_anim_listen, false); + gfx_obj_set_visible(g_obj_label_clock, false); + gfx_obj_set_visible(g_obj_label_toast, false); + + // Show the selected control + switch (mode) { + case UIDisplayMode::SHOW_LISTENING: { + gfx_obj_set_visible(g_obj_anim_listen, true); + const AssetData emoji_data = display->GetIconData(ICON_LISTEN); + if (emoji_data.data) { + gfx_anim_set_src(g_obj_anim_listen, emoji_data.data, emoji_data.size); + gfx_anim_set_segment(g_obj_anim_listen, 0, 0xFFFF, 20, true); + gfx_anim_start(g_obj_anim_listen); + } + break; + } + case UIDisplayMode::SHOW_TIME: + gfx_obj_set_visible(g_obj_label_clock, true); + break; + case UIDisplayMode::SHOW_TIPS: + gfx_obj_set_visible(g_obj_label_toast, true); + break; + } +} + +// ============================================================================ +// Graphics Initialization Functions +// ============================================================================ + +static void InitializeGraphics(const esp_lcd_panel_handle_t panel, gfx_handle_t* const engine_handle, + const int width, const int height) +{ + if (!panel || !engine_handle) { + ESP_LOGE(TAG, "InitializeGraphics: Invalid parameters"); + return; + } + + gfx_core_config_t gfx_cfg = { + .flush_cb = EmoteEngine::OnFlush, + .user_data = panel, + .flags = { + .swap = true, + .double_buffer = true, + .buff_dma = true, + }, + .h_res = static_cast(width), + .v_res = static_cast(height), + .fps = 30, + .buffers = { + .buf1 = nullptr, + .buf2 = nullptr, + .buf_pixels = static_cast(width * 16), + }, + .task = GFX_EMOTE_INIT_CONFIG() + }; + + gfx_cfg.task.task_stack_caps = MALLOC_CAP_DEFAULT; + gfx_cfg.task.task_affinity = 0; + gfx_cfg.task.task_priority = 5; + gfx_cfg.task.task_stack = 8 * 1024; + + *engine_handle = gfx_emote_init(&gfx_cfg); +} + +static void SetupUI(const gfx_handle_t engine_handle, EmoteDisplay* const display) +{ + if (!display) { + ESP_LOGE(TAG, "SetupUI: display is nullptr"); + return; + } + + gfx_emote_set_bg_color(engine_handle, GFX_COLOR_HEX(0x000000)); + + g_obj_anim_eye = gfx_anim_create(engine_handle); + gfx_obj_align(g_obj_anim_eye, GFX_ALIGN_LEFT_MID, 10, 30); + gfx_anim_set_auto_mirror(g_obj_anim_eye, true); + + g_obj_label_toast = gfx_label_create(engine_handle); + gfx_obj_align(g_obj_label_toast, GFX_ALIGN_TOP_MID, 0, 20); + gfx_obj_set_size(g_obj_label_toast, 200, 40); + gfx_label_set_text(g_obj_label_toast, Lang::Strings::INITIALIZING); + gfx_label_set_color(g_obj_label_toast, GFX_COLOR_HEX(0xFFFFFF)); + gfx_label_set_text_align(g_obj_label_toast, GFX_TEXT_ALIGN_CENTER); + gfx_label_set_long_mode(g_obj_label_toast, GFX_LABEL_LONG_SCROLL); + gfx_label_set_scroll_speed(g_obj_label_toast, 20); + gfx_label_set_scroll_loop(g_obj_label_toast, true); + gfx_label_set_font(g_obj_label_toast, (gfx_font_t)&BUILTIN_TEXT_FONT); + + g_obj_label_clock = gfx_label_create(engine_handle); + gfx_obj_align(g_obj_label_clock, GFX_ALIGN_TOP_MID, 0, 15); + gfx_obj_set_size(g_obj_label_clock, 200, 50); + gfx_label_set_text(g_obj_label_clock, "--:--"); + gfx_label_set_color(g_obj_label_clock, GFX_COLOR_HEX(0xFFFFFF)); + gfx_label_set_text_align(g_obj_label_clock, GFX_TEXT_ALIGN_CENTER); + gfx_label_set_font(g_obj_label_clock, (gfx_font_t)&BUILTIN_TEXT_FONT); + + g_obj_anim_listen = gfx_anim_create(engine_handle); + gfx_obj_align(g_obj_anim_listen, GFX_ALIGN_TOP_MID, 0, 5); + gfx_anim_start(g_obj_anim_listen); + gfx_obj_set_visible(g_obj_anim_listen, false); + + g_obj_img_status = gfx_img_create(engine_handle); + gfx_obj_align(g_obj_img_status, GFX_ALIGN_TOP_MID, -120, 18); + + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, display); +} + +static void RegisterCallbacks(const esp_lcd_panel_io_handle_t panel_io, const gfx_handle_t engine_handle) +{ + if (!panel_io) { + ESP_LOGE(TAG, "RegisterCallbacks: panel_io is nullptr"); + return; + } + + const esp_lcd_panel_io_callbacks_t cbs = { + .on_color_trans_done = EmoteEngine::OnFlushIoReady, + }; + esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, engine_handle); +} + +// ============================================================================ +// EmoteEngine Class Implementation +// ============================================================================ + +EmoteEngine::EmoteEngine(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io, + const int width, const int height, EmoteDisplay* const display) +{ + InitializeGraphics(panel, &engine_handle_, width, height); + + if (display) { + DisplayLockGuard lock(display); + SetupUI(engine_handle_, display); + } + + RegisterCallbacks(panel_io, engine_handle_); +} + +EmoteEngine::~EmoteEngine() +{ + if (engine_handle_) { + gfx_emote_deinit(engine_handle_); + engine_handle_ = nullptr; + } +} + +void EmoteEngine::SetEyes(const std::string &emoji_name, const bool repeat, const int fps, EmoteDisplay* const display) +{ + if (!engine_handle_) { + ESP_LOGE(TAG, "SetEyes: engine_handle_ is nullptr"); + return; + } + + if (!display) { + ESP_LOGE(TAG, "SetEyes: display is nullptr"); + return; + } + + const AssetData emoji_data = display->GetEmojiData(emoji_name); + if (emoji_data.data) { + DisplayLockGuard lock(display); + gfx_anim_set_src(g_obj_anim_eye, emoji_data.data, emoji_data.size); + gfx_anim_set_segment(g_obj_anim_eye, 0, 0xFFFF, fps, repeat); + gfx_anim_start(g_obj_anim_eye); + } else { + ESP_LOGW(TAG, "SetEyes: No emoji data found for %s", emoji_name.c_str()); + } +} + +void EmoteEngine::SetIcon(const std::string &icon_name, EmoteDisplay* const display) +{ + if (!engine_handle_) { + ESP_LOGE(TAG, "SetIcon: engine_handle_ is nullptr"); + return; + } + + if (!display) { + ESP_LOGE(TAG, "SetIcon: display is nullptr"); + return; + } + + const AssetData icon_data = display->GetIconData(icon_name); + if (icon_data.data) { + DisplayLockGuard lock(display); + + std::memcpy(&g_icon_img_dsc.header, icon_data.data, sizeof(gfx_image_header_t)); + g_icon_img_dsc.data = static_cast(icon_data.data) + sizeof(gfx_image_header_t); + g_icon_img_dsc.data_size = icon_data.size - sizeof(gfx_image_header_t); + + gfx_img_set_src(g_obj_img_status, &g_icon_img_dsc); + } else { + ESP_LOGW(TAG, "SetIcon: No icon data found for %s", icon_name.c_str()); + } + g_current_icon_type = icon_name; +} + +bool EmoteEngine::OnFlushIoReady(const esp_lcd_panel_io_handle_t panel_io, + esp_lcd_panel_io_event_data_t* const edata, + void* const user_ctx) +{ + return true; +} + +void EmoteEngine::OnFlush(const gfx_handle_t handle, const int x_start, const int y_start, + const int x_end, const int y_end, const void* const color_data) +{ + auto* const panel = static_cast(gfx_emote_get_user_data(handle)); + if (panel) { + esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data); + } + gfx_emote_flush_ready(handle, true); +} + +// ============================================================================ +// EmoteDisplay Class Implementation +// ============================================================================ + +EmoteDisplay::EmoteDisplay(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io, + const int width, const int height) +{ + InitializeEngine(panel, panel_io, width, height); +} + +EmoteDisplay::~EmoteDisplay() = default; + +void EmoteDisplay::SetEmotion(const char* const emotion) +{ + if (!emotion) { + ESP_LOGE(TAG, "SetEmotion: emotion is nullptr"); + return; + } + + ESP_LOGI(TAG, "SetEmotion: %s", emotion); + if (!engine_) { + return; + } + + const AssetData emoji_data = GetEmojiData(emotion); + bool repeat = emoji_data.loop; + int fps = emoji_data.fps > 0 ? emoji_data.fps : 20; + + if (std::strcmp(emotion, "idle") == 0 || std::strcmp(emotion, "neutral") == 0) { + repeat = false; + } + + DisplayLockGuard lock(this); + engine_->SetEyes(emotion, repeat, fps, this); +} + +void EmoteDisplay::SetChatMessage(const char* const role, const char* const content) +{ + if (!engine_) { + return; + } + + DisplayLockGuard lock(this); + if (content && strlen(content) > 0) { + gfx_label_set_text(g_obj_label_toast, content); + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this); + } +} + +void EmoteDisplay::SetStatus(const char* const status) +{ + if (!status) { + ESP_LOGE(TAG, "SetStatus: status is nullptr"); + return; + } + + if (!engine_) { + return; + } + + DisplayLockGuard lock(this); + + if (std::strcmp(status, Lang::Strings::LISTENING) == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_LISTENING, this); + engine_->SetEyes("happy", true, 20, this); + engine_->SetIcon(ICON_MIC, this); + } else if (std::strcmp(status, Lang::Strings::STANDBY) == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_TIME, this); + engine_->SetIcon(ICON_BATTERY, this); + } else if (std::strcmp(status, Lang::Strings::SPEAKING) == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this); + engine_->SetIcon(ICON_SPEAKER_ZZZ, this); + } else if (std::strcmp(status, Lang::Strings::ERROR) == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this); + engine_->SetIcon(ICON_WIFI_FAILED, this); + } + + if (std::strcmp(status, Lang::Strings::CONNECTING) != 0) { + gfx_label_set_text(g_obj_label_toast, status); + } +} + +void EmoteDisplay::ShowNotification(const char* notification, int duration_ms) +{ + if (!notification || !engine_) { + return; + } + ESP_LOGI(TAG, "ShowNotification: %s", notification); + + DisplayLockGuard lock(this); + gfx_label_set_text(g_obj_label_toast, notification); + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this); +} + +void EmoteDisplay::UpdateStatusBar(bool update_all) +{ + if (!engine_) { + return; + } + + // Only display time when battery icon is shown + DisplayLockGuard lock(this); + if (g_current_icon_type == ICON_BATTERY) { + time_t now; + struct tm timeinfo; + time(&now); + + setenv("TZ", "GMT+0", 1); + tzset(); + localtime_r(&now, &timeinfo); + + char time_str[6]; + snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min); + + DisplayLockGuard lock(this); + gfx_label_set_text(g_obj_label_clock, time_str); + SetUIDisplayMode(UIDisplayMode::SHOW_TIME, this); + } +} + +void EmoteDisplay::SetPowerSaveMode(bool on) +{ + if (!engine_) { + return; + } + + DisplayLockGuard lock(this); + ESP_LOGI(TAG, "SetPowerSaveMode: %s", on ? "ON" : "OFF"); + if (on) { + gfx_anim_stop(g_obj_anim_eye); + } else { + gfx_anim_start(g_obj_anim_eye); + } +} + +void EmoteDisplay::SetPreviewImage(const void* image) +{ + if (image) { + ESP_LOGI(TAG, "SetPreviewImage: Preview image not supported, using default icon"); + if (engine_) { + } + } +} + +void EmoteDisplay::SetTheme(Theme* const theme) +{ + ESP_LOGI(TAG, "SetTheme: %p", theme); + +} +void EmoteDisplay::AddEmojiData(const std::string &name, const void* const data, const size_t size, + uint8_t fps, bool loop, bool lack) +{ + emoji_data_map_[name] = AssetData(data, size, fps, loop, lack); + ESP_LOGD(TAG, "Added emoji data: %s, size: %d, fps: %d, loop: %s, lack: %s", + name.c_str(), size, fps, loop ? "true" : "false", lack ? "true" : "false"); + + DisplayLockGuard lock(this); + if (name == "happy") { + engine_->SetEyes("happy", loop, fps > 0 ? fps : 20, this); + } +} + +void EmoteDisplay::AddIconData(const std::string &name, const void* const data, const size_t size) +{ + icon_data_map_[name] = AssetData(data, size); + ESP_LOGD(TAG, "Added icon data: %s, size: %d", name.c_str(), size); + + DisplayLockGuard lock(this); + if (name == ICON_WIFI_FAILED) { + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this); + engine_->SetIcon(ICON_WIFI_FAILED, this); + } +} + +void EmoteDisplay::AddLayoutData(const std::string &name, const std::string &align_str, + const int x, const int y, const int width, const int height) +{ + const char align_enum = StringToGfxAlign(align_str); + ESP_LOGI(TAG, "layout: %-12s | %-20s(%d) | %4d, %4d | %4dx%-4d", + name.c_str(), align_str.c_str(), align_enum, x, y, width, height); + + struct UIElement { + gfx_obj_t* obj; + const char* name; + }; + + const UIElement elements[] = { + {g_obj_anim_eye, UI_ELEMENT_EYE_ANIM}, + {g_obj_label_toast, UI_ELEMENT_TOAST_LABEL}, + {g_obj_label_clock, UI_ELEMENT_CLOCK_LABEL}, + {g_obj_anim_listen, UI_ELEMENT_LISTEN_ANIM}, + {g_obj_img_status, UI_ELEMENT_STATUS_ICON} + }; + + DisplayLockGuard lock(this); + for (const auto &element : elements) { + if (name == element.name && element.obj) { + gfx_obj_align(element.obj, align_enum, x, y); + if (width > 0 && height > 0) { + gfx_obj_set_size(element.obj, width, height); + } + return; + } + } + + ESP_LOGW(TAG, "AddLayoutData: UI element '%s' not found", name.c_str()); +} + +void EmoteDisplay::AddTextFont(std::shared_ptr text_font) +{ + if (!text_font) { + ESP_LOGW(TAG, "AddTextFont: text_font is nullptr"); + return; + } + + text_font_ = text_font; + ESP_LOGD(TAG, "AddTextFont: Text font added successfully"); + + DisplayLockGuard lock(this); + if (g_obj_label_toast && text_font_) { + gfx_label_set_font(g_obj_label_toast, const_cast(static_cast(text_font_->font()))); + } + if (g_obj_label_clock && text_font_) { + gfx_label_set_font(g_obj_label_clock, const_cast(static_cast(text_font_->font()))); + } +} + +AssetData EmoteDisplay::GetEmojiData(const std::string &name) const +{ + const auto it = emoji_data_map_.find(name); + if (it != emoji_data_map_.cend()) { + return it->second; + } + return AssetData(); +} + +AssetData EmoteDisplay::GetIconData(const std::string &name) const +{ + const auto it = icon_data_map_.find(name); + if (it != icon_data_map_.cend()) { + return it->second; + } + return AssetData(); +} + +EmoteEngine* EmoteDisplay::GetEngine() const +{ + return engine_.get(); +} + +void* EmoteDisplay::GetEngineHandle() const +{ + return engine_ ? engine_->GetEngineHandle() : nullptr; +} + +void EmoteDisplay::InitializeEngine(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io, + const int width, const int height) +{ + engine_ = std::make_unique(panel, panel_io, width, height, this); +} + +bool EmoteDisplay::Lock(const int timeout_ms) +{ + if (engine_ && engine_->GetEngineHandle()) { + gfx_emote_lock(engine_->GetEngineHandle()); + return true; + } + return false; +} + +void EmoteDisplay::Unlock() +{ + if (engine_ && engine_->GetEngineHandle()) { + gfx_emote_unlock(engine_->GetEngineHandle()); + } +} + +} // namespace emote \ No newline at end of file diff --git a/main/display/emote_display.h b/main/display/emote_display.h new file mode 100644 index 00000000..ffa02a4c --- /dev/null +++ b/main/display/emote_display.h @@ -0,0 +1,102 @@ +#pragma once + +#include "display.h" +#include "lvgl_font.h" +#include +#include +#include +#include +#include +#include + +namespace emote { + +// Simple data structure for storing asset data without LVGL dependency +struct AssetData { + const void* data; + size_t size; + union { + uint8_t flags; // 1 byte for all animation flags + struct { + uint8_t fps : 6; // FPS (0-63) - 6 bits + uint8_t loop : 1; // Loop animation - 1 bit + uint8_t lack : 1; // Lack animation - 1 bit + }; + }; + + AssetData() : data(nullptr), size(0), flags(0) {} + AssetData(const void* d, size_t s) : data(d), size(s), flags(0) {} + AssetData(const void* d, size_t s, uint8_t f, bool l, bool k) + : data(d), size(s) + { + fps = f > 63 ? 63 : f; // 限制 FPS 到 6 位范围 + loop = l; + lack = k; + } +}; + +// Layout element data structure +struct LayoutData { + char align; // Store as char instead of string + int x; + int y; + int width; + int height; + bool has_size; + + LayoutData() : align(0), x(0), y(0), width(0), height(0), has_size(false) {} + LayoutData(char a, int x_pos, int y_pos, int w = 0, int h = 0) + : align(a), x(x_pos), y(y_pos), width(w), height(h), has_size(w > 0 && h > 0) {} +}; + +// Function to convert align string to GFX_ALIGN enum value +char StringToGfxAlign(const std::string &align_str); + +class EmoteEngine; + +class EmoteDisplay : public Display { +public: + EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io, int width, int height); + virtual ~EmoteDisplay(); + + virtual void SetEmotion(const char* emotion) override; + virtual void SetStatus(const char* status) override; + virtual void SetChatMessage(const char* role, const char* content) override; + virtual void SetTheme(Theme* theme) override; + virtual void ShowNotification(const char* notification, int duration_ms = 3000) override; + virtual void UpdateStatusBar(bool update_all = false) override; + virtual void SetPowerSaveMode(bool on) override; + virtual void SetPreviewImage(const void* image); + + void AddEmojiData(const std::string &name, const void* data, size_t size, uint8_t fps = 0, bool loop = false, bool lack = false); + void AddIconData(const std::string &name, const void* data, size_t size); + void AddLayoutData(const std::string &name, const std::string &align_str, int x, int y, int width = 0, int height = 0); + void AddTextFont(std::shared_ptr text_font); + AssetData GetEmojiData(const std::string &name) const; + AssetData GetIconData(const std::string &name) const; + + EmoteEngine* GetEngine() const; + void* GetEngineHandle() const; + + inline std::shared_ptr text_font() const + { + return text_font_; + } + +private: + void InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io, int width, int height); + virtual bool Lock(int timeout_ms = 0) override; + virtual void Unlock() override; + + std::unique_ptr engine_; + + // Font management + std::shared_ptr text_font_ = nullptr; + + // Non-LVGL asset data storage + std::map emoji_data_map_; + std::map icon_data_map_; + +}; + +} // namespace emote diff --git a/main/idf_component.yml b/main/idf_component.yml index 4091f72c..2de024f1 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -32,7 +32,7 @@ dependencies: esp_lvgl_port: ~2.6.0 espressif/esp_io_expander_tca95xx_16bit: ^2.0.0 espressif2022/image_player: ==1.1.0~1 - espressif2022/esp_emote_gfx: ==1.0.0~2 + espressif2022/esp_emote_gfx: ^1.1.0 espressif/adc_mic: ^0.2.1 espressif/esp_mmap_assets: '>=1.2' txp666/otto-emoji-gif-component: ~1.0.2 diff --git a/scripts/spiffs_assets/build.py b/scripts/spiffs_assets/build.py old mode 100755 new mode 100644 index 9745566f..47069d23 --- a/scripts/spiffs_assets/build.py +++ b/scripts/spiffs_assets/build.py @@ -113,8 +113,170 @@ def process_emoji_collection(emoji_collection_dir, assets_dir): return emoji_list +def load_emoji_config(emoji_collection_dir): + """Load emoji config from config.json file""" + config_path = os.path.join(emoji_collection_dir, "emote.json") + if not os.path.exists(config_path): + print(f"Warning: Config file not found: {config_path}") + return {} + + try: + with open(config_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + + # Convert list format to dict for easy lookup + config_dict = {} + for item in config_data: + if "emote" in item: + config_dict[item["emote"]] = item + + return config_dict + except Exception as e: + print(f"Error loading config file {config_path}: {e}") + return {} -def generate_index_json(assets_dir, srmodels, text_font, emoji_collection): +def process_board_emoji_collection(emoji_collection_dir, target_board_dir, assets_dir): + """Process emoji_collection parameter""" + if not emoji_collection_dir: + return [] + + emoji_config = load_emoji_config(target_board_dir) + print(f"Loaded emoji config with {len(emoji_config)} entries") + + emoji_list = [] + + for emote_name, config in emoji_config.items(): + + if "src" not in config: + print(f"Error: No src field found for emote '{emote_name}' in config") + continue + + eaf_file_path = os.path.join(emoji_collection_dir, config["src"]) + file_exists = os.path.exists(eaf_file_path) + + if not file_exists: + print(f"Warning: EAF file not found for emote '{emote_name}': {eaf_file_path}") + else: + # Copy eaf file to assets directory + copy_file(eaf_file_path, os.path.join(assets_dir, config["src"])) + + # Create emoji entry with src as file (merge file and src) + emoji_entry = { + "name": emote_name, + "file": config["src"] # Use src as the actual file + } + + eaf_properties = {} + + if not file_exists: + eaf_properties["lack"] = True + + if "loop" in config: + eaf_properties["loop"] = config["loop"] + + if "fps" in config: + eaf_properties["fps"] = config["fps"] + + if eaf_properties: + emoji_entry["eaf"] = eaf_properties + + status = "MISSING" if not file_exists else "OK" + eaf_info = emoji_entry.get('eaf', {}) + print(f"emote '{emote_name}': file='{emoji_entry['file']}', status={status}, lack={eaf_info.get('lack', False)}, loop={eaf_info.get('loop', 'none')}, fps={eaf_info.get('fps', 'none')}") + + emoji_list.append(emoji_entry) + + print(f"Successfully processed {len(emoji_list)} emotes from config") + return emoji_list + +def process_board_icon_collection(icon_collection_dir, assets_dir): + """Process emoji_collection parameter""" + if not icon_collection_dir: + return [] + + icon_list = [] + + for root, dirs, files in os.walk(icon_collection_dir): + for file in files: + if file.lower().endswith(('.bin')) or file.lower() == 'listen.eaf': + src_file = os.path.join(root, file) + dst_file = os.path.join(assets_dir, file) + copy_file(src_file, dst_file) + + filename_without_ext = os.path.splitext(file)[0] + + icon_list.append({ + "name": filename_without_ext, + "file": file + }) + + return icon_list +def process_board_layout(layout_json_file, assets_dir): + """Process layout_json parameter""" + if not layout_json_file: + print(f"Warning: Layout json file not provided") + return [] + + print(f"Processing layout_json: {layout_json_file}") + print(f"assets_dir: {assets_dir}") + + if os.path.isdir(layout_json_file): + layout_json_path = os.path.join(layout_json_file, "layout.json") + if not os.path.exists(layout_json_path): + print(f"Warning: layout.json not found in directory: {layout_json_file}") + return [] + layout_json_file = layout_json_path + elif not os.path.isfile(layout_json_file): + print(f"Warning: Layout json file not found: {layout_json_file}") + return [] + + try: + with open(layout_json_file, 'r', encoding='utf-8') as f: + layout_data = json.load(f) + + # Layout data is now directly an array, no need to get "layout" key + layout_items = layout_data if isinstance(layout_data, list) else layout_data.get("layout", []) + + processed_layout = [] + for item in layout_items: + processed_item = { + "name": item.get("name", ""), + "align": item.get("align", ""), + "x": item.get("x", 0), + "y": item.get("y", 0) + } + + if "width" in item: + processed_item["width"] = item["width"] + if "height" in item: + processed_item["height"] = item["height"] + + processed_layout.append(processed_item) + + print(f"Processed {len(processed_layout)} layout elements") + return processed_layout + + except Exception as e: + print(f"Error reading/processing layout.json: {e}") + return [] + +def process_board_collection(target_board_dir, res_path, assets_dir): + """Process board collection - merge icon, emoji, and layout processing""" + + # Process all collections + if os.path.exists(res_path) and os.path.exists(target_board_dir): + emoji_collection = process_board_emoji_collection(res_path, target_board_dir, assets_dir) + icon_collection = process_board_icon_collection(res_path, assets_dir) + layout_json = process_board_layout(target_board_dir, assets_dir) + else: + print(f"Warning: EAF directory not found: {res_path} or {target_board_dir}") + emoji_collection = [] + icon_collection = [] + layout_json = [] + + return emoji_collection, icon_collection, layout_json + +def generate_index_json(assets_dir, srmodels, text_font, emoji_collection, icon_collection, layout_json): """Generate index.json file""" index_data = { "version": 1 @@ -128,6 +290,12 @@ def generate_index_json(assets_dir, srmodels, text_font, emoji_collection): if emoji_collection: index_data["emoji_collection"] = emoji_collection + + if icon_collection: + index_data["icon_collection"] = icon_collection + + if layout_json: + index_data["layout"] = layout_json # Write index.json index_path = os.path.join(assets_dir, "index.json") @@ -148,7 +316,7 @@ def generate_config_json(build_dir, assets_dir): "image_file": os.path.join(workspace_dir, "build/output/assets.bin"), "lvgl_ver": "9.3.0", "assets_size": "0x400000", - "support_format": ".png, .gif, .jpg, .bin, .json", + "support_format": ".png, .gif, .jpg, .bin, .json, .eaf", "name_length": "32", "split_height": "0", "support_qoi": False, @@ -174,6 +342,9 @@ def main(): parser.add_argument('--wakenet_model', help='Path to wakenet model directory') parser.add_argument('--text_font', help='Path to text font file') parser.add_argument('--emoji_collection', help='Path to emoji collection directory') + + parser.add_argument('--res_path', help='Path to res directory') + parser.add_argument('--target_board', help='Path to target board directory') args = parser.parse_args() @@ -195,10 +366,16 @@ def main(): # Process each parameter srmodels = process_wakenet_model(args.wakenet_model, build_dir, assets_dir) text_font = process_text_font(args.text_font, assets_dir) - emoji_collection = process_emoji_collection(args.emoji_collection, assets_dir) + + if(args.target_board): + emoji_collection, icon_collection, layout_json = process_board_collection(args.target_board, args.res_path, assets_dir) + else: + emoji_collection = process_emoji_collection(args.emoji_collection, assets_dir) + icon_collection = [] + layout_json = [] # Generate index.json - generate_index_json(assets_dir, srmodels, text_font, emoji_collection) + generate_index_json(assets_dir, srmodels, text_font, emoji_collection, icon_collection, layout_json) # Generate config.json config_path = generate_config_json(build_dir, assets_dir) diff --git a/scripts/spiffs_assets/build_all.py b/scripts/spiffs_assets/build_all.py index e1581229..e812f8d0 100644 --- a/scripts/spiffs_assets/build_all.py +++ b/scripts/spiffs_assets/build_all.py @@ -31,7 +31,7 @@ def get_file_path(base_dir, filename): return os.path.join(base_dir, f"{filename}.bin" if not filename.startswith("emojis_") else filename) -def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_dir): +def build_assets(wakenet_model, text_font, emoji_collection, target_board, build_dir, final_dir): """Build assets.bin using build.py with given parameters""" # Prepare arguments for build.py @@ -42,14 +42,21 @@ def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_di cmd.extend(["--wakenet_model", wakenet_path]) if text_font != "none": - text_font_path = os.path.join("../../components/xiaozhi-fonts/build", f"{text_font}.bin") + text_font_path = os.path.join("../../components/78__xiaozhi-fonts/cbin", f"{text_font}.bin") cmd.extend(["--text_font", text_font_path]) if emoji_collection != "none": emoji_path = os.path.join("../../components/xiaozhi-fonts/build", emoji_collection) cmd.extend(["--emoji_collection", emoji_path]) + + if target_board != "none": + res_path = os.path.join("../../managed_components/espressif2022__esp_emote_gfx/emoji_large", "") + cmd.extend(["--res_path", res_path]) + + target_board_path = os.path.join("../../main/boards/", f"{target_board}") + cmd.extend(["--target_board", target_board_path]) - print(f"\n正在构建: {wakenet_model}-{text_font}-{emoji_collection}") + print(f"\n正在构建: {wakenet_model}-{text_font}-{emoji_collection}-{target_board}") print(f"执行命令: {' '.join(cmd)}") try: @@ -57,7 +64,10 @@ def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_di result = subprocess.run(cmd, check=True, cwd=os.path.dirname(__file__)) # Generate output filename - output_name = f"{wakenet_model}-{text_font}-{emoji_collection}.bin" + if(target_board != "none"): + output_name = f"{wakenet_model}-{text_font}-{target_board}.bin" + else: + output_name = f"{wakenet_model}-{text_font}-{emoji_collection}.bin" # Copy generated assets.bin to final directory with new name src_path = os.path.join(build_dir, "assets.bin") @@ -80,6 +90,15 @@ def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_di def main(): + # Parse command line arguments + parser = argparse.ArgumentParser(description='构建多个 SPIFFS assets 分区') + parser.add_argument('--mode', + choices=['emoji_collections', 'emoji_target_boards'], + default='emoji_collections', + help='选择运行模式: emoji_collections 或 emoji_target_boards (默认: emoji_collections)') + + args = parser.parse_args() + # Configuration wakenet_models = [ "none", @@ -100,6 +119,11 @@ def main(): "emojis_32", "emojis_64", ] + + emoji_target_boards = [ + "esp-box-3", + "echoear", + ] # Get script directory script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -113,18 +137,33 @@ def main(): ensure_dir(final_dir) print("开始构建多个 SPIFFS assets 分区...") + print(f"运行模式: {args.mode}") print(f"输出目录: {final_dir}") # Track successful builds successful_builds = 0 - total_combinations = len(wakenet_models) * len(text_fonts) * len(emoji_collections) - # Build all combinations - for wakenet_model in wakenet_models: - for text_font in text_fonts: - for emoji_collection in emoji_collections: - if build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_dir): - successful_builds += 1 + if args.mode == 'emoji_collections': + # Calculate total combinations for emoji_collections mode + total_combinations = len(wakenet_models) * len(text_fonts) * len(emoji_collections) + + # Build all combinations with emoji_collections + for wakenet_model in wakenet_models: + for text_font in text_fonts: + for emoji_collection in emoji_collections: + if build_assets(wakenet_model, text_font, emoji_collection, "none", build_dir, final_dir): + successful_builds += 1 + + elif args.mode == 'emoji_target_boards': + # Calculate total combinations for emoji_target_boards mode + total_combinations = len(wakenet_models) * len(text_fonts) * len(emoji_target_boards) + + # Build all combinations with emoji_target_boards + for wakenet_model in wakenet_models: + for text_font in text_fonts: + for emoji_target_board in emoji_target_boards: + if build_assets(wakenet_model, text_font, "none", emoji_target_board, build_dir, final_dir): + successful_builds += 1 print(f"\n构建完成!") print(f"成功构建: {successful_builds}/{total_combinations}")