|
|
@@ -1,29 +1,55 @@
|
|
|
/**
|
|
|
* BLE 蓝牙连接与设备控制 mixin
|
|
|
+ * 基于 Pinia Store 管理蓝牙单例状态
|
|
|
* 负责:权限检查、连接/断连/重连、指令收发、设备状态管理
|
|
|
*/
|
|
|
-import bleManager, { BLE_STATE, MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble'
|
|
|
+import { useBleStore } from '@/stores/ble'
|
|
|
+import { MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble/constants.js'
|
|
|
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: []
|
|
|
+ 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权限检查(iOS由系统自动弹窗,无需手动处理)
|
|
|
+ // 1. Android权限检查
|
|
|
try {
|
|
|
await ensureBlePrerequisite()
|
|
|
} catch (e) {
|
|
|
@@ -37,135 +63,34 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 2. 重置BLE适配器,清除search页面可能残留的扫描状态
|
|
|
- try { await bleManager.destroy() } catch (_) {}
|
|
|
+ // 2. 断开旧连接并关闭适配器(不解绑全局监听器)
|
|
|
+ try { await this.bleStore.disconnect() } catch (_) {}
|
|
|
+ try { await this.bleStore.resetAdapter() } 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一致的可靠方式:先扫描发现设备,再连接)
|
|
|
+ // 3. 扫描并连接设备(优先直连,跳过扫描避免 Android 限流)
|
|
|
try {
|
|
|
const scanOpt = { timeout: 8000 }
|
|
|
+ // 已有 deviceId 时优先直连,不需要扫描
|
|
|
+ if (this.deviceId) {
|
|
|
+ scanOpt.deviceId = this.deviceId
|
|
|
+ }
|
|
|
if (this.deviceName) {
|
|
|
scanOpt.deviceName = this.deviceName
|
|
|
}
|
|
|
- await bleManager.scanAndConnect(scanOpt)
|
|
|
- this.linked = true
|
|
|
+ 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('连接失败: ' + (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 + '%'
|
|
|
+ this.$refs.ayToast.error(this._bleFriendlyMsg(e))
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// ========== 发送指令 ==========
|
|
|
_sendCurrentState() {
|
|
|
const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
|
|
|
- bleManager.sendBasic({
|
|
|
+ this.bleStore.sendBasic({
|
|
|
power: POWER.ON,
|
|
|
moxiState: MOXI_STATE.DONE,
|
|
|
preheatState: MOXI_STATE.DONE,
|
|
|
@@ -180,15 +105,14 @@ export default {
|
|
|
|
|
|
// ========== 开始艾灸 ==========
|
|
|
startDeviceEvt() {
|
|
|
- this.deviceStatus = 1
|
|
|
- this.ispreHot = false
|
|
|
+ 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]
|
|
|
- bleManager.sendBasic({
|
|
|
+ this.bleStore.sendBasic({
|
|
|
power: POWER.ON,
|
|
|
moxiState: MOXI_STATE.START,
|
|
|
preheatState: MOXI_STATE.START,
|
|
|
@@ -200,27 +124,24 @@ export default {
|
|
|
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))
|
|
|
+ this.bleStore.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e))
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// ========== 暂停/继续 ==========
|
|
|
stopAijiu() {
|
|
|
if (this.deviceStatus == 5) {
|
|
|
- // 继续
|
|
|
- this.deviceStatus = 3
|
|
|
- bleManager.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {})
|
|
|
+ this.bleStore.deviceStatus = 3
|
|
|
+ this.bleStore.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {})
|
|
|
} else if (this.deviceStatus == 3) {
|
|
|
- // 暂停
|
|
|
- this.deviceStatus = 5
|
|
|
- bleManager.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {})
|
|
|
+ this.bleStore.deviceStatus = 5
|
|
|
+ this.bleStore.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {})
|
|
|
}
|
|
|
},
|
|
|
|
|
|
@@ -229,9 +150,9 @@ export default {
|
|
|
this.isShowConfirm = true
|
|
|
},
|
|
|
confirmStop() {
|
|
|
- this.deviceStatus = 0
|
|
|
+ this.bleStore.deviceStatus = 0
|
|
|
this.isShowConfirm = false
|
|
|
- bleManager.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {})
|
|
|
+ this.bleStore.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {})
|
|
|
},
|
|
|
|
|
|
// ========== 模式切换 ==========
|
|
|
@@ -243,15 +164,14 @@ export default {
|
|
|
confirmText: '继续',
|
|
|
success: (res) => {
|
|
|
if (res.confirm) {
|
|
|
- this.modeType = num
|
|
|
- this.deviceStatus = 0
|
|
|
+ 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]
|
|
|
- bleManager.sendBasic({
|
|
|
+ this.bleStore.sendBasic({
|
|
|
power: POWER.ON,
|
|
|
moxiState: MOXI_STATE.DONE,
|
|
|
preheatState: MOXI_STATE.DONE,
|
|
|
@@ -269,8 +189,30 @@ export default {
|
|
|
|
|
|
// ========== 断开BLE ==========
|
|
|
disconnectBle() {
|
|
|
- this._cleanListeners()
|
|
|
- bleManager.disconnect().catch(() => {})
|
|
|
+ // 先停止扫描(防止页面销毁后 onBluetoothDeviceFound 回调找不到 taskCenter)
|
|
|
+ this.bleStore.stopScan()
|
|
|
+ this.bleStore.disconnect().catch(() => {})
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 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 + ')')
|
|
|
}
|
|
|
}
|
|
|
}
|