从零到一:在uni-app中构建低功耗蓝牙设备通信全流程(微信小程序通用)
1. 低功耗蓝牙开发基础认知
第一次接触低功耗蓝牙开发时,我盯着文档里那些UUID、特征值之类的术语发懵,这感觉就像突然要和一个说外星语的外星人交流。后来才发现,理解蓝牙通信的关键在于建立正确的认知模型。
低功耗蓝牙(BLE)和我们熟悉的WiFi、4G网络有本质区别。传统网络通信像是打电话,建立连接后双方可以持续对话;而BLE更像是收发快递,每次交互都是独立包裹,且快递员(蓝牙信号)还可能迷路。我在开发智能手环项目时就吃过亏,以为发送指令后设备会立即响应,结果等了半天没反应,后来才明白需要主动监听设备回传的数据包。
uni-app提供的蓝牙API与微信小程序完全一致,这意味着你开发的代码可以无缝迁移。这个设计非常贴心,我去年做的健身器材控制项目,就是先用微信小程序调试蓝牙功能,再移植到uni-app打包成App,整个过程就像复制粘贴那么简单。
2. 开发环境准备
工欲善其事必先利其器,我的HBuilder X总是保持最新版本(当前3.4.7),因为不同版本对蓝牙调试的支持可能有细微差别。记得有次帮客户排查问题,最后发现是旧版IDE的蓝牙适配器初始化存在兼容性问题,更新后立即解决。
创建uni-app项目时,建议选择vue3模板。新版composition API写蓝牙控制逻辑特别顺手,所有功能都可以封装成独立函数。比如我会把蓝牙初始化、设备搜索这些操作都放在setup()里,代码结构清晰得像乐高积木。
真机调试是必须的,模拟器跑不了蓝牙功能。安卓设备要开启定位权限(是的,蓝牙搜索需要定位权限),iOS设备则要注意蓝牙规格限制。我习惯先用安卓手机开发调试,因为iOS对后台蓝牙操作的限制更多,容易踩坑。
3. 蓝牙设备发现与连接
3.1 蓝牙模块初始化
第一次写初始化代码时,我犯了个低级错误——没检查用户是否开启手机蓝牙。结果测试时一直报错10001,查了半天文档才恍然大悟。现在我的初始化函数都会先弹窗提醒用户:
function initBlue() { uni.openBluetoothAdapter({ success(res) { console.log('蓝牙适配器已激活'); startDiscovery(); // 自动开始搜索 }, fail(err) { if (err.code === 10001) { uni.showModal({ title: '提示', content: '请先开启手机蓝牙功能', showCancel: false }) } } }); }3.2 设备搜索优化技巧
搜索附近设备时,不加限制的话会把所有蓝牙设备都列出来,包括那些鼠标、键盘之类的无关设备。后来我学乖了,通过services参数过滤目标设备:
uni.startBluetoothDevicesDiscovery({ services: ['0000FFE0-0000-1000-8000-00805F9B34FB'], // 目标设备服务UUID success(res) { uni.onBluetoothDeviceFound(device => { if(device.devices[0].name === '我的智能秤'){ // 找到目标设备 } }); } });搜索到设备后要立即停止扫描,这个经验是用手机电量换来的。有次忘记调用stopBluetoothDevicesDiscovery,两小时后手机电量直接见底,设备还发烫得能煎鸡蛋。
4. 数据通信实战
4.1 服务与特征值解析
连接设备后要获取服务列表,这里有个坑:某些设备服务需要延迟获取。我在开发中遇到过连接后立即getBLEDeviceServices返回空数组的情况,后来加了个setTimeout就好了:
setTimeout(() => { uni.getBLEDeviceServices({ deviceId: deviceId.value, success(res) { const targetService = res.services.find( s => s.uuid === '0000FFE0-0000-1000-8000-00805F9B34FB' ); } }); }, 1000); // 延迟1秒特征值(characteristic)是通信的核心,每个特征值都有读写属性。硬件工程师应该提供特征值对照表,标明哪个特征值用于发送指令,哪个用于接收数据。没有这个就像没有密码本的情报员,看着一堆乱码干瞪眼。
4.2 数据收发处理
接收到的蓝牙数据是ArrayBuffer类型,需要转换才能读懂。我封装了个万能转换工具函数:
function bufferToString(buffer) { // ArrayBuffer转16进制字符串 const hex = Array.from(new Uint8Array(buffer)) .map(b => b.toString(16).padStart(2, '0')) .join(''); // 16进制转ASCII let str = ''; for (let i = 0; i < hex.length; i += 2) { str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); } return str; }发送数据时要特别注意,字符串需要转成ArrayBuffer。有次发送"ON"指令没转换,设备直接死机,后来发现是数据格式错误导致固件崩溃:
function stringToBuffer(str) { const buffer = new ArrayBuffer(str.length); const view = new DataView(buffer); for (let i = 0; i < str.length; i++) { view.setUint8(i, str.charCodeAt(i)); } return buffer; } uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: stringToBuffer('TEMP:25') // 设置温度指令 });5. 稳定性优化方案
5.1 错误重试机制
蓝牙通信最大的特点就是不稳定。我设计了三重保障机制:首次失败后立即重试,再次失败则延迟重试,第三次失败才报错。这个方案在智能家居项目中将成功率从70%提升到99%:
function safeWrite(data, retry = 0) { uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: data, success() { // 成功处理 }, fail() { if(retry < 2) { setTimeout(() => { safeWrite(data, retry + 1); }, retry * 500); // 延迟重试 } else { uni.showToast({ title: '指令发送失败', icon: 'error' }); } } }); }5.2 连接状态维护
蓝牙连接可能随时断开,需要持续监听连接状态。我在项目中会维护一个心跳检测机制,每隔10秒检查一次连接,发现断开就自动重连:
let heartbeat = null; function startHeartbeat() { heartbeat = setInterval(() => { uni.getBLEDeviceServices({ deviceId, success() {}, // 连接正常 fail() { reconnect(); // 重新连接流程 } }); }, 10000); } function stopHeartbeat() { clearInterval(heartbeat); }实际开发中,蓝牙模块的每个环节都需要异常处理。我的经验法则是:每个API调用都要写fail回调,重要的操作要添加超时检测,关键数据要本地缓存。这些细节决定用户体验的好坏,也是区分初级和高级开发者的重要标准。
