@onekeyfe/hd-transport-react-native 1.2.0-alpha.98 → 1.2.0
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/bleStaleBond.d.ts +5 -0
- package/dist/bleStaleBond.d.ts.map +1 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +354 -132
- package/package.json +5 -5
- package/src/__tests__/bleStaleBond.test.ts +39 -0
- package/src/__tests__/connectTimeout.test.ts +19 -8
- package/src/__tests__/enumerate.test.ts +30 -12
- package/src/__tests__/protocolReprobe.test.ts +24 -0
- package/src/__tests__/protocolV2Link.test.ts +291 -10
- package/src/bleStaleBond.ts +61 -0
- package/src/index.ts +418 -136
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { PermissionsAndroid, Platform } from 'react-native';
|
|
2
2
|
import { Buffer } from 'buffer';
|
|
3
3
|
import {
|
|
4
|
-
BleATTErrorCode,
|
|
5
4
|
BleError,
|
|
6
5
|
BleErrorCode,
|
|
7
6
|
BleManager as BlePlxManager,
|
|
@@ -29,7 +28,6 @@ import {
|
|
|
29
28
|
HardwareErrorCode,
|
|
30
29
|
createDeferred,
|
|
31
30
|
isOnekeyBluetoothDevice,
|
|
32
|
-
isPro2FindMyAdvertisementName,
|
|
33
31
|
} from '@onekeyfe/hd-shared';
|
|
34
32
|
|
|
35
33
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
@@ -49,17 +47,30 @@ import {
|
|
|
49
47
|
getInfosForServiceUuid,
|
|
50
48
|
isSameBleUuid,
|
|
51
49
|
} from './constants';
|
|
50
|
+
import {
|
|
51
|
+
isBleStaleBondHardwareError,
|
|
52
|
+
isNativeBleStaleBondError,
|
|
53
|
+
toBleStaleBondHardwareError,
|
|
54
|
+
} from './bleStaleBond';
|
|
52
55
|
import { isHeaderChunk } from './utils/validateNotify';
|
|
53
56
|
import BleTransport from './BleTransport';
|
|
54
57
|
import timer from './utils/timer';
|
|
55
58
|
import { bleLogger, setBleLogger } from './logger';
|
|
56
|
-
import { createTransportCallLog } from './transportLog';
|
|
57
59
|
|
|
58
60
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
59
61
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
60
62
|
import type EventEmitter from 'events';
|
|
61
63
|
import type { BleAcquireInput, TransportOptions } from './types';
|
|
62
64
|
|
|
65
|
+
type FirmwareInstallBleAcquireInput = BleAcquireInput & {
|
|
66
|
+
/**
|
|
67
|
+
* Reuse the already-verified protocol after an expected firmware-install
|
|
68
|
+
* disconnect. The install loader accepts status requests but may not answer
|
|
69
|
+
* the generic protocol probe used by a normal acquire.
|
|
70
|
+
*/
|
|
71
|
+
skipProtocolProbe?: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
63
74
|
const { check, ProtocolV1, parseConfigure } = transport;
|
|
64
75
|
|
|
65
76
|
const Log = bleLogger;
|
|
@@ -140,7 +151,7 @@ export const getFirmwareUploadWriteRetryType = (
|
|
|
140
151
|
|
|
141
152
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
142
153
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
143
|
-
const PROTOCOL_PROBE_TIMEOUT_MS =
|
|
154
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
144
155
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
145
156
|
/**
|
|
146
157
|
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
@@ -151,6 +162,7 @@ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
|
151
162
|
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
152
163
|
*/
|
|
153
164
|
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
165
|
+
export const BLE_NATIVE_TEARDOWN_TIMEOUT_MS = 3_000;
|
|
154
166
|
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
155
167
|
const isWedgedWriteError = (error: unknown): boolean =>
|
|
156
168
|
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
@@ -204,10 +216,6 @@ export function getProtocolV2BleTuning() {
|
|
|
204
216
|
return { ...protocolV2BleTuning };
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
208
|
-
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
219
|
function getDeviceDisplayName(device?: Device | null) {
|
|
212
220
|
return device?.name || device?.localName || null;
|
|
213
221
|
}
|
|
@@ -259,10 +267,17 @@ export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
|
259
267
|
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
260
268
|
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
261
269
|
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
270
|
+
export const BLE_SETUP_WEDGED_MESSAGE = 'BLE setup wedged repeatedly';
|
|
262
271
|
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
263
272
|
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
264
273
|
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
265
274
|
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
275
|
+
const isWedgedBleSetupError = (error: unknown): boolean =>
|
|
276
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.PollingTimeout &&
|
|
277
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
278
|
+
(error as { message: string }).message.startsWith(BLE_SETUP_WEDGED_MESSAGE);
|
|
279
|
+
const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
280
|
+
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
266
281
|
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
267
282
|
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
268
283
|
|
|
@@ -305,15 +320,10 @@ const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'c
|
|
|
305
320
|
|
|
306
321
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
307
322
|
|
|
308
|
-
function remapError(error: IOBleErrorRemap) {
|
|
323
|
+
function remapError(error: IOBleErrorRemap, mapProtocolV2StaleBond: boolean) {
|
|
309
324
|
if (error instanceof BleError) {
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
// @ts-expect-error
|
|
313
|
-
error.iosErrorCode === BleATTErrorCode.UnlikelyError ||
|
|
314
|
-
error.reason === 'Peer removed pairing information'
|
|
315
|
-
) {
|
|
316
|
-
throw ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
325
|
+
if (mapProtocolV2StaleBond && isNativeBleStaleBondError(error)) {
|
|
326
|
+
throw toBleStaleBondHardwareError(error);
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -382,9 +392,22 @@ export default class ReactNativeBleTransport {
|
|
|
382
392
|
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
383
393
|
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
384
394
|
|
|
395
|
+
/** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
|
|
396
|
+
private confirmedProtocolV2 = new Set<string>();
|
|
397
|
+
|
|
385
398
|
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
386
399
|
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
387
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
403
|
+
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
404
|
+
* fails with ATT 5/15. Remember it so detectProtocol fails immediately.
|
|
405
|
+
*/
|
|
406
|
+
private staleBondErrors: Map<string, Error> = new Map();
|
|
407
|
+
|
|
408
|
+
/** Strict or previously confirmed V2 target while acquire installs notifications. */
|
|
409
|
+
private acquiringProtocolV2 = new Set<string>();
|
|
410
|
+
|
|
388
411
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
389
412
|
|
|
390
413
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -424,6 +447,9 @@ export default class ReactNativeBleTransport {
|
|
|
424
447
|
|
|
425
448
|
private nextMonitorToken = 1;
|
|
426
449
|
|
|
450
|
+
/** Serializes transport lifecycle changes for the same physical device. */
|
|
451
|
+
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
452
|
+
|
|
427
453
|
constructor(options: TransportOptions) {
|
|
428
454
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
429
455
|
}
|
|
@@ -715,17 +741,15 @@ export default class ReactNativeBleTransport {
|
|
|
715
741
|
// iOS may report a service-only advertisement before the named scan response.
|
|
716
742
|
// Do not cache that incomplete advertisement as an unknown device.
|
|
717
743
|
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
718
|
-
const isFindMyPeripheral =
|
|
719
|
-
isPro2FindMyAdvertisementName(device?.name) ||
|
|
720
|
-
isPro2FindMyAdvertisementName(device?.localName);
|
|
721
744
|
const isOneKey =
|
|
722
745
|
!isUnnamedIOSPeripheral &&
|
|
723
|
-
!isFindMyPeripheral &&
|
|
724
746
|
isOnekeyBluetoothDevice({
|
|
725
747
|
id: device?.id,
|
|
726
748
|
name: device?.name,
|
|
727
749
|
localName: device?.localName,
|
|
728
|
-
|
|
750
|
+
// The native scan is already restricted to the OneKey communication service,
|
|
751
|
+
// but ble-plx permits the returned advertisement field to be null.
|
|
752
|
+
serviceUuids: device?.serviceUUIDs ?? getBluetoothServiceUuids(),
|
|
729
753
|
});
|
|
730
754
|
if (isOneKey) {
|
|
731
755
|
addDevice(device as unknown as Device);
|
|
@@ -747,12 +771,7 @@ export default class ReactNativeBleTransport {
|
|
|
747
771
|
'localName' in device && typeof device.localName === 'string'
|
|
748
772
|
? device.localName
|
|
749
773
|
: null;
|
|
750
|
-
const isFindMyPeripheral =
|
|
751
|
-
isPro2FindMyAdvertisementName(device.name) ||
|
|
752
|
-
isPro2FindMyAdvertisementName(localName);
|
|
753
|
-
|
|
754
774
|
if (
|
|
755
|
-
!isFindMyPeripheral &&
|
|
756
775
|
isOnekeyBluetoothDevice({
|
|
757
776
|
id: device.id,
|
|
758
777
|
name: device.name,
|
|
@@ -770,10 +789,7 @@ export default class ReactNativeBleTransport {
|
|
|
770
789
|
const addDevice = (device: Device) => {
|
|
771
790
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
772
791
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
773
|
-
|
|
774
|
-
if (protocolHint) {
|
|
775
|
-
this.deviceProtocolHints.set(device.id, protocolHint);
|
|
776
|
-
}
|
|
792
|
+
|
|
777
793
|
deviceList.push({
|
|
778
794
|
...device,
|
|
779
795
|
name: displayName,
|
|
@@ -783,7 +799,6 @@ export default class ReactNativeBleTransport {
|
|
|
783
799
|
deviceId: device.id,
|
|
784
800
|
name: displayName,
|
|
785
801
|
serviceUUIDs: device.serviceUUIDs,
|
|
786
|
-
protocolHint,
|
|
787
802
|
});
|
|
788
803
|
}
|
|
789
804
|
};
|
|
@@ -868,32 +883,83 @@ export default class ReactNativeBleTransport {
|
|
|
868
883
|
return transport;
|
|
869
884
|
}
|
|
870
885
|
|
|
871
|
-
async acquire(input:
|
|
872
|
-
const { uuid
|
|
886
|
+
async acquire(input: FirmwareInstallBleAcquireInput) {
|
|
887
|
+
const { uuid } = input;
|
|
873
888
|
|
|
874
889
|
if (!uuid) {
|
|
875
890
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
876
891
|
}
|
|
877
892
|
|
|
893
|
+
return this.runLifecycleOperation(uuid, () => this.acquireUnlocked(input));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
897
|
+
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
898
|
+
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
899
|
+
? expectedProtocol === 'V2'
|
|
900
|
+
: this.confirmedProtocolV2.has(uuid);
|
|
901
|
+
|
|
878
902
|
const cachedTransport = transportCache[uuid];
|
|
903
|
+
if (skipProtocolProbe && !cachedTransport && this.blePlxManager) {
|
|
904
|
+
Log?.debug(
|
|
905
|
+
'[ReactNativeBleTransport] refresh uncached BLE connection for firmware install:',
|
|
906
|
+
uuid
|
|
907
|
+
);
|
|
908
|
+
const manager = this.blePlxManager;
|
|
909
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
910
|
+
await this.runBestEffortNativeOperation(
|
|
911
|
+
'firmware install reconnect: cancel uncached device connection',
|
|
912
|
+
() => manager.cancelDeviceConnection(uuid)
|
|
913
|
+
);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
879
916
|
if (cachedTransport) {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
917
|
+
if (skipProtocolProbe) {
|
|
918
|
+
Log?.debug(
|
|
919
|
+
'[ReactNativeBleTransport] refresh cached BLE connection for firmware install:',
|
|
920
|
+
uuid
|
|
921
|
+
);
|
|
922
|
+
const manager = this.blePlxManager;
|
|
923
|
+
await this.releaseUnlocked(uuid, true);
|
|
924
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
925
|
+
const operations: Promise<unknown>[] = [];
|
|
926
|
+
if (manager) {
|
|
927
|
+
operations.push(
|
|
928
|
+
this.runBestEffortNativeOperation(
|
|
929
|
+
'firmware install reconnect: cancel device connection',
|
|
930
|
+
() => manager.cancelDeviceConnection(uuid)
|
|
931
|
+
)
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
operations.push(
|
|
935
|
+
this.runBestEffortNativeOperation(
|
|
936
|
+
'firmware install reconnect: device cancel connection',
|
|
937
|
+
() => cachedTransport.device.cancelConnection()
|
|
938
|
+
)
|
|
939
|
+
);
|
|
940
|
+
await Promise.all(operations);
|
|
941
|
+
});
|
|
942
|
+
} else {
|
|
943
|
+
const cachedProtocol = this.deviceProtocol.get(uuid);
|
|
944
|
+
const isCachedDeviceConnected = await cachedTransport.device
|
|
945
|
+
.isConnected()
|
|
946
|
+
.catch(() => false);
|
|
947
|
+
if (
|
|
948
|
+
isCachedDeviceConnected &&
|
|
949
|
+
cachedProtocol &&
|
|
950
|
+
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
951
|
+
) {
|
|
952
|
+
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
953
|
+
return { uuid, protocolType: cachedProtocol };
|
|
954
|
+
}
|
|
890
955
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
956
|
+
/**
|
|
957
|
+
* If the transport is not reusable due to a protocol mismatch or stale
|
|
958
|
+
* connection, clean it up before creating a new transport instance.
|
|
959
|
+
*/
|
|
960
|
+
Log?.debug('transport not reusable, will release: ', uuid);
|
|
961
|
+
await this.releaseUnlocked(uuid, true);
|
|
962
|
+
}
|
|
897
963
|
}
|
|
898
964
|
|
|
899
965
|
let device: Device | null = null;
|
|
@@ -943,7 +1009,7 @@ export default class ReactNativeBleTransport {
|
|
|
943
1009
|
);
|
|
944
1010
|
} catch (e) {
|
|
945
1011
|
Log?.debug('try to connect to device has error: ', e);
|
|
946
|
-
if (
|
|
1012
|
+
if (shouldRethrowBleSetupError(e)) {
|
|
947
1013
|
throw e;
|
|
948
1014
|
}
|
|
949
1015
|
if (
|
|
@@ -958,7 +1024,7 @@ export default class ReactNativeBleTransport {
|
|
|
958
1024
|
Log?.debug('device already connected');
|
|
959
1025
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
960
1026
|
} else {
|
|
961
|
-
remapError(e);
|
|
1027
|
+
remapError(e, shouldMapProtocolV2StaleBond);
|
|
962
1028
|
}
|
|
963
1029
|
}
|
|
964
1030
|
}
|
|
@@ -977,7 +1043,7 @@ export default class ReactNativeBleTransport {
|
|
|
977
1043
|
);
|
|
978
1044
|
} catch (e) {
|
|
979
1045
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
980
|
-
if (
|
|
1046
|
+
if (shouldRethrowBleSetupError(e)) {
|
|
981
1047
|
throw e;
|
|
982
1048
|
}
|
|
983
1049
|
if (
|
|
@@ -1002,7 +1068,7 @@ export default class ReactNativeBleTransport {
|
|
|
1002
1068
|
}
|
|
1003
1069
|
}
|
|
1004
1070
|
} else {
|
|
1005
|
-
remapError(e);
|
|
1071
|
+
remapError(e, shouldMapProtocolV2StaleBond);
|
|
1006
1072
|
}
|
|
1007
1073
|
}
|
|
1008
1074
|
}
|
|
@@ -1014,22 +1080,56 @@ export default class ReactNativeBleTransport {
|
|
|
1014
1080
|
|
|
1015
1081
|
const protocolHint = expectedProtocol
|
|
1016
1082
|
? undefined
|
|
1017
|
-
: input.protocolHint ??
|
|
1018
|
-
this.deviceProtocolHints.get(uuid) ??
|
|
1019
|
-
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
1083
|
+
: input.protocolHint ?? this.deviceProtocolHints.get(uuid);
|
|
1020
1084
|
|
|
1021
1085
|
// release transport before new transport instance
|
|
1022
|
-
await this.
|
|
1086
|
+
await this.releaseUnlocked(uuid, true);
|
|
1023
1087
|
if (protocolHint) {
|
|
1024
1088
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
1025
1089
|
}
|
|
1026
1090
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
});
|
|
1091
|
+
if (shouldMapProtocolV2StaleBond) {
|
|
1092
|
+
this.acquiringProtocolV2.add(uuid);
|
|
1093
|
+
}
|
|
1031
1094
|
|
|
1032
1095
|
try {
|
|
1096
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
1097
|
+
writeCharacteristic,
|
|
1098
|
+
notifyCharacteristic,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
if (skipProtocolProbe) {
|
|
1102
|
+
if (!expectedProtocol) {
|
|
1103
|
+
throw ERRORS.TypedError(
|
|
1104
|
+
HardwareErrorCode.RuntimeError,
|
|
1105
|
+
'skipProtocolProbe requires an expected BLE protocol'
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
const hasConfirmedProtocol =
|
|
1109
|
+
this.sessionProtocols.get(uuid) === expectedProtocol ||
|
|
1110
|
+
(expectedProtocol === 'V2' && this.confirmedProtocolV2.has(uuid));
|
|
1111
|
+
if (!hasConfirmedProtocol) {
|
|
1112
|
+
throw ERRORS.TypedError(
|
|
1113
|
+
HardwareErrorCode.RuntimeError,
|
|
1114
|
+
'skipProtocolProbe requires a previously confirmed protocol for this BLE endpoint'
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1118
|
+
this.sessionProtocols.set(uuid, expectedProtocol);
|
|
1119
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1120
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected without probe', {
|
|
1121
|
+
deviceId: uuid,
|
|
1122
|
+
protocol: expectedProtocol,
|
|
1123
|
+
source: 'firmware-install-reconnect',
|
|
1124
|
+
});
|
|
1125
|
+
const currentTransport = transportCache[uuid];
|
|
1126
|
+
if (!currentTransport) {
|
|
1127
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1128
|
+
}
|
|
1129
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1130
|
+
return { uuid, protocolType: expectedProtocol };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1033
1133
|
const protocolType = await this.detectProtocol(
|
|
1034
1134
|
uuid,
|
|
1035
1135
|
expectedProtocol,
|
|
@@ -1045,8 +1145,14 @@ export default class ReactNativeBleTransport {
|
|
|
1045
1145
|
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1046
1146
|
return { uuid, protocolType };
|
|
1047
1147
|
} catch (error) {
|
|
1048
|
-
|
|
1148
|
+
if (isBleStaleBondHardwareError(error)) {
|
|
1149
|
+
await this.disconnectUnlocked(uuid);
|
|
1150
|
+
} else {
|
|
1151
|
+
await this.releaseUnlocked(uuid, true);
|
|
1152
|
+
}
|
|
1049
1153
|
throw error;
|
|
1154
|
+
} finally {
|
|
1155
|
+
this.acquiringProtocolV2.delete(uuid);
|
|
1050
1156
|
}
|
|
1051
1157
|
}
|
|
1052
1158
|
|
|
@@ -1074,17 +1180,21 @@ export default class ReactNativeBleTransport {
|
|
|
1074
1180
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
1075
1181
|
return;
|
|
1076
1182
|
}
|
|
1183
|
+
if (
|
|
1184
|
+
(this.getActiveProtocol(uuid) === 'V2' || this.acquiringProtocolV2.has(uuid)) &&
|
|
1185
|
+
isNativeBleStaleBondError(error)
|
|
1186
|
+
) {
|
|
1187
|
+
this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1077
1190
|
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1078
1191
|
let errorCode:
|
|
1079
|
-
| typeof HardwareErrorCode.BleDeviceBondError
|
|
1080
1192
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
1081
1193
|
| typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
1082
1194
|
| typeof HardwareErrorCode.BleTimeoutError =
|
|
1083
1195
|
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
1084
1196
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1085
1197
|
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
1086
|
-
} else if (error.reason?.includes('Encryption is insufficient')) {
|
|
1087
|
-
errorCode = HardwareErrorCode.BleDeviceBondError;
|
|
1088
1198
|
} else if (
|
|
1089
1199
|
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
1090
1200
|
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
@@ -1099,16 +1209,12 @@ export default class ReactNativeBleTransport {
|
|
|
1099
1209
|
}
|
|
1100
1210
|
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
1101
1211
|
let ERROR:
|
|
1102
|
-
| typeof HardwareErrorCode.BleDeviceBondError
|
|
1103
1212
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
1104
1213
|
| typeof HardwareErrorCode.BleTimeoutError =
|
|
1105
1214
|
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
1106
1215
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1107
1216
|
ERROR = HardwareErrorCode.BleTimeoutError;
|
|
1108
1217
|
}
|
|
1109
|
-
if (error.reason?.includes('Encryption is insufficient')) {
|
|
1110
|
-
ERROR = HardwareErrorCode.BleDeviceBondError;
|
|
1111
|
-
}
|
|
1112
1218
|
if (
|
|
1113
1219
|
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
1114
1220
|
error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
@@ -1190,12 +1296,17 @@ export default class ReactNativeBleTransport {
|
|
|
1190
1296
|
}
|
|
1191
1297
|
|
|
1192
1298
|
async release(uuid: string, onclose = false) {
|
|
1299
|
+
return this.runLifecycleOperation(uuid, () => this.releaseUnlocked(uuid, onclose));
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
private async releaseUnlocked(uuid: string, onclose = false) {
|
|
1193
1303
|
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
1194
1304
|
return this.releaseNative(uuid, onclose);
|
|
1195
1305
|
}
|
|
1196
1306
|
|
|
1197
1307
|
private async releaseNative(uuid: string, onclose = false) {
|
|
1198
1308
|
const transport = transportCache[uuid];
|
|
1309
|
+
const manager = this.blePlxManager;
|
|
1199
1310
|
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
1200
1311
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1201
1312
|
this.runPromise.reject(error);
|
|
@@ -1212,8 +1323,6 @@ export default class ReactNativeBleTransport {
|
|
|
1212
1323
|
return Promise.resolve(true);
|
|
1213
1324
|
}
|
|
1214
1325
|
|
|
1215
|
-
await this.restoreAndroidConnectionPriority(uuid, transport);
|
|
1216
|
-
|
|
1217
1326
|
if (transport) {
|
|
1218
1327
|
if (this.monitorTokens.get(uuid) === transport.monitorToken) {
|
|
1219
1328
|
this.monitorTokens.delete(uuid);
|
|
@@ -1231,37 +1340,58 @@ export default class ReactNativeBleTransport {
|
|
|
1231
1340
|
);
|
|
1232
1341
|
transport.notifySubscription?.remove();
|
|
1233
1342
|
transport.notifySubscription = undefined;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
try {
|
|
1237
|
-
await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
|
|
1238
|
-
} catch (e) {
|
|
1239
|
-
Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
|
|
1240
|
-
}
|
|
1343
|
+
if (transportCache[uuid] === transport) {
|
|
1344
|
+
delete transportCache[uuid];
|
|
1241
1345
|
}
|
|
1242
|
-
|
|
1243
|
-
delete transportCache[uuid];
|
|
1244
1346
|
}
|
|
1245
1347
|
|
|
1246
1348
|
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1247
1349
|
|
|
1248
1350
|
this.deviceProtocol.delete(uuid);
|
|
1249
1351
|
this.probingProtocols.delete(uuid);
|
|
1250
|
-
|
|
1352
|
+
this.staleBondErrors.delete(uuid);
|
|
1353
|
+
this.acquiringProtocolV2.delete(uuid);
|
|
1354
|
+
// Confirmed protocol and caller hints stay in deviceProtocol / protocolHint.
|
|
1251
1355
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1252
1356
|
this.protocolV2Assemblers.delete(uuid);
|
|
1253
1357
|
this.resetProtocolV2Frames(uuid);
|
|
1254
1358
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1359
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
1360
|
+
const operations: Promise<unknown>[] = [
|
|
1361
|
+
this.runBestEffortNativeOperation('release: restore connection priority', () =>
|
|
1362
|
+
this.restoreAndroidConnectionPriority(uuid, transport)
|
|
1363
|
+
),
|
|
1364
|
+
];
|
|
1365
|
+
if (transport?.notifyTransactionId && manager) {
|
|
1366
|
+
operations.push(
|
|
1367
|
+
this.runBestEffortNativeOperation('release: cancel notify transaction', () =>
|
|
1368
|
+
manager.cancelTransaction(transport.notifyTransactionId as string)
|
|
1369
|
+
)
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
if (manager) {
|
|
1373
|
+
operations.push(
|
|
1374
|
+
this.runBestEffortNativeOperation('release: cancel transaction', () =>
|
|
1375
|
+
manager.cancelTransaction(uuid)
|
|
1376
|
+
)
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
await Promise.all(operations);
|
|
1380
|
+
});
|
|
1260
1381
|
|
|
1261
1382
|
return Promise.resolve(true);
|
|
1262
1383
|
}
|
|
1263
1384
|
|
|
1264
1385
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
1386
|
+
if (this.getProtocolType(session) === 'V2') {
|
|
1387
|
+
await this.protocolV2Links.sendFlowControl(
|
|
1388
|
+
session,
|
|
1389
|
+
() => this.createProtocolV2Adapter(session),
|
|
1390
|
+
name,
|
|
1391
|
+
data
|
|
1392
|
+
);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1265
1395
|
await this.call(session, name, data);
|
|
1266
1396
|
}
|
|
1267
1397
|
|
|
@@ -1286,8 +1416,6 @@ export default class ReactNativeBleTransport {
|
|
|
1286
1416
|
`Device protocol has not been detected for ${uuid}`
|
|
1287
1417
|
);
|
|
1288
1418
|
}
|
|
1289
|
-
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1290
|
-
|
|
1291
1419
|
if (protocol === 'V2') {
|
|
1292
1420
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1293
1421
|
}
|
|
@@ -1554,8 +1682,13 @@ export default class ReactNativeBleTransport {
|
|
|
1554
1682
|
}
|
|
1555
1683
|
|
|
1556
1684
|
async disconnect(session: string) {
|
|
1685
|
+
return this.runLifecycleOperation(session, () => this.disconnectUnlocked(session));
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
private async disconnectUnlocked(session: string) {
|
|
1557
1689
|
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1558
1690
|
const transport = transportCache[session];
|
|
1691
|
+
const manager = this.blePlxManager;
|
|
1559
1692
|
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1560
1693
|
|
|
1561
1694
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
@@ -1584,37 +1717,13 @@ export default class ReactNativeBleTransport {
|
|
|
1584
1717
|
}
|
|
1585
1718
|
}
|
|
1586
1719
|
|
|
1587
|
-
// cancel the ble transaction
|
|
1588
|
-
if (session) {
|
|
1589
|
-
try {
|
|
1590
|
-
await this.blePlxManager?.cancelTransaction(session);
|
|
1591
|
-
} catch (e) {
|
|
1592
|
-
Log?.debug('resetSession: cancel transaction error (ignored): ', e?.message || e);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// disconnect the device via the device object
|
|
1597
|
-
if (transport?.device) {
|
|
1598
|
-
try {
|
|
1599
|
-
await transport.device.cancelConnection();
|
|
1600
|
-
} catch (e) {
|
|
1601
|
-
Log?.debug('resetSession: device.cancelConnection error (ignored): ', e?.message || e);
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// disconnect the device via the ble manager
|
|
1606
|
-
try {
|
|
1607
|
-
await this.blePlxManager?.cancelDeviceConnection(session);
|
|
1608
|
-
} catch (e) {
|
|
1609
|
-
Log?.debug('resetSession: manager.cancelDeviceConnection error (ignored): ', e?.message || e);
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
1720
|
// clear the transport cache
|
|
1613
|
-
if (transportCache[session]) {
|
|
1721
|
+
if (!transport || transportCache[session] === transport) {
|
|
1614
1722
|
delete transportCache[session];
|
|
1615
1723
|
}
|
|
1616
1724
|
this.deviceProtocol.delete(session);
|
|
1617
1725
|
this.probingProtocols.delete(session);
|
|
1726
|
+
this.staleBondErrors.delete(session);
|
|
1618
1727
|
this.deviceProtocolHints.delete(session);
|
|
1619
1728
|
this.sessionProtocols.delete(session);
|
|
1620
1729
|
this.protocolReprobeFailures.delete(session);
|
|
@@ -1630,10 +1739,97 @@ export default class ReactNativeBleTransport {
|
|
|
1630
1739
|
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1631
1740
|
this.monitorTokens.delete(session);
|
|
1632
1741
|
}
|
|
1742
|
+
|
|
1743
|
+
await this.runNativeTeardown(session, manager, async () => {
|
|
1744
|
+
const operations: Promise<unknown>[] = [];
|
|
1745
|
+
if (manager) {
|
|
1746
|
+
operations.push(
|
|
1747
|
+
this.runBestEffortNativeOperation('disconnect: cancel transaction', () =>
|
|
1748
|
+
manager.cancelTransaction(session)
|
|
1749
|
+
)
|
|
1750
|
+
);
|
|
1751
|
+
operations.push(
|
|
1752
|
+
this.runBestEffortNativeOperation('disconnect: cancel device connection', () =>
|
|
1753
|
+
manager.cancelDeviceConnection(session)
|
|
1754
|
+
)
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
if (transport?.device) {
|
|
1758
|
+
operations.push(
|
|
1759
|
+
this.runBestEffortNativeOperation('disconnect: device cancel connection', () =>
|
|
1760
|
+
transport.device.cancelConnection()
|
|
1761
|
+
)
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
await Promise.all(operations);
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1633
1767
|
// eslint-disable-next-line no-promise-executor-return
|
|
1634
1768
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1635
1769
|
}
|
|
1636
1770
|
|
|
1771
|
+
private async runNativeTeardown(
|
|
1772
|
+
uuid: string,
|
|
1773
|
+
manager: BlePlxManager | undefined,
|
|
1774
|
+
teardown: () => Promise<void>
|
|
1775
|
+
) {
|
|
1776
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1777
|
+
let timedOut = false;
|
|
1778
|
+
const pending = Promise.resolve()
|
|
1779
|
+
.then(teardown)
|
|
1780
|
+
.catch(error => {
|
|
1781
|
+
Log?.debug('BLE native teardown error (ignored): ', error?.message || error);
|
|
1782
|
+
});
|
|
1783
|
+
try {
|
|
1784
|
+
await Promise.race([
|
|
1785
|
+
pending,
|
|
1786
|
+
new Promise<void>(resolve => {
|
|
1787
|
+
timer = setTimeout(() => {
|
|
1788
|
+
timedOut = true;
|
|
1789
|
+
resolve();
|
|
1790
|
+
}, BLE_NATIVE_TEARDOWN_TIMEOUT_MS);
|
|
1791
|
+
}),
|
|
1792
|
+
]);
|
|
1793
|
+
} finally {
|
|
1794
|
+
if (timer) clearTimeout(timer);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (timedOut) {
|
|
1798
|
+
Log?.error('[ReactNativeBleTransport] BLE native teardown timed out:', uuid);
|
|
1799
|
+
if (this.blePlxManager === manager) {
|
|
1800
|
+
this.resetPlxManager();
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
private runBestEffortNativeOperation(label: string, operation: () => Promise<unknown>) {
|
|
1806
|
+
return Promise.resolve()
|
|
1807
|
+
.then(operation)
|
|
1808
|
+
.catch(error => {
|
|
1809
|
+
Log?.debug(`${label} error (ignored): `, error?.message || error);
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
private async runLifecycleOperation<T>(uuid: string, operation: () => Promise<T>): Promise<T> {
|
|
1814
|
+
const previousOperation = this.lifecycleOperations.get(uuid) ?? Promise.resolve();
|
|
1815
|
+
let completeOperation!: () => void;
|
|
1816
|
+
const operationGate = new Promise<void>(resolve => {
|
|
1817
|
+
completeOperation = resolve;
|
|
1818
|
+
});
|
|
1819
|
+
const operationTail = previousOperation.catch(() => undefined).then(() => operationGate);
|
|
1820
|
+
this.lifecycleOperations.set(uuid, operationTail);
|
|
1821
|
+
|
|
1822
|
+
await previousOperation.catch(() => undefined);
|
|
1823
|
+
try {
|
|
1824
|
+
return await operation();
|
|
1825
|
+
} finally {
|
|
1826
|
+
completeOperation();
|
|
1827
|
+
if (this.lifecycleOperations.get(uuid) === operationTail) {
|
|
1828
|
+
this.lifecycleOperations.delete(uuid);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1637
1833
|
cancel() {
|
|
1638
1834
|
Log?.debug('transport-react-native transport cancel');
|
|
1639
1835
|
if (this.runPromise) {
|
|
@@ -1669,7 +1865,13 @@ export default class ReactNativeBleTransport {
|
|
|
1669
1865
|
return result;
|
|
1670
1866
|
} catch (error) {
|
|
1671
1867
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1672
|
-
this.abandonStalledConnection(
|
|
1868
|
+
const resetManager = this.abandonStalledConnection(
|
|
1869
|
+
uuid,
|
|
1870
|
+
timedOut ? 'connect-backstop' : 'connect-native'
|
|
1871
|
+
);
|
|
1872
|
+
if (resetManager) {
|
|
1873
|
+
throw this.createWedgedBleSetupError();
|
|
1874
|
+
}
|
|
1673
1875
|
}
|
|
1674
1876
|
throw error;
|
|
1675
1877
|
} finally {
|
|
@@ -1705,7 +1907,13 @@ export default class ReactNativeBleTransport {
|
|
|
1705
1907
|
return result;
|
|
1706
1908
|
} catch (error) {
|
|
1707
1909
|
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1708
|
-
this.abandonStalledConnection(
|
|
1910
|
+
const resetManager = this.abandonStalledConnection(
|
|
1911
|
+
uuid,
|
|
1912
|
+
timedOut ? 'gatt-backstop' : 'gatt-native'
|
|
1913
|
+
);
|
|
1914
|
+
if (resetManager) {
|
|
1915
|
+
throw this.createWedgedBleSetupError();
|
|
1916
|
+
}
|
|
1709
1917
|
}
|
|
1710
1918
|
throw error;
|
|
1711
1919
|
} finally {
|
|
@@ -1717,11 +1925,12 @@ export default class ReactNativeBleTransport {
|
|
|
1717
1925
|
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1718
1926
|
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1719
1927
|
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1928
|
+
* Returns true when the manager itself was reset so the caller can stop Core retries.
|
|
1720
1929
|
*/
|
|
1721
1930
|
private abandonStalledConnection(
|
|
1722
1931
|
uuid: string,
|
|
1723
1932
|
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1724
|
-
) {
|
|
1933
|
+
): boolean {
|
|
1725
1934
|
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1726
1935
|
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1727
1936
|
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
@@ -1738,6 +1947,8 @@ export default class ReactNativeBleTransport {
|
|
|
1738
1947
|
}
|
|
1739
1948
|
this.deviceProtocol.delete(uuid);
|
|
1740
1949
|
this.probingProtocols.delete(uuid);
|
|
1950
|
+
this.staleBondErrors.delete(uuid);
|
|
1951
|
+
this.acquiringProtocolV2.delete(uuid);
|
|
1741
1952
|
this.protocolV2Assemblers.delete(uuid);
|
|
1742
1953
|
this.resetProtocolV2Frames(uuid);
|
|
1743
1954
|
|
|
@@ -1747,7 +1958,15 @@ export default class ReactNativeBleTransport {
|
|
|
1747
1958
|
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1748
1959
|
this.resetPlxManager();
|
|
1749
1960
|
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1961
|
+
return true;
|
|
1750
1962
|
}
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
private createWedgedBleSetupError() {
|
|
1967
|
+
// PollingTimeout is not retried by connectDeviceForBle and already maps
|
|
1968
|
+
// to the App "connection failed" help text.
|
|
1969
|
+
return ERRORS.TypedError(HardwareErrorCode.PollingTimeout, BLE_SETUP_WEDGED_MESSAGE);
|
|
1751
1970
|
}
|
|
1752
1971
|
|
|
1753
1972
|
private getCachedTransport(uuid: string) {
|
|
@@ -1826,6 +2045,8 @@ export default class ReactNativeBleTransport {
|
|
|
1826
2045
|
}
|
|
1827
2046
|
this.deviceProtocol.delete(uuid);
|
|
1828
2047
|
this.probingProtocols.delete(uuid);
|
|
2048
|
+
this.staleBondErrors.delete(uuid);
|
|
2049
|
+
this.acquiringProtocolV2.delete(uuid);
|
|
1829
2050
|
this.protocolV2Assemblers.delete(uuid);
|
|
1830
2051
|
this.resetProtocolV2Frames(uuid);
|
|
1831
2052
|
|
|
@@ -1842,13 +2063,45 @@ export default class ReactNativeBleTransport {
|
|
|
1842
2063
|
private resetPlxManager() {
|
|
1843
2064
|
const manager = this.blePlxManager;
|
|
1844
2065
|
this.blePlxManager = undefined;
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2066
|
+
const reason = 'React Native BLE manager reset';
|
|
2067
|
+
// Destroying the shared manager invalidates every peripheral it owns. Notify
|
|
2068
|
+
// each cached session before clearing generations so Core cannot retain a
|
|
2069
|
+
// silently stale connection for an unrelated device.
|
|
2070
|
+
Object.entries(transportCache).forEach(([uuid, cachedTransport]) => {
|
|
2071
|
+
try {
|
|
2072
|
+
cachedTransport.disconnectSubscription?.remove();
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
Log?.debug('BLE manager reset disconnect subscription removal failed:', error);
|
|
2075
|
+
}
|
|
2076
|
+
cachedTransport.disconnectSubscription = undefined;
|
|
2077
|
+
try {
|
|
2078
|
+
cachedTransport.notifySubscription?.remove();
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
Log?.debug('BLE manager reset notify subscription removal failed:', error);
|
|
2081
|
+
}
|
|
2082
|
+
cachedTransport.notifySubscription = undefined;
|
|
2083
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
2084
|
+
try {
|
|
2085
|
+
this.emitDeviceDisconnect(
|
|
2086
|
+
uuid,
|
|
2087
|
+
cachedTransport.device?.name,
|
|
2088
|
+
cachedTransport.monitorToken ?? this.monitorTokens.get(uuid)
|
|
2089
|
+
);
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
Log?.debug('BLE manager reset disconnect event failed:', error);
|
|
2092
|
+
}
|
|
2093
|
+
delete transportCache[uuid];
|
|
2094
|
+
});
|
|
2095
|
+
this.protocolV2Links.invalidateAllLinks(reason).catch(error => {
|
|
2096
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager link invalidation failed:', error);
|
|
1848
2097
|
});
|
|
1849
2098
|
this.deviceProtocol.clear();
|
|
1850
2099
|
this.probingProtocols.clear();
|
|
2100
|
+
this.staleBondErrors.clear();
|
|
2101
|
+
this.acquiringProtocolV2.clear();
|
|
1851
2102
|
this.sessionProtocols.clear();
|
|
2103
|
+
// Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
|
|
2104
|
+
// firmware reconnect after the native BLE manager is recreated.
|
|
1852
2105
|
this.protocolReprobeFailures.clear();
|
|
1853
2106
|
this.writeTimeoutCounts.clear();
|
|
1854
2107
|
this.connectionSetupTimeoutCounts.clear();
|
|
@@ -1862,6 +2115,8 @@ export default class ReactNativeBleTransport {
|
|
|
1862
2115
|
}
|
|
1863
2116
|
|
|
1864
2117
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
2118
|
+
// A protocol probe miss alone does not prove that the OS bond is stale.
|
|
2119
|
+
// Native authentication/encryption failures are mapped separately.
|
|
1865
2120
|
return ERRORS.TypedError(
|
|
1866
2121
|
HardwareErrorCode.RuntimeError,
|
|
1867
2122
|
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
@@ -1895,34 +2150,35 @@ export default class ReactNativeBleTransport {
|
|
|
1895
2150
|
protocolHint?: ProtocolType,
|
|
1896
2151
|
rebuildTransport?: () => Promise<void>
|
|
1897
2152
|
): Promise<ProtocolType> {
|
|
1898
|
-
|
|
1899
|
-
|
|
2153
|
+
// A declared V1 is taken at face value on every platform, as iOS has done
|
|
2154
|
+
// since protocol probing arrived: the caller reads the protocol off its own
|
|
2155
|
+
// device record, so the probe re-asks a question that is already answered
|
|
2156
|
+
// and costs a round trip on every acquire. Expected V2 must still Ping so
|
|
2157
|
+
// USB-priority `link disabled` surfaces here instead of as a later unmapped
|
|
2158
|
+
// RuntimeError. sessionProtocols is deliberately NOT stamped here: that map
|
|
2159
|
+
// records protocols the device actually answered on (it gates the
|
|
2160
|
+
// trustSessionProtocol narrowing in forced detection), and this branch has
|
|
2161
|
+
// received no response. skipProtocolProbe does not need it either — its only
|
|
2162
|
+
// caller is the V2 firmware-install reconnect.
|
|
2163
|
+
if (expectedProtocol === 'V1') {
|
|
2164
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1900
2165
|
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1901
2166
|
deviceId: uuid,
|
|
1902
|
-
protocol:
|
|
2167
|
+
protocol: 'V1',
|
|
1903
2168
|
source: 'expected',
|
|
1904
2169
|
});
|
|
1905
|
-
return
|
|
2170
|
+
return 'V1';
|
|
1906
2171
|
}
|
|
1907
2172
|
|
|
1908
|
-
if (expectedProtocol === '
|
|
1909
|
-
|
|
1910
|
-
this.deviceProtocol.set(uuid, 'V1');
|
|
1911
|
-
this.sessionProtocols.set(uuid, 'V1');
|
|
1912
|
-
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1913
|
-
deviceId: uuid,
|
|
1914
|
-
protocol: 'V1',
|
|
1915
|
-
source: 'expected',
|
|
1916
|
-
});
|
|
1917
|
-
return 'V1';
|
|
1918
|
-
}
|
|
1919
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
2173
|
+
if (expectedProtocol === 'V2' || this.acquiringProtocolV2.has(uuid)) {
|
|
2174
|
+
this.throwIfStaleBondError(uuid);
|
|
1920
2175
|
}
|
|
1921
2176
|
|
|
1922
2177
|
if (expectedProtocol === 'V2') {
|
|
1923
2178
|
if (await this.probeProtocolV2(uuid)) {
|
|
1924
2179
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1925
2180
|
this.sessionProtocols.set(uuid, 'V2');
|
|
2181
|
+
this.confirmedProtocolV2.add(uuid);
|
|
1926
2182
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1927
2183
|
deviceId: uuid,
|
|
1928
2184
|
protocol: 'V2',
|
|
@@ -1965,6 +2221,9 @@ export default class ReactNativeBleTransport {
|
|
|
1965
2221
|
if (detected) {
|
|
1966
2222
|
this.deviceProtocol.set(uuid, protocol);
|
|
1967
2223
|
this.sessionProtocols.set(uuid, protocol);
|
|
2224
|
+
if (protocol === 'V2') {
|
|
2225
|
+
this.confirmedProtocolV2.add(uuid);
|
|
2226
|
+
}
|
|
1968
2227
|
this.protocolReprobeFailures.delete(uuid);
|
|
1969
2228
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1970
2229
|
deviceId: uuid,
|
|
@@ -2071,6 +2330,7 @@ export default class ReactNativeBleTransport {
|
|
|
2071
2330
|
|
|
2072
2331
|
this.probingProtocols.set(uuid, 'V2');
|
|
2073
2332
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2333
|
+
this.throwIfStaleBondError(uuid);
|
|
2074
2334
|
const detected = await probeProtocolV2Helper({
|
|
2075
2335
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
2076
2336
|
this.callProtocolV2(uuid, name, data, options),
|
|
@@ -2081,6 +2341,7 @@ export default class ReactNativeBleTransport {
|
|
|
2081
2341
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2082
2342
|
this.resetProtocolV2Frames(uuid);
|
|
2083
2343
|
},
|
|
2344
|
+
shouldRethrow: isBleStaleBondHardwareError,
|
|
2084
2345
|
});
|
|
2085
2346
|
if (!detected) {
|
|
2086
2347
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -2149,6 +2410,21 @@ export default class ReactNativeBleTransport {
|
|
|
2149
2410
|
}
|
|
2150
2411
|
}
|
|
2151
2412
|
|
|
2413
|
+
private rememberStaleBondError(uuid: string, error: Error) {
|
|
2414
|
+
this.staleBondErrors.set(uuid, error);
|
|
2415
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
2416
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
2417
|
+
this.runPromise.reject(error);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
private throwIfStaleBondError(uuid: string) {
|
|
2422
|
+
const error = this.staleBondErrors.get(uuid);
|
|
2423
|
+
if (error) {
|
|
2424
|
+
throw error;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2152
2428
|
private async readProtocolV2Frame(uuid: string) {
|
|
2153
2429
|
const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
|
|
2154
2430
|
if (queuedFrame) {
|
|
@@ -2207,6 +2483,11 @@ export default class ReactNativeBleTransport {
|
|
|
2207
2483
|
assertCurrentGeneration();
|
|
2208
2484
|
return;
|
|
2209
2485
|
} catch (error) {
|
|
2486
|
+
if (isNativeBleStaleBondError(error) || isBleStaleBondHardwareError(error)) {
|
|
2487
|
+
const bondError = toBleStaleBondHardwareError(error);
|
|
2488
|
+
this.rememberStaleBondError(uuid, bondError);
|
|
2489
|
+
throw bondError;
|
|
2490
|
+
}
|
|
2210
2491
|
if (
|
|
2211
2492
|
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2212
2493
|
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
@@ -2443,6 +2724,7 @@ export default class ReactNativeBleTransport {
|
|
|
2443
2724
|
return rxFrame;
|
|
2444
2725
|
},
|
|
2445
2726
|
reset: (reason: string) => {
|
|
2727
|
+
if (this.monitorTokens.get(uuid) !== generation) return;
|
|
2446
2728
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2447
2729
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
2448
2730
|
},
|