@onekeyfe/hd-transport-react-native 1.2.2-alpha.114 → 1.2.2-alpha.116
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 +74 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +274 -32
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +104 -0
- package/src/__tests__/protocolV2Link.test.ts +494 -37
- package/src/index.ts +468 -43
package/src/index.ts
CHANGED
|
@@ -185,7 +185,10 @@ const shouldRethrowProtocolProbeError = (error: unknown): boolean => {
|
|
|
185
185
|
code === HardwareErrorCode.BleDeviceDisconnected ||
|
|
186
186
|
code === HardwareErrorCode.BleCharacteristicNotifyError ||
|
|
187
187
|
code === HardwareErrorCode.BleCharacteristicNotifyChangeFailure ||
|
|
188
|
-
code === HardwareErrorCode.BleWriteCharacteristicError
|
|
188
|
+
code === HardwareErrorCode.BleWriteCharacteristicError ||
|
|
189
|
+
// A link stuck at the default MTU cannot answer Protocol V2 at all; reporting it as a
|
|
190
|
+
// probe miss would send detection on to report a protocol mismatch instead.
|
|
191
|
+
isProtocolV2DefaultMtuError(error)
|
|
189
192
|
);
|
|
190
193
|
};
|
|
191
194
|
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
@@ -261,6 +264,104 @@ const fallbackConnectOptions: Record<string, unknown> = {
|
|
|
261
264
|
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
262
265
|
};
|
|
263
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Android connects with the bare native budget: no MTU request and no GATT cache refresh
|
|
269
|
+
* inside that timer. ble-plx runs establishConnection -> refreshGatt -> requestMtu under one
|
|
270
|
+
* timeout, and refreshGatt makes the Android stack rediscover every service at the default
|
|
271
|
+
* 23-byte MTU before the MTU request can leave the ATT queue. A Pro 2 needs 2-5s for that
|
|
272
|
+
* rediscovery, so the budget expired with the request still unsent. Closing the client then
|
|
273
|
+
* left the exchange marked in progress for the whole LE link, and the stack parked every
|
|
274
|
+
* later MTU request on it ("Put conn_id on wait list"; btsnoop showed no Exchange MTU
|
|
275
|
+
* Request on the air). The MTU is negotiated as a separate first step instead.
|
|
276
|
+
*/
|
|
277
|
+
const androidConnectOptions: Record<string, unknown> = {
|
|
278
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Only used when the cached GATT table is known or likely to be stale; the rediscovery it
|
|
283
|
+
* starts is then allowed to finish before the MTU exchange instead of racing it.
|
|
284
|
+
*/
|
|
285
|
+
const androidRefreshGattConnectOptions: Record<string, unknown> = {
|
|
286
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
287
|
+
refreshGatt: 'OnConnected',
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Bound for the Android MTU exchange. A healthy exchange with a Pro 2 completes in ~50ms.
|
|
292
|
+
* When the phone has no cached GATT table (first connect after bonding, or a connect after
|
|
293
|
+
* an aborted discovery) the stack runs its own discovery first and only executes the MTU
|
|
294
|
+
* request after it; single passes on a Pro 2 at MTU 23 measured 1.7-4.8s and a refresh
|
|
295
|
+
* plus restart 7.7s, so the bound has to sit well above those. An exchange the stack holds
|
|
296
|
+
* as already in progress never completes, so a longer bound costs nothing on that link.
|
|
297
|
+
*/
|
|
298
|
+
export const ANDROID_MTU_EXCHANGE_TIMEOUT_MS = 12_000;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Android keeps an LE link up for its GATT link idle timer (4s) after the last client
|
|
302
|
+
* closes, and per-link ATT state such as a pending MTU exchange lives as long as the link.
|
|
303
|
+
* A reconnect inside that window attaches to the same link. The only connectivity signal
|
|
304
|
+
* reachable from here, BluetoothManager.getConnectedDevices(GATT), is built from the
|
|
305
|
+
* per-app GATT client registry rather than the LE link: it empties the moment this client
|
|
306
|
+
* closes on phones with no resident GATT server, and stays populated for the life of the
|
|
307
|
+
* link on phones that have one. So an unusable link is held for at least the quiet period
|
|
308
|
+
* (past the idle timer), and additionally until that registry no longer lists the device,
|
|
309
|
+
* within the overall bound.
|
|
310
|
+
*/
|
|
311
|
+
export const ANDROID_LINK_DROP_QUIET_MS = 5000;
|
|
312
|
+
export const ANDROID_LINK_DROP_TIMEOUT_MS = 8000;
|
|
313
|
+
const ANDROID_LINK_DROP_POLL_MS = 250;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Nothing works at the default 23-byte ATT MTU on Android. Protocol V1 writes 192-byte
|
|
317
|
+
* packets whatever the MTU, and a Pro 2 answers Protocol V2 with a single ATT_MTU-3
|
|
318
|
+
* notification and never sends the rest of the frame, so the 29-byte reply to a probe Ping
|
|
319
|
+
* arrives as 20 bytes declaring 29 and the call hangs. Only a known default MTU counts; an
|
|
320
|
+
* unknown value keeps the existing conservative-packet behaviour.
|
|
321
|
+
*/
|
|
322
|
+
const isKnownDefaultMtu = (mtu: unknown): boolean =>
|
|
323
|
+
typeof mtu === 'number' && Number.isFinite(mtu) && mtu <= 23;
|
|
324
|
+
const DEFAULT_MTU_LINK_MESSAGE = 'BLE link stayed at the default MTU';
|
|
325
|
+
const createDefaultMtuLinkError = (mtu: unknown) =>
|
|
326
|
+
ERRORS.TypedError(
|
|
327
|
+
HardwareErrorCode.BleConnectedError,
|
|
328
|
+
`${DEFAULT_MTU_LINK_MESSAGE} ${String(mtu)}, reconnecting on a fresh link`
|
|
329
|
+
);
|
|
330
|
+
const PROTOCOL_V2_DEFAULT_MTU_MESSAGE = 'Protocol V2 needs a negotiated BLE MTU';
|
|
331
|
+
const isProtocolV2DefaultMtuError = (error: unknown): boolean =>
|
|
332
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
333
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
334
|
+
(error as { message: string }).message.startsWith(PROTOCOL_V2_DEFAULT_MTU_MESSAGE);
|
|
335
|
+
const createProtocolV2DefaultMtuError = (mtu: unknown) =>
|
|
336
|
+
ERRORS.TypedError(
|
|
337
|
+
HardwareErrorCode.BleConnectedError,
|
|
338
|
+
`${PROTOCOL_V2_DEFAULT_MTU_MESSAGE}, current MTU ${String(mtu)}`
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Symptoms of a cached GATT table that no longer matches the device. The UUIDs still
|
|
343
|
+
* resolve from the cache in the firmware-upgrade case, so the stale handles surface at
|
|
344
|
+
* discovery (missing or mis-typed characteristic) or when notifications are enabled.
|
|
345
|
+
*/
|
|
346
|
+
const isMissingGattShapeError = (error: unknown): boolean => {
|
|
347
|
+
const code = (error as { errorCode?: unknown })?.errorCode;
|
|
348
|
+
const message = (error as { message?: unknown })?.message;
|
|
349
|
+
return (
|
|
350
|
+
code === HardwareErrorCode.BleServiceNotFound ||
|
|
351
|
+
code === HardwareErrorCode.BleCharacteristicNotFound ||
|
|
352
|
+
(typeof message === 'string' &&
|
|
353
|
+
(message.includes('BLECharacteristicNotFound') ||
|
|
354
|
+
message.includes('BLECharacteristicNotWritable') ||
|
|
355
|
+
message.includes('BLECharacteristicNotNotifiable')))
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
const isStaleGattTableNotifyReason = (reason: string | null | undefined): boolean =>
|
|
359
|
+
!!reason &&
|
|
360
|
+
(reason.includes('Cannot write client characteristic config descriptor') ||
|
|
361
|
+
reason.includes('Cannot find client characteristic config descriptor') ||
|
|
362
|
+
reason.includes('The handle is invalid') ||
|
|
363
|
+
reason.includes('Writing is not permitted'));
|
|
364
|
+
|
|
264
365
|
/**
|
|
265
366
|
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
266
367
|
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
@@ -300,16 +401,22 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
|
300
401
|
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
301
402
|
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
302
403
|
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
303
|
-
|
|
404
|
+
/**
|
|
405
|
+
* ble-plx reports its own expired budget as OperationCancelled, not OperationTimedOut:
|
|
406
|
+
* safeConnectToDevice disposes the operation chain and its doFinally reports a
|
|
407
|
+
* cancellation. Either code means the native layer abandoned work that still owned
|
|
408
|
+
* connection/GATT state, so both have to go through the stalled-connection cleanup.
|
|
409
|
+
* Only used where nothing else recovers the link — the connect path does its own
|
|
410
|
+
* ordered teardown before retrying, and routing it here too would both duplicate that
|
|
411
|
+
* and spend the manager-reset budget on a connect that is about to be retried.
|
|
412
|
+
*/
|
|
413
|
+
const isAbandonedNativeOperationError = (error: unknown): boolean => {
|
|
304
414
|
const errorCode = (error as { errorCode?: unknown })?.errorCode;
|
|
305
415
|
return (
|
|
306
|
-
errorCode === BleErrorCode.
|
|
307
|
-
errorCode === BleErrorCode.OperationCancelled
|
|
416
|
+
errorCode === BleErrorCode.OperationTimedOut || errorCode === BleErrorCode.OperationCancelled
|
|
308
417
|
);
|
|
309
418
|
};
|
|
310
419
|
|
|
311
|
-
type NegotiatedMtuResult = { device: Device; timedOut: boolean };
|
|
312
|
-
|
|
313
420
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
314
421
|
|
|
315
422
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -321,13 +428,26 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
321
428
|
return infos;
|
|
322
429
|
};
|
|
323
430
|
|
|
431
|
+
type NegotiatedMtuResult = {
|
|
432
|
+
device: Device;
|
|
433
|
+
/**
|
|
434
|
+
* True when the bounded wait gave up while the native MTU exchange was still
|
|
435
|
+
* outstanding. `cancelTransaction` only disposes the JS-side subscription:
|
|
436
|
+
* RxAndroidBle releases a connection's serial operation queue from onComplete /
|
|
437
|
+
* onError alone, so an abandoned exchange keeps that queue until its own native
|
|
438
|
+
* timeout. Any GATT work issued before then — service discovery above all —
|
|
439
|
+
* queues behind it and cannot settle inside its own budget.
|
|
440
|
+
*/
|
|
441
|
+
abandoned: boolean;
|
|
442
|
+
};
|
|
443
|
+
|
|
324
444
|
const requestNegotiatedMtu = async (
|
|
325
445
|
device: Device,
|
|
326
446
|
stage: 'connected' | 'highThroughput',
|
|
327
447
|
attempt: number,
|
|
328
448
|
cancelTransaction?: (transactionId: string) => Promise<void> | void
|
|
329
449
|
): Promise<NegotiatedMtuResult> => {
|
|
330
|
-
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device,
|
|
450
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, abandoned: false };
|
|
331
451
|
|
|
332
452
|
const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
|
|
333
453
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -350,7 +470,7 @@ const requestNegotiatedMtu = async (
|
|
|
350
470
|
}, BLE_MTU_REQUEST_TIMEOUT_MS);
|
|
351
471
|
}),
|
|
352
472
|
]);
|
|
353
|
-
return { device: mtuDevice,
|
|
473
|
+
return { device: mtuDevice, abandoned: false };
|
|
354
474
|
} catch (error) {
|
|
355
475
|
if (timedOut && cancelTransaction) {
|
|
356
476
|
try {
|
|
@@ -376,10 +496,9 @@ const requestNegotiatedMtu = async (
|
|
|
376
496
|
stage,
|
|
377
497
|
attempt,
|
|
378
498
|
actual: device.mtu,
|
|
379
|
-
timedOut,
|
|
380
499
|
error: error instanceof Error ? error.message : String(error),
|
|
381
500
|
});
|
|
382
|
-
return { device, timedOut };
|
|
501
|
+
return { device, abandoned: timedOut };
|
|
383
502
|
} finally {
|
|
384
503
|
if (timeoutId) clearTimeout(timeoutId);
|
|
385
504
|
}
|
|
@@ -391,7 +510,7 @@ const resolveNegotiatedMtu = (
|
|
|
391
510
|
): Promise<NegotiatedMtuResult> =>
|
|
392
511
|
shouldRefreshNegotiatedMtu(device.mtu)
|
|
393
512
|
? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
|
|
394
|
-
: Promise.resolve({ device,
|
|
513
|
+
: Promise.resolve({ device, abandoned: false });
|
|
395
514
|
|
|
396
515
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
397
516
|
|
|
@@ -475,6 +594,31 @@ export default class ReactNativeBleTransport {
|
|
|
475
594
|
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
476
595
|
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
477
596
|
|
|
597
|
+
/** Endpoints whose last detection got no answer on any protocol. */
|
|
598
|
+
private silentDetections = new Set<string>();
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Android endpoints whose cached GATT table is suspect and must be rediscovered on the
|
|
602
|
+
* next connect. The cache is no longer refreshed on every connect, so a missing OneKey
|
|
603
|
+
* service or characteristic marks the endpoint instead.
|
|
604
|
+
*/
|
|
605
|
+
private androidGattCacheRefreshes = new Set<string>();
|
|
606
|
+
|
|
607
|
+
/** Instance copies of the Android timing bounds so tests can shorten real-time waits. */
|
|
608
|
+
androidMtuExchangeTimeoutMs = ANDROID_MTU_EXCHANGE_TIMEOUT_MS;
|
|
609
|
+
|
|
610
|
+
androidLinkDropQuietMs = ANDROID_LINK_DROP_QUIET_MS;
|
|
611
|
+
|
|
612
|
+
androidLinkDropTimeoutMs = ANDROID_LINK_DROP_TIMEOUT_MS;
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Endpoints already sent a Protocol V1 wake. The wake starts a fresh wallet session
|
|
616
|
+
* on the device, and a failed detection tears the link down and reconnects on every
|
|
617
|
+
* poll, so neither set is cleared on teardown: one wake is spent per endpoint until a
|
|
618
|
+
* detection succeeds, which re-arms it for the next time the device sleeps.
|
|
619
|
+
*/
|
|
620
|
+
private protocolWakeAttempts = new Set<string>();
|
|
621
|
+
|
|
478
622
|
/**
|
|
479
623
|
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
480
624
|
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
@@ -745,9 +889,13 @@ export default class ReactNativeBleTransport {
|
|
|
745
889
|
|
|
746
890
|
let { device } = transport;
|
|
747
891
|
const isConnected = await device.isConnected().catch(() => false);
|
|
892
|
+
// A firmware upload reconnect keeps the GATT refresh; on Android it uses the same
|
|
893
|
+
// order as acquire: bare connect, discovery, then the MTU exchange on its own.
|
|
894
|
+
const reconnectOptions =
|
|
895
|
+
Platform.OS === 'android' ? androidRefreshGattConnectOptions : connectOptions;
|
|
748
896
|
if (!isConnected) {
|
|
749
897
|
try {
|
|
750
|
-
device = await this.connectWithTimeout(uuid, () => device.connect(
|
|
898
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(reconnectOptions));
|
|
751
899
|
} catch (e) {
|
|
752
900
|
if (
|
|
753
901
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
@@ -762,8 +910,17 @@ export default class ReactNativeBleTransport {
|
|
|
762
910
|
|
|
763
911
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
764
912
|
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
913
|
+
if (Platform.OS === 'android') {
|
|
914
|
+
const manager = await this.getPlxManager();
|
|
915
|
+
device = await this.negotiateAndroidMtu(uuid, manager, device);
|
|
916
|
+
if (isKnownDefaultMtu(device.mtu)) {
|
|
917
|
+
await this.dropAndroidLink(uuid, manager, device, 'firmware reconnect default mtu');
|
|
918
|
+
throw createDefaultMtuLinkError(device.mtu);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
765
921
|
|
|
766
922
|
transport.device = device;
|
|
923
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
767
924
|
transport.writeCharacteristic = writeCharacteristic;
|
|
768
925
|
transport.notifyCharacteristic = notifyCharacteristic;
|
|
769
926
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -949,7 +1106,7 @@ export default class ReactNativeBleTransport {
|
|
|
949
1106
|
characteristics?: ResolvedBleCharacteristics
|
|
950
1107
|
) {
|
|
951
1108
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
952
|
-
characteristics ?? (await this.
|
|
1109
|
+
characteristics ?? (await this.resolveCharacteristicsForAcquire(uuid, device));
|
|
953
1110
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
954
1111
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
955
1112
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
@@ -1052,8 +1209,16 @@ export default class ReactNativeBleTransport {
|
|
|
1052
1209
|
const isCachedDeviceConnected = await cachedTransport.device
|
|
1053
1210
|
.isConnected()
|
|
1054
1211
|
.catch(() => false);
|
|
1212
|
+
// A cached Android link at the default MTU, or one whose GATT table is suspect,
|
|
1213
|
+
// has to go through the full acquire again so the MTU exchange and the refresh
|
|
1214
|
+
// connect actually run instead of every retry reusing the same unusable link.
|
|
1215
|
+
const isCachedAndroidLinkUsable =
|
|
1216
|
+
Platform.OS !== 'android' ||
|
|
1217
|
+
(!isKnownDefaultMtu(cachedTransport.mtuSize) &&
|
|
1218
|
+
!this.androidGattCacheRefreshes.has(uuid));
|
|
1055
1219
|
if (
|
|
1056
1220
|
isCachedDeviceConnected &&
|
|
1221
|
+
isCachedAndroidLinkUsable &&
|
|
1057
1222
|
cachedProtocol &&
|
|
1058
1223
|
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
1059
1224
|
) {
|
|
@@ -1072,6 +1237,21 @@ export default class ReactNativeBleTransport {
|
|
|
1072
1237
|
}
|
|
1073
1238
|
|
|
1074
1239
|
let device: Device | null = null;
|
|
1240
|
+
const isAndroid = Platform.OS === 'android';
|
|
1241
|
+
// A firmware-install reconnect keeps the per-connect GATT refresh it always had: the
|
|
1242
|
+
// install loader may expose a different table than the firmware that was cached.
|
|
1243
|
+
const refreshAndroidGattCache =
|
|
1244
|
+
isAndroid && (!!skipProtocolProbe || this.androidGattCacheRefreshes.has(uuid));
|
|
1245
|
+
let nativeConnectOptions = connectOptions;
|
|
1246
|
+
if (isAndroid) {
|
|
1247
|
+
nativeConnectOptions = refreshAndroidGattCache
|
|
1248
|
+
? androidRefreshGattConnectOptions
|
|
1249
|
+
: androidConnectOptions;
|
|
1250
|
+
}
|
|
1251
|
+
// Only a connect that actually carried refreshGatt consumes a pending refresh; the
|
|
1252
|
+
// fallback connects below drop that option, so the marker then survives for the next
|
|
1253
|
+
// attempt instead of being cleared by a connect that never reached the cache.
|
|
1254
|
+
let androidRefreshConnectRan = false;
|
|
1075
1255
|
|
|
1076
1256
|
if (forceCleanRunPromise && this.runPromise) {
|
|
1077
1257
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
@@ -1082,7 +1262,6 @@ export default class ReactNativeBleTransport {
|
|
|
1082
1262
|
}
|
|
1083
1263
|
|
|
1084
1264
|
const blePlxManager = await this.getPlxManager();
|
|
1085
|
-
let skipPostConnectMtu = false;
|
|
1086
1265
|
try {
|
|
1087
1266
|
await subscribeBleOn(blePlxManager);
|
|
1088
1267
|
} catch (error) {
|
|
@@ -1127,16 +1306,25 @@ export default class ReactNativeBleTransport {
|
|
|
1127
1306
|
Log?.debug('try to connect to device: ', uuid);
|
|
1128
1307
|
try {
|
|
1129
1308
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1130
|
-
blePlxManager.connectToDevice(uuid,
|
|
1309
|
+
blePlxManager.connectToDevice(uuid, nativeConnectOptions)
|
|
1131
1310
|
);
|
|
1311
|
+
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1132
1312
|
} catch (e) {
|
|
1133
1313
|
Log?.debug('try to connect to device has error: ', e);
|
|
1134
1314
|
if (shouldRethrowBleSetupError(e)) {
|
|
1135
1315
|
throw e;
|
|
1136
1316
|
}
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1317
|
+
if (
|
|
1318
|
+
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
1319
|
+
e.errorCode === BleErrorCode.OperationCancelled
|
|
1320
|
+
) {
|
|
1139
1321
|
Log?.debug('first try to reconnect without params');
|
|
1322
|
+
// The disposed chain can still own native connection state, and the retry
|
|
1323
|
+
// would otherwise run on top of a half-open GATT client.
|
|
1324
|
+
await this.runBestEffortNativeOperation(
|
|
1325
|
+
'connect cancelled: cancel manager connection',
|
|
1326
|
+
() => blePlxManager.cancelDeviceConnection(uuid)
|
|
1327
|
+
);
|
|
1140
1328
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1141
1329
|
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
1142
1330
|
);
|
|
@@ -1153,22 +1341,38 @@ export default class ReactNativeBleTransport {
|
|
|
1153
1341
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'unable to connect to device');
|
|
1154
1342
|
}
|
|
1155
1343
|
|
|
1344
|
+
if (isAndroid && refreshAndroidGattCache && (await device.isConnected().catch(() => false))) {
|
|
1345
|
+
// refreshGatt only reaches the stack through a connect. A link that is still up would
|
|
1346
|
+
// skip the connect below and keep serving the stale table, so it is dropped first.
|
|
1347
|
+
await this.dropAndroidLink(uuid, blePlxManager, device, 'gatt cache refresh');
|
|
1348
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1156
1351
|
if (!(await device.isConnected())) {
|
|
1157
1352
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
1158
1353
|
const disconnectedDevice = device;
|
|
1159
1354
|
|
|
1160
1355
|
try {
|
|
1161
1356
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1162
|
-
disconnectedDevice.connect(
|
|
1357
|
+
disconnectedDevice.connect(nativeConnectOptions)
|
|
1163
1358
|
);
|
|
1359
|
+
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1164
1360
|
} catch (e) {
|
|
1165
1361
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
1166
1362
|
if (shouldRethrowBleSetupError(e)) {
|
|
1167
1363
|
throw e;
|
|
1168
1364
|
}
|
|
1169
|
-
if (
|
|
1170
|
-
|
|
1365
|
+
if (
|
|
1366
|
+
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
1367
|
+
e.errorCode === BleErrorCode.OperationCancelled
|
|
1368
|
+
) {
|
|
1171
1369
|
Log?.debug('second try to reconnect without params');
|
|
1370
|
+
// The disposed chain can still own native connection state, and the retry
|
|
1371
|
+
// would otherwise run on top of a half-open GATT client.
|
|
1372
|
+
await this.runBestEffortNativeOperation(
|
|
1373
|
+
'connect cancelled: cancel device connection',
|
|
1374
|
+
() => disconnectedDevice.cancelConnection()
|
|
1375
|
+
);
|
|
1172
1376
|
try {
|
|
1173
1377
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1174
1378
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
@@ -1209,33 +1413,48 @@ export default class ReactNativeBleTransport {
|
|
|
1209
1413
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1210
1414
|
}
|
|
1211
1415
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1416
|
+
let characteristics: ResolvedBleCharacteristics | undefined;
|
|
1417
|
+
if (isAndroid) {
|
|
1418
|
+
if (refreshAndroidGattCache) {
|
|
1419
|
+
// refreshGatt has already started a full rediscovery; let it finish before the MTU
|
|
1420
|
+
// exchange so the request is not queued behind it.
|
|
1421
|
+
characteristics = await this.resolveCharacteristicsForAcquire(uuid, device);
|
|
1422
|
+
if (androidRefreshConnectRan) this.androidGattCacheRefreshes.delete(uuid);
|
|
1423
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1424
|
+
}
|
|
1425
|
+
device = await this.negotiateAndroidMtu(uuid, blePlxManager, device);
|
|
1426
|
+
if (isKnownDefaultMtu(device.mtu)) {
|
|
1427
|
+
// Neither protocol works at the default MTU on Android, so the link is not worth
|
|
1428
|
+
// keeping for any device family. Counted like a setup timeout so a device that keeps
|
|
1429
|
+
// ending here reaches the wedged-link guard instead of the full retry ladder.
|
|
1430
|
+
const resetManager = this.abandonStalledConnection(uuid, 'mtu-default');
|
|
1431
|
+
await this.dropAndroidLink(uuid, blePlxManager, device, 'default mtu');
|
|
1432
|
+
if (resetManager) throw this.createWedgedBleSetupError();
|
|
1433
|
+
throw createDefaultMtuLinkError(device.mtu);
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
const negotiatedMtu = await resolveNegotiatedMtu(device, transactionId =>
|
|
1217
1437
|
blePlxManager.cancelTransaction(transactionId)
|
|
1218
1438
|
);
|
|
1219
|
-
device =
|
|
1220
|
-
if (
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
timedOutDevice.connect(fallbackConnectOptions)
|
|
1439
|
+
device = negotiatedMtu.device;
|
|
1440
|
+
if (negotiatedMtu.abandoned) {
|
|
1441
|
+
// Service discovery would queue behind the abandoned exchange and miss its own
|
|
1442
|
+
// budget. Drop the link so the next attempt starts on a queue nothing holds.
|
|
1443
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1444
|
+
await this.runBestEffortNativeOperation('mtu abandoned: cancel manager connection', () =>
|
|
1445
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1446
|
+
);
|
|
1447
|
+
});
|
|
1448
|
+
throw ERRORS.TypedError(
|
|
1449
|
+
HardwareErrorCode.BleConnectedError,
|
|
1450
|
+
'BLE MTU negotiation abandoned, reconnecting'
|
|
1232
1451
|
);
|
|
1233
1452
|
}
|
|
1234
1453
|
}
|
|
1235
1454
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1236
1455
|
const acquiredDevice = device;
|
|
1237
1456
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1238
|
-
await this.
|
|
1457
|
+
characteristics ?? (await this.resolveCharacteristicsForAcquire(uuid, acquiredDevice));
|
|
1239
1458
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1240
1459
|
|
|
1241
1460
|
const protocolHint = expectedProtocol
|
|
@@ -1363,6 +1582,9 @@ export default class ReactNativeBleTransport {
|
|
|
1363
1582
|
) {
|
|
1364
1583
|
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
1365
1584
|
}
|
|
1585
|
+
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1586
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
1587
|
+
}
|
|
1366
1588
|
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
1367
1589
|
return;
|
|
1368
1590
|
}
|
|
@@ -1384,6 +1606,9 @@ export default class ReactNativeBleTransport {
|
|
|
1384
1606
|
const notifyError = ERRORS.TypedError(
|
|
1385
1607
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
1386
1608
|
);
|
|
1609
|
+
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1610
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
1611
|
+
}
|
|
1387
1612
|
this.runPromise.reject(notifyError);
|
|
1388
1613
|
Log?.debug(
|
|
1389
1614
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
@@ -2047,6 +2272,142 @@ export default class ReactNativeBleTransport {
|
|
|
2047
2272
|
}
|
|
2048
2273
|
}
|
|
2049
2274
|
|
|
2275
|
+
/**
|
|
2276
|
+
* Negotiate the ATT MTU as the first request on an Android GATT client, with nothing queued
|
|
2277
|
+
* ahead of it, and never close the client while it is outstanding: an exchange abandoned
|
|
2278
|
+
* mid-flight stays marked in progress for the whole LE link. A link that already negotiated
|
|
2279
|
+
* an MTU answers at once with the recorded value.
|
|
2280
|
+
*/
|
|
2281
|
+
private async negotiateAndroidMtu(
|
|
2282
|
+
uuid: string,
|
|
2283
|
+
manager: BlePlxManager,
|
|
2284
|
+
device: Device
|
|
2285
|
+
): Promise<Device> {
|
|
2286
|
+
if (!shouldRefreshNegotiatedMtu(device.mtu)) return device;
|
|
2287
|
+
|
|
2288
|
+
const startedAt = Date.now();
|
|
2289
|
+
const transactionId = `${device.id}:mtu:connected:0:${startedAt}`;
|
|
2290
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2291
|
+
let timedOut = false;
|
|
2292
|
+
const request = device.requestMTU(ANDROID_REQUEST_MTU, transactionId);
|
|
2293
|
+
request.catch(() => undefined);
|
|
2294
|
+
try {
|
|
2295
|
+
const negotiated = await Promise.race([
|
|
2296
|
+
request,
|
|
2297
|
+
new Promise<never>((_, reject) => {
|
|
2298
|
+
timer = setTimeout(() => {
|
|
2299
|
+
timedOut = true;
|
|
2300
|
+
reject(
|
|
2301
|
+
new Error(`BLE MTU exchange timeout after ${this.androidMtuExchangeTimeoutMs}ms`)
|
|
2302
|
+
);
|
|
2303
|
+
}, this.androidMtuExchangeTimeoutMs);
|
|
2304
|
+
}),
|
|
2305
|
+
]);
|
|
2306
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU exchange completed', {
|
|
2307
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2308
|
+
elapsedMs: Date.now() - startedAt,
|
|
2309
|
+
actual: negotiated.mtu,
|
|
2310
|
+
});
|
|
2311
|
+
return negotiated;
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU exchange failed', {
|
|
2314
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2315
|
+
elapsedMs: Date.now() - startedAt,
|
|
2316
|
+
timedOut,
|
|
2317
|
+
actual: device.mtu,
|
|
2318
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2319
|
+
});
|
|
2320
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2321
|
+
// A request the stack rejected is not left pending; the caller's MTU check decides.
|
|
2322
|
+
if (!timedOut) return device;
|
|
2323
|
+
// The JS-side subscription is released for hygiene only: the native operation, and
|
|
2324
|
+
// the per-link state it may already hold, end with the link below.
|
|
2325
|
+
await Promise.resolve(manager.cancelTransaction(transactionId)).catch(() => undefined);
|
|
2326
|
+
const resetManager = this.abandonStalledConnection(uuid, 'mtu-backstop');
|
|
2327
|
+
await this.dropAndroidLink(uuid, manager, device, 'mtu exchange timeout');
|
|
2328
|
+
if (resetManager) throw this.createWedgedBleSetupError();
|
|
2329
|
+
throw ERRORS.TypedError(
|
|
2330
|
+
HardwareErrorCode.BleConnectedError,
|
|
2331
|
+
'BLE MTU exchange did not complete, reconnecting on a fresh link'
|
|
2332
|
+
);
|
|
2333
|
+
} finally {
|
|
2334
|
+
if (timer) clearTimeout(timer);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
/**
|
|
2339
|
+
* Abandon an Android LE link whose per-link GATT state is unusable. Closing the GATT
|
|
2340
|
+
* client alone keeps the link up for the stack's idle timer, and Core's retry arrives
|
|
2341
|
+
* inside that window, so this holds for at least ANDROID_LINK_DROP_QUIET_MS and until the
|
|
2342
|
+
* GATT registry no longer lists the device, capped at ANDROID_LINK_DROP_TIMEOUT_MS.
|
|
2343
|
+
*/
|
|
2344
|
+
private async dropAndroidLink(
|
|
2345
|
+
uuid: string,
|
|
2346
|
+
manager: BlePlxManager,
|
|
2347
|
+
device: Device,
|
|
2348
|
+
reason: string
|
|
2349
|
+
) {
|
|
2350
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
2351
|
+
await Promise.all([
|
|
2352
|
+
this.runBestEffortNativeOperation(`${reason}: cancel manager connection`, () =>
|
|
2353
|
+
manager.cancelDeviceConnection(uuid)
|
|
2354
|
+
),
|
|
2355
|
+
this.runBestEffortNativeOperation(`${reason}: cancel device connection`, () =>
|
|
2356
|
+
device.cancelConnection()
|
|
2357
|
+
),
|
|
2358
|
+
]);
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
const startedAt = Date.now();
|
|
2362
|
+
const target = uuid.toUpperCase();
|
|
2363
|
+
let registryClear = false;
|
|
2364
|
+
let quietPeriodElapsed = false;
|
|
2365
|
+
while (!this.stopped) {
|
|
2366
|
+
const elapsed = Date.now() - startedAt;
|
|
2367
|
+
quietPeriodElapsed = elapsed >= this.androidLinkDropQuietMs;
|
|
2368
|
+
if (!registryClear) {
|
|
2369
|
+
const connected: Array<{ id?: unknown }> | undefined = await getConnectedDeviceIds(
|
|
2370
|
+
[]
|
|
2371
|
+
).catch(() => undefined);
|
|
2372
|
+
registryClear =
|
|
2373
|
+
!!connected &&
|
|
2374
|
+
!connected.some(peripheral => String(peripheral?.id ?? '').toUpperCase() === target);
|
|
2375
|
+
}
|
|
2376
|
+
if ((registryClear && quietPeriodElapsed) || elapsed >= this.androidLinkDropTimeoutMs) {
|
|
2377
|
+
break;
|
|
2378
|
+
}
|
|
2379
|
+
await delay(ANDROID_LINK_DROP_POLL_MS);
|
|
2380
|
+
}
|
|
2381
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE link drop', {
|
|
2382
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2383
|
+
reason,
|
|
2384
|
+
registryClear,
|
|
2385
|
+
quietPeriodElapsed,
|
|
2386
|
+
stopped: this.stopped,
|
|
2387
|
+
elapsedMs: Date.now() - startedAt,
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
private async resolveCharacteristicsForAcquire(
|
|
2392
|
+
uuid: string,
|
|
2393
|
+
device: Device
|
|
2394
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
2395
|
+
try {
|
|
2396
|
+
return await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
if (Platform.OS === 'android' && isMissingGattShapeError(error)) {
|
|
2399
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
2400
|
+
// The refresh only reaches the stack through a connect, and a link that is still
|
|
2401
|
+
// up would let the next acquire skip that connect and serve the same stale table.
|
|
2402
|
+
const manager = this.blePlxManager;
|
|
2403
|
+
if (manager) {
|
|
2404
|
+
await this.dropAndroidLink(uuid, manager, device, 'stale gatt table');
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
throw error;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2050
2411
|
/** Run a native connect under the JS backstop budget. */
|
|
2051
2412
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
2052
2413
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
@@ -2127,7 +2488,7 @@ export default class ReactNativeBleTransport {
|
|
|
2127
2488
|
succeeded = true;
|
|
2128
2489
|
return result;
|
|
2129
2490
|
} catch (error) {
|
|
2130
|
-
if (timedOut ||
|
|
2491
|
+
if (timedOut || isAbandonedNativeOperationError(error)) {
|
|
2131
2492
|
const resetManager = this.abandonStalledConnection(
|
|
2132
2493
|
uuid,
|
|
2133
2494
|
timedOut ? 'gatt-backstop' : 'gatt-native'
|
|
@@ -2156,7 +2517,13 @@ export default class ReactNativeBleTransport {
|
|
|
2156
2517
|
*/
|
|
2157
2518
|
private abandonStalledConnection(
|
|
2158
2519
|
uuid: string,
|
|
2159
|
-
stage:
|
|
2520
|
+
stage:
|
|
2521
|
+
| 'connect-backstop'
|
|
2522
|
+
| 'connect-native'
|
|
2523
|
+
| 'gatt-backstop'
|
|
2524
|
+
| 'gatt-native'
|
|
2525
|
+
| 'mtu-backstop'
|
|
2526
|
+
| 'mtu-default'
|
|
2160
2527
|
): boolean {
|
|
2161
2528
|
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
2162
2529
|
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
@@ -2331,6 +2698,8 @@ export default class ReactNativeBleTransport {
|
|
|
2331
2698
|
// Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
|
|
2332
2699
|
// firmware reconnect after the native BLE manager is recreated.
|
|
2333
2700
|
this.protocolReprobeFailures.clear();
|
|
2701
|
+
this.silentDetections.clear();
|
|
2702
|
+
this.protocolWakeAttempts.clear();
|
|
2334
2703
|
this.writeTimeoutCounts.clear();
|
|
2335
2704
|
this.connectionSetupTimeoutCounts.clear();
|
|
2336
2705
|
this.monitorTokens.clear();
|
|
@@ -2442,6 +2811,8 @@ export default class ReactNativeBleTransport {
|
|
|
2442
2811
|
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2443
2812
|
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
2444
2813
|
|
|
2814
|
+
await this.wakeSilentProtocolV1Device(uuid, probeOrder);
|
|
2815
|
+
|
|
2445
2816
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
2446
2817
|
const protocol = probeOrder[i];
|
|
2447
2818
|
if (i > 0) {
|
|
@@ -2463,6 +2834,8 @@ export default class ReactNativeBleTransport {
|
|
|
2463
2834
|
this.confirmedProtocolV2.add(uuid);
|
|
2464
2835
|
}
|
|
2465
2836
|
this.protocolReprobeFailures.delete(uuid);
|
|
2837
|
+
this.silentDetections.delete(uuid);
|
|
2838
|
+
this.protocolWakeAttempts.delete(uuid);
|
|
2466
2839
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2467
2840
|
deviceId: uuid,
|
|
2468
2841
|
protocol,
|
|
@@ -2472,6 +2845,10 @@ export default class ReactNativeBleTransport {
|
|
|
2472
2845
|
}
|
|
2473
2846
|
}
|
|
2474
2847
|
|
|
2848
|
+
// Nothing answered on any probed protocol. Remember it so the next detection, which
|
|
2849
|
+
// the caller reaches on a freshly reconnected link, can spend a wake first.
|
|
2850
|
+
this.silentDetections.add(uuid);
|
|
2851
|
+
|
|
2475
2852
|
if (trustSessionProtocol) {
|
|
2476
2853
|
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2477
2854
|
// shortcut so a device that genuinely switched protocols is found again.
|
|
@@ -2485,6 +2862,47 @@ export default class ReactNativeBleTransport {
|
|
|
2485
2862
|
throw this.createProtocolDetectionError();
|
|
2486
2863
|
}
|
|
2487
2864
|
|
|
2865
|
+
/**
|
|
2866
|
+
* Sent once before probing when the previous detection on this endpoint got no answer
|
|
2867
|
+
* at all. On Classic-family firmware that silence is also what a sleeping device looks
|
|
2868
|
+
* like: its BLE co-processor keeps serving connect, GATT and MTU while the main MCU
|
|
2869
|
+
* sits in the screensaver loop, whose host-message filter accepts only Initialize and
|
|
2870
|
+
* the *Ack messages. GetFeatures and Ping are dropped there without a reply, so no
|
|
2871
|
+
* amount of probing can bring the device back.
|
|
2872
|
+
*
|
|
2873
|
+
* Initialize is the one message that breaks that loop, but it also starts a fresh
|
|
2874
|
+
* wallet session, which is why it is not the probe itself — a bare Initialize on every
|
|
2875
|
+
* acquire would drop a hidden-wallet session before Core can restore it. Requiring a
|
|
2876
|
+
* fully silent previous detection keeps it to devices with no session left to protect.
|
|
2877
|
+
*
|
|
2878
|
+
* The firmware consumes the wake to leave its loop and does not reliably answer it, so
|
|
2879
|
+
* the result is ignored: the probes that follow are what decide the protocol.
|
|
2880
|
+
*/
|
|
2881
|
+
private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
|
|
2882
|
+
if (!this._messages) return;
|
|
2883
|
+
if (!probeOrder.includes('V1')) return;
|
|
2884
|
+
if (!this.silentDetections.has(uuid)) return;
|
|
2885
|
+
if (this.protocolWakeAttempts.has(uuid)) return;
|
|
2886
|
+
if (!transportCache[uuid]) return;
|
|
2887
|
+
this.protocolWakeAttempts.add(uuid);
|
|
2888
|
+
|
|
2889
|
+
Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
|
|
2890
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2891
|
+
});
|
|
2892
|
+
try {
|
|
2893
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2894
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2897
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2898
|
+
throw error;
|
|
2899
|
+
}
|
|
2900
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize wake did not answer:', error);
|
|
2901
|
+
} finally {
|
|
2902
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2488
2906
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
2489
2907
|
const transport = transportCache[uuid];
|
|
2490
2908
|
await this.protocolV2Links.invalidateLink(
|
|
@@ -2787,6 +3205,13 @@ export default class ReactNativeBleTransport {
|
|
|
2787
3205
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
2788
3206
|
}
|
|
2789
3207
|
|
|
3208
|
+
if (Platform.OS === 'android') {
|
|
3209
|
+
const activeTransport = transportCache[uuid];
|
|
3210
|
+
if (activeTransport && isKnownDefaultMtu(activeTransport.mtuSize)) {
|
|
3211
|
+
throw createProtocolV2DefaultMtuError(activeTransport.mtuSize);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
|
|
2790
3215
|
const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
|
|
2791
3216
|
const callOptions = options;
|
|
2792
3217
|
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
@@ -2864,7 +3289,7 @@ export default class ReactNativeBleTransport {
|
|
|
2864
3289
|
const transport = this.getCachedTransport(uuid);
|
|
2865
3290
|
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2866
3291
|
|
|
2867
|
-
const { device: refreshedDevice } = await requestNegotiatedMtu(
|
|
3292
|
+
const { device: refreshedDevice, abandoned } = await requestNegotiatedMtu(
|
|
2868
3293
|
transport.device,
|
|
2869
3294
|
'highThroughput',
|
|
2870
3295
|
1,
|
|
@@ -2874,7 +3299,7 @@ export default class ReactNativeBleTransport {
|
|
|
2874
3299
|
transport.mtuSize =
|
|
2875
3300
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2876
3301
|
|
|
2877
|
-
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
3302
|
+
if (abandoned || shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2878
3303
|
throw ERRORS.TypedError(
|
|
2879
3304
|
HardwareErrorCode.BleConnectedError,
|
|
2880
3305
|
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|