@onekeyfe/hd-transport-react-native 1.2.3-alpha.1 → 1.2.3-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -46,6 +46,76 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
46
46
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
47
47
  };
48
48
 
49
+ const lastKeyMissingAt = new Map();
50
+ const listeners$1 = new Set();
51
+ let unsubscribeNative$1;
52
+ const normalizeDeviceId$1 = (deviceId) => deviceId.toLowerCase();
53
+ const getCapableBleUtils$1 = () => {
54
+ if (reactNative.Platform.OS !== 'android')
55
+ return undefined;
56
+ const bleUtils = BleUtils__default["default"];
57
+ if (typeof bleUtils.supportsDeviceKeyMissing !== 'function' ||
58
+ typeof bleUtils.onDeviceKeyMissing !== 'function') {
59
+ return undefined;
60
+ }
61
+ return bleUtils.supportsDeviceKeyMissing() ? bleUtils : undefined;
62
+ };
63
+ const isBleKeyMissingSupported = () => getCapableBleUtils$1() !== undefined;
64
+ const startBleKeyMissingTracking = () => {
65
+ if (unsubscribeNative$1)
66
+ return true;
67
+ const bleUtils = getCapableBleUtils$1();
68
+ if (!(bleUtils === null || bleUtils === void 0 ? void 0 : bleUtils.onDeviceKeyMissing))
69
+ return false;
70
+ unsubscribeNative$1 = bleUtils.onDeviceKeyMissing(event => {
71
+ if (typeof (event === null || event === void 0 ? void 0 : event.id) !== 'string')
72
+ return;
73
+ const deviceId = normalizeDeviceId$1(event.id);
74
+ lastKeyMissingAt.set(deviceId, Date.now());
75
+ listeners$1.forEach(listener => listener(deviceId));
76
+ });
77
+ return true;
78
+ };
79
+ const stopBleKeyMissingTracking = () => {
80
+ unsubscribeNative$1 === null || unsubscribeNative$1 === void 0 ? void 0 : unsubscribeNative$1();
81
+ unsubscribeNative$1 = undefined;
82
+ lastKeyMissingAt.clear();
83
+ listeners$1.clear();
84
+ };
85
+ const hasBleKeyMissingSince = (deviceId, sinceMs) => {
86
+ const at = lastKeyMissingAt.get(normalizeDeviceId$1(deviceId));
87
+ return at !== undefined && at >= sinceMs;
88
+ };
89
+ const onBleKeyMissing = (deviceId, callback) => {
90
+ const target = normalizeDeviceId$1(deviceId);
91
+ const listener = id => {
92
+ if (id === target)
93
+ callback();
94
+ };
95
+ listeners$1.add(listener);
96
+ return () => {
97
+ listeners$1.delete(listener);
98
+ };
99
+ };
100
+ const waitForBleKeyMissing = (deviceId, sinceMs, timeoutMs) => {
101
+ if (!startBleKeyMissingTracking())
102
+ return Promise.resolve(false);
103
+ if (hasBleKeyMissingSince(deviceId, sinceMs))
104
+ return Promise.resolve(true);
105
+ return new Promise(resolve => {
106
+ const settle = (found) => {
107
+ clearTimeout(timer);
108
+ cleanup();
109
+ resolve(found);
110
+ };
111
+ const cleanup = onBleKeyMissing(deviceId, () => {
112
+ if (hasBleKeyMissingSince(deviceId, sinceMs))
113
+ settle(true);
114
+ });
115
+ const timer = setTimeout(() => settle(false), timeoutMs);
116
+ });
117
+ };
118
+
49
119
  let activeLogger;
50
120
  const setBleLogger = (logger) => {
51
121
  activeLogger = logger;
@@ -57,20 +127,71 @@ const bleLogger = {
57
127
  };
58
128
 
59
129
  const Logger = bleLogger;
130
+ const bondFailureReasons = {
131
+ 1: 'authentication_failed',
132
+ 2: 'rejected',
133
+ 3: 'canceled',
134
+ 4: 'device_unreachable',
135
+ 5: 'discovery_in_progress',
136
+ 6: 'timeout',
137
+ 7: 'repeated_attempts',
138
+ 8: 'remote_canceled',
139
+ 9: 'removed',
140
+ };
60
141
  const getConnectedDeviceIds = (serviceUuids) => BleUtils__default["default"].getConnectedPeripherals(serviceUuids);
61
142
  const pairDevice = (macAddress) => BleUtils__default["default"].pairDevice(macAddress);
62
- const onDeviceBondState = (bleMacAddress) => new Promise((resolve, reject) => {
143
+ const SYSTEM_BONDING_RESTART_WINDOW_MS = 1500;
144
+ const createBondFailureError = (bondState) => {
145
+ var _a;
146
+ const nativeReason = 'reason' in bondState && typeof bondState.reason === 'number' ? bondState.reason : undefined;
147
+ const reason = nativeReason === undefined ? 'unknown' : (_a = bondFailureReasons[nativeReason]) !== null && _a !== void 0 ? _a : 'unknown';
148
+ const params = Object.assign({ phase: 'bond', reason }, (nativeReason === undefined ? {} : { nativeReason }));
149
+ if (reason === 'timeout') {
150
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing timed out', params);
151
+ }
152
+ if (reason === 'canceled') {
153
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceBondedCanceled, 'Bluetooth pairing canceled', params);
154
+ }
155
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing failed', params);
156
+ };
157
+ const onDeviceBondState = (bleMacAddress, signal, options) => new Promise((resolve, reject) => {
158
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
159
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
160
+ return;
161
+ }
162
+ let pendingFailure;
63
163
  const cleanup = () => {
64
164
  if (timeout) {
65
165
  clearTimeout(timeout);
66
166
  }
167
+ if (pendingFailure)
168
+ clearTimeout(pendingFailure);
67
169
  if (cleanupListener)
68
170
  cleanupListener();
171
+ cleanupKeyMissing === null || cleanupKeyMissing === void 0 ? void 0 : cleanupKeyMissing();
172
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
69
173
  };
174
+ const onAbort = () => {
175
+ cleanup();
176
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
177
+ };
178
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
70
179
  const timeout = setTimeout(() => {
71
180
  cleanup();
72
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded'));
181
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing timed out', {
182
+ phase: 'bond',
183
+ reason: 'timeout',
184
+ }));
73
185
  }, 60 * 1000);
186
+ const cleanupKeyMissing = startBleKeyMissingTracking()
187
+ ? onBleKeyMissing(bleMacAddress, () => {
188
+ cleanup();
189
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, undefined, {
190
+ phase: 'bond',
191
+ reason: 'key_missing',
192
+ }));
193
+ })
194
+ : undefined;
74
195
  const cleanupListener = BleUtils__default["default"].onDeviceBondState(peripheral => {
75
196
  var _a;
76
197
  if (((_a = peripheral.id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== bleMacAddress.toLowerCase()) {
@@ -78,15 +199,28 @@ const onDeviceBondState = (bleMacAddress) => new Promise((resolve, reject) => {
78
199
  }
79
200
  const { bondState } = peripheral;
80
201
  const hasBonded = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_BONDED';
81
- const hasCanceled = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_NONE';
202
+ const hasFailed = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_NONE';
203
+ const hasRestarted = bondState.preState === 'BOND_NONE' && bondState.state === 'BOND_BONDING';
82
204
  Logger.debug('onDeviceBondState bondState:', bondState);
83
205
  if (hasBonded) {
84
206
  cleanup();
85
207
  resolve(peripheral);
86
208
  }
87
- else if (hasCanceled) {
209
+ else if (hasRestarted && pendingFailure) {
210
+ clearTimeout(pendingFailure);
211
+ pendingFailure = undefined;
212
+ }
213
+ else if (hasFailed && (options === null || options === void 0 ? void 0 : options.systemInitiated)) {
214
+ if (pendingFailure)
215
+ clearTimeout(pendingFailure);
216
+ pendingFailure = setTimeout(() => {
217
+ cleanup();
218
+ reject(createBondFailureError(bondState));
219
+ }, SYSTEM_BONDING_RESTART_WINDOW_MS);
220
+ }
221
+ else if (hasFailed) {
88
222
  cleanup();
89
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceBondedCanceled, 'bonding canceled'));
223
+ reject(createBondFailureError(bondState));
90
224
  }
91
225
  });
92
226
  });
@@ -194,6 +328,193 @@ const subscribeBleOn = (bleManager, ms = 1000) => new Promise((resolve, reject)
194
328
  }, ms);
195
329
  });
196
330
 
331
+ const HCI_PIN_OR_KEY_MISSING = 0x06;
332
+ const lastEncryption = new Map();
333
+ const lastAclDisconnectAt = new Map();
334
+ const listeners = new Set();
335
+ let unsubscribeNative;
336
+ const normalizeDeviceId = (deviceId) => deviceId.toLowerCase();
337
+ const isEncrypted = (record) => record.status === 0 && record.enabled;
338
+ const getCapableBleUtils = () => {
339
+ if (reactNative.Platform.OS !== 'android')
340
+ return undefined;
341
+ const bleUtils = BleUtils__default["default"];
342
+ if (typeof bleUtils.supportsDeviceEncryptionChange !== 'function' ||
343
+ typeof bleUtils.onDeviceEncryptionChange !== 'function' ||
344
+ typeof bleUtils.onDeviceAclDisconnected !== 'function') {
345
+ return undefined;
346
+ }
347
+ return bleUtils.supportsDeviceEncryptionChange() ? bleUtils : undefined;
348
+ };
349
+ const notify = (deviceId) => {
350
+ listeners.forEach(listener => listener(deviceId));
351
+ };
352
+ const startBleEncryptionTracking = () => {
353
+ if (unsubscribeNative)
354
+ return true;
355
+ const bleUtils = getCapableBleUtils();
356
+ if (!(bleUtils === null || bleUtils === void 0 ? void 0 : bleUtils.onDeviceEncryptionChange) || !bleUtils.onDeviceAclDisconnected)
357
+ return false;
358
+ unsubscribeNative = [
359
+ bleUtils.onDeviceEncryptionChange(event => {
360
+ if (typeof (event === null || event === void 0 ? void 0 : event.id) !== 'string')
361
+ return;
362
+ const deviceId = normalizeDeviceId(event.id);
363
+ lastEncryption.set(deviceId, {
364
+ at: Date.now(),
365
+ status: Number(event.status),
366
+ enabled: event.enabled === true,
367
+ });
368
+ notify(deviceId);
369
+ }),
370
+ bleUtils.onDeviceAclDisconnected(event => {
371
+ if (typeof (event === null || event === void 0 ? void 0 : event.id) !== 'string')
372
+ return;
373
+ const deviceId = normalizeDeviceId(event.id);
374
+ lastAclDisconnectAt.set(deviceId, Date.now());
375
+ notify(deviceId);
376
+ }),
377
+ ];
378
+ return true;
379
+ };
380
+ const stopBleEncryptionTracking = () => {
381
+ unsubscribeNative === null || unsubscribeNative === void 0 ? void 0 : unsubscribeNative.forEach(unsubscribe => unsubscribe());
382
+ unsubscribeNative = undefined;
383
+ lastEncryption.clear();
384
+ lastAclDisconnectAt.clear();
385
+ listeners.clear();
386
+ };
387
+ const markBleLinkEncrypted = (deviceId) => {
388
+ if (!unsubscribeNative)
389
+ return;
390
+ lastEncryption.set(normalizeDeviceId(deviceId), { at: Date.now(), status: 0, enabled: true });
391
+ };
392
+ const waitForAndroidLinkEncryption = ({ deviceId, linkStartedAt, resultTimeoutMs, repairTimeoutMs, keyMissingGraceMs, signal, onRepairStarted, }) => {
393
+ if (!startBleEncryptionTracking())
394
+ return Promise.resolve('unresolved');
395
+ startBleKeyMissingTracking();
396
+ const target = normalizeDeviceId(deviceId);
397
+ const previous = lastEncryption.get(target);
398
+ const previousDropAt = lastAclDisconnectAt.get(target);
399
+ if (previous &&
400
+ previous.at < linkStartedAt &&
401
+ isEncrypted(previous) &&
402
+ (previousDropAt === undefined || previousDropAt < previous.at)) {
403
+ return Promise.resolve('already-encrypted');
404
+ }
405
+ return new Promise((resolve, reject) => {
406
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
407
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
408
+ return;
409
+ }
410
+ let repairStartedAt;
411
+ let droppedDuringRepair = false;
412
+ let timer;
413
+ let settled = false;
414
+ const cleanup = () => {
415
+ settled = true;
416
+ if (timer)
417
+ clearTimeout(timer);
418
+ listeners.delete(onLinkEvent);
419
+ cleanupKeyMissing();
420
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
421
+ };
422
+ const finish = (result) => {
423
+ cleanup();
424
+ resolve(result);
425
+ };
426
+ const fail = (error) => {
427
+ cleanup();
428
+ reject(error);
429
+ };
430
+ const arm = (ms, onExpire) => {
431
+ if (timer)
432
+ clearTimeout(timer);
433
+ timer = setTimeout(onExpire, ms);
434
+ };
435
+ const bondInvalid = () => hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, undefined, {
436
+ phase: 'connect',
437
+ reason: 'key_missing',
438
+ });
439
+ const evaluate = () => {
440
+ if (settled)
441
+ return;
442
+ const record = lastEncryption.get(target);
443
+ const dropAt = lastAclDisconnectAt.get(target);
444
+ if (repairStartedAt === undefined) {
445
+ if (hasBleKeyMissingSince(target, linkStartedAt)) {
446
+ fail(bondInvalid());
447
+ return;
448
+ }
449
+ if (dropAt !== undefined && dropAt >= linkStartedAt) {
450
+ finish('unresolved');
451
+ return;
452
+ }
453
+ if (!record || record.at < linkStartedAt)
454
+ return;
455
+ if (isEncrypted(record)) {
456
+ finish('encrypted');
457
+ return;
458
+ }
459
+ if (record.status !== HCI_PIN_OR_KEY_MISSING) {
460
+ finish('unresolved');
461
+ return;
462
+ }
463
+ repairStartedAt = record.at;
464
+ onRepairStarted === null || onRepairStarted === void 0 ? void 0 : onRepairStarted();
465
+ arm(repairTimeoutMs, () => fail(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing timed out', {
466
+ phase: 'bond',
467
+ reason: 'timeout',
468
+ })));
469
+ return;
470
+ }
471
+ if (hasBleKeyMissingSince(target, repairStartedAt)) {
472
+ fail(bondInvalid());
473
+ return;
474
+ }
475
+ if (record && record.at > repairStartedAt && isEncrypted(record)) {
476
+ finish('re-paired');
477
+ return;
478
+ }
479
+ if (!droppedDuringRepair && dropAt !== undefined && dropAt >= repairStartedAt) {
480
+ droppedDuringRepair = true;
481
+ arm(keyMissingGraceMs, () => fail(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected)));
482
+ }
483
+ };
484
+ const onLinkEvent = id => {
485
+ if (id === target)
486
+ evaluate();
487
+ };
488
+ const onAbort = () => fail(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
489
+ listeners.add(onLinkEvent);
490
+ const cleanupKeyMissing = onBleKeyMissing(target, evaluate);
491
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
492
+ arm(resultTimeoutMs, () => finish('unresolved'));
493
+ evaluate();
494
+ });
495
+ };
496
+
497
+ const BLE_PLX_DEVICE_DISCONNECTED = 201;
498
+ const IOS_PERIPHERAL_DISCONNECTED = 7;
499
+ const nativeErrorText$1 = (error) => [error.reason, error.message]
500
+ .filter((value) => typeof value === 'string')
501
+ .join(' ');
502
+ const isNativeBleDisconnectError = (error) => {
503
+ if (!error || typeof error !== 'object') {
504
+ return false;
505
+ }
506
+ const nativeError = error;
507
+ return (nativeError.errorCode === BLE_PLX_DEVICE_DISCONNECTED ||
508
+ nativeError.iosErrorCode === IOS_PERIPHERAL_DISCONNECTED);
509
+ };
510
+ const toBleDisconnectHardwareError = (error) => {
511
+ if ((error === null || error === void 0 ? void 0 : error.errorCode) === hdShared.HardwareErrorCode.BleDeviceDisconnected) {
512
+ return error;
513
+ }
514
+ const nativeError = (error !== null && error !== void 0 ? error : {});
515
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected, nativeErrorText$1(nativeError) || undefined);
516
+ };
517
+
197
518
  const ATT_INSUFFICIENT_AUTHENTICATION = 5;
198
519
  const ATT_INSUFFICIENT_ENCRYPTION = 15;
199
520
  const IOS_PEER_REMOVED_PAIRING_INFORMATION = 14;
@@ -258,6 +579,7 @@ class BleTransport {
258
579
  const { check, ProtocolV1, parseConfigure } = transport__default["default"];
259
580
  const Log = bleLogger;
260
581
  const transportCache = {};
582
+ let bleManagerResetPromise;
261
583
  const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = reactNative.Platform.OS === 'ios' ? 4 : 5;
262
584
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = reactNative.Platform.OS === 'ios' ? 8 : 10;
263
585
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = reactNative.Platform.OS === 'ios' ? 24 : 30;
@@ -316,6 +638,29 @@ const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
316
638
  const isWedgedWriteError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === hdShared.HardwareErrorCode.BleWriteCharacteristicError &&
317
639
  typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' &&
318
640
  error.message.startsWith(WEDGED_WRITE_MESSAGE);
641
+ const shouldRethrowProtocolProbeError = (error) => {
642
+ const code = error === null || error === void 0 ? void 0 : error.errorCode;
643
+ return (hdShared.isBleStaleBondHardwareError(error) ||
644
+ isNativeBleDisconnectError(error) ||
645
+ code === hdShared.HardwareErrorCode.BleDeviceNotBonded ||
646
+ code === hdShared.HardwareErrorCode.BleDeviceBondedCanceled ||
647
+ code === hdShared.HardwareErrorCode.BleDeviceDisconnected ||
648
+ code === hdShared.HardwareErrorCode.BleCharacteristicNotifyError ||
649
+ code === hdShared.HardwareErrorCode.BleCharacteristicNotifyChangeFailure ||
650
+ code === hdShared.HardwareErrorCode.BleWriteCharacteristicError);
651
+ };
652
+ const ANDROID_KEY_MISSING_LINK_WINDOW_MS = 10000;
653
+ const ANDROID_KEY_MISSING_GRACE_MS = 500;
654
+ const ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS = 1500;
655
+ const ANDROID_SYSTEM_REPAIR_TIMEOUT_MS = 35000;
656
+ const isAndroidLinkLossError = (error) => {
657
+ const code = error === null || error === void 0 ? void 0 : error.errorCode;
658
+ return (isNativeBleDisconnectError(error) ||
659
+ code === hdShared.HardwareErrorCode.BleDeviceNotBonded ||
660
+ code === hdShared.HardwareErrorCode.BleDeviceDisconnected ||
661
+ code === hdShared.HardwareErrorCode.BleCharacteristicNotifyError ||
662
+ code === hdShared.HardwareErrorCode.BleConnectedError);
663
+ };
319
664
  const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
320
665
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
321
666
  const IOS_NOTIFY_READY_DELAY_MS = 150;
@@ -350,10 +695,10 @@ function getDeviceDisplayName(device) {
350
695
  }
351
696
  const IOS_REQUEST_MTU = 247;
352
697
  const ANDROID_REQUEST_MTU = 517;
353
- const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
354
698
  const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
355
699
  const getRequestedBleMtu = () => reactNative.Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
356
700
  const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
701
+ const BLE_MTU_REQUEST_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS;
357
702
  const connectOptions = {
358
703
  requestMTU: getRequestedBleMtu(),
359
704
  timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
@@ -362,6 +707,29 @@ const connectOptions = {
362
707
  const fallbackConnectOptions = {
363
708
  timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
364
709
  };
710
+ const androidRefreshGattConnectOptions = {
711
+ timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
712
+ refreshGatt: 'OnConnected',
713
+ };
714
+ const ANDROID_MTU_EXCHANGE_TIMEOUT_MS = 12000;
715
+ const ANDROID_LINK_DROP_QUIET_MS = 5000;
716
+ const ANDROID_LINK_DROP_POLL_MS = 250;
717
+ const isKnownDefaultMtu = (mtu) => typeof mtu === 'number' && Number.isFinite(mtu) && mtu <= 23;
718
+ const isMissingGattShapeError = (error) => {
719
+ const code = error === null || error === void 0 ? void 0 : error.errorCode;
720
+ const message = error === null || error === void 0 ? void 0 : error.message;
721
+ return (code === hdShared.HardwareErrorCode.BleServiceNotFound ||
722
+ code === hdShared.HardwareErrorCode.BleCharacteristicNotFound ||
723
+ (typeof message === 'string' &&
724
+ (message.includes('BLECharacteristicNotFound') ||
725
+ message.includes('BLECharacteristicNotWritable') ||
726
+ message.includes('BLECharacteristicNotNotifiable'))));
727
+ };
728
+ const isStaleGattTableNotifyReason = (reason) => !!reason &&
729
+ (reason.includes('Cannot write client characteristic config descriptor') ||
730
+ reason.includes('Cannot find client characteristic config descriptor') ||
731
+ reason.includes('The handle is invalid') ||
732
+ reason.includes('Writing is not permitted'));
365
733
  const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
366
734
  const BLE_GATT_SETUP_TIMEOUT_MS = 10000;
367
735
  const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
@@ -376,6 +744,11 @@ const isWedgedBleSetupError = (error) => (error === null || error === void 0 ? v
376
744
  error.message.startsWith(BLE_SETUP_WEDGED_MESSAGE);
377
745
  const shouldRethrowBleSetupError = (error) => isConnectTimeoutError(error) || isWedgedBleSetupError(error);
378
746
  const isNativeOperationTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.OperationTimedOut;
747
+ const isMtuOrCancelledConnectError = (error) => {
748
+ const errorCode = error === null || error === void 0 ? void 0 : error.errorCode;
749
+ return (errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
750
+ errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled);
751
+ };
379
752
  const tryToGetConfiguration = (device) => {
380
753
  if (!device || !device.serviceUUIDs)
381
754
  return null;
@@ -387,25 +760,65 @@ const tryToGetConfiguration = (device) => {
387
760
  return null;
388
761
  return infos;
389
762
  };
390
- const requestNegotiatedMtu = (device, stage, attempt) => __awaiter(void 0, void 0, void 0, function* () {
763
+ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __awaiter(void 0, void 0, void 0, function* () {
391
764
  if (reactNative.Platform.OS !== 'ios' && reactNative.Platform.OS !== 'android')
392
- return device;
765
+ return { device, timedOut: false };
766
+ const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
767
+ let timeoutId;
768
+ let timedOut = false;
393
769
  try {
394
- const mtuDevice = yield device.requestMTU(getRequestedBleMtu());
395
- return mtuDevice;
770
+ const request = device.requestMTU(getRequestedBleMtu(), transactionId);
771
+ request.catch(() => undefined);
772
+ const mtuDevice = yield Promise.race([
773
+ request,
774
+ new Promise((_, reject) => {
775
+ timeoutId = setTimeout(() => {
776
+ timedOut = true;
777
+ reject(new Error(`BLE MTU request timeout after ${BLE_MTU_REQUEST_TIMEOUT_MS}ms`));
778
+ }, BLE_MTU_REQUEST_TIMEOUT_MS);
779
+ }),
780
+ ]);
781
+ return { device: mtuDevice, timedOut: false };
396
782
  }
397
783
  catch (error) {
784
+ if (timedOut && cancelTransaction) {
785
+ try {
786
+ Promise.resolve(cancelTransaction(transactionId)).catch(cancelError => {
787
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] MTU cancellation failed', {
788
+ platform: reactNative.Platform.OS,
789
+ stage,
790
+ attempt,
791
+ error: cancelError instanceof Error ? cancelError.message : String(cancelError),
792
+ });
793
+ });
794
+ }
795
+ catch (cancelError) {
796
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] MTU cancellation failed', {
797
+ platform: reactNative.Platform.OS,
798
+ stage,
799
+ attempt,
800
+ error: cancelError instanceof Error ? cancelError.message : String(cancelError),
801
+ });
802
+ }
803
+ }
398
804
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
399
805
  platform: reactNative.Platform.OS,
400
806
  stage,
401
807
  attempt,
402
808
  actual: device.mtu,
809
+ timedOut,
403
810
  error: error instanceof Error ? error.message : String(error),
404
811
  });
405
- return device;
812
+ return { device, timedOut };
813
+ }
814
+ finally {
815
+ if (timeoutId)
816
+ clearTimeout(timeoutId);
406
817
  }
407
818
  });
408
- const resolveNegotiatedMtu = (device) => requestNegotiatedMtu(device, 'connected', 0);
819
+ const resolveNegotiatedMtu = (device, cancelTransaction) => shouldRefreshNegotiatedMtu(device.mtu)
820
+ ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
821
+ : Promise.resolve({ device, timedOut: false });
409
822
  function remapError(error) {
410
823
  var _a;
411
824
  if (error instanceof reactNativeBlePlx.BleError) {
@@ -429,6 +842,7 @@ class ReactNativeBleTransport {
429
842
  this.name = 'ReactNativeBleTransport';
430
843
  this.configured = false;
431
844
  this.stopped = false;
845
+ this.bondAbortController = new AbortController();
432
846
  this.scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
433
847
  this.runPromise = null;
434
848
  this.runPromiseDeviceId = null;
@@ -441,7 +855,10 @@ class ReactNativeBleTransport {
441
855
  this.sessionProtocols = new Map();
442
856
  this.confirmedProtocolV2 = new Set();
443
857
  this.protocolReprobeFailures = new Map();
858
+ this.silentDetections = new Map();
859
+ this.androidGattCacheRefreshes = new Set();
444
860
  this.staleBondErrors = new Map();
861
+ this.androidLinkStartedAt = new Map();
445
862
  this.acquiringProtocolV2 = new Set();
446
863
  this.protocolV2Assemblers = new Map();
447
864
  this.protocolV2FrameQueues = new Map();
@@ -458,11 +875,20 @@ class ReactNativeBleTransport {
458
875
  },
459
876
  classifyError: () => 'link-fatal',
460
877
  onLinkInvalidated: (uuid, reason) => __awaiter(this, void 0, void 0, function* () {
461
- var _b;
878
+ var _b, _c;
462
879
  (_b = this.protocolV2Assemblers.get(uuid)) === null || _b === void 0 ? void 0 : _b.reset();
463
880
  this.rejectProtocolV2Frames(uuid, new Error(reason));
464
881
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
465
882
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
883
+ if (this.probingProtocols.get(uuid) !== 'V2') {
884
+ const transport = transportCache[uuid];
885
+ try {
886
+ this.emitDeviceDisconnect(uuid, (_c = transport === null || transport === void 0 ? void 0 : transport.device) === null || _c === void 0 ? void 0 : _c.name, transport === null || transport === void 0 ? void 0 : transport.monitorToken);
887
+ }
888
+ catch (_d) {
889
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] Protocol V2 disconnect listener failed');
890
+ }
891
+ }
466
892
  yield this.releaseNative(uuid, true);
467
893
  }
468
894
  }),
@@ -474,11 +900,16 @@ class ReactNativeBleTransport {
474
900
  this.androidPriorityResetTimers = new Map();
475
901
  this.nextMonitorToken = 1;
476
902
  this.lifecycleOperations = new Map();
903
+ this.scanCleanups = new Set();
477
904
  this.scanTimeout = (_a = options.scanTimeout) !== null && _a !== void 0 ? _a : DEVICE_SCAN_TIMEOUT_MS;
478
905
  }
479
906
  init(logger, emitter) {
480
907
  setBleLogger(logger);
481
908
  this.emitter = emitter;
909
+ if (reactNative.Platform.OS === 'android') {
910
+ startBleKeyMissingTracking();
911
+ startBleEncryptionTracking();
912
+ }
482
913
  }
483
914
  configure(signedData) {
484
915
  const messages = parseConfigure(signedData);
@@ -502,10 +933,38 @@ class ReactNativeBleTransport {
502
933
  listen() {
503
934
  }
504
935
  getPlxManager() {
505
- if (this.blePlxManager)
506
- return Promise.resolve(this.blePlxManager);
507
- this.blePlxManager = new reactNativeBlePlx.BleManager();
508
- return Promise.resolve(this.blePlxManager);
936
+ return __awaiter(this, void 0, void 0, function* () {
937
+ while (bleManagerResetPromise) {
938
+ yield this.waitForManagerReset();
939
+ }
940
+ if (this.stopped)
941
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
942
+ if (!this.blePlxManager)
943
+ this.blePlxManager = new reactNativeBlePlx.BleManager();
944
+ return this.blePlxManager;
945
+ });
946
+ }
947
+ waitForManagerReset() {
948
+ return __awaiter(this, void 0, void 0, function* () {
949
+ if (!bleManagerResetPromise)
950
+ return;
951
+ let timeout;
952
+ try {
953
+ yield Promise.race([
954
+ bleManagerResetPromise,
955
+ new Promise((_, reject) => {
956
+ timeout = setTimeout(() => reject(this.createWedgedBleSetupError()), BLE_CONNECT_TIMEOUT_MS);
957
+ }),
958
+ ]);
959
+ }
960
+ catch (_a) {
961
+ throw this.createWedgedBleSetupError();
962
+ }
963
+ finally {
964
+ if (timeout)
965
+ clearTimeout(timeout);
966
+ }
967
+ });
509
968
  }
510
969
  resolveCharacteristics(device) {
511
970
  return __awaiter(this, void 0, void 0, function* () {
@@ -670,30 +1129,55 @@ class ReactNativeBleTransport {
670
1129
  }
671
1130
  enumerate() {
672
1131
  return __awaiter(this, void 0, void 0, function* () {
673
- return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
674
- const deviceList = [];
675
- const blePlxManager = yield this.getPlxManager();
676
- try {
677
- yield subscribeBleOn(blePlxManager);
678
- }
679
- catch (error) {
680
- Log === null || Log === void 0 ? void 0 : Log.debug('subscribeBleOn error: ', error);
681
- reject(error);
682
- return;
683
- }
684
- if (reactNative.Platform.OS === 'android' && reactNative.Platform.Version >= 31) {
685
- Log === null || Log === void 0 ? void 0 : Log.debug('requesting permissions, please wait...');
686
- const resultConnect = yield reactNative.PermissionsAndroid.requestMultiple([
687
- reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
688
- reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
689
- ]);
690
- Log === null || Log === void 0 ? void 0 : Log.debug('requesting permissions, result: ', resultConnect);
691
- if (resultConnect[reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
692
- resultConnect[reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted') {
693
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
694
- return;
695
- }
1132
+ const scanStartedAt = Date.now();
1133
+ let firstDeviceMs;
1134
+ const blePlxManager = yield this.getPlxManager();
1135
+ yield subscribeBleOn(blePlxManager);
1136
+ if (reactNative.Platform.OS === 'android' && reactNative.Platform.Version >= 31) {
1137
+ Log === null || Log === void 0 ? void 0 : Log.debug('requesting permissions, please wait...');
1138
+ const resultConnect = yield reactNative.PermissionsAndroid.requestMultiple([
1139
+ reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
1140
+ reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
1141
+ ]);
1142
+ Log === null || Log === void 0 ? void 0 : Log.debug('requesting permissions, result: ', resultConnect);
1143
+ if (resultConnect[reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
1144
+ resultConnect[reactNative.PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted') {
1145
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
696
1146
  }
1147
+ }
1148
+ if (this.stopped)
1149
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1150
+ return new Promise((resolve, reject) => {
1151
+ const deviceList = [];
1152
+ let finished = false;
1153
+ let scanCleanup;
1154
+ const finishScan = (error) => {
1155
+ if (scanCleanup)
1156
+ return scanCleanup;
1157
+ finished = true;
1158
+ clearScanTimer();
1159
+ scanCleanup = this.runNativeTeardown('scan', blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
1160
+ yield blePlxManager.stopDeviceScan();
1161
+ })).then(() => {
1162
+ this.scanCleanups.delete(cancelScan);
1163
+ if (error)
1164
+ reject(error);
1165
+ else
1166
+ resolve(deviceList);
1167
+ });
1168
+ return scanCleanup;
1169
+ };
1170
+ const cancelScan = () => finishScan(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
1171
+ this.scanCleanups.add(cancelScan);
1172
+ const clearScanTimer = timer.timeout(() => {
1173
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] scan completed', {
1174
+ elapsedMs: Date.now() - scanStartedAt,
1175
+ firstDeviceMs,
1176
+ deviceCount: deviceList.length,
1177
+ scanWindowMs: this.scanTimeout,
1178
+ });
1179
+ finishScan();
1180
+ }, this.scanTimeout);
697
1181
  blePlxManager.startDeviceScan(getBluetoothServiceUuids(), {
698
1182
  allowDuplicates: true,
699
1183
  scanMode: reactNativeBlePlx.ScanMode.LowLatency,
@@ -702,19 +1186,17 @@ class ReactNativeBleTransport {
702
1186
  if (error) {
703
1187
  Log === null || Log === void 0 ? void 0 : Log.debug('ble scan error: ', error);
704
1188
  if ([reactNativeBlePlx.BleErrorCode.BluetoothPoweredOff, reactNativeBlePlx.BleErrorCode.BluetoothInUnknownState].includes(error.errorCode)) {
705
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
1189
+ finishScan(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError));
706
1190
  }
707
1191
  else if (error.errorCode === reactNativeBlePlx.BleErrorCode.BluetoothUnauthorized) {
708
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationError));
1192
+ finishScan(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationError));
709
1193
  }
710
1194
  else if (error.errorCode === reactNativeBlePlx.BleErrorCode.LocationServicesDisabled) {
711
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationServicesDisabled));
712
- }
713
- else if (error.errorCode === reactNativeBlePlx.BleErrorCode.ScanStartFailed) {
714
- timer.timeout(() => { }, this.scanTimeout);
1195
+ finishScan(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleLocationServicesDisabled));
715
1196
  }
1197
+ else if (error.errorCode === reactNativeBlePlx.BleErrorCode.ScanStartFailed) ;
716
1198
  else {
717
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, (_a = error.reason) !== null && _a !== void 0 ? _a : ''));
1199
+ finishScan(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, (_a = error.reason) !== null && _a !== void 0 ? _a : ''));
718
1200
  }
719
1201
  return;
720
1202
  }
@@ -739,6 +1221,8 @@ class ReactNativeBleTransport {
739
1221
  });
740
1222
  }
741
1223
  });
1224
+ if (finished)
1225
+ return;
742
1226
  getConnectedDeviceIds(reactNative.Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(devices => {
743
1227
  for (const device of devices) {
744
1228
  const localName = 'localName' in device && typeof device.localName === 'string'
@@ -754,10 +1238,11 @@ class ReactNativeBleTransport {
754
1238
  addDevice(device);
755
1239
  }
756
1240
  }
757
- });
1241
+ }, error => Log === null || Log === void 0 ? void 0 : Log.debug('search connected peripheral failed:', error));
758
1242
  const addDevice = (device) => {
759
1243
  var _a;
760
- if (deviceList.every(d => d.id !== device.id)) {
1244
+ if (!finished && deviceList.every(d => d.id !== device.id)) {
1245
+ firstDeviceMs !== null && firstDeviceMs !== void 0 ? firstDeviceMs : (firstDeviceMs = Date.now() - scanStartedAt);
761
1246
  const displayName = (_a = getDeviceDisplayName(device)) !== null && _a !== void 0 ? _a : 'Unknown BLE Device';
762
1247
  deviceList.push(Object.assign(Object.assign({}, device), { name: displayName, commType: 'ble' }));
763
1248
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
@@ -767,16 +1252,14 @@ class ReactNativeBleTransport {
767
1252
  });
768
1253
  }
769
1254
  };
770
- timer.timeout(() => {
771
- blePlxManager.stopDeviceScan();
772
- resolve(deviceList);
773
- }, this.scanTimeout);
774
- }));
1255
+ });
775
1256
  });
776
1257
  }
777
1258
  installTransportForAcquire(uuid, device, characteristics) {
778
1259
  return __awaiter(this, void 0, void 0, function* () {
779
1260
  const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.resolveCharacteristicsWithTimeout(uuid, device));
1261
+ if (this.stopped)
1262
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
780
1263
  const transport$1 = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
781
1264
  transport$1.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
782
1265
  const monitorToken = this.nextMonitorToken;
@@ -797,30 +1280,12 @@ class ReactNativeBleTransport {
797
1280
  else if (reactNative.Platform.OS === 'android') {
798
1281
  yield delay(ANDROID_NOTIFY_READY_DELAY_MS);
799
1282
  }
800
- const initialMtu = transport$1.mtuSize;
801
- let refreshAttempts = 0;
802
- if ((reactNative.Platform.OS === 'ios' || reactNative.Platform.OS === 'android') &&
803
- shouldRefreshNegotiatedMtu(transport$1.mtuSize)) {
804
- refreshAttempts += 1;
805
- let refreshedDevice = yield requestNegotiatedMtu(transport$1.device, 'servicesAndNotifyReady', 1);
806
- transport$1.device = refreshedDevice;
807
- transport$1.mtuSize =
808
- typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport$1.mtuSize;
809
- if (shouldRefreshNegotiatedMtu(transport$1.mtuSize)) {
810
- yield delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
811
- refreshAttempts += 1;
812
- refreshedDevice = yield requestNegotiatedMtu(transport$1.device, 'servicesAndNotifyReady', 2);
813
- transport$1.device = refreshedDevice;
814
- transport$1.mtuSize =
815
- typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport$1.mtuSize;
816
- }
817
- }
1283
+ if (this.stopped)
1284
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
818
1285
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] BLE MTU ready', {
819
1286
  platform: reactNative.Platform.OS,
820
1287
  requested: getRequestedBleMtu(),
821
- initial: initialMtu,
822
1288
  actual: transport$1.mtuSize,
823
- refreshAttempts,
824
1289
  });
825
1290
  return transport$1;
826
1291
  });
@@ -831,12 +1296,21 @@ class ReactNativeBleTransport {
831
1296
  if (!uuid) {
832
1297
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleRequiredUUID);
833
1298
  }
834
- return this.runLifecycleOperation(uuid, () => this.acquireUnlocked(input));
1299
+ return this.runLifecycleOperation(uuid, () => __awaiter(this, void 0, void 0, function* () {
1300
+ try {
1301
+ return yield this.acquireUnlocked(input);
1302
+ }
1303
+ catch (error) {
1304
+ throw yield this.resolveAndroidBondInvalid(uuid, error);
1305
+ }
1306
+ }));
835
1307
  });
836
1308
  }
837
1309
  acquireUnlocked(input) {
838
1310
  var _a;
839
1311
  return __awaiter(this, void 0, void 0, function* () {
1312
+ if (this.stopped)
1313
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
840
1314
  const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
841
1315
  const shouldMapProtocolV2StaleBond = expectedProtocol
842
1316
  ? expectedProtocol === 'V2'
@@ -868,9 +1342,13 @@ class ReactNativeBleTransport {
868
1342
  const isCachedDeviceConnected = yield cachedTransport.device
869
1343
  .isConnected()
870
1344
  .catch(() => false);
1345
+ const isCachedAndroidLinkUsable = reactNative.Platform.OS !== 'android' || !this.androidGattCacheRefreshes.has(uuid);
871
1346
  if (isCachedDeviceConnected &&
1347
+ isCachedAndroidLinkUsable &&
872
1348
  cachedProtocol &&
873
1349
  (!expectedProtocol || cachedProtocol === expectedProtocol)) {
1350
+ if (this.stopped)
1351
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
874
1352
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
875
1353
  return { uuid, protocolType: cachedProtocol };
876
1354
  }
@@ -879,6 +1357,16 @@ class ReactNativeBleTransport {
879
1357
  }
880
1358
  }
881
1359
  let device = null;
1360
+ const isAndroid = reactNative.Platform.OS === 'android';
1361
+ let androidBondedBeforeConnect = false;
1362
+ const refreshAndroidGattCache = isAndroid && (!!skipProtocolProbe || this.androidGattCacheRefreshes.has(uuid));
1363
+ let nativeConnectOptions = connectOptions;
1364
+ if (isAndroid) {
1365
+ nativeConnectOptions = refreshAndroidGattCache
1366
+ ? androidRefreshGattConnectOptions
1367
+ : fallbackConnectOptions;
1368
+ }
1369
+ let androidRefreshConnectRan = false;
882
1370
  if (forceCleanRunPromise && this.runPromise) {
883
1371
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
884
1372
  this.runPromise.reject(error);
@@ -887,6 +1375,7 @@ class ReactNativeBleTransport {
887
1375
  Log === null || Log === void 0 ? void 0 : Log.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
888
1376
  }
889
1377
  const blePlxManager = yield this.getPlxManager();
1378
+ let skipPostConnectMtu = false;
890
1379
  try {
891
1380
  yield subscribeBleOn(blePlxManager);
892
1381
  }
@@ -894,6 +1383,34 @@ class ReactNativeBleTransport {
894
1383
  Log === null || Log === void 0 ? void 0 : Log.debug('subscribeBleOn error: ', error);
895
1384
  throw error;
896
1385
  }
1386
+ if (this.stopped)
1387
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1388
+ if (reactNative.Platform.OS === 'android') {
1389
+ startBleKeyMissingTracking();
1390
+ startBleEncryptionTracking();
1391
+ this.androidLinkStartedAt.delete(uuid);
1392
+ try {
1393
+ const bondState = yield pairDevice(uuid);
1394
+ androidBondedBeforeConnect = !bondState.bonding && bondState.bonded;
1395
+ if (bondState.bonding) {
1396
+ yield onDeviceBondState(uuid, this.bondAbortController.signal, {
1397
+ systemInitiated: bondState.initiated === false,
1398
+ });
1399
+ }
1400
+ else if (!bondState.bonded) {
1401
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
1402
+ }
1403
+ }
1404
+ catch (error) {
1405
+ yield this.runNativeTeardown(uuid, blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
1406
+ yield this.runBestEffortNativeOperation('bond failure: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid));
1407
+ }));
1408
+ throw error;
1409
+ }
1410
+ this.androidLinkStartedAt.set(uuid, Date.now());
1411
+ }
1412
+ if (this.stopped)
1413
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
897
1414
  if (!device) {
898
1415
  const devices = yield blePlxManager.devices([uuid]);
899
1416
  [device] = devices;
@@ -907,15 +1424,16 @@ class ReactNativeBleTransport {
907
1424
  if (!device) {
908
1425
  Log === null || Log === void 0 ? void 0 : Log.debug('try to connect to device: ', uuid);
909
1426
  try {
910
- device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, connectOptions));
1427
+ device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, nativeConnectOptions));
1428
+ androidRefreshConnectRan = refreshAndroidGattCache;
911
1429
  }
912
1430
  catch (e) {
913
1431
  Log === null || Log === void 0 ? void 0 : Log.debug('try to connect to device has error: ', e);
914
1432
  if (shouldRethrowBleSetupError(e)) {
915
1433
  throw e;
916
1434
  }
917
- if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
918
- e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
1435
+ if (isMtuOrCancelledConnectError(e)) {
1436
+ skipPostConnectMtu = true;
919
1437
  Log === null || Log === void 0 ? void 0 : Log.debug('first try to reconnect without params');
920
1438
  device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, fallbackConnectOptions));
921
1439
  }
@@ -931,19 +1449,27 @@ class ReactNativeBleTransport {
931
1449
  if (!device) {
932
1450
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'unable to connect to device');
933
1451
  }
1452
+ if (refreshAndroidGattCache &&
1453
+ !androidRefreshConnectRan &&
1454
+ (yield device.isConnected().catch(() => false))) {
1455
+ yield this.dropAndroidLink(uuid, blePlxManager, device, 'gatt cache refresh');
1456
+ if (this.stopped)
1457
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1458
+ }
934
1459
  if (!(yield device.isConnected())) {
935
1460
  Log === null || Log === void 0 ? void 0 : Log.debug('not connected, try to connect to device: ', uuid);
936
1461
  const disconnectedDevice = device;
937
1462
  try {
938
- device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(connectOptions));
1463
+ device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(nativeConnectOptions));
1464
+ androidRefreshConnectRan = refreshAndroidGattCache;
939
1465
  }
940
1466
  catch (e) {
941
1467
  Log === null || Log === void 0 ? void 0 : Log.debug('not connected, try to connect to device has error: ', e);
942
1468
  if (shouldRethrowBleSetupError(e)) {
943
1469
  throw e;
944
1470
  }
945
- if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
946
- e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
1471
+ if (isMtuOrCancelledConnectError(e)) {
1472
+ skipPostConnectMtu = true;
947
1473
  Log === null || Log === void 0 ? void 0 : Log.debug('second try to reconnect without params');
948
1474
  try {
949
1475
  device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(fallbackConnectOptions));
@@ -965,33 +1491,70 @@ class ReactNativeBleTransport {
965
1491
  }
966
1492
  }
967
1493
  }
968
- if (reactNative.Platform.OS === 'android') {
969
- const connectedDevice = device;
970
- try {
971
- if (!(yield connectedDevice.isConnected().catch(() => false))) {
972
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Device ${uuid} is not connected before bonding`);
1494
+ if (this.stopped)
1495
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1496
+ if (reactNative.Platform.OS === 'android' && !(yield device.isConnected().catch(() => false))) {
1497
+ const disconnectedDevice = device;
1498
+ yield this.runNativeTeardown(uuid, blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
1499
+ yield Promise.all([
1500
+ this.runBestEffortNativeOperation('connect failure: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid)),
1501
+ this.runBestEffortNativeOperation('connect failure: cancel device connection', () => disconnectedDevice.cancelConnection()),
1502
+ ]);
1503
+ }));
1504
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'device is not connected');
1505
+ }
1506
+ if (this.stopped)
1507
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1508
+ let characteristics;
1509
+ if (isAndroid) {
1510
+ if (refreshAndroidGattCache) {
1511
+ characteristics = yield this.resolveCharacteristicsWithTimeout(uuid, device);
1512
+ if (androidRefreshConnectRan)
1513
+ this.androidGattCacheRefreshes.delete(uuid);
1514
+ if (this.stopped)
1515
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1516
+ }
1517
+ device = yield this.negotiateAndroidMtu(uuid, blePlxManager, device);
1518
+ }
1519
+ else if (!skipPostConnectMtu) {
1520
+ const mtuResult = yield resolveNegotiatedMtu(device, transactionId => blePlxManager.cancelTransaction(transactionId));
1521
+ device = mtuResult.device;
1522
+ if (mtuResult.timedOut) {
1523
+ if (this.stopped)
1524
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1525
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] post-connect MTU timed out, reconnecting without requesting MTU');
1526
+ const timedOutDevice = device;
1527
+ let mtuTeardownSettled = false;
1528
+ yield this.runNativeTeardown(uuid, blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
1529
+ yield this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () => timedOutDevice.cancelConnection());
1530
+ mtuTeardownSettled = true;
1531
+ }));
1532
+ if (this.stopped)
1533
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1534
+ if (!mtuTeardownSettled || this.blePlxManager !== blePlxManager) {
1535
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'BLE MTU cleanup timed out');
973
1536
  }
974
- const bondState = yield pairDevice(uuid);
975
- if (bondState.bonding) {
976
- yield onDeviceBondState(uuid);
1537
+ try {
1538
+ device = yield this.connectWithTimeout(uuid, () => timedOutDevice.connect(fallbackConnectOptions));
977
1539
  }
978
- else if (!bondState.bonded) {
979
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
1540
+ catch (error) {
1541
+ if (shouldRethrowBleSetupError(error))
1542
+ throw error;
1543
+ if ((error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.DeviceAlreadyConnected) {
1544
+ device = timedOutDevice;
1545
+ }
1546
+ else {
1547
+ remapError(error);
1548
+ }
980
1549
  }
981
1550
  }
982
- catch (error) {
983
- yield this.runNativeTeardown(uuid, blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
984
- yield Promise.all([
985
- this.runBestEffortNativeOperation('bond failure: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid)),
986
- this.runBestEffortNativeOperation('bond failure: cancel device connection', () => connectedDevice.cancelConnection()),
987
- ]);
988
- }));
989
- throw error;
990
- }
991
1551
  }
992
- device = yield resolveNegotiatedMtu(device);
1552
+ if (this.stopped)
1553
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
993
1554
  const acquiredDevice = device;
994
- const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
1555
+ const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice));
1556
+ if (this.stopped)
1557
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
995
1558
  const protocolHint = expectedProtocol
996
1559
  ? undefined
997
1560
  : (_a = input.protocolHint) !== null && _a !== void 0 ? _a : this.deviceProtocolHints.get(uuid);
@@ -1002,7 +1565,13 @@ class ReactNativeBleTransport {
1002
1565
  if (shouldMapProtocolV2StaleBond) {
1003
1566
  this.acquiringProtocolV2.add(uuid);
1004
1567
  }
1568
+ let linkEncryption;
1005
1569
  try {
1570
+ if (androidBondedBeforeConnect && !skipProtocolProbe) {
1571
+ linkEncryption = yield this.waitForAndroidLinkSecurity(uuid);
1572
+ if (this.stopped)
1573
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1574
+ }
1006
1575
  yield this.installTransportForAcquire(uuid, acquiredDevice, {
1007
1576
  writeCharacteristic,
1008
1577
  notifyCharacteristic,
@@ -1034,20 +1603,20 @@ class ReactNativeBleTransport {
1034
1603
  const protocolType = yield this.detectProtocol(uuid, expectedProtocol, protocolHint, () => __awaiter(this, void 0, void 0, function* () {
1035
1604
  yield this.installTransportForAcquire(uuid, acquiredDevice);
1036
1605
  }));
1606
+ if (this.stopped)
1607
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1037
1608
  const currentTransport = transportCache[uuid];
1038
1609
  if (!currentTransport) {
1039
1610
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotFound);
1040
1611
  }
1041
1612
  this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
1613
+ if (linkEncryption === 'unresolved') {
1614
+ markBleLinkEncrypted(uuid);
1615
+ }
1042
1616
  return { uuid, protocolType };
1043
1617
  }
1044
1618
  catch (error) {
1045
- if (hdShared.isBleStaleBondHardwareError(error)) {
1046
- yield this.disconnectUnlocked(uuid);
1047
- }
1048
- else {
1049
- yield this.releaseUnlocked(uuid, true);
1050
- }
1619
+ yield this.disconnectUnlocked(uuid);
1051
1620
  throw error;
1052
1621
  }
1053
1622
  finally {
@@ -1059,7 +1628,7 @@ class ReactNativeBleTransport {
1059
1628
  let bufferLength = 0;
1060
1629
  let buffer$1 = [];
1061
1630
  const subscription = characteristic.monitor((error, c) => {
1062
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1631
+ var _a, _b, _c, _d, _e, _f;
1063
1632
  const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
1064
1633
  if (error) {
1065
1634
  Log === null || Log === void 0 ? void 0 : Log.debug(`error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${error}`);
@@ -1076,16 +1645,16 @@ class ReactNativeBleTransport {
1076
1645
  this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
1077
1646
  return;
1078
1647
  }
1648
+ if (reactNative.Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
1649
+ this.androidGattCacheRefreshes.add(uuid);
1650
+ }
1079
1651
  if (this.getActiveProtocol(uuid) === 'V2') {
1080
1652
  let errorCode = hdShared.HardwareErrorCode.BleCharacteristicNotifyError;
1081
1653
  if ((_a = error.reason) === null || _a === void 0 ? void 0 : _a.includes('The connection has timed out unexpectedly')) {
1082
1654
  errorCode = hdShared.HardwareErrorCode.BleTimeoutError;
1083
1655
  }
1084
- else if (((_b = error.reason) === null || _b === void 0 ? void 0 : _b.includes('Cannot write client characteristic config descriptor')) ||
1085
- ((_c = error.reason) === null || _c === void 0 ? void 0 : _c.includes('Cannot find client characteristic config descriptor')) ||
1086
- ((_d = error.reason) === null || _d === void 0 ? void 0 : _d.includes('The handle is invalid')) ||
1087
- ((_e = error.reason) === null || _e === void 0 ? void 0 : _e.includes('Writing is not permitted')) ||
1088
- ((_f = error.reason) === null || _f === void 0 ? void 0 : _f.includes('notify change failed for device'))) {
1656
+ else if (isStaleGattTableNotifyReason(error.reason) ||
1657
+ ((_b = error.reason) === null || _b === void 0 ? void 0 : _b.includes('notify change failed for device'))) {
1089
1658
  errorCode = hdShared.HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
1090
1659
  }
1091
1660
  this.rejectProtocolV2Frames(uuid, hdShared.ERRORS.TypedError(errorCode));
@@ -1093,14 +1662,11 @@ class ReactNativeBleTransport {
1093
1662
  }
1094
1663
  if (this.runPromise && this.runPromiseDeviceId === uuid) {
1095
1664
  let ERROR = hdShared.HardwareErrorCode.BleCharacteristicNotifyError;
1096
- if ((_g = error.reason) === null || _g === void 0 ? void 0 : _g.includes('The connection has timed out unexpectedly')) {
1665
+ if ((_c = error.reason) === null || _c === void 0 ? void 0 : _c.includes('The connection has timed out unexpectedly')) {
1097
1666
  ERROR = hdShared.HardwareErrorCode.BleTimeoutError;
1098
1667
  }
1099
- if (((_h = error.reason) === null || _h === void 0 ? void 0 : _h.includes('Cannot write client characteristic config descriptor')) ||
1100
- ((_j = error.reason) === null || _j === void 0 ? void 0 : _j.includes('Cannot find client characteristic config descriptor')) ||
1101
- ((_k = error.reason) === null || _k === void 0 ? void 0 : _k.includes('The handle is invalid')) ||
1102
- ((_l = error.reason) === null || _l === void 0 ? void 0 : _l.includes('Writing is not permitted')) ||
1103
- ((_m = error.reason) === null || _m === void 0 ? void 0 : _m.includes('notify change failed for device'))) {
1668
+ if (isStaleGattTableNotifyReason(error.reason) ||
1669
+ ((_d = error.reason) === null || _d === void 0 ? void 0 : _d.includes('notify change failed for device'))) {
1104
1670
  const notifyError = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotifyChangeFailure);
1105
1671
  this.runPromise.reject(notifyError);
1106
1672
  Log === null || Log === void 0 ? void 0 : Log.debug(`${hdShared.HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`);
@@ -1142,7 +1708,7 @@ class ReactNativeBleTransport {
1142
1708
  bufferLength = 0;
1143
1709
  buffer$1 = [];
1144
1710
  if (this.runPromiseDeviceId === uuid) {
1145
- (_o = this.runPromise) === null || _o === void 0 ? void 0 : _o.resolve(value.toString('hex'));
1711
+ (_e = this.runPromise) === null || _e === void 0 ? void 0 : _e.resolve(value.toString('hex'));
1146
1712
  }
1147
1713
  }
1148
1714
  }
@@ -1153,7 +1719,7 @@ class ReactNativeBleTransport {
1153
1719
  this.rejectProtocolV2Frames(uuid, notifyError);
1154
1720
  }
1155
1721
  else if (this.runPromiseDeviceId === uuid) {
1156
- (_p = this.runPromise) === null || _p === void 0 ? void 0 : _p.reject(notifyError);
1722
+ (_f = this.runPromise) === null || _f === void 0 ? void 0 : _f.reject(notifyError);
1157
1723
  }
1158
1724
  }
1159
1725
  }, notifyTransactionId);
@@ -1248,14 +1814,64 @@ class ReactNativeBleTransport {
1248
1814
  if (!protocol) {
1249
1815
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${uuid}`);
1250
1816
  }
1251
- if (protocol === 'V2') {
1252
- return this.callProtocolV2(uuid, name, data, options);
1253
- }
1254
1817
  const forceRun = name === 'Initialize' || name === 'Cancel';
1255
- if (this.runPromise && !forceRun) {
1818
+ if (protocol !== 'V2' && this.runPromise && !forceRun) {
1256
1819
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportCallInProgress);
1257
1820
  }
1258
- return this.callProtocolV1(uuid, name, data, options);
1821
+ try {
1822
+ return protocol === 'V2'
1823
+ ? yield this.callProtocolV2(uuid, name, data, options)
1824
+ : yield this.callProtocolV1(uuid, name, data, options);
1825
+ }
1826
+ catch (error) {
1827
+ throw yield this.resolveAndroidBondInvalid(uuid, error);
1828
+ }
1829
+ });
1830
+ }
1831
+ resolveAndroidBondInvalid(uuid, error) {
1832
+ return __awaiter(this, void 0, void 0, function* () {
1833
+ if (reactNative.Platform.OS !== 'android' || !isBleKeyMissingSupported())
1834
+ return error;
1835
+ if (hdShared.isBleStaleBondHardwareError(error) || !isAndroidLinkLossError(error))
1836
+ return error;
1837
+ const linkStartedAt = this.androidLinkStartedAt.get(uuid);
1838
+ if (linkStartedAt === undefined ||
1839
+ Date.now() - linkStartedAt > ANDROID_KEY_MISSING_LINK_WINDOW_MS) {
1840
+ return error;
1841
+ }
1842
+ if (!(yield waitForBleKeyMissing(uuid, linkStartedAt, ANDROID_KEY_MISSING_GRACE_MS))) {
1843
+ return error;
1844
+ }
1845
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Android key missing, bond is invalid:', uuid);
1846
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, undefined, {
1847
+ phase: 'connect',
1848
+ reason: 'key_missing',
1849
+ });
1850
+ });
1851
+ }
1852
+ waitForAndroidLinkSecurity(uuid) {
1853
+ return __awaiter(this, void 0, void 0, function* () {
1854
+ const linkStartedAt = this.androidLinkStartedAt.get(uuid);
1855
+ if (linkStartedAt === undefined)
1856
+ return 'unresolved';
1857
+ const waitStartedAt = Date.now();
1858
+ const result = yield waitForAndroidLinkEncryption({
1859
+ deviceId: uuid,
1860
+ linkStartedAt,
1861
+ resultTimeoutMs: ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS,
1862
+ repairTimeoutMs: ANDROID_SYSTEM_REPAIR_TIMEOUT_MS,
1863
+ keyMissingGraceMs: ANDROID_KEY_MISSING_GRACE_MS,
1864
+ signal: this.bondAbortController.signal,
1865
+ onRepairStarted: () => {
1866
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Android bond lost, waiting for system re-pairing:', uuid);
1867
+ },
1868
+ });
1869
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Android link encryption', {
1870
+ connectIdSuffix: uuid.slice(-8),
1871
+ result,
1872
+ waitedMs: Date.now() - waitStartedAt,
1873
+ });
1874
+ return result;
1259
1875
  });
1260
1876
  }
1261
1877
  callProtocolV1(uuid, name, data, options) {
@@ -1429,13 +2045,14 @@ class ReactNativeBleTransport {
1429
2045
  return check.call(jsonData);
1430
2046
  }
1431
2047
  catch (e) {
1432
- if (name === 'GetFeatures' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS) {
1433
- Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
2048
+ const isProbeTimeout = (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS &&
2049
+ (name === 'GetFeatures' || name === 'Initialize');
2050
+ if (isProbeTimeout) {
2051
+ Log === null || Log === void 0 ? void 0 : Log.debug(`[ReactNativeBleTransport] Protocol V1 ${name} probe call failed:`, e);
1434
2052
  }
1435
2053
  else {
1436
2054
  Log === null || Log === void 0 ? void 0 : Log.error('call error: ', e);
1437
2055
  }
1438
- const isProbeTimeout = name === 'GetFeatures' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS;
1439
2056
  const isStaleCall = this.runPromise !== runPromise;
1440
2057
  if (!isProbeTimeout &&
1441
2058
  !isStaleCall &&
@@ -1455,7 +2072,46 @@ class ReactNativeBleTransport {
1455
2072
  });
1456
2073
  }
1457
2074
  stop() {
2075
+ var _a;
2076
+ if (this.stopPromise)
2077
+ return this.stopPromise;
1458
2078
  this.stopped = true;
2079
+ this.bondAbortController.abort();
2080
+ const deviceIds = new Set([
2081
+ ...this.monitorTokens.keys(),
2082
+ ...this.sessionProtocols.keys(),
2083
+ ...this.lifecycleOperations.keys(),
2084
+ ...(this.runPromiseDeviceId ? [this.runPromiseDeviceId] : []),
2085
+ ]);
2086
+ const scans = Array.from(this.scanCleanups, cleanup => cleanup());
2087
+ this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
2088
+ this.androidPriorityResetTimers.clear();
2089
+ this.androidHighPriorityDevices.clear();
2090
+ this.androidLinkStartedAt.clear();
2091
+ stopBleKeyMissingTracking();
2092
+ stopBleEncryptionTracking();
2093
+ const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
2094
+ (_a = this.runPromise) === null || _a === void 0 ? void 0 : _a.reject(error);
2095
+ this.runPromise = null;
2096
+ this.runPromiseDeviceId = null;
2097
+ deviceIds.forEach(uuid => this.rejectProtocolV2Frames(uuid, error));
2098
+ const manager = this.blePlxManager;
2099
+ const pendingConnections = manager
2100
+ ? Array.from(this.lifecycleOperations.keys(), uuid => this.runNativeTeardown(uuid, manager, () => __awaiter(this, void 0, void 0, function* () {
2101
+ yield this.runBestEffortNativeOperation('stop: cancel pending device connection', () => manager.cancelDeviceConnection(uuid));
2102
+ })))
2103
+ : [];
2104
+ this.stopPromise = Promise.all([
2105
+ ...scans,
2106
+ ...pendingConnections,
2107
+ ...Array.from(deviceIds, uuid => this.disconnect(uuid)),
2108
+ ]).then(() => __awaiter(this, void 0, void 0, function* () {
2109
+ yield this.protocolV2Links.invalidateAllLinks('React Native BLE transport stopped');
2110
+ yield this.waitForManagerReset();
2111
+ this.blePlxManager = undefined;
2112
+ this.emitter = undefined;
2113
+ }));
2114
+ return this.stopPromise;
1459
2115
  }
1460
2116
  disconnect(session) {
1461
2117
  return __awaiter(this, void 0, void 0, function* () {
@@ -1585,13 +2241,93 @@ class ReactNativeBleTransport {
1585
2241
  });
1586
2242
  }
1587
2243
  cancel() {
1588
- Log === null || Log === void 0 ? void 0 : Log.debug('transport-react-native transport cancel');
1589
- if (this.runPromise) ;
1590
- this.runPromise = null;
1591
- this.runPromiseDeviceId = null;
2244
+ return __awaiter(this, void 0, void 0, function* () {
2245
+ Log === null || Log === void 0 ? void 0 : Log.debug('transport-react-native transport cancel');
2246
+ const pending = this.runPromise;
2247
+ const deviceId = this.runPromiseDeviceId;
2248
+ if (pending) {
2249
+ pending.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.CallQueueActionCancelled));
2250
+ if (this.runPromise === pending) {
2251
+ this.runPromise = null;
2252
+ this.runPromiseDeviceId = null;
2253
+ }
2254
+ if (deviceId)
2255
+ yield this.disconnect(deviceId);
2256
+ }
2257
+ });
2258
+ }
2259
+ negotiateAndroidMtu(uuid, manager, device) {
2260
+ return __awaiter(this, void 0, void 0, function* () {
2261
+ if (!shouldRefreshNegotiatedMtu(device.mtu))
2262
+ return device;
2263
+ const startedAt = Date.now();
2264
+ let timer;
2265
+ let timedOut = false;
2266
+ let negotiated = device;
2267
+ let failure;
2268
+ try {
2269
+ negotiated = yield Promise.race([
2270
+ device.requestMTU(ANDROID_REQUEST_MTU, `${device.id}:mtu:connected:0:${startedAt}`),
2271
+ new Promise((_, reject) => {
2272
+ timer = setTimeout(() => {
2273
+ timedOut = true;
2274
+ reject(new Error(`BLE MTU exchange timeout after ${ANDROID_MTU_EXCHANGE_TIMEOUT_MS}ms`));
2275
+ }, ANDROID_MTU_EXCHANGE_TIMEOUT_MS);
2276
+ }),
2277
+ ]);
2278
+ }
2279
+ catch (error) {
2280
+ failure = error instanceof Error ? error.message : String(error);
2281
+ }
2282
+ finally {
2283
+ if (timer)
2284
+ clearTimeout(timer);
2285
+ }
2286
+ Log === null || Log === void 0 ? void 0 : Log.debug(`[ReactNativeBleTransport] BLE MTU exchange ${failure ? 'failed' : 'completed'}`, {
2287
+ connectIdSuffix: uuid.slice(-8),
2288
+ elapsedMs: Date.now() - startedAt,
2289
+ timedOut,
2290
+ actual: negotiated.mtu,
2291
+ error: failure,
2292
+ });
2293
+ if (this.stopped)
2294
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
2295
+ if (!timedOut && !isKnownDefaultMtu(negotiated.mtu))
2296
+ return negotiated;
2297
+ const resetManager = this.abandonStalledConnection(uuid, timedOut ? 'mtu-backstop' : 'mtu-default');
2298
+ yield this.dropAndroidLink(uuid, manager, negotiated, timedOut ? 'mtu exchange timeout' : 'default mtu');
2299
+ if (resetManager)
2300
+ throw this.createWedgedBleSetupError();
2301
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, timedOut
2302
+ ? 'BLE MTU exchange did not complete, reconnecting on a fresh link'
2303
+ : `BLE link stayed at the default MTU ${negotiated.mtu}, reconnecting on a fresh link`);
2304
+ });
2305
+ }
2306
+ dropAndroidLink(uuid, manager, device, reason) {
2307
+ return __awaiter(this, void 0, void 0, function* () {
2308
+ yield this.runNativeTeardown(uuid, manager, () => __awaiter(this, void 0, void 0, function* () {
2309
+ yield Promise.all([
2310
+ this.runBestEffortNativeOperation(`${reason}: cancel manager connection`, () => manager.cancelDeviceConnection(uuid)),
2311
+ this.runBestEffortNativeOperation(`${reason}: cancel device connection`, () => device.cancelConnection()),
2312
+ ]);
2313
+ }));
2314
+ const startedAt = Date.now();
2315
+ while (!this.stopped && Date.now() - startedAt < ANDROID_LINK_DROP_QUIET_MS) {
2316
+ yield delay(ANDROID_LINK_DROP_POLL_MS);
2317
+ }
2318
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Android BLE link drop', {
2319
+ connectIdSuffix: uuid.slice(-8),
2320
+ reason,
2321
+ stopped: this.stopped,
2322
+ });
2323
+ });
1592
2324
  }
1593
2325
  connectWithTimeout(uuid, connect) {
1594
2326
  return __awaiter(this, void 0, void 0, function* () {
2327
+ if (this.stopped)
2328
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
2329
+ const startedAt = Date.now();
2330
+ let succeeded = false;
1595
2331
  let timer;
1596
2332
  let timedOut = false;
1597
2333
  const pending = connect();
@@ -1606,6 +2342,7 @@ class ReactNativeBleTransport {
1606
2342
  }, BLE_CONNECT_TIMEOUT_MS);
1607
2343
  }),
1608
2344
  ]);
2345
+ succeeded = true;
1609
2346
  return result;
1610
2347
  }
1611
2348
  catch (error) {
@@ -1620,11 +2357,19 @@ class ReactNativeBleTransport {
1620
2357
  finally {
1621
2358
  if (timer)
1622
2359
  clearTimeout(timer);
2360
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] connect completed', {
2361
+ connectIdSuffix: uuid.slice(-8),
2362
+ elapsedMs: Date.now() - startedAt,
2363
+ succeeded,
2364
+ backstopExpired: timedOut,
2365
+ });
1623
2366
  }
1624
2367
  });
1625
2368
  }
1626
2369
  resolveCharacteristicsWithTimeout(uuid, device) {
1627
2370
  return __awaiter(this, void 0, void 0, function* () {
2371
+ const startedAt = Date.now();
2372
+ let succeeded = false;
1628
2373
  let timer;
1629
2374
  let timedOut = false;
1630
2375
  const pending = this.resolveCharacteristics(device);
@@ -1640,6 +2385,7 @@ class ReactNativeBleTransport {
1640
2385
  }),
1641
2386
  ]);
1642
2387
  this.connectionSetupTimeoutCounts.delete(uuid);
2388
+ succeeded = true;
1643
2389
  return result;
1644
2390
  }
1645
2391
  catch (error) {
@@ -1649,11 +2395,20 @@ class ReactNativeBleTransport {
1649
2395
  throw this.createWedgedBleSetupError();
1650
2396
  }
1651
2397
  }
2398
+ if (reactNative.Platform.OS === 'android' && isMissingGattShapeError(error)) {
2399
+ this.androidGattCacheRefreshes.add(uuid);
2400
+ }
1652
2401
  throw error;
1653
2402
  }
1654
2403
  finally {
1655
2404
  if (timer)
1656
2405
  clearTimeout(timer);
2406
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] GATT setup completed', {
2407
+ connectIdSuffix: uuid.slice(-8),
2408
+ elapsedMs: Date.now() - startedAt,
2409
+ succeeded,
2410
+ backstopExpired: timedOut,
2411
+ });
1657
2412
  }
1658
2413
  });
1659
2414
  }
@@ -1755,6 +2510,8 @@ class ReactNativeBleTransport {
1755
2510
  }
1756
2511
  }
1757
2512
  resetPlxManager() {
2513
+ if (bleManagerResetPromise)
2514
+ return;
1758
2515
  const manager = this.blePlxManager;
1759
2516
  this.blePlxManager = undefined;
1760
2517
  const reason = 'React Native BLE manager reset';
@@ -1792,16 +2549,25 @@ class ReactNativeBleTransport {
1792
2549
  this.acquiringProtocolV2.clear();
1793
2550
  this.sessionProtocols.clear();
1794
2551
  this.protocolReprobeFailures.clear();
2552
+ this.silentDetections.clear();
1795
2553
  this.writeTimeoutCounts.clear();
1796
2554
  this.connectionSetupTimeoutCounts.clear();
1797
2555
  this.monitorTokens.clear();
1798
2556
  this.protocolV2Assemblers.clear();
2557
+ let reset;
1799
2558
  try {
1800
- manager === null || manager === void 0 ? void 0 : manager.destroy();
2559
+ reset = Promise.resolve(manager === null || manager === void 0 ? void 0 : manager.destroy());
1801
2560
  }
1802
2561
  catch (error) {
1803
- Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
2562
+ reset = Promise.reject(error);
1804
2563
  }
2564
+ bleManagerResetPromise = reset;
2565
+ reset.then(() => {
2566
+ if (bleManagerResetPromise === reset)
2567
+ bleManagerResetPromise = undefined;
2568
+ }, error => {
2569
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE manager destroy failed:', error);
2570
+ });
1805
2571
  }
1806
2572
  createProtocolMismatchError(expected) {
1807
2573
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
@@ -1857,6 +2623,7 @@ class ReactNativeBleTransport {
1857
2623
  !protocolHint &&
1858
2624
  reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1859
2625
  const probeOrder = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
2626
+ yield this.wakeSilentProtocolV1Device(uuid, probeOrder);
1860
2627
  for (let i = 0; i < probeOrder.length; i += 1) {
1861
2628
  const protocol = probeOrder[i];
1862
2629
  if (i > 0) {
@@ -1876,6 +2643,7 @@ class ReactNativeBleTransport {
1876
2643
  this.confirmedProtocolV2.add(uuid);
1877
2644
  }
1878
2645
  this.protocolReprobeFailures.delete(uuid);
2646
+ this.silentDetections.delete(uuid);
1879
2647
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
1880
2648
  deviceId: uuid,
1881
2649
  protocol,
@@ -1884,6 +2652,8 @@ class ReactNativeBleTransport {
1884
2652
  return protocol;
1885
2653
  }
1886
2654
  }
2655
+ if (!this.silentDetections.has(uuid))
2656
+ this.silentDetections.set(uuid, 'silent');
1887
2657
  if (trustSessionProtocol) {
1888
2658
  this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1889
2659
  }
@@ -1895,6 +2665,30 @@ class ReactNativeBleTransport {
1895
2665
  throw this.createProtocolDetectionError();
1896
2666
  });
1897
2667
  }
2668
+ wakeSilentProtocolV1Device(uuid, probeOrder) {
2669
+ return __awaiter(this, void 0, void 0, function* () {
2670
+ if (reactNative.Platform.OS !== 'android' ||
2671
+ probeOrder[0] !== 'V1' ||
2672
+ this.silentDetections.get(uuid) !== 'silent') {
2673
+ return;
2674
+ }
2675
+ this.silentDetections.set(uuid, 'woken');
2676
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
2677
+ connectIdSuffix: uuid.slice(-8),
2678
+ });
2679
+ try {
2680
+ this.probingProtocols.set(uuid, 'V1');
2681
+ yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2682
+ }
2683
+ catch (error) {
2684
+ if (shouldRethrowProtocolProbeError(error))
2685
+ throw error;
2686
+ }
2687
+ finally {
2688
+ this.clearProbeProtocol(uuid, 'V1');
2689
+ }
2690
+ });
2691
+ }
1898
2692
  resetProbeStateAfterProtocolProbe(uuid, protocol) {
1899
2693
  var _a, _b, _c;
1900
2694
  return __awaiter(this, void 0, void 0, function* () {
@@ -1951,7 +2745,7 @@ class ReactNativeBleTransport {
1951
2745
  catch (error) {
1952
2746
  this.clearProbeProtocol(uuid, 'V1');
1953
2747
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
1954
- if (isWedgedWriteError(error)) {
2748
+ if (shouldRethrowProtocolProbeError(error)) {
1955
2749
  throw error;
1956
2750
  }
1957
2751
  return false;
@@ -1977,7 +2771,7 @@ class ReactNativeBleTransport {
1977
2771
  (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1978
2772
  this.resetProtocolV2Frames(uuid);
1979
2773
  },
1980
- shouldRethrow: hdShared.isBleStaleBondHardwareError,
2774
+ shouldRethrow: shouldRethrowProtocolProbeError,
1981
2775
  });
1982
2776
  if (!detected) {
1983
2777
  this.clearProbeProtocol(uuid, 'V2');
@@ -2104,6 +2898,9 @@ class ReactNativeBleTransport {
2104
2898
  this.rememberStaleBondError(uuid, bondError);
2105
2899
  throw bondError;
2106
2900
  }
2901
+ if (isNativeBleDisconnectError(error)) {
2902
+ throw toBleDisconnectHardwareError(error);
2903
+ }
2107
2904
  if (getFirmwareUploadWriteRetryType(error) !== 'congested' ||
2108
2905
  attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
2109
2906
  throw error;
@@ -2146,6 +2943,7 @@ class ReactNativeBleTransport {
2146
2943
  if (!this._messages || !this._messagesV2) {
2147
2944
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
2148
2945
  }
2946
+ const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
2149
2947
  const callOptions = options;
2150
2948
  const highThroughputWrite = transport.isProtocolV2HighThroughputCall(name);
2151
2949
  if (highThroughputWrite) {
@@ -2186,6 +2984,14 @@ class ReactNativeBleTransport {
2186
2984
  }
2187
2985
  catch (e) {
2188
2986
  Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
2987
+ if (!isProtocolProbe &&
2988
+ (e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError &&
2989
+ !this.monitorTokens.has(uuid)) {
2990
+ yield this.runLifecycleOperation(uuid, () => __awaiter(this, void 0, void 0, function* () {
2991
+ if (!this.monitorTokens.has(uuid))
2992
+ yield this.disconnectUnlocked(uuid);
2993
+ }));
2994
+ }
2189
2995
  throw e;
2190
2996
  }
2191
2997
  finally {
@@ -2200,7 +3006,7 @@ class ReactNativeBleTransport {
2200
3006
  const transport = this.getCachedTransport(uuid);
2201
3007
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize))
2202
3008
  return;
2203
- const refreshedDevice = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1);
3009
+ const { device: refreshedDevice } = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
2204
3010
  transport.device = refreshedDevice;
2205
3011
  transport.mtuSize =
2206
3012
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
@@ -2317,6 +3123,12 @@ class ReactNativeBleTransport {
2317
3123
  }
2318
3124
  }
2319
3125
 
3126
+ exports.ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS = ANDROID_ENCRYPTION_RESULT_TIMEOUT_MS;
3127
+ exports.ANDROID_KEY_MISSING_GRACE_MS = ANDROID_KEY_MISSING_GRACE_MS;
3128
+ exports.ANDROID_KEY_MISSING_LINK_WINDOW_MS = ANDROID_KEY_MISSING_LINK_WINDOW_MS;
3129
+ exports.ANDROID_LINK_DROP_QUIET_MS = ANDROID_LINK_DROP_QUIET_MS;
3130
+ exports.ANDROID_MTU_EXCHANGE_TIMEOUT_MS = ANDROID_MTU_EXCHANGE_TIMEOUT_MS;
3131
+ exports.ANDROID_SYSTEM_REPAIR_TIMEOUT_MS = ANDROID_SYSTEM_REPAIR_TIMEOUT_MS;
2320
3132
  exports.BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD;
2321
3133
  exports.BLE_CONNECT_TIMEOUT_MS = BLE_CONNECT_TIMEOUT_MS;
2322
3134
  exports.BLE_GATT_SETUP_TIMEOUT_MS = BLE_GATT_SETUP_TIMEOUT_MS;