511 lines
14 KiB
JavaScript
511 lines
14 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,
|
||
// 默认服务 UUID(需根据实际设备配置)
|
||
serviceUUIDs: [],
|
||
};
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 设置配置
|
||
* @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);
|
||
}
|
||
|
||
/**
|
||
* 初始化蓝牙适配器
|
||
* @returns {Promise}
|
||
*/
|
||
initBluetooth() {
|
||
return new Promise((resolve, reject) => {
|
||
// 移除旧监听器,防止叠加(微信 API 支持注册多个回调)
|
||
if (this._listenersRegistered) {
|
||
try {
|
||
wx.offBLEConnectionStateChange();
|
||
wx.offBLECharacteristicValueChange();
|
||
} catch (e) {
|
||
// 忽略移除失败
|
||
}
|
||
}
|
||
|
||
wx.openBluetoothAdapter({
|
||
success: () => {
|
||
console.log('[BLE] 初始化成功');
|
||
|
||
// 监听连接状态变化(只注册一次)
|
||
wx.onBLEConnectionStateChange((res) => {
|
||
console.log('[BLE] 连接状态变化:', res);
|
||
if (!res.connected) {
|
||
this._setState(BLE_STATE.DISCONNECTED);
|
||
this.deviceId = null;
|
||
}
|
||
});
|
||
|
||
// 监听特征值变化(只注册一次)
|
||
wx.onBLECharacteristicValueChange((res) => {
|
||
const hexData = hex.arrayBufferToHex(res.value);
|
||
console.log('[BLE] 收到数据:', hexData);
|
||
if (this.onDataCallback) {
|
||
this.onDataCallback({
|
||
hex: hexData,
|
||
buffer: res.value,
|
||
deviceId: res.deviceId,
|
||
serviceId: res.serviceId,
|
||
characteristicId: res.characteristicId,
|
||
});
|
||
}
|
||
});
|
||
|
||
this._listenersRegistered = true;
|
||
resolve();
|
||
},
|
||
fail: (err) => {
|
||
console.error('[BLE] 初始化失败:', err);
|
||
reject(new Error('蓝牙初始化失败: ' + (err.errMsg || '未知错误')));
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 扫描蓝牙设备
|
||
* @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;
|
||
|
||
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._setState(BLE_STATE.CONNECTED);
|
||
|
||
// 获取 MTU(提高传输效率,iOS 最大 185,Android 建议 247)
|
||
wx.setBLEMTU({
|
||
deviceId: deviceId,
|
||
mtu: 247,
|
||
complete: () => {
|
||
// 发现服务
|
||
this._discoverServices(deviceId, serviceId)
|
||
.then(resolve)
|
||
.catch(reject);
|
||
},
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
clearTimeout(timeoutId);
|
||
this._setState(BLE_STATE.DISCONNECTED);
|
||
console.error('[BLE] 连接失败:', err);
|
||
reject(new Error('连接失败: ' + (err.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('获取服务失败'));
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 发现特征值
|
||
* @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);
|
||
const characteristics = res.characteristics;
|
||
|
||
// 查找通知特征值(通常具有 notify 或 indicate 属性)
|
||
const notifyChar = characteristics.find(c => c.properties.notify || c.properties.indicate);
|
||
// 查找写入特征值
|
||
const writeChar = characteristics.find(c => c.properties.write || c.properties.writeNoResponse);
|
||
|
||
if (notifyChar) {
|
||
this.notifyCharacteristicId = notifyChar.uuid;
|
||
console.log('[BLE] 通知特征值:', this.notifyCharacteristicId);
|
||
|
||
// 启用通知
|
||
wx.notifyBLECharacteristicValueChange({
|
||
deviceId: deviceId,
|
||
serviceId: serviceId,
|
||
characteristicId: this.notifyCharacteristicId,
|
||
state: true,
|
||
success: () => {
|
||
console.log('[BLE] 已启用通知');
|
||
if (writeChar) {
|
||
this.writeCharacteristicId = writeChar.uuid;
|
||
console.log('[BLE] 写入特征值:', this.writeCharacteristicId);
|
||
}
|
||
this._setState(BLE_STATE.READY);
|
||
resolve({
|
||
serviceId: this.serviceId,
|
||
notifyCharacteristicId: this.notifyCharacteristicId,
|
||
writeCharacteristicId: this.writeCharacteristicId,
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
console.error('[BLE] 启用通知失败:', err);
|
||
reject(new Error('启用通知失败'));
|
||
},
|
||
});
|
||
} else {
|
||
// 没有通知特征值,但可能有写入特征值
|
||
if (writeChar) {
|
||
this.writeCharacteristicId = writeChar.uuid;
|
||
console.log('[BLE] 写入特征值:', this.writeCharacteristicId);
|
||
}
|
||
this._setState(BLE_STATE.READY);
|
||
resolve({
|
||
serviceId: this.serviceId,
|
||
notifyCharacteristicId: null,
|
||
writeCharacteristicId: this.writeCharacteristicId,
|
||
});
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('[BLE] 获取特征值失败:', err);
|
||
reject(new Error('获取特征值失败'));
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 写入数据
|
||
* @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 || '未知错误')));
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 断开连接
|
||
*/
|
||
disconnect() {
|
||
return new Promise((resolve) => {
|
||
if (!this.deviceId) {
|
||
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;
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 清除数据回调(页面卸载时调用)
|
||
*/
|
||
clearCallbacks() {
|
||
this.onDataCallback = null;
|
||
this.onStateChangeCallback = null;
|
||
console.log('[BLE] 已清除回调');
|
||
}
|
||
}
|
||
|
||
// 导出单例
|
||
const bleService = new BleService();
|
||
|
||
module.exports = {
|
||
BLE_STATE,
|
||
BleService,
|
||
bleService,
|
||
}; |