/** * BLE 蓝牙连接与设备控制 mixin * 负责:权限检查、连接/断连/重连、指令收发、设备状态管理 */ import bleManager, { BLE_STATE, MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble' import { ensureBlePrerequisite } from '@/utils/ble/permission.js' export default { data() { return { linked: false, deviceStatus: 0, // 0停止 1预热 2点火 3艾灸 4灭火 5暂停 hotPercentage: '0%', ispreHot: false, reconnectDrawer: false, reconnectCount: 0, excepDrawer: false, exceTxt: 0, isShowConfirm: false, _unbinders: [] } }, methods: { // ========== BLE 初始化 ========== async initBle() { // 1. Android权限检查(iOS由系统自动弹窗,无需手动处理) try { await ensureBlePrerequisite() } catch (e) { if (e.message === 'BLE_PERMISSION_DENIED') { this.$refs.ayToast.error('请授予蓝牙权限后重试') return } if (e.message === 'BLE_LOCATION_OFF') { this.$refs.ayToast.error('请开启位置服务后重试') return } } // 2. 重置BLE适配器,清除search页面可能残留的扫描状态 try { await bleManager.destroy() } catch (_) {} // 3. 重新初始化适配器 await bleManager.init() // 4. 监听BLE事件(必须在destroy之后注册,因为destroy会清除所有监听器) this._unbinders = [ bleManager.on('state', (s) => { this.linked = (s === BLE_STATE.READY_COMM) if (s === BLE_STATE.DISCONNECTED) { this.linked = false this._handleBleDisconnect() } }), bleManager.on('disconnected', (info) => { this.linked = false if (!info.manual) { this._handleBleDisconnect() } }), bleManager.on('reconnected', () => { this.reconnectDrawer = false this.reconnectCount = 0 this.linked = true this.$refs.ayToast.success('重连成功') }), bleManager.on('report:GROUP_1', (data) => { this._handleGroup1(data) }), bleManager.on('report:GROUP_2', (data) => { this._handleGroup2(data) }) ] // 5. 扫描并连接设备(与ble-demo一致的可靠方式:先扫描发现设备,再连接) try { const scanOpt = { timeout: 8000 } if (this.deviceName) { scanOpt.deviceName = this.deviceName } await bleManager.scanAndConnect(scanOpt) this.linked = true this._sendCurrentState() } catch (e) { console.error('BLE连接失败', e) this.$refs.ayToast.error('连接失败: ' + (e.message || e.code || '')) } }, _cleanListeners() { if (this._unbinders && this._unbinders.length) { this._unbinders.forEach(fn => fn && fn()) this._unbinders = [] } }, _handleBleDisconnect() { this.reconnectCount++ if (this.reconnectCount >= 4) { this.reconnectDrawer = false this.$refs.ayToast.error('连接失败,请返回重试') setTimeout(() => { uni.navigateBack() }, 1500) } else { this.reconnectDrawer = true } }, // ========== 设备上报数据解析 ========== _handleGroup1(data) { // 参数组1: 设备状态 switch (data.runtimeState) { case 0x00: this.deviceStatus = 0 break case 0x01: case 0x02: this.deviceStatus = 1 break case 0x03: this.deviceStatus = 3 break case 0x04: this.deviceStatus = 4 break case 0x05: this.deviceStatus = 5 break } // 模式 switch (data.mode) { case MODE.LEISURE: this.modeType = 0; break case MODE.PROFESSIONAL: this.modeType = 1; break case MODE.PERSONAL: this.modeType = 2; break case MODE.EXPERT: this.modeType = 3; break } // 剩余时间 const m = data.remainMinute || 0 const s = data.remainSecond || 0 const h = Math.floor(m / 60) const m1 = m % 60 this.subTime = `${h.toString().padStart(2,'0')}:${m1.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}` // 异常检测 if (data.foreignDetect === 0x02) { this.excepDrawer = true this.exceTxt = 1 } // 耗材状态 this.otherSetting.aijiuNum = String(data.consumable || 0) this.otherSetting.lvxinNum = String(data.filterPercent || 0) this.otherSetting.huishouNum = String(data.recycleBinPct || 0) }, _handleGroup2(data) { // 参数组2: 预热/点火进度 if (data.preheatPercent < 100) { this.ispreHot = false this.hotPercentage = data.preheatPercent + '%' } else { this.ispreHot = true this.hotPercentage = data.ignitePercent + '%' } }, // ========== 发送指令 ========== _sendCurrentState() { const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT] bleManager.sendBasic({ power: POWER.ON, moxiState: MOXI_STATE.DONE, preheatState: MOXI_STATE.DONE, mute: MUTE.OFF, mode: modeMap[this.modeType] || MODE.LEISURE, subMode: 0x01, temperature: TEMPERATURE.MID, angle: CHAIR_ANGLE.OFF, duration: 0 }).catch(e => console.error('sendBasic fail', e)) }, // ========== 开始艾灸 ========== startDeviceEvt() { this.deviceStatus = 1 this.ispreHot = false let totalDuration = 0 this.curCase.forEach(item => { totalDuration += item.time }) // 发送开始指令 const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT] bleManager.sendBasic({ power: POWER.ON, moxiState: MOXI_STATE.START, preheatState: MOXI_STATE.START, mute: MUTE.OFF, mode: modeMap[this.modeType] || MODE.LEISURE, subMode: 0x01, temperature: TEMPERATURE.MID, angle: CHAIR_ANGLE.OFF, duration: totalDuration || 30 }).catch(e => console.error('startDevice fail', e)) // 发送穴位坐标 if (this.curCase.length > 0) { const points = this.curCase.map(item => ({ point: item.id, x: item._x, y: item._y })) bleManager.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e)) } }, // ========== 暂停/继续 ========== stopAijiu() { if (this.deviceStatus == 5) { // 继续 this.deviceStatus = 3 bleManager.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {}) } else if (this.deviceStatus == 3) { // 暂停 this.deviceStatus = 5 bleManager.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {}) } }, // ========== 停止 ========== stopPreHot() { this.isShowConfirm = true }, confirmStop() { this.deviceStatus = 0 this.isShowConfirm = false bleManager.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {}) }, // ========== 模式切换 ========== changeMode(num) { uni.showModal({ title: '提示', content: '切换模式会停止当前的艾灸,是否继续', cancelText: '取消', confirmText: '继续', success: (res) => { if (res.confirm) { this.modeType = num this.deviceStatus = 0 this.isShowDrawer2 = false if (this._stopAudioOnModeChange) { this._stopAudioOnModeChange() } // 发送模式切换指令 const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT] bleManager.sendBasic({ power: POWER.ON, moxiState: MOXI_STATE.DONE, preheatState: MOXI_STATE.DONE, mute: MUTE.OFF, mode: modeMap[num] || MODE.LEISURE, subMode: 0x01, temperature: TEMPERATURE.MID, angle: CHAIR_ANGLE.OFF, duration: 0 }).catch(e => console.error('changeMode fail', e)) } } }) }, // ========== 断开BLE ========== disconnectBle() { this._cleanListeners() bleManager.disconnect().catch(() => {}) } } }