/** * Veepoo 历史数据读取器 — 3天日常数据分批读取 + 上传 * * SDK 日常数据格式(type=5): * - 包含计步、心率、血压、血氧、睡眠、压力、体温等 * - Progress 字段 1-100% 表示读取进度 * - 每次回调可能包含一包数据 */ import Taro from '@tarojs/taro'; import { readDailyData } from '../VeepooBridge'; import type { SdkEventData } from '../VeepooBridge'; import type { NormalizedReading } from '../types'; import type { SleepReading } from './types'; import { uploadReadings } from '@/services/device-sync'; const CHECKPOINT_KEY = 'veepoo_history_checkpoint'; const UPLOAD_BATCH_SIZE = 20; interface Checkpoint { lastProgress: number; packagesRead: number; deviceId: string; timestamp: number; } export type HistoryReadPhase = 'idle' | 'reading' | 'uploading' | 'done' | 'error'; export class VeepooHistoryReader { private phase: HistoryReadPhase = 'idle'; private progress = 0; private packagesRead = 0; private buffer: NormalizedReading[] = []; private day = 0; private patientId = ''; private deviceId = ''; private onProgress?: (progress: number, phase: HistoryReadPhase) => void; private uploadedCount = 0; setCallbacks(cbs: { onProgress?: (progress: number, phase: HistoryReadPhase) => void }): void { this.onProgress = cbs.onProgress; } /** 开始读取3天数据 */ async startRead(patientId: string, deviceId: string): Promise { this.patientId = patientId; this.deviceId = deviceId; this.buffer = []; this.uploadedCount = 0; this.phase = 'reading'; // 依次读取 3 天数据 for (let day = 0; day < 3; day++) { this.day = day; this.progress = 0; this.onProgress?.(0, 'reading'); await this.readDay(day); // 刷新剩余 buffer if (this.buffer.length > 0) { await this.flushBuffer(); } } this.phase = 'done'; this.onProgress?.(100, 'done'); this.clearCheckpoint(); return this.uploadedCount; } /** 读取单天数据 */ private readDay(day: number): Promise { return new Promise((resolve) => { // 发送读取指令 readDailyData(day, 1); // 进度通过 handleDailyEvent 更新 // Progress=100 时 resolve this.dayResolve = resolve; // 超时保护:30s this.dayTimeout = setTimeout(() => { this.dayResolve = null; resolve(); }, 30_000); }); } private dayResolve: (() => void) | null = null; private dayTimeout: ReturnType | null = null; /** 处理 SDK 日常数据回调 */ handleDailyEvent(data: SdkEventData): void { if (this.phase !== 'reading') return; const progress = (data.Progress ?? 0) as number; this.progress = progress; this.onProgress?.(progress, 'reading'); // 解析数据 const readings = this.parseDailyData(data); if (readings.length > 0) { this.buffer.push(...readings); this.packagesRead++; } // 达到批量大小就上传 if (this.buffer.length >= UPLOAD_BATCH_SIZE) { this.flushBuffer(); } // 进度 100% 表示当天数据读取完成 if (progress >= 100) { if (this.dayTimeout) clearTimeout(this.dayTimeout); this.dayTimeout = null; const resolve = this.dayResolve; this.dayResolve = null; resolve?.(); } } /** 解析 SDK 日常数据为 NormalizedReading */ private parseDailyData(data: SdkEventData): NormalizedReading[] { const content = data.content ?? {}; const readings: NormalizedReading[] = []; const now = new Date(); // 偏移到对应天 const baseDate = new Date(now.getTime() - this.day * 86400000); const timestamp = baseDate.toISOString(); // 心率 const hr = content.heartReat ?? content.heartRate; if (typeof hr === 'number' && hr >= 30 && hr <= 250) { readings.push({ device_type: 'heart_rate', values: { heart_rate: hr }, measured_at: timestamp }); } // 血氧 const bo = content.bloodOxygen; if (typeof bo === 'number' && bo >= 70 && bo <= 100) { readings.push({ device_type: 'blood_oxygen', values: { blood_oxygen: bo }, measured_at: timestamp }); } // 血压 const bph = content.bloodPressureHigh; const bpl = content.bloodPressureLow; if (typeof bph === 'number' && typeof bpl === 'number' && bph > 0 && bpl > 0) { readings.push({ device_type: 'blood_pressure', values: { systolic: bph, diastolic: bpl }, measured_at: timestamp }); } // 体温 const temp = content.bodyTemperature; if (typeof temp === 'number' && temp > 30 && temp < 45) { readings.push({ device_type: 'temperature', values: { temperature: temp }, measured_at: timestamp }); } // 压力 const pressure = content.pressure; if (typeof pressure === 'number' && pressure >= 0 && pressure <= 100) { readings.push({ device_type: 'stress', values: { value: pressure }, measured_at: timestamp }); } // 步数 const steps = content.stepCount ?? content.steps; if (typeof steps === 'number' && steps >= 0) { readings.push({ device_type: 'steps', values: { value: steps }, measured_at: timestamp }); } return readings; } /** 上传 buffer 中的数据 */ private async flushBuffer(): Promise { if (this.buffer.length === 0) return; const batch = this.buffer.splice(0, this.buffer.length); this.phase = 'uploading'; this.onProgress?.(this.progress, 'uploading'); try { await uploadReadings(this.patientId, this.deviceId, 'Veepoo M2', batch); this.uploadedCount += batch.length; this.saveCheckpoint(); } catch { // 上传失败,放回 buffer this.buffer.unshift(...batch); } this.phase = 'reading'; } private saveCheckpoint(): void { try { const checkpoint: Checkpoint = { lastProgress: this.progress, packagesRead: this.packagesRead, deviceId: this.deviceId, timestamp: Date.now(), }; Taro.setStorageSync(CHECKPOINT_KEY, JSON.stringify(checkpoint)); } catch { /* ignore */ } } private clearCheckpoint(): void { try { Taro.removeStorageSync(CHECKPOINT_KEY); } catch { /* ignore */ } } getPhase(): HistoryReadPhase { return this.phase; } getProgress(): number { return this.progress; } getUploadedCount(): number { return this.uploadedCount; } // ── 睡眠数据上传 ── /** 将睡眠数据转换为 NormalizedReading 并上传 */ async uploadSleepReadings(patientId: string, deviceId: string, sleepData: SleepReading[]): Promise { if (sleepData.length === 0) return 0; const now = new Date(); const readings: NormalizedReading[] = sleepData.map((sleep) => { // 根据天数偏移计算日期 const baseDate = new Date(now.getTime() - sleep.day * 86400000); return { device_type: 'sleep', values: { deep_sleep_minutes: sleep.deepSleepMinutes, light_sleep_minutes: sleep.lightSleepMinutes, total_sleep_minutes: sleep.totalSleepMinutes, quality_score: sleep.qualityScore, }, measured_at: baseDate.toISOString(), }; }); try { await uploadReadings(patientId, deviceId, 'Veepoo M2', readings); this.uploadedCount += readings.length; console.log('[veepoo-history] 睡眠数据上传成功:', readings.length, '条'); return readings.length; } catch (err) { console.error('[veepoo-history] 睡眠数据上传失败:', err); return 0; } } }