@onekeyfe/hd-transport-react-native 1.2.3-alpha.3 → 1.2.3-alpha.5
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/README.md +1 -1
- package/dist/BleManager.d.ts +1 -1
- package/dist/BleManager.d.ts.map +1 -1
- package/dist/bleNativeDisconnect.d.ts +3 -0
- package/dist/bleNativeDisconnect.d.ts.map +1 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +634 -152
- package/dist/subscribeBleOn.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/BleManager.ts +73 -5
- package/src/__tests__/bleNativeDisconnect.test.ts +54 -0
- package/src/__tests__/connectTimeout.test.ts +465 -3
- package/src/__tests__/enumerate.test.ts +36 -1
- package/src/__tests__/protocolV2Link.test.ts +976 -62
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/__tests__/subscribeBleOn.test.ts +111 -0
- package/src/bleNativeDisconnect.ts +41 -0
- package/src/index.ts +647 -159
- package/src/subscribeBleOn.ts +26 -10
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
getInfosForServiceUuid,
|
|
48
48
|
isSameBleUuid,
|
|
49
49
|
} from './constants';
|
|
50
|
+
import { isNativeBleDisconnectError, toBleDisconnectHardwareError } from './bleNativeDisconnect';
|
|
50
51
|
import {
|
|
51
52
|
isBleStaleBondHardwareError,
|
|
52
53
|
isNativeBleStaleBondError,
|
|
@@ -76,6 +77,8 @@ const { check, ProtocolV1, parseConfigure } = transport;
|
|
|
76
77
|
const Log = bleLogger;
|
|
77
78
|
|
|
78
79
|
const transportCache: Record<string, BleTransport> = {};
|
|
80
|
+
// ble-plx shares one manager across transport instances in this JS runtime.
|
|
81
|
+
let bleManagerResetPromise: Promise<void> | undefined;
|
|
79
82
|
const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
80
83
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
81
84
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
@@ -168,6 +171,23 @@ const isWedgedWriteError = (error: unknown): boolean =>
|
|
|
168
171
|
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
169
172
|
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
170
173
|
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
174
|
+
const shouldRethrowProtocolProbeError = (error: unknown): boolean => {
|
|
175
|
+
const code = (error as { errorCode?: unknown })?.errorCode;
|
|
176
|
+
// Bonding and GATT failures are not evidence of a protocol mismatch. Preserve
|
|
177
|
+
// them instead of probing another protocol on an unusable connection.
|
|
178
|
+
// Native PLX disconnects (errorCode 201 / iOS 7, or the unstructured
|
|
179
|
+
// "was disconnected" fallback) must match before Protocol V2 probing.
|
|
180
|
+
return (
|
|
181
|
+
isBleStaleBondHardwareError(error) ||
|
|
182
|
+
isNativeBleDisconnectError(error) ||
|
|
183
|
+
code === HardwareErrorCode.BleDeviceNotBonded ||
|
|
184
|
+
code === HardwareErrorCode.BleDeviceBondedCanceled ||
|
|
185
|
+
code === HardwareErrorCode.BleDeviceDisconnected ||
|
|
186
|
+
code === HardwareErrorCode.BleCharacteristicNotifyError ||
|
|
187
|
+
code === HardwareErrorCode.BleCharacteristicNotifyChangeFailure ||
|
|
188
|
+
code === HardwareErrorCode.BleWriteCharacteristicError
|
|
189
|
+
);
|
|
190
|
+
};
|
|
171
191
|
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
172
192
|
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
173
193
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
@@ -222,13 +242,13 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
222
242
|
|
|
223
243
|
const IOS_REQUEST_MTU = 247;
|
|
224
244
|
const ANDROID_REQUEST_MTU = 517;
|
|
225
|
-
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
226
245
|
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
227
246
|
|
|
228
247
|
const getRequestedBleMtu = () =>
|
|
229
248
|
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
230
249
|
|
|
231
250
|
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
251
|
+
const BLE_MTU_REQUEST_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS;
|
|
232
252
|
|
|
233
253
|
const connectOptions: Record<string, unknown> = {
|
|
234
254
|
requestMTU: getRequestedBleMtu(),
|
|
@@ -236,11 +256,63 @@ const connectOptions: Record<string, unknown> = {
|
|
|
236
256
|
refreshGatt: 'OnConnected',
|
|
237
257
|
};
|
|
238
258
|
|
|
239
|
-
/**
|
|
259
|
+
/** Connect options without requestMTU: the iOS fallback and every bare Android connect. */
|
|
240
260
|
const fallbackConnectOptions: Record<string, unknown> = {
|
|
241
261
|
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
242
262
|
};
|
|
243
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
|
+
|
|
244
316
|
/**
|
|
245
317
|
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
246
318
|
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
@@ -280,6 +352,15 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
|
280
352
|
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
281
353
|
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
282
354
|
(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 };
|
|
283
364
|
|
|
284
365
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
285
366
|
|
|
@@ -294,29 +375,75 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
294
375
|
|
|
295
376
|
const requestNegotiatedMtu = async (
|
|
296
377
|
device: Device,
|
|
297
|
-
stage: 'connected' | '
|
|
298
|
-
attempt: number
|
|
299
|
-
) =>
|
|
300
|
-
|
|
378
|
+
stage: 'connected' | 'highThroughput',
|
|
379
|
+
attempt: number,
|
|
380
|
+
cancelTransaction?: (transactionId: string) => Promise<void> | void
|
|
381
|
+
): Promise<NegotiatedMtuResult> => {
|
|
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;
|
|
301
387
|
|
|
302
388
|
try {
|
|
303
389
|
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
304
390
|
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
305
|
-
const
|
|
306
|
-
|
|
391
|
+
const request = device.requestMTU(getRequestedBleMtu(), transactionId);
|
|
392
|
+
// The timeout race may settle before the native request does. Attach a
|
|
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 };
|
|
307
406
|
} 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
|
+
}
|
|
308
426
|
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
309
427
|
platform: Platform.OS,
|
|
310
428
|
stage,
|
|
311
429
|
attempt,
|
|
312
430
|
actual: device.mtu,
|
|
431
|
+
timedOut,
|
|
313
432
|
error: error instanceof Error ? error.message : String(error),
|
|
314
433
|
});
|
|
315
|
-
return device;
|
|
434
|
+
return { device, timedOut };
|
|
435
|
+
} finally {
|
|
436
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
316
437
|
}
|
|
317
438
|
};
|
|
318
439
|
|
|
319
|
-
const resolveNegotiatedMtu = (
|
|
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 });
|
|
320
447
|
|
|
321
448
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
322
449
|
|
|
@@ -333,11 +460,11 @@ function remapError(error: IOBleErrorRemap) {
|
|
|
333
460
|
}
|
|
334
461
|
}
|
|
335
462
|
|
|
336
|
-
if (
|
|
337
|
-
error
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
) {
|
|
463
|
+
if (isNativeBleDisconnectError(error)) {
|
|
464
|
+
throw toBleDisconnectHardwareError(error);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (error instanceof Error && error.message?.includes('not found')) {
|
|
341
468
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
342
469
|
}
|
|
343
470
|
|
|
@@ -360,6 +487,8 @@ export default class ReactNativeBleTransport {
|
|
|
360
487
|
|
|
361
488
|
stopped = false;
|
|
362
489
|
|
|
490
|
+
private readonly bondAbortController = new AbortController();
|
|
491
|
+
|
|
363
492
|
scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
|
|
364
493
|
|
|
365
494
|
runPromise: Deferred<any> | null = null;
|
|
@@ -398,6 +527,12 @@ export default class ReactNativeBleTransport {
|
|
|
398
527
|
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
399
528
|
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
400
529
|
|
|
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
|
+
|
|
401
536
|
/**
|
|
402
537
|
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
403
538
|
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
@@ -430,6 +565,14 @@ export default class ReactNativeBleTransport {
|
|
|
430
565
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
431
566
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
432
567
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
568
|
+
if (this.probingProtocols.get(uuid) !== 'V2') {
|
|
569
|
+
const transport = transportCache[uuid];
|
|
570
|
+
try {
|
|
571
|
+
this.emitDeviceDisconnect(uuid, transport?.device?.name, transport?.monitorToken);
|
|
572
|
+
} catch {
|
|
573
|
+
Log?.error('[ReactNativeBleTransport] Protocol V2 disconnect listener failed');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
433
576
|
await this.releaseNative(uuid, true);
|
|
434
577
|
}
|
|
435
578
|
},
|
|
@@ -450,6 +593,10 @@ export default class ReactNativeBleTransport {
|
|
|
450
593
|
/** Serializes transport lifecycle changes for the same physical device. */
|
|
451
594
|
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
452
595
|
|
|
596
|
+
private stopPromise?: Promise<void>;
|
|
597
|
+
|
|
598
|
+
private scanCleanups = new Set<() => Promise<void>>();
|
|
599
|
+
|
|
453
600
|
constructor(options: TransportOptions) {
|
|
454
601
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
455
602
|
}
|
|
@@ -485,10 +632,34 @@ export default class ReactNativeBleTransport {
|
|
|
485
632
|
// empty
|
|
486
633
|
}
|
|
487
634
|
|
|
488
|
-
getPlxManager(): Promise<BlePlxManager> {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
635
|
+
async getPlxManager(): Promise<BlePlxManager> {
|
|
636
|
+
while (bleManagerResetPromise) {
|
|
637
|
+
await this.waitForManagerReset();
|
|
638
|
+
}
|
|
639
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
640
|
+
if (!this.blePlxManager) this.blePlxManager = new BlePlxManager();
|
|
641
|
+
return this.blePlxManager;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private async waitForManagerReset(): Promise<void> {
|
|
645
|
+
if (!bleManagerResetPromise) return;
|
|
646
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
647
|
+
try {
|
|
648
|
+
await Promise.race([
|
|
649
|
+
bleManagerResetPromise,
|
|
650
|
+
new Promise<never>((_, reject) => {
|
|
651
|
+
timeout = setTimeout(
|
|
652
|
+
() => reject(this.createWedgedBleSetupError()),
|
|
653
|
+
BLE_CONNECT_TIMEOUT_MS
|
|
654
|
+
);
|
|
655
|
+
}),
|
|
656
|
+
]);
|
|
657
|
+
} catch {
|
|
658
|
+
// A timeout or failed destroy is not permission to reuse the old singleton.
|
|
659
|
+
throw this.createWedgedBleSetupError();
|
|
660
|
+
} finally {
|
|
661
|
+
if (timeout) clearTimeout(timeout);
|
|
662
|
+
}
|
|
492
663
|
}
|
|
493
664
|
|
|
494
665
|
async resolveCharacteristics(device: Device): Promise<ResolvedBleCharacteristics> {
|
|
@@ -677,35 +848,58 @@ export default class ReactNativeBleTransport {
|
|
|
677
848
|
* @returns
|
|
678
849
|
*/
|
|
679
850
|
async enumerate() {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
} catch (error) {
|
|
687
|
-
Log?.debug('subscribeBleOn error: ', error);
|
|
688
|
-
reject(error);
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (Platform.OS === 'android' && Platform.Version >= 31) {
|
|
693
|
-
Log?.debug('requesting permissions, please wait...');
|
|
851
|
+
const scanStartedAt = Date.now();
|
|
852
|
+
let firstDeviceMs: number | undefined;
|
|
853
|
+
const blePlxManager = await this.getPlxManager();
|
|
854
|
+
await subscribeBleOn(blePlxManager);
|
|
855
|
+
if (Platform.OS === 'android' && Platform.Version >= 31) {
|
|
856
|
+
Log?.debug('requesting permissions, please wait...');
|
|
694
857
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
858
|
+
const resultConnect = await PermissionsAndroid.requestMultiple([
|
|
859
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
|
860
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
|
861
|
+
]);
|
|
699
862
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
863
|
+
Log?.debug('requesting permissions, result: ', resultConnect);
|
|
864
|
+
if (
|
|
865
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
|
|
866
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted'
|
|
867
|
+
) {
|
|
868
|
+
throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
708
869
|
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
873
|
+
return new Promise<IOneKeyDevice[]>((resolve, reject) => {
|
|
874
|
+
const deviceList: IOneKeyDevice[] = [];
|
|
875
|
+
let finished = false;
|
|
876
|
+
let scanCleanup: Promise<void> | undefined;
|
|
877
|
+
const finishScan = (error?: unknown) => {
|
|
878
|
+
if (scanCleanup) return scanCleanup;
|
|
879
|
+
finished = true;
|
|
880
|
+
clearScanTimer();
|
|
881
|
+
scanCleanup = this.runNativeTeardown('scan', blePlxManager, async () => {
|
|
882
|
+
await blePlxManager.stopDeviceScan();
|
|
883
|
+
}).then(() => {
|
|
884
|
+
this.scanCleanups.delete(cancelScan);
|
|
885
|
+
if (error) reject(error);
|
|
886
|
+
else resolve(deviceList);
|
|
887
|
+
});
|
|
888
|
+
return scanCleanup;
|
|
889
|
+
};
|
|
890
|
+
const cancelScan = () =>
|
|
891
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
892
|
+
this.scanCleanups.add(cancelScan);
|
|
893
|
+
|
|
894
|
+
const clearScanTimer = timer.timeout(() => {
|
|
895
|
+
Log?.debug('[ReactNativeBleTransport] scan completed', {
|
|
896
|
+
elapsedMs: Date.now() - scanStartedAt,
|
|
897
|
+
firstDeviceMs,
|
|
898
|
+
deviceCount: deviceList.length,
|
|
899
|
+
scanWindowMs: this.scanTimeout,
|
|
900
|
+
});
|
|
901
|
+
finishScan();
|
|
902
|
+
}, this.scanTimeout);
|
|
709
903
|
|
|
710
904
|
blePlxManager.startDeviceScan(
|
|
711
905
|
getBluetoothServiceUuids(),
|
|
@@ -716,23 +910,20 @@ export default class ReactNativeBleTransport {
|
|
|
716
910
|
(error, device) => {
|
|
717
911
|
if (error) {
|
|
718
912
|
Log?.debug('ble scan error: ', error);
|
|
719
|
-
if (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
)
|
|
723
|
-
) {
|
|
724
|
-
reject(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
|
|
913
|
+
if (error.errorCode === BleErrorCode.BluetoothPoweredOff) {
|
|
914
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BlePoweredOff));
|
|
915
|
+
} else if (error.errorCode === BleErrorCode.BluetoothUnsupported) {
|
|
916
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleUnsupported));
|
|
725
917
|
} else if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
|
|
726
|
-
|
|
918
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
|
|
727
919
|
} else if (error.errorCode === BleErrorCode.LocationServicesDisabled) {
|
|
728
|
-
|
|
920
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationServicesDisabled));
|
|
729
921
|
} else if (error.errorCode === BleErrorCode.ScanStartFailed) {
|
|
730
922
|
// Android Bluetooth will report an error when the search frequency is too fast,
|
|
731
923
|
// then nothing is processed and an empty array of devices is returned.
|
|
732
924
|
// Then the next search will be back to normal
|
|
733
|
-
timer.timeout(() => {}, this.scanTimeout);
|
|
734
925
|
} else {
|
|
735
|
-
|
|
926
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.reason ?? ''));
|
|
736
927
|
}
|
|
737
928
|
return;
|
|
738
929
|
}
|
|
@@ -764,6 +955,7 @@ export default class ReactNativeBleTransport {
|
|
|
764
955
|
}
|
|
765
956
|
);
|
|
766
957
|
|
|
958
|
+
if (finished) return;
|
|
767
959
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
768
960
|
devices => {
|
|
769
961
|
for (const device of devices) {
|
|
@@ -783,11 +975,13 @@ export default class ReactNativeBleTransport {
|
|
|
783
975
|
addDevice(device as unknown as Device);
|
|
784
976
|
}
|
|
785
977
|
}
|
|
786
|
-
}
|
|
978
|
+
},
|
|
979
|
+
error => Log?.debug('search connected peripheral failed:', error)
|
|
787
980
|
);
|
|
788
981
|
|
|
789
982
|
const addDevice = (device: Device) => {
|
|
790
|
-
if (deviceList.every(d => d.id !== device.id)) {
|
|
983
|
+
if (!finished && deviceList.every(d => d.id !== device.id)) {
|
|
984
|
+
firstDeviceMs ??= Date.now() - scanStartedAt;
|
|
791
985
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
792
986
|
|
|
793
987
|
deviceList.push({
|
|
@@ -802,11 +996,6 @@ export default class ReactNativeBleTransport {
|
|
|
802
996
|
});
|
|
803
997
|
}
|
|
804
998
|
};
|
|
805
|
-
|
|
806
|
-
timer.timeout(() => {
|
|
807
|
-
blePlxManager.stopDeviceScan();
|
|
808
|
-
resolve(deviceList);
|
|
809
|
-
}, this.scanTimeout);
|
|
810
999
|
});
|
|
811
1000
|
}
|
|
812
1001
|
|
|
@@ -817,6 +1006,7 @@ export default class ReactNativeBleTransport {
|
|
|
817
1006
|
) {
|
|
818
1007
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
819
1008
|
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
1009
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
820
1010
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
821
1011
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
822
1012
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -845,39 +1035,12 @@ export default class ReactNativeBleTransport {
|
|
|
845
1035
|
} else if (Platform.OS === 'android') {
|
|
846
1036
|
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
847
1037
|
}
|
|
848
|
-
|
|
849
|
-
const initialMtu = transport.mtuSize;
|
|
850
|
-
let refreshAttempts = 0;
|
|
851
|
-
if (
|
|
852
|
-
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
853
|
-
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
854
|
-
) {
|
|
855
|
-
refreshAttempts += 1;
|
|
856
|
-
let refreshedDevice = await requestNegotiatedMtu(
|
|
857
|
-
transport.device,
|
|
858
|
-
'servicesAndNotifyReady',
|
|
859
|
-
1
|
|
860
|
-
);
|
|
861
|
-
transport.device = refreshedDevice;
|
|
862
|
-
transport.mtuSize =
|
|
863
|
-
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
864
|
-
|
|
865
|
-
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
866
|
-
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
867
|
-
refreshAttempts += 1;
|
|
868
|
-
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
869
|
-
transport.device = refreshedDevice;
|
|
870
|
-
transport.mtuSize =
|
|
871
|
-
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
1038
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
874
1039
|
|
|
875
1040
|
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
876
1041
|
platform: Platform.OS,
|
|
877
1042
|
requested: getRequestedBleMtu(),
|
|
878
|
-
initial: initialMtu,
|
|
879
1043
|
actual: transport.mtuSize,
|
|
880
|
-
refreshAttempts,
|
|
881
1044
|
});
|
|
882
1045
|
|
|
883
1046
|
return transport;
|
|
@@ -894,6 +1057,7 @@ export default class ReactNativeBleTransport {
|
|
|
894
1057
|
}
|
|
895
1058
|
|
|
896
1059
|
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
1060
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
897
1061
|
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
898
1062
|
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
899
1063
|
? expectedProtocol === 'V2'
|
|
@@ -944,11 +1108,16 @@ export default class ReactNativeBleTransport {
|
|
|
944
1108
|
const isCachedDeviceConnected = await cachedTransport.device
|
|
945
1109
|
.isConnected()
|
|
946
1110
|
.catch(() => false);
|
|
1111
|
+
// A suspect GATT table is only refreshed through a new connect.
|
|
1112
|
+
const isCachedAndroidLinkUsable =
|
|
1113
|
+
Platform.OS !== 'android' || !this.androidGattCacheRefreshes.has(uuid);
|
|
947
1114
|
if (
|
|
948
1115
|
isCachedDeviceConnected &&
|
|
1116
|
+
isCachedAndroidLinkUsable &&
|
|
949
1117
|
cachedProtocol &&
|
|
950
1118
|
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
951
1119
|
) {
|
|
1120
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
952
1121
|
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
953
1122
|
return { uuid, protocolType: cachedProtocol };
|
|
954
1123
|
}
|
|
@@ -963,6 +1132,18 @@ export default class ReactNativeBleTransport {
|
|
|
963
1132
|
}
|
|
964
1133
|
|
|
965
1134
|
let device: Device | null = null;
|
|
1135
|
+
const isAndroid = Platform.OS === 'android';
|
|
1136
|
+
// A firmware-install reconnect always refreshes: the new firmware may expose a different table.
|
|
1137
|
+
const refreshAndroidGattCache =
|
|
1138
|
+
isAndroid && (!!skipProtocolProbe || this.androidGattCacheRefreshes.has(uuid));
|
|
1139
|
+
let nativeConnectOptions = connectOptions;
|
|
1140
|
+
if (isAndroid) {
|
|
1141
|
+
nativeConnectOptions = refreshAndroidGattCache
|
|
1142
|
+
? androidRefreshGattConnectOptions
|
|
1143
|
+
: fallbackConnectOptions;
|
|
1144
|
+
}
|
|
1145
|
+
// Only a connect that carried refreshGatt clears the marker; the fallback connects drop it.
|
|
1146
|
+
let androidRefreshConnectRan = false;
|
|
966
1147
|
|
|
967
1148
|
if (forceCleanRunPromise && this.runPromise) {
|
|
968
1149
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
@@ -973,6 +1154,7 @@ export default class ReactNativeBleTransport {
|
|
|
973
1154
|
}
|
|
974
1155
|
|
|
975
1156
|
const blePlxManager = await this.getPlxManager();
|
|
1157
|
+
let skipPostConnectMtu = false;
|
|
976
1158
|
try {
|
|
977
1159
|
await subscribeBleOn(blePlxManager);
|
|
978
1160
|
} catch (error) {
|
|
@@ -980,6 +1162,27 @@ export default class ReactNativeBleTransport {
|
|
|
980
1162
|
throw error;
|
|
981
1163
|
}
|
|
982
1164
|
|
|
1165
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1166
|
+
if (Platform.OS === 'android') {
|
|
1167
|
+
// Initiate bonding locally before GATT can trigger peripheral-initiated pairing.
|
|
1168
|
+
try {
|
|
1169
|
+
const bondState = await pairDevice(uuid);
|
|
1170
|
+
if (bondState.bonding) {
|
|
1171
|
+
await onDeviceBondState(uuid, this.bondAbortController.signal);
|
|
1172
|
+
} else if (!bondState.bonded) {
|
|
1173
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
1174
|
+
}
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1177
|
+
await this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
|
|
1178
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1179
|
+
);
|
|
1180
|
+
});
|
|
1181
|
+
throw error;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1185
|
+
|
|
983
1186
|
if (!device) {
|
|
984
1187
|
const devices = await blePlxManager.devices([uuid]);
|
|
985
1188
|
[device] = devices;
|
|
@@ -996,17 +1199,16 @@ export default class ReactNativeBleTransport {
|
|
|
996
1199
|
Log?.debug('try to connect to device: ', uuid);
|
|
997
1200
|
try {
|
|
998
1201
|
device = await this.connectWithTimeout(uuid, () =>
|
|
999
|
-
blePlxManager.connectToDevice(uuid,
|
|
1202
|
+
blePlxManager.connectToDevice(uuid, nativeConnectOptions)
|
|
1000
1203
|
);
|
|
1204
|
+
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1001
1205
|
} catch (e) {
|
|
1002
1206
|
Log?.debug('try to connect to device has error: ', e);
|
|
1003
1207
|
if (shouldRethrowBleSetupError(e)) {
|
|
1004
1208
|
throw e;
|
|
1005
1209
|
}
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
1008
|
-
e.errorCode === BleErrorCode.OperationCancelled
|
|
1009
|
-
) {
|
|
1210
|
+
if (isMtuOrCancelledConnectError(e)) {
|
|
1211
|
+
skipPostConnectMtu = true;
|
|
1010
1212
|
Log?.debug('first try to reconnect without params');
|
|
1011
1213
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1012
1214
|
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
@@ -1024,23 +1226,33 @@ export default class ReactNativeBleTransport {
|
|
|
1024
1226
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'unable to connect to device');
|
|
1025
1227
|
}
|
|
1026
1228
|
|
|
1229
|
+
if (
|
|
1230
|
+
refreshAndroidGattCache &&
|
|
1231
|
+
!androidRefreshConnectRan &&
|
|
1232
|
+
(await device.isConnected().catch(() => false))
|
|
1233
|
+
) {
|
|
1234
|
+
// refreshGatt only reaches the stack through a connect. A link that is still up would
|
|
1235
|
+
// skip the connect below and keep serving the stale table, so it is dropped first.
|
|
1236
|
+
await this.dropAndroidLink(uuid, blePlxManager, device, 'gatt cache refresh');
|
|
1237
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1027
1240
|
if (!(await device.isConnected())) {
|
|
1028
1241
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
1029
1242
|
const disconnectedDevice = device;
|
|
1030
1243
|
|
|
1031
1244
|
try {
|
|
1032
1245
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1033
|
-
disconnectedDevice.connect(
|
|
1246
|
+
disconnectedDevice.connect(nativeConnectOptions)
|
|
1034
1247
|
);
|
|
1248
|
+
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1035
1249
|
} catch (e) {
|
|
1036
1250
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
1037
1251
|
if (shouldRethrowBleSetupError(e)) {
|
|
1038
1252
|
throw e;
|
|
1039
1253
|
}
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1042
|
-
e.errorCode === BleErrorCode.OperationCancelled
|
|
1043
|
-
) {
|
|
1254
|
+
if (isMtuOrCancelledConnectError(e)) {
|
|
1255
|
+
skipPostConnectMtu = true;
|
|
1044
1256
|
Log?.debug('second try to reconnect without params');
|
|
1045
1257
|
try {
|
|
1046
1258
|
device = await this.connectWithTimeout(uuid, () =>
|
|
@@ -1066,42 +1278,80 @@ export default class ReactNativeBleTransport {
|
|
|
1066
1278
|
}
|
|
1067
1279
|
}
|
|
1068
1280
|
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1281
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1282
|
+
if (Platform.OS === 'android' && !(await device.isConnected().catch(() => false))) {
|
|
1283
|
+
const disconnectedDevice = device;
|
|
1284
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1285
|
+
await Promise.all([
|
|
1286
|
+
this.runBestEffortNativeOperation('connect failure: cancel manager connection', () =>
|
|
1287
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1288
|
+
),
|
|
1289
|
+
this.runBestEffortNativeOperation('connect failure: cancel device connection', () =>
|
|
1290
|
+
disconnectedDevice.cancelConnection()
|
|
1291
|
+
),
|
|
1292
|
+
]);
|
|
1293
|
+
});
|
|
1294
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1295
|
+
}
|
|
1296
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1297
|
+
let characteristics: ResolvedBleCharacteristics | undefined;
|
|
1298
|
+
if (isAndroid) {
|
|
1299
|
+
if (refreshAndroidGattCache) {
|
|
1300
|
+
// refreshGatt has already started a full rediscovery; let it finish before the MTU
|
|
1301
|
+
// exchange so the request is not queued behind it.
|
|
1302
|
+
characteristics = await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
1303
|
+
if (androidRefreshConnectRan) this.androidGattCacheRefreshes.delete(uuid);
|
|
1304
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1305
|
+
}
|
|
1306
|
+
device = await this.negotiateAndroidMtu(uuid, blePlxManager, device);
|
|
1307
|
+
} else if (!skipPostConnectMtu) {
|
|
1308
|
+
// Match 1.1.31: MTU is a connect() best-effort. If connect already fell back
|
|
1309
|
+
// without requestMTU, do not put another requestMTU on the native serial
|
|
1310
|
+
// queue — that is what wedges GATT after Account#2 reconnect.
|
|
1311
|
+
const mtuResult = await resolveNegotiatedMtu(device, transactionId =>
|
|
1312
|
+
blePlxManager.cancelTransaction(transactionId)
|
|
1313
|
+
);
|
|
1314
|
+
device = mtuResult.device;
|
|
1315
|
+
if (mtuResult.timedOut) {
|
|
1316
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1317
|
+
Log?.debug(
|
|
1318
|
+
'[ReactNativeBleTransport] post-connect MTU timed out, reconnecting without requesting MTU'
|
|
1319
|
+
);
|
|
1320
|
+
const timedOutDevice = device;
|
|
1321
|
+
let mtuTeardownSettled = false;
|
|
1322
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1323
|
+
await this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () =>
|
|
1324
|
+
timedOutDevice.cancelConnection()
|
|
1078
1325
|
);
|
|
1326
|
+
mtuTeardownSettled = true;
|
|
1327
|
+
});
|
|
1328
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1329
|
+
if (!mtuTeardownSettled || this.blePlxManager !== blePlxManager) {
|
|
1330
|
+
// The teardown budget expired and the manager that owned timedOutDevice was reset.
|
|
1331
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'BLE MTU cleanup timed out');
|
|
1079
1332
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1333
|
+
try {
|
|
1334
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1335
|
+
timedOutDevice.connect(fallbackConnectOptions)
|
|
1336
|
+
);
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
if (shouldRethrowBleSetupError(error)) throw error;
|
|
1339
|
+
if (
|
|
1340
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.DeviceAlreadyConnected
|
|
1341
|
+
) {
|
|
1342
|
+
// GATT resolution and the protocol probe below still validate the retained link.
|
|
1343
|
+
device = timedOutDevice;
|
|
1344
|
+
} else {
|
|
1345
|
+
remapError(error);
|
|
1346
|
+
}
|
|
1085
1347
|
}
|
|
1086
|
-
} catch (error) {
|
|
1087
|
-
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1088
|
-
await Promise.all([
|
|
1089
|
-
this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
|
|
1090
|
-
blePlxManager.cancelDeviceConnection(uuid)
|
|
1091
|
-
),
|
|
1092
|
-
this.runBestEffortNativeOperation('bond failure: cancel device connection', () =>
|
|
1093
|
-
connectedDevice.cancelConnection()
|
|
1094
|
-
),
|
|
1095
|
-
]);
|
|
1096
|
-
});
|
|
1097
|
-
throw error;
|
|
1098
1348
|
}
|
|
1099
1349
|
}
|
|
1100
|
-
|
|
1101
|
-
device = await resolveNegotiatedMtu(device);
|
|
1350
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1102
1351
|
const acquiredDevice = device;
|
|
1103
1352
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1104
|
-
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
1353
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice));
|
|
1354
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1105
1355
|
|
|
1106
1356
|
const protocolHint = expectedProtocol
|
|
1107
1357
|
? undefined
|
|
@@ -1163,6 +1413,7 @@ export default class ReactNativeBleTransport {
|
|
|
1163
1413
|
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1164
1414
|
}
|
|
1165
1415
|
);
|
|
1416
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1166
1417
|
const currentTransport = transportCache[uuid];
|
|
1167
1418
|
if (!currentTransport) {
|
|
1168
1419
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
@@ -1170,11 +1421,9 @@ export default class ReactNativeBleTransport {
|
|
|
1170
1421
|
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1171
1422
|
return { uuid, protocolType };
|
|
1172
1423
|
} catch (error) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
await this.releaseUnlocked(uuid, true);
|
|
1177
|
-
}
|
|
1424
|
+
// A failed acquire must retire the physical link before Core retries. Logical
|
|
1425
|
+
// release leaves GATT connected even when neither protocol receives a response.
|
|
1426
|
+
await this.disconnectUnlocked(uuid);
|
|
1178
1427
|
throw error;
|
|
1179
1428
|
} finally {
|
|
1180
1429
|
this.acquiringProtocolV2.delete(uuid);
|
|
@@ -1212,6 +1461,9 @@ export default class ReactNativeBleTransport {
|
|
|
1212
1461
|
this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
|
|
1213
1462
|
return;
|
|
1214
1463
|
}
|
|
1464
|
+
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1465
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
1466
|
+
}
|
|
1215
1467
|
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1216
1468
|
let errorCode:
|
|
1217
1469
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -1221,10 +1473,7 @@ export default class ReactNativeBleTransport {
|
|
|
1221
1473
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1222
1474
|
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
1223
1475
|
} else if (
|
|
1224
|
-
error.reason
|
|
1225
|
-
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
1226
|
-
error.reason?.includes('The handle is invalid') ||
|
|
1227
|
-
error.reason?.includes('Writing is not permitted') ||
|
|
1476
|
+
isStaleGattTableNotifyReason(error.reason) ||
|
|
1228
1477
|
error.reason?.includes('notify change failed for device')
|
|
1229
1478
|
) {
|
|
1230
1479
|
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
@@ -1241,10 +1490,7 @@ export default class ReactNativeBleTransport {
|
|
|
1241
1490
|
ERROR = HardwareErrorCode.BleTimeoutError;
|
|
1242
1491
|
}
|
|
1243
1492
|
if (
|
|
1244
|
-
error.reason
|
|
1245
|
-
error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
1246
|
-
error.reason?.includes('The handle is invalid') ||
|
|
1247
|
-
error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
|
|
1493
|
+
isStaleGattTableNotifyReason(error.reason) ||
|
|
1248
1494
|
error.reason?.includes('notify change failed for device')
|
|
1249
1495
|
) {
|
|
1250
1496
|
const notifyError = ERRORS.TypedError(
|
|
@@ -1674,13 +1920,14 @@ export default class ReactNativeBleTransport {
|
|
|
1674
1920
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1675
1921
|
return check.call(jsonData);
|
|
1676
1922
|
} catch (e) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1923
|
+
const isProbeTimeout =
|
|
1924
|
+
options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS &&
|
|
1925
|
+
(name === 'GetFeatures' || name === 'Initialize');
|
|
1926
|
+
if (isProbeTimeout) {
|
|
1927
|
+
Log?.debug(`[ReactNativeBleTransport] Protocol V1 ${name} probe call failed:`, e);
|
|
1679
1928
|
} else {
|
|
1680
1929
|
Log?.error('call error: ', e);
|
|
1681
1930
|
}
|
|
1682
|
-
const isProbeTimeout =
|
|
1683
|
-
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1684
1931
|
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1685
1932
|
// transport; its late timeout must not tear down the connection the current
|
|
1686
1933
|
// call is actively using.
|
|
@@ -1703,7 +1950,49 @@ export default class ReactNativeBleTransport {
|
|
|
1703
1950
|
}
|
|
1704
1951
|
|
|
1705
1952
|
stop() {
|
|
1953
|
+
if (this.stopPromise) return this.stopPromise;
|
|
1706
1954
|
this.stopped = true;
|
|
1955
|
+
// Bonding precedes GATT, so cancelDeviceConnection cannot end this wait.
|
|
1956
|
+
this.bondAbortController.abort();
|
|
1957
|
+
const deviceIds = new Set([
|
|
1958
|
+
...this.monitorTokens.keys(),
|
|
1959
|
+
...this.sessionProtocols.keys(),
|
|
1960
|
+
...this.lifecycleOperations.keys(),
|
|
1961
|
+
...(this.runPromiseDeviceId ? [this.runPromiseDeviceId] : []),
|
|
1962
|
+
]);
|
|
1963
|
+
const scans = Array.from(this.scanCleanups, cleanup => cleanup());
|
|
1964
|
+
this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
|
|
1965
|
+
this.androidPriorityResetTimers.clear();
|
|
1966
|
+
this.androidHighPriorityDevices.clear();
|
|
1967
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1968
|
+
this.runPromise?.reject(error);
|
|
1969
|
+
this.runPromise = null;
|
|
1970
|
+
this.runPromiseDeviceId = null;
|
|
1971
|
+
deviceIds.forEach(uuid => this.rejectProtocolV2Frames(uuid, error));
|
|
1972
|
+
const manager = this.blePlxManager;
|
|
1973
|
+
// Cancel native setup before waiting for its lifecycle lock. Otherwise stop
|
|
1974
|
+
// waits for the very connect/MTU/GATT operation it needs to interrupt.
|
|
1975
|
+
const pendingConnections = manager
|
|
1976
|
+
? Array.from(this.lifecycleOperations.keys(), uuid =>
|
|
1977
|
+
this.runNativeTeardown(uuid, manager, async () => {
|
|
1978
|
+
await this.runBestEffortNativeOperation('stop: cancel pending device connection', () =>
|
|
1979
|
+
manager.cancelDeviceConnection(uuid)
|
|
1980
|
+
);
|
|
1981
|
+
})
|
|
1982
|
+
)
|
|
1983
|
+
: [];
|
|
1984
|
+
// Release only this transport's endpoints; other connectors may share ble-plx.
|
|
1985
|
+
this.stopPromise = Promise.all([
|
|
1986
|
+
...scans,
|
|
1987
|
+
...pendingConnections,
|
|
1988
|
+
...Array.from(deviceIds, uuid => this.disconnect(uuid)),
|
|
1989
|
+
]).then(async () => {
|
|
1990
|
+
await this.protocolV2Links.invalidateAllLinks('React Native BLE transport stopped');
|
|
1991
|
+
await this.waitForManagerReset();
|
|
1992
|
+
this.blePlxManager = undefined;
|
|
1993
|
+
this.emitter = undefined;
|
|
1994
|
+
});
|
|
1995
|
+
return this.stopPromise;
|
|
1707
1996
|
}
|
|
1708
1997
|
|
|
1709
1998
|
async disconnect(session: string) {
|
|
@@ -1855,17 +2144,119 @@ export default class ReactNativeBleTransport {
|
|
|
1855
2144
|
}
|
|
1856
2145
|
}
|
|
1857
2146
|
|
|
1858
|
-
cancel() {
|
|
2147
|
+
async cancel() {
|
|
1859
2148
|
Log?.debug('transport-react-native transport cancel');
|
|
1860
|
-
|
|
1861
|
-
|
|
2149
|
+
const pending = this.runPromise;
|
|
2150
|
+
const deviceId = this.runPromiseDeviceId;
|
|
2151
|
+
if (pending) {
|
|
2152
|
+
pending.reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled));
|
|
2153
|
+
if (this.runPromise === pending) {
|
|
2154
|
+
this.runPromise = null;
|
|
2155
|
+
this.runPromiseDeviceId = null;
|
|
2156
|
+
}
|
|
2157
|
+
// A V1 read cannot be safely reused after abandoning its response.
|
|
2158
|
+
// Drain native teardown before DeviceCommands releases the operation.
|
|
2159
|
+
if (deviceId) await this.disconnect(deviceId);
|
|
1862
2160
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Android: request the MTU with nothing queued ahead and never close the client while it is
|
|
2165
|
+
* outstanding; a link that times out or stays at MTU 23 is dropped.
|
|
2166
|
+
*/
|
|
2167
|
+
private async negotiateAndroidMtu(
|
|
2168
|
+
uuid: string,
|
|
2169
|
+
manager: BlePlxManager,
|
|
2170
|
+
device: Device
|
|
2171
|
+
): Promise<Device> {
|
|
2172
|
+
if (!shouldRefreshNegotiatedMtu(device.mtu)) return device;
|
|
2173
|
+
|
|
2174
|
+
const startedAt = Date.now();
|
|
2175
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2176
|
+
let timedOut = false;
|
|
2177
|
+
let negotiated = device;
|
|
2178
|
+
let failure: string | undefined;
|
|
2179
|
+
try {
|
|
2180
|
+
negotiated = await Promise.race([
|
|
2181
|
+
device.requestMTU(ANDROID_REQUEST_MTU, `${device.id}:mtu:connected:0:${startedAt}`),
|
|
2182
|
+
new Promise<never>((_, reject) => {
|
|
2183
|
+
timer = setTimeout(() => {
|
|
2184
|
+
timedOut = true;
|
|
2185
|
+
reject(
|
|
2186
|
+
new Error(`BLE MTU exchange timeout after ${ANDROID_MTU_EXCHANGE_TIMEOUT_MS}ms`)
|
|
2187
|
+
);
|
|
2188
|
+
}, ANDROID_MTU_EXCHANGE_TIMEOUT_MS);
|
|
2189
|
+
}),
|
|
2190
|
+
]);
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
2193
|
+
} finally {
|
|
2194
|
+
if (timer) clearTimeout(timer);
|
|
2195
|
+
}
|
|
2196
|
+
Log?.debug(`[ReactNativeBleTransport] BLE MTU exchange ${failure ? 'failed' : 'completed'}`, {
|
|
2197
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2198
|
+
elapsedMs: Date.now() - startedAt,
|
|
2199
|
+
timedOut,
|
|
2200
|
+
actual: negotiated.mtu,
|
|
2201
|
+
error: failure,
|
|
2202
|
+
});
|
|
2203
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2204
|
+
if (!timedOut && !isKnownDefaultMtu(negotiated.mtu)) return negotiated;
|
|
2205
|
+
|
|
2206
|
+
// Counted like a setup timeout so a link that keeps failing reaches the wedged-link guard.
|
|
2207
|
+
const resetManager = this.abandonStalledConnection(
|
|
2208
|
+
uuid,
|
|
2209
|
+
timedOut ? 'mtu-backstop' : 'mtu-default'
|
|
2210
|
+
);
|
|
2211
|
+
await this.dropAndroidLink(
|
|
2212
|
+
uuid,
|
|
2213
|
+
manager,
|
|
2214
|
+
negotiated,
|
|
2215
|
+
timedOut ? 'mtu exchange timeout' : 'default mtu'
|
|
2216
|
+
);
|
|
2217
|
+
if (resetManager) throw this.createWedgedBleSetupError();
|
|
2218
|
+
throw ERRORS.TypedError(
|
|
2219
|
+
HardwareErrorCode.BleConnectedError,
|
|
2220
|
+
timedOut
|
|
2221
|
+
? 'BLE MTU exchange did not complete, reconnecting on a fresh link'
|
|
2222
|
+
: `BLE link stayed at the default MTU ${negotiated.mtu}, reconnecting on a fresh link`
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/** Close the client and wait out the link idle timer so the next connect gets a fresh link. */
|
|
2227
|
+
private async dropAndroidLink(
|
|
2228
|
+
uuid: string,
|
|
2229
|
+
manager: BlePlxManager,
|
|
2230
|
+
device: Device,
|
|
2231
|
+
reason: string
|
|
2232
|
+
) {
|
|
2233
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
2234
|
+
await Promise.all([
|
|
2235
|
+
this.runBestEffortNativeOperation(`${reason}: cancel manager connection`, () =>
|
|
2236
|
+
manager.cancelDeviceConnection(uuid)
|
|
2237
|
+
),
|
|
2238
|
+
this.runBestEffortNativeOperation(`${reason}: cancel device connection`, () =>
|
|
2239
|
+
device.cancelConnection()
|
|
2240
|
+
),
|
|
2241
|
+
]);
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2244
|
+
const startedAt = Date.now();
|
|
2245
|
+
while (!this.stopped && Date.now() - startedAt < ANDROID_LINK_DROP_QUIET_MS) {
|
|
2246
|
+
await delay(ANDROID_LINK_DROP_POLL_MS);
|
|
2247
|
+
}
|
|
2248
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE link drop', {
|
|
2249
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2250
|
+
reason,
|
|
2251
|
+
stopped: this.stopped,
|
|
2252
|
+
});
|
|
1865
2253
|
}
|
|
1866
2254
|
|
|
1867
2255
|
/** Run a native connect under the JS backstop budget. */
|
|
1868
2256
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
2257
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2258
|
+
const startedAt = Date.now();
|
|
2259
|
+
let succeeded = false;
|
|
1869
2260
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1870
2261
|
let timedOut = false;
|
|
1871
2262
|
const pending = connect();
|
|
@@ -1887,6 +2278,7 @@ export default class ReactNativeBleTransport {
|
|
|
1887
2278
|
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1888
2279
|
}),
|
|
1889
2280
|
]);
|
|
2281
|
+
succeeded = true;
|
|
1890
2282
|
return result;
|
|
1891
2283
|
} catch (error) {
|
|
1892
2284
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1901,6 +2293,12 @@ export default class ReactNativeBleTransport {
|
|
|
1901
2293
|
throw error;
|
|
1902
2294
|
} finally {
|
|
1903
2295
|
if (timer) clearTimeout(timer);
|
|
2296
|
+
Log?.debug('[ReactNativeBleTransport] connect completed', {
|
|
2297
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2298
|
+
elapsedMs: Date.now() - startedAt,
|
|
2299
|
+
succeeded,
|
|
2300
|
+
backstopExpired: timedOut,
|
|
2301
|
+
});
|
|
1904
2302
|
}
|
|
1905
2303
|
}
|
|
1906
2304
|
|
|
@@ -1909,6 +2307,8 @@ export default class ReactNativeBleTransport {
|
|
|
1909
2307
|
uuid: string,
|
|
1910
2308
|
device: Device
|
|
1911
2309
|
): Promise<ResolvedBleCharacteristics> {
|
|
2310
|
+
const startedAt = Date.now();
|
|
2311
|
+
let succeeded = false;
|
|
1912
2312
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1913
2313
|
let timedOut = false;
|
|
1914
2314
|
const pending = this.resolveCharacteristics(device);
|
|
@@ -1929,6 +2329,7 @@ export default class ReactNativeBleTransport {
|
|
|
1929
2329
|
}),
|
|
1930
2330
|
]);
|
|
1931
2331
|
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
2332
|
+
succeeded = true;
|
|
1932
2333
|
return result;
|
|
1933
2334
|
} catch (error) {
|
|
1934
2335
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1940,9 +2341,24 @@ export default class ReactNativeBleTransport {
|
|
|
1940
2341
|
throw this.createWedgedBleSetupError();
|
|
1941
2342
|
}
|
|
1942
2343
|
}
|
|
2344
|
+
if (isNativeBleStaleBondError(error) || isBleStaleBondHardwareError(error)) {
|
|
2345
|
+
throw toBleStaleBondHardwareError(error);
|
|
2346
|
+
}
|
|
2347
|
+
if (isNativeBleDisconnectError(error)) {
|
|
2348
|
+
throw toBleDisconnectHardwareError(error);
|
|
2349
|
+
}
|
|
2350
|
+
if (Platform.OS === 'android' && isMissingGattShapeError(error)) {
|
|
2351
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
2352
|
+
}
|
|
1943
2353
|
throw error;
|
|
1944
2354
|
} finally {
|
|
1945
2355
|
if (timer) clearTimeout(timer);
|
|
2356
|
+
Log?.debug('[ReactNativeBleTransport] GATT setup completed', {
|
|
2357
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2358
|
+
elapsedMs: Date.now() - startedAt,
|
|
2359
|
+
succeeded,
|
|
2360
|
+
backstopExpired: timedOut,
|
|
2361
|
+
});
|
|
1946
2362
|
}
|
|
1947
2363
|
}
|
|
1948
2364
|
|
|
@@ -1954,7 +2370,13 @@ export default class ReactNativeBleTransport {
|
|
|
1954
2370
|
*/
|
|
1955
2371
|
private abandonStalledConnection(
|
|
1956
2372
|
uuid: string,
|
|
1957
|
-
stage:
|
|
2373
|
+
stage:
|
|
2374
|
+
| 'connect-backstop'
|
|
2375
|
+
| 'connect-native'
|
|
2376
|
+
| 'gatt-backstop'
|
|
2377
|
+
| 'gatt-native'
|
|
2378
|
+
| 'mtu-backstop'
|
|
2379
|
+
| 'mtu-default'
|
|
1958
2380
|
): boolean {
|
|
1959
2381
|
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1960
2382
|
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
@@ -2086,6 +2508,7 @@ export default class ReactNativeBleTransport {
|
|
|
2086
2508
|
}
|
|
2087
2509
|
|
|
2088
2510
|
private resetPlxManager() {
|
|
2511
|
+
if (bleManagerResetPromise) return;
|
|
2089
2512
|
const manager = this.blePlxManager;
|
|
2090
2513
|
this.blePlxManager = undefined;
|
|
2091
2514
|
const reason = 'React Native BLE manager reset';
|
|
@@ -2128,15 +2551,26 @@ export default class ReactNativeBleTransport {
|
|
|
2128
2551
|
// Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
|
|
2129
2552
|
// firmware reconnect after the native BLE manager is recreated.
|
|
2130
2553
|
this.protocolReprobeFailures.clear();
|
|
2554
|
+
this.silentDetections.clear();
|
|
2131
2555
|
this.writeTimeoutCounts.clear();
|
|
2132
2556
|
this.connectionSetupTimeoutCounts.clear();
|
|
2133
2557
|
this.monitorTokens.clear();
|
|
2134
2558
|
this.protocolV2Assemblers.clear();
|
|
2559
|
+
let reset: Promise<void>;
|
|
2135
2560
|
try {
|
|
2136
|
-
manager?.destroy();
|
|
2561
|
+
reset = Promise.resolve(manager?.destroy());
|
|
2137
2562
|
} catch (error) {
|
|
2138
|
-
|
|
2563
|
+
reset = Promise.reject(error);
|
|
2139
2564
|
}
|
|
2565
|
+
bleManagerResetPromise = reset;
|
|
2566
|
+
reset.then(
|
|
2567
|
+
() => {
|
|
2568
|
+
if (bleManagerResetPromise === reset) bleManagerResetPromise = undefined;
|
|
2569
|
+
},
|
|
2570
|
+
error => {
|
|
2571
|
+
Log?.error('[ReactNativeBleTransport] BLE manager destroy failed:', error);
|
|
2572
|
+
}
|
|
2573
|
+
);
|
|
2140
2574
|
}
|
|
2141
2575
|
|
|
2142
2576
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
@@ -2229,6 +2663,8 @@ export default class ReactNativeBleTransport {
|
|
|
2229
2663
|
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2230
2664
|
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
2231
2665
|
|
|
2666
|
+
await this.wakeSilentProtocolV1Device(uuid, probeOrder);
|
|
2667
|
+
|
|
2232
2668
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
2233
2669
|
const protocol = probeOrder[i];
|
|
2234
2670
|
if (i > 0) {
|
|
@@ -2250,6 +2686,7 @@ export default class ReactNativeBleTransport {
|
|
|
2250
2686
|
this.confirmedProtocolV2.add(uuid);
|
|
2251
2687
|
}
|
|
2252
2688
|
this.protocolReprobeFailures.delete(uuid);
|
|
2689
|
+
this.silentDetections.delete(uuid);
|
|
2253
2690
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2254
2691
|
deviceId: uuid,
|
|
2255
2692
|
protocol,
|
|
@@ -2259,6 +2696,9 @@ export default class ReactNativeBleTransport {
|
|
|
2259
2696
|
}
|
|
2260
2697
|
}
|
|
2261
2698
|
|
|
2699
|
+
// Arms the wake for the next detection.
|
|
2700
|
+
if (!this.silentDetections.has(uuid)) this.silentDetections.set(uuid, 'silent');
|
|
2701
|
+
|
|
2262
2702
|
if (trustSessionProtocol) {
|
|
2263
2703
|
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2264
2704
|
// shortcut so a device that genuinely switched protocols is found again.
|
|
@@ -2272,6 +2712,34 @@ export default class ReactNativeBleTransport {
|
|
|
2272
2712
|
throw this.createProtocolDetectionError();
|
|
2273
2713
|
}
|
|
2274
2714
|
|
|
2715
|
+
/**
|
|
2716
|
+
* A sleeping Classic drops GetFeatures/Ping and only leaves its screensaver on Initialize, which
|
|
2717
|
+
* resets the wallet session, so it is sent once after a fully silent detection. The firmware does
|
|
2718
|
+
* not reliably answer it, so its timeout must not drop the link. It only runs when V1 is probed
|
|
2719
|
+
* first, so a late reply lands on the V1 probe rather than on a V2 one.
|
|
2720
|
+
*/
|
|
2721
|
+
private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
|
|
2722
|
+
if (
|
|
2723
|
+
Platform.OS !== 'android' ||
|
|
2724
|
+
probeOrder[0] !== 'V1' ||
|
|
2725
|
+
this.silentDetections.get(uuid) !== 'silent'
|
|
2726
|
+
) {
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
this.silentDetections.set(uuid, 'woken');
|
|
2730
|
+
Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
|
|
2731
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2732
|
+
});
|
|
2733
|
+
try {
|
|
2734
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2735
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
if (shouldRethrowProtocolProbeError(error)) throw error;
|
|
2738
|
+
} finally {
|
|
2739
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2275
2743
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
2276
2744
|
const transport = transportCache[uuid];
|
|
2277
2745
|
await this.protocolV2Links.invalidateLink(
|
|
@@ -2339,9 +2807,7 @@ export default class ReactNativeBleTransport {
|
|
|
2339
2807
|
} catch (error) {
|
|
2340
2808
|
this.clearProbeProtocol(uuid, 'V1');
|
|
2341
2809
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2342
|
-
|
|
2343
|
-
// would only fail against a torn-down transport: surface the real cause.
|
|
2344
|
-
if (isWedgedWriteError(error)) {
|
|
2810
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2345
2811
|
throw error;
|
|
2346
2812
|
}
|
|
2347
2813
|
return false;
|
|
@@ -2366,7 +2832,7 @@ export default class ReactNativeBleTransport {
|
|
|
2366
2832
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2367
2833
|
this.resetProtocolV2Frames(uuid);
|
|
2368
2834
|
},
|
|
2369
|
-
shouldRethrow:
|
|
2835
|
+
shouldRethrow: shouldRethrowProtocolProbeError,
|
|
2370
2836
|
});
|
|
2371
2837
|
if (!detected) {
|
|
2372
2838
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -2513,6 +2979,9 @@ export default class ReactNativeBleTransport {
|
|
|
2513
2979
|
this.rememberStaleBondError(uuid, bondError);
|
|
2514
2980
|
throw bondError;
|
|
2515
2981
|
}
|
|
2982
|
+
if (isNativeBleDisconnectError(error)) {
|
|
2983
|
+
throw toBleDisconnectHardwareError(error);
|
|
2984
|
+
}
|
|
2516
2985
|
if (
|
|
2517
2986
|
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2518
2987
|
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
@@ -2573,6 +3042,7 @@ export default class ReactNativeBleTransport {
|
|
|
2573
3042
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
2574
3043
|
}
|
|
2575
3044
|
|
|
3045
|
+
const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
|
|
2576
3046
|
const callOptions = options;
|
|
2577
3047
|
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
2578
3048
|
|
|
@@ -2624,6 +3094,19 @@ export default class ReactNativeBleTransport {
|
|
|
2624
3094
|
);
|
|
2625
3095
|
} catch (e) {
|
|
2626
3096
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
3097
|
+
if (
|
|
3098
|
+
!isProtocolProbe &&
|
|
3099
|
+
e?.errorCode === HardwareErrorCode.BleTimeoutError &&
|
|
3100
|
+
!this.monitorTokens.has(uuid)
|
|
3101
|
+
) {
|
|
3102
|
+
// The failed link has finished invalidating. Disconnect outside that
|
|
3103
|
+
// callback to avoid waiting on its own invalidation or acquire lock.
|
|
3104
|
+
await this.runLifecycleOperation(uuid, async () => {
|
|
3105
|
+
// A queued timeout leaves its active monitor intact; a newer acquire
|
|
3106
|
+
// may also have installed one while cleanup waited for the lifecycle lock.
|
|
3107
|
+
if (!this.monitorTokens.has(uuid)) await this.disconnectUnlocked(uuid);
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
2627
3110
|
throw e;
|
|
2628
3111
|
} finally {
|
|
2629
3112
|
if (highThroughputWrite) {
|
|
@@ -2636,7 +3119,12 @@ export default class ReactNativeBleTransport {
|
|
|
2636
3119
|
const transport = this.getCachedTransport(uuid);
|
|
2637
3120
|
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2638
3121
|
|
|
2639
|
-
const refreshedDevice = await requestNegotiatedMtu(
|
|
3122
|
+
const { device: refreshedDevice } = await requestNegotiatedMtu(
|
|
3123
|
+
transport.device,
|
|
3124
|
+
'highThroughput',
|
|
3125
|
+
1,
|
|
3126
|
+
transactionId => this.blePlxManager?.cancelTransaction(transactionId)
|
|
3127
|
+
);
|
|
2640
3128
|
transport.device = refreshedDevice;
|
|
2641
3129
|
transport.mtuSize =
|
|
2642
3130
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|