| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969 |
- /**
- * 艾灸椅 BLE Pinia Store
- * 真正的应用级单例,跨页面共享蓝牙状态
- *
- * 用法:
- * import { useBleStore } from '@/stores/ble'
- * const bleStore = useBleStore()
- *
- * // 扫描
- * await bleStore.startScan({ timeout: 15000, returnAll: true })
- *
- * // 连接
- * await bleStore.connectDevice(deviceId)
- *
- * // 发送指令
- * await bleStore.sendBasic({ power: 1, moxiState: 1 })
- *
- * // 读取状态(自动响应式)
- * bleStore.linked / bleStore.searching / bleStore.deviceStatus
- */
- import { defineStore } from 'pinia'
- import logger from '@/utils/ble/logger.js'
- import {
- DEFAULT_CONFIG, BLE_STATE, BLE_ERROR,
- POWER, MOXI_STATE, MUTE, MODE, SUB_MODE, TEMPERATURE, CHAIR_ANGLE
- } from '@/utils/ble/constants.js'
- import {
- encodeBasic, encodeModeParam1, encodeModeParam2, encodeAcupoints,
- bufferToArrayBuffer, arrayBufferToU8, bytesToHex, FrameParser
- } from '@/utils/ble/protocol.js'
- import { ensureBlePrerequisite, openLocationSettings } from '@/utils/ble/permission.js'
- // ========== 跨 nvue 页面共享内部变量(通过 globalData 确保单例) ==========
- function _getShared() {
- const app = getApp()
- if (!app.globalData) app.globalData = {}
- if (!app.globalData._bleInternal) {
- app.globalData._bleInternal = {
- instanceId: Date.now() + '_' + Math.random().toString(36).slice(2, 6),
- serviceId: null,
- writeCharId: null,
- notifyCharId: null,
- reconnectTimer: null,
- reconnectCount: 0,
- reconnectGeneration: 0, // 重连代数,用于丢弃僵尸回调
- writeLock: Promise.resolve(),
- bound: false,
- cancelScan: null,
- scanAborted: false, // 标记扫描已被外部中止,防止僵尸扫描
- onDeviceFound: null, // 当前扫描的设备发现回调
- scanStartHistory: [], // 近期 startDiscovery 调用时间戳,用于检测 Android 限流
- intentionalDisconnect: false, // 标记主动断开,防止系统回调误触发重连
- _connectedBeforeScan: false, // 标记扫描前是否处于已连接状态
- parser: null,
- config: { ...DEFAULT_CONFIG }
- }
- console.log(`[BLE Store] 首次创建共享实例,instanceId = ${app.globalData._bleInternal.instanceId}`)
- }
- return app.globalData._bleInternal
- }
- // 兼容模块加载阶段(getApp()可能未就绪),延迟到首次调用时获取
- let _shared = null
- function _S() {
- if (!_shared) _shared = _getShared()
- return _shared
- }
- // 便捷访问
- function _instanceId() { return _S().instanceId }
- // ========== 工具函数 ==========
- function _invoke(apiFn, params) {
- return new Promise((resolve, reject) => {
- apiFn({ ...params, success: resolve, fail: reject, complete: () => {} })
- })
- }
- function _err(code, origin) {
- const err = new Error(code)
- err.code = code
- if (origin) err.origin = origin
- return err
- }
- function _uuidEq(a, b) {
- return String(a || '').toLowerCase() === String(b || '').toLowerCase()
- }
- export const useBleStore = defineStore('ble', {
- state: () => ({
- // ===== 连接状态 =====
- bleState: BLE_STATE.IDLE,
- device: null, // { deviceId, name, RSSI }
- linked: false,
- // ===== 蓝牙开关状态 =====
- bleAdapterOff: false, // 蓝牙适配器未开启,页面可监听此状态展示提示
- // ===== 扫描 =====
- searching: false,
- scannedDevices: [],
- scanThrottled: false, // 检测到 Android 扫描限流时为 true,页面可监听此状态提示用户
- // ===== 设备运行状态(来自上报) =====
- deviceStatus: 0, // 0停止 1预热 2点火 3艾灸 4灭火 5暂停
- hotPercentage: '0%',
- ispreHot: false,
- subTime: '00:00:00',
- modeType: 0, // 0无艾灸 1专业 2自定义 3专家
- chairAngle: 90,
- // ===== 重连 =====
- reconnectDrawer: false,
- reconnectCount: 0,
- // ===== 异常 =====
- excepDrawer: false,
- exceTxt: 0,
- // ===== 耗材状态 =====
- otherSetting: {
- aijiuNum: '0',
- lvxinNum: '0',
- huishouNum: '0'
- }
- }),
- actions: {
- // ============== 配置 ==============
- configure(opt = {}) {
- const s = _S()
- s.config = { ...s.config, ...opt }
- logger.setEnabled(s.config.debug)
- },
- // ============== 内部状态管理 ==============
- _setState(s) {
- if (this.bleState === s) return
- const prevState = this.bleState
- this.bleState = s
- // 进入扫描状态时,如果当前有活跃连接,不应丢失 linked 状态
- if (s === BLE_STATE.SCANNING && prevState === BLE_STATE.READY_COMM) {
- _S()._connectedBeforeScan = true
- // linked 保持 true,仅更新 searching
- } else if (s === BLE_STATE.READY && _S()._connectedBeforeScan && this.device) {
- // 扫描结束恢复连接状态:物理连接仍存活
- _S()._connectedBeforeScan = false
- this.bleState = BLE_STATE.READY_COMM
- this.linked = true
- } else {
- _S()._connectedBeforeScan = false
- this.linked = (s === BLE_STATE.READY_COMM)
- }
- this.searching = (s === BLE_STATE.SCANNING)
- logger.info(`[instanceId=${_instanceId()}] state ->`, s, this.linked ? '(linked)' : '')
- },
- // ============== 初始化 / 释放 ==============
- async init() {
- console.log(`[BLE Store] init() called, instanceId = ${_instanceId()}`)
- if (this.bleState !== BLE_STATE.IDLE && this.bleState !== BLE_STATE.DISCONNECTED) {
- logger.info('蓝牙已初始化,跳过重复初始化')
- return
- }
- try {
- await _invoke(uni.openBluetoothAdapter, {})
- this.bleAdapterOff = false
- } catch (e) {
- logger.error('openBluetoothAdapter fail', e)
- const code = e && (e.errCode || e.code)
- const isOff = code === 10001
- if (isOff) {
- this.bleAdapterOff = true
- this._showBleOffPrompt('请先开启手机蓝牙,才能连接艾灸椅设备')
- }
- throw _err(isOff ? BLE_ERROR.ADAPTER_OFF : BLE_ERROR.NOT_SUPPORT, e)
- }
- this._bindSystemListeners()
- this._setState(BLE_STATE.READY)
- logger.info('蓝牙适配器已初始化')
- },
- async destroy() {
- this._clearReconnect()
- const s = _S()
- if (s.cancelScan) { s.cancelScan(); s.cancelScan = null }
- s.onDeviceFound = null
- try { await this.disconnect() } catch (_) {}
- try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
- this._unbindSystemListeners()
- this._setState(BLE_STATE.IDLE)
- if (s.parser) s.parser.reset()
- },
- /**
- * 轻量级重置:只关闭适配器并重置状态,不解绑全局监听器
- * 适用于页面切换时清除残留扫描/连接状态
- */
- async resetAdapter() {
- const s = _S()
- s.intentionalDisconnect = true
- this._clearReconnect()
- if (s.cancelScan) { s.cancelScan(); s.cancelScan = null }
- s.onDeviceFound = null
- try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
- this._setState(BLE_STATE.IDLE)
- if (s.parser) s.parser.reset()
- s.intentionalDisconnect = false
- },
- /**
- * 绑定BLE系统监听器 - 必须在 App.vue onLaunch 中调用
- * 确保回调绑定到 App.vue 的 taskCenter(永不销毁)
- */
- bindGlobalListeners() {
- if (_S().bound) return
- this._bindSystemListeners()
- },
- _bindSystemListeners() {
- const s = _S()
- if (s.bound) return
- // 初始化帧解析器
- if (!s.parser) {
- s.parser = new FrameParser(
- (decoded, frame) => this._onFrame(decoded, frame),
- (err, frame) => logger.warn('frame parse error', err.message, bytesToHex(frame))
- )
- }
- uni.onBluetoothAdapterStateChange(this._onAdapterStateChange = (res) => {
- logger.info('adapterStateChange', res)
- const s = _S()
- if (!res.available) {
- // 主动关闭适配器时不显示提示且不触发重连
- if (s.intentionalDisconnect) return
- this.bleAdapterOff = true
- this._showBleOffPrompt('蓝牙已关闭,设备连接已断开。请重新开启蓝牙以继续使用')
- this._handleDisconnected(BLE_ERROR.ADAPTER_OFF)
- } else {
- this.bleAdapterOff = false
- // 蓝牙重新开启:如果之前有连接过的设备且当前处于断连状态,自动发起重连
- this._onAdapterRestored()
- }
- })
- 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))
- if (s.parser) s.parser.feed(u8)
- })
- // 永久注册设备发现监听器(不可反复 on/off,否则多次后系统丢失监听)
- uni.onBluetoothDeviceFound((res) => {
- const deviceCount = res && res.devices ? res.devices.length : 0
- console.log(`[BLE Global] onBluetoothDeviceFound 触发, 设备数=${deviceCount}, s.onDeviceFound=${!!s.onDeviceFound}`)
- // 立即在系统回调内转换 advertisData,避免原生 ArrayBuffer 被平台回收
- if (res && res.devices) {
- res.devices.forEach(device => {
- if (device.advertisData && device.advertisData.byteLength > 0) {
- const bytes = new Uint8Array(device.advertisData)
- device._advertisDataHex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
- console.log(`[BLE Scan] 广播数据: ${device._advertisDataHex}`)
- } else {
- device._advertisDataHex = ''
- }
- })
- }
- if (s.onDeviceFound) s.onDeviceFound(res)
- })
- s.bound = true
- logger.info(`[instanceId=${s.instanceId}] BLE系统监听器已全局绑定(含onBluetoothDeviceFound)`)
- },
- _unbindSystemListeners() {
- // 全局监听器不再主动解绑,防止 taskCenter 丢失
- // 仅在极端情况下(如蓝牙完全不再使用)才解绑
- },
- // ============== 确保就绪 ==============
- async _ensureReady() {
- if (this.bleState === BLE_STATE.IDLE) await this.init()
- try {
- const res = await _invoke(uni.getBluetoothAdapterState, {})
- if (!res.available) {
- this.bleAdapterOff = true
- this._showBleOffPrompt('请先开启手机蓝牙,才能连接艾灸椅设备')
- throw _err(BLE_ERROR.ADAPTER_OFF)
- }
- this.bleAdapterOff = false
- } catch (e) {
- if (e && e.code && String(e.code).startsWith('BLE_')) throw e
- throw _err(BLE_ERROR.ADAPTER_OFF, e)
- }
- },
- // ============== 扫描 ==============
- async startScan(opt = {}) {
- const s = _S()
- const platform = uni.getSystemInfoSync().platform
- console.log(`[BLE Scan] ====== startScan 开始 ====== platform=${platform}, instanceId=${s.instanceId}`)
- console.log(`[BLE Scan] 当前状态: bleState=${this.bleState}, searching=${this.searching}, linked=${this.linked}, bound=${s.bound}`)
- // 重置中止标记
- s.scanAborted = false
- this.scanThrottled = false
- // 取消上一次残留扫描
- if (s.cancelScan) {
- console.log('[BLE Scan] 取消上一次残留扫描')
- s.cancelScan(); s.cancelScan = null
- }
- // 停止上一次发现(不使用 await _invoke,因为 iOS 无活跃扫描时不回调会导致卡死)
- console.log('[BLE Scan] 调用 stopBluetoothDevicesDiscovery 清除上一次扫描')
- try {
- uni.stopBluetoothDevicesDiscovery({
- success: () => console.log('[BLE Scan] stopDiscovery 成功'),
- fail: (e) => console.log('[BLE Scan] stopDiscovery 失败(无害):', JSON.stringify(e)),
- complete: () => {}
- })
- } catch (e) {
- console.log('[BLE Scan] stopDiscovery 异常(无害):', e)
- }
- // 给 iOS 一点时间处理 stop
- await new Promise(r => setTimeout(r, 100))
- console.log('[BLE Scan] stopDiscovery 处理完毕,继续...')
- // iOS 专用修复:关闭并重新打开蓝牙适配器以清除 CoreBluetooth 外设缓存
- // iOS 的 CBCentralManager 会缓存已发现的外设,单纯 stop/start 不会清除缓存
- // 导致重新扫描时系统不再上报之前已发现的设备
- // 只有 close + open adapter 才能让 CoreBluetooth 重置外设缓存
- if (platform === 'ios' && !this.linked) {
- console.log('[BLE Scan] [iOS] 开始回收适配器以清除 CoreBluetooth 缓存...')
- s.intentionalDisconnect = true
- s.onDeviceFound = null
- try {
- await _invoke(uni.closeBluetoothAdapter, {})
- console.log('[BLE Scan] [iOS] closeBluetoothAdapter 成功')
- } catch (e) {
- console.log('[BLE Scan] [iOS] closeBluetoothAdapter 失败(无害):', JSON.stringify(e))
- }
- this._setState(BLE_STATE.IDLE)
- s.intentionalDisconnect = false
- // 等待 iOS BLE 栈完全释放资源
- await new Promise(r => setTimeout(r, 300))
- console.log('[BLE Scan] [iOS] 等待 300ms 后重新打开适配器...')
- // 重新打开适配器
- try {
- await _invoke(uni.openBluetoothAdapter, {})
- this.bleAdapterOff = false
- this._setState(BLE_STATE.READY)
- console.log('[BLE Scan] [iOS] openBluetoothAdapter 成功, state=READY')
- } catch (e) {
- console.log('[BLE Scan] [iOS] openBluetoothAdapter 失败:', JSON.stringify(e))
- const code = e && (e.errCode || e.code)
- if (code === 10001) {
- this.bleAdapterOff = true
- throw _err(BLE_ERROR.ADAPTER_OFF, e)
- }
- throw _err(BLE_ERROR.NOT_SUPPORT, e)
- }
- // 再等 200ms 让 adapter 完全就绪
- await new Promise(r => setTimeout(r, 200))
- console.log('[BLE Scan] [iOS] 适配器回收完成,准备开始扫描')
- }
- console.log('[BLE Scan] 调用 _ensureReady...')
- await this._ensureReady()
- console.log('[BLE Scan] _ensureReady 完成, bleState=', this.bleState)
- // 如果在 await 期间页面已卸载并调用了 stopScan,直接中止
- if (s.scanAborted) {
- console.log('[BLE Scan] 扫描已被中止(scanAborted=true),直接返回')
- return opt.returnAll ? [] : null
- }
- const {
- namePrefix = s.config.deviceNamePrefix,
- deviceName,
- services,
- timeout = s.config.scanTimeout,
- returnAll = false
- } = opt
- // 清空上次扫描结果
- this.scannedDevices = []
- console.log(`[BLE Scan] 扫描参数: namePrefix=${namePrefix || '(无)'}, deviceName=${deviceName || '(无)'}, timeout=${timeout}, returnAll=${returnAll}`)
- // Android 限流检测(30秒内最多5次,iOS 无此限制)
- if (platform === 'android') {
- const now = Date.now()
- s.scanStartHistory = s.scanStartHistory.filter(t => now - t < 30000)
- if (s.scanStartHistory.length >= 4) {
- this.scanThrottled = true
- logger.warn(`BLE scan throttle: ${s.scanStartHistory.length + 1} starts in 30s, system may ignore`)
- }
- s.scanStartHistory.push(now)
- }
- const devices = new Map()
- const matched = []
- // iOS 上使用 allowDuplicatesKey=true 确保系统持续上报所有设备
- // 避免 CoreBluetooth 缓存导致重新扫描时不报告已知设备
- const allowDuplicates = (platform === 'ios')
- console.log(`[BLE Scan] allowDuplicatesKey=${allowDuplicates} (iOS=${platform === 'ios'})`)
- return new Promise(async (resolve, reject) => {
- let finished = false
- let foundCount = 0
- // 保存扫描前的连接状态,扫描结束后恢复,避免覆盖 READY_COMM
- const wasConnected = (this.bleState === BLE_STATE.READY_COMM)
- const onFound = (res) => {
- if (finished) return
- for (const d of res.devices) {
- const advertisDataHex = d._advertisDataHex || ''
- const name = d.name || d.localName || ''
- // 重复设备:如果本次带有 advertisData 但之前没有,补充更新
- if (devices.has(d.deviceId)) {
- if (advertisDataHex) {
- const existing = this.scannedDevices.find(item => item.deviceId === d.deviceId)
- if (existing && !existing.advertisData) {
- existing.advertisData = advertisDataHex
- console.log(`[BLE Scan] 补充广播数据: ${name || d.deviceId} -> ${advertisDataHex}`)
- }
- }
- continue
- }
- devices.set(d.deviceId, d)
- foundCount++
- console.log(`[BLE Scan] 发现设备 #${foundCount}: name=${name}, deviceId=${d.deviceId}, RSSI=${d.RSSI}`)
- // advertisData为空的设备跳过,无法区分同名设备
- if (!advertisDataHex) {
- console.log(`[BLE Scan] 跳过无广播数据的设备: ${name || d.deviceId}`)
- continue
- }
- const hit =
- (deviceName && name === deviceName) ||
- (namePrefix && name.startsWith(namePrefix)) ||
- (!deviceName && !namePrefix)
- if (hit) {
- matched.push(d)
- const exists = this.scannedDevices.find(item => item.deviceId === d.deviceId)
- if (!exists) {
- console.log(`[BLE Scan] ✅ 设备匹配并加入列表: ${name || d.deviceId}, 广播数据: ${advertisDataHex}`)
- this.scannedDevices.push({
- deviceId: d.deviceId,
- name: name,
- RSSI: d.RSSI || '',
- advertisData: advertisDataHex
- })
- }
- if (!returnAll) { finish(null, d); return }
- }
- }
- }
- const timer = setTimeout(() => {
- console.log(`[BLE Scan] 扫描超时(${timeout}ms),共发现 ${foundCount} 个设备,匹配 ${matched.length} 个`)
- if (returnAll) finish(null, matched)
- else if (matched.length) finish(null, matched[0])
- else finish(_err(BLE_ERROR.SCAN_FAIL, { msg: '扫描超时,未发现目标设备' }))
- }, timeout)
- const finish = (err, data) => {
- if (finished) return
- finished = true
- console.log(`[BLE Scan] finish 被调用, err=${err ? err.message || err.code : 'null'}, 设备数=${Array.isArray(data) ? data.length : (data ? 1 : 0)}, wasConnected=${wasConnected}`)
- s.cancelScan = null
- s.onDeviceFound = null
- clearTimeout(timer)
- try {
- uni.stopBluetoothDevicesDiscovery({ success() {}, fail() {}, complete() {} })
- } catch (_) {}
- // 如果扫描前是连接状态,恢复为 READY_COMM,不要降级为 READY
- if (wasConnected) {
- this._setState(BLE_STATE.READY_COMM)
- } else {
- this._setState(BLE_STATE.READY)
- }
- this.searching = false
- err ? reject(err) : resolve(data)
- }
- s.cancelScan = () => finish(null, returnAll ? matched : (matched[0] || null))
- // 通过共享回调分发事件(监听器已在 _bindSystemListeners 中永久注册)
- s.onDeviceFound = onFound
- // 如果当前已连接,不调用 _setState 避免覆盖 linked 状态
- if (!wasConnected) {
- this._setState(BLE_STATE.SCANNING)
- } else {
- this.searching = true
- }
- console.log(`[BLE Scan] s.onDeviceFound 已设置, s.bound=${s.bound}, wasConnected=${wasConnected}, 准备调用 startBluetoothDevicesDiscovery`)
- try {
- await _invoke(uni.startBluetoothDevicesDiscovery, {
- allowDuplicatesKey: allowDuplicates,
- interval: 0,
- services
- })
- console.log('[BLE Scan] ✅ startBluetoothDevicesDiscovery 调用成功,等待设备回调...')
- } catch (e) {
- console.log('[BLE Scan] ❌ startBluetoothDevicesDiscovery 失败:', JSON.stringify(e))
- finish(_err(BLE_ERROR.SCAN_FAIL, e))
- }
- })
- },
- /** 停止扫描 */
- stopScan() {
- const s = _S()
- s.scanAborted = true
- s.onDeviceFound = null
- if (s.cancelScan) {
- s.cancelScan()
- s.cancelScan = null
- } else {
- try {
- uni.stopBluetoothDevicesDiscovery({ success() {}, fail() {} })
- } catch (e) {}
- }
- this.searching = false
- this.scanThrottled = false
- if (this.bleState === BLE_STATE.SCANNING) {
- // _setState(READY) 内部会检测 _connectedBeforeScan,
- // 若扫描前有连接则自动恢复为 READY_COMM
- this._setState(BLE_STATE.READY)
- }
- },
- // ============== 连接 ==============
- async connectDevice(deviceId) {
- const s = _S()
- console.log(`[BLE Store] connectDevice() called, instanceId = ${s.instanceId}`)
- if (!deviceId) throw _err(BLE_ERROR.CONNECT_FAIL, { msg: 'deviceId 不能为空' })
- await this._ensureReady()
- if (this.bleState === BLE_STATE.CONNECTING) throw _err(BLE_ERROR.BUSY)
- this._setState(BLE_STATE.CONNECTING)
- this.device = { deviceId, name: '' }
- console.log("连接蓝牙时的设备id",this.device)
- try {
- await _invoke(uni.createBLEConnection, {
- deviceId,
- timeout: s.config.connectTimeout
- })
- this._setState(BLE_STATE.CONNECTED)
- // Android 提升 MTU
- // #ifdef APP-PLUS
- if (uni.getSystemInfoSync().platform === 'android' && uni.setBLEMTU) {
- try { await _invoke(uni.setBLEMTU, { deviceId, mtu: 185 }) } catch (_) {}
- }
- // #endif
- await this._discoverAndSubscribe(deviceId)
- this._setState(BLE_STATE.READY_COMM)
- s.reconnectCount = 0
- } catch (e) {
- console.log("蓝牙连接失败",e)
- this._setState(BLE_STATE.DISCONNECTED)
- this.device = null // 连接失败,清空 device 防止延迟回调误触发重连
- try { await _invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
- const code = e && (e.errCode || e.code)
- if (code === 10003 || code === -1) {
- throw _err(BLE_ERROR.CONNECT_TIMEOUT, e)
- }
- throw _err(BLE_ERROR.CONNECT_FAIL, e)
- }
- },
- /** 扫描 + 连接一步到位(支持 deviceId 直连优先,跳过扫描) */
- async scanAndConnect(opt = {}) {
- const { deviceId: directId, deviceName, advertisData: passedAdvertisData, ...restOpt } = opt
- console.log("连接opt",opt,directId,passedAdvertisData,restOpt,{ ...restOpt })
-
- // 策略:如果已有 deviceId,先尝试直连(不扫描),失败后回退扫描
- if (directId) {
- try {
- logger.info(`directConnect attempt, deviceId=${directId}`)
- await this.connectDevice(directId)
- this.device.name = deviceName || this.device.name || ''
- // 存储 advertisData 作为跨平台设备标识
- if (passedAdvertisData) {
- console.log("连接passedAdvertisData",passedAdvertisData)
- this.device.advertisData = passedAdvertisData
- }
- logger.info('directConnect success, scan skipped')
- return this.device
- } catch (e) {
- logger.warn('directConnect failed, fallback to scan', e.message || e.code)
- // connectDevice 失败时已清空 this.device 并关闭连接,无需额外处理
- }
- }
- // 回退:扫描 + 连接
- const scanOpt = { ...restOpt }
- let device = null
- if (passedAdvertisData) {
- // 有 advertisData 时:不传 deviceName 做名称过滤(后端名与蓝牙广播名可能不一致)
- // 扫描所有设备,再通过 advertisData 精确定位目标
- scanOpt.returnAll = true
- const devices = await this.startScan(scanOpt)
- console.log(`[BLE scanAndConnect] 扫描到 ${devices.length} 个设备,目标 advertisData=${passedAdvertisData}`)
- // 精确匹配 advertisData
- device = devices.find(d => {
- const hex = d._advertisDataHex || d.advertisData || ''
- return hex === passedAdvertisData
- })
- if (!device) {
- throw _err(BLE_ERROR.SCAN_FAIL, { msg: '未找到目标设备(advertisData不匹配)' })
- }
- console.log(`[BLE scanAndConnect] ✅ 通过advertisData匹配到设备: name=${device.name || device.localName}, deviceId=${device.deviceId}`)
- } else {
- // 无 advertisData 时按设备名过滤,返回第一个匹配设备
- if (deviceName) scanOpt.deviceName = deviceName
- device = await this.startScan(scanOpt)
- }
- await this.connectDevice(device.deviceId)
- this.device.name = device.name || device.localName || ''
- console.log("连接device._advertisDataHex",device._advertisDataHex)
- // 存储 advertisData(hex 字符串):优先用传入的(后台 deviceCode),其次用扫描结果的 hex
- // 注意:device.advertisData 是原始 ArrayBuffer,不能直接存储,需使用 _advertisDataHex
- this.device.advertisData = passedAdvertisData || device._advertisDataHex || device.advertisData || ''
- return this.device
- },
- /** 主动断开 */
- async disconnect() {
- const s = _S()
- s.intentionalDisconnect = true
- this._clearReconnect()
- if (!this.device) {
- s.intentionalDisconnect = false
- return
- }
- const { deviceId } = this.device
- // 先置空 device,防止 onBLEConnectionStateChange 回调误匹配
- this.device = null
- this._setState(BLE_STATE.DISCONNECTED)
- try { await _invoke(uni.closeBLEConnection, { deviceId }) } catch (_) {}
- s.intentionalDisconnect = false
- },
- /** 发现服务并订阅通知 */
- async _discoverAndSubscribe(deviceId) {
- const s = _S()
- const svcRes = await _invoke(uni.getBLEDeviceServices, { deviceId })
- const services = svcRes.services || []
- const strict = s.config.strictUUID
- const targetSvc = (strict && services.find(sv => _uuidEq(sv.uuid, s.config.serviceId)))
- || services.find(sv => sv.isPrimary)
- || services[0]
- if (!targetSvc) throw _err(BLE_ERROR.SERVICE_NOT_FOUND)
- s.serviceId = targetSvc.uuid
- const charRes = await _invoke(uni.getBLEDeviceCharacteristics, {
- deviceId, serviceId: s.serviceId
- })
- const chars = charRes.characteristics || []
- const writeChar = (strict && chars.find(c => _uuidEq(c.uuid, s.config.writeCharId)))
- || chars.find(c => c.properties && (c.properties.write || c.properties.writeNoResponse || c.properties.writeDefault))
- const notifyChar = (strict && chars.find(c => _uuidEq(c.uuid, s.config.notifyCharId)))
- || chars.find(c => c.properties && (c.properties.notify || c.properties.indicate))
- if (!writeChar) throw _err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到写特征' })
- if (!notifyChar) throw _err(BLE_ERROR.CHAR_NOT_FOUND, { msg: '未找到通知特征' })
- s.writeCharId = writeChar.uuid
- s.notifyCharId = notifyChar.uuid
- await _invoke(uni.notifyBLECharacteristicValueChange, {
- deviceId,
- serviceId: s.serviceId,
- characteristicId: s.notifyCharId,
- state: true
- })
- },
- // ============== 断连处理 & 自动重连 ==============
- _handleDisconnected(reason) {
- const s = _S()
- // 主动断开/重置时不触发重连
- if (s.intentionalDisconnect) return
- if (this.bleState === BLE_STATE.DISCONNECTED || this.bleState === BLE_STATE.IDLE) return
- const device = this.device
- this._setState(BLE_STATE.DISCONNECTED)
- if (s.parser) s.parser.reset()
- this._attemptReconnect(device, reason)
- },
- _attemptReconnect(device, reason) {
- const s = _S()
- if (!s.config.autoReconnect || !device || s.reconnectCount >= s.config.maxReconnect) {
- if (s.reconnectCount >= s.config.maxReconnect) {
- logger.warn('reconnect max reached, waiting for adapter restore')
- s.reconnectCount = 0
- this.reconnectDrawer = false
- this.reconnectCount = 0
- }
- return
- }
- s.reconnectCount++
- this.reconnectCount = s.reconnectCount
- this.reconnectDrawer = true
- const delay = Math.min(1000 * s.reconnectCount, 5000)
- logger.warn(`reconnect in ${delay}ms (${s.reconnectCount}/${s.config.maxReconnect})`)
- const gen = s.reconnectGeneration
- s.reconnectTimer = setTimeout(async () => {
- if (s.reconnectGeneration !== gen) {
- logger.info('reconnect callback aborted (generation mismatch)')
- return
- }
- try {
- // 先关闭适配器清除脏状态(加标志防止回调干扰)
- s.intentionalDisconnect = true
- try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
- s.intentionalDisconnect = false
- this._setState(BLE_STATE.IDLE)
- if (s.reconnectGeneration !== gen) return
- // 优先使用 deviceId 直连,避免不必要的扫描触发 Android 限流
- const scanOpt = { timeout: 8000 }
- if (device.deviceId) scanOpt.deviceId = device.deviceId
- if (device.name) scanOpt.deviceName = device.name
- await this.scanAndConnect(scanOpt)
- if (s.reconnectGeneration !== gen) return
- this.reconnectDrawer = false
- this.reconnectCount = 0
- logger.info('reconnect success')
- } catch (e) {
- if (s.reconnectGeneration !== gen) return
- logger.error('reconnect fail', e)
- this._attemptReconnect(device, reason)
- }
- }, delay)
- },
- _clearReconnect() {
- const s = _S()
- if (s.reconnectTimer) { clearTimeout(s.reconnectTimer); s.reconnectTimer = null }
- s.reconnectCount = 0
- s.reconnectGeneration++ // 递增代数,使正在执行的僵尸回调自动失效
- this.reconnectCount = 0
- this.reconnectDrawer = false
- },
- /**
- * 蓝牙适配器从关闭恢复为开启时调用
- * 如果之前有连接过的设备且当前处于断连状态,自动扫描并重新连接
- */
- _onAdapterRestored() {
- const device = this.device
- if (!device || !device.deviceId) return
- if (this.bleState !== BLE_STATE.DISCONNECTED && this.bleState !== BLE_STATE.IDLE) return
- logger.info('adapter restored, auto reconnecting to', device.name || device.deviceId)
- // 重置重连计数,发起新一轮重连
- this._clearReconnect()
- this._setState(BLE_STATE.IDLE)
- // 延迟 1.5s 等待适配器完全就绪(Android 蓝牙状态延迟)
- const s = _S()
- const gen = s.reconnectGeneration
- this.reconnectDrawer = true
- this.reconnectCount = 1
- s.reconnectTimer = setTimeout(async () => {
- if (s.reconnectGeneration !== gen) return
- try {
- // 关闭旧适配器,确保重新打开时状态干净
- s.intentionalDisconnect = true
- try { await _invoke(uni.closeBluetoothAdapter, {}) } catch (_) {}
- s.intentionalDisconnect = false
- if (s.reconnectGeneration !== gen) return
- // 优先直连,避免扫描触发限流
- const scanOpt = { timeout: 8000 }
- if (device.deviceId) scanOpt.deviceId = device.deviceId
- if (device.name) scanOpt.deviceName = device.name
- await this.scanAndConnect(scanOpt)
- if (s.reconnectGeneration !== gen) return
- this.reconnectDrawer = false
- this.reconnectCount = 0
- logger.info('adapter restore reconnect success')
- } catch (e) {
- if (s.reconnectGeneration !== gen) return
- logger.error('adapter restore reconnect fail', e)
- // 失败后进入常规重连流程(还有2次机会)
- s.reconnectCount = 1
- this._attemptReconnect(device, BLE_ERROR.ADAPTER_OFF)
- }
- }, 1500)
- },
- // ============== 收包处理 ==============
- _onFrame(decoded, frame) {
- console.log("获取到消息",decoded)
- logger.info('frame received', decoded.funcCode)
- if (decoded.parsed) {
- if (decoded.parsed.type === 'GROUP_1') {
- this._handleGroup1(decoded.parsed)
- } else if (decoded.parsed.type === 'GROUP_2') {
- this._handleGroup2(decoded.parsed)
- }
- }
- },
- /** 解析参数组1 */
- _handleGroup1(data) {
- // 设备运行状态
- switch (data.runtimeState) {
- case 0x00: this.deviceStatus = 0; break
- case 0x01:
- case 0x02: this.deviceStatus = 1; break
- case 0x03: this.deviceStatus = 3; break
- case 0x04: this.deviceStatus = 4; break
- case 0x05: this.deviceStatus = 5; break
- }
- // 模式
- switch (data.mode) {
- case MODE.LEISURE: this.modeType = 0; break
- case MODE.PROFESSIONAL: this.modeType = 1; break
- case MODE.PERSONAL: this.modeType = 2; break
- case MODE.EXPERT: this.modeType = 3; break
- }
- // 剩余时间
- const m = data.remainMinute || 0
- const s = data.remainSecond || 0
- const h = Math.floor(m / 60)
- const m1 = m % 60
- this.subTime = `${h.toString().padStart(2, '0')}:${m1.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
- // 异常检测
- if (data.foreignDetect === 0x02) {
- this.excepDrawer = true
- this.exceTxt = 1
- }
- // 耗材状态
- this.otherSetting.aijiuNum = String(data.consumable || 0)
- this.otherSetting.lvxinNum = String(data.filterPercent || 0)
- this.otherSetting.huishouNum = String(data.recycleBinPct || 0)
- },
- /** 解析参数组2 */
- _handleGroup2(data) {
- if (data.preheatPercent < 100) {
- this.ispreHot = false
- this.hotPercentage = data.preheatPercent + '%'
- } else {
- this.ispreHot = true
- this.hotPercentage = data.ignitePercent + '%'
- }
- // 椅子角度
- if (data.chairAngle) {
- const angleMap = {
- 1: 90, 2: 105, 3: 120, 4: 135, 5: 150
- }
- if (angleMap[data.chairAngle]) {
- this.chairAngle = angleMap[data.chairAngle]
- }
- }
- },
- // ============== 发送指令 ==============
- async writeRaw(u8) {
- if (!this.linked) throw _err(BLE_ERROR.DISCONNECTED)
- const s = _S()
- const payload = bufferToArrayBuffer(u8)
- s.writeLock = s.writeLock.then(async () => {
- logger.log('=>', bytesToHex(u8))
- logger.log('write params:', {
- deviceId: this.device.deviceId,
- serviceId: s.serviceId,
- characteristicId: s.writeCharId,
- valueByteLength: payload.byteLength
- })
- try {
- await _invoke(uni.writeBLECharacteristicValue, {
- deviceId: this.device.deviceId,
- serviceId: s.serviceId,
- characteristicId: s.writeCharId,
- value: payload
- })
- } catch (e) {
- logger.error('writeBLE origin error:', JSON.stringify(e))
- throw _err(BLE_ERROR.WRITE_FAIL, e)
- }
- })
- return s.writeLock
- },
- /** 下发基本功能指令 (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)) },
- /** 下发穴位坐标 */
- 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 }) },
- // ============== 蓝牙关闭提示 ==============
- /**
- * 弹窗提示用户蓝牙未开启,引导用户前往设置开启
- * 内部做防抖,避免短时间内重复弹窗
- */
- _showBleOffPrompt(message) {
- const s = _S()
- // 弹窗正在显示中,不重复弹出
- if (s._bleOffPromptShowing) return
- s._bleOffPromptShowing = true
- uni.showModal({
- title: '蓝牙未开启',
- content: message || '请开启手机蓝牙后重试',
- confirmText: '去设置',
- cancelText: '取消',
- success: (res) => {
- s._bleOffPromptShowing = false
- if (res.confirm) {
- // #ifdef APP-PLUS
- const platform = uni.getSystemInfoSync().platform
- if (platform === 'android') {
- try {
- const main = plus.android.runtimeMainActivity()
- const Intent = plus.android.importClass('android.content.Intent')
- const Settings = plus.android.importClass('android.provider.Settings')
- const intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
- main.startActivity(intent)
- } catch (e) {
- logger.error('跳转蓝牙设置失败', e)
- }
- } else if (platform === 'ios') {
- // iOS 可以打开 App 设置页
- plus.runtime.openURL('App-Prefs:root=Bluetooth')
- }
- // #endif
- }
- }
- })
- }
- }
- })
|