feat: add manual upgrade firmware

This commit is contained in:
Terrence
2025-09-12 08:45:29 +08:00
parent 469ee2d92a
commit f79506d58b
9 changed files with 169 additions and 66 deletions

View File

@@ -164,43 +164,10 @@ void Application::CheckNewVersion(Ota& ota) {
retry_delay = 10; // 重置重试延迟时间
if (ota.HasNewVersion()) {
Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "download", Lang::Sounds::OGG_UPGRADE);
vTaskDelay(pdMS_TO_TICKS(3000));
SetDeviceState(kDeviceStateUpgrading);
std::string message = std::string(Lang::Strings::NEW_VERSION) + ota.GetFirmwareVersion();
display->SetChatMessage("system", message.c_str());
board.SetPowerSaveMode(false);
audio_service_.Stop();
vTaskDelay(pdMS_TO_TICKS(1000));
bool upgrade_success = ota.StartUpgrade([display](int progress, size_t speed) {
std::thread([display, progress, speed]() {
char buffer[32];
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
display->SetChatMessage("system", buffer);
}).detach();
});
if (!upgrade_success) {
// Upgrade failed, restart audio service and continue running
ESP_LOGE(TAG, "Firmware upgrade failed, restarting audio service and continuing operation...");
audio_service_.Start(); // Restart audio service
board.SetPowerSaveMode(true); // Restore power save mode
Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION);
vTaskDelay(pdMS_TO_TICKS(3000));
// Continue to normal operation (don't break, just fall through)
} else {
// Upgrade success, reboot immediately
ESP_LOGI(TAG, "Firmware upgrade successful, rebooting...");
display->SetChatMessage("system", "Upgrade successful, rebooting...");
vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message
Reboot();
if (UpgradeFirmware(ota)) {
return; // This line will never be reached after reboot
}
// If upgrade failed, continue to normal operation (don't break, just fall through)
}
// No new version, mark the current version as valid
@@ -768,9 +735,64 @@ void Application::Reboot() {
}
protocol_.reset();
audio_service_.Stop();
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
}
bool Application::UpgradeFirmware(Ota& ota, const std::string& url) {
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
// Use provided URL or get from OTA object
std::string upgrade_url = url.empty() ? ota.GetFirmwareUrl() : url;
std::string version_info = url.empty() ? ota.GetFirmwareVersion() : "(Manual upgrade)";
// Close audio channel if it's open
if (protocol_ && protocol_->IsAudioChannelOpened()) {
ESP_LOGI(TAG, "Closing audio channel before firmware upgrade");
protocol_->CloseAudioChannel();
}
ESP_LOGI(TAG, "Starting firmware upgrade from URL: %s", upgrade_url.c_str());
Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "download", Lang::Sounds::OGG_UPGRADE);
vTaskDelay(pdMS_TO_TICKS(3000));
SetDeviceState(kDeviceStateUpgrading);
std::string message = std::string(Lang::Strings::NEW_VERSION) + version_info;
display->SetChatMessage("system", message.c_str());
board.SetPowerSaveMode(false);
audio_service_.Stop();
vTaskDelay(pdMS_TO_TICKS(1000));
bool upgrade_success = ota.StartUpgradeFromUrl(upgrade_url, [display](int progress, size_t speed) {
std::thread([display, progress, speed]() {
char buffer[32];
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
display->SetChatMessage("system", buffer);
}).detach();
});
if (!upgrade_success) {
// Upgrade failed, restart audio service and continue running
ESP_LOGE(TAG, "Firmware upgrade failed, restarting audio service and continuing operation...");
audio_service_.Start(); // Restart audio service
board.SetPowerSaveMode(true); // Restore power save mode
Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION);
vTaskDelay(pdMS_TO_TICKS(3000));
return false;
} else {
// Upgrade success, reboot immediately
ESP_LOGI(TAG, "Firmware upgrade successful, rebooting...");
display->SetChatMessage("system", "Upgrade successful, rebooting...");
vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message
Reboot();
return true;
}
}
void Application::WakeWordInvoke(const std::string& wake_word) {
if (device_state_ == kDeviceStateIdle) {
ToggleChatState();

View File

@@ -56,6 +56,7 @@ public:
void StopListening();
void Reboot();
void WakeWordInvoke(const std::string& wake_word);
bool UpgradeFirmware(Ota& ota, const std::string& url = "");
bool CanEnterSleepMode();
void SendMcpMessage(const std::string& payload);
void SetAecMode(AecMode mode);

View File

@@ -161,8 +161,12 @@ bool Assets::Apply() {
ESP_LOGE(TAG, "Failed to load fonts.bin");
return false;
}
light_theme->set_text_font(text_font);
dark_theme->set_text_font(text_font);
if (light_theme != nullptr) {
light_theme->set_text_font(text_font);
}
if (dark_theme != nullptr) {
dark_theme->set_text_font(text_font);
}
} else {
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
}
@@ -186,14 +190,18 @@ bool Assets::Apply() {
}
}
}
light_theme->set_emoji_collection(custom_emoji_collection);
dark_theme->set_emoji_collection(custom_emoji_collection);
if (light_theme != nullptr) {
light_theme->set_emoji_collection(custom_emoji_collection);
}
if (dark_theme != nullptr) {
dark_theme->set_emoji_collection(custom_emoji_collection);
}
}
cJSON* skin = cJSON_GetObjectItem(root, "skin");
if (cJSON_IsObject(skin)) {
cJSON* light_skin = cJSON_GetObjectItem(skin, "light");
if (cJSON_IsObject(light_skin)) {
if (cJSON_IsObject(light_skin) && light_theme != nullptr) {
cJSON* text_color = cJSON_GetObjectItem(light_skin, "text_color");
cJSON* background_color = cJSON_GetObjectItem(light_skin, "background_color");
cJSON* background_image = cJSON_GetObjectItem(light_skin, "background_image");
@@ -214,7 +222,7 @@ bool Assets::Apply() {
}
}
cJSON* dark_skin = cJSON_GetObjectItem(skin, "dark");
if (cJSON_IsObject(dark_skin)) {
if (cJSON_IsObject(dark_skin) && dark_theme != nullptr) {
cJSON* text_color = cJSON_GetObjectItem(dark_skin, "text_color");
cJSON* background_color = cJSON_GetObjectItem(dark_skin, "background_color");
cJSON* background_image = cJSON_GetObjectItem(dark_skin, "background_image");
@@ -239,7 +247,11 @@ bool Assets::Apply() {
auto display = Board::GetInstance().GetDisplay();
ESP_LOGI(TAG, "Refreshing display theme...");
display->SetTheme(display->GetTheme());
auto current_theme = display->GetTheme();
if (current_theme != nullptr) {
display->SetTheme(current_theme);
}
cJSON_Delete(root);
return true;
}
@@ -253,7 +265,6 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
mmap_handle_ = 0;
mmap_root_ = nullptr;
}
partition_valid_ = false;
checksum_valid_ = false;
assets_.clear();

View File

@@ -1,5 +1,7 @@
#include "oled_display.h"
#include "assets/lang_config.h"
#include "lvgl_theme.h"
#include "lvgl_font.h"
#include <string>
#include <algorithm>
@@ -20,8 +22,19 @@ OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handl
: panel_io_(panel_io), panel_(panel) {
width_ = width;
height_ = height;
text_font_ = &LVGL_TEXT_FONT;
icon_font_ = &LVGL_ICON_FONT;
auto text_font = std::make_shared<LvglBuiltInFont>(&LVGL_TEXT_FONT);
auto icon_font = std::make_shared<LvglBuiltInFont>(&LVGL_ICON_FONT);
auto large_icon_font = std::make_shared<LvglBuiltInFont>(&font_awesome_30_1);
auto dark_theme = new LvglTheme("dark");
dark_theme->set_text_font(text_font);
dark_theme->set_icon_font(icon_font);
dark_theme->set_large_icon_font(large_icon_font);
auto& theme_manager = LvglThemeManager::GetInstance();
theme_manager.RegisterTheme("dark", dark_theme);
current_theme_ = dark_theme;
ESP_LOGI(TAG, "Initialize LVGL");
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
@@ -123,8 +136,13 @@ void OledDisplay::SetChatMessage(const char* role, const char* content) {
void OledDisplay::SetupUI_128x64() {
DisplayLockGuard lock(this);
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
auto text_font = lvgl_theme->text_font()->font();
auto icon_font = lvgl_theme->icon_font()->font();
auto large_icon_font = lvgl_theme->large_icon_font()->font();
auto screen = lv_screen_active();
lv_obj_set_style_text_font(screen, text_font_, 0);
lv_obj_set_style_text_font(screen, text_font, 0);
lv_obj_set_style_text_color(screen, lv_color_black(), 0);
/* Container */
@@ -159,7 +177,7 @@ void OledDisplay::SetupUI_128x64() {
lv_obj_set_style_border_width(content_left_, 0, 0);
emotion_label_ = lv_label_create(content_left_);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0);
lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0);
lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI);
lv_obj_center(emotion_label_);
lv_obj_set_style_pad_top(emotion_label_, 8, 0);
@@ -195,7 +213,7 @@ void OledDisplay::SetupUI_128x64() {
network_label_ = lv_label_create(status_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, icon_font_, 0);
lv_obj_set_style_text_font(network_label_, icon_font, 0);
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(notification_label_, 1);
@@ -210,15 +228,15 @@ void OledDisplay::SetupUI_128x64() {
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, icon_font_, 0);
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
battery_label_ = lv_label_create(status_bar_);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font_, 0);
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
low_battery_popup_ = lv_obj_create(screen);
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font_->line_height * 2);
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(low_battery_popup_, lv_color_black(), 0);
lv_obj_set_style_radius(low_battery_popup_, 10, 0);
@@ -232,8 +250,13 @@ void OledDisplay::SetupUI_128x64() {
void OledDisplay::SetupUI_128x32() {
DisplayLockGuard lock(this);
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
auto text_font = lvgl_theme->text_font()->font();
auto icon_font = lvgl_theme->icon_font()->font();
auto large_icon_font = lvgl_theme->large_icon_font()->font();
auto screen = lv_screen_active();
lv_obj_set_style_text_font(screen, text_font_, 0);
lv_obj_set_style_text_font(screen, text_font, 0);
/* Container */
container_ = lv_obj_create(screen);
@@ -251,7 +274,7 @@ void OledDisplay::SetupUI_128x32() {
lv_obj_set_style_radius(content_, 0, 0);
emotion_label_ = lv_label_create(content_);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0);
lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0);
lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI);
lv_obj_center(emotion_label_);
@@ -286,15 +309,15 @@ void OledDisplay::SetupUI_128x32() {
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, icon_font_, 0);
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
network_label_ = lv_label_create(status_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, icon_font_, 0);
lv_obj_set_style_text_font(network_label_, icon_font, 0);
battery_label_ = lv_label_create(status_bar_);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font_, 0);
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
chat_message_label_ = lv_label_create(side_bar_);
lv_obj_set_size(chat_message_label_, width_ - 32, LV_SIZE_CONTENT);
@@ -323,3 +346,13 @@ void OledDisplay::SetEmotion(const char* emotion) {
lv_label_set_text(emotion_label_, FONT_AWESOME_NEUTRAL);
}
}
void OledDisplay::SetTheme(Theme* theme) {
DisplayLockGuard lock(this);
auto lvgl_theme = static_cast<LvglTheme*>(theme);
auto text_font = lvgl_theme->text_font()->font();
auto screen = lv_screen_active();
lv_obj_set_style_text_font(screen, text_font, 0);
}

View File

@@ -20,8 +20,6 @@ private:
lv_obj_t* side_bar_ = nullptr;
lv_obj_t *emotion_label_ = nullptr;
lv_obj_t* chat_message_label_ = nullptr;
const lv_font_t* text_font_ = nullptr;
const lv_font_t* icon_font_ = nullptr;
virtual bool Lock(int timeout_ms = 0) override;
virtual void Unlock() override;
@@ -35,6 +33,7 @@ public:
virtual void SetChatMessage(const char* role, const char* content) override;
virtual void SetEmotion(const char* emotion) override;
virtual void SetTheme(Theme* theme) override;
};
#endif // OLED_DISPLAY_H

View File

@@ -12,6 +12,7 @@
#include "application.h"
#include "display.h"
#include "oled_display.h"
#include "board.h"
#include "settings.h"
#include "lvgl_theme.h"
@@ -136,12 +137,36 @@ void McpServer::AddUserOnlyTools() {
AddUserOnlyTool("self.reboot", "Reboot the system",
PropertyList(),
[this](const PropertyList& properties) -> ReturnValue {
std::thread([]() {
auto& app = Application::GetInstance();
app.Schedule([]() {
ESP_LOGW(TAG, "User requested reboot");
vTaskDelay(pdMS_TO_TICKS(1000));
auto& app = Application::GetInstance();
app.Reboot();
}).detach();
});
return true;
});
// Firmware upgrade
AddUserOnlyTool("self.upgrade_firmware", "Upgrade firmware from a specific URL. This will download and install the firmware, then reboot the device.",
PropertyList({
Property("url", kPropertyTypeString, "The URL of the firmware binary file to download and install")
}),
[this](const PropertyList& properties) -> ReturnValue {
auto url = properties["url"].value<std::string>();
ESP_LOGI(TAG, "User requested firmware upgrade from URL: %s", url.c_str());
auto& app = Application::GetInstance();
app.Schedule([url]() {
auto& app = Application::GetInstance();
auto ota = std::make_unique<Ota>();
bool success = app.UpgradeFirmware(*ota, url);
if (!success) {
ESP_LOGE(TAG, "Firmware upgrade failed");
}
});
return true;
});
@@ -155,6 +180,11 @@ void McpServer::AddUserOnlyTools() {
cJSON *json = cJSON_CreateObject();
cJSON_AddNumberToObject(json, "width", display->width());
cJSON_AddNumberToObject(json, "height", display->height());
if (dynamic_cast<OledDisplay*>(display)) {
cJSON_AddBoolToObject(json, "monochrome", true);
} else {
cJSON_AddBoolToObject(json, "monochrome", false);
}
return json;
});

View File

@@ -325,13 +325,9 @@ bool Ota::Upgrade(const std::string& firmware_url) {
if (image_header.size() >= sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) {
esp_app_desc_t new_app_info;
memcpy(&new_app_info, image_header.data() + sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t), sizeof(esp_app_desc_t));
ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version);
auto current_version = esp_app_get_description()->version;
if (memcmp(new_app_info.version, current_version, sizeof(new_app_info.version)) == 0) {
ESP_LOGE(TAG, "Firmware version is the same, skipping upgrade");
return false;
}
ESP_LOGI(TAG, "Current version: %s, New version: %s", current_version, new_app_info.version);
if (esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle)) {
esp_ota_abort(update_handle);
@@ -377,6 +373,11 @@ bool Ota::StartUpgrade(std::function<void(int progress, size_t speed)> callback)
return Upgrade(firmware_url_);
}
bool Ota::StartUpgradeFromUrl(const std::string& url, std::function<void(int progress, size_t speed)> callback) {
upgrade_callback_ = callback;
return Upgrade(url);
}
std::vector<int> Ota::ParseVersion(const std::string& version) {
std::vector<int> versionNumbers;
std::stringstream ss(version);

View File

@@ -21,10 +21,12 @@ public:
bool HasActivationCode() { return has_activation_code_; }
bool HasServerTime() { return has_server_time_; }
bool StartUpgrade(std::function<void(int progress, size_t speed)> callback);
bool StartUpgradeFromUrl(const std::string& url, std::function<void(int progress, size_t speed)> callback);
void MarkCurrentVersionValid();
const std::string& GetFirmwareVersion() const { return firmware_version_; }
const std::string& GetCurrentVersion() const { return current_version_; }
const std::string& GetFirmwareUrl() const { return firmware_url_; }
const std::string& GetActivationMessage() const { return activation_message_; }
const std::string& GetActivationCode() const { return activation_code_; }
std::string GetCheckVersionUrl();

View File

@@ -36,6 +36,10 @@ MqttProtocol::~MqttProtocol() {
esp_timer_stop(reconnect_timer_);
esp_timer_delete(reconnect_timer_);
}
udp_.reset();
mqtt_.reset();
if (event_group_handle_ != nullptr) {
vEventGroupDelete(event_group_handle_);
}