| | |
| | | type BluetoothTokenPayload = { |
| | | token?: string; // 鉴权凭证 |
| | | macAddress?: string; // 主地址(字符串) |
| | | slaveMacList?: string[]; // 从地址列表(字符串数组) |
| | | }; |
| | | |
| | | type ConnectStatus = 0 | 1 | 2 | 3; |
| | | const RETRY_UNLOCK_MESSAGE = '请点击解锁按钮解锁车门'; // 鉴权失败/解锁失败统一提示文案 |
| | | const UNLOCK_SUCCESS_MESSAGE = '车门已解锁,请上车'; // 解锁成功提示文案 |
| | | const RETRY_DELAY_MS = 1500; // 状态失败后的自动重连延迟毫秒数 |
| | | |
| | | /** |
| | | * 归一化 MAC 地址,统一去空格并转大写。 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 从 token 接口结果构建蓝牙地址候选列表:主地址优先,其次从地址列表,自动去重。 |
| | | */ |
| | | function buildMacCandidates(macAddress: unknown, slaveMacList: unknown): string[] { |
| | | const candidates = [normalizeMac(macAddress)]; |
| | | if (Array.isArray(slaveMacList)) { |
| | | slaveMacList.forEach((item) => { |
| | | candidates.push(normalizeMac(item)); |
| | | }); |
| | | } |
| | | return Array.from(new Set(candidates.filter(Boolean))); |
| | | } |
| | | |
| | | /** |
| | | * 判断错误是否属于可切换 MAC 重试的连接链路错误。 |
| | | */ |
| | | function isRetryableConnectError(error: any): boolean { |
| | | const type = String(error?.type || '').toUpperCase(); |
| | | if (type === 'CONNECT_ERR' || type === 'ADAPTER_ERR') return true; |
| | | const msg = String(error?.msg || error?.errMsg || ''); |
| | | return /(connect|连接|adapter|蓝牙适配器|timeout|超时|discovery)/i.test(msg); |
| | | } |
| | | |
| | | /** |
| | | * 解析设备型号,优先使用系统型号。 |
| | | */ |
| | | function resolveDeviceModel(): string { |
| | | try { |
| | | const info = Taro.getSystemInfoSync(); |
| | | return info?.model || 'MiniProgram Device'; |
| | | const rawModel = String(info?.model || '').trim(); // 小程序系统返回的原始机型字段 |
| | | const bracketMatchedModel = rawModel.match(/<\s*([^>]+)\s*>/); // 提取尖括号内硬件型号,例如 iPhone18,4 |
| | | if (bracketMatchedModel?.[1]) { |
| | | return bracketMatchedModel[1].replace(/\s+/g, ''); |
| | | } |
| | | if (rawModel) { |
| | | return rawModel.replace(/\s+/g, ''); |
| | | } |
| | | return 'MiniProgram Device'; |
| | | } catch (error) { |
| | | return 'MiniProgram Device'; |
| | | } |
| | |
| | | const managerRef = useRef<any>(null); // 蓝牙管理器实例引用 |
| | | const lastStartKeyRef = useRef(''); // 最近一次启动鉴权的参数签名 |
| | | const startAuthTaskRef = useRef<Promise<void> | null>(null); // 当前进行中的鉴权任务,防止并发重复请求 |
| | | const candidateMacsRef = useRef<string[]>([]); // 蓝牙地址候选列表(主 + 从) |
| | | const currentMacIndexRef = useRef(0); // 当前尝试的 MAC 索引 |
| | | const certificateRef = useRef(''); // 当前会话证书缓存 |
| | | const macAddressRef = useRef(''); // 当前会话主蓝牙地址缓存 |
| | | const deviceModelRef = useRef(resolveDeviceModel()); // 当前设备型号缓存 |
| | | const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 状态失败后的自动重连定时器 |
| | | const suppressAutoRetryRef = useRef(false); // 主动关闭/解锁成功后的自动重连抑制标记 |
| | | const unlockFinishedRef = useRef(false); // 当前轮次是否已出现解锁成功 |
| | | const [connectStatus, setConnectStatus] = useState<ConnectStatus>(0); // 蓝牙连接状态:0断开/1连接中/2已连接/3已鉴权 |
| | | const [authSuccess, setAuthSuccess] = useState(false); // RtAuth 鉴权是否成功 |
| | | const [isAuthorizing, setIsAuthorizing] = useState(false); // 鉴权流程进行中 |
| | |
| | | }, []); // 统一展示蓝牙解锁结果提示 |
| | | |
| | | /** |
| | | * 切换到下一个候选 MAC 重试连接。 |
| | | * 清理自动重连定时器。 |
| | | */ |
| | | const tryFallbackMacConnection = useCallback(() => { |
| | | if (currentMacIndexRef.current >= candidateMacsRef.current.length - 1) { |
| | | return false; |
| | | const clearRetryTimer = useCallback(() => { |
| | | if (retryTimerRef.current) { |
| | | clearTimeout(retryTimerRef.current); |
| | | retryTimerRef.current = null; |
| | | } |
| | | const nextIndex = currentMacIndexRef.current + 1; |
| | | const nextMac = candidateMacsRef.current[nextIndex]; |
| | | if (!nextMac || !managerRef.current) { |
| | | return false; |
| | | }, []); |
| | | |
| | | /** |
| | | * 主动断开蓝牙连接并重置状态。 |
| | | */ |
| | | const closeConnection = useCallback(() => { |
| | | console.log('[蓝牙] 主动断开连接'); |
| | | suppressAutoRetryRef.current = true; |
| | | clearRetryTimer(); |
| | | if (managerRef.current) { |
| | | managerRef.current.closeConnection(); |
| | | } |
| | | currentMacIndexRef.current = nextIndex; |
| | | console.log('[蓝牙] 当前地址连接失败,切换从地址重试:', nextMac); |
| | | managerRef.current.updateCarConfig({ |
| | | macAddress: nextMac, |
| | | certificate: certificateRef.current, |
| | | deviceModel: deviceModelRef.current |
| | | }); |
| | | managerRef.current.retryConnection(); |
| | | return true; |
| | | authSuccessRef.current = false; |
| | | isAuthorizingRef.current = false; |
| | | startAuthTaskRef.current = null; |
| | | setConnectStatus(0); |
| | | setAuthSuccess(false); |
| | | setIsAuthorizing(false); |
| | | }, [clearRetryTimer]); |
| | | |
| | | /** |
| | | * 调度一次状态失败后的自动重连。 |
| | | */ |
| | | const scheduleRetryConnection = useCallback(() => { |
| | | if (retryTimerRef.current || suppressAutoRetryRef.current || unlockFinishedRef.current) { |
| | | return; |
| | | } |
| | | retryTimerRef.current = setTimeout(() => { |
| | | retryTimerRef.current = null; |
| | | if (suppressAutoRetryRef.current || unlockFinishedRef.current || !managerRef.current) { |
| | | return; |
| | | } |
| | | isAuthorizingRef.current = true; |
| | | setIsAuthorizing(true); |
| | | setAuthError(''); |
| | | console.log('[蓝牙] status 非 3,触发延迟重连'); |
| | | managerRef.current.retryConnection(); |
| | | }, RETRY_DELAY_MS); |
| | | }, []); |
| | | |
| | | /** |
| | |
| | | console.log('[蓝牙] 连接状态变更:', status); |
| | | setConnectStatus(status); |
| | | if (status === 3) { |
| | | clearRetryTimer(); |
| | | authSuccessRef.current = true; |
| | | isAuthorizingRef.current = false; |
| | | setAuthSuccess(true); |
| | | setAuthError(''); |
| | | setIsAuthorizing(false); |
| | | return; |
| | | } |
| | | scheduleRetryConnection(); |
| | | }, |
| | | onError: (error: any) => { |
| | | console.log('[蓝牙] 连接错误:', error); |
| | | if (isRetryableConnectError(error) && tryFallbackMacConnection()) { |
| | | isAuthorizingRef.current = false; |
| | | if (unlockFinishedRef.current) { |
| | | return; |
| | | } |
| | | isAuthorizingRef.current = false; |
| | | setAuthError(error?.msg || '蓝牙连接失败'); |
| | | setIsAuthorizing(false); |
| | | }, |
| | |
| | | console.log('[蓝牙] 解锁通知:', result); |
| | | if (result?.type === 'pollingUnlock') { |
| | | if (result?.success) { |
| | | unlockFinishedRef.current = true; |
| | | setAuthError(''); |
| | | showUnlockToast(UNLOCK_SUCCESS_MESSAGE); |
| | | closeConnection(); |
| | | } else { |
| | | setAuthError(RETRY_UNLOCK_MESSAGE); |
| | | showUnlockToast(RETRY_UNLOCK_MESSAGE); |
| | |
| | | return; |
| | | } |
| | | if (result?.type === 'error') { |
| | | if (unlockFinishedRef.current) { |
| | | return; |
| | | } |
| | | setAuthError(RETRY_UNLOCK_MESSAGE); |
| | | showUnlockToast(RETRY_UNLOCK_MESSAGE); |
| | | } |
| | | } |
| | | }); |
| | | return managerRef.current; |
| | | }, [showUnlockToast, tryFallbackMacConnection]); |
| | | |
| | | /** |
| | | * 主动断开蓝牙连接并重置状态。 |
| | | */ |
| | | const closeConnection = useCallback(() => { |
| | | console.log('[蓝牙] 主动断开连接'); |
| | | if (managerRef.current) { |
| | | managerRef.current.closeConnection(); |
| | | } |
| | | authSuccessRef.current = false; |
| | | isAuthorizingRef.current = false; |
| | | startAuthTaskRef.current = null; |
| | | setConnectStatus(0); |
| | | setAuthSuccess(false); |
| | | setIsAuthorizing(false); |
| | | }, []); |
| | | }, [showUnlockToast, closeConnection, scheduleRetryConnection, clearRetryTimer]); |
| | | |
| | | /** |
| | | * 触发蓝牙连接与 RtAuth 鉴权。 |
| | |
| | | const startKey = `${orderId}`; |
| | | if (isAuthorizingRef.current || (authSuccessRef.current && lastStartKeyRef.current === startKey)) return; |
| | | lastStartKeyRef.current = startKey; |
| | | suppressAutoRetryRef.current = false; |
| | | unlockFinishedRef.current = false; |
| | | clearRetryTimer(); |
| | | isAuthorizingRef.current = true; |
| | | setIsAuthorizing(true); |
| | | setAuthError(''); |
| | |
| | | const tokenRes = await getBluetoothUnlockToken(orderId); |
| | | const tokenPayload: BluetoothTokenPayload = typeof tokenRes === 'object' && tokenRes !== null ? tokenRes as BluetoothTokenPayload : {}; |
| | | const certificate = typeof tokenPayload.token === 'string' ? tokenPayload.token : String(tokenPayload.token || ''); |
| | | const candidateMacs = buildMacCandidates(tokenPayload.macAddress, tokenPayload.slaveMacList); |
| | | console.log('[蓝牙] 获取凭证与地址成功:', { tokenLength: certificate?.length || 0, macCount: candidateMacs.length }); |
| | | const mainMacAddress = normalizeMac(tokenPayload.macAddress); |
| | | console.log('[蓝牙] 获取凭证与主地址成功:', { tokenLength: certificate?.length || 0, hasMainMac: Boolean(mainMacAddress) }); |
| | | if (!certificate) { |
| | | isAuthorizingRef.current = false; |
| | | setAuthError('蓝牙鉴权凭证为空'); |
| | | setIsAuthorizing(false); |
| | | return; |
| | | } |
| | | if (candidateMacs.length === 0) { |
| | | if (!mainMacAddress) { |
| | | isAuthorizingRef.current = false; |
| | | setAuthError('蓝牙地址为空'); |
| | | setIsAuthorizing(false); |
| | | return; |
| | | } |
| | | certificateRef.current = certificate; |
| | | candidateMacsRef.current = candidateMacs; |
| | | currentMacIndexRef.current = 0; |
| | | macAddressRef.current = mainMacAddress; |
| | | const manager = ensureManager(); |
| | | console.log('[蓝牙] 更新车辆配置(主地址):', { macAddress: candidateMacs[0], deviceModel: deviceModelRef.current }); |
| | | console.log('[蓝牙] 更新车辆配置(主地址):', { macAddress: mainMacAddress, deviceModel: deviceModelRef.current }); |
| | | manager.updateCarConfig({ |
| | | macAddress: candidateMacs[0], |
| | | macAddress: mainMacAddress, |
| | | certificate, |
| | | deviceModel: deviceModelRef.current |
| | | }); |
| | |
| | | console.log('[蓝牙] 当前状态非4/5,断开连接:', tripStatus); |
| | | closeConnection(); |
| | | lastStartKeyRef.current = ''; |
| | | candidateMacsRef.current = []; |
| | | currentMacIndexRef.current = 0; |
| | | certificateRef.current = ''; |
| | | macAddressRef.current = ''; |
| | | startAuthTaskRef.current = null; |
| | | authSuccessRef.current = false; |
| | | isAuthorizingRef.current = false; |
| | | unlockFinishedRef.current = false; |
| | | setAuthError(''); |
| | | } |
| | | }, [tripStatus, closeConnection]); |
| | | |
| | | useEffect(() => { |
| | | return () => { |
| | | clearRetryTimer(); |
| | | if (managerRef.current) { |
| | | managerRef.current.closeConnection(); |
| | | } |
| | | }; |
| | | }, []); |
| | | }, [clearRetryTimer]); |
| | | |
| | | return { |
| | | connectStatus, |