点对点连接流程
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 / Amorlinkvex | VxMi Protocol | 6e400001-b5a3-f393-e0a9-e50e24dcca9e |
| Easy / EasyLive | EasyLive Protocol | fff0 |
其他(isPrivate=1) | PrivateProtocol | 0000ff00-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)
}
},
() => {}
)
})
}