fastbee/yack-mini/miniprogram/services/ble.js

763 lines
23 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,
// 默认服务 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<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();
},
});
});
}
/**
* 连接设备
* @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<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 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,
};