1214 lines
42 KiB
JavaScript
1214 lines
42 KiB
JavaScript
/**
|
||
* 蓝牙 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<string>} serviceUUIDs - 服务 UUID 列表(可选)
|
||
* @param {number} timeout - 超时时间(毫秒)
|
||
* @returns {Promise<Array>} 设备列表
|
||
*/
|
||
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<Array<string>>} 成功启用的特征 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<string|null>} 成功返回特征 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<boolean>}
|
||
*/
|
||
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,
|
||
}; |