bike-ali/utils/bluetooth.js
2024-12-17 10:20:53 +08:00

342 lines
9.1 KiB
JavaScript

export class BluetoothManager {
constructor() {
this.isInitialized = false
this.deviceFoundCallback = null
this.onReceiveDataCallback = null
this.isConnected = false
this.retryTimes = 3 // 重试次数
this.retryDelay = 1000 // 重试延迟(ms)
// 添加常用的服务和特征值UUID
this.SERVICE_UUID = "000000FF-0000-1000-8000-00805F9B34FB"
this.WRITE_CHARACTERISTIC_UUID = "0000FF01-0000-1000-8000-00805F9B34FB"
this.NOTIFY_CHARACTERISTIC_UUID = "0000FF02-0000-1000-8000-00805F9B34FB"
}
// 延迟函数
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// 带重试的异步操作
async withRetry(operation, errorMessage) {
let lastError
for(let i = 0; i < this.retryTimes; i++) {
try {
return await operation()
} catch(error) {
console.log(`${errorMessage} 尝试第 ${i + 1} 次失败:`, error)
lastError = error
if(i < this.retryTimes - 1) {
await this.delay(this.retryDelay)
}
}
}
throw lastError
}
async init() {
if (this.isInitialized) return
try {
// 先关闭蓝牙适配器再重新打开
try {
await my.closeBluetoothAdapter()
await this.delay(1000)
} catch(e) {
console.log('关闭蓝牙适配器失败,可能本来就是关闭状态')
}
await this.withRetry(
() => this.openBluetoothAdapter(),
'初始化蓝牙适配器失败'
)
this.isInitialized = true
// 监听蓝牙连接状态变化
my.onBLEConnectionStateChanged((res) => {
this.isConnected = res.connected
if (!res.connected) {
console.log('蓝牙连接已断开')
}
})
// 监听特征值变化
my.onBLECharacteristicValueChange((res) => {
if(this.onReceiveDataCallback) {
const value = this.ab2str(res.value)
this.onReceiveDataCallback(value)
}
})
} catch (error) {
this.isInitialized = false
throw new Error('蓝牙初始化失败: ' + error.message)
}
}
async openBluetoothAdapter() {
return new Promise((resolve, reject) => {
my.openBluetoothAdapter({
success: () => {
resolve()
},
fail: (error) => {
if(error.error === 10015) {
reject(new Error('蓝牙未打开,请打开手机蓝牙'))
} else {
reject(error)
}
}
})
})
}
// ArrayBuffer转字符串
ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf))
}
// 字符串转ArrayBuffer
str2ab(str) {
let buf = new ArrayBuffer(str.length)
let bufView = new Uint8Array(buf)
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i)
}
return buf
}
async startSearch() {
if(!this.isInitialized) {
await this.init()
}
await this.withRetry(async () => {
return new Promise((resolve, reject) => {
my.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: () => {
this.startListeningForDevices()
resolve()
},
fail: (error) => {
reject(error)
}
})
})
}, '开始搜索设备失败')
}
startListeningForDevices() {
my.onBluetoothDeviceFound((res) => {
if (this.deviceFoundCallback) {
res.devices.forEach(device => {
const deviceName = device.name || device.localName
// 只处理前缀为BBLE的设备
if (deviceName && deviceName.startsWith('BBLE')) {
this.deviceFoundCallback({
name: deviceName,
deviceId: device.deviceId,
RSSI: device.RSSI,
advertisData: device.advertisData,
connected: false,
mac: deviceName.substring(4)
})
}
})
}
})
}
async stopSearch() {
return new Promise((resolve, reject) => {
my.stopBluetoothDevicesDiscovery({
success: () => {
resolve()
},
fail: (error) => {
reject(error)
}
})
})
}
async connect(deviceId) {
console.log('开始连接设备:', deviceId)
try {
// 先断开可能存在的连接
try {
await this.disconnect(deviceId)
await this.delay(1000)
} catch(e) {
console.log('断开旧连接失败,可能本来就未连接')
}
// 重试连接流程
await this.withRetry(async () => {
// 1. 建立连接
await this.createConnection(deviceId)
// 2. 获取服务
const services = await this.getServices(deviceId)
const targetService = services.find(s => s.uuid.toLowerCase() === this.SERVICE_UUID.toLowerCase())
if (!targetService) throw new Error('未找到目标服务')
// 3. 获取特征值
const characteristics = await this.getCharacteristics(deviceId, targetService.uuid)
// 4. 启用通知
await this.enableNotify(deviceId, targetService.uuid, this.NOTIFY_CHARACTERISTIC_UUID)
this.isConnected = true
return true
}, '连接设备失败')
} catch (error) {
this.isConnected = false
console.error('连接失败:', error)
throw error
}
}
async createConnection(deviceId) {
return new Promise((resolve, reject) => {
my.connectBLEDevice({
deviceId,
timeout: 15000,
success: (res) => {
resolve(res)
},
fail: (error) => {
if(error.error === 10015) {
reject(new Error('蓝牙连接失败,请检查设备是否在范围内'))
} else {
reject(error)
}
}
})
})
}
async getServices(deviceId) {
return new Promise((resolve, reject) => {
my.getBLEDeviceServices({
deviceId,
success: (res) => {
console.log('获取服务成功:', res.services)
resolve(res.services)
},
fail: (error) => {
console.error('获取服务失败:', error)
reject(error)
}
})
})
}
async getCharacteristics(deviceId, serviceId) {
return new Promise((resolve, reject) => {
my.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
console.log('获取特征值成功:', res.characteristics)
resolve(res.characteristics)
},
fail: (error) => {
console.error('获取特征值失败:', error)
reject(error)
}
})
})
}
async enableNotify(deviceId, serviceId, characteristicId) {
return new Promise((resolve, reject) => {
my.notifyBLECharacteristicValueChange({
deviceId,
serviceId,
characteristicId,
state: true,
success: (res) => {
resolve(res)
},
fail: (error) => {
reject(error)
}
})
})
}
async disconnect(deviceId) {
return new Promise((resolve, reject) => {
my.disconnectBLEDevice({
deviceId,
success: () => {
this.isConnected = false
resolve()
},
fail: (error) => {
reject(error)
}
})
})
}
// 发送数据
async sendData(deviceId, data) {
if (!this.isConnected) {
throw new Error('蓝牙未连接')
}
const buffer = this.str2ab(data)
return this.writeBLECharacteristicValue(
deviceId,
this.SERVICE_UUID,
this.WRITE_CHARACTERISTIC_UUID,
buffer
)
}
async writeBLECharacteristicValue(deviceId, serviceId, characteristicId, value) {
return new Promise((resolve, reject) => {
my.writeBLECharacteristicValue({
deviceId,
serviceId,
characteristicId,
value,
success: () => {
resolve()
},
fail: (error) => {
reject(error)
}
})
})
}
onDeviceFound(callback) {
this.deviceFoundCallback = callback
}
// 注册数据接收回调
onReceiveData(callback) {
this.onReceiveDataCallback = callback
}
async destroy() {
if (this.isInitialized) {
try {
await this.stopSearch()
await this.delay(1000)
await my.closeBluetoothAdapter()
this.isInitialized = false
this.isConnected = false
} catch (error) {
console.error('销毁蓝牙实例失败:', error)
}
}
}
}