@onekeyfe/hd-transport-react-native 1.2.0-alpha.63 → 1.2.0-alpha.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +29 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -15
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +90 -0
- package/src/__tests__/protocolReprobe.test.ts +133 -0
- package/src/index.ts +110 -25
package/dist/index.d.ts
CHANGED
|
@@ -64,7 +64,22 @@ declare function getProtocolV2BleTuning(): {
|
|
|
64
64
|
* inside the native budget, so this only fires when the native timeout did not.
|
|
65
65
|
*/
|
|
66
66
|
declare const BLE_CONNECT_TIMEOUT_MS: number;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
69
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
70
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
71
|
+
* budget.
|
|
72
|
+
*/
|
|
73
|
+
declare const BLE_GATT_SETUP_TIMEOUT_MS = 10000;
|
|
74
|
+
/**
|
|
75
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
76
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
77
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
78
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
79
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
80
|
+
*/
|
|
81
|
+
declare const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
82
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
68
83
|
declare const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
69
84
|
type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
70
85
|
declare class ReactNativeBleTransport {
|
|
@@ -91,9 +106,13 @@ declare class ReactNativeBleTransport {
|
|
|
91
106
|
private probingProtocols;
|
|
92
107
|
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
93
108
|
private writeTimeoutCounts;
|
|
94
|
-
/**
|
|
95
|
-
private
|
|
109
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
110
|
+
private connectionSetupTimeoutCounts;
|
|
96
111
|
private deviceProtocolHints;
|
|
112
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
113
|
+
private sessionProtocols;
|
|
114
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
115
|
+
private protocolReprobeFailures;
|
|
97
116
|
private protocolV2Assemblers;
|
|
98
117
|
private protocolV2FrameQueues;
|
|
99
118
|
private protocolV2FramePromises;
|
|
@@ -133,13 +152,14 @@ declare class ReactNativeBleTransport {
|
|
|
133
152
|
cancel(): void;
|
|
134
153
|
/** Run a native connect under the JS backstop budget. */
|
|
135
154
|
private connectWithTimeout;
|
|
155
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
156
|
+
private resolveCharacteristicsWithTimeout;
|
|
136
157
|
/**
|
|
137
|
-
* Give up on a
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* queue that just stopped responding.
|
|
158
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
159
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
160
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
141
161
|
*/
|
|
142
|
-
private
|
|
162
|
+
private abandonStalledConnection;
|
|
143
163
|
private getCachedTransport;
|
|
144
164
|
/**
|
|
145
165
|
* Write one packet under a bounded budget. A write that never settles means the
|
|
@@ -178,4 +198,4 @@ declare class ReactNativeBleTransport {
|
|
|
178
198
|
getProtocolType(path: string): ProtocolType | undefined;
|
|
179
199
|
}
|
|
180
200
|
|
|
181
|
-
export { BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD, BLE_CONNECT_TIMEOUT_MS, BLE_WRITE_PACKET_TIMEOUT_MS, BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD, IOneKeyDevice, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
|
|
201
|
+
export { BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD, BLE_CONNECT_TIMEOUT_MS, BLE_GATT_SETUP_TIMEOUT_MS, BLE_WRITE_PACKET_TIMEOUT_MS, BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD, IOneKeyDevice, PROTOCOL_REPROBE_FALLBACK_ATTEMPTS, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAIL,UAAU,IAAI,aAAa,EAE5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,SAAS,EAAE,EAEhB,KAAK,oBAAoB,EAIzB,KAAK,YAAY,EAKjB,KAAK,oBAAoB,EAG1B,MAAM,wBAAwB,CAAC;AAmBhC,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAK1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAgBjE,KAAK,4BAA4B,GAAG,WAAW,CAAC;AAChD,KAAK,0BAA0B,GAAG;IAChC,mBAAmB,EAAE,cAAc,CAAC;IACpC,oBAAoB,EAAE,cAAc,CAAC;CACtC,CAAC;AAOF,eAAO,MAAM,+BAA+B,UACnC,OAAO,KACb,4BAA4B,GAAG,IAsBjC,CAAC;AAcF,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAOlD,eAAO,MAAM,yCAAyC,IAAI,CAAC;AAI3D,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAiBF,wBAAgB,4BAA4B,CAAC,MAAM,GAAE,mBAAwB,QAY5E;AAED,wBAAgB,wBAAwB,SAGvC;AAED,wBAAgB,sBAAsB;;;EAErC;AAgCD,eAAO,MAAM,sBAAsB,QAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -295,11 +295,14 @@ const fallbackConnectOptions = {
|
|
|
295
295
|
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
296
296
|
};
|
|
297
297
|
const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
298
|
+
const BLE_GATT_SETUP_TIMEOUT_MS = 10000;
|
|
299
|
+
const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
298
300
|
const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
299
301
|
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
300
302
|
const isConnectTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === hdShared.HardwareErrorCode.BleConnectedError &&
|
|
301
303
|
typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' &&
|
|
302
304
|
error.message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
305
|
+
const isNativeOperationTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.OperationTimedOut;
|
|
303
306
|
const tryToGetConfiguration = (device) => {
|
|
304
307
|
if (!device || !device.serviceUUIDs)
|
|
305
308
|
return null;
|
|
@@ -359,8 +362,10 @@ class ReactNativeBleTransport {
|
|
|
359
362
|
this.deviceProtocol = new Map();
|
|
360
363
|
this.probingProtocols = new Map();
|
|
361
364
|
this.writeTimeoutCounts = new Map();
|
|
362
|
-
this.
|
|
365
|
+
this.connectionSetupTimeoutCounts = new Map();
|
|
363
366
|
this.deviceProtocolHints = new Map();
|
|
367
|
+
this.sessionProtocols = new Map();
|
|
368
|
+
this.protocolReprobeFailures = new Map();
|
|
364
369
|
this.protocolV2Assemblers = new Map();
|
|
365
370
|
this.protocolV2FrameQueues = new Map();
|
|
366
371
|
this.protocolV2FramePromises = new Map();
|
|
@@ -564,7 +569,7 @@ class ReactNativeBleTransport {
|
|
|
564
569
|
}
|
|
565
570
|
}
|
|
566
571
|
}
|
|
567
|
-
const { writeCharacteristic, notifyCharacteristic } = yield this.
|
|
572
|
+
const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
568
573
|
transport.device = device;
|
|
569
574
|
transport.writeCharacteristic = writeCharacteristic;
|
|
570
575
|
transport.notifyCharacteristic = notifyCharacteristic;
|
|
@@ -693,7 +698,7 @@ class ReactNativeBleTransport {
|
|
|
693
698
|
}
|
|
694
699
|
installTransportForAcquire(uuid, device, characteristics) {
|
|
695
700
|
return __awaiter(this, void 0, void 0, function* () {
|
|
696
|
-
const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.
|
|
701
|
+
const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
697
702
|
const transport$1 = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
698
703
|
if (reactNative.Platform.OS === 'android') {
|
|
699
704
|
transport$1.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport$1.mtuSize;
|
|
@@ -833,7 +838,7 @@ class ReactNativeBleTransport {
|
|
|
833
838
|
}
|
|
834
839
|
device = yield requestAndroidMtu(device);
|
|
835
840
|
const acquiredDevice = device;
|
|
836
|
-
const { writeCharacteristic, notifyCharacteristic } = yield this.
|
|
841
|
+
const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
837
842
|
const protocolHint = expectedProtocol
|
|
838
843
|
? undefined
|
|
839
844
|
: (_b = (_a = input.protocolHint) !== null && _a !== void 0 ? _a : this.deviceProtocolHints.get(uuid)) !== null && _b !== void 0 ? _b : inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
@@ -1304,6 +1309,8 @@ class ReactNativeBleTransport {
|
|
|
1304
1309
|
this.deviceProtocol.delete(session);
|
|
1305
1310
|
this.probingProtocols.delete(session);
|
|
1306
1311
|
this.deviceProtocolHints.delete(session);
|
|
1312
|
+
this.sessionProtocols.delete(session);
|
|
1313
|
+
this.protocolReprobeFailures.delete(session);
|
|
1307
1314
|
this.protocolV2Assemblers.delete(session);
|
|
1308
1315
|
this.resetProtocolV2Frames(session);
|
|
1309
1316
|
try {
|
|
@@ -1340,12 +1347,11 @@ class ReactNativeBleTransport {
|
|
|
1340
1347
|
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1341
1348
|
}),
|
|
1342
1349
|
]);
|
|
1343
|
-
this.connectTimeoutCounts.delete(uuid);
|
|
1344
1350
|
return result;
|
|
1345
1351
|
}
|
|
1346
1352
|
catch (error) {
|
|
1347
|
-
if (timedOut) {
|
|
1348
|
-
this.
|
|
1353
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1354
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1349
1355
|
}
|
|
1350
1356
|
throw error;
|
|
1351
1357
|
}
|
|
@@ -1355,12 +1361,44 @@ class ReactNativeBleTransport {
|
|
|
1355
1361
|
}
|
|
1356
1362
|
});
|
|
1357
1363
|
}
|
|
1358
|
-
|
|
1364
|
+
resolveCharacteristicsWithTimeout(uuid, device) {
|
|
1365
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1366
|
+
let timer;
|
|
1367
|
+
let timedOut = false;
|
|
1368
|
+
const pending = this.resolveCharacteristics(device);
|
|
1369
|
+
pending.catch(() => undefined);
|
|
1370
|
+
try {
|
|
1371
|
+
const result = yield Promise.race([
|
|
1372
|
+
pending,
|
|
1373
|
+
new Promise((_, reject) => {
|
|
1374
|
+
timer = setTimeout(() => {
|
|
1375
|
+
timedOut = true;
|
|
1376
|
+
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`));
|
|
1377
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1378
|
+
}),
|
|
1379
|
+
]);
|
|
1380
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1381
|
+
return result;
|
|
1382
|
+
}
|
|
1383
|
+
catch (error) {
|
|
1384
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1385
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1386
|
+
}
|
|
1387
|
+
throw error;
|
|
1388
|
+
}
|
|
1389
|
+
finally {
|
|
1390
|
+
if (timer)
|
|
1391
|
+
clearTimeout(timer);
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
abandonStalledConnection(uuid, stage) {
|
|
1359
1396
|
var _a, _b;
|
|
1360
|
-
const timeouts = ((_a = this.
|
|
1361
|
-
this.
|
|
1362
|
-
Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE
|
|
1363
|
-
|
|
1397
|
+
const timeouts = ((_a = this.connectionSetupTimeoutCounts.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
1398
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1399
|
+
Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1400
|
+
stage,
|
|
1401
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1364
1402
|
});
|
|
1365
1403
|
(_b = this.blePlxManager) === null || _b === void 0 ? void 0 : _b.cancelDeviceConnection(uuid).catch(() => {
|
|
1366
1404
|
});
|
|
@@ -1373,9 +1411,9 @@ class ReactNativeBleTransport {
|
|
|
1373
1411
|
this.protocolV2Assemblers.delete(uuid);
|
|
1374
1412
|
this.resetProtocolV2Frames(uuid);
|
|
1375
1413
|
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1376
|
-
Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE
|
|
1414
|
+
Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1377
1415
|
this.resetPlxManager();
|
|
1378
|
-
this.
|
|
1416
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1379
1417
|
}
|
|
1380
1418
|
}
|
|
1381
1419
|
getCachedTransport(uuid) {
|
|
@@ -1450,6 +1488,8 @@ class ReactNativeBleTransport {
|
|
|
1450
1488
|
});
|
|
1451
1489
|
this.deviceProtocol.clear();
|
|
1452
1490
|
this.probingProtocols.clear();
|
|
1491
|
+
this.sessionProtocols.clear();
|
|
1492
|
+
this.protocolReprobeFailures.clear();
|
|
1453
1493
|
this.monitorTokens.clear();
|
|
1454
1494
|
this.protocolV2Assemblers.clear();
|
|
1455
1495
|
try {
|
|
@@ -1478,10 +1518,12 @@ class ReactNativeBleTransport {
|
|
|
1478
1518
|
return (_a = this.deviceProtocol.get(uuid)) !== null && _a !== void 0 ? _a : this.probingProtocols.get(uuid);
|
|
1479
1519
|
}
|
|
1480
1520
|
detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
|
|
1521
|
+
var _a;
|
|
1481
1522
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1482
1523
|
if (expectedProtocol === 'V1') {
|
|
1483
1524
|
if (yield this.probeProtocolV1(uuid)) {
|
|
1484
1525
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1526
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1485
1527
|
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1486
1528
|
deviceId: uuid,
|
|
1487
1529
|
protocol: 'V1',
|
|
@@ -1494,6 +1536,7 @@ class ReactNativeBleTransport {
|
|
|
1494
1536
|
if (expectedProtocol === 'V2') {
|
|
1495
1537
|
if (yield this.probeProtocolV2(uuid)) {
|
|
1496
1538
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1539
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1497
1540
|
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1498
1541
|
deviceId: uuid,
|
|
1499
1542
|
protocol: 'V2',
|
|
@@ -1503,7 +1546,13 @@ class ReactNativeBleTransport {
|
|
|
1503
1546
|
}
|
|
1504
1547
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1505
1548
|
}
|
|
1506
|
-
const
|
|
1549
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1550
|
+
const reprobeFailures = (_a = this.protocolReprobeFailures.get(uuid)) !== null && _a !== void 0 ? _a : 0;
|
|
1551
|
+
const fullProbeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1552
|
+
const trustSessionProtocol = sessionProtocol !== undefined &&
|
|
1553
|
+
!protocolHint &&
|
|
1554
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1555
|
+
const probeOrder = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1507
1556
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1508
1557
|
const protocol = probeOrder[i];
|
|
1509
1558
|
if (i > 0) {
|
|
@@ -1518,6 +1567,8 @@ class ReactNativeBleTransport {
|
|
|
1518
1567
|
const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
|
|
1519
1568
|
if (detected) {
|
|
1520
1569
|
this.deviceProtocol.set(uuid, protocol);
|
|
1570
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1571
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1521
1572
|
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1522
1573
|
deviceId: uuid,
|
|
1523
1574
|
protocol,
|
|
@@ -1526,6 +1577,12 @@ class ReactNativeBleTransport {
|
|
|
1526
1577
|
return protocol;
|
|
1527
1578
|
}
|
|
1528
1579
|
}
|
|
1580
|
+
if (trustSessionProtocol) {
|
|
1581
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1585
|
+
}
|
|
1529
1586
|
this.deviceProtocol.delete(uuid);
|
|
1530
1587
|
this.probingProtocols.delete(uuid);
|
|
1531
1588
|
throw this.createProtocolDetectionError();
|
|
@@ -1823,8 +1880,10 @@ class ReactNativeBleTransport {
|
|
|
1823
1880
|
|
|
1824
1881
|
exports.BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD;
|
|
1825
1882
|
exports.BLE_CONNECT_TIMEOUT_MS = BLE_CONNECT_TIMEOUT_MS;
|
|
1883
|
+
exports.BLE_GATT_SETUP_TIMEOUT_MS = BLE_GATT_SETUP_TIMEOUT_MS;
|
|
1826
1884
|
exports.BLE_WRITE_PACKET_TIMEOUT_MS = BLE_WRITE_PACKET_TIMEOUT_MS;
|
|
1827
1885
|
exports.BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD;
|
|
1886
|
+
exports.PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1828
1887
|
exports.configureProtocolV2BleTuning = configureProtocolV2BleTuning;
|
|
1829
1888
|
exports["default"] = ReactNativeBleTransport;
|
|
1830
1889
|
exports.getFirmwareUploadWriteRetryType = getFirmwareUploadWriteRetryType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.65",
|
|
4
4
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.65",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.65",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.65",
|
|
26
26
|
"@onekeyfe/react-native-ble-utils": "^0.1.6",
|
|
27
27
|
"react-native-ble-plx": "3.5.1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "2850b5b34935d4e97a1fe8a6459eb5a96a5cf0f6"
|
|
30
30
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
+
import { BleErrorCode } from 'react-native-ble-plx';
|
|
2
3
|
|
|
3
4
|
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
5
|
|
|
5
6
|
import ReactNativeBleTransport, {
|
|
6
7
|
BLE_CONNECT_TIMEOUT_MS,
|
|
7
8
|
BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
9
|
+
BLE_GATT_SETUP_TIMEOUT_MS,
|
|
8
10
|
} from '../index';
|
|
9
11
|
|
|
10
12
|
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
@@ -31,6 +33,7 @@ jest.mock('react-native-ble-plx', () => ({
|
|
|
31
33
|
OperationStartFailed: 601,
|
|
32
34
|
DeviceMTUChangeFailed: 401,
|
|
33
35
|
OperationCancelled: 2,
|
|
36
|
+
OperationTimedOut: 3,
|
|
34
37
|
DeviceAlreadyConnected: 203,
|
|
35
38
|
},
|
|
36
39
|
BleManager: jest.fn(),
|
|
@@ -66,6 +69,14 @@ async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 25
|
|
|
66
69
|
/** A device whose native connect() never settles — the observed iOS failure mode. */
|
|
67
70
|
function createHarness(connectImpl: () => Promise<unknown>) {
|
|
68
71
|
const connect = jest.fn(connectImpl);
|
|
72
|
+
const writeCharacteristic = {
|
|
73
|
+
uuid: '00000002-0000-1000-8000-00805f9b34fb',
|
|
74
|
+
isWritableWithResponse: true,
|
|
75
|
+
};
|
|
76
|
+
const notifyCharacteristic = {
|
|
77
|
+
uuid: '00000003-0000-1000-8000-00805f9b34fb',
|
|
78
|
+
isNotifiable: true,
|
|
79
|
+
};
|
|
69
80
|
const device = {
|
|
70
81
|
id: UUID,
|
|
71
82
|
name: 'OneKey Classic',
|
|
@@ -74,6 +85,11 @@ function createHarness(connectImpl: () => Promise<unknown>) {
|
|
|
74
85
|
isConnected: jest.fn(() => Promise.resolve(false)),
|
|
75
86
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
76
87
|
connect,
|
|
88
|
+
discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
|
|
89
|
+
characteristicsForService: jest.fn(() =>
|
|
90
|
+
Promise.resolve([writeCharacteristic, notifyCharacteristic])
|
|
91
|
+
),
|
|
92
|
+
services: jest.fn(() => Promise.resolve([])),
|
|
77
93
|
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
78
94
|
};
|
|
79
95
|
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
@@ -189,4 +205,78 @@ describe('BLE connect timeout', () => {
|
|
|
189
205
|
expect(destroy).toHaveBeenCalledTimes(1);
|
|
190
206
|
expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
|
|
191
207
|
});
|
|
208
|
+
|
|
209
|
+
test('native connect timeouts contribute to the same manager reset budget', async () => {
|
|
210
|
+
const { transport, bleManager } = createHarness(() => Promise.resolve());
|
|
211
|
+
const destroy = jest.fn();
|
|
212
|
+
(bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
|
|
213
|
+
const nativeTimeout = Object.assign(new Error('Operation timed out'), {
|
|
214
|
+
errorCode: BleErrorCode.OperationTimedOut,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
218
|
+
// eslint-disable-next-line no-await-in-loop
|
|
219
|
+
await expect(
|
|
220
|
+
(transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
|
|
221
|
+
).rejects.toBe(nativeTimeout);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('GATT discovery is bounded and abandons the native connection', async () => {
|
|
228
|
+
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
229
|
+
device.discoverAllServicesAndCharacteristics.mockImplementation(
|
|
230
|
+
() =>
|
|
231
|
+
new Promise(() => {
|
|
232
|
+
// never settles
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
237
|
+
let settled = false;
|
|
238
|
+
(transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch((error: unknown) => {
|
|
239
|
+
errors.push(error as { errorCode?: unknown });
|
|
240
|
+
settled = true;
|
|
241
|
+
});
|
|
242
|
+
await flush();
|
|
243
|
+
await advanceUntil(() => settled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
|
|
244
|
+
|
|
245
|
+
expect(errors).toHaveLength(1);
|
|
246
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
247
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('a successful GATT retry clears the timeout budget before an abandoned call settles', async () => {
|
|
251
|
+
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
252
|
+
let resolveAbandonedDiscovery: (() => void) | undefined;
|
|
253
|
+
device.discoverAllServicesAndCharacteristics
|
|
254
|
+
.mockImplementationOnce(
|
|
255
|
+
() =>
|
|
256
|
+
new Promise<void>(resolve => {
|
|
257
|
+
resolveAbandonedDiscovery = resolve;
|
|
258
|
+
})
|
|
259
|
+
)
|
|
260
|
+
.mockResolvedValueOnce(undefined);
|
|
261
|
+
|
|
262
|
+
let firstSettled = false;
|
|
263
|
+
(transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch(() => {
|
|
264
|
+
firstSettled = true;
|
|
265
|
+
});
|
|
266
|
+
await flush();
|
|
267
|
+
await advanceUntil(() => firstSettled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
|
|
268
|
+
|
|
269
|
+
await expect(
|
|
270
|
+
(transport as any).resolveCharacteristicsWithTimeout(UUID, device)
|
|
271
|
+
).resolves.toMatchObject({
|
|
272
|
+
writeCharacteristic: expect.any(Object),
|
|
273
|
+
notifyCharacteristic: expect.any(Object),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
resolveAbandonedDiscovery?.();
|
|
277
|
+
await flush();
|
|
278
|
+
|
|
279
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledTimes(1);
|
|
280
|
+
expect((transport as any).connectionSetupTimeoutCounts.has(UUID)).toBe(false);
|
|
281
|
+
});
|
|
192
282
|
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import ReactNativeBleTransport, { PROTOCOL_REPROBE_FALLBACK_ATTEMPTS } from '../index';
|
|
2
|
+
|
|
3
|
+
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
4
|
+
|
|
5
|
+
jest.mock(
|
|
6
|
+
'react-native',
|
|
7
|
+
() => ({
|
|
8
|
+
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
9
|
+
PermissionsAndroid: {
|
|
10
|
+
PERMISSIONS: {},
|
|
11
|
+
RESULTS: {},
|
|
12
|
+
request: jest.fn(),
|
|
13
|
+
requestMultiple: jest.fn(),
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
{ virtual: true }
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
20
|
+
BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
|
|
21
|
+
BleError: Error,
|
|
22
|
+
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
23
|
+
BleManager: jest.fn(),
|
|
24
|
+
ScanMode: { LowLatency: 2 },
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: {
|
|
30
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
31
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
32
|
+
pairDevice: jest.fn(() => Promise.resolve()),
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const UUID = 'reprobe-device';
|
|
37
|
+
|
|
38
|
+
/** Drive detectProtocol with stubbed probes so we can observe which are attempted. */
|
|
39
|
+
function createHarness({ v1, v2 }: { v1: boolean; v2: boolean }) {
|
|
40
|
+
const transport = new ReactNativeBleTransport({});
|
|
41
|
+
transport.configure(messages);
|
|
42
|
+
const probeV1 = jest.fn(() => {
|
|
43
|
+
if (v1) {
|
|
44
|
+
(transport as any).deviceProtocol.set(UUID, 'V1');
|
|
45
|
+
}
|
|
46
|
+
return Promise.resolve(v1);
|
|
47
|
+
});
|
|
48
|
+
const probeV2 = jest.fn(() => {
|
|
49
|
+
if (v2) {
|
|
50
|
+
(transport as any).deviceProtocol.set(UUID, 'V2');
|
|
51
|
+
}
|
|
52
|
+
return Promise.resolve(v2);
|
|
53
|
+
});
|
|
54
|
+
(transport as any).probeProtocolV1 = probeV1;
|
|
55
|
+
(transport as any).probeProtocolV2 = probeV2;
|
|
56
|
+
(transport as any).resetProbeStateAfterProtocolProbe = jest.fn(() => Promise.resolve());
|
|
57
|
+
return { transport, probeV1, probeV2 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const detect = (transport: ReactNativeBleTransport) =>
|
|
61
|
+
(transport as any).detectProtocol(UUID, undefined, undefined, () =>
|
|
62
|
+
Promise.resolve()
|
|
63
|
+
) as Promise<string>;
|
|
64
|
+
|
|
65
|
+
describe('protocol re-probe after a known device goes away', () => {
|
|
66
|
+
test('an unknown device still probes both protocols', async () => {
|
|
67
|
+
const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
|
|
68
|
+
|
|
69
|
+
await expect(detect(transport)).rejects.toBeDefined();
|
|
70
|
+
|
|
71
|
+
expect(probeV1).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(probeV2).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a device already known to speak V1 does not pay the V2 probe while it is away', async () => {
|
|
76
|
+
const known = createHarness({ v1: true, v2: false });
|
|
77
|
+
// First detection confirms V1 (this is the session that just talked to the device).
|
|
78
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
79
|
+
|
|
80
|
+
// The device now reboots after a firmware install: V1 stops answering.
|
|
81
|
+
(known.transport as any).deviceProtocol.delete(UUID);
|
|
82
|
+
const probeV1 = jest.fn(() => Promise.resolve(false));
|
|
83
|
+
const probeV2 = jest.fn(() => Promise.resolve(false));
|
|
84
|
+
(known.transport as any).probeProtocolV1 = probeV1;
|
|
85
|
+
(known.transport as any).probeProtocolV2 = probeV2;
|
|
86
|
+
|
|
87
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
88
|
+
|
|
89
|
+
expect(probeV1).toHaveBeenCalledTimes(1);
|
|
90
|
+
// The 10s Protocol V2 ping is pure waste for a device we just spoke V1 to.
|
|
91
|
+
expect(probeV2).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('after repeated failures it re-probes every protocol again', async () => {
|
|
95
|
+
const known = createHarness({ v1: true, v2: false });
|
|
96
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
97
|
+
|
|
98
|
+
(known.transport as any).deviceProtocol.delete(UUID);
|
|
99
|
+
const probeV1 = jest.fn(() => Promise.resolve(false));
|
|
100
|
+
const probeV2 = jest.fn(() => Promise.resolve(false));
|
|
101
|
+
(known.transport as any).probeProtocolV1 = probeV1;
|
|
102
|
+
(known.transport as any).probeProtocolV2 = probeV2;
|
|
103
|
+
|
|
104
|
+
for (let attempt = 0; attempt < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS; attempt += 1) {
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
106
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
107
|
+
}
|
|
108
|
+
expect(probeV2).not.toHaveBeenCalled();
|
|
109
|
+
|
|
110
|
+
// The device may genuinely have changed protocol, so the shortcut must expire.
|
|
111
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
112
|
+
expect(probeV2).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('a successful detection clears the failure streak', async () => {
|
|
116
|
+
const known = createHarness({ v1: true, v2: false });
|
|
117
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
118
|
+
(known.transport as any).deviceProtocol.delete(UUID);
|
|
119
|
+
|
|
120
|
+
const failingV1 = jest.fn(() => Promise.resolve(false));
|
|
121
|
+
(known.transport as any).probeProtocolV1 = failingV1;
|
|
122
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
123
|
+
|
|
124
|
+
// Device comes back.
|
|
125
|
+
(known.transport as any).probeProtocolV1 = jest.fn(() => {
|
|
126
|
+
(known.transport as any).deviceProtocol.set(UUID, 'V1');
|
|
127
|
+
return Promise.resolve(true);
|
|
128
|
+
});
|
|
129
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
130
|
+
|
|
131
|
+
expect((known.transport as any).protocolReprobeFailures.get(UUID)).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -199,13 +199,30 @@ const fallbackConnectOptions: Record<string, unknown> = {
|
|
|
199
199
|
* inside the native budget, so this only fires when the native timeout did not.
|
|
200
200
|
*/
|
|
201
201
|
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
202
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
204
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
205
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
206
|
+
* budget.
|
|
207
|
+
*/
|
|
208
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
209
|
+
/**
|
|
210
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
211
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
212
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
213
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
214
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
215
|
+
*/
|
|
216
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
217
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
203
218
|
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
204
219
|
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
205
220
|
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
206
221
|
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
207
222
|
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
208
223
|
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
224
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
225
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
209
226
|
|
|
210
227
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
211
228
|
|
|
@@ -306,11 +323,17 @@ export default class ReactNativeBleTransport {
|
|
|
306
323
|
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
307
324
|
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
308
325
|
|
|
309
|
-
/**
|
|
310
|
-
private
|
|
326
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
327
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
311
328
|
|
|
312
329
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
313
330
|
|
|
331
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
332
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
333
|
+
|
|
334
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
335
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
336
|
+
|
|
314
337
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
315
338
|
|
|
316
339
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -541,9 +564,8 @@ export default class ReactNativeBleTransport {
|
|
|
541
564
|
}
|
|
542
565
|
}
|
|
543
566
|
|
|
544
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
545
|
-
device
|
|
546
|
-
);
|
|
567
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
568
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
547
569
|
|
|
548
570
|
transport.device = device;
|
|
549
571
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -708,7 +730,7 @@ export default class ReactNativeBleTransport {
|
|
|
708
730
|
characteristics?: ResolvedBleCharacteristics
|
|
709
731
|
) {
|
|
710
732
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
711
|
-
characteristics ?? (await this.
|
|
733
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
712
734
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
713
735
|
if (Platform.OS === 'android') {
|
|
714
736
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
@@ -883,9 +905,8 @@ export default class ReactNativeBleTransport {
|
|
|
883
905
|
|
|
884
906
|
device = await requestAndroidMtu(device);
|
|
885
907
|
const acquiredDevice = device;
|
|
886
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
887
|
-
acquiredDevice
|
|
888
|
-
);
|
|
908
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
909
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
889
910
|
|
|
890
911
|
const protocolHint = expectedProtocol
|
|
891
912
|
? undefined
|
|
@@ -1482,6 +1503,8 @@ export default class ReactNativeBleTransport {
|
|
|
1482
1503
|
this.deviceProtocol.delete(session);
|
|
1483
1504
|
this.probingProtocols.delete(session);
|
|
1484
1505
|
this.deviceProtocolHints.delete(session);
|
|
1506
|
+
this.sessionProtocols.delete(session);
|
|
1507
|
+
this.protocolReprobeFailures.delete(session);
|
|
1485
1508
|
this.protocolV2Assemblers.delete(session);
|
|
1486
1509
|
this.resetProtocolV2Frames(session);
|
|
1487
1510
|
|
|
@@ -1530,11 +1553,46 @@ export default class ReactNativeBleTransport {
|
|
|
1530
1553
|
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1531
1554
|
}),
|
|
1532
1555
|
]);
|
|
1533
|
-
this.connectTimeoutCounts.delete(uuid);
|
|
1534
1556
|
return result;
|
|
1535
1557
|
} catch (error) {
|
|
1536
|
-
if (timedOut) {
|
|
1537
|
-
this.
|
|
1558
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1559
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1560
|
+
}
|
|
1561
|
+
throw error;
|
|
1562
|
+
} finally {
|
|
1563
|
+
if (timer) clearTimeout(timer);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1568
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1569
|
+
uuid: string,
|
|
1570
|
+
device: Device
|
|
1571
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1572
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1573
|
+
let timedOut = false;
|
|
1574
|
+
const pending = this.resolveCharacteristics(device);
|
|
1575
|
+
pending.catch(() => undefined);
|
|
1576
|
+
try {
|
|
1577
|
+
const result = await Promise.race([
|
|
1578
|
+
pending,
|
|
1579
|
+
new Promise<never>((_, reject) => {
|
|
1580
|
+
timer = setTimeout(() => {
|
|
1581
|
+
timedOut = true;
|
|
1582
|
+
reject(
|
|
1583
|
+
ERRORS.TypedError(
|
|
1584
|
+
HardwareErrorCode.BleConnectedError,
|
|
1585
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1586
|
+
)
|
|
1587
|
+
);
|
|
1588
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1589
|
+
}),
|
|
1590
|
+
]);
|
|
1591
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1592
|
+
return result;
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1595
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1538
1596
|
}
|
|
1539
1597
|
throw error;
|
|
1540
1598
|
} finally {
|
|
@@ -1543,16 +1601,19 @@ export default class ReactNativeBleTransport {
|
|
|
1543
1601
|
}
|
|
1544
1602
|
|
|
1545
1603
|
/**
|
|
1546
|
-
* Give up on a
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1549
|
-
* queue that just stopped responding.
|
|
1604
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1605
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1606
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1550
1607
|
*/
|
|
1551
|
-
private
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1608
|
+
private abandonStalledConnection(
|
|
1609
|
+
uuid: string,
|
|
1610
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1611
|
+
) {
|
|
1612
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1613
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1614
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1615
|
+
stage,
|
|
1616
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1556
1617
|
});
|
|
1557
1618
|
|
|
1558
1619
|
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
@@ -1570,9 +1631,9 @@ export default class ReactNativeBleTransport {
|
|
|
1570
1631
|
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1571
1632
|
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1572
1633
|
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1573
|
-
Log?.error('[ReactNativeBleTransport] BLE
|
|
1634
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1574
1635
|
this.resetPlxManager();
|
|
1575
|
-
this.
|
|
1636
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1576
1637
|
}
|
|
1577
1638
|
}
|
|
1578
1639
|
|
|
@@ -1674,6 +1735,8 @@ export default class ReactNativeBleTransport {
|
|
|
1674
1735
|
});
|
|
1675
1736
|
this.deviceProtocol.clear();
|
|
1676
1737
|
this.probingProtocols.clear();
|
|
1738
|
+
this.sessionProtocols.clear();
|
|
1739
|
+
this.protocolReprobeFailures.clear();
|
|
1677
1740
|
this.monitorTokens.clear();
|
|
1678
1741
|
this.protocolV2Assemblers.clear();
|
|
1679
1742
|
try {
|
|
@@ -1720,6 +1783,7 @@ export default class ReactNativeBleTransport {
|
|
|
1720
1783
|
if (expectedProtocol === 'V1') {
|
|
1721
1784
|
if (await this.probeProtocolV1(uuid)) {
|
|
1722
1785
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1786
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1723
1787
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1724
1788
|
deviceId: uuid,
|
|
1725
1789
|
protocol: 'V1',
|
|
@@ -1733,6 +1797,7 @@ export default class ReactNativeBleTransport {
|
|
|
1733
1797
|
if (expectedProtocol === 'V2') {
|
|
1734
1798
|
if (await this.probeProtocolV2(uuid)) {
|
|
1735
1799
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1800
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1736
1801
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1737
1802
|
deviceId: uuid,
|
|
1738
1803
|
protocol: 'V2',
|
|
@@ -1745,8 +1810,18 @@ export default class ReactNativeBleTransport {
|
|
|
1745
1810
|
|
|
1746
1811
|
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1747
1812
|
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1748
|
-
const
|
|
1813
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1814
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1815
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1749
1816
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1817
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1818
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1819
|
+
// only adds its timeout to every poll.
|
|
1820
|
+
const trustSessionProtocol =
|
|
1821
|
+
sessionProtocol !== undefined &&
|
|
1822
|
+
!protocolHint &&
|
|
1823
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1824
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1750
1825
|
|
|
1751
1826
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1752
1827
|
const protocol = probeOrder[i];
|
|
@@ -1764,6 +1839,8 @@ export default class ReactNativeBleTransport {
|
|
|
1764
1839
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1765
1840
|
if (detected) {
|
|
1766
1841
|
this.deviceProtocol.set(uuid, protocol);
|
|
1842
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1843
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1767
1844
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1768
1845
|
deviceId: uuid,
|
|
1769
1846
|
protocol,
|
|
@@ -1773,6 +1850,14 @@ export default class ReactNativeBleTransport {
|
|
|
1773
1850
|
}
|
|
1774
1851
|
}
|
|
1775
1852
|
|
|
1853
|
+
if (trustSessionProtocol) {
|
|
1854
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1855
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1856
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1857
|
+
} else {
|
|
1858
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1776
1861
|
this.deviceProtocol.delete(uuid);
|
|
1777
1862
|
this.probingProtocols.delete(uuid);
|
|
1778
1863
|
throw this.createProtocolDetectionError();
|