import Taro from '@tarojs/taro'; import type { DeviceAdapter, BLEDevice, BLEConnection, BLEConnectionState, NormalizedReading, SyncResult, BLEManagerConfig, } from './types'; import { DataBuffer } from './DataBuffer'; const DEFAULT_CONFIG: BLEManagerConfig = { scanTimeout: 10000, maxReadingsPerSync: 500, retryCount: 3, }; const MAX_LIVE_READINGS = 200; export class BLEManager { private adapters: DeviceAdapter[] = []; private connection: BLEConnection | null = null; private readings: NormalizedReading[] = []; private dataBuffer: DataBuffer; private config: BLEManagerConfig; private scanTimer: ReturnType | null = null; private onConnectionChange?: (state: BLEConnectionState) => void; private onReadings?: (readings: NormalizedReading[]) => void; private connChangeHandler: ((res: any) => void) | null = null; private charChangeHandler: ((res: any) => void) | null = null; constructor(config?: Partial) { this.config = { ...DEFAULT_CONFIG, ...config }; this.dataBuffer = new DataBuffer(); this.dataBuffer.restore(); } /** 注册设备适配器 */ registerAdapter(adapter: DeviceAdapter): void { this.adapters = [...this.adapters, adapter]; } /** 设置连接状态回调 */ setOnConnectionChange(cb: (state: BLEConnectionState) => void): void { this.onConnectionChange = cb; } /** 设置读数回调 */ setOnReadings(cb: (readings: NormalizedReading[]) => void): void { this.onReadings = cb; } private updateState(state: BLEConnectionState, error?: string): void { if (this.connection) { this.connection = { ...this.connection, state, error }; } this.onConnectionChange?.(state); } /** 匹配设备到适配器 */ private matchAdapter(deviceName: string): DeviceAdapter | undefined { const lower = deviceName.toLowerCase(); return this.adapters.find((a) => a.supportedModels.some((m) => lower.includes(m.toLowerCase())), ); } /** 初始化蓝牙适配器 */ async initialize(): Promise { try { await Taro.openBluetoothAdapter(); } catch (e: any) { throw new Error(e.errMsg || '蓝牙初始化失败,请检查蓝牙是否开启'); } } /** 扫描 BLE 设备 */ async scanDevices(): Promise { await this.initialize(); const discovered = new Map(); const onFound = (res: any) => { for (const device of res.devices || []) { const name = device.name || device.localName || ''; if (!name) continue; const adapter = this.matchAdapter(name); if (adapter) { discovered.set(device.deviceId, { deviceId: device.deviceId, name, RSSI: device.RSSI, localName: device.localName, advertisData: device.advertisData, adapter, }); } } }; Taro.onBluetoothDeviceFound(onFound); const allServiceUUIDs = this.adapters.flatMap((a) => a.serviceUUIDs); await Taro.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, services: allServiceUUIDs.length > 0 ? allServiceUUIDs : undefined, }); return new Promise((resolve) => { this.scanTimer = setTimeout(async () => { await this.stopScan(); Taro.offBluetoothDeviceFound(onFound); resolve(Array.from(discovered.values())); }, this.config.scanTimeout); }); } /** 停止扫描 */ async stopScan(): Promise { if (this.scanTimer) { clearTimeout(this.scanTimer); this.scanTimer = null; } try { await Taro.stopBluetoothDevicesDiscovery(); } catch { // 忽略停止扫描错误 } } /** 连接到设备 */ async connect(device: BLEDevice): Promise { if (!device.adapter) throw new Error('设备无适配器'); this.connection = { deviceId: device.deviceId, state: 'connecting', adapter: device.adapter, }; this.updateState('connecting'); this.readings = []; try { await Taro.createBLEConnection({ deviceId: device.deviceId, timeout: 10000, }); // 移除旧监听器,避免多次 connect 累积 if (this.connChangeHandler) { Taro.offBLEConnectionStateChange(this.connChangeHandler); } if (this.charChangeHandler) { Taro.offBLECharacteristicValueChange(this.charChangeHandler); } // 监听断连 this.connChangeHandler = (res: any) => { if (res.deviceId === device.deviceId && !res.connected) { this.updateState('disconnected', '设备断开连接'); this.connection = null; } }; Taro.onBLEConnectionStateChange(this.connChangeHandler); // 发现服务 const servicesRes = await Taro.getBLEDeviceServices({ deviceId: device.deviceId }); const services = servicesRes.services || []; // 启用通知 for (const { service: svcUUID, characteristic: charUUID } of device.adapter.notifyCharacteristics) { const svc = services.find((s: any) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase())); if (!svc) continue; await Taro.getBLEDeviceCharacteristics({ deviceId: device.deviceId, serviceId: svc.uuid, }); await Taro.notifyBLECharacteristicValueChange({ deviceId: device.deviceId, serviceId: svc.uuid, characteristicId: charUUID, state: true, }); } // 监听数据通知 this.charChangeHandler = (res: any) => { if (res.deviceId !== device.deviceId) return; const newReadings = device.adapter!.parseNotification( res.serviceId, res.characteristicId, res.value, ); if (newReadings.length > 0) { const combined = [...this.readings, ...newReadings]; this.readings = combined.length > MAX_LIVE_READINGS ? combined.slice(-MAX_LIVE_READINGS) : combined; this.dataBuffer.push(newReadings); this.onReadings?.(newReadings); } }; Taro.onBLECharacteristicValueChange(this.charChangeHandler); this.connection = { ...this.connection, state: 'connected', connectedAt: Date.now() }; this.updateState('connected'); } catch (e: any) { this.updateState('error', e.errMsg || e.message || '连接失败'); this.connection = null; throw new Error(e.errMsg || '蓝牙连接失败'); } } /** 手动读取特征值 */ async readCharacteristics(): Promise { if (!this.connection || this.connection.state !== 'connected') { throw new Error('设备未连接'); } const { deviceId, adapter } = this.connection; const results: NormalizedReading[] = []; const servicesRes = await Taro.getBLEDeviceServices({ deviceId }); const services = servicesRes.services || []; for (const { service: svcUUID, characteristic: charUUID } of adapter.readCharacteristics) { const svc = services.find((s: any) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase())); if (!svc) continue; try { await Taro.readBLECharacteristicValue({ deviceId, serviceId: svc.uuid, characteristicId: charUUID, }); } catch { // 某些特征值可能不支持读取 } } if (results.length > 0) { this.readings = [...this.readings, ...results]; this.onReadings?.(results); } return results; } /** 同步收集的读数到后端 */ async syncToServer(uploadFn: (readings: NormalizedReading[]) => Promise): Promise { if (!this.connection) { return { success: false, readingsCount: 0, uploadedCount: 0, error: '未连接设备' }; } this.updateState('syncing'); const batch = this.dataBuffer.flush(); if (batch.length === 0) { this.updateState('connected'); return { success: true, readingsCount: 0, uploadedCount: 0 }; } let lastError: string | undefined; for (let attempt = 1; attempt <= this.config.retryCount; attempt++) { try { const uploaded = await uploadFn(batch); this.readings = []; this.updateState('connected'); return { success: true, readingsCount: batch.length, uploadedCount: uploaded, }; } catch (e: any) { lastError = e.message || '上传失败'; // flush 已取出,失败时需要放回 this.dataBuffer.push(batch); } } this.updateState('error', lastError); return { success: false, readingsCount: batch.length, uploadedCount: 0, error: lastError }; } /** 断开连接 */ async disconnect(): Promise { if (!this.connection) return; const { deviceId } = this.connection; // 移除 BLE 监听器,防止断开后仍收到回调 if (this.connChangeHandler) { Taro.offBLEConnectionStateChange(this.connChangeHandler); this.connChangeHandler = null; } if (this.charChangeHandler) { Taro.offBLECharacteristicValueChange(this.charChangeHandler); this.charChangeHandler = null; } try { await Taro.closeBLEConnection({ deviceId }); } catch { // 忽略断连错误 } this.connection = null; this.readings = []; this.updateState('disconnected'); } /** 关闭蓝牙适配器 */ async destroy(): Promise { await this.disconnect(); try { await Taro.closeBluetoothAdapter(); } catch { // 忽略关闭错误 } } /** 获取当前连接信息 */ getConnection(): BLEConnection | null { return this.connection; } /** 获取缓存的读数 */ getCachedReadings(): NormalizedReading[] { return [...this.readings]; } /** 清除缓存读数 */ clearReadings(): void { this.readings = []; } /** 启动时检查缓存,有未上传数据则自动重传 */ async flushPendingReadings( uploadFn: (readings: NormalizedReading[]) => Promise, ): Promise { const batch = this.dataBuffer.flush(); if (batch.length === 0) return 0; try { const uploaded = await uploadFn(batch); return uploaded; } catch { // 失败时放回 this.dataBuffer.push(batch); return 0; } } }