去年夏天,我接手了一个宠物定位追踪器的开发项目。客户要求通过手机App实时监测宠物位置,并在超出安全范围时发出警报。这个看似简单的需求背后,隐藏着蓝牙开发中的诸多技术挑战——从设备筛选到信号强度计算,从平台差异处理到性能优化。本文将分享我在这个项目中积累的实战经验,特别是那些官方文档没有明确说明的"坑"和解决方案。
宠物定位项目本质上是一个典型的蓝牙低功耗(BLE)应用场景。我们需要通过手机与宠物佩戴的蓝牙信标(Beacon)进行通信,实时获取信号强度(RSSI)并换算为距离。听起来简单?实际开发中会遇到几个关键问题:
我们的技术选型基于UniApp框架,主要考虑其跨平台特性和相对完善的蓝牙API支持。项目核心架构分为三个层次:
实际开发中发现,UniApp虽然封装了原生蓝牙API,但不同平台的底层实现差异仍然需要开发者手动处理
蓝牙开发的第一步永远是正确初始化和设备发现。这个看似标准的流程中,有几个容易踩坑的地方:
javascript复制async initBluetooth() {
try {
const res = await uni.openBluetoothAdapter()
console.log('蓝牙适配器初始化成功', res)
this.checkBluetoothState()
} catch (err) {
console.error('初始化失败', err)
if (err.errCode === 10001) {
this.showModal('蓝牙未开启', '请打开手机蓝牙功能')
}
// 其他错误处理...
}
}
关键点注意:
原始代码中使用了简单的定时扫描机制,在实际测试中发现两个问题:
改进后的扫描策略:
javascript复制let scanTimer = null
startDiscovery() {
// 先停止可能存在的扫描
uni.stopBluetoothDevicesDiscovery()
// 设置扫描超时
scanTimer = setTimeout(() => {
this.stopDiscovery()
}, 10000) // 10秒后自动停止
uni.startBluetoothDevicesDiscovery({
success: () => {
this.listenDeviceFound()
},
fail: (err) => {
clearTimeout(scanTimer)
console.error('扫描启动失败', err)
}
})
}
stopDiscovery() {
clearTimeout(scanTimer)
uni.stopBluetoothDevicesDiscovery()
}
优化效果:
在宠物定位场景中,准确识别目标设备并计算距离是核心功能。这部分代码的优化直接关系到用户体验。
原始代码中主要通过设备名称(BIOTAG)进行筛选,实际测试中发现几个问题:
改进后的筛选逻辑:
javascript复制filterTargetDevice(devices) {
return devices.filter(device => {
// 基础条件:信号强度有效
if (!device.RSSI || Math.abs(device.RSSI) > 100) return false
// 平台特定条件
if (this.platform === 'android') {
return (
(device.name && device.name.includes('BIOTAG')) ||
(device.deviceId && device.deviceId === this.targetMac)
)
} else {
// iOS设备处理
const advertisData = this.parseAdvertisData(device.advertisData)
return (
advertisData.includes(this.targetMac) ||
(device.name && device.name.includes('BIOTAG'))
)
}
})
}
蓝牙信号强度(RSSI)与距离的关系可以用以下公式表示:
code复制distance = 10^((measuredPower - RSSI)/(10 * n))
其中:
实际项目中,我们采用了动态校准策略:
javascript复制class DistanceCalculator {
constructor() {
this.calibrationData = []
this.envFactor = 2.5 // 初始环境因子
}
// 添加校准数据点(已知距离时的RSSI)
addCalibrationPoint(distance, rssi) {
this.calibrationData.push({ distance, rssi })
this.updateEnvFactor()
}
// 动态更新环境因子
updateEnvFactor() {
if (this.calibrationData.length < 3) return
// 使用最小二乘法计算最佳环境因子
let sumXY = 0, sumX = 0, sumY = 0, sumX2 = 0
const n = this.calibrationData.length
this.calibrationData.forEach(point => {
const x = Math.log10(point.distance)
const y = (point.rssi + 59) / 10 // -59为设备1米处RSSI
sumXY += x * y
sumX += x
sumY += y
sumX2 += x * x
})
this.envFactor = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX)
}
// 计算当前距离
calculate(rssi) {
return Math.pow(10, (-59 - rssi) / (10 * this.envFactor))
}
}
实测效果对比:
| 方法 | 1米误差 | 3米误差 | 5米误差 |
|---|---|---|---|
| 固定公式 | ±0.3m | ±1.2m | ±2.5m |
| 动态校准 | ±0.1m | ±0.5m | ±1.0m |
UniApp虽然号称"一次编写,多端运行",但在蓝牙开发中,平台差异仍然需要特别关注。以下是我们在项目中遇到的主要差异及解决方案:
关键差异点对比:
| 功能点 | Android处理 | iOS处理 |
|---|---|---|
| 设备ID格式 | MAC地址(XX:XX:XX:XX) | UUID格式 |
| advertisData | 需要手动解析 | 可直接读取 |
| 后台扫描 | 有限支持 | 需要特殊权限 |
| 扫描间隔 | 可设置较短 | 建议不低于1秒 |
javascript复制// 统一设备ID处理
normalizeDeviceId(device) {
if (this.platform === 'android') {
return device.deviceId.replace(/:/g, '').toUpperCase()
} else {
// iOS设备处理
const advertisData = this.parseAdvertisData(device.advertisData)
return this.extractMacFromAdData(advertisData)
}
}
// 扫描参数设置
getScanOptions() {
const baseOptions = {
allowDuplicatesKey: false,
success: () => console.log('扫描开始')
}
if (this.platform === 'android') {
return {
...baseOptions,
interval: 500,
duration: 10000
}
} else {
return {
...baseOptions,
interval: 1000
}
}
}
蓝牙应用的性能优化主要集中在扫描策略、事件处理和错误恢复三个方面。以下是我们在项目中总结的最佳实践:
分级扫描策略:
设备发现后的优化:
javascript复制let foundDevices = {}
onDeviceFound(device) {
// 去重处理
const deviceId = this.normalizeDeviceId(device)
if (foundDevices[deviceId]) return
foundDevices[deviceId] = Date.now()
// 超过10秒未更新的设备移除
setTimeout(() => {
delete foundDevices[deviceId]
}, 10000)
}
我们在项目中遇到的典型错误及解决方案:
| 错误码 | 可能原因 | 解决方案 |
|---|---|---|
| 10000 | 未初始化蓝牙适配器 | 检查初始化流程 |
| 10001 | 蓝牙未开启 | 引导用户开启蓝牙 |
| 10002 | 没有找到指定设备 | 检查设备是否在范围内 |
| 10003 | 连接失败 | 重试机制+超时处理 |
| 10004 | 没有找到指定服务 | 确认设备GATT服务 |
| 10005 | 没有找到指定特征值 | 检查特征值UUID |
javascript复制// 内存监控
setInterval(() => {
const memory = wx.getPerformance()
if (memory.usedJSHeapSize > memory.jsHeapSizeLimit * 0.7) {
this.cleanCache()
}
}, 5000)
// 缓存清理
cleanCache() {
this.foundDevices = {}
if (this.scanTimer) {
clearTimeout(this.scanTimer)
this.stopDiscovery()
}
}
在项目上线后的三个月内,我们收集了用户实际使用数据,发现这些优化使应用崩溃率降低了72%,电池消耗减少了45%。特别是在户外复杂环境中,宠物定位的准确率从最初的60%提升到了92%。