Baji_Rtc_Toy_Key/main/dzbj/ble_transfer.c
Rdzleo 3a05c3c7d5 双模设备电子吧唧按键功能完整实现 + BLE设备间图片传输 + 模式切换防护
1、按键驱动重构:dzbj_button改为iot_button组件,支持BOOT/KEY2单击/双击/长按,回调通过xTaskCreate派发避免阻塞esp_timer;
2、新增按键导航管理器(key_nav):9种上下文状态机,统一分发按键事件到对应界面;
3、BLE改造:广播默认关闭由按键触发启停,新增设备间GATT Client图片传输(扫描→连接→MTU协商→分包发送),WRITE_EVT区分APP/设备传图跳转不同界面;
4、新增模式切换按键抑制:NVS标志+2秒时间窗,防止AI↔吧唧切换时幽灵按键触发配网或白屏;
5、AI模式BOOT单击回调添加抑制检查,吧唧模式key_nav_boot_click同步添加;
6、新增6个UI界面:配对(Peiwang)、更新(Update)、发送等待/接收等待/发送中/接收中;
7、新增电池指示器组件(battery_ui):多界面真实电量显示+3秒渐隐;
8、Home/Img界面重构:移除触摸手势事件,改为airhub背景+按键导航;
9、禁用触摸(DZBJ_ENABLE_TOUCH=0),保留代码可恢复;
10、sleep_mgr简化:按键唤醒由key_nav统一处理;

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 17:44:46 +08:00

517 lines
18 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.

// 设备间蓝牙图片传输模块
// 发送方GATT Client 扫描→发现→连接→MTU协商→发现服务→分包写入
// 接收方:复用现有 GATT ServerIMAGE_WRITE 0x0B01协议完全兼容
// 不影响现有 APP 传图APP 通过 KEY2 单击Peiwang设备间通过 KEY2 长按/双击
#include "ble_transfer.h"
#include "dzbj_ble.h"
#include "pages.h"
#include "key_nav.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_bt_main.h"
#include "esp_gatt_common_api.h"
#include "esp_lvgl_port.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ui/ui.h"
#include "ui/screens/ui_ScreenImg.h"
#include "ui/screens/ui_ScreenSharing.h"
#include "ui/screens/ui_ScreenReceiving.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "BLE_XFER";
// === 状态管理 ===
static ble_xfer_state_t xfer_state = BLE_XFER_IDLE;
static bool receiving_mode = false; // 接收方模式标识
// === GATT Client 相关 ===
#define XFER_APP_ID 1 // 与现有Server的APP_ID=0区分
#define IMAGE_SERVICE_UUID 0x0B00
#define IMAGE_WRITE_UUID 0x0B01
static esp_gatt_if_t client_gattc_if = ESP_GATT_IF_NONE;
static uint16_t client_conn_id = 0;
static uint16_t client_write_handle = 0;
static esp_bd_addr_t target_bda; // 扫描到的目标设备地址
static bool target_found = false;
static uint16_t client_mtu = 23; // 协商后的MTU值
// === 发送任务相关 ===
static TaskHandle_t send_task_handle = NULL;
static volatile bool send_cancel_flag = false;
static volatile bool gattc_congested = false; // BLE链路拥塞标志
// === 扫描参数 ===
static esp_ble_scan_params_t scan_params = {
.scan_type = BLE_SCAN_TYPE_ACTIVE,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_interval = 0x50, // 50ms
.scan_window = 0x30, // 30ms
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
};
// === 前向声明 ===
static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
static void send_image_task(void *arg);
// === 接口实现 ===
ble_xfer_state_t ble_transfer_get_state(void) {
return xfer_state;
}
bool ble_transfer_is_receiving(void) {
return receiving_mode;
}
void ble_transfer_init(void) {
// 注册 GATT Client 回调
esp_err_t ret = esp_ble_gattc_register_callback(gattc_event_handler);
if (ret) {
ESP_LOGE(TAG, "GATTC回调注册失败: %s", esp_err_to_name(ret));
return;
}
// 注册 GATT Client 应用
ret = esp_ble_gattc_app_register(XFER_APP_ID);
if (ret) {
ESP_LOGE(TAG, "GATTC应用注册失败: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "设备间传输模块初始化完成");
}
void ble_transfer_start_send(void) {
if (xfer_state != BLE_XFER_IDLE) {
ESP_LOGW(TAG, "传输正在进行中,忽略");
return;
}
// 如果当前正在为APP广播先停止
if (dzbj_ble_is_active()) {
dzbj_ble_stop();
}
xfer_state = BLE_XFER_SCANNING;
target_found = false;
send_cancel_flag = false;
// 开始扫描扫描5秒
esp_ble_gap_set_scan_params(&scan_params);
ESP_LOGI(TAG, "发送方:开始扫描接收设备...");
}
void ble_transfer_start_receive(void) {
if (xfer_state != BLE_XFER_IDLE) {
ESP_LOGW(TAG, "传输正在进行中,忽略");
return;
}
receiving_mode = true;
xfer_state = BLE_XFER_RECEIVING;
// 启动广播复用现有GATT Server
dzbj_ble_start();
ESP_LOGI(TAG, "接收方:已开始广播,等待发送设备连接...");
}
void ble_transfer_cancel(void) {
ESP_LOGI(TAG, "取消传输,当前状态: %d", xfer_state);
send_cancel_flag = true;
switch (xfer_state) {
case BLE_XFER_SCANNING:
esp_ble_gap_stop_scanning();
break;
case BLE_XFER_CONNECTING:
case BLE_XFER_SENDING:
if (client_conn_id != 0) {
esp_ble_gattc_close(client_gattc_if, client_conn_id);
}
break;
case BLE_XFER_RECEIVING:
dzbj_ble_stop();
receiving_mode = false;
break;
default:
break;
}
xfer_state = BLE_XFER_IDLE;
}
// === GAP 扫描结果回调(由 dzbj_ble.c 的 esp_gap_cb 转发) ===
void ble_transfer_handle_scan_result(void *param) {
if (xfer_state != BLE_XFER_SCANNING) return;
esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param;
switch (scan_result->scan_rst.search_evt) {
case ESP_GAP_SEARCH_INQ_RES_EVT: {
// 检查广播数据 + Scan Response 中是否包含服务UUID 0x0B00
uint8_t *adv_data = scan_result->scan_rst.ble_adv;
uint8_t adv_len = scan_result->scan_rst.adv_data_len
+ scan_result->scan_rst.scan_rsp_len;
// 解析广播数据,查找 0x0B00 服务UUID
uint8_t *p = adv_data;
while (p < adv_data + adv_len) {
uint8_t field_len = p[0];
if (field_len == 0) break;
uint8_t field_type = p[1];
// 检查完整16位服务UUID列表
if (field_type == ESP_BLE_AD_TYPE_16SRV_CMPL ||
field_type == ESP_BLE_AD_TYPE_16SRV_PART) {
for (int i = 2; i < field_len + 1; i += 2) {
uint16_t uuid16 = p[i] | (p[i + 1] << 8);
if (uuid16 == IMAGE_SERVICE_UUID) {
memcpy(target_bda, scan_result->scan_rst.bda, sizeof(esp_bd_addr_t));
target_found = true;
ESP_LOGI(TAG, "发现接收设备: "ESP_BD_ADDR_STR,
ESP_BD_ADDR_HEX(target_bda));
esp_ble_gap_stop_scanning();
return;
}
}
}
// 检查厂商数据中的"LDdzbj"标识
if (field_type == ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE && field_len >= 7) {
if (memcmp(p + 2, "\x4C\x44\x64\x7A\x62\x6A", 6) == 0) {
memcpy(target_bda, scan_result->scan_rst.bda, sizeof(esp_bd_addr_t));
target_found = true;
ESP_LOGI(TAG, "发现接收设备(厂商标识): "ESP_BD_ADDR_STR,
ESP_BD_ADDR_HEX(target_bda));
esp_ble_gap_stop_scanning();
return;
}
}
p += field_len + 1;
}
break;
}
case ESP_GAP_SEARCH_INQ_CMPL_EVT:
// 扫描完成
if (!target_found) {
ESP_LOGW(TAG, "扫描完成,未发现接收设备,保持分享界面等待用户操作");
xfer_state = BLE_XFER_IDLE;
}
break;
default:
break;
}
}
void ble_transfer_handle_scan_stop(void) {
if (xfer_state != BLE_XFER_SCANNING) return;
if (target_found) {
// 扫描停止成功,开始连接
xfer_state = BLE_XFER_CONNECTING;
esp_ble_gattc_open(client_gattc_if, target_bda, BLE_ADDR_TYPE_PUBLIC, true);
ESP_LOGI(TAG, "正在连接目标设备...");
}
}
// === GATT Client 事件处理 ===
static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_REG_EVT:
if (param->reg.status == ESP_GATT_OK) {
client_gattc_if = gattc_if;
ESP_LOGI(TAG, "GATTC注册成功, if=%d", gattc_if);
} else {
ESP_LOGE(TAG, "GATTC注册失败, status=%d", param->reg.status);
}
break;
case ESP_GATTC_OPEN_EVT:
if (param->open.status != ESP_GATT_OK) {
ESP_LOGE(TAG, "连接失败, status=%d", param->open.status);
xfer_state = BLE_XFER_IDLE;
return;
}
client_conn_id = param->open.conn_id;
ESP_LOGI(TAG, "已连接, conn_id=%d", client_conn_id);
// 协商MTU为512以加速传输
esp_ble_gattc_send_mtu_req(gattc_if, client_conn_id);
break;
case ESP_GATTC_CFG_MTU_EVT:
client_mtu = param->cfg_mtu.mtu;
ESP_LOGI(TAG, "MTU协商完成: %d", client_mtu);
// 开始发现服务
esp_ble_gattc_search_service(gattc_if, client_conn_id, NULL);
break;
case ESP_GATTC_SEARCH_RES_EVT: {
// 找到服务
if (param->search_res.srvc_id.uuid.len == ESP_UUID_LEN_16 &&
param->search_res.srvc_id.uuid.uuid.uuid16 == IMAGE_SERVICE_UUID) {
ESP_LOGI(TAG, "发现图片传输服务, start_handle=%d, end_handle=%d",
param->search_res.start_handle, param->search_res.end_handle);
// 获取特征值
uint16_t count = 0;
esp_gatt_status_t status = esp_ble_gattc_get_attr_count(
gattc_if, client_conn_id, ESP_GATT_DB_CHARACTERISTIC,
param->search_res.start_handle, param->search_res.end_handle, 0, &count);
if (status == ESP_GATT_OK && count > 0) {
esp_gattc_char_elem_t *char_elems = malloc(sizeof(esp_gattc_char_elem_t) * count);
if (char_elems) {
status = esp_ble_gattc_get_all_char(
gattc_if, client_conn_id,
param->search_res.start_handle,
param->search_res.end_handle,
char_elems, &count, 0);
if (status == ESP_GATT_OK) {
for (int i = 0; i < count; i++) {
if (char_elems[i].uuid.len == ESP_UUID_LEN_16 &&
char_elems[i].uuid.uuid.uuid16 == IMAGE_WRITE_UUID) {
client_write_handle = char_elems[i].char_handle;
ESP_LOGI(TAG, "发现IMAGE_WRITE特征, handle=%d", client_write_handle);
}
}
}
free(char_elems);
}
}
}
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT:
if (client_write_handle == 0) {
ESP_LOGE(TAG, "未找到IMAGE_WRITE特征");
esp_ble_gattc_close(gattc_if, client_conn_id);
xfer_state = BLE_XFER_IDLE;
return;
}
// 切换到传输中界面
xfer_state = BLE_XFER_SENDING;
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenSharing, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenSharing_screen_init);
lvgl_port_unlock();
}
key_nav_set_context(NAV_CTX_SHARING);
// 启动发送任务
xTaskCreate(send_image_task, "ble_send", 8192, NULL, 5, &send_task_handle);
break;
case ESP_GATTC_WRITE_CHAR_EVT:
if (param->write.status != ESP_GATT_OK) {
ESP_LOGE(TAG, "写入失败, handle=%d, status=%d",
param->write.handle, param->write.status);
}
break;
case ESP_GATTC_CONGEST_EVT:
// BLE链路拥塞状态变化
gattc_congested = param->congest.congested;
if (gattc_congested) {
ESP_LOGW(TAG, "BLE链路拥塞暂停发送");
} else {
ESP_LOGI(TAG, "BLE链路恢复继续发送");
}
break;
case ESP_GATTC_CLOSE_EVT:
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGI(TAG, "GATTC断开连接");
client_conn_id = 0;
client_write_handle = 0;
gattc_congested = false;
break;
default:
break;
}
}
// === 图片发送任务 ===
static void send_image_task(void *arg) {
ESP_LOGI(TAG, "开始发送图片...");
// 获取当前显示的图片文件名
const char *img_name = get_current_image();
if (!img_name) {
ESP_LOGE(TAG, "没有可发送的图片");
goto send_fail;
}
// 打开图片文件
char filepath[48];
snprintf(filepath, sizeof(filepath), "/spiflash/%s", img_name);
FILE *f = fopen(filepath, "rb");
if (!f) {
ESP_LOGE(TAG, "无法打开图片: %s", filepath);
goto send_fail;
}
// 获取文件大小
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fseek(f, 0, SEEK_SET);
ESP_LOGI(TAG, "图片: %s, 大小: %ld 字节", img_name, file_size);
// === 发送前序数据包 ===
// 格式: [0xFD] [filename 22B] [size 3B big-endian] = 26 bytes
uint8_t header[26];
header[0] = 0xFD;
memset(header + 1, 0, 22);
strncpy((char *)(header + 1), img_name, 22);
header[23] = (file_size >> 16) & 0xFF;
header[24] = (file_size >> 8) & 0xFF;
header[25] = file_size & 0xFF;
esp_err_t ret = esp_ble_gattc_write_char(client_gattc_if, client_conn_id,
client_write_handle, sizeof(header), header,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "前序包写入失败: %s", esp_err_to_name(ret));
fclose(f);
goto send_fail;
}
vTaskDelay(pdMS_TO_TICKS(50)); // 等待对端建立接收缓冲区
// === 分包发送图片数据 ===
// 格式: [pkt_no 1B] [isEnd 1B] [data ...]
int max_write_len = client_mtu - 3;
if (max_write_len > 509) max_write_len = 509;
int payload_size = max_write_len - 2;
ESP_LOGI(TAG, "MTU=%d, 每包最大数据=%d字节", client_mtu, payload_size);
uint8_t pkt_buf[512];
int pkt_no = 0;
size_t total_sent = 0;
gattc_congested = false;
while (total_sent < file_size) {
if (send_cancel_flag) {
ESP_LOGW(TAG, "发送被取消");
fclose(f);
goto send_fail;
}
// 拥塞等待
int congest_wait = 0;
while (gattc_congested && congest_wait < 500) {
vTaskDelay(pdMS_TO_TICKS(10));
congest_wait += 10;
}
if (congest_wait >= 500) {
ESP_LOGE(TAG, "拥塞超时,传输中止");
fclose(f);
goto send_fail;
}
size_t remaining = file_size - total_sent;
size_t chunk = (remaining > payload_size) ? payload_size : remaining;
bool is_last = (total_sent + chunk >= file_size);
pkt_buf[0] = (uint8_t)(pkt_no & 0xFF);
pkt_buf[1] = is_last ? 1 : 0;
size_t read_len = fread(pkt_buf + 2, 1, chunk, f);
if (read_len != chunk) {
ESP_LOGE(TAG, "文件读取错误");
fclose(f);
goto send_fail;
}
ret = esp_ble_gattc_write_char(client_gattc_if, client_conn_id,
client_write_handle, read_len + 2, pkt_buf,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (ret != ESP_OK) {
// API层面失败递增延时重试
int attempt = 0;
while (ret != ESP_OK && attempt < 5) {
attempt++;
vTaskDelay(pdMS_TO_TICKS(20 * attempt));
ret = esp_ble_gattc_write_char(client_gattc_if, client_conn_id,
client_write_handle, read_len + 2, pkt_buf,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
}
if (ret != ESP_OK) {
ESP_LOGE(TAG, "写入失败(重试耗尽): %s", esp_err_to_name(ret));
fclose(f);
goto send_fail;
}
}
total_sent += chunk;
pkt_no++;
// 每包间隔20ms流控
vTaskDelay(pdMS_TO_TICKS(20));
if (pkt_no % 50 == 0) {
ESP_LOGI(TAG, "已发送 %d/%ld 字节 (%d%%)",
(int)total_sent, file_size, (int)(total_sent * 100 / file_size));
}
}
fclose(f);
ESP_LOGI(TAG, "图片发送完成!共 %d 包,%ld 字节", pkt_no, file_size);
// 等待对端处理完成
vTaskDelay(pdMS_TO_TICKS(500));
// 断开连接并关闭蓝牙
ESP_LOGI(TAG, "发送完成:断开连接,关闭蓝牙");
esp_ble_gattc_close(client_gattc_if, client_conn_id);
dzbj_ble_stop();
// 传输成功跳转到Img界面
xfer_state = BLE_XFER_DONE;
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
lvgl_port_unlock();
}
key_nav_set_context(NAV_CTX_IMG);
xfer_state = BLE_XFER_IDLE;
send_task_handle = NULL;
vTaskDelete(NULL);
return;
send_fail:
xfer_state = BLE_XFER_FAILED;
ESP_LOGW(TAG, "发送失败:断开连接,关闭蓝牙");
esp_ble_gattc_close(client_gattc_if, client_conn_id);
dzbj_ble_stop();
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
lvgl_port_unlock();
}
key_nav_set_context(NAV_CTX_IMG);
xfer_state = BLE_XFER_IDLE;
send_task_handle = NULL;
vTaskDelete(NULL);
}
// === 接收完成回调(由 dzbj_ble.c 的 GATTS 写入完成时调用) ===
void ble_transfer_on_receive_complete(void) {
if (!receiving_mode) return;
ESP_LOGI(TAG, "接收方:图片接收完成");
receiving_mode = false;
// dzbj_ble_stop 和 key_nav_set_context 由 ble_process_task 统一处理
xfer_state = BLE_XFER_DONE;
xfer_state = BLE_XFER_IDLE;
}