13404089107
2026-04-17 4d7fc08d66b2df1490db3dc9092049a7d44d221e
Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
4个文件已添加
10个文件已修改
5258 ■■■■■ 已修改文件
config/BuildDonePlugin.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/dev.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/index.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/site-list-item.tsx 29 ●●●●● 补丁 | 查看 | 原始文档 | 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/api/begin-route.ts
@@ -194,3 +194,12 @@
        payload
    }, true);
}
// 订单查询队列状态
export const queryOrderQueueStatus = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + `/itinerary/base/v1/mini-program/order/queue-status`,
        method: 'GET',
        payload
    }, true);
}
src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -34,6 +34,7 @@
  originBoxHidden: boolean;
  originOpacity: number;
  toOrigin: () => void;
  handleWalkNavigation: () => void;
  originIcon: string;
  selectRouteIcon: string;
  packUpIcon: string;
@@ -95,6 +96,7 @@
    originBoxHidden,
    originOpacity,
    toOrigin,
    handleWalkNavigation,
    originIcon,
    selectRouteIcon,
    packUpIcon,
@@ -162,6 +164,9 @@
        <View className='container-inner'>
          {shouldShowOrigin && (
            <View className='origin-box' style={{ opacity: originBoxHidden ? 0 : originOpacity, transition: 'opacity 0.2s ease-out', pointerEvents: originBoxHidden ? 'none' : 'auto' }}>
              {[4, 5].includes(tripStatus) && (
                <Image onClick={handleWalkNavigation} style={{ marginBottom: '16px' }} className='origin-icon' src={walkIcon} />
              )}
              <Image onClick={toOrigin} className='origin-icon' src={originIcon} />
              {
                canToggleRouteSelect &&
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx
@@ -1,4 +1,4 @@
import { View, Text, Image } from "@tarojs/components";
import { View, Text, Image, Button } from "@tarojs/components";
import changeDestinationIcon from "../../../../assets/change-destination.png";
import shareIcon from "../../../../assets/share-trip.png";
import cancelIcon from "../../../../assets/cancel-trip.png";
@@ -48,15 +48,28 @@
    return (
        <View className="trip-action-bar">
            {actionList.map(item => (
                <View
                    key={item.key}
                    className="trip-action-bar-item"
                    hoverClass="trip-action-bar-item-hover"
                    onClick={() => item.onClick?.()}
                >
                    <Image className="trip-action-bar-item-icon" src={item.icon} />
                    <Text className="trip-action-bar-item-text">{item.label}</Text>
                </View>
                item.key === 'share' ? (
                    <Button
                        key={item.key}
                        className="trip-action-bar-item trip-action-bar-share-btn"
                        hoverClass="trip-action-bar-item-hover"
                        openType="share"
                        onClick={() => item.onClick?.()}
                    >
                        <Image className="trip-action-bar-item-icon" src={item.icon} />
                        <Text className="trip-action-bar-item-text">{item.label}</Text>
                    </Button>
                ) : (
                    <View
                        key={item.key}
                        className="trip-action-bar-item"
                        hoverClass="trip-action-bar-item-hover"
                        onClick={() => item.onClick?.()}
                    >
                        <Image className="trip-action-bar-item-icon" src={item.icon} />
                        <Text className="trip-action-bar-item-text">{item.label}</Text>
                    </View>
                )
            ))}
        </View>
    );
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
@@ -1,8 +1,17 @@
import Taro from "@tarojs/taro";
import { View, Image } from "@tarojs/components";
import { useEffect, useState } from "react";
import { formatCountdown } from '../../../../../utils/Robotaxi-public-method';
import { queryOrderQueueStatus } from '../../../../api/begin-route';
type QueueStatus = {
    currentPosition: number;
    estimatedWaitTime: number;
    status: number;
    totalCount: number;
} | null;
type Props = {
    orderId?: string;
    tripStatus: number;
    waitMinutes: number | null;
    durationDisplay: { value: string | number; unit: string };
@@ -16,6 +25,7 @@
};
export default function TripStatusHeader({
    orderId,
    tripStatus,
    waitMinutes,
    durationDisplay,
@@ -27,6 +37,56 @@
    limitMinutes,
    handleOpenCancelPopup
}: Props) {
    const [queueStatus, setQueueStatus] = useState<QueueStatus>(null);
    useEffect(() => {
        if (tripStatus !== 3 || !orderId) {
            setQueueStatus(null);
            return;
        }
        let timer: ReturnType<typeof setInterval> | null = null;
        let unmounted = false;
        // 排队位次是动态变化的,状态为 3 时定时刷新
        const fetchQueueStatus = async () => {
            try {
                const res = await queryOrderQueueStatus({ orderId });
                if (unmounted) {
                    return;
                }
                const queueData = res?.data ?? res;
                if (
                    Number(queueData?.currentPosition) > 0 &&
                    Number(queueData?.totalCount) > 0
                ) {
                    setQueueStatus({
                        currentPosition: Number(queueData.currentPosition),
                        estimatedWaitTime: Number(queueData.estimatedWaitTime) || 0,
                        status: Number(queueData.status) || 0,
                        totalCount: Number(queueData.totalCount)
                    });
                    return;
                }
                setQueueStatus(null);
            } catch (_) {
                if (!unmounted) {
                    setQueueStatus(null);
                }
            }
        };
        fetchQueueStatus();
        timer = setInterval(fetchQueueStatus, 10000);
        return () => {
            unmounted = true;
            if (timer) {
                clearInterval(timer);
            }
        };
    }, [tripStatus, orderId]);
    const titleMap = {
        3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
        4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
@@ -55,7 +115,13 @@
                    <View className='calling-car-content-title'>{titleMap[tripStatus]}</View>
                )}
                {descMap[tripStatus] && (
                    <View style={{ paddingBottom: tripStatus === 3 ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View>
                    // <View style={{ paddingBottom: tripStatus === 3 ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View>
                    <View className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View>
                )}
                {tripStatus === 3 && queueStatus && (
                    <View className='calling-car-content-desc'>
                        第{queueStatus.currentPosition}位/共{queueStatus.totalCount}位
                    </View>
                )}
            </View>
            {
@@ -66,4 +132,4 @@
            }
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -114,6 +114,7 @@
            <Image className="calling-car-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' />
            <TripStatusHeader
                orderId={orderId}
                tripStatus={tripStatus}
                waitMinutes={waitMinutes}
                durationDisplay={durationDisplay}
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -179,6 +179,19 @@
          color: rgba(0, 0, 0, 0.9);
        }
      }
      &-share-btn {
        padding: 0;
        margin: 0;
        min-width: auto;
        line-height: normal;
        background: transparent;
        border: none;
        &::after {
          border: none;
        }
      }
    }
    &-arrive {
src/robotaxi/pages/begin-route/index.tsx
@@ -1,4 +1,4 @@
import Taro, { useDidShow } from "@tarojs/taro"
import Taro, { useDidShow, useShareAppMessage } from "@tarojs/taro"
import { useEffect, useState, useRef, useMemo } from 'react';
import { View, Image } from '@tarojs/components';
import './index.less';
@@ -114,8 +114,8 @@
    1: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false },
    2: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    3: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true },
    4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: true, viewport: 'route', translateCar: true },
    5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: true, viewport: 'car_user', translateCar: true },
    6: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    7: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'dropoff', translateCar: false },
    8: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
@@ -386,6 +386,21 @@
    getCurrentLongitude: () => longitude,
    getCurrentLatitude: () => latitude
  }); // 页面恢复与初始化编排
  const {
    openService,
    toCoupon,
    toOrder,
    toBack,
    handleCalloutTap,
    toSelectSite
  } = usePageActions({
    tripStatus,
    startSite,
    getCheckedEnter: () => isCheckedEnter.current,
    setCheckedEnter: (checked) => {
      isCheckedEnter.current = checked;
    }
  }); // 页面动作编排
  const router = Taro.useRouter();
  const tripRuntime = useTripRuntime({
    waitingTimerStartKey: WAITING_TIMER_START_KEY,
@@ -569,6 +584,13 @@
    latitude,
    longitude
  ]);
  const shareOrderId = orderId || Taro.getStorageSync('orderId') || '';
  const onShareAppMessage = () => ({
    title: '快来查看我的 Robotaxi 行程',
    path: `/robotaxi/pages/share-trip/index?orderId=${shareOrderId}`
  });
  useShareAppMessage(onShareAppMessage);
  useDidShow(() => {
    handleDidShow(router.params?.skipPageFlag);
@@ -617,7 +639,6 @@
  };
  const handleShareTrip = () => {
    isCheckedEnter.current = true;
    guardPrivacyThenNavigate('/robotaxi/pages/share-trip/index');
  };
  const { renderMapOverlays, renderTripCard } = useViewDelegates({
    isAndroid,
@@ -691,6 +712,19 @@
    isCheckedEnter.current = true;
    toOrder();
  };
  const handleWalkNavigation = () => {
    if (![4, 5].includes(tripStatus) || !startSite?.latitude || !startSite?.longitude) {
      return;
    }
    Taro.openLocation({
      latitude: Number(startSite.latitude),
      longitude: Number(startSite.longitude),
      name: startSite.name || '上车点',
      address: startSite.address || startSite.siteAddress || '',
      scale: 18
    });
  };
  return (
    <BeginRouteView
@@ -717,6 +751,7 @@
      originBoxHidden={originBoxHidden}
      originOpacity={originOpacity}
      toOrigin={toOrigin}
      handleWalkNavigation={handleWalkNavigation}
      selectRouteIcon={selectRouteIcon}
      packUpIcon={packUpIcon}
      onSelectRouteClick={() => setIsRouteSelectExpanded(!isRouteSelectExpanded)}
src/robotaxi/pages/site-search/components/site-list-item.tsx
@@ -1,7 +1,7 @@
import Taro from '@tarojs/taro';
import { View, Image } from '@tarojs/components';
import { useState, useEffect } from 'react';
import { collectPoint } from '../../../api/site-search';
import { collectPoint, getCollectPoints } from '../../../api/site-search';
type Props = {
  // 站点数据
@@ -33,16 +33,43 @@
  onSelect,
  renderHighlightedText
}: Props) {
  const MAX_COLLECT_COUNT = 50;
  const [isCollected, setIsCollected] = useState(!!item.isCollected);
  useEffect(() => {
    setIsCollected(item.isCollect === 1);
  }, [item.isCollect]);
  const checkCollectLimit = async () => {
    // 只取总数即可,避免为校验拉完整列表
    const res = await getCollectPoints({
      pageNum: 1,
      pageSize: 1
    });
    if (Number(res?.total) >= MAX_COLLECT_COUNT) {
      Taro.showToast({
        title: `最多收藏${MAX_COLLECT_COUNT}个站点`,
        icon: 'none'
      });
      return false;
    }
    return true;
  };
  const handleCollect = async (e: any) => {
    e.stopPropagation();
    try {
      const type = isCollected ? 2 : 1; // 1=收藏 2=取消收藏
      if (type === 1) {
        const canCollect = await checkCollectLimit();
        if (!canCollect) {
          return;
        }
      }
      const payload: any = {
        itemType: 1, // 默认为站点处理
        latitude: item.latitude,
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