mirror of
https://github.com/m5stack/xiaoling-esp32.git
synced 2026-05-20 11:42:56 -07:00
feat(app): 接入 LSPlatform 链路和激活流程
This commit is contained in:
@@ -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
@@ -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
@@ -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.
@@ -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_; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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显示器
|
||||
|
||||
@@ -73,6 +73,7 @@ dependencies:
|
||||
version: ^0.1.8
|
||||
rules:
|
||||
- if: target in [esp32c3]
|
||||
espressif/esp_new_jpeg: ^0.5.*
|
||||
|
||||
## Required IDF version
|
||||
idf:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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_
|
||||
@@ -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;
|
||||
|
||||
@@ -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 +1,2 @@
|
||||
CONFIG_SERVICE_PROVIDER_LSPLATFORM=y
|
||||
CONFIG_USE_WECHAT_MESSAGE_STYLE=y
|
||||
|
||||
Reference in New Issue
Block a user