BleManager.js 14 KB

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