ble-mixin.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. /**
  2. * BLE 蓝牙连接与设备控制 mixin
  3. * 负责:权限检查、连接/断连/重连、指令收发、设备状态管理
  4. */
  5. import bleManager, { BLE_STATE, MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble'
  6. import { ensureBlePrerequisite } from '@/utils/ble/permission.js'
  7. export default {
  8. data() {
  9. return {
  10. linked: false,
  11. deviceStatus: 0, // 0停止 1预热 2点火 3艾灸 4灭火 5暂停
  12. hotPercentage: '0%',
  13. ispreHot: false,
  14. reconnectDrawer: false,
  15. reconnectCount: 0,
  16. excepDrawer: false,
  17. exceTxt: 0,
  18. isShowConfirm: false,
  19. _unbinders: []
  20. }
  21. },
  22. methods: {
  23. // ========== BLE 初始化 ==========
  24. async initBle() {
  25. // 1. Android权限检查(iOS由系统自动弹窗,无需手动处理)
  26. try {
  27. await ensureBlePrerequisite()
  28. } catch (e) {
  29. if (e.message === 'BLE_PERMISSION_DENIED') {
  30. this.$refs.ayToast.error('请授予蓝牙权限后重试')
  31. return
  32. }
  33. if (e.message === 'BLE_LOCATION_OFF') {
  34. this.$refs.ayToast.error('请开启位置服务后重试')
  35. return
  36. }
  37. }
  38. // 2. 重置BLE适配器,清除search页面可能残留的扫描状态
  39. try { await bleManager.destroy() } catch (_) {}
  40. // 3. 重新初始化适配器
  41. await bleManager.init()
  42. // 4. 监听BLE事件(必须在destroy之后注册,因为destroy会清除所有监听器)
  43. this._unbinders = [
  44. bleManager.on('state', (s) => {
  45. this.linked = (s === BLE_STATE.READY_COMM)
  46. }),
  47. bleManager.on('disconnected', (info) => {
  48. this.linked = false
  49. if (!info.manual) {
  50. this.reconnectDrawer = true
  51. }
  52. }),
  53. bleManager.on('reconnecting', ({ count }) => {
  54. this.reconnectCount = count
  55. this.reconnectDrawer = true
  56. }),
  57. bleManager.on('reconnected', () => {
  58. this.reconnectDrawer = false
  59. this.reconnectCount = 0
  60. this.linked = true
  61. this.$refs.ayToast.success('重连成功')
  62. }),
  63. bleManager.on('reconnectFailed', () => {
  64. this.reconnectDrawer = false
  65. this.$refs.ayToast.error('连接失败,请返回重试')
  66. setTimeout(() => {
  67. uni.navigateBack()
  68. }, 1500)
  69. }),
  70. bleManager.on('report:GROUP_1', (data) => {
  71. this._handleGroup1(data)
  72. }),
  73. bleManager.on('report:GROUP_2', (data) => {
  74. this._handleGroup2(data)
  75. })
  76. ]
  77. // 5. 扫描并连接设备(与ble-demo一致的可靠方式:先扫描发现设备,再连接)
  78. try {
  79. const scanOpt = { timeout: 8000 }
  80. if (this.deviceName) {
  81. scanOpt.deviceName = this.deviceName
  82. }
  83. await bleManager.scanAndConnect(scanOpt)
  84. this.linked = true
  85. this._sendCurrentState()
  86. } catch (e) {
  87. console.error('BLE连接失败', e)
  88. this.$refs.ayToast.error('连接失败: ' + (e.message || e.code || ''))
  89. }
  90. },
  91. _cleanListeners() {
  92. if (this._unbinders && this._unbinders.length) {
  93. this._unbinders.forEach(fn => fn && fn())
  94. this._unbinders = []
  95. }
  96. },
  97. // ========== 设备上报数据解析 ==========
  98. _handleGroup1(data) {
  99. // 参数组1: 设备状态
  100. switch (data.runtimeState) {
  101. case 0x00:
  102. this.deviceStatus = 0
  103. break
  104. case 0x01:
  105. case 0x02:
  106. this.deviceStatus = 1
  107. break
  108. case 0x03:
  109. this.deviceStatus = 3
  110. break
  111. case 0x04:
  112. this.deviceStatus = 4
  113. break
  114. case 0x05:
  115. this.deviceStatus = 5
  116. break
  117. }
  118. // 模式
  119. switch (data.mode) {
  120. case MODE.LEISURE:
  121. this.modeType = 0; break
  122. case MODE.PROFESSIONAL:
  123. this.modeType = 1; break
  124. case MODE.PERSONAL:
  125. this.modeType = 2; break
  126. case MODE.EXPERT:
  127. this.modeType = 3; break
  128. }
  129. // 剩余时间
  130. const m = data.remainMinute || 0
  131. const s = data.remainSecond || 0
  132. const h = Math.floor(m / 60)
  133. const m1 = m % 60
  134. this.subTime = `${h.toString().padStart(2,'0')}:${m1.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`
  135. // 异常检测
  136. if (data.foreignDetect === 0x02) {
  137. this.excepDrawer = true
  138. this.exceTxt = 1
  139. }
  140. // 耗材状态
  141. this.otherSetting.aijiuNum = String(data.consumable || 0)
  142. this.otherSetting.lvxinNum = String(data.filterPercent || 0)
  143. this.otherSetting.huishouNum = String(data.recycleBinPct || 0)
  144. },
  145. _handleGroup2(data) {
  146. // 参数组2: 预热/点火进度
  147. if (data.preheatPercent < 100) {
  148. this.ispreHot = false
  149. this.hotPercentage = data.preheatPercent + '%'
  150. } else {
  151. this.ispreHot = true
  152. this.hotPercentage = data.ignitePercent + '%'
  153. }
  154. },
  155. // ========== 发送指令 ==========
  156. _sendCurrentState() {
  157. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  158. bleManager.sendBasic({
  159. power: POWER.ON,
  160. moxiState: MOXI_STATE.DONE,
  161. preheatState: MOXI_STATE.DONE,
  162. mute: MUTE.OFF,
  163. mode: modeMap[this.modeType] || MODE.LEISURE,
  164. subMode: 0x01,
  165. temperature: TEMPERATURE.MID,
  166. angle: CHAIR_ANGLE.OFF,
  167. duration: 0
  168. }).catch(e => console.error('sendBasic fail', e))
  169. },
  170. // ========== 开始艾灸 ==========
  171. startDeviceEvt() {
  172. this.deviceStatus = 1
  173. this.ispreHot = false
  174. let totalDuration = 0
  175. this.curCase.forEach(item => {
  176. totalDuration += item.time
  177. })
  178. // 发送开始指令
  179. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  180. bleManager.sendBasic({
  181. power: POWER.ON,
  182. moxiState: MOXI_STATE.START,
  183. preheatState: MOXI_STATE.START,
  184. mute: MUTE.OFF,
  185. mode: modeMap[this.modeType] || MODE.LEISURE,
  186. subMode: 0x01,
  187. temperature: TEMPERATURE.MID,
  188. angle: CHAIR_ANGLE.OFF,
  189. duration: totalDuration || 30
  190. }).catch(e => console.error('startDevice fail', e))
  191. // 发送穴位坐标
  192. if (this.curCase.length > 0) {
  193. const points = this.curCase.map(item => ({
  194. point: item.id,
  195. x: item._x,
  196. y: item._y
  197. }))
  198. bleManager.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e))
  199. }
  200. },
  201. // ========== 暂停/继续 ==========
  202. stopAijiu() {
  203. if (this.deviceStatus == 5) {
  204. // 继续
  205. this.deviceStatus = 3
  206. bleManager.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {})
  207. } else if (this.deviceStatus == 3) {
  208. // 暂停
  209. this.deviceStatus = 5
  210. bleManager.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {})
  211. }
  212. },
  213. // ========== 停止 ==========
  214. stopPreHot() {
  215. this.isShowConfirm = true
  216. },
  217. confirmStop() {
  218. this.deviceStatus = 0
  219. this.isShowConfirm = false
  220. bleManager.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {})
  221. },
  222. // ========== 模式切换 ==========
  223. changeMode(num) {
  224. uni.showModal({
  225. title: '提示',
  226. content: '切换模式会停止当前的艾灸,是否继续',
  227. cancelText: '取消',
  228. confirmText: '继续',
  229. success: (res) => {
  230. if (res.confirm) {
  231. this.modeType = num
  232. this.deviceStatus = 0
  233. this.isShowDrawer2 = false
  234. if (this._stopAudioOnModeChange) {
  235. this._stopAudioOnModeChange()
  236. }
  237. // 发送模式切换指令
  238. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  239. bleManager.sendBasic({
  240. power: POWER.ON,
  241. moxiState: MOXI_STATE.DONE,
  242. preheatState: MOXI_STATE.DONE,
  243. mute: MUTE.OFF,
  244. mode: modeMap[num] || MODE.LEISURE,
  245. subMode: 0x01,
  246. temperature: TEMPERATURE.MID,
  247. angle: CHAIR_ANGLE.OFF,
  248. duration: 0
  249. }).catch(e => console.error('changeMode fail', e))
  250. }
  251. }
  252. })
  253. },
  254. // ========== 断开BLE ==========
  255. disconnectBle() {
  256. this._cleanListeners()
  257. bleManager.disconnect().catch(() => {})
  258. }
  259. }
  260. }