@onekeyfe/hd-transport-react-native 1.2.2-alpha.114 → 1.2.2-alpha.115
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 +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -28
- package/package.json +5 -5
- package/src/__tests__/protocolV2Link.test.ts +57 -26
- package/src/index.ts +142 -36
package/dist/index.d.ts
CHANGED
|
@@ -135,6 +135,15 @@ declare class ReactNativeBleTransport {
|
|
|
135
135
|
private confirmedProtocolV2;
|
|
136
136
|
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
137
137
|
private protocolReprobeFailures;
|
|
138
|
+
/** Endpoints whose last detection got no answer on any protocol. */
|
|
139
|
+
private silentDetections;
|
|
140
|
+
/**
|
|
141
|
+
* Endpoints already sent a Protocol V1 wake. The wake starts a fresh wallet session
|
|
142
|
+
* on the device, and a failed detection tears the link down and reconnects on every
|
|
143
|
+
* poll, so neither set is cleared on teardown: one wake is spent per endpoint until a
|
|
144
|
+
* detection succeeds, which re-arms it for the next time the device sleeps.
|
|
145
|
+
*/
|
|
146
|
+
private protocolWakeAttempts;
|
|
138
147
|
/**
|
|
139
148
|
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
140
149
|
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
@@ -228,6 +237,23 @@ declare class ReactNativeBleTransport {
|
|
|
228
237
|
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
229
238
|
private getActiveProtocol;
|
|
230
239
|
private detectProtocol;
|
|
240
|
+
/**
|
|
241
|
+
* Sent once before probing when the previous detection on this endpoint got no answer
|
|
242
|
+
* at all. On Classic-family firmware that silence is also what a sleeping device looks
|
|
243
|
+
* like: its BLE co-processor keeps serving connect, GATT and MTU while the main MCU
|
|
244
|
+
* sits in the screensaver loop, whose host-message filter accepts only Initialize and
|
|
245
|
+
* the *Ack messages. GetFeatures and Ping are dropped there without a reply, so no
|
|
246
|
+
* amount of probing can bring the device back.
|
|
247
|
+
*
|
|
248
|
+
* Initialize is the one message that breaks that loop, but it also starts a fresh
|
|
249
|
+
* wallet session, which is why it is not the probe itself — a bare Initialize on every
|
|
250
|
+
* acquire would drop a hidden-wallet session before Core can restore it. Requiring a
|
|
251
|
+
* fully silent previous detection keeps it to devices with no session left to protect.
|
|
252
|
+
*
|
|
253
|
+
* The firmware consumes the wake to leave its loop and does not reliably answer it, so
|
|
254
|
+
* the result is ignored: the probes that follow are what decide the protocol.
|
|
255
|
+
*/
|
|
256
|
+
private wakeSilentProtocolV1Device;
|
|
231
257
|
private resetProbeStateAfterProtocolProbe;
|
|
232
258
|
private probeProtocolV1;
|
|
233
259
|
private probeProtocolV2;
|
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,EAGL,UAAU,IAAI,aAAa,EAG5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,SAAS,EAAE,EAChB,KAAK,oBAAoB,EAIzB,KAAK,YAAY,EAKjB,KAAK,oBAAoB,EAI1B,MAAM,wBAAwB,CAAC;AAgChC,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAI1C,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;AAEjE,KAAK,8BAA8B,GAAG,eAAe,GAAG;IAMtD,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAkBF,KAAK,4BAA4B,GAAG,WAAW,CAAC;AAChD,KAAK,0BAA0B,GAAG;IAChC,mBAAmB,EAAE,cAAc,CAAC;IACpC,oBAAoB,EAAE,cAAc,CAAC;CACtC,CAAC;AAkCF,eAAO,MAAM,+BAA+B,UACnC,OAAO,KACb,4BAA4B,GAAG,IAsBjC,CAAC;AAcF,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAClD,eAAO,MAAM,8BAA8B,OAAQ,CAAC;AAwBpD,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;AAkCD,eAAO,MAAM,sBAAsB,QAA2C,CAAC;AAO/E,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAQhD,eAAO,MAAM,kCAAkC,IAAI,CAAC;AAEpD,eAAO,MAAM,2CAA2C,IAAI,CAAC;AAE7D,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -446,10 +446,9 @@ const isWedgedBleSetupError = (error) => (error === null || error === void 0 ? v
|
|
|
446
446
|
error.message.startsWith(BLE_SETUP_WEDGED_MESSAGE);
|
|
447
447
|
const shouldRethrowBleSetupError = (error) => isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
448
448
|
const isNativeOperationTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.OperationTimedOut;
|
|
449
|
-
const
|
|
449
|
+
const isAbandonedNativeOperationError = (error) => {
|
|
450
450
|
const errorCode = error === null || error === void 0 ? void 0 : error.errorCode;
|
|
451
|
-
return (errorCode === reactNativeBlePlx.BleErrorCode.
|
|
452
|
-
errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled);
|
|
451
|
+
return (errorCode === reactNativeBlePlx.BleErrorCode.OperationTimedOut || errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled);
|
|
453
452
|
};
|
|
454
453
|
const tryToGetConfiguration = (device) => {
|
|
455
454
|
if (!device || !device.serviceUUIDs)
|
|
@@ -464,7 +463,7 @@ const tryToGetConfiguration = (device) => {
|
|
|
464
463
|
};
|
|
465
464
|
const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __awaiter(void 0, void 0, void 0, function* () {
|
|
466
465
|
if (reactNative.Platform.OS !== 'ios' && reactNative.Platform.OS !== 'android')
|
|
467
|
-
return { device,
|
|
466
|
+
return { device, abandoned: false };
|
|
468
467
|
const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
|
|
469
468
|
let timeoutId;
|
|
470
469
|
let timedOut = false;
|
|
@@ -480,7 +479,7 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
|
|
|
480
479
|
}, BLE_MTU_REQUEST_TIMEOUT_MS);
|
|
481
480
|
}),
|
|
482
481
|
]);
|
|
483
|
-
return { device: mtuDevice,
|
|
482
|
+
return { device: mtuDevice, abandoned: false };
|
|
484
483
|
}
|
|
485
484
|
catch (error) {
|
|
486
485
|
if (timedOut && cancelTransaction) {
|
|
@@ -508,10 +507,9 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
|
|
|
508
507
|
stage,
|
|
509
508
|
attempt,
|
|
510
509
|
actual: device.mtu,
|
|
511
|
-
timedOut,
|
|
512
510
|
error: error instanceof Error ? error.message : String(error),
|
|
513
511
|
});
|
|
514
|
-
return { device, timedOut };
|
|
512
|
+
return { device, abandoned: timedOut };
|
|
515
513
|
}
|
|
516
514
|
finally {
|
|
517
515
|
if (timeoutId)
|
|
@@ -520,7 +518,7 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
|
|
|
520
518
|
});
|
|
521
519
|
const resolveNegotiatedMtu = (device, cancelTransaction) => shouldRefreshNegotiatedMtu(device.mtu)
|
|
522
520
|
? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
|
|
523
|
-
: Promise.resolve({ device,
|
|
521
|
+
: Promise.resolve({ device, abandoned: false });
|
|
524
522
|
function remapError(error) {
|
|
525
523
|
var _a;
|
|
526
524
|
if (error instanceof reactNativeBlePlx.BleError) {
|
|
@@ -557,6 +555,8 @@ class ReactNativeBleTransport {
|
|
|
557
555
|
this.sessionProtocols = new Map();
|
|
558
556
|
this.confirmedProtocolV2 = new Set();
|
|
559
557
|
this.protocolReprobeFailures = new Map();
|
|
558
|
+
this.silentDetections = new Set();
|
|
559
|
+
this.protocolWakeAttempts = new Set();
|
|
560
560
|
this.staleBondErrors = new Map();
|
|
561
561
|
this.acquiringProtocolV2 = new Set();
|
|
562
562
|
this.protocolV2Assemblers = new Map();
|
|
@@ -1043,6 +1043,7 @@ class ReactNativeBleTransport {
|
|
|
1043
1043
|
}
|
|
1044
1044
|
}
|
|
1045
1045
|
let device = null;
|
|
1046
|
+
let mtuHandshakeRefused = false;
|
|
1046
1047
|
if (forceCleanRunPromise && this.runPromise) {
|
|
1047
1048
|
const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
|
|
1048
1049
|
this.runPromise.reject(error);
|
|
@@ -1051,7 +1052,6 @@ class ReactNativeBleTransport {
|
|
|
1051
1052
|
Log === null || Log === void 0 ? void 0 : Log.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
1052
1053
|
}
|
|
1053
1054
|
const blePlxManager = yield this.getPlxManager();
|
|
1054
|
-
let skipPostConnectMtu = false;
|
|
1055
1055
|
try {
|
|
1056
1056
|
yield subscribeBleOn(blePlxManager);
|
|
1057
1057
|
}
|
|
@@ -1100,9 +1100,11 @@ class ReactNativeBleTransport {
|
|
|
1100
1100
|
if (shouldRethrowBleSetupError(e)) {
|
|
1101
1101
|
throw e;
|
|
1102
1102
|
}
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1103
|
+
if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
|
|
1104
|
+
e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
|
|
1105
1105
|
Log === null || Log === void 0 ? void 0 : Log.debug('first try to reconnect without params');
|
|
1106
|
+
mtuHandshakeRefused = true;
|
|
1107
|
+
yield this.runBestEffortNativeOperation('connect cancelled: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid));
|
|
1106
1108
|
device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, fallbackConnectOptions));
|
|
1107
1109
|
}
|
|
1108
1110
|
else if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceAlreadyConnected) {
|
|
@@ -1128,9 +1130,11 @@ class ReactNativeBleTransport {
|
|
|
1128
1130
|
if (shouldRethrowBleSetupError(e)) {
|
|
1129
1131
|
throw e;
|
|
1130
1132
|
}
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1133
|
+
if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
|
|
1134
|
+
e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
|
|
1133
1135
|
Log === null || Log === void 0 ? void 0 : Log.debug('second try to reconnect without params');
|
|
1136
|
+
mtuHandshakeRefused = true;
|
|
1137
|
+
yield this.runBestEffortNativeOperation('connect cancelled: cancel device connection', () => disconnectedDevice.cancelConnection());
|
|
1134
1138
|
try {
|
|
1135
1139
|
device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(fallbackConnectOptions));
|
|
1136
1140
|
}
|
|
@@ -1165,18 +1169,20 @@ class ReactNativeBleTransport {
|
|
|
1165
1169
|
}
|
|
1166
1170
|
if (this.stopped)
|
|
1167
1171
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
|
|
1168
|
-
if (
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1172
|
+
if (mtuHandshakeRefused) {
|
|
1173
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] skipping MTU refresh after cancelled MTU connect', {
|
|
1174
|
+
connectIdSuffix: uuid.slice(-8),
|
|
1175
|
+
actual: device.mtu,
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
const negotiatedMtu = yield resolveNegotiatedMtu(device, transactionId => blePlxManager.cancelTransaction(transactionId));
|
|
1180
|
+
device = negotiatedMtu.device;
|
|
1181
|
+
if (negotiatedMtu.abandoned) {
|
|
1182
|
+
yield this.runNativeTeardown(uuid, blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
|
|
1183
|
+
yield this.runBestEffortNativeOperation('mtu abandoned: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid));
|
|
1184
|
+
}));
|
|
1185
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'BLE MTU negotiation abandoned, reconnecting');
|
|
1180
1186
|
}
|
|
1181
1187
|
}
|
|
1182
1188
|
if (this.stopped)
|
|
@@ -1893,7 +1899,7 @@ class ReactNativeBleTransport {
|
|
|
1893
1899
|
return result;
|
|
1894
1900
|
}
|
|
1895
1901
|
catch (error) {
|
|
1896
|
-
if (timedOut ||
|
|
1902
|
+
if (timedOut || isAbandonedNativeOperationError(error)) {
|
|
1897
1903
|
const resetManager = this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1898
1904
|
if (resetManager) {
|
|
1899
1905
|
throw this.createWedgedBleSetupError();
|
|
@@ -2050,6 +2056,8 @@ class ReactNativeBleTransport {
|
|
|
2050
2056
|
this.acquiringProtocolV2.clear();
|
|
2051
2057
|
this.sessionProtocols.clear();
|
|
2052
2058
|
this.protocolReprobeFailures.clear();
|
|
2059
|
+
this.silentDetections.clear();
|
|
2060
|
+
this.protocolWakeAttempts.clear();
|
|
2053
2061
|
this.writeTimeoutCounts.clear();
|
|
2054
2062
|
this.connectionSetupTimeoutCounts.clear();
|
|
2055
2063
|
this.monitorTokens.clear();
|
|
@@ -2123,6 +2131,7 @@ class ReactNativeBleTransport {
|
|
|
2123
2131
|
!protocolHint &&
|
|
2124
2132
|
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2125
2133
|
const probeOrder = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
2134
|
+
yield this.wakeSilentProtocolV1Device(uuid, probeOrder);
|
|
2126
2135
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
2127
2136
|
const protocol = probeOrder[i];
|
|
2128
2137
|
if (i > 0) {
|
|
@@ -2142,6 +2151,8 @@ class ReactNativeBleTransport {
|
|
|
2142
2151
|
this.confirmedProtocolV2.add(uuid);
|
|
2143
2152
|
}
|
|
2144
2153
|
this.protocolReprobeFailures.delete(uuid);
|
|
2154
|
+
this.silentDetections.delete(uuid);
|
|
2155
|
+
this.protocolWakeAttempts.delete(uuid);
|
|
2145
2156
|
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2146
2157
|
deviceId: uuid,
|
|
2147
2158
|
protocol,
|
|
@@ -2150,6 +2161,7 @@ class ReactNativeBleTransport {
|
|
|
2150
2161
|
return protocol;
|
|
2151
2162
|
}
|
|
2152
2163
|
}
|
|
2164
|
+
this.silentDetections.add(uuid);
|
|
2153
2165
|
if (trustSessionProtocol) {
|
|
2154
2166
|
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
2155
2167
|
}
|
|
@@ -2161,6 +2173,38 @@ class ReactNativeBleTransport {
|
|
|
2161
2173
|
throw this.createProtocolDetectionError();
|
|
2162
2174
|
});
|
|
2163
2175
|
}
|
|
2176
|
+
wakeSilentProtocolV1Device(uuid, probeOrder) {
|
|
2177
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2178
|
+
if (!this._messages)
|
|
2179
|
+
return;
|
|
2180
|
+
if (!probeOrder.includes('V1'))
|
|
2181
|
+
return;
|
|
2182
|
+
if (!this.silentDetections.has(uuid))
|
|
2183
|
+
return;
|
|
2184
|
+
if (this.protocolWakeAttempts.has(uuid))
|
|
2185
|
+
return;
|
|
2186
|
+
if (!transportCache[uuid])
|
|
2187
|
+
return;
|
|
2188
|
+
this.protocolWakeAttempts.add(uuid);
|
|
2189
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
|
|
2190
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2191
|
+
});
|
|
2192
|
+
try {
|
|
2193
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2194
|
+
yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2195
|
+
}
|
|
2196
|
+
catch (error) {
|
|
2197
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2198
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2199
|
+
throw error;
|
|
2200
|
+
}
|
|
2201
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V1 Initialize wake did not answer:', error);
|
|
2202
|
+
}
|
|
2203
|
+
finally {
|
|
2204
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2164
2208
|
resetProbeStateAfterProtocolProbe(uuid, protocol) {
|
|
2165
2209
|
var _a, _b, _c;
|
|
2166
2210
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -2478,11 +2522,11 @@ class ReactNativeBleTransport {
|
|
|
2478
2522
|
const transport = this.getCachedTransport(uuid);
|
|
2479
2523
|
if (!shouldRefreshNegotiatedMtu(transport.mtuSize))
|
|
2480
2524
|
return;
|
|
2481
|
-
const { device: refreshedDevice } = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
|
|
2525
|
+
const { device: refreshedDevice, abandoned } = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
|
|
2482
2526
|
transport.device = refreshedDevice;
|
|
2483
2527
|
transport.mtuSize =
|
|
2484
2528
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2485
|
-
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2529
|
+
if (abandoned || shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2486
2530
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`);
|
|
2487
2531
|
}
|
|
2488
2532
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.2-alpha.
|
|
3
|
+
"version": "1.2.2-alpha.115",
|
|
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.2-alpha.
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.2-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.2-alpha.
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.2-alpha.115",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.2-alpha.115",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.2-alpha.115",
|
|
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": "a115ca6998586c05c22996f278091778fd5471f1"
|
|
30
30
|
}
|
|
@@ -169,13 +169,11 @@ const createHarness = ({
|
|
|
169
169
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
170
170
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
171
171
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
172
|
-
connect: jest.fn(),
|
|
173
172
|
onDisconnected: jest.fn(callback => {
|
|
174
173
|
disconnectCallback = callback;
|
|
175
174
|
return { remove: jest.fn() };
|
|
176
175
|
}),
|
|
177
176
|
} as any;
|
|
178
|
-
device.connect.mockResolvedValue(device);
|
|
179
177
|
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
180
178
|
device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
|
|
181
179
|
const bleManager = {
|
|
@@ -1128,53 +1126,86 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1128
1126
|
await transport.release(uuid, true);
|
|
1129
1127
|
});
|
|
1130
1128
|
|
|
1131
|
-
test('
|
|
1129
|
+
test('drops the link when a stalled MTU negotiation is abandoned', async () => {
|
|
1132
1130
|
const { transport, uuid, device, bleManager } = createHarness();
|
|
1133
1131
|
device.mtu = 23;
|
|
1134
1132
|
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1133
|
+
const { resolveCharacteristics } = transport as any;
|
|
1135
1134
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1135
|
+
// cancelTransaction only disposes the JS subscription: RxAndroidBle releases the
|
|
1136
|
+
// connection's serial queue from onComplete/onError alone, so the abandoned exchange
|
|
1137
|
+
// keeps it until its own native timeout. Service discovery must not queue behind it.
|
|
1138
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1139
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1139
1140
|
});
|
|
1140
1141
|
const transactionId = device.requestMTU.mock.calls[0]?.[1];
|
|
1141
1142
|
expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
|
|
1142
1143
|
expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
|
|
1143
|
-
expect(
|
|
1144
|
-
expect(
|
|
1145
|
-
expect.objectContaining({ timeout: expect.any(Number) })
|
|
1146
|
-
);
|
|
1147
|
-
expect(device.connect.mock.calls.at(-1)?.[0]).not.toHaveProperty('requestMTU');
|
|
1148
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1149
|
-
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1150
|
-
await transport.release(uuid, true);
|
|
1144
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1145
|
+
expect(resolveCharacteristics).not.toHaveBeenCalled();
|
|
1151
1146
|
}, 10_000);
|
|
1152
1147
|
|
|
1153
|
-
test('
|
|
1154
|
-
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1148
|
+
test('skips the MTU refresh after an MTU-bearing connect was cancelled', async () => {
|
|
1155
1149
|
const { transport, uuid, device } = createHarness();
|
|
1156
1150
|
device.mtu = 23;
|
|
1157
|
-
|
|
1158
|
-
device.
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
)
|
|
1164
|
-
|
|
1151
|
+
let connected = false;
|
|
1152
|
+
device.isConnected.mockImplementation(() => Promise.resolve(connected));
|
|
1153
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1154
|
+
device.connect = jest
|
|
1155
|
+
.fn()
|
|
1156
|
+
.mockRejectedValueOnce(cancelled)
|
|
1157
|
+
.mockImplementation(() => {
|
|
1158
|
+
connected = true;
|
|
1159
|
+
return Promise.resolve(device);
|
|
1160
|
+
});
|
|
1165
1161
|
|
|
1166
1162
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1167
1163
|
uuid,
|
|
1168
1164
|
protocolType: 'V2',
|
|
1169
1165
|
});
|
|
1166
|
+
|
|
1167
|
+
// ble-plx applies the connect timeout to establishConnection -> refreshGatt ->
|
|
1168
|
+
// requestMtu as one chain, so the fallback means this peripheral did not finish the
|
|
1169
|
+
// MTU exchange. Asking again would only park a second abandoned exchange.
|
|
1170
1170
|
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
1171
|
-
expect(device.connect.mock.calls[0][0]).toEqual(expect.objectContaining({ requestMTU: 247 }));
|
|
1172
|
-
expect(device.connect.mock.calls[1][0]).not.toHaveProperty('requestMTU');
|
|
1173
1171
|
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1174
1172
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1175
1173
|
await transport.release(uuid, true);
|
|
1176
1174
|
});
|
|
1177
1175
|
|
|
1176
|
+
test('cleans up a cancelled native GATT setup, not only a timed-out one', async () => {
|
|
1177
|
+
const { transport, uuid, bleManager } = createHarness();
|
|
1178
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1179
|
+
(transport as any).resolveCharacteristics = jest.fn(() => Promise.reject(cancelled));
|
|
1180
|
+
|
|
1181
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1182
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test('spends one Initialize wake on the detection after a fully silent one', async () => {
|
|
1186
|
+
const { transport, uuid } = createHarness();
|
|
1187
|
+
const probes = transport as any;
|
|
1188
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1189
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1190
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1191
|
+
|
|
1192
|
+
// Nothing has gone silent yet, so the device keeps whatever session it has.
|
|
1193
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1194
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1195
|
+
|
|
1196
|
+
// The previous detection answered on no protocol, which is what a sleeping Classic
|
|
1197
|
+
// looks like, so this one wakes on the fresh link before probing.
|
|
1198
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1199
|
+
expect(callProtocolV1).toHaveBeenCalledTimes(1);
|
|
1200
|
+
expect(callProtocolV1.mock.calls[0]?.[1]).toBe('Initialize');
|
|
1201
|
+
|
|
1202
|
+
// Still silent: the wake is not repeated, so a device that is simply away is not
|
|
1203
|
+
// pushed into a fresh wallet session on every poll.
|
|
1204
|
+
callProtocolV1.mockClear();
|
|
1205
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1206
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1178
1209
|
test('accepts a stable low MTU without the delayed refresh loop', async () => {
|
|
1179
1210
|
const { transport, uuid, device } = createHarness();
|
|
1180
1211
|
device.mtu = 185;
|
package/src/index.ts
CHANGED
|
@@ -300,16 +300,22 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
|
300
300
|
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
301
301
|
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
302
302
|
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
303
|
-
|
|
303
|
+
/**
|
|
304
|
+
* ble-plx reports its own expired budget as OperationCancelled, not OperationTimedOut:
|
|
305
|
+
* safeConnectToDevice disposes the operation chain and its doFinally reports a
|
|
306
|
+
* cancellation. Either code means the native layer abandoned work that still owned
|
|
307
|
+
* connection/GATT state, so both have to go through the stalled-connection cleanup.
|
|
308
|
+
* Only used where nothing else recovers the link — the connect path does its own
|
|
309
|
+
* ordered teardown before retrying, and routing it here too would both duplicate that
|
|
310
|
+
* and spend the manager-reset budget on a connect that is about to be retried.
|
|
311
|
+
*/
|
|
312
|
+
const isAbandonedNativeOperationError = (error: unknown): boolean => {
|
|
304
313
|
const errorCode = (error as { errorCode?: unknown })?.errorCode;
|
|
305
314
|
return (
|
|
306
|
-
errorCode === BleErrorCode.
|
|
307
|
-
errorCode === BleErrorCode.OperationCancelled
|
|
315
|
+
errorCode === BleErrorCode.OperationTimedOut || errorCode === BleErrorCode.OperationCancelled
|
|
308
316
|
);
|
|
309
317
|
};
|
|
310
318
|
|
|
311
|
-
type NegotiatedMtuResult = { device: Device; timedOut: boolean };
|
|
312
|
-
|
|
313
319
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
314
320
|
|
|
315
321
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -321,13 +327,26 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
321
327
|
return infos;
|
|
322
328
|
};
|
|
323
329
|
|
|
330
|
+
type NegotiatedMtuResult = {
|
|
331
|
+
device: Device;
|
|
332
|
+
/**
|
|
333
|
+
* True when the bounded wait gave up while the native MTU exchange was still
|
|
334
|
+
* outstanding. `cancelTransaction` only disposes the JS-side subscription:
|
|
335
|
+
* RxAndroidBle releases a connection's serial operation queue from onComplete /
|
|
336
|
+
* onError alone, so an abandoned exchange keeps that queue until its own native
|
|
337
|
+
* timeout. Any GATT work issued before then — service discovery above all —
|
|
338
|
+
* queues behind it and cannot settle inside its own budget.
|
|
339
|
+
*/
|
|
340
|
+
abandoned: boolean;
|
|
341
|
+
};
|
|
342
|
+
|
|
324
343
|
const requestNegotiatedMtu = async (
|
|
325
344
|
device: Device,
|
|
326
345
|
stage: 'connected' | 'highThroughput',
|
|
327
346
|
attempt: number,
|
|
328
347
|
cancelTransaction?: (transactionId: string) => Promise<void> | void
|
|
329
348
|
): Promise<NegotiatedMtuResult> => {
|
|
330
|
-
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device,
|
|
349
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, abandoned: false };
|
|
331
350
|
|
|
332
351
|
const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
|
|
333
352
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -350,7 +369,7 @@ const requestNegotiatedMtu = async (
|
|
|
350
369
|
}, BLE_MTU_REQUEST_TIMEOUT_MS);
|
|
351
370
|
}),
|
|
352
371
|
]);
|
|
353
|
-
return { device: mtuDevice,
|
|
372
|
+
return { device: mtuDevice, abandoned: false };
|
|
354
373
|
} catch (error) {
|
|
355
374
|
if (timedOut && cancelTransaction) {
|
|
356
375
|
try {
|
|
@@ -376,10 +395,9 @@ const requestNegotiatedMtu = async (
|
|
|
376
395
|
stage,
|
|
377
396
|
attempt,
|
|
378
397
|
actual: device.mtu,
|
|
379
|
-
timedOut,
|
|
380
398
|
error: error instanceof Error ? error.message : String(error),
|
|
381
399
|
});
|
|
382
|
-
return { device, timedOut };
|
|
400
|
+
return { device, abandoned: timedOut };
|
|
383
401
|
} finally {
|
|
384
402
|
if (timeoutId) clearTimeout(timeoutId);
|
|
385
403
|
}
|
|
@@ -391,7 +409,7 @@ const resolveNegotiatedMtu = (
|
|
|
391
409
|
): Promise<NegotiatedMtuResult> =>
|
|
392
410
|
shouldRefreshNegotiatedMtu(device.mtu)
|
|
393
411
|
? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
|
|
394
|
-
: Promise.resolve({ device,
|
|
412
|
+
: Promise.resolve({ device, abandoned: false });
|
|
395
413
|
|
|
396
414
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
397
415
|
|
|
@@ -475,6 +493,17 @@ export default class ReactNativeBleTransport {
|
|
|
475
493
|
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
476
494
|
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
477
495
|
|
|
496
|
+
/** Endpoints whose last detection got no answer on any protocol. */
|
|
497
|
+
private silentDetections = new Set<string>();
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Endpoints already sent a Protocol V1 wake. The wake starts a fresh wallet session
|
|
501
|
+
* on the device, and a failed detection tears the link down and reconnects on every
|
|
502
|
+
* poll, so neither set is cleared on teardown: one wake is spent per endpoint until a
|
|
503
|
+
* detection succeeds, which re-arms it for the next time the device sleeps.
|
|
504
|
+
*/
|
|
505
|
+
private protocolWakeAttempts = new Set<string>();
|
|
506
|
+
|
|
478
507
|
/**
|
|
479
508
|
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
480
509
|
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
@@ -1072,6 +1101,14 @@ export default class ReactNativeBleTransport {
|
|
|
1072
1101
|
}
|
|
1073
1102
|
|
|
1074
1103
|
let device: Device | null = null;
|
|
1104
|
+
/**
|
|
1105
|
+
* Set when an MTU-bearing connect had to fall back to `fallbackConnectOptions`.
|
|
1106
|
+
* ble-plx applies `connectOptions.timeout` to the whole establishConnection ->
|
|
1107
|
+
* refreshGatt -> requestMtu chain, so that fallback means the peripheral did not
|
|
1108
|
+
* finish the MTU exchange on this link. Asking again below would only park a
|
|
1109
|
+
* second abandoned exchange on the connection's serial queue.
|
|
1110
|
+
*/
|
|
1111
|
+
let mtuHandshakeRefused = false;
|
|
1075
1112
|
|
|
1076
1113
|
if (forceCleanRunPromise && this.runPromise) {
|
|
1077
1114
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
@@ -1082,7 +1119,6 @@ export default class ReactNativeBleTransport {
|
|
|
1082
1119
|
}
|
|
1083
1120
|
|
|
1084
1121
|
const blePlxManager = await this.getPlxManager();
|
|
1085
|
-
let skipPostConnectMtu = false;
|
|
1086
1122
|
try {
|
|
1087
1123
|
await subscribeBleOn(blePlxManager);
|
|
1088
1124
|
} catch (error) {
|
|
@@ -1134,9 +1170,18 @@ export default class ReactNativeBleTransport {
|
|
|
1134
1170
|
if (shouldRethrowBleSetupError(e)) {
|
|
1135
1171
|
throw e;
|
|
1136
1172
|
}
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1173
|
+
if (
|
|
1174
|
+
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
1175
|
+
e.errorCode === BleErrorCode.OperationCancelled
|
|
1176
|
+
) {
|
|
1139
1177
|
Log?.debug('first try to reconnect without params');
|
|
1178
|
+
mtuHandshakeRefused = true;
|
|
1179
|
+
// The disposed chain can still own native connection state, and the retry
|
|
1180
|
+
// would otherwise run on top of a half-open GATT client.
|
|
1181
|
+
await this.runBestEffortNativeOperation(
|
|
1182
|
+
'connect cancelled: cancel manager connection',
|
|
1183
|
+
() => blePlxManager.cancelDeviceConnection(uuid)
|
|
1184
|
+
);
|
|
1140
1185
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1141
1186
|
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
1142
1187
|
);
|
|
@@ -1166,9 +1211,18 @@ export default class ReactNativeBleTransport {
|
|
|
1166
1211
|
if (shouldRethrowBleSetupError(e)) {
|
|
1167
1212
|
throw e;
|
|
1168
1213
|
}
|
|
1169
|
-
if (
|
|
1170
|
-
|
|
1214
|
+
if (
|
|
1215
|
+
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
1216
|
+
e.errorCode === BleErrorCode.OperationCancelled
|
|
1217
|
+
) {
|
|
1171
1218
|
Log?.debug('second try to reconnect without params');
|
|
1219
|
+
mtuHandshakeRefused = true;
|
|
1220
|
+
// The disposed chain can still own native connection state, and the retry
|
|
1221
|
+
// would otherwise run on top of a half-open GATT client.
|
|
1222
|
+
await this.runBestEffortNativeOperation(
|
|
1223
|
+
'connect cancelled: cancel device connection',
|
|
1224
|
+
() => disconnectedDevice.cancelConnection()
|
|
1225
|
+
);
|
|
1172
1226
|
try {
|
|
1173
1227
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1174
1228
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
@@ -1209,26 +1263,27 @@ export default class ReactNativeBleTransport {
|
|
|
1209
1263
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1210
1264
|
}
|
|
1211
1265
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1266
|
+
if (mtuHandshakeRefused) {
|
|
1267
|
+
Log?.debug('[ReactNativeBleTransport] skipping MTU refresh after cancelled MTU connect', {
|
|
1268
|
+
connectIdSuffix: uuid.slice(-8),
|
|
1269
|
+
actual: device.mtu,
|
|
1270
|
+
});
|
|
1271
|
+
} else {
|
|
1272
|
+
const negotiatedMtu = await resolveNegotiatedMtu(device, transactionId =>
|
|
1217
1273
|
blePlxManager.cancelTransaction(transactionId)
|
|
1218
1274
|
);
|
|
1219
|
-
device =
|
|
1220
|
-
if (
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
timedOutDevice.connect(fallbackConnectOptions)
|
|
1275
|
+
device = negotiatedMtu.device;
|
|
1276
|
+
if (negotiatedMtu.abandoned) {
|
|
1277
|
+
// Service discovery would queue behind the abandoned exchange and miss its own
|
|
1278
|
+
// budget. Drop the link so the next attempt starts on a queue nothing holds.
|
|
1279
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1280
|
+
await this.runBestEffortNativeOperation('mtu abandoned: cancel manager connection', () =>
|
|
1281
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1282
|
+
);
|
|
1283
|
+
});
|
|
1284
|
+
throw ERRORS.TypedError(
|
|
1285
|
+
HardwareErrorCode.BleConnectedError,
|
|
1286
|
+
'BLE MTU negotiation abandoned, reconnecting'
|
|
1232
1287
|
);
|
|
1233
1288
|
}
|
|
1234
1289
|
}
|
|
@@ -2127,7 +2182,7 @@ export default class ReactNativeBleTransport {
|
|
|
2127
2182
|
succeeded = true;
|
|
2128
2183
|
return result;
|
|
2129
2184
|
} catch (error) {
|
|
2130
|
-
if (timedOut ||
|
|
2185
|
+
if (timedOut || isAbandonedNativeOperationError(error)) {
|
|
2131
2186
|
const resetManager = this.abandonStalledConnection(
|
|
2132
2187
|
uuid,
|
|
2133
2188
|
timedOut ? 'gatt-backstop' : 'gatt-native'
|
|
@@ -2331,6 +2386,8 @@ export default class ReactNativeBleTransport {
|
|
|
2331
2386
|
// Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
|
|
2332
2387
|
// firmware reconnect after the native BLE manager is recreated.
|
|
2333
2388
|
this.protocolReprobeFailures.clear();
|
|
2389
|
+
this.silentDetections.clear();
|
|
2390
|
+
this.protocolWakeAttempts.clear();
|
|
2334
2391
|
this.writeTimeoutCounts.clear();
|
|
2335
2392
|
this.connectionSetupTimeoutCounts.clear();
|
|
2336
2393
|
this.monitorTokens.clear();
|
|
@@ -2442,6 +2499,8 @@ export default class ReactNativeBleTransport {
|
|
|
2442
2499
|
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2443
2500
|
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
2444
2501
|
|
|
2502
|
+
await this.wakeSilentProtocolV1Device(uuid, probeOrder);
|
|
2503
|
+
|
|
2445
2504
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
2446
2505
|
const protocol = probeOrder[i];
|
|
2447
2506
|
if (i > 0) {
|
|
@@ -2463,6 +2522,8 @@ export default class ReactNativeBleTransport {
|
|
|
2463
2522
|
this.confirmedProtocolV2.add(uuid);
|
|
2464
2523
|
}
|
|
2465
2524
|
this.protocolReprobeFailures.delete(uuid);
|
|
2525
|
+
this.silentDetections.delete(uuid);
|
|
2526
|
+
this.protocolWakeAttempts.delete(uuid);
|
|
2466
2527
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2467
2528
|
deviceId: uuid,
|
|
2468
2529
|
protocol,
|
|
@@ -2472,6 +2533,10 @@ export default class ReactNativeBleTransport {
|
|
|
2472
2533
|
}
|
|
2473
2534
|
}
|
|
2474
2535
|
|
|
2536
|
+
// Nothing answered on any probed protocol. Remember it so the next detection, which
|
|
2537
|
+
// the caller reaches on a freshly reconnected link, can spend a wake first.
|
|
2538
|
+
this.silentDetections.add(uuid);
|
|
2539
|
+
|
|
2475
2540
|
if (trustSessionProtocol) {
|
|
2476
2541
|
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2477
2542
|
// shortcut so a device that genuinely switched protocols is found again.
|
|
@@ -2485,6 +2550,47 @@ export default class ReactNativeBleTransport {
|
|
|
2485
2550
|
throw this.createProtocolDetectionError();
|
|
2486
2551
|
}
|
|
2487
2552
|
|
|
2553
|
+
/**
|
|
2554
|
+
* Sent once before probing when the previous detection on this endpoint got no answer
|
|
2555
|
+
* at all. On Classic-family firmware that silence is also what a sleeping device looks
|
|
2556
|
+
* like: its BLE co-processor keeps serving connect, GATT and MTU while the main MCU
|
|
2557
|
+
* sits in the screensaver loop, whose host-message filter accepts only Initialize and
|
|
2558
|
+
* the *Ack messages. GetFeatures and Ping are dropped there without a reply, so no
|
|
2559
|
+
* amount of probing can bring the device back.
|
|
2560
|
+
*
|
|
2561
|
+
* Initialize is the one message that breaks that loop, but it also starts a fresh
|
|
2562
|
+
* wallet session, which is why it is not the probe itself — a bare Initialize on every
|
|
2563
|
+
* acquire would drop a hidden-wallet session before Core can restore it. Requiring a
|
|
2564
|
+
* fully silent previous detection keeps it to devices with no session left to protect.
|
|
2565
|
+
*
|
|
2566
|
+
* The firmware consumes the wake to leave its loop and does not reliably answer it, so
|
|
2567
|
+
* the result is ignored: the probes that follow are what decide the protocol.
|
|
2568
|
+
*/
|
|
2569
|
+
private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
|
|
2570
|
+
if (!this._messages) return;
|
|
2571
|
+
if (!probeOrder.includes('V1')) return;
|
|
2572
|
+
if (!this.silentDetections.has(uuid)) return;
|
|
2573
|
+
if (this.protocolWakeAttempts.has(uuid)) return;
|
|
2574
|
+
if (!transportCache[uuid]) return;
|
|
2575
|
+
this.protocolWakeAttempts.add(uuid);
|
|
2576
|
+
|
|
2577
|
+
Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
|
|
2578
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2579
|
+
});
|
|
2580
|
+
try {
|
|
2581
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2582
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2583
|
+
} catch (error) {
|
|
2584
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2585
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2586
|
+
throw error;
|
|
2587
|
+
}
|
|
2588
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize wake did not answer:', error);
|
|
2589
|
+
} finally {
|
|
2590
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2488
2594
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
2489
2595
|
const transport = transportCache[uuid];
|
|
2490
2596
|
await this.protocolV2Links.invalidateLink(
|
|
@@ -2864,7 +2970,7 @@ export default class ReactNativeBleTransport {
|
|
|
2864
2970
|
const transport = this.getCachedTransport(uuid);
|
|
2865
2971
|
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2866
2972
|
|
|
2867
|
-
const { device: refreshedDevice } = await requestNegotiatedMtu(
|
|
2973
|
+
const { device: refreshedDevice, abandoned } = await requestNegotiatedMtu(
|
|
2868
2974
|
transport.device,
|
|
2869
2975
|
'highThroughput',
|
|
2870
2976
|
1,
|
|
@@ -2874,7 +2980,7 @@ export default class ReactNativeBleTransport {
|
|
|
2874
2980
|
transport.mtuSize =
|
|
2875
2981
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2876
2982
|
|
|
2877
|
-
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2983
|
+
if (abandoned || shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2878
2984
|
throw ERRORS.TypedError(
|
|
2879
2985
|
HardwareErrorCode.BleConnectedError,
|
|
2880
2986
|
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|