@onekeyfe/hd-transport-react-native 1.2.0-alpha.64 → 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 -71
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -271
- 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 +105 -399
- package/src/__tests__/connectTimeout.test.ts +0 -192
- 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,43 +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
|
-
* How many times a known device may fail its own protocol before we probe the others
|
|
204
|
-
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
205
|
-
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
206
|
-
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
207
|
-
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
208
|
-
*/
|
|
209
|
-
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
210
|
-
/** Consecutive connect timeouts on one device before the BLE manager itself is recreated. */
|
|
211
|
-
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
212
|
-
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
213
|
-
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
214
|
-
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
215
|
-
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
216
|
-
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
217
|
-
|
|
218
200
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
219
201
|
|
|
220
202
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -303,28 +285,8 @@ export default class ReactNativeBleTransport {
|
|
|
303
285
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
304
286
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
305
287
|
|
|
306
|
-
/**
|
|
307
|
-
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
308
|
-
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
309
|
-
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
310
|
-
* transport that was never validated.
|
|
311
|
-
*/
|
|
312
|
-
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
313
|
-
|
|
314
|
-
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
315
|
-
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
316
|
-
|
|
317
|
-
/** Consecutive connect timeouts per device; reset by any connect that settles. */
|
|
318
|
-
private connectTimeoutCounts: Map<string, number> = new Map();
|
|
319
|
-
|
|
320
288
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
321
289
|
|
|
322
|
-
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
323
|
-
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
324
|
-
|
|
325
|
-
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
326
|
-
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
327
|
-
|
|
328
290
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
329
291
|
|
|
330
292
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -542,13 +504,13 @@ export default class ReactNativeBleTransport {
|
|
|
542
504
|
const isConnected = await device.isConnected().catch(() => false);
|
|
543
505
|
if (!isConnected) {
|
|
544
506
|
try {
|
|
545
|
-
device = await
|
|
507
|
+
device = await device.connect(connectOptions);
|
|
546
508
|
} catch (e) {
|
|
547
509
|
if (
|
|
548
510
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
549
511
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
550
512
|
) {
|
|
551
|
-
device = await
|
|
513
|
+
device = await device.connect();
|
|
552
514
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
553
515
|
throw e;
|
|
554
516
|
}
|
|
@@ -647,12 +609,21 @@ export default class ReactNativeBleTransport {
|
|
|
647
609
|
}
|
|
648
610
|
|
|
649
611
|
const displayName = getDeviceDisplayName(device);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
+
});
|
|
656
627
|
if (isOneKey) {
|
|
657
628
|
addDevice(device as unknown as Device);
|
|
658
629
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -673,7 +644,12 @@ export default class ReactNativeBleTransport {
|
|
|
673
644
|
'localName' in device && typeof device.localName === 'string'
|
|
674
645
|
? device.localName
|
|
675
646
|
: null;
|
|
647
|
+
const isFindMyPeripheral =
|
|
648
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
649
|
+
isPro2FindMyAdvertisementName(localName);
|
|
650
|
+
|
|
676
651
|
if (
|
|
652
|
+
!isFindMyPeripheral &&
|
|
677
653
|
isOnekeyBluetoothDevice({
|
|
678
654
|
id: device.id,
|
|
679
655
|
name: device.name,
|
|
@@ -826,22 +802,15 @@ export default class ReactNativeBleTransport {
|
|
|
826
802
|
if (!device) {
|
|
827
803
|
Log?.debug('try to connect to device: ', uuid);
|
|
828
804
|
try {
|
|
829
|
-
device = await
|
|
830
|
-
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
831
|
-
);
|
|
805
|
+
device = await blePlxManager.connectToDevice(uuid, connectOptions);
|
|
832
806
|
} catch (e) {
|
|
833
807
|
Log?.debug('try to connect to device has error: ', e);
|
|
834
|
-
if (isConnectTimeoutError(e)) {
|
|
835
|
-
throw e;
|
|
836
|
-
}
|
|
837
808
|
if (
|
|
838
809
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
839
810
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
840
811
|
) {
|
|
841
812
|
Log?.debug('first try to reconnect without params');
|
|
842
|
-
device = await
|
|
843
|
-
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
844
|
-
);
|
|
813
|
+
device = await blePlxManager.connectToDevice(uuid);
|
|
845
814
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
846
815
|
Log?.debug('device already connected');
|
|
847
816
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -857,36 +826,26 @@ export default class ReactNativeBleTransport {
|
|
|
857
826
|
|
|
858
827
|
if (!(await device.isConnected())) {
|
|
859
828
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
860
|
-
const disconnectedDevice = device;
|
|
861
829
|
|
|
862
830
|
try {
|
|
863
|
-
device = await
|
|
864
|
-
disconnectedDevice.connect(connectOptions)
|
|
865
|
-
);
|
|
831
|
+
device = await device.connect(connectOptions);
|
|
866
832
|
} catch (e) {
|
|
867
833
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
868
|
-
if (isConnectTimeoutError(e)) {
|
|
869
|
-
throw e;
|
|
870
|
-
}
|
|
871
834
|
if (
|
|
872
835
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
873
836
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
874
837
|
) {
|
|
875
838
|
Log?.debug('second try to reconnect without params');
|
|
876
839
|
try {
|
|
877
|
-
device = await
|
|
878
|
-
disconnectedDevice.connect(fallbackConnectOptions)
|
|
879
|
-
);
|
|
840
|
+
device = await device.connect();
|
|
880
841
|
} catch (e) {
|
|
881
842
|
Log?.debug('last try to reconnect error: ', e);
|
|
882
843
|
// last try to reconnect device if this issue exists
|
|
883
844
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
884
845
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
885
846
|
Log?.debug('last try to reconnect');
|
|
886
|
-
await
|
|
887
|
-
device = await
|
|
888
|
-
disconnectedDevice.connect(fallbackConnectOptions)
|
|
889
|
-
);
|
|
847
|
+
await device.cancelConnection();
|
|
848
|
+
device = await device.connect();
|
|
890
849
|
}
|
|
891
850
|
}
|
|
892
851
|
} else {
|
|
@@ -963,7 +922,7 @@ export default class ReactNativeBleTransport {
|
|
|
963
922
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
964
923
|
return;
|
|
965
924
|
}
|
|
966
|
-
if (this.
|
|
925
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
967
926
|
let errorCode:
|
|
968
927
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
969
928
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -1033,7 +992,7 @@ export default class ReactNativeBleTransport {
|
|
|
1033
992
|
|
|
1034
993
|
try {
|
|
1035
994
|
const data = Buffer.from(c.value as string, 'base64');
|
|
1036
|
-
const protocol = this.
|
|
995
|
+
const protocol = this.deviceProtocol.get(uuid);
|
|
1037
996
|
if (!protocol) {
|
|
1038
997
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
1039
998
|
return;
|
|
@@ -1067,7 +1026,7 @@ export default class ReactNativeBleTransport {
|
|
|
1067
1026
|
} catch (error) {
|
|
1068
1027
|
Log?.debug('monitor data error: ', error);
|
|
1069
1028
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1070
|
-
if (this.
|
|
1029
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
1071
1030
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1072
1031
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
1073
1032
|
this.runPromise?.reject(notifyError);
|
|
@@ -1131,7 +1090,6 @@ export default class ReactNativeBleTransport {
|
|
|
1131
1090
|
}
|
|
1132
1091
|
|
|
1133
1092
|
this.deviceProtocol.delete(uuid);
|
|
1134
|
-
this.probingProtocols.delete(uuid);
|
|
1135
1093
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1136
1094
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1137
1095
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1198,25 +1156,8 @@ export default class ReactNativeBleTransport {
|
|
|
1198
1156
|
const transport = this.getCachedTransport(uuid);
|
|
1199
1157
|
const runPromise = createDeferred<string>();
|
|
1200
1158
|
runPromise.promise.catch(() => undefined);
|
|
1201
|
-
const supersededRunPromise = this.runPromise;
|
|
1202
|
-
if (supersededRunPromise) {
|
|
1203
|
-
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1204
|
-
// the superseded deferred now so its response race resolves and its finally block
|
|
1205
|
-
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1206
|
-
// tear down the shared connection while another call is using it.
|
|
1207
|
-
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1208
|
-
}
|
|
1209
1159
|
this.runPromise = runPromise;
|
|
1210
1160
|
this.runPromiseDeviceId = uuid;
|
|
1211
|
-
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1212
|
-
// only the call that still owns the slot may release it.
|
|
1213
|
-
const releaseOwnershipIfCurrent = () => {
|
|
1214
|
-
if (this.runPromise === runPromise) {
|
|
1215
|
-
this.runPromise = null;
|
|
1216
|
-
this.runPromiseDeviceId = null;
|
|
1217
|
-
}
|
|
1218
|
-
};
|
|
1219
|
-
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1220
1161
|
const messages = this._messages;
|
|
1221
1162
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1222
1163
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1242,9 +1183,6 @@ export default class ReactNativeBleTransport {
|
|
|
1242
1183
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1243
1184
|
} catch (e) {
|
|
1244
1185
|
onError(e);
|
|
1245
|
-
if (isWedgedWriteError(e)) {
|
|
1246
|
-
throw e;
|
|
1247
|
-
}
|
|
1248
1186
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1249
1187
|
}
|
|
1250
1188
|
}
|
|
@@ -1276,9 +1214,6 @@ export default class ReactNativeBleTransport {
|
|
|
1276
1214
|
}
|
|
1277
1215
|
} catch (e) {
|
|
1278
1216
|
onError(e);
|
|
1279
|
-
if (isWedgedWriteError(e)) {
|
|
1280
|
-
throw e;
|
|
1281
|
-
}
|
|
1282
1217
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1283
1218
|
}
|
|
1284
1219
|
}
|
|
@@ -1292,15 +1227,9 @@ export default class ReactNativeBleTransport {
|
|
|
1292
1227
|
if (name === 'EmmcFileWrite') {
|
|
1293
1228
|
await writeChunkedData(
|
|
1294
1229
|
buffers,
|
|
1295
|
-
data =>
|
|
1296
|
-
this.writeBlePacket(
|
|
1297
|
-
uuid,
|
|
1298
|
-
data,
|
|
1299
|
-
payload => transport.writeWithRetry(payload),
|
|
1300
|
-
isCurrentOwner
|
|
1301
|
-
),
|
|
1230
|
+
data => transport.writeWithRetry(data),
|
|
1302
1231
|
e => {
|
|
1303
|
-
|
|
1232
|
+
this.runPromise = null;
|
|
1304
1233
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1305
1234
|
}
|
|
1306
1235
|
);
|
|
@@ -1321,12 +1250,7 @@ export default class ReactNativeBleTransport {
|
|
|
1321
1250
|
// eslint-disable-next-line no-constant-condition
|
|
1322
1251
|
while (true) {
|
|
1323
1252
|
try {
|
|
1324
|
-
await
|
|
1325
|
-
uuid,
|
|
1326
|
-
data,
|
|
1327
|
-
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1328
|
-
isCurrentOwner
|
|
1329
|
-
);
|
|
1253
|
+
await transport.writeWithRetry(data);
|
|
1330
1254
|
return;
|
|
1331
1255
|
} catch (error) {
|
|
1332
1256
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
@@ -1345,7 +1269,7 @@ export default class ReactNativeBleTransport {
|
|
|
1345
1269
|
}
|
|
1346
1270
|
},
|
|
1347
1271
|
e => {
|
|
1348
|
-
|
|
1272
|
+
this.runPromise = null;
|
|
1349
1273
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1350
1274
|
}
|
|
1351
1275
|
);
|
|
@@ -1354,18 +1278,16 @@ export default class ReactNativeBleTransport {
|
|
|
1354
1278
|
const outData = o.toString('base64');
|
|
1355
1279
|
// Upload resources on low-end phones may OOM
|
|
1356
1280
|
try {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
+
}
|
|
1363
1288
|
} catch (e) {
|
|
1364
1289
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1365
|
-
|
|
1366
|
-
if (isWedgedWriteError(e)) {
|
|
1367
|
-
throw e;
|
|
1368
|
-
}
|
|
1290
|
+
this.runPromise = null;
|
|
1369
1291
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1370
1292
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1371
1293
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1408,13 +1330,8 @@ export default class ReactNativeBleTransport {
|
|
|
1408
1330
|
}
|
|
1409
1331
|
const isProbeTimeout =
|
|
1410
1332
|
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1411
|
-
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1412
|
-
// transport; its late timeout must not tear down the connection the current
|
|
1413
|
-
// call is actively using.
|
|
1414
|
-
const isStaleCall = this.runPromise !== runPromise;
|
|
1415
1333
|
if (
|
|
1416
1334
|
!isProbeTimeout &&
|
|
1417
|
-
!isStaleCall &&
|
|
1418
1335
|
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1419
1336
|
) {
|
|
1420
1337
|
await this.disconnect(uuid);
|
|
@@ -1494,10 +1411,7 @@ export default class ReactNativeBleTransport {
|
|
|
1494
1411
|
delete transportCache[session];
|
|
1495
1412
|
}
|
|
1496
1413
|
this.deviceProtocol.delete(session);
|
|
1497
|
-
this.probingProtocols.delete(session);
|
|
1498
1414
|
this.deviceProtocolHints.delete(session);
|
|
1499
|
-
this.sessionProtocols.delete(session);
|
|
1500
|
-
this.protocolReprobeFailures.delete(session);
|
|
1501
1415
|
this.protocolV2Assemblers.delete(session);
|
|
1502
1416
|
this.resetProtocolV2Frames(session);
|
|
1503
1417
|
|
|
@@ -1523,75 +1437,6 @@ export default class ReactNativeBleTransport {
|
|
|
1523
1437
|
this.runPromiseDeviceId = null;
|
|
1524
1438
|
}
|
|
1525
1439
|
|
|
1526
|
-
/** Run a native connect under the JS backstop budget. */
|
|
1527
|
-
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1528
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1529
|
-
let timedOut = false;
|
|
1530
|
-
const pending = connect();
|
|
1531
|
-
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1532
|
-
// surface as an unhandled rejection after we have already given up on it.
|
|
1533
|
-
pending.catch(() => undefined);
|
|
1534
|
-
try {
|
|
1535
|
-
const result = await Promise.race([
|
|
1536
|
-
pending,
|
|
1537
|
-
new Promise<never>((_, reject) => {
|
|
1538
|
-
timer = setTimeout(() => {
|
|
1539
|
-
timedOut = true;
|
|
1540
|
-
reject(
|
|
1541
|
-
ERRORS.TypedError(
|
|
1542
|
-
HardwareErrorCode.BleConnectedError,
|
|
1543
|
-
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1544
|
-
)
|
|
1545
|
-
);
|
|
1546
|
-
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1547
|
-
}),
|
|
1548
|
-
]);
|
|
1549
|
-
this.connectTimeoutCounts.delete(uuid);
|
|
1550
|
-
return result;
|
|
1551
|
-
} catch (error) {
|
|
1552
|
-
if (timedOut) {
|
|
1553
|
-
this.abandonStalledConnect(uuid);
|
|
1554
|
-
}
|
|
1555
|
-
throw error;
|
|
1556
|
-
} finally {
|
|
1557
|
-
if (timer) clearTimeout(timer);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
/**
|
|
1562
|
-
* Give up on a connect the native layer never settled. The abandoned attempt still
|
|
1563
|
-
* holds a native "connecting" entry that would cancel the NEXT attempt out from under
|
|
1564
|
-
* itself, so it is cleared here — fire and forget, because that call talks to the very
|
|
1565
|
-
* queue that just stopped responding.
|
|
1566
|
-
*/
|
|
1567
|
-
private abandonStalledConnect(uuid: string) {
|
|
1568
|
-
const timeouts = (this.connectTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1569
|
-
this.connectTimeoutCounts.set(uuid, timeouts);
|
|
1570
|
-
Log?.error('[ReactNativeBleTransport] BLE connect timed out:', uuid, {
|
|
1571
|
-
consecutiveConnectTimeouts: timeouts,
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1575
|
-
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1576
|
-
});
|
|
1577
|
-
const stalled = transportCache[uuid];
|
|
1578
|
-
if (stalled) {
|
|
1579
|
-
delete transportCache[uuid];
|
|
1580
|
-
}
|
|
1581
|
-
this.deviceProtocol.delete(uuid);
|
|
1582
|
-
this.probingProtocols.delete(uuid);
|
|
1583
|
-
this.protocolV2Assemblers.delete(uuid);
|
|
1584
|
-
this.resetProtocolV2Frames(uuid);
|
|
1585
|
-
|
|
1586
|
-
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1587
|
-
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1588
|
-
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1589
|
-
Log?.error('[ReactNativeBleTransport] BLE connects wedged repeatedly, resetting BLE manager');
|
|
1590
|
-
this.resetPlxManager();
|
|
1591
|
-
this.connectTimeoutCounts.delete(uuid);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
1440
|
private getCachedTransport(uuid: string) {
|
|
1596
1441
|
const transport = transportCache[uuid];
|
|
1597
1442
|
if (!transport) {
|
|
@@ -1600,107 +1445,6 @@ export default class ReactNativeBleTransport {
|
|
|
1600
1445
|
return transport;
|
|
1601
1446
|
}
|
|
1602
1447
|
|
|
1603
|
-
/**
|
|
1604
|
-
* Write one packet under a bounded budget. A write that never settles means the
|
|
1605
|
-
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1606
|
-
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1607
|
-
* cached and every later call would hang on it again.
|
|
1608
|
-
*/
|
|
1609
|
-
private async writeBlePacket(
|
|
1610
|
-
uuid: string,
|
|
1611
|
-
data: string,
|
|
1612
|
-
write: (payload: string) => Promise<unknown>,
|
|
1613
|
-
isCurrentOwner?: () => boolean
|
|
1614
|
-
) {
|
|
1615
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1616
|
-
let timedOut = false;
|
|
1617
|
-
try {
|
|
1618
|
-
await Promise.race([
|
|
1619
|
-
write(data),
|
|
1620
|
-
new Promise<never>((_, reject) => {
|
|
1621
|
-
timer = setTimeout(() => {
|
|
1622
|
-
timedOut = true;
|
|
1623
|
-
reject(
|
|
1624
|
-
ERRORS.TypedError(
|
|
1625
|
-
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1626
|
-
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1627
|
-
)
|
|
1628
|
-
);
|
|
1629
|
-
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1630
|
-
}),
|
|
1631
|
-
]);
|
|
1632
|
-
this.writeTimeoutCounts.delete(uuid);
|
|
1633
|
-
} catch (error) {
|
|
1634
|
-
if (timedOut) {
|
|
1635
|
-
// A superseded call's late write must not tear down the link the current
|
|
1636
|
-
// call is using; only the owner of the transport may declare it dead.
|
|
1637
|
-
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1638
|
-
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1639
|
-
} else {
|
|
1640
|
-
this.tearDownWedgedLink(uuid);
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
throw error;
|
|
1644
|
-
} finally {
|
|
1645
|
-
if (timer) clearTimeout(timer);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
/**
|
|
1650
|
-
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1651
|
-
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1652
|
-
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1653
|
-
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1654
|
-
*/
|
|
1655
|
-
private tearDownWedgedLink(uuid: string) {
|
|
1656
|
-
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1657
|
-
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1658
|
-
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1659
|
-
consecutiveWriteTimeouts: timeouts,
|
|
1660
|
-
});
|
|
1661
|
-
|
|
1662
|
-
const wedged = transportCache[uuid];
|
|
1663
|
-
this.disconnect(uuid).catch(error => {
|
|
1664
|
-
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1665
|
-
});
|
|
1666
|
-
if (wedged && transportCache[uuid] === wedged) {
|
|
1667
|
-
delete transportCache[uuid];
|
|
1668
|
-
}
|
|
1669
|
-
this.deviceProtocol.delete(uuid);
|
|
1670
|
-
this.probingProtocols.delete(uuid);
|
|
1671
|
-
this.protocolV2Assemblers.delete(uuid);
|
|
1672
|
-
this.resetProtocolV2Frames(uuid);
|
|
1673
|
-
|
|
1674
|
-
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1675
|
-
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1676
|
-
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1677
|
-
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1678
|
-
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1679
|
-
this.resetPlxManager();
|
|
1680
|
-
this.writeTimeoutCounts.delete(uuid);
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
private resetPlxManager() {
|
|
1685
|
-
const manager = this.blePlxManager;
|
|
1686
|
-
this.blePlxManager = undefined;
|
|
1687
|
-
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1688
|
-
Object.keys(transportCache).forEach(key => {
|
|
1689
|
-
delete transportCache[key];
|
|
1690
|
-
});
|
|
1691
|
-
this.deviceProtocol.clear();
|
|
1692
|
-
this.probingProtocols.clear();
|
|
1693
|
-
this.sessionProtocols.clear();
|
|
1694
|
-
this.protocolReprobeFailures.clear();
|
|
1695
|
-
this.monitorTokens.clear();
|
|
1696
|
-
this.protocolV2Assemblers.clear();
|
|
1697
|
-
try {
|
|
1698
|
-
manager?.destroy();
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
1448
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1705
1449
|
return ERRORS.TypedError(
|
|
1706
1450
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1716,29 +1460,30 @@ export default class ReactNativeBleTransport {
|
|
|
1716
1460
|
}
|
|
1717
1461
|
|
|
1718
1462
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1719
|
-
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1720
|
-
this.probingProtocols.delete(uuid);
|
|
1721
|
-
}
|
|
1722
1463
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1723
1464
|
this.deviceProtocol.delete(uuid);
|
|
1724
1465
|
}
|
|
1725
1466
|
}
|
|
1726
1467
|
|
|
1727
|
-
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1728
|
-
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1729
|
-
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
1468
|
private async detectProtocol(
|
|
1733
1469
|
uuid: string,
|
|
1734
1470
|
expectedProtocol?: ProtocolType,
|
|
1735
1471
|
protocolHint?: ProtocolType,
|
|
1736
1472
|
rebuildTransport?: () => Promise<void>
|
|
1737
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
|
+
|
|
1738
1484
|
if (expectedProtocol === 'V1') {
|
|
1739
1485
|
if (await this.probeProtocolV1(uuid)) {
|
|
1740
1486
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1741
|
-
this.sessionProtocols.set(uuid, 'V1');
|
|
1742
1487
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1743
1488
|
deviceId: uuid,
|
|
1744
1489
|
protocol: 'V1',
|
|
@@ -1752,7 +1497,6 @@ export default class ReactNativeBleTransport {
|
|
|
1752
1497
|
if (expectedProtocol === 'V2') {
|
|
1753
1498
|
if (await this.probeProtocolV2(uuid)) {
|
|
1754
1499
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1755
|
-
this.sessionProtocols.set(uuid, 'V2');
|
|
1756
1500
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1757
1501
|
deviceId: uuid,
|
|
1758
1502
|
protocol: 'V2',
|
|
@@ -1765,18 +1509,8 @@ export default class ReactNativeBleTransport {
|
|
|
1765
1509
|
|
|
1766
1510
|
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1767
1511
|
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1768
|
-
const
|
|
1769
|
-
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1770
|
-
const fullProbeOrder: ProtocolType[] =
|
|
1512
|
+
const probeOrder: ProtocolType[] =
|
|
1771
1513
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1772
|
-
// A device that already answered on a protocol in this session keeps answering on
|
|
1773
|
-
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1774
|
-
// only adds its timeout to every poll.
|
|
1775
|
-
const trustSessionProtocol =
|
|
1776
|
-
sessionProtocol !== undefined &&
|
|
1777
|
-
!protocolHint &&
|
|
1778
|
-
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1779
|
-
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1780
1514
|
|
|
1781
1515
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1782
1516
|
const protocol = probeOrder[i];
|
|
@@ -1794,8 +1528,6 @@ export default class ReactNativeBleTransport {
|
|
|
1794
1528
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1795
1529
|
if (detected) {
|
|
1796
1530
|
this.deviceProtocol.set(uuid, protocol);
|
|
1797
|
-
this.sessionProtocols.set(uuid, protocol);
|
|
1798
|
-
this.protocolReprobeFailures.delete(uuid);
|
|
1799
1531
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1800
1532
|
deviceId: uuid,
|
|
1801
1533
|
protocol,
|
|
@@ -1805,16 +1537,7 @@ export default class ReactNativeBleTransport {
|
|
|
1805
1537
|
}
|
|
1806
1538
|
}
|
|
1807
1539
|
|
|
1808
|
-
if (trustSessionProtocol) {
|
|
1809
|
-
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1810
|
-
// shortcut so a device that genuinely switched protocols is found again.
|
|
1811
|
-
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1812
|
-
} else {
|
|
1813
|
-
this.protocolReprobeFailures.delete(uuid);
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
1540
|
this.deviceProtocol.delete(uuid);
|
|
1817
|
-
this.probingProtocols.delete(uuid);
|
|
1818
1541
|
throw this.createProtocolDetectionError();
|
|
1819
1542
|
}
|
|
1820
1543
|
|
|
@@ -1876,20 +1599,14 @@ export default class ReactNativeBleTransport {
|
|
|
1876
1599
|
}
|
|
1877
1600
|
|
|
1878
1601
|
try {
|
|
1879
|
-
this.
|
|
1602
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1880
1603
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1881
1604
|
// session before Core has a chance to restore a hidden wallet.
|
|
1882
1605
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1883
|
-
this.probingProtocols.delete(uuid);
|
|
1884
1606
|
return true;
|
|
1885
1607
|
} catch (error) {
|
|
1886
1608
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1887
1609
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1888
|
-
// A wedged write already dropped the link, so probing another protocol on it
|
|
1889
|
-
// would only fail against a torn-down transport: surface the real cause.
|
|
1890
|
-
if (isWedgedWriteError(error)) {
|
|
1891
|
-
throw error;
|
|
1892
|
-
}
|
|
1893
1610
|
return false;
|
|
1894
1611
|
}
|
|
1895
1612
|
}
|
|
@@ -1899,7 +1616,7 @@ export default class ReactNativeBleTransport {
|
|
|
1899
1616
|
return false;
|
|
1900
1617
|
}
|
|
1901
1618
|
|
|
1902
|
-
this.
|
|
1619
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1903
1620
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1904
1621
|
const detected = await probeProtocolV2Helper({
|
|
1905
1622
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1914,8 +1631,6 @@ export default class ReactNativeBleTransport {
|
|
|
1914
1631
|
});
|
|
1915
1632
|
if (!detected) {
|
|
1916
1633
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1917
|
-
} else {
|
|
1918
|
-
this.probingProtocols.delete(uuid);
|
|
1919
1634
|
}
|
|
1920
1635
|
return detected;
|
|
1921
1636
|
}
|
|
@@ -1997,12 +1712,14 @@ export default class ReactNativeBleTransport {
|
|
|
1997
1712
|
}
|
|
1998
1713
|
|
|
1999
1714
|
private async writeProtocolV2Packet(
|
|
2000
|
-
uuid: string,
|
|
2001
1715
|
transport: BleTransport,
|
|
2002
1716
|
base64: string,
|
|
2003
1717
|
context: ProtocolV2CallContext,
|
|
2004
1718
|
assertCurrentGeneration: () => void
|
|
2005
1719
|
) {
|
|
1720
|
+
const shouldUseWriteWithResponse =
|
|
1721
|
+
transport.writeCharacteristic.isWritableWithResponse &&
|
|
1722
|
+
(context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
|
|
2006
1723
|
let attempt = 0;
|
|
2007
1724
|
for (;;) {
|
|
2008
1725
|
assertCurrentGeneration();
|
|
@@ -2010,21 +1727,11 @@ export default class ReactNativeBleTransport {
|
|
|
2010
1727
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2011
1728
|
}
|
|
2012
1729
|
try {
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
// tear down the link that the current generation is using.
|
|
2019
|
-
() => {
|
|
2020
|
-
try {
|
|
2021
|
-
assertCurrentGeneration();
|
|
2022
|
-
return !context.signal.aborted;
|
|
2023
|
-
} catch {
|
|
2024
|
-
return false;
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
);
|
|
1730
|
+
if (shouldUseWriteWithResponse) {
|
|
1731
|
+
await transport.writeCharacteristic.writeWithResponse(base64);
|
|
1732
|
+
} else {
|
|
1733
|
+
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1734
|
+
}
|
|
2028
1735
|
assertCurrentGeneration();
|
|
2029
1736
|
return;
|
|
2030
1737
|
} catch (error) {
|
|
@@ -2047,7 +1754,6 @@ export default class ReactNativeBleTransport {
|
|
|
2047
1754
|
}
|
|
2048
1755
|
|
|
2049
1756
|
private async writeProtocolV2Frame(
|
|
2050
|
-
uuid: string,
|
|
2051
1757
|
transport: BleTransport,
|
|
2052
1758
|
frame: Uint8Array,
|
|
2053
1759
|
context: ProtocolV2CallContext,
|
|
@@ -2060,19 +1766,25 @@ export default class ReactNativeBleTransport {
|
|
|
2060
1766
|
androidPacketLength: tuning.androidPacketLength,
|
|
2061
1767
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
2062
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;
|
|
2063
1775
|
await writeProtocolV2BleFrame({
|
|
2064
1776
|
frame,
|
|
2065
1777
|
packetCapacity,
|
|
2066
1778
|
assertActive: assertCurrentGeneration,
|
|
2067
1779
|
signal: context.signal,
|
|
2068
1780
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1781
|
+
initialDelayMs,
|
|
2069
1782
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
2070
1783
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
2071
1784
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
2072
1785
|
wait: delay,
|
|
2073
1786
|
writePacket: packet =>
|
|
2074
1787
|
this.writeProtocolV2Packet(
|
|
2075
|
-
uuid,
|
|
2076
1788
|
transport,
|
|
2077
1789
|
Buffer.from(packet).toString('base64'),
|
|
2078
1790
|
context,
|
|
@@ -2098,7 +1810,7 @@ export default class ReactNativeBleTransport {
|
|
|
2098
1810
|
const tuning = getProtocolV2BleTuning();
|
|
2099
1811
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2100
1812
|
name,
|
|
2101
|
-
writeMode: 'withoutResponse',
|
|
1813
|
+
writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
|
|
2102
1814
|
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
2103
1815
|
});
|
|
2104
1816
|
}
|
|
@@ -2137,13 +1849,7 @@ export default class ReactNativeBleTransport {
|
|
|
2137
1849
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
2138
1850
|
assertCurrentGeneration();
|
|
2139
1851
|
const currentTransport = this.getCachedTransport(uuid);
|
|
2140
|
-
await this.writeProtocolV2Frame(
|
|
2141
|
-
uuid,
|
|
2142
|
-
currentTransport,
|
|
2143
|
-
frame,
|
|
2144
|
-
context,
|
|
2145
|
-
assertCurrentGeneration
|
|
2146
|
-
);
|
|
1852
|
+
await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
|
|
2147
1853
|
},
|
|
2148
1854
|
readFrame: async () => {
|
|
2149
1855
|
assertCurrentGeneration();
|
|
@@ -2168,6 +1874,6 @@ export default class ReactNativeBleTransport {
|
|
|
2168
1874
|
}
|
|
2169
1875
|
|
|
2170
1876
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
2171
|
-
return this.
|
|
1877
|
+
return this.deviceProtocol.get(path);
|
|
2172
1878
|
}
|
|
2173
1879
|
}
|