@onekeyfe/hd-transport-react-native 1.2.2-alpha.105 → 1.2.2-alpha.107
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.map +1 -1
- package/dist/bleNativeDisconnect.d.ts +3 -0
- package/dist/bleNativeDisconnect.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -78
- package/package.json +6 -6
- package/src/BleManager.ts +58 -4
- package/src/__tests__/bleNativeDisconnect.test.ts +33 -0
- package/src/__tests__/connectTimeout.test.ts +282 -3
- package/src/__tests__/protocolV2Link.test.ts +193 -31
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/index.ts +246 -85
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;
|
|
@@ -450,6 +470,10 @@ export default class ReactNativeBleTransport {
|
|
|
450
470
|
/** Serializes transport lifecycle changes for the same physical device. */
|
|
451
471
|
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
452
472
|
|
|
473
|
+
private stopPromise?: Promise<void>;
|
|
474
|
+
|
|
475
|
+
private scanCleanups = new Set<() => Promise<void>>();
|
|
476
|
+
|
|
453
477
|
constructor(options: TransportOptions) {
|
|
454
478
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
455
479
|
}
|
|
@@ -485,10 +509,34 @@ export default class ReactNativeBleTransport {
|
|
|
485
509
|
// empty
|
|
486
510
|
}
|
|
487
511
|
|
|
488
|
-
getPlxManager(): Promise<BlePlxManager> {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
512
|
+
async getPlxManager(): Promise<BlePlxManager> {
|
|
513
|
+
while (bleManagerResetPromise) {
|
|
514
|
+
await this.waitForManagerReset();
|
|
515
|
+
}
|
|
516
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
517
|
+
if (!this.blePlxManager) this.blePlxManager = new BlePlxManager();
|
|
518
|
+
return this.blePlxManager;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private async waitForManagerReset(): Promise<void> {
|
|
522
|
+
if (!bleManagerResetPromise) return;
|
|
523
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
524
|
+
try {
|
|
525
|
+
await Promise.race([
|
|
526
|
+
bleManagerResetPromise,
|
|
527
|
+
new Promise<never>((_, reject) => {
|
|
528
|
+
timeout = setTimeout(
|
|
529
|
+
() => reject(this.createWedgedBleSetupError()),
|
|
530
|
+
BLE_CONNECT_TIMEOUT_MS
|
|
531
|
+
);
|
|
532
|
+
}),
|
|
533
|
+
]);
|
|
534
|
+
} catch {
|
|
535
|
+
// A timeout or failed destroy is not permission to reuse the old singleton.
|
|
536
|
+
throw this.createWedgedBleSetupError();
|
|
537
|
+
} finally {
|
|
538
|
+
if (timeout) clearTimeout(timeout);
|
|
539
|
+
}
|
|
492
540
|
}
|
|
493
541
|
|
|
494
542
|
async resolveCharacteristics(device: Device): Promise<ResolvedBleCharacteristics> {
|
|
@@ -677,35 +725,58 @@ export default class ReactNativeBleTransport {
|
|
|
677
725
|
* @returns
|
|
678
726
|
*/
|
|
679
727
|
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...');
|
|
728
|
+
const scanStartedAt = Date.now();
|
|
729
|
+
let firstDeviceMs: number | undefined;
|
|
730
|
+
const blePlxManager = await this.getPlxManager();
|
|
731
|
+
await subscribeBleOn(blePlxManager);
|
|
732
|
+
if (Platform.OS === 'android' && Platform.Version >= 31) {
|
|
733
|
+
Log?.debug('requesting permissions, please wait...');
|
|
694
734
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
735
|
+
const resultConnect = await PermissionsAndroid.requestMultiple([
|
|
736
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
|
737
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
|
738
|
+
]);
|
|
699
739
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
740
|
+
Log?.debug('requesting permissions, result: ', resultConnect);
|
|
741
|
+
if (
|
|
742
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] !== 'granted' ||
|
|
743
|
+
resultConnect[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] !== 'granted'
|
|
744
|
+
) {
|
|
745
|
+
throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
708
746
|
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
750
|
+
return new Promise<IOneKeyDevice[]>((resolve, reject) => {
|
|
751
|
+
const deviceList: IOneKeyDevice[] = [];
|
|
752
|
+
let finished = false;
|
|
753
|
+
let scanCleanup: Promise<void> | undefined;
|
|
754
|
+
const finishScan = (error?: unknown) => {
|
|
755
|
+
if (scanCleanup) return scanCleanup;
|
|
756
|
+
finished = true;
|
|
757
|
+
clearScanTimer();
|
|
758
|
+
scanCleanup = this.runNativeTeardown('scan', blePlxManager, async () => {
|
|
759
|
+
await blePlxManager.stopDeviceScan();
|
|
760
|
+
}).then(() => {
|
|
761
|
+
this.scanCleanups.delete(cancelScan);
|
|
762
|
+
if (error) reject(error);
|
|
763
|
+
else resolve(deviceList);
|
|
764
|
+
});
|
|
765
|
+
return scanCleanup;
|
|
766
|
+
};
|
|
767
|
+
const cancelScan = () =>
|
|
768
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
769
|
+
this.scanCleanups.add(cancelScan);
|
|
770
|
+
|
|
771
|
+
const clearScanTimer = timer.timeout(() => {
|
|
772
|
+
Log?.debug('[ReactNativeBleTransport] scan completed', {
|
|
773
|
+
elapsedMs: Date.now() - scanStartedAt,
|
|
774
|
+
firstDeviceMs,
|
|
775
|
+
deviceCount: deviceList.length,
|
|
776
|
+
scanWindowMs: this.scanTimeout,
|
|
777
|
+
});
|
|
778
|
+
finishScan();
|
|
779
|
+
}, this.scanTimeout);
|
|
709
780
|
|
|
710
781
|
blePlxManager.startDeviceScan(
|
|
711
782
|
getBluetoothServiceUuids(),
|
|
@@ -721,18 +792,17 @@ export default class ReactNativeBleTransport {
|
|
|
721
792
|
error.errorCode
|
|
722
793
|
)
|
|
723
794
|
) {
|
|
724
|
-
|
|
795
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BlePermissionError));
|
|
725
796
|
} else if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
|
|
726
|
-
|
|
797
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationError));
|
|
727
798
|
} else if (error.errorCode === BleErrorCode.LocationServicesDisabled) {
|
|
728
|
-
|
|
799
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleLocationServicesDisabled));
|
|
729
800
|
} else if (error.errorCode === BleErrorCode.ScanStartFailed) {
|
|
730
801
|
// Android Bluetooth will report an error when the search frequency is too fast,
|
|
731
802
|
// then nothing is processed and an empty array of devices is returned.
|
|
732
803
|
// Then the next search will be back to normal
|
|
733
|
-
timer.timeout(() => {}, this.scanTimeout);
|
|
734
804
|
} else {
|
|
735
|
-
|
|
805
|
+
finishScan(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.reason ?? ''));
|
|
736
806
|
}
|
|
737
807
|
return;
|
|
738
808
|
}
|
|
@@ -764,6 +834,7 @@ export default class ReactNativeBleTransport {
|
|
|
764
834
|
}
|
|
765
835
|
);
|
|
766
836
|
|
|
837
|
+
if (finished) return;
|
|
767
838
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
768
839
|
devices => {
|
|
769
840
|
for (const device of devices) {
|
|
@@ -783,11 +854,13 @@ export default class ReactNativeBleTransport {
|
|
|
783
854
|
addDevice(device as unknown as Device);
|
|
784
855
|
}
|
|
785
856
|
}
|
|
786
|
-
}
|
|
857
|
+
},
|
|
858
|
+
error => Log?.debug('search connected peripheral failed:', error)
|
|
787
859
|
);
|
|
788
860
|
|
|
789
861
|
const addDevice = (device: Device) => {
|
|
790
|
-
if (deviceList.every(d => d.id !== device.id)) {
|
|
862
|
+
if (!finished && deviceList.every(d => d.id !== device.id)) {
|
|
863
|
+
firstDeviceMs ??= Date.now() - scanStartedAt;
|
|
791
864
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
792
865
|
|
|
793
866
|
deviceList.push({
|
|
@@ -802,11 +875,6 @@ export default class ReactNativeBleTransport {
|
|
|
802
875
|
});
|
|
803
876
|
}
|
|
804
877
|
};
|
|
805
|
-
|
|
806
|
-
timer.timeout(() => {
|
|
807
|
-
blePlxManager.stopDeviceScan();
|
|
808
|
-
resolve(deviceList);
|
|
809
|
-
}, this.scanTimeout);
|
|
810
878
|
});
|
|
811
879
|
}
|
|
812
880
|
|
|
@@ -817,6 +885,7 @@ export default class ReactNativeBleTransport {
|
|
|
817
885
|
) {
|
|
818
886
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
819
887
|
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
888
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
820
889
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
821
890
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
822
891
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -845,6 +914,7 @@ export default class ReactNativeBleTransport {
|
|
|
845
914
|
} else if (Platform.OS === 'android') {
|
|
846
915
|
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
847
916
|
}
|
|
917
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
848
918
|
|
|
849
919
|
const initialMtu = transport.mtuSize;
|
|
850
920
|
let refreshAttempts = 0;
|
|
@@ -858,12 +928,14 @@ export default class ReactNativeBleTransport {
|
|
|
858
928
|
'servicesAndNotifyReady',
|
|
859
929
|
1
|
|
860
930
|
);
|
|
931
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
861
932
|
transport.device = refreshedDevice;
|
|
862
933
|
transport.mtuSize =
|
|
863
934
|
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
864
935
|
|
|
865
936
|
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
866
937
|
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
938
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
867
939
|
refreshAttempts += 1;
|
|
868
940
|
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
869
941
|
transport.device = refreshedDevice;
|
|
@@ -872,6 +944,7 @@ export default class ReactNativeBleTransport {
|
|
|
872
944
|
}
|
|
873
945
|
}
|
|
874
946
|
|
|
947
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
875
948
|
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
876
949
|
platform: Platform.OS,
|
|
877
950
|
requested: getRequestedBleMtu(),
|
|
@@ -894,6 +967,7 @@ export default class ReactNativeBleTransport {
|
|
|
894
967
|
}
|
|
895
968
|
|
|
896
969
|
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
970
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
897
971
|
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
898
972
|
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
899
973
|
? expectedProtocol === 'V2'
|
|
@@ -949,6 +1023,7 @@ export default class ReactNativeBleTransport {
|
|
|
949
1023
|
cachedProtocol &&
|
|
950
1024
|
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
951
1025
|
) {
|
|
1026
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
952
1027
|
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
953
1028
|
return { uuid, protocolType: cachedProtocol };
|
|
954
1029
|
}
|
|
@@ -980,6 +1055,27 @@ export default class ReactNativeBleTransport {
|
|
|
980
1055
|
throw error;
|
|
981
1056
|
}
|
|
982
1057
|
|
|
1058
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1059
|
+
if (Platform.OS === 'android') {
|
|
1060
|
+
// Initiate bonding locally before GATT can trigger peripheral-initiated pairing.
|
|
1061
|
+
try {
|
|
1062
|
+
const bondState = await pairDevice(uuid);
|
|
1063
|
+
if (bondState.bonding) {
|
|
1064
|
+
await onDeviceBondState(uuid);
|
|
1065
|
+
} else if (!bondState.bonded) {
|
|
1066
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
1067
|
+
}
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1070
|
+
await this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
|
|
1071
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1072
|
+
);
|
|
1073
|
+
});
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1078
|
+
|
|
983
1079
|
if (!device) {
|
|
984
1080
|
const devices = await blePlxManager.devices([uuid]);
|
|
985
1081
|
[device] = devices;
|
|
@@ -1066,42 +1162,28 @@ export default class ReactNativeBleTransport {
|
|
|
1066
1162
|
}
|
|
1067
1163
|
}
|
|
1068
1164
|
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
} else if (!bondState.bonded) {
|
|
1084
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
1085
|
-
}
|
|
1086
|
-
} catch (error) {
|
|
1087
|
-
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1088
|
-
await Promise.all([
|
|
1089
|
-
this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
|
|
1090
|
-
blePlxManager.cancelDeviceConnection(uuid)
|
|
1091
|
-
),
|
|
1092
|
-
this.runBestEffortNativeOperation('bond failure: cancel device connection', () =>
|
|
1093
|
-
connectedDevice.cancelConnection()
|
|
1094
|
-
),
|
|
1095
|
-
]);
|
|
1096
|
-
});
|
|
1097
|
-
throw error;
|
|
1098
|
-
}
|
|
1165
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1166
|
+
if (Platform.OS === 'android' && !(await device.isConnected().catch(() => false))) {
|
|
1167
|
+
const disconnectedDevice = device;
|
|
1168
|
+
await this.runNativeTeardown(uuid, blePlxManager, async () => {
|
|
1169
|
+
await Promise.all([
|
|
1170
|
+
this.runBestEffortNativeOperation('connect failure: cancel manager connection', () =>
|
|
1171
|
+
blePlxManager.cancelDeviceConnection(uuid)
|
|
1172
|
+
),
|
|
1173
|
+
this.runBestEffortNativeOperation('connect failure: cancel device connection', () =>
|
|
1174
|
+
disconnectedDevice.cancelConnection()
|
|
1175
|
+
),
|
|
1176
|
+
]);
|
|
1177
|
+
});
|
|
1178
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
|
|
1099
1179
|
}
|
|
1100
|
-
|
|
1180
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1101
1181
|
device = await resolveNegotiatedMtu(device);
|
|
1182
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1102
1183
|
const acquiredDevice = device;
|
|
1103
1184
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
1104
1185
|
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
1186
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1105
1187
|
|
|
1106
1188
|
const protocolHint = expectedProtocol
|
|
1107
1189
|
? undefined
|
|
@@ -1163,6 +1245,7 @@ export default class ReactNativeBleTransport {
|
|
|
1163
1245
|
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1164
1246
|
}
|
|
1165
1247
|
);
|
|
1248
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1166
1249
|
const currentTransport = transportCache[uuid];
|
|
1167
1250
|
if (!currentTransport) {
|
|
1168
1251
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
@@ -1170,7 +1253,7 @@ export default class ReactNativeBleTransport {
|
|
|
1170
1253
|
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1171
1254
|
return { uuid, protocolType };
|
|
1172
1255
|
} catch (error) {
|
|
1173
|
-
if (isBleStaleBondHardwareError(error)) {
|
|
1256
|
+
if (isBleStaleBondHardwareError(error) || shouldRethrowProtocolProbeError(error)) {
|
|
1174
1257
|
await this.disconnectUnlocked(uuid);
|
|
1175
1258
|
} else {
|
|
1176
1259
|
await this.releaseUnlocked(uuid, true);
|
|
@@ -1703,7 +1786,47 @@ export default class ReactNativeBleTransport {
|
|
|
1703
1786
|
}
|
|
1704
1787
|
|
|
1705
1788
|
stop() {
|
|
1789
|
+
if (this.stopPromise) return this.stopPromise;
|
|
1706
1790
|
this.stopped = true;
|
|
1791
|
+
const deviceIds = new Set([
|
|
1792
|
+
...this.monitorTokens.keys(),
|
|
1793
|
+
...this.sessionProtocols.keys(),
|
|
1794
|
+
...this.lifecycleOperations.keys(),
|
|
1795
|
+
...(this.runPromiseDeviceId ? [this.runPromiseDeviceId] : []),
|
|
1796
|
+
]);
|
|
1797
|
+
const scans = Array.from(this.scanCleanups, cleanup => cleanup());
|
|
1798
|
+
this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
|
|
1799
|
+
this.androidPriorityResetTimers.clear();
|
|
1800
|
+
this.androidHighPriorityDevices.clear();
|
|
1801
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1802
|
+
this.runPromise?.reject(error);
|
|
1803
|
+
this.runPromise = null;
|
|
1804
|
+
this.runPromiseDeviceId = null;
|
|
1805
|
+
deviceIds.forEach(uuid => this.rejectProtocolV2Frames(uuid, error));
|
|
1806
|
+
const manager = this.blePlxManager;
|
|
1807
|
+
// Cancel native setup before waiting for its lifecycle lock. Otherwise stop
|
|
1808
|
+
// waits for the very connect/MTU/GATT operation it needs to interrupt.
|
|
1809
|
+
const pendingConnections = manager
|
|
1810
|
+
? Array.from(this.lifecycleOperations.keys(), uuid =>
|
|
1811
|
+
this.runNativeTeardown(uuid, manager, async () => {
|
|
1812
|
+
await this.runBestEffortNativeOperation('stop: cancel pending device connection', () =>
|
|
1813
|
+
manager.cancelDeviceConnection(uuid)
|
|
1814
|
+
);
|
|
1815
|
+
})
|
|
1816
|
+
)
|
|
1817
|
+
: [];
|
|
1818
|
+
// Release only this transport's endpoints; other connectors may share ble-plx.
|
|
1819
|
+
this.stopPromise = Promise.all([
|
|
1820
|
+
...scans,
|
|
1821
|
+
...pendingConnections,
|
|
1822
|
+
...Array.from(deviceIds, uuid => this.disconnect(uuid)),
|
|
1823
|
+
]).then(async () => {
|
|
1824
|
+
await this.protocolV2Links.invalidateAllLinks('React Native BLE transport stopped');
|
|
1825
|
+
await this.waitForManagerReset();
|
|
1826
|
+
this.blePlxManager = undefined;
|
|
1827
|
+
this.emitter = undefined;
|
|
1828
|
+
});
|
|
1829
|
+
return this.stopPromise;
|
|
1707
1830
|
}
|
|
1708
1831
|
|
|
1709
1832
|
async disconnect(session: string) {
|
|
@@ -1855,17 +1978,27 @@ export default class ReactNativeBleTransport {
|
|
|
1855
1978
|
}
|
|
1856
1979
|
}
|
|
1857
1980
|
|
|
1858
|
-
cancel() {
|
|
1981
|
+
async cancel() {
|
|
1859
1982
|
Log?.debug('transport-react-native transport cancel');
|
|
1860
|
-
|
|
1861
|
-
|
|
1983
|
+
const pending = this.runPromise;
|
|
1984
|
+
const deviceId = this.runPromiseDeviceId;
|
|
1985
|
+
if (pending) {
|
|
1986
|
+
pending.reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled));
|
|
1987
|
+
if (this.runPromise === pending) {
|
|
1988
|
+
this.runPromise = null;
|
|
1989
|
+
this.runPromiseDeviceId = null;
|
|
1990
|
+
}
|
|
1991
|
+
// A V1 read cannot be safely reused after abandoning its response.
|
|
1992
|
+
// Drain native teardown before DeviceCommands releases the operation.
|
|
1993
|
+
if (deviceId) await this.disconnect(deviceId);
|
|
1862
1994
|
}
|
|
1863
|
-
this.runPromise = null;
|
|
1864
|
-
this.runPromiseDeviceId = null;
|
|
1865
1995
|
}
|
|
1866
1996
|
|
|
1867
1997
|
/** Run a native connect under the JS backstop budget. */
|
|
1868
1998
|
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1999
|
+
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
2000
|
+
const startedAt = Date.now();
|
|
2001
|
+
let succeeded = false;
|
|
1869
2002
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1870
2003
|
let timedOut = false;
|
|
1871
2004
|
const pending = connect();
|
|
@@ -1887,6 +2020,7 @@ export default class ReactNativeBleTransport {
|
|
|
1887
2020
|
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1888
2021
|
}),
|
|
1889
2022
|
]);
|
|
2023
|
+
succeeded = true;
|
|
1890
2024
|
return result;
|
|
1891
2025
|
} catch (error) {
|
|
1892
2026
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1901,6 +2035,12 @@ export default class ReactNativeBleTransport {
|
|
|
1901
2035
|
throw error;
|
|
1902
2036
|
} finally {
|
|
1903
2037
|
if (timer) clearTimeout(timer);
|
|
2038
|
+
Log?.debug('[ReactNativeBleTransport] connect completed', {
|
|
2039
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2040
|
+
elapsedMs: Date.now() - startedAt,
|
|
2041
|
+
succeeded,
|
|
2042
|
+
backstopExpired: timedOut,
|
|
2043
|
+
});
|
|
1904
2044
|
}
|
|
1905
2045
|
}
|
|
1906
2046
|
|
|
@@ -1909,6 +2049,8 @@ export default class ReactNativeBleTransport {
|
|
|
1909
2049
|
uuid: string,
|
|
1910
2050
|
device: Device
|
|
1911
2051
|
): Promise<ResolvedBleCharacteristics> {
|
|
2052
|
+
const startedAt = Date.now();
|
|
2053
|
+
let succeeded = false;
|
|
1912
2054
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1913
2055
|
let timedOut = false;
|
|
1914
2056
|
const pending = this.resolveCharacteristics(device);
|
|
@@ -1929,6 +2071,7 @@ export default class ReactNativeBleTransport {
|
|
|
1929
2071
|
}),
|
|
1930
2072
|
]);
|
|
1931
2073
|
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
2074
|
+
succeeded = true;
|
|
1932
2075
|
return result;
|
|
1933
2076
|
} catch (error) {
|
|
1934
2077
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
@@ -1943,6 +2086,12 @@ export default class ReactNativeBleTransport {
|
|
|
1943
2086
|
throw error;
|
|
1944
2087
|
} finally {
|
|
1945
2088
|
if (timer) clearTimeout(timer);
|
|
2089
|
+
Log?.debug('[ReactNativeBleTransport] GATT setup completed', {
|
|
2090
|
+
connectIdSuffix: uuid.slice(-8),
|
|
2091
|
+
elapsedMs: Date.now() - startedAt,
|
|
2092
|
+
succeeded,
|
|
2093
|
+
backstopExpired: timedOut,
|
|
2094
|
+
});
|
|
1946
2095
|
}
|
|
1947
2096
|
}
|
|
1948
2097
|
|
|
@@ -2086,6 +2235,7 @@ export default class ReactNativeBleTransport {
|
|
|
2086
2235
|
}
|
|
2087
2236
|
|
|
2088
2237
|
private resetPlxManager() {
|
|
2238
|
+
if (bleManagerResetPromise) return;
|
|
2089
2239
|
const manager = this.blePlxManager;
|
|
2090
2240
|
this.blePlxManager = undefined;
|
|
2091
2241
|
const reason = 'React Native BLE manager reset';
|
|
@@ -2132,11 +2282,21 @@ export default class ReactNativeBleTransport {
|
|
|
2132
2282
|
this.connectionSetupTimeoutCounts.clear();
|
|
2133
2283
|
this.monitorTokens.clear();
|
|
2134
2284
|
this.protocolV2Assemblers.clear();
|
|
2285
|
+
let reset: Promise<void>;
|
|
2135
2286
|
try {
|
|
2136
|
-
manager?.destroy();
|
|
2287
|
+
reset = Promise.resolve(manager?.destroy());
|
|
2137
2288
|
} catch (error) {
|
|
2138
|
-
|
|
2289
|
+
reset = Promise.reject(error);
|
|
2139
2290
|
}
|
|
2291
|
+
bleManagerResetPromise = reset;
|
|
2292
|
+
reset.then(
|
|
2293
|
+
() => {
|
|
2294
|
+
if (bleManagerResetPromise === reset) bleManagerResetPromise = undefined;
|
|
2295
|
+
},
|
|
2296
|
+
error => {
|
|
2297
|
+
Log?.error('[ReactNativeBleTransport] BLE manager destroy failed:', error);
|
|
2298
|
+
}
|
|
2299
|
+
);
|
|
2140
2300
|
}
|
|
2141
2301
|
|
|
2142
2302
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
@@ -2339,9 +2499,7 @@ export default class ReactNativeBleTransport {
|
|
|
2339
2499
|
} catch (error) {
|
|
2340
2500
|
this.clearProbeProtocol(uuid, 'V1');
|
|
2341
2501
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2342
|
-
|
|
2343
|
-
// would only fail against a torn-down transport: surface the real cause.
|
|
2344
|
-
if (isWedgedWriteError(error)) {
|
|
2502
|
+
if (shouldRethrowProtocolProbeError(error)) {
|
|
2345
2503
|
throw error;
|
|
2346
2504
|
}
|
|
2347
2505
|
return false;
|
|
@@ -2366,7 +2524,7 @@ export default class ReactNativeBleTransport {
|
|
|
2366
2524
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2367
2525
|
this.resetProtocolV2Frames(uuid);
|
|
2368
2526
|
},
|
|
2369
|
-
shouldRethrow:
|
|
2527
|
+
shouldRethrow: shouldRethrowProtocolProbeError,
|
|
2370
2528
|
});
|
|
2371
2529
|
if (!detected) {
|
|
2372
2530
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -2513,6 +2671,9 @@ export default class ReactNativeBleTransport {
|
|
|
2513
2671
|
this.rememberStaleBondError(uuid, bondError);
|
|
2514
2672
|
throw bondError;
|
|
2515
2673
|
}
|
|
2674
|
+
if (isNativeBleDisconnectError(error)) {
|
|
2675
|
+
throw toBleDisconnectHardwareError(error);
|
|
2676
|
+
}
|
|
2516
2677
|
if (
|
|
2517
2678
|
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2518
2679
|
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|