Files
hms/apps/miniprogram/native/pkg-veepoo/index.js
iven bc3c056c8d fix(mp): 只在 connection:true 最终回调触发认证,修复过早认证无响应
连接回调触发 4 次(createBLEConnection → services → characteristics → connection:true),
errno:0 在第一次回调就匹配了条件,导致认证指令在特征值订阅完成前发送,
BLE 通知通道未建立,认证响应无法送达。

修正:仅 result.connection === true 时触发认证流程,
这是 SDK 合并接口的最终就绪信号。
2026-05-30 13:53:31 +08:00

501 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Veepoo M2 原生小程序页面 — 连接 + 测量
*
* 完全脱离 Taro 框架,直接使用微信原生 API + Veepoo SDK。
* 连接流程严格对齐官方 Demo
* scan → stopScan → connect(callback, 等待 connection:true)
* → registerDataListener → delay 500ms → authenticate
* → 轮询 deviceChipStatus → ready
*/
// eslint-disable-next-line no-undef
const { veepooBle, veepooFeature, veepooLogger } = require('./libs/veepoo-sdk');
// ── 常量 ──
var SDK_EVENT_AUTH = 1;
var SDK_EVENT_BATTERY = 2;
var SDK_EVENT_TEMPERATURE = 6;
var SDK_EVENT_BLOOD_PRESSURE = 18;
var SDK_EVENT_BLOOD_OXYGEN = 31;
var SDK_EVENT_HEART_RATE = 51;
var SDK_EVENT_PRESSURE = 58;
var MEASURE_TYPES = [
{ type: 'heart_rate', label: '心率', unit: 'bpm', icon: '♥', color: '#EF4444', sdkType: SDK_EVENT_HEART_RATE },
{ type: 'blood_oxygen', label: '血氧', unit: '%', icon: 'O₂', color: '#3B82F6', sdkType: SDK_EVENT_BLOOD_OXYGEN },
{ type: 'blood_pressure', label: '血压', unit: 'mmHg', icon: '↕', color: '#8B5CF6', sdkType: SDK_EVENT_BLOOD_PRESSURE },
{ type: 'temperature', label: '体温', unit: '°C', icon: 'T', color: '#F59E0B', sdkType: SDK_EVENT_TEMPERATURE },
{ type: 'pressure', label: '压力', unit: '', icon: '~', color: '#6366F1', sdkType: SDK_EVENT_PRESSURE },
];
var MEASURE_TIMEOUTS = {
heart_rate: 60000,
blood_oxygen: 60000,
blood_pressure: 120000,
temperature: 60000,
pressure: 90000,
};
var MEASURE_SETTLE_DELAY = 1500;
function _findConfig(type) {
for (var i = 0; i < MEASURE_TYPES.length; i++) {
if (MEASURE_TYPES[i].type === type) return MEASURE_TYPES[i];
}
return MEASURE_TYPES[0];
}
// ── Page ──
// eslint-disable-next-line no-undef
Page({
data: {
phase: 'idle',
deviceId: '',
deviceName: 'M2',
batteryLevel: null,
error: '',
selectedType: 'heart_rate',
selectedIcon: '♥',
selectedColor: '#EF4444',
selectedLabel: '心率',
selectedUnit: 'bpm',
measureTypes: MEASURE_TYPES,
measurePhase: 'idle',
measureProgress: 0,
measureDisplayValue: '',
measureError: '',
results: {},
hasResults: false,
},
_authTimer: null,
_authTimeout: null,
_scanTimer: null,
_scanFound: null,
_measureTimer: null,
_settleTimer: null,
_lastValues: null,
_connected: false,
_eventChannel: null,
_connecting: false,
// ── 生命周期 ──
onLoad: function () {
// eslint-disable-next-line no-undef
this._eventChannel = this.getOpenerEventChannel();
// eslint-disable-next-line no-undef
wx.setNavigationBarTitle({ title: 'M2 手环测量' });
this._updateSelectedDisplay('heart_rate');
// 诊断:确认 SDK 加载状态
// eslint-disable-next-line no-undef
console.log('[veepoo-native] SDK 加载状态:', {
veepooBle: typeof veepooBle,
veepooFeature: typeof veepooFeature,
veepooLogger: typeof veepooLogger,
scanFn: typeof veepooBle.veepooWeiXinSDKStartScanDeviceAndReceiveScanningDevice,
});
},
onUnload: function () {
this._cleanup();
},
_cleanup: function () {
if (this._authTimer) { clearInterval(this._authTimer); this._authTimer = null; }
if (this._authTimeout) { clearTimeout(this._authTimeout); this._authTimeout = null; }
if (this._scanTimer) { clearTimeout(this._scanTimer); this._scanTimer = null; }
if (this._measureTimer) { clearTimeout(this._measureTimer); this._measureTimer = null; }
if (this._settleTimer) { clearTimeout(this._settleTimer); this._settleTimer = null; }
this._connecting = false;
if (this._connected) {
try { veepooBle.veepooWeiXinSDKloseBluetoothAdapterManager(function () {}); } catch (e) {
// eslint-disable-next-line no-undef
console.warn('[veepoo-native] 断开异常:', e);
}
this._connected = false;
}
},
_updateSelectedDisplay: function (type) {
var cfg = _findConfig(type);
this.setData({
selectedType: type,
selectedIcon: cfg.icon,
selectedColor: cfg.color,
selectedLabel: cfg.label,
selectedUnit: cfg.unit,
});
},
// ── 连接流程(严格对齐官方 Demo ──
handleConnect: function () {
// H2 防重入
if (this.data.phase !== 'idle' && this.data.phase !== 'error' && this.data.phase !== 'disconnected') return;
if (this._connecting) return;
this._connecting = true;
this.setData({ phase: 'scanning', error: '' });
veepooLogger.setLevel(0);
var self = this;
self._scanFound = null;
veepooBle.veepooWeiXinSDKStartScanDeviceAndReceiveScanningDevice(function (res) {
var device = Array.isArray(res) ? res[0] : res;
if (!device) return;
var name = (device.localName || device.name || '').toUpperCase();
var deviceId = device.deviceId || device.mac || '';
// eslint-disable-next-line no-undef
console.log('[veepoo-native] 扫描到:', name, deviceId, JSON.stringify(device).substring(0, 200));
// 放宽匹配:包含 M2 / VPM / VEEPOO 均视为目标设备
if (!self._scanFound && (name.indexOf('M2') !== -1 || name.indexOf('VPM') !== -1 || name.indexOf('VEEPOO') !== -1)) {
self._scanFound = device;
veepooBle.veepooWeiXinSDKStopSearchBleManager(function () {
self._doConnect(device);
});
}
});
this._scanTimer = setTimeout(function () {
if (!self._scanFound) {
veepooBle.veepooWeiXinSDKStopSearchBleManager(function () {});
self._connecting = false;
self.setData({ phase: 'error', error: '未找到 M2 设备,请确保手环已开机且蓝牙已开启' });
}
}, 15000);
},
_doConnect: function (device) {
this.setData({ phase: 'connecting' });
// eslint-disable-next-line no-undef
console.log('[veepoo-native] 连接设备:', device.deviceId || device.mac);
var self = this;
// 关键修复:在连接前注册数据监听器
// SDK 的 veepooWeiXinSDKNotifyMonitorValueChange 内部封装 wx.onBLECharacteristicValueChange
// 必须在连接 + 订阅特征值之前注册否则会丢失认证响应type=1
self._registerListeners();
veepooBle.veepooWeiXinSDKBleConnectionServicesCharacteristicsNotifyManager(device, function (result) {
// eslint-disable-next-line no-undef
console.log('[veepoo-native] 连接回调:', JSON.stringify(result));
// 只响应最终回调connection:true忽略中间阶段services/characteristics/errno:0
// 连接回调会触发多次createBLEConnection → services → characteristics → connection:true
// 认证必须在特征值订阅完成后发送
if (result.connection === true) {
self._connected = true;
self._connecting = false;
self.setData({
deviceId: device.deviceId || device.mac || '',
});
// 连接成功后延迟 500ms 发送认证指令(等待特征值订阅就绪)
// eslint-disable-next-line no-undef
setTimeout(function () {
veepooFeature.veepooBlePasswordCheckManager();
self.setData({ phase: 'authenticating' });
// eslint-disable-next-line no-undef
console.log('[veepoo-native] 认证指令已发送');
}, 500);
// 双重检测认证结果事件监听器type=1+ Storage 轮询兜底
self._authTimer = setInterval(function () {
try {
// eslint-disable-next-line no-undef
var status = wx.getStorageSync('deviceChipStatus');
if (status === 'successfulVerification' || status === 'passTheVerification') {
clearInterval(self._authTimer);
self._authTimer = null;
self._onReady();
}
} catch (_) { /* ignore */ }
}, 500);
self._authTimeout = setTimeout(function () {
if (self._authTimer) {
clearInterval(self._authTimer);
self._authTimer = null;
self._connecting = false;
// eslint-disable-next-line no-undef
console.error('[veepoo-native] 认证超时deviceChipStatus=', wx.getStorageSync('deviceChipStatus'));
self.setData({ phase: 'error', error: '设备认证超时,请重新连接' });
}
}, 8000);
}
});
},
_registerListeners: function () {
var self = this;
veepooBle.veepooWeiXinSDKNotifyMonitorValueChange(function (data) {
self._handleSdkEvent(data);
});
veepooBle.veepooWeiXinSDKBLEConnectionStateChangeManager(function (res) {
if (!res.connected) {
self._connected = false;
self._connecting = false;
self._cancelPendingMeasure();
self.setData({ phase: 'disconnected' });
}
});
},
_onReady: function () {
// C5 修复:清除 authTimeout 防止泄漏
if (this._authTimeout) { clearTimeout(this._authTimeout); this._authTimeout = null; }
this._connecting = false;
// eslint-disable-next-line no-undef
console.log('[veepoo-native] 认证成功,设备就绪');
this.setData({ phase: 'ready' });
veepooFeature.veepooReadElectricQuantityManager();
},
// ── SDK 事件路由 ──
_handleSdkEvent: function (data) {
if (!data || data.type === undefined) return;
var type = data.type;
// eslint-disable-next-line no-undef
console.log('[veepoo-native] SDK 事件: type=' + type, JSON.stringify(data).substring(0, 300));
if (type === SDK_EVENT_AUTH) {
var password = (data.content || {}).VPDevicepassword;
if (password === 'passTheVerification' || password === 'successfulVerification') {
if (this._authTimer) { clearInterval(this._authTimer); this._authTimer = null; }
if (this._authTimeout) { clearTimeout(this._authTimeout); this._authTimeout = null; }
this._onReady();
}
return;
}
if (type === SDK_EVENT_BATTERY) {
var pct = (data.content || {}).VPDeviceElectricPercent;
if (pct !== undefined && pct !== null) {
this.setData({ batteryLevel: Number(pct) });
}
return;
}
for (var i = 0; i < MEASURE_TYPES.length; i++) {
if (MEASURE_TYPES[i].sdkType === type) {
this._handleMeasureEvent(MEASURE_TYPES[i].type, data);
return;
}
}
},
// ── 测量流程 ──
handleSelectType: function (e) {
var type = e.currentTarget.dataset.type;
if (this.data.measurePhase === 'measuring') return;
this._updateSelectedDisplay(type);
this.setData({ measureError: '' });
},
handleStartMeasure: function () {
var type = this.data.selectedType;
if (this.data.measurePhase === 'measuring') return;
if (!this._connected) {
this.setData({ measureError: '设备未连接' });
return;
}
var self = this;
self._lastValues = null;
self.setData({
measurePhase: 'measuring',
measureProgress: 0,
measureDisplayValue: '',
measureError: '',
});
self._sendMeasureCommand(type, true);
self._measureTimer = setTimeout(function () {
self._onMeasureError('测量超时,请重试');
}, MEASURE_TIMEOUTS[type] || 60000);
},
handleCancelMeasure: function () {
this._cancelPendingMeasure();
this.setData({
measurePhase: 'idle',
measureProgress: 0,
measureDisplayValue: '',
});
},
handleDisconnect: function () {
this._cleanup();
this.setData({ phase: 'idle', deviceId: '', batteryLevel: null, error: '' });
},
handleBack: function () {
var results = this.data.results;
if (Object.keys(results).length > 0) {
try {
// eslint-disable-next-line no-undef
wx.setStorageSync('hms:veepoo_measure_results', JSON.stringify(results));
} catch (_) { /* ignore */ }
}
if (this._eventChannel) {
this._eventChannel.emit('measureComplete', { results: results, count: Object.keys(results).length });
}
// eslint-disable-next-line no-undef
wx.navigateBack({ delta: 1 });
},
handleResetResult: function () {
var type = this.data.selectedType;
var newResults = Object.assign({}, this.data.results);
delete newResults[type];
this.setData({
measurePhase: 'idle',
measureProgress: 0,
measureDisplayValue: '',
measureError: '',
results: newResults,
hasResults: Object.keys(newResults).length > 0,
});
},
// ── 测量事件处理 ──
_handleMeasureEvent: function (type, data) {
if (this.data.selectedType !== type || this.data.measurePhase !== 'measuring') return;
var content = data.content || {};
var self = this;
if (content.deviceBusy === true) { self._onMeasureError('设备正忙,请稍后重试'); return; }
if (content.notWear === true || data.state === 6) { self._onMeasureError('请将手环佩戴到手腕上'); return; }
if (data.state === 7) { self._onMeasureError('设备正在充电'); return; }
if (data.state === 8) { self._onMeasureError('设备电量不足'); return; }
if (type === 'pressure' && data.ack === 2) { self._onMeasureError('设备电量不足'); return; }
if (type === 'pressure' && data.ack === 3) { self._onMeasureError('设备正在测量其他数据'); return; }
if (type === 'pressure' && data.ack === 4) { self._onMeasureError('佩戴检测未通过'); return; }
var values = self._extractValues(type, content);
if (!values) return;
self._lastValues = values;
var displayVal = self._formatValues(type, values);
var progress = data.Progress !== undefined ? data.Progress : 0;
self.setData({
measureDisplayValue: displayVal,
measureProgress: Math.max(progress, 0),
});
if (progress >= 100) {
self._onMeasureSuccess(type, values);
return;
}
if (!self._settleTimer) {
self._settleTimer = setTimeout(function () {
if (self._lastValues && self.data.measurePhase === 'measuring') {
self._onMeasureSuccess(type, self._lastValues);
}
}, MEASURE_SETTLE_DELAY);
}
},
_extractValues: function (type, content) {
switch (type) {
case 'heart_rate': {
var hr = Number(content.heartRate);
return (hr >= 30 && hr <= 250) ? { heart_rate: hr } : null;
}
case 'blood_oxygen': {
var bo = Number(content.bloodOxygen);
return (bo >= 70 && bo <= 100) ? { blood_oxygen: bo } : null;
}
case 'blood_pressure': {
var high = Number(content.bloodPressureHigh);
var low = Number(content.bloodPressureLow);
return (high > 0 && low > 0) ? { systolic: high, diastolic: low } : null;
}
case 'temperature': {
var temp = Number(content.bodyTemperature);
return (temp > 30 && temp < 45) ? { temperature: temp } : null;
}
case 'pressure': {
var p = Number(content.pressure);
return (p >= 0 && p <= 100) ? { pressure: p } : null;
}
default: return null;
}
},
_formatValues: function (type, values) {
if (type === 'blood_pressure') {
return (values.systolic != null ? values.systolic : '--') + '/' + (values.diastolic != null ? values.diastolic : '--');
}
var v = Object.values(values)[0];
// H5 修复:零值合法,仅 undefined/null 显示占位符
return (v !== undefined && v !== null) ? String(v) : '--';
},
_onMeasureSuccess: function (type, values) {
this._cancelPendingMeasure();
var result = { type: type, values: values, measuredAt: Date.now() };
var newResults = Object.assign({}, this.data.results);
newResults[type] = result;
this.setData({
measurePhase: 'success',
measureProgress: 100,
measureDisplayValue: this._formatValues(type, values),
results: newResults,
hasResults: true,
});
if (this._eventChannel) {
this._eventChannel.emit('measureResult', result);
}
},
_onMeasureError: function (msg) {
this._cancelPendingMeasure();
this.setData({ measurePhase: 'error', measureError: msg });
},
_cancelPendingMeasure: function () {
if (this._measureTimer) { clearTimeout(this._measureTimer); this._measureTimer = null; }
if (this._settleTimer) { clearTimeout(this._settleTimer); this._settleTimer = null; }
this._lastValues = null;
var type = this.data.selectedType;
if (type) this._sendMeasureCommand(type, false);
},
_sendMeasureCommand: function (type, on) {
switch (type) {
case 'heart_rate':
veepooFeature.veepooSendHeartRateTestSwitchManager({ switch: !!on });
break;
case 'blood_oxygen':
veepooFeature.veepooSendBloodOxygenControlDataManager({ switch: on ? 'start' : 'stop' });
break;
case 'blood_pressure':
veepooFeature.veepooSendReadUniversalBloodPressureDataManager({ switch: on ? 'start' : 'stop' });
break;
// C1 修复:体温测量传 { switch: boolean } 参数,停止时也调用
case 'temperature':
veepooFeature.veepooSendTemperatureMeasurementSwitchManager({ switch: !!on });
break;
case 'pressure':
veepooFeature.veepooSendPressureTestManager({ switch: !!on });
break;
}
},
});