import { describe, it, expect } from 'vitest'; import { createGenericBleAdapter } from '@/services/ble/adapters/GenericBleAdapter'; import type { GenericBLEProfile } from '@/services/ble/types'; // ---- Heart Rate (0x180D / 0x2A37) ---- // Flag byte=0x00 (UINT8), HR=75 function makeHeartRateData(hr: number, isUint16 = false): ArrayBuffer { const buf = new ArrayBuffer(isUint16 ? 3 : 2); const view = new DataView(buf); view.setUint8(0, isUint16 ? 0x01 : 0x00); if (isUint16) { view.setUint16(1, hr, true); } else { view.setUint8(1, hr); } return buf; } // ---- Health Thermometer (0x1809 / 0x2A1C) ---- // IEEE 11073 FLOAT: 32-bit — mantissa (24-bit) + exponent (8-bit) function makeTemperatureData(tempCelsius: number): ArrayBuffer { const buf = new ArrayBuffer(4); const view = new DataView(buf); // flags byte: 0x00 = Celsius, no timestamp, no type view.setUint8(0, 0x00); // 11073 FLOAT: mantissa * 10^exponent // For 36.5: mantissa=365, exponent=-1 const mantissa = Math.round(tempCelsius * 10); const exponent = -1; view.setInt16(1, mantissa, true); view.setInt8(3, exponent); return buf; } describe('GenericBleAdapter', () => { describe('心率解析', () => { const adapter = createGenericBleAdapter({ name: 'Test Wristband', supportedModels: ['TestBand'], profiles: ['heart_rate'], }); it('解析 UINT8 心率', () => { const data = makeHeartRateData(75); const results = adapter.parseNotification( '0000180D-0000-1000-8000-00805f9b34fb', '00002A37-0000-1000-8000-00805f9b34fb', data, ); expect(results.length).toBe(1); expect(results[0].device_type).toBe('heart_rate'); expect(results[0].values.heart_rate).toBe(75); }); it('解析 UINT16 心率', () => { const data = makeHeartRateData(200, true); const results = adapter.parseNotification( '0000180D-0000-1000-8000-00805f9b34fb', '00002A37-0000-1000-8000-00805f9b34fb', data, ); expect(results.length).toBe(1); expect(results[0].values.heart_rate).toBe(200); }); it('忽略非目标 Characteristic', () => { const data = makeHeartRateData(75); const results = adapter.parseNotification( '0000180D-0000-1000-8000-00805f9b34fb', '00002A38-0000-1000-8000-00805f9b34fb', // Body Sensor Location data, ); expect(results.length).toBe(0); }); }); describe('体温解析', () => { const adapter = createGenericBleAdapter({ name: 'Test Thermometer', supportedModels: ['TestThermo'], profiles: ['health_thermometer'], }); it('解析体温读数', () => { const data = makeTemperatureData(36.5); const results = adapter.parseNotification( '00001809-0000-1000-8000-00805f9b34fb', '00002A1C-0000-1000-8000-00805f9b34fb', data, ); expect(results.length).toBe(1); expect(results[0].device_type).toBe('temperature'); expect(results[0].values.value).toBeCloseTo(36.5, 0); }); }); describe('多 Profile 适配器', () => { const adapter = createGenericBleAdapter({ name: 'Multi-Profile Band', supportedModels: ['CustomBand', 'MedicalBand'], profiles: ['heart_rate', 'health_thermometer'], }); it('包含两个 Service UUID', () => { expect(adapter.serviceUUIDs.length).toBe(2); }); it('包含两个 Profile 的 Characteristic', () => { expect(adapter.notifyCharacteristics.length).toBe(2); }); it('supportedModels 配置正确', () => { expect(adapter.supportedModels).toEqual(['CustomBand', 'MedicalBand']); }); it('解析心率 + 体温', () => { const hrResults = adapter.parseNotification( '0000180D-0000-1000-8000-00805f9b34fb', '00002A37-0000-1000-8000-00805f9b34fb', makeHeartRateData(80), ); expect(hrResults[0].device_type).toBe('heart_rate'); const tempResults = adapter.parseNotification( '00001809-0000-1000-8000-00805f9b34fb', '00002A1C-0000-1000-8000-00805f9b34fb', makeTemperatureData(37.2), ); expect(tempResults[0].device_type).toBe('temperature'); }); }); describe('边界情况', () => { const adapter = createGenericBleAdapter({ name: 'Edge Case Band', supportedModels: ['Edge'], profiles: ['heart_rate'], }); it('空数据返回空数组', () => { const results = adapter.parseNotification( '0000180D-0000-1000-8000-00805f9b34fb', '00002A37-0000-1000-8000-00805f9b34fb', new ArrayBuffer(0), ); expect(results.length).toBe(0); }); it('心率超范围 (>300) 返回空数组', () => { const results = adapter.parseNotification( '0000180D-0000-1000-8000-00805f9b34fb', '00002A37-0000-1000-8000-00805f9b34fb', makeHeartRateData(0), ); expect(results.length).toBe(0); }); }); });