/** * 蓝牙 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, // 默认服务 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; } /** * 设置配置 * @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) { const wasConnected = this.state === BLE_STATE.READY || this.state === BLE_STATE.CONNECTED; this._setState(BLE_STATE.DISCONNECTED); // 保留 deviceId 供重连使用(不再置空),掉线自动重连 if (wasConnected && this.config.autoReconnect && !this._manuallyDisconnected && 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, }); } }); this._listenersRegistered = true; console.log('[BLE] 已注册全局监听'); }; if (this._openedAdapter) { registerListeners(); resolve(); return; } wx.openBluetoothAdapter({ success: () => { console.log('[BLE] 初始化成功'); this._openedAdapter = true; registerListeners(); resolve(); }, fail: (err) => { const msg = (err && err.errMsg) || ''; // 部分平台适配器已开启时 open 会报 already 类错误,视为成功 if (/already/i.test(msg)) { console.log('[BLE] 适配器已开启(忽略 already)'); this._openedAdapter = true; registerListeners(); resolve(); } else { console.error('[BLE] 初始化失败:', err); 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(); }, }); }); } /** * 连接设备 * @param {string} deviceId - 设备 ID * @param {string} serviceId - 服务 UUID(可选,不传则自动发现) * @returns {Promise} */ connectDevice(deviceId, serviceId = null) { return new Promise((resolve, reject) => { this._setState(BLE_STATE.CONNECTING); this.deviceId = deviceId; // 记录连接目标,供自动重连使用 this._lastDeviceId = deviceId; this._lastServiceId = serviceId; this._manuallyDisconnected = false; this._clearReconnectTimer(); const connectTimeout = this.config.connectTimeout; let timeoutId = null; // 设置连接超时 timeoutId = setTimeout(() => { wx.closeBLEConnection({ deviceId: deviceId, complete: () => { this._setState(BLE_STATE.DISCONNECTED); reject(new Error('连接超时')); }, }); }, connectTimeout); // 发起连接 wx.createBLEConnection({ deviceId: deviceId, success: () => { clearTimeout(timeoutId); console.log('[BLE] 连接成功:', deviceId); // 连接成功,重置重连退避计数 this._reconnectAttempts = 0; this._setState(BLE_STATE.CONNECTED); // 获取 MTU(提高传输效率,iOS 最大 185,Android 建议 247) wx.setBLEMTU({ deviceId: deviceId, mtu: this._getMtu(), complete: () => { // 发现服务 this._discoverServices(deviceId, serviceId) .then(resolve) .catch(reject); }, }); }, fail: (err) => { clearTimeout(timeoutId); const errMsg = (err && err.errMsg) || ''; // 如果报错 "already connect",先断开再重连 if (/already connect/i.test(errMsg)) { console.warn('[BLE] 检测到已存在连接,先断开再重连'); wx.closeBLEConnection({ deviceId: deviceId, complete: () => { // 断开后重新连接 setTimeout(() => { wx.createBLEConnection({ deviceId: deviceId, success: () => { console.log('[BLE] 重连成功:', deviceId); this._reconnectAttempts = 0; this._setState(BLE_STATE.CONNECTED); wx.setBLEMTU({ deviceId: deviceId, mtu: this._getMtu(), complete: () => { this._discoverServices(deviceId, serviceId) .then(resolve) .catch(reject); }, }); }, fail: (err2) => { this._setState(BLE_STATE.DISCONNECTED); console.error('[BLE] 重连失败:', err2); reject(new Error('重连失败: ' + (err2.errMsg || '未知错误'))); }, }); }, 500); // 等待 500ms 确保断开完成 }, }); return; } this._setState(BLE_STATE.DISCONNECTED); console.error('[BLE] 连接失败:', err); reject(new Error('连接失败: ' + errMsg || '未知错误')); }, }); }); } /** * 发现服务和特征值 * @param {string} deviceId * @param {string} serviceId * @returns {Promise} */ _discoverServices(deviceId, serviceId = null) { return new Promise((resolve, reject) => { this._setState(BLE_STATE.DISCOVERING); wx.getBLEDeviceServices({ deviceId: deviceId, success: (res) => { 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) { reject(new Error('未找到目标服务')); return; } this.serviceId = targetService.uuid; console.log('[BLE] 目标服务:', this.serviceId); // 获取特征值 this._discoverCharacteristics(deviceId, this.serviceId) .then(resolve) .catch(reject); }, fail: (err) => { console.error('[BLE] 获取服务失败:', err); reject(new Error('获取服务失败')); }, }); }); } /** * 发现特征值 * 目标服务下所有 notify/indicate 特征都尝试启用(数据特征未必是第一个 notify 特征), * 单个特征启用失败仅告警跳过,至少一个成功即视为就绪。 * @param {string} deviceId * @param {string} serviceId * @returns {Promise} */ _discoverCharacteristics(deviceId, serviceId) { return new Promise((resolve, reject) => { wx.getBLEDeviceCharacteristics({ deviceId: deviceId, serviceId: serviceId, success: (res) => { 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); resolve({ serviceId: this.serviceId, notifyCharacteristicIds: [], notifyCharacteristicId: null, writeCharacteristicId: this.writeCharacteristicId, }); return; } // 串行逐个启用 notify this._enableAllNotify(deviceId, serviceId, notifyChars).then((enabled) => { this._notifyCharacteristicIds = enabled; this.notifyCharacteristicId = enabled[0] || null; if (enabled.length === 0) { console.error('[BLE] 所有 notify 特征启用均失败'); reject(new Error('启用通知失败')); return; } console.log('[BLE] 已启用 notify 特征:', enabled); // 作为后续“静默/掉线”探测的基线 this._lastRecvAt = Date.now(); this._setState(BLE_STATE.READY); resolve({ serviceId: this.serviceId, notifyCharacteristicIds: enabled, notifyCharacteristicId: this.notifyCharacteristicId, writeCharacteristicId: this.writeCharacteristicId, }); }).catch((err) => { console.error('[BLE] 启用通知异常:', err); reject(new Error('启用通知失败')); }); }, fail: (err) => { console.error('[BLE] 获取特征值失败:', err); reject(new Error('获取特征值失败')); }, }); }); } /** * 串行启用多个 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 new Promise((resolve) => { wx.notifyBLECharacteristicValueChange({ deviceId: deviceId, serviceId: serviceId, characteristicId: characteristicId, state: true, success: () => { console.log('[BLE] 启用通知:', characteristicId); resolve(characteristicId); }, fail: (err) => { console.warn('[BLE] 启用通知失败(跳过):', characteristicId, err); resolve(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; 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; this.connectDevice(this._lastDeviceId, this._lastServiceId).catch((err) => { console.error('[BLE] 自动重连失败:', err.message); this._scheduleReconnect(); }); }, delay); } /** * 断开连接 */ disconnect() { return new Promise((resolve) => { // 用户主动断开,停止自动重连 this._manuallyDisconnected = true; 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; } /** * 是否正处于连接流程中(用于避免与自动重连并发发起 createBLEConnection) * @returns {boolean} */ isConnecting() { return this.state === BLE_STATE.CONNECTING || this.state === BLE_STATE.DISCOVERING; } /** * 强制重连当前目标设备(用于静默/掉线恢复) * @returns {Promise} */ forceReconnect() { if (!this._lastDeviceId) { return Promise.reject(new Error('无重连目标')); } this._reconnectAttempts = 0; this._manuallyDisconnected = false; this._clearReconnectTimer(); console.log('[BLE] 强制重连:', this._lastDeviceId); return this.connectDevice(this._lastDeviceId, this._lastServiceId); } /** * 探测连接是否仍活跃(超过 silentMs 无收包视为不活跃) * @param {number} silentMs - 静默阈值(毫秒) * @returns {boolean} */ probeConnection(silentMs = 30000) { if (!this.isReady()) return false; if (!this._lastRecvAt) return true; // 尚未收到任何包,不做静默判定 return (Date.now() - this._lastRecvAt) <= silentMs; } /** * 诊断信息(真机排查“收不到/收一条即停”用) * @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, }; } /** * 清除数据回调(页面卸载时调用) */ clearCallbacks() { this.onDataCallback = null; this.onStateChangeCallback = null; console.log('[BLE] 已清除回调'); } } // 导出单例 const bleService = new BleService(); module.exports = { BLE_STATE, BleService, bleService, };