@onekeyfe/hd-transport-react-native 1.2.0-alpha.66 → 1.2.0-alpha.68
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/index.d.ts +79 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -40
- package/jest.config.js +5 -0
- package/package.json +5 -5
- 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 +10 -1
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/index.ts +449 -39
package/src/index.ts
CHANGED
|
@@ -135,6 +135,22 @@ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, max
|
|
|
135
135
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
136
136
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
137
137
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
138
|
+
/**
|
|
139
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
140
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
141
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
142
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
143
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
144
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
145
|
+
*/
|
|
146
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
147
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
148
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
149
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
150
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
151
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
152
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
153
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
138
154
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
139
155
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
140
156
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -191,12 +207,52 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
191
207
|
|
|
192
208
|
const ANDROID_REQUEST_MTU = 256;
|
|
193
209
|
|
|
210
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
211
|
+
|
|
194
212
|
const connectOptions: Record<string, unknown> = {
|
|
195
213
|
requestMTU: ANDROID_REQUEST_MTU,
|
|
196
|
-
timeout:
|
|
214
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
197
215
|
refreshGatt: 'OnConnected',
|
|
198
216
|
};
|
|
199
217
|
|
|
218
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
219
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
220
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
225
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
226
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
227
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
228
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
229
|
+
*/
|
|
230
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
231
|
+
/**
|
|
232
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
233
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
234
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
235
|
+
* budget.
|
|
236
|
+
*/
|
|
237
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
238
|
+
/**
|
|
239
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
240
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
241
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
242
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
243
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
244
|
+
*/
|
|
245
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
246
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
247
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
248
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
249
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
250
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
251
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
252
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
253
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
254
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
255
|
+
|
|
200
256
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
201
257
|
|
|
202
258
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -285,8 +341,28 @@ export default class ReactNativeBleTransport {
|
|
|
285
341
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
286
342
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
287
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
346
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
347
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
348
|
+
* transport that was never validated.
|
|
349
|
+
*/
|
|
350
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
351
|
+
|
|
352
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
353
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
354
|
+
|
|
355
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
356
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
357
|
+
|
|
288
358
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
289
359
|
|
|
360
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
361
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
362
|
+
|
|
363
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
364
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
365
|
+
|
|
290
366
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
291
367
|
|
|
292
368
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -504,22 +580,21 @@ export default class ReactNativeBleTransport {
|
|
|
504
580
|
const isConnected = await device.isConnected().catch(() => false);
|
|
505
581
|
if (!isConnected) {
|
|
506
582
|
try {
|
|
507
|
-
device = await device.connect(connectOptions);
|
|
583
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
508
584
|
} catch (e) {
|
|
509
585
|
if (
|
|
510
586
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
511
587
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
512
588
|
) {
|
|
513
|
-
device = await device.connect();
|
|
589
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
514
590
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
515
591
|
throw e;
|
|
516
592
|
}
|
|
517
593
|
}
|
|
518
594
|
}
|
|
519
595
|
|
|
520
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
521
|
-
device
|
|
522
|
-
);
|
|
596
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
597
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
523
598
|
|
|
524
599
|
transport.device = device;
|
|
525
600
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -698,7 +773,7 @@ export default class ReactNativeBleTransport {
|
|
|
698
773
|
characteristics?: ResolvedBleCharacteristics
|
|
699
774
|
) {
|
|
700
775
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
701
|
-
characteristics ?? (await this.
|
|
776
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
702
777
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
703
778
|
if (Platform.OS === 'android') {
|
|
704
779
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
@@ -802,15 +877,22 @@ export default class ReactNativeBleTransport {
|
|
|
802
877
|
if (!device) {
|
|
803
878
|
Log?.debug('try to connect to device: ', uuid);
|
|
804
879
|
try {
|
|
805
|
-
device = await
|
|
880
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
881
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
882
|
+
);
|
|
806
883
|
} catch (e) {
|
|
807
884
|
Log?.debug('try to connect to device has error: ', e);
|
|
885
|
+
if (isConnectTimeoutError(e)) {
|
|
886
|
+
throw e;
|
|
887
|
+
}
|
|
808
888
|
if (
|
|
809
889
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
810
890
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
811
891
|
) {
|
|
812
892
|
Log?.debug('first try to reconnect without params');
|
|
813
|
-
device = await
|
|
893
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
894
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
895
|
+
);
|
|
814
896
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
815
897
|
Log?.debug('device already connected');
|
|
816
898
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -826,26 +908,36 @@ export default class ReactNativeBleTransport {
|
|
|
826
908
|
|
|
827
909
|
if (!(await device.isConnected())) {
|
|
828
910
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
911
|
+
const disconnectedDevice = device;
|
|
829
912
|
|
|
830
913
|
try {
|
|
831
|
-
device = await
|
|
914
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
915
|
+
disconnectedDevice.connect(connectOptions)
|
|
916
|
+
);
|
|
832
917
|
} catch (e) {
|
|
833
918
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
919
|
+
if (isConnectTimeoutError(e)) {
|
|
920
|
+
throw e;
|
|
921
|
+
}
|
|
834
922
|
if (
|
|
835
923
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
836
924
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
837
925
|
) {
|
|
838
926
|
Log?.debug('second try to reconnect without params');
|
|
839
927
|
try {
|
|
840
|
-
device = await
|
|
928
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
929
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
930
|
+
);
|
|
841
931
|
} catch (e) {
|
|
842
932
|
Log?.debug('last try to reconnect error: ', e);
|
|
843
933
|
// last try to reconnect device if this issue exists
|
|
844
934
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
845
935
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
846
936
|
Log?.debug('last try to reconnect');
|
|
847
|
-
await
|
|
848
|
-
device = await
|
|
937
|
+
await disconnectedDevice.cancelConnection();
|
|
938
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
939
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
940
|
+
);
|
|
849
941
|
}
|
|
850
942
|
}
|
|
851
943
|
} else {
|
|
@@ -856,9 +948,8 @@ export default class ReactNativeBleTransport {
|
|
|
856
948
|
|
|
857
949
|
device = await requestAndroidMtu(device);
|
|
858
950
|
const acquiredDevice = device;
|
|
859
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
860
|
-
acquiredDevice
|
|
861
|
-
);
|
|
951
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
952
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
862
953
|
|
|
863
954
|
const protocolHint = expectedProtocol
|
|
864
955
|
? undefined
|
|
@@ -922,7 +1013,7 @@ export default class ReactNativeBleTransport {
|
|
|
922
1013
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
923
1014
|
return;
|
|
924
1015
|
}
|
|
925
|
-
if (this.
|
|
1016
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
926
1017
|
let errorCode:
|
|
927
1018
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
928
1019
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -992,7 +1083,7 @@ export default class ReactNativeBleTransport {
|
|
|
992
1083
|
|
|
993
1084
|
try {
|
|
994
1085
|
const data = Buffer.from(c.value as string, 'base64');
|
|
995
|
-
const protocol = this.
|
|
1086
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
996
1087
|
if (!protocol) {
|
|
997
1088
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
998
1089
|
return;
|
|
@@ -1026,7 +1117,7 @@ export default class ReactNativeBleTransport {
|
|
|
1026
1117
|
} catch (error) {
|
|
1027
1118
|
Log?.debug('monitor data error: ', error);
|
|
1028
1119
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1029
|
-
if (this.
|
|
1120
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1030
1121
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1031
1122
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
1032
1123
|
this.runPromise?.reject(notifyError);
|
|
@@ -1090,6 +1181,7 @@ export default class ReactNativeBleTransport {
|
|
|
1090
1181
|
}
|
|
1091
1182
|
|
|
1092
1183
|
this.deviceProtocol.delete(uuid);
|
|
1184
|
+
this.probingProtocols.delete(uuid);
|
|
1093
1185
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1094
1186
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1095
1187
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1156,8 +1248,25 @@ export default class ReactNativeBleTransport {
|
|
|
1156
1248
|
const transport = this.getCachedTransport(uuid);
|
|
1157
1249
|
const runPromise = createDeferred<string>();
|
|
1158
1250
|
runPromise.promise.catch(() => undefined);
|
|
1251
|
+
const supersededRunPromise = this.runPromise;
|
|
1252
|
+
if (supersededRunPromise) {
|
|
1253
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1254
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1255
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1256
|
+
// tear down the shared connection while another call is using it.
|
|
1257
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1258
|
+
}
|
|
1159
1259
|
this.runPromise = runPromise;
|
|
1160
1260
|
this.runPromiseDeviceId = uuid;
|
|
1261
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1262
|
+
// only the call that still owns the slot may release it.
|
|
1263
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1264
|
+
if (this.runPromise === runPromise) {
|
|
1265
|
+
this.runPromise = null;
|
|
1266
|
+
this.runPromiseDeviceId = null;
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1161
1270
|
const messages = this._messages;
|
|
1162
1271
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1163
1272
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1183,6 +1292,9 @@ export default class ReactNativeBleTransport {
|
|
|
1183
1292
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1184
1293
|
} catch (e) {
|
|
1185
1294
|
onError(e);
|
|
1295
|
+
if (isWedgedWriteError(e)) {
|
|
1296
|
+
throw e;
|
|
1297
|
+
}
|
|
1186
1298
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1187
1299
|
}
|
|
1188
1300
|
}
|
|
@@ -1214,6 +1326,9 @@ export default class ReactNativeBleTransport {
|
|
|
1214
1326
|
}
|
|
1215
1327
|
} catch (e) {
|
|
1216
1328
|
onError(e);
|
|
1329
|
+
if (isWedgedWriteError(e)) {
|
|
1330
|
+
throw e;
|
|
1331
|
+
}
|
|
1217
1332
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1218
1333
|
}
|
|
1219
1334
|
}
|
|
@@ -1227,9 +1342,15 @@ export default class ReactNativeBleTransport {
|
|
|
1227
1342
|
if (name === 'EmmcFileWrite') {
|
|
1228
1343
|
await writeChunkedData(
|
|
1229
1344
|
buffers,
|
|
1230
|
-
data =>
|
|
1345
|
+
data =>
|
|
1346
|
+
this.writeBlePacket(
|
|
1347
|
+
uuid,
|
|
1348
|
+
data,
|
|
1349
|
+
payload => transport.writeWithRetry(payload),
|
|
1350
|
+
isCurrentOwner
|
|
1351
|
+
),
|
|
1231
1352
|
e => {
|
|
1232
|
-
|
|
1353
|
+
releaseOwnershipIfCurrent();
|
|
1233
1354
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1234
1355
|
}
|
|
1235
1356
|
);
|
|
@@ -1250,7 +1371,12 @@ export default class ReactNativeBleTransport {
|
|
|
1250
1371
|
// eslint-disable-next-line no-constant-condition
|
|
1251
1372
|
while (true) {
|
|
1252
1373
|
try {
|
|
1253
|
-
await
|
|
1374
|
+
await this.writeBlePacket(
|
|
1375
|
+
uuid,
|
|
1376
|
+
data,
|
|
1377
|
+
payload => transport.writeWithRetry(payload),
|
|
1378
|
+
isCurrentOwner
|
|
1379
|
+
);
|
|
1254
1380
|
return;
|
|
1255
1381
|
} catch (error) {
|
|
1256
1382
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
@@ -1269,7 +1395,7 @@ export default class ReactNativeBleTransport {
|
|
|
1269
1395
|
}
|
|
1270
1396
|
},
|
|
1271
1397
|
e => {
|
|
1272
|
-
|
|
1398
|
+
releaseOwnershipIfCurrent();
|
|
1273
1399
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1274
1400
|
}
|
|
1275
1401
|
);
|
|
@@ -1280,14 +1406,21 @@ export default class ReactNativeBleTransport {
|
|
|
1280
1406
|
try {
|
|
1281
1407
|
const shouldUseWriteWithResponse =
|
|
1282
1408
|
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1409
|
+
await this.writeBlePacket(
|
|
1410
|
+
uuid,
|
|
1411
|
+
outData,
|
|
1412
|
+
payload =>
|
|
1413
|
+
shouldUseWriteWithResponse
|
|
1414
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1415
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1416
|
+
isCurrentOwner
|
|
1417
|
+
);
|
|
1288
1418
|
} catch (e) {
|
|
1289
1419
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1290
|
-
|
|
1420
|
+
releaseOwnershipIfCurrent();
|
|
1421
|
+
if (isWedgedWriteError(e)) {
|
|
1422
|
+
throw e;
|
|
1423
|
+
}
|
|
1291
1424
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1292
1425
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1293
1426
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1330,8 +1463,13 @@ export default class ReactNativeBleTransport {
|
|
|
1330
1463
|
}
|
|
1331
1464
|
const isProbeTimeout =
|
|
1332
1465
|
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1466
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1467
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1468
|
+
// call is actively using.
|
|
1469
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1333
1470
|
if (
|
|
1334
1471
|
!isProbeTimeout &&
|
|
1472
|
+
!isStaleCall &&
|
|
1335
1473
|
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1336
1474
|
) {
|
|
1337
1475
|
await this.disconnect(uuid);
|
|
@@ -1411,7 +1549,10 @@ export default class ReactNativeBleTransport {
|
|
|
1411
1549
|
delete transportCache[session];
|
|
1412
1550
|
}
|
|
1413
1551
|
this.deviceProtocol.delete(session);
|
|
1552
|
+
this.probingProtocols.delete(session);
|
|
1414
1553
|
this.deviceProtocolHints.delete(session);
|
|
1554
|
+
this.sessionProtocols.delete(session);
|
|
1555
|
+
this.protocolReprobeFailures.delete(session);
|
|
1415
1556
|
this.protocolV2Assemblers.delete(session);
|
|
1416
1557
|
this.resetProtocolV2Frames(session);
|
|
1417
1558
|
|
|
@@ -1437,6 +1578,113 @@ export default class ReactNativeBleTransport {
|
|
|
1437
1578
|
this.runPromiseDeviceId = null;
|
|
1438
1579
|
}
|
|
1439
1580
|
|
|
1581
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1582
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1583
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1584
|
+
let timedOut = false;
|
|
1585
|
+
const pending = connect();
|
|
1586
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1587
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1588
|
+
pending.catch(() => undefined);
|
|
1589
|
+
try {
|
|
1590
|
+
const result = await Promise.race([
|
|
1591
|
+
pending,
|
|
1592
|
+
new Promise<never>((_, reject) => {
|
|
1593
|
+
timer = setTimeout(() => {
|
|
1594
|
+
timedOut = true;
|
|
1595
|
+
reject(
|
|
1596
|
+
ERRORS.TypedError(
|
|
1597
|
+
HardwareErrorCode.BleConnectedError,
|
|
1598
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1599
|
+
)
|
|
1600
|
+
);
|
|
1601
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1602
|
+
}),
|
|
1603
|
+
]);
|
|
1604
|
+
return result;
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1607
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1608
|
+
}
|
|
1609
|
+
throw error;
|
|
1610
|
+
} finally {
|
|
1611
|
+
if (timer) clearTimeout(timer);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1616
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1617
|
+
uuid: string,
|
|
1618
|
+
device: Device
|
|
1619
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1620
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1621
|
+
let timedOut = false;
|
|
1622
|
+
const pending = this.resolveCharacteristics(device);
|
|
1623
|
+
pending.catch(() => undefined);
|
|
1624
|
+
try {
|
|
1625
|
+
const result = await Promise.race([
|
|
1626
|
+
pending,
|
|
1627
|
+
new Promise<never>((_, reject) => {
|
|
1628
|
+
timer = setTimeout(() => {
|
|
1629
|
+
timedOut = true;
|
|
1630
|
+
reject(
|
|
1631
|
+
ERRORS.TypedError(
|
|
1632
|
+
HardwareErrorCode.BleConnectedError,
|
|
1633
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1634
|
+
)
|
|
1635
|
+
);
|
|
1636
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1637
|
+
}),
|
|
1638
|
+
]);
|
|
1639
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1640
|
+
return result;
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1643
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1644
|
+
}
|
|
1645
|
+
throw error;
|
|
1646
|
+
} finally {
|
|
1647
|
+
if (timer) clearTimeout(timer);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1653
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1654
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1655
|
+
*/
|
|
1656
|
+
private abandonStalledConnection(
|
|
1657
|
+
uuid: string,
|
|
1658
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1659
|
+
) {
|
|
1660
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1661
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1662
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1663
|
+
stage,
|
|
1664
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1668
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1669
|
+
});
|
|
1670
|
+
const stalled = transportCache[uuid];
|
|
1671
|
+
if (stalled) {
|
|
1672
|
+
delete transportCache[uuid];
|
|
1673
|
+
}
|
|
1674
|
+
this.deviceProtocol.delete(uuid);
|
|
1675
|
+
this.probingProtocols.delete(uuid);
|
|
1676
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1677
|
+
this.resetProtocolV2Frames(uuid);
|
|
1678
|
+
|
|
1679
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1680
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1681
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1682
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1683
|
+
this.resetPlxManager();
|
|
1684
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1440
1688
|
private getCachedTransport(uuid: string) {
|
|
1441
1689
|
const transport = transportCache[uuid];
|
|
1442
1690
|
if (!transport) {
|
|
@@ -1445,6 +1693,107 @@ export default class ReactNativeBleTransport {
|
|
|
1445
1693
|
return transport;
|
|
1446
1694
|
}
|
|
1447
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1698
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1699
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1700
|
+
* cached and every later call would hang on it again.
|
|
1701
|
+
*/
|
|
1702
|
+
private async writeBlePacket(
|
|
1703
|
+
uuid: string,
|
|
1704
|
+
data: string,
|
|
1705
|
+
write: (payload: string) => Promise<unknown>,
|
|
1706
|
+
isCurrentOwner?: () => boolean
|
|
1707
|
+
) {
|
|
1708
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1709
|
+
let timedOut = false;
|
|
1710
|
+
try {
|
|
1711
|
+
await Promise.race([
|
|
1712
|
+
write(data),
|
|
1713
|
+
new Promise<never>((_, reject) => {
|
|
1714
|
+
timer = setTimeout(() => {
|
|
1715
|
+
timedOut = true;
|
|
1716
|
+
reject(
|
|
1717
|
+
ERRORS.TypedError(
|
|
1718
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1719
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1720
|
+
)
|
|
1721
|
+
);
|
|
1722
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1723
|
+
}),
|
|
1724
|
+
]);
|
|
1725
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
if (timedOut) {
|
|
1728
|
+
// A superseded call's late write must not tear down the link the current
|
|
1729
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1730
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1731
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1732
|
+
} else {
|
|
1733
|
+
this.tearDownWedgedLink(uuid);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
throw error;
|
|
1737
|
+
} finally {
|
|
1738
|
+
if (timer) clearTimeout(timer);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1744
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1745
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1746
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1747
|
+
*/
|
|
1748
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1749
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1750
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1751
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1752
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
const wedged = transportCache[uuid];
|
|
1756
|
+
this.disconnect(uuid).catch(error => {
|
|
1757
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1758
|
+
});
|
|
1759
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1760
|
+
delete transportCache[uuid];
|
|
1761
|
+
}
|
|
1762
|
+
this.deviceProtocol.delete(uuid);
|
|
1763
|
+
this.probingProtocols.delete(uuid);
|
|
1764
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1765
|
+
this.resetProtocolV2Frames(uuid);
|
|
1766
|
+
|
|
1767
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1768
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1769
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1770
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1771
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1772
|
+
this.resetPlxManager();
|
|
1773
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
private resetPlxManager() {
|
|
1778
|
+
const manager = this.blePlxManager;
|
|
1779
|
+
this.blePlxManager = undefined;
|
|
1780
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1781
|
+
Object.keys(transportCache).forEach(key => {
|
|
1782
|
+
delete transportCache[key];
|
|
1783
|
+
});
|
|
1784
|
+
this.deviceProtocol.clear();
|
|
1785
|
+
this.probingProtocols.clear();
|
|
1786
|
+
this.sessionProtocols.clear();
|
|
1787
|
+
this.protocolReprobeFailures.clear();
|
|
1788
|
+
this.monitorTokens.clear();
|
|
1789
|
+
this.protocolV2Assemblers.clear();
|
|
1790
|
+
try {
|
|
1791
|
+
manager?.destroy();
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1448
1797
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1449
1798
|
return ERRORS.TypedError(
|
|
1450
1799
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1460,11 +1809,19 @@ export default class ReactNativeBleTransport {
|
|
|
1460
1809
|
}
|
|
1461
1810
|
|
|
1462
1811
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1812
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1813
|
+
this.probingProtocols.delete(uuid);
|
|
1814
|
+
}
|
|
1463
1815
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1464
1816
|
this.deviceProtocol.delete(uuid);
|
|
1465
1817
|
}
|
|
1466
1818
|
}
|
|
1467
1819
|
|
|
1820
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1821
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1822
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1468
1825
|
private async detectProtocol(
|
|
1469
1826
|
uuid: string,
|
|
1470
1827
|
expectedProtocol?: ProtocolType,
|
|
@@ -1484,6 +1841,7 @@ export default class ReactNativeBleTransport {
|
|
|
1484
1841
|
if (expectedProtocol === 'V1') {
|
|
1485
1842
|
if (await this.probeProtocolV1(uuid)) {
|
|
1486
1843
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1844
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1487
1845
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1488
1846
|
deviceId: uuid,
|
|
1489
1847
|
protocol: 'V1',
|
|
@@ -1497,6 +1855,7 @@ export default class ReactNativeBleTransport {
|
|
|
1497
1855
|
if (expectedProtocol === 'V2') {
|
|
1498
1856
|
if (await this.probeProtocolV2(uuid)) {
|
|
1499
1857
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1858
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1500
1859
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1501
1860
|
deviceId: uuid,
|
|
1502
1861
|
protocol: 'V2',
|
|
@@ -1509,8 +1868,18 @@ export default class ReactNativeBleTransport {
|
|
|
1509
1868
|
|
|
1510
1869
|
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1511
1870
|
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1512
|
-
const
|
|
1871
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1872
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1873
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1513
1874
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1875
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1876
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1877
|
+
// only adds its timeout to every poll.
|
|
1878
|
+
const trustSessionProtocol =
|
|
1879
|
+
sessionProtocol !== undefined &&
|
|
1880
|
+
!protocolHint &&
|
|
1881
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1882
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1514
1883
|
|
|
1515
1884
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1516
1885
|
const protocol = probeOrder[i];
|
|
@@ -1528,6 +1897,8 @@ export default class ReactNativeBleTransport {
|
|
|
1528
1897
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1529
1898
|
if (detected) {
|
|
1530
1899
|
this.deviceProtocol.set(uuid, protocol);
|
|
1900
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1901
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1531
1902
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1532
1903
|
deviceId: uuid,
|
|
1533
1904
|
protocol,
|
|
@@ -1537,7 +1908,16 @@ export default class ReactNativeBleTransport {
|
|
|
1537
1908
|
}
|
|
1538
1909
|
}
|
|
1539
1910
|
|
|
1911
|
+
if (trustSessionProtocol) {
|
|
1912
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1913
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1914
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1915
|
+
} else {
|
|
1916
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1540
1919
|
this.deviceProtocol.delete(uuid);
|
|
1920
|
+
this.probingProtocols.delete(uuid);
|
|
1541
1921
|
throw this.createProtocolDetectionError();
|
|
1542
1922
|
}
|
|
1543
1923
|
|
|
@@ -1599,14 +1979,20 @@ export default class ReactNativeBleTransport {
|
|
|
1599
1979
|
}
|
|
1600
1980
|
|
|
1601
1981
|
try {
|
|
1602
|
-
this.
|
|
1982
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1603
1983
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1604
1984
|
// session before Core has a chance to restore a hidden wallet.
|
|
1605
1985
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1986
|
+
this.probingProtocols.delete(uuid);
|
|
1606
1987
|
return true;
|
|
1607
1988
|
} catch (error) {
|
|
1608
1989
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1609
1990
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1991
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
1992
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
1993
|
+
if (isWedgedWriteError(error)) {
|
|
1994
|
+
throw error;
|
|
1995
|
+
}
|
|
1610
1996
|
return false;
|
|
1611
1997
|
}
|
|
1612
1998
|
}
|
|
@@ -1616,7 +2002,7 @@ export default class ReactNativeBleTransport {
|
|
|
1616
2002
|
return false;
|
|
1617
2003
|
}
|
|
1618
2004
|
|
|
1619
|
-
this.
|
|
2005
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1620
2006
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1621
2007
|
const detected = await probeProtocolV2Helper({
|
|
1622
2008
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1631,6 +2017,8 @@ export default class ReactNativeBleTransport {
|
|
|
1631
2017
|
});
|
|
1632
2018
|
if (!detected) {
|
|
1633
2019
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2020
|
+
} else {
|
|
2021
|
+
this.probingProtocols.delete(uuid);
|
|
1634
2022
|
}
|
|
1635
2023
|
return detected;
|
|
1636
2024
|
}
|
|
@@ -1712,6 +2100,7 @@ export default class ReactNativeBleTransport {
|
|
|
1712
2100
|
}
|
|
1713
2101
|
|
|
1714
2102
|
private async writeProtocolV2Packet(
|
|
2103
|
+
uuid: string,
|
|
1715
2104
|
transport: BleTransport,
|
|
1716
2105
|
base64: string,
|
|
1717
2106
|
context: ProtocolV2CallContext,
|
|
@@ -1727,11 +2116,24 @@ export default class ReactNativeBleTransport {
|
|
|
1727
2116
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1728
2117
|
}
|
|
1729
2118
|
try {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2119
|
+
await this.writeBlePacket(
|
|
2120
|
+
uuid,
|
|
2121
|
+
base64,
|
|
2122
|
+
payload =>
|
|
2123
|
+
shouldUseWriteWithResponse
|
|
2124
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2125
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2126
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2127
|
+
// tear down the link that the current generation is using.
|
|
2128
|
+
() => {
|
|
2129
|
+
try {
|
|
2130
|
+
assertCurrentGeneration();
|
|
2131
|
+
return !context.signal.aborted;
|
|
2132
|
+
} catch {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
);
|
|
1735
2137
|
assertCurrentGeneration();
|
|
1736
2138
|
return;
|
|
1737
2139
|
} catch (error) {
|
|
@@ -1754,6 +2156,7 @@ export default class ReactNativeBleTransport {
|
|
|
1754
2156
|
}
|
|
1755
2157
|
|
|
1756
2158
|
private async writeProtocolV2Frame(
|
|
2159
|
+
uuid: string,
|
|
1757
2160
|
transport: BleTransport,
|
|
1758
2161
|
frame: Uint8Array,
|
|
1759
2162
|
context: ProtocolV2CallContext,
|
|
@@ -1785,6 +2188,7 @@ export default class ReactNativeBleTransport {
|
|
|
1785
2188
|
wait: delay,
|
|
1786
2189
|
writePacket: packet =>
|
|
1787
2190
|
this.writeProtocolV2Packet(
|
|
2191
|
+
uuid,
|
|
1788
2192
|
transport,
|
|
1789
2193
|
Buffer.from(packet).toString('base64'),
|
|
1790
2194
|
context,
|
|
@@ -1849,7 +2253,13 @@ export default class ReactNativeBleTransport {
|
|
|
1849
2253
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1850
2254
|
assertCurrentGeneration();
|
|
1851
2255
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1852
|
-
await this.writeProtocolV2Frame(
|
|
2256
|
+
await this.writeProtocolV2Frame(
|
|
2257
|
+
uuid,
|
|
2258
|
+
currentTransport,
|
|
2259
|
+
frame,
|
|
2260
|
+
context,
|
|
2261
|
+
assertCurrentGeneration
|
|
2262
|
+
);
|
|
1853
2263
|
},
|
|
1854
2264
|
readFrame: async () => {
|
|
1855
2265
|
assertCurrentGeneration();
|
|
@@ -1874,6 +2284,6 @@ export default class ReactNativeBleTransport {
|
|
|
1874
2284
|
}
|
|
1875
2285
|
|
|
1876
2286
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1877
|
-
return this.
|
|
2287
|
+
return this.getActiveProtocol(path);
|
|
1878
2288
|
}
|
|
1879
2289
|
}
|