Uni-App BLE 蓝牙通讯跨平台适配经验分享
一、背景说明
这篇文章用于整理当前项目在 uni-app 中做 BLE 蓝牙通讯的实战经验,重点覆盖以下内容:
- BLE 蓝牙通讯初始化示例代码;
- BLE 发送命令并等待回包的示例代码;
- 微信小程序、iOS 环境下的标准流程;
- 安卓 APP 环境下的通用兼容处理;
- 华为安卓 APP 环境下的特殊时序兼容处理;
- 实际踩过的问题,以及最终的处理思路。
当前项目的协议属于典型的“一问一答”模式,核心约定如下:
- APP 下发
type=cmd; - 设备回包
type=ack; - 设备主动事件上报为
type=evt; ack需要回显相同的cmd与req;- 当前主控制链路主要使用:
E0F00001-0000-1000-8000-00805F9B34FB:控制命令通道,APP 写入;E0F00002-0000-1000-8000-00805F9B34FB:控制响应通道,设备 Notify 回包。
二、推荐的初始化流程
对于 BLE 通讯,推荐把初始化流程固定成下面这个顺序:
- 初始化蓝牙模块;
- 扫描设备;
- 建立连接;
- 获取服务;
- 协商 MTU;
- 获取特征;
- 注册 Notify 监听;
- 开启响应特征的 Notify;
- 等待链路稳定后再发送首条命令。
注意:
- 微信里和 iOS 基本可以按标准流程直接走;
- 安卓 APP 需要增加“首条命令延迟发送”的兼容;
- 华为安卓 APP 还要进一步把“开启 Notify”本身也延后。
三、BLE 初始化代码示例
下面的示例代码是分享思路用的简化版本。
const SERVICE_UUID = 'E0F00000-0000-1000-8000-00805F9B34FB';
const CHAR_UUIDS = {
COMMAND: 'E0F00001-0000-1000-8000-00805F9B34FB',
RESPONSE: 'E0F00002-0000-1000-8000-00805F9B34FB',
};
let deviceId = '';
let negotiatedMtu = 20;
let notifyReadyAt = 0;
let isFirstCommandAfterConnect = false;
let pendingReqMap = {};
let reqSeed = 1;
/**
* 延迟函数
* 说明:
* 1. BLE 初始化和命令发送时,很多兼容逻辑都依赖显式延时。
* 2. 这里单独抽出,便于统一控制所有时序等待。
*/
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 注册全局 Notify 监听
* 说明:
* 1. 设备通过 RESPONSE 特征回传 ack。
* 2. 这里拿到回包后,通过 req 去匹配对应的待处理 Promise。
* 3. 如果收到 evt,也可以在这里统一分发给页面。
*/
function registerNotifyListener() {
uni.onBLECharacteristicValueChange((res) => {
if ((res.characteristicId || '').toUpperCase() !== CHAR_UUIDS.RESPONSE) {
return;
}
const raw = String.fromCharCode.apply(null, new Uint8Array(res.value));
const packet = JSON.parse(raw);
if (packet.type === 'ack' && packet.req && pendingReqMap[packet.req]) {
const pending = pendingReqMap[packet.req];
clearTimeout(pending.timer);
delete pendingReqMap[packet.req];
pending.resolve(packet);
}
if (packet.type === 'evt') {
console.log('[BLE][evt] 收到设备主动事件:', packet);
}
});
}
/**
* 协商 MTU
* 说明:
* 1. 安卓 / 鸿蒙支持主动设置 MTU。
* 2. iOS 通常由系统自动协商,不需要手动设置。
* 3. 这里的示例只展示安卓风格写法。
*/
function setMtu(currentDeviceId, mtu = 480) {
return new Promise((resolve) => {
if (!uni.setBLEMTU) {
resolve();
return;
}
uni.setBLEMTU({
deviceId: currentDeviceId,
mtu,
success: (res) => {
negotiatedMtu = res.mtu || mtu;
console.log('[BLE] MTU 协商成功:', negotiatedMtu);
resolve();
},
fail: () => {
negotiatedMtu = 20;
resolve();
},
});
});
}
/**
* 标准 BLE 初始化
* 说明:
* 1. 这个函数展示的是“普通安卓 / 微信 / iOS”的标准初始化流程。
* 2. 华为安卓 APP 的特殊处理会放到后面单独说明。
* 3. 当前项目中,实际也是在服务发现后再协商 MTU,再找特征,再开 Notify。
*/
async function initBleConnection(targetDeviceId) {
deviceId = targetDeviceId;
await new Promise((resolve, reject) => {
uni.createBLEConnection({
deviceId,
success: resolve,
fail: reject,
});
});
// 连接建立后先等一小段时间,避免刚连上时底层状态还不稳定。
await delay(300);
await new Promise((resolve, reject) => {
uni.getBLEDeviceServices({
deviceId,
success: resolve,
fail: reject,
});
});
// 安卓平台建议在发现服务后再协商 MTU。
await setMtu(deviceId, 480);
await new Promise((resolve, reject) => {
uni.getBLEDeviceCharacteristics({
deviceId,
serviceId: SERVICE_UUID,
success: resolve,
fail: reject,
});
});
// 先注册监听,避免 Notify 打开后设备太快回包而前端还没开始监听。
registerNotifyListener();
await new Promise((resolve, reject) => {
uni.notifyBLECharacteristicValueChange({
deviceId,
serviceId: SERVICE_UUID,
characteristicId: CHAR_UUIDS.RESPONSE,
state: true,
success: resolve,
fail: reject,
});
});
notifyReadyAt = Date.now();
isFirstCommandAfterConnect = true;
}
四、发送命令代码示例
发送命令时,建议统一封装成“发送 JSON 指令并等待对应 ack”的模式。
/**
* 字符串转 ArrayBuffer
* 说明:
* 1. uni.writeBLECharacteristicValue 需要传 ArrayBuffer。
* 2. 这里把 JSON 字符串编码成 Uint8Array 后再取 buffer。
*/
function strToArrayBuffer(str) {
const encoder = new TextEncoder();
return encoder.encode(str).buffer;
}
/**
* 生成业务报文
* 说明:
* 1. req 用于把一次请求和对应 ack 做精确匹配。
* 2. ts 主要用于诊断日志和设备侧排障。
*/
function buildCmdPacket(cmd, payload = {}) {
const req = reqSeed++;
return {
_req: req,
ver: '1.0',
type: 'cmd',
cmd,
req,
ts: Date.now(),
payload,
};
}
/**
* 按 MTU 分包写入
* 说明:
* 1. 大部分短命令在 MTU=480 时可以单包写完。
* 2. 这里仍保留分包逻辑,作为 MTU 协商失败时的兜底。
* 3. 安卓平台多包之间建议保留短暂间隔,减少底层写队列拥堵。
*/
async function writeInChunks(currentDeviceId, buffer, writeType = 'write') {
const chunkSize = Math.max(negotiatedMtu - 3, 20);
const bytes = new Uint8Array(buffer);
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
const chunk = bytes.slice(offset, offset + chunkSize);
await new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: currentDeviceId,
serviceId: SERVICE_UUID,
characteristicId: CHAR_UUIDS.COMMAND,
value: chunk.buffer,
writeType,
success: resolve,
fail: reject,
});
});
// 安卓多包写入建议保留一个小间隔,降低连续写入过快导致失败的概率。
await delay(180);
}
}
/**
* 发送命令并等待 ack
* 说明:
* 1. 首条命令在安卓 APP 环境通常不要紧接着 Notify 成功后立即发送。
* 2. 微信和 iOS 大多数情况下可以按标准流程直接写入。
* 3. 这里为了演示,统一保留了“首条命令延迟发送”的处理。
*/
async function sendCommand(cmd, payload = {}, timeout = 10000) {
const packet = buildCmdPacket(cmd, payload);
const req = packet._req;
delete packet._req;
if (isFirstCommandAfterConnect) {
const waitMs = Math.max(notifyReadyAt + 1000 - Date.now(), 0);
if (waitMs > 0) {
await delay(waitMs);
}
isFirstCommandAfterConnect = false;
}
const json = JSON.stringify(packet);
const buffer = strToArrayBuffer(json);
return new Promise(async (resolve, reject) => {
const timer = setTimeout(() => {
delete pendingReqMap[req];
reject(new Error(`命令 ${cmd} 超时`));
}, timeout);
pendingReqMap[req] = { resolve, reject, timer };
try {
await writeInChunks(deviceId, buffer, 'write');
} catch (err) {
clearTimeout(timer);
delete pendingReqMap[req];
reject(err);
}
});
}
/**
* 示例:读取设备信息
* 说明:
* 1. 当前项目里最常用的测试命令之一就是 device_info_get。
* 2. 这个命令非常适合用来验证“连接成功后整条链路是否已经打通”。
*/
async function getDeviceInfo() {
const ack = await sendCommand('device_info_get', {});
return ack.payload.info;
}
五、微信小程序与 iOS:按标准流程走即可
从当前项目的实测结果来看:
- 微信小程序:标准流程稳定;
- iOS APP:标准流程稳定;
- 安卓非华为:大多数情况下标准流程也能跑通,但建议保留通用兼容处理。
微信小程序与 iOS 的推荐流程如下:
- 连接设备;
- 获取服务;
- 获取特征;
- 开启响应特征 Notify;
- 注册监听;
- 发送首条业务命令;
- 设备返回 ack;
- 前端按
req匹配回包。
标准时序图如下:
APP 设备
| |
|-- 连接设备 ----------------------------------------->|
|<------------------------- 连接成功 -------------------|
|-- 获取服务 ----------------------------------------->|
|<------------------------- 服务列表 -------------------|
|-- 获取特征 ----------------------------------------->|
|<------------------------- 特征列表 -------------------|
|-- 开启 E0F00002 Notify ----------------------------->|
|<------------------------- Notify 开启成功 ------------|
|-- 写 E0F00001: type=cmd ---------------------------->|
| |-- 执行业务逻辑
|<===== Notify E0F00002: type=ack =====================|
|-- 按 req 匹配 pending 请求并返回结果 ----------------|
六、安卓 APP:需要增加通用兼容处理
虽然安卓非华为很多时候也能按标准流程直接成功,但在真实项目里,建议仍然加上通用兼容逻辑,原因是安卓不同 ROM、不同蓝牙芯片和不同系统版本差异很大。
当前项目对安卓 APP 的通用处理主要包括:
1. 连接成功后先等 300ms
目的:
- 避免刚建立连接后马上做 GATT 操作,底层状态还没稳定。
2. 发现服务后再协商 MTU
目的:
- 避免过早协商 MTU 导致某些安卓机型初始化顺序异常。
3. Notify 开启后,首条命令额外延后 1000ms
目的:
- 减少 CCCD 刚写成功后,立即写命令造成失败的概率。
4. 如果首写失败,再补等到 1600ms 再重试一次
目的:
- 对偶发的
10007做一次时序层面的兜底。
5. 多包写入时每包间隔约 180ms
目的:
- 避免连续快速写入把安卓底层队列打满。
6. 写入失败时尝试切换 writeType
当前项目会在必要时尝试:
writeomitwriteNoResponse
这样做的原因是:
- 不同安卓机型对
writeType的兼容表现不完全一致; - 某些机型表面上特征支持
write,但实际写入时仍然会表现异常; - 多一种候选写法,能提高项目现场的兼容成功率。
七、华为安卓 APP:不是普通安卓问题,而是更强的时序敏感
华为安卓 APP 是当前项目里最典型、也最容易踩坑的平台。
1. 第一阶段遇到的问题
一开始按标准流程:
- 连接成功;
- 发现特征;
- 立即开启 Notify;
- 很快发送首条命令;
结果经常出现错误:
writeBLECharacteristicValue:fail property not support- 错误码
10007
这个现象的关键点是:
- 设备的
E0F00001明明支持写; - 微信小程序、iOS、小米安卓都能正常工作;
- 只有华为安卓 APP 在“开 Notify 后很快发送首条命令”时容易失败。
所以问题不在协议本身,而在华为安卓 APP 下的 GATT 时序。
2. 第二阶段验证后的新结论
后续继续做时序测试后发现:
- 华为并不是“绝对不能开 Notify”;
- 真正敏感的是“何时开 Notify”和“开完 Notify 后多久发送第一条命令”;
- 如果调整成更保守的时序,标准 Notify 链路是可以恢复成功的。
3. 华为当前推荐时序
当前项目里,华为安卓 APP 推荐按下面这个顺序走:
- 连接设备;
- 获取服务;
- 协商 MTU;
- 获取特征;
- 先注册监听;
- 发现特征后等待约
2000ms; - 开启
E0F00002的 Notify; - Notify 成功后再等待约
2000ms; - 再发送首条业务命令;
- 后续按标准 Notify 回包处理。
对应简化时序如下:
APP 设备
| |
|-- 连接设备 ----------------------------------------->|
|<------------------------- 连接成功 -------------------|
|-- 获取服务 / MTU / 特征 ----------------------------->|
|<------------------------- 初始化完成 -----------------|
|-- 注册监听 ------------------------------------------|
|-- 等待约 2 秒 -------------------------------------->|
|-- 开启 E0F00002 Notify ----------------------------->|
|<------------------------- Notify 开启成功 ------------|
|-- 再等待约 2 秒 ------------------------------------>|
|-- 写 E0F00001: type=cmd ---------------------------->|
| |-- 执行业务逻辑
|<===== Notify E0F00002: type=ack =====================|
|-- 按 req 匹配 pending 请求并返回结果 ----------------|
4. 华为兼容代码示例
下面是一个简化后的华为时序示例:
/**
* 判断是否属于华为安卓 APP
* 说明:
* 1. 项目里通常会结合 brand、manufacturer、romName、osName 等信息综合判断。
* 2. 这里示例只表达“识别华为家族设备后走更严格的时序”这个思路。
*/
function isHuaweiAndroidApp(systemInfo) {
const brand = (systemInfo.brand || '').toLowerCase();
const platform = (systemInfo.platform || '').toLowerCase();
return platform === 'android' && (brand.includes('huawei') || brand.includes('honor'));
}
/**
* 华为安卓 APP 初始化
* 说明:
* 1. 和普通安卓相比,关键差异是“先等 2 秒再开 Notify”。
* 2. Notify 成功后,还要把首条命令的发送时间继续往后推。
*/
async function initBleConnectionForHuawei(targetDeviceId) {
const systemInfo = uni.getSystemInfoSync();
await initBleConnectionBase(targetDeviceId);
if (isHuaweiAndroidApp(systemInfo)) {
// 发现特征后,不要马上开 Notify,先等待底层 GATT 队列更稳定。
await delay(2000);
await new Promise((resolve, reject) => {
uni.notifyBLECharacteristicValueChange({
deviceId: targetDeviceId,
serviceId: SERVICE_UUID,
characteristicId: CHAR_UUIDS.RESPONSE,
state: true,
success: resolve,
fail: reject,
});
});
// 记录 Notify 就绪时刻,后面首条命令还要继续延迟。
notifyReadyAt = Date.now();
isFirstCommandAfterConnect = true;
}
}
/**
* 华为首条命令发送保护
* 说明:
* 1. 华为不是只延后开启 Notify,还要延后首条命令。
* 2. 当前项目里实测可用的是“Notify 成功后再等待约 2 秒”。
*/
async function prepareBeforeHuaweiFirstCommand() {
const waitMs = Math.max(notifyReadyAt + 2000 - Date.now(), 0);
if (waitMs > 0) {
await delay(waitMs);
}
isFirstCommandAfterConnect = false;
}
八、一次次遇到的问题,以及如何解决
下面按真实踩坑顺序,总结一次次遇到的问题和处理思路。
问题 1:标准流程在华为安卓 APP 下出现 10007
现象:
- 设备连接成功;
- 服务、特征都发现成功;
- 一旦开启 Notify 后很快发送首条命令;
- 就出现
writeBLECharacteristicValue:fail property not support; - 命令实际没有写到设备。
排查结果:
- 设备并不是不支持写;
- 协议命令也没有错;
- 因为微信小程序、iOS、小米安卓都正常;
- 所以问题集中在华为安卓 APP 下的 GATT 时序。
解决办法:
- 不再把“Notify 开启”和“首条命令发送”挤得太近;
- 改成更保守的延后时序。
问题 2:只是“开完 Notify 后等 2 秒”仍然不稳定
现象:
- 有些测试里,仅仅把“开完 Notify 后发送命令”延后到 2 秒,仍然失败;
- 说明不仅是首条命令时间有问题,Notify 本身的开启时机也有问题。
排查结果:
- 华为底层可能对 CCCD 写入前后的 GATT 操作都比较敏感;
- 不能只延后命令发送,还要延后 Notify 的开启时机。
解决办法:
- 发现特征后先等待约
2000ms; - 再开启 Notify;
- Notify 成功后再等待约
2000ms; - 然后再发首条命令。
问题 3:安卓平台个别机型偶发首写失败
现象:
- 非华为安卓大多数可以成功;
- 但个别安卓机型偶发首条命令失败。
排查结果:
- 很多时候不是协议问题,而是初始化顺序、首写时机、包间隔过快导致。
解决办法:
- 连接后先等
300ms; - 服务发现后再协商 MTU;
- Notify 开启后首条命令默认等
1000ms; - 如果首写报
10007,再补等到1600ms再重试一次; - 多包写入时每包间隔约
180ms。
问题 4:设备日志出现 GATTS_SendRsp / Sending response failed
现象:
- 设备日志里可见:
BT_GATT: GATTS_SendRsp ... waiting for op_code = 00BT_APPL: Sending response failed
排查结果:
- 从当前成功样本看,这两条日志并没有阻断最终
ack返回; - 所以它是固件端需要单独排查的问题,但不是当前 APP 成功与否的唯一决定因素。
解决办法:
- 前端层面先把 BLE 时序兼容处理到位;
- 固件侧继续检查是否存在“自动响应模式”和“手动响应”冲突问题。
问题 5:客户现场无法提供足够日志
现象:
- 很多 BLE 问题只在客户手机上出现;
- 客户通常无法提供完整控制台日志。
解决办法:
- 在前端维护最近 BLE 关键日志的内存缓冲区;
- 页面出错时,把系统信息、设备信息、BLE 快照、当前阶段、最近日志统一上传后端;
- 这样即使用户不会看 console,也能还原失败链路。
九、平台建议总结
当前建议如下:
- 微信小程序:按标准 BLE 流程开发;
- iOS APP:按标准 BLE 流程开发;
- 安卓 APP(非华为):按标准流程开发,但建议保留通用兼容时序;
- 华为安卓 APP:必须走“延后开 Notify + 延后首发命令”的兼容逻辑;
- 纯血鸿蒙:还需要继续单独验证。
十、结语
这次 BLE 跨平台兼容实践最重要的经验,不是“协议写对了就一定能通”,而是:
- BLE 在不同平台、不同 ROM、不同蓝牙栈上的时序表现差异非常大;
- 微信小程序和 iOS 相对标准;
- 安卓需要更谨慎地处理首条命令发送时机;
- 华为安卓 APP 对 Notify 时序尤其敏感;
- 只要日志留得够完整,问题最终都能从“看起来像协议错了”还原到“其实是平台时序差异”。
如果后续要继续提升稳定性,建议优先做两件事:
- 继续验证更多业务指令在华为安卓 APP 下的稳定性;
- 继续验证设备主动上报
evt在当前延后时序下是否同样稳定。
以上经验分享,你可以把以上信息扔给AI,让AI封装一个符合你项目的bleHelper.js工具类,用于处理BLE通讯。
0 个评论
要回复文章请先登录或注册