@onekeyfe/hd-transport-react-native 1.2.2-alpha.9 → 1.2.2
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/bleStaleBond.d.ts.map +1 -1
- package/dist/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +598 -127
- package/package.json +6 -6
- package/src/BleManager.ts +73 -5
- package/src/__tests__/bleNativeDisconnect.test.ts +33 -0
- package/src/__tests__/bleStaleBond.test.ts +8 -0
- package/src/__tests__/connectTimeout.test.ts +427 -3
- package/src/__tests__/protocolV2Link.test.ts +1075 -33
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/bleStaleBond.ts +3 -0
- package/src/index.ts +643 -134
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) must match before they are
|
|
179
|
+
// mapped: Protocol V2 writes rethrow them unchanged unless normalized first.
|
|
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,35 +375,81 @@ 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
|
|
|
323
|
-
function remapError(error: IOBleErrorRemap
|
|
450
|
+
function remapError(error: IOBleErrorRemap) {
|
|
324
451
|
if (error instanceof BleError) {
|
|
325
|
-
if (
|
|
452
|
+
if (isNativeBleStaleBondError(error)) {
|
|
326
453
|
throw toBleStaleBondHardwareError(error);
|
|
327
454
|
}
|
|
328
455
|
|
|
@@ -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(),
|
|
@@ -721,18 +915,17 @@ export default class ReactNativeBleTransport {
|
|
|
721
915
|
error.errorCode
|
|
722
916
|
)
|
|
723
917
|
) {
|
|
724
|
-
|
|
918
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
|
|
725
919
|
} else if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
|
|
726
|
-
|
|
920
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
|
|
727
921
|
} else if (error.errorCode === BleErrorCode.LocationServicesDisabled) {
|
|
728
|
-
|
|
922
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationServicesDisabled));
|
|
729
923
|
} else if (error.errorCode === BleErrorCode.ScanStartFailed) {
|
|
730
924
|
// Android Bluetooth will report an error when the search frequency is too fast,
|
|
731
925
|
// then nothing is processed and an empty array of devices is returned.
|
|
732
926
|
// Then the next search will be back to normal
|
|
733
|
-
timer.timeout(() => {}, this.scanTimeout);
|
|
734
927
|
} else {
|
|
735
|
-
|
|
928
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.reason ?? ''));
|
|
736
929
|
}
|
|
737
930
|
return;
|
|
738
931
|
}
|
|
@@ -764,6 +957,7 @@ export default class ReactNativeBleTransport {
|
|
|
764
957
|
}
|
|
765
958
|
);
|
|
766
959
|
|
|
960
|
+
if (finished) return;
|
|
767
961
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
768
962
|
devices => {
|
|
769
963
|
for (const device of devices) {
|
|
@@ -783,11 +977,13 @@ export default class ReactNativeBleTransport {
|
|
|
783
977
|
addDevice(device as unknown as Device);
|
|
784
978
|
}
|
|
785
979
|
}
|
|
786
|
-
}
|
|
980
|
+
},
|
|
981
|
+
error => Log?.debug('search connected peripheral failed:', error)
|
|
787
982
|
);
|
|
788
983
|
|
|
789
984
|
const addDevice = (device: Device) => {
|
|
790
|
-
if (deviceList.every(d => d.id !== device.id)) {
|
|
985
|
+
if (!finished && deviceList.every(d => d.id !== device.id)) {
|
|
986
|
+
firstDeviceMs ??= Date.now() - scanStartedAt;
|
|
791
987
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
792
988
|
|
|
793
989
|
deviceList.push({
|
|
@@ -802,11 +998,6 @@ export default class ReactNativeBleTransport {
|
|
|
802
998
|
});
|
|
803
999
|
}
|
|
804
1000
|
};
|
|
805
|
-
|
|
806
|
-
timer.timeout(() => {
|
|
807
|
-
blePlxManager.stopDeviceScan();
|
|
808
|
-
resolve(deviceList);
|
|
809
|
-
}, this.scanTimeout);
|
|
810
1001
|
});
|
|
811
1002
|
}
|
|
812
1003
|
|
|
@@ -817,6 +1008,7 @@ export default class ReactNativeBleTransport {
|
|
|
817
1008
|
) {
|
|
818
1009
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
819
1010
|
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
1011
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
820
1012
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
821
1013
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
822
1014
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -845,39 +1037,12 @@ export default class ReactNativeBleTransport {
|
|
|
845
1037
|
} else if (Platform.OS === 'android') {
|
|
846
1038
|
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
847
1039
|
}
|
|
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
|
-
}
|
|
1040
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
874
1041
|
|
|
875
1042
|
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
876
1043
|
platform: Platform.OS,
|
|
877
1044
|
requested: getRequestedBleMtu(),
|
|
878
|
-
initial: initialMtu,
|
|
879
1045
|
actual: transport.mtuSize,
|
|
880
|
-
refreshAttempts,
|
|
881
1046
|
});
|
|
882
1047
|
|
|
883
1048
|
return transport;
|
|
@@ -894,6 +1059,7 @@ export default class ReactNativeBleTransport {
|
|
|
894
1059
|
}
|
|
895
1060
|
|
|
896
1061
|
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
1062
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
897
1063
|
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
898
1064
|
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
899
1065
|
? expectedProtocol === 'V2'
|
|
@@ -944,11 +1110,16 @@ export default class ReactNativeBleTransport {
|
|
|
944
1110
|
const isCachedDeviceConnected = await cachedTransport.device
|
|
945
1111
|
.isConnected()
|
|
946
1112
|
.catch(() => false);
|
|
1113
|
+
// A suspect GATT table is only refreshed through a new connect.
|
|
1114
|
+
const isCachedAndroidLinkUsable =
|
|
1115
|
+
Platform.OS !== 'android' || !this.androidGattCacheRefreshes.has(uuid);
|
|
947
1116
|
if (
|
|
948
1117
|
isCachedDeviceConnected &&
|
|
1118
|
+
isCachedAndroidLinkUsable &&
|
|
949
1119
|
cachedProtocol &&
|
|
950
1120
|
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
951
1121
|
) {
|
|
1122
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
952
1123
|
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
953
1124
|
return { uuid, protocolType: cachedProtocol };
|
|
954
1125
|
}
|
|
@@ -963,6 +1134,18 @@ export default class ReactNativeBleTransport {
|
|
|
963
1134
|
}
|
|
964
1135
|
|
|
965
1136
|
let device: Device | null = null;
|
|
1137
|
+
const isAndroid = Platform.OS === 'android';
|
|
1138
|
+
// A firmware-install reconnect always refreshes: the new firmware may expose a different table.
|
|
1139
|
+
const refreshAndroidGattCache =
|
|
1140
|
+
isAndroid && (!!skipProtocolProbe || this.androidGattCacheRefreshes.has(uuid));
|
|
1141
|
+
let nativeConnectOptions = connectOptions;
|
|
1142
|
+
if (isAndroid) {
|
|
1143
|
+
nativeConnectOptions = refreshAndroidGattCache
|
|
1144
|
+
? androidRefreshGattConnectOptions
|
|
1145
|
+
: fallbackConnectOptions;
|
|
1146
|
+
}
|
|
1147
|
+
// Only a connect that carried refreshGatt clears the marker; the fallback connects drop it.
|
|
1148
|
+
let androidRefreshConnectRan = false;
|
|
966
1149
|
|
|
967
1150
|
if (forceCleanRunPromise && this.runPromise) {
|
|
968
1151
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
@@ -973,6 +1156,7 @@ export default class ReactNativeBleTransport {
|
|
|
973
1156
|
}
|
|
974
1157
|
|
|
975
1158
|
const blePlxManager = await this.getPlxManager();
|
|
1159
|
+
let skipPostConnectMtu = false;
|
|
976
1160
|
try {
|
|
977
1161
|
await subscribeBleOn(blePlxManager);
|
|
978
1162
|
} catch (error) {
|
|
@@ -980,14 +1164,26 @@ export default class ReactNativeBleTransport {
|
|
|
980
1164
|
throw error;
|
|
981
1165
|
}
|
|
982
1166
|
|
|
1167
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
983
1168
|
if (Platform.OS === 'android') {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
await
|
|
987
|
-
|
|
988
|
-
|
|
1169
|
+
// Initiate bonding locally before GATT can trigger peripheral-initiated pairing.
|
|
1170
|
+
try {
|
|
1171
|
+
const bondState = await pairDevice(uuid);
|
|
1172
|
+
if (bondState.bonding) {
|
|
1173
|
+
await onDeviceBondState(uuid, this.bondAbortController.signal);
|
|
1174
|
+
} else if (!bondState.bonded) {
|
|
1175
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1179
|
+
await this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
|
|
1180
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1181
|
+
);
|
|
1182
|
+
});
|
|
1183
|
+
throw error;
|
|
989
1184
|
}
|
|
990
1185
|
}
|
|
1186
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
991
1187
|
|
|
992
1188
|
if (!device) {
|
|
993
1189
|
const devices = await blePlxManager.devices([uuid]);
|
|
@@ -1005,17 +1201,16 @@ export default class ReactNativeBleTransport {
|
|
|
1005
1201
|
Log?.debug('try to connect to device: ', uuid);
|
|
1006
1202
|
try {
|
|
1007
1203
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1008
|
-
blePlxManager.connectToDevice(uuid,
|
|
1204
|
+
blePlxManager.connectToDevice(uuid, nativeConnectOptions)
|
|
1009
1205
|
);
|
|
1206
|
+
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1010
1207
|
} catch (e) {
|
|
1011
1208
|
Log?.debug('try to connect to device has error: ', e);
|
|
1012
1209
|
if (shouldRethrowBleSetupError(e)) {
|
|
1013
1210
|
throw e;
|
|
1014
1211
|
}
|
|
1015
|
-
if (
|
|
1016
|
-
|
|
1017
|
-
e.errorCode === BleErrorCode.OperationCancelled
|
|
1018
|
-
) {
|
|
1212
|
+
if (isMtuOrCancelledConnectError(e)) {
|
|
1213
|
+
skipPostConnectMtu = true;
|
|
1019
1214
|
Log?.debug('first try to reconnect without params');
|
|
1020
1215
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1021
1216
|
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
@@ -1024,7 +1219,7 @@ export default class ReactNativeBleTransport {
|
|
|
1024
1219
|
Log?.debug('device already connected');
|
|
1025
1220
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
1026
1221
|
} else {
|
|
1027
|
-
remapError(e
|
|
1222
|
+
remapError(e);
|
|
1028
1223
|
}
|
|
1029
1224
|
}
|
|
1030
1225
|
}
|
|
@@ -1033,50 +1228,132 @@ export default class ReactNativeBleTransport {
|
|
|
1033
1228
|
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'unable to connect to device');
|
|
1034
1229
|
}
|
|
1035
1230
|
|
|
1231
|
+
if (
|
|
1232
|
+
refreshAndroidGattCache &&
|
|
1233
|
+
!androidRefreshConnectRan &&
|
|
1234
|
+
(await device.isConnected().catch(() => false))
|
|
1235
|
+
) {
|
|
1236
|
+
// refreshGatt only reaches the stack through a connect. A link that is still up would
|
|
1237
|
+
// skip the connect below and keep serving the stale table, so it is dropped first.
|
|
1238
|
+
await this.dropAndroidLink(uuid, blePlxManager, device, 'gatt cache refresh');
|
|
1239
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1036
1242
|
if (!(await device.isConnected())) {
|
|
1037
1243
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
1038
1244
|
const disconnectedDevice = device;
|
|
1039
1245
|
|
|
1040
1246
|
try {
|
|
1041
1247
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1042
|
-
disconnectedDevice.connect(
|
|
1248
|
+
disconnectedDevice.connect(nativeConnectOptions)
|
|
1043
1249
|
);
|
|
1250
|
+
androidRefreshConnectRan = refreshAndroidGattCache;
|
|
1044
1251
|
} catch (e) {
|
|
1045
1252
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
1046
1253
|
if (shouldRethrowBleSetupError(e)) {
|
|
1047
1254
|
throw e;
|
|
1048
1255
|
}
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1051
|
-
e.errorCode === BleErrorCode.OperationCancelled
|
|
1052
|
-
) {
|
|
1256
|
+
if (isMtuOrCancelledConnectError(e)) {
|
|
1257
|
+
skipPostConnectMtu = true;
|
|
1053
1258
|
Log?.debug('second try to reconnect without params');
|
|
1054
1259
|
try {
|
|
1055
1260
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1056
1261
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1057
1262
|
);
|
|
1058
|
-
} catch (
|
|
1059
|
-
Log?.debug('last try to reconnect error: ',
|
|
1263
|
+
} catch (fallbackError) {
|
|
1264
|
+
Log?.debug('last try to reconnect error: ', fallbackError);
|
|
1060
1265
|
// last try to reconnect device if this issue exists
|
|
1061
1266
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
1062
|
-
if (
|
|
1267
|
+
if (fallbackError.errorCode === BleErrorCode.OperationCancelled) {
|
|
1063
1268
|
Log?.debug('last try to reconnect');
|
|
1064
1269
|
await disconnectedDevice.cancelConnection();
|
|
1065
1270
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1066
1271
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1067
1272
|
);
|
|
1273
|
+
} else {
|
|
1274
|
+
remapError(fallbackError);
|
|
1068
1275
|
}
|
|
1069
1276
|
}
|
|
1070
1277
|
} else {
|
|
1071
|
-
remapError(e
|
|
1278
|
+
remapError(e);
|
|
1072
1279
|
}
|
|
1073
1280
|
}
|
|
1074
1281
|
}
|
|
1075
1282
|
|
|
1076
|
-
|
|
1283
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1284
|
+
if (Platform.OS === 'android' && !(await device.isConnected().catch(() => false))) {
|
|
1285
|
+
const disconnectedDevice = device;
|
|
1286
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1287
|
+
await Promise.all([
|
|
1288
|
+
this.runBestEffortNativeOperation('connect failure: cancel manager connection', () =>
|
|
1289
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1290
|
+
),
|
|
1291
|
+
this.runBestEffortNativeOperation('connect failure: cancel device connection', () =>
|
|
1292
|
+
disconnectedDevice.cancelConnection()
|
|
1293
|
+
),
|
|
1294
|
+
]);
|
|
1295
|
+
});
|
|
1296
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1297
|
+
}
|
|
1298
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1299
|
+
let characteristics: ResolvedBleCharacteristics | undefined;
|
|
1300
|
+
if (isAndroid) {
|
|
1301
|
+
if (refreshAndroidGattCache) {
|
|
1302
|
+
// refreshGatt has already started a full rediscovery; let it finish before the MTU
|
|
1303
|
+
// exchange so the request is not queued behind it.
|
|
1304
|
+
characteristics = await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
1305
|
+
if (androidRefreshConnectRan) this.androidGattCacheRefreshes.delete(uuid);
|
|
1306
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1307
|
+
}
|
|
1308
|
+
device = await this.negotiateAndroidMtu(uuid, blePlxManager, device);
|
|
1309
|
+
} else if (!skipPostConnectMtu) {
|
|
1310
|
+
// Match 1.1.31: MTU is a connect() best-effort. If connect already fell back
|
|
1311
|
+
// without requestMTU, do not put another requestMTU on the native serial
|
|
1312
|
+
// queue — that is what wedges GATT after Account#2 reconnect.
|
|
1313
|
+
const mtuResult = await resolveNegotiatedMtu(device, transactionId =>
|
|
1314
|
+
blePlxManager.cancelTransaction(transactionId)
|
|
1315
|
+
);
|
|
1316
|
+
device = mtuResult.device;
|
|
1317
|
+
if (mtuResult.timedOut) {
|
|
1318
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1319
|
+
Log?.debug(
|
|
1320
|
+
'[ReactNativeBleTransport] post-connect MTU timed out, reconnecting without requesting MTU'
|
|
1321
|
+
);
|
|
1322
|
+
const timedOutDevice = device;
|
|
1323
|
+
let mtuTeardownSettled = false;
|
|
1324
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1325
|
+
await this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () =>
|
|
1326
|
+
timedOutDevice.cancelConnection()
|
|
1327
|
+
);
|
|
1328
|
+
mtuTeardownSettled = true;
|
|
1329
|
+
});
|
|
1330
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1331
|
+
if (!mtuTeardownSettled || this.blePlxManager !== blePlxManager) {
|
|
1332
|
+
// The teardown budget expired and the manager that owned timedOutDevice was reset.
|
|
1333
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'BLE MTU cleanup timed out');
|
|
1334
|
+
}
|
|
1335
|
+
try {
|
|
1336
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1337
|
+
timedOutDevice.connect(fallbackConnectOptions)
|
|
1338
|
+
);
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
if (shouldRethrowBleSetupError(error)) throw error;
|
|
1341
|
+
if (
|
|
1342
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.DeviceAlreadyConnected
|
|
1343
|
+
) {
|
|
1344
|
+
// GATT resolution and the protocol probe below still validate the retained link.
|
|
1345
|
+
device = timedOutDevice;
|
|
1346
|
+
} else {
|
|
1347
|
+
remapError(error);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1077
1353
|
const acquiredDevice = device;
|
|
1078
1354
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1079
|
-
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
1355
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice));
|
|
1356
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1080
1357
|
|
|
1081
1358
|
const protocolHint = expectedProtocol
|
|
1082
1359
|
? undefined
|
|
@@ -1138,6 +1415,7 @@ export default class ReactNativeBleTransport {
|
|
|
1138
1415
|
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1139
1416
|
}
|
|
1140
1417
|
);
|
|
1418
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1141
1419
|
const currentTransport = transportCache[uuid];
|
|
1142
1420
|
if (!currentTransport) {
|
|
1143
1421
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
@@ -1145,11 +1423,9 @@ export default class ReactNativeBleTransport {
|
|
|
1145
1423
|
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1146
1424
|
return { uuid, protocolType };
|
|
1147
1425
|
} catch (error) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
await this.releaseUnlocked(uuid, true);
|
|
1152
|
-
}
|
|
1426
|
+
// A failed acquire must retire the physical link before Core retries. Logical
|
|
1427
|
+
// release leaves GATT connected even when neither protocol receives a response.
|
|
1428
|
+
await this.disconnectUnlocked(uuid);
|
|
1153
1429
|
throw error;
|
|
1154
1430
|
} finally {
|
|
1155
1431
|
this.acquiringProtocolV2.delete(uuid);
|
|
@@ -1187,6 +1463,9 @@ export default class ReactNativeBleTransport {
|
|
|
1187
1463
|
this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
|
|
1188
1464
|
return;
|
|
1189
1465
|
}
|
|
1466
|
+
if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
|
|
1467
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
1468
|
+
}
|
|
1190
1469
|
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1191
1470
|
let errorCode:
|
|
1192
1471
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -1196,10 +1475,7 @@ export default class ReactNativeBleTransport {
|
|
|
1196
1475
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1197
1476
|
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
1198
1477
|
} else if (
|
|
1199
|
-
error.reason
|
|
1200
|
-
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
1201
|
-
error.reason?.includes('The handle is invalid') ||
|
|
1202
|
-
error.reason?.includes('Writing is not permitted') ||
|
|
1478
|
+
isStaleGattTableNotifyReason(error.reason) ||
|
|
1203
1479
|
error.reason?.includes('notify change failed for device')
|
|
1204
1480
|
) {
|
|
1205
1481
|
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
@@ -1216,10 +1492,7 @@ export default class ReactNativeBleTransport {
|
|
|
1216
1492
|
ERROR = HardwareErrorCode.BleTimeoutError;
|
|
1217
1493
|
}
|
|
1218
1494
|
if (
|
|
1219
|
-
error.reason
|
|
1220
|
-
error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
1221
|
-
error.reason?.includes('The handle is invalid') ||
|
|
1222
|
-
error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
|
|
1495
|
+
isStaleGattTableNotifyReason(error.reason) ||
|
|
1223
1496
|
error.reason?.includes('notify change failed for device')
|
|
1224
1497
|
) {
|
|
1225
1498
|
const notifyError = ERRORS.TypedError(
|
|
@@ -1649,13 +1922,14 @@ export default class ReactNativeBleTransport {
|
|
|
1649
1922
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1650
1923
|
return check.call(jsonData);
|
|
1651
1924
|
} catch (e) {
|
|
1652
|
-
|
|
1653
|
-
|
|
1925
|
+
const isProbeTimeout =
|
|
1926
|
+
options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS &&
|
|
1927
|
+
(name === 'GetFeatures' || name === 'Initialize');
|
|
1928
|
+
if (isProbeTimeout) {
|
|
1929
|
+
Log?.debug(`[ReactNativeBleTransport] Protocol V1 ${name} probe call failed:`, e);
|
|
1654
1930
|
} else {
|
|
1655
1931
|
Log?.error('call error: ', e);
|
|
1656
1932
|
}
|
|
1657
|
-
const isProbeTimeout =
|
|
1658
|
-
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1659
1933
|
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1660
1934
|
// transport; its late timeout must not tear down the connection the current
|
|
1661
1935
|
// call is actively using.
|
|
@@ -1678,7 +1952,49 @@ export default class ReactNativeBleTransport {
|
|
|
1678
1952
|
}
|
|
1679
1953
|
|
|
1680
1954
|
stop() {
|
|
1955
|
+
if (this.stopPromise) return this.stopPromise;
|
|
1681
1956
|
this.stopped = true;
|
|
1957
|
+
// Bonding precedes GATT, so cancelDeviceConnection cannot end this wait.
|
|
1958
|
+
this.bondAbortController.abort();
|
|
1959
|
+
const deviceIds = new Set([
|
|
1960
|
+
...this.monitorTokens.keys(),
|
|
1961
|
+
...this.sessionProtocols.keys(),
|
|
1962
|
+
...this.lifecycleOperations.keys(),
|
|
1963
|
+
...(this.runPromiseDeviceId ? [this.runPromiseDeviceId] : []),
|
|
1964
|
+
]);
|
|
1965
|
+
const scans = Array.from(this.scanCleanups, cleanup => cleanup());
|
|
1966
|
+
this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
|
|
1967
|
+
this.androidPriorityResetTimers.clear();
|
|
1968
|
+
this.androidHighPriorityDevices.clear();
|
|
1969
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1970
|
+
this.runPromise?.reject(error);
|
|
1971
|
+
this.runPromise = null;
|
|
1972
|
+
this.runPromiseDeviceId = null;
|
|
1973
|
+
deviceIds.forEach(uuid => this.rejectProtocolV2Frames(uuid, error));
|
|
1974
|
+
const manager = this.blePlxManager;
|
|
1975
|
+
// Cancel native setup before waiting for its lifecycle lock. Otherwise stop
|
|
1976
|
+
// waits for the very connect/MTU/GATT operation it needs to interrupt.
|
|
1977
|
+
const pendingConnections = manager
|
|
1978
|
+
? Array.from(this.lifecycleOperations.keys(), uuid =>
|
|
1979
|
+
this.runNativeTeardown(uuid, manager, async () => {
|
|
1980
|
+
await this.runBestEffortNativeOperation('stop: cancel pending device connection', () =>
|
|
1981
|
+
manager.cancelDeviceConnection(uuid)
|
|
1982
|
+
);
|
|
1983
|
+
})
|
|
1984
|
+
)
|
|
1985
|
+
: [];
|
|
1986
|
+
// Release only this transport's endpoints; other connectors may share ble-plx.
|
|
1987
|
+
this.stopPromise = Promise.all([
|
|
1988
|
+
...scans,
|
|
1989
|
+
...pendingConnections,
|
|
1990
|
+
...Array.from(deviceIds, uuid => this.disconnect(uuid)),
|
|
1991
|
+
]).then(async () => {
|
|
1992
|
+
await this.protocolV2Links.invalidateAllLinks('React Native BLE transport stopped');
|
|
1993
|
+
await this.waitForManagerReset();
|
|
1994
|
+
this.blePlxManager = undefined;
|
|
1995
|
+
this.emitter = undefined;
|
|
1996
|
+
});
|
|
1997
|
+
return this.stopPromise;
|
|
1682
1998
|
}
|
|
1683
1999
|
|
|
1684
2000
|
async disconnect(session: string) {
|
|
@@ -1830,17 +2146,119 @@ export default class ReactNativeBleTransport {
|
|
|
1830
2146
|
}
|
|
1831
2147
|
}
|
|
1832
2148
|
|
|
1833
|
-
cancel() {
|
|
2149
|
+
async cancel() {
|
|
1834
2150
|
Log?.debug('transport-react-native transport cancel');
|
|
1835
|
-
|
|
1836
|
-
|
|
2151
|
+
const pending = this.runPromise;
|
|
2152
|
+
const deviceId = this.runPromiseDeviceId;
|
|
2153
|
+
if (pending) {
|
|
2154
|
+
pending.reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled));
|
|
2155
|
+
if (this.runPromise === pending) {
|
|
2156
|
+
this.runPromise = null;
|
|
2157
|
+
this.runPromiseDeviceId = null;
|
|
2158
|
+
}
|
|
2159
|
+
// A V1 read cannot be safely reused after abandoning its response.
|
|
2160
|
+
// Drain native teardown before DeviceCommands releases the operation.
|
|
2161
|
+
if (deviceId) await this.disconnect(deviceId);
|
|
1837
2162
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/**
|
|
2166
|
+
* Android: request the MTU with nothing queued ahead and never close the client while it is
|
|
2167
|
+
* outstanding; a link that times out or stays at MTU 23 is dropped.
|
|
2168
|
+
*/
|
|
2169
|
+
private async negotiateAndroidMtu(
|
|
2170
|
+
uuid: string,
|
|
2171
|
+
manager: BlePlxManager,
|
|
2172
|
+
device: Device
|
|
2173
|
+
): Promise<Device> {
|
|
2174
|
+
if (!shouldRefreshNegotiatedMtu(device.mtu)) return device;
|
|
2175
|
+
|
|
2176
|
+
const startedAt = Date.now();
|
|
2177
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2178
|
+
let timedOut = false;
|
|
2179
|
+
let negotiated = device;
|
|
2180
|
+
let failure: string | undefined;
|
|
2181
|
+
try {
|
|
2182
|
+
negotiated = await Promise.race([
|
|
2183
|
+
device.requestMTU(ANDROID_REQUEST_MTU, `${device.id}:mtu:connected:0:${startedAt}`),
|
|
2184
|
+
new Promise<never>((_, reject) => {
|
|
2185
|
+
timer = setTimeout(() => {
|
|
2186
|
+
timedOut = true;
|
|
2187
|
+
reject(
|
|
2188
|
+
new Error(`BLE MTU exchange timeout after ${ANDROID_MTU_EXCHANGE_TIMEOUT_MS}ms`)
|
|
2189
|
+
);
|
|
2190
|
+
}, ANDROID_MTU_EXCHANGE_TIMEOUT_MS);
|
|
2191
|
+
}),
|
|
2192
|
+
]);
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
2195
|
+
} finally {
|
|
2196
|
+
if (timer) clearTimeout(timer);
|
|
2197
|
+
}
|
|
2198
|
+
Log?.debug(`[ReactNativeBleTransport] BLE MTU exchange ${failure ? 'failed' : 'completed'}`, {
|
|
2199
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2200
|
+
elapsedMs: Date.now() - startedAt,
|
|
2201
|
+
timedOut,
|
|
2202
|
+
actual: negotiated.mtu,
|
|
2203
|
+
error: failure,
|
|
2204
|
+
});
|
|
2205
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2206
|
+
if (!timedOut && !isKnownDefaultMtu(negotiated.mtu)) return negotiated;
|
|
2207
|
+
|
|
2208
|
+
// Counted like a setup timeout so a link that keeps failing reaches the wedged-link guard.
|
|
2209
|
+
const resetManager = this.abandonStalledConnection(
|
|
2210
|
+
uuid,
|
|
2211
|
+
timedOut ? 'mtu-backstop' : 'mtu-default'
|
|
2212
|
+
);
|
|
2213
|
+
await this.dropAndroidLink(
|
|
2214
|
+
uuid,
|
|
2215
|
+
manager,
|
|
2216
|
+
negotiated,
|
|
2217
|
+
timedOut ? 'mtu exchange timeout' : 'default mtu'
|
|
2218
|
+
);
|
|
2219
|
+
if (resetManager) throw this.createWedgedBleSetupError();
|
|
2220
|
+
throw ERRORS.TypedError(
|
|
2221
|
+
HardwareErrorCode.BleConnectedError,
|
|
2222
|
+
timedOut
|
|
2223
|
+
? 'BLE MTU exchange did not complete, reconnecting on a fresh link'
|
|
2224
|
+
: `BLE link stayed at the default MTU ${negotiated.mtu}, reconnecting on a fresh link`
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/** Close the client and wait out the link idle timer so the next connect gets a fresh link. */
|
|
2229
|
+
private async dropAndroidLink(
|
|
2230
|
+
uuid: string,
|
|
2231
|
+
manager: BlePlxManager,
|
|
2232
|
+
device: Device,
|
|
2233
|
+
reason: string
|
|
2234
|
+
) {
|
|
2235
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
2236
|
+
await Promise.all([
|
|
2237
|
+
this.runBestEffortNativeOperation(`${reason}: cancel manager connection`, () =>
|
|
2238
|
+
manager.cancelDeviceConnection(uuid)
|
|
2239
|
+
),
|
|
2240
|
+
this.runBestEffortNativeOperation(`${reason}: cancel device connection`, () =>
|
|
2241
|
+
device.cancelConnection()
|
|
2242
|
+
),
|
|
2243
|
+
]);
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
const startedAt = Date.now();
|
|
2247
|
+
while (!this.stopped && Date.now() - startedAt < ANDROID_LINK_DROP_QUIET_MS) {
|
|
2248
|
+
await delay(ANDROID_LINK_DROP_POLL_MS);
|
|
2249
|
+
}
|
|
2250
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE link drop', {
|
|
2251
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2252
|
+
reason,
|
|
2253
|
+
stopped: this.stopped,
|
|
2254
|
+
});
|
|
1840
2255
|
}
|
|
1841
2256
|
|
|
1842
2257
|
/** Run a native connect under the JS backstop budget. */
|
|
1843
2258
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
2259
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2260
|
+
const startedAt = Date.now();
|
|
2261
|
+
let succeeded = false;
|
|
1844
2262
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1845
2263
|
let timedOut = false;
|
|
1846
2264
|
const pending = connect();
|
|
@@ -1862,6 +2280,7 @@ export default class ReactNativeBleTransport {
|
|
|
1862
2280
|
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1863
2281
|
}),
|
|
1864
2282
|
]);
|
|
2283
|
+
succeeded = true;
|
|
1865
2284
|
return result;
|
|
1866
2285
|
} catch (error) {
|
|
1867
2286
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1876,6 +2295,12 @@ export default class ReactNativeBleTransport {
|
|
|
1876
2295
|
throw error;
|
|
1877
2296
|
} finally {
|
|
1878
2297
|
if (timer) clearTimeout(timer);
|
|
2298
|
+
Log?.debug('[ReactNativeBleTransport] connect completed', {
|
|
2299
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2300
|
+
elapsedMs: Date.now() - startedAt,
|
|
2301
|
+
succeeded,
|
|
2302
|
+
backstopExpired: timedOut,
|
|
2303
|
+
});
|
|
1879
2304
|
}
|
|
1880
2305
|
}
|
|
1881
2306
|
|
|
@@ -1884,6 +2309,8 @@ export default class ReactNativeBleTransport {
|
|
|
1884
2309
|
uuid: string,
|
|
1885
2310
|
device: Device
|
|
1886
2311
|
): Promise<ResolvedBleCharacteristics> {
|
|
2312
|
+
const startedAt = Date.now();
|
|
2313
|
+
let succeeded = false;
|
|
1887
2314
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1888
2315
|
let timedOut = false;
|
|
1889
2316
|
const pending = this.resolveCharacteristics(device);
|
|
@@ -1904,6 +2331,7 @@ export default class ReactNativeBleTransport {
|
|
|
1904
2331
|
}),
|
|
1905
2332
|
]);
|
|
1906
2333
|
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
2334
|
+
succeeded = true;
|
|
1907
2335
|
return result;
|
|
1908
2336
|
} catch (error) {
|
|
1909
2337
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1915,9 +2343,18 @@ export default class ReactNativeBleTransport {
|
|
|
1915
2343
|
throw this.createWedgedBleSetupError();
|
|
1916
2344
|
}
|
|
1917
2345
|
}
|
|
2346
|
+
if (Platform.OS === 'android' && isMissingGattShapeError(error)) {
|
|
2347
|
+
this.androidGattCacheRefreshes.add(uuid);
|
|
2348
|
+
}
|
|
1918
2349
|
throw error;
|
|
1919
2350
|
} finally {
|
|
1920
2351
|
if (timer) clearTimeout(timer);
|
|
2352
|
+
Log?.debug('[ReactNativeBleTransport] GATT setup completed', {
|
|
2353
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2354
|
+
elapsedMs: Date.now() - startedAt,
|
|
2355
|
+
succeeded,
|
|
2356
|
+
backstopExpired: timedOut,
|
|
2357
|
+
});
|
|
1921
2358
|
}
|
|
1922
2359
|
}
|
|
1923
2360
|
|
|
@@ -1929,7 +2366,13 @@ export default class ReactNativeBleTransport {
|
|
|
1929
2366
|
*/
|
|
1930
2367
|
private abandonStalledConnection(
|
|
1931
2368
|
uuid: string,
|
|
1932
|
-
stage:
|
|
2369
|
+
stage:
|
|
2370
|
+
| 'connect-backstop'
|
|
2371
|
+
| 'connect-native'
|
|
2372
|
+
| 'gatt-backstop'
|
|
2373
|
+
| 'gatt-native'
|
|
2374
|
+
| 'mtu-backstop'
|
|
2375
|
+
| 'mtu-default'
|
|
1933
2376
|
): boolean {
|
|
1934
2377
|
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1935
2378
|
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
@@ -2061,6 +2504,7 @@ export default class ReactNativeBleTransport {
|
|
|
2061
2504
|
}
|
|
2062
2505
|
|
|
2063
2506
|
private resetPlxManager() {
|
|
2507
|
+
if (bleManagerResetPromise) return;
|
|
2064
2508
|
const manager = this.blePlxManager;
|
|
2065
2509
|
this.blePlxManager = undefined;
|
|
2066
2510
|
const reason = 'React Native BLE manager reset';
|
|
@@ -2103,15 +2547,26 @@ export default class ReactNativeBleTransport {
|
|
|
2103
2547
|
// Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
|
|
2104
2548
|
// firmware reconnect after the native BLE manager is recreated.
|
|
2105
2549
|
this.protocolReprobeFailures.clear();
|
|
2550
|
+
this.silentDetections.clear();
|
|
2106
2551
|
this.writeTimeoutCounts.clear();
|
|
2107
2552
|
this.connectionSetupTimeoutCounts.clear();
|
|
2108
2553
|
this.monitorTokens.clear();
|
|
2109
2554
|
this.protocolV2Assemblers.clear();
|
|
2555
|
+
let reset: Promise<void>;
|
|
2110
2556
|
try {
|
|
2111
|
-
manager?.destroy();
|
|
2557
|
+
reset = Promise.resolve(manager?.destroy());
|
|
2112
2558
|
} catch (error) {
|
|
2113
|
-
|
|
2559
|
+
reset = Promise.reject(error);
|
|
2114
2560
|
}
|
|
2561
|
+
bleManagerResetPromise = reset;
|
|
2562
|
+
reset.then(
|
|
2563
|
+
() => {
|
|
2564
|
+
if (bleManagerResetPromise === reset) bleManagerResetPromise = undefined;
|
|
2565
|
+
},
|
|
2566
|
+
error => {
|
|
2567
|
+
Log?.error('[ReactNativeBleTransport] BLE manager destroy failed:', error);
|
|
2568
|
+
}
|
|
2569
|
+
);
|
|
2115
2570
|
}
|
|
2116
2571
|
|
|
2117
2572
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
@@ -2204,6 +2659,8 @@ export default class ReactNativeBleTransport {
|
|
|
2204
2659
|
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2205
2660
|
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
2206
2661
|
|
|
2662
|
+
await this.wakeSilentProtocolV1Device(uuid, probeOrder);
|
|
2663
|
+
|
|
2207
2664
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
2208
2665
|
const protocol = probeOrder[i];
|
|
2209
2666
|
if (i > 0) {
|
|
@@ -2225,6 +2682,7 @@ export default class ReactNativeBleTransport {
|
|
|
2225
2682
|
this.confirmedProtocolV2.add(uuid);
|
|
2226
2683
|
}
|
|
2227
2684
|
this.protocolReprobeFailures.delete(uuid);
|
|
2685
|
+
this.silentDetections.delete(uuid);
|
|
2228
2686
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2229
2687
|
deviceId: uuid,
|
|
2230
2688
|
protocol,
|
|
@@ -2234,6 +2692,9 @@ export default class ReactNativeBleTransport {
|
|
|
2234
2692
|
}
|
|
2235
2693
|
}
|
|
2236
2694
|
|
|
2695
|
+
// Arms the wake for the next detection.
|
|
2696
|
+
if (!this.silentDetections.has(uuid)) this.silentDetections.set(uuid, 'silent');
|
|
2697
|
+
|
|
2237
2698
|
if (trustSessionProtocol) {
|
|
2238
2699
|
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2239
2700
|
// shortcut so a device that genuinely switched protocols is found again.
|
|
@@ -2247,6 +2708,34 @@ export default class ReactNativeBleTransport {
|
|
|
2247
2708
|
throw this.createProtocolDetectionError();
|
|
2248
2709
|
}
|
|
2249
2710
|
|
|
2711
|
+
/**
|
|
2712
|
+
* A sleeping Classic drops GetFeatures/Ping and only leaves its screensaver on Initialize, which
|
|
2713
|
+
* resets the wallet session, so it is sent once after a fully silent detection. The firmware does
|
|
2714
|
+
* not reliably answer it, so its timeout must not drop the link. It only runs when V1 is probed
|
|
2715
|
+
* first, so a late reply lands on the V1 probe rather than on a V2 one.
|
|
2716
|
+
*/
|
|
2717
|
+
private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
|
|
2718
|
+
if (
|
|
2719
|
+
Platform.OS !== 'android' ||
|
|
2720
|
+
probeOrder[0] !== 'V1' ||
|
|
2721
|
+
this.silentDetections.get(uuid) !== 'silent'
|
|
2722
|
+
) {
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
this.silentDetections.set(uuid, 'woken');
|
|
2726
|
+
Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
|
|
2727
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2728
|
+
});
|
|
2729
|
+
try {
|
|
2730
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2731
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
if (shouldRethrowProtocolProbeError(error)) throw error;
|
|
2734
|
+
} finally {
|
|
2735
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2250
2739
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
2251
2740
|
const transport = transportCache[uuid];
|
|
2252
2741
|
await this.protocolV2Links.invalidateLink(
|
|
@@ -2314,9 +2803,7 @@ export default class ReactNativeBleTransport {
|
|
|
2314
2803
|
} catch (error) {
|
|
2315
2804
|
this.clearProbeProtocol(uuid, 'V1');
|
|
2316
2805
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2317
|
-
|
|
2318
|
-
// would only fail against a torn-down transport: surface the real cause.
|
|
2319
|
-
if (isWedgedWriteError(error)) {
|
|
2806
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2320
2807
|
throw error;
|
|
2321
2808
|
}
|
|
2322
2809
|
return false;
|
|
@@ -2341,7 +2828,7 @@ export default class ReactNativeBleTransport {
|
|
|
2341
2828
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2342
2829
|
this.resetProtocolV2Frames(uuid);
|
|
2343
2830
|
},
|
|
2344
|
-
shouldRethrow:
|
|
2831
|
+
shouldRethrow: shouldRethrowProtocolProbeError,
|
|
2345
2832
|
});
|
|
2346
2833
|
if (!detected) {
|
|
2347
2834
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -2488,6 +2975,9 @@ export default class ReactNativeBleTransport {
|
|
|
2488
2975
|
this.rememberStaleBondError(uuid, bondError);
|
|
2489
2976
|
throw bondError;
|
|
2490
2977
|
}
|
|
2978
|
+
if (isNativeBleDisconnectError(error)) {
|
|
2979
|
+
throw toBleDisconnectHardwareError(error);
|
|
2980
|
+
}
|
|
2491
2981
|
if (
|
|
2492
2982
|
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2493
2983
|
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
@@ -2548,6 +3038,7 @@ export default class ReactNativeBleTransport {
|
|
|
2548
3038
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
2549
3039
|
}
|
|
2550
3040
|
|
|
3041
|
+
const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
|
|
2551
3042
|
const callOptions = options;
|
|
2552
3043
|
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
2553
3044
|
|
|
@@ -2599,6 +3090,19 @@ export default class ReactNativeBleTransport {
|
|
|
2599
3090
|
);
|
|
2600
3091
|
} catch (e) {
|
|
2601
3092
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
3093
|
+
if (
|
|
3094
|
+
!isProtocolProbe &&
|
|
3095
|
+
e?.errorCode === HardwareErrorCode.BleTimeoutError &&
|
|
3096
|
+
!this.monitorTokens.has(uuid)
|
|
3097
|
+
) {
|
|
3098
|
+
// The failed link has finished invalidating. Disconnect outside that
|
|
3099
|
+
// callback to avoid waiting on its own invalidation or acquire lock.
|
|
3100
|
+
await this.runLifecycleOperation(uuid, async () => {
|
|
3101
|
+
// A queued timeout leaves its active monitor intact; a newer acquire
|
|
3102
|
+
// may also have installed one while cleanup waited for the lifecycle lock.
|
|
3103
|
+
if (!this.monitorTokens.has(uuid)) await this.disconnectUnlocked(uuid);
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
2602
3106
|
throw e;
|
|
2603
3107
|
} finally {
|
|
2604
3108
|
if (highThroughputWrite) {
|
|
@@ -2611,7 +3115,12 @@ export default class ReactNativeBleTransport {
|
|
|
2611
3115
|
const transport = this.getCachedTransport(uuid);
|
|
2612
3116
|
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2613
3117
|
|
|
2614
|
-
const refreshedDevice = await requestNegotiatedMtu(
|
|
3118
|
+
const { device: refreshedDevice } = await requestNegotiatedMtu(
|
|
3119
|
+
transport.device,
|
|
3120
|
+
'highThroughput',
|
|
3121
|
+
1,
|
|
3122
|
+
transactionId => this.blePlxManager?.cancelTransaction(transactionId)
|
|
3123
|
+
);
|
|
2615
3124
|
transport.device = refreshedDevice;
|
|
2616
3125
|
transport.mtuSize =
|
|
2617
3126
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|