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:
@@ -8,9 +8,7 @@ import type {
|
||||
SyncResult,
|
||||
BLEManagerConfig,
|
||||
} from './types';
|
||||
|
||||
const CACHE_KEY = 'ble_pending_readings';
|
||||
const CACHE_MAX = 2000;
|
||||
import { DataBuffer } from './DataBuffer';
|
||||
|
||||
const DEFAULT_CONFIG: BLEManagerConfig = {
|
||||
scanTimeout: 10000,
|
||||
@@ -22,6 +20,7 @@ export class BLEManager {
|
||||
private adapters: DeviceAdapter[] = [];
|
||||
private connection: BLEConnection | null = null;
|
||||
private readings: NormalizedReading[] = [];
|
||||
private dataBuffer: DataBuffer;
|
||||
private config: BLEManagerConfig;
|
||||
private scanTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private onConnectionChange?: (state: BLEConnectionState) => void;
|
||||
@@ -29,6 +28,8 @@ export class BLEManager {
|
||||
|
||||
constructor(config?: Partial<BLEManagerConfig>) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
this.dataBuffer = new DataBuffer();
|
||||
this.dataBuffer.restore();
|
||||
}
|
||||
|
||||
/** 注册设备适配器 */
|
||||
@@ -182,7 +183,7 @@ export class BLEManager {
|
||||
);
|
||||
if (newReadings.length > 0) {
|
||||
this.readings = [...this.readings, ...newReadings];
|
||||
this.persistPendingReadings();
|
||||
this.dataBuffer.push(newReadings);
|
||||
this.onReadings?.(newReadings);
|
||||
}
|
||||
});
|
||||
@@ -213,8 +214,6 @@ export class BLEManager {
|
||||
if (!svc) continue;
|
||||
|
||||
try {
|
||||
// Taro readBLECharacteristicValue 触发 onBLECharacteristicValueChange 回调
|
||||
// 读取结果会通过 BLEManager 已注册的 onBLECharacteristicValueChange 监听器返回
|
||||
await Taro.readBLECharacteristicValue({
|
||||
deviceId,
|
||||
serviceId: svc.uuid,
|
||||
@@ -241,7 +240,7 @@ export class BLEManager {
|
||||
|
||||
this.updateState('syncing');
|
||||
|
||||
const batch = this.readings.slice(-this.config.maxReadingsPerSync);
|
||||
const batch = this.dataBuffer.flush();
|
||||
if (batch.length === 0) {
|
||||
this.updateState('connected');
|
||||
return { success: true, readingsCount: 0, uploadedCount: 0 };
|
||||
@@ -251,8 +250,7 @@ export class BLEManager {
|
||||
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
|
||||
try {
|
||||
const uploaded = await uploadFn(batch);
|
||||
this.readings = this.readings.slice(batch.length);
|
||||
this.clearPendingReadings();
|
||||
this.readings = [];
|
||||
this.updateState('connected');
|
||||
return {
|
||||
success: true,
|
||||
@@ -261,6 +259,8 @@ export class BLEManager {
|
||||
};
|
||||
} catch (e: any) {
|
||||
lastError = e.message || '上传失败';
|
||||
// flush 已取出,失败时需要放回
|
||||
this.dataBuffer.push(batch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,46 +309,19 @@ export class BLEManager {
|
||||
this.readings = [];
|
||||
}
|
||||
|
||||
// ── 离线缓存 ──
|
||||
|
||||
/** 将当前未上传读数同步写入 Storage */
|
||||
private persistPendingReadings(): void {
|
||||
try {
|
||||
const batch = this.readings.slice(-CACHE_MAX);
|
||||
Taro.setStorageSync(CACHE_KEY, JSON.stringify(batch));
|
||||
} catch {
|
||||
// Storage 写入失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传成功后清除缓存 */
|
||||
private clearPendingReadings(): void {
|
||||
try {
|
||||
Taro.removeStorageSync(CACHE_KEY);
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动时检查缓存,有未上传数据则自动重传。
|
||||
* 返回重传的记录数 */
|
||||
/** 启动时检查缓存,有未上传数据则自动重传 */
|
||||
async flushPendingReadings(
|
||||
uploadFn: (readings: NormalizedReading[]) => Promise<number>,
|
||||
): Promise<number> {
|
||||
try {
|
||||
const raw = Taro.getStorageSync(CACHE_KEY) as string;
|
||||
if (!raw) return 0;
|
||||
const cached: NormalizedReading[] = JSON.parse(raw);
|
||||
if (!Array.isArray(cached) || cached.length === 0) {
|
||||
Taro.removeStorageSync(CACHE_KEY);
|
||||
return 0;
|
||||
}
|
||||
const batch = this.dataBuffer.flush();
|
||||
if (batch.length === 0) return 0;
|
||||
|
||||
const batch = cached.slice(0, this.config.maxReadingsPerSync);
|
||||
try {
|
||||
const uploaded = await uploadFn(batch);
|
||||
Taro.removeStorageSync(CACHE_KEY);
|
||||
return uploaded;
|
||||
} catch {
|
||||
// 失败时放回
|
||||
this.dataBuffer.push(batch);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user