/** * 蓝牙 BLE 采集服务 */ const hex = require('../utils/hex.js'); // BLE 状态常量 const BLE_STATE = { DISCONNECTED: 'disconnected', CONNECTING: 'connecting', CONNECTED: 'connected', DISCOVERING: 'discovering', READY: 'ready', }; // 默认配置 const DEFAULT_CONFIG = { // 扫描超时时间(毫秒) scanTimeout: 10000, // 连接超时时间(毫秒) connectTimeout: 10000, // 主动拆链后等待系统断链回调的时间(毫秒,超时兜底) // native 断链是异步的,固定短延时会造成"以为断了其实没断"→ 随即建链又拿 already connect closeWaitTimeout: 1500, // 单个 GATT 操作(发现服务/特征/启用通知/设 MTU)的回调超时(毫秒) // native 回调不返回时不能让状态机永久卡住(会连带卡死后续所有重连) opTimeout: 5000, // 适配器级操作(open/closeBluetoothAdapter)的回调超时(毫秒) // 与 GATT 操作分开:这两个回调丢失的后果最重 —— 复位流程永不结算会把 // 网关的串行队列一起锁死,之后所有启停/重连都排队不执行("怎么点都连不上") adapterOpTimeout: 5000, // 连续建链失败多少次后复位蓝牙适配器(清掉 native 残留的半开连接) adapterResetAfter: 3, // 默认服务 UUID(需根据实际设备配置) serviceUUIDs: [], // 调试日志开关(高频数据禁止逐包打印,否则阻塞 BLE 接收) debug: false, // 掉线自动重连(工业场景建议开启) autoReconnect: true, }; class BleService { constructor() { this.state = BLE_STATE.DISCONNECTED; this.deviceId = null; this.serviceId = null; this.notifyCharacteristicId = null; this.writeCharacteristicId = null; this.onDataCallback = null; this.onStateChangeCallback = null; this.config = { ...DEFAULT_CONFIG }; // 监听器注册标志(防止叠加) this._listenersRegistered = false; // 适配器是否已开启 this._openedAdapter = false; // 收包计数/最近收包时间(诊断、静默探测用) this._recvCount = 0; this._lastRecvAt = 0; // 当前会话已启用 notify 的特征列表 this._notifyCharacteristicIds = []; // 自动重连相关 this._lastDeviceId = null; this._lastServiceId = null; this._reconnectTimer = null; this._reconnectAttempts = 0; this._manuallyDisconnected = false; // 并发控制:连接流程在同一时刻只允许一个,同设备在途连接复用同一 Promise // (并发发起 createBLEConnection 会拿到 fail:already connect,是历史报错根源) this._connectPromise = null; this._connectingDeviceId = null; // 流程序号:自增即作废此前所有在途流程(切设备/主动断开时用),防止旧流程写脏会话状态 this._connectSeq = 0; // 主动拆链时等待系统断链回调的唤醒器:deviceId -> resolve this._disconnectWaiters = new Map(); // 连续建链失败计数(达到阈值复位适配器,用于清掉 native 半开连接) this._consecutiveCreateFails = 0; // 系统蓝牙可用性(onBluetoothAdapterStateChange 去抖用:部分平台注册时会回放当前状态) this._adapterAvailable = true; } /** * 设置配置 * @param {Object} config */ setConfig(config) { this.config = { ...DEFAULT_CONFIG, ...config }; } /** * 设置数据回调 * @param {Function} callback - (data: {hex, buffer}) => void */ onData(callback) { this.onDataCallback = callback; } /** * 设置状态变化回调 * @param {Function} callback - (state) => void */ onStateChange(callback) { this.onStateChangeCallback = callback; } /** * 更新状态并通知 * @param {string} state */ _setState(state) { this.state = state; if (this.onStateChangeCallback) { this.onStateChangeCallback(state); } console.log('[BLE] 状态变更:', state); } /** * 初始化蓝牙适配器(幂等) * wx 级全局监听只注册一次,不再每次 off/on 抖动,避免多页/重连切换时的丢包窗口。 * @returns {Promise} */ initBluetooth() { return new Promise((resolve, reject) => { // 已开启且监听已挂:直接复用 if (this._openedAdapter && this._listenersRegistered) { resolve(); return; } // 已开适配器但监听未挂(异常恢复路径),补挂监听即可 const registerListeners = () => { if (this._listenersRegistered) return; wx.onBLEConnectionStateChange((res) => { console.log('[BLE] 连接状态变化:', res); if (res.connected) return; // 唤醒"拆链后重建"的等待方:等系统确认断开,替代不可靠的固定延时 this._settleDisconnectWait(res.deviceId); const wasConnected = this.state === BLE_STATE.READY || this.state === BLE_STATE.CONNECTED; this._setState(BLE_STATE.DISCONNECTED); // 保留 deviceId 供重连使用(不再置空),掉线自动重连。 // 已有连接流程在途时不重排(该流程失败会自行排重连),避免并发 createBLEConnection if (wasConnected && this.config.autoReconnect && !this._manuallyDisconnected && !this._connectPromise && this._lastDeviceId) { this._scheduleReconnect(); } }); wx.onBLECharacteristicValueChange((res) => { // 只处理当前会话设备的值变化,防止切换设备后旧连接/残留连接串包 if (this.deviceId && res.deviceId && res.deviceId !== this.deviceId) { return; } const hexData = hex.arrayBufferToHex(res.value); this._recvCount++; this._lastRecvAt = Date.now(); // 首包 + 每 20 包打印摘要,debug 时逐包打印,避免高频逐包日志阻塞接收 if (this.config.debug || this._recvCount === 1 || this._recvCount % 20 === 0) { console.log(`[BLE] 收到数据 #${this._recvCount} (${hexData.length / 2}字节):`, hexData); } if (this.onDataCallback) { this.onDataCallback({ hex: hexData, buffer: res.value, deviceId: res.deviceId, serviceId: res.serviceId, characteristicId: res.characteristicId, }); } }); // 系统层蓝牙开关变化:必须跟踪,否则 native 已断开全部链路而本进程 // 仍停在 ready 的假象上(状态显示"已连接"却永远收不到数据,也无法自愈) wx.onBluetoothAdapterStateChange((res) => { console.log('[BLE] 适配器状态变化:', res); const available = res.available !== false; if (available === this._adapterAvailable) return; // 去抖:注册时可能回放当前状态 this._adapterAvailable = available; if (!available) { this._invalidateSession('系统蓝牙已关闭'); return; } // 重新打开蓝牙:native 侧适配器已被系统作废,不能假设自己还开着 —— // 置 false 让下一次 initBluetooth 真正 open 一次,并立刻补一次连接 this._openedAdapter = false; if (this.config.autoReconnect && !this._manuallyDisconnected && this._lastDeviceId) { this._clearReconnectTimer(); this._reconnectAttempts = 0; this._scheduleReconnect(); } }); this._listenersRegistered = true; console.log('[BLE] 已注册全局监听'); }; if (this._openedAdapter) { registerListeners(); resolve(); return; } // native 不回调时必须超时作废:否则 _openedAdapter 停在 false、 // 上层的连接闸门也永不释放,之后所有重连都会被排队卡死 let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; console.error('[BLE] 初始化适配器回调超时'); reject(new Error('蓝牙初始化超时')); }, this.config.adapterOpTimeout); const finish = (fn, arg) => { if (settled) return; settled = true; clearTimeout(timer); fn(arg); }; wx.openBluetoothAdapter({ success: () => { console.log('[BLE] 初始化成功'); this._openedAdapter = true; registerListeners(); finish(resolve); }, fail: (err) => { const msg = (err && err.errMsg) || ''; // 部分平台适配器已开启时 open 会报 already 类错误,视为成功 if (/already/i.test(msg)) { console.log('[BLE] 适配器已开启(忽略 already)'); this._openedAdapter = true; registerListeners(); finish(resolve); } else { console.error('[BLE] 初始化失败:', err); finish(reject, new Error('蓝牙初始化失败: ' + (msg || '未知错误'))); } }, }); }); } /** * 获取平台合适的 MTU 值 * @returns {number} */ _getMtu() { try { const platform = wx.getSystemInfoSync().platform; // iOS 最大 185,超出会协商失败;Android 建议 247 return platform === 'ios' ? 185 : 247; } catch (e) { return 247; } } /** * 扫描蓝牙设备 * @param {Array} serviceUUIDs - 服务 UUID 列表(可选) * @param {number} timeout - 超时时间(毫秒) * @returns {Promise} 设备列表 */ scanDevices(serviceUUIDs = null, timeout = null) { return new Promise((resolve, reject) => { const uuids = serviceUUIDs || this.config.serviceUUIDs; const scanTimeout = timeout || this.config.scanTimeout; const devices = []; const deviceMap = new Map(); // 移除旧的扫描监听器,防止叠加 try { wx.offBluetoothDeviceFound(); } catch (e) { // 忽略移除失败 } // 开始扫描 wx.startBluetoothDevicesDiscovery({ services: uuids.length > 0 ? uuids : undefined, allowDuplicatesKey: false, success: () => { console.log('[BLE] 开始扫描, 超时:', scanTimeout); // 监听扫描结果 wx.onBluetoothDeviceFound((res) => { res.devices.forEach((device) => { // 去重 if (!deviceMap.has(device.deviceId)) { deviceMap.set(device.deviceId, true); devices.push({ deviceId: device.deviceId, name: device.name || '未知设备', localName: device.localName, advertisData: device.advertisData, RSSI: device.RSSI, }); } }); }); // 设置超时 setTimeout(() => { wx.stopBluetoothDevicesDiscovery({ success: () => { console.log('[BLE] 扫描完成, 发现设备:', devices.length); // 移除扫描监听器 try { wx.offBluetoothDeviceFound(); } catch (e) { // 忽略 } resolve(devices); }, fail: () => { try { wx.offBluetoothDeviceFound(); } catch (e) { // 忽略 } resolve(devices); }, }); }, scanTimeout); }, fail: (err) => { console.error('[BLE] 扫描失败:', err); reject(new Error('扫描失败: ' + (err.errMsg || '未知错误'))); }, }); }); } /** * 停止扫描 */ stopScan() { return new Promise((resolve) => { wx.stopBluetoothDevicesDiscovery({ success: () => { console.log('[BLE] 已停止扫描'); resolve(); }, fail: () => { resolve(); }, }); }); } /** * 连接设备(幂等 + 并发安全) * * 依据微信 BLE 的三条硬约束设计: * 1) 同一设备重复发起会返回 fail:already connect —— 这是"链路已存在"而非失败, * 必须视为连接成功直接复用;拆链重建只会引入竞态(native 断链是异步的)。 * 2) 并发的第二个 createBLEConnection 必然拿到 already connect —— * 因此同一设备在途连接复用同一 Promise,绝不并发发起。 * 3) 切换设备时旧流程必须作废 —— 用自增 seq 作废在途流程,防止旧流程写脏 serviceId/状态。 * * @param {string} deviceId - 设备 ID * @param {string} serviceId - 服务 UUID(可选,不传则自动发现) * @returns {Promise} */ connectDevice(deviceId, serviceId = null) { if (!deviceId) { return Promise.reject(new Error('设备ID为空')); } // 同设备已有在途连接:复用,不再发起第二次(并发发起是 already connect 的根源) if (this._connectPromise && this._connectingDeviceId === deviceId) { console.log('[BLE] 连接已在进行中,复用本次连接:', deviceId); return this._connectPromise; } // 记录连接目标,供自动重连使用 this.deviceId = deviceId; this._lastDeviceId = deviceId; this._lastServiceId = serviceId; this._manuallyDisconnected = false; this._clearReconnectTimer(); this._setState(BLE_STATE.CONNECTING); const seq = ++this._connectSeq; // 作废此前所有在途流程 this._connectingDeviceId = deviceId; const promise = this._connectFlow(deviceId, serviceId, seq).catch((err) => { // 主动断开/切设备导致的作废:不算失败,不上屏 if (err && err.cancelled) throw err; // 失败详情在各自重试循环里记录(_scheduleReconnect / forceReconnect), // connectDevice 只负责"一次尝试"的语义,重试策略交给调用方 if (this.deviceId === deviceId) this._setState(BLE_STATE.DISCONNECTED); throw err; }); this._connectPromise = promise; const clear = () => { if (this._connectPromise === promise) { this._connectPromise = null; this._connectingDeviceId = null; } }; promise.then(clear, clear); return promise; } /** * 连接主流程:建链 → 设 MTU → 发现服务/启用 notify * @param {string} deviceId * @param {string} serviceId * @param {number} seq - 流程序号(作废判定) */ async _connectFlow(deviceId, serviceId, seq) { this._assertCurrent(seq); // 建链的前置条件:适配器必须是打开的。冷启动由 gateway 先开,但自动重连 / // 硬重连是绕过 gateway 启动流程直接进来的,期间适配器可能已被系统作废 // (关蓝牙、复位失败等),不在这里兜住就会一直以 not init(10001) 失败。 // initBluetooth 幂等:已打开且监听已挂时立即返回,不产生额外 native 调用。 await this.initBluetooth(); this._assertCurrent(seq); let reused = false; try { await this._createConnection(deviceId, seq); } catch (err) { const errMsg = (err && err.errMsg) || ''; if (!/already connect/i.test(errMsg)) { // 已被作废的流程(用户停用设备 / 切设备 / 主动断开)不得升级成"复位适配器": // 复位是全局破坏性操作(拆掉所有链路 + 关开适配器),替一个已经没人要的连接 // 去复位,正是"设备关了还在反复复位蓝牙"的来源。作废即抛 cancelled, // 上层按"不算失败"处理(不上屏、不排重试) this._assertCurrent(seq); await this._escalateCreateFailure(); throw new Error('连接失败: ' + (errMsg || err.message || '未知错误')); } // 系统层链路已存在(本机其它调用、上次会话残留):视为连接成功,直接复用该链路 reused = true; this._consecutiveCreateFails = 0; console.warn('[BLE] 系统层已存在连接,复用现有链路:', deviceId); } this._assertCurrent(seq); await this._setMtu(deviceId); this._assertLinkFresh(seq, deviceId); try { return await this._discoverServices(deviceId, serviceId, seq); } catch (err) { // 仅当复用到的是"僵尸链路"(服务表取不到)才做一次干净重建; // 新建链路仍失败说明是真实错误,流程已作废也不必再折腾 if (!reused || (err && err.cancelled)) throw err; console.warn('[BLE] 复用链路不可用,重建:', err && err.message); } await this._rebuildConnection(deviceId, seq); this._assertLinkFresh(seq, deviceId); await this._setMtu(deviceId); return this._discoverServices(deviceId, serviceId, seq); } /** * 建链失败升级:连续失败(尤其"连接超时")会让 native 栈停在半开连接, * 其后 createBLEConnection 会反复 already connect / 超时,连"关开关重开"都救不回来。 * 达到阈值即复位适配器清掉残留连接,把状态机从死循环里拉出来。 */ async _escalateCreateFailure() { this._consecutiveCreateFails++; if (this._consecutiveCreateFails < this.config.adapterResetAfter) return; this._consecutiveCreateFails = 0; console.warn(`[BLE] 连续 ${this.config.adapterResetAfter} 次建链失败,复位适配器`); try { await this.resetAdapter(); } catch (err) { console.error('[BLE] 适配器复位失败:', err && err.message); } } /** * 包装 wx 异步 API:加回调超时兜底 * native 回调不返回时,Promise 会永久 pending,把在途连接闸门(_connectPromise)卡死, * 表现就是"之后怎么点都连不上" * @param {Function} executor - (resolve, reject) => void,内部调用 wx API * @param {string} what - 操作名(日志与错误文案) * @param {number} [timeout] - 覆盖默认 opTimeout(适配器级操作用自己的超时) * @returns {Promise} */ _callWithTimeout(executor, what, timeout = this.config.opTimeout) { return new Promise((resolve, reject) => { let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; console.warn(`[BLE] ${what}回调超时:`, what); reject(new Error(`${what}超时`)); }, timeout); const wrap = (fn) => (arg) => { if (settled) return; settled = true; clearTimeout(timer); fn(arg); }; executor(wrap(resolve), wrap(reject)); }); } /** * 发起 createBLEConnection(带超时;超时后清理可能已建立的链路) * fail 原样 reject,交由上层判读 already connect * @returns {Promise} */ _createConnection(deviceId, seq) { return new Promise((resolve, reject) => { let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; console.warn('[BLE] 连接超时:', deviceId); // 超时不代表没连上:native 可能仍在连接中,先拆链清掉可能已建立的链路 wx.closeBLEConnection({ deviceId, complete: () => {} }); const err = new Error('连接超时'); err.timeout = true; reject(err); }, this.config.connectTimeout); wx.createBLEConnection({ deviceId, success: () => { if (settled) return; settled = true; clearTimeout(timer); console.log('[BLE] 连接成功:', deviceId); if (seq === this._connectSeq) { this._reconnectAttempts = 0; this._consecutiveCreateFails = 0; } resolve(); }, fail: (err) => { if (settled) return; settled = true; clearTimeout(timer); reject(err); }, }); }); } /** * 设置 MTU(提高传输效率,iOS 最大 185,Android 建议 247) * 失败不影响连接,故用 complete 结算;回调超时同样忽略 * @returns {Promise} */ _setMtu(deviceId) { return this._callWithTimeout( (resolve, reject) => wx.setBLEMTU({ deviceId, mtu: this._getMtu(), complete: resolve, fail: reject, }), '设置MTU' ).catch(() => { /* MTU 非关键路径,失败照常继续 */ }); } /** * 干净重建链路:先拆链并等系统确认断开,再建链 * 只用于"复用到的链路不可用"这一条路径,且每次连接流程最多一次 */ async _rebuildConnection(deviceId, seq) { await this._closeConnection(deviceId); this._assertCurrent(seq); try { await this._createConnection(deviceId, seq); } catch (err) { const errMsg = (err && err.errMsg) || ''; // 断链尚未走完时会再次 already connect:链路已在,继续用即可 if (/already connect/i.test(errMsg)) { this._consecutiveCreateFails = 0; return; } // 同 _connectFlow:作废的流程不得复位适配器(那是全局破坏性操作) this._assertCurrent(seq); await this._escalateCreateFailure(); throw new Error('重连失败: ' + (errMsg || err.message || '未知错误')); } } /** * 主动断开并等待系统断链回调(最多 closeWaitTimeout 兜底) * @returns {Promise} */ async _closeConnection(deviceId) { const wait = new Promise((resolve) => { this._disconnectWaiters.set(deviceId, resolve); setTimeout(() => { // 超时兜底:不再无限等下去 if (this._disconnectWaiters.get(deviceId) === resolve) { this._disconnectWaiters.delete(deviceId); console.warn('[BLE] 等待断链回调超时,按已断开处理:', deviceId); resolve(); } }, this.config.closeWaitTimeout); }); wx.closeBLEConnection({ deviceId, fail: () => { // 本就未连接:没有断链事件可等,立即结算 this._settleDisconnectWait(deviceId); }, }); await wait; } /** 唤醒等待断链的结算器 */ _settleDisconnectWait(deviceId) { const resolve = this._disconnectWaiters.get(deviceId); if (resolve) { this._disconnectWaiters.delete(deviceId); resolve(); } } /** * 唤醒所有等待断链的结算器 * native 层整片失效/复位时必须主动唤醒,否则调用方只能干等自己的超时兜底 */ _settleAllDisconnectWaits() { const waiters = Array.from(this._disconnectWaiters.values()); this._disconnectWaiters.clear(); waiters.forEach((resolve) => resolve()); } /** * 作废当前会话(native 层已不可用时的统一出口) * * 用于"系统蓝牙被关掉"这类场景:native 已断开全部链路,本进程若继续停在 * ready 就是假象(状态显示"已连接"却永远收不到数据,且没有任何回调能纠正它)。 * 只归零会话,**不触碰 `_manuallyDisconnected`** —— 那是"用户主动断开链路"的语义, * 保留它才能让自动重连在适配器恢复后自然接上。 * @param {string} reason - 日志原因 */ _invalidateSession(reason) { console.warn('[BLE] 作废会话:', reason); this._openedAdapter = false; // 适配器已失效,下次 initBluetooth 会重开 this._connectSeq++; // 作废所有在途流程 this._connectPromise = null; this._connectingDeviceId = null; this._clearReconnectTimer(); this._settleAllDisconnectWaits(); this.serviceId = null; this.notifyCharacteristicId = null; this.writeCharacteristicId = null; this._notifyCharacteristicIds = []; this._lastRecvAt = 0; if (this.state !== BLE_STATE.DISCONNECTED) { this._setState(BLE_STATE.DISCONNECTED); } // 适配器可能只是被临时关掉:按退避重排重连,用户重新打开蓝牙后自然接上 if (this.config.autoReconnect && !this._manuallyDisconnected && this._lastDeviceId) { this._scheduleReconnect(); } } /** * 流程是否仍然有效(未被新连接流程取代、未被主动断开) * @param {number} seq * @returns {boolean} */ _isCurrent(seq) { return seq === this._connectSeq && !this._manuallyDisconnected; } /** 作废判定:失效则抛出带 cancelled 标记的错误(上层不报错、不重排) */ _assertCurrent(seq) { if (this._isCurrent(seq)) return; throw this._cancelError('连接流程已作废'); } /** * 作废判定 + 链路清理:建链成功后流程若被作废,该链路已无人管理,需要主动关掉, * 否则会残留成下一次连接时"already connect"的僵尸链路 */ _assertLinkFresh(seq, deviceId) { if (this._isCurrent(seq)) return; this._discardLink(deviceId); throw this._cancelError('连接流程已作废'); } /** * 丢弃非当前目标的链路(fire-and-forget) * 若该设备仍是当前目标且非用户主动断开,则保留给新流程复用,不做无谓拆链 */ _discardLink(deviceId) { if (deviceId === this._lastDeviceId && !this._manuallyDisconnected) return; console.log('[BLE] 丢弃作废流程的链路:', deviceId); wx.closeBLEConnection({ deviceId, complete: () => {} }); } _cancelError(message) { const err = new Error(message); err.cancelled = true; return err; } /** * 发现服务和特征值 * @param {string} deviceId * @param {string} serviceId * @param {number} seq * @returns {Promise} */ _discoverServices(deviceId, serviceId, seq) { this._setState(BLE_STATE.DISCOVERING); const fetchServices = this._callWithTimeout( (resolve, reject) => wx.getBLEDeviceServices({ deviceId, success: resolve, fail: reject }), '获取服务' ); return fetchServices .then((res) => { if (!this._isCurrent(seq)) throw this._cancelError('连接流程已作废'); console.log('[BLE] 发现服务:', res.services); const services = res.services; // 选择目标服务 let targetService = null; if (serviceId) { // 指定了服务 UUID targetService = services.find(s => s.uuid.toLowerCase().includes(serviceId.toLowerCase())); } else { // 自动选择:排除通用服务(GAP 0x1800, GATT 0x1801 等) const GENERIC_SERVICE_PREFIXES = ['00001800', '00001801', '0000180a', '0000180f']; targetService = services.find(s => !GENERIC_SERVICE_PREFIXES.some(p => s.uuid.toLowerCase().startsWith(p)) ); // 如果没有非通用服务,选择第一个主服务 if (!targetService) { targetService = services.find(s => s.isPrimary) || services[0]; } } if (!targetService) { throw new Error('未找到目标服务'); } return targetService.uuid; }) .catch((err) => { if (err && err.cancelled) throw err; console.error('[BLE] 获取服务失败:', err && err.message); throw new Error('获取服务失败: ' + ((err && err.message) || '未知错误')); }) .then((uuid) => { this.serviceId = uuid; console.log('[BLE] 目标服务:', this.serviceId); return this._discoverCharacteristics(deviceId, uuid, seq); }); } /** * 发现特征值 * 目标服务下所有 notify/indicate 特征都尝试启用(数据特征未必是第一个 notify 特征), * 单个特征启用失败仅告警跳过,至少一个成功即视为就绪。 * @param {string} deviceId * @param {string} serviceId * @param {number} seq * @returns {Promise} */ _discoverCharacteristics(deviceId, serviceId, seq) { return this._callWithTimeout( (resolve, reject) => wx.getBLEDeviceCharacteristics({ deviceId, serviceId, success: resolve, fail: reject, }), '获取特征值' ) .then((res) => { if (!this._isCurrent(seq)) throw this._cancelError('连接流程已作废'); console.log('[BLE] 发现特征值:', res.characteristics.map(c => ({ uuid: c.uuid, properties: c.properties, }))); const characteristics = res.characteristics; const notifyChars = characteristics.filter(c => c.properties.notify || c.properties.indicate); const writeChar = characteristics.find(c => c.properties.write || c.properties.writeNoResponse); if (writeChar) { this.writeCharacteristicId = writeChar.uuid; console.log('[BLE] 写入特征值:', this.writeCharacteristicId); } if (notifyChars.length === 0) { // 没有通知特征值,但可能有写入特征值 console.warn('[BLE] ⚠️ 未找到 notify/indicate 特征值!设备将无法主动上报数据,只能被动写入。请核对设备的 GATT 服务/特征值。'); this._notifyCharacteristicIds = []; this.notifyCharacteristicId = null; this._lastRecvAt = Date.now(); this._setState(BLE_STATE.READY); return { serviceId: this.serviceId, notifyCharacteristicIds: [], notifyCharacteristicId: null, writeCharacteristicId: this.writeCharacteristicId, }; } // 串行逐个启用 notify return this._enableAllNotify(deviceId, serviceId, notifyChars).then((enabled) => { if (!this._isCurrent(seq)) throw this._cancelError('连接流程已作废'); this._notifyCharacteristicIds = enabled; this.notifyCharacteristicId = enabled[0] || null; if (enabled.length === 0) { console.error('[BLE] 所有 notify 特征启用均失败'); const err = new Error('启用通知失败'); err.notifyFailed = true; throw err; } console.log('[BLE] 已启用 notify 特征:', enabled); // 作为后续“静默/掉线”探测的基线 this._lastRecvAt = Date.now(); this._setState(BLE_STATE.READY); return { serviceId: this.serviceId, notifyCharacteristicIds: enabled, notifyCharacteristicId: this.notifyCharacteristicId, writeCharacteristicId: this.writeCharacteristicId, }; }); }) .catch((err) => { if (err && (err.cancelled || err.notifyFailed)) throw err; console.error('[BLE] 获取特征值失败:', err && err.message); throw new Error('获取特征值失败: ' + ((err && err.message) || '未知错误')); }); } /** * 串行启用多个 notify/indicate 特征 * @param {string} deviceId * @param {string} serviceId * @param {Array} chars - 特征数组(含 uuid) * @returns {Promise>} 成功启用的特征 UUID 列表 */ _enableAllNotify(deviceId, serviceId, chars) { return chars.reduce((chain, c) => { return chain.then((enabled) => { return this._enableNotify(deviceId, serviceId, c.uuid).then((ok) => { if (ok) enabled.push(c.uuid); return enabled; }); }); }, Promise.resolve([])); } /** * 启用单个特征的 notify/indicate * @returns {Promise} 成功返回特征 UUID,失败返回 null(不抛错) */ _enableNotify(deviceId, serviceId, characteristicId) { return this._callWithTimeout( (resolve, reject) => wx.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId, state: true, success: resolve, fail: reject, }), '启用通知' ) .then(() => { console.log('[BLE] 启用通知:', characteristicId); return characteristicId; }) .catch((err) => { console.warn('[BLE] 启用通知失败(跳过):', characteristicId, err && err.message); return null; }); } /** * 写入数据 * @param {string} hexData - 十六进制字符串 * @returns {Promise} */ writeCommand(hexData) { return new Promise((resolve, reject) => { if (!this.deviceId || !this.serviceId || !this.writeCharacteristicId) { reject(new Error('设备未连接或无写入特征值')); return; } const buffer = hex.hexToArrayBuffer(hexData); wx.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeCharacteristicId, value: buffer, success: () => { console.log('[BLE] 写入成功:', hexData); resolve(); }, fail: (err) => { console.error('[BLE] 写入失败:', err); reject(new Error('写入失败: ' + (err.errMsg || '未知错误'))); }, }); }); } /** * 清除自动重连定时器 */ _clearReconnectTimer() { if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } } /** * 调度自动重连(指数退避:1s → 2s → 4s ... 最大 15s) * 重试循环的唯一归属地:失败继续退避,直到成功或用户主动断开 */ _scheduleReconnect() { if (this._reconnectTimer) return; // 用户主动断开(含设备被停用后的收尾断开)后不得再排重连 if (this._manuallyDisconnected) return; const delay = Math.min(1000 * Math.pow(2, this._reconnectAttempts), 15000); this._reconnectAttempts++; console.log(`[BLE] ${delay}ms 后自动重连(第 ${this._reconnectAttempts} 次)`); this._reconnectTimer = setTimeout(() => { this._reconnectTimer = null; if (!this._lastDeviceId) return; // 排队到触发之间用户可能已经主动断开/停用设备:触发时必须重新确认意图, // 否则定时器会把刚断开的链路又拉起来 if (this._manuallyDisconnected) return; this.connectDevice(this._lastDeviceId, this._lastServiceId).catch((err) => { if (err && err.cancelled) return; console.error('[BLE] 自动重连失败:', err && err.message); this._scheduleReconnect(); }); }, delay); } /** * 断开连接 */ disconnect() { return new Promise((resolve) => { // 用户主动断开:停止自动重连,并作废在途连接流程(否则流程收尾会写脏会话状态) this._manuallyDisconnected = true; this._connectSeq++; this._connectPromise = null; this._connectingDeviceId = null; this._reconnectAttempts = 0; this._clearReconnectTimer(); if (!this.deviceId) { if (this.state !== BLE_STATE.DISCONNECTED) { this._setState(BLE_STATE.DISCONNECTED); } resolve(); return; } wx.closeBLEConnection({ deviceId: this.deviceId, success: () => { console.log('[BLE] 已断开连接'); }, complete: () => { this._setState(BLE_STATE.DISCONNECTED); this.deviceId = null; this.serviceId = null; this.notifyCharacteristicId = null; this.writeCharacteristicId = null; this._notifyCharacteristicIds = []; this._lastRecvAt = 0; resolve(); }, }); }); } /** * 关闭蓝牙适配器 */ closeBluetooth() { return new Promise((resolve) => { this.disconnect().then(() => { wx.closeBluetoothAdapter({ success: () => { console.log('[BLE] 已关闭蓝牙适配器'); }, complete: () => { resolve(); }, }); }); }); } /** * 获取当前状态 * @returns {string} */ getState() { return this.state; } /** * 是否已连接 * @returns {boolean} */ isConnected() { return this.state === BLE_STATE.READY || this.state === BLE_STATE.CONNECTED; } /** * 是否处于可接收数据的就绪态 * @returns {boolean} */ isReady() { return this.state === BLE_STATE.READY || this.state === BLE_STATE.CONNECTED; } /** * 是否正处于连接流程中 * @returns {boolean} */ isConnecting() { return !!this._connectPromise; } /** * 复位蓝牙适配器(close → open) * * 用于 native 栈停在"半开连接"的场景:此时 closeBLEConnection + createBLEConnection * 会反复 already connect / 连接超时,连"关开关重开"都救不回来, * 只有关掉适配器(同时清掉进程内所有 GATT 状态)再重开才能真正复位。 * 关闭期间禁止自动重连与在途流程,避免复位过程被自己打断。 * * 两条硬约束(都带超时兜底,任一环节 native 不回调也必须走完): * - 必须在 finally 里**复原** `_manuallyDisconnected`(而不是无条件置 false)。 * 入口置 true 是"复位期间禁自动重连"的需要;无条件置 false 会把复位之前 * 就已存在的"用户主动断开/设备已停用"意图一起抹掉,复位结束后自动重连被 * 重新拉起 —— 表现为"设备关了还在连蓝牙"。若无条件残留 true 则相反: * 同时禁用自动重连 + 让 `_isCurrent()` 恒 false(所有在途流程被判作废)= 死态。 * - 必须保证本 Promise 一定会结算,否则网关的串行队列(_chain)会被锁死, * 之后所有 start/stop/ensureStarted/retryBle 全部排队不执行。 * @returns {Promise} */ async resetAdapter() { console.log('[BLE] 复位蓝牙适配器'); const wasManuallyDisconnected = this._manuallyDisconnected; this._manuallyDisconnected = true; this._connectSeq++; // 作废所有在途流程 this._connectPromise = null; this._connectingDeviceId = null; this._clearReconnectTimer(); this._settleAllDisconnectWaits(); try { await this._closeAdapter(); this._openedAdapter = false; this.serviceId = null; this.notifyCharacteristicId = null; this.writeCharacteristicId = null; this._notifyCharacteristicIds = []; this._lastRecvAt = 0; this._setState(BLE_STATE.DISCONNECTED); await this.initBluetooth(); } finally { this._manuallyDisconnected = wasManuallyDisconnected; } } /** * 关闭蓝牙适配器(带超时兜底;失败也按已关闭继续) * 关不掉必须继续走 open:停在"半关"状态比重新开一次更糟, * 而卡在等待回调则会把整条重连链一起拖死 * @returns {Promise} */ _closeAdapter() { return this._callWithTimeout( (resolve) => wx.closeBluetoothAdapter({ success: resolve, fail: resolve, complete: resolve }), '关闭蓝牙适配器', this.config.adapterOpTimeout ).catch((err) => { console.warn('[BLE] 关闭适配器未正常回调,按已关闭继续:', err && err.message); }); } /** * 手动硬重连:复位适配器 + 重建链路 * 详情页 BLE 状态点击触发,是"自动重连卡死"时的确定性出口 * @param {string} deviceId - 目标设备(缺省用上次目标) * @param {string} serviceId - 服务 UUID(可选) * @returns {Promise} */ hardReconnect(deviceId = null, serviceId = null) { const target = deviceId || this._lastDeviceId; if (!target) { return Promise.reject(new Error('无重连目标')); } console.log('[BLE] 手动硬重连:', target); return this.resetAdapter() .then(() => this.connectDevice(target, serviceId || this._lastServiceId)); } /** * 强制重连当前目标设备(用于静默/掉线恢复) * 先拆旧链再建链,确保拿到的是全新链路(静默可能只是"没有测量",由调用方先探活) * @returns {Promise} */ forceReconnect() { const deviceId = this._lastDeviceId; if (!deviceId) { return Promise.reject(new Error('无重连目标')); } this._clearReconnectTimer(); this._reconnectAttempts = 0; this._manuallyDisconnected = false; // 已有在途连接:直接复用,绝不并发发起第二次 createBLEConnection if (this._connectPromise && this._connectingDeviceId === deviceId) { console.log('[BLE] 连接已在进行中,强制重连复用该流程:', deviceId); return this._connectPromise; } console.log('[BLE] 强制重连(先拆旧链):', deviceId); return this._closeConnection(deviceId) .then(() => this.connectDevice(deviceId, this._lastServiceId)) .catch((err) => { if (err && err.cancelled) throw err; console.error('[BLE] 强制重连失败:', err && err.message); // 强制重连由页面/网关按需触发,无人再排重试 → 此处必须自愈, // 否则链路会停在未连接状态直到下次回前台 if (this.config.autoReconnect && !this._manuallyDisconnected && this._lastDeviceId) { this._scheduleReconnect(); } throw err; }); } /** * GATT 层探活:读一次服务表,确认链路是否真的还可用 * 用于区分"设备没在测量(静默正常)"与"链路已死",避免无谓拆链重建 * @returns {Promise} */ checkLinkAlive() { const deviceId = this.deviceId; if (!deviceId || !this.isReady()) return Promise.resolve(false); return this._callWithTimeout( (resolve, reject) => wx.getBLEDeviceServices({ deviceId, success: resolve, fail: reject }), '链路探活' ) .then(() => true) .catch((err) => { console.warn('[BLE] 链路探活失败:', err && (err.errMsg || err.message)); return false; }); } /** * 诊断信息(真机排查“收不到/收一条即停”用) * @returns {Object} */ getDiagnostics() { return { state: this.state, adapterOpen: !!this._openedAdapter, deviceId: this.deviceId, serviceId: this.serviceId, notifyCharacteristicId: this.notifyCharacteristicId, notifyCharacteristicIds: this._notifyCharacteristicIds || [], writeCharacteristicId: this.writeCharacteristicId, recvCount: this._recvCount, lastRecvAt: this._lastRecvAt, autoReconnect: this.config.autoReconnect, connecting: !!this._connectPromise, consecutiveCreateFails: this._consecutiveCreateFails, }; } /** * 清除数据回调(页面卸载时调用) */ clearCallbacks() { this.onDataCallback = null; this.onStateChangeCallback = null; console.log('[BLE] 已清除回调'); } } // 导出单例 const bleService = new BleService(); module.exports = { BLE_STATE, BleService, bleService, };