| New file |
| | |
| | | // RtAuthBluetoothManager.js |
| | | // 精简版蓝牙管理器,只支持 RtAuth 鉴权模式(Robotaxi 场景) |
| | | import Taro from '@tarojs/taro'; |
| | | import { ab2hex, madAddressToLocalName, hexStringToArray } from '../../../utils/canControlTools'; |
| | | |
| | | // 引入日志和WASM模块 |
| | | var log = require('./log.js'); |
| | | const bleWasmModule = require('./bleMethod'); |
| | | |
| | | export default class RtAuthBluetoothManager { |
| | | constructor(options) { |
| | | // 回调函数 |
| | | this.onStatusChange = options.onStatusChange || (() => {}); // 连接状态回调 |
| | | this.onError = options.onError || (() => {}); // 错误回调 |
| | | this.onRtAuthResult = options.onRtAuthResult || (() => {}); // RtAuth 鉴权结果回调 |
| | | this.onRtNotification = options.onRtNotification || (() => {}); // RtNotification 通知回调 |
| | | |
| | | // 常量 |
| | | this.serviceId = '0000ED28-ABAB-406D-832D-7AD799B479F7'; |
| | | this.writeId = '0000ED28-ABAB-9C8C-2801-B1A015E46D6C'; |
| | | this.MAX_RETRY_TIMES = 1; |
| | | |
| | | // 内部状态 |
| | | this.state = { |
| | | deviceId: '', |
| | | macAddress: '', |
| | | certificate: '', // 鉴权凭证 |
| | | deviceModel: '', // 设备型号 |
| | | isIOS: false, |
| | | connected: false, |
| | | isScaning: false, |
| | | connectStatus: 0, // 0:断开, 1:连接中, 2:已连接, 3:已鉴权 |
| | | tryTimes: 0, |
| | | // 特征值 |
| | | readServiceId: '', |
| | | writeServiceId: '', |
| | | readChs: '', |
| | | writeChs: '', |
| | | indecateChs: '', |
| | | indecateServiceId: '' |
| | | }; |
| | | |
| | | this.initSystemInfo(); |
| | | } |
| | | |
| | | initSystemInfo() { |
| | | Taro.getSystemInfo({ |
| | | success: res => { |
| | | this.state.isIOS = res.system.toLowerCase().indexOf('ios') >= 0; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // 更新车辆配置(macAddress 必填,certificate 和 deviceModel 可选,用于自动鉴权) |
| | | updateCarConfig(config) { |
| | | this.state.macAddress = config.macAddress; |
| | | this.state.certificate = config.certificate || ''; |
| | | this.state.deviceModel = config.deviceModel || ''; |
| | | this.resetState(); |
| | | } |
| | | |
| | | resetState() { |
| | | this.state.connectStatus = 0; |
| | | this.state.connected = false; |
| | | this.state.tryTimes = 0; |
| | | this.notifyStatus(0); |
| | | } |
| | | |
| | | notifyStatus(status) { |
| | | this.state.connectStatus = status; |
| | | this.onStatusChange(status); |
| | | } |
| | | |
| | | // --- 蓝牙连接流程 --- |
| | | |
| | | openBluetoothAdapter() { |
| | | if (this.state.connectStatus === 2) return; |
| | | |
| | | log.info('RtAuth: 准备初始化蓝牙...'); |
| | | |
| | | Taro.getLocation({ |
| | | type: 'wgs84', |
| | | success: () => this._realOpenAdapter(), |
| | | fail: () => this._realOpenAdapter() |
| | | }); |
| | | } |
| | | |
| | | _realOpenAdapter() { |
| | | wx.openBluetoothAdapter({ |
| | | success: () => { |
| | | log.info('RtAuth: 打开蓝牙适配器成功'); |
| | | this.findAndConnect(); |
| | | }, |
| | | fail: (res) => { |
| | | log.info('RtAuth: 打开蓝牙适配器失败', res); |
| | | if (res.errMsg.includes('already opened')) { |
| | | wx.closeBluetoothAdapter({ |
| | | success: () => this._realOpenAdapter() |
| | | }); |
| | | } else { |
| | | this.onError({ type: 'ADAPTER_ERR', msg: '请打开蓝牙和定位功能' }); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | findAndConnect() { |
| | | const { macAddress, isIOS } = this.state; |
| | | let key = madAddressToLocalName(macAddress); |
| | | if (isIOS) key = key + 'IOS'; |
| | | |
| | | const cacheDeviceId = Taro.getStorageSync(key); |
| | | if (cacheDeviceId) { |
| | | log.info('RtAuth: 使用缓存DeviceId连接', cacheDeviceId); |
| | | this.state.deviceId = cacheDeviceId; |
| | | this.connectDevice(); |
| | | } else { |
| | | this.startScan(); |
| | | } |
| | | } |
| | | |
| | | startScan() { |
| | | if (this.state.isScaning) return; |
| | | this.state.isScaning = true; |
| | | |
| | | wx.startBluetoothDevicesDiscovery({ |
| | | allowDuplicatesKey: true, |
| | | success: () => this.onBluetoothDeviceFound() |
| | | }); |
| | | } |
| | | |
| | | onBluetoothDeviceFound() { |
| | | const { macAddress, isIOS } = this.state; |
| | | const targetLocalName = madAddressToLocalName(macAddress); |
| | | |
| | | wx.onBluetoothDeviceFound((res) => { |
| | | res.devices.forEach(device => { |
| | | if (!device.name && !device.localName) return; |
| | | |
| | | if (device.localName === targetLocalName) { |
| | | log.info('RtAuth: 扫描到目标设备', device.deviceId); |
| | | |
| | | const keyS = device.localName + (isIOS ? 'IOS' : ''); |
| | | Taro.setStorage({ key: keyS, data: device.deviceId }); |
| | | |
| | | this.state.deviceId = device.deviceId; |
| | | this.stopScan(); |
| | | this.connectDevice(); |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | stopScan() { |
| | | wx.stopBluetoothDevicesDiscovery(); |
| | | this.state.isScaning = false; |
| | | } |
| | | |
| | | connectDevice() { |
| | | if (this.state.connectStatus === 2) return; |
| | | |
| | | this.notifyStatus(1); |
| | | log.info('RtAuth: 开始连接设备', this.state.deviceId); |
| | | |
| | | wx.createBLEConnection({ |
| | | deviceId: this.state.deviceId, |
| | | success: () => { |
| | | log.info('RtAuth: 连接设备成功'); |
| | | this.state.tryTimes = 0; |
| | | this.state.connected = true; |
| | | if (this.state.isScaning) this.stopScan(); |
| | | this.getServices(); |
| | | }, |
| | | fail: (err) => { |
| | | log.info('RtAuth: 连接失败', err); |
| | | |
| | | if (err.errno === 1509007 || err.errMsg.includes('already connected')) { |
| | | this.state.tryTimes = 0; |
| | | this.state.connected = true; |
| | | if (this.state.isScaning) this.stopScan(); |
| | | this.getServices(); |
| | | return; |
| | | } |
| | | |
| | | if (this.state.tryTimes < this.MAX_RETRY_TIMES) { |
| | | this.state.tryTimes++; |
| | | wx.closeBLEConnection({ deviceId: this.state.deviceId }); |
| | | setTimeout(() => this.openBluetoothAdapter(), 1500); |
| | | } else { |
| | | this.notifyStatus(0); |
| | | this.closeConnection(); |
| | | this.onError({ type: 'CONNECT_ERR', msg: '蓝牙连接失败,请靠近车辆重试' }); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | getServices() { |
| | | wx.getBLEDeviceServices({ |
| | | deviceId: this.state.deviceId, |
| | | success: (res) => { |
| | | const targetService = res.services.find(s => |
| | | s.uuid === this.serviceId || s.uuid.toUpperCase() === this.serviceId |
| | | ); |
| | | if (targetService) { |
| | | this.getCharacteristics(targetService.uuid); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | getCharacteristics(serviceId) { |
| | | wx.getBLEDeviceCharacteristics({ |
| | | deviceId: this.state.deviceId, |
| | | serviceId, |
| | | success: (res) => { |
| | | res.characteristics.forEach(item => { |
| | | if (item.properties.read) { |
| | | this.state.readChs = item.uuid; |
| | | this.state.readServiceId = serviceId; |
| | | } |
| | | if (item.properties.write && item.uuid === this.writeId) { |
| | | this.state.writeChs = item.uuid; |
| | | this.state.writeServiceId = serviceId; |
| | | } |
| | | if (item.properties.indicate) { |
| | | this.state.indecateChs = item.uuid; |
| | | this.state.indecateServiceId = serviceId; |
| | | this.setIndicateListener(); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | setIndicateListener() { |
| | | wx.notifyBLECharacteristicValueChange({ |
| | | deviceId: this.state.deviceId, |
| | | serviceId: this.state.indecateServiceId, |
| | | characteristicId: this.state.indecateChs, |
| | | state: true, |
| | | success: () => { |
| | | log.info('RtAuth: 蓝牙连接就绪'); |
| | | this.notifyStatus(2); |
| | | |
| | | wx.onBLECharacteristicValueChange((characteristic) => { |
| | | this.handleReceivedData(characteristic.value); |
| | | }); |
| | | |
| | | // 如果已配置 certificate 和 deviceModel,自动发送鉴权 |
| | | if (this.state.certificate && this.state.deviceModel) { |
| | | log.info('RtAuth: 自动发送鉴权'); |
| | | this.sendRtAuthData(this.state.certificate, this.state.deviceModel); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | this.listenConnectionState(); |
| | | } |
| | | |
| | | listenConnectionState() { |
| | | wx.onBLEConnectionStateChange((res) => { |
| | | if (!res.connected && this.state.connected) { |
| | | log.info('RtAuth: 蓝牙异常断开'); |
| | | this.closeConnection(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | closeConnection() { |
| | | log.info('RtAuth: 断开连接'); |
| | | this.stopScan(); |
| | | if (this.state.connected) { |
| | | wx.closeBLEConnection({ deviceId: this.state.deviceId }); |
| | | } |
| | | this.resetState(); |
| | | } |
| | | |
| | | // --- 数据处理 --- |
| | | |
| | | handleReceivedData(value) { |
| | | const dataHex = ab2hex(value); |
| | | log.info('RtAuth: 收到数据', dataHex); |
| | | |
| | | // RtAuth 响应 (MsgId = 0x0B) |
| | | if (dataHex.startsWith('0b') || dataHex.startsWith('0B')) { |
| | | this.handleRtAuthResponse(dataHex); |
| | | return; |
| | | } |
| | | |
| | | // RtNotification (MsgId = 0x0C) |
| | | if (dataHex.startsWith('0c') || dataHex.startsWith('0C')) { |
| | | this.handleRtNotification(dataHex); |
| | | return; |
| | | } |
| | | |
| | | log.info('RtAuth: 忽略非 0x0B/0x0C 数据'); |
| | | } |
| | | |
| | | handleRtAuthResponse(dataHex) { |
| | | log.info('RtAuth: 解析 RtAuth 响应'); |
| | | |
| | | try { |
| | | const wasmData = bleWasmModule.allocateUTF8(dataHex); |
| | | const wasmResult = bleWasmModule._parseRtAuthResponse(wasmData, dataHex.length / 2); |
| | | const resultStr = bleWasmModule.UTF8ToString(wasmResult); |
| | | bleWasmModule._free(wasmData); |
| | | |
| | | log.info('RtAuth: 解析结果', resultStr); |
| | | const result = JSON.parse(resultStr); |
| | | |
| | | if (result.error) { |
| | | this.onRtAuthResult({ success: false, error: result.error }); |
| | | } else if (result.rtAuthResult) { |
| | | const isSuccess = result.rtAuthResult.status === 0; |
| | | |
| | | // 鉴权成功,更新状态为 3(已鉴权) |
| | | if (isSuccess) { |
| | | this.notifyStatus(3); |
| | | } |
| | | |
| | | this.onRtAuthResult({ |
| | | success: isSuccess, |
| | | data: result.rtAuthResult |
| | | }); |
| | | } |
| | | } catch (e) { |
| | | log.info('RtAuth: 解析异常', e); |
| | | this.onRtAuthResult({ success: false, error: e.message || e }); |
| | | } |
| | | } |
| | | |
| | | handleRtNotification(dataHex) { |
| | | log.info('RtAuth: 解析 RtNotification'); |
| | | |
| | | try { |
| | | const wasmData = bleWasmModule.allocateUTF8(dataHex); |
| | | const wasmResult = bleWasmModule._parseRtNotification(wasmData, dataHex.length / 2); |
| | | const resultStr = bleWasmModule.UTF8ToString(wasmResult); |
| | | bleWasmModule._free(wasmData); |
| | | |
| | | log.info('RtAuth: 解析结果', resultStr); |
| | | const result = JSON.parse(resultStr); |
| | | |
| | | if (result.error) { |
| | | this.onRtNotification({ type: 'error', error: result.error }); |
| | | } else if (result.rtNotification) { |
| | | this.onRtNotification({ |
| | | type: 'pollingUnlock', |
| | | success: result.rtNotification.result === 0, |
| | | data: result.rtNotification |
| | | }); |
| | | } |
| | | } catch (e) { |
| | | log.info('RtAuth: 解析异常', e); |
| | | this.onRtNotification({ type: 'error', error: e.message || e }); |
| | | } |
| | | } |
| | | |
| | | // --- 发送数据 --- |
| | | |
| | | writeBLEData(data) { |
| | | const buffer = Uint8Array.from(data).buffer; |
| | | |
| | | wx.writeBLECharacteristicValue({ |
| | | deviceId: this.state.deviceId, |
| | | serviceId: this.state.writeServiceId, |
| | | characteristicId: this.state.writeChs, |
| | | value: buffer, |
| | | success: () => log.info('RtAuth: 发送成功'), |
| | | fail: (err) => log.info('RtAuth: 发送失败', err) |
| | | }); |
| | | } |
| | | |
| | | // 发送 RtAuth 鉴权数据 |
| | | sendRtAuthData(certificate, deviceModel) { |
| | | log.info('RtAuth: 发送鉴权数据'); |
| | | |
| | | if (this.state.connectStatus !== 2) { |
| | | this.onError({ type: 'RT_AUTH_ERR', msg: '蓝牙未连接' }); |
| | | return false; |
| | | } |
| | | |
| | | if (!certificate || !deviceModel) { |
| | | this.onError({ type: 'RT_AUTH_ERR', msg: '缺少 certificate 或 deviceModel' }); |
| | | return false; |
| | | } |
| | | |
| | | try { |
| | | // certificate: hex 字符串,长度除以 2 得到字节数 |
| | | // deviceModel: 普通字符串,使用 lengthBytesUTF8 计算 UTF-8 编码后的字节长度 |
| | | const deviceModelLength = bleWasmModule.lengthBytesUTF8(deviceModel); |
| | | |
| | | const wasmCertificate = bleWasmModule.allocateUTF8(certificate); |
| | | const wasmDeviceModel = bleWasmModule.allocateUTF8(deviceModel); |
| | | |
| | | const wasmResult = bleWasmModule._generateRtAuthData( |
| | | wasmCertificate, certificate.length / 2, // certificate 是 hex,字节数 = 长度/2 |
| | | wasmDeviceModel, deviceModelLength // deviceModel 使用 UTF-8 字节长度 |
| | | ); |
| | | |
| | | const rtAuthHex = bleWasmModule.UTF8ToString(wasmResult); |
| | | |
| | | bleWasmModule._free(wasmCertificate); |
| | | bleWasmModule._free(wasmDeviceModel); |
| | | |
| | | log.info('RtAuth: 生成数据', rtAuthHex); |
| | | this.writeBLEData(hexStringToArray(rtAuthHex)); |
| | | |
| | | return true; |
| | | } catch (e) { |
| | | log.info('RtAuth: 发送失败', e); |
| | | this.onError({ type: 'RT_AUTH_ERR', msg: e.message || e }); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 重试连接 |
| | | retryConnection() { |
| | | log.info('RtAuth: 重连'); |
| | | this.closeConnection(); |
| | | this.state.tryTimes = 0; |
| | | |
| | | setTimeout(() => { |
| | | if (!this.state.macAddress) { |
| | | this.onError({ type: 'CONFIG_ERR', msg: '缺少 macAddress' }); |
| | | return; |
| | | } |
| | | this.openBluetoothAdapter(); |
| | | }, 500); |
| | | } |
| | | } |