feat(assets): embed LCD and audio resources in OTA app
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# PNG/Opus 素材已链接进应用镜像,两块 OTA 应用分区均需容纳完整固件和素材。
|
||||
nvs, data, nvs, 0x9000, 0x4000,
|
||||
otadata, data, ota, 0xd000, 0x2000,
|
||||
phy_init, data, phy, 0xf000, 0x1000,
|
||||
gifpack, data, littlefs, , 0x200000,
|
||||
ota_0, app, ota_0, , 0x380000,
|
||||
ota_1, app, ota_1, , 0x380000,
|
||||
ota_0, app, ota_0, , 0x480000,
|
||||
ota_1, app, ota_1, , 0x480000,
|
||||
model, data, spiffs, , 0x300000,
|
||||
coredump, data, coredump,, 0x10000,
|
||||
|
||||
|
@@ -20,3 +20,16 @@ add_custom_target(versioned_firmware ALL
|
||||
COMMENT "Creating versioned firmware ${CMAKE_PROJECT_NAME}-${TQ_PROJECT_VERSION}.bin"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# 依赖应用和模型产物,确保每次构建输出的是本次镜像的分区及空间数据。
|
||||
add_custom_target(flash_usage_report ALL
|
||||
COMMAND ${PYTHON} "${CMAKE_SOURCE_DIR}/tools/report_flash_usage.py"
|
||||
--partition-table "${CMAKE_BINARY_DIR}/partition_table/partition-table.bin"
|
||||
--app "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin"
|
||||
--asset-pack "${CMAKE_BINARY_DIR}/esp-idf/platform/platform_assets.pack"
|
||||
--model "${CMAKE_BINARY_DIR}/srmodels/srmodels.bin"
|
||||
--flash-size "${CONFIG_ESPTOOLPY_FLASHSIZE}"
|
||||
DEPENDS versioned_firmware srmodels_bin
|
||||
COMMENT "Reporting Flash partition and embedded asset usage"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE
|
||||
#define CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE "local_wake_response.opus"
|
||||
#define CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE "audio_prompts/local_wake_response.opus"
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_LOCAL_WAKE_RETRY_MS
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
* 2 秒则给播放器正常收尾机会,超过后才按停滞恢复,避免网络抖动造成频繁强停。
|
||||
*/
|
||||
#define CONTROL_PLANE_WATCHDOG_TASK_STACK_SIZE CONFIG_TQ_MULTIMODAL_WATCHDOG_TASK_STACK_BYTES
|
||||
#define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_FILE "/littlefs/audio_prompts/server_connect_success.opus"
|
||||
#define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_FILE "audio_prompts/server_connect_success.opus"
|
||||
#define CONTROL_PLANE_CLIENT_LIFECYCLE_WAIT_MS 30000U
|
||||
#define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_RETRY_MS 1000U
|
||||
#define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_MAX_ATTEMPTS 3U
|
||||
|
||||
@@ -42,14 +42,14 @@
|
||||
* 多次确认用于排除负载突变导致的瞬时低压;紧急阈值只需两次以缩短保护延迟。
|
||||
* 轻待机下采样间隔显著拉长,充电 GPIO 中断仍可及时唤醒状态更新。
|
||||
*/
|
||||
#define BATTERY_LOW_PROMPT_FILE "/littlefs/audio_prompts/battery_low_20.opus"
|
||||
#define BATTERY_FULL_PROMPT_FILE "/littlefs/audio_prompts/charge_complete.opus"
|
||||
#define BATTERY_LOW_PROMPT_FILE "audio_prompts/battery_low_20.opus"
|
||||
#define BATTERY_FULL_PROMPT_FILE "audio_prompts/charge_complete.opus"
|
||||
|
||||
#ifndef CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
#define CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE 0
|
||||
#endif
|
||||
#ifndef CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE
|
||||
#define CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE "/littlefs/audio_prompts/shutdown_power_empty.opus"
|
||||
#define CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE "audio_prompts/shutdown_power_empty.opus"
|
||||
#endif
|
||||
|
||||
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES 12288
|
||||
#endif
|
||||
|
||||
#define CONTROL_PLANE_WIFI_SUCCESS_PROMPT_FILE "/littlefs/audio_prompts/network_connect_success.opus"
|
||||
#define CONTROL_PLANE_WIFI_SUCCESS_PROMPT_FILE "audio_prompts/network_connect_success.opus"
|
||||
|
||||
#define CONTROL_PLANE_STA_CONNECT_MAX_ATTEMPTS 3
|
||||
#define CONTROL_PLANE_STA_RETRY_BASE_DELAY_MS 300
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE
|
||||
#define CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE "standby_5min.opus"
|
||||
#define CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE "audio_prompts/standby_5min.opus"
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT
|
||||
|
||||
@@ -25,15 +25,15 @@
|
||||
/* 3 秒宽限过滤 WebSocket 短重连,5 秒冷却防止 Wi-Fi 与服务端事件交替造成提示音轰炸。 */
|
||||
|
||||
#define CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/network_interrupted_repairing.opus"
|
||||
"audio_prompts/network_interrupted_repairing.opus"
|
||||
#define CONTROL_PLANE_WIFI_ACTION_BLOCKED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/network_unstable.opus"
|
||||
"audio_prompts/network_unstable.opus"
|
||||
#define CONTROL_PLANE_SERVER_INTERRUPTED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/server_abnormal_interrupted.opus"
|
||||
"audio_prompts/server_abnormal_interrupted.opus"
|
||||
#define CONTROL_PLANE_SERVER_ACTION_BLOCKED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/server_repair_wait_reopen.opus"
|
||||
"audio_prompts/server_repair_wait_reopen.opus"
|
||||
#define CONTROL_PLANE_NETWORK_RESTORED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/signal_restored_business_resume.opus"
|
||||
"audio_prompts/signal_restored_business_resume.opus"
|
||||
|
||||
typedef enum {
|
||||
NETWORK_FEEDBACK_STATE_BOOTING = 0,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
#define CONTROL_PLANE_PTT_HOLD_STEP_MS 80
|
||||
|
||||
/* 长按 PTT 以 80 ms 小步等待,既能及时感知释放,也能周期检查会话已失败或被取消。 */
|
||||
#define CONTROL_PLANE_MUYU_PROMPT_FILE "muyu.opus"
|
||||
#define CONTROL_PLANE_MUYU_PROMPT_FILE "audio_prompts/muyu.opus"
|
||||
|
||||
#ifndef CONFIG_TQ_LOCAL_TASK_CORE
|
||||
#define CONFIG_TQ_LOCAL_TASK_CORE 1
|
||||
|
||||
@@ -336,7 +336,6 @@ esp_err_t domain_runtime_slot_feature_switch_init(void);
|
||||
bool domain_runtime_slot_feature_switch_is_active(void);
|
||||
|
||||
esp_err_t domain_runtime_prepare_storage(void);
|
||||
esp_err_t domain_runtime_mount_littlefs(void);
|
||||
void domain_runtime_read_device_identity(char *device_id,
|
||||
size_t device_id_len,
|
||||
char *serial_number,
|
||||
|
||||
@@ -905,11 +905,6 @@ esp_err_t domain_runtime_prepare_storage(void)
|
||||
return platform_prepare_storage();
|
||||
}
|
||||
|
||||
esp_err_t domain_runtime_mount_littlefs(void)
|
||||
{
|
||||
return platform_mount_littlefs();
|
||||
}
|
||||
|
||||
void domain_runtime_read_device_identity(char *device_id,
|
||||
size_t device_id_len,
|
||||
char *serial_number,
|
||||
|
||||
@@ -16,6 +16,7 @@ set(platform_srcs
|
||||
"src/platform_io_effects.c"
|
||||
"src/lcd_direct.c"
|
||||
"src/lcd_direct_assets.c"
|
||||
"src/platform_asset_bundle.c"
|
||||
"third_party/lodepng/lodepng.cpp"
|
||||
"third_party/lodepng/lodepng_esp_alloc.cpp"
|
||||
"third_party/lodepng/lodepng_esp_bridge.cpp"
|
||||
@@ -54,7 +55,6 @@ set(platform_priv_requires
|
||||
esp_wifi
|
||||
esp_websocket_client
|
||||
json
|
||||
littlefs
|
||||
log
|
||||
espressif__mdns
|
||||
mbedtls
|
||||
@@ -90,6 +90,8 @@ set(platform_lcd_asset_manifest "${platform_lcd_asset_src_dir}/manifest.json")
|
||||
set(platform_lcd_asset_optimizer "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/optimize_lcd_assets.py")
|
||||
set(platform_asset_image_dir "${CMAKE_CURRENT_BINARY_DIR}/asset_image")
|
||||
set(platform_asset_stage_stamp "${CMAKE_CURRENT_BINARY_DIR}/asset_image.stamp")
|
||||
set(platform_asset_pack "${CMAKE_CURRENT_BINARY_DIR}/platform_assets.pack")
|
||||
set(platform_asset_packer "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/pack_embedded_assets.py")
|
||||
file(GLOB_RECURSE platform_audio_prompt_files CONFIGURE_DEPENDS
|
||||
"${platform_audio_prompt_src_dir}/*.opus"
|
||||
)
|
||||
@@ -199,6 +201,7 @@ foreach(lcd_png_asset IN LISTS platform_lcd_png_assets)
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# 先校验并暂存实际引用的 PNG/Opus,再由打包脚本生成应用内只读资源包。
|
||||
add_custom_command(
|
||||
OUTPUT "${platform_asset_stage_stamp}"
|
||||
COMMAND ${PYTHON} "${platform_lcd_asset_optimizer}" check-current
|
||||
@@ -214,9 +217,14 @@ add_custom_command(
|
||||
"${platform_lcd_asset_optimizer}"
|
||||
)
|
||||
|
||||
littlefs_create_partition_image(
|
||||
gifpack
|
||||
"${platform_asset_image_dir}"
|
||||
FLASH_IN_PROJECT
|
||||
DEPENDS "${platform_asset_stage_stamp}"
|
||||
# 素材或打包脚本变化时重新生成包,并通过二进制链接目标纳入 OTA 应用镜像。
|
||||
add_custom_command(
|
||||
OUTPUT "${platform_asset_pack}"
|
||||
COMMAND ${PYTHON} "${platform_asset_packer}"
|
||||
"${platform_asset_image_dir}" "${platform_asset_pack}"
|
||||
DEPENDS "${platform_asset_stage_stamp}" "${platform_asset_packer}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
target_add_binary_data(${COMPONENT_LIB} "${platform_asset_pack}" BINARY
|
||||
RENAME_TO platform_assets_pack)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
dependencies:
|
||||
idf: '>=4.4'
|
||||
joltwallet/littlefs: 1.20.4
|
||||
espressif/esp_websocket_client: ^1.6.1
|
||||
espressif/esp_codec_dev: ^1.5.4
|
||||
espressif/esp_audio_codec: ^2.4.1
|
||||
|
||||
@@ -221,7 +221,6 @@ typedef struct {
|
||||
esp_err_t platform_init(void);
|
||||
esp_err_t platform_prepare_storage(void);
|
||||
bool platform_storage_is_ready(void);
|
||||
esp_err_t platform_mount_littlefs(void);
|
||||
void platform_read_device_identity(char *device_id,
|
||||
size_t device_id_len,
|
||||
char *serial_number,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* RGB565 图片描述符,实际像素由渲染任务根据路径加载和解码。 */
|
||||
/* RGB565 图片描述符,实际像素由渲染任务根据逻辑名称查找和解码。 */
|
||||
typedef struct {
|
||||
const char *path;
|
||||
} lcd_direct_image_t;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *data;
|
||||
size_t size;
|
||||
} platform_asset_view_t;
|
||||
|
||||
/* 按包内完整相对名称查找素材,例如 lcd/ota1.png、audio_prompts/boot.opus;成功后 data 指向当前固件的只读映射区,无需释放。 */
|
||||
esp_err_t platform_asset_find(const char *name, platform_asset_view_t *out_asset);
|
||||
@@ -7,8 +7,8 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 同步播放 LittleFS 中的 Opus 提示音。函数负责分帧、下行播放会话和完成等待,
|
||||
* 返回时文件句柄已关闭;路径解析和播放超时均在实现内处理。
|
||||
* 同步播放应用镜像中的 Opus 提示音。函数负责分帧、下行播放会话和完成等待;
|
||||
* 素材查找和播放超时均在实现内处理。
|
||||
*/
|
||||
esp_err_t voice_prompt_play_file(const char *path);
|
||||
|
||||
|
||||
@@ -7,15 +7,13 @@
|
||||
|
||||
#include "lcd_direct.h"
|
||||
#include "lcd_direct_assets.h"
|
||||
#include "platform_asset_bundle.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/ledc.h"
|
||||
@@ -55,7 +53,7 @@
|
||||
#define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U
|
||||
/*
|
||||
* PNG 缓存固定6项并使用 LRU 淘汰,限制 PSRAM 峰值;边界测试恰好覆盖一次完整
|
||||
* 缓存命中周期。路径上限同时约束缓存键和 LittleFS 资源路径复制。
|
||||
* 缓存命中周期。名称上限同时约束缓存键和嵌入式素材索引。
|
||||
*/
|
||||
#define LCD_DIRECT_FRAME_CACHE_MAX 6U
|
||||
#define LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES LCD_DIRECT_FRAME_CACHE_MAX
|
||||
@@ -316,55 +314,6 @@ static uint16_t lcd_direct_blend_rgb565(uint16_t dst, uint16_t src, uint8_t alph
|
||||
return lcd_direct_rgb888_to_rgb565(out_r, out_g, out_b);
|
||||
}
|
||||
|
||||
static esp_err_t lcd_direct_read_file_psram(const char *path, unsigned char **out_data, size_t *out_size)
|
||||
{
|
||||
/* 整个 PNG 文件优先读入 PSRAM;任何短读都视为失败并关闭句柄。 */
|
||||
ESP_RETURN_ON_FALSE(path != NULL && out_data != NULL && out_size != NULL,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
TAG,
|
||||
"invalid PNG read request");
|
||||
*out_data = NULL;
|
||||
*out_size = 0;
|
||||
|
||||
errno = 0;
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
ESP_LOGE(TAG, "open PNG asset failed path=%s errno=%d", path, errno);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
off_t file_size = lseek(fd, 0, SEEK_END);
|
||||
if (file_size <= 0 || lseek(fd, 0, SEEK_SET) < 0) {
|
||||
ESP_LOGE(TAG, "stat PNG asset failed path=%s errno=%d", path, errno);
|
||||
close(fd);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
unsigned char *data = (unsigned char *)lcd_direct_alloc_psram((size_t)file_size);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "allocate PNG file buffer failed path=%s size=%u", path, (unsigned)file_size);
|
||||
close(fd);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
while (offset < (size_t)file_size) {
|
||||
ssize_t bytes_read = read(fd, data + offset, (size_t)file_size - offset);
|
||||
if (bytes_read <= 0) {
|
||||
ESP_LOGE(TAG, "read PNG asset failed path=%s errno=%d", path, errno);
|
||||
free(data);
|
||||
close(fd);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
offset += (size_t)bytes_read;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
*out_data = data;
|
||||
*out_size = (size_t)file_size;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void lcd_direct_release_decoded_image(lcd_direct_decoded_image_t *decoded)
|
||||
{
|
||||
if (decoded == NULL) {
|
||||
@@ -386,18 +335,19 @@ static esp_err_t lcd_direct_decode_png(const char *path,
|
||||
"invalid PNG decode request");
|
||||
memset(out_decoded, 0, sizeof(*out_decoded));
|
||||
|
||||
unsigned char *png_data = NULL;
|
||||
size_t png_size = 0;
|
||||
esp_err_t read_err = lcd_direct_read_file_psram(path, &png_data, &png_size);
|
||||
if (read_err != ESP_OK) {
|
||||
return read_err;
|
||||
/* PNG 字节直接取自应用镜像的只读映射区;解码器只读取输入,不持有或释放该指针。 */
|
||||
platform_asset_view_t asset = {0};
|
||||
esp_err_t find_err = platform_asset_find(path, &asset);
|
||||
if (find_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "find PNG asset failed path=%s err=%s", path, esp_err_to_name(find_err));
|
||||
return find_err;
|
||||
}
|
||||
|
||||
unsigned char *rgba = NULL;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
unsigned decode_err = lodepng_esp_decode32(&rgba, &width, &height, png_data, png_size);
|
||||
free(png_data);
|
||||
unsigned decode_err = lodepng_esp_decode32(&rgba, &width, &height,
|
||||
asset.data, asset.size);
|
||||
if (decode_err != 0U) {
|
||||
ESP_LOGE(TAG, "decode PNG failed path=%s error=%u text=%s",
|
||||
path,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define LCD_ASSET_BASE_PATH "/littlefs/lcd/"
|
||||
#define LCD_ASSET_BASE_PATH "lcd/"
|
||||
|
||||
/*
|
||||
* 静态表只保存资源元数据和路径,不在此文件执行 PNG 解码。
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "platform_asset_bundle.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#define ASSET_BUNDLE_HEADER_BYTES 12U
|
||||
#define ASSET_ENTRY_HEADER_BYTES 6U
|
||||
|
||||
/* 素材包由构建脚本链接进应用镜像,链接符号界定只读 Flash 映射区。格式为 magic(8) + 数量(u32 LE),随后逐项存放名称长度(u16 LE)、数据长度(u32 LE)、UTF-8 名称和原始数据。 */
|
||||
extern const uint8_t platform_assets_pack_start[]
|
||||
asm("_binary_platform_assets_pack_start");
|
||||
extern const uint8_t platform_assets_pack_end[]
|
||||
asm("_binary_platform_assets_pack_end");
|
||||
|
||||
static uint16_t read_u16_le(const uint8_t *data)
|
||||
{
|
||||
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
|
||||
}
|
||||
|
||||
static uint32_t read_u32_le(const uint8_t *data)
|
||||
{
|
||||
return (uint32_t)data[0] | ((uint32_t)data[1] << 8) |
|
||||
((uint32_t)data[2] << 16) | ((uint32_t)data[3] << 24);
|
||||
}
|
||||
|
||||
esp_err_t platform_asset_find(const char *name, platform_asset_view_t *out_asset)
|
||||
{
|
||||
if (name == NULL || out_asset == NULL || name[0] == '\0') {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*out_asset = (platform_asset_view_t) {0};
|
||||
|
||||
/* 调用方必须使用包内完整相对名称,例如 audio_prompts/boot.opus。 */
|
||||
const size_t name_len = strlen(name);
|
||||
if (name_len == 0U) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const uint8_t *cursor = platform_assets_pack_start;
|
||||
const uint8_t *end = platform_assets_pack_end;
|
||||
if ((size_t)(end - cursor) < ASSET_BUNDLE_HEADER_BYTES ||
|
||||
memcmp(cursor, "TQASSET1", 8U) != 0) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const uint32_t count = read_u32_le(cursor + 8U);
|
||||
cursor += ASSET_BUNDLE_HEADER_BYTES;
|
||||
|
||||
/* 每次推进游标前校验剩余字节,避免损坏的包头或条目越过链接区间。 */
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if ((size_t)(end - cursor) < ASSET_ENTRY_HEADER_BYTES) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const size_t stored_name_len = read_u16_le(cursor);
|
||||
const size_t data_len = read_u32_le(cursor + 2U);
|
||||
cursor += ASSET_ENTRY_HEADER_BYTES;
|
||||
if (stored_name_len == 0U || data_len == 0U ||
|
||||
stored_name_len > (size_t)(end - cursor)) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const uint8_t *stored_name = cursor;
|
||||
cursor += stored_name_len;
|
||||
if (data_len > (size_t)(end - cursor)) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const bool matches = stored_name_len == name_len &&
|
||||
memcmp(stored_name, name, name_len) == 0;
|
||||
if (matches) {
|
||||
/* 直接返回镜像内数据视图,调用方只能读取,不能释放或写入。 */
|
||||
out_asset->data = cursor;
|
||||
out_asset->size = data_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
cursor += data_len;
|
||||
}
|
||||
return cursor == end ? ESP_ERR_NOT_FOUND : ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@@ -14,10 +13,10 @@
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "multimodal/audio_pipeline.h"
|
||||
#include "platform_asset_bundle.h"
|
||||
#include "power_manager.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "voice_prompt.h"
|
||||
@@ -30,12 +29,8 @@
|
||||
#error "CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL must be defined in Kconfig"
|
||||
#endif
|
||||
|
||||
/* 提示音从 gifpack LittleFS 分区读取。 */
|
||||
#define VOICE_PROMPT_DEFAULT_BASE_PATH "/littlefs/audio_prompts"
|
||||
#define VOICE_PROMPT_LITTLEFS_BASE_PATH "/littlefs"
|
||||
#define VOICE_PROMPT_LITTLEFS_PARTITION "gifpack"
|
||||
/*
|
||||
* 提示音文件由4字节小端包长头和对应 raw-Opus payload 重复组成;单包上限2KB
|
||||
* 提示音由4字节大端包长头和对应 raw-Opus payload 重复组成;单包上限2KB
|
||||
* 防止损坏文件触发异常分配。帧时长按60ms估算播放总时长,并额外留3秒用于队列
|
||||
* 排空、codec 尾部播放和调度抖动。
|
||||
*/
|
||||
@@ -47,8 +42,6 @@
|
||||
static const char *TAG = "power_manager";
|
||||
static const char *VOICE_TAG = "voice_prompt";
|
||||
static bool s_power_hold_configured;
|
||||
static bool s_voice_prompt_littlefs_mount_attempted;
|
||||
static esp_err_t s_voice_prompt_littlefs_mount_result = ESP_ERR_INVALID_STATE;
|
||||
|
||||
/* GPIO 掩码集中构造,负数配置代表该板型不使用电源保持引脚。 */
|
||||
static uint64_t power_manager_gpio_bit_mask(int gpio_num)
|
||||
@@ -120,90 +113,25 @@ void platform_power_hold_init(void)
|
||||
power_manager_set_keep_awake(true);
|
||||
}
|
||||
|
||||
esp_err_t platform_mount_littlefs(void)
|
||||
{
|
||||
/* 记录首次挂载结果,使重复调用幂等且不会重复注册同一 VFS 路径。 */
|
||||
if (s_voice_prompt_littlefs_mount_attempted) {
|
||||
return s_voice_prompt_littlefs_mount_result;
|
||||
}
|
||||
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
.base_path = VOICE_PROMPT_LITTLEFS_BASE_PATH,
|
||||
.partition_label = VOICE_PROMPT_LITTLEFS_PARTITION,
|
||||
.partition = NULL,
|
||||
.format_if_mount_failed = false,
|
||||
.dont_mount = false,
|
||||
};
|
||||
|
||||
esp_err_t err = esp_vfs_littlefs_register(&conf);
|
||||
if (err == ESP_ERR_INVALID_STATE &&
|
||||
esp_littlefs_mounted(VOICE_PROMPT_LITTLEFS_PARTITION)) {
|
||||
err = ESP_OK;
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
size_t total_bytes = 0;
|
||||
size_t used_bytes = 0;
|
||||
esp_err_t info_err = esp_littlefs_info(VOICE_PROMPT_LITTLEFS_PARTITION,
|
||||
&total_bytes,
|
||||
&used_bytes);
|
||||
if (info_err == ESP_OK) {
|
||||
ESP_LOGI(VOICE_TAG,
|
||||
"LittleFS mounted at %s, used=%u total=%u",
|
||||
VOICE_PROMPT_LITTLEFS_BASE_PATH,
|
||||
(unsigned)used_bytes,
|
||||
(unsigned)total_bytes);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(VOICE_TAG,
|
||||
"Mount LittleFS failed: %s",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
s_voice_prompt_littlefs_mount_attempted = true;
|
||||
s_voice_prompt_littlefs_mount_result = err;
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t voice_prompt_play_file(const char *path)
|
||||
{
|
||||
/* 文件采用“长度头 + Opus 数据”帧格式,逐帧送入统一的下行播放管线。 */
|
||||
/* 素材位于应用镜像的只读区,逐帧送入统一的下行播放管线。 */
|
||||
if (path == NULL || path[0] == '\0') {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t mount_err = platform_mount_littlefs();
|
||||
if (mount_err != ESP_OK) {
|
||||
return mount_err;
|
||||
}
|
||||
|
||||
char resolved_path[160] = {0};
|
||||
if (path[0] == '/') {
|
||||
int written = snprintf(resolved_path, sizeof(resolved_path), "%s", path);
|
||||
if (written < 0 || written >= (int)sizeof(resolved_path)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
} else {
|
||||
int written = snprintf(resolved_path,
|
||||
sizeof(resolved_path),
|
||||
"%s/%s",
|
||||
VOICE_PROMPT_DEFAULT_BASE_PATH,
|
||||
path);
|
||||
if (written < 0 || written >= (int)sizeof(resolved_path)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
FILE *file = fopen(resolved_path, "rb");
|
||||
if (file == NULL) {
|
||||
ESP_LOGW(VOICE_TAG, "Open prompt failed: %s", resolved_path);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
/* 查找结果是固件内的只读视图,播放期间无需挂载文件系统或管理文件句柄。 */
|
||||
platform_asset_view_t asset = {0};
|
||||
esp_err_t err = platform_asset_find(path, &asset);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(VOICE_TAG, "Find prompt failed: %s (%s)", path, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
uint32_t playback_generation = 0U;
|
||||
esp_err_t err = multimodal_audio_begin_downstream_response(&playback_generation);
|
||||
err = multimodal_audio_begin_downstream_response(&playback_generation);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(VOICE_TAG, "Begin prompt playback failed: %s", esp_err_to_name(err));
|
||||
fclose(file);
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -213,25 +141,23 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
ESP_LOGW(VOICE_TAG,
|
||||
"Allocate prompt opus packet buffer in PSRAM failed: %u bytes",
|
||||
(unsigned)VOICE_PROMPT_MAX_OPUS_PACKET_BYTES);
|
||||
fclose(file);
|
||||
(void)multimodal_audio_finish_downstream_response();
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
uint32_t packet_count = 0;
|
||||
size_t total_bytes = 0;
|
||||
size_t offset = 0;
|
||||
|
||||
while (true) {
|
||||
uint8_t header[VOICE_PROMPT_FRAME_HEADER_BYTES] = {0};
|
||||
size_t header_read = fread(header, 1, sizeof(header), file);
|
||||
if (header_read == 0U) {
|
||||
break;
|
||||
}
|
||||
if (header_read != sizeof(header)) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt frame header: %s", resolved_path);
|
||||
/* 原 Opus 文件仍按 4 字节大端帧长逐帧存放;先校验边界,再拷入播放缓冲区。 */
|
||||
while (offset < asset.size) {
|
||||
if (asset.size - offset < VOICE_PROMPT_FRAME_HEADER_BYTES) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt frame header: %s", path);
|
||||
err = ESP_ERR_INVALID_SIZE;
|
||||
break;
|
||||
}
|
||||
const uint8_t *header = asset.data + offset;
|
||||
offset += VOICE_PROMPT_FRAME_HEADER_BYTES;
|
||||
|
||||
uint32_t packet_len = ((uint32_t)header[0] << 24) |
|
||||
((uint32_t)header[1] << 16) |
|
||||
@@ -241,17 +167,18 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
ESP_LOGW(VOICE_TAG,
|
||||
"Invalid prompt opus packet size: %u in %s",
|
||||
(unsigned)packet_len,
|
||||
resolved_path);
|
||||
path);
|
||||
err = ESP_ERR_INVALID_SIZE;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t payload_read = fread(packet, 1, packet_len, file);
|
||||
if (payload_read != packet_len) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt opus packet: %s", resolved_path);
|
||||
if (packet_len > asset.size - offset) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt opus packet: %s", path);
|
||||
err = ESP_ERR_INVALID_SIZE;
|
||||
break;
|
||||
}
|
||||
memcpy(packet, asset.data + offset, packet_len);
|
||||
offset += packet_len;
|
||||
|
||||
err = multimodal_audio_handle_downstream_opus(packet, packet_len);
|
||||
if (err != ESP_OK) {
|
||||
@@ -265,7 +192,6 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
}
|
||||
|
||||
free(packet);
|
||||
fclose(file);
|
||||
esp_err_t finish_err = multimodal_audio_finish_downstream_response();
|
||||
if (err == ESP_OK) {
|
||||
err = finish_err;
|
||||
@@ -286,7 +212,7 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(VOICE_TAG,
|
||||
"Prompt completed: %s packets=%u bytes=%u",
|
||||
resolved_path,
|
||||
path,
|
||||
(unsigned)packet_count,
|
||||
(unsigned)total_bytes);
|
||||
}
|
||||
|
||||
+1
-12
@@ -111,16 +111,6 @@ dependencies:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.5.4
|
||||
joltwallet/littlefs:
|
||||
component_hash: dcea25bcef2de023f089f5f01e8d8c46ad1b8ffef75861ad5ffb4098555839df
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.20.4
|
||||
direct_dependencies:
|
||||
- 78/esp-wifi-connect
|
||||
- espressif/esp-sr
|
||||
@@ -129,7 +119,6 @@ direct_dependencies:
|
||||
- espressif/esp_websocket_client
|
||||
- espressif/mdns
|
||||
- idf
|
||||
- joltwallet/littlefs
|
||||
manifest_hash: c05cc39447d3758499f6a9a2b947c3fe21cf4fd47e0e9905cde8d79dc68d0472
|
||||
manifest_hash: ba6b786f567d22dab6c465344a66b6b05775703588552240fe540a6e7e29bc2c
|
||||
target: esp32s3
|
||||
version: 2.0.0
|
||||
|
||||
@@ -52,7 +52,7 @@ flowchart LR
|
||||
LCD["ST7789 direct LCD backend"]
|
||||
Audio["ES8311 audio pipeline"]
|
||||
Network["Wi-Fi, BLE provisioning, HTTP, WebSocket"]
|
||||
Storage["NVS, LittleFS"]
|
||||
Storage["NVS, embedded assets, model partition"]
|
||||
GPIO["GPIO, stepper, console UART"]
|
||||
end
|
||||
|
||||
@@ -187,7 +187,7 @@ flowchart LR
|
||||
PlatformReq["platform_lcd_request_presentation()"]
|
||||
Pending["lcd_direct pending request"]
|
||||
Render["cp_lcd_render loop"]
|
||||
Draw["decode PNG asset frame\nfrom /littlefs/lcd"]
|
||||
Draw["decode PNG asset frame\nfrom embedded app image"]
|
||||
Panel["ST7789 panel"]
|
||||
|
||||
CP --> DomainReq --> Resolve --> Catalog --> PlatformReq --> Pending --> Render --> Draw --> Panel
|
||||
|
||||
@@ -273,7 +273,7 @@ key: last_shutdown
|
||||
紧急确认次数:2次
|
||||
确认期间采样间隔:1秒
|
||||
实体按键唤醒 GPIO:-1(关闭)
|
||||
关机提示音:/littlefs/audio_prompts/shutdown_power_empty.opus
|
||||
关机提示音:audio_prompts/shutdown_power_empty.opus
|
||||
充电唤醒 GPIO:GPIO10,低电平有效
|
||||
```
|
||||
|
||||
@@ -326,7 +326,7 @@ key: last_shutdown
|
||||
|
||||
因为要求“播完才能关机”,提示文件缺失、损坏、Codec 初始化失败或播放通路持续故障时,软件会每秒重试而不会休眠。这符合当前产品决策,但可能导致电池最终依靠保护板断电。
|
||||
|
||||
量产前必须验证 LittleFS 中始终包含 `shutdown_power_empty.opus`,并验证关机场景下音频通路可初始化。
|
||||
量产前必须验证应用镜像中包含 `audio_prompts/shutdown_power_empty.opus`,并验证关机场景下音频通路可初始化。
|
||||
|
||||
### 5.4 GPIO10 只能在实际充电时可靠唤醒
|
||||
|
||||
@@ -336,7 +336,7 @@ TP4057 状态脚低电平表示正在充电,高电平可能表示充满或未
|
||||
|
||||
### 5.5 Deep Sleep 唤醒是新的软件生命周期
|
||||
|
||||
唤醒后会重新执行 Bootloader 和 `app_main()`,运行内存、任务状态和普通全局变量不会保留。需要跨休眠保留的数据必须写入 NVS、LittleFS 或 RTC 内存。
|
||||
唤醒后会重新执行 Bootloader 和 `app_main()`,运行内存、任务状态和普通全局变量不会保留。需要跨休眠保留的数据必须写入 NVS 或 RTC 内存;应用内嵌素材是只读的,不能保存运行时数据。
|
||||
|
||||
### 5.6 NVS 写入带来少量 Flash 磨损
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# 应用内嵌素材与 Flash 布局
|
||||
|
||||
LCD PNG 和 Opus 提示音随应用固件一起发布。`ota_0`、`ota_1` 各为 4.5 MiB;每个槽位包含代码和同版本素材。语音识别模型仍单独使用 `model` 分区,NVS、OTA 数据和 coredump 维持各自用途。分区的实际偏移以 `16m.csv` 和每次构建生成的空间报告为准。
|
||||
|
||||
## 构建流程
|
||||
|
||||
1. `tools/optimize_lcd_assets.py check-current` 核对已提交的优化 PNG 与原图、manifest 一致。
|
||||
2. `components/platform/CMakeLists.txt` 将明确列出的 PNG 和 `assets/audio_prompts/` 中的 Opus 复制到构建暂存目录。
|
||||
3. `tools/pack_embedded_assets.py` 按名称排序,生成 `platform_assets.pack`。包内每项包含名称、长度及原始文件字节;程序通过名称查找,不需要 LittleFS 挂载。未列入 PNG 清单的原图不会进入固件。
|
||||
4. 构建系统将整个包嵌入 `TQ_Cat_Code.bin`。OTA 对应用镜像执行原有长度、SHA-256 和镜像校验,素材随固件一起切换或回滚。
|
||||
5. 每次 `idf.py build` 结束后,`tools/report_flash_usage.py` 输出完整分区地址、应用槽位占用、素材包大小、模型镜像占用和 Flash 未分配尾部;超出分区容量时构建失败。
|
||||
|
||||
`idf.py flash` 首次烧录 bootloader、分区表、OTA 数据、应用和模型。不再生成或烧录 `gifpack.bin`。旧分区布局的开发板需先整片擦除,再烧录新布局;只执行应用 OTA 不会改写分区表或模型分区。
|
||||
|
||||
## 运行时读取
|
||||
|
||||
`platform` 内部的 `platform_asset_find()` 返回只读地址和长度。调用方必须使用完整包内名称:LCD 使用 `lcd/<名称>.png`,提示音使用 `audio_prompts/<名称>.opus`。PNG 解码器直接读取应用镜像内的字节,不复制整张 PNG 到 PSRAM;解码后的 RGB565 缓存仍保留。Opus 按原有四字节大端长度头逐帧送入音频管线,并逐帧检查边界。
|
||||
|
||||
素材只随应用固件发布。仅修改某张图或提示音,也需要发布新的应用 OTA 镜像。`model` 分区仍通过 ESP-SR 独立加载,不属于应用 OTA。
|
||||
|
||||
## 验证边界
|
||||
|
||||
本地构建和素材包校验可证明文件被打包且镜像装入分区;LCD 显示、提示音播放、双槽 OTA、回滚及模型加载仍须在开发板上验证。
|
||||
@@ -1,10 +1,10 @@
|
||||
# Flash 与内存容量分析、风险及后续开发建议
|
||||
|
||||
> 历史容量快照:文中远程日志与 `logfs` 已从当前固件移除,分区地址和容量应以现行 `16m.csv` 与最新构建产物为准。
|
||||
> 历史容量快照:文中远程日志、`logfs` 和独立 `gifpack` 分区已从当前固件移除。下文的“当前”“建议”和旧构建步骤仅针对当时版本,不能作为现行操作指南。现行分区以 `16m.csv` 与最新构建报告为准,资源操作见 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。
|
||||
|
||||
本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。
|
||||
|
||||
LittleFS 资源从源码到设备的详细流程见:
|
||||
历史 LittleFS 资源流程归档见:
|
||||
|
||||
- [`docs/littlefs-resource-build-flash-mount.md`](littlefs-resource-build-flash-mount.md)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
项目当前直接通过 ESP-IDF `esp_lcd` 驱动 ST7789P3,LCD 渲染链路主要支持:
|
||||
|
||||
- 从 LittleFS 读取 PNG;
|
||||
- 从应用镜像内嵌素材包读取 PNG;
|
||||
- 将 PNG 解码为 RGB565;
|
||||
- 按 8 行一组复制到 LCD DMA 行缓冲;
|
||||
- 合成充电、低电量等状态图标;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# LittleFS 资源构建、烧录与挂载流程
|
||||
|
||||
> 历史流程:当前固件已将 LCD 图片和提示音嵌入 OTA 应用镜像。下文的“当前”、命令和故障排查仅针对旧版 `gifpack` LittleFS,不适用于现行工程;请使用 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。
|
||||
|
||||
本文说明项目中的 LCD 图片和提示音资源如何从源码目录进入设备 Flash,以及设备运行时如何通过 LittleFS 访问这些资源。同时给出新增、替换和排查资源问题的完整操作步骤。
|
||||
|
||||
## 1. 先建立整体认识
|
||||
|
||||
@@ -46,7 +46,7 @@ if (!s_server_success_prompt_played) {
|
||||
|
||||
if (should_play_prompt) {
|
||||
(void)domain_runtime_behavior_play_prompt_file(
|
||||
"/littlefs/audio_prompts/server_connect_success.opus");
|
||||
"audio_prompts/server_connect_success.opus");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- Wi-Fi 连接、重连和配置热点;
|
||||
- 设备身份、JWT 和会话令牌;
|
||||
- 用户出生资料、每日运势和财富方向;
|
||||
- NVS、LittleFS 和工厂 CLI。
|
||||
- NVS、应用内嵌素材、模型分区和工厂 CLI。
|
||||
|
||||
## 2. 分层架构
|
||||
|
||||
|
||||
@@ -238,12 +238,14 @@ TQ_Cat_Code-1.0.3.bin
|
||||
|
||||
### 6.4 该文件只是应用镜像,不是完整烧录包
|
||||
|
||||
`TQ_Cat_Code-1.0.2.bin` 只对应应用程序分区。它不包含以下独立镜像或资源:
|
||||
`TQ_Cat_Code-1.0.2.bin` 只对应应用程序分区。它不包含以下独立镜像:
|
||||
|
||||
- Bootloader;
|
||||
- 分区表;
|
||||
- OTA data 初始化镜像;
|
||||
- LittleFS/LCD/音频资源镜像。
|
||||
- 独立模型分区镜像。
|
||||
|
||||
当前 LCD 图片和提示音已包含在应用镜像内,会随应用 OTA 一起更新。
|
||||
|
||||
该文件适合应用分区 OTA,不能被误认为包含全部分区的整机合并烧录镜像。工厂烧录仍需按照 ESP-IDF 生成的烧录参数写入各个分区。
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ Transport disconnect/error events are not server error expressions.
|
||||
## Asset Migration Direction
|
||||
|
||||
The image playback path is a platform-owned direct LCD asset pipeline. Runtime
|
||||
PNG files are stored in the `gifpack` LittleFS partition, decoded through
|
||||
PNG files are embedded in each OTA application image, decoded through
|
||||
`lodepng`, converted to RGB565 in PSRAM, and written to the ST7789 panel through
|
||||
the existing bounded row-transfer buffer.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
固件使用的 LCD 为 RGB565 显示格式,而设计工具导出的 PNG 通常是 RGB888 或 RGBA8888。原始 PNG 中包含 LCD 无法显示的颜色精度,部分图片还携带未实际使用的 Alpha 通道。
|
||||
|
||||
本项目在不改变 LCD 最终 RGB565 像素的前提下,将固件图片转换成 RGB565 颜色精度,并在颜色数量不超过 256 时保存为索引色 PNG,以降低 `gifpack` LittleFS 分区的占用。
|
||||
本项目在不改变 LCD 最终 RGB565 像素的前提下,将固件图片转换成 RGB565 颜色精度,并在颜色数量不超过 256 时保存为索引色 PNG,以降低应用镜像中的素材占用。
|
||||
|
||||
正常固件编译直接使用已经提交到 Git 的优化图片,不要求开发环境安装 Pillow。只有修改或验证图片资源时才需要 Pillow。
|
||||
|
||||
@@ -16,9 +16,9 @@ components/platform/assets/
|
||||
└── lcd_optimized/ 优化图片和 manifest.json,固件实际使用
|
||||
```
|
||||
|
||||
两个目录中的 PNG 文件名必须一致。运行时路径仍为 `/lcd/<文件名>.png`,业务代码不需要区分原图和优化图。
|
||||
两个目录中的 PNG 文件名必须一致。运行时逻辑名称为 `lcd/<文件名>.png`,业务代码不需要区分原图和优化图。
|
||||
|
||||
`lcd_source` 中的 `OTA.gif` 是历史资源,不属于当前 PNG 优化和 `gifpack` 打包清单。
|
||||
`lcd_source` 中的 `OTA.gif` 是历史资源,不属于当前 PNG 优化和嵌入素材清单。
|
||||
|
||||
## 优化规则
|
||||
|
||||
@@ -81,7 +81,7 @@ components/platform/assets/lcd_optimized/manifest.json
|
||||
- 优化图片没有缺失或被手动修改;
|
||||
- 原图、优化图和清单中的文件集合一致。
|
||||
|
||||
资源过期时,构建会停止并提示重新运行 `generate`。资源有效时,构建系统把 `lcd_optimized` 中的 PNG 复制到构建目录并生成 `gifpack` 分区镜像。
|
||||
资源过期时,构建会停止并提示重新运行 `generate`。资源有效时,构建系统把 `lcd_optimized` 中的 PNG 复制到构建目录,生成只读素材包并链接到应用镜像。
|
||||
|
||||
## 命令说明
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# LCD PNG 存储优化实施报告
|
||||
|
||||
> 历史优化报告:本文记录当时的 `gifpack` LittleFS 布局及测试数据,不代表当前固件。现行素材流程见 [应用内嵌素材使用与添加指南](应用内嵌素材使用与添加指南.md)。
|
||||
|
||||
## 1. 基本信息
|
||||
|
||||
- 对应提交:`549f495 perf(assets): optimize LCD PNG storage`
|
||||
|
||||
@@ -74,7 +74,7 @@ CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS=300000
|
||||
CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10
|
||||
CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="standby_5min.opus"
|
||||
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="audio_prompts/standby_5min.opus"
|
||||
CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y
|
||||
@@ -606,7 +606,7 @@ esp_err_t control_plane_light_standby_record_activity(
|
||||
|
||||
- ESP-IDF 5.5.4 完整构建通过。
|
||||
- 当前构建生成的应用固件约 2.08 MiB,应用分区剩余约 41%。
|
||||
- `standby_5min.opus` 已纳入 LittleFS 音频资源。
|
||||
- `audio_prompts/standby_5min.opus` 已纳入应用镜像中的音频素材包。
|
||||
- 源码差异格式检查通过。
|
||||
- 已确认提示音等待的是播放管线终止事件,而不是固定时长硬等待。
|
||||
|
||||
|
||||
@@ -321,13 +321,13 @@ Wi-Fi 断开表情在离线期间每 5 秒刷新一次,但被动断网提示
|
||||
|
||||
```c
|
||||
if (!control_plane_is_sta_connected()) {
|
||||
play_prompt("network_unstable.opus");
|
||||
play_prompt("audio_prompts/network_unstable.opus");
|
||||
request_immediate_wifi_reconnect();
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (server_outage_confirmed) {
|
||||
play_prompt("server_repair_wait_reopen.opus");
|
||||
play_prompt("audio_prompts/server_repair_wait_reopen.opus");
|
||||
schedule_fresh_auth_websocket_reconnect();
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 网络异常语音反馈与业务快速失败改造说明
|
||||
|
||||
> 本文中的提交、构建容量和测试结果是历史记录;资源路径已更新为现行应用内嵌素材名称。现行添加流程见 [应用内嵌素材使用与添加指南](应用内嵌素材使用与添加指南.md)。
|
||||
|
||||
## 1. 提交概况
|
||||
|
||||
```text
|
||||
@@ -95,19 +97,19 @@ static portMUX_TYPE s_feedback_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
### 3.2 复用现有音频资源
|
||||
|
||||
本次没有新增录音文件,直接复用 LittleFS 中已有的五个 Opus 资源:
|
||||
本次没有新增录音文件,直接复用应用镜像中已有的五个 Opus 资源:
|
||||
|
||||
```c
|
||||
#define CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/network_interrupted_repairing.opus"
|
||||
"audio_prompts/network_interrupted_repairing.opus"
|
||||
#define CONTROL_PLANE_WIFI_ACTION_BLOCKED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/network_unstable.opus"
|
||||
"audio_prompts/network_unstable.opus"
|
||||
#define CONTROL_PLANE_SERVER_INTERRUPTED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/server_abnormal_interrupted.opus"
|
||||
"audio_prompts/server_abnormal_interrupted.opus"
|
||||
#define CONTROL_PLANE_SERVER_ACTION_BLOCKED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/server_repair_wait_reopen.opus"
|
||||
"audio_prompts/server_repair_wait_reopen.opus"
|
||||
#define CONTROL_PLANE_NETWORK_RESTORED_PROMPT_FILE \
|
||||
"/littlefs/audio_prompts/signal_restored_business_resume.opus"
|
||||
"audio_prompts/signal_restored_business_resume.opus"
|
||||
```
|
||||
|
||||
对应关系如下:
|
||||
@@ -486,7 +488,7 @@ Free space: 0x1682b0 bytes(40%)
|
||||
|
||||
- 新增网络反馈模块编译通过;
|
||||
- 固件链接通过;
|
||||
- 五个提示音均进入 LittleFS 镜像;
|
||||
- 五个提示音均进入应用镜像中的素材包;
|
||||
- 应用分区容量检查通过;
|
||||
- Markdown 格式和空白错误检查通过。
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ return domain_runtime_request_expression(&request);
|
||||
- 财位、校准、抽签等已有模型指令的枚举值未改变。
|
||||
- 表达式请求结构增加字段,但现有请求均使用指定字段初始化,新增字段默认置零。
|
||||
- 原五行调用的 `wuxing_element_valid` 默认为 `false`,继续随机显示。
|
||||
- 未新增 LCD 图片,不增加 LittleFS 资源占用。
|
||||
- 未新增 LCD 图片,不增加应用镜像中的素材占用。
|
||||
- 未新增任务、队列或锁,不改变任务生命周期和并发模型。
|
||||
|
||||
### 5.2 当前限制
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# 应用内嵌素材使用与添加指南
|
||||
|
||||
## 当前机制
|
||||
|
||||
LCD PNG 和提示音 Opus 在构建时生成 `platform_assets.pack`,作为二进制数据链接进应用固件。`ota_0`、`ota_1` 都装入完整的代码和素材,因此应用 OTA 会同步更新两者。运行时按**包内完整相对名称**查找,例如 `lcd/ota1.png`、`audio_prompts/boot.opus`;这些名称不是 VFS 文件路径,不能用 `fopen()` 读取,也不需要挂载 LittleFS。模型仍在独立的 `model` 分区,不随应用 OTA 更新。
|
||||
|
||||
源码、打包器和运行时入口:
|
||||
|
||||
| 用途 | 位置 |
|
||||
| --- | --- |
|
||||
| 原始 LCD PNG | `components/platform/assets/lcd_source/` |
|
||||
| 优化后实际入包的 PNG | `components/platform/assets/lcd_optimized/` |
|
||||
| 实际入包的 Opus | `components/platform/assets/audio_prompts/` |
|
||||
| PNG 入包清单与构建暂存 | `components/platform/CMakeLists.txt` |
|
||||
| 素材包格式 | `tools/pack_embedded_assets.py` |
|
||||
| 只读查找实现 | `components/platform/src/platform_asset_bundle.c` |
|
||||
| Flash 空间报告 | `tools/report_flash_usage.py` |
|
||||
|
||||
素材查找器属于 `platform` 内部接口。业务代码不要读取包内字节或拼接 Flash 地址,应调用下述 `domain`/`control_plane` 接口。
|
||||
|
||||
## 添加或替换 LCD 图片
|
||||
|
||||
1. 将原始 PNG 放到 `components/platform/assets/lcd_source/<名称>.png`。替换图片时只修改原图,不直接编辑 `lcd_optimized/`。
|
||||
2. 按 [LCD 图片资源优化与维护](lcd-image-asset-optimization.zh-CN.md) 准备 Pillow 环境,在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
python tools/optimize_lcd_assets.py generate
|
||||
python tools/optimize_lcd_assets.py verify
|
||||
```
|
||||
|
||||
3. 在 `components/platform/CMakeLists.txt` 的 `platform_lcd_png_assets` 中加入 `<名称>`,不带 `.png`。未列入清单的 PNG 不会打包。
|
||||
4. 在 `components/platform/src/lcd_direct_assets.c` 登记图片及其帧索引,并在 `lcd_direct_asset_frame()` 中登记对应的资源组名。**仅入包不会让 LCD 自动播放它。**
|
||||
5. 如需新增业务表情,在 `components/domain/include/domain.h` 增加 `domain_expression_id_t`,并在 `components/domain/src/runtime/domain_runtime_adapters.c` 的目录映射中配置帧顺序、时长、重复次数、优先级和完成后行为。现有表情只换图时,通常不需要新增表情 ID。
|
||||
|
||||
业务触发点放在 `control_plane`,通过 `domain` 的语义接口请求,示例:
|
||||
|
||||
```c
|
||||
domain_expression_request_t request = {
|
||||
.id = DOMAIN_EXPRESSION_OTA_UPDATE,
|
||||
.trigger = DOMAIN_EXPRESSION_TRIGGER_OTA_UPDATE,
|
||||
};
|
||||
esp_err_t err = domain_runtime_request_expression(&request);
|
||||
```
|
||||
|
||||
`app_composition` 只负责启动接线;若启动阶段需要触发表情,应调用 `control_plane` 的公开入口。不要从上层直接调用 `platform_lcd_*` 或 `platform_asset_find()`。例如现有开机流程调用 `control_plane_request_boot_expression()`。
|
||||
|
||||
## 添加或替换提示音
|
||||
|
||||
1. 将文件放到 `components/platform/assets/audio_prompts/<名称>.opus`。该目录中的 `.opus` 会自动入包,无须修改 PNG 清单。当前打包格式只接受 `audio_prompts/` 下的单层文件,勿另建子目录。
|
||||
2. 文件必须符合项目现有提示音格式:每个 Opus 包前有 **4 字节大端包长**,单包长度为 1–2048 字节。普通 Ogg Opus 文件不能仅改名为 `.opus` 使用。
|
||||
3. 在负责该业务时机的 `control_plane` 模块中使用完整名称:
|
||||
|
||||
```c
|
||||
esp_err_t err = domain_runtime_behavior_play_prompt_file(
|
||||
"audio_prompts/new_prompt.opus");
|
||||
```
|
||||
|
||||
4. 如果提示音路径由 Kconfig 配置,配置值也必须是完整包内名称,例如 `audio_prompts/boot.opus`;禁用可选提示音时按对应配置的空值规则处理。旧的 `/littlefs/...` 路径和裸文件名不再被解析。
|
||||
|
||||
播放接口同步完成分帧、下行播放会话和等待,调用位置应符合该业务的阻塞预算。需要新的语义触发或重试策略时,在对应的 `control_plane` 模块实现,不在 `app_composition` 中加入业务策略。
|
||||
|
||||
## 构建、更新与验收
|
||||
|
||||
使用项目指定的 ESP-IDF v5.5.4 环境执行 `idf.py build`。构建会先检查原图、优化图和 manifest 的一致性,再打包所选 PNG 与全部提示音。输出包括 `build/esp-idf/platform/platform_assets.pack` 和 `build/TQ_Cat_Code.bin`,并打印应用槽位、素材包、模型及 Flash 尾部空间。**素材包大小已包含在应用镜像大小中,不要重复相加。**
|
||||
|
||||
检查新增素材时,先确认构建通过、空间报告未超出 OTA 槽位,再在设备上验证实际画面或声音。更新素材需要重新发布应用 OTA;对于当前分区布局,首次烧录需同时烧录新的分区表、应用与模型。仅改素材也要生成新的应用镜像。
|
||||
|
||||
若运行时报告素材未找到,按顺序检查:调用名称是否含 `lcd/` 或 `audio_prompts/` 前缀;PNG 是否在 CMake 清单中;PNG 是否已登记到平台帧表和领域表情映射;提示音是否为项目分帧格式;设备运行的是否为本次构建的应用版本。构建目录的暂存文件不是设备上的文件系统。
|
||||
@@ -227,7 +227,7 @@ menu "TQ Cat Configuration"
|
||||
string "Boot sound file"
|
||||
default ""
|
||||
help
|
||||
Optional boot sound file path for the voice prompt backend.
|
||||
Optional embedded asset name, such as audio_prompts/boot.opus.
|
||||
Leave empty to use the semantic connecting prompt fallback.
|
||||
|
||||
config TQ_NETWORK_TASK_CORE
|
||||
@@ -410,10 +410,10 @@ menu "TQ Cat Configuration"
|
||||
|
||||
config TQ_LIGHT_STANDBY_PROMPT_FILE
|
||||
string "Light-standby entry prompt file"
|
||||
default "standby_5min.opus"
|
||||
default "audio_prompts/standby_5min.opus"
|
||||
depends on TQ_LIGHT_STANDBY_PROMPT_ENABLE
|
||||
help
|
||||
Prompt file in the LittleFS audio_prompts directory.
|
||||
Full embedded asset name, such as audio_prompts/standby_5min.opus.
|
||||
|
||||
config TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT
|
||||
bool "Disable speaker output in light standby"
|
||||
@@ -1022,7 +1022,7 @@ menu "TQ Cat Configuration"
|
||||
|
||||
config TQ_BATTERY_SHUTDOWN_PROMPT_FILE
|
||||
string "Critical-battery shutdown prompt file"
|
||||
default "/littlefs/audio_prompts/shutdown_power_empty.opus"
|
||||
default "audio_prompts/shutdown_power_empty.opus"
|
||||
depends on TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
|
||||
endmenu
|
||||
@@ -1380,10 +1380,9 @@ menu "TQ Cat Configuration"
|
||||
config TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE
|
||||
string "Local wake response prompt file"
|
||||
depends on TQ_LOCAL_WAKE_ENABLE
|
||||
default "local_wake_response.opus"
|
||||
default "audio_prompts/local_wake_response.opus"
|
||||
help
|
||||
Prompt filename relative to /littlefs/audio_prompts. Absolute
|
||||
LittleFS paths are also supported.
|
||||
Full embedded asset name, such as audio_prompts/local_wake_response.opus.
|
||||
|
||||
config TQ_LOCAL_WAKE_RETRY_MS
|
||||
int "Local wake startup retry delay (ms)"
|
||||
|
||||
+4
-4
@@ -37,7 +37,7 @@ CONFIG_FREERTOS_USE_TRACE_FACILITY=y
|
||||
# CONFIG_TQ_TASK_MONITOR_ENABLE is not set
|
||||
# CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN is not set
|
||||
CONFIG_TQ_BOOT_SOUND_TIMEOUT_MS=3000
|
||||
CONFIG_TQ_BOOT_SOUND_FILE="boot.opus"
|
||||
CONFIG_TQ_BOOT_SOUND_FILE="audio_prompts/boot.opus"
|
||||
CONFIG_TQ_NETWORK_TASK_CORE=0
|
||||
CONFIG_TQ_LOCAL_TASK_CORE=1
|
||||
CONFIG_TQ_LCD_RENDER_TASK_STACK_BYTES=8192
|
||||
@@ -59,7 +59,7 @@ CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS=300000
|
||||
CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10
|
||||
CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="standby_5min.opus"
|
||||
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="audio_prompts/standby_5min.opus"
|
||||
CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y
|
||||
CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y
|
||||
@@ -106,7 +106,7 @@ CONFIG_TQ_OTA_MIN_LARGEST_DMA_BYTES=4096
|
||||
CONFIG_TQ_OTA_MIN_BATTERY_PERCENT=20
|
||||
CONFIG_TQ_OTA_POWER_RETRY_MS=60000
|
||||
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE=y
|
||||
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE="local_wake_response.opus"
|
||||
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE="audio_prompts/local_wake_response.opus"
|
||||
CONFIG_TQ_LOCAL_WAKE_RETRY_MS=5000
|
||||
CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS=10000
|
||||
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS=1000
|
||||
@@ -338,5 +338,5 @@ CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV=3450
|
||||
CONFIG_TQ_BATTERY_EMERGENCY_VOLTAGE_MV=3350
|
||||
CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV=3550
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO=-1
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE="/littlefs/audio_prompts/shutdown_power_empty.opus"
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE="audio_prompts/shutdown_power_empty.opus"
|
||||
CONFIG_MULTIMODAL_AUDIO_PA_GPIO=11
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""将素材确定性地打包为只读资源,随应用镜像一起参与 OTA。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MAGIC = b"TQASSET1"
|
||||
# 与 platform_asset_bundle.c 的解析格式一致:小端包头及条目头,后接名称和原始字节。
|
||||
HEADER = struct.Struct("<8sI")
|
||||
ENTRY = struct.Struct("<HI")
|
||||
ALLOWED = {"lcd": ".png", "audio_prompts": ".opus"}
|
||||
|
||||
|
||||
def pack(source: Path) -> bytes:
|
||||
# 排序固定条目顺序,保证相同输入生成相同的包;只接受当前运行时使用的两类素材。
|
||||
files = sorted(path for path in source.rglob("*") if path.is_file())
|
||||
if not files:
|
||||
raise ValueError("asset staging directory is empty")
|
||||
|
||||
records: list[tuple[bytes, bytes]] = []
|
||||
names: set[str] = set()
|
||||
for path in files:
|
||||
relative = path.relative_to(source)
|
||||
if len(relative.parts) != 2 or relative.parts[0] not in ALLOWED:
|
||||
raise ValueError(f"unexpected asset path: {relative}")
|
||||
if path.suffix.lower() != ALLOWED[relative.parts[0]]:
|
||||
raise ValueError(f"unexpected asset type: {relative}")
|
||||
name = relative.as_posix()
|
||||
if name in names:
|
||||
raise ValueError(f"duplicate asset: {name}")
|
||||
names.add(name)
|
||||
encoded = name.encode("utf-8")
|
||||
payload = path.read_bytes()
|
||||
if not encoded or len(encoded) > 0xFFFF or not payload or len(payload) > 0xFFFFFFFF:
|
||||
raise ValueError(f"invalid asset size: {name}")
|
||||
records.append((encoded, payload))
|
||||
|
||||
output = bytearray(HEADER.pack(MAGIC, len(records)))
|
||||
for name, payload in records:
|
||||
output.extend(ENTRY.pack(len(name), len(payload)))
|
||||
output.extend(name)
|
||||
output.extend(payload)
|
||||
print(f"Embedded assets: {len(records)} files, {sum(len(data) for _, data in records)} payload bytes, {len(output)} bundle bytes")
|
||||
return bytes(output)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("source", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
args = parser.parse_args()
|
||||
data = pack(args.source)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not args.output.exists() or args.output.read_bytes() != data:
|
||||
args.output.write_bytes(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""每次构建后报告分区和镜像空间占用。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ENTRY = struct.Struct("<2sBBII16sI")
|
||||
|
||||
|
||||
def partitions(path: Path) -> list[tuple[str, int, int]]:
|
||||
# 读取本次构建生成的分区表二进制条目;报告不代表设备当前烧录状态。
|
||||
data = path.read_bytes()
|
||||
result = []
|
||||
for offset in range(0, len(data) - ENTRY.size + 1, ENTRY.size):
|
||||
magic, _type, _subtype, start, size, raw_name, _flags = ENTRY.unpack_from(data, offset)
|
||||
if magic != b"\xaa\x50":
|
||||
break
|
||||
result.append((raw_name.split(b"\0", 1)[0].decode("ascii"), start, size))
|
||||
if not result:
|
||||
raise ValueError(f"no partitions found in {path}")
|
||||
return result
|
||||
|
||||
|
||||
def mib(size: int) -> str:
|
||||
return f"{size / 1048576:.2f} MiB"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--partition-table", required=True, type=Path)
|
||||
parser.add_argument("--app", required=True, type=Path)
|
||||
parser.add_argument("--asset-pack", required=True, type=Path)
|
||||
parser.add_argument("--model", required=True, type=Path)
|
||||
parser.add_argument("--flash-size", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
flash_size = int(args.flash_size.upper().removesuffix("MB")) * 1048576
|
||||
entries = partitions(args.partition_table)
|
||||
by_name = {name: (start, size) for name, start, size in entries}
|
||||
# 素材包已计入应用镜像,单独显示仅用于分析,不能再加到应用占用中。
|
||||
app_size = args.app.stat().st_size
|
||||
asset_size = args.asset_pack.stat().st_size
|
||||
model_size = args.model.stat().st_size
|
||||
for name in ("ota_0", "ota_1", "model"):
|
||||
if name not in by_name:
|
||||
raise ValueError(f"missing required partition: {name}")
|
||||
if app_size > min(by_name[name][1] for name in ("ota_0", "ota_1")):
|
||||
raise ValueError("application image exceeds an OTA slot")
|
||||
if model_size > by_name["model"][1]:
|
||||
raise ValueError("model image exceeds model partition")
|
||||
last_end = max(start + size for _, start, size in entries)
|
||||
if last_end > flash_size:
|
||||
raise ValueError("partition table exceeds configured Flash size")
|
||||
|
||||
print("\nFlash usage (local build images; device contents may differ):")
|
||||
print(f" Flash capacity: {mib(flash_size)}")
|
||||
for name, start, size in entries:
|
||||
print(f" {name:9s} 0x{start:06x}-0x{start + size:06x} {mib(size)}")
|
||||
slot_size = min(by_name[name][1] for name in ("ota_0", "ota_1"))
|
||||
print(f" App image: {app_size:,} B / {slot_size:,} B ({app_size / slot_size:.1%}); free {mib(slot_size - app_size)} per OTA slot")
|
||||
print(f" Embedded assets: {asset_size:,} B (included in app image)")
|
||||
print(f" Model image: {model_size:,} B / {by_name['model'][1]:,} B ({model_size / by_name['model'][1]:.1%})")
|
||||
print(f" Unpartitioned tail: {mib(flash_size - last_end)}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user