feat(assets): embed LCD and audio resources in OTA app

This commit is contained in:
chenjintao
2026-10-08 16:58:33 +08:00
parent 3160a33f50
commit 7bc9a3dc76
43 changed files with 443 additions and 243 deletions
+3 -3
View File
@@ -1,10 +1,10 @@
# ESP-IDF Partition Table # ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
# PNG/Opus 素材已链接进应用镜像,两块 OTA 应用分区均需容纳完整固件和素材。
nvs, data, nvs, 0x9000, 0x4000, nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000, otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000, phy_init, data, phy, 0xf000, 0x1000,
gifpack, data, littlefs, , 0x200000, ota_0, app, ota_0, , 0x480000,
ota_0, app, ota_0, , 0x380000, ota_1, app, ota_1, , 0x480000,
ota_1, app, ota_1, , 0x380000,
model, data, spiffs, , 0x300000, model, data, spiffs, , 0x300000,
coredump, data, coredump,, 0x10000, coredump, data, coredump,, 0x10000,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 # PNG/Opus 素材已链接进应用镜像,两块 OTA 应用分区均需容纳完整固件和素材。
4 nvs, data, nvs, 0x9000, 0x4000,
5 otadata, data, ota, 0xd000, 0x2000,
6 phy_init, data, phy, 0xf000, 0x1000,
7 gifpack, data, littlefs, , 0x200000, ota_0, app, ota_0, , 0x480000,
8 ota_0, app, ota_0, , 0x380000, ota_1, app, ota_1, , 0x480000,
ota_1, app, ota_1, , 0x380000,
9 model, data, spiffs, , 0x300000,
10 coredump, data, coredump,, 0x10000,
+13
View File
@@ -20,3 +20,16 @@ add_custom_target(versioned_firmware ALL
COMMENT "Creating versioned firmware ${CMAKE_PROJECT_NAME}-${TQ_PROJECT_VERSION}.bin" COMMENT "Creating versioned firmware ${CMAKE_PROJECT_NAME}-${TQ_PROJECT_VERSION}.bin"
VERBATIM 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 #endif
#ifndef CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE #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 #endif
#ifndef CONFIG_TQ_LOCAL_WAKE_RETRY_MS #ifndef CONFIG_TQ_LOCAL_WAKE_RETRY_MS
@@ -55,7 +55,7 @@
* 2 秒则给播放器正常收尾机会,超过后才按停滞恢复,避免网络抖动造成频繁强停。 * 2 秒则给播放器正常收尾机会,超过后才按停滞恢复,避免网络抖动造成频繁强停。
*/ */
#define CONTROL_PLANE_WATCHDOG_TASK_STACK_SIZE CONFIG_TQ_MULTIMODAL_WATCHDOG_TASK_STACK_BYTES #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_CLIENT_LIFECYCLE_WAIT_MS 30000U
#define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_RETRY_MS 1000U #define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_RETRY_MS 1000U
#define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_MAX_ATTEMPTS 3U #define CONTROL_PLANE_SERVER_SUCCESS_PROMPT_MAX_ATTEMPTS 3U
@@ -42,14 +42,14 @@
* 多次确认用于排除负载突变导致的瞬时低压;紧急阈值只需两次以缩短保护延迟。 * 多次确认用于排除负载突变导致的瞬时低压;紧急阈值只需两次以缩短保护延迟。
* 轻待机下采样间隔显著拉长,充电 GPIO 中断仍可及时唤醒状态更新。 * 轻待机下采样间隔显著拉长,充电 GPIO 中断仍可及时唤醒状态更新。
*/ */
#define BATTERY_LOW_PROMPT_FILE "/littlefs/audio_prompts/battery_low_20.opus" #define BATTERY_LOW_PROMPT_FILE "audio_prompts/battery_low_20.opus"
#define BATTERY_FULL_PROMPT_FILE "/littlefs/audio_prompts/charge_complete.opus" #define BATTERY_FULL_PROMPT_FILE "audio_prompts/charge_complete.opus"
#ifndef CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE #ifndef CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
#define CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE 0 #define CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE 0
#endif #endif
#ifndef CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE #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 #endif
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE #if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
@@ -55,7 +55,7 @@
#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES 12288 #define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES 12288
#endif #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_CONNECT_MAX_ATTEMPTS 3
#define CONTROL_PLANE_STA_RETRY_BASE_DELAY_MS 300 #define CONTROL_PLANE_STA_RETRY_BASE_DELAY_MS 300
@@ -52,7 +52,7 @@
#endif #endif
#ifndef CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE #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 #endif
#ifndef CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT #ifndef CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT
@@ -25,15 +25,15 @@
/* 3 秒宽限过滤 WebSocket 短重连,5 秒冷却防止 Wi-Fi 与服务端事件交替造成提示音轰炸。 */ /* 3 秒宽限过滤 WebSocket 短重连,5 秒冷却防止 Wi-Fi 与服务端事件交替造成提示音轰炸。 */
#define CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE \ #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 \ #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 \ #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 \ #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 \ #define CONTROL_PLANE_NETWORK_RESTORED_PROMPT_FILE \
"/littlefs/audio_prompts/signal_restored_business_resume.opus" "audio_prompts/signal_restored_business_resume.opus"
typedef enum { typedef enum {
NETWORK_FEEDBACK_STATE_BOOTING = 0, NETWORK_FEEDBACK_STATE_BOOTING = 0,
@@ -43,7 +43,7 @@
#define CONTROL_PLANE_PTT_HOLD_STEP_MS 80 #define CONTROL_PLANE_PTT_HOLD_STEP_MS 80
/* 长按 PTT 以 80 ms 小步等待,既能及时感知释放,也能周期检查会话已失败或被取消。 */ /* 长按 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 #ifndef CONFIG_TQ_LOCAL_TASK_CORE
#define CONFIG_TQ_LOCAL_TASK_CORE 1 #define CONFIG_TQ_LOCAL_TASK_CORE 1
-1
View File
@@ -336,7 +336,6 @@ esp_err_t domain_runtime_slot_feature_switch_init(void);
bool domain_runtime_slot_feature_switch_is_active(void); bool domain_runtime_slot_feature_switch_is_active(void);
esp_err_t domain_runtime_prepare_storage(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, void domain_runtime_read_device_identity(char *device_id,
size_t device_id_len, size_t device_id_len,
char *serial_number, char *serial_number,
@@ -905,11 +905,6 @@ esp_err_t domain_runtime_prepare_storage(void)
return platform_prepare_storage(); 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, void domain_runtime_read_device_identity(char *device_id,
size_t device_id_len, size_t device_id_len,
char *serial_number, char *serial_number,
+14 -6
View File
@@ -16,6 +16,7 @@ set(platform_srcs
"src/platform_io_effects.c" "src/platform_io_effects.c"
"src/lcd_direct.c" "src/lcd_direct.c"
"src/lcd_direct_assets.c" "src/lcd_direct_assets.c"
"src/platform_asset_bundle.c"
"third_party/lodepng/lodepng.cpp" "third_party/lodepng/lodepng.cpp"
"third_party/lodepng/lodepng_esp_alloc.cpp" "third_party/lodepng/lodepng_esp_alloc.cpp"
"third_party/lodepng/lodepng_esp_bridge.cpp" "third_party/lodepng/lodepng_esp_bridge.cpp"
@@ -54,7 +55,6 @@ set(platform_priv_requires
esp_wifi esp_wifi
esp_websocket_client esp_websocket_client
json json
littlefs
log log
espressif__mdns espressif__mdns
mbedtls 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_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_image_dir "${CMAKE_CURRENT_BINARY_DIR}/asset_image")
set(platform_asset_stage_stamp "${CMAKE_CURRENT_BINARY_DIR}/asset_image.stamp") 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 file(GLOB_RECURSE platform_audio_prompt_files CONFIGURE_DEPENDS
"${platform_audio_prompt_src_dir}/*.opus" "${platform_audio_prompt_src_dir}/*.opus"
) )
@@ -199,6 +201,7 @@ foreach(lcd_png_asset IN LISTS platform_lcd_png_assets)
) )
endforeach() endforeach()
# 先校验并暂存实际引用的 PNG/Opus,再由打包脚本生成应用内只读资源包。
add_custom_command( add_custom_command(
OUTPUT "${platform_asset_stage_stamp}" OUTPUT "${platform_asset_stage_stamp}"
COMMAND ${PYTHON} "${platform_lcd_asset_optimizer}" check-current COMMAND ${PYTHON} "${platform_lcd_asset_optimizer}" check-current
@@ -214,9 +217,14 @@ add_custom_command(
"${platform_lcd_asset_optimizer}" "${platform_lcd_asset_optimizer}"
) )
littlefs_create_partition_image( # 素材或打包脚本变化时重新生成包,并通过二进制链接目标纳入 OTA 应用镜像。
gifpack add_custom_command(
"${platform_asset_image_dir}" OUTPUT "${platform_asset_pack}"
FLASH_IN_PROJECT COMMAND ${PYTHON} "${platform_asset_packer}"
DEPENDS "${platform_asset_stage_stamp}" "${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
View File
@@ -1,6 +1,5 @@
dependencies: dependencies:
idf: '>=4.4' idf: '>=4.4'
joltwallet/littlefs: 1.20.4
espressif/esp_websocket_client: ^1.6.1 espressif/esp_websocket_client: ^1.6.1
espressif/esp_codec_dev: ^1.5.4 espressif/esp_codec_dev: ^1.5.4
espressif/esp_audio_codec: ^2.4.1 espressif/esp_audio_codec: ^2.4.1
-1
View File
@@ -221,7 +221,6 @@ typedef struct {
esp_err_t platform_init(void); esp_err_t platform_init(void);
esp_err_t platform_prepare_storage(void); esp_err_t platform_prepare_storage(void);
bool platform_storage_is_ready(void); bool platform_storage_is_ready(void);
esp_err_t platform_mount_littlefs(void);
void platform_read_device_identity(char *device_id, void platform_read_device_identity(char *device_id,
size_t device_id_len, size_t device_id_len,
char *serial_number, char *serial_number,
@@ -7,7 +7,7 @@
extern "C" { extern "C" {
#endif #endif
/* RGB565 图片描述符,实际像素由渲染任务根据路径加载和解码。 */ /* RGB565 图片描述符,实际像素由渲染任务根据逻辑名称查找和解码。 */
typedef struct { typedef struct {
const char *path; const char *path;
} lcd_direct_image_t; } 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);
+2 -2
View File
@@ -7,8 +7,8 @@ extern "C" {
#endif #endif
/* /*
* 同步播放 LittleFS 中的 Opus 提示音。函数负责分帧、下行播放会话和完成等待, * 同步播放应用镜像中的 Opus 提示音。函数负责分帧、下行播放会话和完成等待;
* 返回时文件句柄已关闭;路径解析和播放超时均在实现内处理。 * 素材查找和播放超时均在实现内处理。
*/ */
esp_err_t voice_prompt_play_file(const char *path); esp_err_t voice_prompt_play_file(const char *path);
+10 -60
View File
@@ -7,15 +7,13 @@
#include "lcd_direct.h" #include "lcd_direct.h"
#include "lcd_direct_assets.h" #include "lcd_direct_assets.h"
#include "platform_asset_bundle.h"
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <unistd.h>
#include "driver/gpio.h" #include "driver/gpio.h"
#include "driver/ledc.h" #include "driver/ledc.h"
@@ -55,7 +53,7 @@
#define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U #define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U
/* /*
* PNG 缓存固定6项并使用 LRU 淘汰,限制 PSRAM 峰值;边界测试恰好覆盖一次完整 * PNG 缓存固定6项并使用 LRU 淘汰,限制 PSRAM 峰值;边界测试恰好覆盖一次完整
* 缓存命中周期。路径上限同时约束缓存键和 LittleFS 资源路径复制。 * 缓存命中周期。名称上限同时约束缓存键和嵌入式素材索引。
*/ */
#define LCD_DIRECT_FRAME_CACHE_MAX 6U #define LCD_DIRECT_FRAME_CACHE_MAX 6U
#define LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES LCD_DIRECT_FRAME_CACHE_MAX #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); 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) static void lcd_direct_release_decoded_image(lcd_direct_decoded_image_t *decoded)
{ {
if (decoded == NULL) { if (decoded == NULL) {
@@ -386,18 +335,19 @@ static esp_err_t lcd_direct_decode_png(const char *path,
"invalid PNG decode request"); "invalid PNG decode request");
memset(out_decoded, 0, sizeof(*out_decoded)); memset(out_decoded, 0, sizeof(*out_decoded));
unsigned char *png_data = NULL; /* PNG 字节直接取自应用镜像的只读映射区;解码器只读取输入,不持有或释放该指针。 */
size_t png_size = 0; platform_asset_view_t asset = {0};
esp_err_t read_err = lcd_direct_read_file_psram(path, &png_data, &png_size); esp_err_t find_err = platform_asset_find(path, &asset);
if (read_err != ESP_OK) { if (find_err != ESP_OK) {
return read_err; 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 char *rgba = NULL;
unsigned width = 0; unsigned width = 0;
unsigned height = 0; unsigned height = 0;
unsigned decode_err = lodepng_esp_decode32(&rgba, &width, &height, png_data, png_size); unsigned decode_err = lodepng_esp_decode32(&rgba, &width, &height,
free(png_data); asset.data, asset.size);
if (decode_err != 0U) { if (decode_err != 0U) {
ESP_LOGE(TAG, "decode PNG failed path=%s error=%u text=%s", ESP_LOGE(TAG, "decode PNG failed path=%s error=%u text=%s",
path, path,
+1 -1
View File
@@ -2,7 +2,7 @@
#include <string.h> #include <string.h>
#define LCD_ASSET_BASE_PATH "/littlefs/lcd/" #define LCD_ASSET_BASE_PATH "lcd/"
/* /*
* 静态表只保存资源元数据和路径,不在此文件执行 PNG 解码。 * 静态表只保存资源元数据和路径,不在此文件执行 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 * SPDX-License-Identifier: Apache-2.0
*/ */
#include <stdio.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
@@ -14,10 +13,10 @@
#include "esp_err.h" #include "esp_err.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_littlefs.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "multimodal/audio_pipeline.h" #include "multimodal/audio_pipeline.h"
#include "platform_asset_bundle.h"
#include "power_manager.h" #include "power_manager.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include "voice_prompt.h" #include "voice_prompt.h"
@@ -30,12 +29,8 @@
#error "CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL must be defined in Kconfig" #error "CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL must be defined in Kconfig"
#endif #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秒用于队列 * 防止损坏文件触发异常分配。帧时长按60ms估算播放总时长,并额外留3秒用于队列
* 排空、codec 尾部播放和调度抖动。 * 排空、codec 尾部播放和调度抖动。
*/ */
@@ -47,8 +42,6 @@
static const char *TAG = "power_manager"; static const char *TAG = "power_manager";
static const char *VOICE_TAG = "voice_prompt"; static const char *VOICE_TAG = "voice_prompt";
static bool s_power_hold_configured; 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 掩码集中构造,负数配置代表该板型不使用电源保持引脚。 */ /* GPIO 掩码集中构造,负数配置代表该板型不使用电源保持引脚。 */
static uint64_t power_manager_gpio_bit_mask(int gpio_num) 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); 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) esp_err_t voice_prompt_play_file(const char *path)
{ {
/* 文件采用“长度头 + Opus 数据”帧格式,逐帧送入统一的下行播放管线。 */ /* 素材位于应用镜像的只读区,逐帧送入统一的下行播放管线。 */
if (path == NULL || path[0] == '\0') { if (path == NULL || path[0] == '\0') {
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
esp_err_t mount_err = platform_mount_littlefs(); /* 查找结果是固件内的只读视图,播放期间无需挂载文件系统或管理文件句柄。 */
if (mount_err != ESP_OK) { platform_asset_view_t asset = {0};
return mount_err; 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));
char resolved_path[160] = {0}; return err;
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;
} }
uint32_t playback_generation = 0U; 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) { if (err != ESP_OK) {
ESP_LOGW(VOICE_TAG, "Begin prompt playback failed: %s", esp_err_to_name(err)); ESP_LOGW(VOICE_TAG, "Begin prompt playback failed: %s", esp_err_to_name(err));
fclose(file);
return err; return err;
} }
@@ -213,25 +141,23 @@ esp_err_t voice_prompt_play_file(const char *path)
ESP_LOGW(VOICE_TAG, ESP_LOGW(VOICE_TAG,
"Allocate prompt opus packet buffer in PSRAM failed: %u bytes", "Allocate prompt opus packet buffer in PSRAM failed: %u bytes",
(unsigned)VOICE_PROMPT_MAX_OPUS_PACKET_BYTES); (unsigned)VOICE_PROMPT_MAX_OPUS_PACKET_BYTES);
fclose(file);
(void)multimodal_audio_finish_downstream_response(); (void)multimodal_audio_finish_downstream_response();
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
uint32_t packet_count = 0; uint32_t packet_count = 0;
size_t total_bytes = 0; size_t total_bytes = 0;
size_t offset = 0;
while (true) { /* 原 Opus 文件仍按 4 字节大端帧长逐帧存放;先校验边界,再拷入播放缓冲区。 */
uint8_t header[VOICE_PROMPT_FRAME_HEADER_BYTES] = {0}; while (offset < asset.size) {
size_t header_read = fread(header, 1, sizeof(header), file); if (asset.size - offset < VOICE_PROMPT_FRAME_HEADER_BYTES) {
if (header_read == 0U) { ESP_LOGW(VOICE_TAG, "Truncated prompt frame header: %s", path);
break;
}
if (header_read != sizeof(header)) {
ESP_LOGW(VOICE_TAG, "Truncated prompt frame header: %s", resolved_path);
err = ESP_ERR_INVALID_SIZE; err = ESP_ERR_INVALID_SIZE;
break; 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 packet_len = ((uint32_t)header[0] << 24) |
((uint32_t)header[1] << 16) | ((uint32_t)header[1] << 16) |
@@ -241,17 +167,18 @@ esp_err_t voice_prompt_play_file(const char *path)
ESP_LOGW(VOICE_TAG, ESP_LOGW(VOICE_TAG,
"Invalid prompt opus packet size: %u in %s", "Invalid prompt opus packet size: %u in %s",
(unsigned)packet_len, (unsigned)packet_len,
resolved_path); path);
err = ESP_ERR_INVALID_SIZE; err = ESP_ERR_INVALID_SIZE;
break; break;
} }
size_t payload_read = fread(packet, 1, packet_len, file); if (packet_len > asset.size - offset) {
if (payload_read != packet_len) { ESP_LOGW(VOICE_TAG, "Truncated prompt opus packet: %s", path);
ESP_LOGW(VOICE_TAG, "Truncated prompt opus packet: %s", resolved_path);
err = ESP_ERR_INVALID_SIZE; err = ESP_ERR_INVALID_SIZE;
break; break;
} }
memcpy(packet, asset.data + offset, packet_len);
offset += packet_len;
err = multimodal_audio_handle_downstream_opus(packet, packet_len); err = multimodal_audio_handle_downstream_opus(packet, packet_len);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -265,7 +192,6 @@ esp_err_t voice_prompt_play_file(const char *path)
} }
free(packet); free(packet);
fclose(file);
esp_err_t finish_err = multimodal_audio_finish_downstream_response(); esp_err_t finish_err = multimodal_audio_finish_downstream_response();
if (err == ESP_OK) { if (err == ESP_OK) {
err = finish_err; err = finish_err;
@@ -286,7 +212,7 @@ esp_err_t voice_prompt_play_file(const char *path)
if (err == ESP_OK) { if (err == ESP_OK) {
ESP_LOGI(VOICE_TAG, ESP_LOGI(VOICE_TAG,
"Prompt completed: %s packets=%u bytes=%u", "Prompt completed: %s packets=%u bytes=%u",
resolved_path, path,
(unsigned)packet_count, (unsigned)packet_count,
(unsigned)total_bytes); (unsigned)total_bytes);
} }
+1 -12
View File
@@ -111,16 +111,6 @@ dependencies:
source: source:
type: idf type: idf
version: 5.5.4 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: direct_dependencies:
- 78/esp-wifi-connect - 78/esp-wifi-connect
- espressif/esp-sr - espressif/esp-sr
@@ -129,7 +119,6 @@ direct_dependencies:
- espressif/esp_websocket_client - espressif/esp_websocket_client
- espressif/mdns - espressif/mdns
- idf - idf
- joltwallet/littlefs manifest_hash: ba6b786f567d22dab6c465344a66b6b05775703588552240fe540a6e7e29bc2c
manifest_hash: c05cc39447d3758499f6a9a2b947c3fe21cf4fd47e0e9905cde8d79dc68d0472
target: esp32s3 target: esp32s3
version: 2.0.0 version: 2.0.0
+2 -2
View File
@@ -52,7 +52,7 @@ flowchart LR
LCD["ST7789 direct LCD backend"] LCD["ST7789 direct LCD backend"]
Audio["ES8311 audio pipeline"] Audio["ES8311 audio pipeline"]
Network["Wi-Fi, BLE provisioning, HTTP, WebSocket"] Network["Wi-Fi, BLE provisioning, HTTP, WebSocket"]
Storage["NVS, LittleFS"] Storage["NVS, embedded assets, model partition"]
GPIO["GPIO, stepper, console UART"] GPIO["GPIO, stepper, console UART"]
end end
@@ -187,7 +187,7 @@ flowchart LR
PlatformReq["platform_lcd_request_presentation()"] PlatformReq["platform_lcd_request_presentation()"]
Pending["lcd_direct pending request"] Pending["lcd_direct pending request"]
Render["cp_lcd_render loop"] 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"] Panel["ST7789 panel"]
CP --> DomainReq --> Resolve --> Catalog --> PlatformReq --> Pending --> Render --> Draw --> Panel CP --> DomainReq --> Resolve --> Catalog --> PlatformReq --> Pending --> Render --> Draw --> Panel
@@ -273,7 +273,7 @@ key: last_shutdown
紧急确认次数:2次 紧急确认次数:2次
确认期间采样间隔:1秒 确认期间采样间隔:1秒
实体按键唤醒 GPIO:-1(关闭) 实体按键唤醒 GPIO:-1(关闭)
关机提示音:/littlefs/audio_prompts/shutdown_power_empty.opus 关机提示音:audio_prompts/shutdown_power_empty.opus
充电唤醒 GPIO:GPIO10,低电平有效 充电唤醒 GPIO:GPIO10,低电平有效
``` ```
@@ -326,7 +326,7 @@ key: last_shutdown
因为要求“播完才能关机”,提示文件缺失、损坏、Codec 初始化失败或播放通路持续故障时,软件会每秒重试而不会休眠。这符合当前产品决策,但可能导致电池最终依靠保护板断电。 因为要求“播完才能关机”,提示文件缺失、损坏、Codec 初始化失败或播放通路持续故障时,软件会每秒重试而不会休眠。这符合当前产品决策,但可能导致电池最终依靠保护板断电。
量产前必须验证 LittleFS 中始终包含 `shutdown_power_empty.opus`,并验证关机场景下音频通路可初始化。 量产前必须验证应用镜像中包含 `audio_prompts/shutdown_power_empty.opus`,并验证关机场景下音频通路可初始化。
### 5.4 GPIO10 只能在实际充电时可靠唤醒 ### 5.4 GPIO10 只能在实际充电时可靠唤醒
@@ -336,7 +336,7 @@ TP4057 状态脚低电平表示正在充电,高电平可能表示充满或未
### 5.5 Deep Sleep 唤醒是新的软件生命周期 ### 5.5 Deep Sleep 唤醒是新的软件生命周期
唤醒后会重新执行 Bootloader 和 `app_main()`,运行内存、任务状态和普通全局变量不会保留。需要跨休眠保留的数据必须写入 NVS、LittleFS 或 RTC 内存。 唤醒后会重新执行 Bootloader 和 `app_main()`,运行内存、任务状态和普通全局变量不会保留。需要跨休眠保留的数据必须写入 NVS 或 RTC 内存;应用内嵌素材是只读的,不能保存运行时数据。
### 5.6 NVS 写入带来少量 Flash 磨损 ### 5.6 NVS 写入带来少量 Flash 磨损
+23
View File
@@ -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 与内存容量分析、风险及后续开发建议 # Flash 与内存容量分析、风险及后续开发建议
> 历史容量快照:文中远程日志与 `logfs` 已从当前固件移除,分区地址和容量应以现行 `16m.csv` 与最新构建产物为准。 > 历史容量快照:文中远程日志、`logfs` 和独立 `gifpack` 分区已从当前固件移除。下文的“当前”“建议”和旧构建步骤仅针对当时版本,不能作为现行操作指南。现行分区以 `16m.csv` 与最新构建报告为准,资源操作见 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。
本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。 本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。
LittleFS 资源从源码到设备的详细流程见: 历史 LittleFS 资源流程归档见:
- [`docs/littlefs-resource-build-flash-mount.md`](littlefs-resource-build-flash-mount.md) - [`docs/littlefs-resource-build-flash-mount.md`](littlefs-resource-build-flash-mount.md)
@@ -12,7 +12,7 @@
项目当前直接通过 ESP-IDF `esp_lcd` 驱动 ST7789P3,LCD 渲染链路主要支持: 项目当前直接通过 ESP-IDF `esp_lcd` 驱动 ST7789P3,LCD 渲染链路主要支持:
- 从 LittleFS 读取 PNG; - 从应用镜像内嵌素材包读取 PNG;
- 将 PNG 解码为 RGB565; - 将 PNG 解码为 RGB565;
- 按 8 行一组复制到 LCD DMA 行缓冲; - 按 8 行一组复制到 LCD DMA 行缓冲;
- 合成充电、低电量等状态图标; - 合成充电、低电量等状态图标;
@@ -1,5 +1,7 @@
# LittleFS 资源构建、烧录与挂载流程 # LittleFS 资源构建、烧录与挂载流程
> 历史流程:当前固件已将 LCD 图片和提示音嵌入 OTA 应用镜像。下文的“当前”、命令和故障排查仅针对旧版 `gifpack` LittleFS,不适用于现行工程;请使用 [应用内嵌素材使用与添加指南](../应用内嵌素材使用与添加指南.md)。
本文说明项目中的 LCD 图片和提示音资源如何从源码目录进入设备 Flash,以及设备运行时如何通过 LittleFS 访问这些资源。同时给出新增、替换和排查资源问题的完整操作步骤。 本文说明项目中的 LCD 图片和提示音资源如何从源码目录进入设备 Flash,以及设备运行时如何通过 LittleFS 访问这些资源。同时给出新增、替换和排查资源问题的完整操作步骤。
## 1. 先建立整体认识 ## 1. 先建立整体认识
@@ -46,7 +46,7 @@ if (!s_server_success_prompt_played) {
if (should_play_prompt) { if (should_play_prompt) {
(void)domain_runtime_behavior_play_prompt_file( (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 连接、重连和配置热点; - Wi-Fi 连接、重连和配置热点;
- 设备身份、JWT 和会话令牌; - 设备身份、JWT 和会话令牌;
- 用户出生资料、每日运势和财富方向; - 用户出生资料、每日运势和财富方向;
- NVS、LittleFS 和工厂 CLI。 - NVS、应用内嵌素材、模型分区和工厂 CLI。
## 2. 分层架构 ## 2. 分层架构
@@ -238,12 +238,14 @@ TQ_Cat_Code-1.0.3.bin
### 6.4 该文件只是应用镜像,不是完整烧录包 ### 6.4 该文件只是应用镜像,不是完整烧录包
`TQ_Cat_Code-1.0.2.bin` 只对应应用程序分区。它不包含以下独立镜像或资源: `TQ_Cat_Code-1.0.2.bin` 只对应应用程序分区。它不包含以下独立镜像:
- Bootloader; - Bootloader;
- 分区表; - 分区表;
- OTA data 初始化镜像; - OTA data 初始化镜像;
- LittleFS/LCD/音频资源镜像。 - 独立模型分区镜像。
当前 LCD 图片和提示音已包含在应用镜像内,会随应用 OTA 一起更新。
该文件适合应用分区 OTA,不能被误认为包含全部分区的整机合并烧录镜像。工厂烧录仍需按照 ESP-IDF 生成的烧录参数写入各个分区。 该文件适合应用分区 OTA,不能被误认为包含全部分区的整机合并烧录镜像。工厂烧录仍需按照 ESP-IDF 生成的烧录参数写入各个分区。
+1 -1
View File
@@ -65,7 +65,7 @@ Transport disconnect/error events are not server error expressions.
## Asset Migration Direction ## Asset Migration Direction
The image playback path is a platform-owned direct LCD asset pipeline. Runtime 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 `lodepng`, converted to RGB565 in PSRAM, and written to the ST7789 panel through
the existing bounded row-transfer buffer. the existing bounded row-transfer buffer.
+4 -4
View File
@@ -4,7 +4,7 @@
固件使用的 LCD 为 RGB565 显示格式,而设计工具导出的 PNG 通常是 RGB888 或 RGBA8888。原始 PNG 中包含 LCD 无法显示的颜色精度,部分图片还携带未实际使用的 Alpha 通道。 固件使用的 LCD 为 RGB565 显示格式,而设计工具导出的 PNG 通常是 RGB888 或 RGBA8888。原始 PNG 中包含 LCD 无法显示的颜色精度,部分图片还携带未实际使用的 Alpha 通道。
本项目在不改变 LCD 最终 RGB565 像素的前提下,将固件图片转换成 RGB565 颜色精度,并在颜色数量不超过 256 时保存为索引色 PNG,以降低 `gifpack` LittleFS 分区的占用。 本项目在不改变 LCD 最终 RGB565 像素的前提下,将固件图片转换成 RGB565 颜色精度,并在颜色数量不超过 256 时保存为索引色 PNG,以降低应用镜像中的素材占用。
正常固件编译直接使用已经提交到 Git 的优化图片,不要求开发环境安装 Pillow。只有修改或验证图片资源时才需要 Pillow。 正常固件编译直接使用已经提交到 Git 的优化图片,不要求开发环境安装 Pillow。只有修改或验证图片资源时才需要 Pillow。
@@ -16,9 +16,9 @@ components/platform/assets/
└── lcd_optimized/ 优化图片和 manifest.json,固件实际使用 └── 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 存储优化实施报告 # LCD PNG 存储优化实施报告
> 历史优化报告:本文记录当时的 `gifpack` LittleFS 布局及测试数据,不代表当前固件。现行素材流程见 [应用内嵌素材使用与添加指南](应用内嵌素材使用与添加指南.md)。
## 1. 基本信息 ## 1. 基本信息
- 对应提交:`549f495 perf(assets): optimize LCD PNG storage` - 对应提交:`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_DIM_BACKLIGHT=y
CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10 CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10
CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE=y 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_DISABLE_SPEAKER_OUTPUT=y
CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y
CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=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 完整构建通过。 - ESP-IDF 5.5.4 完整构建通过。
- 当前构建生成的应用固件约 2.08 MiB,应用分区剩余约 41%。 - 当前构建生成的应用固件约 2.08 MiB,应用分区剩余约 41%。
- `standby_5min.opus` 已纳入 LittleFS 音频资源。 - `audio_prompts/standby_5min.opus` 已纳入应用镜像中的音频素材包。
- 源码差异格式检查通过。 - 源码差异格式检查通过。
- 已确认提示音等待的是播放管线终止事件,而不是固定时长硬等待。 - 已确认提示音等待的是播放管线终止事件,而不是固定时长硬等待。
@@ -321,13 +321,13 @@ Wi-Fi 断开表情在离线期间每 5 秒刷新一次,但被动断网提示
```c ```c
if (!control_plane_is_sta_connected()) { if (!control_plane_is_sta_connected()) {
play_prompt("network_unstable.opus"); play_prompt("audio_prompts/network_unstable.opus");
request_immediate_wifi_reconnect(); request_immediate_wifi_reconnect();
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
if (server_outage_confirmed) { 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(); schedule_fresh_auth_websocket_reconnect();
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
+9 -7
View File
@@ -1,5 +1,7 @@
# 网络异常语音反馈与业务快速失败改造说明 # 网络异常语音反馈与业务快速失败改造说明
> 本文中的提交、构建容量和测试结果是历史记录;资源路径已更新为现行应用内嵌素材名称。现行添加流程见 [应用内嵌素材使用与添加指南](应用内嵌素材使用与添加指南.md)。
## 1. 提交概况 ## 1. 提交概况
```text ```text
@@ -95,19 +97,19 @@ static portMUX_TYPE s_feedback_lock = portMUX_INITIALIZER_UNLOCKED;
### 3.2 复用现有音频资源 ### 3.2 复用现有音频资源
本次没有新增录音文件,直接复用 LittleFS 中已有的五个 Opus 资源: 本次没有新增录音文件,直接复用应用镜像中已有的五个 Opus 资源:
```c ```c
#define CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE \ #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 \ #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 \ #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 \ #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 \ #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 格式和空白错误检查通过。 - Markdown 格式和空白错误检查通过。
@@ -272,7 +272,7 @@ return domain_runtime_request_expression(&request);
- 财位、校准、抽签等已有模型指令的枚举值未改变。 - 财位、校准、抽签等已有模型指令的枚举值未改变。
- 表达式请求结构增加字段,但现有请求均使用指定字段初始化,新增字段默认置零。 - 表达式请求结构增加字段,但现有请求均使用指定字段初始化,新增字段默认置零。
- 原五行调用的 `wuxing_element_valid` 默认为 `false`,继续随机显示。 - 原五行调用的 `wuxing_element_valid` 默认为 `false`,继续随机显示。
- 未新增 LCD 图片,不增加 LittleFS 资源占用。 - 未新增 LCD 图片,不增加应用镜像中的素材占用。
- 未新增任务、队列或锁,不改变任务生命周期和并发模型。 - 未新增任务、队列或锁,不改变任务生命周期和并发模型。
### 5.2 当前限制 ### 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 是否已登记到平台帧表和领域表情映射;提示音是否为项目分帧格式;设备运行的是否为本次构建的应用版本。构建目录的暂存文件不是设备上的文件系统。
+6 -7
View File
@@ -227,7 +227,7 @@ menu "TQ Cat Configuration"
string "Boot sound file" string "Boot sound file"
default "" default ""
help 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. Leave empty to use the semantic connecting prompt fallback.
config TQ_NETWORK_TASK_CORE config TQ_NETWORK_TASK_CORE
@@ -410,10 +410,10 @@ menu "TQ Cat Configuration"
config TQ_LIGHT_STANDBY_PROMPT_FILE config TQ_LIGHT_STANDBY_PROMPT_FILE
string "Light-standby entry 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 depends on TQ_LIGHT_STANDBY_PROMPT_ENABLE
help 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 config TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT
bool "Disable speaker output in light standby" bool "Disable speaker output in light standby"
@@ -1022,7 +1022,7 @@ menu "TQ Cat Configuration"
config TQ_BATTERY_SHUTDOWN_PROMPT_FILE config TQ_BATTERY_SHUTDOWN_PROMPT_FILE
string "Critical-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 depends on TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
endmenu endmenu
@@ -1380,10 +1380,9 @@ menu "TQ Cat Configuration"
config TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE config TQ_LOCAL_WAKE_RESPONSE_PROMPT_FILE
string "Local wake response prompt file" string "Local wake response prompt file"
depends on TQ_LOCAL_WAKE_ENABLE depends on TQ_LOCAL_WAKE_ENABLE
default "local_wake_response.opus" default "audio_prompts/local_wake_response.opus"
help help
Prompt filename relative to /littlefs/audio_prompts. Absolute Full embedded asset name, such as audio_prompts/local_wake_response.opus.
LittleFS paths are also supported.
config TQ_LOCAL_WAKE_RETRY_MS config TQ_LOCAL_WAKE_RETRY_MS
int "Local wake startup retry delay (ms)" int "Local wake startup retry delay (ms)"
+4 -4
View File
@@ -37,7 +37,7 @@ CONFIG_FREERTOS_USE_TRACE_FACILITY=y
# CONFIG_TQ_TASK_MONITOR_ENABLE is not set # CONFIG_TQ_TASK_MONITOR_ENABLE is not set
# CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN is not set # CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN is not set
CONFIG_TQ_BOOT_SOUND_TIMEOUT_MS=3000 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_NETWORK_TASK_CORE=0
CONFIG_TQ_LOCAL_TASK_CORE=1 CONFIG_TQ_LOCAL_TASK_CORE=1
CONFIG_TQ_LCD_RENDER_TASK_STACK_BYTES=8192 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_DIM_BACKLIGHT=y
CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10 CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10
CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE=y 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_DISABLE_SPEAKER_OUTPUT=y
CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y
CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=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_MIN_BATTERY_PERCENT=20
CONFIG_TQ_OTA_POWER_RETRY_MS=60000 CONFIG_TQ_OTA_POWER_RETRY_MS=60000
CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE=y 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_RETRY_MS=5000
CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS=10000 CONFIG_TQ_LOCAL_WAKE_READY_TIMEOUT_MS=10000
CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS=1000 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_EMERGENCY_VOLTAGE_MV=3350
CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV=3550 CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV=3550
CONFIG_TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO=-1 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 CONFIG_MULTIMODAL_AUDIO_PA_GPIO=11
+63
View File
@@ -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()
+71
View File
@@ -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()