@onekeyfe/hd-transport-react-native 1.2.0-alpha.75 → 1.2.0-alpha.76
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 +88 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -44
- package/jest.config.js +5 -0
- package/package.json +5 -5
- package/src/BleTransport.ts +9 -6
- package/src/__tests__/BleTransport.test.ts +24 -3
- package/src/__tests__/connectTimeout.test.ts +280 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +35 -5
- 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
|
@@ -142,6 +142,22 @@ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, max
|
|
|
142
142
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
143
143
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
144
144
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
145
|
+
/**
|
|
146
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
147
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
148
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
149
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
150
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
151
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
152
|
+
*/
|
|
153
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
154
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
155
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
156
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
157
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
158
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
159
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
160
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
145
161
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
146
162
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
147
163
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -204,12 +220,52 @@ const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
|
204
220
|
const getRequestedBleMtu = () =>
|
|
205
221
|
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
206
222
|
|
|
223
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
224
|
+
|
|
207
225
|
const connectOptions: Record<string, unknown> = {
|
|
208
226
|
requestMTU: getRequestedBleMtu(),
|
|
209
|
-
timeout:
|
|
227
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
210
228
|
refreshGatt: 'OnConnected',
|
|
211
229
|
};
|
|
212
230
|
|
|
231
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
232
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
233
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
238
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
239
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
240
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
241
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
242
|
+
*/
|
|
243
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
244
|
+
/**
|
|
245
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
246
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
247
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
248
|
+
* budget.
|
|
249
|
+
*/
|
|
250
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
251
|
+
/**
|
|
252
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
253
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
254
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
255
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
256
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
257
|
+
*/
|
|
258
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
259
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
260
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
261
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
262
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
263
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
264
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
265
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
266
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
267
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
268
|
+
|
|
213
269
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
214
270
|
|
|
215
271
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -307,8 +363,28 @@ export default class ReactNativeBleTransport {
|
|
|
307
363
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
308
364
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
309
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
368
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
369
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
370
|
+
* transport that was never validated.
|
|
371
|
+
*/
|
|
372
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
373
|
+
|
|
374
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
375
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
376
|
+
|
|
377
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
378
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
379
|
+
|
|
310
380
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
311
381
|
|
|
382
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
383
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
384
|
+
|
|
385
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
386
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
387
|
+
|
|
312
388
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
313
389
|
|
|
314
390
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -532,22 +608,21 @@ export default class ReactNativeBleTransport {
|
|
|
532
608
|
const isConnected = await device.isConnected().catch(() => false);
|
|
533
609
|
if (!isConnected) {
|
|
534
610
|
try {
|
|
535
|
-
device = await device.connect(connectOptions);
|
|
611
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
536
612
|
} catch (e) {
|
|
537
613
|
if (
|
|
538
614
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
539
615
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
540
616
|
) {
|
|
541
|
-
device = await device.connect();
|
|
617
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
542
618
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
543
619
|
throw e;
|
|
544
620
|
}
|
|
545
621
|
}
|
|
546
622
|
}
|
|
547
623
|
|
|
548
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
549
|
-
device
|
|
550
|
-
);
|
|
624
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
625
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
551
626
|
|
|
552
627
|
transport.device = device;
|
|
553
628
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -726,7 +801,7 @@ export default class ReactNativeBleTransport {
|
|
|
726
801
|
characteristics?: ResolvedBleCharacteristics
|
|
727
802
|
) {
|
|
728
803
|
const { writeCharacteristic, notifyCharacteristic } =
|
|
729
|
-
characteristics ?? (await this.
|
|
804
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
730
805
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
731
806
|
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
732
807
|
const monitorToken = this.nextMonitorToken;
|
|
@@ -863,15 +938,22 @@ export default class ReactNativeBleTransport {
|
|
|
863
938
|
if (!device) {
|
|
864
939
|
Log?.debug('try to connect to device: ', uuid);
|
|
865
940
|
try {
|
|
866
|
-
device = await
|
|
941
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
942
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
943
|
+
);
|
|
867
944
|
} catch (e) {
|
|
868
945
|
Log?.debug('try to connect to device has error: ', e);
|
|
946
|
+
if (isConnectTimeoutError(e)) {
|
|
947
|
+
throw e;
|
|
948
|
+
}
|
|
869
949
|
if (
|
|
870
950
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
871
951
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
872
952
|
) {
|
|
873
953
|
Log?.debug('first try to reconnect without params');
|
|
874
|
-
device = await
|
|
954
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
955
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
956
|
+
);
|
|
875
957
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
876
958
|
Log?.debug('device already connected');
|
|
877
959
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -887,26 +969,36 @@ export default class ReactNativeBleTransport {
|
|
|
887
969
|
|
|
888
970
|
if (!(await device.isConnected())) {
|
|
889
971
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
972
|
+
const disconnectedDevice = device;
|
|
890
973
|
|
|
891
974
|
try {
|
|
892
|
-
device = await
|
|
975
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
976
|
+
disconnectedDevice.connect(connectOptions)
|
|
977
|
+
);
|
|
893
978
|
} catch (e) {
|
|
894
979
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
980
|
+
if (isConnectTimeoutError(e)) {
|
|
981
|
+
throw e;
|
|
982
|
+
}
|
|
895
983
|
if (
|
|
896
984
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
897
985
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
898
986
|
) {
|
|
899
987
|
Log?.debug('second try to reconnect without params');
|
|
900
988
|
try {
|
|
901
|
-
device = await
|
|
989
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
990
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
991
|
+
);
|
|
902
992
|
} catch (e) {
|
|
903
993
|
Log?.debug('last try to reconnect error: ', e);
|
|
904
994
|
// last try to reconnect device if this issue exists
|
|
905
995
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
906
996
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
907
997
|
Log?.debug('last try to reconnect');
|
|
908
|
-
await
|
|
909
|
-
device = await
|
|
998
|
+
await disconnectedDevice.cancelConnection();
|
|
999
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1000
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1001
|
+
);
|
|
910
1002
|
}
|
|
911
1003
|
}
|
|
912
1004
|
} else {
|
|
@@ -917,9 +1009,8 @@ export default class ReactNativeBleTransport {
|
|
|
917
1009
|
|
|
918
1010
|
device = await resolveNegotiatedMtu(device);
|
|
919
1011
|
const acquiredDevice = device;
|
|
920
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
921
|
-
acquiredDevice
|
|
922
|
-
);
|
|
1012
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
1013
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
923
1014
|
|
|
924
1015
|
const protocolHint = expectedProtocol
|
|
925
1016
|
? undefined
|
|
@@ -983,7 +1074,7 @@ export default class ReactNativeBleTransport {
|
|
|
983
1074
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
984
1075
|
return;
|
|
985
1076
|
}
|
|
986
|
-
if (this.
|
|
1077
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
987
1078
|
let errorCode:
|
|
988
1079
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
989
1080
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -1053,7 +1144,7 @@ export default class ReactNativeBleTransport {
|
|
|
1053
1144
|
|
|
1054
1145
|
try {
|
|
1055
1146
|
const data = Buffer.from(c.value as string, 'base64');
|
|
1056
|
-
const protocol = this.
|
|
1147
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
1057
1148
|
if (!protocol) {
|
|
1058
1149
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
1059
1150
|
return;
|
|
@@ -1087,7 +1178,7 @@ export default class ReactNativeBleTransport {
|
|
|
1087
1178
|
} catch (error) {
|
|
1088
1179
|
Log?.debug('monitor data error: ', error);
|
|
1089
1180
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1090
|
-
if (this.
|
|
1181
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1091
1182
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1092
1183
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
1093
1184
|
this.runPromise?.reject(notifyError);
|
|
@@ -1155,6 +1246,7 @@ export default class ReactNativeBleTransport {
|
|
|
1155
1246
|
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1156
1247
|
|
|
1157
1248
|
this.deviceProtocol.delete(uuid);
|
|
1249
|
+
this.probingProtocols.delete(uuid);
|
|
1158
1250
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1159
1251
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1160
1252
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1221,8 +1313,25 @@ export default class ReactNativeBleTransport {
|
|
|
1221
1313
|
const transport = this.getCachedTransport(uuid);
|
|
1222
1314
|
const runPromise = createDeferred<string>();
|
|
1223
1315
|
runPromise.promise.catch(() => undefined);
|
|
1316
|
+
const supersededRunPromise = this.runPromise;
|
|
1317
|
+
if (supersededRunPromise) {
|
|
1318
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1319
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1320
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1321
|
+
// tear down the shared connection while another call is using it.
|
|
1322
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1323
|
+
}
|
|
1224
1324
|
this.runPromise = runPromise;
|
|
1225
1325
|
this.runPromiseDeviceId = uuid;
|
|
1326
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1327
|
+
// only the call that still owns the slot may release it.
|
|
1328
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1329
|
+
if (this.runPromise === runPromise) {
|
|
1330
|
+
this.runPromise = null;
|
|
1331
|
+
this.runPromiseDeviceId = null;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1226
1335
|
const messages = this._messages;
|
|
1227
1336
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1228
1337
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1248,6 +1357,9 @@ export default class ReactNativeBleTransport {
|
|
|
1248
1357
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1249
1358
|
} catch (e) {
|
|
1250
1359
|
onError(e);
|
|
1360
|
+
if (isWedgedWriteError(e)) {
|
|
1361
|
+
throw e;
|
|
1362
|
+
}
|
|
1251
1363
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1252
1364
|
}
|
|
1253
1365
|
}
|
|
@@ -1279,6 +1391,9 @@ export default class ReactNativeBleTransport {
|
|
|
1279
1391
|
}
|
|
1280
1392
|
} catch (e) {
|
|
1281
1393
|
onError(e);
|
|
1394
|
+
if (isWedgedWriteError(e)) {
|
|
1395
|
+
throw e;
|
|
1396
|
+
}
|
|
1282
1397
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1283
1398
|
}
|
|
1284
1399
|
}
|
|
@@ -1292,9 +1407,15 @@ export default class ReactNativeBleTransport {
|
|
|
1292
1407
|
if (name === 'EmmcFileWrite') {
|
|
1293
1408
|
await writeChunkedData(
|
|
1294
1409
|
buffers,
|
|
1295
|
-
data =>
|
|
1410
|
+
data =>
|
|
1411
|
+
this.writeBlePacket(
|
|
1412
|
+
uuid,
|
|
1413
|
+
data,
|
|
1414
|
+
payload => transport.writeWithRetry(payload),
|
|
1415
|
+
isCurrentOwner
|
|
1416
|
+
),
|
|
1296
1417
|
e => {
|
|
1297
|
-
|
|
1418
|
+
releaseOwnershipIfCurrent();
|
|
1298
1419
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1299
1420
|
}
|
|
1300
1421
|
);
|
|
@@ -1315,7 +1436,12 @@ export default class ReactNativeBleTransport {
|
|
|
1315
1436
|
// eslint-disable-next-line no-constant-condition
|
|
1316
1437
|
while (true) {
|
|
1317
1438
|
try {
|
|
1318
|
-
await
|
|
1439
|
+
await this.writeBlePacket(
|
|
1440
|
+
uuid,
|
|
1441
|
+
data,
|
|
1442
|
+
payload => transport.writeWithRetry(payload),
|
|
1443
|
+
isCurrentOwner
|
|
1444
|
+
);
|
|
1319
1445
|
return;
|
|
1320
1446
|
} catch (error) {
|
|
1321
1447
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
@@ -1334,7 +1460,7 @@ export default class ReactNativeBleTransport {
|
|
|
1334
1460
|
}
|
|
1335
1461
|
},
|
|
1336
1462
|
e => {
|
|
1337
|
-
|
|
1463
|
+
releaseOwnershipIfCurrent();
|
|
1338
1464
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1339
1465
|
}
|
|
1340
1466
|
);
|
|
@@ -1345,14 +1471,21 @@ export default class ReactNativeBleTransport {
|
|
|
1345
1471
|
try {
|
|
1346
1472
|
const shouldUseWriteWithResponse =
|
|
1347
1473
|
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1474
|
+
await this.writeBlePacket(
|
|
1475
|
+
uuid,
|
|
1476
|
+
outData,
|
|
1477
|
+
payload =>
|
|
1478
|
+
shouldUseWriteWithResponse
|
|
1479
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1480
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1481
|
+
isCurrentOwner
|
|
1482
|
+
);
|
|
1353
1483
|
} catch (e) {
|
|
1354
1484
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1355
|
-
|
|
1485
|
+
releaseOwnershipIfCurrent();
|
|
1486
|
+
if (isWedgedWriteError(e)) {
|
|
1487
|
+
throw e;
|
|
1488
|
+
}
|
|
1356
1489
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1357
1490
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1358
1491
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1395,8 +1528,13 @@ export default class ReactNativeBleTransport {
|
|
|
1395
1528
|
}
|
|
1396
1529
|
const isProbeTimeout =
|
|
1397
1530
|
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1531
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1532
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1533
|
+
// call is actively using.
|
|
1534
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1398
1535
|
if (
|
|
1399
1536
|
!isProbeTimeout &&
|
|
1537
|
+
!isStaleCall &&
|
|
1400
1538
|
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1401
1539
|
) {
|
|
1402
1540
|
await this.disconnect(uuid);
|
|
@@ -1476,7 +1614,10 @@ export default class ReactNativeBleTransport {
|
|
|
1476
1614
|
delete transportCache[session];
|
|
1477
1615
|
}
|
|
1478
1616
|
this.deviceProtocol.delete(session);
|
|
1617
|
+
this.probingProtocols.delete(session);
|
|
1479
1618
|
this.deviceProtocolHints.delete(session);
|
|
1619
|
+
this.sessionProtocols.delete(session);
|
|
1620
|
+
this.protocolReprobeFailures.delete(session);
|
|
1480
1621
|
this.protocolV2Assemblers.delete(session);
|
|
1481
1622
|
this.resetProtocolV2Frames(session);
|
|
1482
1623
|
|
|
@@ -1502,6 +1643,113 @@ export default class ReactNativeBleTransport {
|
|
|
1502
1643
|
this.runPromiseDeviceId = null;
|
|
1503
1644
|
}
|
|
1504
1645
|
|
|
1646
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1647
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1648
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1649
|
+
let timedOut = false;
|
|
1650
|
+
const pending = connect();
|
|
1651
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1652
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1653
|
+
pending.catch(() => undefined);
|
|
1654
|
+
try {
|
|
1655
|
+
const result = await Promise.race([
|
|
1656
|
+
pending,
|
|
1657
|
+
new Promise<never>((_, reject) => {
|
|
1658
|
+
timer = setTimeout(() => {
|
|
1659
|
+
timedOut = true;
|
|
1660
|
+
reject(
|
|
1661
|
+
ERRORS.TypedError(
|
|
1662
|
+
HardwareErrorCode.BleConnectedError,
|
|
1663
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1664
|
+
)
|
|
1665
|
+
);
|
|
1666
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1667
|
+
}),
|
|
1668
|
+
]);
|
|
1669
|
+
return result;
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1672
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1673
|
+
}
|
|
1674
|
+
throw error;
|
|
1675
|
+
} finally {
|
|
1676
|
+
if (timer) clearTimeout(timer);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1681
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1682
|
+
uuid: string,
|
|
1683
|
+
device: Device
|
|
1684
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1685
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1686
|
+
let timedOut = false;
|
|
1687
|
+
const pending = this.resolveCharacteristics(device);
|
|
1688
|
+
pending.catch(() => undefined);
|
|
1689
|
+
try {
|
|
1690
|
+
const result = await Promise.race([
|
|
1691
|
+
pending,
|
|
1692
|
+
new Promise<never>((_, reject) => {
|
|
1693
|
+
timer = setTimeout(() => {
|
|
1694
|
+
timedOut = true;
|
|
1695
|
+
reject(
|
|
1696
|
+
ERRORS.TypedError(
|
|
1697
|
+
HardwareErrorCode.BleConnectedError,
|
|
1698
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1699
|
+
)
|
|
1700
|
+
);
|
|
1701
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1702
|
+
}),
|
|
1703
|
+
]);
|
|
1704
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1705
|
+
return result;
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1708
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1709
|
+
}
|
|
1710
|
+
throw error;
|
|
1711
|
+
} finally {
|
|
1712
|
+
if (timer) clearTimeout(timer);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1718
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1719
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1720
|
+
*/
|
|
1721
|
+
private abandonStalledConnection(
|
|
1722
|
+
uuid: string,
|
|
1723
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1724
|
+
) {
|
|
1725
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1726
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1727
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1728
|
+
stage,
|
|
1729
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1733
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1734
|
+
});
|
|
1735
|
+
const stalled = transportCache[uuid];
|
|
1736
|
+
if (stalled) {
|
|
1737
|
+
delete transportCache[uuid];
|
|
1738
|
+
}
|
|
1739
|
+
this.deviceProtocol.delete(uuid);
|
|
1740
|
+
this.probingProtocols.delete(uuid);
|
|
1741
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1742
|
+
this.resetProtocolV2Frames(uuid);
|
|
1743
|
+
|
|
1744
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1745
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1746
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1747
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1748
|
+
this.resetPlxManager();
|
|
1749
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1505
1753
|
private getCachedTransport(uuid: string) {
|
|
1506
1754
|
const transport = transportCache[uuid];
|
|
1507
1755
|
if (!transport) {
|
|
@@ -1510,6 +1758,107 @@ export default class ReactNativeBleTransport {
|
|
|
1510
1758
|
return transport;
|
|
1511
1759
|
}
|
|
1512
1760
|
|
|
1761
|
+
/**
|
|
1762
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1763
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1764
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1765
|
+
* cached and every later call would hang on it again.
|
|
1766
|
+
*/
|
|
1767
|
+
private async writeBlePacket(
|
|
1768
|
+
uuid: string,
|
|
1769
|
+
data: string,
|
|
1770
|
+
write: (payload: string) => Promise<unknown>,
|
|
1771
|
+
isCurrentOwner?: () => boolean
|
|
1772
|
+
) {
|
|
1773
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1774
|
+
let timedOut = false;
|
|
1775
|
+
try {
|
|
1776
|
+
await Promise.race([
|
|
1777
|
+
write(data),
|
|
1778
|
+
new Promise<never>((_, reject) => {
|
|
1779
|
+
timer = setTimeout(() => {
|
|
1780
|
+
timedOut = true;
|
|
1781
|
+
reject(
|
|
1782
|
+
ERRORS.TypedError(
|
|
1783
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1784
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1785
|
+
)
|
|
1786
|
+
);
|
|
1787
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1788
|
+
}),
|
|
1789
|
+
]);
|
|
1790
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (timedOut) {
|
|
1793
|
+
// A superseded call's late write must not tear down the link the current
|
|
1794
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1795
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1796
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1797
|
+
} else {
|
|
1798
|
+
this.tearDownWedgedLink(uuid);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
throw error;
|
|
1802
|
+
} finally {
|
|
1803
|
+
if (timer) clearTimeout(timer);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1809
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1810
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1811
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1812
|
+
*/
|
|
1813
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1814
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1815
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1816
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1817
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
const wedged = transportCache[uuid];
|
|
1821
|
+
this.disconnect(uuid).catch(error => {
|
|
1822
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1823
|
+
});
|
|
1824
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1825
|
+
delete transportCache[uuid];
|
|
1826
|
+
}
|
|
1827
|
+
this.deviceProtocol.delete(uuid);
|
|
1828
|
+
this.probingProtocols.delete(uuid);
|
|
1829
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1830
|
+
this.resetProtocolV2Frames(uuid);
|
|
1831
|
+
|
|
1832
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1833
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1834
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1835
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1836
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1837
|
+
this.resetPlxManager();
|
|
1838
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
private resetPlxManager() {
|
|
1843
|
+
const manager = this.blePlxManager;
|
|
1844
|
+
this.blePlxManager = undefined;
|
|
1845
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1846
|
+
Object.keys(transportCache).forEach(key => {
|
|
1847
|
+
delete transportCache[key];
|
|
1848
|
+
});
|
|
1849
|
+
this.deviceProtocol.clear();
|
|
1850
|
+
this.probingProtocols.clear();
|
|
1851
|
+
this.sessionProtocols.clear();
|
|
1852
|
+
this.protocolReprobeFailures.clear();
|
|
1853
|
+
this.monitorTokens.clear();
|
|
1854
|
+
this.protocolV2Assemblers.clear();
|
|
1855
|
+
try {
|
|
1856
|
+
manager?.destroy();
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1513
1862
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1514
1863
|
return ERRORS.TypedError(
|
|
1515
1864
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1525,11 +1874,19 @@ export default class ReactNativeBleTransport {
|
|
|
1525
1874
|
}
|
|
1526
1875
|
|
|
1527
1876
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1877
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1878
|
+
this.probingProtocols.delete(uuid);
|
|
1879
|
+
}
|
|
1528
1880
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1529
1881
|
this.deviceProtocol.delete(uuid);
|
|
1530
1882
|
}
|
|
1531
1883
|
}
|
|
1532
1884
|
|
|
1885
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1886
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1887
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1533
1890
|
private async detectProtocol(
|
|
1534
1891
|
uuid: string,
|
|
1535
1892
|
expectedProtocol?: ProtocolType,
|
|
@@ -1549,6 +1906,7 @@ export default class ReactNativeBleTransport {
|
|
|
1549
1906
|
if (expectedProtocol === 'V1') {
|
|
1550
1907
|
if (await this.probeProtocolV1(uuid)) {
|
|
1551
1908
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1909
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1552
1910
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1553
1911
|
deviceId: uuid,
|
|
1554
1912
|
protocol: 'V1',
|
|
@@ -1562,6 +1920,7 @@ export default class ReactNativeBleTransport {
|
|
|
1562
1920
|
if (expectedProtocol === 'V2') {
|
|
1563
1921
|
if (await this.probeProtocolV2(uuid)) {
|
|
1564
1922
|
this.deviceProtocol.set(uuid, 'V2');
|
|
1923
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1565
1924
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1566
1925
|
deviceId: uuid,
|
|
1567
1926
|
protocol: 'V2',
|
|
@@ -1574,8 +1933,18 @@ export default class ReactNativeBleTransport {
|
|
|
1574
1933
|
|
|
1575
1934
|
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1576
1935
|
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1577
|
-
const
|
|
1936
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1937
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1938
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1578
1939
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1940
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1941
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1942
|
+
// only adds its timeout to every poll.
|
|
1943
|
+
const trustSessionProtocol =
|
|
1944
|
+
sessionProtocol !== undefined &&
|
|
1945
|
+
!protocolHint &&
|
|
1946
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1947
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1579
1948
|
|
|
1580
1949
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1581
1950
|
const protocol = probeOrder[i];
|
|
@@ -1593,6 +1962,8 @@ export default class ReactNativeBleTransport {
|
|
|
1593
1962
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1594
1963
|
if (detected) {
|
|
1595
1964
|
this.deviceProtocol.set(uuid, protocol);
|
|
1965
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1966
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1596
1967
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1597
1968
|
deviceId: uuid,
|
|
1598
1969
|
protocol,
|
|
@@ -1602,7 +1973,16 @@ export default class ReactNativeBleTransport {
|
|
|
1602
1973
|
}
|
|
1603
1974
|
}
|
|
1604
1975
|
|
|
1976
|
+
if (trustSessionProtocol) {
|
|
1977
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1978
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1979
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1980
|
+
} else {
|
|
1981
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1605
1984
|
this.deviceProtocol.delete(uuid);
|
|
1985
|
+
this.probingProtocols.delete(uuid);
|
|
1606
1986
|
throw this.createProtocolDetectionError();
|
|
1607
1987
|
}
|
|
1608
1988
|
|
|
@@ -1664,14 +2044,20 @@ export default class ReactNativeBleTransport {
|
|
|
1664
2044
|
}
|
|
1665
2045
|
|
|
1666
2046
|
try {
|
|
1667
|
-
this.
|
|
2047
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1668
2048
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1669
2049
|
// session before Core has a chance to restore a hidden wallet.
|
|
1670
2050
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2051
|
+
this.probingProtocols.delete(uuid);
|
|
1671
2052
|
return true;
|
|
1672
2053
|
} catch (error) {
|
|
1673
2054
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1674
2055
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2056
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
2057
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
2058
|
+
if (isWedgedWriteError(error)) {
|
|
2059
|
+
throw error;
|
|
2060
|
+
}
|
|
1675
2061
|
return false;
|
|
1676
2062
|
}
|
|
1677
2063
|
}
|
|
@@ -1681,7 +2067,7 @@ export default class ReactNativeBleTransport {
|
|
|
1681
2067
|
return false;
|
|
1682
2068
|
}
|
|
1683
2069
|
|
|
1684
|
-
this.
|
|
2070
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1685
2071
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1686
2072
|
const detected = await probeProtocolV2Helper({
|
|
1687
2073
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1696,6 +2082,8 @@ export default class ReactNativeBleTransport {
|
|
|
1696
2082
|
});
|
|
1697
2083
|
if (!detected) {
|
|
1698
2084
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2085
|
+
} else {
|
|
2086
|
+
this.probingProtocols.delete(uuid);
|
|
1699
2087
|
}
|
|
1700
2088
|
return detected;
|
|
1701
2089
|
}
|
|
@@ -1777,6 +2165,7 @@ export default class ReactNativeBleTransport {
|
|
|
1777
2165
|
}
|
|
1778
2166
|
|
|
1779
2167
|
private async writeProtocolV2Packet(
|
|
2168
|
+
uuid: string,
|
|
1780
2169
|
transport: BleTransport,
|
|
1781
2170
|
base64: string,
|
|
1782
2171
|
context: ProtocolV2CallContext,
|
|
@@ -1795,11 +2184,24 @@ export default class ReactNativeBleTransport {
|
|
|
1795
2184
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1796
2185
|
}
|
|
1797
2186
|
try {
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2187
|
+
await this.writeBlePacket(
|
|
2188
|
+
uuid,
|
|
2189
|
+
base64,
|
|
2190
|
+
payload =>
|
|
2191
|
+
shouldUseWriteWithResponse
|
|
2192
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2193
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2194
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2195
|
+
// tear down the link that the current generation is using.
|
|
2196
|
+
() => {
|
|
2197
|
+
try {
|
|
2198
|
+
assertCurrentGeneration();
|
|
2199
|
+
return !context.signal.aborted;
|
|
2200
|
+
} catch {
|
|
2201
|
+
return false;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
);
|
|
1803
2205
|
assertCurrentGeneration();
|
|
1804
2206
|
return;
|
|
1805
2207
|
} catch (error) {
|
|
@@ -1822,6 +2224,7 @@ export default class ReactNativeBleTransport {
|
|
|
1822
2224
|
}
|
|
1823
2225
|
|
|
1824
2226
|
private async writeProtocolV2Frame(
|
|
2227
|
+
uuid: string,
|
|
1825
2228
|
transport: BleTransport,
|
|
1826
2229
|
frame: Uint8Array,
|
|
1827
2230
|
context: ProtocolV2CallContext,
|
|
@@ -1843,6 +2246,7 @@ export default class ReactNativeBleTransport {
|
|
|
1843
2246
|
wait: delay,
|
|
1844
2247
|
writePacket: packet =>
|
|
1845
2248
|
this.writeProtocolV2Packet(
|
|
2249
|
+
uuid,
|
|
1846
2250
|
transport,
|
|
1847
2251
|
Buffer.from(packet).toString('base64'),
|
|
1848
2252
|
context,
|
|
@@ -2020,7 +2424,13 @@ export default class ReactNativeBleTransport {
|
|
|
2020
2424
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
2021
2425
|
assertCurrentGeneration();
|
|
2022
2426
|
const currentTransport = this.getCachedTransport(uuid);
|
|
2023
|
-
await this.writeProtocolV2Frame(
|
|
2427
|
+
await this.writeProtocolV2Frame(
|
|
2428
|
+
uuid,
|
|
2429
|
+
currentTransport,
|
|
2430
|
+
frame,
|
|
2431
|
+
context,
|
|
2432
|
+
assertCurrentGeneration
|
|
2433
|
+
);
|
|
2024
2434
|
},
|
|
2025
2435
|
readFrame: async () => {
|
|
2026
2436
|
assertCurrentGeneration();
|
|
@@ -2045,6 +2455,6 @@ export default class ReactNativeBleTransport {
|
|
|
2045
2455
|
}
|
|
2046
2456
|
|
|
2047
2457
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
2048
|
-
return this.
|
|
2458
|
+
return this.getActiveProtocol(path);
|
|
2049
2459
|
}
|
|
2050
2460
|
}
|