uni-app BLE蓝牙通信常见问题
uni-app BLE蓝牙通信常见问题
总结下最近在 ble 通信的时候,碰到的一些奇奇怪怪的问题。这里面会涉及 BLE 协议、网络通信、移动端系统等。
1. createBLEConnection:fail already connect
{"errMsg":"createBLEConnection:fail already connect","errCode":-1,"code":-1}
上一次连接没有断开,APP端要做好连接状态的维护。如果出现这个错误,可以立即调用断开接口进行断开。
2. 数据写入蓝牙成功,但是蓝牙模块无应答
写入代码大致如下:
function write(deviceId, serviceId, characteristicId, arrayBuffer) {
return new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: characteristicId,
value: arrayBuffer,
success(res) {
console.log('writeBLECharacteristicValue success', JSON.stringify(res))
resolve(res)
},
fail(err) {
console.error('writeBLECharacteristicValue error', JSON.stringify(err))
reject(err)
}
})
})
}
这里出现的情况有好几种,可以一个个排查,但是最简单明了的方式是直接抓包看报文。
2.1 安卓手机端蓝牙抓包
如果是安卓手机,抓包比较方便,直接去【设置】> 【开发者选项】> 【打开启用蓝牙HCI信息收集日志】。 这样手机的所有蓝牙通讯相关日志,就可以被抓到文件中。
不同的蓝牙通讯文件保存位置不一样,通常都是在 /sdcard/btsnoop_hci.log 文件里面。 将这个文件发送到电脑上。
adb shell pull /sdcard/btsnoop_hci.log
这样就会把文件传输到电脑上,然后可以使用 Wireshark 进行分析。
2.2 蓝牙模块端抓包
蓝牙模块抓包就稍微更难一点,我们之前是去抓串口的数据。 具体的步骤是从蓝牙模块引 2 根线出来,然后通过转换口连接到电脑的 USB 口上。 然后电脑上安装一个串口监控软件即可,通过串口软件监控报文。
接线需要注意,RX 引脚用于接收数据,TX 引脚用于发送数据。接线的时候注意要交叉接线,比如蓝牙模块的 TX 应接到 RX,蓝牙模块的 RX 应接到 TX。
还有个需要注意的是,波特率要保持一致,否则收到的数据将会乱码。
3. writeBLECharacteristicValue:fail property not support
错误应答:
{
"errMsg": "writeBLECharacteristicValue:fail property not support",
"errCode": 10007
}
写入数据的时候要进行延时,延时主要在以下几个地方:
- 刚连接上设备,不要立即写数据,延时 200-500 毫秒后再进行数据写入。
connectToBleDevice();
sleep(200);
writeDataToBle();
- 如果应用层分帧,不要连续写入数据,每次写完可以适当休眠。
writeDataToBle();
sleep(200);
writeDataToBle();
4. 超过 20个字节的包发下去无响应
在网上搜了很多资料,都是说 BLE 限制了每包是 20个字节,超过的这个大小的发下去可能会被丢掉。
这个限制来自蓝牙核心规范(Bluetooth Core Specification),由蓝牙技术联盟(Bluetooth Special Interest Group, SIG)制定。
BLE 协议中也有 MTU 概念,MTU(Maximum Transmission Unit)定义了在一个 ATT 包中可以传输的最大数据量。默认的 MTU 大小是 23 字节,包括下面的数据:
- 3 字节的 ATT 头
- 20 字节的有效负载(payload)
选择 20 字节作为默认限制是为了在低功耗和传输效率之间取得平衡。
但是从蓝牙 4.2 开始,设备可以协商更大的 MTU,最大可达 512 字节。但这需要双方设备都支持。在 uni-app 中,可以使用 uni.setBLEMTU() 方法来尝试设置更大的 MTU:
uni.setBLEMTU({
deviceId: '你的设备ID',
mtu: 512,
success: function(res) {
console.log('MTU 设置成功,新的 MTU 大小为:', res.mtu)
},
fail: function(err) {
console.error('MTU 设置失败:', err)
}
})
不过在实际的测试中,MTU 设置太大大部分都没有效果。 通过 Wireshark 抓包可以看到,设置太大的 MTU 有时候会被拒绝掉,但是应用层又拿不到这个错误。
建议这个值设置 100左右的值,不要太大。另外需要注意的是,安卓跟IOS 处理 MTU 又有差异。
安卓的默认MTU也是23字节,20字节可以存放应用层数据。原生的安卓可以使用 BluetoothGatt.requestMtu(int mtu) 方法请求更大的MTU。理论上,安卓端支持的最大MTU是517字节,512字节应用层数据。 注意,是安卓端自己支持的,对端不一定支持。
iOS的默认MTU也是23字节,20字节可以存放应用层数据。但是iOS会自动进行MTU协商,不需要开发者手动请求,更坑的是开发者无法直接控制MTU大小。根据 IOS 官网文档,iOS支持的最大 MTU 通常是 185字节,180 个字节的应用层数据。
通过上面两点,如果你要做跨端同时支持 Android 以及 IOS ,那么 MTU 你只需要在 Android 环境下进行调整。
5. 获取ble设备服务出错 no service 10004 没有找到指定服务
出现这个错误,大概率是由于连接设备后,立马就去获取服务,这样获取服务是会失败的。
解决办法:连接成功后,增加延时几百毫秒再获取服务列表。
const deviceId = '设备ID';
function connectAndGetServices(deviceId) {
return new Promise((resolve, reject) => {
// 先连接设备
uni.createBLEConnection({
deviceId: deviceId,
success: (res) => {
console.log('设备连接成功');
// 连接成功后,延时 500 毫秒再获取服务
setTimeout(() => {
uni.getBLEDeviceServices({
deviceId: deviceId,
success: (res) => {
console.log('获取服务列表成功');
resolve(res.services);
},
fail: (error) => {
console.error('获取服务列表失败', error);
reject(error);
}
});
}, 500);
},
fail: (error) => {
console.error('设备连接失败', error);
reject(error);
}
});
});
}
6. 安卓跟IOS平台获取的设备唯一标识不一样
Android 平台通常使用 MAC 地址作为设备的唯一标识,比如 11:22:33:AA:BB:CC。
但是 IOS 不一样,出于隐私考虑,不直接暴露设备的 MAC 地址。iOS 使用一个 UUID作为设备标识,这个 UUID 是由 iOS 系统生成的,格式类似 XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX。
可以区分下处理:
function getDeviceIdentifier(device) {
const platform = uni.getSystemInfoSync().platform;
if (platform === 'android') {
// Android 平台使用 deviceId。通常是 MAC 地址
return device.deviceId;
} else if (platform === 'ios') {
// iOS 平台使用 UUID
return device.uuid;
} else {
// 其他平台可能需要特殊处理
console.warn('Unsupported platform for BLE device identification');
return device.deviceId || device.uuid || '';
}
}
7. createBLEConnection:fail operate time out
连接 Ble 设备后断开,再重新连接就有可能碰到这个错误。
解决办法:这个问题测试多次验证,只能通过开启关闭系统蓝牙功能、重启 APP 等方式恢复。
8. ble 设备返回的数据直接打印不出来
Uniapp 接口传入的值是 ArrayBuffer 类型的数据,这种类型不能直接打印或显示。
这种情况有 2 种方式处理:
- 将 ArrayBuffer 转换为可读的十六进制字符串。
- 使用 Uint8Array 来查看原始字节值。
一般我们会选择第一种方式打印输出:
arrayBufferToHex(buffer) {
return Array.from(new Uint8Array(buffer))
.map(bit => bit.toString(16).padStart(2, '0'))
.join('');
},
9. 在不同的电脑上调试同一份代码效果不一样
升级 Hbuilder X 的版本,两台电脑保持一致。 不同的版本 Hbuilder 打包出来的程序可能会不一样。