// 设备间蓝牙图片传输模块 // 发送方:GATT Client 扫描→发现→连接→MTU协商→发现服务→分包写入 // 接收方:复用现有 GATT Server(IMAGE_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 #include 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; }