lucl
2026-04-17 14e63b5502b93ded904238e5949fb89c1741a8a3
feat: 导入蓝牙车控 sdk
4个文件已添加
2个文件已修改
5053 ■■■■■ 已修改文件
config/BuildDonePlugin.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/dev.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/index.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/car-control/RtAuthBluetoothManager.js 432 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/car-control/bleMethod.js 4557 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/car-control/bleMethod.wasm.br 补丁 | 查看 | 原始文档 | blame | 历史
config/BuildDonePlugin.js
New file
@@ -0,0 +1,45 @@
const fs = require('fs-extra');
const path = require('path');
// 需要在构建完成后拷贝到产物目录的文件列表
// src: 相对于项目根目录的源文件路径
// dest: 相对于产物目录(outputPath)的目标文件路径
const filesToCopy = [
  // {
  //   src: 'src/otherPage/pages/carControl/bleMethod.wasm.br',
  //   dest: 'otherPage/pages/carControl/bleMethod.wasm.br',
  // },
  {
    src: 'src/robotaxi/utils/car-control/bleMethod.wasm.br',
    dest: 'robotaxi/utils/car-control/bleMethod.wasm.br',
  },
];
class BuildDonePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('BuildDonePlugin', (stats) => {
      if (process.env.TARO_ENV !== 'weapp') return;
      const outputPath = stats.compilation.outputOptions.path;
      filesToCopy.forEach(({ src, dest }) => {
        const srcFile = path.resolve(src);
        const destFile = path.resolve(outputPath, dest);
        const destDir = path.dirname(destFile);
        if (!fs.existsSync(destDir)) {
          fs.mkdirSync(destDir, { recursive: true });
        }
        try {
          fs.copyFileSync(srcFile, destFile);
          console.log(`✅ copy: ${src} -> ${dest}`);
        } catch (error) {
          console.error(`❌ copy: ${src} -> ${dest}: ${error.message}`);
        }
      });
    });
  }
}
module.exports = BuildDonePlugin;
config/dev.js
@@ -116,6 +116,9 @@
              },
            ],
          },
          buildDone: {
            plugin: require('./BuildDonePlugin'),
          },
        },
      })
    },
config/index.js
@@ -48,16 +48,6 @@
  ]
}
class BuildDonePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Build Done Plugin', () => {
      // if (process.env.TARO_ENV === 'alipay') {
      //   fs.copyFileSync('./mini.project.json', './dist/alipay/mini.project.json')
      // }
    });
  }
}
const config = {
  projectName: "xp-robotaxi-miniprogram",
  date: "2020-2-11",
@@ -138,9 +128,9 @@
    webpackChain(chain) {
      chain.merge({
        plugin: {
          install: {
            plugin: BuildDonePlugin
          }
          buildDone: {
            plugin: require('./BuildDonePlugin'),
          },
        }
      });
    }
src/robotaxi/utils/car-control/RtAuthBluetoothManager.js
New file
@@ -0,0 +1,432 @@
// 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);
  }
}
src/robotaxi/utils/car-control/bleMethod.js
New file
Diff too large
src/robotaxi/utils/car-control/bleMethod.wasm.br
Binary files differ