diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b76c39b..162f887 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -24,6 +24,7 @@ set(SOURCES "audio/audio_codec.cc" "settings.cc" "device_state_event.cc" "main.cc" + "image_fetcher.cc" ) set(INCLUDE_DIRS "." "display" "audio" "protocols") diff --git a/main/application.cc b/main/application.cc index 05f3f03..d558afc 100644 --- a/main/application.cc +++ b/main/application.cc @@ -9,6 +9,10 @@ #include "assets/lang_config.h" #include "mcp_server.h" +#ifdef CONFIG_LSPLATFORM +#include "image_fetcher.h" +#endif // CONFIG_LSPLATFORM + #include #include #include @@ -154,7 +158,11 @@ void Application::CheckNewVersion(Ota& ota) { display->SetStatus(Lang::Strings::ACTIVATION); // Activation code is shown to the user and waiting for the user to input if (ota.HasActivationCode()) { +#ifdef CONFIG_LSPLATFORM + ShowActivationCode(ota.GetActivationCode(), ota.GetActivationMessage(), ota.GetActivationQRCode()); +#else // !CONFIG_LSPLATFORM ShowActivationCode(ota.GetActivationCode(), ota.GetActivationMessage()); +#endif // CONFIG_LSPLATFORM } // This will block the loop until the activation is done or timeout @@ -162,6 +170,10 @@ void Application::CheckNewVersion(Ota& ota) { ESP_LOGI(TAG, "Activating... %d/%d", i + 1, 10); esp_err_t err = ota.Activate(); if (err == ESP_OK) { +#ifdef CONFIG_LSPLATFORM + ESP_LOGI(TAG, "Activation successful, dismiss UI"); + display->DismissActivation(); +#endif // CONFIG_LSPLATFORM xEventGroupSetBits(event_group_, MAIN_EVENT_CHECK_NEW_VERSION_DONE); break; } else if (err == ESP_ERR_TIMEOUT) { @@ -176,7 +188,7 @@ void Application::CheckNewVersion(Ota& ota) { } } -void Application::ShowActivationCode(const std::string& code, const std::string& message) { +void Application::ShowActivationCode(const std::string& code, const std::string& message, const std::string& qrcode) { struct digit_sound { char digit; const std::string_view& sound; @@ -194,8 +206,28 @@ void Application::ShowActivationCode(const std::string& code, const std::string& digit_sound{'9', Lang::Sounds::OGG_9} }}; +#ifdef CONFIG_LSPLATFORM + ESP_LOGI(TAG, "Showing activation code: %s, QR url: %s", code.c_str(), qrcode.c_str()); + if (!qrcode.empty()) { + auto& board = Board::GetInstance(); + auto fetcher = ImageFetcher::From(board.GetNetwork()); + auto display = board.GetDisplay(); + lv_img_dsc_t img_dsc; + if (fetcher.Fetch(qrcode, &img_dsc)) { + display->ShowActivation(&img_dsc, message + code); + } else { + ESP_LOGE(TAG, "Failed to fetch QR code image"); + } + } else { + ESP_LOGW(TAG, "QR code URL is empty, skipping image download"); + } + + audio_service_.PlaySound(Lang::Sounds::OGG_WECHAT_QRCODE); + audio_service_.PlaySound(Lang::Sounds::OGG_BINDING); +#else // !CONFIG_LSPLATFORM // This sentence uses 9KB of SRAM, so we need to wait for it to finish Alert(Lang::Strings::ACTIVATION, message.c_str(), "happy", Lang::Sounds::OGG_ACTIVATION); +#endif // CONFIG_LSPLATFORM for (const auto& digit : code) { auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(), diff --git a/main/application.h b/main/application.h index 6997f1c..822baba 100644 --- a/main/application.h +++ b/main/application.h @@ -83,7 +83,7 @@ private: void OnWakeWordDetected(); void CheckNewVersion(Ota& ota); - void ShowActivationCode(const std::string& code, const std::string& message); + void ShowActivationCode(const std::string& code, const std::string& message, const std::string& qrcode = ""); void OnClockTimer(); void SetListeningMode(ListeningMode mode); }; diff --git a/main/assets/common/binding.ogg b/main/assets/common/binding.ogg new file mode 100644 index 0000000..8478a16 Binary files /dev/null and b/main/assets/common/binding.ogg differ diff --git a/main/assets/common/wechat_qrcode.ogg b/main/assets/common/wechat_qrcode.ogg new file mode 100644 index 0000000..14162f8 Binary files /dev/null and b/main/assets/common/wechat_qrcode.ogg differ diff --git a/main/display/display.h b/main/display/display.h index 244e3de..c2fdeaa 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -32,6 +32,11 @@ public: virtual void UpdateStatusBar(bool update_all = false); virtual void SetPowerSaveMode(bool on); +#ifdef CONFIG_LSPLATFORM + virtual void ShowActivation(const lv_img_dsc_t* qrcode, const std::string& message) {}; + virtual void DismissActivation() {}; +#endif // CONFIG_LSPLATFORM + inline int width() const { return width_; } inline int height() const { return height_; } diff --git a/main/display/lcd_display.cc b/main/display/lcd_display.cc index 309d214..09f085f 100644 --- a/main/display/lcd_display.cc +++ b/main/display/lcd_display.cc @@ -412,6 +412,10 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0); lv_obj_center(low_battery_label_); lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); + +#ifdef CONFIG_LSPLATFORM + SetupActivationUI(); +#endif // CONFIG_LSPLATFORM } #if CONFIG_IDF_TARGET_ESP32P4 #define MAX_MESSAGES 40 @@ -805,6 +809,10 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0); lv_obj_center(low_battery_label_); lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); + +#ifdef CONFIG_LSPLATFORM + SetupActivationUI(); +#endif // CONFIG_LSPLATFORM } void LcdDisplay::SetPreviewImage(const lv_img_dsc_t* img_dsc) { @@ -1101,3 +1109,57 @@ void LcdDisplay::SetTheme(const std::string& theme_name) { // No errors occurred. Save theme to settings Display::SetTheme(theme_name); } + +#ifdef CONFIG_LSPLATFORM +void LcdDisplay::SetupActivationUI() { + DisplayLockGuard lock(this); + + activation_container_ = lv_obj_create(container_); + lv_obj_set_scrollbar_mode(activation_container_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_width(activation_container_, LV_HOR_RES); + lv_obj_set_flex_grow(activation_container_, 1); + lv_obj_set_flex_flow(activation_container_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(activation_container_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); + lv_obj_set_style_radius(activation_container_, 0, LV_PART_MAIN); + lv_obj_set_style_bg_color(activation_container_, current_theme_.background, LV_PART_MAIN); + lv_obj_set_style_text_color(activation_container_, current_theme_.text, LV_PART_MAIN); + lv_obj_add_flag(activation_container_, LV_OBJ_FLAG_HIDDEN); + + activation_qrcode_ = lv_image_create(activation_container_); + lv_obj_set_size(activation_qrcode_, LV_HOR_RES, LV_SIZE_CONTENT); + + activation_message_ = lv_label_create(activation_container_); + lv_obj_set_size(activation_message_, LV_HOR_RES, LV_SIZE_CONTENT); + lv_obj_set_style_pad_hor(activation_message_, 16, LV_PART_MAIN); + lv_label_set_long_mode(activation_message_, LV_LABEL_LONG_WRAP); +} + +void LcdDisplay::ShowActivation(const lv_img_dsc_t* qrcode, const std::string& message) { + DisplayLockGuard lock(this); + if (activation_container_ == nullptr || activation_qrcode_ == nullptr || activation_message_ == nullptr) { + return; + } + + lv_obj_add_flag(content_, LV_OBJ_FLAG_HIDDEN); + + lv_image_set_src(activation_qrcode_, qrcode); + lv_image_set_scale(activation_qrcode_, 64); // 256/4 + lv_obj_set_size(activation_qrcode_, LV_HOR_RES, qrcode->header.h / 4); + + lv_label_set_text(activation_message_, message.c_str()); + + lv_obj_remove_flag(activation_container_, LV_OBJ_FLAG_HIDDEN); +} + +void LcdDisplay::DismissActivation() { + DisplayLockGuard lock(this); + if (activation_container_ == nullptr) { + return; + } + + lv_obj_add_flag(activation_container_, LV_OBJ_FLAG_HIDDEN); + lv_image_set_src(activation_qrcode_, NULL); + + lv_obj_remove_flag(content_, LV_OBJ_FLAG_HIDDEN); +} +#endif // CONFIG_LSPLATFORM diff --git a/main/display/lcd_display.h b/main/display/lcd_display.h index 3c6de33..ea27a3f 100644 --- a/main/display/lcd_display.h +++ b/main/display/lcd_display.h @@ -35,6 +35,12 @@ protected: lv_obj_t* side_bar_ = nullptr; lv_obj_t* preview_image_ = nullptr; +#ifdef CONFIG_LSPLATFORM + lv_obj_t* activation_container_ = nullptr; + lv_obj_t* activation_qrcode_ = nullptr; + lv_obj_t* activation_message_ = nullptr; +#endif // CONFIG_LSPLATFORM + DisplayFonts fonts_; ThemeColors current_theme_; @@ -57,6 +63,12 @@ public: // Add theme switching function virtual void SetTheme(const std::string& theme_name) override; + +#ifdef CONFIG_LSPLATFORM + void SetupActivationUI(); + virtual void ShowActivation(const lv_img_dsc_t* qrcode, const std::string& message) override; + virtual void DismissActivation() override; +#endif // CONFIG_LSPLATFORM }; // RGB LCD显示器 diff --git a/main/idf_component.yml b/main/idf_component.yml index 6a2899b..ed895cb 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -73,6 +73,7 @@ dependencies: version: ^0.1.8 rules: - if: target in [esp32c3] + espressif/esp_new_jpeg: ^0.5.* ## Required IDF version idf: diff --git a/main/image_fetcher.cc b/main/image_fetcher.cc new file mode 100644 index 0000000..14fe77b --- /dev/null +++ b/main/image_fetcher.cc @@ -0,0 +1,147 @@ +#include "image_fetcher.h" + +#include + +#include +#include +#include + +#define TAG "ImageFetcher" + +bool ImageFetcher::Fetch(const std::string& url, lv_img_dsc_t* into, int timeout_ms) { + int ret; + + ESP_LOGI(TAG, "Fetching image from %s", url.c_str()); + + try { + auto http = network_->CreateHttp(); + if (!http) { + ESP_LOGE(TAG, "Failed to create HTTP client"); + return false; + } + + http->SetTimeout(timeout_ms); + + auto app_desc = esp_app_get_description(); + auto user_agent = std::string(BOARD_NAME "/") + app_desc->version; + http->SetHeader("User-Agent", user_agent); + + if (!http->Open("GET", url)) { + ESP_LOGE(TAG, "Failed to connect to %s", url.c_str()); + return false; + } + + int status_code = http->GetStatusCode(); + if (status_code != 200) { + ESP_LOGE(TAG, "Failed to fetch image, status code: %d", status_code); + http->Close(); + return false; + } + + std::string image_data = http->ReadAll(); + http->Close(); + + if (image_data.empty()) { + ESP_LOGE(TAG, "No image data received"); + return false; + } + + ESP_LOGI(TAG, "Downloaded image data: %u bytes", image_data.size()); + + if (image_data.size() > 5 * 1024 * 1024) { + ESP_LOGE(TAG, "Image data too large"); + return false; + } + + if (image_data.size() < 10 || + image_data[0] != 0xFF || image_data[1] != 0xD8) { + ESP_LOGE(TAG, "Invalid JPEG header"); + return false; + } + + jpeg_dec_config_t config = DEFAULT_JPEG_DEC_CONFIG(); + config.output_type = JPEG_PIXEL_FORMAT_RGB565_LE; + + jpeg_dec_handle_t jpeg_dec = NULL; + ret = jpeg_dec_open(&config, &jpeg_dec); + if (ret != JPEG_ERR_OK) { + ESP_LOGE(TAG, "Failed to open JPEG decoder, ret=%d", ret); + return false; + } + + jpeg_dec_io_t jpeg_io = {0}; + jpeg_io.inbuf = (uint8_t *)image_data.data(); + jpeg_io.inbuf_len = image_data.size(); + + jpeg_dec_header_info_t out_info = {0}; + + ret = jpeg_dec_parse_header(jpeg_dec, &jpeg_io, &out_info); + if (ret != JPEG_ERR_OK) { + ESP_LOGE(TAG, "Failed to get JPEG header info, ret=%d", ret); + jpeg_dec_close(jpeg_dec); + return false; + } + + ESP_LOGI(TAG, "JPEG header info: width=%d, height=%d", out_info.width, out_info.height); + + if (out_info.width == 0 || out_info.height == 0 || + out_info.width > 2048 || out_info.height > 2048) { + ESP_LOGE(TAG, "Invalid JPEG dimensions: %dx%d", out_info.width, out_info.height); + jpeg_dec_close(jpeg_dec); + return false; + } + + size_t out_size = (size_t)out_info.width * (size_t)out_info.height * 2; // RGB565 = 2 bytes/pixel + if (out_size > 2 * 1024 * 1024) { + ESP_LOGE(TAG, "Decoded image too large"); + jpeg_dec_close(jpeg_dec); + return false; + } + + uint8_t* out_buf = (uint8_t*)jpeg_calloc_align(out_size, 16); + if (!out_buf) { + ESP_LOGE(TAG, "Failed to allocate memory for decoded image, size: %d bytes", (int)out_size); + jpeg_dec_close(jpeg_dec); + return false; + } + + jpeg_io.inbuf += jpeg_io.inbuf_len - jpeg_io.inbuf_remain; + jpeg_io.inbuf_len = jpeg_io.inbuf_remain; + jpeg_io.outbuf = out_buf; + + ESP_LOGI(TAG, "Decoding JPEG image..."); + + ret = jpeg_dec_process(jpeg_dec, &jpeg_io); + if (ret != JPEG_ERR_OK) { + ESP_LOGE(TAG, "Failed to decode JPEG image, ret=%d", ret); + jpeg_free_align(out_buf); + jpeg_dec_close(jpeg_dec); + return false; + } + + ESP_LOGI(TAG, "JPEG image decoded successfully, size: %d bytes", (int)out_size); + + static std::vector rgb_buffer; + rgb_buffer.resize(out_size); + memcpy(rgb_buffer.data(), out_buf, out_size); + + jpeg_free_align(out_buf); + jpeg_dec_close(jpeg_dec); + + memset(into, 0, sizeof(lv_img_dsc_t)); + into->header.magic = LV_IMAGE_HEADER_MAGIC; + into->header.cf = LV_COLOR_FORMAT_RGB565; + into->header.w = out_info.width; + into->header.h = out_info.height; + into->data_size = rgb_buffer.size(); + into->data = rgb_buffer.data(); + + return true; + } catch (const std::exception& e) { + ESP_LOGE(TAG, "Error fetching image: %s", e.what()); + return false; + } catch (...) { + ESP_LOGE(TAG, "Unknown error fetching image"); + return false; + } +} diff --git a/main/image_fetcher.h b/main/image_fetcher.h new file mode 100644 index 0000000..ca4ba2c --- /dev/null +++ b/main/image_fetcher.h @@ -0,0 +1,26 @@ +#ifndef _IMAGE_FETCHER_H_ +#define _IMAGE_FETCHER_H_ + +#include +#include +#include + +class ImageFetcher { +protected: + ImageFetcher(NetworkInterface* network) : network_(network) { + } + +public: + static ImageFetcher From(NetworkInterface* network) { + return ImageFetcher(network); + } + + virtual ~ImageFetcher() = default; + + bool Fetch(const std::string& url, lv_img_dsc_t* into, int timeout_ms = 15000); + +private: + NetworkInterface* network_; +}; + +#endif // _IMAGE_FETCHER_H_ diff --git a/main/ota.cc b/main/ota.cc index bf3d949..3acd5ba 100644 --- a/main/ota.cc +++ b/main/ota.cc @@ -143,6 +143,13 @@ bool Ota::CheckVersion() { if (cJSON_IsNumber(timeout_ms)) { activation_timeout_ms_ = timeout_ms->valueint; } +#ifdef CONFIG_LSPLATFORM + cJSON* qrcode = cJSON_GetObjectItem(activation, "qrcode"); + if (cJSON_IsString(qrcode)) { + activation_qrcode_ = qrcode->valuestring; + has_activation_qrcode_ = true; + } +#endif // CONFIG_LSPLATFORM } has_mqtt_config_ = false; diff --git a/main/ota.h b/main/ota.h index 7957698..2419cb7 100644 --- a/main/ota.h +++ b/main/ota.h @@ -28,6 +28,10 @@ public: const std::string& GetActivationMessage() const { return activation_message_; } const std::string& GetActivationCode() const { return activation_code_; } std::string GetCheckVersionUrl(); +#ifdef CONFIG_LSPLATFORM + bool HasACtivationQRCode() { return has_activation_qrcode_; } + const std::string& GetActivationQRCode() const {return activation_qrcode_; } +#endif // CONFIG_LSPLATFORM private: std::string activation_message_; @@ -45,6 +49,10 @@ private: std::string activation_challenge_; std::string serial_number_; int activation_timeout_ms_ = 30000; +#ifdef CONFIG_LSPLATFORM + bool has_activation_qrcode_ = false; + std::string activation_qrcode_; +#endif // CONFIG_LSPLATFORM bool Upgrade(const std::string& firmware_url); std::function upgrade_callback_; diff --git a/sdkconfig.lsplatform b/sdkconfig.lsplatform index e1f30a5..fc31049 100644 --- a/sdkconfig.lsplatform +++ b/sdkconfig.lsplatform @@ -1 +1,2 @@ CONFIG_SERVICE_PROVIDER_LSPLATFORM=y +CONFIG_USE_WECHAT_MESSAGE_STYLE=y