Baji_Rtc_Toy/BLE图片传输问题分析与优化建议.md
Rdzleo 4e2f6906f9 feat: 启用 BLE 5.0 2M PHY 图传加速 + 移除未使用的 BluFi 组件 + BLE 断连内存泄漏修复
1、dzbj_ble.c 新增 BLE 5.0 2M PHY 请求(连接时自动协商,不支持则回退 1M);
2、dzbj_ble.c 新增 PHY 更新事件日志(tx_phy/rx_phy: 1=1M, 2=2M, 3=Coded);
3、dzbj_ble.c 断连时清理未完成的图片传输状态,释放 img_data/filepath/file_img 防止内存泄漏;
4、移除未使用的 BluFi 组件依赖(Kconfig select、编译检查、sdkconfig),解除与 BLE 5.0 的兼容冲突;
5、sdkconfig.defaults 及生产环境配置统一启用 BLE 5.0 + 保留 BLE 4.2 legacy advertising 兼容;
6、新增 BLE 图片传输问题分析与优化建议文档(含 UniApp vs Flutter 对比分析,供 APP 开发者参考);

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 17:12:35 +08:00

696 lines
26 KiB
Markdown
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.

# 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 APIMTU 由浏览器自动协商
- 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<void> transferImage(List<int> 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<bool> transferWithRetry(List<int> 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<void> transferImage(
List<int> 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.02017 年后主流机型):自动协商 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 连接后能发现服务和特征,但仍然断开,可能的原因:
#### 原因 1MTU 协商失败导致后续写入异常
`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<void> 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);
}
```
#### 原因 3APP 连接了两次
日志显示完整的连接流程重复出现两次。第一次连接可能在写入数据时失败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 个关键因素
#### 因素 1MTU 协商失败不阻塞主流程
```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后续服务发现和传输都不会执行。
#### 因素 2UUID 灵活匹配(兼容多种格式)
```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/iOSBLE 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<void> bleWriteWithRetry(
BluetoothCharacteristic char,
List<int> 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<void> transferImage(
BluetoothCharacteristic writeChar,
List<int> 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 = <int>[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 波特率)配合定位问题。