@onekeyfe/hd-transport-react-native 1.2.0-alpha.65 → 1.2.0-alpha.66
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 +1 -79
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -308
- package/jest.config.js +0 -5
- package/package.json +5 -5
- package/src/BleTransport.ts +6 -0
- package/src/__tests__/BleTransport.test.ts +54 -2
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +281 -48
- package/src/index.ts +112 -451
- package/src/__tests__/connectTimeout.test.ts +0 -282
- package/src/__tests__/protocolReprobe.test.ts +0 -133
- package/src/__tests__/staleCallTimeout.test.ts +0 -211
- package/src/__tests__/writePacketTimeout.test.ts +0 -306
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
HardwareErrorCode,
|
|
29
29
|
createDeferred,
|
|
30
30
|
isOnekeyBluetoothDevice,
|
|
31
|
+
isPro2FindMyAdvertisementName,
|
|
31
32
|
} from '@onekeyfe/hd-shared';
|
|
32
33
|
|
|
33
34
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
@@ -60,6 +61,7 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
60
61
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
61
62
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
62
63
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
64
|
+
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
63
65
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
64
66
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
65
67
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
@@ -71,6 +73,33 @@ type ResolvedBleCharacteristics = {
|
|
|
71
73
|
notifyCharacteristic: Characteristic;
|
|
72
74
|
};
|
|
73
75
|
|
|
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
|
+
|
|
74
103
|
const delay = (ms: number) =>
|
|
75
104
|
new Promise<void>(resolve => {
|
|
76
105
|
setTimeout(resolve, ms);
|
|
@@ -99,29 +128,13 @@ export const getFirmwareUploadWriteRetryType = (
|
|
|
99
128
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
100
129
|
.filter(value => typeof value === 'string')
|
|
101
130
|
.join(' ');
|
|
102
|
-
return
|
|
131
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
103
132
|
};
|
|
104
133
|
|
|
105
134
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
106
135
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
107
136
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
108
137
|
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;
|
|
125
138
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
126
139
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
127
140
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -178,52 +191,12 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
178
191
|
|
|
179
192
|
const ANDROID_REQUEST_MTU = 256;
|
|
180
193
|
|
|
181
|
-
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
182
|
-
|
|
183
194
|
const connectOptions: Record<string, unknown> = {
|
|
184
195
|
requestMTU: ANDROID_REQUEST_MTU,
|
|
185
|
-
timeout:
|
|
196
|
+
timeout: 3000,
|
|
186
197
|
refreshGatt: 'OnConnected',
|
|
187
198
|
};
|
|
188
199
|
|
|
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
|
-
|
|
227
200
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
228
201
|
|
|
229
202
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -312,28 +285,8 @@ export default class ReactNativeBleTransport {
|
|
|
312
285
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
313
286
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
314
287
|
|
|
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
|
-
|
|
329
288
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
330
289
|
|
|
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
|
-
|
|
337
290
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
338
291
|
|
|
339
292
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -551,21 +504,22 @@ export default class ReactNativeBleTransport {
|
|
|
551
504
|
const isConnected = await device.isConnected().catch(() => false);
|
|
552
505
|
if (!isConnected) {
|
|
553
506
|
try {
|
|
554
|
-
device = await
|
|
507
|
+
device = await device.connect(connectOptions);
|
|
555
508
|
} catch (e) {
|
|
556
509
|
if (
|
|
557
510
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
558
511
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
559
512
|
) {
|
|
560
|
-
device = await
|
|
513
|
+
device = await device.connect();
|
|
561
514
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
562
515
|
throw e;
|
|
563
516
|
}
|
|
564
517
|
}
|
|
565
518
|
}
|
|
566
519
|
|
|
567
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
568
|
-
|
|
520
|
+
const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
|
|
521
|
+
device
|
|
522
|
+
);
|
|
569
523
|
|
|
570
524
|
transport.device = device;
|
|
571
525
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -655,12 +609,21 @@ export default class ReactNativeBleTransport {
|
|
|
655
609
|
}
|
|
656
610
|
|
|
657
611
|
const displayName = getDeviceDisplayName(device);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
612
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
613
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
614
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
615
|
+
const isFindMyPeripheral =
|
|
616
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
617
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
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
|
+
});
|
|
664
627
|
if (isOneKey) {
|
|
665
628
|
addDevice(device as unknown as Device);
|
|
666
629
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -681,7 +644,12 @@ export default class ReactNativeBleTransport {
|
|
|
681
644
|
'localName' in device && typeof device.localName === 'string'
|
|
682
645
|
? device.localName
|
|
683
646
|
: null;
|
|
647
|
+
const isFindMyPeripheral =
|
|
648
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
649
|
+
isPro2FindMyAdvertisementName(localName);
|
|
650
|
+
|
|
684
651
|
if (
|
|
652
|
+
!isFindMyPeripheral &&
|
|
685
653
|
isOnekeyBluetoothDevice({
|
|
686
654
|
id: device.id,
|
|
687
655
|
name: device.name,
|
|
@@ -730,7 +698,7 @@ export default class ReactNativeBleTransport {
|
|
|
730
698
|
characteristics?: ResolvedBleCharacteristics
|
|
731
699
|
) {
|
|
732
700
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
733
|
-
characteristics ?? (await this.
|
|
701
|
+
characteristics ?? (await this.resolveCharacteristics(device));
|
|
734
702
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
735
703
|
if (Platform.OS === 'android') {
|
|
736
704
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
@@ -834,22 +802,15 @@ export default class ReactNativeBleTransport {
|
|
|
834
802
|
if (!device) {
|
|
835
803
|
Log?.debug('try to connect to device: ', uuid);
|
|
836
804
|
try {
|
|
837
|
-
device = await
|
|
838
|
-
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
839
|
-
);
|
|
805
|
+
device = await blePlxManager.connectToDevice(uuid, connectOptions);
|
|
840
806
|
} catch (e) {
|
|
841
807
|
Log?.debug('try to connect to device has error: ', e);
|
|
842
|
-
if (isConnectTimeoutError(e)) {
|
|
843
|
-
throw e;
|
|
844
|
-
}
|
|
845
808
|
if (
|
|
846
809
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
847
810
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
848
811
|
) {
|
|
849
812
|
Log?.debug('first try to reconnect without params');
|
|
850
|
-
device = await
|
|
851
|
-
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
852
|
-
);
|
|
813
|
+
device = await blePlxManager.connectToDevice(uuid);
|
|
853
814
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
854
815
|
Log?.debug('device already connected');
|
|
855
816
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -865,36 +826,26 @@ export default class ReactNativeBleTransport {
|
|
|
865
826
|
|
|
866
827
|
if (!(await device.isConnected())) {
|
|
867
828
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
868
|
-
const disconnectedDevice = device;
|
|
869
829
|
|
|
870
830
|
try {
|
|
871
|
-
device = await
|
|
872
|
-
disconnectedDevice.connect(connectOptions)
|
|
873
|
-
);
|
|
831
|
+
device = await device.connect(connectOptions);
|
|
874
832
|
} catch (e) {
|
|
875
833
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
876
|
-
if (isConnectTimeoutError(e)) {
|
|
877
|
-
throw e;
|
|
878
|
-
}
|
|
879
834
|
if (
|
|
880
835
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
881
836
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
882
837
|
) {
|
|
883
838
|
Log?.debug('second try to reconnect without params');
|
|
884
839
|
try {
|
|
885
|
-
device = await
|
|
886
|
-
disconnectedDevice.connect(fallbackConnectOptions)
|
|
887
|
-
);
|
|
840
|
+
device = await device.connect();
|
|
888
841
|
} catch (e) {
|
|
889
842
|
Log?.debug('last try to reconnect error: ', e);
|
|
890
843
|
// last try to reconnect device if this issue exists
|
|
891
844
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
892
845
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
893
846
|
Log?.debug('last try to reconnect');
|
|
894
|
-
await
|
|
895
|
-
device = await
|
|
896
|
-
disconnectedDevice.connect(fallbackConnectOptions)
|
|
897
|
-
);
|
|
847
|
+
await device.cancelConnection();
|
|
848
|
+
device = await device.connect();
|
|
898
849
|
}
|
|
899
850
|
}
|
|
900
851
|
} else {
|
|
@@ -905,8 +856,9 @@ export default class ReactNativeBleTransport {
|
|
|
905
856
|
|
|
906
857
|
device = await requestAndroidMtu(device);
|
|
907
858
|
const acquiredDevice = device;
|
|
908
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
909
|
-
|
|
859
|
+
const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
|
|
860
|
+
acquiredDevice
|
|
861
|
+
);
|
|
910
862
|
|
|
911
863
|
const protocolHint = expectedProtocol
|
|
912
864
|
? undefined
|
|
@@ -970,7 +922,7 @@ export default class ReactNativeBleTransport {
|
|
|
970
922
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
971
923
|
return;
|
|
972
924
|
}
|
|
973
|
-
if (this.
|
|
925
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
974
926
|
let errorCode:
|
|
975
927
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
976
928
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -1040,7 +992,7 @@ export default class ReactNativeBleTransport {
|
|
|
1040
992
|
|
|
1041
993
|
try {
|
|
1042
994
|
const data = Buffer.from(c.value as string, 'base64');
|
|
1043
|
-
const protocol = this.
|
|
995
|
+
const protocol = this.deviceProtocol.get(uuid);
|
|
1044
996
|
if (!protocol) {
|
|
1045
997
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
1046
998
|
return;
|
|
@@ -1074,7 +1026,7 @@ export default class ReactNativeBleTransport {
|
|
|
1074
1026
|
} catch (error) {
|
|
1075
1027
|
Log?.debug('monitor data error: ', error);
|
|
1076
1028
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1077
|
-
if (this.
|
|
1029
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
1078
1030
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1079
1031
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
1080
1032
|
this.runPromise?.reject(notifyError);
|
|
@@ -1138,7 +1090,6 @@ export default class ReactNativeBleTransport {
|
|
|
1138
1090
|
}
|
|
1139
1091
|
|
|
1140
1092
|
this.deviceProtocol.delete(uuid);
|
|
1141
|
-
this.probingProtocols.delete(uuid);
|
|
1142
1093
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1143
1094
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1144
1095
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1205,25 +1156,8 @@ export default class ReactNativeBleTransport {
|
|
|
1205
1156
|
const transport = this.getCachedTransport(uuid);
|
|
1206
1157
|
const runPromise = createDeferred<string>();
|
|
1207
1158
|
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
|
-
}
|
|
1216
1159
|
this.runPromise = runPromise;
|
|
1217
1160
|
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;
|
|
1227
1161
|
const messages = this._messages;
|
|
1228
1162
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1229
1163
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1249,9 +1183,6 @@ export default class ReactNativeBleTransport {
|
|
|
1249
1183
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1250
1184
|
} catch (e) {
|
|
1251
1185
|
onError(e);
|
|
1252
|
-
if (isWedgedWriteError(e)) {
|
|
1253
|
-
throw e;
|
|
1254
|
-
}
|
|
1255
1186
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1256
1187
|
}
|
|
1257
1188
|
}
|
|
@@ -1283,9 +1214,6 @@ export default class ReactNativeBleTransport {
|
|
|
1283
1214
|
}
|
|
1284
1215
|
} catch (e) {
|
|
1285
1216
|
onError(e);
|
|
1286
|
-
if (isWedgedWriteError(e)) {
|
|
1287
|
-
throw e;
|
|
1288
|
-
}
|
|
1289
1217
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1290
1218
|
}
|
|
1291
1219
|
}
|
|
@@ -1299,15 +1227,9 @@ export default class ReactNativeBleTransport {
|
|
|
1299
1227
|
if (name === 'EmmcFileWrite') {
|
|
1300
1228
|
await writeChunkedData(
|
|
1301
1229
|
buffers,
|
|
1302
|
-
data =>
|
|
1303
|
-
this.writeBlePacket(
|
|
1304
|
-
uuid,
|
|
1305
|
-
data,
|
|
1306
|
-
payload => transport.writeWithRetry(payload),
|
|
1307
|
-
isCurrentOwner
|
|
1308
|
-
),
|
|
1230
|
+
data => transport.writeWithRetry(data),
|
|
1309
1231
|
e => {
|
|
1310
|
-
|
|
1232
|
+
this.runPromise = null;
|
|
1311
1233
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1312
1234
|
}
|
|
1313
1235
|
);
|
|
@@ -1328,12 +1250,7 @@ export default class ReactNativeBleTransport {
|
|
|
1328
1250
|
// eslint-disable-next-line no-constant-condition
|
|
1329
1251
|
while (true) {
|
|
1330
1252
|
try {
|
|
1331
|
-
await
|
|
1332
|
-
uuid,
|
|
1333
|
-
data,
|
|
1334
|
-
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1335
|
-
isCurrentOwner
|
|
1336
|
-
);
|
|
1253
|
+
await transport.writeWithRetry(data);
|
|
1337
1254
|
return;
|
|
1338
1255
|
} catch (error) {
|
|
1339
1256
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
@@ -1352,7 +1269,7 @@ export default class ReactNativeBleTransport {
|
|
|
1352
1269
|
}
|
|
1353
1270
|
},
|
|
1354
1271
|
e => {
|
|
1355
|
-
|
|
1272
|
+
this.runPromise = null;
|
|
1356
1273
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1357
1274
|
}
|
|
1358
1275
|
);
|
|
@@ -1361,18 +1278,16 @@ export default class ReactNativeBleTransport {
|
|
|
1361
1278
|
const outData = o.toString('base64');
|
|
1362
1279
|
// Upload resources on low-end phones may OOM
|
|
1363
1280
|
try {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1281
|
+
const shouldUseWriteWithResponse =
|
|
1282
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1283
|
+
if (shouldUseWriteWithResponse) {
|
|
1284
|
+
await transport.writeCharacteristic.writeWithResponse(outData);
|
|
1285
|
+
} else {
|
|
1286
|
+
await transport.writeCharacteristic.writeWithoutResponse(outData);
|
|
1287
|
+
}
|
|
1370
1288
|
} catch (e) {
|
|
1371
1289
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1372
|
-
|
|
1373
|
-
if (isWedgedWriteError(e)) {
|
|
1374
|
-
throw e;
|
|
1375
|
-
}
|
|
1290
|
+
this.runPromise = null;
|
|
1376
1291
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1377
1292
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1378
1293
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1415,13 +1330,8 @@ export default class ReactNativeBleTransport {
|
|
|
1415
1330
|
}
|
|
1416
1331
|
const isProbeTimeout =
|
|
1417
1332
|
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;
|
|
1422
1333
|
if (
|
|
1423
1334
|
!isProbeTimeout &&
|
|
1424
|
-
!isStaleCall &&
|
|
1425
1335
|
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1426
1336
|
) {
|
|
1427
1337
|
await this.disconnect(uuid);
|
|
@@ -1501,10 +1411,7 @@ export default class ReactNativeBleTransport {
|
|
|
1501
1411
|
delete transportCache[session];
|
|
1502
1412
|
}
|
|
1503
1413
|
this.deviceProtocol.delete(session);
|
|
1504
|
-
this.probingProtocols.delete(session);
|
|
1505
1414
|
this.deviceProtocolHints.delete(session);
|
|
1506
|
-
this.sessionProtocols.delete(session);
|
|
1507
|
-
this.protocolReprobeFailures.delete(session);
|
|
1508
1415
|
this.protocolV2Assemblers.delete(session);
|
|
1509
1416
|
this.resetProtocolV2Frames(session);
|
|
1510
1417
|
|
|
@@ -1530,113 +1437,6 @@ export default class ReactNativeBleTransport {
|
|
|
1530
1437
|
this.runPromiseDeviceId = null;
|
|
1531
1438
|
}
|
|
1532
1439
|
|
|
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
|
-
|
|
1640
1440
|
private getCachedTransport(uuid: string) {
|
|
1641
1441
|
const transport = transportCache[uuid];
|
|
1642
1442
|
if (!transport) {
|
|
@@ -1645,107 +1445,6 @@ export default class ReactNativeBleTransport {
|
|
|
1645
1445
|
return transport;
|
|
1646
1446
|
}
|
|
1647
1447
|
|
|
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
|
-
|
|
1749
1448
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1750
1449
|
return ERRORS.TypedError(
|
|
1751
1450
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1761,29 +1460,30 @@ export default class ReactNativeBleTransport {
|
|
|
1761
1460
|
}
|
|
1762
1461
|
|
|
1763
1462
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1764
|
-
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1765
|
-
this.probingProtocols.delete(uuid);
|
|
1766
|
-
}
|
|
1767
1463
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1768
1464
|
this.deviceProtocol.delete(uuid);
|
|
1769
1465
|
}
|
|
1770
1466
|
}
|
|
1771
1467
|
|
|
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
|
-
|
|
1777
1468
|
private async detectProtocol(
|
|
1778
1469
|
uuid: string,
|
|
1779
1470
|
expectedProtocol?: ProtocolType,
|
|
1780
1471
|
protocolHint?: ProtocolType,
|
|
1781
1472
|
rebuildTransport?: () => Promise<void>
|
|
1782
1473
|
): 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
|
+
|
|
1783
1484
|
if (expectedProtocol === 'V1') {
|
|
1784
1485
|
if (await this.probeProtocolV1(uuid)) {
|
|
1785
1486
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1786
|
-
this.sessionProtocols.set(uuid, 'V1');
|
|
1787
1487
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1788
1488
|
deviceId: uuid,
|
|
1789
1489
|
protocol: 'V1',
|
|
@@ -1797,7 +1497,6 @@ export default class ReactNativeBleTransport {
|
|
|
1797
1497
|
if (expectedProtocol === 'V2') {
|
|
1798
1498
|
if (await this.probeProtocolV2(uuid)) {
|
|
1799
1499
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1800
|
-
this.sessionProtocols.set(uuid, 'V2');
|
|
1801
1500
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1802
1501
|
deviceId: uuid,
|
|
1803
1502
|
protocol: 'V2',
|
|
@@ -1810,18 +1509,8 @@ export default class ReactNativeBleTransport {
|
|
|
1810
1509
|
|
|
1811
1510
|
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1812
1511
|
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1813
|
-
const
|
|
1814
|
-
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1815
|
-
const fullProbeOrder: ProtocolType[] =
|
|
1512
|
+
const probeOrder: ProtocolType[] =
|
|
1816
1513
|
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;
|
|
1825
1514
|
|
|
1826
1515
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1827
1516
|
const protocol = probeOrder[i];
|
|
@@ -1839,8 +1528,6 @@ export default class ReactNativeBleTransport {
|
|
|
1839
1528
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1840
1529
|
if (detected) {
|
|
1841
1530
|
this.deviceProtocol.set(uuid, protocol);
|
|
1842
|
-
this.sessionProtocols.set(uuid, protocol);
|
|
1843
|
-
this.protocolReprobeFailures.delete(uuid);
|
|
1844
1531
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1845
1532
|
deviceId: uuid,
|
|
1846
1533
|
protocol,
|
|
@@ -1850,16 +1537,7 @@ export default class ReactNativeBleTransport {
|
|
|
1850
1537
|
}
|
|
1851
1538
|
}
|
|
1852
1539
|
|
|
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
|
-
|
|
1861
1540
|
this.deviceProtocol.delete(uuid);
|
|
1862
|
-
this.probingProtocols.delete(uuid);
|
|
1863
1541
|
throw this.createProtocolDetectionError();
|
|
1864
1542
|
}
|
|
1865
1543
|
|
|
@@ -1921,20 +1599,14 @@ export default class ReactNativeBleTransport {
|
|
|
1921
1599
|
}
|
|
1922
1600
|
|
|
1923
1601
|
try {
|
|
1924
|
-
this.
|
|
1602
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1925
1603
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1926
1604
|
// session before Core has a chance to restore a hidden wallet.
|
|
1927
1605
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1928
|
-
this.probingProtocols.delete(uuid);
|
|
1929
1606
|
return true;
|
|
1930
1607
|
} catch (error) {
|
|
1931
1608
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1932
1609
|
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
|
-
}
|
|
1938
1610
|
return false;
|
|
1939
1611
|
}
|
|
1940
1612
|
}
|
|
@@ -1944,7 +1616,7 @@ export default class ReactNativeBleTransport {
|
|
|
1944
1616
|
return false;
|
|
1945
1617
|
}
|
|
1946
1618
|
|
|
1947
|
-
this.
|
|
1619
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1948
1620
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1949
1621
|
const detected = await probeProtocolV2Helper({
|
|
1950
1622
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1959,8 +1631,6 @@ export default class ReactNativeBleTransport {
|
|
|
1959
1631
|
});
|
|
1960
1632
|
if (!detected) {
|
|
1961
1633
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1962
|
-
} else {
|
|
1963
|
-
this.probingProtocols.delete(uuid);
|
|
1964
1634
|
}
|
|
1965
1635
|
return detected;
|
|
1966
1636
|
}
|
|
@@ -2042,12 +1712,14 @@ export default class ReactNativeBleTransport {
|
|
|
2042
1712
|
}
|
|
2043
1713
|
|
|
2044
1714
|
private async writeProtocolV2Packet(
|
|
2045
|
-
uuid: string,
|
|
2046
1715
|
transport: BleTransport,
|
|
2047
1716
|
base64: string,
|
|
2048
1717
|
context: ProtocolV2CallContext,
|
|
2049
1718
|
assertCurrentGeneration: () => void
|
|
2050
1719
|
) {
|
|
1720
|
+
const shouldUseWriteWithResponse =
|
|
1721
|
+
transport.writeCharacteristic.isWritableWithResponse &&
|
|
1722
|
+
(context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
|
|
2051
1723
|
let attempt = 0;
|
|
2052
1724
|
for (;;) {
|
|
2053
1725
|
assertCurrentGeneration();
|
|
@@ -2055,21 +1727,11 @@ export default class ReactNativeBleTransport {
|
|
|
2055
1727
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2056
1728
|
}
|
|
2057
1729
|
try {
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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
|
-
);
|
|
1730
|
+
if (shouldUseWriteWithResponse) {
|
|
1731
|
+
await transport.writeCharacteristic.writeWithResponse(base64);
|
|
1732
|
+
} else {
|
|
1733
|
+
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1734
|
+
}
|
|
2073
1735
|
assertCurrentGeneration();
|
|
2074
1736
|
return;
|
|
2075
1737
|
} catch (error) {
|
|
@@ -2092,7 +1754,6 @@ export default class ReactNativeBleTransport {
|
|
|
2092
1754
|
}
|
|
2093
1755
|
|
|
2094
1756
|
private async writeProtocolV2Frame(
|
|
2095
|
-
uuid: string,
|
|
2096
1757
|
transport: BleTransport,
|
|
2097
1758
|
frame: Uint8Array,
|
|
2098
1759
|
context: ProtocolV2CallContext,
|
|
@@ -2105,19 +1766,25 @@ export default class ReactNativeBleTransport {
|
|
|
2105
1766
|
androidPacketLength: tuning.androidPacketLength,
|
|
2106
1767
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
2107
1768
|
});
|
|
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;
|
|
2108
1775
|
await writeProtocolV2BleFrame({
|
|
2109
1776
|
frame,
|
|
2110
1777
|
packetCapacity,
|
|
2111
1778
|
assertActive: assertCurrentGeneration,
|
|
2112
1779
|
signal: context.signal,
|
|
2113
1780
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1781
|
+
initialDelayMs,
|
|
2114
1782
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
2115
1783
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
2116
1784
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
2117
1785
|
wait: delay,
|
|
2118
1786
|
writePacket: packet =>
|
|
2119
1787
|
this.writeProtocolV2Packet(
|
|
2120
|
-
uuid,
|
|
2121
1788
|
transport,
|
|
2122
1789
|
Buffer.from(packet).toString('base64'),
|
|
2123
1790
|
context,
|
|
@@ -2143,7 +1810,7 @@ export default class ReactNativeBleTransport {
|
|
|
2143
1810
|
const tuning = getProtocolV2BleTuning();
|
|
2144
1811
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2145
1812
|
name,
|
|
2146
|
-
writeMode: 'withoutResponse',
|
|
1813
|
+
writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
|
|
2147
1814
|
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
2148
1815
|
});
|
|
2149
1816
|
}
|
|
@@ -2182,13 +1849,7 @@ export default class ReactNativeBleTransport {
|
|
|
2182
1849
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
2183
1850
|
assertCurrentGeneration();
|
|
2184
1851
|
const currentTransport = this.getCachedTransport(uuid);
|
|
2185
|
-
await this.writeProtocolV2Frame(
|
|
2186
|
-
uuid,
|
|
2187
|
-
currentTransport,
|
|
2188
|
-
frame,
|
|
2189
|
-
context,
|
|
2190
|
-
assertCurrentGeneration
|
|
2191
|
-
);
|
|
1852
|
+
await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
|
|
2192
1853
|
},
|
|
2193
1854
|
readFrame: async () => {
|
|
2194
1855
|
assertCurrentGeneration();
|
|
@@ -2213,6 +1874,6 @@ export default class ReactNativeBleTransport {
|
|
|
2213
1874
|
}
|
|
2214
1875
|
|
|
2215
1876
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
2216
|
-
return this.
|
|
1877
|
+
return this.deviceProtocol.get(path);
|
|
2217
1878
|
}
|
|
2218
1879
|
}
|