根因:veepooWeiXinSDKNotifyMonitorValueChange 封装 wx.onBLECharacteristicValueChange,必须在连接+订阅 特征值之前注册,否则认证响应(type=1)丢失。 流程修正:registerListeners → connect → auth 原流程:connect → callback → registerListeners → auth(错误) 同时增加 SDK 事件诊断日志和认证超时时输出 deviceChipStatus 实际值便于排查。
499 lines
17 KiB
JavaScript
499 lines
17 KiB
JavaScript
/**
|
||
* 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));
|
||
|
||
// 按官方 Demo 检查 connection:true,同时兼容 errno:0 / errCode:0
|
||
if (result.connection === true || result.errno === 0 || result.errCode === 0) {
|
||
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;
|
||
}
|
||
},
|
||
});
|