| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- /**
- * BLE 蓝牙连接与设备控制 mixin
- * 负责:权限检查、连接/断连/重连、指令收发、设备状态管理
- */
- import bleManager, { BLE_STATE, MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble'
- import { ensureBlePrerequisite } from '@/utils/ble/permission.js'
- import { selectDevice } from '@/utils/api/device'
- 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)
- }),
- bleManager.on('disconnected', (info) => {
- this.linked = false
- if (!info.manual) {
- this.reconnectDrawer = true
- }
- }),
- bleManager.on('reconnecting', ({ count }) => {
- this.reconnectCount = count
- this.reconnectDrawer = true
- }),
- bleManager.on('reconnected', () => {
- this.reconnectDrawer = false
- this.reconnectCount = 0
- this.linked = true
- this.$refs.ayToast.success('重连成功')
- }),
- bleManager.on('reconnectFailed', () => {
- this.reconnectDrawer = false
- this.$refs.ayToast.error('连接失败,请返回重试')
- setTimeout(() => {
- uni.navigateBack()
- }, 1500)
- }),
- 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 = []
- }
- },
- // ========== 设备上报数据解析 ==========
- _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))
- },
- // ========== 开始艾灸 ==========
- async startDeviceEvt() {
- const ready = await this.ensureMoxibustionReady()
- if (!ready) return
- 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))
- }
- },
- async ensureMoxibustionReady() {
- const token = uni.getStorageSync('user_token')
- if (!token) {
- this.$refs.ayToast.error('请先登录')
- uni.navigateTo({ url: '/pages/login/login' })
- return false
- }
- const currentDevice = uni.getStorageSync('current_device') || {}
- const currentUser = uni.getStorageSync('current_manage_user') || {}
- const deviceCode = currentDevice.deviceCode || this.deviceCode || this.deviceName || this.deviceId
- if (!deviceCode) {
- this.$refs.ayToast.error('请先选择设备')
- return false
- }
- const profileId = currentUser.profileId || currentUser.id || currentDevice.profileId
- if (!profileId) {
- this.$refs.ayToast.error('请先选择用户')
- uni.navigateTo({ url: '/pages/device/userManage/userManage' })
- return false
- }
- let selected
- try {
- selected = await selectDevice({
- deviceCode,
- groupId: currentDevice.groupId || null,
- profileId
- })
- } catch (e) {
- console.error('设备使用权限校验失败', e)
- return false
- }
- const profile = selected.profile || currentUser
- uni.setStorageSync('current_device', { ...currentDevice, ...selected })
- uni.setStorageSync('current_manage_user', profile)
- uni.setStorageSync('current_manage_user_id', profile.profileId || profile.id)
- if (!this.isProfileCompleteForMoxibustion(profile)) {
- this.$refs.ayToast.error('请先补全用户资料')
- setTimeout(() => {
- uni.navigateTo({
- url: '/pages/device/bodyParams/bodyParams?mode=edit&userId=' + encodeURIComponent(profile.profileId || profile.id || '')
- })
- }, 600)
- return false
- }
- return true
- },
- isProfileCompleteForMoxibustion(profile) {
- if (!profile) return false
- const bodyHeight = profile.bodyHeight || profile.shoulderHeight
- return !!(
- profile.name &&
- profile.gender &&
- profile.age &&
- profile.shoulderWidth &&
- bodyHeight &&
- profile.spineLength
- )
- },
- // ========== 暂停/继续 ==========
- 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(() => {})
- }
- }
- }
|