feat(miniprogram): BLE 增强层 — DataBuffer + GenericBleAdapter + DataSyncScheduler
- DataBuffer: 离线持久化缓冲(分桶存储 + 去重 + 容量管理) - GenericBleAdapter: 基于 Bluetooth SIG 标准 Health Profile 的通用适配器 (Heart Rate 0x180D / Health Thermometer 0x1809 / Blood Pressure 0x1810) - DataSyncScheduler: 定时自动同步调度(基于时间间隔判断是否需要同步) - BLEManager: 集成 DataBuffer 替换简单 Storage 缓存 - device-sync 页面: 注册 CustomBandAdapter + 自动同步 + 状态显示 - 新增 vitest 单元测试配置,30 个测试全部通过
This commit is contained in:
145
apps/miniprogram/src/services/ble/adapters/GenericBleAdapter.ts
Normal file
145
apps/miniprogram/src/services/ble/adapters/GenericBleAdapter.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { DeviceAdapter, NormalizedReading, GenericBLEProfile } from '../types';
|
||||
|
||||
// ── Bluetooth SIG 标准 Service UUID ──
|
||||
const SERVICES: Record<string, { uuid: string; chars: { notify: string; read: string } }> = {
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ── 解析器 ──
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── 工厂函数 ──
|
||||
|
||||
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
|
||||
const hrsChar = SERVICES.heart_rate.chars.notify.toUpperCase();
|
||||
if (upper === hrsChar || upper.includes('2A37')) {
|
||||
const result = parseHeartRate(data);
|
||||
return result ? [result] : [];
|
||||
}
|
||||
|
||||
// Temperature Measurement
|
||||
const htChar = SERVICES.health_thermometer.chars.notify.toUpperCase();
|
||||
if (upper === htChar || upper.includes('2A1C')) {
|
||||
const result = parseTemperature(data);
|
||||
return result ? [result] : [];
|
||||
}
|
||||
|
||||
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'],
|
||||
});
|
||||
|
||||
export default CustomBandAdapter;
|
||||
Reference in New Issue
Block a user