diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 03ae1d8..061ab18 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -651,14 +651,14 @@ elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307) set(DEFAULT_EMOJI_COLLECTION twemoji_64) elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM) set(BOARD_TYPE "zhengchen-cam") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) + set(BUILTIN_TEXT_FONT font_puhui_20_4) set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(DEFAULT_EMOJI_COLLECTION main/boards/zhengchen-cam/custom-emojis) elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM_ML307) set(BOARD_TYPE "zhengchen-cam-ml307") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) + set(BUILTIN_TEXT_FONT font_puhui_20_4) set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(DEFAULT_EMOJI_COLLECTION main/boards/zhengchen-cam/custom-emojis) elseif(CONFIG_BOARD_TYPE_SPOTPEAR_ESP32_S3_1_54_MUMA) set(BOARD_TYPE "sp-esp32-s3-1.54-muma") set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) @@ -912,6 +912,7 @@ idf_component_register(SRCS ${SOURCES} efuse bt fatfs + lwip ) # Use target_compile_definitions to define BOARD_TYPE, BOARD_NAME @@ -1016,6 +1017,25 @@ function(build_default_assets_bin) if(DEFAULT_ASSETS_EXTRA_FILES) list(APPEND BUILD_ARGS "--extra_files" "${DEFAULT_ASSETS_EXTRA_FILES}") endif() + + set(DEFAULT_ASSETS_DEPENDS + ${SDKCONFIG} + ${PROJECT_DIR}/scripts/build_default_assets.py + ) + if(DEFAULT_EMOJI_COLLECTION) + if(IS_ABSOLUTE "${DEFAULT_EMOJI_COLLECTION}") + set(DEFAULT_EMOJI_COLLECTION_PATH "${DEFAULT_EMOJI_COLLECTION}") + else() + set(DEFAULT_EMOJI_COLLECTION_PATH "${PROJECT_DIR}/${DEFAULT_EMOJI_COLLECTION}") + endif() + if(IS_DIRECTORY "${DEFAULT_EMOJI_COLLECTION_PATH}") + file(GLOB_RECURSE DEFAULT_EMOJI_COLLECTION_FILES CONFIGURE_DEPENDS + "${DEFAULT_EMOJI_COLLECTION_PATH}/*.png" + "${DEFAULT_EMOJI_COLLECTION_PATH}/*.gif" + ) + list(APPEND DEFAULT_ASSETS_DEPENDS ${DEFAULT_EMOJI_COLLECTION_FILES}) + endif() + endif() list(APPEND BUILD_ARGS "--esp_sr_model_path" "${ESP_SR_MODEL_PATH}") list(APPEND BUILD_ARGS "--xiaozhi_fonts_path" "${XIAOZHI_FONTS_PATH}") @@ -1024,9 +1044,7 @@ function(build_default_assets_bin) add_custom_command( OUTPUT ${GENERATED_ASSETS_BIN} COMMAND python ${PROJECT_DIR}/scripts/build_default_assets.py ${BUILD_ARGS} - DEPENDS - ${SDKCONFIG} - ${PROJECT_DIR}/scripts/build_default_assets.py + DEPENDS ${DEFAULT_ASSETS_DEPENDS} COMMENT "Building default assets.bin based on configuration" VERBATIM ) diff --git a/main/application.cc b/main/application.cc index d69d43d..8221ddb 100644 --- a/main/application.cc +++ b/main/application.cc @@ -16,9 +16,43 @@ #include #include #include +#include +#include +#include #define TAG "Application" +namespace { +constexpr const char* kDirectWebsocketUrl = "ws://172.19.0.240:8080"; +constexpr int kDirectWebsocketVersion = 3; +constexpr bool kUseDirectWebsocketWithoutOta = true; + +void StartDirectTimeSync() { + setenv("TZ", "CST-8", 1); + tzset(); + + static bool sntp_started = false; + if (sntp_started) { + return; + } + sntp_started = true; + + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, "ntp.aliyun.com"); + sntp_init(); +} + +void ConfigureDirectWebsocket() { + Settings settings("websocket", true); + if (settings.GetString("url") != kDirectWebsocketUrl) { + settings.SetString("url", kDirectWebsocketUrl); + } + if (settings.GetInt("version") != kDirectWebsocketVersion) { + settings.SetInt("version", kDirectWebsocketVersion); + } +} +} // namespace + Application::Application() { event_group_ = xEventGroupCreate(); @@ -324,6 +358,16 @@ void Application::ActivationTask() { // Create OTA object for activation process ota_ = std::make_unique(); + if (kUseDirectWebsocketWithoutOta) { + ConfigureDirectWebsocket(); + StartDirectTimeSync(); + ESP_LOGI(TAG, "Using direct websocket without OTA: %s", kDirectWebsocketUrl); + CheckAssetsVersion(); + InitializeProtocol(); + xEventGroupSetBits(event_group_, MAIN_EVENT_ACTIVATION_DONE); + return; + } + // Check for new assets version CheckAssetsVersion(); @@ -477,9 +521,12 @@ void Application::InitializeProtocol() { display->SetStatus(Lang::Strings::LOADING_PROTOCOL); + Settings websocket_settings("websocket", false); + bool has_direct_websocket_config = !websocket_settings.GetString("url").empty(); + if (ota_->HasMqttConfig()) { protocol_ = std::make_unique(); - } else if (ota_->HasWebsocketConfig()) { + } else if (ota_->HasWebsocketConfig() || has_direct_websocket_config) { protocol_ = std::make_unique(); } else { ESP_LOGW(TAG, "No protocol specified in the OTA config, using MQTT"); @@ -1116,4 +1163,3 @@ void Application::ResetProtocol() { protocol_.reset(); }); } - diff --git a/main/boards/zhengchen-cam/custom-emojis/README.md b/main/boards/zhengchen-cam/custom-emojis/README.md new file mode 100644 index 0000000..4c43afe --- /dev/null +++ b/main/boards/zhengchen-cam/custom-emojis/README.md @@ -0,0 +1,39 @@ +# Custom Emojis + +Put your custom PNG or GIF files in this directory, or in `png/` and `gif/` +subdirectories. + +The filename without extension is used as the emotion name. Directory names are +not part of the emotion name, so `png/neutral.png` is loaded as `neutral`. + +The display code looks up images by names such as: + +- `neutral.png` or `neutral.gif` +- `happy.png` or `happy.gif` +- `sad.png` or `sad.gif` +- `angry.png` or `angry.gif` +- `thinking.png` or `thinking.gif` +- `confused.png` or `confused.gif` +- `surprised.png` or `surprised.gif` +- `shocked.png` or `shocked.gif` +- `sleepy.png` or `sleepy.gif` +- `relaxed.png` or `relaxed.gif` + +Recommended minimum set: + +- `neutral` +- `happy` +- `thinking` +- `sad` +- `angry` + +If an emotion-specific image is missing, the firmware falls back to `neutral` +before using the built-in icon. Do not add both `png/happy.png` and +`gif/happy.gif`; only one image per emotion should be used. + +After adding or replacing files, run a full flash so the assets partition is +updated: + +```bash +idf.py flash +``` diff --git a/main/boards/zhengchen-cam/custom-emojis/gif/neutral.gif b/main/boards/zhengchen-cam/custom-emojis/gif/neutral.gif new file mode 100644 index 0000000..3f784a6 Binary files /dev/null and b/main/boards/zhengchen-cam/custom-emojis/gif/neutral.gif differ diff --git a/main/boards/zhengchen-cam/custom-emojis/png/neutral.png b/main/boards/zhengchen-cam/custom-emojis/png/neutral.png new file mode 100644 index 0000000..9fe63a3 Binary files /dev/null and b/main/boards/zhengchen-cam/custom-emojis/png/neutral.png differ diff --git a/main/display/lcd_display.cc b/main/display/lcd_display.cc index 0303d12..a707257 100644 --- a/main/display/lcd_display.cc +++ b/main/display/lcd_display.cc @@ -22,6 +22,57 @@ LV_FONT_DECLARE(BUILTIN_TEXT_FONT); LV_FONT_DECLARE(BUILTIN_ICON_FONT); LV_FONT_DECLARE(font_awesome_30_4); +namespace { +constexpr int kEmojiMaxScale = 1024; + +lv_coord_t ObjectHeight(lv_obj_t* obj) { + if (obj == nullptr) { + return 0; + } + lv_obj_update_layout(obj); + return lv_obj_get_height(obj); +} + +void ApplyEmojiImageScale(lv_obj_t* image_obj, lv_obj_t* image_box, const lv_image_dsc_t* image_dsc, + lv_coord_t top_reserved, lv_coord_t bottom_reserved) { + if (image_obj == nullptr || image_dsc == nullptr) { + return; + } + + lv_coord_t image_width = image_dsc->header.w; + lv_coord_t image_height = image_dsc->header.h; + if (image_width <= 0 || image_height <= 0) { + lv_image_set_scale(image_obj, 256); + return; + } + + lv_coord_t max_width = LV_HOR_RES; + lv_coord_t max_height = LV_VER_RES - top_reserved - bottom_reserved; + max_height = std::max(max_height, 1); + lv_coord_t scale_w = max_width * 256 / image_width; + lv_coord_t scale_h = max_height * 256 / image_height; + lv_coord_t scale = std::min(scale_w, scale_h); + scale = std::min(scale, kEmojiMaxScale); + scale = std::max(scale, 1); + lv_coord_t scaled_width = image_width * scale / 256; + lv_coord_t scaled_height = image_height * scale / 256; + + lv_image_set_scale(image_obj, scale); + lv_obj_set_size(image_obj, scaled_width, scaled_height); + lv_obj_t* align_obj = image_box != nullptr ? image_box : image_obj; + lv_obj_set_size(align_obj, scaled_width, scaled_height); + lv_obj_align(align_obj, LV_ALIGN_TOP_MID, 0, top_reserved + (max_height - scaled_height) / 2); + if (image_box != nullptr) { + lv_obj_center(image_obj); + } + ESP_LOGI(TAG, "Emoji image scale=%ld reserved=%ld/%ld size=%ldx%ld -> %ldx%ld", + static_cast(scale), static_cast(top_reserved), + static_cast(bottom_reserved), static_cast(image_width), + static_cast(image_height), + static_cast(scaled_width), static_cast(scaled_height)); +} +} // namespace + void LcdDisplay::InitializeLcdThemes() { auto text_font = std::make_shared(&BUILTIN_TEXT_FONT); auto icon_font = std::make_shared(&BUILTIN_ICON_FONT); @@ -1136,6 +1187,9 @@ void LcdDisplay::SetEmotion(const char* emotion) { auto emoji_collection = static_cast(current_theme_)->emoji_collection(); auto image = emoji_collection != nullptr ? emoji_collection->GetEmojiImage(emotion) : nullptr; + if (image == nullptr && emoji_collection != nullptr && strcmp(emotion, "neutral") != 0) { + image = emoji_collection->GetEmojiImage("neutral"); + } if (image == nullptr) { const char* utf8 = font_awesome_get_utf8(emotion); if (utf8 != nullptr && emoji_label_ != nullptr) { @@ -1148,17 +1202,28 @@ void LcdDisplay::SetEmotion(const char* emotion) { } DisplayLockGuard lock(this); + lv_coord_t top_reserved = ObjectHeight(top_bar_); + lv_coord_t bottom_reserved = ObjectHeight(status_bar_) + ObjectHeight(bottom_bar_); + auto apply_emoji_layout = [this, top_reserved, bottom_reserved](const lv_image_dsc_t* image_dsc) { + ApplyEmojiImageScale(emoji_image_, emoji_box_, image_dsc, top_reserved, bottom_reserved); + }; + if (image->IsGif()) { // Create new GIF controller gif_controller_ = std::make_unique(image->image_dsc()); if (gif_controller_->IsLoaded()) { // Set up frame update callback - gif_controller_->SetFrameCallback( - [this]() { lv_image_set_src(emoji_image_, gif_controller_->image_dsc()); }); + gif_controller_->SetFrameCallback([this, apply_emoji_layout]() { + auto frame = gif_controller_->image_dsc(); + lv_image_set_src(emoji_image_, frame); + apply_emoji_layout(frame); + }); // Set initial frame and start animation - lv_image_set_src(emoji_image_, gif_controller_->image_dsc()); + auto frame = gif_controller_->image_dsc(); + lv_image_set_src(emoji_image_, frame); + apply_emoji_layout(frame); gif_controller_->Start(); // Show GIF, hide others @@ -1169,7 +1234,9 @@ void LcdDisplay::SetEmotion(const char* emotion) { gif_controller_.reset(); } } else { - lv_image_set_src(emoji_image_, image->image_dsc()); + auto image_dsc = image->image_dsc(); + lv_image_set_src(emoji_image_, image_dsc); + apply_emoji_layout(image_dsc); lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN); } diff --git a/scripts/build_default_assets.py b/scripts/build_default_assets.py index 9ca1464..d5f64ce 100644 --- a/scripts/build_default_assets.py +++ b/scripts/build_default_assets.py @@ -715,9 +715,20 @@ def get_emoji_collection_path(default_emoji_collection, xiaozhi_fonts_path, proj - PNG emoji collections from xiaozhi-fonts (e.g., emojis_32, twemoji_64) - GIF emoji collections from xiaozhi-fonts (e.g., noto-emoji_128, noto-emoji_64) - Otto GIF emoji collection (otto-gif) + - Custom project-relative or absolute directory paths """ if not default_emoji_collection: return None + + candidate_paths = [] + if os.path.isabs(default_emoji_collection): + candidate_paths.append(default_emoji_collection) + elif project_root: + candidate_paths.append(os.path.join(project_root, default_emoji_collection)) + + for candidate_path in candidate_paths: + if os.path.isdir(candidate_path): + return candidate_path # Special handling for otto-gif collection if default_emoji_collection == 'otto-gif':