383 lines
11 KiB
JavaScript
383 lines
11 KiB
JavaScript
/**
|
||
* 蓝牙多设备管理模块(Android 优化版)
|
||
* 功能:
|
||
* - 统一初始化蓝牙适配器
|
||
* - 扫描设备(可指定服务 UUID)
|
||
* - 直接连接已知设备(通过 deviceId/MAC)
|
||
* - 管理多个设备(打印机、电子秤等)
|
||
* - 写入数据、接收数据、连接状态监听
|
||
* - 自动重连(可选)
|
||
*
|
||
* 使用方法:
|
||
* import bluetoothManager from '@/utils/BluetoothManager.js';
|
||
* await bluetoothManager.init();
|
||
* bluetoothManager.setDeviceFoundCallback(...);
|
||
* await bluetoothManager.connectKnownDevice(deviceId, serviceUUID, writeUUID);
|
||
*/
|
||
class BluetoothManager {
|
||
constructor() {
|
||
this.adapterOpen = false; // 蓝牙适配器是否已打开
|
||
this.discovering = false; // 是否正在扫描
|
||
this.devices = new Map(); // 存储已连接设备的信息:key=deviceId, value={ serviceId, writeCharId, notifyCharId, name }
|
||
|
||
// 全局回调(只注册一次)
|
||
this.globalListeners = {
|
||
onDeviceFound: null,
|
||
onConnectionChange: null,
|
||
onDataReceived: null,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 初始化蓝牙适配器(全局只需调用一次)
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async init() {
|
||
if (this.adapterOpen) return;
|
||
return new Promise((resolve, reject) => {
|
||
uni.openBluetoothAdapter({
|
||
success: () => {
|
||
this.adapterOpen = true;
|
||
this._registerGlobalListeners();
|
||
resolve();
|
||
},
|
||
fail: (err) => {
|
||
console.error('蓝牙初始化失败', err);
|
||
reject(err);
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 注册全局唯一的监听器(避免覆盖)
|
||
*/
|
||
_registerGlobalListeners() {
|
||
// 设备发现监听
|
||
uni.onBluetoothDeviceFound((res) => {
|
||
const { devices } = res;
|
||
if (this.globalListeners.onDeviceFound) {
|
||
devices.forEach(device => this.globalListeners.onDeviceFound(device));
|
||
}
|
||
});
|
||
|
||
// 连接状态变化监听
|
||
uni.onBLEConnectionStateChange((res) => {
|
||
const { deviceId, connected } = res;
|
||
if (this.globalListeners.onConnectionChange) {
|
||
this.globalListeners.onConnectionChange(deviceId, connected);
|
||
}
|
||
// 如果连接断开,从 devices Map 中移除(可选,保留以便重连)
|
||
if (!connected && this.devices.has(deviceId)) {
|
||
// 注意:不断开 Map 中的记录,因为可能用于重连;但可以标记一个状态
|
||
// 这里不做自动删除,由业务层决定是否移除
|
||
}
|
||
});
|
||
|
||
// 特征值数据接收监听
|
||
uni.onBLECharacteristicValueChange((res) => {
|
||
const { deviceId, value } = res;
|
||
if (this.globalListeners.onDataReceived) {
|
||
this.globalListeners.onDataReceived(deviceId, value);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 设置设备发现回调
|
||
* @param {Function} callback (device) => {}
|
||
*/
|
||
setDeviceFoundCallback(callback) {
|
||
this.globalListeners.onDeviceFound = callback;
|
||
}
|
||
|
||
/**
|
||
* 设置连接状态变化回调
|
||
* @param {Function} callback (deviceId, connected) => {}
|
||
*/
|
||
setConnectionChangeCallback(callback) {
|
||
this.globalListeners.onConnectionChange = callback;
|
||
}
|
||
|
||
/**
|
||
* 设置数据接收回调
|
||
* @param {Function} callback (deviceId, arrayBuffer) => {}
|
||
*/
|
||
setDataReceivedCallback(callback) {
|
||
this.globalListeners.onDataReceived = callback;
|
||
}
|
||
|
||
/**
|
||
* 开始扫描设备
|
||
* @param {Array} services 要过滤的服务 UUID 数组(可选)
|
||
* @returns {Promise}
|
||
*/
|
||
async startScan(services = []) {
|
||
if (!this.adapterOpen) {
|
||
throw new Error('蓝牙未初始化,请先调用 init()');
|
||
}
|
||
if (this.discovering) {
|
||
await this.stopScan();
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
uni.startBluetoothDevicesDiscovery({
|
||
services,
|
||
success: () => {
|
||
this.discovering = true;
|
||
resolve();
|
||
},
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 停止扫描
|
||
*/
|
||
stopScan() {
|
||
if (!this.discovering) return Promise.resolve();
|
||
return new Promise((resolve, reject) => {
|
||
uni.stopBluetoothDevicesDiscovery({
|
||
success: () => {
|
||
this.discovering = false;
|
||
resolve();
|
||
},
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 直接连接已知设备(不依赖扫描结果)
|
||
* @param {string} deviceId 设备 MAC 地址(Android)
|
||
* @param {string} targetServiceUUID 目标服务的 UUID
|
||
* @param {string} writeUUID 写入特征值 UUID(可选,自动查找第一个可写)
|
||
* @param {string} notifyUUID 通知特征值 UUID(可选,自动查找第一个可通知)
|
||
* @returns {Promise<object>} 返回设备信息 { deviceId, serviceId, writeCharId, notifyCharId }
|
||
*/
|
||
async connectKnownDevice(deviceId, targetServiceUUID, writeUUID = null, notifyUUID = null) {
|
||
if (!this.adapterOpen) {
|
||
throw new Error('蓝牙未初始化,请先调用 init()');
|
||
}
|
||
// 如果已经连接且 Map 中有记录,直接返回(可选,但为了避免重复连接,先断开)
|
||
if (this.devices.has(deviceId)) {
|
||
await this.disconnectDevice(deviceId);
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
uni.createBLEConnection({
|
||
deviceId,
|
||
success: async () => {
|
||
// 等待服务发现稳定(重要:解决连接成功但查不到服务的问题)
|
||
await this._delay(800);
|
||
try {
|
||
// 1. 获取服务列表
|
||
const { services } = await this._getBLEDeviceServices(deviceId);
|
||
const targetService = services.find(s => s.uuid.toLowerCase().includes(targetServiceUUID.toLowerCase()));
|
||
if (!targetService) {
|
||
throw new Error(`未找到服务 ${targetServiceUUID}`);
|
||
}
|
||
// 2. 获取特征值
|
||
const characteristics = await this._getBLEDeviceCharacteristics(deviceId, targetService.uuid);
|
||
let writeChar = null;
|
||
if (writeUUID) {
|
||
writeChar = characteristics.find(c => c.uuid.toLowerCase() === writeUUID.toLowerCase());
|
||
} else {
|
||
writeChar = characteristics.find(c => c.properties.write || c.properties.writeNoResponse);
|
||
}
|
||
if (!writeChar) {
|
||
throw new Error('未找到可写入的特征值');
|
||
}
|
||
let notifyChar = null;
|
||
if (notifyUUID) {
|
||
notifyChar = characteristics.find(c => c.uuid.toLowerCase() === notifyUUID.toLowerCase());
|
||
} else {
|
||
notifyChar = characteristics.find(c => c.properties.notify || c.properties.indicate);
|
||
}
|
||
// 3. 启用通知(如果有 notify 特征值)
|
||
if (notifyChar && (notifyChar.properties.notify || notifyChar.properties.indicate)) {
|
||
await this._notifyBLECharacteristicValueChange(deviceId, targetService.uuid, notifyChar.uuid, true);
|
||
}
|
||
// 4. 存储设备信息
|
||
this.devices.set(deviceId, {
|
||
serviceId: targetService.uuid,
|
||
writeCharId: writeChar.uuid,
|
||
notifyCharId: notifyChar ? notifyChar.uuid : null,
|
||
});
|
||
resolve({
|
||
deviceId,
|
||
serviceId: targetService.uuid,
|
||
writeCharId: writeChar.uuid,
|
||
notifyCharId: notifyChar ? notifyChar.uuid : null,
|
||
});
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
},
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 向指定设备写入数据
|
||
* @param {string} deviceId
|
||
* @param {ArrayBuffer} buffer
|
||
* @returns {Promise}
|
||
*/
|
||
async writeData(deviceId, buffer) {
|
||
const device = this.devices.get(deviceId);
|
||
if (!device) {
|
||
throw new Error('设备未连接或未初始化,请先调用 connectKnownDevice');
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
uni.writeBLECharacteristicValue({
|
||
deviceId,
|
||
serviceId: device.serviceId,
|
||
characteristicId: device.writeCharId,
|
||
value: buffer,
|
||
success: resolve,
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 断开指定设备
|
||
* @param {string} deviceId
|
||
* @returns {Promise}
|
||
*/
|
||
disconnectDevice(deviceId) {
|
||
if (!this.devices.has(deviceId)) return Promise.resolve();
|
||
return new Promise((resolve, reject) => {
|
||
uni.closeBLEConnection({
|
||
deviceId,
|
||
success: () => {
|
||
this.devices.delete(deviceId);
|
||
resolve();
|
||
},
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 断开所有已连接设备并关闭蓝牙适配器
|
||
*/
|
||
async closeAll() {
|
||
const disconnectPromises = Array.from(this.devices.keys()).map(id => this.disconnectDevice(id));
|
||
await Promise.all(disconnectPromises);
|
||
if (this.adapterOpen) {
|
||
return new Promise((resolve) => {
|
||
uni.closeBluetoothAdapter({
|
||
success: () => {
|
||
this.adapterOpen = false;
|
||
this.discovering = false;
|
||
resolve();
|
||
},
|
||
fail: () => {
|
||
this.adapterOpen = false;
|
||
resolve();
|
||
},
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// ---------- 内部辅助方法 ----------
|
||
_delay(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
_getBLEDeviceServices(deviceId) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.getBLEDeviceServices({
|
||
deviceId,
|
||
success: resolve,
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
_getBLEDeviceCharacteristics(deviceId, serviceId) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.getBLEDeviceCharacteristics({
|
||
deviceId,
|
||
serviceId,
|
||
success: resolve,
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
_notifyBLECharacteristicValueChange(deviceId, serviceId, characteristicId, state = true) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.notifyBLECharacteristicValueChange({
|
||
deviceId,
|
||
serviceId,
|
||
characteristicId,
|
||
state,
|
||
success: resolve,
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 仅连接设备并获取服务列表(不预设任何特征值)
|
||
* 用于设备配置阶段,让用户手动选择服务和特征值
|
||
* @param {string} deviceId
|
||
* @returns {Promise<{services: Array}>} 返回服务列表
|
||
*/
|
||
async connectForDiscovery(deviceId) {
|
||
if (!this.adapterOpen) {
|
||
throw new Error('蓝牙未初始化');
|
||
}
|
||
// 如果已连接,先断开
|
||
if (this.devices.has(deviceId)) {
|
||
await this.disconnectDevice(deviceId);
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
uni.createBLEConnection({
|
||
deviceId,
|
||
success: async () => {
|
||
await this._delay(800);
|
||
try {
|
||
const { services } = await this._getBLEDeviceServices(deviceId);
|
||
// 暂不存储到 devices Map,因为还没确定特征值
|
||
resolve({ services, deviceId });
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
},
|
||
fail: reject,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 保存用户选择的配置,并完成最终连接(启用通知等)
|
||
* @param {string} deviceId
|
||
* @param {string} serviceId
|
||
* @param {string} writeCharId
|
||
* @param {string} notifyCharId (可选)
|
||
* @returns {Promise}
|
||
*/
|
||
async saveAndConnect(deviceId, serviceId, writeCharId, notifyCharId = null) {
|
||
if (!this.adapterOpen) {
|
||
throw new Error('蓝牙未初始化');
|
||
}
|
||
// 启用通知(如果需要)
|
||
if (notifyCharId) {
|
||
await this._notifyBLECharacteristicValueChange(deviceId, serviceId, notifyCharId, true);
|
||
}
|
||
// 存储设备信息
|
||
this.devices.set(deviceId, {
|
||
serviceId: serviceId,
|
||
writeCharId: writeCharId,
|
||
notifyCharId: notifyCharId,
|
||
});
|
||
return { deviceId, serviceId, writeCharId, notifyCharId };
|
||
}
|
||
}
|
||
|
||
export default new BluetoothManager(); |