/** * Veepoo M2 原生小程序页面 — 连接 + 测量 * * 完全脱离 Taro 框架,直接使用微信原生 API + Veepoo SDK。 * 流程严格对齐官方 Demo: * onLoad 注册全局监听器 * → scan → stopScan → connect(等待 connection:true) * → delay 500ms → authenticate * → SDK 事件(type=1) / Storage 轮询 → 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, _listenersRegistered: 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'); // 注意:不在 onLoad 注册 veepooWeiXinSDKNotifyMonitorValueChange! // 该函数内部会调用 wx.notifyBLECharacteristicValueChange,需要蓝牙适配器已初始化。 // onLoad 时适配器未初始化 → 返回 "notifyBLECharacteristicValueChange:fail:not init" // 监听器改在 _doConnect 的 connection:true 回调中注册。 // eslint-disable-next-line no-undef console.log('[veepoo-native] 页面已加载'); }, 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; } }, // ── 全局监听器(onLoad 注册一次) ── _registerGlobalListeners: function () { if (this._listenersRegistered) return; this._listenersRegistered = true; var self = this; // SDK 数据监听 — 接收所有解析后的事件(auth/measure/battery 等) veepooBle.veepooWeiXinSDKNotifyMonitorValueChange(function (data) { // eslint-disable-next-line no-undef console.log('[veepoo-native] SDK 数据事件:', JSON.stringify(data).substring(0, 500)); self._handleSdkEvent(data); }); // BLE 连接状态变化 veepooBle.veepooWeiXinSDKBLEConnectionStateChangeManager(function (res) { // eslint-disable-next-line no-undef console.log('[veepoo-native] 连接状态变化:', JSON.stringify(res)); if (!res.connected) { self._connected = false; self._connecting = false; self._cancelPendingMeasure(); self.setData({ phase: 'disconnected' }); } }); // eslint-disable-next-line no-undef console.log('[veepoo-native] SDK 函数类型:', { scanFn: typeof veepooBle.veepooWeiXinSDKStartScanDeviceAndReceiveScanningDevice, connectFn: typeof veepooBle.veepooWeiXinSDKBleConnectionServicesCharacteristicsNotifyManager, dataFn: typeof veepooBle.veepooWeiXinSDKNotifyMonitorValueChange, authFn: typeof veepooFeature.veepooBlePasswordCheckManager, }); }, _updateSelectedDisplay: function (type) { var cfg = _findConfig(type); this.setData({ selectedType: type, selectedIcon: cfg.icon, selectedColor: cfg.color, selectedLabel: cfg.label, selectedUnit: cfg.unit, }); }, // ── 连接流程 ── handleConnect: function () { 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); 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; veepooBle.veepooWeiXinSDKBleConnectionServicesCharacteristicsNotifyManager(device, function (result) { // eslint-disable-next-line no-undef console.log('[veepoo-native] 连接阶段回调:', JSON.stringify(result).substring(0, 300)); // 只响应最终就绪回调(connection:true) if (result.connection === true) { self._connected = true; self._connecting = false; self.setData({ deviceId: device.deviceId || device.mac || '', }); // 关键:在连接就绪后注册数据监听器 // veepooWeiXinSDKNotifyMonitorValueChange 内部会调用 // wx.notifyBLECharacteristicValueChange,需要蓝牙适配器已初始化+已连接 self._registerGlobalListeners(); // eslint-disable-next-line no-undef console.log('[veepoo-native] 连接就绪,监听器已注册,500ms 后发送认证'); setTimeout(function () { // eslint-disable-next-line no-undef console.log('[veepoo-native] 调用 veepooBlePasswordCheckManager'); veepooFeature.veepooBlePasswordCheckManager(); self.setData({ phase: 'authenticating' }); }, 500); // 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); } }); }, _onReady: function () { 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; if (type === SDK_EVENT_AUTH) { var password = (data.content || {}).VPDevicepassword; // eslint-disable-next-line no-undef console.log('[veepoo-native] 认证事件: VPDevicepassword=' + password); 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]; 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; case 'temperature': veepooFeature.veepooSendTemperatureMeasurementSwitchManager({ switch: !!on }); break; case 'pressure': veepooFeature.veepooSendPressureTestManager({ switch: !!on }); break; } }, });