Baji_Rtc_Toy/main/dzbj/dzbj_ble.c
Rdzleo 14776acb0a feat: 完成 AI/吧唧双模式完全隔离重构 + 触摸坐标日志 + SPIFFS 预烧录
## 核心变更

### 1. 双模式完全隔离 (Phase 2+4)
- 拆分 InitializeButtons() 为 InitializeBadgeModeButtons() + InitializeAiModeButtons()
- 构造函数按 device_mode 分支:吧唧模式不创建 PowerSaveTimer/BackgroundTask
- 吧唧模式不注册音量/故事按键回调,避免调用 GetAudioCodec() 崩溃
- GPIO0 由 iot_button 统一处理,dzbj_button 仅注册 KEY2(GPIO4)
- SetDeviceState() 中 background_task_ 空指针保护

### 2. 吧唧模式 BOOT 按键崩溃修复
- 新增 dzbj_boot_click_handler()(C 函数,避免 lvgl.h 与 display.h 冲突)
- 移植 dzbj 的唤醒屏幕/退出手电筒/返回Home 完整逻辑

### 3. esp_timer 阻塞 LVGL 渲染修复
- iot_button 回调在 esp_timer 任务中执行,vTaskDelay 会阻塞 lv_tick_inc
- 改为 xTaskCreate 派发到独立 FreeRTOS 任务,避免冻结 LVGL 渲染

### 4. 触摸坐标日志 + SPIFFS 预烧录
- esp_lvgl_port_touch.c 添加触摸坐标打印
- CMakeLists.txt 添加 spiffs_create_partition_image 自动打包 spiffs_image/

### 5. dzbj 模块文件新增
- device_mode: NVS 设备模式管理 (AI=0/吧唧=1)
- dzbj_button: GPIO4 KEY2 中断 + BOOT 点击处理
- dzbj_ble: BLE GATT 图传服务 (0x0B00)
- dzbj_battery: ADC 电池电压监测
- sleep_mgr: 10s 超时熄屏低功耗管理
- pages: 图片浏览/GIF播放/PWM亮度
- fatfs: SPIFFS 文件管理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:23:04 +08:00

411 lines
16 KiB
C
Raw Permalink 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"
#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 = "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;
}
}
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);
}
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);
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;
}
}