fastbee/yack-mini/miniprogram/services/ble.js
2026-09-05 10:19:07 +08:00

511 lines
14 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: [],
};
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,
};