UNPKG

t-comm

Version:

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

260 lines (257 loc) 10.5 kB
import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import _typeof from '@babel/runtime/helpers/typeof'; import { BUMP_NAME_PREFIX, EMPTY_PEER_ID, PAYLOAD_SEPARATOR, TEMP_ID_LENGTH } from './constants.mjs'; 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; } /** 生成 6 位随机大写临时 ID(写到广播特征值里) */ function genTempId() { return Math.random().toString(36).slice(2, 8).toUpperCase(); } /** 把字符串编码为 ArrayBuffer,用于写入特征值 */ function str2ArrayBuffer(str) { var buf = new ArrayBuffer(str.length); var view = new Uint8Array(buf); for (var i = 0; i < str.length; i++) { view[i] = str.charCodeAt(i); } return buf; } /** ArrayBuffer 转十六进制大写字符串 */ function arrayBuffer2Hex(buf) { if (!buf) return ''; var view = new Uint8Array(buf); var s = ''; for (var i = 0; i < view.length; i++) { var h = view[i].toString(16); s += h.length < 2 ? "0".concat(h) : h; } return s.toUpperCase(); } /** 反转十六进制字符串的字节序(小端 ↔ 大端) */ function reverseHexBytes(hex) { var s = ''; for (var i = hex.length - 2; i >= 0; i -= 2) { s += hex.substr(i, 2); } return s; } /** * 判断扫到的设备是否携带我们的 service * * iOS 上 startBluetoothDevicesDiscovery 不传 services 时收到的设备都需要手动过滤。 * 依次检查:advertisServiceUUIDs / serviceData / advertisData / 设备名前缀(BUMP_)。 * * ⚠️ iOS↔iOS 时前 3 项几乎都拿不到,必须靠设备名前缀做最终兜底。 */ function matchesService(dev, serviceUuid) { var target = serviceUuid.toUpperCase(); if (Array.isArray(dev.advertisServiceUUIDs)) { var _iterator = _createForOfIteratorHelper(dev.advertisServiceUUIDs), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var uuid = _step.value; if (typeof uuid === 'string' && uuid.toUpperCase() === target) return true; } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } if (dev.serviceData && _typeof(dev.serviceData) === 'object') { for (var _i = 0, _Object$keys = Object.keys(dev.serviceData); _i < _Object$keys.length; _i++) { var key = _Object$keys[_i]; if (key.toUpperCase() === target) return true; } } if (dev.advertisData) { var hex = arrayBuffer2Hex(dev.advertisData); var uuidNoDash = serviceUuid.replace(/-/g, '').toUpperCase(); var reversed = reverseHexBytes(uuidNoDash); if (hex.indexOf(reversed) >= 0 || hex.indexOf(uuidNoDash) >= 0) { return true; } } // 兜底:设备名以 BUMP_ 开头说明是同游戏的 Peripheral var nameStr = dev.localName || dev.name; if (nameStr && nameStr.indexOf(BUMP_NAME_PREFIX) === 0) return true; return false; } /** * 判断这次扫描结果是否应该触发"碰一碰" * * 条件: * 1. RSSI 不低于阈值(够近) * 2. 距离上次同设备触发已超过冷却时间(避免重复) * * 抽成纯函数后,边界条件可以一行测一个。 */ function shouldTriggerBump(params) { if (params.rssi < params.threshold) return false; if (params.now - params.lastBumpTime < params.cooldownMs) return false; return true; } // ==================== mutual 模式相关 ==================== /** * 把 myTempId / seenPeerId 组装为广播 payload 字符串。 * 固定长度,便于解析:`${myTempId}|${seenPeerId}` → 13 字节。 * * - myTempId 如果不足 TEMP_ID_LENGTH 位会自动补 '-'(防御性处理) * - seenPeerId 为空时填占位符 '------' */ function encodePayload(myTempId, seenPeerId) { var my = padOrTrim(myTempId || '', TEMP_ID_LENGTH); var seen = seenPeerId ? padOrTrim(seenPeerId, TEMP_ID_LENGTH) : EMPTY_PEER_ID; return "".concat(my).concat(PAYLOAD_SEPARATOR).concat(seen); } /** * 把 payload 编码进设备名(iOS Peripheral 唯一可控的广播字段)。 * `BUMP_<myTempId>_<seenPeerId>` → 例 `BUMP_ABC123_------`(19 字符) * * 不少机型对 localName 长度有限制(iOS Peripheral 推荐 ≤ 28 字节),这里 19 字符够用。 */ function encodeBumpName(myTempId, seenPeerId) { var my = padOrTrim(myTempId || '', TEMP_ID_LENGTH); var seen = seenPeerId ? padOrTrim(seenPeerId, TEMP_ID_LENGTH) : EMPTY_PEER_ID; return "".concat(BUMP_NAME_PREFIX).concat(my, "_").concat(seen); } /** * 解析设备名形如 `BUMP_ABC123_XYZ789` → { myTempId, seenPeerId }。 * 仅需 myTempId 时(旧格式 `BUMP_ABC123`)也兼容,seenPeerId 返回空串。 */ function parseBumpName(name) { if (!name) return null; if (name.indexOf(BUMP_NAME_PREFIX) !== 0) return null; var body = name.slice(BUMP_NAME_PREFIX.length); // 可能是 "ABC123" 或 "ABC123_XYZ789" var parts = body.split('_'); var my = parts[0]; var seen = parts[1] || EMPTY_PEER_ID; if (!my || my.length !== TEMP_ID_LENGTH) return null; if (seen.length !== TEMP_ID_LENGTH) return null; return { myTempId: my, seenPeerId: seen === EMPTY_PEER_ID ? '' : seen }; } /** * 解析广播 payload 字符串 → { myTempId, seenPeerId } * 解析失败返回 null(payload 不是本协议)。 * * seenPeerId 为占位符 '------' 时返回空字符串,方便上层 `if (seenPeerId)` 判断。 */ function parsePayloadString(raw) { if (!raw) return null; var parts = raw.split(PAYLOAD_SEPARATOR); if (parts.length !== 2) return null; var _parts = _slicedToArray(parts, 2), my = _parts[0], seen = _parts[1]; if (!my || my.length !== TEMP_ID_LENGTH) return null; if (!seen || seen.length !== TEMP_ID_LENGTH) return null; return { myTempId: my, seenPeerId: seen === EMPTY_PEER_ID ? '' : seen }; } /** * 从扫描到的设备里提取 mutual 协议 payload。 * * 解析顺序(按"iOS 上能拿到的可能性"由高到低): * 1. 设备名 / localName 前缀 `BUMP_` —— iOS↔iOS 唯一可靠通道 * 2. serviceData[serviceUuid] —— Android 标准方案 * 3. advertisData 整段 —— 部分机型把 payload 塞这里 * 4. 设备名按旧格式 `myTempId|seenPeerId` 解析(向后兼容) */ function parsePayload(dev, serviceUuid) { var target = serviceUuid.toUpperCase(); // 1) 优先:设备名 BUMP_ 前缀(iOS↔iOS 必走这里) var nameStr = dev.localName || dev.name; if (nameStr) { var p = parseBumpName(nameStr); if (p) return p; } // 2) serviceData[serviceUuid](Android 标准) if (dev.serviceData && _typeof(dev.serviceData) === 'object') { for (var _i2 = 0, _Object$keys2 = Object.keys(dev.serviceData); _i2 < _Object$keys2.length; _i2++) { var key = _Object$keys2[_i2]; if (key.toUpperCase() === target) { var str = arrayBufferToUtf8(dev.serviceData[key]); var _p = parsePayloadString(str); if (_p) return _p; } } } // 3) advertisData 整段(部分机型) if (dev.advertisData) { var _str = arrayBufferToUtf8(dev.advertisData); var _p2 = parsePayloadString(_str); if (_p2) return _p2; } // 4) 旧格式 name 兜底(向后兼容) if (nameStr) { var _p3 = parsePayloadString(nameStr); if (_p3) return _p3; } return null; } /** 从候选池里挑选"信号最强且在 TTL 内"的 peer(用于决定广播里要"点名"谁) */ function pickBestPeer(peers, now, ttlMs, rssiThreshold) { var best = null; var _iterator2 = _createForOfIteratorHelper(peers.values()), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var rec = _step2.value; if (now - rec.lastSeen > ttlMs) continue; if (rec.rssi < rssiThreshold) continue; if (!best || rec.rssi > best.rssi) best = rec; } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } return best; } /** 清理候选池里过期的 peer(就地修改) */ function pruneExpiredPeers(peers, now, ttlMs) { var _iterator3 = _createForOfIteratorHelper(peers), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var _step3$value = _slicedToArray(_step3.value, 2), id = _step3$value[0], rec = _step3$value[1]; if (now - rec.lastSeen > ttlMs) peers["delete"](id); } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } // ==================== 内部辅助 ==================== /** ArrayBuffer 转 UTF-8 字符串(失败返回空字符串) */ function arrayBufferToUtf8(buf) { if (!buf) return ''; try { var view = new Uint8Array(buf); var s = ''; for (var i = 0; i < view.length; i++) { s += String.fromCharCode(view[i]); } return s; } catch (e) { return ''; } } /** 定长裁剪/补齐(补 '-') */ function padOrTrim(s, len) { if (s.length === len) return s; if (s.length > len) return s.slice(0, len); return s + '-'.repeat(len - s.length); } export { arrayBuffer2Hex, encodeBumpName, encodePayload, genTempId, matchesService, parseBumpName, parsePayload, parsePayloadString, pickBestPeer, pruneExpiredPeers, reverseHexBytes, shouldTriggerBump, str2ArrayBuffer };