ble-mixin.js 9.5 KB

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