蓝牙设备对接文档

点对点连接流程

BLE GATT 设备的扫描、连接与服务发现

架构概览

点对点连接通过 UnifiedBluetoothManager 统一管理,它采用适配器模式,在运行时通过 Capacitor.isNativePlatform() 自动检测当前环境,并委托给对应的底层实现:

  • Web 环境 -- 使用 WebBleManager,基于 Web Bluetooth API
  • Native 环境 -- 使用 NativeBleManager,基于 @capacitor-community/bluetooth-le
// UnifiedBluetoothManager 单例模式
export class UnifiedBluetoothManager {
  private static instance: UnifiedBluetoothManager
  private isNative: boolean
  private webManager: typeof webBleManager
  private nativeManager: typeof nativeBleManager

  private constructor() {
    this.isNative = Capacitor.isNativePlatform()
    this.webManager = webBleManager
    this.nativeManager = nativeBleManager
  }

  public static getInstance(): UnifiedBluetoothManager {
    if (!UnifiedBluetoothManager.instance) {
      UnifiedBluetoothManager.instance = new UnifiedBluetoothManager()
    }
    return UnifiedBluetoothManager.instance
  }
}

// 导出单例实例
export const unifiedBluetoothManager = UnifiedBluetoothManager.getInstance()

连接流程图

用户选择点对点设备
  |
  v
startScanCallbackByCode(modelName, deviceModel, onDeviceFound, onScanComplete)
  |
  +-- Web 环境 --+-- Native 环境 --+
  |               |                  |
  v               v                  v
navigator.       BleClient.         BleClient.
bluetooth.       initialize()       requestLEScan()
requestDevice()  |                  |
  |              v                  v
  |              检查蓝牙是否启用     按名称过滤设备
  |              |                  (30秒超时)
  v              v                  |
选择设备         开始扫描             v
  |              |                  匹配 modelName
  +------+-------+                  |
         |                          v
         v                     停止扫描
    匹配设备名称 (includeBleDeviceName)
         |
         v
    handleDeviceConnection()
         |
         v
    connectAuth() / connect()
         |
         v
    建立 GATT 连接
         |
         v
    发现 Service -> 获取特征值
         |
         v
    订阅 Notify -> 等待认证(如需要)
         |
         v
    连接完成,保存设备信息

扫描流程

启动扫描

通过 startScanCallbackByCode 方法启动扫描,根据设备型号名称 modelName 过滤目标设备:

import { unifiedBluetoothManager } from '@/utils/bluetooth/UnifiedBluetoothManager'

const { modelName, isPrivate, isAuth } = device.deviceModel

await unifiedBluetoothManager.startScanCallbackByCode(
  modelName,                    // 设备型号名称,用于名称匹配
  { isPrivate, isAuth },        // 设备模型参数
  (foundDevice) => {            // 设备发现回调
    if (foundDevice) {
      console.log('设备连接成功:', foundDevice.name)
    } else {
      console.log('设备连接失败')
    }
  },
  (devices) => {                // 扫描完成回调
    console.log('扫描完成')
  }
)

Web Bluetooth 扫描

Web 环境使用 navigator.bluetooth.requestDevice() 弹出系统蓝牙选择器:

// WebBleManager.requestDevice()
public async requestDevice(
  modelName: string,
  deviceModel: { isPrivate?: number; isAuth?: number } | any
): Promise<BluetoothDevice | null> {
  // 根据协议类型确定 Service UUID
  const { serviceUUID } = findBleCharacteristic('', modelName, deviceModel?.isPrivate)
  const optionalServices = serviceUUID
    ? [serviceUUID]
    : [WebBleManager.SERVICE_UUID]

  // 弹出系统蓝牙设备选择器
  const device = await navigator.bluetooth!.requestDevice({
    acceptAllDevices: true,
    optionalServices: optionalServices
  })

  return device
}

Web Bluetooth API 要求:

  • 必须在 HTTPS 或 localhost 环境下运行
  • 必须由用户手势(如点击按钮)触发
  • 支持浏览器:Chrome 56+、Edge 79+、Opera 43+
  • 不支持:Firefox、Safari、iOS

Native 扫描

Native 环境使用 Capacitor BLE 插件,支持后台扫描和名称自动过滤:

// 1. 初始化 BLE 客户端
await BleClient.initialize()

// 2. 检查蓝牙状态
const isEnabled = await BleClient.isEnabled()
if (!isEnabled) {
  await BleClient.enable()
}

// 3. 启动扫描(30 秒超时)
await BleClient.requestLEScan(
  { /* scanOptions */ },
  (result: ScanResult) => {
    // 通过 includeBleDeviceName 匹配设备名称
    if (includeBleDeviceName(modelName, result.device.name)) {
      onDeviceFound(result.device)
    }
  }
)

设备名称匹配

使用 includeBleDeviceName 函数判断扫描到的设备是否为目标设备:

export const includeBleDeviceName = (
  modelName: string,
  deviceName: any
): Boolean => {
  if (!modelName || !deviceName) return false
  return deviceName && deviceName.includes(modelName)
}

设备的蓝牙名称需要包含 modelName 字符串才能匹配成功。例如 modelName"PH-001" 的设备,蓝牙广播名可能是 "PH-001_XXXX"

GATT 连接

连接流程

设备匹配成功后,进入 GATT 连接阶段:

// WebBleManager.connect()
public async connect(
  device: BluetoothDevice,
  modelName: string,
  isPrivate: any,
  callback: ConnectCallback
): Promise<void> {
  // 1. 监听断开事件
  device.addEventListener('gattserverdisconnected', () => {
    this.cleanup()
    callback.onResult(false, device)
  })

  // 2. 连接到 GATT 服务器
  this.server = await device.gatt!.connect()

  // 3. 根据协议查找对应的 Service UUID
  const { serviceUUID, writeCharacteristicUUID, nodifyCharacteristicUUID } =
    findBleCharacteristic('', modelName, isPrivate)

  // 4. 获取主服务
  this.service = await this.server.getPrimaryService(
    serviceUUID || WebBleManager.SERVICE_UUID
  )

  // 5. 获取写入特征值
  this.writeCharacteristic = await this.service.getCharacteristic(
    writeCharacteristicUUID || WebBleManager.WRITE_UUID
  )

  // 6. 获取通知特征值
  this.notifyCharacteristic = await this.service.getCharacteristic(
    nodifyCharacteristicUUID || WebBleManager.NOTIFY_UUID
  )

  // 7. 启用通知
  await this.enableNotifications()

  callback.onResult(true, device)
}

协议自动选择

findBleCharacteristic 根据 isPrivate 字段返回对应的 UUID。同时,findBleService 支持多协议服务发现:

// 查找蓝牙服务
export const findBleService = (services: any[]) => {
  return services.find(s =>
    s.uuid.toLowerCase().includes(PrivateProtocol.SERVICE_UUID) ||
    s.uuid.toLowerCase().includes(VxMiProtocol.SERVICE_UUID) ||
    s.uuid.toLowerCase().includes(ELProtocol.SERVICE_UUID)
  )
}
设备名称前缀使用协议Service UUID
Vx / Mi / AmorlinkvexVxMi Protocol6e400001-b5a3-f393-e0a9-e50e24dcca9e
Easy / EasyLiveEasyLive Protocolfff0
其他(isPrivate=1PrivateProtocol0000ff00-0000-1000-8000-00805f9b34fb

通知订阅

连接成功后自动订阅 Notify 特征值以接收设备上报数据:

private async enableNotifications(): Promise<void> {
  this.notifyCharacteristic.addEventListener(
    'characteristicvaluechanged',
    (event: Event) => {
      const characteristic = event.target as BluetoothRemoteGATTCharacteristic
      const value = new Uint8Array(characteristic.value!.buffer)

      // 调用通知回调(优先 notifyCallback,兼容 notifyListener)
      if (this.notifyCallback) {
        this.notifyCallback(value)
      }
      if (this.notifyListener) {
        this.notifyListener.onData(value)
      }
    }
  )

  await this.notifyCharacteristic.startNotifications()
}

带认证的连接

当设备 isAuth === 1 时,使用 connectAuth 方法。连接成功后会自动监听设备认证帧并完成握手:

public async connectAuth(
  device: any,
  isPrivate: any,
  callback: ConnectCallback,
  authCallback: (data: any) => void
): Promise<void> {
  // 设置通知监听器,等待设备认证帧
  this.setNotifyListener({
    onData: (data: Uint8Array) => {
      let payload: Uint8Array
      try {
        payload = FrameCodec.decode(data)
      } catch {
        payload = data
      }

      const notification = PrivateProtocol.parseDeviceNotification(payload)
      if (notification) {
        this.handleDeviceNotification(
          notification,
          authCallback,
          { deviceModel: { isPrivate } }
        )
      }
    }
  })

  await this.connect(device, isPrivate, callback)
}

认证握手的详细过程请参阅 设备认证

自动重连

系统将上次连接的设备信息保存到 localStorage(键名 ble_last_connected_device),支持自动重连:

// 保存的数据结构
interface SavedDevice {
  name: string
  id: string
  modelName?: string
  isPrivate?: number
}

// 连接成功时自动保存
this.saveDeviceToStorage({
  name: this._deviceInfo.name,
  id: this._deviceInfo.id,
  isPrivate
})

// 尝试重连上次连接的设备
public async reconnectLastDevice(
  callback: ConnectCallback,
  authCallback?: (data: any) => void
): Promise<boolean> {
  const lastDevice = this.loadDeviceFromStorage()
  if (!lastDevice) return false

  const device = {
    name: lastDevice.name,
    id: lastDevice.id,
    _nativeDevice: { deviceId: lastDevice.id }
  }

  if (authCallback) {
    await this.connectAuth(device, lastDevice.isPrivate, callback, authCallback)
  } else {
    await this.connect(device, lastDevice.isPrivate, callback)
  }
  return true
}

断开连接

public async disconnect(): Promise<void> {
  if (this.isNative) {
    if (this._deviceInfo) {
      await this.nativeManager.disconnect(this._deviceInfo.id)
    }
    this._isConnected = false
    this._deviceInfo = null
    this.clearDeviceFromStorage()
  } else {
    return this.webManager.disconnect()
  }
}

完整连接示例

import { unifiedBluetoothManager } from '@/utils/bluetooth/UnifiedBluetoothManager'
import { useDeviceStore } from '@/stores/device-store'

async function connectPointToPoint(device: Device): Promise<boolean> {
  const { modelName, isPrivate, isAuth } = device.deviceModel

  return new Promise((resolve) => {
    // 设置 15 秒连接超时
    const timeoutId = setTimeout(() => {
      unifiedBluetoothManager.stopScan()
      resolve(false)
    }, 15000)

    unifiedBluetoothManager.startScanCallbackByCode(
      modelName,
      { isPrivate, isAuth },
      async (foundDevice) => {
        clearTimeout(timeoutId)
        if (foundDevice) {
          const { connectDevice } = useDeviceStore.getState()
          connectDevice(device, 'home')
          resolve(true)
        } else {
          resolve(false)
        }
      },
      () => {}
    )
  })
}