611 lines
21 KiB
TypeScript
611 lines
21 KiB
TypeScript
import * as Sentry from '@sentry/react-native'
|
||
import { Directory, Paths } from 'expo-file-system'
|
||
import { Alert, Linking, PermissionsAndroid, Platform } from 'react-native'
|
||
import { ScanMode } from 'react-native-ble-plx'
|
||
|
||
import { bleStore } from '@/stores'
|
||
import { aniStorage } from '@/utils/aniStorage'
|
||
import { extractCdnKey } from '@/utils/getCDNKey'
|
||
|
||
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,
|
||
} from '../protocol/types'
|
||
import { BleProtocolService } from '../services/BleProtocolService'
|
||
import { DeviceInfoService } from '../services/DeviceInfoService'
|
||
import { FileTransferService } from '../services/FileTransferService'
|
||
|
||
class BleManager {
|
||
private static instance: BleManager
|
||
private initialized = false
|
||
|
||
// Services (单例)
|
||
private bleClient = BleClient.getInstance()
|
||
private deviceInfoService = DeviceInfoService.getInstance()
|
||
private fileTransferService = FileTransferService.getInstance()
|
||
private protocolService = BleProtocolService.getInstance()
|
||
|
||
// 全局的 refs,避免重复创建
|
||
private pendingDevicesRef: BleDevice[] = []
|
||
private flushTimerRef: ReturnType<typeof setTimeout> | null = null
|
||
private pendingPrepareTransfersRef = new Map<
|
||
string,
|
||
{
|
||
resolve: (value: PrepareTransferResponse) => void
|
||
reject: (reason?: any) => void
|
||
timeoutId: ReturnType<typeof setTimeout>
|
||
}
|
||
>()
|
||
|
||
static getInstance(): BleManager {
|
||
if (!BleManager.instance) {
|
||
BleManager.instance = new BleManager()
|
||
}
|
||
return BleManager.instance
|
||
}
|
||
|
||
initialize() {
|
||
if (this.initialized || Platform.OS === 'web') {
|
||
console.log('BLE Manager already initialized or on web platform')
|
||
return
|
||
}
|
||
this.initialized = true
|
||
console.log('Initializing BLE Manager...')
|
||
this.setupEventListeners()
|
||
}
|
||
|
||
private setupEventListeners() {
|
||
console.log('Setting up BLE event listeners...')
|
||
|
||
const onConnectionStateChange = ({ deviceId, state: connState }: { deviceId: string; state: ConnectionState }) => {
|
||
const isConnected = connState === ConnectionState.CONNECTED
|
||
bleStore.setState((prev) => {
|
||
const updatedDevices = prev.discoveredDevices.map((d) => {
|
||
if (d.id === deviceId) {
|
||
const newDevice = Object.assign(Object.create(Object.getPrototypeOf(d)), d) as BleDevice
|
||
newDevice.connected = isConnected
|
||
return newDevice
|
||
}
|
||
return d
|
||
})
|
||
|
||
return {
|
||
...prev,
|
||
isConnected,
|
||
connectedDevice: connState === ConnectionState.DISCONNECTED ? null : prev.connectedDevice,
|
||
discoveredDevices: updatedDevices,
|
||
}
|
||
})
|
||
console.log(`Connection state ${deviceId}: ${connState}`)
|
||
}
|
||
|
||
const onScanError = (error: BleError) => {
|
||
this.setError(`Scan error: ${error.message}`)
|
||
bleStore.setState((prev) => ({ ...prev, isScanning: false }))
|
||
}
|
||
|
||
const onDeviceInfo = (info: DeviceInfo) => {
|
||
bleStore.setState((prev) => ({ ...prev, deviceInfo: info }))
|
||
}
|
||
|
||
const onVersionInfo = (version: string) => {
|
||
bleStore.setState((prev) => ({ ...prev, version }))
|
||
}
|
||
|
||
const onBindStatus = (status: BindingResponse) => {
|
||
const activated = status.success === 1
|
||
const contents = status.contents || []
|
||
console.log('onBindStatus-----', status)
|
||
bleStore.setState((prev) => ({ ...prev, isActivated: activated, contents }))
|
||
}
|
||
|
||
const onUnBindStatus = (status: UnBindResponse) => {
|
||
const activated = status.success === 1
|
||
bleStore.setState((prev) => ({ ...prev, isActivated: activated }))
|
||
}
|
||
|
||
const onDeleteFile = (response: DeleteFileResponse) => {
|
||
if (response.success === 0) {
|
||
console.log('File deleted successfully')
|
||
} else {
|
||
const errorMsg = response.success === 1 ? '删除失败' : response.success === 2 ? '文件不存在' : '未知错误'
|
||
console.error('Delete file failed:', errorMsg)
|
||
}
|
||
}
|
||
|
||
const onPrepareTransfer = (response: PrepareTransferResponse) => {
|
||
console.log('Prepare transfer response:', response)
|
||
const pending = this.pendingPrepareTransfersRef.get(response.key)
|
||
if (pending) {
|
||
clearTimeout(pending.timeoutId)
|
||
this.pendingPrepareTransfersRef.delete(response.key)
|
||
pending.resolve(response)
|
||
return
|
||
}
|
||
|
||
if (response.status === 'ready') {
|
||
console.log('Device is ready to receive file:', response.key)
|
||
} else if (response.status === 'no_space') {
|
||
console.warn('Device has no space for file:', response.key)
|
||
} else if (response.status === 'duplicated') {
|
||
console.warn('File already exists on device:', response.key)
|
||
}
|
||
}
|
||
|
||
// 注册监听器
|
||
this.bleClient.addListener('connectionStateChange', onConnectionStateChange)
|
||
this.bleClient.addListener('scanError', onScanError)
|
||
this.deviceInfoService.addListener(EVENT_TYPES.DEVICE_INFO.name, onDeviceInfo)
|
||
this.deviceInfoService.addListener(EVENT_TYPES.VERSION_INFO.name, onVersionInfo)
|
||
this.deviceInfoService.addListener(EVENT_TYPES.BIND_DEVICE.name, onBindStatus)
|
||
this.deviceInfoService.addListener(EVENT_TYPES.UNBIND_DEVICE.name, onUnBindStatus)
|
||
this.deviceInfoService.addListener(EVENT_TYPES.DELETE_FILE.name, onDeleteFile)
|
||
this.deviceInfoService.addListener(EVENT_TYPES.PREPARE_TRANSFER.name, onPrepareTransfer)
|
||
}
|
||
|
||
private setError(error: string | null) {
|
||
bleStore.setState((prev) => ({ ...prev, error }))
|
||
if (error) {
|
||
console.error(`BLE ERROR: ${error}`)
|
||
}
|
||
}
|
||
|
||
private logToSentry(operation: string) {
|
||
try {
|
||
const state = bleStore.state
|
||
Sentry.captureMessage(`ble_${operation}`, {
|
||
level: 'info',
|
||
tags: {
|
||
sessionId: bleStore.bleSessionId,
|
||
component: 'BleManager',
|
||
operation,
|
||
has_device: !!state.connectedDevice,
|
||
is_scanning: state.isScanning,
|
||
},
|
||
contexts: {
|
||
ble_state: {
|
||
connection: {
|
||
connected: state.isConnected,
|
||
device_id: state.connectedDevice?.id,
|
||
device_name: state.connectedDevice?.name,
|
||
},
|
||
device: {
|
||
activated: state.isActivated,
|
||
version: state.version,
|
||
free_space: state.deviceInfo?.freespace,
|
||
brand: state.deviceInfo?.brand,
|
||
},
|
||
transfer: {
|
||
progress: state.transferProgress,
|
||
converting: state.loading.converting,
|
||
transferring: state.loading.transferring,
|
||
},
|
||
discovery: {
|
||
scanning: state.isScanning,
|
||
devices_found: state.discoveredDevices.length,
|
||
},
|
||
},
|
||
},
|
||
extra: {
|
||
error_message: state.error,
|
||
loading_states: state.loading,
|
||
},
|
||
})
|
||
} catch (error) {
|
||
console.error('Sentry log failed:', error)
|
||
}
|
||
}
|
||
|
||
private flushPendingDevices() {
|
||
if (this.pendingDevicesRef.length === 0) return
|
||
|
||
const devicesToAdd = [...this.pendingDevicesRef]
|
||
this.pendingDevicesRef = []
|
||
|
||
this.logToSentry('device_discovered_batch')
|
||
|
||
bleStore.setState((prev) => {
|
||
const newDevices = devicesToAdd.filter((nd) => !prev.discoveredDevices?.some((ed) => ed?.id === nd?.id))
|
||
if (newDevices.length === 0) return prev
|
||
console.debug(`Batch adding ${newDevices.length} devices`)
|
||
return { ...prev, discoveredDevices: [...prev.discoveredDevices, ...newDevices] }
|
||
})
|
||
}
|
||
|
||
private queueDevice(device: BleDevice) {
|
||
if (this.pendingDevicesRef?.some((d) => d?.id === device?.id)) return
|
||
this.pendingDevicesRef.push(device)
|
||
|
||
if (this.flushTimerRef) {
|
||
clearTimeout(this.flushTimerRef)
|
||
}
|
||
this.flushTimerRef = setTimeout(() => this.flushPendingDevices(), 500)
|
||
}
|
||
|
||
private async requestBluetoothPermissions(): 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 preChecks = await Promise.all(perms.map((p) => PermissionsAndroid.check(p as any)))
|
||
const needRequest = perms.filter((_, i) => !preChecks[i])
|
||
if (needRequest.length === 0) return true
|
||
|
||
const results = await PermissionsAndroid.requestMultiple(needRequest as any)
|
||
const allGranted = Object.values(results).every((r) => r === PermissionsAndroid.RESULTS.GRANTED)
|
||
if (allGranted) return true
|
||
|
||
const neverAsk = Object.values(results).some((r) => r === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN)
|
||
if (neverAsk) {
|
||
Alert.alert(
|
||
'需要权限',
|
||
'检测到蓝牙权限已被禁止且选择了不再询问。请在系统设置中为应用手动开启蓝牙和位置权限。',
|
||
[
|
||
{ text: '取消', style: 'cancel' },
|
||
{
|
||
text: '打开设置',
|
||
onPress: () =>
|
||
Linking.openSettings().catch(() => {
|
||
this.setError('无法打开系统设置,请手动前往应用权限设置')
|
||
}),
|
||
},
|
||
],
|
||
{ cancelable: true },
|
||
)
|
||
}
|
||
|
||
return false
|
||
} catch (err) {
|
||
this.setError(`Permission request failed: ${err}`)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// Public methods
|
||
async startScan() {
|
||
if (Platform.OS === 'web') return
|
||
try {
|
||
this.setError(null)
|
||
console.log('Starting scan...')
|
||
const hasPerms = await this.requestBluetoothPermissions()
|
||
if (!hasPerms) return
|
||
|
||
bleStore.setState((prev) => ({ ...prev, isScanning: true, discoveredDevices: [] }))
|
||
|
||
try {
|
||
const relevantDevices = await this.bleClient.getConnectedDevices([BLE_UUIDS.SERVICE])
|
||
if (relevantDevices.length > 0) {
|
||
console.log(`Found ${relevantDevices.length} system-connected devices`)
|
||
bleStore.setState((prev) => {
|
||
const newDevices = relevantDevices.filter((nd) => !prev.discoveredDevices.some((ed) => ed.id === nd.id))
|
||
newDevices.forEach((d) => {
|
||
d.connected = false
|
||
})
|
||
return { ...prev, discoveredDevices: [...prev.discoveredDevices, ...newDevices] }
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.warn('Failed to check connected devices', e)
|
||
}
|
||
|
||
await this.bleClient.startScan(
|
||
[BLE_UUIDS.SERVICE],
|
||
{ scanMode: ScanMode.Balanced, allowDuplicates: false },
|
||
(result) => {
|
||
const device = result.device as BleDevice
|
||
const targetServiceUUID = BLE_UUIDS.SERVICE.toLowerCase()
|
||
const hasTargetService = device.serviceUUIDs?.some((uuid) => uuid?.toLowerCase() === targetServiceUUID)
|
||
|
||
if (!hasTargetService || !device?.id) return
|
||
|
||
device.connected = false
|
||
console.debug(
|
||
`Device found: ${device.name} (${device.id}), serviceUUIDs: ${JSON.stringify(device.serviceUUIDs)}`,
|
||
)
|
||
this.queueDevice(device)
|
||
},
|
||
)
|
||
} catch (e: any) {
|
||
this.setError(`Start scan failed: ${e.message}`)
|
||
bleStore.setState((prev) => ({ ...prev, isScanning: false, discoveredDevices: [] }))
|
||
Alert.alert(
|
||
'手机蓝牙未开启',
|
||
'检测到手机蓝牙未打开,请打开手机蓝牙再试。',
|
||
[{ text: '确定', style: 'default' }],
|
||
{ cancelable: true },
|
||
)
|
||
}
|
||
}
|
||
|
||
stopScan() {
|
||
if (this.flushTimerRef) {
|
||
clearTimeout(this.flushTimerRef)
|
||
this.flushTimerRef = null
|
||
}
|
||
this.pendingDevicesRef = []
|
||
|
||
this.bleClient.stopScan()
|
||
bleStore.setState((prev) => ({ ...prev, isScanning: false }))
|
||
console.log('Scan stopped')
|
||
}
|
||
|
||
async connectToDevice(device: BleDevice): Promise<BleDevice> {
|
||
this.stopScan()
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, connecting: true } }))
|
||
console.log(`Connecting to ${device.name}...${device.id}`)
|
||
|
||
if (!device?.id) {
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, connecting: false } }))
|
||
return Promise.reject('Device ID is missing')
|
||
}
|
||
|
||
try {
|
||
const connectedDevice = (await this.bleClient.connect(device.id)) as BleDevice
|
||
if (!connectedDevice) {
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, connecting: false } }))
|
||
return Promise.reject('Failed to connect to device')
|
||
}
|
||
connectedDevice.connected = true
|
||
|
||
await this.protocolService.initialize(device.id)
|
||
|
||
bleStore.setState((prev) => ({
|
||
...prev,
|
||
connectedDevice,
|
||
isConnected: true,
|
||
loading: { ...prev.loading, connecting: false },
|
||
}))
|
||
|
||
this.logToSentry('device_connected')
|
||
console.log('Connected and Protocol initialized')
|
||
return connectedDevice
|
||
} catch (e: any) {
|
||
const errorMsg = e?.message || String(e) || 'Unknown connection error'
|
||
console.error(`Connection failed: ${errorMsg}`, e)
|
||
this.setError(`Connection failed: ${errorMsg}`)
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, connecting: false } }))
|
||
throw new Error(errorMsg)
|
||
}
|
||
}
|
||
|
||
async disconnectDevice() {
|
||
try {
|
||
console.log('Disconnecting...')
|
||
await this.bleClient.disconnect()
|
||
this.protocolService.disconnect()
|
||
} catch (e: any) {
|
||
console.error(`Disconnect failed: ${e.message}`)
|
||
}
|
||
}
|
||
|
||
async getDeviceInfo() {
|
||
const state = bleStore.state
|
||
if (!state.connectedDevice) return
|
||
try {
|
||
console.log(`[${state.connectedDevice.id}] Requesting Device Info...`)
|
||
await this.deviceInfoService.getDeviceInfo(state.connectedDevice.id)
|
||
console.log(`[${state.connectedDevice.id}] Device Info query request sent.`)
|
||
return Promise.resolve()
|
||
} catch (e: any) {
|
||
this.setError(`Request failed: ${e.message}`)
|
||
return Promise.reject(e.message)
|
||
}
|
||
}
|
||
|
||
async getDeviceVersion() {
|
||
const state = bleStore.state
|
||
if (!state.connectedDevice) return
|
||
try {
|
||
console.log(`[${state.connectedDevice.id}] Requesting Device Version...`)
|
||
await this.deviceInfoService.getDeviceVersion(state.connectedDevice.id)
|
||
console.log(`[${state.connectedDevice.id}] Device Version query request sent`)
|
||
return Promise.resolve()
|
||
} catch (e: any) {
|
||
this.setError(`Request failed: ${e.message}`)
|
||
return Promise.reject(e.message)
|
||
}
|
||
}
|
||
|
||
async bindDevice(userId: string) {
|
||
const state = bleStore.state
|
||
if (!state.connectedDevice) return
|
||
try {
|
||
await this.deviceInfoService.bindDevice(state.connectedDevice.id, userId)
|
||
return Promise.resolve()
|
||
} catch (e: any) {
|
||
this.setError(`Request failed: ${e.message}`)
|
||
return Promise.reject(e.message)
|
||
}
|
||
}
|
||
|
||
async unBindDevice(userId: string) {
|
||
const state = bleStore.state
|
||
if (!state.connectedDevice) return
|
||
try {
|
||
await this.deviceInfoService.unbindDevice(state.connectedDevice.id, userId)
|
||
return Promise.resolve()
|
||
} catch (e: any) {
|
||
this.setError(`Request failed: ${e.message}`)
|
||
return Promise.reject(e.message)
|
||
}
|
||
}
|
||
|
||
async deleteFile(key: string) {
|
||
const state = bleStore.state
|
||
if (!state.connectedDevice) {
|
||
const error = 'No device connected'
|
||
this.setError(error)
|
||
return Promise.reject(error)
|
||
}
|
||
|
||
try {
|
||
await this.deviceInfoService.deleteFile(state.connectedDevice.id, key)
|
||
return Promise.resolve()
|
||
} catch (e: any) {
|
||
this.setError(`Delete file failed: ${e.message}`)
|
||
return Promise.reject(e.message)
|
||
}
|
||
}
|
||
|
||
async transferMediaSingle(uriOrUrl: string) {
|
||
try {
|
||
const state = bleStore.state
|
||
if (!state.connectedDevice) {
|
||
this.setError('No device connected')
|
||
return Promise.reject('No device connected')
|
||
}
|
||
|
||
const tempDir = new Directory(Paths.cache, 'anis')
|
||
if (!tempDir.exists) tempDir.create()
|
||
|
||
if (!uriOrUrl) return Promise.reject('No uriOrUrl provided')
|
||
|
||
let tempBuffer: ArrayBuffer
|
||
const tempBufferExist = await aniStorage.has(uriOrUrl)
|
||
|
||
if (!tempBufferExist) {
|
||
console.debug(`Converting video: ${uriOrUrl || 'video'}...`)
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: true } }))
|
||
tempBuffer = await this.convertImgToANIAsBuffer(uriOrUrl)
|
||
await aniStorage.set(uriOrUrl, tempBuffer)
|
||
} else {
|
||
tempBuffer = await aniStorage.get(uriOrUrl)
|
||
}
|
||
|
||
const fileSizeByte = tempBuffer.byteLength
|
||
const key = extractCdnKey(uriOrUrl)
|
||
if (!key) {
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false } }))
|
||
return Promise.reject('Invalid uriOrUrl key')
|
||
}
|
||
|
||
const prepareResp = await this.prepareTransfer(key, fileSizeByte)
|
||
if (prepareResp.status !== 'ready') {
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false } }))
|
||
return Promise.reject({ status: `${prepareResp.status}` })
|
||
}
|
||
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false, transferring: true } }))
|
||
console.log(`Transferring converted file to device...`)
|
||
|
||
await this.fileTransferService.transferFile(
|
||
state.connectedDevice.id,
|
||
tempBuffer,
|
||
COMMAND_TYPES.TRANSFER_ANI_VIDEO,
|
||
(progress) => {
|
||
bleStore.setState((prev) => ({ ...prev, transferProgress: progress * 100 }))
|
||
},
|
||
)
|
||
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, transferring: false } }))
|
||
console.log(`Transfer successful`)
|
||
return Promise.resolve()
|
||
} catch (error: any) {
|
||
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, transferring: false } }))
|
||
console.log(`Transfer failed: ${error?.message}`)
|
||
return Promise.reject(error?.message)
|
||
}
|
||
}
|
||
|
||
private async prepareTransfer(key: string, size: number): Promise<PrepareTransferResponse> {
|
||
const state = bleStore.state
|
||
if (!state.connectedDevice) {
|
||
const error = 'No device connected'
|
||
this.setError(error)
|
||
return Promise.reject(error)
|
||
}
|
||
|
||
try {
|
||
const responsePromise = new Promise<PrepareTransferResponse>((resolve, reject) => {
|
||
const timeoutId = setTimeout(() => {
|
||
this.pendingPrepareTransfersRef.delete(key)
|
||
reject(new Error('Prepare transfer timeout'))
|
||
}, 10e3)
|
||
|
||
this.pendingPrepareTransfersRef.set(key, { resolve, reject, timeoutId })
|
||
})
|
||
|
||
await this.deviceInfoService.prepareTransfer(state.connectedDevice.id, key, size)
|
||
return await responsePromise
|
||
} catch (e: any) {
|
||
this.setError(`Prepare transfer failed: ${e.message}`)
|
||
return Promise.reject(e.message)
|
||
}
|
||
}
|
||
|
||
private async convertImgToANIAsBuffer(uriOrUrl: string): Promise<ArrayBuffer> {
|
||
const res = await fetch(uriOrUrl)
|
||
if (!res.ok) {
|
||
return Promise.reject(`Failed to fetch media: ${res.status}`)
|
||
}
|
||
|
||
const blob = await res.blob()
|
||
const name = uriOrUrl.split('/').pop()?.split('?')[0] || 'media.bin'
|
||
const type = blob.type || this.guessMimeType(name)
|
||
|
||
const formData = new FormData()
|
||
formData.append('file', { uri: uriOrUrl, name, type } as any)
|
||
|
||
const aniProd = 'https://bowongai-prod--ani-video-converter-fastapi-app.modal.run/api/convert/ani'
|
||
const response = await fetch(aniProd, { method: 'POST', body: formData })
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Conversion failed: ${response.status}`)
|
||
}
|
||
|
||
const content = await response.arrayBuffer()
|
||
console.debug(`Converted video size: ${content.byteLength} bytes`)
|
||
return content
|
||
}
|
||
|
||
private guessMimeType(fileName: string): string {
|
||
const ext = fileName.split('.').pop()?.toLowerCase()
|
||
switch (ext) {
|
||
case 'mp4':
|
||
return 'video/mp4'
|
||
case 'mov':
|
||
return 'video/quicktime'
|
||
case 'webm':
|
||
return 'video/webm'
|
||
case 'jpg':
|
||
case 'jpeg':
|
||
return 'image/jpeg'
|
||
case 'png':
|
||
return 'image/png'
|
||
default:
|
||
return 'application/octet-stream'
|
||
}
|
||
}
|
||
|
||
clearLogs() {
|
||
this.setError(null)
|
||
}
|
||
|
||
destroy() {
|
||
if (this.flushTimerRef) {
|
||
clearTimeout(this.flushTimerRef)
|
||
}
|
||
this.pendingPrepareTransfersRef.forEach(({ timeoutId }) => {
|
||
clearTimeout(timeoutId)
|
||
})
|
||
this.pendingPrepareTransfersRef.clear()
|
||
this.initialized = false
|
||
}
|
||
}
|
||
|
||
// Global instance
|
||
export const bleManager = BleManager.getInstance()
|