ble-mixin.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. if (s === BLE_STATE.DISCONNECTED) {
  47. this.linked = false
  48. this._handleBleDisconnect()
  49. }
  50. }),
  51. bleManager.on('disconnected', (info) => {
  52. this.linked = false
  53. if (!info.manual) {
  54. this._handleBleDisconnect()
  55. }
  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('report:GROUP_1', (data) => {
  64. this._handleGroup1(data)
  65. }),
  66. bleManager.on('report:GROUP_2', (data) => {
  67. this._handleGroup2(data)
  68. })
  69. ]
  70. // 5. 扫描并连接设备(与ble-demo一致的可靠方式:先扫描发现设备,再连接)
  71. try {
  72. const scanOpt = { timeout: 8000 }
  73. if (this.deviceName) {
  74. scanOpt.deviceName = this.deviceName
  75. }
  76. await bleManager.scanAndConnect(scanOpt)
  77. this.linked = true
  78. this._sendCurrentState()
  79. } catch (e) {
  80. console.error('BLE连接失败', e)
  81. this.$refs.ayToast.error('连接失败: ' + (e.message || e.code || ''))
  82. }
  83. },
  84. _cleanListeners() {
  85. if (this._unbinders && this._unbinders.length) {
  86. this._unbinders.forEach(fn => fn && fn())
  87. this._unbinders = []
  88. }
  89. },
  90. _handleBleDisconnect() {
  91. this.reconnectCount++
  92. if (this.reconnectCount >= 4) {
  93. this.reconnectDrawer = false
  94. this.$refs.ayToast.error('连接失败,请返回重试')
  95. setTimeout(() => {
  96. uni.navigateBack()
  97. }, 1500)
  98. } else {
  99. this.reconnectDrawer = true
  100. }
  101. },
  102. // ========== 设备上报数据解析 ==========
  103. _handleGroup1(data) {
  104. // 参数组1: 设备状态
  105. switch (data.runtimeState) {
  106. case 0x00:
  107. this.deviceStatus = 0
  108. break
  109. case 0x01:
  110. case 0x02:
  111. this.deviceStatus = 1
  112. break
  113. case 0x03:
  114. this.deviceStatus = 3
  115. break
  116. case 0x04:
  117. this.deviceStatus = 4
  118. break
  119. case 0x05:
  120. this.deviceStatus = 5
  121. break
  122. }
  123. // 模式
  124. switch (data.mode) {
  125. case MODE.LEISURE:
  126. this.modeType = 0; break
  127. case MODE.PROFESSIONAL:
  128. this.modeType = 1; break
  129. case MODE.PERSONAL:
  130. this.modeType = 2; break
  131. case MODE.EXPERT:
  132. this.modeType = 3; break
  133. }
  134. // 剩余时间
  135. const m = data.remainMinute || 0
  136. const s = data.remainSecond || 0
  137. const h = Math.floor(m / 60)
  138. const m1 = m % 60
  139. this.subTime = `${h.toString().padStart(2,'0')}:${m1.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`
  140. // 异常检测
  141. if (data.foreignDetect === 0x02) {
  142. this.excepDrawer = true
  143. this.exceTxt = 1
  144. }
  145. // 耗材状态
  146. this.otherSetting.aijiuNum = String(data.consumable || 0)
  147. this.otherSetting.lvxinNum = String(data.filterPercent || 0)
  148. this.otherSetting.huishouNum = String(data.recycleBinPct || 0)
  149. },
  150. _handleGroup2(data) {
  151. // 参数组2: 预热/点火进度
  152. if (data.preheatPercent < 100) {
  153. this.ispreHot = false
  154. this.hotPercentage = data.preheatPercent + '%'
  155. } else {
  156. this.ispreHot = true
  157. this.hotPercentage = data.ignitePercent + '%'
  158. }
  159. },
  160. // ========== 发送指令 ==========
  161. _sendCurrentState() {
  162. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  163. bleManager.sendBasic({
  164. power: POWER.ON,
  165. moxiState: MOXI_STATE.DONE,
  166. preheatState: MOXI_STATE.DONE,
  167. mute: MUTE.OFF,
  168. mode: modeMap[this.modeType] || MODE.LEISURE,
  169. subMode: 0x01,
  170. temperature: TEMPERATURE.MID,
  171. angle: CHAIR_ANGLE.OFF,
  172. duration: 0
  173. }).catch(e => console.error('sendBasic fail', e))
  174. },
  175. // ========== 开始艾灸 ==========
  176. startDeviceEvt() {
  177. this.deviceStatus = 1
  178. this.ispreHot = false
  179. let totalDuration = 0
  180. this.curCase.forEach(item => {
  181. totalDuration += item.time
  182. })
  183. // 发送开始指令
  184. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  185. bleManager.sendBasic({
  186. power: POWER.ON,
  187. moxiState: MOXI_STATE.START,
  188. preheatState: MOXI_STATE.START,
  189. mute: MUTE.OFF,
  190. mode: modeMap[this.modeType] || MODE.LEISURE,
  191. subMode: 0x01,
  192. temperature: TEMPERATURE.MID,
  193. angle: CHAIR_ANGLE.OFF,
  194. duration: totalDuration || 30
  195. }).catch(e => console.error('startDevice fail', e))
  196. // 发送穴位坐标
  197. if (this.curCase.length > 0) {
  198. const points = this.curCase.map(item => ({
  199. point: item.id,
  200. x: item._x,
  201. y: item._y
  202. }))
  203. bleManager.sendAcupoints(points).catch(e => console.error('sendAcupoints fail', e))
  204. }
  205. },
  206. // ========== 暂停/继续 ==========
  207. stopAijiu() {
  208. if (this.deviceStatus == 5) {
  209. // 继续
  210. this.deviceStatus = 3
  211. bleManager.sendBasic({ moxiState: MOXI_STATE.START }).catch(() => {})
  212. } else if (this.deviceStatus == 3) {
  213. // 暂停
  214. this.deviceStatus = 5
  215. bleManager.sendBasic({ moxiState: MOXI_STATE.PAUSE }).catch(() => {})
  216. }
  217. },
  218. // ========== 停止 ==========
  219. stopPreHot() {
  220. this.isShowConfirm = true
  221. },
  222. confirmStop() {
  223. this.deviceStatus = 0
  224. this.isShowConfirm = false
  225. bleManager.sendBasic({ moxiState: MOXI_STATE.DONE }).catch(() => {})
  226. },
  227. // ========== 模式切换 ==========
  228. changeMode(num) {
  229. uni.showModal({
  230. title: '提示',
  231. content: '切换模式会停止当前的艾灸,是否继续',
  232. cancelText: '取消',
  233. confirmText: '继续',
  234. success: (res) => {
  235. if (res.confirm) {
  236. this.modeType = num
  237. this.deviceStatus = 0
  238. this.isShowDrawer2 = false
  239. if (this._stopAudioOnModeChange) {
  240. this._stopAudioOnModeChange()
  241. }
  242. // 发送模式切换指令
  243. const modeMap = [MODE.LEISURE, MODE.PROFESSIONAL, MODE.PERSONAL, MODE.EXPERT]
  244. bleManager.sendBasic({
  245. power: POWER.ON,
  246. moxiState: MOXI_STATE.DONE,
  247. preheatState: MOXI_STATE.DONE,
  248. mute: MUTE.OFF,
  249. mode: modeMap[num] || MODE.LEISURE,
  250. subMode: 0x01,
  251. temperature: TEMPERATURE.MID,
  252. angle: CHAIR_ANGLE.OFF,
  253. duration: 0
  254. }).catch(e => console.error('changeMode fail', e))
  255. }
  256. }
  257. })
  258. },
  259. // ========== 断开BLE ==========
  260. disconnectBle() {
  261. this._cleanListeners()
  262. bleManager.disconnect().catch(() => {})
  263. }
  264. }
  265. }