# BLE 图片传输失败分析与优化建议 ## 一、问题现象 APP 通过 BLE 向设备传输一张 533934 字节(约 521KB)的 JPEG 图片,传输过程中 BLE 连接断开,图片未完整接收,设备屏幕未更新。 --- ## 二、双端日志对照分析 ### 2.1 APP 端日志(Flutter Web) ``` [BLE Transfer] MTU 协商失败,使用默认值: FlutterBluePlusException | requestMtu | fbp-code: 2 | android-only [BLE Transfer] MTU=512, chunkSize=507 [BLE Transfer] 服务: 0b00 [BLE Transfer] 特征: 0b01 (write + writeWithoutResponse) [BLE Transfer] 特征: 0b02 (write + writeWithoutResponse) [BLE Transfer] JPEG 大小: 533934 字节 [FBP] stopScan: already stopped (以上日志重复出现两次 —— APP 进行了两次连接尝试) ``` ### 2.2 设备端日志(ESP32-S3) ``` I (14939) DZBJ_BLE: Connected, conn_id 0, remote 9c:76:0e:47:1b:de I (15959) DZBJ_BLE: 处理前序数据 I (15959) DZBJ_BLE: 图片数据长度:533934 I (16069) DZBJ_BLE: 传输通道建立成功,文件名称:face_1774336190.jpg,文件大小:533934 I (16069) DZBJ_BLE: 获取到数据:第:1包,长度:509,是否结束:0 I (19169) DZBJ_BLE: 获取到数据:第:101包,长度:509,是否结束:0 ← 3秒收100包 I (23309) DZBJ_BLE: 获取到数据:第:201包,长度:509,是否结束:0 ← 4秒收100包 I (25379) DZBJ_BLE: 获取到数据:第:1包,长度:509,是否结束:0 ← 包序号溢出* W (26369) BT_HCI: hcif disc complete: hdl 0x1, rsn 0x13 dev_find 1 I (26369) DZBJ_BLE: Disconnected, reason 0x13 ← APP主动断开 ``` > *注:包序号字段为 `uint8_t`(0~255),第 256 包时溢出回到 0,日志显示为"第1包"。**传输本身未中断,仅是日志显示溢出**。设备端后续会修复此日志。 ### 2.3 时间线还原 | 时间点 | 事件 | 说明 | |--------|------|------| | T+0s | BLE 连接建立 | MTU=512 生效 | | T+1s | 前序数据解析 | 文件名 + 文件大小 | | T+1.1s | 传输开始 | chunkSize=507 字节/包 | | T+1.1s ~ T+9.3s | 数据传输中 | 约收到 300 包 ≈ 150KB | | **T+10.3s** | **APP 断开连接** | reason 0x13 = Remote User Terminated | | - | 无"传输完成"日志 | 图片未保存,屏幕未更新 | ### 2.4 传输进度估算 - 需要总包数:533934 ÷ 505(每包纯数据) ≈ **1058 包** - 实际传输时间:约 10 秒 - 已接收约:300~400 包(150KB~200KB) - **完成度:约 30%** --- ## 三、问题根因 ### 3.1 核心原因:WriteWithoutResponse 无流控导致 BLE 缓冲区溢出 APP 使用 `writeWithoutResponse` 模式连续发送 1058 个数据包,此模式特点: - **不等待设备端 ACK**,发送速度完全由 APP 端控制 - ESP32-S3 的 BLE 协议栈有内部缓冲区(Bluedroid TX buffer),连续高速写入导致缓冲区积压 - 当缓冲区满且链路层来不及发送时,**底层协议栈会主动断开连接** - BLE 规范中 WriteWithoutResponse 没有流控机制,必须由应用层自行控制节奏 ### 3.2 次要原因:Flutter Web 环境 BLE 限制 - `js_primitives.dart` 表明 APP 运行在 **Flutter Web**(浏览器)环境 - Web Bluetooth API 的 `requestMtu` 不可用(Android-only API),MTU 由浏览器自动协商 - Web Bluetooth 的写入吞吐量和稳定性**显著低于** Android/iOS 原生 BLE - 浏览器对 BLE 操作有隐式超时和安全限制 ### 3.3 APP 连接了两次 APP 日志出现两组完整的服务发现记录,但设备端只有一次连接/断连,可能原因: - 第一次连接极短(未成功开始传输),设备端未触发 CONNECT_EVT - 或第一次只完成了服务发现就断开,第二次才开始数据传输 --- ## 四、APP 端优化建议 ### 4.1 【P0 - 必须修复】添加发送流控 这是导致传输失败的直接原因,**必须修复**。 #### 方案 A:混合写入模式(推荐,改动最小) 每 N 包使用一次 `write`(带响应)作为同步点,其余用 `writeWithoutResponse`: ```dart const int SYNC_INTERVAL = 10; // 每 10 包同步一次 for (var i = 0; i < chunks.length; i++) { if (i % SYNC_INTERVAL == 0) { // 带响应写入:等待设备端 ACK,天然起到流控作用 await characteristic.write(chunks[i], withoutResponse: false); } else { // 无响应写入:速度快 await characteristic.write(chunks[i], withoutResponse: true); } } ``` **原理**:`write`(withoutResponse: false)会等待设备端 GATT 层 ACK,这个等待天然给设备端缓冲区消化数据的时间,避免溢出。 #### 方案 B:固定间隔延迟 如果不想用混合模式,在每批包之间加短延迟: ```dart for (var i = 0; i < chunks.length; i++) { await characteristic.write(chunks[i], withoutResponse: true); // 每 20 包暂停一下,让设备端消化缓冲区 if (i % 20 == 0) { await Future.delayed(Duration(milliseconds: 10)); } } ``` #### 方案 C:基于 FlutterBluePlus 队列深度控制 FlutterBluePlus 3.x+ 支持写入队列管理: ```dart // 限制最大并发写入数,防止缓冲区溢出 FlutterBluePlus.setWriteQueueSize(maxConcurrent: 5); ``` ### 4.2 【P0 - 必须修复】添加断连检测与错误处理 当前 APP 在传输中断后没有任何错误反馈,用户不知道发生了什么: ```dart // 监听连接状态变化 device.connectionState.listen((state) { if (state == BluetoothConnectionState.disconnected) { if (transferInProgress) { showError("传输中断,已发送 ${sentBytes}/${totalBytes} 字节,请重试"); transferInProgress = false; } } }); // 传输函数中添加异常捕获 Future transferImage(List imageData) async { try { transferInProgress = true; for (var i = 0; i < chunks.length; i++) { if (!device.isConnected) { throw Exception("BLE 连接断开,传输中止于第 $i/${chunks.length} 包"); } await characteristic.write(chunks[i], withoutResponse: i % 10 != 0); sentBytes = i * chunkSize; } transferInProgress = false; } catch (e) { transferInProgress = false; log("[BLE Transfer] 传输失败: $e"); rethrow; // 向上层抛出,由 UI 处理 } } ``` ### 4.3 【P1 - 建议修复】断连自动重试 ```dart const int MAX_RETRIES = 3; Future transferWithRetry(List imageData) async { for (var attempt = 0; attempt < MAX_RETRIES; attempt++) { try { if (!device.isConnected) { await device.connect(timeout: Duration(seconds: 10)); await device.discoverServices(); } await transferImage(imageData); return true; // 传输成功 } catch (e) { log("[BLE Transfer] 第 ${attempt + 1} 次尝试失败: $e"); await device.disconnect(); await Future.delayed(Duration(seconds: 2)); // 等待后重试 } } return false; // 全部重试失败 } ``` ### 4.4 【P1 - 建议修复】Flutter Web 平台适配 Web Bluetooth 吞吐量低于原生 BLE,需要更保守的传输策略: ```dart import 'package:flutter/foundation.dart' show kIsWeb; // 根据平台调整传输参数 int getSyncInterval() { if (kIsWeb) { return 5; // Web 环境:每 5 包同步一次(更保守) } else { return 15; // Native 环境:每 15 包同步一次 } } // Web 端建议同时降低传输并发 int getChunkBatchSize() { if (kIsWeb) { return 10; // Web:每批 10 包 + 10ms 间隔 } else { return 30; // Native:每批 30 包 } } ``` ### 4.5 【P2 - 可选优化】传输进度 UI ```dart Future transferImage( List imageData, { void Function(double progress)? onProgress, }) async { final chunks = splitIntoChunks(imageData, chunkSize); for (var i = 0; i < chunks.length; i++) { await characteristic.write(chunks[i], withoutResponse: i % 10 != 0); onProgress?.call((i + 1) / chunks.length); } } // UI 层调用 transferImage(jpegData, onProgress: (progress) { setState(() => transferProgress = progress); // 0.0 ~ 1.0 }); ``` --- ## 五、BLE 图传协议参考 ### 5.1 协议格式 #### 前序包(第一包,建立传输通道) 写入特征:`0x0B01` | 偏移 | 长度 | 字段 | 说明 | |------|------|------|------| | 0 | 1 | type | 固定 `0xFD` 表示图片传输 | | 1 | 22 | filename | 文件名(UTF-8,'\0'填充) | | 23 | 3 | length | 文件总大小(大端序,3字节) | #### 数据包(后续包) 写入特征:`0x0B01` | 偏移 | 长度 | 字段 | 说明 | |------|------|------|------| | 0 | 1 | pkt_no | 包序号(0~255 循环,仅用于日志) | | 1 | 1 | isEnd | 0=继续传输, 1=最后一包 | | 2 | N | data | 图片数据(N = chunkSize - 2 = 505 字节) | #### 图片编辑命令 写入特征:`0x0B02` | 类型字节(末字节) | 说明 | |----------|------| | `0xFF` | 切换显示指定图片(payload 前 22 字节为文件名) | | `0xF1` | 删除指定图片 | ### 5.2 MTU 与包大小关系 ``` 协商 MTU = 512 ATT Header = 3 字节 GATT Write Header = 2 字节(attribute handle) ───────────────────────── 可用 Payload = 512 - 3 - 2 = 507 字节(即 chunkSize) 数据包头 = 2 字节(pkt_no + isEnd) 每包纯图片数据 = 507 - 2 = 505 字节 ``` ### 5.3 传输耗时预估 基于连接间隔 7.5~20ms(设备端配置 min_int=6, max_int=16),加入方案 A 流控后: | 图片大小 | 总包数 | 无流控(当前) | 方案A 流控后预估 | |----------|--------|---------------|-----------------| | 100KB | ~203 | ~2s(可能断连) | ~4s | | 300KB | ~608 | ~6s(可能断连) | ~12s | | 500KB | ~1013 | ~10s(大概率断连) | ~20s | | 1MB | ~2026 | 必定断连 | ~40s | --- ## 六、设备端已完成的修复 以下问题已在最新固件中修复: | 问题 | 影响 | 修复状态 | |------|------|----------| | BLE 断连后未清理传输状态 | `img_data` 内存泄漏,`SendStatus.isSend` 残留 | ✅ 已修复:`DISCONNECT_EVT` 中自动释放资源 | | BLE 5.0 2M PHY 未启用 | 传输速度只有 1M PHY | ✅ 已启用:连接后自动协商 2M PHY,速度翻倍 | | 未使用的 BluFi 组件占用编译资源 | 与 BLE 5.0 不兼容 | ✅ 已移除:两种模式均使用自定义 GATT Server | --- ## 七、测试验证清单 完成 APP 端优化后,请按以下步骤验证: - [ ] **小图测试**:传输 50KB 图片,确认完整接收并显示 - [ ] **大图测试**:传输 500KB+ 图片,确认不中途断连 - [ ] **连续测试**:连续传 3~5 张图片,每次都成功 - [ ] **弱信号测试**:手机距设备 3~5 米,测试传输稳定性 - [ ] **Web 端测试**:在 Chrome 浏览器中完成上述全部测试 - [ ] **断连恢复测试**:传输中手动关闭蓝牙再打开,确认 APP 能正确提示并重试 ### 传输成功的设备端日志标志 ``` I DZBJ_BLE: 传输通道建立成功,文件名称:xxx.jpg,文件大小:533934 I DZBJ_BLE: 获取到数据:第:101包,长度:509,是否结束:0 I DZBJ_BLE: 获取到数据:第:201包,长度:509,是否结束:0 ...(中间持续接收) I DZBJ_BLE: 数据接收完毕,累计:533934字节,预期:533934字节 ← 累计 = 预期 I DZBJ_BLE: 图片接收成功,数据直通显示(533934字节) ← 屏幕更新触发 ``` --- ## 八、BLE 5.0 2M PHY 已启用(设备端已完成) 设备端固件已启用 BLE 5.0 2M PHY,**APP 端无需任何修改**,PHY 协商在 BLE 链路层自动完成,对 GATT 操作完全透明。 ### 8.1 设备端日志确认 ``` I BLE_INIT: Feature Config, ADV:1, BLE_50:1, ... ← BLE 5.0 已启用 I DZBJ_BLE: Connected, conn_id 0, remote 73:8f:af:0d:36:7a I DZBJ_BLE: PHY update, status 0, tx_phy 1, rx_phy 1 ← 初始 1M PHY I DZBJ_BLE: PHY update, status 0, tx_phy 2, rx_phy 2 ← 自动切换到 2M PHY ✅ ``` ### 8.2 速度对比 | PHY 模式 | 物理层速率 | 实际应用层吞吐量 | 533KB 图片预估耗时 | |----------|-----------|-----------------|-------------------| | 1M PHY(旧固件) | 1 Mbps | ~100-200 KB/s | ~3-5s | | **2M PHY(当前固件)** | **2 Mbps** | **~200-350 KB/s** | **~1.5-2.5s** | ### 8.3 兼容性 - 手机支持 BLE 5.0(2017 年后主流机型):自动协商 2M PHY,速度翻倍 - 手机不支持 BLE 5.0:自动保持 1M PHY,不报错,功能不受影响 - APP 端 FlutterBluePlus 的 `write` / `writeWithoutResponse` 调用方式完全不变 --- ## 九、【P0 关键问题】Flutter APP 无法连接电子吧唧模式 ### 9.1 问题现象 Flutter APP 在电子吧唧模式下可以扫描到设备,**BLE 连接成功,但约 3 秒后 APP 主动断开**,无法进行图片传输。而在 AI 对话模式下,同一 APP 可以正常连接和蓝牙配网。 ### 9.2 设备端日志分析(设备端完全正常) ``` I (14509) DZBJ_BLE: Connected, conn_id 0, remote 73:8f:af:0d:36:7a ← 连接成功 ✅ I (14539) DZBJ_BLE: PHY update, status 0, tx_phy 1, rx_phy 1 ← PHY 初始化 ✅ I (14879) DZBJ_BLE: Connection params update, conn_int 12 ← 参数协商 ✅ I (15039) DZBJ_BLE: Connection params update, conn_int 6 ← 参数协商 ✅ I (16329) DZBJ_BLE: PHY update, status 0, tx_phy 2, rx_phy 2 ← 2M PHY 切换 ✅ W (17429) BT_HCI: hcif disc complete: rsn 0x13 ← APP 主动断开 ❌ I (17429) DZBJ_BLE: Disconnected, reason 0x13 ``` **设备端没有任何错误**。BLE 连接正常建立,参数协商正常,PHY 成功切换到 2M。是 APP 端在连接后约 3 秒主动断开了连接(reason 0x13 = Remote User Terminated Connection)。 ### 9.3 APP 端日志分析 ``` [BLE Transfer] MTU 协商失败,使用默认值: FlutterBluePlusException | requestMtu | fbp-code: 2 | android-only [BLE Transfer] MTU=512, chunkSize=507 [BLE Transfer] 服务: 0b00 [BLE Transfer] 特征: 0b01 (write + writeWithoutResponse) [BLE Transfer] 特征: 0b02 (write + writeWithoutResponse) [BLE Transfer] JPEG 大小: 533934 字节 (以上日志重复出现两次 —— APP 进行了两次连接尝试) ``` APP 日志显示: 1. MTU 协商失败(`fbp-code: 2, android-only` 表明 `requestMtu` 在非 Android 平台不可用) 2. 服务和特征**已成功发现**(0x0B00 服务、0x0B01/0x0B02 特征) 3. 整个流程重复了两次(两次连接尝试都失败) ### 9.4 根因分析 APP 连接后能发现服务和特征,但仍然断开,可能的原因: #### 原因 1:MTU 协商失败导致后续写入异常 `requestMtu` 失败后,APP 代码中 `MTU=512, chunkSize=507` 是写死的回退值。如果实际系统 MTU 远小于 512(某些平台默认 MTU=23),每包发送 509 字节(2字节ATT头 + 507 数据)会超出实际 MTU,**导致 BLE 协议栈直接丢包或报错**。 **修复方案**:MTU 协商失败时,应使用系统默认 MTU 或通过平台 API 获取实际值,而非写死 512。 ```dart try { await device.requestMtu(512); } catch (e) { print('MTU 协商失败: $e'); } // 关键:无论 requestMtu 是否成功,都从系统获取实际 MTU final actualMtu = await device.mtu.first; // 获取实际协商的 MTU final chunkSize = actualMtu - 3; // ATT 头部 3 字节 print('实际 MTU: $actualMtu, chunkSize: $chunkSize'); ``` #### 原因 2:连接后流程异常导致超时断开 APP 可能在 MTU 协商失败后进入了错误处理分支,没有继续执行图片传输,等待超时后断开。 **排查方法**:在 Flutter 代码中检查 `requestMtu` 失败后是否有 `return` 或异常抛出中断了后续的 `discoverServices` / `write` 流程。 **修复方案**:确保 `requestMtu` 失败不会中断主流程。 ```dart Future connectAndTransfer(BluetoothDevice device) async { await device.connect(); // MTU 协商(失败不影响后续流程) try { await device.requestMtu(512); } catch (e) { print('MTU 协商失败,使用默认值: $e'); // ✅ 不要 return 或 throw,继续执行 } // 服务发现(必须执行) final services = await device.discoverServices(); final imageService = services.firstWhere( (s) => s.uuid.toString().contains('0b00'), orElse: () => throw Exception('未找到图传服务 0x0B00'), ); // 获取特征 final writeChar = imageService.characteristics.firstWhere( (c) => c.uuid.toString().contains('0b01'), ); // 开始传输... await transferImage(writeChar, imageData); } ``` #### 原因 3:APP 连接了两次 日志显示完整的连接流程重复出现两次。第一次连接可能在写入数据时失败,APP 重试了一次但结果相同。 **排查方法**:检查 APP 中是否有自动重连/重试逻辑,以及第一次连接失败时的具体错误信息。 ### 9.5 两种模式 BLE 服务对比 APP 需要支持连接两种模式的 BLE 服务,它们的差异: | 维度 | AI 对话模式(配网) | 电子吧唧模式(图传) | |------|-------------------|-------------------| | 服务 UUID | `0xABF0` | `0x0B00` | | 写入特征 | `0xABF1` (Write) | `0x0B01` (Write + WriteNoResponse) | | 通知特征 | `0xABF2` (Notify) | `0x0B02` (Write,非 Notify) | | 设备名称 | `Airhub_xx:xx:xx:xx:xx:xx` | `Airhub_xx:xx:xx:xx:xx:xx`(相同) | | 广播标识 | 扫描响应含 `ABF0` UUID | 扫描响应含厂商数据 `LDdzbj` + `0B00` UUID | | 协议格式 | 二进制命令(1字节CMD + payload) | 二进制帧(前序帧 + 数据帧,见第五章) | **APP 端区分两种模式的方法**: - 扫描响应(Scan Response)中的厂商数据包含 ASCII `"dzbj"` → 电子吧唧模式 - 连接后发现服务 `0x0B00` → 图传模式;发现服务 `0xABF0` → 配网模式 - 两种模式不会同时存在(设备重启切换),APP 只需按发现的服务 UUID 走对应流程 ### 9.6 APP 端自查清单 请 Flutter APP 开发者按以下顺序排查: - [ ] `requestMtu(512)` 失败后,是否 `return` / `throw` 中断了后续流程? - [ ] `requestMtu` 失败后,`chunkSize` 是否仍为 507?实际 MTU 是否支持这个大小? - [ ] `discoverServices()` 是否在 `requestMtu` 之后执行?是否能找到 `0x0B00` 服务? - [ ] 写入特征时使用 `writeWithoutResponse` 还是 `write`?是否有流控/节流机制? - [ ] 连接断开时是否有更详细的错误日志(FlutterBluePlus 的 `onConnectionStateChanged`)? - [ ] 两次连接尝试之间是否有足够的间隔(建议 2 秒以上)? --- ## 十、UniApp 成功 vs Flutter 失败——对比分析 UniApp 测试 APP 可以正常连接电子吧唧模式并成功传输图片,而 Flutter 正式 APP 连接后约 3 秒断开。以下是两者 BLE 实现的关键差异分析。 ### 10.1 连接流程对比 | 步骤 | UniApp(成功) | Flutter(失败) | |------|--------------|----------------| | **BLE 连接** | `uni.createBLEConnection()` 原生 API | `device.connect()` FlutterBluePlus | | **MTU 协商** | `uni.setBLEMTU({mtu:512})`,失败仅 log 不中断 | `device.requestMtu(512)` 抛异常 `android-only` | | **MTU 失败处理** | 成功 → `getBleService()`;失败 → 仅打印日志,**不中断流程** | 异常后 `chunkSize` 写死 507,**可能中断后续流程** | | **服务发现** | `uni.getBLEDeviceServices()` → `findByUuid16()` 灵活匹配 | `device.discoverServices()` → UUID 字符串匹配 | | **特征发现** | `uni.getBLEDeviceCharacteristics()` → 同样灵活匹配 | 从服务对象直接获取 | ### 10.2 UniApp 成功的 5 个关键因素 #### 因素 1:MTU 协商失败不阻塞主流程 ```javascript // UniApp connect.vue:426 setBleMtu() { uni.setBLEMTU({ deviceId: this.deviceId, mtu: 512, success() { that.isConnected = true; that.getBleService(); // ✅ 成功后继续 }, fail() { console.log('MTU设置失败'); // ✅ 仅打印,不阻塞 } }) } ``` **注意**:UniApp 的 `fail` 回调中确实**没有**调用 `getBleService()`,但在原生 APP 环境下 `setBLEMTU` 通常会成功。如果 MTU 设置失败,UniApp 也无法继续——但它运行在原生环境中,所以 MTU 协商基本都成功。 **Flutter 问题**:`requestMtu` 在非 Android 平台(iOS/Web)直接抛异常,如果异常处理不当(catch 中 return/throw),后续服务发现和传输都不会执行。 #### 因素 2:UUID 灵活匹配(兼容多种格式) ```javascript // UniApp connect.vue:358 findByUuid16(list, uuid16) { const hex4 = uuid16.toString(16).padStart(4, '0').toLowerCase(); const fullTarget = '0000' + hex4 + '-0000-1000-8000-00805f9b34fb'; return list.find(item => { const uuid = item.uuid.toLowerCase(); if (uuid === fullTarget) return true; // 128-bit 完整格式 if (uuid === hex4 || uuid === '0000' + hex4) return true; // 短格式 if (uuid.startsWith('0000' + hex4 + '-')) return true; // 前缀匹配 return false; }); } ``` UniApp 同时匹配 `"0b00"`、`"00000b00"`、`"00000b00-0000-1000-8000-00805f9b34fb"` 三种格式。**不同平台/不同 BLE 库返回的 UUID 格式不统一**,灵活匹配避免了因格式不匹配导致"找不到服务"的问题。 **Flutter 建议**:检查 `discoverServices()` 返回的 UUID 格式,确保匹配逻辑兼容短 UUID 和完整 128-bit UUID。 #### 因素 3:每包 5ms 延迟 + 重试机制(基础流控) ```javascript // UniApp connect.vue:308 writeBleImage() while (offset < len) { await this.bleWrite(this.imageWriteuuid, packet.buffer); await this.delay(5); // ✅ 每包 5ms 延迟,防止 BLE 缓冲区溢出 offset += chunkLen; packetNo++; } // UniApp connect.vue:237 bleWrite()(带重试) async bleWrite(characteristicId, buffer) { const MAX_RETRY = 3; for (let i = 0; i < MAX_RETRY; i++) { try { await this._bleWriteOnce(characteristicId, buffer, 'writeNoResponse'); return; } catch (err) { if (i < MAX_RETRY - 1) { await this.delay(20 * (i + 1)); // ✅ 退避递增:20ms, 40ms, 60ms } else { // ✅ 最后一次尝试降级为 write(带应答) await this._bleWriteOnce(characteristicId, buffer, 'write'); return; } } } } ``` 这是 UniApp 传输成功的**最核心因素**: - **每包 5ms 延迟**:给 ESP32 BLE 协议栈缓冲区消化时间 - **3 次重试**:写入失败不立即放弃 - **退避递增**:重试间隔 20ms → 40ms → 60ms,给系统更多恢复时间 - **降级机制**:最终尝试用 `write`(带 ACK 应答),天然起到流控作用 #### 因素 4:前序帧发送后等待 50ms ```javascript // UniApp connect.vue:323 await this.bleWrite(this.imageWriteuuid, header.buffer); await this.delay(50); // ✅ 等待设备端处理前序帧(malloc内存、创建文件等) ``` 设备端收到前序帧后需要执行 `malloc()`、`fopen()` 等操作,50ms 的等待确保设备端准备好接收数据帧。 #### 因素 5:原生运行环境(非 Web) UniApp 编译为原生 APP 运行(Android/iOS),BLE API 直接调用系统原生接口: - MTU 协商通过原生 API 实现,成功率高 - BLE 写入性能接近原生(无浏览器 Web Bluetooth 的限制) - 无 Web Bluetooth 的隐式超时和安全限制 ### 10.3 Flutter APP 需要修复的差异点 | 序号 | 差异项 | UniApp 做法 | Flutter 当前问题 | 修复优先级 | |------|--------|------------|-----------------|-----------| | 1 | **MTU 失败处理** | fail 仅 log,不阻断 | 异常可能中断后续流程 | **P0** | | 2 | **发送流控** | 每包 5ms 延迟 | 无延迟,全速发送 | **P0** | | 3 | **写入重试** | 3 次重试 + 退避递增 + 降级为 write | 无重试机制 | **P0** | | 4 | **前序帧等待** | header 后 50ms | 未知(需检查) | **P1** | | 5 | **UUID 匹配** | 兼容短/长/前缀三种格式 | 可能只匹配一种格式 | **P1** | | 6 | **运行环境** | 原生 APP | 可能运行在 Web 环境 | **P1** | ### 10.4 Flutter 参考实现(对标 UniApp) ```dart /// 发送单包数据(带重试 + 降级机制,对标 UniApp bleWrite) Future bleWriteWithRetry( BluetoothCharacteristic char, List data, { int maxRetry = 3, }) async { for (var i = 0; i < maxRetry; i++) { try { await char.write(data, withoutResponse: true); return; } catch (e) { if (i < maxRetry - 1) { // 退避递增:20ms, 40ms, 60ms await Future.delayed(Duration(milliseconds: 20 * (i + 1))); } else { // 最后一次降级为 write(带应答) await char.write(data, withoutResponse: false); return; } } } } /// 图片分包传输(对标 UniApp writeBleImage) Future transferImage( BluetoothCharacteristic writeChar, List imageData, String filename, { void Function(double)? onProgress, }) async { // 1. 发送前序帧 final header = buildHeader(filename, imageData.length); // 26 字节 await bleWriteWithRetry(writeChar, header); await Future.delayed(Duration(milliseconds: 50)); // ← 关键:等待设备端准备 // 2. 分包发送数据 final chunkSize = 507; // MTU(512) - ATT(3) - Handle(2) var offset = 0; var packetNo = 0; while (offset < imageData.length) { final remaining = imageData.length - offset; final chunkLen = remaining < chunkSize ? remaining : chunkSize; final isEnd = (offset + chunkLen >= imageData.length) ? 0x01 : 0x00; final packet = [packetNo & 0xFF, isEnd, ...imageData.sublist(offset, offset + chunkLen)]; await bleWriteWithRetry(writeChar, packet); await Future.delayed(Duration(milliseconds: 5)); // ← 关键:每包 5ms 延迟 offset += chunkLen; packetNo++; if (packetNo % 10 == 0 || isEnd == 1) { onProgress?.call(offset / imageData.length); } } } ``` ### 10.5 总结 **UniApp 能成功而 Flutter 失败的核心原因是:UniApp 实现了基础的 BLE 流控机制(每包延迟 + 重试 + 降级),而 Flutter APP 缺少这些关键防护。** BLE `WriteWithoutResponse` 是"发后不管"模式,ESP32 的 Bluedroid 协议栈缓冲区有限(约 8-10 个包),不加延迟的全速发送会导致缓冲区溢出 → 协议栈断开连接。UniApp 的 5ms/包延迟虽然简单,但足以让缓冲区不积压。 --- ## 十一、联系方式 如有疑问请联系固件端开发。设备端调试可提供串口日志(115200 波特率)配合定位问题。