ble-mixin.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /**
  2. * BLE 蓝牙连接与设备控制 mixin
  3. * 基于 Pinia Store 管理蓝牙单例状态
  4. * 负责:权限检查、连接/断连/重连、指令收发、设备状态管理
  5. */
  6. import { useBleStore } from '@/stores/ble'
  7. import { MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble/constants.js'
  8. import { ensureBlePrerequisite, openBluetoothSettings, openLocationSettings } from '@/utils/ble/permission.js'
  9. export default {
  10. data() {
  11. return {
  12. isShowConfirm: false
  13. }
  14. },
  15. computed: {
  16. bleStore() {
  17. return useBleStore()
  18. },
  19. linked() {
  20. return this.bleStore.linked
  21. },
  22. deviceStatus: {
  23. get() { return this.bleStore.deviceStatus },
  24. set(val) { this.bleStore.deviceStatus = val }
  25. },
  26. hotPercentage() {
  27. return this.bleStore.hotPercentage
  28. },
  29. ispreHot: {
  30. get() { return this.bleStore.ispreHot },
  31. set(val) { this.bleStore.ispreHot = val }
  32. },
  33. reconnectDrawer() {
  34. return this.bleStore.reconnectDrawer
  35. },
  36. reconnectCount() {
  37. return this.bleStore.reconnectCount
  38. },
  39. excepDrawer: {
  40. get() { return this.bleStore.excepDrawer },
  41. set(val) { this.bleStore.excepDrawer = val }
  42. },
  43. exceTxt: {
  44. get() { return this.bleStore.exceTxt },
  45. set(val) { this.bleStore.exceTxt = val }
  46. }
  47. },
  48. methods: {
  49. // ========== BLE 初始化 ==========
  50. async initBle() {
  51. // 1. Android权限检查
  52. try {
  53. await ensureBlePrerequisite()
  54. } catch (e) {
  55. if (e.message === 'BLE_PERMISSION_DENIED') {
  56. this.$refs.ayToast.error('请授予蓝牙权限后重试')
  57. return
  58. }
  59. if (e.message === 'BLE_LOCATION_OFF') {
  60. this.$refs.ayToast.error('请开启位置服务后重试')
  61. return
  62. }
  63. if (e.message === 'BLE_ADAPTER_OFF') {
  64. uni.showModal({
  65. title: '提示',
  66. content: '蓝牙未开启,请先开启蓝牙',
  67. cancelText: '取消',
  68. confirmText: '去开启',
  69. success: (res) => {
  70. if (res.confirm) {
  71. openBluetoothSettings()
  72. }
  73. }
  74. })
  75. return
  76. }
  77. }
  78. // 2. 判断当前是否已连接同一设备,如果是则跳过重连
  79. // 使用 advertisData(= 后台 deviceCode)作为跨平台设备唯一标识
  80. const currentDevice = this.bleStore.device
  81. console.log('[BLE] 当前设备', currentDevice, this.bleStore.linked, this.advertisData)
  82. if (this.bleStore.linked && currentDevice && this.advertisData && currentDevice.advertisData === this.advertisData) {
  83. console.log('[BLE] 已连接相同设备(advertisData匹配),跳过重连', this.advertisData)
  84. return
  85. }
  86. // 3. 不同设备或未连接:断开旧连接并关闭适配器(不解绑全局监听器)
  87. try { await this.bleStore.disconnect() } catch (_) {}
  88. try { await this.bleStore.resetAdapter() } catch (_) {}
  89. // 4. 扫描并连接设备
  90. // 注意:this.deviceId 是后台数据库ID,不是BLE设备ID,不能用于直连
  91. // 优先从缓存中获取上次成功连接的真实BLE deviceId
  92. try {
  93. let realBleDeviceId = ''
  94. if (this.advertisData) {
  95. // 优先从bleStore当前设备获取(可能连接已断但device对象仍在)
  96. if (currentDevice && currentDevice.advertisData === this.advertisData && currentDevice.deviceId) {
  97. realBleDeviceId = currentDevice.deviceId
  98. }
  99. // 其次从本地缓存获取
  100. if (!realBleDeviceId) {
  101. try {
  102. realBleDeviceId = uni.getStorageSync('ble_deviceId_' + this.advertisData) || ''
  103. console.log("当前设备id",realBleDeviceId)
  104. } catch (_) {}
  105. }
  106. }
  107. const scanOpt = { timeout: 8000 }
  108. // 用真实BLE deviceId直连,避免扫描(减少Android限流风险)
  109. if (realBleDeviceId) {
  110. scanOpt.deviceId = realBleDeviceId
  111. }
  112. if (this.deviceName) {
  113. scanOpt.deviceName = this.deviceName
  114. }
  115. // 传递 advertisData 用于连接后存储到 device 对象
  116. if (this.advertisData) {
  117. scanOpt.advertisData = this.advertisData
  118. }
  119. console.log("当前连接蓝牙参数",scanOpt)
  120. await this.bleStore.scanAndConnect(scanOpt)
  121. // 连接成功后缓存真实BLE deviceId,供下次直连使用
  122. if (this.advertisData && this.bleStore.device && this.bleStore.device.deviceId) {
  123. try {
  124. uni.setStorageSync('ble_deviceId_' + this.advertisData, this.bleStore.device.deviceId)
  125. } catch (_) {}
  126. }
  127. // BLE连接建立后需短暂等待设备就绪再发首条指令
  128. await new Promise(r => setTimeout(r, 1500))
  129. this._sendCurrentState()
  130. } catch (e) {
  131. console.error('BLE连接失败', e)
  132. this.$refs.ayToast.error(this._bleFriendlyMsg(e))
  133. }
  134. },
  135. // ========== 发送指令 ==========
  136. _sendCurrentState() {
  137. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  138. this.bleStore.sendBasic({
  139. power: POWER.ON,
  140. moxiState: MOXI_STATE.DONE,
  141. preheatState: MOXI_STATE.DONE,
  142. mute: MUTE.OFF,
  143. mode: modeMap[this.modeType] || MODE.LEISURE,
  144. subMode: 0x01,
  145. temperature: TEMPERATURE.MID,
  146. angle: CHAIR_ANGLE.OFF,
  147. duration: 0
  148. }).catch(e => console.error('sendBasic fail', e))
  149. },
  150. // ========== 开始艾灸 ==========
  151. startDeviceEvt() {
  152. this.bleStore.deviceStatus = 1
  153. this.bleStore.ispreHot = false
  154. let totalDuration = 0
  155. this.curCase.forEach(item => {
  156. totalDuration += item.time
  157. })
  158. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  159. this.bleStore.sendBasic({
  160. power: POWER.ON,
  161. moxiState: MOXI_STATE.START,
  162. preheatState: MOXI_STATE.START,
  163. mute: MUTE.OFF,
  164. mode: modeMap[this.modeType] || MODE.LEISURE,
  165. subMode: 0x01,
  166. temperature: TEMPERATURE.MID,
  167. angle: CHAIR_ANGLE.OFF,
  168. duration: totalDuration || 30
  169. }).catch(e => console.error('startDevice fail', e))
  170. if (this.curCase.length > 0) {
  171. const points = this.curCase.map(item => ({
  172. point: item.id,
  173. x: item._x,
  174. y: item._y
  175. }))
  176. this.bleStore.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e))
  177. }
  178. },
  179. // ========== 暂停/继续 ==========
  180. stopAijiu() {
  181. if (this.deviceStatus == 5) {
  182. this.bleStore.deviceStatus = 3
  183. this.bleStore.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {})
  184. } else if (this.deviceStatus == 3) {
  185. this.bleStore.deviceStatus = 5
  186. this.bleStore.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {})
  187. }
  188. },
  189. // ========== 停止 ==========
  190. stopPreHot() {
  191. this.isShowConfirm = true
  192. },
  193. confirmStop() {
  194. this.bleStore.deviceStatus = 0
  195. this.isShowConfirm = false
  196. this.bleStore.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {})
  197. },
  198. // ========== 模式切换 ==========
  199. changeMode(num) {
  200. uni.showModal({
  201. title: '提示',
  202. content: '切换模式会停止当前的艾灸,是否继续',
  203. cancelText: '取消',
  204. confirmText: '继续',
  205. success: (res) => {
  206. if (res.confirm) {
  207. this.bleStore.modeType = num
  208. this.bleStore.deviceStatus = 0
  209. this.isShowDrawer2 = false
  210. if (this._stopAudioOnModeChange) {
  211. this._stopAudioOnModeChange()
  212. }
  213. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  214. this.bleStore.sendBasic({
  215. power: POWER.ON,
  216. moxiState: MOXI_STATE.DONE,
  217. preheatState: MOXI_STATE.DONE,
  218. mute: MUTE.OFF,
  219. mode: modeMap[num] || MODE.LEISURE,
  220. subMode: 0x01,
  221. temperature: TEMPERATURE.MID,
  222. angle: CHAIR_ANGLE.OFF,
  223. duration: 0
  224. }).catch(e => console.error('changeMode fail', e))
  225. }
  226. }
  227. })
  228. },
  229. // ========== 断开BLE ==========
  230. disconnectBle() {
  231. // 先停止扫描(防止页面销毁后 onBluetoothDeviceFound 回调找不到 taskCenter)
  232. this.bleStore.stopScan()
  233. this.bleStore.disconnect().catch(() => {})
  234. },
  235. /**
  236. * 页面离开时的轻量清理:只停止扫描,不断开BLE连接
  237. * 保持蓝牙连接状态,下次进入相同设备页面可复用
  238. */
  239. cleanupOnLeave() {
  240. this.bleStore.stopScan()
  241. },
  242. // ========== BLE错误码转用户友好提示 ==========
  243. _bleFriendlyMsg(e) {
  244. const code = e && (e.code || e.message || '')
  245. const originMsg = e && e.origin && e.origin.msg
  246. const map = {
  247. BLE_SCAN_FAIL: this.bleStore.scanThrottled
  248. ? '扫描过于频繁,请等待30秒后再试'
  249. : (originMsg || '未搜索到设备,请确保设备已开机并靠近手机'),
  250. BLE_CONNECT_FAIL: '连接设备失败,请确保设备在范围内并重试',
  251. BLE_CONNECT_TIMEOUT: '连接超时,请靠近设备后重试',
  252. BLE_ADAPTER_OFF: '蓝牙未开启,请先开启蓝牙',
  253. BLE_PERMISSION_DENIED: '蓝牙权限未授予,请在设置中开启',
  254. BLE_LOCATION_OFF: '位置服务未开启,请开启后重试',
  255. BLE_DISCONNECTED: '设备已断开连接',
  256. BLE_SERVICE_NOT_FOUND: '设备服务异常,请重启设备后重试',
  257. BLE_WRITE_FAIL: '指令发送失败,请重试',
  258. BLE_BUSY: '设备正忙,请稍后再试'
  259. }
  260. return map[code] || ('连接失败,请重试 (' + code + ')')
  261. }
  262. }
  263. }