import Taro from '@tarojs/taro'; import type { BLEDevice, BLEConnection as BLEConnectionInfo, BLEConnectionState, NormalizedReading, BLEConnectionChangeResult, BLECharacteristicChangeResult, BLEServiceItem, BLEDiscoveredService, BLEDiscoveredCharacteristic, } from './types'; /** BLE 连接管理 — 封装连接/断开/服务发现/通知订阅/数据监听 */ export class BLEConnection { private conn: BLEConnectionInfo | null = null; private readings: NormalizedReading[] = []; private connChangeHandler: ((res: BLEConnectionChangeResult) => void) | null = null; private charChangeHandler: ((res: BLECharacteristicChangeResult) => void) | null = null; private onStateChange: (state: BLEConnectionState, error?: string) => void; private onNewReadings: (readings: NormalizedReading[]) => void; constructor(callbacks: { onStateChange: (state: BLEConnectionState, error?: string) => void; onNewReadings: (readings: NormalizedReading[]) => void; }) { this.onStateChange = callbacks.onStateChange; this.onNewReadings = callbacks.onNewReadings; } /** 获取当前连接信息 */ getConnection(): BLEConnectionInfo | null { return this.conn; } /** 获取缓存的读数 */ getCachedReadings(): NormalizedReading[] { return [...this.readings]; } /** 清除缓存读数 */ clearReadings(): void { this.readings = []; } /** 设置缓存读数(供外部 BLEManager 使用) */ setReadings(readings: NormalizedReading[]): void { this.readings = readings; } private updateState(state: BLEConnectionState, error?: string): void { if (this.conn) { this.conn = { ...this.conn, state, error }; } this.onStateChange(state, error); } /** 连接到设备 */ async connect(device: BLEDevice, maxLiveReadings: number): Promise { if (!device.adapter) throw new Error('设备无适配器'); this.conn = { 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: BLEConnectionChangeResult) => { if (res.deviceId === device.deviceId && !res.connected) { this.updateState('disconnected', '设备断开连接'); this.conn = null; } }; Taro.onBLEConnectionStateChange(this.connChangeHandler); // 发现服务 await this.discoverServices(device); // 监听数据通知 this.charChangeHandler = (res: BLECharacteristicChangeResult) => { 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 > maxLiveReadings ? combined.slice(-maxLiveReadings) : combined; this.onNewReadings(newReadings); } }; Taro.onBLECharacteristicValueChange(this.charChangeHandler); this.conn = { ...this.conn, state: 'connected', connectedAt: Date.now() }; this.updateState('connected'); } catch (e: unknown) { const errMsg = (e as { errMsg?: string })?.errMsg; const msg = errMsg || (e instanceof Error ? e.message : '') || '连接失败'; this.updateState('error', msg); this.conn = null; throw new Error(errMsg || '蓝牙连接失败'); } } /** 已知的健康相关 Characteristic UUID(用于自动发现和订阅) */ private static readonly HEALTH_CHAR_UUIDS: Record = { '2A37': 'heart_rate', // Heart Rate Measurement '2A38': 'heart_rate_loc', // Body Sensor Location '2A1C': 'temperature', // Temperature Measurement '2A35': 'blood_pressure', // Blood Pressure Measurement '2A5F': 'blood_oxygen', // PLX Continuous Measurement '2A5E': 'blood_oxygen_spot',// PLX Spot-Check Measurement }; /** 发现服务并启用通知 — 先订阅适配器指定的,再扫描全部服务尝试自动发现 */ private async discoverServices(device: BLEDevice): Promise { const servicesRes = await Taro.getBLEDeviceServices({ deviceId: device.deviceId }); const services = servicesRes.services || []; const discoveredServices: BLEDiscoveredService[] = []; // ── 第一轮:订阅适配器预定义的 Characteristic(保持向后兼容) ── const subscribedCharUUIDs = new Set(); for (const { service: svcUUID, characteristic: charUUID } of device.adapter!.notifyCharacteristics) { const svc = services.find((s: BLEServiceItem) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()), ); if (!svc) continue; await Taro.getBLEDeviceCharacteristics({ deviceId: device.deviceId, serviceId: svc.uuid, }); try { await Taro.notifyBLECharacteristicValueChange({ deviceId: device.deviceId, serviceId: svc.uuid, characteristicId: charUUID, state: true, }); subscribedCharUUIDs.add(charUUID.toUpperCase().replace(/-/g, '').slice(-4)); console.log(`[ble] 已订阅适配器预定义: ${svcUUID} / ${charUUID}`); } catch (err) { console.warn(`[ble] 订阅失败 (预定义): ${charUUID}`, err); } } // ── 第二轮:扫描全部服务,发现并订阅健康相关 Characteristic ── for (const svc of services) { const svcUUID = svc.uuid.toUpperCase(); const discoveredChars: BLEDiscoveredCharacteristic[] = []; let charsRes: Taro.getBLEDeviceCharacteristics.SuccessCallbackResult; try { charsRes = await Taro.getBLEDeviceCharacteristics({ deviceId: device.deviceId, serviceId: svc.uuid, }); } catch (err) { console.warn(`[ble] 读取特征列表失败: ${svcUUID}`, err); continue; } const characteristics = charsRes.characteristics || []; for (const char of characteristics) { const charUUIDShort = char.uuid.toUpperCase().replace(/-/g, '').slice(-4); const props = char.properties || {}; const discoveredChar: BLEDiscoveredCharacteristic = { uuid: char.uuid, properties: { read: !!props.read, write: !!props.write, notify: !!props.notify, indicate: !!props.indicate, }, }; discoveredChars.push(discoveredChar); // 如果是已知的健康 UUID 且尚未订阅,尝试订阅 if ( BLEConnection.HEALTH_CHAR_UUIDS[charUUIDShort] && !subscribedCharUUIDs.has(charUUIDShort) && (props.notify || props.indicate) ) { try { await Taro.notifyBLECharacteristicValueChange({ deviceId: device.deviceId, serviceId: svc.uuid, characteristicId: char.uuid, state: true, }); subscribedCharUUIDs.add(charUUIDShort); console.log(`[ble] 自动发现并订阅: ${BLEConnection.HEALTH_CHAR_UUIDS[charUUIDShort]} (${svcUUID} / ${char.uuid})`); } catch (err) { console.warn(`[ble] 自动订阅失败: ${char.uuid}`, err); } } } discoveredServices.push({ uuid: svc.uuid, isPrimary: !!svc.isPrimary, characteristics: discoveredChars, }); } // 存储发现结果到连接信息 if (this.conn) { this.conn = { ...this.conn, discoveredServices }; } console.log(`[ble] 服务发现完成: ${discoveredServices.length} 个服务, 已订阅 ${subscribedCharUUIDs.size} 个特征`); console.log(`[ble] 已订阅的健康特征:`, [...subscribedCharUUIDs].map( (s) => `${s}(${BLEConnection.HEALTH_CHAR_UUIDS[s] ?? '未知'})` ).join(', ')); } /** 手动读取特征值 */ async readCharacteristics(): Promise { if (!this.conn || this.conn.state !== 'connected') { throw new Error('设备未连接'); } const { deviceId, adapter } = this.conn; 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: BLEServiceItem) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()), ); if (!svc) continue; try { await Taro.readBLECharacteristicValue({ deviceId, serviceId: svc.uuid, characteristicId: charUUID, }); } catch (err) { console.warn('[ble] 读取特征值失败:', err); } } if (results.length > 0) { this.readings = [...this.readings, ...results]; this.onNewReadings(results); } return results; } /** 断开连接 */ async disconnect(): Promise { if (!this.conn) return; const { deviceId } = this.conn; // 移除 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.conn = null; this.readings = []; this.updateState('disconnected'); } }