diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 53f91c32..fe479703 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -171,14 +171,14 @@ elseif(CONFIG_BOARD_TYPE_EDA_SUPER_BEAR) set(BOARD_TYPE "eda-super-bear") elseif(CONFIG_BOARD_TYPE_MAGICLICK_S3_2P4) set(BOARD_TYPE "magiclick-2p4") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) + set(BUILTIN_TEXT_FONT font_noto_basic_16_4) set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(DEFAULT_EMOJI_COLLECTION noto-emoji_64) elseif(CONFIG_BOARD_TYPE_MAGICLICK_S3_2P5) set(BOARD_TYPE "magiclick-2p5") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) + set(BUILTIN_TEXT_FONT font_noto_basic_16_4) set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(DEFAULT_EMOJI_COLLECTION noto-emoji_64) elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3) set(BOARD_TYPE "magiclick-c3") set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index ca8abf7b..01421392 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -635,6 +635,17 @@ choice DISPLAY_STYLE || BOARD_TYPE_ESP_SENSAIRSHUTTLE endchoice +config USE_MULTILINE_CHAT_MESSAGE + bool "Use multiline chat message display (default mode only)" + depends on USE_DEFAULT_MESSAGE_STYLE + default n + help + When enabled, the chat message area in the default display mode shows + multiple wrapped lines that grow upward from the bottom of the screen, + with auto-adaptive height. + When disabled (default), a single-line horizontally scrolling label + is shown at the bottom of the screen. + choice WAKE_WORD_TYPE prompt "Wake Word Implementation Type" default USE_AFE_WAKE_WORD if (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM diff --git a/main/boards/electron-bot/electron_emoji_display.cc b/main/boards/electron-bot/electron_emoji_display.cc index 5876ca55..7020a354 100644 --- a/main/boards/electron-bot/electron_emoji_display.cc +++ b/main/boards/electron-bot/electron_emoji_display.cc @@ -15,7 +15,6 @@ ElectronEmojiDisplay::ElectronEmojiDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy) { - SetupChatLabel(); } void ElectronEmojiDisplay::SetupUI() { @@ -25,9 +24,12 @@ void ElectronEmojiDisplay::SetupUI() { return; } - // Call parent SetupUI() first to create all lvgl objects + // Call parent SetupUI() first to create all lvgl objects (including container_) SpiLcdDisplay::SetupUI(); - + + // Setup chat label after parent UI is initialized so that container_ is valid + SetupChatLabel(); + // Set default emotion after UI is initialized SetEmotion("staticstate"); } @@ -40,18 +42,22 @@ void ElectronEmojiDisplay::InitializeElectronEmojis() { } void ElectronEmojiDisplay::SetupChatLabel() { - DisplayLockGuard lock(this); + // Create/recreate the chat label under the display lock + { + DisplayLockGuard lock(this); - if (chat_message_label_) { - lv_obj_del(chat_message_label_); + if (chat_message_label_) { + lv_obj_del(chat_message_label_); + } + + chat_message_label_ = lv_label_create(container_); + lv_label_set_text(chat_message_label_, ""); + lv_obj_set_width(chat_message_label_, width_ * 0.9); + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); } - - chat_message_label_ = lv_label_create(container_); - lv_label_set_text(chat_message_label_, ""); - lv_obj_set_width(chat_message_label_, width_ * 0.9); // 限制宽度为屏幕宽度的 90% - lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); // 设置为自动换行模式 - lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐 - lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); + // SetTheme acquires DisplayLockGuard internally, so call it after releasing the lock above SetTheme(LvglThemeManager::GetInstance().GetTheme("dark")); } diff --git a/main/boards/magiclick-2p4/magiclick_2p4_board.cc b/main/boards/magiclick-2p4/magiclick_2p4_board.cc index 16c8fc1c..fca17e87 100644 --- a/main/boards/magiclick-2p4/magiclick_2p4_board.cc +++ b/main/boards/magiclick-2p4/magiclick_2p4_board.cc @@ -23,16 +23,18 @@ public: NV3023Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy) { + } + void SetupUI() override { + SpiLcdDisplay::SetupUI(); + + // Apply custom color styles after parent creates all LVGL objects DisplayLockGuard lock(this); - // 只需要覆盖颜色相关的样式 auto screen = lv_disp_get_scr_act(lv_disp_get_default()); lv_obj_set_style_text_color(screen, lv_color_black(), 0); - // 设置容器背景色 lv_obj_set_style_bg_color(container_, lv_color_black(), 0); - // 设置状态栏背景色和文本颜色 lv_obj_set_style_bg_color(status_bar_, lv_color_white(), 0); lv_obj_set_style_text_color(network_label_, lv_color_black(), 0); lv_obj_set_style_text_color(notification_label_, lv_color_black(), 0); @@ -40,7 +42,6 @@ public: lv_obj_set_style_text_color(mute_label_, lv_color_black(), 0); lv_obj_set_style_text_color(battery_label_, lv_color_black(), 0); - // 设置内容区背景色和文本颜色 lv_obj_set_style_bg_color(content_, lv_color_black(), 0); lv_obj_set_style_border_width(content_, 0, 0); lv_obj_set_style_text_color(emoji_label_, lv_color_white(), 0); diff --git a/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc b/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc index bdf3afc1..bf99ccee 100644 --- a/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc +++ b/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc @@ -23,16 +23,18 @@ public: GC9107Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy) { + } + void SetupUI() override { + SpiLcdDisplay::SetupUI(); + + // Apply custom color styles after parent creates all LVGL objects DisplayLockGuard lock(this); - // 只需要覆盖颜色相关的样式 auto screen = lv_disp_get_scr_act(lv_disp_get_default()); lv_obj_set_style_text_color(screen, lv_color_black(), 0); - // 设置容器背景色 lv_obj_set_style_bg_color(container_, lv_color_black(), 0); - // 设置状态栏背景色和文本颜色 lv_obj_set_style_bg_color(status_bar_, lv_color_make(0x1e, 0x90, 0xff), 0); lv_obj_set_style_text_color(network_label_, lv_color_black(), 0); lv_obj_set_style_text_color(notification_label_, lv_color_black(), 0); @@ -40,12 +42,11 @@ public: lv_obj_set_style_text_color(mute_label_, lv_color_black(), 0); lv_obj_set_style_text_color(battery_label_, lv_color_black(), 0); - // 设置内容区背景色和文本颜色 lv_obj_set_style_bg_color(content_, lv_color_black(), 0); lv_obj_set_style_border_width(content_, 0, 0); lv_obj_set_style_text_color(emoji_label_, lv_color_white(), 0); lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); - } + } }; static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { diff --git a/main/boards/magiclick-c3/magiclick_c3_board.cc b/main/boards/magiclick-c3/magiclick_c3_board.cc index 4df269eb..ae86bfc2 100644 --- a/main/boards/magiclick-c3/magiclick_c3_board.cc +++ b/main/boards/magiclick-c3/magiclick_c3_board.cc @@ -21,16 +21,18 @@ public: NV3023Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy) { + } + void SetupUI() override { + SpiLcdDisplay::SetupUI(); + + // Apply custom color styles after parent creates all LVGL objects DisplayLockGuard lock(this); - // 只需要覆盖颜色相关的样式 auto screen = lv_disp_get_scr_act(lv_disp_get_default()); lv_obj_set_style_text_color(screen, lv_color_black(), 0); - // 设置容器背景色 lv_obj_set_style_bg_color(container_, lv_color_black(), 0); - // 设置状态栏背景色和文本颜色 lv_obj_set_style_bg_color(status_bar_, lv_color_white(), 0); lv_obj_set_style_text_color(network_label_, lv_color_black(), 0); lv_obj_set_style_text_color(notification_label_, lv_color_black(), 0); @@ -38,7 +40,6 @@ public: lv_obj_set_style_text_color(mute_label_, lv_color_black(), 0); lv_obj_set_style_text_color(battery_label_, lv_color_black(), 0); - // 设置内容区背景色和文本颜色 lv_obj_set_style_bg_color(content_, lv_color_black(), 0); lv_obj_set_style_border_width(content_, 0, 0); lv_obj_set_style_text_color(emoji_label_, lv_color_white(), 0); diff --git a/main/boards/otto-robot/otto_emoji_display.cc b/main/boards/otto-robot/otto_emoji_display.cc index b4fedd27..86449bc6 100644 --- a/main/boards/otto-robot/otto_emoji_display.cc +++ b/main/boards/otto-robot/otto_emoji_display.cc @@ -27,10 +27,13 @@ void OttoEmojiDisplay::SetupUI() { // Call parent SetupUI() first to create all lvgl objects SpiLcdDisplay::SetupUI(); - // Setup preview image after UI is initialized - DisplayLockGuard lock(this); - lv_obj_set_size(preview_image_, width_ , height_ ); - + // Setup preview image after UI is initialized - release lock before calling SetEmotion + // to avoid deadlock (SetEmotion also acquires DisplayLockGuard internally) + { + DisplayLockGuard lock(this); + lv_obj_set_size(preview_image_, width_ , height_ ); + } + // Set default emotion after UI is initialized SetEmotion("staticstate"); } diff --git a/main/boards/waveshare/esp32-s3-epaper-1.54/config.json b/main/boards/waveshare/esp32-s3-epaper-1.54/config.json index 0ec9252a..a298dad8 100644 --- a/main/boards/waveshare/esp32-s3-epaper-1.54/config.json +++ b/main/boards/waveshare/esp32-s3-epaper-1.54/config.json @@ -6,7 +6,8 @@ "name": "esp32-s3-epaper-1.54-v2", "sdkconfig_append": [ "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", - "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"", + "CONFIG_USE_MULTILINE_CHAT_MESSAGE=y" ] }, { @@ -14,7 +15,8 @@ "sdkconfig_append": [ "CONFIG_SPIRAM_MODE_QUAD=y", "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y", - "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\"" + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\"", + "CONFIG_USE_MULTILINE_CHAT_MESSAGE=y" ] } ] diff --git a/main/display/lcd_display.cc b/main/display/lcd_display.cc index d2260523..239e10fa 100644 --- a/main/display/lcd_display.cc +++ b/main/display/lcd_display.cc @@ -924,9 +924,33 @@ void LcdDisplay::SetupUI() { lv_label_set_text(status_label_, Lang::Strings::INITIALIZING); lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0); +#if CONFIG_USE_MULTILINE_CHAT_MESSAGE + /* Bottom bar - auto height, grows upward with wrapped text */ + bottom_bar_ = lv_obj_create(screen); + lv_obj_set_width(bottom_bar_, LV_HOR_RES); + lv_obj_set_height(bottom_bar_, LV_SIZE_CONTENT); + lv_obj_set_style_radius(bottom_bar_, 0, 0); + lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0); + lv_obj_set_style_bg_opa(bottom_bar_, LV_OPA_50, 0); + lv_obj_set_style_text_color(bottom_bar_, lvgl_theme->text_color(), 0); + lv_obj_set_style_pad_all(bottom_bar_, lvgl_theme->spacing(4), 0); + lv_obj_set_style_border_width(bottom_bar_, 0, 0); + lv_obj_set_scrollbar_mode(bottom_bar_, LV_SCROLLBAR_MODE_OFF); + lv_obj_align(bottom_bar_, LV_ALIGN_BOTTOM_MID, 0, 0); + + /* chat_message_label_ placed in bottom_bar_, multiline wrapped display */ + chat_message_label_ = lv_label_create(bottom_bar_); + lv_label_set_text(chat_message_label_, ""); + lv_obj_set_width(chat_message_label_, LV_HOR_RES - lvgl_theme->spacing(8)); + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0); + lv_obj_align(chat_message_label_, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); // Hide until there is content +#else /* Top layer: Bottom bar - fixed height at bottom */ bottom_bar_ = lv_obj_create(screen); - lv_obj_set_size(bottom_bar_, LV_HOR_RES, text_font->line_height + lvgl_theme->spacing(12)); + lv_obj_set_size(bottom_bar_, LV_HOR_RES, text_font->line_height + lvgl_theme->spacing(8)); lv_obj_set_style_radius(bottom_bar_, 0, 0); lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0); lv_obj_set_style_text_color(bottom_bar_, lvgl_theme->text_color(), 0); @@ -953,6 +977,8 @@ void LcdDisplay::SetupUI() { lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN); lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN); + lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); // Hide until there is content +#endif low_battery_popup_ = lv_obj_create(screen); lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); @@ -1016,14 +1042,32 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { return; } lv_label_set_text(chat_message_label_, content); + // Show bottom_bar_ only when there is content (and subtitle is not globally hidden) + if (bottom_bar_ != nullptr) { + if (content == nullptr || content[0] == '\0') { + lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); + } else if (!hide_subtitle_) { + lv_obj_remove_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); + } + } +#if CONFIG_USE_MULTILINE_CHAT_MESSAGE + // Re-align bottom_bar_ after text change so it stays anchored to the bottom + // as its height adapts to the wrapped content. + if (bottom_bar_ != nullptr) { + lv_obj_align(bottom_bar_, LV_ALIGN_BOTTOM_MID, 0, 0); + } +#endif } void LcdDisplay::ClearChatMessages() { DisplayLockGuard lock(this); - // In non-wechat mode, just clear the chat message label + // In non-wechat mode, just clear the chat message label and hide the bar if (chat_message_label_ != nullptr) { lv_label_set_text(chat_message_label_, ""); } + if (bottom_bar_ != nullptr) { + lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); + } } #endif @@ -1253,7 +1297,11 @@ void LcdDisplay::SetHideSubtitle(bool hide) { if (hide) { lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); } else { - lv_obj_remove_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); + // Only show if there is actual content to display + const char* text = (chat_message_label_ != nullptr) ? lv_label_get_text(chat_message_label_) : nullptr; + if (text != nullptr && text[0] != '\0') { + lv_obj_remove_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); + } } } } diff --git a/scripts/download_github_runs.py b/scripts/download_github_runs.py index b98c8f8c..d8bf0ee3 100644 --- a/scripts/download_github_runs.py +++ b/scripts/download_github_runs.py @@ -4,6 +4,10 @@ Download GitHub Actions artifacts and rename them with version numbers. Usage: python download_github_runs.py 2.0.4 https://github.com/78/xiaozhi-esp32/actions/runs/18866246016 + +Output: + Files are downloaded to releases// directory relative to the project root. + Example: releases/2.0.4/v2.0.4_atk-dnesp32s3-box0.zip """ import argparse @@ -147,12 +151,17 @@ def rename_artifact(original_name: str, version: str) -> str: if name.startswith("xiaozhi_"): name = name[len("xiaozhi_"):] - # Remove extension - name_without_ext = os.path.splitext(name)[0] + # Remove known extensions only (not using splitext to avoid issues with + # names containing dots like "esp32-s3-touch-amoled-2.06") + known_extensions = ('.bin', '.zip') + for ext in known_extensions: + if name.endswith(ext): + name = name[:-len(ext)] + break # Remove hash suffix (pattern: underscore followed by 40+ hex characters) # This matches Git commit hashes and similar identifiers - name_without_hash = re.sub(r'_[a-f0-9]{40,}$', '', name_without_ext) + name_without_hash = re.sub(r'_[a-f0-9]{40,}$', '', name) # Add version prefix and .zip extension new_name = f"v{version}_{name_without_hash}.zip" @@ -160,6 +169,17 @@ def rename_artifact(original_name: str, version: str) -> str: return new_name +def get_default_releases_dir() -> Path: + """ + Get the default releases directory path relative to this script's location. + + Returns: + Path to the releases directory (script_dir/../releases) + """ + script_dir = Path(__file__).resolve().parent + return script_dir.parent / "releases" + + def main(): """Main function to download and rename GitHub Actions artifacts.""" parser = argparse.ArgumentParser( @@ -175,8 +195,8 @@ def main(): ) parser.add_argument( "--output-dir", - default="../releases", - help="Output directory for downloaded artifacts (default: ../releases)" + default=None, + help="Output directory for downloaded artifacts (default: releases/ relative to project root)" ) args = parser.parse_args() @@ -211,8 +231,15 @@ def main(): print(f" - {artifact['name']}") print() + # Determine output directory + if args.output_dir: + # User specified custom output directory + output_dir = Path(args.output_dir) / args.version + else: + # Default: releases/ relative to script location + output_dir = get_default_releases_dir() / args.version + # Create output directory - output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) # Download and rename each artifact