fastbee/yack-mini/miniprogram/services/ble.js
2026-09-27 14:46:05 +08:00

1214 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 蓝牙 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,
};