262 lines
8.9 KiB
TypeScript
262 lines
8.9 KiB
TypeScript
import { Buffer } from 'buffer'
|
|
import { type Subscription } from 'react-native-ble-plx'
|
|
|
|
import { BleClient } from '../core/BleClient'
|
|
import { type APP_COMMAND_TYPES, BLE_UUIDS, FRAME_CONSTANTS } from '../protocol/Constants'
|
|
import { ProtocolManager } from '../protocol/ProtocolManager'
|
|
import { type ProtocolFrame } from '../protocol/types'
|
|
|
|
export class BleProtocolService {
|
|
private static instance: BleProtocolService
|
|
private client = BleClient.getInstance()
|
|
private listeners: Map<number, Set<(data: ArrayBuffer, deviceId: string) => void>> = new Map()
|
|
private subscription: Subscription | null = null
|
|
|
|
// deviceId_type -> { total: number, frames: ArrayBuffer[] }
|
|
private fragments: Map<string, { total: number; frames: (ArrayBuffer | null)[] }> = new Map()
|
|
|
|
private constructor() {}
|
|
|
|
public static getInstance(): BleProtocolService {
|
|
if (!BleProtocolService.instance) {
|
|
BleProtocolService.instance = new BleProtocolService()
|
|
}
|
|
return BleProtocolService.instance
|
|
}
|
|
|
|
public addListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) {
|
|
if (!this.listeners.has(type)) {
|
|
this.listeners.set(type, new Set())
|
|
}
|
|
this.listeners.get(type)!.add(callback)
|
|
}
|
|
|
|
public removeListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) {
|
|
if (this.listeners.has(type)) {
|
|
this.listeners.get(type)!.delete(callback)
|
|
}
|
|
}
|
|
|
|
private emit(type: number, data: ArrayBuffer, deviceId: string) {
|
|
if (this.listeners.has(type)) {
|
|
this.listeners.get(type)!.forEach((cb) => cb(data, deviceId))
|
|
}
|
|
}
|
|
|
|
public async initialize(deviceId: string) {
|
|
// Clean up previous subscription
|
|
this.disconnect()
|
|
|
|
// Clear fragments for this device
|
|
this.clearFragments(deviceId)
|
|
|
|
try {
|
|
console.log('Initializing BLE protocol service for device:', deviceId)
|
|
|
|
this.subscription = await this.client.monitor(
|
|
deviceId,
|
|
BLE_UUIDS.SERVICE,
|
|
BLE_UUIDS.READ_CHARACTERISTIC,
|
|
(error, value) => {
|
|
if (error) {
|
|
// ✅ 修复:增强错误处理,避免崩溃
|
|
const errorMsg = error.message || 'Unknown monitor error'
|
|
|
|
// 忽略预期的断开连接错误
|
|
if (this.isExpectedDisconnectionError(error)) {
|
|
console.debug('Device disconnected during monitoring:', errorMsg)
|
|
return
|
|
}
|
|
|
|
// 忽略已知的原生崩溃错误
|
|
if (this.isKnownNativeCrashError(error)) {
|
|
console.debug('Ignored known native monitor error:', errorMsg)
|
|
return
|
|
}
|
|
|
|
console.warn('BLE monitor error:', errorMsg)
|
|
return
|
|
}
|
|
|
|
if (value) {
|
|
try {
|
|
const buffer = Buffer.from(value, 'base64')
|
|
const hexString =
|
|
buffer
|
|
.toString('hex')
|
|
.match(/.{1,2}/g)
|
|
?.join(' ')
|
|
.toUpperCase() || ''
|
|
|
|
console.log(`[BleProtocol] Received ${buffer.byteLength} bytes:`, hexString)
|
|
this.handleRawData(deviceId, buffer)
|
|
} catch (parseError: any) {
|
|
console.warn('Failed to parse received data:', parseError.message)
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
console.log('BLE protocol service initialized successfully')
|
|
} catch (error: any) {
|
|
console.error('Failed to initialize protocol service:', error.message)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
public disconnect() {
|
|
if (this.subscription) {
|
|
try {
|
|
console.log('Cleaning up BLE protocol subscription...')
|
|
this.subscription.remove()
|
|
console.log('BLE protocol subscription removed successfully')
|
|
} catch (e: any) {
|
|
// ✅ 修复:安全处理订阅清理错误,避免崩溃
|
|
const errorMsg = e?.message || String(e)
|
|
console.debug('Subscription cleanup completed with warning:', errorMsg)
|
|
} finally {
|
|
// ✅ 确保订阅引用被清理
|
|
this.subscription = null
|
|
}
|
|
}
|
|
|
|
// ✅ 清理分片缓存
|
|
this.fragments.clear()
|
|
console.log('BLE protocol service disconnected')
|
|
}
|
|
|
|
private handleRawData(deviceId: string, data: Buffer) {
|
|
const frame = ProtocolManager.parseFrame(data.buffer)
|
|
if (!frame) return
|
|
|
|
if (frame.subpageTotal > 0) {
|
|
this.handleFragment(deviceId, frame)
|
|
} else {
|
|
this.emit(frame.type, frame.data, deviceId)
|
|
}
|
|
}
|
|
|
|
private handleFragment(deviceId: string, frame: ProtocolFrame) {
|
|
const key = `${deviceId}_${frame.type}`
|
|
|
|
if (!this.fragments.has(key)) {
|
|
this.fragments.set(key, {
|
|
total: frame.subpageTotal,
|
|
frames: new Array(frame.subpageTotal).fill(null),
|
|
})
|
|
}
|
|
|
|
const session = this.fragments.get(key)!
|
|
// Basic validation
|
|
if (frame.curPage >= session.total) return
|
|
|
|
session.frames[frame.curPage] = frame.data
|
|
|
|
// Check if complete
|
|
if (session.frames.every((f) => f !== null)) {
|
|
const combinedLength = session.frames.reduce((acc, val) => acc + (val ? val.byteLength : 0), 0)
|
|
const combined = new Uint8Array(combinedLength)
|
|
let offset = 0
|
|
|
|
// Reassemble from High to Low pages
|
|
for (let i = session.total - 1; i >= 0; i--) {
|
|
const part = session.frames[i]
|
|
if (part) {
|
|
combined.set(new Uint8Array(part), offset)
|
|
offset += part.byteLength
|
|
}
|
|
}
|
|
|
|
this.fragments.delete(key)
|
|
this.emit(frame.type, combined.buffer as ArrayBuffer, deviceId)
|
|
}
|
|
}
|
|
|
|
private clearFragments(deviceId: string) {
|
|
for (const key of this.fragments.keys()) {
|
|
if (key.startsWith(deviceId)) {
|
|
this.fragments.delete(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
public async send(
|
|
deviceId: string,
|
|
type: APP_COMMAND_TYPES,
|
|
data: object | ArrayBuffer | Uint8Array,
|
|
onProgress?: (progress: number) => void,
|
|
): Promise<void> {
|
|
let payload: Uint8Array
|
|
if (data instanceof ArrayBuffer) {
|
|
console.debug('[BleProtocolService] Sending ArrayBuffer')
|
|
payload = new Uint8Array(data)
|
|
} else if (data instanceof Uint8Array) {
|
|
console.debug('[BleProtocolService] Sending Uint8Array')
|
|
payload = data
|
|
} else {
|
|
console.debug('[BleProtocolService] Sending JSON payload')
|
|
const jsonStr = JSON.stringify(data)
|
|
payload = new Uint8Array(Buffer.from(jsonStr))
|
|
}
|
|
|
|
const device = this.client.getConnectedDevice()
|
|
const mtu = device?.mtu || 23
|
|
// MTU - 3 bytes (ATT overhead) - Protocol Header - Protocol Footer
|
|
const maxPayloadSize = mtu - 3 - FRAME_CONSTANTS.HEADER_SIZE - FRAME_CONSTANTS.FOOTER_SIZE
|
|
// Ensure reasonable bounds (at least 1 byte, max constrained by protocol constant)
|
|
const safeMaxDataSize = Math.max(1, Math.min(maxPayloadSize, FRAME_CONSTANTS.MAX_DATA_SIZE))
|
|
|
|
console.debug(`[BleProtocolService] Sending with MTU=${mtu}, maxDataSize=${safeMaxDataSize}`)
|
|
const rawPayloadHex = payload.reduce((acc, val) => acc + val.toString(16).padStart(2, '0') + ' ', '')
|
|
const formattedRawPayloadHex =
|
|
rawPayloadHex.substring(0, 512 * 2) + '\n......\n' + rawPayloadHex.substring(rawPayloadHex.length - 512 * 2)
|
|
console.debug(
|
|
`[BleProtocolService] Sending payload size=${payload.byteLength}, raw payload hex=\n${formattedRawPayloadHex}`,
|
|
)
|
|
const frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, safeMaxDataSize)
|
|
const total = frames.length
|
|
console.debug(`Sending ${total} frames`)
|
|
for (let i = 0; i < total; i++) {
|
|
const frame = frames[i]
|
|
if (i < 3) {
|
|
const rawFrame = Array.from(frame)
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join(' ')
|
|
console.debug(`raw ${i + 1} frame \n ${rawFrame}`)
|
|
}
|
|
console.debug(`Writing frame ${i + 1}/${total}, length = ${frame.length}`)
|
|
const base64 = Buffer.from(frame).toString('base64')
|
|
const result = await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false)
|
|
await new Promise((resolve) => setTimeout(resolve, FRAME_CONSTANTS.FRAME_INTERVAL))
|
|
if (onProgress) {
|
|
onProgress((i + 1) / total)
|
|
}
|
|
// console.debug("Wrote frame", result);
|
|
}
|
|
}
|
|
|
|
private isExpectedDisconnectionError(error: any): boolean {
|
|
const message = error?.message?.toLowerCase() || ''
|
|
const reason = error?.reason?.toLowerCase() || ''
|
|
|
|
return (
|
|
message.includes('disconnect') ||
|
|
message.includes('gatt_conn_terminate') ||
|
|
message.includes('connection terminated') ||
|
|
reason.includes('disconnect') ||
|
|
error?.errorCode === 19
|
|
) // GATT_CONN_TERMINATE_PEER_USER
|
|
}
|
|
|
|
private isKnownNativeCrashError(error: any): boolean {
|
|
const message = error?.message || ''
|
|
const reason = error?.reason || ''
|
|
|
|
return (
|
|
(error.errorCode === 0 && message.includes('Unknown error') && reason?.includes('PromiseImpl.reject')) ||
|
|
message.includes('Parameter specified as non-null is null') ||
|
|
reason?.includes('SafePromise.reject')
|
|
)
|
|
}
|
|
}
|