t-comm
Version:
专业、稳定、纯粹的工具库
116 lines (115 loc) • 5.54 kB
TypeScript
/**
* 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;
}