Compare commits

..

4 Commits

Author SHA1 Message Date
Terrence
14d85f22ba Update font and emoji settings for Magiclick boards; enhance bottom bar visibility logic in LCD display
- Changed the default text and emoji fonts for Magiclick S3 2P4 and S3 2P5 boards to Noto fonts.
- Improved bottom bar visibility logic in LcdDisplay to hide when there is no content, ensuring a cleaner UI experience.
2026-02-19 17:32:05 +08:00
Terrence
763a62c6c5 Add multiline chat message support in display configuration
- Introduced a new Kconfig option to enable multiline chat message display in the default mode.
- Updated the LCD display setup to accommodate a dynamic height bottom bar for multiline messages.
- Modified the configuration files for the waveshare-esp32-s3-epaper-1.54 board to include the new chat message setting.
2026-02-19 17:15:13 +08:00
Terrence
c6e34f42ef Refactor UI setup in ElectronEmojiDisplay and OttoEmojiDisplay classes
- Moved SetupChatLabel call in ElectronEmojiDisplay to ensure it is executed after the parent UI is initialized, preventing potential issues with container validity.
- Updated SetupUI in OttoEmojiDisplay to release the display lock before calling SetEmotion, avoiding deadlock scenarios during UI setup.
2026-02-19 16:54:02 +08:00
Terrence
fc2ade8993 Enhance GitHub Actions artifact download script
- Updated the output directory structure to save downloaded files in a version-specific subdirectory (releases/<version>).
- Added a new function to determine the default releases directory path relative to the script's location.
- Improved artifact renaming logic to handle known extensions more robustly and ensure compatibility with filenames containing dots.
2026-02-19 16:54:02 +08:00
10 changed files with 145 additions and 45 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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"));
}

View File

@@ -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);

View File

@@ -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[] = {

View File

@@ -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);

View File

@@ -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");
}

View File

@@ -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"
]
}
]

View File

@@ -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);
}
}
}
}

View File

@@ -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/<version>/ 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/<version> 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/<version> 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