@onekeyfe/hd-transport-react-native 1.2.2-alpha.12 → 1.2.2-alpha.120
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 +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +339 -74
- 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 +343 -3
- package/src/__tests__/protocolV2Link.test.ts +475 -8
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/bleStaleBond.ts +3 -0
- package/src/index.ts +280 -70
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;
|
|
@@ -320,9 +340,9 @@ const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'c
|
|
|
320
340
|
|
|
321
341
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
322
342
|
|
|
323
|
-
function remapError(error: IOBleErrorRemap
|
|
343
|
+
function remapError(error: IOBleErrorRemap) {
|
|
324
344
|
if (error instanceof BleError) {
|
|
325
|
-
if (
|
|
345
|
+
if (isNativeBleStaleBondError(error)) {
|
|
326
346
|
throw toBleStaleBondHardwareError(error);
|
|
327
347
|
}
|
|
328
348
|
|
|
@@ -360,6 +380,8 @@ export default class ReactNativeBleTransport {
|
|
|
360
380
|
|
|
361
381
|
stopped = false;
|
|
362
382
|
|
|
383
|
+
private readonly bondAbortController = new AbortController();
|
|
384
|
+
|
|
363
385
|
scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
|
|
364
386
|
|
|
365
387
|
runPromise: Deferred<any> | null = null;
|
|
@@ -430,6 +452,14 @@ export default class ReactNativeBleTransport {
|
|
|
430
452
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
431
453
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
432
454
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
455
|
+
if (this.probingProtocols.get(uuid) !== 'V2') {
|
|
456
|
+
const transport = transportCache[uuid];
|
|
457
|
+
try {
|
|
458
|
+
this.emitDeviceDisconnect(uuid, transport?.device?.name, transport?.monitorToken);
|
|
459
|
+
} catch {
|
|
460
|
+
Log?.error('[ReactNativeBleTransport] Protocol V2 disconnect listener failed');
|
|
461
|
+
}
|
|
462
|
+
}
|
|
433
463
|
await this.releaseNative(uuid, true);
|
|
434
464
|
}
|
|
435
465
|
},
|
|
@@ -450,6 +480,10 @@ export default class ReactNativeBleTransport {
|
|
|
450
480
|
/** Serializes transport lifecycle changes for the same physical device. */
|
|
451
481
|
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
452
482
|
|
|
483
|
+
private stopPromise?: Promise<void>;
|
|
484
|
+
|
|
485
|
+
private scanCleanups = new Set<() => Promise<void>>();
|
|
486
|
+
|
|
453
487
|
constructor(options: TransportOptions) {
|
|
454
488
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
455
489
|
}
|
|
@@ -485,10 +519,34 @@ export default class ReactNativeBleTransport {
|
|
|
485
519
|
// empty
|
|
486
520
|
}
|
|
487
521
|
|
|
488
|
-
getPlxManager(): Promise<BlePlxManager> {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
522
|
+
async getPlxManager(): Promise<BlePlxManager> {
|
|
523
|
+
while (bleManagerResetPromise) {
|
|
524
|
+
await this.waitForManagerReset();
|
|
525
|
+
}
|
|
526
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
527
|
+
if (!this.blePlxManager) this.blePlxManager = new BlePlxManager();
|
|
528
|
+
return this.blePlxManager;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private async waitForManagerReset(): Promise<void> {
|
|
532
|
+
if (!bleManagerResetPromise) return;
|
|
533
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
534
|
+
try {
|
|
535
|
+
await Promise.race([
|
|
536
|
+
bleManagerResetPromise,
|
|
537
|
+
new Promise<never>((_, reject) => {
|
|
538
|
+
timeout = setTimeout(
|
|
539
|
+
() => reject(this.createWedgedBleSetupError()),
|
|
540
|
+
BLE_CONNECT_TIMEOUT_MS
|
|
541
|
+
);
|
|
542
|
+
}),
|
|
543
|
+
]);
|
|
544
|
+
} catch {
|
|
545
|
+
// A timeout or failed destroy is not permission to reuse the old singleton.
|
|
546
|
+
throw this.createWedgedBleSetupError();
|
|
547
|
+
} finally {
|
|
548
|
+
if (timeout) clearTimeout(timeout);
|
|
549
|
+
}
|
|
492
550
|
}
|
|
493
551
|
|
|
494
552
|
async resolveCharacteristics(device: Device): Promise<ResolvedBleCharacteristics> {
|
|
@@ -677,35 +735,58 @@ export default class ReactNativeBleTransport {
|
|
|
677
735
|
* @returns
|
|
678
736
|
*/
|
|
679
737
|
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...');
|
|
738
|
+
const scanStartedAt = Date.now();
|
|
739
|
+
let firstDeviceMs: number | undefined;
|
|
740
|
+
const blePlxManager = await this.getPlxManager();
|
|
741
|
+
await subscribeBleOn(blePlxManager);
|
|
742
|
+
if (Platform.OS === 'android' && Platform.Version >= 31) {
|
|
743
|
+
Log?.debug('requesting permissions, please wait...');
|
|
694
744
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
745
|
+
const resultConnect = await PermissionsAndroid.requestMultiple([
|
|
746
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
|
747
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
|
748
|
+
]);
|
|
699
749
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
750
|
+
Log?.debug('requesting permissions, result: ', resultConnect);
|
|
751
|
+
if (
|
|
752
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
|
|
753
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted'
|
|
754
|
+
) {
|
|
755
|
+
throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
708
756
|
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
760
|
+
return new Promise<IOneKeyDevice[]>((resolve, reject) => {
|
|
761
|
+
const deviceList: IOneKeyDevice[] = [];
|
|
762
|
+
let finished = false;
|
|
763
|
+
let scanCleanup: Promise<void> | undefined;
|
|
764
|
+
const finishScan = (error?: unknown) => {
|
|
765
|
+
if (scanCleanup) return scanCleanup;
|
|
766
|
+
finished = true;
|
|
767
|
+
clearScanTimer();
|
|
768
|
+
scanCleanup = this.runNativeTeardown('scan', blePlxManager, async () => {
|
|
769
|
+
await blePlxManager.stopDeviceScan();
|
|
770
|
+
}).then(() => {
|
|
771
|
+
this.scanCleanups.delete(cancelScan);
|
|
772
|
+
if (error) reject(error);
|
|
773
|
+
else resolve(deviceList);
|
|
774
|
+
});
|
|
775
|
+
return scanCleanup;
|
|
776
|
+
};
|
|
777
|
+
const cancelScan = () =>
|
|
778
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
779
|
+
this.scanCleanups.add(cancelScan);
|
|
780
|
+
|
|
781
|
+
const clearScanTimer = timer.timeout(() => {
|
|
782
|
+
Log?.debug('[ReactNativeBleTransport] scan completed', {
|
|
783
|
+
elapsedMs: Date.now() - scanStartedAt,
|
|
784
|
+
firstDeviceMs,
|
|
785
|
+
deviceCount: deviceList.length,
|
|
786
|
+
scanWindowMs: this.scanTimeout,
|
|
787
|
+
});
|
|
788
|
+
finishScan();
|
|
789
|
+
}, this.scanTimeout);
|
|
709
790
|
|
|
710
791
|
blePlxManager.startDeviceScan(
|
|
711
792
|
getBluetoothServiceUuids(),
|
|
@@ -721,18 +802,17 @@ export default class ReactNativeBleTransport {
|
|
|
721
802
|
error.errorCode
|
|
722
803
|
)
|
|
723
804
|
) {
|
|
724
|
-
|
|
805
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
|
|
725
806
|
} else if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
|
|
726
|
-
|
|
807
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
|
|
727
808
|
} else if (error.errorCode === BleErrorCode.LocationServicesDisabled) {
|
|
728
|
-
|
|
809
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationServicesDisabled));
|
|
729
810
|
} else if (error.errorCode === BleErrorCode.ScanStartFailed) {
|
|
730
811
|
// Android Bluetooth will report an error when the search frequency is too fast,
|
|
731
812
|
// then nothing is processed and an empty array of devices is returned.
|
|
732
813
|
// Then the next search will be back to normal
|
|
733
|
-
timer.timeout(() => {}, this.scanTimeout);
|
|
734
814
|
} else {
|
|
735
|
-
|
|
815
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.reason ?? ''));
|
|
736
816
|
}
|
|
737
817
|
return;
|
|
738
818
|
}
|
|
@@ -764,6 +844,7 @@ export default class ReactNativeBleTransport {
|
|
|
764
844
|
}
|
|
765
845
|
);
|
|
766
846
|
|
|
847
|
+
if (finished) return;
|
|
767
848
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
768
849
|
devices => {
|
|
769
850
|
for (const device of devices) {
|
|
@@ -783,11 +864,13 @@ export default class ReactNativeBleTransport {
|
|
|
783
864
|
addDevice(device as unknown as Device);
|
|
784
865
|
}
|
|
785
866
|
}
|
|
786
|
-
}
|
|
867
|
+
},
|
|
868
|
+
error => Log?.debug('search connected peripheral failed:', error)
|
|
787
869
|
);
|
|
788
870
|
|
|
789
871
|
const addDevice = (device: Device) => {
|
|
790
|
-
if (deviceList.every(d => d.id !== device.id)) {
|
|
872
|
+
if (!finished && deviceList.every(d => d.id !== device.id)) {
|
|
873
|
+
firstDeviceMs ??= Date.now() - scanStartedAt;
|
|
791
874
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
792
875
|
|
|
793
876
|
deviceList.push({
|
|
@@ -802,11 +885,6 @@ export default class ReactNativeBleTransport {
|
|
|
802
885
|
});
|
|
803
886
|
}
|
|
804
887
|
};
|
|
805
|
-
|
|
806
|
-
timer.timeout(() => {
|
|
807
|
-
blePlxManager.stopDeviceScan();
|
|
808
|
-
resolve(deviceList);
|
|
809
|
-
}, this.scanTimeout);
|
|
810
888
|
});
|
|
811
889
|
}
|
|
812
890
|
|
|
@@ -817,6 +895,7 @@ export default class ReactNativeBleTransport {
|
|
|
817
895
|
) {
|
|
818
896
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
819
897
|
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
898
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
820
899
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
821
900
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
822
901
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -845,6 +924,7 @@ export default class ReactNativeBleTransport {
|
|
|
845
924
|
} else if (Platform.OS === 'android') {
|
|
846
925
|
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
847
926
|
}
|
|
927
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
848
928
|
|
|
849
929
|
const initialMtu = transport.mtuSize;
|
|
850
930
|
let refreshAttempts = 0;
|
|
@@ -858,12 +938,14 @@ export default class ReactNativeBleTransport {
|
|
|
858
938
|
'servicesAndNotifyReady',
|
|
859
939
|
1
|
|
860
940
|
);
|
|
941
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
861
942
|
transport.device = refreshedDevice;
|
|
862
943
|
transport.mtuSize =
|
|
863
944
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
864
945
|
|
|
865
946
|
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
866
947
|
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
948
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
867
949
|
refreshAttempts += 1;
|
|
868
950
|
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
869
951
|
transport.device = refreshedDevice;
|
|
@@ -872,6 +954,7 @@ export default class ReactNativeBleTransport {
|
|
|
872
954
|
}
|
|
873
955
|
}
|
|
874
956
|
|
|
957
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
875
958
|
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
876
959
|
platform: Platform.OS,
|
|
877
960
|
requested: getRequestedBleMtu(),
|
|
@@ -894,6 +977,7 @@ export default class ReactNativeBleTransport {
|
|
|
894
977
|
}
|
|
895
978
|
|
|
896
979
|
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
980
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
897
981
|
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
898
982
|
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
899
983
|
? expectedProtocol === 'V2'
|
|
@@ -949,6 +1033,7 @@ export default class ReactNativeBleTransport {
|
|
|
949
1033
|
cachedProtocol &&
|
|
950
1034
|
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
951
1035
|
) {
|
|
1036
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
952
1037
|
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
953
1038
|
return { uuid, protocolType: cachedProtocol };
|
|
954
1039
|
}
|
|
@@ -980,14 +1065,26 @@ export default class ReactNativeBleTransport {
|
|
|
980
1065
|
throw error;
|
|
981
1066
|
}
|
|
982
1067
|
|
|
1068
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
983
1069
|
if (Platform.OS === 'android') {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
await
|
|
987
|
-
|
|
988
|
-
|
|
1070
|
+
// Initiate bonding locally before GATT can trigger peripheral-initiated pairing.
|
|
1071
|
+
try {
|
|
1072
|
+
const bondState = await pairDevice(uuid);
|
|
1073
|
+
if (bondState.bonding) {
|
|
1074
|
+
await onDeviceBondState(uuid, this.bondAbortController.signal);
|
|
1075
|
+
} else if (!bondState.bonded) {
|
|
1076
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
1077
|
+
}
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1080
|
+
await this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
|
|
1081
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1082
|
+
);
|
|
1083
|
+
});
|
|
1084
|
+
throw error;
|
|
989
1085
|
}
|
|
990
1086
|
}
|
|
1087
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
991
1088
|
|
|
992
1089
|
if (!device) {
|
|
993
1090
|
const devices = await blePlxManager.devices([uuid]);
|
|
@@ -1024,7 +1121,7 @@ export default class ReactNativeBleTransport {
|
|
|
1024
1121
|
Log?.debug('device already connected');
|
|
1025
1122
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
1026
1123
|
} else {
|
|
1027
|
-
remapError(e
|
|
1124
|
+
remapError(e);
|
|
1028
1125
|
}
|
|
1029
1126
|
}
|
|
1030
1127
|
}
|
|
@@ -1055,28 +1152,48 @@ export default class ReactNativeBleTransport {
|
|
|
1055
1152
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1056
1153
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1057
1154
|
);
|
|
1058
|
-
} catch (
|
|
1059
|
-
Log?.debug('last try to reconnect error: ',
|
|
1155
|
+
} catch (fallbackError) {
|
|
1156
|
+
Log?.debug('last try to reconnect error: ', fallbackError);
|
|
1060
1157
|
// last try to reconnect device if this issue exists
|
|
1061
1158
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
1062
|
-
if (
|
|
1159
|
+
if (fallbackError.errorCode === BleErrorCode.OperationCancelled) {
|
|
1063
1160
|
Log?.debug('last try to reconnect');
|
|
1064
1161
|
await disconnectedDevice.cancelConnection();
|
|
1065
1162
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1066
1163
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1067
1164
|
);
|
|
1165
|
+
} else {
|
|
1166
|
+
remapError(fallbackError);
|
|
1068
1167
|
}
|
|
1069
1168
|
}
|
|
1070
1169
|
} else {
|
|
1071
|
-
remapError(e
|
|
1170
|
+
remapError(e);
|
|
1072
1171
|
}
|
|
1073
1172
|
}
|
|
1074
1173
|
}
|
|
1075
1174
|
|
|
1175
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1176
|
+
if (Platform.OS === 'android' && !(await device.isConnected().catch(() => false))) {
|
|
1177
|
+
const disconnectedDevice = device;
|
|
1178
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1179
|
+
await Promise.all([
|
|
1180
|
+
this.runBestEffortNativeOperation('connect failure: cancel manager connection', () =>
|
|
1181
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1182
|
+
),
|
|
1183
|
+
this.runBestEffortNativeOperation('connect failure: cancel device connection', () =>
|
|
1184
|
+
disconnectedDevice.cancelConnection()
|
|
1185
|
+
),
|
|
1186
|
+
]);
|
|
1187
|
+
});
|
|
1188
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1189
|
+
}
|
|
1190
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1076
1191
|
device = await resolveNegotiatedMtu(device);
|
|
1192
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1077
1193
|
const acquiredDevice = device;
|
|
1078
1194
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1079
1195
|
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
1196
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1080
1197
|
|
|
1081
1198
|
const protocolHint = expectedProtocol
|
|
1082
1199
|
? undefined
|
|
@@ -1138,6 +1255,7 @@ export default class ReactNativeBleTransport {
|
|
|
1138
1255
|
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1139
1256
|
}
|
|
1140
1257
|
);
|
|
1258
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1141
1259
|
const currentTransport = transportCache[uuid];
|
|
1142
1260
|
if (!currentTransport) {
|
|
1143
1261
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
@@ -1145,11 +1263,9 @@ export default class ReactNativeBleTransport {
|
|
|
1145
1263
|
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1146
1264
|
return { uuid, protocolType };
|
|
1147
1265
|
} catch (error) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
await this.releaseUnlocked(uuid, true);
|
|
1152
|
-
}
|
|
1266
|
+
// A failed acquire must retire the physical link before Core retries. Logical
|
|
1267
|
+
// release leaves GATT connected even when neither protocol receives a response.
|
|
1268
|
+
await this.disconnectUnlocked(uuid);
|
|
1153
1269
|
throw error;
|
|
1154
1270
|
} finally {
|
|
1155
1271
|
this.acquiringProtocolV2.delete(uuid);
|
|
@@ -1678,7 +1794,49 @@ export default class ReactNativeBleTransport {
|
|
|
1678
1794
|
}
|
|
1679
1795
|
|
|
1680
1796
|
stop() {
|
|
1797
|
+
if (this.stopPromise) return this.stopPromise;
|
|
1681
1798
|
this.stopped = true;
|
|
1799
|
+
// Bonding precedes GATT, so cancelDeviceConnection cannot end this wait.
|
|
1800
|
+
this.bondAbortController.abort();
|
|
1801
|
+
const deviceIds = new Set([
|
|
1802
|
+
...this.monitorTokens.keys(),
|
|
1803
|
+
...this.sessionProtocols.keys(),
|
|
1804
|
+
...this.lifecycleOperations.keys(),
|
|
1805
|
+
...(this.runPromiseDeviceId ? [this.runPromiseDeviceId] : []),
|
|
1806
|
+
]);
|
|
1807
|
+
const scans = Array.from(this.scanCleanups, cleanup => cleanup());
|
|
1808
|
+
this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
|
|
1809
|
+
this.androidPriorityResetTimers.clear();
|
|
1810
|
+
this.androidHighPriorityDevices.clear();
|
|
1811
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1812
|
+
this.runPromise?.reject(error);
|
|
1813
|
+
this.runPromise = null;
|
|
1814
|
+
this.runPromiseDeviceId = null;
|
|
1815
|
+
deviceIds.forEach(uuid => this.rejectProtocolV2Frames(uuid, error));
|
|
1816
|
+
const manager = this.blePlxManager;
|
|
1817
|
+
// Cancel native setup before waiting for its lifecycle lock. Otherwise stop
|
|
1818
|
+
// waits for the very connect/MTU/GATT operation it needs to interrupt.
|
|
1819
|
+
const pendingConnections = manager
|
|
1820
|
+
? Array.from(this.lifecycleOperations.keys(), uuid =>
|
|
1821
|
+
this.runNativeTeardown(uuid, manager, async () => {
|
|
1822
|
+
await this.runBestEffortNativeOperation('stop: cancel pending device connection', () =>
|
|
1823
|
+
manager.cancelDeviceConnection(uuid)
|
|
1824
|
+
);
|
|
1825
|
+
})
|
|
1826
|
+
)
|
|
1827
|
+
: [];
|
|
1828
|
+
// Release only this transport's endpoints; other connectors may share ble-plx.
|
|
1829
|
+
this.stopPromise = Promise.all([
|
|
1830
|
+
...scans,
|
|
1831
|
+
...pendingConnections,
|
|
1832
|
+
...Array.from(deviceIds, uuid => this.disconnect(uuid)),
|
|
1833
|
+
]).then(async () => {
|
|
1834
|
+
await this.protocolV2Links.invalidateAllLinks('React Native BLE transport stopped');
|
|
1835
|
+
await this.waitForManagerReset();
|
|
1836
|
+
this.blePlxManager = undefined;
|
|
1837
|
+
this.emitter = undefined;
|
|
1838
|
+
});
|
|
1839
|
+
return this.stopPromise;
|
|
1682
1840
|
}
|
|
1683
1841
|
|
|
1684
1842
|
async disconnect(session: string) {
|
|
@@ -1830,17 +1988,27 @@ export default class ReactNativeBleTransport {
|
|
|
1830
1988
|
}
|
|
1831
1989
|
}
|
|
1832
1990
|
|
|
1833
|
-
cancel() {
|
|
1991
|
+
async cancel() {
|
|
1834
1992
|
Log?.debug('transport-react-native transport cancel');
|
|
1835
|
-
|
|
1836
|
-
|
|
1993
|
+
const pending = this.runPromise;
|
|
1994
|
+
const deviceId = this.runPromiseDeviceId;
|
|
1995
|
+
if (pending) {
|
|
1996
|
+
pending.reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled));
|
|
1997
|
+
if (this.runPromise === pending) {
|
|
1998
|
+
this.runPromise = null;
|
|
1999
|
+
this.runPromiseDeviceId = null;
|
|
2000
|
+
}
|
|
2001
|
+
// A V1 read cannot be safely reused after abandoning its response.
|
|
2002
|
+
// Drain native teardown before DeviceCommands releases the operation.
|
|
2003
|
+
if (deviceId) await this.disconnect(deviceId);
|
|
1837
2004
|
}
|
|
1838
|
-
this.runPromise = null;
|
|
1839
|
-
this.runPromiseDeviceId = null;
|
|
1840
2005
|
}
|
|
1841
2006
|
|
|
1842
2007
|
/** Run a native connect under the JS backstop budget. */
|
|
1843
2008
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
2009
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2010
|
+
const startedAt = Date.now();
|
|
2011
|
+
let succeeded = false;
|
|
1844
2012
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1845
2013
|
let timedOut = false;
|
|
1846
2014
|
const pending = connect();
|
|
@@ -1862,6 +2030,7 @@ export default class ReactNativeBleTransport {
|
|
|
1862
2030
|
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1863
2031
|
}),
|
|
1864
2032
|
]);
|
|
2033
|
+
succeeded = true;
|
|
1865
2034
|
return result;
|
|
1866
2035
|
} catch (error) {
|
|
1867
2036
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1876,6 +2045,12 @@ export default class ReactNativeBleTransport {
|
|
|
1876
2045
|
throw error;
|
|
1877
2046
|
} finally {
|
|
1878
2047
|
if (timer) clearTimeout(timer);
|
|
2048
|
+
Log?.debug('[ReactNativeBleTransport] connect completed', {
|
|
2049
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2050
|
+
elapsedMs: Date.now() - startedAt,
|
|
2051
|
+
succeeded,
|
|
2052
|
+
backstopExpired: timedOut,
|
|
2053
|
+
});
|
|
1879
2054
|
}
|
|
1880
2055
|
}
|
|
1881
2056
|
|
|
@@ -1884,6 +2059,8 @@ export default class ReactNativeBleTransport {
|
|
|
1884
2059
|
uuid: string,
|
|
1885
2060
|
device: Device
|
|
1886
2061
|
): Promise<ResolvedBleCharacteristics> {
|
|
2062
|
+
const startedAt = Date.now();
|
|
2063
|
+
let succeeded = false;
|
|
1887
2064
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1888
2065
|
let timedOut = false;
|
|
1889
2066
|
const pending = this.resolveCharacteristics(device);
|
|
@@ -1904,6 +2081,7 @@ export default class ReactNativeBleTransport {
|
|
|
1904
2081
|
}),
|
|
1905
2082
|
]);
|
|
1906
2083
|
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
2084
|
+
succeeded = true;
|
|
1907
2085
|
return result;
|
|
1908
2086
|
} catch (error) {
|
|
1909
2087
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1918,6 +2096,12 @@ export default class ReactNativeBleTransport {
|
|
|
1918
2096
|
throw error;
|
|
1919
2097
|
} finally {
|
|
1920
2098
|
if (timer) clearTimeout(timer);
|
|
2099
|
+
Log?.debug('[ReactNativeBleTransport] GATT setup completed', {
|
|
2100
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2101
|
+
elapsedMs: Date.now() - startedAt,
|
|
2102
|
+
succeeded,
|
|
2103
|
+
backstopExpired: timedOut,
|
|
2104
|
+
});
|
|
1921
2105
|
}
|
|
1922
2106
|
}
|
|
1923
2107
|
|
|
@@ -2061,6 +2245,7 @@ export default class ReactNativeBleTransport {
|
|
|
2061
2245
|
}
|
|
2062
2246
|
|
|
2063
2247
|
private resetPlxManager() {
|
|
2248
|
+
if (bleManagerResetPromise) return;
|
|
2064
2249
|
const manager = this.blePlxManager;
|
|
2065
2250
|
this.blePlxManager = undefined;
|
|
2066
2251
|
const reason = 'React Native BLE manager reset';
|
|
@@ -2107,11 +2292,21 @@ export default class ReactNativeBleTransport {
|
|
|
2107
2292
|
this.connectionSetupTimeoutCounts.clear();
|
|
2108
2293
|
this.monitorTokens.clear();
|
|
2109
2294
|
this.protocolV2Assemblers.clear();
|
|
2295
|
+
let reset: Promise<void>;
|
|
2110
2296
|
try {
|
|
2111
|
-
manager?.destroy();
|
|
2297
|
+
reset = Promise.resolve(manager?.destroy());
|
|
2112
2298
|
} catch (error) {
|
|
2113
|
-
|
|
2299
|
+
reset = Promise.reject(error);
|
|
2114
2300
|
}
|
|
2301
|
+
bleManagerResetPromise = reset;
|
|
2302
|
+
reset.then(
|
|
2303
|
+
() => {
|
|
2304
|
+
if (bleManagerResetPromise === reset) bleManagerResetPromise = undefined;
|
|
2305
|
+
},
|
|
2306
|
+
error => {
|
|
2307
|
+
Log?.error('[ReactNativeBleTransport] BLE manager destroy failed:', error);
|
|
2308
|
+
}
|
|
2309
|
+
);
|
|
2115
2310
|
}
|
|
2116
2311
|
|
|
2117
2312
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
@@ -2314,9 +2509,7 @@ export default class ReactNativeBleTransport {
|
|
|
2314
2509
|
} catch (error) {
|
|
2315
2510
|
this.clearProbeProtocol(uuid, 'V1');
|
|
2316
2511
|
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)) {
|
|
2512
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2320
2513
|
throw error;
|
|
2321
2514
|
}
|
|
2322
2515
|
return false;
|
|
@@ -2341,7 +2534,7 @@ export default class ReactNativeBleTransport {
|
|
|
2341
2534
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2342
2535
|
this.resetProtocolV2Frames(uuid);
|
|
2343
2536
|
},
|
|
2344
|
-
shouldRethrow:
|
|
2537
|
+
shouldRethrow: shouldRethrowProtocolProbeError,
|
|
2345
2538
|
});
|
|
2346
2539
|
if (!detected) {
|
|
2347
2540
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -2488,6 +2681,9 @@ export default class ReactNativeBleTransport {
|
|
|
2488
2681
|
this.rememberStaleBondError(uuid, bondError);
|
|
2489
2682
|
throw bondError;
|
|
2490
2683
|
}
|
|
2684
|
+
if (isNativeBleDisconnectError(error)) {
|
|
2685
|
+
throw toBleDisconnectHardwareError(error);
|
|
2686
|
+
}
|
|
2491
2687
|
if (
|
|
2492
2688
|
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2493
2689
|
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
@@ -2548,6 +2744,7 @@ export default class ReactNativeBleTransport {
|
|
|
2548
2744
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
2549
2745
|
}
|
|
2550
2746
|
|
|
2747
|
+
const isProtocolProbe = this.probingProtocols.get(uuid) === 'V2';
|
|
2551
2748
|
const callOptions = options;
|
|
2552
2749
|
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
2553
2750
|
|
|
@@ -2599,6 +2796,19 @@ export default class ReactNativeBleTransport {
|
|
|
2599
2796
|
);
|
|
2600
2797
|
} catch (e) {
|
|
2601
2798
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
2799
|
+
if (
|
|
2800
|
+
!isProtocolProbe &&
|
|
2801
|
+
e?.errorCode === HardwareErrorCode.BleTimeoutError &&
|
|
2802
|
+
!this.monitorTokens.has(uuid)
|
|
2803
|
+
) {
|
|
2804
|
+
// The failed link has finished invalidating. Disconnect outside that
|
|
2805
|
+
// callback to avoid waiting on its own invalidation or acquire lock.
|
|
2806
|
+
await this.runLifecycleOperation(uuid, async () => {
|
|
2807
|
+
// A queued timeout leaves its active monitor intact; a newer acquire
|
|
2808
|
+
// may also have installed one while cleanup waited for the lifecycle lock.
|
|
2809
|
+
if (!this.monitorTokens.has(uuid)) await this.disconnectUnlocked(uuid);
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2602
2812
|
throw e;
|
|
2603
2813
|
} finally {
|
|
2604
2814
|
if (highThroughputWrite) {
|