#include "assets.h" #include "board.h" #include "display.h" #include "application.h" #include "lvgl_theme.h" #include #include #include #include #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 { 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(item->asset_size), .offset = static_cast(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(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(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(ptr); if (text_font->font() == nullptr) { ESP_LOGE(TAG, "Failed to load fonts.bin"); return false; } 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(); 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)); } } } 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) && 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(ptr); light_theme->set_background_image(background_image); } } cJSON* dark_skin = cJSON_GetObjectItem(skin, "dark"); 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(ptr); dark_theme->set_background_image(background_image); } } } #endif auto display = Board::GetInstance().GetDisplay(); ESP_LOGI(TAG, "Refreshing display theme..."); 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 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; } // 定义扇区大小为4KB(ESP32的标准扇区大小) 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(const_cast(data + 2)); size = asset->second.size; return true; }