Files
duooomi-ble-sdk/ble/sdk/BleSDK.ts

439 lines
14 KiB
TypeScript

/**
* Duooomi BLE SDK - Brownfield API Layer
*
* 对外暴露的蓝牙 SDK 接口,通过 expo-brownfield 的消息机制与原生 App 通信。
* 原生 App 发送 action 消息 → SDK 执行蓝牙操作 → 通过事件/SharedState 返回结果。
*/
import Brownfield, {
addMessageListener,
sendMessage,
setSharedStateValue,
} from 'expo-brownfield'
import { Platform, PermissionsAndroid, Linking, Alert } from 'react-native'
import { ScanMode } from 'react-native-ble-plx'
import { BleClient } from '../core/BleClient'
import { type BleDevice, type BleError, ConnectionState } from '../core/types'
import { BLE_UUIDS, COMMAND_TYPES, EVENT_TYPES } from '../protocol/Constants'
import {
type BindingResponse,
type DeleteFileResponse,
type DeviceInfo,
type PrepareTransferResponse,
type UnBindResponse,
type VersionInfo,
} from '../protocol/types'
import { BleProtocolService } from '../services/BleProtocolService'
import { DeviceInfoService } from '../services/DeviceInfoService'
import { FileTransferService } from '../services/FileTransferService'
// ====== SharedState Keys ======
const STATE_KEYS = {
BT_STATE: 'btState', // 'idle' | 'scanning' | 'connecting' | 'connected' | 'disconnected'
CONNECTED_DEVICE: 'connectedDevice', // { id, name } | null
DEVICE_INFO: 'deviceInfo', // DeviceInfo | null
VERSION: 'version', // string
IS_ACTIVATED: 'isActivated', // boolean
TRANSFER_PROGRESS: 'transferProgress', // number 0-100
ERROR: 'error', // string | null
DISCOVERED_DEVICES: 'discoveredDevices', // Array<{ id, name, rssi }>
} as const
class BleSDK {
private bleClient = BleClient.getInstance()
private deviceInfoService = DeviceInfoService.getInstance()
private fileTransferService = FileTransferService.getInstance()
private protocolService = BleProtocolService.getInstance()
private connectedDeviceId: string | null = null
private pendingOperations = new Map<
string,
{
resolve: (value: any) => void
reject: (reason?: any) => void
timeoutId: ReturnType<typeof setTimeout>
}
>()
// 批量设备发现缓冲
private pendingDevices: Array<{ id: string; name: string | null; rssi: number | null }> = []
private flushTimer: ReturnType<typeof setTimeout> | null = null
initialize() {
this.setupBleListeners()
this.setupMessageHandler()
this.updateState(STATE_KEYS.BT_STATE, 'idle')
console.log('[BleSDK] Initialized')
}
// ====== 设置 BLE 事件监听 ======
private setupBleListeners() {
this.bleClient.addListener('connectionStateChange', ({ deviceId, state }: { deviceId: string; state: ConnectionState }) => {
const stateMap: Record<ConnectionState, string> = {
[ConnectionState.CONNECTING]: 'connecting',
[ConnectionState.CONNECTED]: 'connected',
[ConnectionState.DISCONNECTING]: 'disconnecting',
[ConnectionState.DISCONNECTED]: 'disconnected',
}
this.updateState(STATE_KEYS.BT_STATE, stateMap[state] || 'idle')
if (state === ConnectionState.DISCONNECTED) {
this.connectedDeviceId = null
this.updateState(STATE_KEYS.CONNECTED_DEVICE, null)
}
sendMessage({ event: 'connectionStateChange', deviceId, state })
})
this.bleClient.addListener('scanError', (error: BleError) => {
this.updateState(STATE_KEYS.ERROR, error.message)
sendMessage({ event: 'error', type: 'scanError', message: error.message })
})
// 设备信息响应
this.deviceInfoService.addListener(EVENT_TYPES.DEVICE_INFO.name, (info: DeviceInfo) => {
this.updateState(STATE_KEYS.DEVICE_INFO, info)
this.resolvePending('deviceInfo', info)
sendMessage({ event: 'deviceInfo', data: info })
})
this.deviceInfoService.addListener(EVENT_TYPES.VERSION_INFO.name, (info: VersionInfo) => {
this.updateState(STATE_KEYS.VERSION, info.version)
this.resolvePending('versionInfo', info)
sendMessage({ event: 'versionInfo', data: info })
})
this.deviceInfoService.addListener(EVENT_TYPES.BIND_DEVICE.name, (resp: BindingResponse) => {
this.updateState(STATE_KEYS.IS_ACTIVATED, resp.success === 1)
if (resp.success === 1) {
this.resolvePending('bindDevice', resp)
} else {
this.rejectPending('bindDevice', 'Device already bound to another user')
}
sendMessage({ event: 'bindDevice', data: resp })
})
this.deviceInfoService.addListener(EVENT_TYPES.UNBIND_DEVICE.name, (resp: UnBindResponse) => {
if (resp.success === 1) {
this.updateState(STATE_KEYS.IS_ACTIVATED, false)
this.resolvePending('unbindDevice', resp)
} else {
this.rejectPending('unbindDevice', 'Unbind failed')
}
sendMessage({ event: 'unbindDevice', data: resp })
})
this.deviceInfoService.addListener(EVENT_TYPES.DELETE_FILE.name, (resp: DeleteFileResponse) => {
if (resp.success === 0) {
this.resolvePending('deleteFile', resp)
} else {
this.rejectPending('deleteFile', resp.success === 2 ? 'File not found' : 'Delete failed')
}
sendMessage({ event: 'deleteFile', data: resp })
})
this.deviceInfoService.addListener(EVENT_TYPES.PREPARE_TRANSFER.name, (resp: PrepareTransferResponse) => {
const opId = `prepareTransfer_${resp.key}`
if (resp.status === 'ready') {
this.resolvePending(opId, resp)
} else {
this.rejectPending(opId, resp.status === 'no_space' ? 'No space on device' : 'File already exists')
}
sendMessage({ event: 'prepareTransfer', data: resp })
})
}
// ====== 接收原生 App 的消息指令 ======
private setupMessageHandler() {
addMessageListener((event: Record<string, any>) => {
const { action, ...params } = event
console.log(`[BleSDK] Received action: ${action}`, params)
switch (action) {
case 'scan':
this.startScan()
break
case 'stopScan':
this.stopScan()
break
case 'connect':
this.connect(params.deviceId)
break
case 'disconnect':
this.disconnect()
break
case 'getDeviceInfo':
this.getDeviceInfo()
break
case 'getVersion':
this.getVersion()
break
case 'bind':
this.bind(params.userId)
break
case 'unbind':
this.unbind(params.userId)
break
case 'deleteFile':
this.deleteFile(params.key)
break
case 'transferFile':
this.transferFile(params.fileUri, params.commandType)
break
default:
sendMessage({ event: 'error', type: 'unknownAction', message: `Unknown action: ${action}` })
}
})
}
// ====== 蓝牙操作实现 ======
private async startScan() {
try {
const hasPerms = await this.requestPermissions()
if (!hasPerms) {
sendMessage({ event: 'error', type: 'permission', message: 'Bluetooth permission denied' })
return
}
this.updateState(STATE_KEYS.BT_STATE, 'scanning')
this.updateState(STATE_KEYS.DISCOVERED_DEVICES, [])
this.pendingDevices = []
await this.bleClient.startScan(
[BLE_UUIDS.SERVICE],
{ scanMode: ScanMode.Balanced, allowDuplicates: false },
(result) => {
const device = result.device as BleDevice
const hasService = device.serviceUUIDs?.some(
(uuid) => uuid?.toLowerCase() === BLE_UUIDS.SERVICE.toLowerCase()
)
if (!hasService || !device?.id) return
this.queueDevice({
id: device.id,
name: device.name || device.localName || null,
rssi: result.rssi,
})
}
)
sendMessage({ event: 'scanStarted' })
} catch (e: any) {
this.updateState(STATE_KEYS.BT_STATE, 'idle')
this.updateState(STATE_KEYS.ERROR, e.message)
sendMessage({ event: 'error', type: 'scan', message: e.message })
}
}
private stopScan() {
this.bleClient.stopScan()
this.flushDevices()
this.updateState(STATE_KEYS.BT_STATE, this.connectedDeviceId ? 'connected' : 'idle')
sendMessage({ event: 'scanStopped' })
}
private async connect(deviceId: string) {
if (!deviceId) {
sendMessage({ event: 'error', type: 'connect', message: 'deviceId is required' })
return
}
this.stopScan()
try {
this.updateState(STATE_KEYS.BT_STATE, 'connecting')
const device = await this.bleClient.connect(deviceId)
this.connectedDeviceId = deviceId
await this.protocolService.initialize(deviceId)
this.updateState(STATE_KEYS.CONNECTED_DEVICE, {
id: device.id,
name: device.name || device.localName || null,
})
this.updateState(STATE_KEYS.BT_STATE, 'connected')
sendMessage({ event: 'connected', deviceId: device.id, name: device.name })
// 自动获取设备信息
this.getDeviceInfo().catch(() => {})
} catch (e: any) {
this.updateState(STATE_KEYS.BT_STATE, 'idle')
this.updateState(STATE_KEYS.ERROR, e.message)
sendMessage({ event: 'error', type: 'connect', message: e.message })
}
}
private async disconnect() {
try {
await this.bleClient.disconnect()
this.protocolService.disconnect()
this.connectedDeviceId = null
this.updateState(STATE_KEYS.CONNECTED_DEVICE, null)
this.updateState(STATE_KEYS.DEVICE_INFO, null)
this.updateState(STATE_KEYS.VERSION, '')
this.updateState(STATE_KEYS.IS_ACTIVATED, false)
this.updateState(STATE_KEYS.BT_STATE, 'disconnected')
sendMessage({ event: 'disconnected' })
} catch (e: any) {
sendMessage({ event: 'error', type: 'disconnect', message: e.message })
}
}
private async getDeviceInfo() {
if (!this.connectedDeviceId) return
try {
const promise = this.createWaitable<DeviceInfo>('deviceInfo')
await this.deviceInfoService.getDeviceInfo(this.connectedDeviceId)
return await promise
} catch (e: any) {
sendMessage({ event: 'error', type: 'deviceInfo', message: e.message })
}
}
private async getVersion() {
if (!this.connectedDeviceId) return
try {
const promise = this.createWaitable<VersionInfo>('versionInfo')
await this.deviceInfoService.getDeviceVersion(this.connectedDeviceId)
return await promise
} catch (e: any) {
sendMessage({ event: 'error', type: 'version', message: e.message })
}
}
private async bind(userId: string) {
if (!this.connectedDeviceId) return
try {
const promise = this.createWaitable<BindingResponse>('bindDevice')
await this.deviceInfoService.bindDevice(this.connectedDeviceId, userId)
return await promise
} catch (e: any) {
sendMessage({ event: 'error', type: 'bind', message: e.message })
}
}
private async unbind(userId: string) {
if (!this.connectedDeviceId) return
try {
const promise = this.createWaitable<UnBindResponse>('unbindDevice')
await this.deviceInfoService.unbindDevice(this.connectedDeviceId, userId)
return await promise
} catch (e: any) {
sendMessage({ event: 'error', type: 'unbind', message: e.message })
}
}
private async deleteFile(key: string) {
if (!this.connectedDeviceId) return
try {
const promise = this.createWaitable<DeleteFileResponse>('deleteFile')
await this.deviceInfoService.deleteFile(this.connectedDeviceId, key)
return await promise
} catch (e: any) {
sendMessage({ event: 'error', type: 'deleteFile', message: e.message })
}
}
private async transferFile(fileUri: string, commandType?: number) {
if (!this.connectedDeviceId) return
try {
this.updateState(STATE_KEYS.TRANSFER_PROGRESS, 0)
const type = commandType ?? COMMAND_TYPES.TRANSFER_ANI_VIDEO
await this.fileTransferService.transferFile(
this.connectedDeviceId,
fileUri,
type,
(progress) => {
const pct = Math.round(progress * 100)
this.updateState(STATE_KEYS.TRANSFER_PROGRESS, pct)
sendMessage({ event: 'transferProgress', progress: pct })
}
)
sendMessage({ event: 'transferComplete' })
} catch (e: any) {
this.updateState(STATE_KEYS.ERROR, e.message)
sendMessage({ event: 'error', type: 'transfer', message: e.message })
}
}
// ====== 工具方法 ======
private updateState(key: string, value: any) {
try {
setSharedStateValue(key, value)
} catch (e) {
console.warn(`[BleSDK] Failed to set shared state ${key}:`, e)
}
}
private queueDevice(device: { id: string; name: string | null; rssi: number | null }) {
if (this.pendingDevices.some((d) => d.id === device.id)) return
this.pendingDevices.push(device)
if (this.flushTimer) clearTimeout(this.flushTimer)
this.flushTimer = setTimeout(() => this.flushDevices(), 500)
}
private flushDevices() {
if (this.pendingDevices.length === 0) return
const devices = [...this.pendingDevices]
this.pendingDevices = []
this.updateState(STATE_KEYS.DISCOVERED_DEVICES, devices)
sendMessage({ event: 'devicesFound', devices })
}
private createWaitable<T>(opId: string, timeout = 10000): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.pendingOperations.delete(opId)
reject(new Error(`Operation ${opId} timeout`))
}, timeout)
this.pendingOperations.set(opId, { resolve, reject, timeoutId })
})
}
private resolvePending(opId: string, value: any) {
const pending = this.pendingOperations.get(opId)
if (pending) {
clearTimeout(pending.timeoutId)
this.pendingOperations.delete(opId)
pending.resolve(value)
}
}
private rejectPending(opId: string, error: string) {
const pending = this.pendingOperations.get(opId)
if (pending) {
clearTimeout(pending.timeoutId)
this.pendingOperations.delete(opId)
pending.reject(new Error(error))
}
}
private async requestPermissions(): Promise<boolean> {
if (Platform.OS !== 'android') return true
try {
const sdk = Number(Platform.Version) || 0
const perms: string[] = []
if (sdk >= 31) {
perms.push(PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN)
perms.push(PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT)
} else {
perms.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION)
}
const results = await PermissionsAndroid.requestMultiple(perms as any)
return Object.values(results).every((r) => r === PermissionsAndroid.RESULTS.GRANTED)
} catch {
return false
}
}
}
export const bleSDK = new BleSDK()