@onekeyfe/hd-transport-react-native 1.2.2-alpha.116 → 1.2.2-alpha.118
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 +13 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -179
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +18 -38
- package/src/__tests__/protocolV2Link.test.ts +255 -298
- package/src/index.ts +135 -330
package/src/index.ts
CHANGED
|
@@ -185,10 +185,7 @@ const shouldRethrowProtocolProbeError = (error: unknown): boolean => {
|
|
|
185
185
|
code === HardwareErrorCode.BleDeviceDisconnected ||
|
|
186
186
|
code === HardwareErrorCode.BleCharacteristicNotifyError ||
|
|
187
187
|
code === HardwareErrorCode.BleCharacteristicNotifyChangeFailure ||
|
|
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)
|
|
188
|
+
code === HardwareErrorCode.BleWriteCharacteristicError
|
|
192
189
|
);
|
|
193
190
|
};
|
|
194
191
|
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
@@ -259,28 +256,16 @@ const connectOptions: Record<string, unknown> = {
|
|
|
259
256
|
refreshGatt: 'OnConnected',
|
|
260
257
|
};
|
|
261
258
|
|
|
262
|
-
/**
|
|
259
|
+
/** Connect options without requestMTU: the iOS fallback and every bare Android connect. */
|
|
263
260
|
const fallbackConnectOptions: Record<string, unknown> = {
|
|
264
261
|
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
265
262
|
};
|
|
266
263
|
|
|
267
264
|
/**
|
|
268
|
-
* Android
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
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.
|
|
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.
|
|
284
269
|
*/
|
|
285
270
|
const androidRefreshGattConnectOptions: Record<string, unknown> = {
|
|
286
271
|
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
@@ -288,61 +273,27 @@ const androidRefreshGattConnectOptions: Record<string, unknown> = {
|
|
|
288
273
|
};
|
|
289
274
|
|
|
290
275
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
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.
|
|
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.
|
|
297
278
|
*/
|
|
298
279
|
export const ANDROID_MTU_EXCHANGE_TIMEOUT_MS = 12_000;
|
|
299
280
|
|
|
300
281
|
/**
|
|
301
|
-
* Android keeps an LE link
|
|
302
|
-
*
|
|
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.
|
|
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.
|
|
310
284
|
*/
|
|
311
285
|
export const ANDROID_LINK_DROP_QUIET_MS = 5000;
|
|
312
|
-
export const ANDROID_LINK_DROP_TIMEOUT_MS = 8000;
|
|
313
286
|
const ANDROID_LINK_DROP_POLL_MS = 250;
|
|
314
287
|
|
|
315
288
|
/**
|
|
316
|
-
*
|
|
317
|
-
* packets
|
|
318
|
-
*
|
|
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.
|
|
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.
|
|
321
292
|
*/
|
|
322
293
|
const isKnownDefaultMtu = (mtu: unknown): boolean =>
|
|
323
294
|
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
295
|
|
|
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
|
-
*/
|
|
296
|
+
/** Discovery found no OneKey service, or a characteristic of the wrong shape: the cached GATT table may be stale. */
|
|
346
297
|
const isMissingGattShapeError = (error: unknown): boolean => {
|
|
347
298
|
const code = (error as { errorCode?: unknown })?.errorCode;
|
|
348
299
|
const message = (error as { message?: unknown })?.message;
|
|
@@ -358,9 +309,9 @@ const isMissingGattShapeError = (error: unknown): boolean => {
|
|
|
358
309
|
const isStaleGattTableNotifyReason = (reason: string | null | undefined): boolean =>
|
|
359
310
|
!!reason &&
|
|
360
311
|
(reason.includes('Cannot write client characteristic config descriptor') ||
|
|
361
|
-
reason.includes('Cannot find client characteristic config descriptor') ||
|
|
312
|
+
reason.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
362
313
|
reason.includes('The handle is invalid') ||
|
|
363
|
-
reason.includes('Writing is not permitted'));
|
|
314
|
+
reason.includes('Writing is not permitted')); // pro firmware 2.3.4 upgrade
|
|
364
315
|
|
|
365
316
|
/**
|
|
366
317
|
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
@@ -401,22 +352,16 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
|
401
352
|
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
402
353
|
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
403
354
|
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
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 => {
|
|
355
|
+
const isMtuOrCancelledConnectError = (error: unknown): boolean => {
|
|
414
356
|
const errorCode = (error as { errorCode?: unknown })?.errorCode;
|
|
415
357
|
return (
|
|
416
|
-
errorCode === BleErrorCode.
|
|
358
|
+
errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
359
|
+
errorCode === BleErrorCode.OperationCancelled
|
|
417
360
|
);
|
|
418
361
|
};
|
|
419
362
|
|
|
363
|
+
type NegotiatedMtuResult = { device: Device; timedOut: boolean };
|
|
364
|
+
|
|
420
365
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
421
366
|
|
|
422
367
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -428,26 +373,13 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
428
373
|
return infos;
|
|
429
374
|
};
|
|
430
375
|
|
|
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
|
-
|
|
444
376
|
const requestNegotiatedMtu = async (
|
|
445
377
|
device: Device,
|
|
446
378
|
stage: 'connected' | 'highThroughput',
|
|
447
379
|
attempt: number,
|
|
448
380
|
cancelTransaction?: (transactionId: string) => Promise<void> | void
|
|
449
381
|
): Promise<NegotiatedMtuResult> => {
|
|
450
|
-
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device,
|
|
382
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, timedOut: false };
|
|
451
383
|
|
|
452
384
|
const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
|
|
453
385
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -470,7 +402,7 @@ const requestNegotiatedMtu = async (
|
|
|
470
402
|
}, BLE_MTU_REQUEST_TIMEOUT_MS);
|
|
471
403
|
}),
|
|
472
404
|
]);
|
|
473
|
-
return { device: mtuDevice,
|
|
405
|
+
return { device: mtuDevice, timedOut: false };
|
|
474
406
|
} catch (error) {
|
|
475
407
|
if (timedOut && cancelTransaction) {
|
|
476
408
|
try {
|
|
@@ -496,9 +428,10 @@ const requestNegotiatedMtu = async (
|
|
|
496
428
|
stage,
|
|
497
429
|
attempt,
|
|
498
430
|
actual: device.mtu,
|
|
431
|
+
timedOut,
|
|
499
432
|
error: error instanceof Error ? error.message : String(error),
|
|
500
433
|
});
|
|
501
|
-
return { device,
|
|
434
|
+
return { device, timedOut };
|
|
502
435
|
} finally {
|
|
503
436
|
if (timeoutId) clearTimeout(timeoutId);
|
|
504
437
|
}
|
|
@@ -510,7 +443,7 @@ const resolveNegotiatedMtu = (
|
|
|
510
443
|
): Promise<NegotiatedMtuResult> =>
|
|
511
444
|
shouldRefreshNegotiatedMtu(device.mtu)
|
|
512
445
|
? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
|
|
513
|
-
: Promise.resolve({ device,
|
|
446
|
+
: Promise.resolve({ device, timedOut: false });
|
|
514
447
|
|
|
515
448
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
516
449
|
|
|
@@ -594,31 +527,12 @@ export default class ReactNativeBleTransport {
|
|
|
594
527
|
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
595
528
|
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
596
529
|
|
|
597
|
-
/** Endpoints whose last detection got no answer
|
|
598
|
-
private silentDetections = new
|
|
530
|
+
/** Endpoints whose last detection got no answer; 'woken' once their Initialize wake is spent. */
|
|
531
|
+
private silentDetections = new Map<string, 'silent' | 'woken'>();
|
|
599
532
|
|
|
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
|
-
*/
|
|
533
|
+
/** Android endpoints whose cached GATT table is suspect; the next connect refreshes it. */
|
|
605
534
|
private androidGattCacheRefreshes = new Set<string>();
|
|
606
535
|
|
|
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
|
-
|
|
622
536
|
/**
|
|
623
537
|
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
624
538
|
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
@@ -889,13 +803,9 @@ export default class ReactNativeBleTransport {
|
|
|
889
803
|
|
|
890
804
|
let { device } = transport;
|
|
891
805
|
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;
|
|
896
806
|
if (!isConnected) {
|
|
897
807
|
try {
|
|
898
|
-
device = await this.connectWithTimeout(uuid, () => device.connect(
|
|
808
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
899
809
|
} catch (e) {
|
|
900
810
|
if (
|
|
901
811
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
@@ -910,17 +820,8 @@ export default class ReactNativeBleTransport {
|
|
|
910
820
|
|
|
911
821
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
912
822
|
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
|
-
}
|
|
921
823
|
|
|
922
824
|
transport.device = device;
|
|
923
|
-
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
924
825
|
transport.writeCharacteristic = writeCharacteristic;
|
|
925
826
|
transport.notifyCharacteristic = notifyCharacteristic;
|
|
926
827
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -1106,7 +1007,7 @@ export default class ReactNativeBleTransport {
|
|
|
1106
1007
|
characteristics?: ResolvedBleCharacteristics
|
|
1107
1008
|
) {
|
|
1108
1009
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1109
|
-
characteristics ?? (await this.
|
|
1010
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
1110
1011
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1111
1012
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
1112
1013
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
@@ -1209,13 +1110,9 @@ export default class ReactNativeBleTransport {
|
|
|
1209
1110
|
const isCachedDeviceConnected = await cachedTransport.device
|
|
1210
1111
|
.isConnected()
|
|
1211
1112
|
.catch(() => false);
|
|
1212
|
-
// A
|
|
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.
|
|
1113
|
+
// A suspect GATT table is only refreshed through a new connect.
|
|
1215
1114
|
const isCachedAndroidLinkUsable =
|
|
1216
|
-
Platform.OS !== 'android' ||
|
|
1217
|
-
(!isKnownDefaultMtu(cachedTransport.mtuSize) &&
|
|
1218
|
-
!this.androidGattCacheRefreshes.has(uuid));
|
|
1115
|
+
Platform.OS !== 'android' || !this.androidGattCacheRefreshes.has(uuid);
|
|
1219
1116
|
if (
|
|
1220
1117
|
isCachedDeviceConnected &&
|
|
1221
1118
|
isCachedAndroidLinkUsable &&
|
|
@@ -1238,19 +1135,16 @@ export default class ReactNativeBleTransport {
|
|
|
1238
1135
|
|
|
1239
1136
|
let device: Device | null = null;
|
|
1240
1137
|
const isAndroid = Platform.OS === 'android';
|
|
1241
|
-
// A firmware-install reconnect
|
|
1242
|
-
// install loader may expose a different table than the firmware that was cached.
|
|
1138
|
+
// A firmware-install reconnect always refreshes: the new firmware may expose a different table.
|
|
1243
1139
|
const refreshAndroidGattCache =
|
|
1244
1140
|
isAndroid && (!!skipProtocolProbe || this.androidGattCacheRefreshes.has(uuid));
|
|
1245
1141
|
let nativeConnectOptions = connectOptions;
|
|
1246
1142
|
if (isAndroid) {
|
|
1247
1143
|
nativeConnectOptions = refreshAndroidGattCache
|
|
1248
1144
|
? androidRefreshGattConnectOptions
|
|
1249
|
-
:
|
|
1145
|
+
: fallbackConnectOptions;
|
|
1250
1146
|
}
|
|
1251
|
-
// Only a connect that
|
|
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.
|
|
1147
|
+
// Only a connect that carried refreshGatt clears the marker; the fallback connects drop it.
|
|
1254
1148
|
let androidRefreshConnectRan = false;
|
|
1255
1149
|
|
|
1256
1150
|
if (forceCleanRunPromise && this.runPromise) {
|
|
@@ -1262,6 +1156,7 @@ export default class ReactNativeBleTransport {
|
|
|
1262
1156
|
}
|
|
1263
1157
|
|
|
1264
1158
|
const blePlxManager = await this.getPlxManager();
|
|
1159
|
+
let skipPostConnectMtu = false;
|
|
1265
1160
|
try {
|
|
1266
1161
|
await subscribeBleOn(blePlxManager);
|
|
1267
1162
|
} catch (error) {
|
|
@@ -1314,17 +1209,9 @@ export default class ReactNativeBleTransport {
|
|
|
1314
1209
|
if (shouldRethrowBleSetupError(e)) {
|
|
1315
1210
|
throw e;
|
|
1316
1211
|
}
|
|
1317
|
-
if (
|
|
1318
|
-
|
|
1319
|
-
e.errorCode === BleErrorCode.OperationCancelled
|
|
1320
|
-
) {
|
|
1212
|
+
if (isMtuOrCancelledConnectError(e)) {
|
|
1213
|
+
skipPostConnectMtu = true;
|
|
1321
1214
|
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
|
-
);
|
|
1328
1215
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1329
1216
|
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
1330
1217
|
);
|
|
@@ -1341,7 +1228,11 @@ export default class ReactNativeBleTransport {
|
|
|
1341
1228
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'unable to connect to device');
|
|
1342
1229
|
}
|
|
1343
1230
|
|
|
1344
|
-
if (
|
|
1231
|
+
if (
|
|
1232
|
+
refreshAndroidGattCache &&
|
|
1233
|
+
!androidRefreshConnectRan &&
|
|
1234
|
+
(await device.isConnected().catch(() => false))
|
|
1235
|
+
) {
|
|
1345
1236
|
// refreshGatt only reaches the stack through a connect. A link that is still up would
|
|
1346
1237
|
// skip the connect below and keep serving the stale table, so it is dropped first.
|
|
1347
1238
|
await this.dropAndroidLink(uuid, blePlxManager, device, 'gatt cache refresh');
|
|
@@ -1362,17 +1253,9 @@ export default class ReactNativeBleTransport {
|
|
|
1362
1253
|
if (shouldRethrowBleSetupError(e)) {
|
|
1363
1254
|
throw e;
|
|
1364
1255
|
}
|
|
1365
|
-
if (
|
|
1366
|
-
|
|
1367
|
-
e.errorCode === BleErrorCode.OperationCancelled
|
|
1368
|
-
) {
|
|
1256
|
+
if (isMtuOrCancelledConnectError(e)) {
|
|
1257
|
+
skipPostConnectMtu = true;
|
|
1369
1258
|
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
|
-
);
|
|
1376
1259
|
try {
|
|
1377
1260
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1378
1261
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
@@ -1418,43 +1301,38 @@ export default class ReactNativeBleTransport {
|
|
|
1418
1301
|
if (refreshAndroidGattCache) {
|
|
1419
1302
|
// refreshGatt has already started a full rediscovery; let it finish before the MTU
|
|
1420
1303
|
// exchange so the request is not queued behind it.
|
|
1421
|
-
characteristics = await this.
|
|
1304
|
+
characteristics = await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
1422
1305
|
if (androidRefreshConnectRan) this.androidGattCacheRefreshes.delete(uuid);
|
|
1423
1306
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1424
1307
|
}
|
|
1425
1308
|
device = await this.negotiateAndroidMtu(uuid, blePlxManager, device);
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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 =>
|
|
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 =>
|
|
1437
1314
|
blePlxManager.cancelTransaction(transactionId)
|
|
1438
1315
|
);
|
|
1439
|
-
device =
|
|
1440
|
-
if (
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
+
await this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () =>
|
|
1324
|
+
timedOutDevice.cancelConnection()
|
|
1325
|
+
);
|
|
1326
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1327
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1328
|
+
timedOutDevice.connect(fallbackConnectOptions)
|
|
1451
1329
|
);
|
|
1452
1330
|
}
|
|
1453
1331
|
}
|
|
1454
1332
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1455
1333
|
const acquiredDevice = device;
|
|
1456
1334
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1457
|
-
characteristics ?? (await this.
|
|
1335
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice));
|
|
1458
1336
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1459
1337
|
|
|
1460
1338
|
const protocolHint = expectedProtocol
|
|
@@ -1565,6 +1443,9 @@ export default class ReactNativeBleTransport {
|
|
|
1565
1443
|
this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
|
|
1566
1444
|
return;
|
|
1567
1445
|
}
|
|
1446
|
+
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1447
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
1448
|
+
}
|
|
1568
1449
|
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1569
1450
|
let errorCode:
|
|
1570
1451
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -1574,17 +1455,11 @@ export default class ReactNativeBleTransport {
|
|
|
1574
1455
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1575
1456
|
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
1576
1457
|
} else if (
|
|
1577
|
-
error.reason
|
|
1578
|
-
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
1579
|
-
error.reason?.includes('The handle is invalid') ||
|
|
1580
|
-
error.reason?.includes('Writing is not permitted') ||
|
|
1458
|
+
isStaleGattTableNotifyReason(error.reason) ||
|
|
1581
1459
|
error.reason?.includes('notify change failed for device')
|
|
1582
1460
|
) {
|
|
1583
1461
|
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
1584
1462
|
}
|
|
1585
|
-
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1586
|
-
this.androidGattCacheRefreshes.add(uuid);
|
|
1587
|
-
}
|
|
1588
1463
|
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
1589
1464
|
return;
|
|
1590
1465
|
}
|
|
@@ -1597,18 +1472,12 @@ export default class ReactNativeBleTransport {
|
|
|
1597
1472
|
ERROR = HardwareErrorCode.BleTimeoutError;
|
|
1598
1473
|
}
|
|
1599
1474
|
if (
|
|
1600
|
-
error.reason
|
|
1601
|
-
error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
1602
|
-
error.reason?.includes('The handle is invalid') ||
|
|
1603
|
-
error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
|
|
1475
|
+
isStaleGattTableNotifyReason(error.reason) ||
|
|
1604
1476
|
error.reason?.includes('notify change failed for device')
|
|
1605
1477
|
) {
|
|
1606
1478
|
const notifyError = ERRORS.TypedError(
|
|
1607
1479
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
1608
1480
|
);
|
|
1609
|
-
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1610
|
-
this.androidGattCacheRefreshes.add(uuid);
|
|
1611
|
-
}
|
|
1612
1481
|
this.runPromise.reject(notifyError);
|
|
1613
1482
|
Log?.debug(
|
|
1614
1483
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
@@ -2033,13 +1902,14 @@ export default class ReactNativeBleTransport {
|
|
|
2033
1902
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
2034
1903
|
return check.call(jsonData);
|
|
2035
1904
|
} catch (e) {
|
|
2036
|
-
|
|
2037
|
-
|
|
1905
|
+
const isProbeTimeout =
|
|
1906
|
+
options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS &&
|
|
1907
|
+
(name === 'GetFeatures' || name === 'Initialize');
|
|
1908
|
+
if (isProbeTimeout) {
|
|
1909
|
+
Log?.debug(`[ReactNativeBleTransport] Protocol V1 ${name} probe call failed:`, e);
|
|
2038
1910
|
} else {
|
|
2039
1911
|
Log?.error('call error: ', e);
|
|
2040
1912
|
}
|
|
2041
|
-
const isProbeTimeout =
|
|
2042
|
-
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
2043
1913
|
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
2044
1914
|
// transport; its late timeout must not tear down the connection the current
|
|
2045
1915
|
// call is actively using.
|
|
@@ -2273,10 +2143,8 @@ export default class ReactNativeBleTransport {
|
|
|
2273
2143
|
}
|
|
2274
2144
|
|
|
2275
2145
|
/**
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
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.
|
|
2146
|
+
* Android: request the MTU with nothing queued ahead and never close the client while it is
|
|
2147
|
+
* outstanding; a link that times out or stays at MTU 23 is dropped.
|
|
2280
2148
|
*/
|
|
2281
2149
|
private async negotiateAndroidMtu(
|
|
2282
2150
|
uuid: string,
|
|
@@ -2286,61 +2154,58 @@ export default class ReactNativeBleTransport {
|
|
|
2286
2154
|
if (!shouldRefreshNegotiatedMtu(device.mtu)) return device;
|
|
2287
2155
|
|
|
2288
2156
|
const startedAt = Date.now();
|
|
2289
|
-
const transactionId = `${device.id}:mtu:connected:0:${startedAt}`;
|
|
2290
2157
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2291
2158
|
let timedOut = false;
|
|
2292
|
-
|
|
2293
|
-
|
|
2159
|
+
let negotiated = device;
|
|
2160
|
+
let failure: string | undefined;
|
|
2294
2161
|
try {
|
|
2295
|
-
|
|
2296
|
-
|
|
2162
|
+
negotiated = await Promise.race([
|
|
2163
|
+
device.requestMTU(ANDROID_REQUEST_MTU, `${device.id}:mtu:connected:0:${startedAt}`),
|
|
2297
2164
|
new Promise<never>((_, reject) => {
|
|
2298
2165
|
timer = setTimeout(() => {
|
|
2299
2166
|
timedOut = true;
|
|
2300
2167
|
reject(
|
|
2301
|
-
new Error(`BLE MTU exchange timeout after ${
|
|
2168
|
+
new Error(`BLE MTU exchange timeout after ${ANDROID_MTU_EXCHANGE_TIMEOUT_MS}ms`)
|
|
2302
2169
|
);
|
|
2303
|
-
},
|
|
2170
|
+
}, ANDROID_MTU_EXCHANGE_TIMEOUT_MS);
|
|
2304
2171
|
}),
|
|
2305
2172
|
]);
|
|
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
2173
|
} catch (error) {
|
|
2313
|
-
|
|
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
|
-
);
|
|
2174
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
2333
2175
|
} finally {
|
|
2334
2176
|
if (timer) clearTimeout(timer);
|
|
2335
2177
|
}
|
|
2178
|
+
Log?.debug(`[ReactNativeBleTransport] BLE MTU exchange ${failure ? 'failed' : 'completed'}`, {
|
|
2179
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2180
|
+
elapsedMs: Date.now() - startedAt,
|
|
2181
|
+
timedOut,
|
|
2182
|
+
actual: negotiated.mtu,
|
|
2183
|
+
error: failure,
|
|
2184
|
+
});
|
|
2185
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2186
|
+
if (!timedOut && !isKnownDefaultMtu(negotiated.mtu)) return negotiated;
|
|
2187
|
+
|
|
2188
|
+
// Counted like a setup timeout so a link that keeps failing reaches the wedged-link guard.
|
|
2189
|
+
const resetManager = this.abandonStalledConnection(
|
|
2190
|
+
uuid,
|
|
2191
|
+
timedOut ? 'mtu-backstop' : 'mtu-default'
|
|
2192
|
+
);
|
|
2193
|
+
await this.dropAndroidLink(
|
|
2194
|
+
uuid,
|
|
2195
|
+
manager,
|
|
2196
|
+
negotiated,
|
|
2197
|
+
timedOut ? 'mtu exchange timeout' : 'default mtu'
|
|
2198
|
+
);
|
|
2199
|
+
if (resetManager) throw this.createWedgedBleSetupError();
|
|
2200
|
+
throw ERRORS.TypedError(
|
|
2201
|
+
HardwareErrorCode.BleConnectedError,
|
|
2202
|
+
timedOut
|
|
2203
|
+
? 'BLE MTU exchange did not complete, reconnecting on a fresh link'
|
|
2204
|
+
: `BLE link stayed at the default MTU ${negotiated.mtu}, reconnecting on a fresh link`
|
|
2205
|
+
);
|
|
2336
2206
|
}
|
|
2337
2207
|
|
|
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
|
-
*/
|
|
2208
|
+
/** Close the client and wait out the link idle timer so the next connect gets a fresh link. */
|
|
2344
2209
|
private async dropAndroidLink(
|
|
2345
2210
|
uuid: string,
|
|
2346
2211
|
manager: BlePlxManager,
|
|
@@ -2359,55 +2224,16 @@ export default class ReactNativeBleTransport {
|
|
|
2359
2224
|
});
|
|
2360
2225
|
|
|
2361
2226
|
const startedAt = Date.now();
|
|
2362
|
-
|
|
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
|
-
}
|
|
2227
|
+
while (!this.stopped && Date.now() - startedAt < ANDROID_LINK_DROP_QUIET_MS) {
|
|
2379
2228
|
await delay(ANDROID_LINK_DROP_POLL_MS);
|
|
2380
2229
|
}
|
|
2381
2230
|
Log?.debug('[ReactNativeBleTransport] Android BLE link drop', {
|
|
2382
2231
|
connectIdSuffix: uuid.slice(-8),
|
|
2383
2232
|
reason,
|
|
2384
|
-
registryClear,
|
|
2385
|
-
quietPeriodElapsed,
|
|
2386
2233
|
stopped: this.stopped,
|
|
2387
|
-
elapsedMs: Date.now() - startedAt,
|
|
2388
2234
|
});
|
|
2389
2235
|
}
|
|
2390
2236
|
|
|
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
|
-
|
|
2411
2237
|
/** Run a native connect under the JS backstop budget. */
|
|
2412
2238
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
2413
2239
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
@@ -2488,7 +2314,7 @@ export default class ReactNativeBleTransport {
|
|
|
2488
2314
|
succeeded = true;
|
|
2489
2315
|
return result;
|
|
2490
2316
|
} catch (error) {
|
|
2491
|
-
if (timedOut ||
|
|
2317
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
2492
2318
|
const resetManager = this.abandonStalledConnection(
|
|
2493
2319
|
uuid,
|
|
2494
2320
|
timedOut ? 'gatt-backstop' : 'gatt-native'
|
|
@@ -2497,6 +2323,9 @@ export default class ReactNativeBleTransport {
|
|
|
2497
2323
|
throw this.createWedgedBleSetupError();
|
|
2498
2324
|
}
|
|
2499
2325
|
}
|
|
2326
|
+
if (Platform.OS === 'android' && isMissingGattShapeError(error)) {
|
|
2327
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
2328
|
+
}
|
|
2500
2329
|
throw error;
|
|
2501
2330
|
} finally {
|
|
2502
2331
|
if (timer) clearTimeout(timer);
|
|
@@ -2699,7 +2528,6 @@ export default class ReactNativeBleTransport {
|
|
|
2699
2528
|
// firmware reconnect after the native BLE manager is recreated.
|
|
2700
2529
|
this.protocolReprobeFailures.clear();
|
|
2701
2530
|
this.silentDetections.clear();
|
|
2702
|
-
this.protocolWakeAttempts.clear();
|
|
2703
2531
|
this.writeTimeoutCounts.clear();
|
|
2704
2532
|
this.connectionSetupTimeoutCounts.clear();
|
|
2705
2533
|
this.monitorTokens.clear();
|
|
@@ -2835,7 +2663,6 @@ export default class ReactNativeBleTransport {
|
|
|
2835
2663
|
}
|
|
2836
2664
|
this.protocolReprobeFailures.delete(uuid);
|
|
2837
2665
|
this.silentDetections.delete(uuid);
|
|
2838
|
-
this.protocolWakeAttempts.delete(uuid);
|
|
2839
2666
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2840
2667
|
deviceId: uuid,
|
|
2841
2668
|
protocol,
|
|
@@ -2845,9 +2672,8 @@ export default class ReactNativeBleTransport {
|
|
|
2845
2672
|
}
|
|
2846
2673
|
}
|
|
2847
2674
|
|
|
2848
|
-
//
|
|
2849
|
-
|
|
2850
|
-
this.silentDetections.add(uuid);
|
|
2675
|
+
// Arms the wake for the next detection.
|
|
2676
|
+
if (!this.silentDetections.has(uuid)) this.silentDetections.set(uuid, 'silent');
|
|
2851
2677
|
|
|
2852
2678
|
if (trustSessionProtocol) {
|
|
2853
2679
|
// Still silent on its own protocol: count it, and let the streak expire the
|
|
@@ -2863,29 +2689,19 @@ export default class ReactNativeBleTransport {
|
|
|
2863
2689
|
}
|
|
2864
2690
|
|
|
2865
2691
|
/**
|
|
2866
|
-
*
|
|
2867
|
-
*
|
|
2868
|
-
*
|
|
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.
|
|
2692
|
+
* A sleeping Classic drops GetFeatures/Ping and only leaves its screensaver on Initialize, which
|
|
2693
|
+
* resets the wallet session, so it is sent once after a fully silent detection. The firmware does
|
|
2694
|
+
* not reliably answer it, so its timeout must not drop the link.
|
|
2880
2695
|
*/
|
|
2881
2696
|
private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
|
|
2882
|
-
if (
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2697
|
+
if (
|
|
2698
|
+
Platform.OS !== 'android' ||
|
|
2699
|
+
!probeOrder.includes('V1') ||
|
|
2700
|
+
this.silentDetections.get(uuid) !== 'silent'
|
|
2701
|
+
) {
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
this.silentDetections.set(uuid, 'woken');
|
|
2889
2705
|
Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
|
|
2890
2706
|
connectIdSuffix: uuid.slice(-8),
|
|
2891
2707
|
});
|
|
@@ -2893,11 +2709,7 @@ export default class ReactNativeBleTransport {
|
|
|
2893
2709
|
this.probingProtocols.set(uuid, 'V1');
|
|
2894
2710
|
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2895
2711
|
} 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);
|
|
2712
|
+
if (shouldRethrowProtocolProbeError(error)) throw error;
|
|
2901
2713
|
} finally {
|
|
2902
2714
|
this.clearProbeProtocol(uuid, 'V1');
|
|
2903
2715
|
}
|
|
@@ -3205,13 +3017,6 @@ export default class ReactNativeBleTransport {
|
|
|
3205
3017
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
3206
3018
|
}
|
|
3207
3019
|
|
|
3208
|
-
if (Platform.OS === 'android') {
|
|
3209
|
-
const activeTransport = transportCache[uuid];
|
|
3210
|
-
if (activeTransport && isKnownDefaultMtu(activeTransport.mtuSize)) {
|
|
3211
|
-
throw createProtocolV2DefaultMtuError(activeTransport.mtuSize);
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
3020
|
const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
|
|
3216
3021
|
const callOptions = options;
|
|
3217
3022
|
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
@@ -3289,7 +3094,7 @@ export default class ReactNativeBleTransport {
|
|
|
3289
3094
|
const transport = this.getCachedTransport(uuid);
|
|
3290
3095
|
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
3291
3096
|
|
|
3292
|
-
const { device: refreshedDevice
|
|
3097
|
+
const { device: refreshedDevice } = await requestNegotiatedMtu(
|
|
3293
3098
|
transport.device,
|
|
3294
3099
|
'highThroughput',
|
|
3295
3100
|
1,
|
|
@@ -3299,7 +3104,7 @@ export default class ReactNativeBleTransport {
|
|
|
3299
3104
|
transport.mtuSize =
|
|
3300
3105
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
3301
3106
|
|
|
3302
|
-
if (
|
|
3107
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
3303
3108
|
throw ERRORS.TypedError(
|
|
3304
3109
|
HardwareErrorCode.BleConnectedError,
|
|
3305
3110
|
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|