import type { DeviceAdapter, NormalizedReading, GenericBLEProfile } from '../types'; // ── Bluetooth SIG 标准 Service UUID ── const SERVICES: Record = { heart_rate: { uuid: '0000180D-0000-1000-8000-00805F9B34FB', chars: { notify: '00002A37-0000-1000-8000-00805F9B34FB', read: '00002A37-0000-1000-8000-00805F9B34FB', }, }, health_thermometer: { uuid: '00001809-0000-1000-8000-00805F9B34FB', chars: { notify: '00002A1C-0000-1000-8000-00805F9B34FB', read: '00002A1C-0000-1000-8000-00805F9B34FB', }, }, blood_pressure: { uuid: '00001810-0000-1000-8000-00805F9B34FB', chars: { notify: '00002A35-0000-1000-8000-00805F9B34FB', read: '00002A35-0000-1000-8000-00805F9B34FB', }, }, pulse_oximeter: { uuid: '00001822-0000-1000-8000-00805F9B34FB', chars: { // PLX Continuous Measurement — 实时血氧+脉率 notify: '00002A5F-0000-1000-8000-00805F9B34FB', // PLX Spot-Check Measurement — 单次测量 read: '00002A5E-0000-1000-8000-00805F9B34FB', }, }, }; // ── IEEE 11073 SFLOAT 解析(Bluetooth SIG 医疗 Profile 通用格式) ── /** 特殊 SFLOAT 值 */ const SFLOAT_NAN = 0x07FF; const SFLOAT_NRES = 0x0800; const SFLOAT_POS_INF = 0x07FE; const SFLOAT_NEG_INF = 0x0802; function parseSFLOAT(view: DataView, offset: number): number | null { if (offset + 2 > view.byteLength) return null; const raw = view.getUint16(offset, true); if (raw === SFLOAT_NAN || raw === SFLOAT_NRES) return null; if (raw === SFLOAT_POS_INF) return Infinity; if (raw === SFLOAT_NEG_INF) return -Infinity; const signM = (raw >> 15) & 0x01; const exp = (raw >> 12) & 0x07; const mantissa = raw & 0x0FFF; // 指数用 3 位补码表示(0-3 正,4-7 负) const exponent = exp >= 4 ? exp - 8 : exp; const signedMantissa = signM ? -(mantissa ^ 0x0FFF) - 1 : mantissa; return signedMantissa * Math.pow(10, exponent); } // ── 解析器 ── function parseHeartRate(data: ArrayBuffer): NormalizedReading | null { const view = new DataView(data); if (view.byteLength < 2) return null; const flags = view.getUint8(0); const isUINT16 = (flags & 0x01) !== 0; const hr = isUINT16 ? (view.byteLength >= 3 ? view.getUint16(1, true) : null) : view.getUint8(1); if (hr === null || hr <= 0 || hr > 300) return null; return { device_type: 'heart_rate', values: { heart_rate: hr }, measured_at: new Date().toISOString(), }; } function parseTemperature(data: ArrayBuffer): NormalizedReading | null { const view = new DataView(data); if (view.byteLength < 4) return null; const flags = view.getUint8(0); const isFahrenheit = (flags & 0x01) !== 0; const mantissa = view.getInt16(1, true); const exponent = view.getInt8(3); const temp = mantissa * Math.pow(10, exponent); const tempCelsius = isFahrenheit ? (temp - 32) * 5 / 9 : temp; return { device_type: 'temperature', values: { value: Math.round(tempCelsius * 10) / 10, unit: '°C' }, measured_at: new Date().toISOString(), }; } /** * 解析 Pulse Oximeter Service 数据 * PLX Continuous Measurement (0x2A5F) 和 Spot-Check (0x2A5E) 共用 * 格式: Flags(1B) + SpO2(SFLOAT 2B) + PulseRate(SFLOAT 2B) + optional... */ function parsePulseOximeter(data: ArrayBuffer): NormalizedReading[] { const view = new DataView(data); if (view.byteLength < 5) return []; const spO2 = parseSFLOAT(view, 1); const pulseRate = parseSFLOAT(view, 3); const now = new Date().toISOString(); const results: NormalizedReading[] = []; if (spO2 !== null && spO2 >= 0 && spO2 <= 100) { results.push({ device_type: 'blood_oxygen', values: { blood_oxygen: Math.round(spO2), unit: '%' }, measured_at: now, }); } if (pulseRate !== null && pulseRate > 0 && pulseRate <= 300) { results.push({ device_type: 'heart_rate', values: { heart_rate: Math.round(pulseRate) }, measured_at: now, }); } return results; } // ── 工厂函数 ── export interface GenericAdapterConfig { name: string; supportedModels: string[]; profiles: GenericBLEProfile[]; } /** 创建通用 BLE 适配器 — 基于 Bluetooth SIG 标准 Health Profile */ export function createGenericBleAdapter(config: GenericAdapterConfig): DeviceAdapter { const activeServices = config.profiles .map((p) => SERVICES[p]) .filter(Boolean); return { name: config.name, supportedModels: config.supportedModels, serviceUUIDs: activeServices.map((s) => s.uuid), notifyCharacteristics: activeServices.map((s) => ({ service: s.uuid, characteristic: s.chars.notify, })), readCharacteristics: activeServices.map((s) => ({ service: s.uuid, characteristic: s.chars.read, })), parseNotification( _serviceUUID: string, charUUID: string, data: ArrayBuffer, ): NormalizedReading[] { const upper = charUUID.toUpperCase(); // Heart Rate Measurement (0x2A37) if (upper.includes('2A37')) { const result = parseHeartRate(data); return result ? [result] : []; } // Temperature Measurement (0x2A1C) if (upper.includes('2A1C')) { const result = parseTemperature(data); return result ? [result] : []; } // Pulse Oximeter Continuous (0x2A5F) / Spot-Check (0x2A5E) if (upper.includes('2A5F') || upper.includes('2A5E')) { return parsePulseOximeter(data); } return []; }, parseReadResponse( serviceUUID: string, charUUID: string, data: ArrayBuffer, ): NormalizedReading[] { return this.parseNotification(serviceUUID, charUUID, data); }, }; } /** * 预配置:通用定制手环(心率 + 体温) * 未来定制手环只需在 supportedModels 中加入型号名 */ export const CustomBandAdapter = createGenericBleAdapter({ name: 'Custom Health Band', supportedModels: [ 'Health Band', 'Medical Band', 'Smart Bracelet', '健康手环', ], profiles: ['heart_rate', 'health_thermometer'], }); /** 华为手环/手表 BLE 适配器 */ export const HuaweiBandAdapter = createGenericBleAdapter({ name: 'Huawei Band', supportedModels: [ 'HUAWEI Band', 'HUAWEI Watch', 'Huawei Band', 'Huawei Watch', 'HW-B', 'HUAW', '华为手环', '华为手表', ], profiles: ['heart_rate', 'health_thermometer', 'pulse_oximeter'], }); /** * 万能 fallback 适配器 — 匹配所有有名称的设备 * 尝试标准 BLE 健康协议(心率/体温/血压),设备不支持的服务会被安全跳过 */ export const FallbackAdapter = createGenericBleAdapter({ name: '通用设备', supportedModels: [], // 不参与 matchAdapter,仅作为 fallback profiles: ['heart_rate', 'health_thermometer', 'blood_pressure'], }); export default CustomBandAdapter;