1. 蓝牙通信基础与小程序开发准备
第一次接触蓝牙开发时,我被各种专业术语搞得头晕——UUID、特征值、广播数据...后来发现理解蓝牙通信就像理解快递收发:设备相当于快递站点,服务是不同类型的快递柜(比如顺丰柜、菜鸟柜),特征值就是柜子上的取件码。微信小程序提供的BLE API就是我们的"快递员",负责在手机和硬件设备之间传递数据包。
开发前的三个必备条件:
- 硬件支持BLE 4.0及以上协议(现在市面上90%的智能设备都满足)
- 小程序后台已开通蓝牙权限(在app.json中配置
requiredPrivateInfos: ["getBluetoothAdapterState"]) - 获取设备厂商提供的服务UUID和特征值(就像拿到快递柜的柜号和取件码)
实测中发现个有趣现象:安卓和iOS对蓝牙设备的识别方式完全不同。安卓直接使用MAC地址作为deviceId,而iOS会生成随机UUID。这就导致同一台设备在两个系统上显示不同的标识符,需要特殊处理:
// 统一设备标识显示(不影响实际连接) function formatDeviceId(device) { const isIOS = /ios/i.test(wx.getSystemInfoSync().system); return isIOS ? parseUUID(device.advertisData) : device.deviceId; }提示:测试阶段建议准备安卓和iOS真机各一部,微信开发者工具的蓝牙模拟与实际设备有差异,我曾在模拟器上调试通过的代码,到真机上报错"no characteristic"。
2. 设备扫描与连接实战技巧
扫描蓝牙设备就像用雷达搜索信号,但比想象中更耗电。有次忘记关闭扫描,测试机两小时电量从100%掉到30%。正确操作流程应该是:
- 初始化适配器(打开蓝牙"总开关")
- 开始扫描(启动雷达)
- 发现目标设备后立即停止扫描(关闭雷达)
- 建立连接(握手建立专属通道)
// 推荐扫描配置(带超时自动停止) function startScan(serviceUUIDs) { wx.startBluetoothDevicesDiscovery({ services: serviceUUIDs, // 只扫描特定服务设备 allowDuplicatesKey: false, success: () => { // 设置15秒超时停止 this._scanTimer = setTimeout(() => { wx.stopBluetoothDevicesDiscovery(); }, 15000); // 监听发现设备事件 wx.onBluetoothDeviceFound((res) => { if(res.devices.some(d => d.name?.includes("跳绳"))) { clearTimeout(this._scanTimer); this.connectDevice(res.devices[0]); } }); } }); }避坑指南:
- iOS需要用户授权定位权限才能扫描(安卓仅需蓝牙权限)
- 设备名称可能存储在
device.name或device.localName字段 - 华为手机可能遇到扫描不到设备的情况,尝试关闭手机蓝牙后重新开启
3. 数据通信核心:特征值操作
连接设备后,真正的挑战才开始。就像打通了电话却听不懂对方方言,需要按照特征值的"语言规则"交流。以跳绳设备为例,通常需要处理三种特征值:
- Write特征:下发指令(开始计数、设置模式)
- Notify特征:接收实时数据(当前跳绳次数)
- Read特征:读取设备信息(电量、版本号)
// 完整通信流程示例 async function initCommunication(deviceId) { // 1. 获取服务列表 const services = await wx.getBLEDeviceServices({ deviceId }); const targetService = services.find(s => s.uuid.includes('FFE0')); // 2. 获取特征值 const chars = await wx.getBLEDeviceCharacteristics({ deviceId, serviceId: targetService.uuid }); // 3. 订阅通知特征 const notifyChar = chars.find(c => c.properties.notify); await wx.notifyBLECharacteristicValueChange({ deviceId, serviceId: targetService.uuid, characteristicId: notifyChar.uuid, state: true }); // 4. 监听数据变化 wx.onBLECharacteristicValueChange((res) => { const value = ab2hex(res.value); // ArrayBuffer转16进制 console.log('实时数据:', value); }); }数据转换是最大难点,设备返回的ArrayBuffer需要按协议解析。有次遇到设备返回"0xAA 0xBB 0x01",前两位是帧头,最后一位1表示跳绳一次。关键工具函数:
// ArrayBuffer转16进制字符串 function ab2hex(buffer) { return Array.from(new Uint8Array(buffer)) .map(b => b.toString(16).padStart(2, '0')) .join(' '); } // 字符串转ArrayBuffer(用于下发指令) function str2ab(str) { const buf = new ArrayBuffer(str.length); const view = new Uint8Array(buf); for (let i = 0; i < str.length; i++) { view[i] = str.charCodeAt(i); } return buf; }4. 性能优化与异常处理
在用户实际使用中,会遇到各种边界情况。有用户反馈跳绳数据突然中断,排查发现是手机自动休眠导致蓝牙断开。必须实现的四个监听器:
// 蓝牙适配器状态变化 wx.onBluetoothAdapterStateChange((res) => { if (!res.available) { showToast('蓝牙已关闭,请重新开启'); } }); // 设备连接状态 wx.onBLEConnectionStateChange((res) => { if (!res.connected) { reconnectDevice(); // 实现自动重连逻辑 } }); // 特征值变化错误 wx.onBLECharacteristicValueChangeError((err) => { console.error('监听失败:', err); }); // 系统蓝牙状态 wx.onBluetoothAdapterStateChange((res) => { if (!res.available) { this._resetAllStatus(); } });传输优化技巧:
- 大数据分包发送(每次不超过20字节)
- 增加数据校验位(CRC或累加和校验)
- 关键指令添加重试机制(我通常设置3次重试)
async function sendCommand(cmd, retry = 3) { try { await wx.writeBLECharacteristicValue({ deviceId: this._deviceId, serviceId: this._serviceId, characteristicId: this._writeCharId, value: str2ab(cmd) }); } catch (err) { if (retry > 0) { await new Promise(r => setTimeout(r, 300)); return this.sendCommand(cmd, retry - 1); } throw err; } }5. 完整业务封装示例
经过多个项目迭代,我总结出可复用的蓝牙基类结构:
BLEController ├── 设备管理 │ ├── startScan() // 带过滤的扫描 │ ├── connect() // 自动重连机制 │ └── disconnect() // 资源清理 ├── 数据通信 │ ├── sendCommand() // 指令发送 │ ├── listenData() // 数据监听 │ └── parseData() // 协议解析 └── 状态管理 ├── errorHandler() // 统一错误处理 └── statusMonitor() // 连接状态维护实际业务继承示例(跳绳场景):
class SkipRopeController extends BLEController { constructor() { super({ targetDeviceName: '智能跳绳', serviceUUID: '0000FFE0-0000-1000-8000-00805F9B34FB' }); } // 自定义数据解析 parseData(buffer) { const data = ab2hex(buffer); if (data.startsWith('AA BB')) { return { type: 'count', value: parseInt(data.substr(6,2), 16) }; } return { type: 'unknown', raw: data }; } // 业务方法 async startCounting() { await this.sendCommand('START'); this._startTime = Date.now(); } }在真实项目中,这种封装使得业务代码量减少70%,特别是处理iOS/安卓兼容性时,所有特殊逻辑都集中在基类中。有个值得分享的案例:某次更新后iOS用户突然无法连接,最后发现是新版本蓝牙协议要求特征值必须包含"notify"属性,而在安卓上是可选的,通过基类统一处理避免了这类问题。