FumadocsZDecode
小程序

蓝牙

以 uni-app 为例,用 Pinia Store 统一管理蓝牙低功耗(BLE)的完整生命周期:权限检测、设备扫描、连接建立和特征值读取。Store 在创建时注册三个全局监听器,页面卸载时调用 closeBluetoothAdapter() 统一释放资源。

类型与工具函数

DeviceType 扩展 uni-app 标准设备信息,新增 macAddress 字段。iOS 的 deviceId 是系统分配的 UUID,无法直接识别设备,需从广播数据(advertisData)解析真实 MAC 地址。ab2hexArrayBuffer 转换为十六进制字符串,用于特征值的可读展示。

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 方法触发。

状态类型说明
connectedRef<boolean>BLE 是否已连接
devicesRef<DeviceType[]>扫描到的设备列表
connectedDeviceRef<DeviceType | undefined>当前连接的设备
bluetoothPermissionsRef<boolean>蓝牙权限是否已授权
bluetoothAvailableRef<boolean>蓝牙适配器是否可用
discoveryStartedRef<boolean>是否正在扫描
chsRef<{ 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 查询当前授权状态。若未授权且 authorizetrue,则调用 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 的立即读取一次;支持 notifyindicate 的开启推送订阅。

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()

On this page