Baji_Rtc_Toy/main/dzbj/dzbj_ble.c
Rdzleo 6b166f4463 feat: add EAF RTC and badge dual mode
1. 固定 RTC 数字人链路使用 ai_chat_ui_eaf,双模式开启后不再回退 LVGL/GIF 旧 RTC UI。

2. 保留电子吧唧 LVGL/SquareLine UI,只在电子吧唧运行模式启动 LVGL,避免与 EAF 抢同一 LCD flush。

3. 拆分 dzbj_hw_display_init 与 dzbj_display_init,AI/配网只初始化 LCD Touch 硬件,电子吧唧再启动 LVGL UI。

4. 配网模式使用 EAF 最小显示栈显示中文提示,请使用APP 蓝牙配网,不加载数字人资源和动画。

5. 开启 CONFIG_BAJI_BADGE_MODE,形成 RTC 数字人对话与电子吧唧图片显示双模式固件。

6. 电子吧唧图片扫描跳过 Background_360x360.jpg,避免 RTC 数字人背景进入吧唧图片列表。

7. BLE 图传在 BLE 5.0 关闭时跳过 2M PHY API,保持 legacy 1M PHY 兼容配网和图传。

8. sdkconfig.defaults 同步 BLE 内存优化,限制连接数和缓存,保留 GATT 与扫描能力。

9. 移除 ota.cc 编译和 app_update 直接依赖,双模式固件不创建 OTA 检查任务。

10. Ota 接口改为禁用 stub,保留接口兼容但不执行升级和版本检查。

11. Board 上报 JSON 的 OTA label 改为 disabled,避免依赖 OTA 运行分区。

12. partitions.csv 改为 factory 单 app 分区,扩大 app 到 0x900000,并扩大 storage 到 0x6F0000。

13. application 去除 OTA 任务句柄和服务器时间依赖,减少运行时资源占用。

14. system_info 去除 esp_ota_ops 依赖,配合无 OTA 分区配置。

15. 同步最新烧录运行日志,记录本轮双模式与配网测试结果。
2026-06-02 13:16:39 +08:00

512 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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"
#include "ble_transfer.h"
#include "esp_lvgl_port.h"
#include "ui/ui.h"
#include "ui/screens/ui_ScreenUpdate.h"
#include "ui/screens/ui_ScreenReceiving.h"
#include "dzbj_button.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 bool ble_advertising_enabled = false;
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 = "DZBJ_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;
dzbj_button_set_context(BTN_CTX_IMG);
}
}
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 dzbj_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);
// 初始化设备间传输模块注册GATT Client
ble_transfer_init();
}
void dzbj_ble_deinit(void)
{
esp_ble_gap_stop_advertising();
esp_ble_gatts_app_unregister(0);
esp_bluedroid_disable();
esp_bluedroid_deinit();
esp_bt_controller_disable();
esp_bt_controller_deinit();
}
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);
// 仅在广播已启用时开始广播(开机默认不广播)
if (ble_advertising_enabled) {
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;
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT:
ESP_LOGI(CONN_TAG, "PHY update, status %d, tx_phy %d, rx_phy %d",
param->phy_update.status,
param->phy_update.tx_phy,
param->phy_update.rx_phy);
// tx_phy/rx_phy: 1=1M, 2=2M, 3=Coded
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
// 扫描参数设置完成开始扫描5秒
esp_ble_gap_start_scanning(5);
break;
case ESP_GAP_BLE_SCAN_RESULT_EVT:
// 扫描结果转发给传输模块
ble_transfer_handle_scan_result(param);
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
ESP_LOGI(CONN_TAG, "Scan stopped");
ble_transfer_handle_scan_stop();
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);
// 根据传输来源切换界面
if (ble_transfer_is_receiving()) {
// 设备间传输:切换到接收中界面
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenReceiving, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenReceiving_screen_init);
lvgl_port_unlock();
}
dzbj_button_set_context(BTN_CTX_RECEIVING);
} else {
// APP传图切换到Update更新界面
ESP_LOGI(CONN_TAG, "APP传图跳转到Update界面");
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenUpdate, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenUpdate_screen_init);
lvgl_port_unlock();
}
dzbj_button_set_context(BTN_CTX_UPDATE);
}
}
}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';
// 通知传输模块接收完成(设备间传输模式时)
ble_transfer_on_receive_complete();
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);
// BLE 5.0 开启时请求 2M PHYRTC 双模式默认关闭 BLE 5.0,走 legacy 1M 保配网稳定。
#if CONFIG_BT_BLE_50_FEATURES_SUPPORTED
esp_ble_gap_set_preferred_phy(param->connect.remote_bda,
ESP_BLE_GAP_NO_PREFER_TRANSMIT_PHY | ESP_BLE_GAP_NO_PREFER_RECEIVE_PHY,
ESP_BLE_GAP_PHY_2M_PREF_MASK,
ESP_BLE_GAP_PHY_2M_PREF_MASK,
ESP_BLE_GAP_PHY_OPTIONS_NO_PREF);
#else
ESP_LOGI(CONN_TAG, "BLE 5.0 disabled, keep legacy 1M PHY for compatibility");
#endif
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);
// 清理未完成的传输,防止内存泄漏
if (SendStatus.isSend) {
ESP_LOGW(CONN_TAG, "传输中断,已接收 %d/%d 字节",
(int)SendStatus.port, (int)firstMeg.len);
SendStatus.isSend = false;
SendStatus.port = 0;
if (img_data) { free(img_data); img_data = NULL; }
if (filepath) { free(filepath); filepath = NULL; }
if (file_img) { fclose(file_img); file_img = NULL; }
}
// 断连后仅在广播启用时重新广播
if (ble_advertising_enabled) {
esp_ble_gap_start_advertising(&adv_params);
}
break;
default:
break;
}
}
// === BLE 广播启停控制 ===
void dzbj_ble_start(void)
{
ble_advertising_enabled = true;
esp_ble_gap_start_advertising(&adv_params);
ESP_LOGI(CONN_TAG, "BLE广播已启动");
}
void dzbj_ble_stop(void)
{
ble_advertising_enabled = false;
esp_ble_gap_stop_advertising();
ESP_LOGI(CONN_TAG, "BLE广播已停止");
}
bool dzbj_ble_is_active(void)
{
return ble_advertising_enabled;
}