BleManager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. /**
  2. * 艾灸椅 BLE 单例管理器
  3. *
  4. * 使用说明:
  5. * import bleManager from '@/utils/ble'
  6. *
  7. * // 1. 初始化适配器
  8. * await bleManager.init()
  9. *
  10. * // 2. 扫描并连接(按名称)
  11. * const device = await bleManager.scan({ namePrefix: 'AJY', timeout: 8000 })
  12. * await bleManager.connect(device.deviceId)
  13. *
  14. * // 3. 订阅上报
  15. * bleManager.on('report', data => console.log(data))
  16. * bleManager.on('state', s => console.log('状态:', s))
  17. *
  18. * // 4. 发送指令
  19. * await bleManager.sendBasic({ power: 1, moxiState: 1, mode: 2, subMode: 1, temperature: 2, duration: 30 })
  20. */
  21. import EventEmitter from './EventEmitter.js'
  22. import logger from './logger.js'
  23. import {
  24. DEFAULT_CONFIG, BLE_STATE, BLE_ERROR,
  25. POWER, MOXI_STATE, MUTE, MODE, SUB_MODE, TEMPERATURE, CHAIR_ANGLE
  26. } from './constants.js'
  27. import {
  28. encodeBasic, encodeModeParam1, encodeModeParam2, encodeAcupoints,
  29. bufferToArrayBuffer, arrayBufferToU8, bytesToHex, FrameParser
  30. } from './protocol.js'
  31. class BleManager extends EventEmitter {
  32. constructor() {
  33. super()
  34. this.config = { ...DEFAULT_CONFIG }
  35. this._state = BLE_STATE.IDLE
  36. this._device = null // { deviceId, name, RSSI }
  37. this._serviceId = null
  38. this._writeCharId = null
  39. this._notifyCharId = null
  40. this._reconnectCount = 0
  41. this._reconnectTimer = null
  42. this._parser = new FrameParser(
  43. (decoded, frame) => this._onFrame(decoded, frame),
  44. (err, frame) => logger.warn('frame parse error', err.message, bytesToHex(frame))
  45. )
  46. this._writeLock = Promise.resolve() // 写入串行化
  47. this._bound = false // 是否已绑定系统监听
  48. this._cancelScan = null // 取消残留扫描的回调
  49. }
  50. // ============== 基础 ==============
  51. /** 获取单例 */
  52. static getInstance() {
  53. if (!BleManager._instance) BleManager._instance = new BleManager()
  54. return BleManager._instance
  55. }
  56. /** 覆盖配置 */
  57. configure(opt = {}) {
  58. this.config = { ...this.config, ...opt }
  59. logger.setEnabled(this.config.debug)
  60. }
  61. get state() { return this._state }
  62. get device() { return this._device }
  63. get isConnected() { return this._state === BLE_STATE.READY_COMM }
  64. _setState(s) {
  65. if (this._state === s) return
  66. this._state = s
  67. logger.info('state ->', s)
  68. this.emit('state', s)
  69. }
  70. // ============== 初始化 / 释放 ==============
  71. /** 打开蓝牙适配器 */
  72. async init() {
  73. if (this._state !== BLE_STATE.IDLE && this._state !== BLE_STATE.DISCONNECTED) {
  74. return
  75. }
  76. try {
  77. await this._invoke(uni.openBluetoothAdapter, {})
  78. } catch (e) {
  79. logger.error('openBluetoothAdapter fail', e)
  80. // errCode 10001 = 蓝牙未开启
  81. const code = e && (e.errCode || e.code)
  82. throw this._err(code === 10001 ? BLE_ERROR.ADAPTER_OFF : BLE_ERROR.NOT_SUPPORT, e)
  83. }
  84. this._bindSystemListeners()
  85. this._setState(BLE_STATE.READY)
  86. }
  87. /** 彻底释放 */
  88. async destroy() {
  89. this._clearReconnect()
  90. // 取消残留的扫描
  91. if (this._cancelScan) { this._cancelScan(); this._cancelScan = null }
  92. try { await this.disconnect() } catch (_) {}
  93. try { await this._invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
  94. this._unbindSystemListeners()
  95. this._setState(BLE_STATE.IDLE)
  96. this.clear()
  97. }
  98. _bindSystemListeners() {
  99. if (this._bound) return
  100. uni.onBluetoothAdapterStateChange(this._onAdapterStateChange = res => {
  101. logger.info('adapterStateChange', res)
  102. this.emit('adapterState', res)
  103. if (!res.available) {
  104. this._handleDisconnected(BLE_ERROR.ADAPTER_OFF)
  105. }
  106. })
  107. uni.onBLEConnectionStateChange(this._onConnStateChange = res => {
  108. logger.info('connectionStateChange', res)
  109. if (!res.connected && this._device && res.deviceId === this._device.deviceId) {
  110. this._handleDisconnected(BLE_ERROR.DISCONNECTED)
  111. }
  112. })
  113. uni.onBLECharacteristicValueChange(this._onCharChange = res => {
  114. const u8 = arrayBufferToU8(res.value)
  115. logger.log('<=', bytesToHex(u8))
  116. this._parser.feed(u8)
  117. })
  118. this._bound = true
  119. }
  120. _unbindSystemListeners() {
  121. if (!this._bound) return
  122. try { uni.offBluetoothAdapterStateChange && uni.offBluetoothAdapterStateChange(this._onAdapterStateChange) } catch (_) {}
  123. try { uni.offBLEConnectionStateChange && uni.offBLEConnectionStateChange(this._onConnStateChange) } catch (_) {}
  124. try { uni.offBLECharacteristicValueChange && uni.offBLECharacteristicValueChange(this._onCharChange) } catch (_) {}
  125. this._bound = false
  126. }
  127. // ============== 扫描 ==============
  128. /**
  129. * 扫描设备
  130. * @param {Object} opt
  131. * @param {string} [opt.namePrefix] 名称前缀过滤
  132. * @param {string} [opt.deviceName] 精确匹配名
  133. * @param {string[]} [opt.services] 服务UUID过滤(iOS需要)
  134. * @param {number} [opt.timeout]
  135. * @param {boolean} [opt.returnAll] 为 true 时返回所有匹配设备数组, 默认返回首个
  136. * @returns {Promise<Object|Object[]>}
  137. */
  138. async scan(opt = {}) {
  139. // 取消上一次残留的扫描(如 search 页面未正确清理)
  140. if (this._cancelScan) { this._cancelScan(); this._cancelScan = null }
  141. await this._ensureReady()
  142. const {
  143. namePrefix = this.config.deviceNamePrefix,
  144. deviceName,
  145. services,
  146. timeout = this.config.scanTimeout,
  147. returnAll = false
  148. } = opt
  149. const devices = new Map()
  150. const matched = []
  151. return new Promise(async (resolve, reject) => {
  152. let finished = false // 防重入:确保 finish 只执行一次
  153. const onFound = res => {
  154. if (finished) return
  155. for (const d of res.devices) {
  156. if (devices.has(d.deviceId)) continue
  157. devices.set(d.deviceId, d)
  158. const name = d.name || d.localName || ''
  159. const hit =
  160. (deviceName && name === deviceName) ||
  161. (namePrefix && name.startsWith(namePrefix)) ||
  162. (!deviceName && !namePrefix)
  163. if (hit) {
  164. matched.push(d)
  165. this.emit('deviceFound', d)
  166. if (!returnAll) { finish(null, d); return }
  167. }
  168. }
  169. }
  170. const timer = setTimeout(() => {
  171. if (returnAll) finish(null, matched)
  172. else if (matched.length) finish(null, matched[0])
  173. else finish(this._err(BLE_ERROR.SCAN_FAIL, { msg: '扫描超时,未发现目标设备' }))
  174. }, timeout)
  175. const finish = (err, data) => {
  176. if (finished) return // 防重入
  177. finished = true
  178. this._cancelScan = null
  179. clearTimeout(timer)
  180. uni.offBluetoothDeviceFound && uni.offBluetoothDeviceFound(onFound)
  181. this._invoke(uni.stopBluetoothDevicesDiscovery, {}).catch(() => {})
  182. this._setState(BLE_STATE.READY)
  183. err ? reject(err) : resolve(data)
  184. }
  185. // 暴露取消句柄,供 destroy() / 下次 scan() 调用
  186. this._cancelScan = () => finish(this._err(BLE_ERROR.SCAN_FAIL, { msg: '扫描已取消' }))
  187. uni.onBluetoothDeviceFound(onFound)
  188. this._setState(BLE_STATE.SCANNING)
  189. try {
  190. await this._invoke(uni.startBluetoothDevicesDiscovery, {
  191. allowDuplicatesKey: false,
  192. interval: 0,
  193. services
  194. })
  195. } catch (e) {
  196. finish(this._err(BLE_ERROR.SCAN_FAIL, e))
  197. }
  198. })
  199. }
  200. // ============== 连接 ==============
  201. /**
  202. * 连接指定设备
  203. * @param {string} deviceId
  204. */
  205. async connect(deviceId) {
  206. if (!deviceId) throw this._err(BLE_ERROR.CONNECT_FAIL, { msg: 'deviceId 不能为空' })
  207. await this._ensureReady()
  208. if (this._state === BLE_STATE.CONNECTING) throw this._err(BLE_ERROR.BUSY)
  209. this._setState(BLE_STATE.CONNECTING)
  210. this._device = { deviceId, name: '' }
  211. try {
  212. await this._invoke(uni.createBLEConnection, {
  213. deviceId,
  214. timeout: this.config.connectTimeout
  215. })
  216. this._setState(BLE_STATE.CONNECTED)
  217. // Android 提升 MTU (忽略失败)
  218. // #ifdef APP-PLUS
  219. if (uni.getSystemInfoSync().platform === 'android' && uni.setBLEMTU) {
  220. try { await this._invoke(uni.setBLEMTU, { deviceId, mtu: 185 }) } catch (_) {}
  221. }
  222. // #endif
  223. await this._discoverAndSubscribe(deviceId)
  224. this._setState(BLE_STATE.READY_COMM)
  225. this._reconnectCount = 0
  226. this.emit('connected', this._device)
  227. } catch (e) {
  228. this._setState(BLE_STATE.DISCONNECTED)
  229. try { await this._invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
  230. const code = e && (e.errCode || e.code)
  231. if (code === 10003 || code === -1) {
  232. throw this._err(BLE_ERROR.CONNECT_TIMEOUT, e)
  233. }
  234. throw this._err(BLE_ERROR.CONNECT_FAIL, e)
  235. }
  236. }
  237. /** 扫描 + 连接一步到位 */
  238. async scanAndConnect(opt) {
  239. const device = await this.scan(opt)
  240. await this.connect(device.deviceId)
  241. this._device.name = device.name || device.localName || ''
  242. return this._device
  243. }
  244. /** 主动断开 */
  245. async disconnect() {
  246. this._clearReconnect()
  247. if (!this._device) return
  248. const { deviceId } = this._device
  249. try { await this._invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
  250. this._device = null
  251. this._setState(BLE_STATE.DISCONNECTED)
  252. this.emit('disconnected', { manual: true })
  253. }
  254. /** 发现服务并订阅通知 */
  255. async _discoverAndSubscribe(deviceId) {
  256. // 1) 服务列表
  257. const svcRes = await this._invoke(uni.getBLEDeviceServices, { deviceId })
  258. const services = svcRes.services || []
  259. const targetSvc = services.find(s => this._uuidEq(s.uuid, this.config.serviceId)) || services.find(s => s.isPrimary) || services[0]
  260. if (!targetSvc) throw this._err(BLE_ERROR.SERVICE_NOT_FOUND)
  261. this._serviceId = targetSvc.uuid
  262. // 2) 特征值
  263. const charRes = await this._invoke(uni.getBLEDeviceCharacteristics, {
  264. deviceId, serviceId: this._serviceId
  265. })
  266. const chars = charRes.characteristics || []
  267. const writeChar = chars.find(c => this._uuidEq(c.uuid, this.config.writeCharId))
  268. || chars.find(c => c.properties && (c.properties.write || c.properties.writeNoResponse || c.properties.writeDefault))
  269. const notifyChar = chars.find(c => this._uuidEq(c.uuid, this.config.notifyCharId))
  270. || chars.find(c => c.properties && (c.properties.notify || c.properties.indicate))
  271. if (!writeChar) throw this._err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到写特征' })
  272. if (!notifyChar) throw this._err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到通知特征' })
  273. this._writeCharId = writeChar.uuid
  274. this._notifyCharId = notifyChar.uuid
  275. // 3) 开启通知
  276. await this._invoke(uni.notifyBLECharacteristicValueChange, {
  277. deviceId,
  278. serviceId: this._serviceId,
  279. characteristicId: this._notifyCharId,
  280. state: true
  281. })
  282. }
  283. _uuidEq(a, b) {
  284. return String(a || '').toLowerCase() === String(b || '').toLowerCase()
  285. }
  286. // ============== 断连处理 & 自动重连 ==============
  287. _handleDisconnected(reason) {
  288. if (this._state === BLE_STATE.DISCONNECTED || this._state === BLE_STATE.IDLE) return
  289. const device = this._device
  290. this._setState(BLE_STATE.DISCONNECTED)
  291. this.emit('disconnected', { manual: false, reason })
  292. this._parser.reset()
  293. // 尝试自动重连
  294. if (this.config.autoReconnect && device && this._reconnectCount < this.config.maxReconnect) {
  295. this._reconnectCount++
  296. const delay = Math.min(1000 * this._reconnectCount, 5000)
  297. logger.warn(`reconnect in ${delay}ms (${this._reconnectCount}/${this.config.maxReconnect})`)
  298. this._reconnectTimer = setTimeout(async () => {
  299. try {
  300. await this.init()
  301. await this.connect(device.deviceId)
  302. this.emit('reconnected', this._device)
  303. } catch (e) {
  304. logger.error('reconnect fail', e)
  305. this._handleDisconnected(reason)
  306. }
  307. }, delay)
  308. }
  309. }
  310. _clearReconnect() {
  311. if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null }
  312. this._reconnectCount = 0
  313. }
  314. // ============== 收包 ==============
  315. _onFrame(decoded, frame) {
  316. // 结构化: { funcCode, payload, parsed }
  317. this.emit('frame', { ...decoded, raw: frame })
  318. if (decoded.parsed) this.emit('report', decoded.parsed)
  319. // 分类事件, 便于精细订阅
  320. if (decoded.parsed && decoded.parsed.type) {
  321. this.emit(`report:${decoded.parsed.type}`, decoded.parsed)
  322. }
  323. }
  324. // ============== 发送 ==============
  325. /** 低层: 写入任意字节 */
  326. async writeRaw(u8) {
  327. if (!this.isConnected) throw this._err(BLE_ERROR.DISCONNECTED)
  328. const payload = bufferToArrayBuffer(u8)
  329. // 串行写入, 避免并发写同一特征
  330. this._writeLock = this._writeLock.then(async () => {
  331. logger.log('=>', bytesToHex(u8))
  332. try {
  333. await this._invoke(uni.writeBLECharacteristicValue, {
  334. deviceId: this._device.deviceId,
  335. serviceId: this._serviceId,
  336. characteristicId: this._writeCharId,
  337. value: payload
  338. })
  339. } catch (e) {
  340. throw this._err(BLE_ERROR.WRITE_FAIL, e)
  341. }
  342. })
  343. return this._writeLock
  344. }
  345. /**
  346. * 发送任意帧 (不等应用层回复, 只等底层 ack)
  347. * @param {Uint8Array} frame
  348. */
  349. send(frame) { return this.writeRaw(frame) }
  350. // ============== 高层业务接口 ==============
  351. /** 下发基本功能指令 (0x01) */
  352. sendBasic(opt) { return this.writeRaw(encodeBasic(opt)) }
  353. /** 下发模式参数 1 (步骤 1-7) */
  354. sendModeParam1(opt) { return this.writeRaw(encodeModeParam1(opt)) }
  355. /** 下发模式参数 2 (步骤 8-14) */
  356. sendModeParam2(opt) { return this.writeRaw(encodeModeParam2(opt)) }
  357. /** 下发穴位坐标 (每包 2 个穴位, 可传任意数量, 内部自动分包) */
  358. async sendAcupoints(points = []) {
  359. for (let i = 0; i < points.length; i += 2) {
  360. const pair = points.slice(i, i + 2)
  361. await this.writeRaw(encodeAcupoints(pair))
  362. }
  363. }
  364. // ---- 常用快捷方法 ----
  365. powerOn() { return this.sendBasic({ power: POWER.ON }) }
  366. powerOff() { return this.sendBasic({ power: POWER.OFF }) }
  367. startMoxi(opt = {}) { return this.sendBasic({ power: POWER.ON, moxiState: MOXI_STATE.START, ...opt }) }
  368. pauseMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.PAUSE }) }
  369. stopMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.DONE }) }
  370. setMute(on) { return this.sendBasic({ mute: on ? MUTE.ON : MUTE.OFF }) }
  371. setTemperature(v) { return this.sendBasic({ temperature: v }) }
  372. setChairAngle(v) { return this.sendBasic({ angle: v }) }
  373. // ============== 工具 ==============
  374. /** 封装 uni 回调为 Promise */
  375. _invoke(apiFn, params) {
  376. return new Promise((resolve, reject) => {
  377. apiFn({ ...params, success: resolve, fail: reject, complete: () => {} })
  378. })
  379. }
  380. async _ensureReady() {
  381. if (this._state === BLE_STATE.IDLE) await this.init()
  382. // 二次校验
  383. try {
  384. const res = await this._invoke(uni.getBluetoothAdapterState, {})
  385. if (!res.available) throw this._err(BLE_ERROR.ADAPTER_OFF)
  386. } catch (e) {
  387. if (e && e.code && String(e.code).startsWith('BLE_')) throw e
  388. throw this._err(BLE_ERROR.ADAPTER_OFF, e)
  389. }
  390. }
  391. _err(code, origin) {
  392. const err = new Error(code)
  393. err.code = code
  394. if (origin) err.origin = origin
  395. return err
  396. }
  397. }
  398. export default BleManager