expo-duooomi-app/ble/managers/bleManager.ts

891 lines
30 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 * 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 { uploadAndConvertToAniUrl } from '@/utils/uploadFile'
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'
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
// 存储待处理的 Promise通用
private pendingOperations = new Map<
string,
{
resolve: (value: any) => 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...')
// 先移除所有现有监听器,避免重复监听
this.removeAllListeners()
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 }))
this.resolvePendingOperation('deviceInfo', info)
}
const onVersionInfo = (versionInfo: VersionInfo) => {
bleStore.setState((prev) => ({ ...prev, version: versionInfo?.version }))
this.resolvePendingOperation('versionInfo', versionInfo)
}
const onBindStatus = (status: BindingResponse) => {
const activated = status.success === 1
const galleryList = status.contents || []
// console.log('onBindStatus-----', status)
bleStore.setGalleryList(galleryList)
bleStore.setState((prev) => ({ ...prev, isActivated: activated }))
if (status.success === 1) {
this.resolvePendingOperation('bindDevice', status)
} else {
this.rejectPendingOperation('bindDevice', '绑定失败')
}
}
const onUnBindStatus = (status: UnBindResponse) => {
const activated = status.success === 1
console.log('onUnBindStatus-----------', status)
bleStore.setGalleryList([])
bleStore.setState((prev) => ({ ...prev, isActivated: activated }))
if (status.success === 1) {
this.resolvePendingOperation('unbindDevice', status)
} else {
this.rejectPendingOperation('unbindDevice', '解绑失败')
}
}
const onDeleteFile = (response: DeleteFileResponse) => {
// delete 返回没有 key 字段
const operationId = `deleteFile`
if (response.success === 0) {
console.log('File deleted successfully')
this.resolvePendingOperation(operationId, response)
} else {
const errorMsg = response.success === 1 ? '删除失败' : response.success === 2 ? '文件不存在' : '未知错误'
console.error('Delete file failed:', errorMsg)
this.rejectPendingOperation(operationId, errorMsg)
}
}
const onPrepareTransfer = (response: PrepareTransferResponse) => {
console.log('Prepare transfer response:', response)
const operationId = `prepareTransfer_${response.key}`
if (response.status === 'ready') {
console.log('Device is ready to receive file:', response.key)
this.resolvePendingOperation(operationId, response)
} else if (response.status === 'no_space') {
console.warn('Device has no space for file:', response.key)
this.rejectPendingOperation(operationId, '设备存储空间不足')
} else if (response.status === 'duplicated') {
console.warn('File already exists on device:', response.key)
this.rejectPendingOperation(operationId, '文件已存在于设备上')
} else {
this.rejectPendingOperation(operationId, `未知状态: ${response.status}`)
}
}
// 注册监听器
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 removeAllListeners() {
// 移除所有监听器,避免重复监听
this.bleClient.removeAllListeners()
this.deviceInfoService.removeAllListeners()
}
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()
console.log('Bluetooth permissions:', hasPerms)
if (!hasPerms) {
console.log('Permissions denied, stopping scan')
bleStore.setState((prev) => ({ ...prev, isScanning: false }))
return
}
console.log('Setting isScanning to true')
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 currentConnectedId = prev.connectedDevice?.id
const newDevices = relevantDevices.filter((nd) => !prev.discoveredDevices.some((ed) => ed.id === nd.id))
// 正确设置设备连接状态
newDevices.forEach((d) => {
d.connected = d.id === currentConnectedId
})
// 同时更新现有设备列表中的连接状态
const updatedExistingDevices = prev.discoveredDevices.map((existingDevice) => {
const systemConnectedDevice = relevantDevices.find((rd) => rd.id === existingDevice.id)
if (systemConnectedDevice) {
const newDevice = Object.assign(
Object.create(Object.getPrototypeOf(existingDevice)),
existingDevice,
) as BleDevice
newDevice.connected = existingDevice.id === currentConnectedId
return newDevice
}
return existingDevice
})
return { ...prev, discoveredDevices: [...updatedExistingDevices, ...newDevices] }
})
}
} catch (e) {
console.warn('Failed to check connected devices', e)
}
console.log('Starting BLE scan with service UUID:', BLE_UUIDS.SERVICE)
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)
},
)
console.log('BLE scan started successfully')
} catch (e: any) {
console.error('Start scan error:', e)
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
try {
const services = await connectedDevice.services()
for (const service of services) {
console.log(`[Service] ${service.uuid}`)
const characteristics = await service.characteristics()
for (const char of characteristics) {
const props = [
char.isReadable ? 'Read' : '',
char.isWritableWithResponse ? 'Write' : '',
char.isWritableWithoutResponse ? 'WriteNoResp' : '',
char.isNotifiable ? 'Notify' : '',
char.isIndicatable ? 'Indicate' : '',
]
.filter(Boolean)
.join(',')
console.log(` -> [Char] ${char.uuid} (${props})`)
}
}
} catch (scanError) {
console.error(`Error scanning services: ${scanError}`)
}
await this.protocolService.initialize(device.id)
bleStore.setState((prev) => {
// 更新设备列表中对应设备的连接状态
const updatedDevices = prev.discoveredDevices.map((d) => {
if (d.id === device.id) {
const newDevice = Object.assign(Object.create(Object.getPrototypeOf(d)), d) as BleDevice
newDevice.connected = true
return newDevice
}
return d
})
return {
...prev,
connectedDevice,
isConnected: true,
discoveredDevices: updatedDevices,
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...')
// ✅ 修复:先断开 BLE 连接,让系统监听器处理状态更新
await this.bleClient.disconnect()
console.log('Disconnected successfully')
} catch (error: any) {
// ✅ 修复:忽略断开连接时的常见错误
const errorMsg = error?.message || String(error)
if (
errorMsg.includes('Disconnected') ||
errorMsg.includes('GATT_SUCCESS') ||
errorMsg.includes('Unknown error')
) {
console.debug('Disconnect completed with expected error:', errorMsg)
} else {
console.error('Disconnect failed:', errorMsg)
}
} finally {
// ✅ 修复:延迟清理协议服务,确保状态监听器先执行
setTimeout(() => {
this.protocolService.disconnect()
// 只清理业务相关状态,连接状态由监听器处理
bleStore.setState((prev) => ({
...prev,
// discoveredDevices: [], // 清空设备发现列表,断开后需要重新扫描
deviceInfo: null,
version: '',
isActivated: false,
transferProgress: 0,
contents: [],
loading: {
...prev.loading,
connecting: false,
querying: false,
converting: false,
transferring: false,
},
error: null,
}))
}, 200) // 给监听器足够时间处理连接状态变化
}
}
// 通用的 Promise 解决/拒绝方法
private resolvePendingOperation(operationId: string, value: any) {
const pending = this.pendingOperations.get(operationId)
if (pending) {
clearTimeout(pending.timeoutId)
this.pendingOperations.delete(operationId)
pending.resolve(value)
}
}
private rejectPendingOperation(operationId: string, error: any) {
const pending = this.pendingOperations.get(operationId)
if (pending) {
clearTimeout(pending.timeoutId)
this.pendingOperations.delete(operationId)
pending.reject(new Error(error))
}
}
// 创建可等待的操作
private createWaitableOperation<T>(operationId: string, timeout = 10000): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.pendingOperations.delete(operationId)
reject(new Error(`Operation ${operationId} timeout`))
}, timeout)
this.pendingOperations.set(operationId, { resolve, reject, timeoutId })
})
}
// ✅ 改造为返回真实结果的方法
async getDeviceInfo(): Promise<DeviceInfo> {
const state = bleStore.state
if (!state.connectedDevice) {
return Promise.reject(new Error('No device connected'))
}
try {
console.log(`[${state.connectedDevice.id}] Requesting Device Info...`)
const resultPromise = this.createWaitableOperation<DeviceInfo>('deviceInfo')
await this.deviceInfoService.getDeviceInfo(state.connectedDevice.id)
return await resultPromise
} catch (e: any) {
this.setError(`Request failed: ${e.message}`)
throw e
}
}
async getDeviceVersion(): Promise<VersionInfo> {
const state = bleStore.state
if (!state.connectedDevice) {
return Promise.reject(new Error('No device connected'))
}
try {
console.log(`[${state.connectedDevice.id}] Requesting Device Version...`)
const resultPromise = this.createWaitableOperation<VersionInfo>('versionInfo')
await this.deviceInfoService.getDeviceVersion(state.connectedDevice.id)
return await resultPromise
} catch (e: any) {
this.setError(`Request failed: ${e.message}`)
throw e
}
}
async bindDevice(userId: string): Promise<BindingResponse> {
const state = bleStore.state
if (!state.connectedDevice) {
return Promise.reject(new Error('No device connected'))
}
try {
const resultPromise = this.createWaitableOperation<BindingResponse>('bindDevice')
await this.deviceInfoService.bindDevice(state.connectedDevice.id, userId)
const result = await resultPromise
// 只有成功时才添加到绑定列表
if (result.success === 1) {
bleStore.addBindDeviceItem({
id: state.connectedDevice.id,
name: state.connectedDevice.name || '',
userId,
createAt: Date.now(),
})
}
return result
} catch (e: any) {
this.setError(`Request failed: ${e.message}`)
throw e
}
}
async unBindDevice(userId: string): Promise<UnBindResponse> {
const state = bleStore.state
if (!state.connectedDevice) {
return Promise.reject(new Error('No device connected'))
}
try {
const resultPromise = this.createWaitableOperation<UnBindResponse>('unbindDevice')
await this.deviceInfoService.unbindDevice(state.connectedDevice.id, userId)
return await resultPromise
} catch (e: any) {
this.setError(`Request failed: ${e.message}`)
throw e
}
}
async deleteFile(key: string): Promise<DeleteFileResponse> {
const state = bleStore.state
if (!state.connectedDevice) {
return Promise.reject(new Error('No device connected'))
}
try {
const resultPromise = this.createWaitableOperation<DeleteFileResponse>(`deleteFile`)
await this.deviceInfoService.deleteFile(state.connectedDevice.id, key)
return await resultPromise
} catch (e: any) {
this.setError(`Delete file failed: ${e.message}`)
throw e
}
}
async transferMediaSingle(uriOrUrl: string) {
try {
const state = bleStore.state
if (!state.connectedDevice) {
this.setError('No device connected')
return Promise.reject('No device connected')
}
// 检查是否已经有文件在上传
if (state.loading.transferring) {
const errorMsg = '已有文件正在上传中,请等待当前文件上传完成'
this.setError(errorMsg)
return Promise.reject(errorMsg)
}
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)
// console.log('prepareResp:=-------------', prepareResp)
// console.log('tempBuffer------------', tempBuffer.byteLength)
if (prepareResp.status !== 'ready') {
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, converting: false } }))
const message = prepareResp.status === 'no_space' ? '设备存储空间不足' : `文件已存在于设备上`
return Promise.reject(message)
}
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)
}
}
async performOtaUpgrade(
url: string,
comType = '0x02',
onProgress?: (progress: number) => void,
): Promise<ArrayBuffer> {
const state = bleStore.state
if (!state.connectedDevice) {
const error = 'No device connected'
this.setError(error)
return Promise.reject(error)
}
// 检查是否已经有文件在上传
if (state.loading.transferring) {
const errorMsg = '已有文件正在传输中,请等待当前文件传输完成'
this.setError(errorMsg)
return Promise.reject(errorMsg)
}
try {
console.log(`[${state.connectedDevice.id}] Fetching OTA package from: ${url}`)
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, transferring: true } }))
const response = await fetch(url)
console.log('performOtaUpgrade----------', response)
if (!response.ok) {
throw new Error(`Failed to fetch OTA package: ${response.status} ${response.statusText}`)
}
const blob = await response.blob()
const reader = new FileReader()
const arrayBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.onerror = reject
reader.readAsArrayBuffer(blob)
})
console.log(`OTA package fetched, size: ${arrayBuffer.byteLength} bytes`)
await this.fileTransferService.transferFile(
state.connectedDevice.id,
arrayBuffer,
// COMMAND_TYPES.OTA_PACKAGE,
Number(comType),
(progress) => {
bleStore.setState((prev) => ({ ...prev, transferProgress: progress * 100 }))
onProgress?.(progress)
},
)
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, transferring: false } }))
console.log('OTA upgrade transfer completed successfully')
return arrayBuffer
} catch (error: any) {
bleStore.setState((prev) => ({ ...prev, loading: { ...prev.loading, transferring: false } }))
const errorMsg = error?.message || 'OTA upgrade failed'
this.setError(errorMsg)
console.error(`OTA upgrade failed: ${errorMsg}`)
return Promise.reject(errorMsg)
}
}
// ✅ 统一的 Promise 实现
private async prepareTransfer(key: string, size: number): Promise<PrepareTransferResponse> {
const state = bleStore.state
if (!state.connectedDevice) {
throw new Error('No device connected')
}
try {
const resultPromise = this.createWaitableOperation<PrepareTransferResponse>(`prepareTransfer_${key}`)
await this.deviceInfoService.prepareTransfer(state.connectedDevice.id, key, size)
return await resultPromise
} catch (e: any) {
this.setError(`Prepare transfer failed: ${e.message}`)
throw e
}
}
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)
// formData.append('fps', 24)
const aniUrl = await uploadAndConvertToAniUrl({
url: uriOrUrl,
})
const aniBuffer = await fetch(aniUrl).then((r) => r.arrayBuffer())
console.log('aniBuffer-----------', aniBuffer.byteLength)
// return
// 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 aniBuffer
}
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)
}
// ✅ 只需清理统一的 pendingOperations
this.pendingOperations.forEach(({ timeoutId, reject }) => {
clearTimeout(timeoutId)
reject(new Error('BleManager destroyed'))
})
this.pendingOperations.clear()
this.removeAllListeners()
this.initialized = false
}
}
// Global instance
export const bleManager = BleManager.getInstance()