UNPKG

t-comm

Version:

专业、稳定、纯粹的工具库

116 lines (115 loc) 5.54 kB
/** * BluetoothBump —— 纯蓝牙"碰一碰"模块 * * 设计目标: * - 只负责蓝牙:隐私授权 / 打开适配器 / 广播自己 / 扫描他人 / RSSI 判定 / 去重 / 优雅停止 * - 不感知业务:碰到对方后通过 onBump 回调把 (peerDeviceId, rssi) 抛出去 * - 跨平台兼容:iOS 必须分别初始化 central / peripheral;Android 一次 openBluetoothAdapter 即可 * - 可测试:把 wx 调用抽成 WxBluetoothAdapter,主类通过依赖注入,单测可注入 mock * * 使用方式: * const bt = new BluetoothBump({ onBump: (peerId, rssi) => { ... } }); * bt.start().then(() => { ... }); * bt.stop(); * * ⚠️ start() 必须在【用户点击事件回调】中调用,否则 iOS 报: * "openBluetoothAdapter:fail click action before resolve is needed" */ import { type WxBluetoothAdapter } from './wx-adapter'; import type { BluetoothBumpOptions } from './types'; /** 主类的额外依赖(用于测试时注入假时钟、假 timer、假 adapter) */ export interface BluetoothBumpDeps { adapter?: WxBluetoothAdapter; now?: () => number; setTimeoutFn?: (fn: () => void, ms: number) => any; clearTimeoutFn?: (handle: any) => void; setIntervalFn?: (fn: () => void, ms: number) => any; clearIntervalFn?: (handle: any) => void; } export declare class BluetoothBump { /** 我方临时 ID(每次启动随机生成,写到广播特征值里) */ myTempId: string; /** 当前运行平台 */ platform: string; /** 是否跳过广播(peripheral 初始化失败时降级为只扫描) */ skipAdvertising: boolean; /** 是否已经成功触发过碰一碰 */ hasBumped: boolean; private readonly serviceUuid; private readonly characteristicUuid; private readonly rssiThreshold; private readonly cooldownMs; private readonly lingerBeforeStopMs; private readonly pollIntervalMs; private readonly privacyContent; /** 匹配模式:simple(单向发现即触发)/ mutual(双向互认) */ private readonly mode; private readonly peerTtlMs; private readonly advertiseUpdateThrottleMs; private readonly onBump?; private readonly onError?; private readonly onLog?; private readonly onDeviceFoundCallback?; private readonly adapter; private readonly now; private readonly setTimeoutFn; private readonly clearTimeoutFn; private readonly setIntervalFn; private readonly clearIntervalFn; private peripheralServer; private isDiscovering; private bumpedMap; private gracefulStopTimer; private pollTimer; /** mutual 模式:候选 peer 池,key = peerTempId */ private seenPeers; /** mutual 模式:当前广播中"点名"的 peerId(为空表示还没看到任何人) */ private currentAdvertisedPeer; /** mutual 模式:上次更新广播的时间戳(用于节流) */ private lastAdvertiseUpdateAt; /** mutual 模式:updateAdvertisedValue 连续失败后降级为 simple(仅当前会话内) */ private advertiseUpdateBroken; private deviceFoundCb; private adapterStateChangeCb; constructor(opts?: BluetoothBumpOptions, deps?: BluetoothBumpDeps); /** 启动碰一碰(必须在用户点击事件回调中调用) */ start(): Promise<void>; /** 停止:解绑所有监听 + 停止扫描 + 停止广播 + 关闭适配器 */ stop(): void; /** 启动失败的统一处理(业务方可通过 onError 拿到原始错误) */ private handleStartError; /** 监听蓝牙适配器状态变化:用户打开蓝牙后自动重试 */ private watchAdapterStateForRetry; /** 监听设备发现 + 启动 iOS 兜底轮询 */ private bindDeviceFound; /** 调试用:定期 getBluetoothDevices,避免 iOS 上 allowDuplicatesKey 不上报重复的问题 */ private startPolling; /** 处理扫到的单个设备 */ private handleDevice; /** * mutual 模式(v2,方案2 实现):协议双向 simple * * 设计取舍: * v1 mutual 用"动态更新广播 payload 的 seenPeerId"做双向互认,但 iOS 上 * wx 的 updateBLEAdvertising → stop+addService+start 链路在真机上极不可靠: * - deviceName 不会真正刷新(CoreBluetooth 缓存) * - addService 重复注册会失败、走兜底分支后广播就停了 * - 高频 update 还会让 iOS 端事件循环卡死 * 导致弱信号端永远收不到"被点名"广播 → 不弹窗。 * * v2 直接退一步: * - 不再动态更新广播 payload;广播一次启动后保持初始 payload 不变 * - 触发条件:扫到的对端必须能解析出 BUMP_ 协议 payload(协议匹配) + 本机 RSSI ≥ 阈值 * - "双向"靠两端都跑这套规则、各自独立达阈值时触发;阈值收紧(≈10cm)就足够防误触 * - iOS↔Android RSSI 不对称的代价:可能两端弹窗时差最大到秒级(取决于谁先稳定过阈值) * 但相比 v1 的"一端永远不弹",这是更可接受的退化 */ private handleDeviceMutual; /** mutual 模式:更新自己广播里的 seenPeerId(带节流 + 失败降级) */ private updateAdvertisedPeer; /** 触发碰一碰成功:通知业务方 + 优雅停止(保持广播一段时间) */ private triggerBump; /** 仅停止扫描(保留广播) */ private stopDiscoveryOnly; private log; }