保姆级教程:用Uni-App+微信小程序连接智能硬件(蓝牙BLE完整项目代码)
Uni-App+微信小程序蓝牙BLE开发实战:从设备连接到数据解析
在智能硬件蓬勃发展的今天,蓝牙BLE技术已经成为连接各类智能设备的首选方案。作为一名长期耕耘在跨平台开发领域的工程师,我发现Uni-App框架配合微信小程序生态,能够为智能硬件开发者提供一套高效、低成本的解决方案。不同于原生小程序开发,Uni-App的跨端特性让我们可以用同一套代码同时覆盖iOS、Android和微信小程序平台,这在需要快速迭代的智能硬件项目中尤为珍贵。
1. 项目环境搭建与基础配置
1.1 Uni-App项目初始化
首先确保已安装最新版HBuilderX(当前推荐版本3.6.18),这是Uni-App官方推荐的开发工具。创建项目时选择"uni-app"模板,并在manifest.json中配置微信小程序特有的蓝牙权限:
{ "mp-weixin": { "appid": "你的小程序ID", "requiredPrivateInfos": [ "getBLEDeviceServices", "writeBLECharacteristicValue" ], "permission": { "scope.userLocation": { "desc": "需要获取位置信息以使用蓝牙功能" } } } }注意:微信小程序平台要求必须获取位置权限才能使用蓝牙功能,这是很多开发者容易忽略的关键点。
1.2 蓝牙适配器检测与初始化
在pages/index/index.vue中,我们需要先检测设备蓝牙状态。这里我封装了一个更健壮的检查方法:
async checkBluetoothAdapter() { try { const res = await uni.getSystemInfoSync() if (!res.bluetoothEnabled) { await this.showModal('请开启手机蓝牙') return false } const adapterRes = await uni.openBluetoothAdapter() console.log('蓝牙适配器初始化成功', adapterRes) // 监听蓝牙适配器状态变化 uni.onBluetoothAdapterStateChange((state) => { if (!state.available) { this.showToast('蓝牙适配器不可用') } }) return true } catch (err) { console.error('蓝牙初始化失败', err) this.showToast('蓝牙初始化失败,请检查权限') return false } }2. 设备发现与连接管理
2.1 智能设备扫描策略
针对智能硬件常见的AA前缀设备名过滤,我优化了扫描逻辑,增加了信号强度(RSSI)筛选:
startScan() { this.scanTimer = setInterval(() => { uni.getBluetoothDevices({ success: (res) => { const validDevices = res.devices .filter(device => (device.name || device.localName)?.startsWith('AA')) .sort((a, b) => b.RSSI - a.RSSI) // 按信号强度排序 this.deviceList = validDevices.map(device => ({ ...device, connecting: false })) } }) }, 1500) uni.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true, success: () => { setTimeout(() => this.stopScan(), 10000) // 10秒后自动停止扫描 } }) }2.2 稳定的设备连接实现
连接智能硬件时,我发现很多开发者会遇到连接不稳定的问题。经过多次测试,总结出以下最佳实践:
- 连接超时处理:添加15秒超时机制
- 自动重连策略:失败后自动重试2次
- 状态同步管理:确保UI与连接状态一致
async connectDevice(deviceId) { this.currentDevice.connecting = true try { const res = await Promise.race([ uni.createBLEConnection({ deviceId }), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 15000)) ]) this.onConnectSuccess(deviceId) } catch (err) { console.error('连接失败', err) if (this.retryCount < 2) { this.retryCount++ await this.connectDevice(deviceId) } else { this.showToast('连接失败,请重试') } } finally { this.currentDevice.connecting = false this.retryCount = 0 } }3. 服务发现与数据通信
3.1 服务与特征值解析
智能硬件通常会定义特定的服务UUID和特征值。以常见的智能温湿度计为例:
| 服务类型 | UUID | 说明 |
|---|---|---|
| 主服务 | 0xFE60 | 设备主服务 |
| 数据服务 | 0xFE61 | 温湿度数据传输 |
| 配置服务 | 0xFE62 | 设备参数配置 |
获取特征值的代码需要处理Android和iOS的平台差异:
async getCharacteristics(deviceId, serviceId) { const res = await uni.getBLEDeviceCharacteristics({ deviceId, serviceId }) const characteristics = res.characteristics.map(c => { // 处理iOS平台特征值属性表示方式 const properties = [] if (c.properties.notify) properties.push('notify') if (c.properties.write) properties.push('write') if (c.properties.read) properties.push('read') return { uuid: c.uuid, properties } }) return characteristics }3.2 数据订阅与实时处理
智能硬件数据通常通过通知(notify)方式推送。这里分享一个处理16进制温湿度数据的完整方案:
enableNotification(serviceId, characteristicId) { uni.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId, characteristicId, state: true, success: () => { uni.onBLECharacteristicValueChange((res) => { const hexData = this.ab2hex(res.value) this.processSensorData(hexData) }) } }) } processSensorData(hexStr) { // 假设数据格式:01 23 45 67 // 其中0123表示温度,4567表示湿度 const tempHex = hexStr.substr(0, 4) const humiHex = hexStr.substr(4, 4) this.temperature = this.hexToTemperature(tempHex) this.humidity = this.hexToHumidity(humiHex) } hexToTemperature(hex) { // 将16进制转换为实际温度值 const rawValue = parseInt(hex, 16) return (rawValue / 100).toFixed(1) // 假设数据放大100倍传输 }4. 实战优化与异常处理
4.1 跨平台兼容性方案
在真实项目中,我发现不同平台对蓝牙API的实现存在差异。以下是关键兼容性处理点:
- iOS连接限制:iOS系统不允许同时连接多个BLE设备
- Android服务发现:某些Android设备需要延迟获取服务
- 微信小程序限制:单次数据传输不能超过20字节
// 分片发送大数据 async sendLargeData(data, chunkSize = 20) { for (let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, i + chunkSize) await this.writeBLECharacteristicValue(chunk) await this.delay(50) // 添加适当延迟 } } // 平台特定的连接处理 async platformSpecificConnect(deviceId) { if (uni.getSystemInfoSync().platform === 'ios') { await this.disconnectAll() // iOS需要先断开已有连接 } return this.connectDevice(deviceId) }4.2 常见问题排查指南
根据实战经验,整理出蓝牙开发中最常遇到的5大问题及解决方案:
监听不到设备通知
- 检查特征值是否支持notify
- 确认已成功启用通知(notifyBLECharacteristicValueChange)
- 尝试添加100-200ms延迟
写入数据无响应
- 确认特征值支持write
- 尝试使用writeNoResponse类型
- 检查数据格式是否符合硬件要求
设备频繁断开连接
- 检查设备电量
- 优化重连机制
- 降低数据传输频率
Android设备服务发现失败
- 添加1-2秒延迟后再获取服务
- 确认蓝牙权限已正确获取
iOS设备连接限制
- 确保同一时间只维持一个活跃连接
- 及时释放不再使用的连接
// 健壮的写入数据实现 async writeData(data) { try { const buffer = this.str2ab(data) await uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeCharId, value: buffer, writeType: 'writeNoResponse' }) await this.delay(150) // 写入后适当延迟 } catch (err) { console.error('写入失败', err) if (err.errCode === 10008) { // 连接断开错误码 this.reconnect() } } }5. 项目架构与代码优化
5.1 可复用的蓝牙管理组件
为了提升代码复用性,我将核心蓝牙功能封装为独立组件ble-manager:
// components/ble-manager/index.vue export default { props: { deviceFilter: { type: Function, default: device => device.name?.startsWith('AA') } }, data() { return { state: 'disconnected', // 连接状态机 devices: [], currentDevice: null } }, methods: { // 暴露给父组件的方法 async startScan() { if (this.state !== 'disconnected') return this.state = 'scanning' await this._startScan() }, async connect(deviceId) { this.state = 'connecting' await this._connectDevice(deviceId) } } }5.2 状态管理与事件总线
复杂项目建议使用Vuex或Pinia管理蓝牙状态。以下是一个精简的状态设计:
// store/bluetooth.js export const useBluetoothStore = defineStore('bluetooth', { state: () => ({ devices: [], connectedDevice: null, services: [], characteristics: [] }), actions: { async discoverDevices() { const devices = await bluetoothApi.scan() this.devices = devices }, async connectDevice(deviceId) { this.connectedDevice = await bluetoothApi.connect(deviceId) } } })在项目中使用事件总线处理蓝牙通知:
// utils/event-bus.js import mitt from 'mitt' const emitter = mitt() export const onBLEDataReceived = (callback) => { emitter.on('ble-data', callback) } export const emitBLEData = (data) => { emitter.emit('ble-data', data) } // 在蓝牙回调中 uni.onBLECharacteristicValueChange((res) => { const data = processData(res.value) emitBLEData(data) })6. 性能优化与用户体验
6.1 低功耗连接策略
智能硬件通常对功耗敏感,我们可以优化连接参数:
// 设置连接参数(仅Android支持) function setConnectionParams(deviceId, interval = 60, latency = 0, timeout = 500) { uni.setBLEMTU({ deviceId, mtu: 247 // 设置最大传输单元 }) // Android特有API if (uni.getSystemInfoSync().platform === 'android') { uni.setBLEConnectionParameter({ deviceId, interval, latency, timeout }) } }6.2 数据缓存与批量处理
对于频繁上报数据的设备,建议实现数据批处理:
class DataBatcher { constructor(maxSize = 10, timeout = 1000) { this.batch = [] this.maxSize = maxSize this.timeout = timeout this.timer = null } add(data) { this.batch.push(data) if (this.batch.length >= this.maxSize) { this.flush() return } if (!this.timer) { this.timer = setTimeout(() => this.flush(), this.timeout) } } flush() { if (this.timer) { clearTimeout(this.timer) this.timer = null } if (this.batch.length > 0) { const batchCopy = [...this.batch] this.batch = [] emitBLEData(batchCopy) } } }7. 安全策略与数据验证
7.1 数据传输加密
虽然BLE本身提供了一定安全机制,但对敏感数据建议额外加密:
// 简单的AES加密示例(实际项目应使用更安全的密钥管理) import CryptoJS from 'crypto-js' const SECRET_KEY = 'your-secret-key' function encryptData(data) { return CryptoJS.AES.encrypt(JSON.stringify(data), SECRET_KEY).toString() } function decryptData(ciphertext) { const bytes = CryptoJS.AES.decrypt(ciphertext, SECRET_KEY) return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)) }7.2 数据完整性校验
为预防数据损坏或干扰,实现CRC校验:
function calculateCRC(data) { let crc = 0xFFFF for (let i = 0; i < data.length; i++) { crc ^= data[i] for (let j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001 } else { crc = crc >> 1 } } } return crc } function verifyData(data, receivedCRC) { return calculateCRC(data) === receivedCRC }8. 测试与调试技巧
8.1 使用BLE调试工具
推荐以下工具辅助开发:
- nRF Connect:功能强大的BLE调试APP
- LightBlue:iOS平台BLE探索工具
- Wireshark+BLE嗅探器:高级数据包分析
8.2 模拟器测试策略
虽然真机测试必不可少,但模拟器可以快速验证逻辑:
// 开发环境模拟BLE设备 if (process.env.NODE_ENV === 'development') { const mockDevice = { name: 'AA_TempSensor_Mock', deviceId: 'mock-device-id', services: [ { uuid: '0000FE60-0000-1000-8000-00805F9B34FB', isPrimary: true } ] } window.mockBLE = { connect: () => new Promise(resolve => { setTimeout(() => resolve(mockDevice), 500) }), sendData: (data) => { console.log('模拟设备接收:', data) } } }9. 项目部署与性能监控
9.1 生产环境优化建议
- 压缩依赖库:使用uni-app的优化配置减少包体积
- 按需加载:将蓝牙功能拆分为独立分包
- 错误监控:集成Sentry等错误追踪系统
9.2 用户体验监控指标
建立关键性能指标(KPI)监控体系:
| 指标 | 目标值 | 监控方式 |
|---|---|---|
| 连接成功率 | >95% | 埋点统计 |
| 平均连接时间 | <3s | 性能API |
| 数据传输成功率 | >99% | 数据校验 |
| 异常断开率 | <1% | 错误日志 |
// 性能监控示例 const startTime = Date.now() try { await connectDevice(deviceId) const connectTime = Date.now() - startTime reportMetric('connect_time', connectTime) } catch (err) { reportError('connect_failed', err) }10. 扩展思路与进阶方案
10.1 多设备组网管理
对于需要连接多个设备的场景,实现设备池管理:
class DevicePool { constructor(maxConnections = 3) { this.devices = new Map() this.maxConnections = maxConnections } async addDevice(deviceId) { if (this.devices.size >= this.maxConnections) { throw new Error('达到最大连接数') } const device = await connectDevice(deviceId) this.devices.set(deviceId, { device, lastActive: Date.now() }) return device } pruneInactiveDevices(timeout = 30000) { const now = Date.now() for (const [id, entry] of this.devices) { if (now - entry.lastActive > timeout) { this.disconnectDevice(id) this.devices.delete(id) } } } }10.2 OTA固件升级方案
实现蓝牙设备固件无线升级:
async performOTA(deviceId, firmware) { // 1. 进入OTA模式 await this.writeData(ENTER_OTA_MODE_CMD) // 2. 分片发送固件 const chunkSize = 128 for (let offset = 0; offset < firmware.length; offset += chunkSize) { const chunk = firmware.slice(offset, offset + chunkSize) await this.writeData(chunk) // 验证CRC const crc = calculateCRC(chunk) await this.verifyCRC(crc) // 更新进度 this.otaProgress = Math.min( 100, Math.round((offset / firmware.length) * 100) ) } // 3. 触发重启 await this.writeData(REBOOT_CMD) }在实际项目中,我发现最有效的调试方式是结合硬件日志和前端调试工具。当遇到通信问题时,先确认硬件端是否正确接收和响应了数据,再检查小程序端的处理逻辑。这种双向验证的方法能快速定位大多数通信问题。
