t-comm
Version:
专业、稳定、纯粹的工具库
260 lines (257 loc) • 10.5 kB
JavaScript
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 };