Files
xiaozhi-esp32/main/assets.cc

407 lines
15 KiB
C++
Raw Normal View History

#include "assets.h"
#include "board.h"
#include "display.h"
#include "application.h"
#include "lvgl_theme.h"
#include <esp_log.h>
#include <spi_flash_mmap.h>
#include <esp_timer.h>
#include <cbin_font.h>
#define TAG "Assets"
struct mmap_assets_table {
char asset_name[32]; /*!< Name of the asset */
uint32_t asset_size; /*!< Size of the asset */
uint32_t asset_offset; /*!< Offset of the asset */
uint16_t asset_width; /*!< Width of the asset */
uint16_t asset_height; /*!< Height of the asset */
};
Assets::Assets(std::string default_assets_url) {
if (default_assets_url.find("http") == 0) {
default_assets_url_ = default_assets_url;
} else {
2025-09-10 01:02:23 +08:00
ESP_LOGE(TAG, "The default assets url is not a http url: %s", default_assets_url.c_str());
}
// Initialize the partition
InitializePartition();
}
Assets::~Assets() {
if (mmap_handle_ != 0) {
esp_partition_munmap(mmap_handle_);
}
}
uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) {
uint32_t checksum = 0;
for (uint32_t i = 0; i < length; i++) {
checksum += data[i];
}
return checksum & 0xFFFF;
}
bool Assets::InitializePartition() {
partition_valid_ = false;
checksum_valid_ = false;
assets_.clear();
partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, "assets");
if (partition_ == nullptr) {
ESP_LOGI(TAG, "No assets partition found");
return false;
}
int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
uint32_t storage_size = free_pages * 64 * 1024;
ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024);
ESP_LOGI(TAG, "The partition size is %ld KB", partition_->size / 1024);
if (storage_size < partition_->size) {
ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, partition_->size / 1024);
return false;
}
esp_err_t err = esp_partition_mmap(partition_, 0, partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err));
return false;
}
partition_valid_ = true;
uint32_t stored_files = *(uint32_t*)(mmap_root_ + 0);
uint32_t stored_chksum = *(uint32_t*)(mmap_root_ + 4);
uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8);
if (stored_len > partition_->size - 12) {
ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, partition_->size);
return false;
}
auto start_time = esp_timer_get_time();
uint32_t calculated_checksum = CalculateChecksum(mmap_root_ + 12, stored_len);
auto end_time = esp_timer_get_time();
ESP_LOGI(TAG, "The checksum calculation time is %d ms", int((end_time - start_time) / 1000));
if (calculated_checksum != stored_chksum) {
ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)", calculated_checksum, stored_chksum);
return false;
}
checksum_valid_ = true;
for (uint32_t i = 0; i < stored_files; i++) {
auto item = (const mmap_assets_table*)(mmap_root_ + 12 + i * sizeof(mmap_assets_table));
auto asset = Asset{
.size = static_cast<size_t>(item->asset_size),
.offset = static_cast<size_t>(12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset)
};
assets_[item->asset_name] = asset;
}
return checksum_valid_;
}
bool Assets::Apply() {
void* ptr = nullptr;
size_t size = 0;
if (!GetAssetData("index.json", ptr, size)) {
ESP_LOGE(TAG, "The index.json file is not found");
return false;
}
cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
if (root == nullptr) {
ESP_LOGE(TAG, "The index.json file is not valid");
return false;
}
cJSON* version = cJSON_GetObjectItem(root, "version");
if (cJSON_IsNumber(version)) {
if (version->valuedouble > 1) {
ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware", version->valueint);
return false;
}
}
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
if (cJSON_IsString(srmodels)) {
std::string srmodels_file = srmodels->valuestring;
if (GetAssetData(srmodels_file, ptr, size)) {
if (models_list_ != nullptr) {
esp_srmodel_deinit(models_list_);
models_list_ = nullptr;
}
models_list_ = srmodel_load(static_cast<uint8_t*>(ptr));
if (models_list_ != nullptr) {
auto& app = Application::GetInstance();
app.GetAudioService().SetModelsList(models_list_);
} else {
ESP_LOGE(TAG, "Failed to load srmodels.bin");
}
} else {
ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
}
}
#ifdef HAVE_LVGL
auto& theme_manager = LvglThemeManager::GetInstance();
auto light_theme = theme_manager.GetTheme("light");
auto dark_theme = theme_manager.GetTheme("dark");
cJSON* font = cJSON_GetObjectItem(root, "text_font");
if (cJSON_IsString(font)) {
std::string fonts_text_file = font->valuestring;
if (GetAssetData(fonts_text_file, ptr, size)) {
auto text_font = std::make_shared<LvglCBinFont>(ptr);
if (text_font->font() == nullptr) {
ESP_LOGE(TAG, "Failed to load fonts.bin");
return false;
}
2025-09-12 08:45:29 +08:00
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());
}
}
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
if (cJSON_IsArray(emoji_collection)) {
auto custom_emoji_collection = std::make_shared<EmojiCollection>();
int emoji_count = cJSON_GetArraySize(emoji_collection);
for (int i = 0; i < emoji_count; i++) {
cJSON* emoji = cJSON_GetArrayItem(emoji_collection, i);
if (cJSON_IsObject(emoji)) {
cJSON* name = cJSON_GetObjectItem(emoji, "name");
cJSON* file = cJSON_GetObjectItem(emoji, "file");
if (cJSON_IsString(name) && cJSON_IsString(file)) {
if (!GetAssetData(file->valuestring, ptr, size)) {
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
continue;
}
custom_emoji_collection->AddEmoji(name->valuestring, new LvglRawImage(ptr, size));
}
}
}
2025-09-12 08:45:29 +08:00
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");
2025-09-12 08:45:29 +08:00
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");
if (cJSON_IsString(text_color)) {
light_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
}
if (cJSON_IsString(background_color)) {
light_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring));
light_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
}
if (cJSON_IsString(background_image)) {
if (!GetAssetData(background_image->valuestring, ptr, size)) {
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
return false;
}
auto background_image = std::make_shared<LvglCBinImage>(ptr);
light_theme->set_background_image(background_image);
}
}
cJSON* dark_skin = cJSON_GetObjectItem(skin, "dark");
2025-09-12 08:45:29 +08:00
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");
if (cJSON_IsString(text_color)) {
dark_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
}
if (cJSON_IsString(background_color)) {
dark_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring));
dark_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
}
if (cJSON_IsString(background_image)) {
if (!GetAssetData(background_image->valuestring, ptr, size)) {
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
return false;
}
auto background_image = std::make_shared<LvglCBinImage>(ptr);
dark_theme->set_background_image(background_image);
}
}
}
#endif
auto display = Board::GetInstance().GetDisplay();
ESP_LOGI(TAG, "Refreshing display theme...");
2025-09-12 08:45:29 +08:00
auto current_theme = display->GetTheme();
if (current_theme != nullptr) {
display->SetTheme(current_theme);
}
cJSON_Delete(root);
return true;
}
bool Assets::Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback) {
ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str());
// 取消当前资源分区的内存映射
if (mmap_handle_ != 0) {
esp_partition_munmap(mmap_handle_);
mmap_handle_ = 0;
mmap_root_ = nullptr;
}
checksum_valid_ = false;
assets_.clear();
// 下载新的资源文件
auto network = Board::GetInstance().GetNetwork();
auto http = network->CreateHttp(0);
if (!http->Open("GET", url)) {
ESP_LOGE(TAG, "Failed to open HTTP connection");
return false;
}
if (http->GetStatusCode() != 200) {
ESP_LOGE(TAG, "Failed to get assets, status code: %d", http->GetStatusCode());
return false;
}
size_t content_length = http->GetBodyLength();
if (content_length == 0) {
ESP_LOGE(TAG, "Failed to get content length");
return false;
}
if (content_length > partition_->size) {
ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", content_length, partition_->size);
return false;
}
// 定义扇区大小为4KBESP32的标准扇区大小
const size_t SECTOR_SIZE = esp_partition_get_main_flash_sector_size();
// 计算需要擦除的扇区数量
size_t sectors_to_erase = (content_length + SECTOR_SIZE - 1) / SECTOR_SIZE; // 向上取整
size_t total_erase_size = sectors_to_erase * SECTOR_SIZE;
ESP_LOGI(TAG, "Sector size: %u, content length: %u, sectors to erase: %u, total erase size: %u",
SECTOR_SIZE, content_length, sectors_to_erase, total_erase_size);
// 写入新的资源文件到分区一边erase一边写入
char buffer[512];
size_t total_written = 0;
size_t recent_written = 0;
size_t current_sector = 0;
auto last_calc_time = esp_timer_get_time();
while (true) {
int ret = http->Read(buffer, sizeof(buffer));
if (ret < 0) {
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));
return false;
}
if (ret == 0) {
break;
}
// 检查是否需要擦除新的扇区
size_t write_end_offset = total_written + ret;
size_t needed_sectors = (write_end_offset + SECTOR_SIZE - 1) / SECTOR_SIZE;
// 擦除需要的新扇区
while (current_sector < needed_sectors) {
size_t sector_start = current_sector * SECTOR_SIZE;
size_t sector_end = (current_sector + 1) * SECTOR_SIZE;
// 确保擦除范围不超过分区大小
if (sector_end > partition_->size) {
ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", sector_end, partition_->size);
return false;
}
ESP_LOGD(TAG, "Erasing sector %u (offset: %u, size: %u)", current_sector, sector_start, SECTOR_SIZE);
esp_err_t err = esp_partition_erase_range(partition_, sector_start, SECTOR_SIZE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector, sector_start, esp_err_to_name(err));
return false;
}
current_sector++;
}
// 写入数据到分区
esp_err_t err = esp_partition_write(partition_, total_written, buffer, ret);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write to assets partition at offset %u: %s", total_written, esp_err_to_name(err));
return false;
}
total_written += ret;
recent_written += ret;
// 计算进度和速度
if (esp_timer_get_time() - last_calc_time >= 1000000 || total_written == content_length || ret == 0) {
size_t progress = total_written * 100 / content_length;
size_t speed = recent_written; // 每秒的字节数
ESP_LOGI(TAG, "Progress: %u%% (%u/%u), Speed: %u B/s, Sectors erased: %u",
progress, total_written, content_length, speed, current_sector);
if (progress_callback) {
progress_callback(progress, speed);
}
last_calc_time = esp_timer_get_time();
recent_written = 0; // 重置最近写入的字节数
}
}
http->Close();
if (total_written != content_length) {
ESP_LOGE(TAG, "Downloaded size (%u) does not match expected size (%u)", total_written, content_length);
return false;
}
ESP_LOGI(TAG, "Assets download completed, total written: %u bytes, total sectors erased: %u",
total_written, current_sector);
// 重新初始化资源分区
if (!InitializePartition()) {
ESP_LOGE(TAG, "Failed to re-initialize assets partition");
return false;
}
return true;
}
bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) {
auto asset = assets_.find(name);
if (asset == assets_.end()) {
return false;
}
auto data = (const char*)(mmap_root_ + asset->second.offset);
if (data[0] != 'Z' || data[1] != 'Z') {
ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]);
return false;
}
ptr = static_cast<void*>(const_cast<char*>(data + 2));
size = asset->second.size;
return true;
}