UNPKG

t-comm

Version:

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

432 lines (425 loc) 20.8 kB
'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;