第一次接触UniApp蓝牙开发时,我完全被各种专业术语搞懵了。deviceId、serviceId、characteristic这些概念就像天书一样,直到真正动手做了几个项目才明白其中的门道。现在回头看,其实只要掌握几个关键点,就能快速搭建起蓝牙通信的基础框架。
最核心的准备工作是确认设备支持BLE(低功耗蓝牙)协议。市面上很多老式蓝牙设备使用的是经典蓝牙协议,这和我们要用的BLE完全不同。判断方法很简单:如果设备说明书上写着"Bluetooth 4.0"或更高版本,通常就支持BLE。我遇到过不少开发者在这个基础问题上栽跟头,花几小时调试才发现协议不兼容。
初始化蓝牙模块是整个流程的第一步,也是容易出问题的环节。很多新手会忽略用户可能没开启手机蓝牙的情况,直接导致应用崩溃。正确的做法应该像这样:
javascript复制uni.openBluetoothAdapter({
success(res) {
console.log('蓝牙模块初始化成功')
this.startDiscovery()
},
fail(err) {
console.error('初始化失败:', err)
if (err.errCode === 10001) {
uni.showToast({ title: '请开启手机蓝牙', icon: 'none' })
}
}
})
这里有个实用技巧:在fail回调中根据errCode给出具体提示。比如代码10001表示蓝牙适配器不可用,可能是用户没开蓝牙;10002表示当前设备不支持蓝牙。这些细节处理能让用户体验提升好几个档次。
设备搜索环节要注意性能优化。很多开发者会忘记停止搜索,导致手机电量快速消耗。我建议设置超时自动停止,比如搜索10秒后无论是否找到设备都调用uni.stopBluetoothDevicesDiscovery()。实测发现,持续搜索会使iOS设备发热明显,Android耗电也会加快。
连接蓝牙设备看似简单,实则暗藏玄机。我做过压力测试,在50次连续连接中,有3-5次会莫名其妙失败。后来发现这是蓝牙协议栈的固有特性,必须通过重试机制来解决。
最可靠的连接方案应该包含三个关键要素:超时控制、自动重连和异常处理。下面这段代码是我在多个项目中验证过的稳定版本:
javascript复制let retryCount = 0
const connectDevice = (deviceId) => {
uni.createBLEConnection({
deviceId,
success: () => {
retryCount = 0
this.getServices(deviceId)
},
fail: () => {
if (retryCount++ < 3) {
setTimeout(() => connectDevice(deviceId), 1000)
} else {
uni.showToast({ title: '连接失败', icon: 'none' })
}
}
})
}
这段代码实现了:1) 失败后自动重试3次;2) 每次间隔1秒;3) 最终仍失败则提示用户。实际项目中,我还添加了连接超时计时器,防止某些机型上的"僵尸连接"状态。
获取服务(service)和特征值(characteristic)时,有个容易踩的坑:不同厂商对UUID的处理方式不同。有些设备使用标准的16位UUID(如0x180D),有些则用128位自定义UUID。我建议在代码中统一处理:
javascript复制function normalizeUUID(uuid) {
return uuid.toLowerCase().replace(/-/g, '')
}
// 使用时
const targetService = services.find(s =>
normalizeUUID(s.uuid) === normalizeUUID('0000180D-0000-1000-8000-00805F9B34FB')
)
特征值权限判断也很关键。发送数据需要特征值有write属性,接收数据则需要notify或indicate属性。我曾浪费半天时间调试发送功能,最后发现是特征值权限配置错误:
javascript复制const writeCharacteristic = characteristics.find(c => {
const props = c.properties || {}
return props.write && !props.writeNoResponse
})
蓝牙通信最复杂的部分莫过于数据收发处理。由于MTU(最大传输单元)限制,BLE协议每次只能传输20字节左右数据。这意味着任何稍大的数据包都需要拆包发送和组包接收。
数据封包方案我推荐采用"长度+类型+数据+校验"的格式。下面是一个经过验证的封包函数:
javascript复制function packData(commandType, payload) {
const HEADER_SIZE = 3
const MAX_PAYLOAD = 17 // 20 - 3
const chunks = []
// 拆分大数据包
for (let i = 0; i < payload.length; i += MAX_PAYLOAD) {
const chunk = payload.slice(i, i + MAX_PAYLOAD)
const buffer = new ArrayBuffer(20)
const view = new DataView(buffer)
// 设置包头
view.setUint8(0, 0xAA) // 起始标志
view.setUint8(1, commandType)
view.setUint8(2, chunk.length)
// 填充数据
chunk.forEach((byte, index) => {
view.setUint8(HEADER_SIZE + index, byte)
})
chunks.push(buffer)
}
return chunks
}
接收端处理更复杂,需要处理三种特殊情况:1) 数据分包;2) 数据粘包;3) 数据错误。我的解决方案是使用状态机管理接收过程:
javascript复制let recvBuffer = []
let currentCommand = null
uni.onBLECharacteristicValueChange(res => {
const data = new Uint8Array(res.value)
// 包头检测
if (data[0] === 0xAA && !currentCommand) {
currentCommand = {
type: data[1],
length: data[2],
data: Array.from(data.slice(3, 3 + data[2]))
}
return
}
// 数据包拼接
if (currentCommand) {
const remaining = currentCommand.length - currentCommand.data.length
const append = Array.from(data.slice(0, remaining))
currentCommand.data.push(...append)
// 包尾处理
if (currentCommand.data.length >= currentCommand.length) {
recvBuffer.push(currentCommand)
currentCommand = null
this.processCompletePackets()
}
}
})
实际项目中还需要添加超时重置机制,防止半包状态卡死。我通常设置3秒超时,超过时间就清空currentCommand。
在实验室能跑通的代码,到真实环境可能完全不行。根据我的实战经验,这些稳定性优化措施必不可少:
连接保活机制是最关键的。BLE连接随时可能断开,原因包括:设备超出范围、系统休眠、其他应用干扰等。我的方案是:
javascript复制// 心跳实现示例
let heartbeatTimer = null
function startHeartbeat() {
heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.sendCommand(HEARTBEAT_CMD)
}
}, 30000)
}
uni.onBLEConnectionStateChange(res => {
if (!res.connected) {
this.reconnect()
}
})
信号强度处理也很重要。RSSI(接收信号强度指示)可以预估连接质量。我通常在UI上显示信号强度,并在低于-85dBm时提示用户靠近设备:
javascript复制setInterval(() => {
uni.getBLEDeviceRSSI({
deviceId: this.deviceId,
success: (res) => {
this.rssi = res.RSSI
if (res.RSSI < -85) {
this.showWeakSignalWarning()
}
}
})
}, 2000)
错误恢复策略需要分层设计:
javascript复制async sendWithRetry(command, retries = 3) {
try {
await this.sendCommand(command)
} catch (err) {
if (retries > 0) {
await delay(1000)
return this.sendWithRetry(command, retries - 1)
}
throw err
}
}
在Android平台上,还需要特别注意后台运行限制。建议:
蓝牙调试比普通网络调试复杂得多,因为没有直接可用的抓包工具。我总结了一套实用的调试方法:
日志增强是基础中的基础。除了常规的console.log,我还会记录:
javascript复制function enhancedLog(tag, message, data) {
const timestamp = new Date().toISOString()
let log = `[${timestamp}] ${tag}: ${message}`
if (data instanceof ArrayBuffer) {
log += ` DATA:${ab2hex(data)}`
}
console.log(log)
this.saveToLogFile(log) // 持久化存储
}
跨平台差异处理需要特别注意。iOS和Android在蓝牙实现上有不少差异:
javascript复制// Android位置权限检查
function checkAndroidPermissions() {
return new Promise((resolve) => {
if (uni.getSystemInfoSync().platform === 'android') {
uni.authorize({
scope: 'scope.location',
success: resolve,
fail: () => {
uni.showModal({
content: '需要位置权限才能扫描蓝牙设备',
success: (res) => {
if (res.confirm) {
uni.openSetting()
}
}
})
}
})
} else {
resolve()
}
})
}
性能优化方面,有几个实测有效的技巧:
javascript复制// 批量发送优化
async sendBulkCommands(commands) {
for (const cmd of commands) {
await this.sendCommand(cmd)
await delay(50) // 关键间隔
}
}
内存管理也很重要,特别是长时间运行的App要注意:
javascript复制// 正确的事件监听管理
function setupListeners() {
this._listeners = {
onConnect: res => this.handleConnect(res),
onDisconnect: res => this.handleDisconnect(res)
}
uni.onBLEConnectionStateChange(this._listeners.onConnect)
}
function cleanup() {
uni.offBLEConnectionStateChange(this._listeners.onConnect)
// 其他清理...
}
去年我负责开发了一个智能家居控制系统,需要同时管理多个蓝牙设备。这个项目让我对UniApp蓝牙开发有了更深的理解,特别是多设备管理方面。
设备管理架构采用集中式控制器模式:
javascript复制class DeviceManager {
constructor() {
this.devices = new Map()
this.connection = new ConnectionManager()
}
addDevice(deviceId) {
if (!this.devices.has(deviceId)) {
const device = new BluetoothDevice(deviceId, this.connection)
this.devices.set(deviceId, device)
}
return this.devices.get(deviceId)
}
removeDevice(deviceId) {
const device = this.devices.get(deviceId)
if (device) {
device.disconnect()
this.devices.delete(deviceId)
}
}
}
状态同步是另一个挑战。当多个界面都需要显示设备状态时,我采用发布-订阅模式:
javascript复制class DeviceState {
constructor() {
this._state = {}
this._subscribers = []
}
update(key, value) {
this._state[key] = value
this._notify(key)
}
subscribe(callback) {
this._subscribers.push(callback)
return () => {
this._subscribers = this._subscribers.filter(cb => cb !== callback)
}
}
_notify(changedKey) {
this._subscribers.forEach(cb => cb(changedKey, this._state[changedKey]))
}
}
固件升级功能特别考验蓝牙通信的稳定性。我们的方案是:
javascript复制async updateFirmware(device, firmware) {
const chunks = splitIntoChunks(firmware)
let offset = await this.getCurrentOffset(device)
while (offset < chunks.length) {
try {
await this.sendChunk(device, chunks[offset])
offset = await this.confirmChunk(device, offset)
} catch (err) {
await this.handleUpdateError(err, device)
}
}
}
这个项目上线后稳定运行了8个月,日均活跃设备超过5000台。最关键的经验是:蓝牙通信一定要做到各环节可监控、可恢复。我们实现了完整的日志上报系统,能准确追踪每个指令的生命周期。