feat(miniprogram): BLE 增强层 — DataBuffer + GenericBleAdapter + DataSyncScheduler
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 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:
iven
2026-05-04 02:42:58 +08:00
parent 70aacf47a0
commit 62c02e0f15
15 changed files with 1272 additions and 119 deletions

View 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;