一、BLE 蓝牙优化 - 设备名称改为动态名称 Airhub_MAC(基于BLE MAC地址) - 广播数据拆分为 ADV + Scan Response 两包 - 图片接收完成后数据直通显示(跳过SPIFFS重读,减少200-500ms延迟) - BLE耗时操作(NVS写入+导航显示)转移到独立FreeRTOS任务,避免BTC_TASK栈溢出 - 缩短BLE连接间隔(min=7.5ms, max=20ms),提升传输吞吐量 - 减少传输日志输出(每100包打印一次),提升传输速度 二、显示性能优化 - LVGL绘制缓冲区从DMA 30行改为PSRAM 120行大缓冲,减少flush次数 - CPU最大频率从160MHz提升到240MHz,提升解码性能 三、GIF动图支持(条件编译,当前默认关闭) - 实现自定义GIF播放器:Palette LUT查表 + TRUE_COLOR无Alpha + 后台线程解码流水线 - 使用 #if LV_USE_GIF 条件编译包裹所有GIF代码,sdkconfig中CONFIG_LV_USE_GIF=n时零开销 - 启用GIF时需设置 CONFIG_LV_USE_GIF=y 即可 四、图片管理优化 - BLE接收新图片后直接追加到列表(避免重扫SPIFFS目录) - SPIFFS图片扫描支持.gif扩展名(条件编译控制) 五、文档更新 - 设备运行日志:GIF性能瓶颈分析与优化方案 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
401 lines
16 KiB
C
401 lines
16 KiB
C
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/task.h"
|
||
#include "esp_log.h"
|
||
#include "esp_bt.h"
|
||
#include "esp_gap_ble_api.h"
|
||
#include "esp_gatts_api.h"
|
||
#include "esp_bt_main.h"
|
||
#include "esp_bt_device.h"
|
||
#include "esp_gatt_common_api.h"
|
||
#include "esp_mac.h"
|
||
#include "fatfs.h"
|
||
#include "pages.h"
|
||
|
||
|
||
#define APP_ID_PLACEHOLDER 0
|
||
|
||
#define IMAGE_SERVICE_INSTID 0x0B
|
||
#define IMAGE_SERVICE_UUID 0x0B00
|
||
#define IMAGE_WRITE_UUID 0x0B01
|
||
#define IMAGE_EDIT_UUID 0x0B02
|
||
|
||
static uint16_t image_service_handle = 0;
|
||
static uint16_t image_write_handle = 0;
|
||
static uint16_t image_edit_handle = 0;
|
||
|
||
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
|
||
|
||
|
||
static const char *CONN_TAG = "CONN_BLE";
|
||
static char ble_device_name[32];
|
||
static uint8_t adv_raw_len = 0;
|
||
|
||
static uint16_t conn_id;
|
||
|
||
static char *filepath;
|
||
|
||
typedef struct
|
||
{
|
||
uint8_t type;
|
||
char filename[23];
|
||
uint32_t len;
|
||
} Megtype;
|
||
|
||
typedef struct{
|
||
bool isSend;
|
||
uint32_t port;
|
||
} MegStatus;
|
||
|
||
Megtype firstMeg;
|
||
MegStatus SendStatus = {false,0};
|
||
|
||
uint8_t *img_data = 0;
|
||
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_edit[20] = {0};
|
||
|
||
static esp_attr_value_t char_val_image_write = {
|
||
.attr_max_len = 512,
|
||
.attr_len = 512,
|
||
.attr_value = attr_value_write
|
||
} ;
|
||
static esp_attr_value_t char_val_image_edit = {
|
||
.attr_max_len = 20,
|
||
.attr_len = 20,
|
||
.attr_value = attr_value_edit
|
||
} ;
|
||
|
||
static esp_attr_control_t control_image_write = {
|
||
.auto_rsp = ESP_GATT_AUTO_RSP
|
||
};
|
||
static esp_attr_control_t control_image_edit = {
|
||
.auto_rsp = ESP_GATT_AUTO_RSP
|
||
};
|
||
|
||
// 图片传输服务
|
||
static esp_gatt_srvc_id_t server_id_image = {
|
||
.id.uuid.len = ESP_UUID_LEN_16,
|
||
.id.uuid.uuid.uuid16 = IMAGE_SERVICE_UUID,
|
||
.id.inst_id = IMAGE_SERVICE_INSTID,
|
||
.is_primary = true,
|
||
};
|
||
static esp_bt_uuid_t image_write_uuid = {
|
||
.len = ESP_UUID_LEN_16,
|
||
.uuid.uuid16 = IMAGE_WRITE_UUID,
|
||
};
|
||
static esp_bt_uuid_t image_edit_uuid = {
|
||
.len = ESP_UUID_LEN_16,
|
||
.uuid.uuid16 = IMAGE_EDIT_UUID,
|
||
};
|
||
|
||
|
||
static esp_ble_adv_params_t adv_params = {
|
||
.adv_int_min = 0x20,
|
||
.adv_int_max = 0x20,
|
||
.adv_type = ADV_TYPE_IND,
|
||
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
||
.channel_map = ADV_CHNL_ALL,
|
||
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
|
||
};
|
||
|
||
|
||
static uint8_t adv_raw_data[31];
|
||
|
||
// Scan Response 数据:厂商标识 + 服务UUID
|
||
static uint8_t scan_rsp_data[] = {
|
||
0x07, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x4C, 0x44, 0x64, 0x7A, 0x62, 0x6A, // "LDdzbj"
|
||
0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0x00, 0x0B, // 服务UUID 0x0B00
|
||
};
|
||
|
||
|
||
void ble_init(void)
|
||
{
|
||
esp_err_t ret;
|
||
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
|
||
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
||
ret = esp_bt_controller_init(&bt_cfg);
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
|
||
return;
|
||
}
|
||
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
|
||
return;
|
||
}
|
||
ret = esp_bluedroid_init();
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
|
||
return;
|
||
}
|
||
ret = esp_bluedroid_enable();
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
|
||
return;
|
||
}
|
||
ret = esp_ble_gap_register_callback(esp_gap_cb);
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "%s gap register failed, error code = %x", __func__, ret);
|
||
return;
|
||
}
|
||
|
||
ret = esp_ble_gatts_register_callback(gatts_event_handler);
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "%s gatts register failed, error code = %x", __func__, ret);
|
||
return;
|
||
}
|
||
ret = esp_ble_gatts_app_register(APP_ID_PLACEHOLDER);
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "%s gatts app register failed, error code = %x", __func__, ret);
|
||
return;
|
||
}
|
||
ret = esp_ble_gatt_set_local_mtu(512);
|
||
if (ret) {
|
||
ESP_LOGE(CONN_TAG, "set local MTU failed, error code = %x", ret);
|
||
return;
|
||
}
|
||
|
||
// 获取 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) {
|
||
ESP_LOGE(CONN_TAG, "set device name failed, error code = %x", ret);
|
||
return;
|
||
}
|
||
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) {
|
||
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);
|
||
}
|
||
|
||
|
||
|
||
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
|
||
{
|
||
switch (event) {
|
||
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);
|
||
// 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);
|
||
break;
|
||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
||
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
|
||
ESP_LOGE(CONN_TAG, "Advertising start failed, status %d", param->adv_start_cmpl.status);
|
||
break;
|
||
}
|
||
ESP_LOGI(CONN_TAG, "Advertising start successfully");
|
||
break;
|
||
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
|
||
if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
|
||
ESP_LOGE(CONN_TAG, "Advertising stop failed, status %d", param->adv_stop_cmpl.status);
|
||
}
|
||
ESP_LOGI(CONN_TAG, "Advertising stop successfully");
|
||
break;
|
||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
|
||
ESP_LOGI(CONN_TAG, "Connection params update, status %d, conn_int %d, latency %d, timeout %d",
|
||
param->update_conn_params.status,
|
||
param->update_conn_params.conn_int,
|
||
param->update_conn_params.latency,
|
||
param->update_conn_params.timeout);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
// GATT服务器事件处理函数
|
||
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
|
||
{
|
||
switch (event) {
|
||
case ESP_GATTS_REG_EVT:
|
||
ESP_LOGI(CONN_TAG, "GATT server register, status %d, app_id %d",param->reg.status, param->reg.app_id);
|
||
// 创建图片传输服务
|
||
esp_ble_gatts_create_service(gatts_if,&server_id_image,10);
|
||
break;
|
||
case ESP_GATTS_CREATE_EVT:
|
||
if (param->create.status == ESP_GATT_OK) {
|
||
image_service_handle = param->create.service_handle;
|
||
esp_ble_gatts_add_char(
|
||
image_service_handle,
|
||
&image_write_uuid,
|
||
ESP_GATT_PERM_WRITE,
|
||
ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_WRITE_NR,
|
||
&char_val_image_write,
|
||
&control_image_write
|
||
);
|
||
esp_ble_gatts_add_char(
|
||
image_service_handle,
|
||
&image_edit_uuid,
|
||
ESP_GATT_PERM_WRITE,
|
||
ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_WRITE_NR,
|
||
&char_val_image_edit,
|
||
&control_image_edit
|
||
);
|
||
ESP_LOGI(CONN_TAG, "图片传输服务创建成功,句柄: %x", image_service_handle);
|
||
} else {
|
||
ESP_LOGE(CONN_TAG, "服务创建失败,状态: %d", param->create.status);
|
||
}
|
||
break;
|
||
|
||
case ESP_GATTS_ADD_CHAR_EVT:
|
||
if (param->add_char.status == ESP_GATT_OK) {
|
||
if (param->add_char.char_uuid.uuid.uuid16 == (uint16_t)IMAGE_WRITE_UUID) {
|
||
image_write_handle = param->add_char.attr_handle;
|
||
ESP_LOGI(CONN_TAG, "图片写入特征创建成功,句柄: %d", image_write_handle);
|
||
} else if (param->add_char.char_uuid.uuid.uuid16 == (uint16_t)IMAGE_EDIT_UUID) {
|
||
image_edit_handle = param->add_char.attr_handle;
|
||
ESP_LOGI(CONN_TAG, "图片编辑特征创建成功,句柄: %d", image_edit_handle);
|
||
esp_ble_gatts_start_service(image_service_handle);
|
||
}
|
||
} else {
|
||
ESP_LOGE(CONN_TAG, "特征创建失败,状态: %d", param->add_char.status);
|
||
}
|
||
break;
|
||
case ESP_GATTS_WRITE_EVT:
|
||
if(param->write.handle == image_write_handle){
|
||
uint8_t *value = param->write.value;
|
||
if(!SendStatus.isSend){
|
||
ESP_LOGI(CONN_TAG, "处理前序数据");
|
||
firstMeg.type = value[0];
|
||
memcpy(firstMeg.filename, value + 1, 22);
|
||
firstMeg.filename[22] = '\0';
|
||
firstMeg.len = (value[23] << 16) | (value[24] << 8) | value[25];
|
||
ESP_LOGI(CONN_TAG, "图片数据长度:%d",(int)firstMeg.len);
|
||
if(firstMeg.type == 0xfd){
|
||
SendStatus.isSend = true;
|
||
img_data = malloc((int)firstMeg.len);
|
||
filepath = malloc(sizeof(char) * 33);
|
||
sprintf(filepath,"/spiflash/%s",firstMeg.filename);
|
||
file_img = fopen(filepath,"wb");
|
||
ESP_LOGI(CONN_TAG,"传输通道建立成功,数据指针:%p,文件名称:%s,文件大小:%d",img_data,firstMeg.filename,(int)firstMeg.len);
|
||
}
|
||
}else if(SendStatus.isSend){
|
||
uint8_t pkt_no = *value;
|
||
uint8_t isEnd = *(value + 1);
|
||
// 每 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;
|
||
memcpy(img_data + SendStatus.port,data,(int)param->write.len-2);
|
||
SendStatus.port += param->write.len-2;
|
||
if(isEnd){
|
||
ESP_LOGI(CONN_TAG,"数据接收完毕,累计:%d字节,预期:%d字节,首字节:%02X %02X",
|
||
(int)SendStatus.port,(int)firstMeg.len,img_data[0],img_data[1]);
|
||
fwrite(img_data,sizeof(uint8_t),firstMeg.len,file_img);
|
||
fclose(file_img);
|
||
SendStatus.isSend = false;
|
||
SendStatus.port = 0;
|
||
// img_data 不释放,传给显示任务直通显示(跳过 SPIFFS 重读)
|
||
ble_pending_data = img_data;
|
||
ble_pending_data_size = firstMeg.len;
|
||
img_data = NULL; // 转移所有权
|
||
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);
|
||
}
|
||
}
|
||
}// 图片编辑特征写入事件
|
||
else if(param->write.handle == image_edit_handle){
|
||
uint8_t *value = param->write.value;
|
||
char imgName[23];
|
||
uint8_t type = *(value + param->write.len - 1);
|
||
memcpy(imgName, value, 23);
|
||
if(type == 0xff){
|
||
// 耗时操作转移到独立任务执行
|
||
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){
|
||
remove(filepath);
|
||
SendStatus.isSend = false;
|
||
SendStatus.port = 0;
|
||
free(img_data);
|
||
free(filepath);
|
||
}
|
||
}
|
||
break;
|
||
case ESP_GATTS_CONNECT_EVT:
|
||
esp_ble_conn_update_params_t conn_params = {0};
|
||
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
|
||
conn_params.latency = 0;
|
||
conn_params.max_int = 16; // 16 × 1.25ms = 20ms(缩短连接间隔提升传输吞吐量)
|
||
conn_params.min_int = 6; // 6 × 1.25ms = 7.5ms
|
||
conn_params.timeout = 400;
|
||
conn_id = param->connect.conn_id;
|
||
ESP_LOGI(CONN_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR"",
|
||
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda));
|
||
esp_ble_gap_update_conn_params(&conn_params);
|
||
break;
|
||
case ESP_GATTS_DISCONNECT_EVT:
|
||
ESP_LOGI(CONN_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
|
||
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
|
||
esp_ble_gap_start_advertising(&adv_params);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|