| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- /**
- * 艾灸椅 BLE 单例管理器
- *
- * 使用说明:
- * import bleManager from '@/utils/ble'
- *
- * // 1. 初始化适配器
- * await bleManager.init()
- *
- * // 2. 扫描并连接(按名称)
- * const device = await bleManager.scan({ namePrefix: 'AJY', timeout: 8000 })
- * await bleManager.connect(device.deviceId)
- *
- * // 3. 订阅上报
- * bleManager.on('report', data => console.log(data))
- * bleManager.on('state', s => console.log('状态:', s))
- *
- * // 4. 发送指令
- * await bleManager.sendBasic({ power: 1, moxiState: 1, mode: 2, subMode: 1, temperature: 2, duration: 30 })
- */
- import EventEmitter from './EventEmitter.js'
- import logger from './logger.js'
- import {
- DEFAULT_CONFIG, BLE_STATE, BLE_ERROR,
- POWER, MOXI_STATE, MUTE, MODE, SUB_MODE, TEMPERATURE, CHAIR_ANGLE
- } from './constants.js'
- import {
- encodeBasic, encodeModeParam1, encodeModeParam2, encodeAcupoints,
- bufferToArrayBuffer, arrayBufferToU8, bytesToHex, FrameParser
- } from './protocol.js'
- class BleManager extends EventEmitter {
- constructor() {
- super()
- this.config = { ...DEFAULT_CONFIG }
- this._state = BLE_STATE.IDLE
- this._device = null // { deviceId, name, RSSI }
- this._serviceId = null
- this._writeCharId = null
- this._notifyCharId = null
- this._reconnectCount = 0
- this._reconnectTimer = null
- this._parser = new FrameParser(
- (decoded, frame) => this._onFrame(decoded, frame),
- (err, frame) => logger.warn('frame parse error', err.message, bytesToHex(frame))
- )
- this._writeLock = Promise.resolve() // 写入串行化
- this._bound = false // 是否已绑定系统监听
- this._cancelScan = null // 取消残留扫描的回调
- }
- // ============== 基础 ==============
- /** 获取单例 */
- static getInstance() {
- if (!BleManager._instance) BleManager._instance = new BleManager()
- return BleManager._instance
- }
- /** 覆盖配置 */
- configure(opt = {}) {
- this.config = { ...this.config, ...opt }
- logger.setEnabled(this.config.debug)
- }
- get state() { return this._state }
- get device() { return this._device }
- get isConnected() { return this._state === BLE_STATE.READY_COMM }
- _setState(s) {
- if (this._state === s) return
- this._state = s
- logger.info('state ->', s)
- this.emit('state', s)
- }
- // ============== 初始化 / 释放 ==============
- /** 打开蓝牙适配器 */
- async init() {
- if (this._state !== BLE_STATE.IDLE && this._state !== BLE_STATE.DISCONNECTED) {
- return
- }
- try {
- await this._invoke(uni.openBluetoothAdapter, {})
- } catch (e) {
- logger.error('openBluetoothAdapter fail', e)
- // errCode 10001 = 蓝牙未开启
- const code = e && (e.errCode || e.code)
- throw this._err(code === 10001 ? BLE_ERROR.ADAPTER_OFF : BLE_ERROR.NOT_SUPPORT, e)
- }
- this._bindSystemListeners()
- this._setState(BLE_STATE.READY)
- }
- /** 彻底释放 */
- async destroy() {
- this._clearReconnect()
- // 取消残留的扫描
- if (this._cancelScan) { this._cancelScan(); this._cancelScan = null }
- try { await this.disconnect() } catch (_) {}
- try { await this._invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
- this._unbindSystemListeners()
- this._setState(BLE_STATE.IDLE)
- this.clear()
- }
- _bindSystemListeners() {
- if (this._bound) return
- uni.onBluetoothAdapterStateChange(this._onAdapterStateChange = res => {
- logger.info('adapterStateChange', res)
- this.emit('adapterState', res)
- if (!res.available) {
- this._handleDisconnected(BLE_ERROR.ADAPTER_OFF)
- }
- })
- uni.onBLEConnectionStateChange(this._onConnStateChange = res => {
- logger.info('connectionStateChange', res)
- if (!res.connected && this._device && res.deviceId === this._device.deviceId) {
- this._handleDisconnected(BLE_ERROR.DISCONNECTED)
- }
- })
- uni.onBLECharacteristicValueChange(this._onCharChange = res => {
- const u8 = arrayBufferToU8(res.value)
- logger.log('<=', bytesToHex(u8))
- this._parser.feed(u8)
- })
- this._bound = true
- }
- _unbindSystemListeners() {
- if (!this._bound) return
- try { uni.offBluetoothAdapterStateChange && uni.offBluetoothAdapterStateChange(this._onAdapterStateChange) } catch (_) {}
- try { uni.offBLEConnectionStateChange && uni.offBLEConnectionStateChange(this._onConnStateChange) } catch (_) {}
- try { uni.offBLECharacteristicValueChange && uni.offBLECharacteristicValueChange(this._onCharChange) } catch (_) {}
- this._bound = false
- }
- // ============== 扫描 ==============
- /**
- * 扫描设备
- * @param {Object} opt
- * @param {string} [opt.namePrefix] 名称前缀过滤
- * @param {string} [opt.deviceName] 精确匹配名
- * @param {string[]} [opt.services] 服务UUID过滤(iOS需要)
- * @param {number} [opt.timeout]
- * @param {boolean} [opt.returnAll] 为 true 时返回所有匹配设备数组, 默认返回首个
- * @returns {Promise<Object|Object[]>}
- */
- async scan(opt = {}) {
- // 取消上一次残留的扫描(如 search 页面未正确清理)
- if (this._cancelScan) { this._cancelScan(); this._cancelScan = null }
- await this._ensureReady()
- const {
- namePrefix = this.config.deviceNamePrefix,
- deviceName,
- services,
- timeout = this.config.scanTimeout,
- returnAll = false
- } = opt
- const devices = new Map()
- const matched = []
- return new Promise(async (resolve, reject) => {
- let finished = false // 防重入:确保 finish 只执行一次
- const onFound = res => {
- if (finished) return
- for (const d of res.devices) {
- if (devices.has(d.deviceId)) continue
- devices.set(d.deviceId, d)
- const name = d.name || d.localName || ''
- const hit =
- (deviceName && name === deviceName) ||
- (namePrefix && name.startsWith(namePrefix)) ||
- (!deviceName && !namePrefix)
- if (hit) {
- matched.push(d)
- this.emit('deviceFound', d)
- if (!returnAll) { finish(null, d); return }
- }
- }
- }
- const timer = setTimeout(() => {
- if (returnAll) finish(null, matched)
- else if (matched.length) finish(null, matched[0])
- else finish(this._err(BLE_ERROR.SCAN_FAIL, { msg: '扫描超时,未发现目标设备' }))
- }, timeout)
- const finish = (err, data) => {
- if (finished) return // 防重入
- finished = true
- this._cancelScan = null
- clearTimeout(timer)
- uni.offBluetoothDeviceFound && uni.offBluetoothDeviceFound(onFound)
- this._invoke(uni.stopBluetoothDevicesDiscovery, {}).catch(() => {})
- this._setState(BLE_STATE.READY)
- err ? reject(err) : resolve(data)
- }
- // 暴露取消句柄,供 destroy() / 下次 scan() 调用
- this._cancelScan = () => finish(this._err(BLE_ERROR.SCAN_FAIL, { msg: '扫描已取消' }))
- uni.onBluetoothDeviceFound(onFound)
- this._setState(BLE_STATE.SCANNING)
- try {
- await this._invoke(uni.startBluetoothDevicesDiscovery, {
- allowDuplicatesKey: false,
- interval: 0,
- services
- })
- } catch (e) {
- finish(this._err(BLE_ERROR.SCAN_FAIL, e))
- }
- })
- }
- // ============== 连接 ==============
- /**
- * 连接指定设备
- * @param {string} deviceId
- */
- async connect(deviceId) {
- if (!deviceId) throw this._err(BLE_ERROR.CONNECT_FAIL, { msg: 'deviceId 不能为空' })
- await this._ensureReady()
- if (this._state === BLE_STATE.CONNECTING) throw this._err(BLE_ERROR.BUSY)
- this._setState(BLE_STATE.CONNECTING)
- this._device = { deviceId, name: '' }
- try {
- await this._invoke(uni.createBLEConnection, {
- deviceId,
- timeout: this.config.connectTimeout
- })
- this._setState(BLE_STATE.CONNECTED)
- // Android 提升 MTU (忽略失败)
- // #ifdef APP-PLUS
- if (uni.getSystemInfoSync().platform === 'android' && uni.setBLEMTU) {
- try { await this._invoke(uni.setBLEMTU, { deviceId, mtu: 185 }) } catch (_) {}
- }
- // #endif
- await this._discoverAndSubscribe(deviceId)
- this._setState(BLE_STATE.READY_COMM)
- this._reconnectCount = 0
- this.emit('connected', this._device)
- } catch (e) {
- this._setState(BLE_STATE.DISCONNECTED)
- try { await this._invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
- const code = e && (e.errCode || e.code)
- if (code === 10003 || code === -1) {
- throw this._err(BLE_ERROR.CONNECT_TIMEOUT, e)
- }
- throw this._err(BLE_ERROR.CONNECT_FAIL, e)
- }
- }
- /** 扫描 + 连接一步到位 */
- async scanAndConnect(opt) {
- const device = await this.scan(opt)
- await this.connect(device.deviceId)
- this._device.name = device.name || device.localName || ''
- return this._device
- }
- /** 主动断开 */
- async disconnect() {
- this._clearReconnect()
- if (!this._device) return
- const { deviceId } = this._device
- try { await this._invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
- this._device = null
- this._setState(BLE_STATE.DISCONNECTED)
- this.emit('disconnected', { manual: true })
- }
- /** 发现服务并订阅通知 */
- async _discoverAndSubscribe(deviceId) {
- // 1) 服务列表
- const svcRes = await this._invoke(uni.getBLEDeviceServices, { deviceId })
- const services = svcRes.services || []
- const targetSvc = services.find(s => this._uuidEq(s.uuid, this.config.serviceId)) || services.find(s => s.isPrimary) || services[0]
- if (!targetSvc) throw this._err(BLE_ERROR.SERVICE_NOT_FOUND)
- this._serviceId = targetSvc.uuid
- // 2) 特征值
- const charRes = await this._invoke(uni.getBLEDeviceCharacteristics, {
- deviceId, serviceId: this._serviceId
- })
- const chars = charRes.characteristics || []
- const writeChar = chars.find(c => this._uuidEq(c.uuid, this.config.writeCharId))
- || chars.find(c => c.properties && (c.properties.write || c.properties.writeNoResponse || c.properties.writeDefault))
- const notifyChar = chars.find(c => this._uuidEq(c.uuid, this.config.notifyCharId))
- || chars.find(c => c.properties && (c.properties.notify || c.properties.indicate))
- if (!writeChar) throw this._err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到写特征' })
- if (!notifyChar) throw this._err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到通知特征' })
- this._writeCharId = writeChar.uuid
- this._notifyCharId = notifyChar.uuid
- // 3) 开启通知
- await this._invoke(uni.notifyBLECharacteristicValueChange, {
- deviceId,
- serviceId: this._serviceId,
- characteristicId: this._notifyCharId,
- state: true
- })
- }
- _uuidEq(a, b) {
- return String(a || '').toLowerCase() === String(b || '').toLowerCase()
- }
- // ============== 断连处理 & 自动重连 ==============
- _handleDisconnected(reason) {
- if (this._state === BLE_STATE.DISCONNECTED || this._state === BLE_STATE.IDLE) return
- const device = this._device
- this._setState(BLE_STATE.DISCONNECTED)
- this.emit('disconnected', { manual: false, reason })
- this._parser.reset()
- // 尝试自动重连
- this._attemptReconnect(device, reason)
- }
- _attemptReconnect(device, reason) {
- if (!this.config.autoReconnect || !device || this._reconnectCount >= this.config.maxReconnect) {
- if (this._reconnectCount >= this.config.maxReconnect) {
- this.emit('reconnectFailed')
- }
- return
- }
- this._reconnectCount++
- const delay = Math.min(1000 * this._reconnectCount, 5000)
- logger.warn(`reconnect in ${delay}ms (${this._reconnectCount}/${this.config.maxReconnect})`)
- this.emit('reconnecting', { count: this._reconnectCount, max: this.config.maxReconnect })
- this._reconnectTimer = setTimeout(async () => {
- try {
- await this.init()
- await this.connect(device.deviceId)
- this.emit('reconnected', this._device)
- } catch (e) {
- logger.error('reconnect fail', e)
- this._attemptReconnect(device, reason)
- }
- }, delay)
- }
- _clearReconnect() {
- if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null }
- this._reconnectCount = 0
- }
- // ============== 收包 ==============
- _onFrame(decoded, frame) {
- // 结构化: { funcCode, payload, parsed }
- this.emit('frame', { ...decoded, raw: frame })
- if (decoded.parsed) this.emit('report', decoded.parsed)
- // 分类事件, 便于精细订阅
- if (decoded.parsed && decoded.parsed.type) {
- this.emit(`report:${decoded.parsed.type}`, decoded.parsed)
- }
- }
- // ============== 发送 ==============
- /** 低层: 写入任意字节 */
- async writeRaw(u8) {
- if (!this.isConnected) throw this._err(BLE_ERROR.DISCONNECTED)
- const payload = bufferToArrayBuffer(u8)
- // 串行写入, 避免并发写同一特征
- this._writeLock = this._writeLock.then(async () => {
- logger.log('=>', bytesToHex(u8))
- try {
- await this._invoke(uni.writeBLECharacteristicValue, {
- deviceId: this._device.deviceId,
- serviceId: this._serviceId,
- characteristicId: this._writeCharId,
- value: payload
- })
- } catch (e) {
- throw this._err(BLE_ERROR.WRITE_FAIL, e)
- }
- })
- return this._writeLock
- }
- /**
- * 发送任意帧 (不等应用层回复, 只等底层 ack)
- * @param {Uint8Array} frame
- */
- send(frame) { return this.writeRaw(frame) }
- // ============== 高层业务接口 ==============
- /** 下发基本功能指令 (0x01) */
- sendBasic(opt) { return this.writeRaw(encodeBasic(opt)) }
- /** 下发模式参数 1 (步骤 1-7) */
- sendModeParam1(opt) { return this.writeRaw(encodeModeParam1(opt)) }
- /** 下发模式参数 2 (步骤 8-14) */
- sendModeParam2(opt) { return this.writeRaw(encodeModeParam2(opt)) }
- /** 下发穴位坐标 (每包 2 个穴位, 可传任意数量, 内部自动分包) */
- async sendAcupoints(points = []) {
- for (let i = 0; i < points.length; i += 2) {
- const pair = points.slice(i, i + 2)
- await this.writeRaw(encodeAcupoints(pair))
- }
- }
- // ---- 常用快捷方法 ----
- powerOn() { return this.sendBasic({ power: POWER.ON }) }
- powerOff() { return this.sendBasic({ power: POWER.OFF }) }
- startMoxi(opt = {}) { return this.sendBasic({ power: POWER.ON, moxiState: MOXI_STATE.START, ...opt }) }
- pauseMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.PAUSE }) }
- stopMoxi() { return this.sendBasic({ moxiState: MOXI_STATE.DONE }) }
- setMute(on) { return this.sendBasic({ mute: on ? MUTE.ON : MUTE.OFF }) }
- setTemperature(v) { return this.sendBasic({ temperature: v }) }
- setChairAngle(v) { return this.sendBasic({ angle: v }) }
- // ============== 工具 ==============
- /** 封装 uni 回调为 Promise */
- _invoke(apiFn, params) {
- return new Promise((resolve, reject) => {
- apiFn({ ...params, success: resolve, fail: reject, complete: () => {} })
- })
- }
- async _ensureReady() {
- if (this._state === BLE_STATE.IDLE) await this.init()
- // 二次校验
- try {
- const res = await this._invoke(uni.getBluetoothAdapterState, {})
- if (!res.available) throw this._err(BLE_ERROR.ADAPTER_OFF)
- } catch (e) {
- if (e && e.code && String(e.code).startsWith('BLE_')) throw e
- throw this._err(BLE_ERROR.ADAPTER_OFF, e)
- }
- }
- _err(code, origin) {
- const err = new Error(code)
- err.code = code
- if (origin) err.origin = origin
- return err
- }
- }
- export default BleManager
|