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

26 KiB
Raw Blame History

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_t0~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 秒
  • 已接收约300400 包150KB200KB
  • 完成度:约 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

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);
  }
}

原理writewithoutResponse: false会等待设备端 GATT 层 ACK这个等待天然给设备端缓冲区消化数据的时间避免溢出。

方案 B固定间隔延迟

如果不想用混合模式,在每批包之间加短延迟:

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+ 支持写入队列管理:

// 限制最大并发写入数,防止缓冲区溢出
FlutterBluePlus.setWriteQueueSize(maxConcurrent: 5);

4.2 【P0 - 必须修复】添加断连检测与错误处理

当前 APP 在传输中断后没有任何错误反馈,用户不知道发生了什么:

// 监听连接状态变化
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 - 建议修复】断连自动重试

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需要更保守的传输策略

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

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 PHYAPP 端无需任何修改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。

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 失败不会中断主流程。

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 协商失败不阻塞主流程

// 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 灵活匹配(兼容多种格式)

// 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 延迟 + 重试机制(基础流控)

// 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

// 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

/// 发送单包数据(带重试 + 降级机制,对标 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 波特率)配合定位问题。