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:
iven
2026-05-31 21:48:06 +08:00
parent 6d073840aa
commit 92ffd8cecb
14 changed files with 3419 additions and 603 deletions

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

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

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

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

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