@onekeyfe/hd-transport-react-native 1.2.2-alpha.119 → 1.2.2-alpha.120
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 +1 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -265
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +1 -85
- package/src/__tests__/protocolV2Link.test.ts +25 -600
- package/src/index.ts +67 -366
package/src/index.ts
CHANGED
|
@@ -242,13 +242,13 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
242
242
|
|
|
243
243
|
const IOS_REQUEST_MTU = 247;
|
|
244
244
|
const ANDROID_REQUEST_MTU = 517;
|
|
245
|
+
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
245
246
|
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
246
247
|
|
|
247
248
|
const getRequestedBleMtu = () =>
|
|
248
249
|
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
249
250
|
|
|
250
251
|
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
251
|
-
const BLE_MTU_REQUEST_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS;
|
|
252
252
|
|
|
253
253
|
const connectOptions: Record<string, unknown> = {
|
|
254
254
|
requestMTU: getRequestedBleMtu(),
|
|
@@ -256,63 +256,11 @@ const connectOptions: Record<string, unknown> = {
|
|
|
256
256
|
refreshGatt: 'OnConnected',
|
|
257
257
|
};
|
|
258
258
|
|
|
259
|
-
/**
|
|
259
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
260
260
|
const fallbackConnectOptions: Record<string, unknown> = {
|
|
261
261
|
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
262
262
|
};
|
|
263
263
|
|
|
264
|
-
/**
|
|
265
|
-
* Android never requests the MTU inside the native connect budget: refreshGatt makes the stack
|
|
266
|
-
* rediscover first, and a budget that expires with the MTU request unsent parks every later MTU
|
|
267
|
-
* request on that LE link. refreshGatt itself is only added after a firmware install or a
|
|
268
|
-
* stale-table symptom, and discovery finishes before the MTU exchange.
|
|
269
|
-
*/
|
|
270
|
-
const androidRefreshGattConnectOptions: Record<string, unknown> = {
|
|
271
|
-
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
272
|
-
refreshGatt: 'OnConnected',
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* With no cached GATT table the stack runs its own discovery (up to ~8s) before the MTU
|
|
277
|
-
* exchange, so the bound sits above that; a stuck exchange never completes.
|
|
278
|
-
*/
|
|
279
|
-
export const ANDROID_MTU_EXCHANGE_TIMEOUT_MS = 12_000;
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Android keeps an LE link, with per-link ATT state such as a pending MTU exchange, for its
|
|
283
|
-
* 4s GATT link idle timer after the last client closes; a reconnect inside it reuses the link.
|
|
284
|
-
*/
|
|
285
|
-
export const ANDROID_LINK_DROP_QUIET_MS = 5000;
|
|
286
|
-
const ANDROID_LINK_DROP_POLL_MS = 250;
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Android cannot use a link at the default 23-byte ATT MTU: Protocol V1 writes 192-byte
|
|
290
|
-
* packets regardless, and a Pro 2 sends only the first ATT_MTU-3 bytes of a V2 reply.
|
|
291
|
-
* An unknown MTU is not treated as default.
|
|
292
|
-
*/
|
|
293
|
-
const isKnownDefaultMtu = (mtu: unknown): boolean =>
|
|
294
|
-
typeof mtu === 'number' && Number.isFinite(mtu) && mtu <= 23;
|
|
295
|
-
|
|
296
|
-
/** Discovery found no OneKey service, or a characteristic of the wrong shape: the cached GATT table may be stale. */
|
|
297
|
-
const isMissingGattShapeError = (error: unknown): boolean => {
|
|
298
|
-
const code = (error as { errorCode?: unknown })?.errorCode;
|
|
299
|
-
const message = (error as { message?: unknown })?.message;
|
|
300
|
-
return (
|
|
301
|
-
code === HardwareErrorCode.BleServiceNotFound ||
|
|
302
|
-
code === HardwareErrorCode.BleCharacteristicNotFound ||
|
|
303
|
-
(typeof message === 'string' &&
|
|
304
|
-
(message.includes('BLECharacteristicNotFound') ||
|
|
305
|
-
message.includes('BLECharacteristicNotWritable') ||
|
|
306
|
-
message.includes('BLECharacteristicNotNotifiable')))
|
|
307
|
-
);
|
|
308
|
-
};
|
|
309
|
-
const isStaleGattTableNotifyReason = (reason: string | null | undefined): boolean =>
|
|
310
|
-
!!reason &&
|
|
311
|
-
(reason.includes('Cannot write client characteristic config descriptor') ||
|
|
312
|
-
reason.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
313
|
-
reason.includes('The handle is invalid') ||
|
|
314
|
-
reason.includes('Writing is not permitted')); // pro firmware 2.3.4 upgrade
|
|
315
|
-
|
|
316
264
|
/**
|
|
317
265
|
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
318
266
|
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
@@ -352,15 +300,6 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
|
352
300
|
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
353
301
|
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
354
302
|
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
355
|
-
const isMtuOrCancelledConnectError = (error: unknown): boolean => {
|
|
356
|
-
const errorCode = (error as { errorCode?: unknown })?.errorCode;
|
|
357
|
-
return (
|
|
358
|
-
errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
359
|
-
errorCode === BleErrorCode.OperationCancelled
|
|
360
|
-
);
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
type NegotiatedMtuResult = { device: Device; timedOut: boolean };
|
|
364
303
|
|
|
365
304
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
366
305
|
|
|
@@ -375,75 +314,29 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
375
314
|
|
|
376
315
|
const requestNegotiatedMtu = async (
|
|
377
316
|
device: Device,
|
|
378
|
-
stage: 'connected' | 'highThroughput',
|
|
379
|
-
attempt: number
|
|
380
|
-
|
|
381
|
-
)
|
|
382
|
-
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, timedOut: false };
|
|
383
|
-
|
|
384
|
-
const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
|
|
385
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
386
|
-
let timedOut = false;
|
|
317
|
+
stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
|
|
318
|
+
attempt: number
|
|
319
|
+
) => {
|
|
320
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
|
|
387
321
|
|
|
388
322
|
try {
|
|
389
323
|
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
390
324
|
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
// rejection handler so a late native cancellation cannot become an
|
|
394
|
-
// unhandled rejection after we continue with the current MTU.
|
|
395
|
-
request.catch(() => undefined);
|
|
396
|
-
const mtuDevice = await Promise.race([
|
|
397
|
-
request,
|
|
398
|
-
new Promise<never>((_, reject) => {
|
|
399
|
-
timeoutId = setTimeout(() => {
|
|
400
|
-
timedOut = true;
|
|
401
|
-
reject(new Error(`BLE MTU request timeout after ${BLE_MTU_REQUEST_TIMEOUT_MS}ms`));
|
|
402
|
-
}, BLE_MTU_REQUEST_TIMEOUT_MS);
|
|
403
|
-
}),
|
|
404
|
-
]);
|
|
405
|
-
return { device: mtuDevice, timedOut: false };
|
|
325
|
+
const mtuDevice = await device.requestMTU(getRequestedBleMtu());
|
|
326
|
+
return mtuDevice;
|
|
406
327
|
} catch (error) {
|
|
407
|
-
if (timedOut && cancelTransaction) {
|
|
408
|
-
try {
|
|
409
|
-
Promise.resolve(cancelTransaction(transactionId)).catch(cancelError => {
|
|
410
|
-
Log?.debug('[ReactNativeBleTransport] MTU cancellation failed', {
|
|
411
|
-
platform: Platform.OS,
|
|
412
|
-
stage,
|
|
413
|
-
attempt,
|
|
414
|
-
error: cancelError instanceof Error ? cancelError.message : String(cancelError),
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
} catch (cancelError) {
|
|
418
|
-
Log?.debug('[ReactNativeBleTransport] MTU cancellation failed', {
|
|
419
|
-
platform: Platform.OS,
|
|
420
|
-
stage,
|
|
421
|
-
attempt,
|
|
422
|
-
error: cancelError instanceof Error ? cancelError.message : String(cancelError),
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
328
|
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
427
329
|
platform: Platform.OS,
|
|
428
330
|
stage,
|
|
429
331
|
attempt,
|
|
430
332
|
actual: device.mtu,
|
|
431
|
-
timedOut,
|
|
432
333
|
error: error instanceof Error ? error.message : String(error),
|
|
433
334
|
});
|
|
434
|
-
return
|
|
435
|
-
} finally {
|
|
436
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
335
|
+
return device;
|
|
437
336
|
}
|
|
438
337
|
};
|
|
439
338
|
|
|
440
|
-
const resolveNegotiatedMtu = (
|
|
441
|
-
device: Device,
|
|
442
|
-
cancelTransaction?: (transactionId: string) => Promise<void> | void
|
|
443
|
-
): Promise<NegotiatedMtuResult> =>
|
|
444
|
-
shouldRefreshNegotiatedMtu(device.mtu)
|
|
445
|
-
? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
|
|
446
|
-
: Promise.resolve({ device, timedOut: false });
|
|
339
|
+
const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
|
|
447
340
|
|
|
448
341
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
449
342
|
|
|
@@ -527,12 +420,6 @@ export default class ReactNativeBleTransport {
|
|
|
527
420
|
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
528
421
|
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
529
422
|
|
|
530
|
-
/** Endpoints whose last detection got no answer; 'woken' once their Initialize wake is spent. */
|
|
531
|
-
private silentDetections = new Map<string, 'silent' | 'woken'>();
|
|
532
|
-
|
|
533
|
-
/** Android endpoints whose cached GATT table is suspect; the next connect refreshes it. */
|
|
534
|
-
private androidGattCacheRefreshes = new Set<string>();
|
|
535
|
-
|
|
536
423
|
/**
|
|
537
424
|
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
538
425
|
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
@@ -1039,10 +926,41 @@ export default class ReactNativeBleTransport {
|
|
|
1039
926
|
}
|
|
1040
927
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1041
928
|
|
|
929
|
+
const initialMtu = transport.mtuSize;
|
|
930
|
+
let refreshAttempts = 0;
|
|
931
|
+
if (
|
|
932
|
+
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
933
|
+
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
934
|
+
) {
|
|
935
|
+
refreshAttempts += 1;
|
|
936
|
+
let refreshedDevice = await requestNegotiatedMtu(
|
|
937
|
+
transport.device,
|
|
938
|
+
'servicesAndNotifyReady',
|
|
939
|
+
1
|
|
940
|
+
);
|
|
941
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
942
|
+
transport.device = refreshedDevice;
|
|
943
|
+
transport.mtuSize =
|
|
944
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
945
|
+
|
|
946
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
947
|
+
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
948
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
949
|
+
refreshAttempts += 1;
|
|
950
|
+
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
951
|
+
transport.device = refreshedDevice;
|
|
952
|
+
transport.mtuSize =
|
|
953
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1042
958
|
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
1043
959
|
platform: Platform.OS,
|
|
1044
960
|
requested: getRequestedBleMtu(),
|
|
961
|
+
initial: initialMtu,
|
|
1045
962
|
actual: transport.mtuSize,
|
|
963
|
+
refreshAttempts,
|
|
1046
964
|
});
|
|
1047
965
|
|
|
1048
966
|
return transport;
|
|
@@ -1110,12 +1028,8 @@ export default class ReactNativeBleTransport {
|
|
|
1110
1028
|
const isCachedDeviceConnected = await cachedTransport.device
|
|
1111
1029
|
.isConnected()
|
|
1112
1030
|
.catch(() => false);
|
|
1113
|
-
// A suspect GATT table is only refreshed through a new connect.
|
|
1114
|
-
const isCachedAndroidLinkUsable =
|
|
1115
|
-
Platform.OS !== 'android' || !this.androidGattCacheRefreshes.has(uuid);
|
|
1116
1031
|
if (
|
|
1117
1032
|
isCachedDeviceConnected &&
|
|
1118
|
-
isCachedAndroidLinkUsable &&
|
|
1119
1033
|
cachedProtocol &&
|
|
1120
1034
|
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
1121
1035
|
) {
|
|
@@ -1134,18 +1048,6 @@ export default class ReactNativeBleTransport {
|
|
|
1134
1048
|
}
|
|
1135
1049
|
|
|
1136
1050
|
let device: Device | null = null;
|
|
1137
|
-
const isAndroid = Platform.OS === 'android';
|
|
1138
|
-
// A firmware-install reconnect always refreshes: the new firmware may expose a different table.
|
|
1139
|
-
const refreshAndroidGattCache =
|
|
1140
|
-
isAndroid && (!!skipProtocolProbe || this.androidGattCacheRefreshes.has(uuid));
|
|
1141
|
-
let nativeConnectOptions = connectOptions;
|
|
1142
|
-
if (isAndroid) {
|
|
1143
|
-
nativeConnectOptions = refreshAndroidGattCache
|
|
1144
|
-
? androidRefreshGattConnectOptions
|
|
1145
|
-
: fallbackConnectOptions;
|
|
1146
|
-
}
|
|
1147
|
-
// Only a connect that carried refreshGatt clears the marker; the fallback connects drop it.
|
|
1148
|
-
let androidRefreshConnectRan = false;
|
|
1149
1051
|
|
|
1150
1052
|
if (forceCleanRunPromise && this.runPromise) {
|
|
1151
1053
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
@@ -1156,7 +1058,6 @@ export default class ReactNativeBleTransport {
|
|
|
1156
1058
|
}
|
|
1157
1059
|
|
|
1158
1060
|
const blePlxManager = await this.getPlxManager();
|
|
1159
|
-
let skipPostConnectMtu = false;
|
|
1160
1061
|
try {
|
|
1161
1062
|
await subscribeBleOn(blePlxManager);
|
|
1162
1063
|
} catch (error) {
|
|
@@ -1201,16 +1102,17 @@ export default class ReactNativeBleTransport {
|
|
|
1201
1102
|
Log?.debug('try to connect to device: ', uuid);
|
|
1202
1103
|
try {
|
|
1203
1104
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1204
|
-
blePlxManager.connectToDevice(uuid,
|
|
1105
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
1205
1106
|
);
|
|
1206
|
-
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1207
1107
|
} catch (e) {
|
|
1208
1108
|
Log?.debug('try to connect to device has error: ', e);
|
|
1209
1109
|
if (shouldRethrowBleSetupError(e)) {
|
|
1210
1110
|
throw e;
|
|
1211
1111
|
}
|
|
1212
|
-
if (
|
|
1213
|
-
|
|
1112
|
+
if (
|
|
1113
|
+
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
1114
|
+
e.errorCode === BleErrorCode.OperationCancelled
|
|
1115
|
+
) {
|
|
1214
1116
|
Log?.debug('first try to reconnect without params');
|
|
1215
1117
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1216
1118
|
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
@@ -1228,33 +1130,23 @@ export default class ReactNativeBleTransport {
|
|
|
1228
1130
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'unable to connect to device');
|
|
1229
1131
|
}
|
|
1230
1132
|
|
|
1231
|
-
if (
|
|
1232
|
-
refreshAndroidGattCache &&
|
|
1233
|
-
!androidRefreshConnectRan &&
|
|
1234
|
-
(await device.isConnected().catch(() => false))
|
|
1235
|
-
) {
|
|
1236
|
-
// refreshGatt only reaches the stack through a connect. A link that is still up would
|
|
1237
|
-
// skip the connect below and keep serving the stale table, so it is dropped first.
|
|
1238
|
-
await this.dropAndroidLink(uuid, blePlxManager, device, 'gatt cache refresh');
|
|
1239
|
-
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
1133
|
if (!(await device.isConnected())) {
|
|
1243
1134
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
1244
1135
|
const disconnectedDevice = device;
|
|
1245
1136
|
|
|
1246
1137
|
try {
|
|
1247
1138
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1248
|
-
disconnectedDevice.connect(
|
|
1139
|
+
disconnectedDevice.connect(connectOptions)
|
|
1249
1140
|
);
|
|
1250
|
-
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1251
1141
|
} catch (e) {
|
|
1252
1142
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
1253
1143
|
if (shouldRethrowBleSetupError(e)) {
|
|
1254
1144
|
throw e;
|
|
1255
1145
|
}
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1146
|
+
if (
|
|
1147
|
+
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
1148
|
+
e.errorCode === BleErrorCode.OperationCancelled
|
|
1149
|
+
) {
|
|
1258
1150
|
Log?.debug('second try to reconnect without params');
|
|
1259
1151
|
try {
|
|
1260
1152
|
device = await this.connectWithTimeout(uuid, () =>
|
|
@@ -1296,63 +1188,11 @@ export default class ReactNativeBleTransport {
|
|
|
1296
1188
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1297
1189
|
}
|
|
1298
1190
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1299
|
-
|
|
1300
|
-
if (isAndroid) {
|
|
1301
|
-
if (refreshAndroidGattCache) {
|
|
1302
|
-
// refreshGatt has already started a full rediscovery; let it finish before the MTU
|
|
1303
|
-
// exchange so the request is not queued behind it.
|
|
1304
|
-
characteristics = await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
1305
|
-
if (androidRefreshConnectRan) this.androidGattCacheRefreshes.delete(uuid);
|
|
1306
|
-
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1307
|
-
}
|
|
1308
|
-
device = await this.negotiateAndroidMtu(uuid, blePlxManager, device);
|
|
1309
|
-
} else if (!skipPostConnectMtu) {
|
|
1310
|
-
// Match 1.1.31: MTU is a connect() best-effort. If connect already fell back
|
|
1311
|
-
// without requestMTU, do not put another requestMTU on the native serial
|
|
1312
|
-
// queue — that is what wedges GATT after Account#2 reconnect.
|
|
1313
|
-
const mtuResult = await resolveNegotiatedMtu(device, transactionId =>
|
|
1314
|
-
blePlxManager.cancelTransaction(transactionId)
|
|
1315
|
-
);
|
|
1316
|
-
device = mtuResult.device;
|
|
1317
|
-
if (mtuResult.timedOut) {
|
|
1318
|
-
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1319
|
-
Log?.debug(
|
|
1320
|
-
'[ReactNativeBleTransport] post-connect MTU timed out, reconnecting without requesting MTU'
|
|
1321
|
-
);
|
|
1322
|
-
const timedOutDevice = device;
|
|
1323
|
-
let mtuTeardownSettled = false;
|
|
1324
|
-
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1325
|
-
await this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () =>
|
|
1326
|
-
timedOutDevice.cancelConnection()
|
|
1327
|
-
);
|
|
1328
|
-
mtuTeardownSettled = true;
|
|
1329
|
-
});
|
|
1330
|
-
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1331
|
-
if (!mtuTeardownSettled || this.blePlxManager !== blePlxManager) {
|
|
1332
|
-
// The teardown budget expired and the manager that owned timedOutDevice was reset.
|
|
1333
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'BLE MTU cleanup timed out');
|
|
1334
|
-
}
|
|
1335
|
-
try {
|
|
1336
|
-
device = await this.connectWithTimeout(uuid, () =>
|
|
1337
|
-
timedOutDevice.connect(fallbackConnectOptions)
|
|
1338
|
-
);
|
|
1339
|
-
} catch (error) {
|
|
1340
|
-
if (shouldRethrowBleSetupError(error)) throw error;
|
|
1341
|
-
if (
|
|
1342
|
-
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.DeviceAlreadyConnected
|
|
1343
|
-
) {
|
|
1344
|
-
// GATT resolution and the protocol probe below still validate the retained link.
|
|
1345
|
-
device = timedOutDevice;
|
|
1346
|
-
} else {
|
|
1347
|
-
remapError(error);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1191
|
+
device = await resolveNegotiatedMtu(device);
|
|
1352
1192
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1353
1193
|
const acquiredDevice = device;
|
|
1354
1194
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1355
|
-
|
|
1195
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
1356
1196
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1357
1197
|
|
|
1358
1198
|
const protocolHint = expectedProtocol
|
|
@@ -1463,9 +1303,6 @@ export default class ReactNativeBleTransport {
|
|
|
1463
1303
|
this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
|
|
1464
1304
|
return;
|
|
1465
1305
|
}
|
|
1466
|
-
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1467
|
-
this.androidGattCacheRefreshes.add(uuid);
|
|
1468
|
-
}
|
|
1469
1306
|
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1470
1307
|
let errorCode:
|
|
1471
1308
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -1475,7 +1312,10 @@ export default class ReactNativeBleTransport {
|
|
|
1475
1312
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1476
1313
|
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
1477
1314
|
} else if (
|
|
1478
|
-
|
|
1315
|
+
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
1316
|
+
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
1317
|
+
error.reason?.includes('The handle is invalid') ||
|
|
1318
|
+
error.reason?.includes('Writing is not permitted') ||
|
|
1479
1319
|
error.reason?.includes('notify change failed for device')
|
|
1480
1320
|
) {
|
|
1481
1321
|
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
@@ -1492,7 +1332,10 @@ export default class ReactNativeBleTransport {
|
|
|
1492
1332
|
ERROR = HardwareErrorCode.BleTimeoutError;
|
|
1493
1333
|
}
|
|
1494
1334
|
if (
|
|
1495
|
-
|
|
1335
|
+
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
1336
|
+
error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
1337
|
+
error.reason?.includes('The handle is invalid') ||
|
|
1338
|
+
error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
|
|
1496
1339
|
error.reason?.includes('notify change failed for device')
|
|
1497
1340
|
) {
|
|
1498
1341
|
const notifyError = ERRORS.TypedError(
|
|
@@ -1922,14 +1765,13 @@ export default class ReactNativeBleTransport {
|
|
|
1922
1765
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1923
1766
|
return check.call(jsonData);
|
|
1924
1767
|
} catch (e) {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
(name === 'GetFeatures' || name === 'Initialize');
|
|
1928
|
-
if (isProbeTimeout) {
|
|
1929
|
-
Log?.debug(`[ReactNativeBleTransport] Protocol V1 ${name} probe call failed:`, e);
|
|
1768
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1769
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1930
1770
|
} else {
|
|
1931
1771
|
Log?.error('call error: ', e);
|
|
1932
1772
|
}
|
|
1773
|
+
const isProbeTimeout =
|
|
1774
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1933
1775
|
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1934
1776
|
// transport; its late timeout must not tear down the connection the current
|
|
1935
1777
|
// call is actively using.
|
|
@@ -2162,98 +2004,6 @@ export default class ReactNativeBleTransport {
|
|
|
2162
2004
|
}
|
|
2163
2005
|
}
|
|
2164
2006
|
|
|
2165
|
-
/**
|
|
2166
|
-
* Android: request the MTU with nothing queued ahead and never close the client while it is
|
|
2167
|
-
* outstanding; a link that times out or stays at MTU 23 is dropped.
|
|
2168
|
-
*/
|
|
2169
|
-
private async negotiateAndroidMtu(
|
|
2170
|
-
uuid: string,
|
|
2171
|
-
manager: BlePlxManager,
|
|
2172
|
-
device: Device
|
|
2173
|
-
): Promise<Device> {
|
|
2174
|
-
if (!shouldRefreshNegotiatedMtu(device.mtu)) return device;
|
|
2175
|
-
|
|
2176
|
-
const startedAt = Date.now();
|
|
2177
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2178
|
-
let timedOut = false;
|
|
2179
|
-
let negotiated = device;
|
|
2180
|
-
let failure: string | undefined;
|
|
2181
|
-
try {
|
|
2182
|
-
negotiated = await Promise.race([
|
|
2183
|
-
device.requestMTU(ANDROID_REQUEST_MTU, `${device.id}:mtu:connected:0:${startedAt}`),
|
|
2184
|
-
new Promise<never>((_, reject) => {
|
|
2185
|
-
timer = setTimeout(() => {
|
|
2186
|
-
timedOut = true;
|
|
2187
|
-
reject(
|
|
2188
|
-
new Error(`BLE MTU exchange timeout after ${ANDROID_MTU_EXCHANGE_TIMEOUT_MS}ms`)
|
|
2189
|
-
);
|
|
2190
|
-
}, ANDROID_MTU_EXCHANGE_TIMEOUT_MS);
|
|
2191
|
-
}),
|
|
2192
|
-
]);
|
|
2193
|
-
} catch (error) {
|
|
2194
|
-
failure = error instanceof Error ? error.message : String(error);
|
|
2195
|
-
} finally {
|
|
2196
|
-
if (timer) clearTimeout(timer);
|
|
2197
|
-
}
|
|
2198
|
-
Log?.debug(`[ReactNativeBleTransport] BLE MTU exchange ${failure ? 'failed' : 'completed'}`, {
|
|
2199
|
-
connectIdSuffix: uuid.slice(-8),
|
|
2200
|
-
elapsedMs: Date.now() - startedAt,
|
|
2201
|
-
timedOut,
|
|
2202
|
-
actual: negotiated.mtu,
|
|
2203
|
-
error: failure,
|
|
2204
|
-
});
|
|
2205
|
-
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2206
|
-
if (!timedOut && !isKnownDefaultMtu(negotiated.mtu)) return negotiated;
|
|
2207
|
-
|
|
2208
|
-
// Counted like a setup timeout so a link that keeps failing reaches the wedged-link guard.
|
|
2209
|
-
const resetManager = this.abandonStalledConnection(
|
|
2210
|
-
uuid,
|
|
2211
|
-
timedOut ? 'mtu-backstop' : 'mtu-default'
|
|
2212
|
-
);
|
|
2213
|
-
await this.dropAndroidLink(
|
|
2214
|
-
uuid,
|
|
2215
|
-
manager,
|
|
2216
|
-
negotiated,
|
|
2217
|
-
timedOut ? 'mtu exchange timeout' : 'default mtu'
|
|
2218
|
-
);
|
|
2219
|
-
if (resetManager) throw this.createWedgedBleSetupError();
|
|
2220
|
-
throw ERRORS.TypedError(
|
|
2221
|
-
HardwareErrorCode.BleConnectedError,
|
|
2222
|
-
timedOut
|
|
2223
|
-
? 'BLE MTU exchange did not complete, reconnecting on a fresh link'
|
|
2224
|
-
: `BLE link stayed at the default MTU ${negotiated.mtu}, reconnecting on a fresh link`
|
|
2225
|
-
);
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
/** Close the client and wait out the link idle timer so the next connect gets a fresh link. */
|
|
2229
|
-
private async dropAndroidLink(
|
|
2230
|
-
uuid: string,
|
|
2231
|
-
manager: BlePlxManager,
|
|
2232
|
-
device: Device,
|
|
2233
|
-
reason: string
|
|
2234
|
-
) {
|
|
2235
|
-
await this.runNativeTeardown(uuid, manager, async () => {
|
|
2236
|
-
await Promise.all([
|
|
2237
|
-
this.runBestEffortNativeOperation(`${reason}: cancel manager connection`, () =>
|
|
2238
|
-
manager.cancelDeviceConnection(uuid)
|
|
2239
|
-
),
|
|
2240
|
-
this.runBestEffortNativeOperation(`${reason}: cancel device connection`, () =>
|
|
2241
|
-
device.cancelConnection()
|
|
2242
|
-
),
|
|
2243
|
-
]);
|
|
2244
|
-
});
|
|
2245
|
-
|
|
2246
|
-
const startedAt = Date.now();
|
|
2247
|
-
while (!this.stopped && Date.now() - startedAt < ANDROID_LINK_DROP_QUIET_MS) {
|
|
2248
|
-
await delay(ANDROID_LINK_DROP_POLL_MS);
|
|
2249
|
-
}
|
|
2250
|
-
Log?.debug('[ReactNativeBleTransport] Android BLE link drop', {
|
|
2251
|
-
connectIdSuffix: uuid.slice(-8),
|
|
2252
|
-
reason,
|
|
2253
|
-
stopped: this.stopped,
|
|
2254
|
-
});
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
2007
|
/** Run a native connect under the JS backstop budget. */
|
|
2258
2008
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
2259
2009
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
@@ -2343,9 +2093,6 @@ export default class ReactNativeBleTransport {
|
|
|
2343
2093
|
throw this.createWedgedBleSetupError();
|
|
2344
2094
|
}
|
|
2345
2095
|
}
|
|
2346
|
-
if (Platform.OS === 'android' && isMissingGattShapeError(error)) {
|
|
2347
|
-
this.androidGattCacheRefreshes.add(uuid);
|
|
2348
|
-
}
|
|
2349
2096
|
throw error;
|
|
2350
2097
|
} finally {
|
|
2351
2098
|
if (timer) clearTimeout(timer);
|
|
@@ -2366,13 +2113,7 @@ export default class ReactNativeBleTransport {
|
|
|
2366
2113
|
*/
|
|
2367
2114
|
private abandonStalledConnection(
|
|
2368
2115
|
uuid: string,
|
|
2369
|
-
stage:
|
|
2370
|
-
| 'connect-backstop'
|
|
2371
|
-
| 'connect-native'
|
|
2372
|
-
| 'gatt-backstop'
|
|
2373
|
-
| 'gatt-native'
|
|
2374
|
-
| 'mtu-backstop'
|
|
2375
|
-
| 'mtu-default'
|
|
2116
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
2376
2117
|
): boolean {
|
|
2377
2118
|
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
2378
2119
|
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
@@ -2547,7 +2288,6 @@ export default class ReactNativeBleTransport {
|
|
|
2547
2288
|
// Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
|
|
2548
2289
|
// firmware reconnect after the native BLE manager is recreated.
|
|
2549
2290
|
this.protocolReprobeFailures.clear();
|
|
2550
|
-
this.silentDetections.clear();
|
|
2551
2291
|
this.writeTimeoutCounts.clear();
|
|
2552
2292
|
this.connectionSetupTimeoutCounts.clear();
|
|
2553
2293
|
this.monitorTokens.clear();
|
|
@@ -2659,8 +2399,6 @@ export default class ReactNativeBleTransport {
|
|
|
2659
2399
|
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2660
2400
|
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
2661
2401
|
|
|
2662
|
-
await this.wakeSilentProtocolV1Device(uuid, probeOrder);
|
|
2663
|
-
|
|
2664
2402
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
2665
2403
|
const protocol = probeOrder[i];
|
|
2666
2404
|
if (i > 0) {
|
|
@@ -2682,7 +2420,6 @@ export default class ReactNativeBleTransport {
|
|
|
2682
2420
|
this.confirmedProtocolV2.add(uuid);
|
|
2683
2421
|
}
|
|
2684
2422
|
this.protocolReprobeFailures.delete(uuid);
|
|
2685
|
-
this.silentDetections.delete(uuid);
|
|
2686
2423
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2687
2424
|
deviceId: uuid,
|
|
2688
2425
|
protocol,
|
|
@@ -2692,9 +2429,6 @@ export default class ReactNativeBleTransport {
|
|
|
2692
2429
|
}
|
|
2693
2430
|
}
|
|
2694
2431
|
|
|
2695
|
-
// Arms the wake for the next detection.
|
|
2696
|
-
if (!this.silentDetections.has(uuid)) this.silentDetections.set(uuid, 'silent');
|
|
2697
|
-
|
|
2698
2432
|
if (trustSessionProtocol) {
|
|
2699
2433
|
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2700
2434
|
// shortcut so a device that genuinely switched protocols is found again.
|
|
@@ -2708,34 +2442,6 @@ export default class ReactNativeBleTransport {
|
|
|
2708
2442
|
throw this.createProtocolDetectionError();
|
|
2709
2443
|
}
|
|
2710
2444
|
|
|
2711
|
-
/**
|
|
2712
|
-
* A sleeping Classic drops GetFeatures/Ping and only leaves its screensaver on Initialize, which
|
|
2713
|
-
* resets the wallet session, so it is sent once after a fully silent detection. The firmware does
|
|
2714
|
-
* not reliably answer it, so its timeout must not drop the link. It only runs when V1 is probed
|
|
2715
|
-
* first, so a late reply lands on the V1 probe rather than on a V2 one.
|
|
2716
|
-
*/
|
|
2717
|
-
private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
|
|
2718
|
-
if (
|
|
2719
|
-
Platform.OS !== 'android' ||
|
|
2720
|
-
probeOrder[0] !== 'V1' ||
|
|
2721
|
-
this.silentDetections.get(uuid) !== 'silent'
|
|
2722
|
-
) {
|
|
2723
|
-
return;
|
|
2724
|
-
}
|
|
2725
|
-
this.silentDetections.set(uuid, 'woken');
|
|
2726
|
-
Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
|
|
2727
|
-
connectIdSuffix: uuid.slice(-8),
|
|
2728
|
-
});
|
|
2729
|
-
try {
|
|
2730
|
-
this.probingProtocols.set(uuid, 'V1');
|
|
2731
|
-
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2732
|
-
} catch (error) {
|
|
2733
|
-
if (shouldRethrowProtocolProbeError(error)) throw error;
|
|
2734
|
-
} finally {
|
|
2735
|
-
this.clearProbeProtocol(uuid, 'V1');
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
2445
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
2740
2446
|
const transport = transportCache[uuid];
|
|
2741
2447
|
await this.protocolV2Links.invalidateLink(
|
|
@@ -3115,12 +2821,7 @@ export default class ReactNativeBleTransport {
|
|
|
3115
2821
|
const transport = this.getCachedTransport(uuid);
|
|
3116
2822
|
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
3117
2823
|
|
|
3118
|
-
const
|
|
3119
|
-
transport.device,
|
|
3120
|
-
'highThroughput',
|
|
3121
|
-
1,
|
|
3122
|
-
transactionId => this.blePlxManager?.cancelTransaction(transactionId)
|
|
3123
|
-
);
|
|
2824
|
+
const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
|
|
3124
2825
|
transport.device = refreshedDevice;
|
|
3125
2826
|
transport.mtuSize =
|
|
3126
2827
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|