feat(app): 接入 LSPlatform 链路和激活流程

This commit is contained in:
eric
2025-08-30 14:24:34 +08:00
parent 5f149c37d7
commit a2f7f67901
14 changed files with 304 additions and 2 deletions
+1
View File
@@ -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")
+33 -1
View File
@@ -9,6 +9,10 @@
#include "assets/lang_config.h"
#include "mcp_server.h"
#ifdef CONFIG_LSPLATFORM
#include "image_fetcher.h"
#endif // CONFIG_LSPLATFORM
#include <cstring>
#include <esp_log.h>
#include <cJSON.h>
@@ -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(),
+1 -1
View File
@@ -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);
};
Binary file not shown.
Binary file not shown.
+5
View File
@@ -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_; }
+62
View File
@@ -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
+12
View File
@@ -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显示器
+1
View File
@@ -73,6 +73,7 @@ dependencies:
version: ^0.1.8
rules:
- if: target in [esp32c3]
espressif/esp_new_jpeg: ^0.5.*
## Required IDF version
idf:
+147
View File
@@ -0,0 +1,147 @@
#include "image_fetcher.h"
#include <cstring>
#include <esp_log.h>
#include <esp_app_desc.h>
#include <esp_jpeg_dec.h>
#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<uint8_t> 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;
}
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef _IMAGE_FETCHER_H_
#define _IMAGE_FETCHER_H_
#include <string>
#include <network_interface.h>
#include <lvgl.h>
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_
+7
View File
@@ -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;
+8
View File
@@ -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<void(int progress, size_t speed)> upgrade_callback_;
+1
View File
@@ -1 +1,2 @@
CONFIG_SERVICE_PROVIDER_LSPLATFORM=y
CONFIG_USE_WECHAT_MESSAGE_STYLE=y