@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/index.js CHANGED
@@ -216,10 +216,6 @@ class BleTransport {
216
216
  }
217
217
  writeWithRetry(data) {
218
218
  return __awaiter(this, void 0, void 0, function* () {
219
- if (reactNative.Platform.OS === 'ios' && this.writeCharacteristic.isWritableWithResponse) {
220
- yield this.writeCharacteristic.writeWithResponse(data);
221
- return;
222
- }
223
219
  yield this.writeCharacteristic.writeWithoutResponse(data);
224
220
  });
225
221
  }
@@ -280,6 +276,12 @@ const getFirmwareUploadWriteRetryType = (error) => {
280
276
  const resolveFirmwareUploadRetryDelay = (attempt, baseDelayMs = 200, maxDelayMs = 1200) => Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
281
277
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
282
278
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10000;
279
+ const BLE_WRITE_PACKET_TIMEOUT_MS = 10000;
280
+ const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
281
+ const isWedgedWriteError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === hdShared.HardwareErrorCode.BleWriteCharacteristicError &&
282
+ typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' &&
283
+ error.message.startsWith(WEDGED_WRITE_MESSAGE);
284
+ const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
283
285
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
284
286
  const IOS_NOTIFY_READY_DELAY_MS = 150;
285
287
  const ANDROID_NOTIFY_READY_DELAY_MS = 300;
@@ -319,11 +321,24 @@ const ANDROID_REQUEST_MTU = 517;
319
321
  const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
320
322
  const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
321
323
  const getRequestedBleMtu = () => reactNative.Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
324
+ const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
322
325
  const connectOptions = {
323
326
  requestMTU: getRequestedBleMtu(),
324
- timeout: 3000,
327
+ timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
325
328
  refreshGatt: 'OnConnected',
326
329
  };
330
+ const fallbackConnectOptions = {
331
+ timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
332
+ };
333
+ const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
334
+ const BLE_GATT_SETUP_TIMEOUT_MS = 10000;
335
+ const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
336
+ const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
337
+ const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
338
+ const isConnectTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === hdShared.HardwareErrorCode.BleConnectedError &&
339
+ typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' &&
340
+ error.message.startsWith(CONNECT_TIMEOUT_MESSAGE);
341
+ const isNativeOperationTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.OperationTimedOut;
327
342
  const tryToGetConfiguration = (device) => {
328
343
  if (!device || !device.serviceUUIDs)
329
344
  return null;
@@ -383,7 +398,12 @@ class ReactNativeBleTransport {
383
398
  this.runPromiseDeviceId = null;
384
399
  this.firmwareUploadWriteRecoveryIds = new Set();
385
400
  this.deviceProtocol = new Map();
401
+ this.probingProtocols = new Map();
402
+ this.writeTimeoutCounts = new Map();
403
+ this.connectionSetupTimeoutCounts = new Map();
386
404
  this.deviceProtocolHints = new Map();
405
+ this.sessionProtocols = new Map();
406
+ this.protocolReprobeFailures = new Map();
387
407
  this.protocolV2Assemblers = new Map();
388
408
  this.protocolV2FrameQueues = new Map();
389
409
  this.protocolV2FramePromises = new Map();
@@ -578,19 +598,19 @@ class ReactNativeBleTransport {
578
598
  const isConnected = yield device.isConnected().catch(() => false);
579
599
  if (!isConnected) {
580
600
  try {
581
- device = yield device.connect(connectOptions);
601
+ device = yield this.connectWithTimeout(uuid, () => device.connect(connectOptions));
582
602
  }
583
603
  catch (e) {
584
604
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
585
605
  e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
586
- device = yield device.connect();
606
+ device = yield this.connectWithTimeout(uuid, () => device.connect());
587
607
  }
588
608
  else if (e.errorCode !== reactNativeBlePlx.BleErrorCode.DeviceAlreadyConnected) {
589
609
  throw e;
590
610
  }
591
611
  }
592
612
  }
593
- const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristics(device);
613
+ const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristicsWithTimeout(uuid, device);
594
614
  transport.device = device;
595
615
  transport.writeCharacteristic = writeCharacteristic;
596
616
  transport.notifyCharacteristic = notifyCharacteristic;
@@ -727,7 +747,7 @@ class ReactNativeBleTransport {
727
747
  }
728
748
  installTransportForAcquire(uuid, device, characteristics) {
729
749
  return __awaiter(this, void 0, void 0, function* () {
730
- const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.resolveCharacteristics(device));
750
+ const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.resolveCharacteristicsWithTimeout(uuid, device));
731
751
  const transport$1 = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
732
752
  transport$1.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
733
753
  const monitorToken = this.nextMonitorToken;
@@ -834,14 +854,17 @@ class ReactNativeBleTransport {
834
854
  if (!device) {
835
855
  Log === null || Log === void 0 ? void 0 : Log.debug('try to connect to device: ', uuid);
836
856
  try {
837
- device = yield blePlxManager.connectToDevice(uuid, connectOptions);
857
+ device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, connectOptions));
838
858
  }
839
859
  catch (e) {
840
860
  Log === null || Log === void 0 ? void 0 : Log.debug('try to connect to device has error: ', e);
861
+ if (isConnectTimeoutError(e)) {
862
+ throw e;
863
+ }
841
864
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
842
865
  e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
843
866
  Log === null || Log === void 0 ? void 0 : Log.debug('first try to reconnect without params');
844
- device = yield blePlxManager.connectToDevice(uuid);
867
+ device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, fallbackConnectOptions));
845
868
  }
846
869
  else if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceAlreadyConnected) {
847
870
  Log === null || Log === void 0 ? void 0 : Log.debug('device already connected');
@@ -857,23 +880,27 @@ class ReactNativeBleTransport {
857
880
  }
858
881
  if (!(yield device.isConnected())) {
859
882
  Log === null || Log === void 0 ? void 0 : Log.debug('not connected, try to connect to device: ', uuid);
883
+ const disconnectedDevice = device;
860
884
  try {
861
- device = yield device.connect(connectOptions);
885
+ device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(connectOptions));
862
886
  }
863
887
  catch (e) {
864
888
  Log === null || Log === void 0 ? void 0 : Log.debug('not connected, try to connect to device has error: ', e);
889
+ if (isConnectTimeoutError(e)) {
890
+ throw e;
891
+ }
865
892
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
866
893
  e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
867
894
  Log === null || Log === void 0 ? void 0 : Log.debug('second try to reconnect without params');
868
895
  try {
869
- device = yield device.connect();
896
+ device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(fallbackConnectOptions));
870
897
  }
871
898
  catch (e) {
872
899
  Log === null || Log === void 0 ? void 0 : Log.debug('last try to reconnect error: ', e);
873
900
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
874
901
  Log === null || Log === void 0 ? void 0 : Log.debug('last try to reconnect');
875
- yield device.cancelConnection();
876
- device = yield device.connect();
902
+ yield disconnectedDevice.cancelConnection();
903
+ device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(fallbackConnectOptions));
877
904
  }
878
905
  }
879
906
  }
@@ -884,7 +911,7 @@ class ReactNativeBleTransport {
884
911
  }
885
912
  device = yield resolveNegotiatedMtu(device);
886
913
  const acquiredDevice = device;
887
- const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristics(acquiredDevice);
914
+ const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
888
915
  const protocolHint = expectedProtocol
889
916
  ? undefined
890
917
  : (_b = (_a = input.protocolHint) !== null && _a !== void 0 ? _a : this.deviceProtocolHints.get(uuid)) !== null && _b !== void 0 ? _b : inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
@@ -929,7 +956,7 @@ class ReactNativeBleTransport {
929
956
  Log === null || Log === void 0 ? void 0 : Log.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
930
957
  return;
931
958
  }
932
- if (this.deviceProtocol.get(uuid) === 'V2') {
959
+ if (this.getActiveProtocol(uuid) === 'V2') {
933
960
  let errorCode = hdShared.HardwareErrorCode.BleCharacteristicNotifyError;
934
961
  if ((_a = error.reason) === null || _a === void 0 ? void 0 : _a.includes('The connection has timed out unexpectedly')) {
935
962
  errorCode = hdShared.HardwareErrorCode.BleTimeoutError;
@@ -980,7 +1007,7 @@ class ReactNativeBleTransport {
980
1007
  }
981
1008
  try {
982
1009
  const data = buffer.Buffer.from(c.value, 'base64');
983
- const protocol = this.deviceProtocol.get(uuid);
1010
+ const protocol = this.getActiveProtocol(uuid);
984
1011
  if (!protocol) {
985
1012
  Log === null || Log === void 0 ? void 0 : Log.debug('monitor data ignored before protocol detection: ', uuid);
986
1013
  return;
@@ -1008,7 +1035,7 @@ class ReactNativeBleTransport {
1008
1035
  catch (error) {
1009
1036
  Log === null || Log === void 0 ? void 0 : Log.debug('monitor data error: ', error);
1010
1037
  const notifyError = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError);
1011
- if (this.deviceProtocol.get(uuid) === 'V2') {
1038
+ if (this.getActiveProtocol(uuid) === 'V2') {
1012
1039
  this.rejectProtocolV2Frames(uuid, notifyError);
1013
1040
  }
1014
1041
  else if (this.runPromiseDeviceId === uuid) {
@@ -1066,6 +1093,7 @@ class ReactNativeBleTransport {
1066
1093
  }
1067
1094
  this.protocolV2HighVolumeLogSignatures.delete(uuid);
1068
1095
  this.deviceProtocol.delete(uuid);
1096
+ this.probingProtocols.delete(uuid);
1069
1097
  (_f = this.protocolV2Assemblers.get(uuid)) === null || _f === void 0 ? void 0 : _f.reset();
1070
1098
  this.protocolV2Assemblers.delete(uuid);
1071
1099
  this.resetProtocolV2Frames(uuid);
@@ -1114,8 +1142,19 @@ class ReactNativeBleTransport {
1114
1142
  const transport = this.getCachedTransport(uuid);
1115
1143
  const runPromise = hdShared.createDeferred();
1116
1144
  runPromise.promise.catch(() => undefined);
1145
+ const supersededRunPromise = this.runPromise;
1146
+ if (supersededRunPromise) {
1147
+ supersededRunPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise));
1148
+ }
1117
1149
  this.runPromise = runPromise;
1118
1150
  this.runPromiseDeviceId = uuid;
1151
+ const releaseOwnershipIfCurrent = () => {
1152
+ if (this.runPromise === runPromise) {
1153
+ this.runPromise = null;
1154
+ this.runPromiseDeviceId = null;
1155
+ }
1156
+ };
1157
+ const isCurrentOwner = () => this.runPromise === runPromise;
1119
1158
  const messages = this._messages;
1120
1159
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1121
1160
  let timeout;
@@ -1136,6 +1175,9 @@ class ReactNativeBleTransport {
1136
1175
  }
1137
1176
  catch (e) {
1138
1177
  onError(e);
1178
+ if (isWedgedWriteError(e)) {
1179
+ throw e;
1180
+ }
1139
1181
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError);
1140
1182
  }
1141
1183
  }
@@ -1163,6 +1205,9 @@ class ReactNativeBleTransport {
1163
1205
  }
1164
1206
  catch (e) {
1165
1207
  onError(e);
1208
+ if (isWedgedWriteError(e)) {
1209
+ throw e;
1210
+ }
1166
1211
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError);
1167
1212
  }
1168
1213
  }
@@ -1173,8 +1218,8 @@ class ReactNativeBleTransport {
1173
1218
  });
1174
1219
  }
1175
1220
  if (name === 'EmmcFileWrite') {
1176
- yield writeChunkedData(buffers, data => transport.writeWithRetry(data), e => {
1177
- this.runPromise = null;
1221
+ yield writeChunkedData(buffers, data => this.writeBlePacket(uuid, data, payload => transport.writeWithRetry(payload), isCurrentOwner), e => {
1222
+ releaseOwnershipIfCurrent();
1178
1223
  Log === null || Log === void 0 ? void 0 : Log.error('writeCharacteristic write error: ', e);
1179
1224
  });
1180
1225
  }
@@ -1190,7 +1235,7 @@ class ReactNativeBleTransport {
1190
1235
  let attempt = 0;
1191
1236
  while (true) {
1192
1237
  try {
1193
- yield transport.writeWithRetry(data);
1238
+ yield this.writeBlePacket(uuid, data, payload => transport.writeWithRetry(payload), isCurrentOwner);
1194
1239
  return;
1195
1240
  }
1196
1241
  catch (error) {
@@ -1209,7 +1254,7 @@ class ReactNativeBleTransport {
1209
1254
  }
1210
1255
  }
1211
1256
  }), e => {
1212
- this.runPromise = null;
1257
+ releaseOwnershipIfCurrent();
1213
1258
  Log === null || Log === void 0 ? void 0 : Log.error('writeCharacteristic write error: ', e);
1214
1259
  });
1215
1260
  }
@@ -1218,16 +1263,16 @@ class ReactNativeBleTransport {
1218
1263
  const outData = o.toString('base64');
1219
1264
  try {
1220
1265
  const shouldUseWriteWithResponse = reactNative.Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1221
- if (shouldUseWriteWithResponse) {
1222
- yield transport.writeCharacteristic.writeWithResponse(outData);
1223
- }
1224
- else {
1225
- yield transport.writeCharacteristic.writeWithoutResponse(outData);
1226
- }
1266
+ yield this.writeBlePacket(uuid, outData, payload => shouldUseWriteWithResponse
1267
+ ? transport.writeCharacteristic.writeWithResponse(payload)
1268
+ : transport.writeCharacteristic.writeWithoutResponse(payload), isCurrentOwner);
1227
1269
  }
1228
1270
  catch (e) {
1229
1271
  Log === null || Log === void 0 ? void 0 : Log.debug('writeCharacteristic write error: ', e);
1230
- this.runPromise = null;
1272
+ releaseOwnershipIfCurrent();
1273
+ if (isWedgedWriteError(e)) {
1274
+ throw e;
1275
+ }
1231
1276
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceDisconnected) {
1232
1277
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded);
1233
1278
  }
@@ -1267,7 +1312,9 @@ class ReactNativeBleTransport {
1267
1312
  Log === null || Log === void 0 ? void 0 : Log.error('call error: ', e);
1268
1313
  }
1269
1314
  const isProbeTimeout = name === 'GetFeatures' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS;
1315
+ const isStaleCall = this.runPromise !== runPromise;
1270
1316
  if (!isProbeTimeout &&
1317
+ !isStaleCall &&
1271
1318
  (e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError) {
1272
1319
  yield this.disconnect(uuid);
1273
1320
  }
@@ -1338,7 +1385,10 @@ class ReactNativeBleTransport {
1338
1385
  delete transportCache[session];
1339
1386
  }
1340
1387
  this.deviceProtocol.delete(session);
1388
+ this.probingProtocols.delete(session);
1341
1389
  this.deviceProtocolHints.delete(session);
1390
+ this.sessionProtocols.delete(session);
1391
+ this.protocolReprobeFailures.delete(session);
1342
1392
  this.protocolV2Assemblers.delete(session);
1343
1393
  this.resetProtocolV2Frames(session);
1344
1394
  try {
@@ -1359,6 +1409,91 @@ class ReactNativeBleTransport {
1359
1409
  this.runPromise = null;
1360
1410
  this.runPromiseDeviceId = null;
1361
1411
  }
1412
+ connectWithTimeout(uuid, connect) {
1413
+ return __awaiter(this, void 0, void 0, function* () {
1414
+ let timer;
1415
+ let timedOut = false;
1416
+ const pending = connect();
1417
+ pending.catch(() => undefined);
1418
+ try {
1419
+ const result = yield Promise.race([
1420
+ pending,
1421
+ new Promise((_, reject) => {
1422
+ timer = setTimeout(() => {
1423
+ timedOut = true;
1424
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`));
1425
+ }, BLE_CONNECT_TIMEOUT_MS);
1426
+ }),
1427
+ ]);
1428
+ return result;
1429
+ }
1430
+ catch (error) {
1431
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1432
+ this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
1433
+ }
1434
+ throw error;
1435
+ }
1436
+ finally {
1437
+ if (timer)
1438
+ clearTimeout(timer);
1439
+ }
1440
+ });
1441
+ }
1442
+ resolveCharacteristicsWithTimeout(uuid, device) {
1443
+ return __awaiter(this, void 0, void 0, function* () {
1444
+ let timer;
1445
+ let timedOut = false;
1446
+ const pending = this.resolveCharacteristics(device);
1447
+ pending.catch(() => undefined);
1448
+ try {
1449
+ const result = yield Promise.race([
1450
+ pending,
1451
+ new Promise((_, reject) => {
1452
+ timer = setTimeout(() => {
1453
+ timedOut = true;
1454
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`));
1455
+ }, BLE_GATT_SETUP_TIMEOUT_MS);
1456
+ }),
1457
+ ]);
1458
+ this.connectionSetupTimeoutCounts.delete(uuid);
1459
+ return result;
1460
+ }
1461
+ catch (error) {
1462
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1463
+ this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1464
+ }
1465
+ throw error;
1466
+ }
1467
+ finally {
1468
+ if (timer)
1469
+ clearTimeout(timer);
1470
+ }
1471
+ });
1472
+ }
1473
+ abandonStalledConnection(uuid, stage) {
1474
+ var _a, _b;
1475
+ const timeouts = ((_a = this.connectionSetupTimeoutCounts.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
1476
+ this.connectionSetupTimeoutCounts.set(uuid, timeouts);
1477
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
1478
+ stage,
1479
+ setupTimeoutsSinceSuccess: timeouts,
1480
+ });
1481
+ (_b = this.blePlxManager) === null || _b === void 0 ? void 0 : _b.cancelDeviceConnection(uuid).catch(() => {
1482
+ });
1483
+ const stalled = transportCache[uuid];
1484
+ if (stalled) {
1485
+ delete transportCache[uuid];
1486
+ }
1487
+ this.deviceProtocol.delete(uuid);
1488
+ this.probingProtocols.delete(uuid);
1489
+ this.protocolV2Assemblers.delete(uuid);
1490
+ this.resetProtocolV2Frames(uuid);
1491
+ if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1492
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
1493
+ this.resetPlxManager();
1494
+ this.connectionSetupTimeoutCounts.delete(uuid);
1495
+ }
1496
+ }
1362
1497
  getCachedTransport(uuid) {
1363
1498
  const transport = transportCache[uuid];
1364
1499
  if (!transport) {
@@ -1366,6 +1501,82 @@ class ReactNativeBleTransport {
1366
1501
  }
1367
1502
  return transport;
1368
1503
  }
1504
+ writeBlePacket(uuid, data, write, isCurrentOwner) {
1505
+ return __awaiter(this, void 0, void 0, function* () {
1506
+ let timer;
1507
+ let timedOut = false;
1508
+ try {
1509
+ yield Promise.race([
1510
+ write(data),
1511
+ new Promise((_, reject) => {
1512
+ timer = setTimeout(() => {
1513
+ timedOut = true;
1514
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError, `BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`));
1515
+ }, BLE_WRITE_PACKET_TIMEOUT_MS);
1516
+ }),
1517
+ ]);
1518
+ this.writeTimeoutCounts.delete(uuid);
1519
+ }
1520
+ catch (error) {
1521
+ if (timedOut) {
1522
+ if (isCurrentOwner && !isCurrentOwner()) {
1523
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
1524
+ }
1525
+ else {
1526
+ this.tearDownWedgedLink(uuid);
1527
+ }
1528
+ }
1529
+ throw error;
1530
+ }
1531
+ finally {
1532
+ if (timer)
1533
+ clearTimeout(timer);
1534
+ }
1535
+ });
1536
+ }
1537
+ tearDownWedgedLink(uuid) {
1538
+ var _a;
1539
+ const timeouts = ((_a = this.writeTimeoutCounts.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
1540
+ this.writeTimeoutCounts.set(uuid, timeouts);
1541
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
1542
+ consecutiveWriteTimeouts: timeouts,
1543
+ });
1544
+ const wedged = transportCache[uuid];
1545
+ this.disconnect(uuid).catch(error => {
1546
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
1547
+ });
1548
+ if (wedged && transportCache[uuid] === wedged) {
1549
+ delete transportCache[uuid];
1550
+ }
1551
+ this.deviceProtocol.delete(uuid);
1552
+ this.probingProtocols.delete(uuid);
1553
+ this.protocolV2Assemblers.delete(uuid);
1554
+ this.resetProtocolV2Frames(uuid);
1555
+ if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1556
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
1557
+ this.resetPlxManager();
1558
+ this.writeTimeoutCounts.delete(uuid);
1559
+ }
1560
+ }
1561
+ resetPlxManager() {
1562
+ const manager = this.blePlxManager;
1563
+ this.blePlxManager = undefined;
1564
+ Object.keys(transportCache).forEach(key => {
1565
+ delete transportCache[key];
1566
+ });
1567
+ this.deviceProtocol.clear();
1568
+ this.probingProtocols.clear();
1569
+ this.sessionProtocols.clear();
1570
+ this.protocolReprobeFailures.clear();
1571
+ this.monitorTokens.clear();
1572
+ this.protocolV2Assemblers.clear();
1573
+ try {
1574
+ manager === null || manager === void 0 ? void 0 : manager.destroy();
1575
+ }
1576
+ catch (error) {
1577
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
1578
+ }
1579
+ }
1369
1580
  createProtocolMismatchError(expected) {
1370
1581
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1371
1582
  }
@@ -1373,11 +1584,19 @@ class ReactNativeBleTransport {
1373
1584
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping');
1374
1585
  }
1375
1586
  clearProbeProtocol(uuid, protocol) {
1587
+ if (this.probingProtocols.get(uuid) === protocol) {
1588
+ this.probingProtocols.delete(uuid);
1589
+ }
1376
1590
  if (this.deviceProtocol.get(uuid) === protocol) {
1377
1591
  this.deviceProtocol.delete(uuid);
1378
1592
  }
1379
1593
  }
1594
+ getActiveProtocol(uuid) {
1595
+ var _a;
1596
+ return (_a = this.deviceProtocol.get(uuid)) !== null && _a !== void 0 ? _a : this.probingProtocols.get(uuid);
1597
+ }
1380
1598
  detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
1599
+ var _a;
1381
1600
  return __awaiter(this, void 0, void 0, function* () {
1382
1601
  if (reactNative.Platform.OS === 'ios' && expectedProtocol) {
1383
1602
  this.deviceProtocol.set(uuid, expectedProtocol);
@@ -1391,6 +1610,7 @@ class ReactNativeBleTransport {
1391
1610
  if (expectedProtocol === 'V1') {
1392
1611
  if (yield this.probeProtocolV1(uuid)) {
1393
1612
  this.deviceProtocol.set(uuid, 'V1');
1613
+ this.sessionProtocols.set(uuid, 'V1');
1394
1614
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
1395
1615
  deviceId: uuid,
1396
1616
  protocol: 'V1',
@@ -1403,6 +1623,7 @@ class ReactNativeBleTransport {
1403
1623
  if (expectedProtocol === 'V2') {
1404
1624
  if (yield this.probeProtocolV2(uuid)) {
1405
1625
  this.deviceProtocol.set(uuid, 'V2');
1626
+ this.sessionProtocols.set(uuid, 'V2');
1406
1627
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
1407
1628
  deviceId: uuid,
1408
1629
  protocol: 'V2',
@@ -1412,7 +1633,13 @@ class ReactNativeBleTransport {
1412
1633
  }
1413
1634
  throw this.createProtocolMismatchError(expectedProtocol);
1414
1635
  }
1415
- const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1636
+ const sessionProtocol = this.sessionProtocols.get(uuid);
1637
+ const reprobeFailures = (_a = this.protocolReprobeFailures.get(uuid)) !== null && _a !== void 0 ? _a : 0;
1638
+ const fullProbeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1639
+ const trustSessionProtocol = sessionProtocol !== undefined &&
1640
+ !protocolHint &&
1641
+ reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1642
+ const probeOrder = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
1416
1643
  for (let i = 0; i < probeOrder.length; i += 1) {
1417
1644
  const protocol = probeOrder[i];
1418
1645
  if (i > 0) {
@@ -1427,6 +1654,8 @@ class ReactNativeBleTransport {
1427
1654
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1428
1655
  if (detected) {
1429
1656
  this.deviceProtocol.set(uuid, protocol);
1657
+ this.sessionProtocols.set(uuid, protocol);
1658
+ this.protocolReprobeFailures.delete(uuid);
1430
1659
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
1431
1660
  deviceId: uuid,
1432
1661
  protocol,
@@ -1435,7 +1664,14 @@ class ReactNativeBleTransport {
1435
1664
  return protocol;
1436
1665
  }
1437
1666
  }
1667
+ if (trustSessionProtocol) {
1668
+ this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1669
+ }
1670
+ else {
1671
+ this.protocolReprobeFailures.delete(uuid);
1672
+ }
1438
1673
  this.deviceProtocol.delete(uuid);
1674
+ this.probingProtocols.delete(uuid);
1439
1675
  throw this.createProtocolDetectionError();
1440
1676
  });
1441
1677
  }
@@ -1487,13 +1723,17 @@ class ReactNativeBleTransport {
1487
1723
  return false;
1488
1724
  }
1489
1725
  try {
1490
- this.deviceProtocol.set(uuid, 'V1');
1726
+ this.probingProtocols.set(uuid, 'V1');
1491
1727
  yield this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1728
+ this.probingProtocols.delete(uuid);
1492
1729
  return true;
1493
1730
  }
1494
1731
  catch (error) {
1495
1732
  this.clearProbeProtocol(uuid, 'V1');
1496
1733
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
1734
+ if (isWedgedWriteError(error)) {
1735
+ throw error;
1736
+ }
1497
1737
  return false;
1498
1738
  }
1499
1739
  });
@@ -1504,7 +1744,7 @@ class ReactNativeBleTransport {
1504
1744
  if (!this._messages || !this._messagesV2) {
1505
1745
  return false;
1506
1746
  }
1507
- this.deviceProtocol.set(uuid, 'V2');
1747
+ this.probingProtocols.set(uuid, 'V2');
1508
1748
  (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1509
1749
  const detected = yield transport.probeProtocolV2({
1510
1750
  call: (name, data, options) => this.callProtocolV2(uuid, name, data, options),
@@ -1520,6 +1760,9 @@ class ReactNativeBleTransport {
1520
1760
  if (!detected) {
1521
1761
  this.clearProbeProtocol(uuid, 'V2');
1522
1762
  }
1763
+ else {
1764
+ this.probingProtocols.delete(uuid);
1765
+ }
1523
1766
  return detected;
1524
1767
  });
1525
1768
  }
@@ -1591,7 +1834,7 @@ class ReactNativeBleTransport {
1591
1834
  }
1592
1835
  });
1593
1836
  }
1594
- writeProtocolV2Packet(transport, base64, context, assertCurrentGeneration) {
1837
+ writeProtocolV2Packet(uuid, transport, base64, context, assertCurrentGeneration) {
1595
1838
  return __awaiter(this, void 0, void 0, function* () {
1596
1839
  const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
1597
1840
  platform: reactNative.Platform.OS,
@@ -1606,12 +1849,17 @@ class ReactNativeBleTransport {
1606
1849
  throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
1607
1850
  }
1608
1851
  try {
1609
- if (shouldUseWriteWithResponse) {
1610
- yield transport.writeCharacteristic.writeWithResponse(base64);
1611
- }
1612
- else {
1613
- yield transport.writeCharacteristic.writeWithoutResponse(base64);
1614
- }
1852
+ yield this.writeBlePacket(uuid, base64, payload => shouldUseWriteWithResponse
1853
+ ? transport.writeCharacteristic.writeWithResponse(payload)
1854
+ : transport.writeCharacteristic.writeWithoutResponse(payload), () => {
1855
+ try {
1856
+ assertCurrentGeneration();
1857
+ return !context.signal.aborted;
1858
+ }
1859
+ catch (_a) {
1860
+ return false;
1861
+ }
1862
+ });
1615
1863
  assertCurrentGeneration();
1616
1864
  return;
1617
1865
  }
@@ -1632,7 +1880,7 @@ class ReactNativeBleTransport {
1632
1880
  }
1633
1881
  });
1634
1882
  }
1635
- writeProtocolV2Frame(transport$1, frame, context, assertCurrentGeneration) {
1883
+ writeProtocolV2Frame(uuid, transport$1, frame, context, assertCurrentGeneration) {
1636
1884
  return __awaiter(this, void 0, void 0, function* () {
1637
1885
  const tuning = getProtocolV2BleTuning();
1638
1886
  const packetCapacity = resolveProtocolV2PacketCapacity({
@@ -1648,7 +1896,7 @@ class ReactNativeBleTransport {
1648
1896
  signal: context.signal,
1649
1897
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1650
1898
  wait: delay,
1651
- writePacket: packet => this.writeProtocolV2Packet(transport$1, buffer.Buffer.from(packet).toString('base64'), context, assertCurrentGeneration),
1899
+ writePacket: packet => this.writeProtocolV2Packet(uuid, transport$1, buffer.Buffer.from(packet).toString('base64'), context, assertCurrentGeneration),
1652
1900
  });
1653
1901
  });
1654
1902
  }
@@ -1802,7 +2050,7 @@ class ReactNativeBleTransport {
1802
2050
  writeFrame: (frame, context) => __awaiter(this, void 0, void 0, function* () {
1803
2051
  assertCurrentGeneration();
1804
2052
  const currentTransport = this.getCachedTransport(uuid);
1805
- yield this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
2053
+ yield this.writeProtocolV2Frame(uuid, currentTransport, frame, context, assertCurrentGeneration);
1806
2054
  }),
1807
2055
  readFrame: () => __awaiter(this, void 0, void 0, function* () {
1808
2056
  assertCurrentGeneration();
@@ -1823,10 +2071,16 @@ class ReactNativeBleTransport {
1823
2071
  };
1824
2072
  }
1825
2073
  getProtocolType(path) {
1826
- return this.deviceProtocol.get(path);
2074
+ return this.getActiveProtocol(path);
1827
2075
  }
1828
2076
  }
1829
2077
 
2078
+ exports.BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD;
2079
+ exports.BLE_CONNECT_TIMEOUT_MS = BLE_CONNECT_TIMEOUT_MS;
2080
+ exports.BLE_GATT_SETUP_TIMEOUT_MS = BLE_GATT_SETUP_TIMEOUT_MS;
2081
+ exports.BLE_WRITE_PACKET_TIMEOUT_MS = BLE_WRITE_PACKET_TIMEOUT_MS;
2082
+ exports.BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD;
2083
+ exports.PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1830
2084
  exports.configureProtocolV2BleTuning = configureProtocolV2BleTuning;
1831
2085
  exports["default"] = ReactNativeBleTransport;
1832
2086
  exports.getFirmwareUploadWriteRetryType = getFirmwareUploadWriteRetryType;
package/jest.config.js CHANGED
@@ -2,4 +2,9 @@ module.exports = {
2
2
  preset: '../../jest.config.js',
3
3
  testEnvironment: 'node',
4
4
  modulePathIgnorePatterns: ['node_modules', '<rootDir>/dist'],
5
+ moduleNameMapper: {
6
+ // The workspace symlinks resolve to prebuilt dist outputs that can lag behind src.
7
+ '^@onekeyfe/hd-transport$': '<rootDir>/../hd-transport/src/index.ts',
8
+ '^@onekeyfe/hd-shared$': '<rootDir>/../shared/src/index.ts',
9
+ },
5
10
  };