BleManager.js 15 KB

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