一. 技术背景
在蓝牙 4.0 协议规范中,低功耗蓝牙(BLE)的 MTU(Maximum Transmission Unit)默认值为 23 字节。实际传输中有效载荷通常为 20 字节,超过该限制会导致写入错误,并行传输也会写入失败,除此之外,Android 平台和 iOS 平台还有以下区别:
- Android 平台 :连续调用
writeBLECharacteristicValue
时触发 10008 系统错误(特征值操作频繁) - iOS 平台:大数据包写入时可能丢失回调事件(包括成功/失败回调)
如下图所示:

所以可以建立分片传输机制,通过将数据包拆解为 20 字节的原子单元,同时在发送数据时考虑时序控制,避免并行写入,要确保前序数据包传输完成后再发送后续分片。
本篇文章,我们主要实现 uni-app 蓝牙打印场景下,因低功耗蓝牙(BLE)MTU 默认值限制,通过移动端分片传输机制来解决数据传输问题。
二. 分片算法
由于蓝牙设备的传输限制,我们需要将打印数据进行分片,要控制在 20 个字节以内,以确保数据能够完整地传输到打印机。
因此我们写一个函数,该函数用于将一个 ArrayBuffer
类型的缓冲区数据按照指定的最大块大小进行分片。它会将输入的缓冲区数据分割成多个小块,每个小块的大小不超过指定的最大块大小(默认为 20 字节)。
2.1 初步实现
主要思路如下:
- 初始化一个空数组
result
用于存储分片结果 - 初始化一个变量
start
用于记录当前分片的起始位置,初始值为 0 - 使用循环进行分片操作,直到处理完整个缓冲区
- 在每次循环中,计算当前分片的结束位置,确保不超过缓冲区的总长度
- 使用
slice
方法从缓冲区中提取当前分片,并将其添加到结果数组result
中 - 更新起始位置
start
,准备下一次分片 - 循环结束后,返回包含所有分片的数组
result
js
function getSliceBufferList(buffer, maxChunk = 20) {
// 用于存储分片结果的数组
const result = []
// 记录当前分片的起始位置
let start = 0
// 循环进行分片操作,直到处理完整个缓冲区
while (start < buffer.byteLength) {
// 计算当前分片的结束位置,确保不超过缓冲区的总长度
const end = Math.min(start + maxChunk, buffer.byteLength)
// 将当前分片添加到结果数组中
result.push(buffer.slice(start, end))
// 更新起始位置,准备下一次分片
start = end
}
return result
}
2.2 优化后
使用 Tare AI 优化提示后,新增了以下优化点:

- 输入验证:检查输入缓冲区是否有效
- 边界处理:处理最后一个不完整分片
优化后的代码如下:
js
/**
* 获取分片数组
* @param {ArrayBuffer} buffer - 要分片的二进制数据缓冲区
* @param {number} [maxChunk=20] - 每包控制大小,默认为 20 字节
* @returns {Array<ArrayBuffer>} - 分片后的缓冲区数组
* @throws {Error} - 当输入缓冲区无效时抛出异常
*/
function getSliceBufferList(buffer, maxChunk = 20) {
// 输入验证
if (!(buffer instanceof ArrayBuffer)) {
throw new Error('Invalid input: expected ArrayBuffer')
}
if (buffer.byteLength === 0) {
return []
}
const result = []
let start = 0
// 循环分片
while (start < buffer.byteLength) {
// 边界处理:确保不超过缓冲区长度
const end = Math.min(start + maxChunk, buffer.byteLength)
// 处理最后一个不完整分片
if (end - start > 0) {
result.push(buffer.slice(start, end))
}
start = end
}
return result
}
在优化后的代码中,首先进行了输入验证,确保输入的 buffer
是一个有效的 ArrayBuffer
类型。然后,对输入缓冲区进行了边界处理,确保不会超出缓冲区的长度。
通过以上的优化,确保输入缓冲区的有效性,并处理边界情况,从而得到正确的分片结果。
三. 写入数据
通过上述的分片算法,我们可以将一个 ArrayBuffer
类型的缓冲区数据按照指定的最大块大小进行分片,得到一个分片后的 buffers
数组。
接下来,我们需要将每个分片数据写入到蓝牙设备中。但在实际写入数据过程中,我们除了要确保每次写入数据不能超过 20 个字节外,也要避免过快的写入操作,避免并行操作,不然会出现系统错误。
- 分片数据大小不能超过 20 个字节
- 分片数据按照队列以此执行
- 通过
setTimeout
来控制写入数据的时间间隔,
在 uni-app 中,使用 uni.writeBLECharacteristicValue
方法来写入数据。
js
async writeBLEValue(buffer, delayTime = 100) {
let buffers = this.getSliceBufferList(buffer);
for (const buffer of buffers) {
await new Promise((resolve) => {
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.characteristicId,
value: buffer,
success: () => {
console.log('写入数据成功');
// 使用 setTimeout 控制时间间隔
setTimeout(() => {
resolve();
}, delayTime);
},
fail: (err) => {
console.error('写入数据失败:', err);
// 使用 setTimeout 控制时间间隔
setTimeout(() => {
resolve();
}, delayTime);
}
});
});
}
}
writeBLEValue
这个方法主要会执行以下流程:
- 输入参数 :接收一个
buffer
,该buffer
是需要写入蓝牙设备的总的二进制数据。 - 分片处理 :通过
getSliceBufferList
方法将输入的buffer
进行分片处理,得到一个分片后的buffers
数组。 - 遍历分片 :循环遍历
buffers
数组,对每个分片进行处理。 - 写入数据 :针对每个分片,创建一个
Promise
对象,并调用uni.writeBLECharacteristicValue
方法将分片数据写入蓝牙设备。- 成功处理 :若写入操作成功,并调用
resolve
方法来解决Promise
。 - 失败处理 :若写入操作失败,同样调用
resolve
方法来解决Promise
。 - 超时处理 :使用
setTimeout
来控制写入数据的时间间隔,避免过快的写入操作,避免系统错误。
- 成功处理 :若写入操作成功,并调用
- 等待完成 :使用
await
等待当前分片的Promise
解决,确保前序数据包传输完成后再处理后续分片。 - 结束方法:当所有分片都处理完毕后,方法执行结束。
关于以上的写入数据的操作,我们还可以继续进行优化和完善,比如:
- 异常处理 :在
Promise
失败的情况下,我们可以添加重试逻辑或用户提示。 - 错误隔离:针对单个分片的写入失败,我们可以实现错误隔离,避免影响整体任务。
- 重试策略:针对写入失败的分片,我们可以实现指数退避重试策略,以避免频繁重试导致系统负载过高。
后续我们可以实现一个打印队列管理,来实现时序控制、错误隔离和重试策略,以及中断后恢复传输机制,来更完美的确保打印数据完整、稳定地传输到打印机。
四. 总结
通过本篇文章,我们简单实现了分片算法和写入策略来解决低功耗蓝牙(BLE)MTU 默认值限制的问题。通过合理的分片策略和控制写入数据的时间间隔,我们可以确保数据能够完整地传输到打印机,避免系统错误和数据丢失。
在实际应用中,我们可以根据具体的需求和打印机的支持情况,进一步优化和完善分片算法和写入数据的逻辑,以提高打印数据的传输效率和稳定性。