蓝牙
以 uni-app 为例,用 Pinia Store 统一管理蓝牙低功耗(BLE)的完整生命周期:权限检测、设备扫描、连接建立和特征值读取。Store 在创建时注册三个全局监听器,页面卸载时调用 closeBluetoothAdapter() 统一释放资源。
类型与工具函数
DeviceType 扩展 uni-app 标准设备信息,新增 macAddress 字段。iOS 的 deviceId 是系统分配的 UUID,无法直接识别设备,需从广播数据(advertisData)解析真实 MAC 地址。ab2hex 将 ArrayBuffer 转换为十六进制字符串,用于特征值的可读展示。
export interface DeviceType extends UniNamespace.BluetoothDeviceInfo {
macAddress?: string
}
function ab2hex(buffer: any) {
const hexArr = Array.prototype.map.call(new Uint8Array(buffer), (bit) => {
return `00${bit.toString(16)}`.slice(-2)
})
return hexArr.join('')
}核心状态
Store 对外暴露以下响应式状态,外部只读,变更只通过 Store 方法触发。
| 状态 | 类型 | 说明 |
|---|---|---|
connected | Ref<boolean> | BLE 是否已连接 |
devices | Ref<DeviceType[]> | 扫描到的设备列表 |
connectedDevice | Ref<DeviceType | undefined> | 当前连接的设备 |
bluetoothPermissions | Ref<boolean> | 蓝牙权限是否已授权 |
bluetoothAvailable | Ref<boolean> | 蓝牙适配器是否可用 |
discoveryStarted | Ref<boolean> | 是否正在扫描 |
chs | Ref<{ uuid: string; value: any }[]> | 特征值列表 |
devicesSortBySignal 过滤出目标设备名(BLUETOOTH BP),按信号强度(RSSI 绝对值越小越强)升序排列,并将 RSSI 转换为 0–100 的信号强度百分比。
const devicesSortBySignal = computed(() => {
return devices.value
.sort((a, b) => Math.abs(a.RSSI) - Math.abs(b.RSSI))
.filter((d) => {
return (
d.name?.toLocaleUpperCase() === DEVICE_NAME ||
d.localName?.toLocaleUpperCase() === DEVICE_NAME
)
})
.map((e) => ({ ...e, signal: Math.abs(100 + e.RSSI) }))
})权限管理
getBluetoothPermissions(authorize?) 先调用 uni.getSetting 查询当前授权状态。若未授权且 authorize 为 true,则调用 uni.authorize 向用户发起请求。用户拒绝后,引导其通过 authorizeBluetooth() 进入系统设置手动开启。
function getBluetoothPermissions(authorize = false) {
return uni
.getSetting()
.then((res) => {
const authSetting = res.authSetting as unknown as Record<string, boolean>
if (
Object.hasOwn(authSetting, 'scope.bluetooth') &&
authSetting['scope.bluetooth'] === true
) {
bluetoothPermissions.value = true
} else if (authorize) {
return uni.authorize({ scope: 'scope.bluetooth' })
} else {
bluetoothPermissions.value = false
}
})
.then((res) => {
if (res !== undefined) bluetoothPermissions.value = true
})
.catch(() => {
bluetoothPermissions.value = false
})
}
function authorizeBluetooth() {
uni.openSetting({ withSubscriptions: true })
}设备扫描
openBluetoothAdapter() 初始化蓝牙模块并启动扫描。失败时根据错误码分支处理:
errCode 10001:蓝牙未开启,注册适配器状态监听,待用户开启后自动重试errno 1509008:缺少位置权限,关闭适配器并提示用户
onBluetoothDeviceFound() 监听新设备,按 deviceId 去重更新列表:已存在则更新,否则追加。
async function openBluetoothAdapter() {
return uni
.openBluetoothAdapter()
.then(() => startBluetoothDevicesDiscovery())
.catch((e) => {
if (e.errCode === 10001) {
bluetoothAvailable.value = false
watchBluetoothAdapterStateChange((res) => {
if (res.available) return startBluetoothDevicesDiscovery()
else resetStatus()
})
} else if (e.errno === 1509008) {
closeBluetoothAdapter()
throw new Error('请打开[微信]位置权限后重试')
} else {
handleError(e)
}
})
}
function startBluetoothDevicesDiscovery() {
if (discoveryStarted.value) return
discoveryStarted.value = true
return uni
.stopBluetoothDevicesDiscovery()
.then(() => uni.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true }))
.then(() => onBluetoothDeviceFound())
}
function onBluetoothDeviceFound() {
uni.onBluetoothDeviceFound((res) => {
for (let index = 0; index < res.devices.length; index++) {
const device = res.devices[index]
if (!device.name && !device.localName) break
const macAddress = macRecord[device.deviceId]
const idx = devices.value
.map((e) => e.deviceId)
.findIndex((id) => id === device.deviceId)
if (idx === -1) {
devices.value.push({ ...device, macAddress })
} else {
devices.value[idx] = { ...device, macAddress }
}
}
})
}
async function stopBluetoothDevicesDiscovery() {
await uni.stopBluetoothDevicesDiscovery()
discoveryStarted.value = false
}
async function openOrCloseDevicesDiscovery() {
if (discoveryStarted.value) {
await stopBluetoothDevicesDiscovery()
} else {
await openBluetoothAdapter()
}
}设备连接与服务发现
createBLEConnection() 按顺序执行:断开已有连接 → 停止扫描 → 建立新连接 → 自动获取服务和特征值。
服务发现取第一个主服务(isPrimary),遍历其特征值:支持 read 的立即读取一次;支持 notify 或 indicate 的开启推送订阅。
async function createBLEConnection(ds: DeviceType, autoGetBLEDeviceServices = true) {
return closeBLEConnection()
.then(() => stopBluetoothDevicesDiscovery())
.then(() => uni.createBLEConnection({ deviceId: ds.deviceId }))
.then(() => {
connectedDevice.value = ds
connected.value = true
autoGetBLEDeviceServices && getBLEDeviceServices(ds.deviceId)
return ds.deviceId
})
.catch(handleError)
}
function closeBLEConnection() {
return new Promise((resolve, reject) => {
if (connectedDevice.value) {
uni
.closeBLEConnection({ deviceId: connectedDevice.value.deviceId })
.then(() => {
connectedDevice.value = undefined
chs.value = []
canWrite.value = false
resolve('')
})
.catch(reject)
} else {
resolve('')
}
})
}
async function handleToggleBluetoothDevice(_device: DeviceType) {
showLoading()
try {
if (connectedDevice.value) {
await closeBLEConnection()
} else {
await createBLEConnection(_device)
}
hideLoading()
} catch (e: any) {
if (e?.errCode === 10003) {
showToastError('设备连接失败!')
} else {
handleError(e)
}
}
}
function getBLEDeviceServices(deviceId: string) {
return uni
.getBLEDeviceServices({ deviceId })
.then((res) => {
for (let i = 0; i < res.services.length; i++) {
if (res.services[i].isPrimary) {
getBLEDeviceCharacteristics(deviceId, res.services[i].uuid)
return
}
}
})
.catch(handleError)
}
function getBLEDeviceCharacteristics(deviceId: string, serviceId: string) {
uni
.getBLEDeviceCharacteristics({ deviceId, serviceId })
.then((res) => {
for (let i = 0; i < res.characteristics.length; i++) {
const item = res.characteristics[i]
if (item.properties.read) {
uni.readBLECharacteristicValue({ deviceId, serviceId, characteristicId: item.uuid })
}
if (item.properties.notify || item.properties.indicate) {
uni.notifyBLECharacteristicValueChange({
deviceId,
serviceId,
characteristicId: item.uuid,
state: true,
})
}
}
})
.catch(handleError)
}特征值监听
watchBLECharacteristicValueChange() 在 Store 初始化时调用,内部维护 chs 列表,按特征值 UUID 去重更新。onBLECharacteristicValueChange(cb) 供外部组件订阅,将原始 ArrayBuffer 转为十六进制字符串后回调。
function watchBLECharacteristicValueChange() {
uni.onBLECharacteristicValueChange((characteristic) => {
const idx = chs.value.findIndex((e) => e.uuid === characteristic.characteristicId)
if (idx === -1) {
chs.value.push({
uuid: characteristic.characteristicId,
value: ab2hex(characteristic.value),
})
} else {
chs.value[idx] = {
uuid: characteristic.characteristicId,
value: ab2hex(characteristic.value),
}
}
})
}
function onBLECharacteristicValueChange(
cb: (data: { uuid: string; value: any }) => void,
) {
uni.onBLECharacteristicValueChange((characteristic) => {
cb?.({
uuid: characteristic.characteristicId,
value: ab2hex(characteristic.value),
})
})
}MAC 地址解析
iOS 的 deviceId 是系统生成的 UUID,无法用于设备唯一识别。getMacAddr() 仅在非 Android 平台执行,从广播数据(advertisData)中逆序提取字节,拼接为 XX:XX:XX:XX:XX:XX 格式的 MAC 地址。通过 throttle 节流,避免设备列表高频更新时重复触发。
function getMacAddr() {
if (isAndroid) return
uni.getBluetoothDevices().then((res) => {
const _devices = res.devices.filter(
(d) => d.name.toLocaleUpperCase() === DEVICE_NAME,
)
for (let i = 0; i < _devices.length; i++) {
const e = _devices[i]
if (e.advertisData) {
const byteArray = new Uint8Array(e.advertisData)
const macAddress = Array.from(byteArray)
.map((byte) => byte.toString(16).toUpperCase().padStart(2, '0'))
.reverse()
.join(':')
macRecord[e.deviceId] = macAddress
const index = devices.value.findIndex((d) => d.deviceId === e.deviceId)
if (index !== -1) devices.value[index].macAddress = macAddress
}
}
})
}
const _getMacAddr = throttle(getMacAddr, 500)
watch(devices.value, _getMacAddr, { deep: true, immediate: true })初始化与资源释放
init() 在 Store 创建时执行,注册三个全局监听器:
watchBluetoothAdapterStateChange:监听蓝牙开关,关闭时重置所有状态watchBLECharacteristicValueChange:接收特征值推送并更新chs列表watchBLEConnectionStateChange:监听连接断开,自动清理已连接设备信息
调用 closeBluetoothAdapter() 关闭蓝牙适配器并释放所有监听器。
function watchBluetoothAdapterStateChange(
cb?: (res: UniApp.OnBluetoothAdapterStateChangeResult) => void,
) {
uni.offBluetoothAdapterStateChange()
uni.onBluetoothAdapterStateChange((res) => {
if (!res.available) {
resetStatus()
} else {
bluetoothAvailable.value = true
}
cb?.(res)
})
}
function watchBLEConnectionStateChange() {
return uni.onBLEConnectionStateChange((res) => {
if (res.connected === false) {
connected.value = false
if (connectedDevice.value) {
connectedDevice.value = undefined
showToastError('设备已断开连接')
}
}
})
}
function resetStatus() {
bluetoothAvailable.value = false
connectedDevice.value = undefined
discoveryStarted.value = false
chs.value = []
canWrite.value = false
}
function closeBluetoothAdapter() {
uni.offBluetoothDeviceFound()
uni.offBluetoothAdapterStateChange()
uni.closeBluetoothAdapter()
resetStatus()
}
function init() {
watchBluetoothAdapterStateChange()
watchBLECharacteristicValueChange()
watchBLEConnectionStateChange()
}
init()