@onekeyfe/hd-transport-react-native 1.2.0-alpha.66 → 1.2.0-alpha.67
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/BleTransport.d.ts.map +1 -1
- package/dist/index.d.ts +79 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +308 -109
- package/jest.config.js +5 -0
- package/package.json +5 -5
- package/src/BleTransport.ts +0 -6
- package/src/__tests__/BleTransport.test.ts +2 -54
- package/src/__tests__/connectTimeout.test.ts +281 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +48 -281
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/index.ts +451 -112
- package/src/__tests__/enumerate.test.ts +0 -132
package/src/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
HardwareErrorCode,
|
|
29
29
|
createDeferred,
|
|
30
30
|
isOnekeyBluetoothDevice,
|
|
31
|
-
isPro2FindMyAdvertisementName,
|
|
32
31
|
} from '@onekeyfe/hd-shared';
|
|
33
32
|
|
|
34
33
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
@@ -61,7 +60,6 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
61
60
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
62
61
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
63
62
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
64
|
-
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
65
63
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
66
64
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
67
65
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
@@ -73,33 +71,6 @@ type ResolvedBleCharacteristics = {
|
|
|
73
71
|
notifyCharacteristic: Characteristic;
|
|
74
72
|
};
|
|
75
73
|
|
|
76
|
-
const isAsciiWhitespace = (code: number) =>
|
|
77
|
-
code === 0x09 ||
|
|
78
|
-
code === 0x0a ||
|
|
79
|
-
code === 0x0b ||
|
|
80
|
-
code === 0x0c ||
|
|
81
|
-
code === 0x0d ||
|
|
82
|
-
code === 0x20;
|
|
83
|
-
|
|
84
|
-
const hasGattCongestedStatus = (text: string) => {
|
|
85
|
-
let searchFrom = 0;
|
|
86
|
-
while (searchFrom < text.length) {
|
|
87
|
-
const statusIndex = text.indexOf('status', searchFrom);
|
|
88
|
-
if (statusIndex < 0) return false;
|
|
89
|
-
|
|
90
|
-
let cursor = statusIndex + 'status'.length;
|
|
91
|
-
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
92
|
-
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
93
|
-
cursor += 1;
|
|
94
|
-
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
95
|
-
}
|
|
96
|
-
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
97
|
-
|
|
98
|
-
searchFrom = statusIndex + 'status'.length;
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
74
|
const delay = (ms: number) =>
|
|
104
75
|
new Promise<void>(resolve => {
|
|
105
76
|
setTimeout(resolve, ms);
|
|
@@ -128,13 +99,29 @@ export const getFirmwareUploadWriteRetryType = (
|
|
|
128
99
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
129
100
|
.filter(value => typeof value === 'string')
|
|
130
101
|
.join(' ');
|
|
131
|
-
return
|
|
102
|
+
return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
|
|
132
103
|
};
|
|
133
104
|
|
|
134
105
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
135
106
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
136
107
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
137
108
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
109
|
+
/**
|
|
110
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
111
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
112
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
113
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
114
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
115
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
116
|
+
*/
|
|
117
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
118
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
119
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
120
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
121
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
122
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
123
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
124
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
138
125
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
139
126
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
140
127
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -191,12 +178,52 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
191
178
|
|
|
192
179
|
const ANDROID_REQUEST_MTU = 256;
|
|
193
180
|
|
|
181
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
182
|
+
|
|
194
183
|
const connectOptions: Record<string, unknown> = {
|
|
195
184
|
requestMTU: ANDROID_REQUEST_MTU,
|
|
196
|
-
timeout:
|
|
185
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
197
186
|
refreshGatt: 'OnConnected',
|
|
198
187
|
};
|
|
199
188
|
|
|
189
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
190
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
191
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
196
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
197
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
198
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
199
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
200
|
+
*/
|
|
201
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
202
|
+
/**
|
|
203
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
204
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
205
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
206
|
+
* budget.
|
|
207
|
+
*/
|
|
208
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
209
|
+
/**
|
|
210
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
211
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
212
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
213
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
214
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
215
|
+
*/
|
|
216
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
217
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
218
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
219
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
220
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
221
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
222
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
223
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
224
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
225
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
226
|
+
|
|
200
227
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
201
228
|
|
|
202
229
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -285,8 +312,28 @@ export default class ReactNativeBleTransport {
|
|
|
285
312
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
286
313
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
287
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
317
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
318
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
319
|
+
* transport that was never validated.
|
|
320
|
+
*/
|
|
321
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
322
|
+
|
|
323
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
324
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
325
|
+
|
|
326
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
327
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
328
|
+
|
|
288
329
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
289
330
|
|
|
331
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
332
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
333
|
+
|
|
334
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
335
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
336
|
+
|
|
290
337
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
291
338
|
|
|
292
339
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -504,22 +551,21 @@ export default class ReactNativeBleTransport {
|
|
|
504
551
|
const isConnected = await device.isConnected().catch(() => false);
|
|
505
552
|
if (!isConnected) {
|
|
506
553
|
try {
|
|
507
|
-
device = await device.connect(connectOptions);
|
|
554
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
508
555
|
} catch (e) {
|
|
509
556
|
if (
|
|
510
557
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
511
558
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
512
559
|
) {
|
|
513
|
-
device = await device.connect();
|
|
560
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
514
561
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
515
562
|
throw e;
|
|
516
563
|
}
|
|
517
564
|
}
|
|
518
565
|
}
|
|
519
566
|
|
|
520
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
521
|
-
device
|
|
522
|
-
);
|
|
567
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
568
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
523
569
|
|
|
524
570
|
transport.device = device;
|
|
525
571
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -609,21 +655,12 @@ export default class ReactNativeBleTransport {
|
|
|
609
655
|
}
|
|
610
656
|
|
|
611
657
|
const displayName = getDeviceDisplayName(device);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const isOneKey =
|
|
619
|
-
!isUnnamedIOSPeripheral &&
|
|
620
|
-
!isFindMyPeripheral &&
|
|
621
|
-
isOnekeyBluetoothDevice({
|
|
622
|
-
id: device?.id,
|
|
623
|
-
name: device?.name,
|
|
624
|
-
localName: device?.localName,
|
|
625
|
-
serviceUuids: device?.serviceUUIDs,
|
|
626
|
-
});
|
|
658
|
+
const isOneKey = isOnekeyBluetoothDevice({
|
|
659
|
+
id: device?.id,
|
|
660
|
+
name: device?.name,
|
|
661
|
+
localName: device?.localName,
|
|
662
|
+
serviceUuids: device?.serviceUUIDs,
|
|
663
|
+
});
|
|
627
664
|
if (isOneKey) {
|
|
628
665
|
addDevice(device as unknown as Device);
|
|
629
666
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -644,12 +681,7 @@ export default class ReactNativeBleTransport {
|
|
|
644
681
|
'localName' in device && typeof device.localName === 'string'
|
|
645
682
|
? device.localName
|
|
646
683
|
: null;
|
|
647
|
-
const isFindMyPeripheral =
|
|
648
|
-
isPro2FindMyAdvertisementName(device.name) ||
|
|
649
|
-
isPro2FindMyAdvertisementName(localName);
|
|
650
|
-
|
|
651
684
|
if (
|
|
652
|
-
!isFindMyPeripheral &&
|
|
653
685
|
isOnekeyBluetoothDevice({
|
|
654
686
|
id: device.id,
|
|
655
687
|
name: device.name,
|
|
@@ -698,7 +730,7 @@ export default class ReactNativeBleTransport {
|
|
|
698
730
|
characteristics?: ResolvedBleCharacteristics
|
|
699
731
|
) {
|
|
700
732
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
701
|
-
characteristics ?? (await this.
|
|
733
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
702
734
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
703
735
|
if (Platform.OS === 'android') {
|
|
704
736
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
@@ -802,15 +834,22 @@ export default class ReactNativeBleTransport {
|
|
|
802
834
|
if (!device) {
|
|
803
835
|
Log?.debug('try to connect to device: ', uuid);
|
|
804
836
|
try {
|
|
805
|
-
device = await
|
|
837
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
838
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
839
|
+
);
|
|
806
840
|
} catch (e) {
|
|
807
841
|
Log?.debug('try to connect to device has error: ', e);
|
|
842
|
+
if (isConnectTimeoutError(e)) {
|
|
843
|
+
throw e;
|
|
844
|
+
}
|
|
808
845
|
if (
|
|
809
846
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
810
847
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
811
848
|
) {
|
|
812
849
|
Log?.debug('first try to reconnect without params');
|
|
813
|
-
device = await
|
|
850
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
851
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
852
|
+
);
|
|
814
853
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
815
854
|
Log?.debug('device already connected');
|
|
816
855
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -826,26 +865,36 @@ export default class ReactNativeBleTransport {
|
|
|
826
865
|
|
|
827
866
|
if (!(await device.isConnected())) {
|
|
828
867
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
868
|
+
const disconnectedDevice = device;
|
|
829
869
|
|
|
830
870
|
try {
|
|
831
|
-
device = await
|
|
871
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
872
|
+
disconnectedDevice.connect(connectOptions)
|
|
873
|
+
);
|
|
832
874
|
} catch (e) {
|
|
833
875
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
876
|
+
if (isConnectTimeoutError(e)) {
|
|
877
|
+
throw e;
|
|
878
|
+
}
|
|
834
879
|
if (
|
|
835
880
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
836
881
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
837
882
|
) {
|
|
838
883
|
Log?.debug('second try to reconnect without params');
|
|
839
884
|
try {
|
|
840
|
-
device = await
|
|
885
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
886
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
887
|
+
);
|
|
841
888
|
} catch (e) {
|
|
842
889
|
Log?.debug('last try to reconnect error: ', e);
|
|
843
890
|
// last try to reconnect device if this issue exists
|
|
844
891
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
845
892
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
846
893
|
Log?.debug('last try to reconnect');
|
|
847
|
-
await
|
|
848
|
-
device = await
|
|
894
|
+
await disconnectedDevice.cancelConnection();
|
|
895
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
896
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
897
|
+
);
|
|
849
898
|
}
|
|
850
899
|
}
|
|
851
900
|
} else {
|
|
@@ -856,9 +905,8 @@ export default class ReactNativeBleTransport {
|
|
|
856
905
|
|
|
857
906
|
device = await requestAndroidMtu(device);
|
|
858
907
|
const acquiredDevice = device;
|
|
859
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
860
|
-
acquiredDevice
|
|
861
|
-
);
|
|
908
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
909
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
862
910
|
|
|
863
911
|
const protocolHint = expectedProtocol
|
|
864
912
|
? undefined
|
|
@@ -922,7 +970,7 @@ export default class ReactNativeBleTransport {
|
|
|
922
970
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
923
971
|
return;
|
|
924
972
|
}
|
|
925
|
-
if (this.
|
|
973
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
926
974
|
let errorCode:
|
|
927
975
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
928
976
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -992,7 +1040,7 @@ export default class ReactNativeBleTransport {
|
|
|
992
1040
|
|
|
993
1041
|
try {
|
|
994
1042
|
const data = Buffer.from(c.value as string, 'base64');
|
|
995
|
-
const protocol = this.
|
|
1043
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
996
1044
|
if (!protocol) {
|
|
997
1045
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
998
1046
|
return;
|
|
@@ -1026,7 +1074,7 @@ export default class ReactNativeBleTransport {
|
|
|
1026
1074
|
} catch (error) {
|
|
1027
1075
|
Log?.debug('monitor data error: ', error);
|
|
1028
1076
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1029
|
-
if (this.
|
|
1077
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1030
1078
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1031
1079
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
1032
1080
|
this.runPromise?.reject(notifyError);
|
|
@@ -1090,6 +1138,7 @@ export default class ReactNativeBleTransport {
|
|
|
1090
1138
|
}
|
|
1091
1139
|
|
|
1092
1140
|
this.deviceProtocol.delete(uuid);
|
|
1141
|
+
this.probingProtocols.delete(uuid);
|
|
1093
1142
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1094
1143
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1095
1144
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1156,8 +1205,25 @@ export default class ReactNativeBleTransport {
|
|
|
1156
1205
|
const transport = this.getCachedTransport(uuid);
|
|
1157
1206
|
const runPromise = createDeferred<string>();
|
|
1158
1207
|
runPromise.promise.catch(() => undefined);
|
|
1208
|
+
const supersededRunPromise = this.runPromise;
|
|
1209
|
+
if (supersededRunPromise) {
|
|
1210
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1211
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1212
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1213
|
+
// tear down the shared connection while another call is using it.
|
|
1214
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1215
|
+
}
|
|
1159
1216
|
this.runPromise = runPromise;
|
|
1160
1217
|
this.runPromiseDeviceId = uuid;
|
|
1218
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1219
|
+
// only the call that still owns the slot may release it.
|
|
1220
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1221
|
+
if (this.runPromise === runPromise) {
|
|
1222
|
+
this.runPromise = null;
|
|
1223
|
+
this.runPromiseDeviceId = null;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1161
1227
|
const messages = this._messages;
|
|
1162
1228
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1163
1229
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1183,6 +1249,9 @@ export default class ReactNativeBleTransport {
|
|
|
1183
1249
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1184
1250
|
} catch (e) {
|
|
1185
1251
|
onError(e);
|
|
1252
|
+
if (isWedgedWriteError(e)) {
|
|
1253
|
+
throw e;
|
|
1254
|
+
}
|
|
1186
1255
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1187
1256
|
}
|
|
1188
1257
|
}
|
|
@@ -1214,6 +1283,9 @@ export default class ReactNativeBleTransport {
|
|
|
1214
1283
|
}
|
|
1215
1284
|
} catch (e) {
|
|
1216
1285
|
onError(e);
|
|
1286
|
+
if (isWedgedWriteError(e)) {
|
|
1287
|
+
throw e;
|
|
1288
|
+
}
|
|
1217
1289
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1218
1290
|
}
|
|
1219
1291
|
}
|
|
@@ -1227,9 +1299,15 @@ export default class ReactNativeBleTransport {
|
|
|
1227
1299
|
if (name === 'EmmcFileWrite') {
|
|
1228
1300
|
await writeChunkedData(
|
|
1229
1301
|
buffers,
|
|
1230
|
-
data =>
|
|
1302
|
+
data =>
|
|
1303
|
+
this.writeBlePacket(
|
|
1304
|
+
uuid,
|
|
1305
|
+
data,
|
|
1306
|
+
payload => transport.writeWithRetry(payload),
|
|
1307
|
+
isCurrentOwner
|
|
1308
|
+
),
|
|
1231
1309
|
e => {
|
|
1232
|
-
|
|
1310
|
+
releaseOwnershipIfCurrent();
|
|
1233
1311
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1234
1312
|
}
|
|
1235
1313
|
);
|
|
@@ -1250,7 +1328,12 @@ export default class ReactNativeBleTransport {
|
|
|
1250
1328
|
// eslint-disable-next-line no-constant-condition
|
|
1251
1329
|
while (true) {
|
|
1252
1330
|
try {
|
|
1253
|
-
await
|
|
1331
|
+
await this.writeBlePacket(
|
|
1332
|
+
uuid,
|
|
1333
|
+
data,
|
|
1334
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1335
|
+
isCurrentOwner
|
|
1336
|
+
);
|
|
1254
1337
|
return;
|
|
1255
1338
|
} catch (error) {
|
|
1256
1339
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
@@ -1269,7 +1352,7 @@ export default class ReactNativeBleTransport {
|
|
|
1269
1352
|
}
|
|
1270
1353
|
},
|
|
1271
1354
|
e => {
|
|
1272
|
-
|
|
1355
|
+
releaseOwnershipIfCurrent();
|
|
1273
1356
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1274
1357
|
}
|
|
1275
1358
|
);
|
|
@@ -1278,16 +1361,18 @@ export default class ReactNativeBleTransport {
|
|
|
1278
1361
|
const outData = o.toString('base64');
|
|
1279
1362
|
// Upload resources on low-end phones may OOM
|
|
1280
1363
|
try {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1364
|
+
await this.writeBlePacket(
|
|
1365
|
+
uuid,
|
|
1366
|
+
outData,
|
|
1367
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1368
|
+
isCurrentOwner
|
|
1369
|
+
);
|
|
1288
1370
|
} catch (e) {
|
|
1289
1371
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1290
|
-
|
|
1372
|
+
releaseOwnershipIfCurrent();
|
|
1373
|
+
if (isWedgedWriteError(e)) {
|
|
1374
|
+
throw e;
|
|
1375
|
+
}
|
|
1291
1376
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1292
1377
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1293
1378
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1330,8 +1415,13 @@ export default class ReactNativeBleTransport {
|
|
|
1330
1415
|
}
|
|
1331
1416
|
const isProbeTimeout =
|
|
1332
1417
|
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1418
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1419
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1420
|
+
// call is actively using.
|
|
1421
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1333
1422
|
if (
|
|
1334
1423
|
!isProbeTimeout &&
|
|
1424
|
+
!isStaleCall &&
|
|
1335
1425
|
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1336
1426
|
) {
|
|
1337
1427
|
await this.disconnect(uuid);
|
|
@@ -1411,7 +1501,10 @@ export default class ReactNativeBleTransport {
|
|
|
1411
1501
|
delete transportCache[session];
|
|
1412
1502
|
}
|
|
1413
1503
|
this.deviceProtocol.delete(session);
|
|
1504
|
+
this.probingProtocols.delete(session);
|
|
1414
1505
|
this.deviceProtocolHints.delete(session);
|
|
1506
|
+
this.sessionProtocols.delete(session);
|
|
1507
|
+
this.protocolReprobeFailures.delete(session);
|
|
1415
1508
|
this.protocolV2Assemblers.delete(session);
|
|
1416
1509
|
this.resetProtocolV2Frames(session);
|
|
1417
1510
|
|
|
@@ -1437,6 +1530,113 @@ export default class ReactNativeBleTransport {
|
|
|
1437
1530
|
this.runPromiseDeviceId = null;
|
|
1438
1531
|
}
|
|
1439
1532
|
|
|
1533
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1534
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1535
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1536
|
+
let timedOut = false;
|
|
1537
|
+
const pending = connect();
|
|
1538
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1539
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1540
|
+
pending.catch(() => undefined);
|
|
1541
|
+
try {
|
|
1542
|
+
const result = await Promise.race([
|
|
1543
|
+
pending,
|
|
1544
|
+
new Promise<never>((_, reject) => {
|
|
1545
|
+
timer = setTimeout(() => {
|
|
1546
|
+
timedOut = true;
|
|
1547
|
+
reject(
|
|
1548
|
+
ERRORS.TypedError(
|
|
1549
|
+
HardwareErrorCode.BleConnectedError,
|
|
1550
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1551
|
+
)
|
|
1552
|
+
);
|
|
1553
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1554
|
+
}),
|
|
1555
|
+
]);
|
|
1556
|
+
return result;
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1559
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1560
|
+
}
|
|
1561
|
+
throw error;
|
|
1562
|
+
} finally {
|
|
1563
|
+
if (timer) clearTimeout(timer);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1568
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1569
|
+
uuid: string,
|
|
1570
|
+
device: Device
|
|
1571
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1572
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1573
|
+
let timedOut = false;
|
|
1574
|
+
const pending = this.resolveCharacteristics(device);
|
|
1575
|
+
pending.catch(() => undefined);
|
|
1576
|
+
try {
|
|
1577
|
+
const result = await Promise.race([
|
|
1578
|
+
pending,
|
|
1579
|
+
new Promise<never>((_, reject) => {
|
|
1580
|
+
timer = setTimeout(() => {
|
|
1581
|
+
timedOut = true;
|
|
1582
|
+
reject(
|
|
1583
|
+
ERRORS.TypedError(
|
|
1584
|
+
HardwareErrorCode.BleConnectedError,
|
|
1585
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1586
|
+
)
|
|
1587
|
+
);
|
|
1588
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1589
|
+
}),
|
|
1590
|
+
]);
|
|
1591
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1592
|
+
return result;
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1595
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1596
|
+
}
|
|
1597
|
+
throw error;
|
|
1598
|
+
} finally {
|
|
1599
|
+
if (timer) clearTimeout(timer);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1605
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1606
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1607
|
+
*/
|
|
1608
|
+
private abandonStalledConnection(
|
|
1609
|
+
uuid: string,
|
|
1610
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1611
|
+
) {
|
|
1612
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1613
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1614
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1615
|
+
stage,
|
|
1616
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1620
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1621
|
+
});
|
|
1622
|
+
const stalled = transportCache[uuid];
|
|
1623
|
+
if (stalled) {
|
|
1624
|
+
delete transportCache[uuid];
|
|
1625
|
+
}
|
|
1626
|
+
this.deviceProtocol.delete(uuid);
|
|
1627
|
+
this.probingProtocols.delete(uuid);
|
|
1628
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1629
|
+
this.resetProtocolV2Frames(uuid);
|
|
1630
|
+
|
|
1631
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1632
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1633
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1634
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1635
|
+
this.resetPlxManager();
|
|
1636
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1440
1640
|
private getCachedTransport(uuid: string) {
|
|
1441
1641
|
const transport = transportCache[uuid];
|
|
1442
1642
|
if (!transport) {
|
|
@@ -1445,6 +1645,107 @@ export default class ReactNativeBleTransport {
|
|
|
1445
1645
|
return transport;
|
|
1446
1646
|
}
|
|
1447
1647
|
|
|
1648
|
+
/**
|
|
1649
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1650
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1651
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1652
|
+
* cached and every later call would hang on it again.
|
|
1653
|
+
*/
|
|
1654
|
+
private async writeBlePacket(
|
|
1655
|
+
uuid: string,
|
|
1656
|
+
data: string,
|
|
1657
|
+
write: (payload: string) => Promise<unknown>,
|
|
1658
|
+
isCurrentOwner?: () => boolean
|
|
1659
|
+
) {
|
|
1660
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1661
|
+
let timedOut = false;
|
|
1662
|
+
try {
|
|
1663
|
+
await Promise.race([
|
|
1664
|
+
write(data),
|
|
1665
|
+
new Promise<never>((_, reject) => {
|
|
1666
|
+
timer = setTimeout(() => {
|
|
1667
|
+
timedOut = true;
|
|
1668
|
+
reject(
|
|
1669
|
+
ERRORS.TypedError(
|
|
1670
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1671
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1672
|
+
)
|
|
1673
|
+
);
|
|
1674
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1675
|
+
}),
|
|
1676
|
+
]);
|
|
1677
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
if (timedOut) {
|
|
1680
|
+
// A superseded call's late write must not tear down the link the current
|
|
1681
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1682
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1683
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1684
|
+
} else {
|
|
1685
|
+
this.tearDownWedgedLink(uuid);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
throw error;
|
|
1689
|
+
} finally {
|
|
1690
|
+
if (timer) clearTimeout(timer);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1696
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1697
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1698
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1699
|
+
*/
|
|
1700
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1701
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1702
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1703
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1704
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
const wedged = transportCache[uuid];
|
|
1708
|
+
this.disconnect(uuid).catch(error => {
|
|
1709
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1710
|
+
});
|
|
1711
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1712
|
+
delete transportCache[uuid];
|
|
1713
|
+
}
|
|
1714
|
+
this.deviceProtocol.delete(uuid);
|
|
1715
|
+
this.probingProtocols.delete(uuid);
|
|
1716
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1717
|
+
this.resetProtocolV2Frames(uuid);
|
|
1718
|
+
|
|
1719
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1720
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1721
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1722
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1723
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1724
|
+
this.resetPlxManager();
|
|
1725
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
private resetPlxManager() {
|
|
1730
|
+
const manager = this.blePlxManager;
|
|
1731
|
+
this.blePlxManager = undefined;
|
|
1732
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1733
|
+
Object.keys(transportCache).forEach(key => {
|
|
1734
|
+
delete transportCache[key];
|
|
1735
|
+
});
|
|
1736
|
+
this.deviceProtocol.clear();
|
|
1737
|
+
this.probingProtocols.clear();
|
|
1738
|
+
this.sessionProtocols.clear();
|
|
1739
|
+
this.protocolReprobeFailures.clear();
|
|
1740
|
+
this.monitorTokens.clear();
|
|
1741
|
+
this.protocolV2Assemblers.clear();
|
|
1742
|
+
try {
|
|
1743
|
+
manager?.destroy();
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1448
1749
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1449
1750
|
return ERRORS.TypedError(
|
|
1450
1751
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1460,30 +1761,29 @@ export default class ReactNativeBleTransport {
|
|
|
1460
1761
|
}
|
|
1461
1762
|
|
|
1462
1763
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1764
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1765
|
+
this.probingProtocols.delete(uuid);
|
|
1766
|
+
}
|
|
1463
1767
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1464
1768
|
this.deviceProtocol.delete(uuid);
|
|
1465
1769
|
}
|
|
1466
1770
|
}
|
|
1467
1771
|
|
|
1772
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1773
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1774
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1468
1777
|
private async detectProtocol(
|
|
1469
1778
|
uuid: string,
|
|
1470
1779
|
expectedProtocol?: ProtocolType,
|
|
1471
1780
|
protocolHint?: ProtocolType,
|
|
1472
1781
|
rebuildTransport?: () => Promise<void>
|
|
1473
1782
|
): Promise<ProtocolType> {
|
|
1474
|
-
if (Platform.OS === 'ios' && expectedProtocol) {
|
|
1475
|
-
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1476
|
-
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1477
|
-
deviceId: uuid,
|
|
1478
|
-
protocol: expectedProtocol,
|
|
1479
|
-
source: 'expected',
|
|
1480
|
-
});
|
|
1481
|
-
return expectedProtocol;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
1783
|
if (expectedProtocol === 'V1') {
|
|
1485
1784
|
if (await this.probeProtocolV1(uuid)) {
|
|
1486
1785
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1786
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1487
1787
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1488
1788
|
deviceId: uuid,
|
|
1489
1789
|
protocol: 'V1',
|
|
@@ -1497,6 +1797,7 @@ export default class ReactNativeBleTransport {
|
|
|
1497
1797
|
if (expectedProtocol === 'V2') {
|
|
1498
1798
|
if (await this.probeProtocolV2(uuid)) {
|
|
1499
1799
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1800
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1500
1801
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1501
1802
|
deviceId: uuid,
|
|
1502
1803
|
protocol: 'V2',
|
|
@@ -1509,8 +1810,18 @@ export default class ReactNativeBleTransport {
|
|
|
1509
1810
|
|
|
1510
1811
|
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1511
1812
|
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1512
|
-
const
|
|
1813
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1814
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1815
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1513
1816
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1817
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1818
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1819
|
+
// only adds its timeout to every poll.
|
|
1820
|
+
const trustSessionProtocol =
|
|
1821
|
+
sessionProtocol !== undefined &&
|
|
1822
|
+
!protocolHint &&
|
|
1823
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1824
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1514
1825
|
|
|
1515
1826
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1516
1827
|
const protocol = probeOrder[i];
|
|
@@ -1528,6 +1839,8 @@ export default class ReactNativeBleTransport {
|
|
|
1528
1839
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1529
1840
|
if (detected) {
|
|
1530
1841
|
this.deviceProtocol.set(uuid, protocol);
|
|
1842
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1843
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1531
1844
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1532
1845
|
deviceId: uuid,
|
|
1533
1846
|
protocol,
|
|
@@ -1537,7 +1850,16 @@ export default class ReactNativeBleTransport {
|
|
|
1537
1850
|
}
|
|
1538
1851
|
}
|
|
1539
1852
|
|
|
1853
|
+
if (trustSessionProtocol) {
|
|
1854
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1855
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1856
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1857
|
+
} else {
|
|
1858
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1540
1861
|
this.deviceProtocol.delete(uuid);
|
|
1862
|
+
this.probingProtocols.delete(uuid);
|
|
1541
1863
|
throw this.createProtocolDetectionError();
|
|
1542
1864
|
}
|
|
1543
1865
|
|
|
@@ -1599,14 +1921,20 @@ export default class ReactNativeBleTransport {
|
|
|
1599
1921
|
}
|
|
1600
1922
|
|
|
1601
1923
|
try {
|
|
1602
|
-
this.
|
|
1924
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1603
1925
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1604
1926
|
// session before Core has a chance to restore a hidden wallet.
|
|
1605
1927
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1928
|
+
this.probingProtocols.delete(uuid);
|
|
1606
1929
|
return true;
|
|
1607
1930
|
} catch (error) {
|
|
1608
1931
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1609
1932
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1933
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
1934
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
1935
|
+
if (isWedgedWriteError(error)) {
|
|
1936
|
+
throw error;
|
|
1937
|
+
}
|
|
1610
1938
|
return false;
|
|
1611
1939
|
}
|
|
1612
1940
|
}
|
|
@@ -1616,7 +1944,7 @@ export default class ReactNativeBleTransport {
|
|
|
1616
1944
|
return false;
|
|
1617
1945
|
}
|
|
1618
1946
|
|
|
1619
|
-
this.
|
|
1947
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1620
1948
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1621
1949
|
const detected = await probeProtocolV2Helper({
|
|
1622
1950
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1631,6 +1959,8 @@ export default class ReactNativeBleTransport {
|
|
|
1631
1959
|
});
|
|
1632
1960
|
if (!detected) {
|
|
1633
1961
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1962
|
+
} else {
|
|
1963
|
+
this.probingProtocols.delete(uuid);
|
|
1634
1964
|
}
|
|
1635
1965
|
return detected;
|
|
1636
1966
|
}
|
|
@@ -1712,14 +2042,12 @@ export default class ReactNativeBleTransport {
|
|
|
1712
2042
|
}
|
|
1713
2043
|
|
|
1714
2044
|
private async writeProtocolV2Packet(
|
|
2045
|
+
uuid: string,
|
|
1715
2046
|
transport: BleTransport,
|
|
1716
2047
|
base64: string,
|
|
1717
2048
|
context: ProtocolV2CallContext,
|
|
1718
2049
|
assertCurrentGeneration: () => void
|
|
1719
2050
|
) {
|
|
1720
|
-
const shouldUseWriteWithResponse =
|
|
1721
|
-
transport.writeCharacteristic.isWritableWithResponse &&
|
|
1722
|
-
(context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
|
|
1723
2051
|
let attempt = 0;
|
|
1724
2052
|
for (;;) {
|
|
1725
2053
|
assertCurrentGeneration();
|
|
@@ -1727,11 +2055,21 @@ export default class ReactNativeBleTransport {
|
|
|
1727
2055
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1728
2056
|
}
|
|
1729
2057
|
try {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2058
|
+
await this.writeBlePacket(
|
|
2059
|
+
uuid,
|
|
2060
|
+
base64,
|
|
2061
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2062
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2063
|
+
// tear down the link that the current generation is using.
|
|
2064
|
+
() => {
|
|
2065
|
+
try {
|
|
2066
|
+
assertCurrentGeneration();
|
|
2067
|
+
return !context.signal.aborted;
|
|
2068
|
+
} catch {
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
);
|
|
1735
2073
|
assertCurrentGeneration();
|
|
1736
2074
|
return;
|
|
1737
2075
|
} catch (error) {
|
|
@@ -1754,6 +2092,7 @@ export default class ReactNativeBleTransport {
|
|
|
1754
2092
|
}
|
|
1755
2093
|
|
|
1756
2094
|
private async writeProtocolV2Frame(
|
|
2095
|
+
uuid: string,
|
|
1757
2096
|
transport: BleTransport,
|
|
1758
2097
|
frame: Uint8Array,
|
|
1759
2098
|
context: ProtocolV2CallContext,
|
|
@@ -1766,25 +2105,19 @@ export default class ReactNativeBleTransport {
|
|
|
1766
2105
|
androidPacketLength: tuning.androidPacketLength,
|
|
1767
2106
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1768
2107
|
});
|
|
1769
|
-
// Match Desktop BLE pacing so Pro2 firmware can finish the previous response
|
|
1770
|
-
// before the next single-packet control command is written.
|
|
1771
|
-
const initialDelayMs =
|
|
1772
|
-
Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
1773
|
-
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
1774
|
-
: 0;
|
|
1775
2108
|
await writeProtocolV2BleFrame({
|
|
1776
2109
|
frame,
|
|
1777
2110
|
packetCapacity,
|
|
1778
2111
|
assertActive: assertCurrentGeneration,
|
|
1779
2112
|
signal: context.signal,
|
|
1780
2113
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1781
|
-
initialDelayMs,
|
|
1782
2114
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1783
2115
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1784
2116
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1785
2117
|
wait: delay,
|
|
1786
2118
|
writePacket: packet =>
|
|
1787
2119
|
this.writeProtocolV2Packet(
|
|
2120
|
+
uuid,
|
|
1788
2121
|
transport,
|
|
1789
2122
|
Buffer.from(packet).toString('base64'),
|
|
1790
2123
|
context,
|
|
@@ -1810,7 +2143,7 @@ export default class ReactNativeBleTransport {
|
|
|
1810
2143
|
const tuning = getProtocolV2BleTuning();
|
|
1811
2144
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
1812
2145
|
name,
|
|
1813
|
-
writeMode:
|
|
2146
|
+
writeMode: 'withoutResponse',
|
|
1814
2147
|
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
1815
2148
|
});
|
|
1816
2149
|
}
|
|
@@ -1849,7 +2182,13 @@ export default class ReactNativeBleTransport {
|
|
|
1849
2182
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1850
2183
|
assertCurrentGeneration();
|
|
1851
2184
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1852
|
-
await this.writeProtocolV2Frame(
|
|
2185
|
+
await this.writeProtocolV2Frame(
|
|
2186
|
+
uuid,
|
|
2187
|
+
currentTransport,
|
|
2188
|
+
frame,
|
|
2189
|
+
context,
|
|
2190
|
+
assertCurrentGeneration
|
|
2191
|
+
);
|
|
1853
2192
|
},
|
|
1854
2193
|
readFrame: async () => {
|
|
1855
2194
|
assertCurrentGeneration();
|
|
@@ -1874,6 +2213,6 @@ export default class ReactNativeBleTransport {
|
|
|
1874
2213
|
}
|
|
1875
2214
|
|
|
1876
2215
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1877
|
-
return this.
|
|
2216
|
+
return this.getActiveProtocol(path);
|
|
1878
2217
|
}
|
|
1879
2218
|
}
|