expo-duooomi-app/ble/core/BleClient.ts

248 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BleManager, Device, Characteristic, ScanOptions } from 'react-native-ble-plx'
import { Platform } from 'react-native'
import { BleDevice, ConnectionState, BleError, ScanResult } from './types'
import { BLE_UUIDS } from '../protocol/Constants'
import { de } from 'zod/v4/locales'
import { any } from 'zod'
export class BleClient {
private static instance: BleClient
private manager: BleManager | null = null
private connectedDevice: Device | null = null
// Simple event system
private listeners: Map<string, Set<Function>> = new Map()
private constructor() {
if (Platform.OS !== 'web') {
this.manager = new BleManager()
}
}
public static getInstance(): BleClient {
if (!BleClient.instance) {
BleClient.instance = new BleClient()
}
return BleClient.instance
}
public addListener(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)!.add(callback)
}
public removeListener(event: string, callback: Function) {
if (this.listeners.has(event)) {
this.listeners.get(event)!.delete(callback)
}
}
private emit(event: string, ...args: any[]) {
if (this.listeners.has(event)) {
this.listeners.get(event)!.forEach((cb) => cb(...args))
}
}
public async startScan(
serviceUUIDs: string[] | null = null,
options: ScanOptions = {},
onDeviceFound: (result: ScanResult) => void,
): Promise<void> {
if (!this.manager) {
console.warn('BLE not supported on web')
return
}
const state = ((await this.manager.state()) || '').toString()
if (!state.toLowerCase().includes('poweredon')) {
throw new Error(`Bluetooth is not powered on. State: ${state}`)
}
this.manager.startDeviceScan(serviceUUIDs, options, (error, device) => {
if (error) {
this.emit('scanError', error)
return
}
if (device) {
onDeviceFound({
device,
rssi: device.rssi,
localName: device.name,
})
}
})
}
public async getConnectedDevices(serviceUUIDs: string[]): Promise<BleDevice[]> {
if (!this.manager) return []
try {
const devices = await this.manager.connectedDevices(serviceUUIDs)
return devices as BleDevice[]
} catch (e) {
throw this.normalizeError(e)
}
}
public stopScan() {
if (!this.manager) return
this.manager.stopDeviceScan()
}
public async connect(deviceId: string, timeout: number = 30000): Promise<BleDevice> {
if (!this.manager) throw new Error('BLE not supported on web')
// 创建超时 Promise
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Connection timeout after ${timeout / 1000}s`))
}, timeout)
})
try {
this.emit('connectionStateChange', { deviceId, state: ConnectionState.CONNECTING })
// 使用 Promise.race 实现超时控制
const connectPromise = this.manager.connectToDevice(deviceId, { autoConnect: false })
let device = await Promise.race([connectPromise, timeoutPromise])
if (!device) {
throw new Error('Failed to connect: device is null')
}
// 连上后再单独请求 MTU容错处理
if (Platform.OS === 'android') {
try {
if (typeof (device as any).requestMTU === 'function') {
const newDevice = await (device as any).requestMTU?.(BLE_UUIDS.REQUEST_MTU)
if (newDevice && typeof newDevice.mtu === 'number') {
device = newDevice
} else {
console.warn('requestMTU did not return a device with MTU info, but continuing...')
}
await new Promise((resolve) => setTimeout(resolve, 500))
} else {
console.warn('requestMTU not supported on this platform/device, but continuing...')
}
} catch (mtuErr) {
console.warn('requestMTU failed, continuing without MTU change', mtuErr)
}
}
await device.discoverAllServicesAndCharacteristics()
console.log('Connected to device with MTU = ', device.mtu)
this.connectedDevice = await device.discoverAllServicesAndCharacteristics()
this.emit('connectionStateChange', { deviceId, state: ConnectionState.CONNECTED })
// Handle disconnection monitoring
device.onDisconnected((error: any, disconnectedDevice?: Device) => {
this.connectedDevice = null
const id = disconnectedDevice?.id || device.id || deviceId
this.emit('connectionStateChange', {
deviceId: id,
state: ConnectionState.DISCONNECTED,
})
this.emit('disconnected', disconnectedDevice || { id })
})
return this.connectedDevice
} catch (error: any) {
this.emit('connectionStateChange', { deviceId, state: ConnectionState.DISCONNECTED })
throw this.normalizeError(error)
}
}
public async disconnect(deviceId?: string): Promise<void> {
const id = deviceId || this.connectedDevice?.id
if (!id) return
if (!this.manager) return
try {
this.emit('connectionStateChange', { deviceId: id, state: ConnectionState.DISCONNECTING })
await this.manager.cancelDeviceConnection(id)
// onDisconnected callback will handle the state update
} catch (error: any) {
throw this.normalizeError(error)
}
}
public async read(deviceId: string, serviceUUID: string, characteristicUUID: string): Promise<string> {
if (!this.manager) throw new Error('BLE not supported on web')
try {
const char = await this.manager.readCharacteristicForDevice(deviceId, serviceUUID, characteristicUUID)
return char.value || '' // Base64 string
} catch (e) {
throw this.normalizeError(e)
}
}
public async write(
deviceId: string,
serviceUUID: string,
characteristicUUID: string,
dataBase64: string,
response: boolean = true,
): Promise<Characteristic> {
if (!this.manager) throw new Error('BLE not supported on web')
let result: Characteristic | null
try {
if (response) {
result = await this.manager.writeCharacteristicWithResponseForDevice(deviceId, serviceUUID, characteristicUUID, dataBase64)
} else {
result = await this.manager.writeCharacteristicWithoutResponseForDevice(deviceId, serviceUUID, characteristicUUID, dataBase64)
}
return result
} catch (e) {
throw this.normalizeError(e)
}
}
public async monitor(
deviceId: string,
serviceUUID: string,
characteristicUUID: string,
listener: (error: BleError | null, value: string | null) => void,
) {
if (!this.manager) {
listener({ message: 'BLE not supported on web', errorCode: 0, reason: null }, null)
return {
remove: () => {},
}
}
return this.manager.monitorCharacteristicForDevice(deviceId, serviceUUID, characteristicUUID, (error, char) => {
if (error) {
listener(this.normalizeError(error), null)
} else {
listener(null, char?.value || null)
}
})
}
public async requestMtu(deviceId: string, mtu: number): Promise<number> {
if (!this.manager) return 23
try {
const device = await this.manager.requestMTUForDevice(deviceId, mtu)
return device.mtu
} catch (e) {
console.warn('MTU negotiation failed', e)
// iOS doesn't allow explicit MTU request usually, so we might ignore or return default
return 23
}
}
public getConnectedDevice(): Device | null {
return this.connectedDevice
}
private normalizeError(e: any): BleError {
// wrapper to convert PlxError to our BleError
return {
errorCode: e.errorCode || 0,
message: e.message || 'Unknown error',
reason: e.reason,
}
}
}