/** * BLE 蓝牙连接与设备控制 mixin * 基于 Pinia Store 管理蓝牙单例状态 * 负责:权限检查、连接/断连/重连、指令收发、设备状态管理 */ import { useBleStore } from '@/stores/ble' import { MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble/constants.js' import { ensureBlePrerequisite, openBluetoothSettings, openLocationSettings } from '@/utils/ble/permission.js' export default { data() { return { isShowConfirm: false } }, computed: { bleStore() { return useBleStore() }, linked() { return this.bleStore.linked }, deviceStatus: { get() { return this.bleStore.deviceStatus }, set(val) { this.bleStore.deviceStatus = val } }, hotPercentage() { return this.bleStore.hotPercentage }, ispreHot: { get() { return this.bleStore.ispreHot }, set(val) { this.bleStore.ispreHot = val } }, reconnectDrawer() { return this.bleStore.reconnectDrawer }, reconnectCount() { return this.bleStore.reconnectCount }, excepDrawer: { get() { return this.bleStore.excepDrawer }, set(val) { this.bleStore.excepDrawer = val } }, exceTxt: { get() { return this.bleStore.exceTxt }, set(val) { this.bleStore.exceTxt = val } } }, methods: { // ========== BLE 初始化 ========== async initBle() { // 1. Android权限检查 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 } if (e.message === 'BLE_ADAPTER_OFF') { uni.showModal({ title: '提示', content: '蓝牙未开启,请先开启蓝牙', cancelText: '取消', confirmText: '去开启', success: (res) => { if (res.confirm) { openBluetoothSettings() } } }) return } } // 2. 判断当前是否已连接同一设备,如果是则跳过重连 // 使用 advertisData(= 后台 deviceCode)作为跨平台设备唯一标识 const currentDevice = this.bleStore.device console.log('[BLE] 当前设备', currentDevice, this.bleStore.linked, this.advertisData) if (this.bleStore.linked && currentDevice && this.advertisData && currentDevice.advertisData === this.advertisData) { console.log('[BLE] 已连接相同设备(advertisData匹配),跳过重连', this.advertisData) return } // 3. 不同设备或未连接:断开旧连接并关闭适配器(不解绑全局监听器) try { await this.bleStore.disconnect() } catch (_) {} try { await this.bleStore.resetAdapter() } catch (_) {} // 4. 扫描并连接设备(优先直连,跳过扫描避免 Android 限流) try { const scanOpt = { timeout: 8000 } // 已有 deviceId 时优先直连,不需要扫描 if (this.deviceId) { scanOpt.deviceId = this.deviceId } if (this.deviceName) { scanOpt.deviceName = this.deviceName } // 传递 advertisData 用于连接后存储到 device 对象 if (this.advertisData) { scanOpt.advertisData = this.advertisData } await this.bleStore.scanAndConnect(scanOpt) // BLE连接建立后需短暂等待设备就绪再发首条指令 await new Promise(r => setTimeout(r, 1500)) this._sendCurrentState() } catch (e) { console.error('BLE连接失败', e) this.$refs.ayToast.error(this._bleFriendlyMsg(e)) } }, // ========== 发送指令 ========== _sendCurrentState() { const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT] this.bleStore.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.bleStore.deviceStatus = 1 this.bleStore.ispreHot = false let totalDuration = 0 this.curCase.forEach(item => { totalDuration += item.time }) const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT] this.bleStore.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 })) this.bleStore.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e)) } }, // ========== 暂停/继续 ========== stopAijiu() { if (this.deviceStatus == 5) { this.bleStore.deviceStatus = 3 this.bleStore.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {}) } else if (this.deviceStatus == 3) { this.bleStore.deviceStatus = 5 this.bleStore.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {}) } }, // ========== 停止 ========== stopPreHot() { this.isShowConfirm = true }, confirmStop() { this.bleStore.deviceStatus = 0 this.isShowConfirm = false this.bleStore.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {}) }, // ========== 模式切换 ========== changeMode(num) { uni.showModal({ title: '提示', content: '切换模式会停止当前的艾灸,是否继续', cancelText: '取消', confirmText: '继续', success: (res) => { if (res.confirm) { this.bleStore.modeType = num this.bleStore.deviceStatus = 0 this.isShowDrawer2 = false if (this._stopAudioOnModeChange) { this._stopAudioOnModeChange() } const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT] this.bleStore.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() { // 先停止扫描(防止页面销毁后 onBluetoothDeviceFound 回调找不到 taskCenter) this.bleStore.stopScan() this.bleStore.disconnect().catch(() => {}) }, /** * 页面离开时的轻量清理:只停止扫描,不断开BLE连接 * 保持蓝牙连接状态,下次进入相同设备页面可复用 */ cleanupOnLeave() { this.bleStore.stopScan() }, // ========== BLE错误码转用户友好提示 ========== _bleFriendlyMsg(e) { const code = e && (e.code || e.message || '') const originMsg = e && e.origin && e.origin.msg const map = { BLE_SCAN_FAIL: this.bleStore.scanThrottled ? '扫描过于频繁,请等待30秒后再试' : (originMsg || '未搜索到设备,请确保设备已开机并靠近手机'), BLE_CONNECT_FAIL: '连接设备失败,请确保设备在范围内并重试', BLE_CONNECT_TIMEOUT: '连接超时,请靠近设备后重试', BLE_ADAPTER_OFF: '蓝牙未开启,请先开启蓝牙', BLE_PERMISSION_DENIED: '蓝牙权限未授予,请在设置中开启', BLE_LOCATION_OFF: '位置服务未开启,请开启后重试', BLE_DISCONNECTED: '设备已断开连接', BLE_SERVICE_NOT_FOUND: '设备服务异常,请重启设备后重试', BLE_WRITE_FAIL: '指令发送失败,请重试', BLE_BUSY: '设备正忙,请稍后再试' } return map[code] || ('连接失败,请重试 (' + code + ')') } } }