@onekeyfe/hd-transport-react-native 1.2.2-alpha.11 → 1.2.2-alpha.110
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 +320 -68
- 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 +292 -2
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/bleStaleBond.ts +3 -0
- package/src/index.ts +256 -66
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;
|
|
@@ -450,6 +472,10 @@ export default class ReactNativeBleTransport {
|
|
|
450
472
|
/** Serializes transport lifecycle changes for the same physical device. */
|
|
451
473
|
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
452
474
|
|
|
475
|
+
private stopPromise?: Promise<void>;
|
|
476
|
+
|
|
477
|
+
private scanCleanups = new Set<() => Promise<void>>();
|
|
478
|
+
|
|
453
479
|
constructor(options: TransportOptions) {
|
|
454
480
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
455
481
|
}
|
|
@@ -485,10 +511,34 @@ export default class ReactNativeBleTransport {
|
|
|
485
511
|
// empty
|
|
486
512
|
}
|
|
487
513
|
|
|
488
|
-
getPlxManager(): Promise<BlePlxManager> {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
514
|
+
async getPlxManager(): Promise<BlePlxManager> {
|
|
515
|
+
while (bleManagerResetPromise) {
|
|
516
|
+
await this.waitForManagerReset();
|
|
517
|
+
}
|
|
518
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
519
|
+
if (!this.blePlxManager) this.blePlxManager = new BlePlxManager();
|
|
520
|
+
return this.blePlxManager;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async waitForManagerReset(): Promise<void> {
|
|
524
|
+
if (!bleManagerResetPromise) return;
|
|
525
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
526
|
+
try {
|
|
527
|
+
await Promise.race([
|
|
528
|
+
bleManagerResetPromise,
|
|
529
|
+
new Promise<never>((_, reject) => {
|
|
530
|
+
timeout = setTimeout(
|
|
531
|
+
() => reject(this.createWedgedBleSetupError()),
|
|
532
|
+
BLE_CONNECT_TIMEOUT_MS
|
|
533
|
+
);
|
|
534
|
+
}),
|
|
535
|
+
]);
|
|
536
|
+
} catch {
|
|
537
|
+
// A timeout or failed destroy is not permission to reuse the old singleton.
|
|
538
|
+
throw this.createWedgedBleSetupError();
|
|
539
|
+
} finally {
|
|
540
|
+
if (timeout) clearTimeout(timeout);
|
|
541
|
+
}
|
|
492
542
|
}
|
|
493
543
|
|
|
494
544
|
async resolveCharacteristics(device: Device): Promise<ResolvedBleCharacteristics> {
|
|
@@ -677,35 +727,58 @@ export default class ReactNativeBleTransport {
|
|
|
677
727
|
* @returns
|
|
678
728
|
*/
|
|
679
729
|
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...');
|
|
730
|
+
const scanStartedAt = Date.now();
|
|
731
|
+
let firstDeviceMs: number | undefined;
|
|
732
|
+
const blePlxManager = await this.getPlxManager();
|
|
733
|
+
await subscribeBleOn(blePlxManager);
|
|
734
|
+
if (Platform.OS === 'android' && Platform.Version >= 31) {
|
|
735
|
+
Log?.debug('requesting permissions, please wait...');
|
|
694
736
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
737
|
+
const resultConnect = await PermissionsAndroid.requestMultiple([
|
|
738
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
|
739
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
|
740
|
+
]);
|
|
699
741
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
742
|
+
Log?.debug('requesting permissions, result: ', resultConnect);
|
|
743
|
+
if (
|
|
744
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
|
|
745
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted'
|
|
746
|
+
) {
|
|
747
|
+
throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
708
748
|
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
752
|
+
return new Promise<IOneKeyDevice[]>((resolve, reject) => {
|
|
753
|
+
const deviceList: IOneKeyDevice[] = [];
|
|
754
|
+
let finished = false;
|
|
755
|
+
let scanCleanup: Promise<void> | undefined;
|
|
756
|
+
const finishScan = (error?: unknown) => {
|
|
757
|
+
if (scanCleanup) return scanCleanup;
|
|
758
|
+
finished = true;
|
|
759
|
+
clearScanTimer();
|
|
760
|
+
scanCleanup = this.runNativeTeardown('scan', blePlxManager, async () => {
|
|
761
|
+
await blePlxManager.stopDeviceScan();
|
|
762
|
+
}).then(() => {
|
|
763
|
+
this.scanCleanups.delete(cancelScan);
|
|
764
|
+
if (error) reject(error);
|
|
765
|
+
else resolve(deviceList);
|
|
766
|
+
});
|
|
767
|
+
return scanCleanup;
|
|
768
|
+
};
|
|
769
|
+
const cancelScan = () =>
|
|
770
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
771
|
+
this.scanCleanups.add(cancelScan);
|
|
772
|
+
|
|
773
|
+
const clearScanTimer = timer.timeout(() => {
|
|
774
|
+
Log?.debug('[ReactNativeBleTransport] scan completed', {
|
|
775
|
+
elapsedMs: Date.now() - scanStartedAt,
|
|
776
|
+
firstDeviceMs,
|
|
777
|
+
deviceCount: deviceList.length,
|
|
778
|
+
scanWindowMs: this.scanTimeout,
|
|
779
|
+
});
|
|
780
|
+
finishScan();
|
|
781
|
+
}, this.scanTimeout);
|
|
709
782
|
|
|
710
783
|
blePlxManager.startDeviceScan(
|
|
711
784
|
getBluetoothServiceUuids(),
|
|
@@ -721,18 +794,17 @@ export default class ReactNativeBleTransport {
|
|
|
721
794
|
error.errorCode
|
|
722
795
|
)
|
|
723
796
|
) {
|
|
724
|
-
|
|
797
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
|
|
725
798
|
} else if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
|
|
726
|
-
|
|
799
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
|
|
727
800
|
} else if (error.errorCode === BleErrorCode.LocationServicesDisabled) {
|
|
728
|
-
|
|
801
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationServicesDisabled));
|
|
729
802
|
} else if (error.errorCode === BleErrorCode.ScanStartFailed) {
|
|
730
803
|
// Android Bluetooth will report an error when the search frequency is too fast,
|
|
731
804
|
// then nothing is processed and an empty array of devices is returned.
|
|
732
805
|
// Then the next search will be back to normal
|
|
733
|
-
timer.timeout(() => {}, this.scanTimeout);
|
|
734
806
|
} else {
|
|
735
|
-
|
|
807
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.reason ?? ''));
|
|
736
808
|
}
|
|
737
809
|
return;
|
|
738
810
|
}
|
|
@@ -764,6 +836,7 @@ export default class ReactNativeBleTransport {
|
|
|
764
836
|
}
|
|
765
837
|
);
|
|
766
838
|
|
|
839
|
+
if (finished) return;
|
|
767
840
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
768
841
|
devices => {
|
|
769
842
|
for (const device of devices) {
|
|
@@ -783,11 +856,13 @@ export default class ReactNativeBleTransport {
|
|
|
783
856
|
addDevice(device as unknown as Device);
|
|
784
857
|
}
|
|
785
858
|
}
|
|
786
|
-
}
|
|
859
|
+
},
|
|
860
|
+
error => Log?.debug('search connected peripheral failed:', error)
|
|
787
861
|
);
|
|
788
862
|
|
|
789
863
|
const addDevice = (device: Device) => {
|
|
790
|
-
if (deviceList.every(d => d.id !== device.id)) {
|
|
864
|
+
if (!finished && deviceList.every(d => d.id !== device.id)) {
|
|
865
|
+
firstDeviceMs ??= Date.now() - scanStartedAt;
|
|
791
866
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
792
867
|
|
|
793
868
|
deviceList.push({
|
|
@@ -802,11 +877,6 @@ export default class ReactNativeBleTransport {
|
|
|
802
877
|
});
|
|
803
878
|
}
|
|
804
879
|
};
|
|
805
|
-
|
|
806
|
-
timer.timeout(() => {
|
|
807
|
-
blePlxManager.stopDeviceScan();
|
|
808
|
-
resolve(deviceList);
|
|
809
|
-
}, this.scanTimeout);
|
|
810
880
|
});
|
|
811
881
|
}
|
|
812
882
|
|
|
@@ -817,6 +887,7 @@ export default class ReactNativeBleTransport {
|
|
|
817
887
|
) {
|
|
818
888
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
819
889
|
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
890
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
820
891
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
821
892
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
822
893
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -845,6 +916,7 @@ export default class ReactNativeBleTransport {
|
|
|
845
916
|
} else if (Platform.OS === 'android') {
|
|
846
917
|
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
847
918
|
}
|
|
919
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
848
920
|
|
|
849
921
|
const initialMtu = transport.mtuSize;
|
|
850
922
|
let refreshAttempts = 0;
|
|
@@ -858,12 +930,14 @@ export default class ReactNativeBleTransport {
|
|
|
858
930
|
'servicesAndNotifyReady',
|
|
859
931
|
1
|
|
860
932
|
);
|
|
933
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
861
934
|
transport.device = refreshedDevice;
|
|
862
935
|
transport.mtuSize =
|
|
863
936
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
864
937
|
|
|
865
938
|
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
866
939
|
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
940
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
867
941
|
refreshAttempts += 1;
|
|
868
942
|
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
869
943
|
transport.device = refreshedDevice;
|
|
@@ -872,6 +946,7 @@ export default class ReactNativeBleTransport {
|
|
|
872
946
|
}
|
|
873
947
|
}
|
|
874
948
|
|
|
949
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
875
950
|
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
876
951
|
platform: Platform.OS,
|
|
877
952
|
requested: getRequestedBleMtu(),
|
|
@@ -894,6 +969,7 @@ export default class ReactNativeBleTransport {
|
|
|
894
969
|
}
|
|
895
970
|
|
|
896
971
|
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
972
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
897
973
|
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
898
974
|
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
899
975
|
? expectedProtocol === 'V2'
|
|
@@ -949,6 +1025,7 @@ export default class ReactNativeBleTransport {
|
|
|
949
1025
|
cachedProtocol &&
|
|
950
1026
|
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
951
1027
|
) {
|
|
1028
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
952
1029
|
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
953
1030
|
return { uuid, protocolType: cachedProtocol };
|
|
954
1031
|
}
|
|
@@ -980,14 +1057,26 @@ export default class ReactNativeBleTransport {
|
|
|
980
1057
|
throw error;
|
|
981
1058
|
}
|
|
982
1059
|
|
|
1060
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
983
1061
|
if (Platform.OS === 'android') {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
await
|
|
987
|
-
|
|
988
|
-
|
|
1062
|
+
// Initiate bonding locally before GATT can trigger peripheral-initiated pairing.
|
|
1063
|
+
try {
|
|
1064
|
+
const bondState = await pairDevice(uuid);
|
|
1065
|
+
if (bondState.bonding) {
|
|
1066
|
+
await onDeviceBondState(uuid, this.bondAbortController.signal);
|
|
1067
|
+
} else if (!bondState.bonded) {
|
|
1068
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
1069
|
+
}
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1072
|
+
await this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
|
|
1073
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1074
|
+
);
|
|
1075
|
+
});
|
|
1076
|
+
throw error;
|
|
989
1077
|
}
|
|
990
1078
|
}
|
|
1079
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
991
1080
|
|
|
992
1081
|
if (!device) {
|
|
993
1082
|
const devices = await blePlxManager.devices([uuid]);
|
|
@@ -1024,7 +1113,7 @@ export default class ReactNativeBleTransport {
|
|
|
1024
1113
|
Log?.debug('device already connected');
|
|
1025
1114
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
1026
1115
|
} else {
|
|
1027
|
-
remapError(e
|
|
1116
|
+
remapError(e);
|
|
1028
1117
|
}
|
|
1029
1118
|
}
|
|
1030
1119
|
}
|
|
@@ -1055,28 +1144,48 @@ export default class ReactNativeBleTransport {
|
|
|
1055
1144
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1056
1145
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1057
1146
|
);
|
|
1058
|
-
} catch (
|
|
1059
|
-
Log?.debug('last try to reconnect error: ',
|
|
1147
|
+
} catch (fallbackError) {
|
|
1148
|
+
Log?.debug('last try to reconnect error: ', fallbackError);
|
|
1060
1149
|
// last try to reconnect device if this issue exists
|
|
1061
1150
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
1062
|
-
if (
|
|
1151
|
+
if (fallbackError.errorCode === BleErrorCode.OperationCancelled) {
|
|
1063
1152
|
Log?.debug('last try to reconnect');
|
|
1064
1153
|
await disconnectedDevice.cancelConnection();
|
|
1065
1154
|
device = await this.connectWithTimeout(uuid, () =>
|
|
1066
1155
|
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1067
1156
|
);
|
|
1157
|
+
} else {
|
|
1158
|
+
remapError(fallbackError);
|
|
1068
1159
|
}
|
|
1069
1160
|
}
|
|
1070
1161
|
} else {
|
|
1071
|
-
remapError(e
|
|
1162
|
+
remapError(e);
|
|
1072
1163
|
}
|
|
1073
1164
|
}
|
|
1074
1165
|
}
|
|
1075
1166
|
|
|
1167
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1168
|
+
if (Platform.OS === 'android' && !(await device.isConnected().catch(() => false))) {
|
|
1169
|
+
const disconnectedDevice = device;
|
|
1170
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1171
|
+
await Promise.all([
|
|
1172
|
+
this.runBestEffortNativeOperation('connect failure: cancel manager connection', () =>
|
|
1173
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1174
|
+
),
|
|
1175
|
+
this.runBestEffortNativeOperation('connect failure: cancel device connection', () =>
|
|
1176
|
+
disconnectedDevice.cancelConnection()
|
|
1177
|
+
),
|
|
1178
|
+
]);
|
|
1179
|
+
});
|
|
1180
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1181
|
+
}
|
|
1182
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1076
1183
|
device = await resolveNegotiatedMtu(device);
|
|
1184
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1077
1185
|
const acquiredDevice = device;
|
|
1078
1186
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1079
1187
|
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
1188
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1080
1189
|
|
|
1081
1190
|
const protocolHint = expectedProtocol
|
|
1082
1191
|
? undefined
|
|
@@ -1138,6 +1247,7 @@ export default class ReactNativeBleTransport {
|
|
|
1138
1247
|
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1139
1248
|
}
|
|
1140
1249
|
);
|
|
1250
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1141
1251
|
const currentTransport = transportCache[uuid];
|
|
1142
1252
|
if (!currentTransport) {
|
|
1143
1253
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
@@ -1145,7 +1255,7 @@ export default class ReactNativeBleTransport {
|
|
|
1145
1255
|
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1146
1256
|
return { uuid, protocolType };
|
|
1147
1257
|
} catch (error) {
|
|
1148
|
-
if (isBleStaleBondHardwareError(error)) {
|
|
1258
|
+
if (isBleStaleBondHardwareError(error) || shouldRethrowProtocolProbeError(error)) {
|
|
1149
1259
|
await this.disconnectUnlocked(uuid);
|
|
1150
1260
|
} else {
|
|
1151
1261
|
await this.releaseUnlocked(uuid, true);
|
|
@@ -1678,7 +1788,49 @@ export default class ReactNativeBleTransport {
|
|
|
1678
1788
|
}
|
|
1679
1789
|
|
|
1680
1790
|
stop() {
|
|
1791
|
+
if (this.stopPromise) return this.stopPromise;
|
|
1681
1792
|
this.stopped = true;
|
|
1793
|
+
// Bonding precedes GATT, so cancelDeviceConnection cannot end this wait.
|
|
1794
|
+
this.bondAbortController.abort();
|
|
1795
|
+
const deviceIds = new Set([
|
|
1796
|
+
...this.monitorTokens.keys(),
|
|
1797
|
+
...this.sessionProtocols.keys(),
|
|
1798
|
+
...this.lifecycleOperations.keys(),
|
|
1799
|
+
...(this.runPromiseDeviceId ? [this.runPromiseDeviceId] : []),
|
|
1800
|
+
]);
|
|
1801
|
+
const scans = Array.from(this.scanCleanups, cleanup => cleanup());
|
|
1802
|
+
this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
|
|
1803
|
+
this.androidPriorityResetTimers.clear();
|
|
1804
|
+
this.androidHighPriorityDevices.clear();
|
|
1805
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1806
|
+
this.runPromise?.reject(error);
|
|
1807
|
+
this.runPromise = null;
|
|
1808
|
+
this.runPromiseDeviceId = null;
|
|
1809
|
+
deviceIds.forEach(uuid => this.rejectProtocolV2Frames(uuid, error));
|
|
1810
|
+
const manager = this.blePlxManager;
|
|
1811
|
+
// Cancel native setup before waiting for its lifecycle lock. Otherwise stop
|
|
1812
|
+
// waits for the very connect/MTU/GATT operation it needs to interrupt.
|
|
1813
|
+
const pendingConnections = manager
|
|
1814
|
+
? Array.from(this.lifecycleOperations.keys(), uuid =>
|
|
1815
|
+
this.runNativeTeardown(uuid, manager, async () => {
|
|
1816
|
+
await this.runBestEffortNativeOperation('stop: cancel pending device connection', () =>
|
|
1817
|
+
manager.cancelDeviceConnection(uuid)
|
|
1818
|
+
);
|
|
1819
|
+
})
|
|
1820
|
+
)
|
|
1821
|
+
: [];
|
|
1822
|
+
// Release only this transport's endpoints; other connectors may share ble-plx.
|
|
1823
|
+
this.stopPromise = Promise.all([
|
|
1824
|
+
...scans,
|
|
1825
|
+
...pendingConnections,
|
|
1826
|
+
...Array.from(deviceIds, uuid => this.disconnect(uuid)),
|
|
1827
|
+
]).then(async () => {
|
|
1828
|
+
await this.protocolV2Links.invalidateAllLinks('React Native BLE transport stopped');
|
|
1829
|
+
await this.waitForManagerReset();
|
|
1830
|
+
this.blePlxManager = undefined;
|
|
1831
|
+
this.emitter = undefined;
|
|
1832
|
+
});
|
|
1833
|
+
return this.stopPromise;
|
|
1682
1834
|
}
|
|
1683
1835
|
|
|
1684
1836
|
async disconnect(session: string) {
|
|
@@ -1830,17 +1982,27 @@ export default class ReactNativeBleTransport {
|
|
|
1830
1982
|
}
|
|
1831
1983
|
}
|
|
1832
1984
|
|
|
1833
|
-
cancel() {
|
|
1985
|
+
async cancel() {
|
|
1834
1986
|
Log?.debug('transport-react-native transport cancel');
|
|
1835
|
-
|
|
1836
|
-
|
|
1987
|
+
const pending = this.runPromise;
|
|
1988
|
+
const deviceId = this.runPromiseDeviceId;
|
|
1989
|
+
if (pending) {
|
|
1990
|
+
pending.reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled));
|
|
1991
|
+
if (this.runPromise === pending) {
|
|
1992
|
+
this.runPromise = null;
|
|
1993
|
+
this.runPromiseDeviceId = null;
|
|
1994
|
+
}
|
|
1995
|
+
// A V1 read cannot be safely reused after abandoning its response.
|
|
1996
|
+
// Drain native teardown before DeviceCommands releases the operation.
|
|
1997
|
+
if (deviceId) await this.disconnect(deviceId);
|
|
1837
1998
|
}
|
|
1838
|
-
this.runPromise = null;
|
|
1839
|
-
this.runPromiseDeviceId = null;
|
|
1840
1999
|
}
|
|
1841
2000
|
|
|
1842
2001
|
/** Run a native connect under the JS backstop budget. */
|
|
1843
2002
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
2003
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2004
|
+
const startedAt = Date.now();
|
|
2005
|
+
let succeeded = false;
|
|
1844
2006
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1845
2007
|
let timedOut = false;
|
|
1846
2008
|
const pending = connect();
|
|
@@ -1862,6 +2024,7 @@ export default class ReactNativeBleTransport {
|
|
|
1862
2024
|
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1863
2025
|
}),
|
|
1864
2026
|
]);
|
|
2027
|
+
succeeded = true;
|
|
1865
2028
|
return result;
|
|
1866
2029
|
} catch (error) {
|
|
1867
2030
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1876,6 +2039,12 @@ export default class ReactNativeBleTransport {
|
|
|
1876
2039
|
throw error;
|
|
1877
2040
|
} finally {
|
|
1878
2041
|
if (timer) clearTimeout(timer);
|
|
2042
|
+
Log?.debug('[ReactNativeBleTransport] connect completed', {
|
|
2043
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2044
|
+
elapsedMs: Date.now() - startedAt,
|
|
2045
|
+
succeeded,
|
|
2046
|
+
backstopExpired: timedOut,
|
|
2047
|
+
});
|
|
1879
2048
|
}
|
|
1880
2049
|
}
|
|
1881
2050
|
|
|
@@ -1884,6 +2053,8 @@ export default class ReactNativeBleTransport {
|
|
|
1884
2053
|
uuid: string,
|
|
1885
2054
|
device: Device
|
|
1886
2055
|
): Promise<ResolvedBleCharacteristics> {
|
|
2056
|
+
const startedAt = Date.now();
|
|
2057
|
+
let succeeded = false;
|
|
1887
2058
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1888
2059
|
let timedOut = false;
|
|
1889
2060
|
const pending = this.resolveCharacteristics(device);
|
|
@@ -1904,6 +2075,7 @@ export default class ReactNativeBleTransport {
|
|
|
1904
2075
|
}),
|
|
1905
2076
|
]);
|
|
1906
2077
|
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
2078
|
+
succeeded = true;
|
|
1907
2079
|
return result;
|
|
1908
2080
|
} catch (error) {
|
|
1909
2081
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1918,6 +2090,12 @@ export default class ReactNativeBleTransport {
|
|
|
1918
2090
|
throw error;
|
|
1919
2091
|
} finally {
|
|
1920
2092
|
if (timer) clearTimeout(timer);
|
|
2093
|
+
Log?.debug('[ReactNativeBleTransport] GATT setup completed', {
|
|
2094
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2095
|
+
elapsedMs: Date.now() - startedAt,
|
|
2096
|
+
succeeded,
|
|
2097
|
+
backstopExpired: timedOut,
|
|
2098
|
+
});
|
|
1921
2099
|
}
|
|
1922
2100
|
}
|
|
1923
2101
|
|
|
@@ -2061,6 +2239,7 @@ export default class ReactNativeBleTransport {
|
|
|
2061
2239
|
}
|
|
2062
2240
|
|
|
2063
2241
|
private resetPlxManager() {
|
|
2242
|
+
if (bleManagerResetPromise) return;
|
|
2064
2243
|
const manager = this.blePlxManager;
|
|
2065
2244
|
this.blePlxManager = undefined;
|
|
2066
2245
|
const reason = 'React Native BLE manager reset';
|
|
@@ -2107,11 +2286,21 @@ export default class ReactNativeBleTransport {
|
|
|
2107
2286
|
this.connectionSetupTimeoutCounts.clear();
|
|
2108
2287
|
this.monitorTokens.clear();
|
|
2109
2288
|
this.protocolV2Assemblers.clear();
|
|
2289
|
+
let reset: Promise<void>;
|
|
2110
2290
|
try {
|
|
2111
|
-
manager?.destroy();
|
|
2291
|
+
reset = Promise.resolve(manager?.destroy());
|
|
2112
2292
|
} catch (error) {
|
|
2113
|
-
|
|
2293
|
+
reset = Promise.reject(error);
|
|
2114
2294
|
}
|
|
2295
|
+
bleManagerResetPromise = reset;
|
|
2296
|
+
reset.then(
|
|
2297
|
+
() => {
|
|
2298
|
+
if (bleManagerResetPromise === reset) bleManagerResetPromise = undefined;
|
|
2299
|
+
},
|
|
2300
|
+
error => {
|
|
2301
|
+
Log?.error('[ReactNativeBleTransport] BLE manager destroy failed:', error);
|
|
2302
|
+
}
|
|
2303
|
+
);
|
|
2115
2304
|
}
|
|
2116
2305
|
|
|
2117
2306
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
@@ -2314,9 +2503,7 @@ export default class ReactNativeBleTransport {
|
|
|
2314
2503
|
} catch (error) {
|
|
2315
2504
|
this.clearProbeProtocol(uuid, 'V1');
|
|
2316
2505
|
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)) {
|
|
2506
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2320
2507
|
throw error;
|
|
2321
2508
|
}
|
|
2322
2509
|
return false;
|
|
@@ -2341,7 +2528,7 @@ export default class ReactNativeBleTransport {
|
|
|
2341
2528
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2342
2529
|
this.resetProtocolV2Frames(uuid);
|
|
2343
2530
|
},
|
|
2344
|
-
shouldRethrow:
|
|
2531
|
+
shouldRethrow: shouldRethrowProtocolProbeError,
|
|
2345
2532
|
});
|
|
2346
2533
|
if (!detected) {
|
|
2347
2534
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -2488,6 +2675,9 @@ export default class ReactNativeBleTransport {
|
|
|
2488
2675
|
this.rememberStaleBondError(uuid, bondError);
|
|
2489
2676
|
throw bondError;
|
|
2490
2677
|
}
|
|
2678
|
+
if (isNativeBleDisconnectError(error)) {
|
|
2679
|
+
throw toBleDisconnectHardwareError(error);
|
|
2680
|
+
}
|
|
2491
2681
|
if (
|
|
2492
2682
|
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2493
2683
|
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|