BleManager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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. if (hit) {
  163. matched.push(d)
  164. this.emit('deviceFound', d)
  165. if (!returnAll) { finish(null, d); return }
  166. }
  167. }
  168. }
  169. const timer = setTimeout(() => {
  170. if (returnAll) finish(null, matched)
  171. else if (matched.length) finish(null, matched[0])
  172. else finish(this._err(BLE_ERROR.SCAN_FAIL, { msg: '扫描超时,未发现目标设备' }))
  173. }, timeout)
  174. const finish = (err, data) => {
  175. if (finished) return // 防重入
  176. finished = true
  177. this._cancelScan = null
  178. clearTimeout(timer)
  179. uni.offBluetoothDeviceFound && uni.offBluetoothDeviceFound(onFound)
  180. this._invoke(uni.stopBluetoothDevicesDiscovery, {}).catch(() => {})
  181. this._setState(BLE_STATE.READY)
  182. err ? reject(err) : resolve(data)
  183. }
  184. // 暴露取消句柄,供 destroy() / 下次 scan() 调用
  185. this._cancelScan = () => finish(this._err(BLE_ERROR.SCAN_FAIL, { msg: '扫描已取消' }))
  186. uni.onBluetoothDeviceFound(onFound)
  187. this._setState(BLE_STATE.SCANNING)
  188. try {
  189. await this._invoke(uni.startBluetoothDevicesDiscovery, {
  190. allowDuplicatesKey: false,
  191. interval: 0,
  192. services
  193. })
  194. } catch (e) {
  195. finish(this._err(BLE_ERROR.SCAN_FAIL, e))
  196. }
  197. })
  198. }
  199. // ============== 连接 ==============
  200. /**
  201. * 连接指定设备
  202. * @param {string} deviceId
  203. */
  204. async connect(deviceId) {
  205. if (!deviceId) throw this._err(BLE_ERROR.CONNECT_FAIL, { msg: 'deviceId 不能为空' })
  206. await this._ensureReady()
  207. if (this._state === BLE_STATE.CONNECTING) throw this._err(BLE_ERROR.BUSY)
  208. this._setState(BLE_STATE.CONNECTING)
  209. this._device = { deviceId, name: '' }
  210. try {
  211. await this._invoke(uni.createBLEConnection, {
  212. deviceId,
  213. timeout: this.config.connectTimeout
  214. })
  215. this._setState(BLE_STATE.CONNECTED)
  216. // Android 提升 MTU (忽略失败)
  217. // #ifdef APP-PLUS
  218. if (uni.getSystemInfoSync().platform === 'android' && uni.setBLEMTU) {
  219. try { await this._invoke(uni.setBLEMTU, { deviceId, mtu: 185 }) } catch (_) {}
  220. }
  221. // #endif
  222. await this._discoverAndSubscribe(deviceId)
  223. this._setState(BLE_STATE.READY_COMM)
  224. this._reconnectCount = 0
  225. this.emit('connected', this._device)
  226. } catch (e) {
  227. this._setState(BLE_STATE.DISCONNECTED)
  228. try { await this._invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
  229. const code = e && (e.errCode || e.code)
  230. if (code === 10003 || code === -1) {
  231. throw this._err(BLE_ERROR.CONNECT_TIMEOUT, e)
  232. }
  233. throw this._err(BLE_ERROR.CONNECT_FAIL, e)
  234. }
  235. }
  236. /** 扫描 + 连接一步到位 */
  237. async scanAndConnect(opt) {
  238. const device = await this.scan(opt)
  239. await this.connect(device.deviceId)
  240. this._device.name = device.name || device.localName || ''
  241. return this._device
  242. }
  243. /** 主动断开 */
  244. async disconnect() {
  245. this._clearReconnect()
  246. if (!this._device) return
  247. const { deviceId } = this._device
  248. try { await this._invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
  249. this._device = null
  250. this._setState(BLE_STATE.DISCONNECTED)
  251. this.emit('disconnected', { manual: true })
  252. }
  253. /** 发现服务并订阅通知 */
  254. async _discoverAndSubscribe(deviceId) {
  255. // 1) 服务列表
  256. const svcRes = await this._invoke(uni.getBLEDeviceServices, { deviceId })
  257. const services = svcRes.services || []
  258. const targetSvc = services.find(s => this._uuidEq(s.uuid, this.config.serviceId)) || services.find(s => s.isPrimary) || services[0]
  259. if (!targetSvc) throw this._err(BLE_ERROR.SERVICE_NOT_FOUND)
  260. this._serviceId = targetSvc.uuid
  261. // 2) 特征值
  262. const charRes = await this._invoke(uni.getBLEDeviceCharacteristics, {
  263. deviceId, serviceId: this._serviceId
  264. })
  265. const chars = charRes.characteristics || []
  266. const writeChar = chars.find(c => this._uuidEq(c.uuid, this.config.writeCharId))
  267. || chars.find(c => c.properties && (c.properties.write || c.properties.writeNoResponse || c.properties.writeDefault))
  268. const notifyChar = chars.find(c => this._uuidEq(c.uuid, this.config.notifyCharId))
  269. || chars.find(c => c.properties && (c.properties.notify || c.properties.indicate))
  270. if (!writeChar) throw this._err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到写特征' })
  271. if (!notifyChar) throw this._err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到通知特征' })
  272. this._writeCharId = writeChar.uuid
  273. this._notifyCharId = notifyChar.uuid
  274. // 3) 开启通知
  275. await this._invoke(uni.notifyBLECharacteristicValueChange, {
  276. deviceId,
  277. serviceId: this._serviceId,
  278. characteristicId: this._notifyCharId,
  279. state: true
  280. })
  281. }
  282. _uuidEq(a, b) {
  283. return String(a || '').toLowerCase() === String(b || '').toLowerCase()
  284. }
  285. // ============== 断连处理 & 自动重连 ==============
  286. _handleDisconnected(reason) {
  287. if (this._state === BLE_STATE.DISCONNECTED || this._state === BLE_STATE.IDLE) return
  288. const device = this._device
  289. this._setState(BLE_STATE.DISCONNECTED)
  290. this.emit('disconnected', { manual: false, reason })
  291. this._parser.reset()
  292. // 尝试自动重连
  293. if (this.config.autoReconnect && device && this._reconnectCount < this.config.maxReconnect) {
  294. this._reconnectCount++
  295. const delay = Math.min(1000 * this._reconnectCount, 5000)
  296. logger.warn(`reconnect in ${delay}ms (${this._reconnectCount}/${this.config.maxReconnect})`)
  297. this._reconnectTimer = setTimeout(async () => {
  298. try {
  299. await this.init()
  300. await this.connect(device.deviceId)
  301. this.emit('reconnected', this._device)
  302. } catch (e) {
  303. logger.error('reconnect fail', e)
  304. this._handleDisconnected(reason)
  305. }
  306. }, delay)
  307. }
  308. }
  309. _clearReconnect() {
  310. if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null }
  311. this._reconnectCount = 0
  312. }
  313. // ============== 收包 ==============
  314. _onFrame(decoded, frame) {
  315. // 结构化: { funcCode, payload, parsed }
  316. this.emit('frame', { ...decoded, raw: frame })
  317. if (decoded.parsed) this.emit('report', decoded.parsed)
  318. // 分类事件, 便于精细订阅
  319. if (decoded.parsed && decoded.parsed.type) {
  320. this.emit(`report:${decoded.parsed.type}`, decoded.parsed)
  321. }
  322. }
  323. // ============== 发送 ==============
  324. /** 低层: 写入任意字节 */
  325. async writeRaw(u8) {
  326. if (!this.isConnected) throw this._err(BLE_ERROR.DISCONNECTED)
  327. const payload = bufferToArrayBuffer(u8)
  328. // 串行写入, 避免并发写同一特征
  329. this._writeLock = this._writeLock.then(async () => {
  330. logger.log('=>', bytesToHex(u8))
  331. try {
  332. await this._invoke(uni.writeBLECharacteristicValue, {
  333. deviceId: this._device.deviceId,
  334. serviceId: this._serviceId,
  335. characteristicId: this._writeCharId,
  336. value: payload
  337. })
  338. } catch (e) {
  339. throw this._err(BLE_ERROR.WRITE_FAIL, e)
  340. }
  341. })
  342. return this._writeLock
  343. }
  344. /**
  345. * 发送任意帧 (不等应用层回复, 只等底层 ack)
  346. * @param {Uint8Array} frame
  347. */
  348. send(frame) { return this.writeRaw(frame) }
  349. // ============== 高层业务接口 ==============
  350. /** 下发基本功能指令 (0x01) */
  351. sendBasic(opt) { return this.writeRaw(encodeBasic(opt)) }
  352. /** 下发模式参数 1 (步骤 1-7) */
  353. sendModeParam1(opt) { return this.writeRaw(encodeModeParam1(opt)) }
  354. /** 下发模式参数 2 (步骤 8-14) */
  355. sendModeParam2(opt) { return this.writeRaw(encodeModeParam2(opt)) }
  356. /** 下发穴位坐标 (每包 2 个穴位, 可传任意数量, 内部自动分包) */
  357. async sendAcupoints(points = []) {
  358. for (let i = 0; i < points.length; i += 2) {
  359. const pair = points.slice(i, i + 2)
  360. await this.writeRaw(encodeAcupoints(pair))
  361. }
  362. }
  363. // ---- 常用快捷方法 ----
  364. powerOn() { return this.sendBasic({ power: POWER.ON }) }
  365. powerOff() { return this.sendBasic({ power: POWER.OFF }) }
  366. startMoxi(opt = {}) { return this.sendBasic({ power: POWER.ON, moxiState: MOXI_STATE.START, ...opt }) }
  367. pauseMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.PAUSE }) }
  368. stopMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.DONE }) }
  369. setMute(on) { return this.sendBasic({ mute: on ? MUTE.ON : MUTE.OFF }) }
  370. setTemperature(v) { return this.sendBasic({ temperature: v }) }
  371. setChairAngle(v) { return this.sendBasic({ angle: v }) }
  372. // ============== 工具 ==============
  373. /** 封装 uni 回调为 Promise */
  374. _invoke(apiFn, params) {
  375. return new Promise((resolve, reject) => {
  376. apiFn({ ...params, success: resolve, fail: reject, complete: () => {} })
  377. })
  378. }
  379. async _ensureReady() {
  380. if (this._state === BLE_STATE.IDLE) await this.init()
  381. // 二次校验
  382. try {
  383. const res = await this._invoke(uni.getBluetoothAdapterState, {})
  384. if (!res.available) throw this._err(BLE_ERROR.ADAPTER_OFF)
  385. } catch (e) {
  386. if (e && e.code && String(e.code).startsWith('BLE_')) throw e
  387. throw this._err(BLE_ERROR.ADAPTER_OFF, e)
  388. }
  389. }
  390. _err(code, origin) {
  391. const err = new Error(code)
  392. err.code = code
  393. if (origin) err.origin = origin
  394. return err
  395. }
  396. }
  397. export default BleManager