feat(mp): Veepoo M2 BLE 管线扩展 — 精准睡眠数据 + 自动测量 + UI 重构
- 新增 VeepooBridge API:精准睡眠读取(readPreciseSleepData)、B3自动测量配置 (readAutoTestConfig/setAutoTestConfig)、开关设置(setAutoHeartRate/BP/Temp)、 体温自动数据读取(readAutoTemperatureData),共 10 个新 API - 新增 SDK 事件类型:SDK_EVENT_SLEEP(4)、SDK_EVENT_AUTO_TEST(54) - VeepooPipeline 新增:readSleepData/readAllSleepData(enableAutoMeasurement 睡眠数据 Promise 化读取 + 自动测量一键开启 - VeepooHistoryReader 新增:uploadSleepReadings 睡眠数据上传 - stores/veepoo.ts 实装:注册 onSleepData 回调、syncHistory 实际读取+上传、 readSleepData 状态管理、enableAutoMeasurement、连接后自动触发三件事 - 原生页面(native/pkg-veepoo):_onReady 后自动读取 3 天睡眠 + 开启自动测量, 新增 _readSleepData/_handleSleepEvent/_enableAutoMeasurement - UI 重构:测量页药丸式选择器+SVG 圆环仪表盘+健康评估标签 - 数据上传页:2 列结果卡片网格+彩色条标识+睡眠数据卡片(★评分+总时长) - 修复上传按钮无响应 bug:patientId 增加 URL fallback + 错误提示不再静默 - 设计原型:docs/design/veepoo-measure-prototype.html(4 状态预览)
This commit is contained in:
305
apps/miniprogram/src/services/ble/VeepooBridge.ts
Normal file
305
apps/miniprogram/src/services/ble/VeepooBridge.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Veepoo SDK 桥接模块
|
||||
*
|
||||
* 调用顺序(基于 SDK Demo 验证):
|
||||
* 1. startScan() — 初始化蓝牙 + 扫描
|
||||
* 2. stopScan() — 找到设备后停止扫描
|
||||
* 3. connectDevice(deviceObj) — 传入完整设备对象(非 deviceId 字符串)
|
||||
* 4. registerDataListener() — 连接成功后注册数据监听
|
||||
* 5. authenticate() — 延迟 500ms 后调用秘钥认证
|
||||
* 6. 认证结果通过数据监听回调 type=1 返回
|
||||
*/
|
||||
|
||||
// @ts-ignore — SDK 类型声明为 any
|
||||
import { veepooBle, veepooFeature, veepooLogger } from './veepoo-sdk';
|
||||
|
||||
// ── SDK 事件类型常量 ──
|
||||
|
||||
/** 秘钥认证结果 */
|
||||
export const SDK_EVENT_AUTH = 1;
|
||||
/** 日常数据 */
|
||||
export const SDK_EVENT_DAILY = 5;
|
||||
/** 体温检测 */
|
||||
export const SDK_EVENT_TEMPERATURE = 6;
|
||||
/** 血压 */
|
||||
export const SDK_EVENT_BLOOD_PRESSURE = 18;
|
||||
/** 血氧手动测量 */
|
||||
export const SDK_EVENT_BLOOD_OXYGEN = 31;
|
||||
/** 心率测量 */
|
||||
export const SDK_EVENT_HEART_RATE = 51;
|
||||
/** 压力测量 */
|
||||
export const SDK_EVENT_PRESSURE = 58;
|
||||
|
||||
/** 设备正忙状态枚举(SDK state 字段) */
|
||||
export const DEVICE_STATE = {
|
||||
IDLE: 0,
|
||||
MEASURING_BP: 1,
|
||||
MEASURING_HR: 2,
|
||||
AUTO_TEST: 3,
|
||||
MEASURING_SPO2: 4,
|
||||
MEASURING_FATIGUE: 5,
|
||||
NOT_WORN: 6,
|
||||
CHARGING: 7,
|
||||
LOW_BATTERY: 8,
|
||||
BUSY: 9,
|
||||
} as const;
|
||||
|
||||
/** 连接回调中 connection 字段为 true 表示连接成功 */
|
||||
export interface VeepooConnectionResult {
|
||||
connection?: boolean;
|
||||
errno?: number;
|
||||
errCode?: number;
|
||||
errMsg?: string;
|
||||
}
|
||||
|
||||
/** SDK 事件回调数据(统一格式) */
|
||||
export interface SdkEventData {
|
||||
name: string;
|
||||
type: number;
|
||||
content: Record<string, unknown>;
|
||||
Progress?: number;
|
||||
state?: number;
|
||||
control?: number;
|
||||
ack?: number;
|
||||
}
|
||||
|
||||
// ── 蓝牙模块 ──
|
||||
|
||||
/** 初始化蓝牙 + 开始扫描 */
|
||||
export function startScan(onDeviceFound: (device: unknown) => void): void {
|
||||
veepooBle.veepooWeiXinSDKStartScanDeviceAndReceiveScanningDevice(
|
||||
(res: unknown) => {
|
||||
const device = Array.isArray(res) ? res[0] : res;
|
||||
if (device) onDeviceFound(device);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 停止扫描 */
|
||||
export function stopScan(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
veepooBle.veepooWeiXinSDKStopSearchBleManager(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
/** 连接设备 — 传入完整设备对象 */
|
||||
export function connectDevice(device: unknown): Promise<VeepooConnectionResult> {
|
||||
return new Promise((resolve) => {
|
||||
veepooBle.veepooWeiXinSDKBleConnectionServicesCharacteristicsNotifyManager(
|
||||
device,
|
||||
(res: VeepooConnectionResult) => resolve(res),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** 注册数据监听(必须在连接成功后调用) */
|
||||
export function registerDataListener(callback: (data: SdkEventData) => void): void {
|
||||
veepooBle.veepooWeiXinSDKNotifyMonitorValueChange(callback);
|
||||
}
|
||||
|
||||
/** 监听蓝牙连接状态变化 */
|
||||
export function registerConnectionListener(callback: (res: { deviceId: string; connected: boolean }) => void): void {
|
||||
veepooBle.veepooWeiXinSDKBLEConnectionStateChangeManager(callback);
|
||||
}
|
||||
|
||||
/** 断开连接 */
|
||||
export function disconnect(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
veepooBle.veepooWeiXinSDKloseBluetoothAdapterManager(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
// ── 功能模块:认证 ──
|
||||
|
||||
/** 秘钥认证(无参数无回调,结果通过数据监听 type=1 返回) */
|
||||
export function authenticate(): void {
|
||||
veepooFeature.veepooBlePasswordCheckManager();
|
||||
}
|
||||
|
||||
// ── 功能模块:测量指令 ──
|
||||
|
||||
/** 心率测量开关(true=开启,false=关闭) */
|
||||
export function setHeartRateMeasure(on: boolean): void {
|
||||
veepooFeature.veepooSendHeartRateTestSwitchManager({ switch: on });
|
||||
}
|
||||
|
||||
/** 血氧测量开关('start'=开启,'stop'=关闭) */
|
||||
export function setBloodOxygenMeasure(action: 'start' | 'stop'): void {
|
||||
veepooFeature.veepooSendBloodOxygenControlDataManager({ switch: action });
|
||||
}
|
||||
|
||||
/** 血压测量开关('start'=开启,'stop'=关闭) */
|
||||
export function setBloodPressureMeasure(action: 'start' | 'stop'): void {
|
||||
veepooFeature.veepooSendReadUniversalBloodPressureDataManager({ switch: action });
|
||||
}
|
||||
|
||||
/** 体温测量(单次触发) */
|
||||
export function startTemperatureMeasure(): void {
|
||||
veepooFeature.veepooSendTemperatureMeasurementSwitchManager();
|
||||
}
|
||||
|
||||
/** 压力测量开关(true=开启,false=关闭) */
|
||||
export function setPressureMeasure(on: boolean): void {
|
||||
veepooFeature.veepooSendPressureTestManager({ switch: on });
|
||||
}
|
||||
|
||||
// ── 功能模块:日常数据 ──
|
||||
|
||||
/** 读取日常数据(day: 0=今天, 1=昨天, 2=前天;package: 开始包序号,默认1) */
|
||||
export function readDailyData(day: number, pkg: number = 1): void {
|
||||
veepooFeature.veepooSendReadDailyDataManager({ day, package: pkg });
|
||||
}
|
||||
|
||||
// ── 功能模块:精准睡眠数据 ──
|
||||
|
||||
/** 精准睡眠事件类型 */
|
||||
export const SDK_EVENT_SLEEP = 4;
|
||||
|
||||
/** 精准睡眠数据(SDK 回调 type=4) */
|
||||
export interface SleepData {
|
||||
/** 入睡时间(时间戳字符串) */
|
||||
fallAsleepTime: string;
|
||||
/** 退出睡眠时间(时间戳字符串) */
|
||||
exitSleepTime: string;
|
||||
/** 起夜得分 */
|
||||
nightScore: number;
|
||||
/** 深睡得分 */
|
||||
deepSleepScore: number;
|
||||
/** 睡眠效率得分 */
|
||||
sleepEfficiencyScore: number;
|
||||
/** 入睡效率得分 */
|
||||
fallAsleepEfficiencyScore: number;
|
||||
/** 睡眠时长得分 */
|
||||
sleepTimeScore: number;
|
||||
/** 睡眠质量(1-5 星) */
|
||||
sleepQuality: number;
|
||||
/** 深睡时长(分钟) */
|
||||
deepSleepTime: number;
|
||||
/** 浅睡时长(分钟) */
|
||||
lightSleepTime: number;
|
||||
/** 其他睡眠时长(分钟) */
|
||||
otherSleepTime: number;
|
||||
/** 睡眠总时长(分钟) */
|
||||
sleepTotalTime: number;
|
||||
/** 首次深睡眠时长(分钟) */
|
||||
firstDeepSleepTime: number;
|
||||
/** 起夜总时长(分钟) */
|
||||
nightTotalTime: number;
|
||||
/** 起夜到深睡均值 */
|
||||
nightDeepSleepMeanValue: number;
|
||||
/** 失眠得分 */
|
||||
insomniaScore: number;
|
||||
/** 失眠次数 */
|
||||
insomniaCount: number;
|
||||
/** 睡眠曲线字符串(0=深睡, 1=浅睡, 2=REM, 3=失眠, 4=苏醒) */
|
||||
sleepCurve: string;
|
||||
}
|
||||
|
||||
/** 读取精准睡眠数据(day: 0=今天, 1=昨天, 2=前天) */
|
||||
export function readPreciseSleepData(day: number): void {
|
||||
veepooFeature.veepooSendReadPreciseSleepManager({ day });
|
||||
}
|
||||
|
||||
// ── 功能模块:自动测量(B3) ──
|
||||
|
||||
/** 自动测量事件类型 */
|
||||
export const SDK_EVENT_AUTO_TEST = 54;
|
||||
|
||||
/** B3 自动测量功能类型枚举 */
|
||||
export const AUTO_TEST_FUN_TYPES = {
|
||||
PULSE_RATE: 0, // 脉率
|
||||
BLOOD_PRESSURE: 1, // 血压
|
||||
BLOOD_GLUCOSE: 2, // 血糖
|
||||
PRESSURE: 3, // 压力
|
||||
BLOOD_OXYGEN: 4, // 血氧
|
||||
TEMPERATURE: 5, // 体温
|
||||
LORENTZ_SCATTER: 6, // 洛伦兹散点图
|
||||
HRV: 7, // HRV
|
||||
BLOOD_COMPONENT: 8, // 血液成分
|
||||
} as const;
|
||||
|
||||
export type AutoTestFunType = (typeof AUTO_TEST_FUN_TYPES)[keyof typeof AUTO_TEST_FUN_TYPES];
|
||||
|
||||
/** B3 自动测量配置项 */
|
||||
export interface AutoTestConfig {
|
||||
/** 协议类型(不可修改) */
|
||||
protocolType: number;
|
||||
/** 功能类型 0-8(可修改) */
|
||||
funTypeContent: AutoTestFunType;
|
||||
/** 开关:0=关闭, 1=开启 */
|
||||
funSwitch: number;
|
||||
/** 最小步进(分钟) */
|
||||
stepUnit: number;
|
||||
/** 是否支持时间段修改 */
|
||||
timeSlotModify: number;
|
||||
/** 是否支持时间间隔修改 */
|
||||
timeIntervalModify: number;
|
||||
/** 支持的测试时间段 */
|
||||
supportTimeSlot: { startTime: string; stopTime: string };
|
||||
/** 测量间隔(分钟,按 stepUnit 递增) */
|
||||
measInterval: number;
|
||||
/** 当前测试时间段 */
|
||||
currentTimeSlot: { startTime: string; stopTime: string };
|
||||
}
|
||||
|
||||
/** 读取自动测量功能配置 */
|
||||
export function readAutoTestConfig(): void {
|
||||
veepooFeature.veepooSendReadB3AutoTestFeatureDataManager();
|
||||
}
|
||||
|
||||
/** 设置自动测量功能 */
|
||||
export function setAutoTestConfig(config: AutoTestConfig): void {
|
||||
veepooFeature.veepooSendSetupB3AutoTestFeatureDataManager({
|
||||
p_protocol_type: config.protocolType,
|
||||
p_fun_type_content: config.funTypeContent,
|
||||
p_fun_switch: config.funSwitch,
|
||||
p_step_unit: config.stepUnit,
|
||||
p_time_slot_modify: config.timeSlotModify,
|
||||
p_time_interval_modify: config.timeIntervalModify,
|
||||
p_support_time_slot: config.supportTimeSlot,
|
||||
p_meas_inv: config.measInterval,
|
||||
p_cur_time_slot: config.currentTimeSlot,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 功能模块:开关设置 ──
|
||||
|
||||
/** 自动心率监测开关 */
|
||||
export function setAutoHeartRate(enabled: boolean): void {
|
||||
veepooFeature.veepooSendSwitchSettingDataManager({
|
||||
VPSettingAutomaticHRTest: enabled ? 'open' : 'close',
|
||||
});
|
||||
}
|
||||
|
||||
/** 自动血压监测开关 */
|
||||
export function setAutoBloodPressure(enabled: boolean): void {
|
||||
veepooFeature.veepooSendSwitchSettingDataManager({
|
||||
VPSettingAutomaticBPTest: enabled ? 'open' : 'close',
|
||||
});
|
||||
}
|
||||
|
||||
/** 体温自动监测 */
|
||||
export function setAutoTemperature(enabled: boolean): void {
|
||||
veepooFeature.veepooSendSwitchSettingDataManager({
|
||||
VPSettingAutomaticTemperatureTest: enabled ? 'open' : 'close',
|
||||
});
|
||||
}
|
||||
|
||||
/** 读取体温自动监测数据 */
|
||||
export function readAutoTemperatureData(): void {
|
||||
veepooFeature.veepooReadAutoTemperatureMeasurementDataManager({ day: 0 });
|
||||
}
|
||||
|
||||
// ── 功能模块:设备信息 ──
|
||||
|
||||
/** 读取设备电量 */
|
||||
export function readBatteryLevel(): void {
|
||||
veepooFeature.veepooReadElectricQuantityManager();
|
||||
}
|
||||
|
||||
// ── 日志模块 ──
|
||||
|
||||
/** 设置日志级别(0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=NONE) */
|
||||
export function setLogLevel(level: number): void {
|
||||
veepooLogger.setLevel(level);
|
||||
}
|
||||
245
apps/miniprogram/src/services/ble/veepoo/VeepooHistoryReader.ts
Normal file
245
apps/miniprogram/src/services/ble/veepoo/VeepooHistoryReader.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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<number> {
|
||||
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<void> {
|
||||
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<typeof setTimeout> | 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<void> {
|
||||
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<number> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
588
apps/miniprogram/src/services/ble/veepoo/VeepooPipeline.ts
Normal file
588
apps/miniprogram/src/services/ble/veepoo/VeepooPipeline.ts
Normal file
@@ -0,0 +1,588 @@
|
||||
/**
|
||||
* Veepoo 管线 — SDK 事件路由 + 连接编排 + 测量 Promise 封装
|
||||
*
|
||||
* 职责:
|
||||
* 1. 连接流程编排:扫描 → 连接 → 注册监听 → 认证 → 就绪
|
||||
* 2. SDK 事件路由:registerDataListener 按 type 分发
|
||||
* 3. 测量 Promise 化:startMeasure(type) → Promise<MeasureResult>
|
||||
*/
|
||||
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
startScan,
|
||||
stopScan,
|
||||
connectDevice,
|
||||
registerDataListener,
|
||||
registerConnectionListener,
|
||||
authenticate,
|
||||
disconnect as veepooDisconnect,
|
||||
setHeartRateMeasure,
|
||||
setBloodOxygenMeasure,
|
||||
setBloodPressureMeasure,
|
||||
startTemperatureMeasure,
|
||||
setPressureMeasure,
|
||||
readBatteryLevel,
|
||||
readPreciseSleepData,
|
||||
readAutoTestConfig,
|
||||
setAutoHeartRate,
|
||||
setAutoBloodPressure,
|
||||
setAutoTemperature,
|
||||
setLogLevel,
|
||||
SDK_EVENT_AUTH,
|
||||
SDK_EVENT_HEART_RATE,
|
||||
SDK_EVENT_BLOOD_OXYGEN,
|
||||
SDK_EVENT_BLOOD_PRESSURE,
|
||||
SDK_EVENT_TEMPERATURE,
|
||||
SDK_EVENT_PRESSURE,
|
||||
SDK_EVENT_DAILY,
|
||||
SDK_EVENT_SLEEP,
|
||||
SDK_EVENT_AUTO_TEST,
|
||||
DEVICE_STATE,
|
||||
} from '../VeepooBridge';
|
||||
import type { SdkEventData } from '../VeepooBridge';
|
||||
import type { MeasureType, MeasureResult, SleepReading } from './types';
|
||||
|
||||
const AUTH_TIMEOUT = 8_000;
|
||||
const AUTH_POLL_INTERVAL = 500;
|
||||
const MEASURE_SETTLE_DELAY = 1_500;
|
||||
|
||||
/** pending 测量的 resolve/reject 句柄 */
|
||||
interface PendingMeasure {
|
||||
type: MeasureType;
|
||||
resolve: (result: MeasureResult) => void;
|
||||
reject: (error: Error) => void;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
lastValue: number | null;
|
||||
lastValues: Record<string, number>;
|
||||
settleTimer: ReturnType<typeof setTimeout> | null;
|
||||
}
|
||||
|
||||
/** SDK type 到 MeasureType 的映射 */
|
||||
const SDK_TYPE_TO_MEASURE: Record<number, MeasureType> = {
|
||||
[SDK_EVENT_HEART_RATE]: 'heart_rate',
|
||||
[SDK_EVENT_BLOOD_OXYGEN]: 'blood_oxygen',
|
||||
[SDK_EVENT_BLOOD_PRESSURE]: 'blood_pressure',
|
||||
[SDK_EVENT_TEMPERATURE]: 'temperature',
|
||||
[SDK_EVENT_PRESSURE]: 'pressure',
|
||||
};
|
||||
|
||||
export type ConnectionChangeCallback = (connected: boolean, deviceId: string) => void;
|
||||
export type AuthResultCallback = (success: boolean) => void;
|
||||
export type MeasureEventCallback = (type: MeasureType, data: Record<string, unknown>) => void;
|
||||
export type DailyDataCallback = (data: SdkEventData) => void;
|
||||
export type SleepDataCallback = (day: number, sleep: SleepReading) => void;
|
||||
|
||||
export class VeepooPipeline {
|
||||
private pending: PendingMeasure | null = null;
|
||||
private isConnected = false;
|
||||
private deviceId = '';
|
||||
|
||||
/** 睡眠数据读取 Promise resolve 队列 */
|
||||
private sleepResolvers: Map<number, (sleep: SleepReading | null) => void> = new Map();
|
||||
private sleepTimeouts: Map<number, ReturnType<typeof setTimeout>> = new Map();
|
||||
|
||||
private onConnectionChange?: ConnectionChangeCallback;
|
||||
private onAuthResult?: AuthResultCallback;
|
||||
private onMeasureEvent?: MeasureEventCallback;
|
||||
private onDailyData?: DailyDataCallback;
|
||||
private onSleepData?: SleepDataCallback;
|
||||
|
||||
/** 注册回调 */
|
||||
setCallbacks(cbs: {
|
||||
onConnectionChange?: ConnectionChangeCallback;
|
||||
onAuthResult?: AuthResultCallback;
|
||||
onMeasureEvent?: MeasureEventCallback;
|
||||
onDailyData?: DailyDataCallback;
|
||||
onSleepData?: SleepDataCallback;
|
||||
}): void {
|
||||
this.onConnectionChange = cbs.onConnectionChange;
|
||||
this.onAuthResult = cbs.onAuthResult;
|
||||
this.onMeasureEvent = cbs.onMeasureEvent;
|
||||
this.onDailyData = cbs.onDailyData;
|
||||
this.onSleepData = cbs.onSleepData;
|
||||
}
|
||||
|
||||
/** 全流程:扫描 → 连接 → 注册监听 → 认证 */
|
||||
async connect(targetName: string, debug = false): Promise<string> {
|
||||
console.log('[veepoo-pipeline] connect() 开始, target:', targetName);
|
||||
if (debug) setLogLevel(0);
|
||||
|
||||
// 1. 扫描
|
||||
console.log('[veepoo-pipeline] Step 1: 扫描...');
|
||||
const device = await this.scanFor(targetName);
|
||||
if (!device) {
|
||||
console.error('[veepoo-pipeline] 扫描未找到设备');
|
||||
throw new Error(`未找到设备 ${targetName}`);
|
||||
}
|
||||
console.log('[veepoo-pipeline] 找到设备:', (device as Record<string, unknown>)?.deviceId);
|
||||
|
||||
// 2. 连接
|
||||
console.log('[veepoo-pipeline] Step 2: 连接...');
|
||||
const connRes = await connectDevice(device);
|
||||
console.log('[veepoo-pipeline] 连接结果:', JSON.stringify(connRes));
|
||||
// SDK 连接成功返回 errno=0 或 connection=true,两种都要兼容
|
||||
const ok = connRes?.connection === true || connRes?.errno === 0 || connRes?.errCode === 0;
|
||||
if (!ok) throw new Error('连接失败');
|
||||
|
||||
const id = (device as Record<string, unknown>).deviceId as string;
|
||||
this.deviceId = id;
|
||||
this.isConnected = true;
|
||||
|
||||
// 3. 注册数据监听(连接成功后)
|
||||
registerDataListener((data) => this.routeEvent(data));
|
||||
registerConnectionListener((res) => {
|
||||
this.isConnected = res.connected;
|
||||
this.onConnectionChange?.(res.connected, res.deviceId);
|
||||
});
|
||||
|
||||
// 4. 认证(延迟 500ms)
|
||||
await delay(500);
|
||||
authenticate();
|
||||
|
||||
// 5. 等待认证结果
|
||||
const authOk = await this.waitForAuth();
|
||||
if (!authOk) throw new Error('设备认证失败,请重新连接');
|
||||
|
||||
// 6. 读取电量
|
||||
readBatteryLevel();
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/** 扫描指定名称的设备 */
|
||||
private scanFor(targetName: string): Promise<unknown | null> {
|
||||
return new Promise((resolve) => {
|
||||
let found: unknown = null;
|
||||
const upper = targetName.toUpperCase();
|
||||
|
||||
startScan((device) => {
|
||||
const d = device as Record<string, unknown>;
|
||||
const name = String(d.localName ?? d.name ?? '').toUpperCase();
|
||||
if (name.includes(upper) && !found) {
|
||||
found = device;
|
||||
stopScan().then(() => resolve(found));
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!found) {
|
||||
stopScan().then(() => resolve(null));
|
||||
}
|
||||
}, 10_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** 等待认证结果(轮询 deviceChipStatus) */
|
||||
private waitForAuth(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
|
||||
const poll = () => {
|
||||
try {
|
||||
const status = Taro.getStorageSync('deviceChipStatus');
|
||||
if (status === 'successfulVerification' || status === 'passTheVerification') {
|
||||
this.onAuthResult?.(true);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
if (Date.now() - start >= AUTH_TIMEOUT) {
|
||||
this.onAuthResult?.(false);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(poll, AUTH_POLL_INTERVAL);
|
||||
};
|
||||
|
||||
poll();
|
||||
});
|
||||
}
|
||||
|
||||
/** SDK 事件路由 */
|
||||
private routeEvent(data: SdkEventData): void {
|
||||
const eventType = data.type;
|
||||
|
||||
// 认证回调
|
||||
if (eventType === SDK_EVENT_AUTH) {
|
||||
const content = data.content ?? {};
|
||||
const password = content.VPDevicepassword;
|
||||
if (password === 'passTheVerification' || password === 'successfulVerification') {
|
||||
this.onAuthResult?.(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 日常数据
|
||||
if (eventType === SDK_EVENT_DAILY) {
|
||||
this.onDailyData?.(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// 精准睡眠数据
|
||||
if (eventType === SDK_EVENT_SLEEP) {
|
||||
this.handleSleepEvent(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动测量功能回调
|
||||
if (eventType === SDK_EVENT_AUTO_TEST) {
|
||||
console.log('[veepoo-pipeline] 自动测量配置回调:', JSON.stringify(data).substring(0, 300));
|
||||
return;
|
||||
}
|
||||
|
||||
// 测量数据
|
||||
const measureType = SDK_TYPE_TO_MEASURE[eventType];
|
||||
if (!measureType) return;
|
||||
|
||||
this.handleMeasureEvent(measureType, data);
|
||||
this.onMeasureEvent?.(measureType, data.content ?? {});
|
||||
}
|
||||
|
||||
/** 处理测量事件 */
|
||||
private handleMeasureEvent(type: MeasureType, data: SdkEventData): void {
|
||||
if (!this.pending || this.pending.type !== type) return;
|
||||
|
||||
const content = data.content ?? {};
|
||||
|
||||
// 检查设备状态错误
|
||||
const deviceBusy = content.deviceBusy === true;
|
||||
const notWear = content.notWear === true;
|
||||
const state = data.state;
|
||||
const ack = data.ack;
|
||||
|
||||
if (deviceBusy) {
|
||||
this.rejectPending(new Error('设备正忙,请稍后重试'));
|
||||
return;
|
||||
}
|
||||
if (notWear || state === DEVICE_STATE.NOT_WORN) {
|
||||
this.rejectPending(new Error('请将手环佩戴到手腕上'));
|
||||
return;
|
||||
}
|
||||
if (state === DEVICE_STATE.CHARGING) {
|
||||
this.rejectPending(new Error('设备正在充电,请取出后重试'));
|
||||
return;
|
||||
}
|
||||
if (state === DEVICE_STATE.LOW_BATTERY) {
|
||||
this.rejectPending(new Error('设备电量不足,请充电后重试'));
|
||||
return;
|
||||
}
|
||||
if (type === 'pressure' && ack === 2) {
|
||||
this.rejectPending(new Error('设备电量不足'));
|
||||
return;
|
||||
}
|
||||
if (type === 'pressure' && ack === 3) {
|
||||
this.rejectPending(new Error('设备正在测量其他数据'));
|
||||
return;
|
||||
}
|
||||
if (type === 'pressure' && ack === 4) {
|
||||
this.rejectPending(new Error('佩戴检测未通过'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取数值
|
||||
const values = this.extractValues(type, content);
|
||||
if (!values) return;
|
||||
|
||||
// 更新 pending 最新值
|
||||
this.pending.lastValues = values;
|
||||
|
||||
// 对于进度型指标,检查是否完成
|
||||
const progress = data.Progress;
|
||||
if (progress !== undefined && progress >= 100) {
|
||||
this.resolvePending(values);
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于持续测量型/单次型,收到第一个有效值后延迟 settle
|
||||
if (this.pending.settleTimer === null) {
|
||||
this.pending.settleTimer = setTimeout(() => {
|
||||
if (this.pending && this.pending.lastValues && Object.keys(this.pending.lastValues).length > 0) {
|
||||
this.resolvePending(this.pending.lastValues);
|
||||
}
|
||||
}, MEASURE_SETTLE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 SDK content 提取标准化数值 */
|
||||
private extractValues(type: MeasureType, content: Record<string, unknown>): Record<string, number> | null {
|
||||
switch (type) {
|
||||
case 'heart_rate': {
|
||||
const hr = Number(content.heartRate);
|
||||
if (hr >= 30 && hr <= 250) return { heart_rate: hr };
|
||||
return null;
|
||||
}
|
||||
case 'blood_oxygen': {
|
||||
const bo = Number(content.bloodOxygen);
|
||||
if (bo >= 70 && bo <= 100) return { blood_oxygen: bo };
|
||||
return null;
|
||||
}
|
||||
case 'blood_pressure': {
|
||||
const high = Number(content.bloodPressureHigh);
|
||||
const low = Number(content.bloodPressureLow);
|
||||
if (high > 0 && low > 0) return { systolic: high, diastolic: low };
|
||||
return null;
|
||||
}
|
||||
case 'temperature': {
|
||||
const temp = Number(content.bodyTemperature);
|
||||
if (temp > 30 && temp < 45) return { temperature: temp };
|
||||
return null;
|
||||
}
|
||||
case 'pressure': {
|
||||
const p = Number(content.pressure);
|
||||
if (p >= 0 && p <= 100) return { pressure: p };
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 发起测量 */
|
||||
startMeasure(type: MeasureType): Promise<MeasureResult> {
|
||||
if (this.pending) {
|
||||
throw new Error(`正在测量 ${this.pending.type},请等待完成`);
|
||||
}
|
||||
if (!this.isConnected) {
|
||||
throw new Error('设备未连接');
|
||||
}
|
||||
|
||||
return new Promise<MeasureResult>((resolve, reject) => {
|
||||
const timeout = getMeasureTimeout(type);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.rejectPending(new Error('测量超时,请重试'));
|
||||
}, timeout);
|
||||
|
||||
this.pending = {
|
||||
type,
|
||||
resolve,
|
||||
reject,
|
||||
timer,
|
||||
lastValue: null,
|
||||
lastValues: {},
|
||||
settleTimer: null,
|
||||
};
|
||||
|
||||
// 发送 SDK 测量指令
|
||||
this.sendMeasureCommand(type);
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消当前测量 */
|
||||
cancelMeasure(): void {
|
||||
if (!this.pending) return;
|
||||
this.stopMeasureCommand(this.pending.type);
|
||||
if (this.pending.lastValues && Object.keys(this.pending.lastValues).length > 0) {
|
||||
this.resolvePending(this.pending.lastValues);
|
||||
} else {
|
||||
this.rejectPending(new Error('测量已取消'));
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送 SDK 测量指令 */
|
||||
private sendMeasureCommand(type: MeasureType): void {
|
||||
switch (type) {
|
||||
case 'heart_rate':
|
||||
setHeartRateMeasure(true);
|
||||
break;
|
||||
case 'blood_oxygen':
|
||||
setBloodOxygenMeasure('start');
|
||||
break;
|
||||
case 'blood_pressure':
|
||||
setBloodPressureMeasure('start');
|
||||
break;
|
||||
case 'temperature':
|
||||
startTemperatureMeasure();
|
||||
break;
|
||||
case 'pressure':
|
||||
setPressureMeasure(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送 SDK 停止测量指令 */
|
||||
private stopMeasureCommand(type: MeasureType): void {
|
||||
switch (type) {
|
||||
case 'heart_rate':
|
||||
setHeartRateMeasure(false);
|
||||
break;
|
||||
case 'blood_oxygen':
|
||||
setBloodOxygenMeasure('stop');
|
||||
break;
|
||||
case 'blood_pressure':
|
||||
setBloodPressureMeasure('stop');
|
||||
break;
|
||||
case 'temperature':
|
||||
break; // 体温是单次触发,无法停止
|
||||
case 'pressure':
|
||||
setPressureMeasure(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** 成功 resolve pending 测量 */
|
||||
private resolvePending(values: Record<string, number>): void {
|
||||
if (!this.pending) return;
|
||||
const p = this.pending;
|
||||
this.pending = null;
|
||||
|
||||
clearTimeout(p.timer);
|
||||
if (p.settleTimer) clearTimeout(p.settleTimer);
|
||||
|
||||
// 停止持续测量型指标的 SDK 指令
|
||||
this.stopMeasureCommand(p.type);
|
||||
|
||||
p.resolve({
|
||||
type: p.type,
|
||||
values,
|
||||
measuredAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/** 失败 reject pending 测量 */
|
||||
private rejectPending(error: Error): void {
|
||||
if (!this.pending) return;
|
||||
const p = this.pending;
|
||||
this.pending = null;
|
||||
|
||||
clearTimeout(p.timer);
|
||||
if (p.settleTimer) clearTimeout(p.settleTimer);
|
||||
|
||||
// 停止 SDK 指令
|
||||
this.stopMeasureCommand(p.type);
|
||||
|
||||
p.reject(error);
|
||||
}
|
||||
|
||||
// ── 睡眠数据 ──
|
||||
|
||||
/** 读取单天精准睡眠数据,返回 Promise */
|
||||
readSleepData(day: number): Promise<SleepReading | null> {
|
||||
if (!this.isConnected) {
|
||||
return Promise.reject(new Error('设备未连接'));
|
||||
}
|
||||
|
||||
return new Promise<SleepReading | null>((resolve) => {
|
||||
this.sleepResolvers.set(day, resolve);
|
||||
|
||||
// 超时保护 30s
|
||||
const timer = setTimeout(() => {
|
||||
this.sleepResolvers.delete(day);
|
||||
this.sleepTimeouts.delete(day);
|
||||
resolve(null);
|
||||
}, 30_000);
|
||||
this.sleepTimeouts.set(day, timer);
|
||||
|
||||
// 发送 SDK 读取指令
|
||||
readPreciseSleepData(day);
|
||||
});
|
||||
}
|
||||
|
||||
/** 读取 3 天睡眠数据 */
|
||||
async readAllSleepData(): Promise<SleepReading[]> {
|
||||
const results: SleepReading[] = [];
|
||||
for (let day = 0; day < 3; day++) {
|
||||
const sleep = await this.readSleepData(day);
|
||||
if (sleep) {
|
||||
results.push(sleep);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** 处理 SDK 睡眠数据回调(type=4) */
|
||||
private handleSleepEvent(data: SdkEventData): void {
|
||||
const progress = data.Progress ?? 0;
|
||||
const readDay = (data as { readDay?: number }).readDay ?? 0;
|
||||
|
||||
// 进度未达 100% 忽略
|
||||
if (progress < 100) return;
|
||||
|
||||
const content = data.content ?? {};
|
||||
const sleep = this.parseSleepData(readDay, content as Record<string, unknown>);
|
||||
|
||||
// 通知回调
|
||||
if (sleep) {
|
||||
this.onSleepData?.(readDay, sleep);
|
||||
}
|
||||
|
||||
// resolve 等待中的 Promise
|
||||
const resolve = this.sleepResolvers.get(readDay);
|
||||
if (resolve) {
|
||||
const timer = this.sleepTimeouts.get(readDay);
|
||||
if (timer) clearTimeout(timer);
|
||||
this.sleepResolvers.delete(readDay);
|
||||
this.sleepTimeouts.delete(readDay);
|
||||
resolve(sleep);
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 SDK content 解析精准睡眠数据 */
|
||||
private parseSleepData(day: number, content: Record<string, unknown>): SleepReading | null {
|
||||
const total = Number(content.sleepTotalTime ?? 0);
|
||||
if (total <= 0) return null;
|
||||
|
||||
return {
|
||||
day,
|
||||
deepSleepMinutes: Number(content.deepSleepTime ?? 0),
|
||||
lightSleepMinutes: Number(content.lightSleepTime ?? 0),
|
||||
otherSleepMinutes: Number(content.otherSleepTime ?? 0),
|
||||
totalSleepMinutes: total,
|
||||
qualityScore: Number(content.sleepQuality ?? 0),
|
||||
fallAsleepTime: String(content.fallAsleepTime ?? ''),
|
||||
exitSleepTime: String(content.exitSleepTime ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
// ── 自动测量 ──
|
||||
|
||||
/** 开启自动测量(心率 + 血压 + 血氧 + 体温) */
|
||||
enableAutoMeasurement(): void {
|
||||
if (!this.isConnected) return;
|
||||
|
||||
console.log('[veepoo-pipeline] 开启自动测量功能');
|
||||
setAutoHeartRate(true);
|
||||
setAutoBloodPressure(true);
|
||||
setAutoTemperature(true);
|
||||
|
||||
// 读取当前自动测量配置
|
||||
readAutoTestConfig();
|
||||
}
|
||||
|
||||
/** 断开连接 */
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.pending) {
|
||||
this.rejectPending(new Error('设备已断开'));
|
||||
}
|
||||
this.isConnected = false;
|
||||
this.deviceId = '';
|
||||
await veepooDisconnect();
|
||||
}
|
||||
|
||||
/** 获取连接状态 */
|
||||
getConnected(): boolean {
|
||||
return this.isConnected;
|
||||
}
|
||||
|
||||
/** 获取设备 ID */
|
||||
getDeviceId(): string {
|
||||
return this.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
function getMeasureTimeout(type: MeasureType): number {
|
||||
const timeouts: Record<MeasureType, number> = {
|
||||
heart_rate: 60_000,
|
||||
blood_oxygen: 60_000,
|
||||
blood_pressure: 120_000,
|
||||
temperature: 60_000,
|
||||
pressure: 90_000,
|
||||
};
|
||||
return timeouts[type];
|
||||
}
|
||||
21
apps/miniprogram/src/services/ble/veepoo/index.ts
Normal file
21
apps/miniprogram/src/services/ble/veepoo/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export { VeepooPipeline } from './VeepooPipeline';
|
||||
export { VeepooHistoryReader } from './VeepooHistoryReader';
|
||||
export type {
|
||||
ConnectionChangeCallback,
|
||||
AuthResultCallback,
|
||||
MeasureEventCallback,
|
||||
DailyDataCallback,
|
||||
} from './VeepooPipeline';
|
||||
export type {
|
||||
MeasureType,
|
||||
MeasurePhase,
|
||||
MeasureStatus,
|
||||
MeasureResult,
|
||||
MeasureConfig,
|
||||
ConnectionPhase,
|
||||
VeepooDeviceInfo,
|
||||
HistorySyncState,
|
||||
SleepReading,
|
||||
AutoTestSyncState,
|
||||
} from './types';
|
||||
export { MEASURE_TYPES, MEASURE_CONFIG } from './types';
|
||||
152
apps/miniprogram/src/services/ble/veepoo/types.ts
Normal file
152
apps/miniprogram/src/services/ble/veepoo/types.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/** Veepoo 管线专用类型定义 */
|
||||
|
||||
/** 测量指标类型 */
|
||||
export type MeasureType =
|
||||
| 'heart_rate'
|
||||
| 'blood_oxygen'
|
||||
| 'blood_pressure'
|
||||
| 'temperature'
|
||||
| 'pressure';
|
||||
|
||||
/** 所有支持的测量指标 */
|
||||
export const MEASURE_TYPES: readonly MeasureType[] = [
|
||||
'heart_rate',
|
||||
'blood_oxygen',
|
||||
'blood_pressure',
|
||||
'temperature',
|
||||
'pressure',
|
||||
] as const;
|
||||
|
||||
/** 测量指标配置 */
|
||||
export interface MeasureConfig {
|
||||
label: string;
|
||||
unit: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
/** 正常范围 [min, max] */
|
||||
normalRange: [number, number];
|
||||
/** 测量超时(毫秒) */
|
||||
timeout: number;
|
||||
/** 测量模式 */
|
||||
mode: 'continuous' | 'progress' | 'single';
|
||||
}
|
||||
|
||||
/** 各指标配置表 */
|
||||
export const MEASURE_CONFIG: Record<MeasureType, MeasureConfig> = {
|
||||
heart_rate: {
|
||||
label: '心率',
|
||||
unit: 'bpm',
|
||||
icon: '♥',
|
||||
color: '#EF4444',
|
||||
normalRange: [60, 100],
|
||||
timeout: 60_000,
|
||||
mode: 'continuous',
|
||||
},
|
||||
blood_oxygen: {
|
||||
label: '血氧',
|
||||
unit: '%',
|
||||
icon: 'O₂',
|
||||
color: '#3B82F6',
|
||||
normalRange: [95, 100],
|
||||
timeout: 60_000,
|
||||
mode: 'continuous',
|
||||
},
|
||||
blood_pressure: {
|
||||
label: '血压',
|
||||
unit: 'mmHg',
|
||||
icon: '↕',
|
||||
color: '#8B5CF6',
|
||||
normalRange: [90, 140],
|
||||
timeout: 120_000,
|
||||
mode: 'progress',
|
||||
},
|
||||
temperature: {
|
||||
label: '体温',
|
||||
unit: '°C',
|
||||
icon: 'T',
|
||||
color: '#F59E0B',
|
||||
normalRange: [36.0, 37.3],
|
||||
timeout: 60_000,
|
||||
mode: 'single',
|
||||
},
|
||||
pressure: {
|
||||
label: '压力',
|
||||
unit: '',
|
||||
icon: '~',
|
||||
color: '#6366F1',
|
||||
normalRange: [1, 40],
|
||||
timeout: 90_000,
|
||||
mode: 'progress',
|
||||
},
|
||||
};
|
||||
|
||||
/** 连接阶段 */
|
||||
export type ConnectionPhase =
|
||||
| 'idle'
|
||||
| 'scanning'
|
||||
| 'connecting'
|
||||
| 'authenticating'
|
||||
| 'ready'
|
||||
| 'disconnected'
|
||||
| 'error';
|
||||
|
||||
/** 测量阶段 */
|
||||
export type MeasurePhase = 'idle' | 'measuring' | 'success' | 'error';
|
||||
|
||||
/** 单个指标的测量状态 */
|
||||
export interface MeasureStatus {
|
||||
phase: MeasurePhase;
|
||||
progress: number;
|
||||
currentValue: number | null;
|
||||
result: MeasureResult | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/** 测量结果 */
|
||||
export interface MeasureResult {
|
||||
type: MeasureType;
|
||||
values: Record<string, number>;
|
||||
measuredAt: number;
|
||||
}
|
||||
|
||||
/** 设备信息 */
|
||||
export interface VeepooDeviceInfo {
|
||||
deviceId: string;
|
||||
name: string;
|
||||
batteryLevel: number | null;
|
||||
}
|
||||
|
||||
/** 历史数据同步状态 */
|
||||
export interface HistorySyncState {
|
||||
phase: 'idle' | 'reading' | 'uploading' | 'done';
|
||||
progress: number;
|
||||
packagesRead: number;
|
||||
lastCheckpoint: number;
|
||||
}
|
||||
|
||||
/** 睡眠数据(从 SDK 精准睡眠解析) */
|
||||
export interface SleepReading {
|
||||
/** 读取天数(0=今天, 1=昨天, 2=前天) */
|
||||
day: number;
|
||||
/** 深睡时长(分钟) */
|
||||
deepSleepMinutes: number;
|
||||
/** 浅睡时长(分钟) */
|
||||
lightSleepMinutes: number;
|
||||
/** 其他睡眠时长(分钟) */
|
||||
otherSleepMinutes: number;
|
||||
/** 睡眠总时长(分钟) */
|
||||
totalSleepMinutes: number;
|
||||
/** 睡眠质量评分(1-5 星) */
|
||||
qualityScore: number;
|
||||
/** 入睡时间(时间戳字符串) */
|
||||
fallAsleepTime: string;
|
||||
/** 退出睡眠时间(时间戳字符串) */
|
||||
exitSleepTime: string;
|
||||
}
|
||||
|
||||
/** 自动测量同步状态 */
|
||||
export interface AutoTestSyncState {
|
||||
phase: 'idle' | 'reading_config' | 'configuring' | 'configured';
|
||||
enabledTypes: string[];
|
||||
intervalMinutes: number;
|
||||
}
|
||||
Reference in New Issue
Block a user