Compare commits
1 Commits
main
...
feat/ble-o
| Author | SHA1 | Date | |
|---|---|---|---|
| 9223fd5a7d |
110
main/ble/ble.c
110
main/ble/ble.c
@ -1,6 +1,8 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_bt.h"
|
#include "esp_bt.h"
|
||||||
#include "esp_gap_ble_api.h"
|
#include "esp_gap_ble_api.h"
|
||||||
@ -29,7 +31,8 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_
|
|||||||
|
|
||||||
|
|
||||||
static const char *CONN_TAG = "CONN_BLE";
|
static const char *CONN_TAG = "CONN_BLE";
|
||||||
static const char device_name[] = "MY-BLE";
|
static char ble_device_name[32];
|
||||||
|
static uint8_t adv_raw_len = 0;
|
||||||
|
|
||||||
static uint16_t conn_id;
|
static uint16_t conn_id;
|
||||||
|
|
||||||
@ -53,6 +56,22 @@ MegStatus SendStatus = {false,0};
|
|||||||
uint8_t *img_data = 0;
|
uint8_t *img_data = 0;
|
||||||
FILE *file_img;
|
FILE *file_img;
|
||||||
|
|
||||||
|
// BLE 图片处理任务(NVS 写入 + 导航显示在独立任务中执行,避免 BTC_TASK 栈溢出)
|
||||||
|
static TaskHandle_t ble_process_task_handle = NULL;
|
||||||
|
static char ble_pending_filename[24];
|
||||||
|
static uint8_t *ble_pending_data = NULL; // 传输完成的图片数据(直通显示,跳过 SPIFFS 重读)
|
||||||
|
static size_t ble_pending_data_size = 0;
|
||||||
|
|
||||||
|
static void ble_process_task(void *arg) {
|
||||||
|
while (1) {
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
nvs_change_img(ble_pending_filename);
|
||||||
|
ble_image_navigate_with_data(ble_pending_filename, ble_pending_data, ble_pending_data_size);
|
||||||
|
ble_pending_data = NULL; // 所有权已转移,不再释放
|
||||||
|
ble_pending_data_size = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static uint8_t attr_value_write[512] = {0};
|
static uint8_t attr_value_write[512] = {0};
|
||||||
static uint8_t attr_value_edit[20] = {0};
|
static uint8_t attr_value_edit[20] = {0};
|
||||||
|
|
||||||
@ -101,12 +120,12 @@ static esp_ble_adv_params_t adv_params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
static uint8_t adv_raw_data[] = {
|
static uint8_t adv_raw_data[31];
|
||||||
0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,
|
|
||||||
0x07, ESP_BLE_AD_TYPE_NAME_CMPL, 'M','Y','-','B','L','E',
|
// Scan Response 数据:厂商标识 + 服务UUID
|
||||||
0x02, ESP_BLE_AD_TYPE_TX_PWR, 0x09,
|
static uint8_t scan_rsp_data[] = {
|
||||||
0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xB0, 0x00,
|
0x07, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x4C, 0x44, 0x64, 0x7A, 0x62, 0x6A, // "LDdzbj"
|
||||||
0x07, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE,0x4C,0x44,0x64,0x7A,0x62,0x6A
|
0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0x00, 0x0B, // 服务UUID 0x0B00
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -156,15 +175,54 @@ void ble_init(void)
|
|||||||
ESP_LOGE(CONN_TAG, "set local MTU failed, error code = %x", ret);
|
ESP_LOGE(CONN_TAG, "set local MTU failed, error code = %x", ret);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ret = esp_ble_gap_set_device_name(device_name);
|
|
||||||
|
// 获取 BLE MAC 地址并构建设备名称: Airhub_xx:xx:xx:xx:xx:xx
|
||||||
|
const uint8_t *ble_addr = esp_bt_dev_get_address();
|
||||||
|
if (ble_addr) {
|
||||||
|
snprintf(ble_device_name, sizeof(ble_device_name),
|
||||||
|
"Airhub_%02x:%02x:%02x:%02x:%02x:%02x",
|
||||||
|
ble_addr[0], ble_addr[1], ble_addr[2],
|
||||||
|
ble_addr[3], ble_addr[4], ble_addr[5]);
|
||||||
|
ESP_LOGI(CONN_TAG, "BLE MAC: %02x:%02x:%02x:%02x:%02x:%02x",
|
||||||
|
ble_addr[0], ble_addr[1], ble_addr[2],
|
||||||
|
ble_addr[3], ble_addr[4], ble_addr[5]);
|
||||||
|
} else {
|
||||||
|
strcpy(ble_device_name, "Airhub_BLE");
|
||||||
|
ESP_LOGW(CONN_TAG, "获取BLE MAC失败,使用默认名称: %s", ble_device_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = esp_ble_gap_set_device_name(ble_device_name);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
ESP_LOGE(CONN_TAG, "set device name failed, error code = %x", ret);
|
ESP_LOGE(CONN_TAG, "set device name failed, error code = %x", ret);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ret = esp_ble_gap_config_adv_data_raw(adv_raw_data, sizeof(adv_raw_data));
|
ESP_LOGI(CONN_TAG, "蓝牙设备名称: %s", ble_device_name);
|
||||||
|
|
||||||
|
// 构建广播数据: Flags + Complete Local Name
|
||||||
|
uint8_t name_len = strlen(ble_device_name);
|
||||||
|
int offset = 0;
|
||||||
|
adv_raw_data[offset++] = 0x02;
|
||||||
|
adv_raw_data[offset++] = ESP_BLE_AD_TYPE_FLAG;
|
||||||
|
adv_raw_data[offset++] = 0x06;
|
||||||
|
adv_raw_data[offset++] = name_len + 1;
|
||||||
|
adv_raw_data[offset++] = ESP_BLE_AD_TYPE_NAME_CMPL;
|
||||||
|
memcpy(&adv_raw_data[offset], ble_device_name, name_len);
|
||||||
|
offset += name_len;
|
||||||
|
adv_raw_len = offset;
|
||||||
|
|
||||||
|
ret = esp_ble_gap_config_adv_data_raw(adv_raw_data, adv_raw_len);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
ESP_LOGE(CONN_TAG, "config adv data failed, error code = %x", ret);
|
ESP_LOGE(CONN_TAG, "config adv data failed, error code = %x", ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配置 Scan Response 数据(厂商标识 "dzbj" + 服务UUID)
|
||||||
|
ret = esp_ble_gap_config_scan_rsp_data_raw(scan_rsp_data, sizeof(scan_rsp_data));
|
||||||
|
if (ret) {
|
||||||
|
ESP_LOGE(CONN_TAG, "config scan response data failed, error code = %x", ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建图片处理任务(8KB 栈,足够 SPIFFS 扫描 + LVGL + GIF 解码)
|
||||||
|
xTaskCreate(ble_process_task, "ble_img", 8192, NULL, 5, &ble_process_task_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -174,6 +232,10 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
|
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
|
||||||
ESP_LOGI(CONN_TAG, "Advertising data set, status %d", param->adv_data_raw_cmpl.status);
|
ESP_LOGI(CONN_TAG, "Advertising data set, status %d", param->adv_data_raw_cmpl.status);
|
||||||
|
// ADV 数据设置完成,等待 Scan Response 也设置完成后再开始广播
|
||||||
|
break;
|
||||||
|
case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
|
||||||
|
ESP_LOGI(CONN_TAG, "Scan response data set, status %d", param->scan_rsp_data_raw_cmpl.status);
|
||||||
esp_ble_gap_start_advertising(&adv_params);
|
esp_ble_gap_start_advertising(&adv_params);
|
||||||
break;
|
break;
|
||||||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
||||||
@ -268,9 +330,12 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_
|
|||||||
ESP_LOGI(CONN_TAG,"传输通道建立成功,数据指针:%p,文件名称:%s,文件大小:%d",img_data,firstMeg.filename,(int)firstMeg.len);
|
ESP_LOGI(CONN_TAG,"传输通道建立成功,数据指针:%p,文件名称:%s,文件大小:%d",img_data,firstMeg.filename,(int)firstMeg.len);
|
||||||
}
|
}
|
||||||
}else if(SendStatus.isSend){
|
}else if(SendStatus.isSend){
|
||||||
ESP_LOGI(CONN_TAG, "获取到数据:第:%d包,长度:%d,是否结束:%d",*value+1,(int)param->write.len,*(value+1));
|
uint8_t pkt_no = *value;
|
||||||
uint8_t isEnd = *(value + 1);
|
uint8_t isEnd = *(value + 1);
|
||||||
uint8_t port = *(value);
|
// 每 100 包或最后一包打印日志(减少串口输出提升传输速度)
|
||||||
|
if (pkt_no % 100 == 0 || isEnd) {
|
||||||
|
ESP_LOGI(CONN_TAG, "获取到数据:第:%d包,长度:%d,是否结束:%d", pkt_no+1, (int)param->write.len, isEnd);
|
||||||
|
}
|
||||||
uint8_t *data = value + 2;
|
uint8_t *data = value + 2;
|
||||||
memcpy(img_data + SendStatus.port,data,(int)param->write.len-2);
|
memcpy(img_data + SendStatus.port,data,(int)param->write.len-2);
|
||||||
SendStatus.port += param->write.len-2;
|
SendStatus.port += param->write.len-2;
|
||||||
@ -281,12 +346,15 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_
|
|||||||
fclose(file_img);
|
fclose(file_img);
|
||||||
SendStatus.isSend = false;
|
SendStatus.isSend = false;
|
||||||
SendStatus.port = 0;
|
SendStatus.port = 0;
|
||||||
ESP_LOGI(CONN_TAG,"图片接收成功");
|
// img_data 不释放,传给显示任务直通显示(跳过 SPIFFS 重读)
|
||||||
nvs_change_img(firstMeg.filename);
|
ble_pending_data = img_data;
|
||||||
// 导航到ScreenImg显示新图片(内部刷新列表+设置索引+切换界面)
|
ble_pending_data_size = firstMeg.len;
|
||||||
ble_image_navigate(firstMeg.filename);
|
img_data = NULL; // 转移所有权
|
||||||
free(img_data);
|
|
||||||
free(filepath);
|
free(filepath);
|
||||||
|
ESP_LOGI(CONN_TAG,"图片接收成功,数据直通显示(%d字节)", (int)ble_pending_data_size);
|
||||||
|
strncpy(ble_pending_filename, firstMeg.filename, sizeof(ble_pending_filename) - 1);
|
||||||
|
ble_pending_filename[sizeof(ble_pending_filename) - 1] = '\0';
|
||||||
|
xTaskNotifyGive(ble_process_task_handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}// 图片编辑特征写入事件
|
}// 图片编辑特征写入事件
|
||||||
@ -296,8 +364,10 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_
|
|||||||
uint8_t type = *(value + param->write.len - 1);
|
uint8_t type = *(value + param->write.len - 1);
|
||||||
memcpy(imgName, value, 23);
|
memcpy(imgName, value, 23);
|
||||||
if(type == 0xff){
|
if(type == 0xff){
|
||||||
nvs_change_img(imgName);
|
// 耗时操作转移到独立任务执行
|
||||||
ble_image_navigate(imgName);
|
strncpy(ble_pending_filename, imgName, sizeof(ble_pending_filename) - 1);
|
||||||
|
ble_pending_filename[sizeof(ble_pending_filename) - 1] = '\0';
|
||||||
|
xTaskNotifyGive(ble_process_task_handle);
|
||||||
}else if(type == 0xF1){
|
}else if(type == 0xF1){
|
||||||
remove(filepath);
|
remove(filepath);
|
||||||
SendStatus.isSend = false;
|
SendStatus.isSend = false;
|
||||||
@ -311,8 +381,8 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_
|
|||||||
esp_ble_conn_update_params_t conn_params = {0};
|
esp_ble_conn_update_params_t conn_params = {0};
|
||||||
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
|
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
|
||||||
conn_params.latency = 0;
|
conn_params.latency = 0;
|
||||||
conn_params.max_int = 32;
|
conn_params.max_int = 16; // 16 × 1.25ms = 20ms(缩短连接间隔提升传输吞吐量)
|
||||||
conn_params.min_int = 16;
|
conn_params.min_int = 6; // 6 × 1.25ms = 7.5ms
|
||||||
conn_params.timeout = 400;
|
conn_params.timeout = 400;
|
||||||
conn_id = param->connect.conn_id;
|
conn_id = param->connect.conn_id;
|
||||||
ESP_LOGI(CONN_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR"",
|
ESP_LOGI(CONN_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR"",
|
||||||
|
|||||||
@ -109,7 +109,7 @@ void lvgl_lcd_init(){
|
|||||||
};
|
};
|
||||||
lvgl_port_init(&lvgl_cfg);
|
lvgl_port_init(&lvgl_cfg);
|
||||||
|
|
||||||
#define LVGL_DRAW_BUF_LINES 30
|
#define LVGL_DRAW_BUF_LINES 120
|
||||||
size_t buffer_size = LCD_WID * LVGL_DRAW_BUF_LINES;
|
size_t buffer_size = LCD_WID * LVGL_DRAW_BUF_LINES;
|
||||||
|
|
||||||
ESP_LOGI(LCD_TAG, "LVGL buffer size: %d bytes (W: %d, Lines: %d)",
|
ESP_LOGI(LCD_TAG, "LVGL buffer size: %d bytes (W: %d, Lines: %d)",
|
||||||
@ -129,7 +129,8 @@ void lvgl_lcd_init(){
|
|||||||
.mirror_y = false,// 垂直镜像
|
.mirror_y = false,// 垂直镜像
|
||||||
},
|
},
|
||||||
.flags = {
|
.flags = {
|
||||||
.buff_dma = true,// 使用DMA传输显示缓冲区
|
.buff_dma = false,
|
||||||
|
.buff_spiram = true,// PSRAM 120行大缓冲,每帧仅3次flush,GIF最流畅
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
disp_handle = lvgl_port_add_disp(&disp_cfg);
|
disp_handle = lvgl_port_add_disp(&disp_cfg);
|
||||||
|
|||||||
@ -213,13 +213,13 @@ void app_main(void)
|
|||||||
|
|
||||||
// 配置 Power Management(低功耗管理)
|
// 配置 Power Management(低功耗管理)
|
||||||
esp_pm_config_t pm_config = {
|
esp_pm_config_t pm_config = {
|
||||||
.max_freq_mhz = 160, // 最大频率 160MHz(与当前CPU频率一致)
|
.max_freq_mhz = 240, // 最大频率 240MHz(GIF解码需要高算力)
|
||||||
.min_freq_mhz = 40, // 最小频率 40MHz(保证LVGL正常刷新)
|
.min_freq_mhz = 40, // 最小频率 40MHz(保证LVGL正常刷新)
|
||||||
.light_sleep_enable = true // 启用自动 Light Sleep
|
.light_sleep_enable = true // 启用自动 Light Sleep
|
||||||
};
|
};
|
||||||
esp_err_t pm_err = esp_pm_configure(&pm_config);
|
esp_err_t pm_err = esp_pm_configure(&pm_config);
|
||||||
if (pm_err == ESP_OK) {
|
if (pm_err == ESP_OK) {
|
||||||
ESP_LOGI("MAIN", "2.1 Power Management已启用:40-160MHz动态频率 + 自动Light Sleep");
|
ESP_LOGI("MAIN", "2.1 Power Management已启用:40-240MHz动态频率 + 自动Light Sleep");
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW("MAIN", "2.1 Power Management启用失败:%s", esp_err_to_name(pm_err));
|
ESP_LOGW("MAIN", "2.1 Power Management启用失败:%s", esp_err_to_name(pm_err));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include "lvgl.h"
|
||||||
|
|
||||||
void app_test_display(); // 测试显示
|
void app_test_display(); // 测试显示
|
||||||
void app_img_display(); // 显示图片
|
void app_img_display(); // 显示图片
|
||||||
@ -19,3 +20,7 @@ void init_spiffs_image_list(void); // 初始化/扫描SPIFFS图片列表
|
|||||||
void free_spiffs_image_list(void); // 重置图片列表
|
void free_spiffs_image_list(void); // 重置图片列表
|
||||||
bool set_image_index_by_name(const char *name); // 根据文件名设置当前图片索引
|
bool set_image_index_by_name(const char *name); // 根据文件名设置当前图片索引
|
||||||
void ble_image_navigate(const char *filename); // BLE接收后导航到ScreenImg显示
|
void ble_image_navigate(const char *filename); // BLE接收后导航到ScreenImg显示
|
||||||
|
void ble_image_navigate_with_data(const char *filename, uint8_t *data, size_t data_size); // BLE接收后直通显示(跳过SPIFFS重读)
|
||||||
|
#if LV_USE_GIF
|
||||||
|
void pages_cleanup_gif(void); // 清理 GIF 控件资源
|
||||||
|
#endif
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
#include "lvgl.h"
|
#include "lvgl.h"
|
||||||
|
#if LV_USE_GIF
|
||||||
|
#include "extra/libs/gif/lv_gif.h"
|
||||||
|
#include "extra/libs/gif/gifdec.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#endif
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
#include "fatfs.h"
|
#include "fatfs.h"
|
||||||
#include "driver/ledc.h"
|
#include "driver/ledc.h"
|
||||||
#include "gpio.h"
|
#include "gpio.h"
|
||||||
@ -29,6 +36,29 @@ static int spiffs_image_count = 0;
|
|||||||
static int current_image_index = 0;
|
static int current_image_index = 0;
|
||||||
static bool image_list_initialized = false;
|
static bool image_list_initialized = false;
|
||||||
|
|
||||||
|
#if LV_USE_GIF
|
||||||
|
// === 自定义 GIF 播放器(替代 lv_gif,性能优化) ===
|
||||||
|
// 优化: 1.Palette LUT查表 2.TRUE_COLOR无Alpha 3.后台线程解码流水线
|
||||||
|
static gd_GIF *gif_decoder = NULL; // gifdec 解码句柄
|
||||||
|
static lv_obj_t *gif_img_obj = NULL; // 普通 lv_img 控件(替代 lv_gif)
|
||||||
|
static uint16_t *gif_rgb565_buf[2] = {NULL, NULL}; // 双缓冲 RGB565 帧 (PSRAM)
|
||||||
|
static lv_img_dsc_t gif_frame_dsc; // LVGL 图片描述符(TRUE_COLOR,无Alpha)
|
||||||
|
static uint16_t gif_palette_lut[256]; // 调色板 RGB565 查找表
|
||||||
|
static volatile uint8_t gif_front_idx = 0; // 当前显示的缓冲区索引
|
||||||
|
static lv_timer_t *gif_play_timer = NULL; // LVGL 播放定时器
|
||||||
|
static TaskHandle_t gif_decode_task_handle = NULL; // 后台解码任务句柄
|
||||||
|
static volatile bool gif_playing = false; // 播放状态标志
|
||||||
|
static volatile bool gif_new_frame_ready = false; // 新帧就绪标志
|
||||||
|
static uint32_t gif_last_frame_ms = 0; // 上一帧显示时间戳
|
||||||
|
static uint8_t *gif_psram_buf = NULL; // GIF 文件数据(PSRAM)
|
||||||
|
static bool current_is_gif = false; // 当前是否为 GIF 模式
|
||||||
|
|
||||||
|
// 前向声明
|
||||||
|
static bool is_gif_file(const char *filename);
|
||||||
|
static void gif_player_start(void);
|
||||||
|
static void gif_player_stop(void);
|
||||||
|
#endif // LV_USE_GIF
|
||||||
|
|
||||||
// 当前亮度值(用于休眠恢复)
|
// 当前亮度值(用于休眠恢复)
|
||||||
static uint8_t current_brightness = 50;
|
static uint8_t current_brightness = 50;
|
||||||
|
|
||||||
@ -600,7 +630,11 @@ void init_spiffs_image_list(void) {
|
|||||||
if(len > 4 && len < MAX_FILENAME_LEN) {
|
if(len > 4 && len < MAX_FILENAME_LEN) {
|
||||||
const char *ext = name + len - 4;
|
const char *ext = name + len - 4;
|
||||||
if(strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 ||
|
if(strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 ||
|
||||||
strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0) {
|
strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0
|
||||||
|
#if LV_USE_GIF
|
||||||
|
|| strcasecmp(ext, ".gif") == 0
|
||||||
|
#endif
|
||||||
|
) {
|
||||||
// 存储图片文件名到静态缓冲区
|
// 存储图片文件名到静态缓冲区
|
||||||
strncpy(spiffs_image_files[spiffs_image_count], name, MAX_FILENAME_LEN - 1);
|
strncpy(spiffs_image_files[spiffs_image_count], name, MAX_FILENAME_LEN - 1);
|
||||||
spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0';
|
spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0';
|
||||||
@ -686,12 +720,25 @@ bool set_image_index_by_name(const char *name) {
|
|||||||
|
|
||||||
// BLE接收图片后导航到ScreenImg显示
|
// BLE接收图片后导航到ScreenImg显示
|
||||||
void ble_image_navigate(const char *filename) {
|
void ble_image_navigate(const char *filename) {
|
||||||
// 刷新图片列表
|
// 将新文件直接追加到列表(避免重扫 SPIFFS 目录,节省 ~200ms)
|
||||||
free_spiffs_image_list();
|
if (!image_list_initialized) {
|
||||||
init_spiffs_image_list();
|
init_spiffs_image_list();
|
||||||
|
}
|
||||||
// 设置当前索引为新接收的图片
|
// 检查文件是否已在列表中(避免重复)
|
||||||
set_image_index_by_name(filename);
|
bool found = false;
|
||||||
|
for (int i = 0; i < spiffs_image_count; i++) {
|
||||||
|
if (strcmp(spiffs_image_files[i], filename) == 0) {
|
||||||
|
current_image_index = i;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found && spiffs_image_count < MAX_IMAGE_FILES) {
|
||||||
|
strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1);
|
||||||
|
spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0';
|
||||||
|
current_image_index = spiffs_image_count;
|
||||||
|
spiffs_image_count++;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否已在ScreenImg界面
|
// 检查是否已在ScreenImg界面
|
||||||
lvgl_port_lock(0);
|
lvgl_port_lock(0);
|
||||||
@ -710,6 +757,82 @@ void ble_image_navigate(const char *filename) {
|
|||||||
ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename);
|
ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BLE接收图片后导航显示(携带预加载数据,跳过 SPIFFS 重读)
|
||||||
|
void ble_image_navigate_with_data(const char *filename, uint8_t *data, size_t data_size) {
|
||||||
|
// 将新文件追加到列表
|
||||||
|
if (!image_list_initialized) {
|
||||||
|
init_spiffs_image_list();
|
||||||
|
}
|
||||||
|
bool found = false;
|
||||||
|
for (int i = 0; i < spiffs_image_count; i++) {
|
||||||
|
if (strcmp(spiffs_image_files[i], filename) == 0) {
|
||||||
|
current_image_index = i;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found && spiffs_image_count < MAX_IMAGE_FILES) {
|
||||||
|
strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1);
|
||||||
|
spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0';
|
||||||
|
current_image_index = spiffs_image_count;
|
||||||
|
spiffs_image_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if LV_USE_GIF
|
||||||
|
// 如果有预加载数据且是 GIF,直接用内存数据显示(跳过 SPIFFS 重读)
|
||||||
|
if (data && data_size > 0 && is_gif_file(filename)) {
|
||||||
|
// 停止旧 GIF 播放器
|
||||||
|
gif_player_stop();
|
||||||
|
if (gif_psram_buf) {
|
||||||
|
free(gif_psram_buf);
|
||||||
|
}
|
||||||
|
// BLE 数据直接作为 GIF 源(所有权转移)
|
||||||
|
gif_psram_buf = data;
|
||||||
|
|
||||||
|
// 打开 gifdec 解码器(从 PSRAM 内存源)
|
||||||
|
gif_decoder = gd_open_gif_data(gif_psram_buf);
|
||||||
|
if (!gif_decoder) {
|
||||||
|
ESP_LOGE("GIF", "gifdec 打开失败");
|
||||||
|
free(gif_psram_buf);
|
||||||
|
gif_psram_buf = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保在 ScreenImg 界面
|
||||||
|
lvgl_port_lock(0);
|
||||||
|
bool already_on_screen = (lv_scr_act() == ui_ScreenImg);
|
||||||
|
if (!already_on_screen) {
|
||||||
|
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
|
||||||
|
}
|
||||||
|
lvgl_port_unlock();
|
||||||
|
|
||||||
|
// 启动自定义 GIF 播放器
|
||||||
|
gif_player_start();
|
||||||
|
|
||||||
|
ESP_LOGI("IMG_LIST", "BLE GIF直通显示(优化): %s", filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif // LV_USE_GIF
|
||||||
|
|
||||||
|
// 非 GIF 或无预加载数据,释放 BLE 数据,走常规 SPIFFS 路径
|
||||||
|
if (data) {
|
||||||
|
free(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
lvgl_port_lock(0);
|
||||||
|
bool already_on_screen = (lv_scr_act() == ui_ScreenImg);
|
||||||
|
if (!already_on_screen) {
|
||||||
|
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
|
||||||
|
}
|
||||||
|
lvgl_port_unlock();
|
||||||
|
|
||||||
|
if (already_on_screen) {
|
||||||
|
update_ui_ImgBle(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前图片文件名
|
// 获取当前图片文件名
|
||||||
const char* get_current_image(void) {
|
const char* get_current_image(void) {
|
||||||
if(!image_list_initialized || spiffs_image_count == 0) {
|
if(!image_list_initialized || spiffs_image_count == 0) {
|
||||||
@ -770,7 +893,237 @@ bool delete_current_image(void) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新ui_ImgBle控件的图片
|
#if LV_USE_GIF
|
||||||
|
// 判断文件是否为 GIF 格式
|
||||||
|
static bool is_gif_file(const char *filename) {
|
||||||
|
int len = strlen(filename);
|
||||||
|
if (len < 4) return false;
|
||||||
|
return strcasecmp(filename + len - 4, ".gif") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === GIF 播放器内部函数 ===
|
||||||
|
|
||||||
|
// 构建调色板 RGB565 查找表(每次帧解码前调用,处理局部调色板)
|
||||||
|
static void gif_build_palette_lut(gd_Palette *palette) {
|
||||||
|
for (int i = 0; i < palette->size; i++) {
|
||||||
|
uint8_t r = palette->colors[i * 3 + 0];
|
||||||
|
uint8_t g = palette->colors[i * 3 + 1];
|
||||||
|
uint8_t b = palette->colors[i * 3 + 2];
|
||||||
|
lv_color_t c = lv_color_make(r, g, b); // 自动处理 LV_COLOR_16_SWAP
|
||||||
|
gif_palette_lut[i] = c.full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速渲染帧到 canvas(palette LUT 替代逐像素 lv_color_make)
|
||||||
|
static void gif_render_frame_fast(gd_GIF *gif) {
|
||||||
|
int i = gif->fy * gif->width + gif->fx;
|
||||||
|
for (int j = 0; j < gif->fh; j++) {
|
||||||
|
for (int k = 0; k < gif->fw; k++) {
|
||||||
|
uint8_t index = gif->frame[(gif->fy + j) * gif->width + gif->fx + k];
|
||||||
|
if (!gif->gce.transparency || index != gif->gce.tindex) {
|
||||||
|
uint16_t c = gif_palette_lut[index];
|
||||||
|
gif->canvas[(i + k) * 3 + 0] = c & 0xff;
|
||||||
|
gif->canvas[(i + k) * 3 + 1] = (c >> 8) & 0xff;
|
||||||
|
gif->canvas[(i + k) * 3 + 2] = 0xff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += gif->width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// canvas (RGB565+Alpha 3字节/像素) → RGB565 (2字节/像素) 快速拷贝
|
||||||
|
static void gif_canvas_to_rgb565(gd_GIF *gif, uint16_t *out) {
|
||||||
|
uint8_t *canvas = gif->canvas;
|
||||||
|
int total = gif->width * gif->height;
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
out[i] = (uint16_t)canvas[i * 3] | ((uint16_t)canvas[i * 3 + 1] << 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后台解码任务(FreeRTOS,与 LVGL 显示并行)
|
||||||
|
static void gif_decode_task(void *pvParameters) {
|
||||||
|
while (1) {
|
||||||
|
// 等待解码请求
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
if (!gif_playing || !gif_decoder) break;
|
||||||
|
|
||||||
|
// LZW 解码(含 dispose 处理)
|
||||||
|
int ret = gd_get_frame(gif_decoder);
|
||||||
|
if (ret == 0) {
|
||||||
|
// GIF 循环播放
|
||||||
|
gd_rewind(gif_decoder);
|
||||||
|
ret = gd_get_frame(gif_decoder);
|
||||||
|
}
|
||||||
|
if (ret < 0 || !gif_playing) break;
|
||||||
|
|
||||||
|
// 更新调色板 LUT(可能有局部调色板变化)
|
||||||
|
gif_build_palette_lut(gif_decoder->palette);
|
||||||
|
|
||||||
|
// 快速渲染到 canvas + 拷贝到后台 RGB565 缓冲
|
||||||
|
gif_render_frame_fast(gif_decoder);
|
||||||
|
uint8_t back = 1 - gif_front_idx;
|
||||||
|
gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[back]);
|
||||||
|
|
||||||
|
// 标记新帧就绪
|
||||||
|
gif_new_frame_ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
gif_decode_task_handle = NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LVGL 定时器回调(检查新帧并切换显示)
|
||||||
|
static void gif_play_timer_cb(lv_timer_t *t) {
|
||||||
|
if (!gif_playing || !gif_decoder || !gif_img_obj) return;
|
||||||
|
|
||||||
|
// 检查 GIF 帧延时
|
||||||
|
uint32_t delay_ms = gif_decoder->gce.delay * 10;
|
||||||
|
if (delay_ms < 20) delay_ms = 20;
|
||||||
|
|
||||||
|
uint32_t elapsed = lv_tick_elaps(gif_last_frame_ms);
|
||||||
|
if (elapsed < delay_ms) return;
|
||||||
|
|
||||||
|
if (!gif_new_frame_ready) return;
|
||||||
|
|
||||||
|
gif_last_frame_ms = lv_tick_get();
|
||||||
|
gif_new_frame_ready = false;
|
||||||
|
|
||||||
|
// 切换前后缓冲
|
||||||
|
uint8_t back = 1 - gif_front_idx;
|
||||||
|
gif_front_idx = back;
|
||||||
|
|
||||||
|
// 更新 LVGL 图片源(TRUE_COLOR,无 Alpha 混合)
|
||||||
|
gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[gif_front_idx];
|
||||||
|
lv_img_cache_invalidate_src(&gif_frame_dsc);
|
||||||
|
lv_obj_invalidate(gif_img_obj);
|
||||||
|
|
||||||
|
// 通知后台线程解码下一帧
|
||||||
|
if (gif_decode_task_handle) {
|
||||||
|
xTaskNotifyGive(gif_decode_task_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动自定义 GIF 播放器
|
||||||
|
static void gif_player_start(void) {
|
||||||
|
if (!gif_decoder || !gif_psram_buf) return;
|
||||||
|
|
||||||
|
uint16_t w = gif_decoder->width;
|
||||||
|
uint16_t h = gif_decoder->height;
|
||||||
|
size_t buf_size = w * h * sizeof(uint16_t);
|
||||||
|
|
||||||
|
// 分配双缓冲 RGB565 帧 (PSRAM)
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
if (!gif_rgb565_buf[i]) {
|
||||||
|
gif_rgb565_buf[i] = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
|
||||||
|
if (!gif_rgb565_buf[i]) {
|
||||||
|
ESP_LOGE("GIF", "RGB565 缓冲分配失败: %d", i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memset(gif_rgb565_buf[i], 0, buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化调色板 LUT
|
||||||
|
gif_build_palette_lut(gif_decoder->palette);
|
||||||
|
|
||||||
|
// 同步解码第一帧(确保立即显示)
|
||||||
|
int ret = gd_get_frame(gif_decoder);
|
||||||
|
if (ret <= 0) {
|
||||||
|
ESP_LOGE("GIF", "首帧解码失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gif_render_frame_fast(gif_decoder);
|
||||||
|
gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[0]);
|
||||||
|
gif_front_idx = 0;
|
||||||
|
gif_new_frame_ready = false;
|
||||||
|
gif_last_frame_ms = lv_tick_get();
|
||||||
|
|
||||||
|
// 配置 LVGL 图片描述符(TRUE_COLOR,无 Alpha)
|
||||||
|
gif_frame_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
|
||||||
|
gif_frame_dsc.header.always_zero = 0;
|
||||||
|
gif_frame_dsc.header.reserved = 0;
|
||||||
|
gif_frame_dsc.header.w = w;
|
||||||
|
gif_frame_dsc.header.h = h;
|
||||||
|
gif_frame_dsc.data_size = buf_size;
|
||||||
|
gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[0];
|
||||||
|
|
||||||
|
// 创建 lv_img 控件 + LVGL 播放定时器
|
||||||
|
lvgl_port_lock(0);
|
||||||
|
lv_obj_add_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
gif_img_obj = lv_img_create(ui_ScreenImg);
|
||||||
|
lv_obj_set_size(gif_img_obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_align(gif_img_obj, LV_ALIGN_CENTER);
|
||||||
|
lv_obj_clear_flag(gif_img_obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_move_background(gif_img_obj);
|
||||||
|
lv_img_set_src(gif_img_obj, &gif_frame_dsc);
|
||||||
|
gif_play_timer = lv_timer_create(gif_play_timer_cb, 10, NULL);
|
||||||
|
lvgl_port_unlock();
|
||||||
|
|
||||||
|
// 启动后台解码任务
|
||||||
|
gif_playing = true;
|
||||||
|
current_is_gif = true;
|
||||||
|
xTaskCreatePinnedToCore(gif_decode_task, "gif_dec", 4096, NULL, 5, &gif_decode_task_handle, 1);
|
||||||
|
|
||||||
|
// 触发后台解码第2帧
|
||||||
|
xTaskNotifyGive(gif_decode_task_handle);
|
||||||
|
|
||||||
|
ESP_LOGI("GIF", "播放器启动: %dx%d, 双缓冲 %dKB×2", w, h, buf_size / 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止自定义 GIF 播放器
|
||||||
|
static void gif_player_stop(void) {
|
||||||
|
// 停止后台解码任务
|
||||||
|
gif_playing = false;
|
||||||
|
if (gif_decode_task_handle) {
|
||||||
|
xTaskNotifyGive(gif_decode_task_handle);
|
||||||
|
// 等待任务退出(最多 500ms)
|
||||||
|
for (int i = 0; i < 50 && gif_decode_task_handle; i++) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 LVGL 定时器和控件
|
||||||
|
lvgl_port_lock(0);
|
||||||
|
if (gif_play_timer) {
|
||||||
|
lv_timer_del(gif_play_timer);
|
||||||
|
gif_play_timer = NULL;
|
||||||
|
}
|
||||||
|
if (gif_img_obj) {
|
||||||
|
lv_obj_del(gif_img_obj);
|
||||||
|
gif_img_obj = NULL;
|
||||||
|
}
|
||||||
|
lvgl_port_unlock();
|
||||||
|
|
||||||
|
// 关闭 gifdec 解码器
|
||||||
|
if (gif_decoder) {
|
||||||
|
gd_close_gif(gif_decoder);
|
||||||
|
gif_decoder = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放 RGB565 双缓冲
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
if (gif_rgb565_buf[i]) {
|
||||||
|
free(gif_rgb565_buf[i]);
|
||||||
|
gif_rgb565_buf[i] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_is_gif = false;
|
||||||
|
gif_new_frame_ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 GIF 资源(公开接口,供界面切换时调用)
|
||||||
|
void pages_cleanup_gif(void) {
|
||||||
|
gif_player_stop();
|
||||||
|
// 释放 GIF 源文件缓冲区
|
||||||
|
if (gif_psram_buf) {
|
||||||
|
free(gif_psram_buf);
|
||||||
|
gif_psram_buf = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // LV_USE_GIF
|
||||||
|
|
||||||
|
// 更新ui_ImgBle控件的图片(支持 JPEG)
|
||||||
void update_ui_ImgBle(const char *img_name) {
|
void update_ui_ImgBle(const char *img_name) {
|
||||||
if(!img_name) {
|
if(!img_name) {
|
||||||
ESP_LOGE("IMG_UI", "图片名为空");
|
ESP_LOGE("IMG_UI", "图片名为空");
|
||||||
@ -785,13 +1138,6 @@ void update_ui_ImgBle(const char *img_name) {
|
|||||||
static uint8_t *ui_img_data = NULL;
|
static uint8_t *ui_img_data = NULL;
|
||||||
static lv_img_dsc_t ui_image;
|
static lv_img_dsc_t ui_image;
|
||||||
|
|
||||||
// 释放之前的图片数据
|
|
||||||
if(ui_img_data) {
|
|
||||||
free(ui_img_data);
|
|
||||||
ui_img_data = NULL;
|
|
||||||
ESP_LOGI("IMG_UI", "释放之前的图片数据");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建图片路径
|
// 构建图片路径
|
||||||
snprintf(img_path, sizeof(img_path), "/spiflash/%s", img_name);
|
snprintf(img_path, sizeof(img_path), "/spiflash/%s", img_name);
|
||||||
ESP_LOGI("IMG_UI", "准备显示图片: %s, 路径: %s", img_name, img_path);
|
ESP_LOGI("IMG_UI", "准备显示图片: %s, 路径: %s", img_name, img_path);
|
||||||
@ -804,13 +1150,80 @@ void update_ui_ImgBle(const char *img_name) {
|
|||||||
}
|
}
|
||||||
ESP_LOGI("IMG_UI", "文件大小: %ld 字节", file_stat.st_size);
|
ESP_LOGI("IMG_UI", "文件大小: %ld 字节", file_stat.st_size);
|
||||||
|
|
||||||
|
#if LV_USE_GIF
|
||||||
|
if (is_gif_file(img_name)) {
|
||||||
|
// === GIF 显示路径(自定义播放器:Palette LUT + 无Alpha + 后台解码) ===
|
||||||
|
// 释放之前的 JPEG 数据
|
||||||
|
if(ui_img_data) {
|
||||||
|
free(ui_img_data);
|
||||||
|
ui_img_data = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止旧 GIF 播放器 + 释放旧 GIF 数据
|
||||||
|
gif_player_stop();
|
||||||
|
if (gif_psram_buf) {
|
||||||
|
free(gif_psram_buf);
|
||||||
|
gif_psram_buf = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 GIF 文件整体读入 PSRAM
|
||||||
|
FILE *gif_file = fopen(img_path, "rb");
|
||||||
|
if (!gif_file) {
|
||||||
|
ESP_LOGE("IMG_UI", "GIF文件打开失败: %s", img_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gif_psram_buf = heap_caps_malloc(file_stat.st_size, MALLOC_CAP_SPIRAM);
|
||||||
|
if (!gif_psram_buf) {
|
||||||
|
ESP_LOGE("IMG_UI", "PSRAM分配失败: %ld 字节", file_stat.st_size);
|
||||||
|
fclose(gif_file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fread(gif_psram_buf, 1, file_stat.st_size, gif_file);
|
||||||
|
fclose(gif_file);
|
||||||
|
|
||||||
|
// 打开 gifdec 解码器(从 PSRAM 内存源)
|
||||||
|
gif_decoder = gd_open_gif_data(gif_psram_buf);
|
||||||
|
if (!gif_decoder) {
|
||||||
|
ESP_LOGE("IMG_UI", "gifdec 打开失败: %s", img_name);
|
||||||
|
free(gif_psram_buf);
|
||||||
|
gif_psram_buf = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动自定义 GIF 播放器(Palette LUT + 双缓冲流水线)
|
||||||
|
gif_player_start();
|
||||||
|
|
||||||
|
ESP_LOGI("IMG_UI", "GIF显示启动(优化): %s", img_name);
|
||||||
|
} else
|
||||||
|
#endif // LV_USE_GIF
|
||||||
|
{
|
||||||
|
// === JPEG 显示路径 ===
|
||||||
|
#if LV_USE_GIF
|
||||||
|
// 停止 GIF 播放器并释放资源
|
||||||
|
gif_player_stop();
|
||||||
|
if (gif_psram_buf) {
|
||||||
|
free(gif_psram_buf);
|
||||||
|
gif_psram_buf = NULL;
|
||||||
|
}
|
||||||
|
// 恢复 JPEG 控件显示
|
||||||
|
lvgl_port_lock(0);
|
||||||
|
lv_obj_clear_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lvgl_port_unlock();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 释放之前的图片数据
|
||||||
|
if(ui_img_data) {
|
||||||
|
free(ui_img_data);
|
||||||
|
ui_img_data = NULL;
|
||||||
|
ESP_LOGI("IMG_UI", "释放之前的图片数据");
|
||||||
|
}
|
||||||
|
|
||||||
// 解码图片
|
// 解码图片
|
||||||
esp_jpeg_image_output_t ui_outdata;
|
esp_jpeg_image_output_t ui_outdata;
|
||||||
esp_err_t ret = DecodeImg(img_path, &ui_img_data, &ui_outdata);
|
esp_err_t ret = DecodeImg(img_path, &ui_img_data, &ui_outdata);
|
||||||
if(ret == ESP_OK) {
|
if(ret == ESP_OK) {
|
||||||
ESP_LOGI("IMG_UI", "图片解码成功,宽度: %d, 高度: %d", ui_outdata.width, ui_outdata.height);
|
ESP_LOGI("IMG_UI", "图片解码成功,宽度: %d, 高度: %d", ui_outdata.width, ui_outdata.height);
|
||||||
|
|
||||||
// 检查解码后的数据
|
|
||||||
if(ui_img_data == NULL) {
|
if(ui_img_data == NULL) {
|
||||||
ESP_LOGE("IMG_UI", "解码数据为空");
|
ESP_LOGE("IMG_UI", "解码数据为空");
|
||||||
return;
|
return;
|
||||||
@ -825,16 +1238,16 @@ void update_ui_ImgBle(const char *img_name) {
|
|||||||
ui_image.data_size = ui_outdata.output_len;
|
ui_image.data_size = ui_outdata.output_len;
|
||||||
ui_image.data = ui_img_data;
|
ui_image.data = ui_img_data;
|
||||||
|
|
||||||
// 更新ui_ImgBle控件的图片
|
|
||||||
lvgl_port_lock(0);
|
lvgl_port_lock(0);
|
||||||
lv_img_set_src(ui_ImgBle, &ui_image);
|
lv_img_set_src(ui_ImgBle, &ui_image);
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
|
|
||||||
ESP_LOGI("IMG_UI", "ui_ImgBle图片更新成功: %s", img_name);
|
ESP_LOGI("IMG_UI", "JPEG图片更新成功: %s", img_name);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE("IMG_UI", "图片解码失败,错误码: %d", ret);
|
ESP_LOGE("IMG_UI", "图片解码失败,错误码: %d", ret);
|
||||||
ui_img_data = NULL;
|
ui_img_data = NULL;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -104,13 +104,19 @@ if ( event_code == LV_EVENT_SCREEN_LOADED ) {
|
|||||||
|
|
||||||
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_TOP ) {
|
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_TOP ) {
|
||||||
lv_indev_wait_release(lv_indev_get_act());
|
lv_indev_wait_release(lv_indev_get_act());
|
||||||
// 离开界面前隐藏容器
|
// 离开界面前清理资源和隐藏容器
|
||||||
|
#if LV_USE_GIF
|
||||||
|
pages_cleanup_gif();
|
||||||
|
#endif
|
||||||
ui_ScreenImg_hide_delete_container();
|
ui_ScreenImg_hide_delete_container();
|
||||||
_ui_screen_change( &ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
|
_ui_screen_change( &ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
|
||||||
}
|
}
|
||||||
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM ) {
|
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM ) {
|
||||||
lv_indev_wait_release(lv_indev_get_act());
|
lv_indev_wait_release(lv_indev_get_act());
|
||||||
// 离开界面前隐藏容器
|
// 离开界面前清理资源和隐藏容器
|
||||||
|
#if LV_USE_GIF
|
||||||
|
pages_cleanup_gif();
|
||||||
|
#endif
|
||||||
ui_ScreenImg_hide_delete_container();
|
ui_ScreenImg_hide_delete_container();
|
||||||
// 设置返回到Img界面
|
// 设置返回到Img界面
|
||||||
ui_ScreenSet_set_previous(&ui_ScreenImg, &ui_ScreenImg_screen_init);
|
ui_ScreenSet_set_previous(&ui_ScreenImg, &ui_ScreenImg_screen_init);
|
||||||
@ -196,6 +202,9 @@ lv_obj_add_event_cb(ui_ScreenImg, ui_event_ScreenImg, LV_EVENT_ALL, NULL);
|
|||||||
|
|
||||||
void ui_ScreenImg_screen_destroy(void)
|
void ui_ScreenImg_screen_destroy(void)
|
||||||
{
|
{
|
||||||
|
#if LV_USE_GIF
|
||||||
|
pages_cleanup_gif();
|
||||||
|
#endif
|
||||||
if (ui_ScreenImg) lv_obj_del(ui_ScreenImg);
|
if (ui_ScreenImg) lv_obj_del(ui_ScreenImg);
|
||||||
|
|
||||||
// NULL screen variables
|
// NULL screen variables
|
||||||
|
|||||||
44
sdkconfig
44
sdkconfig
@ -14,6 +14,7 @@ CONFIG_SOC_GDMA_SUPPORTED=y
|
|||||||
CONFIG_SOC_AHB_GDMA_SUPPORTED=y
|
CONFIG_SOC_AHB_GDMA_SUPPORTED=y
|
||||||
CONFIG_SOC_GPTIMER_SUPPORTED=y
|
CONFIG_SOC_GPTIMER_SUPPORTED=y
|
||||||
CONFIG_SOC_LCDCAM_SUPPORTED=y
|
CONFIG_SOC_LCDCAM_SUPPORTED=y
|
||||||
|
CONFIG_SOC_LCDCAM_CAM_SUPPORTED=y
|
||||||
CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED=y
|
CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED=y
|
||||||
CONFIG_SOC_LCDCAM_RGB_LCD_SUPPORTED=y
|
CONFIG_SOC_LCDCAM_RGB_LCD_SUPPORTED=y
|
||||||
CONFIG_SOC_MCPWM_SUPPORTED=y
|
CONFIG_SOC_MCPWM_SUPPORTED=y
|
||||||
@ -101,7 +102,7 @@ CONFIG_SOC_CPU_HAS_FPU=y
|
|||||||
CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y
|
CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y
|
||||||
CONFIG_SOC_CPU_BREAKPOINTS_NUM=2
|
CONFIG_SOC_CPU_BREAKPOINTS_NUM=2
|
||||||
CONFIG_SOC_CPU_WATCHPOINTS_NUM=2
|
CONFIG_SOC_CPU_WATCHPOINTS_NUM=2
|
||||||
CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=64
|
CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=0x40
|
||||||
CONFIG_SOC_SIMD_PREFERRED_DATA_ALIGNMENT=16
|
CONFIG_SOC_SIMD_PREFERRED_DATA_ALIGNMENT=16
|
||||||
CONFIG_SOC_DS_SIGNATURE_MAX_BIT_LEN=4096
|
CONFIG_SOC_DS_SIGNATURE_MAX_BIT_LEN=4096
|
||||||
CONFIG_SOC_DS_KEY_PARAM_MD_IV_LENGTH=16
|
CONFIG_SOC_DS_KEY_PARAM_MD_IV_LENGTH=16
|
||||||
@ -208,7 +209,7 @@ CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y
|
|||||||
CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y
|
CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y
|
||||||
CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y
|
CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y
|
||||||
CONFIG_SOC_LP_IO_CLOCK_IS_INDEPENDENT=y
|
CONFIG_SOC_LP_IO_CLOCK_IS_INDEPENDENT=y
|
||||||
CONFIG_SOC_SDM_GROUPS=y
|
CONFIG_SOC_SDM_GROUPS=1
|
||||||
CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8
|
CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8
|
||||||
CONFIG_SOC_SDM_CLK_SUPPORT_APB=y
|
CONFIG_SOC_SDM_CLK_SUPPORT_APB=y
|
||||||
CONFIG_SOC_SPI_PERIPH_NUM=3
|
CONFIG_SOC_SPI_PERIPH_NUM=3
|
||||||
@ -369,6 +370,9 @@ CONFIG_SOC_BLE_DEVICE_PRIVACY_SUPPORTED=y
|
|||||||
CONFIG_SOC_BLUFI_SUPPORTED=y
|
CONFIG_SOC_BLUFI_SUPPORTED=y
|
||||||
CONFIG_SOC_ULP_HAS_ADC=y
|
CONFIG_SOC_ULP_HAS_ADC=y
|
||||||
CONFIG_SOC_PHY_COMBO_MODULE=y
|
CONFIG_SOC_PHY_COMBO_MODULE=y
|
||||||
|
CONFIG_SOC_LCDCAM_CAM_SUPPORT_RGB_YUV_CONV=y
|
||||||
|
CONFIG_SOC_LCDCAM_CAM_PERIPH_NUM=1
|
||||||
|
CONFIG_SOC_LCDCAM_CAM_DATA_WIDTH_MAX=16
|
||||||
CONFIG_IDF_CMAKE=y
|
CONFIG_IDF_CMAKE=y
|
||||||
CONFIG_IDF_TOOLCHAIN="gcc"
|
CONFIG_IDF_TOOLCHAIN="gcc"
|
||||||
CONFIG_IDF_TOOLCHAIN_GCC=y
|
CONFIG_IDF_TOOLCHAIN_GCC=y
|
||||||
@ -836,6 +840,7 @@ CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
|
|||||||
CONFIG_BT_BLE_42_DTM_TEST_EN=y
|
CONFIG_BT_BLE_42_DTM_TEST_EN=y
|
||||||
CONFIG_BT_BLE_42_ADV_EN=y
|
CONFIG_BT_BLE_42_ADV_EN=y
|
||||||
CONFIG_BT_BLE_42_SCAN_EN=y
|
CONFIG_BT_BLE_42_SCAN_EN=y
|
||||||
|
CONFIG_BT_BLE_VENDOR_HCI_EN=y
|
||||||
# CONFIG_BT_BLE_HIGH_DUTY_ADV_INTERVAL is not set
|
# CONFIG_BT_BLE_HIGH_DUTY_ADV_INTERVAL is not set
|
||||||
# CONFIG_BT_ABORT_WHEN_ALLOCATION_FAILS is not set
|
# CONFIG_BT_ABORT_WHEN_ALLOCATION_FAILS is not set
|
||||||
# end of Bluedroid Options
|
# end of Bluedroid Options
|
||||||
@ -1057,6 +1062,7 @@ CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y
|
|||||||
# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set
|
# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set
|
||||||
# CONFIG_ESP_TLS_PSK_VERIFICATION is not set
|
# CONFIG_ESP_TLS_PSK_VERIFICATION is not set
|
||||||
# CONFIG_ESP_TLS_INSECURE is not set
|
# CONFIG_ESP_TLS_INSECURE is not set
|
||||||
|
CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y
|
||||||
# end of ESP-TLS
|
# end of ESP-TLS
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1083,6 +1089,12 @@ CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
|
|||||||
CONFIG_ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
|
CONFIG_ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
|
||||||
# end of Common ESP-related
|
# end of Common ESP-related
|
||||||
|
|
||||||
|
#
|
||||||
|
# ESP-Driver:Camera Controller Configurations
|
||||||
|
#
|
||||||
|
# CONFIG_CAM_CTLR_DVP_CAM_ISR_CACHE_SAFE is not set
|
||||||
|
# end of ESP-Driver:Camera Controller Configurations
|
||||||
|
|
||||||
#
|
#
|
||||||
# ESP-Driver:GPIO Configurations
|
# ESP-Driver:GPIO Configurations
|
||||||
#
|
#
|
||||||
@ -1403,8 +1415,11 @@ CONFIG_ESP_PHY_RF_CAL_PARTIAL=y
|
|||||||
# CONFIG_ESP_PHY_RF_CAL_NONE is not set
|
# CONFIG_ESP_PHY_RF_CAL_NONE is not set
|
||||||
# CONFIG_ESP_PHY_RF_CAL_FULL is not set
|
# CONFIG_ESP_PHY_RF_CAL_FULL is not set
|
||||||
CONFIG_ESP_PHY_CALIBRATION_MODE=0
|
CONFIG_ESP_PHY_CALIBRATION_MODE=0
|
||||||
|
CONFIG_ESP_PHY_PLL_TRACK_PERIOD_MS=1000
|
||||||
# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set
|
# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set
|
||||||
# CONFIG_ESP_PHY_RECORD_USED_TIME is not set
|
# CONFIG_ESP_PHY_RECORD_USED_TIME is not set
|
||||||
|
CONFIG_ESP_PHY_IRAM_OPT=y
|
||||||
|
# CONFIG_ESP_PHY_DEBUG is not set
|
||||||
# end of PHY
|
# end of PHY
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1473,9 +1488,9 @@ CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY=y
|
|||||||
# ESP System Settings
|
# ESP System Settings
|
||||||
#
|
#
|
||||||
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set
|
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set
|
||||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
|
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set
|
||||||
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 is not set
|
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
|
||||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160
|
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cache config
|
# Cache config
|
||||||
@ -2084,6 +2099,7 @@ CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
|
|||||||
# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set
|
# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set
|
||||||
# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set
|
# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set
|
||||||
CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y
|
CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y
|
||||||
|
# CONFIG_MBEDTLS_SSL_KEYING_MATERIAL_EXPORT is not set
|
||||||
CONFIG_MBEDTLS_PKCS7_C=y
|
CONFIG_MBEDTLS_PKCS7_C=y
|
||||||
# end of mbedTLS v3.x related
|
# end of mbedTLS v3.x related
|
||||||
|
|
||||||
@ -2564,9 +2580,8 @@ CONFIG_LV_COLOR_CHROMA_KEY_HEX=0x00FF00
|
|||||||
#
|
#
|
||||||
# Memory settings
|
# Memory settings
|
||||||
#
|
#
|
||||||
# CONFIG_LV_MEM_CUSTOM is not set
|
CONFIG_LV_MEM_CUSTOM=y
|
||||||
CONFIG_LV_MEM_SIZE_KILOBYTES=64
|
CONFIG_LV_MEM_CUSTOM_INCLUDE="stdlib.h"
|
||||||
CONFIG_LV_MEM_ADDR=0x0
|
|
||||||
CONFIG_LV_MEM_BUF_MAX_NUM=16
|
CONFIG_LV_MEM_BUF_MAX_NUM=16
|
||||||
# CONFIG_LV_MEMCPY_MEMSET_STD is not set
|
# CONFIG_LV_MEMCPY_MEMSET_STD is not set
|
||||||
# end of Memory settings
|
# end of Memory settings
|
||||||
@ -2631,7 +2646,6 @@ CONFIG_LV_ASSERT_HANDLER_INCLUDE="assert.h"
|
|||||||
# Others
|
# Others
|
||||||
#
|
#
|
||||||
# CONFIG_LV_USE_PERF_MONITOR is not set
|
# CONFIG_LV_USE_PERF_MONITOR is not set
|
||||||
# CONFIG_LV_USE_MEM_MONITOR is not set
|
|
||||||
# CONFIG_LV_USE_REFR_DEBUG is not set
|
# CONFIG_LV_USE_REFR_DEBUG is not set
|
||||||
# CONFIG_LV_SPRINTF_CUSTOM is not set
|
# CONFIG_LV_SPRINTF_CUSTOM is not set
|
||||||
# CONFIG_LV_SPRINTF_USE_FLOAT is not set
|
# CONFIG_LV_SPRINTF_USE_FLOAT is not set
|
||||||
@ -2802,7 +2816,10 @@ CONFIG_LV_USE_GRID=y
|
|||||||
# 3rd Party Libraries
|
# 3rd Party Libraries
|
||||||
#
|
#
|
||||||
# CONFIG_LV_USE_FS_STDIO is not set
|
# CONFIG_LV_USE_FS_STDIO is not set
|
||||||
# CONFIG_LV_USE_FS_POSIX is not set
|
CONFIG_LV_USE_FS_POSIX=y
|
||||||
|
CONFIG_LV_FS_POSIX_LETTER=83
|
||||||
|
CONFIG_LV_FS_POSIX_PATH="/spiflash/"
|
||||||
|
CONFIG_LV_FS_POSIX_CACHE_SIZE=0
|
||||||
# CONFIG_LV_USE_FS_WIN32 is not set
|
# CONFIG_LV_USE_FS_WIN32 is not set
|
||||||
# CONFIG_LV_USE_FS_FATFS is not set
|
# CONFIG_LV_USE_FS_FATFS is not set
|
||||||
# CONFIG_LV_USE_FS_LITTLEFS is not set
|
# CONFIG_LV_USE_FS_LITTLEFS is not set
|
||||||
@ -3067,6 +3084,7 @@ CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS=y
|
|||||||
CONFIG_SW_COEXIST_ENABLE=y
|
CONFIG_SW_COEXIST_ENABLE=y
|
||||||
CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y
|
CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y
|
||||||
CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y
|
CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y
|
||||||
|
# CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set
|
||||||
# CONFIG_MCPWM_ISR_IN_IRAM is not set
|
# CONFIG_MCPWM_ISR_IN_IRAM is not set
|
||||||
# CONFIG_EVENT_LOOP_PROFILING is not set
|
# CONFIG_EVENT_LOOP_PROFILING is not set
|
||||||
CONFIG_POST_EVENTS_FROM_ISR=y
|
CONFIG_POST_EVENTS_FROM_ISR=y
|
||||||
@ -3095,9 +3113,9 @@ CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
|||||||
CONFIG_DEFAULT_PSRAM_CLK_IO=30
|
CONFIG_DEFAULT_PSRAM_CLK_IO=30
|
||||||
CONFIG_DEFAULT_PSRAM_CS_IO=26
|
CONFIG_DEFAULT_PSRAM_CS_IO=26
|
||||||
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set
|
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set
|
||||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160=y
|
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set
|
||||||
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set
|
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=160
|
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240
|
||||||
CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32
|
CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32
|
||||||
CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304
|
CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304
|
||||||
CONFIG_MAIN_TASK_STACK_SIZE=3584
|
CONFIG_MAIN_TASK_STACK_SIZE=3584
|
||||||
|
|||||||
266
设备运行日志.md
266
设备运行日志.md
@ -0,0 +1,266 @@
|
|||||||
|
一、GIF卡顿原因深度分析
|
||||||
|
1. 核心瓶颈定位
|
||||||
|
瓶颈点
|
||||||
|
|
||||||
|
具体问题
|
||||||
|
|
||||||
|
对360×360屏幕的影响
|
||||||
|
|
||||||
|
|
||||||
|
CPU开销
|
||||||
|
|
||||||
|
图片中列出LZW+调色板+缩放+RGBA→565转换,每帧需多次转换
|
||||||
|
|
||||||
|
每帧约5-10ms计算时间,15fps需6.7ms/帧,已接近极限
|
||||||
|
|
||||||
|
|
||||||
|
循环重播
|
||||||
|
|
||||||
|
dispose+rewind+重解码首帧,每次循环额外开销
|
||||||
|
|
||||||
|
每次循环增加10-20ms延迟,导致循环衔接卡顿
|
||||||
|
|
||||||
|
|
||||||
|
闪烁问题
|
||||||
|
|
||||||
|
Canvas重置时的可见空白帧
|
||||||
|
|
||||||
|
用户感知明显的"闪一下"
|
||||||
|
|
||||||
|
|
||||||
|
BLE延迟
|
||||||
|
|
||||||
|
SPIFFS写→SPIFFS读→解码,I/O操作过多
|
||||||
|
|
||||||
|
传输到显示延迟达200-500ms
|
||||||
|
|
||||||
|
|
||||||
|
内存占用
|
||||||
|
|
||||||
|
Canvas约504KB + 解码缓冲区 + 文件缓冲
|
||||||
|
|
||||||
|
总内存需求约1.5-2MB,挤占其他功能
|
||||||
|
2. 您当前配置的极限分析
|
||||||
|
ESP32-S3-WROOM-1-N16R8(8MB PSRAM)的资源分配:
|
||||||
|
火山RTC SDK:2-2.5MB(音频处理+网络缓冲)
|
||||||
|
LVGL基础:1-1.5MB(框架+字体+UI缓存)
|
||||||
|
GIF显示:
|
||||||
|
Canvas缓冲区:504KB
|
||||||
|
解码工作区:300-500KB
|
||||||
|
文件缓冲:100-200KB
|
||||||
|
小计:约1-1.2MB
|
||||||
|
系统基础:1-1.5MB
|
||||||
|
合计:5.2-6.7MB → 剩余仅1.3-2.8MB缓冲
|
||||||
|
关键发现:剩余PSRAM不足,导致:
|
||||||
|
频繁GC:内存碎片整理导致卡顿
|
||||||
|
无预解码空间:无法缓存下一帧
|
||||||
|
I/O阻塞:频繁读写SPIFFS
|
||||||
|
二、市面上电子吧唧配置对比
|
||||||
|
1. 主流产品硬件方案
|
||||||
|
产品/方案
|
||||||
|
|
||||||
|
主控配置
|
||||||
|
|
||||||
|
内存规格
|
||||||
|
|
||||||
|
显示方案
|
||||||
|
|
||||||
|
GIF优化技术
|
||||||
|
|
||||||
|
|
||||||
|
EchoEar喵伴
|
||||||
|
|
||||||
|
ESP32-S3-WROOM-1-N16R16V
|
||||||
|
|
||||||
|
16MB PSRAM
|
||||||
|
|
||||||
|
360×360 QSPI
|
||||||
|
|
||||||
|
硬件缩放+直接帧传输
|
||||||
|
|
||||||
|
|
||||||
|
四博智联方案
|
||||||
|
|
||||||
|
ESP32-S3-WROOM-1-N16R16VA
|
||||||
|
|
||||||
|
16MB PSRAM
|
||||||
|
|
||||||
|
240×240 SPI
|
||||||
|
|
||||||
|
预解码+多级缓存
|
||||||
|
|
||||||
|
|
||||||
|
利尔达AI豆
|
||||||
|
|
||||||
|
ESP32-S3-WROOM-2-N32R16V
|
||||||
|
|
||||||
|
16MB PSRAM+硬件加速
|
||||||
|
|
||||||
|
320×320 RGB接口
|
||||||
|
|
||||||
|
专用DMA通道
|
||||||
|
|
||||||
|
|
||||||
|
NN家族徽章
|
||||||
|
|
||||||
|
定制芯片+外置DDR
|
||||||
|
|
||||||
|
32-64MB
|
||||||
|
|
||||||
|
电子墨水屏
|
||||||
|
|
||||||
|
云端预渲染+本地流式更新
|
||||||
|
2. 与您配置的关键差异
|
||||||
|
您的配置 vs 市场主流配置
|
||||||
|
┌─────────────────┬─────────────────────────┬─────────────────────────┐
|
||||||
|
│ 维度 │ 您的配置 (N16R8) │ 市场主流 (N16R16V+) │
|
||||||
|
├─────────────────┼─────────────────────────┼─────────────────────────┤
|
||||||
|
│ PSRAM │ 8MB (紧张) │ 16MB (充裕) │
|
||||||
|
│ GIF处理策略 │ 实时解码+文件中转 │ 预解码+内存直出 │
|
||||||
|
│ 显示接口 │ 可能为SPI │ 多为QSPI/RGB并行 │
|
||||||
|
│ 传输延迟 │ 200-500ms (SPIFFS中转) │ 50-100ms (内存直传) │
|
||||||
|
│ 可缓存帧数 │ 0-1帧 (无预缓冲) │ 3-5帧 (流畅预缓冲) │
|
||||||
|
│ 循环优化 │ 每次重解码 │ 首帧缓存+增量更新 │
|
||||||
|
└─────────────────┴─────────────────────────┴─────────────────────────┘
|
||||||
|
三、他们为何不卡顿?关键技术优化
|
||||||
|
1. 内存优化策略
|
||||||
|
// 市场方案典型的内存分配(16MB PSRAM)
|
||||||
|
#define GIF_POOL_SIZE (3 * 1024 * 1024) // 3MB GIF专用池
|
||||||
|
#define FRAME_BUFFER_SIZE (2 * 1024 * 1024) // 2MB双缓冲
|
||||||
|
#define DECODE_BUFFER_SIZE (1 * 1024 * 1024) // 1MB解码工作区
|
||||||
|
|
||||||
|
// 您的配置对比(8MB PSRAM)
|
||||||
|
// 难以分配专用池,需与其他功能共享
|
||||||
|
2. 显示流水线优化
|
||||||
|
市场方案的四级流水线:
|
||||||
|
接收级:BLE直接写入PSRAM,绕过文件系统
|
||||||
|
预解码级:后台线程预解码3-5帧
|
||||||
|
显示级:专用DMA从PSRAM直传屏幕
|
||||||
|
清理级:显示完成后异步释放
|
||||||
|
您的现状:单线程串行处理,每步都阻塞。
|
||||||
|
3. 解码算法优化
|
||||||
|
市场方案:使用NEON指令集加速LZW,硬件RGBA→565转换
|
||||||
|
您的现状:软件解码,CPU占用高
|
||||||
|
四、具体优化建议(基于8MB PSRAM)
|
||||||
|
1. 内存管理重构
|
||||||
|
// 创建专用内存池,避免碎片
|
||||||
|
static uint8_t gif_pool[2 * 1024 * 1024] __attribute__((section(".spiram")));
|
||||||
|
|
||||||
|
// 替换malloc/free
|
||||||
|
void* gif_malloc(size_t size) {
|
||||||
|
static size_t offset = 0;
|
||||||
|
if(offset + size > sizeof(gif_pool)) return NULL;
|
||||||
|
void* ptr = &gif_pool[offset];
|
||||||
|
offset += size;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
2. 绕过文件系统中转
|
||||||
|
// 原流程:BLE→SPIFFS→读文件→解码→显示
|
||||||
|
// 新流程:BLE→PSRAM→解码→显示
|
||||||
|
|
||||||
|
// BLE接收直接写入PSRAM缓冲区
|
||||||
|
void ble_on_receive(uint8_t* data, size_t len) {
|
||||||
|
static uint8_t* gif_buffer = heap_caps_malloc(MAX_GIF_SIZE, MALLOC_CAP_SPIRAM);
|
||||||
|
memcpy(gif_buffer + offset, data, len);
|
||||||
|
offset += len;
|
||||||
|
|
||||||
|
if(offset == total_size) {
|
||||||
|
// 直接解码PSRAM中的GIF
|
||||||
|
decode_gif_from_ram(gif_buffer, total_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3. 降低GIF规格
|
||||||
|
必须妥协的参数:
|
||||||
|
分辨率:240×240(在360×360屏幕上居中显示)
|
||||||
|
帧率:10-12fps(人眼可接受下限)
|
||||||
|
颜色:64色(从256色降低)
|
||||||
|
循环:单次播放,避免重解码开销
|
||||||
|
效果:内存需求从1.2MB降至约400KB。
|
||||||
|
4. 显示技术优化
|
||||||
|
// 使用LVGL的部分刷新
|
||||||
|
lv_disp_set_draw_buffers(disp, buf1, buf2, 360 * 120, LV_DISP_RENDER_MODE_PARTIAL);
|
||||||
|
|
||||||
|
// 启用脏区域标记
|
||||||
|
lv_obj_add_flag(gif_obj, LV_OBJ_FLAG_DIRTY_UPDATE);
|
||||||
|
五、终极解决方案:模组升级
|
||||||
|
升级到N16R16V(16MB PSRAM)的收益
|
||||||
|
性能指标
|
||||||
|
|
||||||
|
升级前
|
||||||
|
|
||||||
|
升级后
|
||||||
|
|
||||||
|
改善幅度
|
||||||
|
|
||||||
|
|
||||||
|
可用PSRAM
|
||||||
|
|
||||||
|
1.3-2.8MB
|
||||||
|
|
||||||
|
8-10MB
|
||||||
|
|
||||||
|
+300%
|
||||||
|
|
||||||
|
|
||||||
|
GIF缓存帧
|
||||||
|
|
||||||
|
0-1帧
|
||||||
|
|
||||||
|
3-5帧
|
||||||
|
|
||||||
|
流畅循环
|
||||||
|
|
||||||
|
|
||||||
|
传输延迟
|
||||||
|
|
||||||
|
200-500ms
|
||||||
|
|
||||||
|
50-100ms
|
||||||
|
|
||||||
|
-80%
|
||||||
|
|
||||||
|
|
||||||
|
解码并发
|
||||||
|
|
||||||
|
串行
|
||||||
|
|
||||||
|
流水线并行
|
||||||
|
|
||||||
|
帧率提升2-3倍
|
||||||
|
|
||||||
|
|
||||||
|
系统稳定性
|
||||||
|
|
||||||
|
频繁GC卡顿
|
||||||
|
|
||||||
|
稳定运行
|
||||||
|
|
||||||
|
用户体验质变
|
||||||
|
成本分析:
|
||||||
|
N16R8模组:约25-30元
|
||||||
|
N16R16V模组:约30-35元
|
||||||
|
增量成本:5元左右,但体验提升显著
|
||||||
|
六、实施路线图
|
||||||
|
短期(1-2周):软件优化
|
||||||
|
实现BLE直存PSRAM,绕过SPIFFS
|
||||||
|
降低GIF规格(240×240,10fps,64色)
|
||||||
|
使用LVGL部分刷新减少绘制区域
|
||||||
|
中期(2-4周):架构优化
|
||||||
|
实现双缓冲显示流水线
|
||||||
|
添加后台预解码线程
|
||||||
|
优化内存管理,减少碎片
|
||||||
|
长期(如有必要):硬件升级
|
||||||
|
更换为ESP32-S3-WROOM-1-N16R16V
|
||||||
|
重新设计PCB优化布局
|
||||||
|
考虑QSPI屏幕接口提升传输速度
|
||||||
|
总结
|
||||||
|
卡顿的根本原因是8MB PSRAM在多任务(RTC+LVGL+BLE+GIF)下资源不足,导致必须通过文件系统中转、无预解码缓冲、频繁内存回收。
|
||||||
|
市场主流产品通过以下方式避免卡顿:
|
||||||
|
硬件升级:16MB+ PSRAM提供充足缓冲
|
||||||
|
架构优化:内存直传+预解码流水线
|
||||||
|
显示优化:QSPI/RGB接口+硬件加速
|
||||||
|
对您的建议:
|
||||||
|
立即实施:软件优化方案,特别是绕过SPIFFS
|
||||||
|
评估升级:如果优化后仍不满足需求,强烈建议升级到N16R16V
|
||||||
|
平衡妥协:适当降低GIF规格,在现有硬件上获得最佳体验
|
||||||
|
5元左右的模组升级成本,可换来用户体验的质变,在产品化阶段是值得的投资。
|
||||||
Loading…
x
Reference in New Issue
Block a user