t-comm
Version:
专业、稳定、纯粹的工具库
432 lines (425 loc) • 20.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _classCallCheck = require('@babel/runtime/helpers/classCallCheck');
var _createClass = require('@babel/runtime/helpers/createClass');
var bluetoothBump_constants = require('./constants.js');
var bluetoothBump_utils = require('./utils.js');
var bluetoothBump_wxAdapter = require('./wx-adapter.js');
require('@babel/runtime/helpers/slicedToArray');
require('@babel/runtime/helpers/typeof');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var _classCallCheck__default = /*#__PURE__*/_interopDefaultLegacy(_classCallCheck);
var _createClass__default = /*#__PURE__*/_interopDefaultLegacy(_createClass);
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
var BluetoothBump = /*#__PURE__*/function () {
function BluetoothBump() {
var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var deps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck__default["default"](this, BluetoothBump);
// -------- 运行时状态(public) --------
/** 我方临时 ID(每次启动随机生成,写到广播特征值里) */
this.myTempId = '';
/** 当前运行平台 */
this.platform = 'unknown';
/** 是否跳过广播(peripheral 初始化失败时降级为只扫描) */
this.skipAdvertising = false;
/** 是否已经成功触发过碰一碰 */
this.hasBumped = false;
// -------- 运行时状态(private) --------
this.peripheralServer = null;
this.isDiscovering = false;
this.bumpedMap = new Map();
this.gracefulStopTimer = null;
this.pollTimer = null;
/** mutual 模式:候选 peer 池,key = peerTempId */
this.seenPeers = new Map();
/** mutual 模式:当前广播中"点名"的 peerId(为空表示还没看到任何人) */
this.currentAdvertisedPeer = '';
/** mutual 模式:上次更新广播的时间戳(用于节流) */
this.lastAdvertiseUpdateAt = 0;
/** mutual 模式:updateAdvertisedValue 连续失败后降级为 simple(仅当前会话内) */
this.advertiseUpdateBroken = false;
this.deviceFoundCb = null;
this.adapterStateChangeCb = null;
this.serviceUuid = opts.serviceUuid || bluetoothBump_constants.DEFAULT_SERVICE_UUID;
this.characteristicUuid = opts.characteristicUuid || bluetoothBump_constants.DEFAULT_CHARACTERISTIC_UUID;
this.rssiThreshold = typeof opts.rssiThreshold === 'number' ? opts.rssiThreshold : bluetoothBump_constants.DEFAULT_RSSI_THRESHOLD;
this.cooldownMs = typeof opts.cooldownMs === 'number' ? opts.cooldownMs : bluetoothBump_constants.DEFAULT_COOLDOWN_MS;
this.lingerBeforeStopMs = typeof opts.lingerBeforeStopMs === 'number' ? opts.lingerBeforeStopMs : bluetoothBump_constants.DEFAULT_LINGER_BEFORE_STOP_MS;
this.pollIntervalMs = typeof opts.pollIntervalMs === 'number' ? opts.pollIntervalMs : bluetoothBump_constants.DEFAULT_POLL_INTERVAL_MS;
this.privacyContent = opts.privacyContent || bluetoothBump_constants.DEFAULT_PRIVACY_CONTENT;
this.mode = opts.mode === 'mutual' ? 'mutual' : 'simple';
this.peerTtlMs = typeof opts.peerTtlMs === 'number' ? opts.peerTtlMs : bluetoothBump_constants.DEFAULT_PEER_TTL_MS;
this.advertiseUpdateThrottleMs = typeof opts.advertiseUpdateThrottleMs === 'number' ? opts.advertiseUpdateThrottleMs : bluetoothBump_constants.DEFAULT_ADVERTISE_UPDATE_THROTTLE_MS;
this.onBump = opts.onBump;
this.onError = opts.onError;
this.onLog = opts.onLog;
this.onDeviceFoundCallback = opts.onDeviceFound;
this.adapter = deps.adapter || bluetoothBump_wxAdapter.wxBluetoothAdapter;
this.now = deps.now || function () {
return Date.now();
};
this.setTimeoutFn = deps.setTimeoutFn || function (fn, ms) {
return setTimeout(fn, ms);
};
this.clearTimeoutFn = deps.clearTimeoutFn || function (h) {
return clearTimeout(h);
};
this.setIntervalFn = deps.setIntervalFn || function (fn, ms) {
return setInterval(fn, ms);
};
this.clearIntervalFn = deps.clearIntervalFn || function (h) {
return clearInterval(h);
};
}
// ==================== 对外 API ====================
/** 启动碰一碰(必须在用户点击事件回调中调用) */
return _createClass__default["default"](BluetoothBump, [{
key: "start",
value: function start() {
var _this = this;
var _a;
if (!this.adapter.isAvailable()) {
this.log('当前环境不是微信小游戏,无法使用蓝牙 API');
(_a = this.onError) === null || _a === void 0 ? void 0 : _a.call(this, {
errMsg: 'not in wx env'
});
return Promise.resolve();
}
this.myTempId = bluetoothBump_utils.genTempId();
this.platform = this.adapter.detectPlatform();
this.skipAdvertising = false;
this.seenPeers.clear();
this.currentAdvertisedPeer = '';
this.lastAdvertiseUpdateAt = 0;
this.advertiseUpdateBroken = false;
this.log("start: platform=".concat(this.platform, ", myTempId=").concat(this.myTempId, ", mode=").concat(this.mode));
return this.adapter.ensurePrivacyAuthorized(this.privacyContent, function (m) {
return _this.log(m);
}).then(function () {
_this.log('✅ 隐私协议已授权');
return _this.adapter.openAdapter(_this.platform, function (m) {
return _this.log(m);
});
}).then(function (openRes) {
_this.skipAdvertising = openRes.skipAdvertising;
_this.log('✅ 蓝牙适配器已打开');
if (_this.skipAdvertising) {
_this.log('⚠️ 跳过广播(仅扫描模式)');
return undefined;
}
return _this.adapter.startAdvertising({
serviceUuid: _this.serviceUuid,
characteristicUuid: _this.characteristicUuid,
tempId: _this.myTempId,
// 始终广播完整 payload(simple 模式下 seenPeerId 留空):
// 这样对端不论 simple/mutual 都能稳定解析出我们的 myTempId,
// 业务层 onBump 拿到的 peerTempId 才不会为空,两台手机的"我方ID/对方ID"才能对上。
// mutual 模式后续会通过 updateAdvertisedValue 把 seenPeerId 填上。
payload: bluetoothBump_utils.encodePayload(_this.myTempId, '')
}).then(function (advRes) {
_this.peripheralServer = advRes.server;
_this.log("\u2705 \u5DF2\u5F00\u59CB\u5E7F\u64AD\uFF0C\u4E34\u65F6ID=".concat(_this.myTempId));
})["catch"](function (advErr) {
_this.log("\u26A0\uFE0F \u5E7F\u64AD\u542F\u52A8\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u53EA\u626B\u63CF\u6A21\u5F0F\uFF1A".concat((advErr === null || advErr === void 0 ? void 0 : advErr.errMsg) || advErr));
});
}).then(function () {
return _this.adapter.startDiscovery({
platform: _this.platform,
serviceUuid: _this.serviceUuid
});
}).then(function () {
_this.isDiscovering = true;
_this.log('✅ 已开始扫描附近设备');
_this.bindDeviceFound();
_this.log("\uD83C\uDFAF \u7B49\u5F85\u9760\u8FD1\u53E6\u4E00\u53F0\u624B\u673A\uFF08RSSI > ".concat(_this.rssiThreshold, " \u65F6\u89E6\u53D1\uFF09"));
})["catch"](function (err) {
_this.handleStartError(err);
});
}
/** 停止:解绑所有监听 + 停止扫描 + 停止广播 + 关闭适配器 */
}, {
key: "stop",
value: function stop() {
if (this.gracefulStopTimer) {
this.clearTimeoutFn(this.gracefulStopTimer);
this.gracefulStopTimer = null;
}
if (this.pollTimer) {
this.clearIntervalFn(this.pollTimer);
this.pollTimer = null;
}
if (this.deviceFoundCb) {
this.adapter.offDeviceFound(this.deviceFoundCb);
this.deviceFoundCb = null;
}
if (this.adapterStateChangeCb) {
this.adapter.offAdapterStateChange(this.adapterStateChangeCb);
this.adapterStateChangeCb = null;
}
if (this.isDiscovering) {
this.adapter.stopDiscovery();
this.isDiscovering = false;
}
if (this.peripheralServer) {
this.adapter.stopAdvertising(this.peripheralServer);
this.peripheralServer = null;
}
this.seenPeers.clear();
this.currentAdvertisedPeer = '';
this.adapter.closeAdapter();
this.log('🛑 已停止');
}
// ==================== 内部流程 ====================
/** 启动失败的统一处理(业务方可通过 onError 拿到原始错误) */
}, {
key: "handleStartError",
value: function handleStartError(err) {
var _a;
var errCode = err === null || err === void 0 ? void 0 : err.errCode;
var errMsg = (err === null || err === void 0 ? void 0 : err.errMsg) || JSON.stringify(err);
this.log("\u274C \u542F\u52A8\u5931\u8D25\uFF1A".concat(errMsg));
if (errCode === 10001) {
this.log('⚠️ 系统蓝牙未开启,监听适配器状态以便自动重试');
this.watchAdapterStateForRetry();
}
(_a = this.onError) === null || _a === void 0 ? void 0 : _a.call(this, {
errCode: errCode,
errMsg: errMsg,
raw: err
});
}
/** 监听蓝牙适配器状态变化:用户打开蓝牙后自动重试 */
}, {
key: "watchAdapterStateForRetry",
value: function watchAdapterStateForRetry() {
var _this2 = this;
if (this.adapterStateChangeCb) return;
this.adapterStateChangeCb = function (res) {
_this2.log('🔄 蓝牙适配器状态变化', res);
if (res.available && !_this2.isDiscovering) {
_this2.log('✅ 检测到蓝牙已开启,自动重试');
if (_this2.adapterStateChangeCb) {
_this2.adapter.offAdapterStateChange(_this2.adapterStateChangeCb);
_this2.adapterStateChangeCb = null;
}
_this2.start();
}
};
this.adapter.onAdapterStateChange(this.adapterStateChangeCb);
}
/** 监听设备发现 + 启动 iOS 兜底轮询 */
}, {
key: "bindDeviceFound",
value: function bindDeviceFound() {
var _this3 = this;
this.deviceFoundCb = function (res) {
var _a;
var devices = (res === null || res === void 0 ? void 0 : res.devices) || [];
var _iterator = _createForOfIteratorHelper(devices),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var dev = _step.value;
(_a = _this3.onDeviceFoundCallback) === null || _a === void 0 ? void 0 : _a.call(_this3, dev);
_this3.handleDevice(dev);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
};
this.adapter.onDeviceFound(this.deviceFoundCb);
this.startPolling();
}
/** 调试用:定期 getBluetoothDevices,避免 iOS 上 allowDuplicatesKey 不上报重复的问题 */
}, {
key: "startPolling",
value: function startPolling() {
var _this4 = this;
if (this.pollTimer) return;
var tick = function tick() {
if (_this4.hasBumped || !_this4.isDiscovering) {
if (_this4.pollTimer) {
_this4.clearIntervalFn(_this4.pollTimer);
_this4.pollTimer = null;
}
return;
}
_this4.adapter.getDevices().then(function (res) {
var list = res.devices || [];
var _iterator2 = _createForOfIteratorHelper(list),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var dev = _step2.value;
if (bluetoothBump_utils.matchesService(dev, _this4.serviceUuid)) {
_this4.handleDevice(dev);
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
})["catch"](function () {
// 轮询失败忽略,等下次 tick
});
};
this.pollTimer = this.setIntervalFn(tick, this.pollIntervalMs);
}
/** 处理扫到的单个设备 */
}, {
key: "handleDevice",
value: function handleDevice(dev) {
if (this.hasBumped) return;
// iOS 不传 services 时,需要手动过滤同伴
if (this.platform === 'ios' && !bluetoothBump_utils.matchesService(dev, this.serviceUuid)) {
return;
}
// mutual 模式:走双向互认流程
if (this.mode === 'mutual' && !this.advertiseUpdateBroken) {
this.handleDeviceMutual(dev);
return;
}
var lastTime = this.bumpedMap.get(dev.deviceId) || 0;
var now = this.now();
var triggerable = bluetoothBump_utils.shouldTriggerBump({
rssi: dev.RSSI,
threshold: this.rssiThreshold,
lastBumpTime: lastTime,
now: now,
cooldownMs: this.cooldownMs
});
if (!triggerable) return;
this.bumpedMap.set(dev.deviceId, now);
// simple 模式下也尽量解析出对方 payload 里的 myTempId(解析失败也无所谓,传空串)
var peerPayload = bluetoothBump_utils.parsePayload(dev, this.serviceUuid);
if (!peerPayload) {
// 调试:解析失败时把对端广播的"形态"打出来,方便定位是 serviceData/name 没下发还是格式不对
var sdKeys = dev.serviceData ? Object.keys(dev.serviceData).join(',') : '(无)';
var nameStr = dev.localName || dev.name || '(无)';
this.log("\u26A0\uFE0F parsePayload \u5931\u8D25\uFF1AdeviceId=".concat(dev.deviceId, " name=\"").concat(nameStr, "\" serviceData.keys=[").concat(sdKeys, "] hasAdvertisData=").concat(!!dev.advertisData));
}
this.triggerBump(dev, (peerPayload === null || peerPayload === void 0 ? void 0 : peerPayload.myTempId) || '');
}
/**
* 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 的"一端永远不弹",这是更可接受的退化
*/
}, {
key: "handleDeviceMutual",
value: function handleDeviceMutual(dev) {
var payload = bluetoothBump_utils.parsePayload(dev, this.serviceUuid);
if (!payload) return; // 不是本协议的广播,忽略(核心防误触:必须是 BUMP_ 协议设备)
if (payload.myTempId === this.myTempId) return; // 扫到自己,忽略
var lastTime = this.bumpedMap.get(dev.deviceId) || 0;
var now = this.now();
var triggerable = bluetoothBump_utils.shouldTriggerBump({
rssi: dev.RSSI,
threshold: this.rssiThreshold,
lastBumpTime: lastTime,
now: now,
cooldownMs: this.cooldownMs
});
if (!triggerable) return;
this.bumpedMap.set(dev.deviceId, now);
this.triggerBump(dev, payload.myTempId);
}
/** mutual 模式:更新自己广播里的 seenPeerId(带节流 + 失败降级) */
}, {
key: "updateAdvertisedPeer",
value: function updateAdvertisedPeer(peerId) {
var _this5 = this;
if (this.skipAdvertising || !this.peripheralServer) return;
if (this.advertiseUpdateBroken) return;
var now = this.now();
if (now - this.lastAdvertiseUpdateAt < this.advertiseUpdateThrottleMs) return;
this.lastAdvertiseUpdateAt = now;
var payload = bluetoothBump_utils.encodePayload(this.myTempId, peerId);
this.log("\uD83D\uDCE2 \u66F4\u65B0\u5E7F\u64AD payload = ".concat(payload));
this.adapter.updateAdvertisedValue(this.peripheralServer, {
serviceUuid: this.serviceUuid,
characteristicUuid: this.characteristicUuid,
tempId: this.myTempId,
payload: payload
}).then(function () {
_this5.currentAdvertisedPeer = peerId;
})["catch"](function (err) {
_this5.log("\u26A0\uFE0F \u66F4\u65B0\u5E7F\u64AD\u5931\u8D25\uFF0C\u672C\u4F1A\u8BDD\u5185\u964D\u7EA7\u4E3A simple \u6A21\u5F0F\uFF1A".concat((err === null || err === void 0 ? void 0 : err.errMsg) || err));
_this5.advertiseUpdateBroken = true;
});
}
/** 触发碰一碰成功:通知业务方 + 优雅停止(保持广播一段时间) */
}, {
key: "triggerBump",
value: function triggerBump(dev, peerTempId) {
var _this6 = this;
var _a;
this.hasBumped = true;
this.log("\uD83C\uDF89 \u78B0\u4E00\u78B0\u6210\u529F\uFF01peerId=".concat(dev.deviceId, ", peerTempId=").concat(peerTempId || '(未解析)', ", RSSI=").concat(dev.RSSI));
try {
(_a = this.onBump) === null || _a === void 0 ? void 0 : _a.call(this, dev.deviceId, dev.RSSI, dev, peerTempId);
} catch (e) {
this.log("onBump \u56DE\u8C03\u629B\u9519\uFF1A".concat((e === null || e === void 0 ? void 0 : e.message) || e));
}
// 不立刻 stop():保持广播一段时间,让对方也扫到自己
this.stopDiscoveryOnly();
this.log("\u23F3 \u5DF2\u505C\u6B62\u626B\u63CF\uFF0C\u4FDD\u6301\u5E7F\u64AD ".concat(this.lingerBeforeStopMs, "ms \u8BA9\u5BF9\u65B9\u8BC6\u522B\u81EA\u5DF1"));
if (this.gracefulStopTimer) this.clearTimeoutFn(this.gracefulStopTimer);
this.gracefulStopTimer = this.setTimeoutFn(function () {
_this6.log('🛑 延迟关闭:彻底停止');
_this6.stop();
}, this.lingerBeforeStopMs);
}
/** 仅停止扫描(保留广播) */
}, {
key: "stopDiscoveryOnly",
value: function stopDiscoveryOnly() {
if (this.pollTimer) {
this.clearIntervalFn(this.pollTimer);
this.pollTimer = null;
}
if (this.deviceFoundCb) {
this.adapter.offDeviceFound(this.deviceFoundCb);
this.deviceFoundCb = null;
}
if (this.isDiscovering) {
this.adapter.stopDiscovery();
this.isDiscovering = false;
}
}
// ==================== 工具 ====================
}, {
key: "log",
value: function log(msg, data) {
if (this.onLog) {
this.onLog(msg, data);
} else if (data !== undefined) {
// eslint-disable-next-line no-console
console.log('[BluetoothBump]', msg, data);
} else {
// eslint-disable-next-line no-console
console.log('[BluetoothBump]', msg);
}
}
}]);
}();
exports.BluetoothBump = BluetoothBump;