@onekeyfe/hd-transport-react-native 1.2.0-alpha.66 → 1.2.0-alpha.67

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/src/index.ts CHANGED
@@ -28,7 +28,6 @@ import {
28
28
  HardwareErrorCode,
29
29
  createDeferred,
30
30
  isOnekeyBluetoothDevice,
31
- isPro2FindMyAdvertisementName,
32
31
  } from '@onekeyfe/hd-shared';
33
32
 
34
33
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
@@ -61,7 +60,6 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
61
60
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
62
61
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
63
62
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
64
- const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
65
63
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
66
64
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
67
65
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
@@ -73,33 +71,6 @@ type ResolvedBleCharacteristics = {
73
71
  notifyCharacteristic: Characteristic;
74
72
  };
75
73
 
76
- const isAsciiWhitespace = (code: number) =>
77
- code === 0x09 ||
78
- code === 0x0a ||
79
- code === 0x0b ||
80
- code === 0x0c ||
81
- code === 0x0d ||
82
- code === 0x20;
83
-
84
- const hasGattCongestedStatus = (text: string) => {
85
- let searchFrom = 0;
86
- while (searchFrom < text.length) {
87
- const statusIndex = text.indexOf('status', searchFrom);
88
- if (statusIndex < 0) return false;
89
-
90
- let cursor = statusIndex + 'status'.length;
91
- while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
92
- if (text[cursor] === ':' || text[cursor] === '=') {
93
- cursor += 1;
94
- while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
95
- }
96
- if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
97
-
98
- searchFrom = statusIndex + 'status'.length;
99
- }
100
- return false;
101
- };
102
-
103
74
  const delay = (ms: number) =>
104
75
  new Promise<void>(resolve => {
105
76
  setTimeout(resolve, ms);
@@ -128,13 +99,29 @@ export const getFirmwareUploadWriteRetryType = (
128
99
  const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
129
100
  .filter(value => typeof value === 'string')
130
101
  .join(' ');
131
- return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
102
+ return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
132
103
  };
133
104
 
134
105
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
135
106
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
136
107
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
137
108
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
109
+ /**
110
+ * Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
111
+ * reports the peripheral ready again; a peripheral wedged by its own firmware reboot
112
+ * stops reporting ready while staying connected, so the write promise never settles.
113
+ * Response timeouts cannot cover that — they are armed after the writes complete —
114
+ * and an unbounded write leaves the whole transport unusable until the process dies.
115
+ * A healthy packet completes in milliseconds, so this only fires on a dead link.
116
+ */
117
+ export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
118
+ const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
119
+ const isWedgedWriteError = (error: unknown): boolean =>
120
+ (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
121
+ typeof (error as { message?: unknown })?.message === 'string' &&
122
+ (error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
123
+ /** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
124
+ export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
138
125
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
139
126
  const IOS_NOTIFY_READY_DELAY_MS = 150;
140
127
  const ANDROID_NOTIFY_READY_DELAY_MS = 300;
@@ -191,12 +178,52 @@ function getDeviceDisplayName(device?: Device | null) {
191
178
 
192
179
  const ANDROID_REQUEST_MTU = 256;
193
180
 
181
+ const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
182
+
194
183
  const connectOptions: Record<string, unknown> = {
195
184
  requestMTU: ANDROID_REQUEST_MTU,
196
- timeout: 3000,
185
+ timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
197
186
  refreshGatt: 'OnConnected',
198
187
  };
199
188
 
189
+ /** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
190
+ const fallbackConnectOptions: Record<string, unknown> = {
191
+ timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
192
+ };
193
+
194
+ /**
195
+ * JS backstop for connect. The native adapter applies its own 3s budget, but it
196
+ * schedules that timeout on its serial queue, so a busy queue (e.g. right after a
197
+ * firmware install tears the link down) can leave the promise unsettled — observed
198
+ * blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
199
+ * inside the native budget, so this only fires when the native timeout did not.
200
+ */
201
+ export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
202
+ /**
203
+ * Service discovery and characteristic resolution run after connect() succeeds, but
204
+ * CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
205
+ * device reboot, these calls can remain pending forever unless they have their own
206
+ * budget.
207
+ */
208
+ export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
209
+ /**
210
+ * How many times a known device may fail its own protocol before we probe the others
211
+ * again. Reconnect polling during a device reboot repeats this every few seconds, and
212
+ * probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
213
+ * device we just spoke V1 to dominates the wait. A firmware update can legitimately
214
+ * change a device's protocol, so the shortcut has to expire rather than stick.
215
+ */
216
+ export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
217
+ /** BLE setup timeouts since the last successful setup before the manager is recreated. */
218
+ export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
219
+ const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
220
+ const isConnectTimeoutError = (error: unknown): boolean =>
221
+ (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
222
+ typeof (error as { message?: unknown })?.message === 'string' &&
223
+ (error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
224
+ const isNativeOperationTimeoutError = (error: unknown): boolean =>
225
+ (error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
226
+
200
227
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
201
228
 
202
229
  const tryToGetConfiguration = (device: Device) => {
@@ -285,8 +312,28 @@ export default class ReactNativeBleTransport {
285
312
  /** Per-device protocol type detected by active wire-level probe after connect. */
286
313
  private deviceProtocol: Map<string, ProtocolType> = new Map();
287
314
 
315
+ /**
316
+ * Protocol a probe is currently trying, before the device has confirmed it. Calls
317
+ * must route with it, but acquire() must not treat it as a detected protocol: a
318
+ * probe that never answers would otherwise leave the reuse fast path handing out a
319
+ * transport that was never validated.
320
+ */
321
+ private probingProtocols: Map<string, ProtocolType> = new Map();
322
+
323
+ /** Consecutive write timeouts per device; reset by any write that completes. */
324
+ private writeTimeoutCounts: Map<string, number> = new Map();
325
+
326
+ /** BLE setup timeouts per device since the last complete characteristic resolution. */
327
+ private connectionSetupTimeoutCounts: Map<string, number> = new Map();
328
+
288
329
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
289
330
 
331
+ /** Protocol this device actually answered on, kept across reconnects of one session. */
332
+ private sessionProtocols: Map<string, ProtocolType> = new Map();
333
+
334
+ /** Consecutive detections that failed while trusting sessionProtocols. */
335
+ private protocolReprobeFailures: Map<string, number> = new Map();
336
+
290
337
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
291
338
 
292
339
  private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
@@ -504,22 +551,21 @@ export default class ReactNativeBleTransport {
504
551
  const isConnected = await device.isConnected().catch(() => false);
505
552
  if (!isConnected) {
506
553
  try {
507
- device = await device.connect(connectOptions);
554
+ device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
508
555
  } catch (e) {
509
556
  if (
510
557
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
511
558
  e.errorCode === BleErrorCode.OperationCancelled
512
559
  ) {
513
- device = await device.connect();
560
+ device = await this.connectWithTimeout(uuid, () => device.connect());
514
561
  } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
515
562
  throw e;
516
563
  }
517
564
  }
518
565
  }
519
566
 
520
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
521
- device
522
- );
567
+ const { writeCharacteristic, notifyCharacteristic } =
568
+ await this.resolveCharacteristicsWithTimeout(uuid, device);
523
569
 
524
570
  transport.device = device;
525
571
  transport.writeCharacteristic = writeCharacteristic;
@@ -609,21 +655,12 @@ export default class ReactNativeBleTransport {
609
655
  }
610
656
 
611
657
  const displayName = getDeviceDisplayName(device);
612
- // iOS may report a service-only advertisement before the named scan response.
613
- // Do not cache that incomplete advertisement as an unknown device.
614
- const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
615
- const isFindMyPeripheral =
616
- isPro2FindMyAdvertisementName(device?.name) ||
617
- isPro2FindMyAdvertisementName(device?.localName);
618
- const isOneKey =
619
- !isUnnamedIOSPeripheral &&
620
- !isFindMyPeripheral &&
621
- isOnekeyBluetoothDevice({
622
- id: device?.id,
623
- name: device?.name,
624
- localName: device?.localName,
625
- serviceUuids: device?.serviceUUIDs,
626
- });
658
+ const isOneKey = isOnekeyBluetoothDevice({
659
+ id: device?.id,
660
+ name: device?.name,
661
+ localName: device?.localName,
662
+ serviceUuids: device?.serviceUUIDs,
663
+ });
627
664
  if (isOneKey) {
628
665
  addDevice(device as unknown as Device);
629
666
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
@@ -644,12 +681,7 @@ export default class ReactNativeBleTransport {
644
681
  'localName' in device && typeof device.localName === 'string'
645
682
  ? device.localName
646
683
  : null;
647
- const isFindMyPeripheral =
648
- isPro2FindMyAdvertisementName(device.name) ||
649
- isPro2FindMyAdvertisementName(localName);
650
-
651
684
  if (
652
- !isFindMyPeripheral &&
653
685
  isOnekeyBluetoothDevice({
654
686
  id: device.id,
655
687
  name: device.name,
@@ -698,7 +730,7 @@ export default class ReactNativeBleTransport {
698
730
  characteristics?: ResolvedBleCharacteristics
699
731
  ) {
700
732
  const { writeCharacteristic, notifyCharacteristic } =
701
- characteristics ?? (await this.resolveCharacteristics(device));
733
+ characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
702
734
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
703
735
  if (Platform.OS === 'android') {
704
736
  transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
@@ -802,15 +834,22 @@ export default class ReactNativeBleTransport {
802
834
  if (!device) {
803
835
  Log?.debug('try to connect to device: ', uuid);
804
836
  try {
805
- device = await blePlxManager.connectToDevice(uuid, connectOptions);
837
+ device = await this.connectWithTimeout(uuid, () =>
838
+ blePlxManager.connectToDevice(uuid, connectOptions)
839
+ );
806
840
  } catch (e) {
807
841
  Log?.debug('try to connect to device has error: ', e);
842
+ if (isConnectTimeoutError(e)) {
843
+ throw e;
844
+ }
808
845
  if (
809
846
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
810
847
  e.errorCode === BleErrorCode.OperationCancelled
811
848
  ) {
812
849
  Log?.debug('first try to reconnect without params');
813
- device = await blePlxManager.connectToDevice(uuid);
850
+ device = await this.connectWithTimeout(uuid, () =>
851
+ blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
852
+ );
814
853
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
815
854
  Log?.debug('device already connected');
816
855
  throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
@@ -826,26 +865,36 @@ export default class ReactNativeBleTransport {
826
865
 
827
866
  if (!(await device.isConnected())) {
828
867
  Log?.debug('not connected, try to connect to device: ', uuid);
868
+ const disconnectedDevice = device;
829
869
 
830
870
  try {
831
- device = await device.connect(connectOptions);
871
+ device = await this.connectWithTimeout(uuid, () =>
872
+ disconnectedDevice.connect(connectOptions)
873
+ );
832
874
  } catch (e) {
833
875
  Log?.debug('not connected, try to connect to device has error: ', e);
876
+ if (isConnectTimeoutError(e)) {
877
+ throw e;
878
+ }
834
879
  if (
835
880
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
836
881
  e.errorCode === BleErrorCode.OperationCancelled
837
882
  ) {
838
883
  Log?.debug('second try to reconnect without params');
839
884
  try {
840
- device = await device.connect();
885
+ device = await this.connectWithTimeout(uuid, () =>
886
+ disconnectedDevice.connect(fallbackConnectOptions)
887
+ );
841
888
  } catch (e) {
842
889
  Log?.debug('last try to reconnect error: ', e);
843
890
  // last try to reconnect device if this issue exists
844
891
  // https://github.com/dotintent/react-native-ble-plx/issues/426
845
892
  if (e.errorCode === BleErrorCode.OperationCancelled) {
846
893
  Log?.debug('last try to reconnect');
847
- await device.cancelConnection();
848
- device = await device.connect();
894
+ await disconnectedDevice.cancelConnection();
895
+ device = await this.connectWithTimeout(uuid, () =>
896
+ disconnectedDevice.connect(fallbackConnectOptions)
897
+ );
849
898
  }
850
899
  }
851
900
  } else {
@@ -856,9 +905,8 @@ export default class ReactNativeBleTransport {
856
905
 
857
906
  device = await requestAndroidMtu(device);
858
907
  const acquiredDevice = device;
859
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
860
- acquiredDevice
861
- );
908
+ const { writeCharacteristic, notifyCharacteristic } =
909
+ await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
862
910
 
863
911
  const protocolHint = expectedProtocol
864
912
  ? undefined
@@ -922,7 +970,7 @@ export default class ReactNativeBleTransport {
922
970
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
923
971
  return;
924
972
  }
925
- if (this.deviceProtocol.get(uuid) === 'V2') {
973
+ if (this.getActiveProtocol(uuid) === 'V2') {
926
974
  let errorCode:
927
975
  | typeof HardwareErrorCode.BleDeviceBondError
928
976
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -992,7 +1040,7 @@ export default class ReactNativeBleTransport {
992
1040
 
993
1041
  try {
994
1042
  const data = Buffer.from(c.value as string, 'base64');
995
- const protocol = this.deviceProtocol.get(uuid);
1043
+ const protocol = this.getActiveProtocol(uuid);
996
1044
  if (!protocol) {
997
1045
  Log?.debug('monitor data ignored before protocol detection: ', uuid);
998
1046
  return;
@@ -1026,7 +1074,7 @@ export default class ReactNativeBleTransport {
1026
1074
  } catch (error) {
1027
1075
  Log?.debug('monitor data error: ', error);
1028
1076
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1029
- if (this.deviceProtocol.get(uuid) === 'V2') {
1077
+ if (this.getActiveProtocol(uuid) === 'V2') {
1030
1078
  this.rejectProtocolV2Frames(uuid, notifyError);
1031
1079
  } else if (this.runPromiseDeviceId === uuid) {
1032
1080
  this.runPromise?.reject(notifyError);
@@ -1090,6 +1138,7 @@ export default class ReactNativeBleTransport {
1090
1138
  }
1091
1139
 
1092
1140
  this.deviceProtocol.delete(uuid);
1141
+ this.probingProtocols.delete(uuid);
1093
1142
  // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
1094
1143
  this.protocolV2Assemblers.get(uuid)?.reset();
1095
1144
  this.protocolV2Assemblers.delete(uuid);
@@ -1156,8 +1205,25 @@ export default class ReactNativeBleTransport {
1156
1205
  const transport = this.getCachedTransport(uuid);
1157
1206
  const runPromise = createDeferred<string>();
1158
1207
  runPromise.promise.catch(() => undefined);
1208
+ const supersededRunPromise = this.runPromise;
1209
+ if (supersededRunPromise) {
1210
+ // Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
1211
+ // the superseded deferred now so its response race resolves and its finally block
1212
+ // clears its timeout timer; an orphaned timer would otherwise fire much later and
1213
+ // tear down the shared connection while another call is using it.
1214
+ supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
1215
+ }
1159
1216
  this.runPromise = runPromise;
1160
1217
  this.runPromiseDeviceId = uuid;
1218
+ // A superseded call's late write failure must not clear the successor's ownership;
1219
+ // only the call that still owns the slot may release it.
1220
+ const releaseOwnershipIfCurrent = () => {
1221
+ if (this.runPromise === runPromise) {
1222
+ this.runPromise = null;
1223
+ this.runPromiseDeviceId = null;
1224
+ }
1225
+ };
1226
+ const isCurrentOwner = () => this.runPromise === runPromise;
1161
1227
  const messages = this._messages;
1162
1228
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1163
1229
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1183,6 +1249,9 @@ export default class ReactNativeBleTransport {
1183
1249
  chunk = ByteBuffer.allocate(packetCapacity);
1184
1250
  } catch (e) {
1185
1251
  onError(e);
1252
+ if (isWedgedWriteError(e)) {
1253
+ throw e;
1254
+ }
1186
1255
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1187
1256
  }
1188
1257
  }
@@ -1214,6 +1283,9 @@ export default class ReactNativeBleTransport {
1214
1283
  }
1215
1284
  } catch (e) {
1216
1285
  onError(e);
1286
+ if (isWedgedWriteError(e)) {
1287
+ throw e;
1288
+ }
1217
1289
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1218
1290
  }
1219
1291
  }
@@ -1227,9 +1299,15 @@ export default class ReactNativeBleTransport {
1227
1299
  if (name === 'EmmcFileWrite') {
1228
1300
  await writeChunkedData(
1229
1301
  buffers,
1230
- data => transport.writeWithRetry(data),
1302
+ data =>
1303
+ this.writeBlePacket(
1304
+ uuid,
1305
+ data,
1306
+ payload => transport.writeWithRetry(payload),
1307
+ isCurrentOwner
1308
+ ),
1231
1309
  e => {
1232
- this.runPromise = null;
1310
+ releaseOwnershipIfCurrent();
1233
1311
  Log?.error('writeCharacteristic write error: ', e);
1234
1312
  }
1235
1313
  );
@@ -1250,7 +1328,12 @@ export default class ReactNativeBleTransport {
1250
1328
  // eslint-disable-next-line no-constant-condition
1251
1329
  while (true) {
1252
1330
  try {
1253
- await transport.writeWithRetry(data);
1331
+ await this.writeBlePacket(
1332
+ uuid,
1333
+ data,
1334
+ payload => transport.writeCharacteristic.writeWithoutResponse(payload),
1335
+ isCurrentOwner
1336
+ );
1254
1337
  return;
1255
1338
  } catch (error) {
1256
1339
  const retryType = getFirmwareUploadWriteRetryType(error);
@@ -1269,7 +1352,7 @@ export default class ReactNativeBleTransport {
1269
1352
  }
1270
1353
  },
1271
1354
  e => {
1272
- this.runPromise = null;
1355
+ releaseOwnershipIfCurrent();
1273
1356
  Log?.error('writeCharacteristic write error: ', e);
1274
1357
  }
1275
1358
  );
@@ -1278,16 +1361,18 @@ export default class ReactNativeBleTransport {
1278
1361
  const outData = o.toString('base64');
1279
1362
  // Upload resources on low-end phones may OOM
1280
1363
  try {
1281
- const shouldUseWriteWithResponse =
1282
- Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1283
- if (shouldUseWriteWithResponse) {
1284
- await transport.writeCharacteristic.writeWithResponse(outData);
1285
- } else {
1286
- await transport.writeCharacteristic.writeWithoutResponse(outData);
1287
- }
1364
+ await this.writeBlePacket(
1365
+ uuid,
1366
+ outData,
1367
+ payload => transport.writeCharacteristic.writeWithoutResponse(payload),
1368
+ isCurrentOwner
1369
+ );
1288
1370
  } catch (e) {
1289
1371
  Log?.debug('writeCharacteristic write error: ', e);
1290
- this.runPromise = null;
1372
+ releaseOwnershipIfCurrent();
1373
+ if (isWedgedWriteError(e)) {
1374
+ throw e;
1375
+ }
1291
1376
  if (e.errorCode === BleErrorCode.DeviceDisconnected) {
1292
1377
  throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
1293
1378
  } else if (e.errorCode === BleErrorCode.OperationStartFailed) {
@@ -1330,8 +1415,13 @@ export default class ReactNativeBleTransport {
1330
1415
  }
1331
1416
  const isProbeTimeout =
1332
1417
  name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
1418
+ // A call that has been superseded (forceRun) or cleaned up no longer owns the
1419
+ // transport; its late timeout must not tear down the connection the current
1420
+ // call is actively using.
1421
+ const isStaleCall = this.runPromise !== runPromise;
1333
1422
  if (
1334
1423
  !isProbeTimeout &&
1424
+ !isStaleCall &&
1335
1425
  (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1336
1426
  ) {
1337
1427
  await this.disconnect(uuid);
@@ -1411,7 +1501,10 @@ export default class ReactNativeBleTransport {
1411
1501
  delete transportCache[session];
1412
1502
  }
1413
1503
  this.deviceProtocol.delete(session);
1504
+ this.probingProtocols.delete(session);
1414
1505
  this.deviceProtocolHints.delete(session);
1506
+ this.sessionProtocols.delete(session);
1507
+ this.protocolReprobeFailures.delete(session);
1415
1508
  this.protocolV2Assemblers.delete(session);
1416
1509
  this.resetProtocolV2Frames(session);
1417
1510
 
@@ -1437,6 +1530,113 @@ export default class ReactNativeBleTransport {
1437
1530
  this.runPromiseDeviceId = null;
1438
1531
  }
1439
1532
 
1533
+ /** Run a native connect under the JS backstop budget. */
1534
+ private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
1535
+ let timer: ReturnType<typeof setTimeout> | undefined;
1536
+ let timedOut = false;
1537
+ const pending = connect();
1538
+ // The abandoned attempt keeps running; swallow its late outcome so it cannot
1539
+ // surface as an unhandled rejection after we have already given up on it.
1540
+ pending.catch(() => undefined);
1541
+ try {
1542
+ const result = await Promise.race([
1543
+ pending,
1544
+ new Promise<never>((_, reject) => {
1545
+ timer = setTimeout(() => {
1546
+ timedOut = true;
1547
+ reject(
1548
+ ERRORS.TypedError(
1549
+ HardwareErrorCode.BleConnectedError,
1550
+ `BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
1551
+ )
1552
+ );
1553
+ }, BLE_CONNECT_TIMEOUT_MS);
1554
+ }),
1555
+ ]);
1556
+ return result;
1557
+ } catch (error) {
1558
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1559
+ this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
1560
+ }
1561
+ throw error;
1562
+ } finally {
1563
+ if (timer) clearTimeout(timer);
1564
+ }
1565
+ }
1566
+
1567
+ /** Resolve the complete GATT shape under a budget so acquire() always settles. */
1568
+ private async resolveCharacteristicsWithTimeout(
1569
+ uuid: string,
1570
+ device: Device
1571
+ ): Promise<ResolvedBleCharacteristics> {
1572
+ let timer: ReturnType<typeof setTimeout> | undefined;
1573
+ let timedOut = false;
1574
+ const pending = this.resolveCharacteristics(device);
1575
+ pending.catch(() => undefined);
1576
+ try {
1577
+ const result = await Promise.race([
1578
+ pending,
1579
+ new Promise<never>((_, reject) => {
1580
+ timer = setTimeout(() => {
1581
+ timedOut = true;
1582
+ reject(
1583
+ ERRORS.TypedError(
1584
+ HardwareErrorCode.BleConnectedError,
1585
+ `BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
1586
+ )
1587
+ );
1588
+ }, BLE_GATT_SETUP_TIMEOUT_MS);
1589
+ }),
1590
+ ]);
1591
+ this.connectionSetupTimeoutCounts.delete(uuid);
1592
+ return result;
1593
+ } catch (error) {
1594
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1595
+ this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1596
+ }
1597
+ throw error;
1598
+ } finally {
1599
+ if (timer) clearTimeout(timer);
1600
+ }
1601
+ }
1602
+
1603
+ /**
1604
+ * Give up on a BLE setup operation the native layer did not settle. The abandoned
1605
+ * operation still owns native connection/GATT state that can poison the next attempt,
1606
+ * so it is cleared here without awaiting the same queue that stopped responding.
1607
+ */
1608
+ private abandonStalledConnection(
1609
+ uuid: string,
1610
+ stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
1611
+ ) {
1612
+ const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
1613
+ this.connectionSetupTimeoutCounts.set(uuid, timeouts);
1614
+ Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
1615
+ stage,
1616
+ setupTimeoutsSinceSuccess: timeouts,
1617
+ });
1618
+
1619
+ this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
1620
+ // Rejects with "Operation was cancelled" while merely connecting — expected.
1621
+ });
1622
+ const stalled = transportCache[uuid];
1623
+ if (stalled) {
1624
+ delete transportCache[uuid];
1625
+ }
1626
+ this.deviceProtocol.delete(uuid);
1627
+ this.probingProtocols.delete(uuid);
1628
+ this.protocolV2Assemblers.delete(uuid);
1629
+ this.resetProtocolV2Frames(uuid);
1630
+
1631
+ if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1632
+ // BleManager.destroy() force-rejects every promise the native queue abandoned —
1633
+ // the only JS-reachable way to settle them — and drops all cached peripherals.
1634
+ Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
1635
+ this.resetPlxManager();
1636
+ this.connectionSetupTimeoutCounts.delete(uuid);
1637
+ }
1638
+ }
1639
+
1440
1640
  private getCachedTransport(uuid: string) {
1441
1641
  const transport = transportCache[uuid];
1442
1642
  if (!transport) {
@@ -1445,6 +1645,107 @@ export default class ReactNativeBleTransport {
1445
1645
  return transport;
1446
1646
  }
1447
1647
 
1648
+ /**
1649
+ * Write one packet under a bounded budget. A write that never settles means the
1650
+ * peripheral is wedged even though the GATT link still reports connected, so the
1651
+ * link is torn down: releasing JS state alone would leave the poisoned peripheral
1652
+ * cached and every later call would hang on it again.
1653
+ */
1654
+ private async writeBlePacket(
1655
+ uuid: string,
1656
+ data: string,
1657
+ write: (payload: string) => Promise<unknown>,
1658
+ isCurrentOwner?: () => boolean
1659
+ ) {
1660
+ let timer: ReturnType<typeof setTimeout> | undefined;
1661
+ let timedOut = false;
1662
+ try {
1663
+ await Promise.race([
1664
+ write(data),
1665
+ new Promise<never>((_, reject) => {
1666
+ timer = setTimeout(() => {
1667
+ timedOut = true;
1668
+ reject(
1669
+ ERRORS.TypedError(
1670
+ HardwareErrorCode.BleWriteCharacteristicError,
1671
+ `BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
1672
+ )
1673
+ );
1674
+ }, BLE_WRITE_PACKET_TIMEOUT_MS);
1675
+ }),
1676
+ ]);
1677
+ this.writeTimeoutCounts.delete(uuid);
1678
+ } catch (error) {
1679
+ if (timedOut) {
1680
+ // A superseded call's late write must not tear down the link the current
1681
+ // call is using; only the owner of the transport may declare it dead.
1682
+ if (isCurrentOwner && !isCurrentOwner()) {
1683
+ Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
1684
+ } else {
1685
+ this.tearDownWedgedLink(uuid);
1686
+ }
1687
+ }
1688
+ throw error;
1689
+ } finally {
1690
+ if (timer) clearTimeout(timer);
1691
+ }
1692
+ }
1693
+
1694
+ /**
1695
+ * Drop a link whose writes stopped completing. The JS state is purged synchronously
1696
+ * so the next acquire() cannot reuse the dead transport, while the native teardown is
1697
+ * intentionally NOT awaited: it talks to the very layer that just stopped settling
1698
+ * promises, so awaiting it could hang exactly like the write it is recovering from.
1699
+ */
1700
+ private tearDownWedgedLink(uuid: string) {
1701
+ const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
1702
+ this.writeTimeoutCounts.set(uuid, timeouts);
1703
+ Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
1704
+ consecutiveWriteTimeouts: timeouts,
1705
+ });
1706
+
1707
+ const wedged = transportCache[uuid];
1708
+ this.disconnect(uuid).catch(error => {
1709
+ Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
1710
+ });
1711
+ if (wedged && transportCache[uuid] === wedged) {
1712
+ delete transportCache[uuid];
1713
+ }
1714
+ this.deviceProtocol.delete(uuid);
1715
+ this.probingProtocols.delete(uuid);
1716
+ this.protocolV2Assemblers.delete(uuid);
1717
+ this.resetProtocolV2Frames(uuid);
1718
+
1719
+ if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1720
+ // Reconnecting reuses the same native peripheral object. When it stays wedged
1721
+ // across attempts the poison lives in the BLE manager itself, and only a fresh
1722
+ // manager drops every cached peripheral — the JS equivalent of restarting the app.
1723
+ Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
1724
+ this.resetPlxManager();
1725
+ this.writeTimeoutCounts.delete(uuid);
1726
+ }
1727
+ }
1728
+
1729
+ private resetPlxManager() {
1730
+ const manager = this.blePlxManager;
1731
+ this.blePlxManager = undefined;
1732
+ // Every cached transport belongs to the destroyed manager's peripherals.
1733
+ Object.keys(transportCache).forEach(key => {
1734
+ delete transportCache[key];
1735
+ });
1736
+ this.deviceProtocol.clear();
1737
+ this.probingProtocols.clear();
1738
+ this.sessionProtocols.clear();
1739
+ this.protocolReprobeFailures.clear();
1740
+ this.monitorTokens.clear();
1741
+ this.protocolV2Assemblers.clear();
1742
+ try {
1743
+ manager?.destroy();
1744
+ } catch (error) {
1745
+ Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
1746
+ }
1747
+ }
1748
+
1448
1749
  private createProtocolMismatchError(expected: ProtocolType) {
1449
1750
  return ERRORS.TypedError(
1450
1751
  HardwareErrorCode.RuntimeError,
@@ -1460,30 +1761,29 @@ export default class ReactNativeBleTransport {
1460
1761
  }
1461
1762
 
1462
1763
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1764
+ if (this.probingProtocols.get(uuid) === protocol) {
1765
+ this.probingProtocols.delete(uuid);
1766
+ }
1463
1767
  if (this.deviceProtocol.get(uuid) === protocol) {
1464
1768
  this.deviceProtocol.delete(uuid);
1465
1769
  }
1466
1770
  }
1467
1771
 
1772
+ /** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
1773
+ private getActiveProtocol(uuid: string): ProtocolType | undefined {
1774
+ return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
1775
+ }
1776
+
1468
1777
  private async detectProtocol(
1469
1778
  uuid: string,
1470
1779
  expectedProtocol?: ProtocolType,
1471
1780
  protocolHint?: ProtocolType,
1472
1781
  rebuildTransport?: () => Promise<void>
1473
1782
  ): Promise<ProtocolType> {
1474
- if (Platform.OS === 'ios' && expectedProtocol) {
1475
- this.deviceProtocol.set(uuid, expectedProtocol);
1476
- Log?.debug('[ReactNativeBleTransport] protocol selected', {
1477
- deviceId: uuid,
1478
- protocol: expectedProtocol,
1479
- source: 'expected',
1480
- });
1481
- return expectedProtocol;
1482
- }
1483
-
1484
1783
  if (expectedProtocol === 'V1') {
1485
1784
  if (await this.probeProtocolV1(uuid)) {
1486
1785
  this.deviceProtocol.set(uuid, 'V1');
1786
+ this.sessionProtocols.set(uuid, 'V1');
1487
1787
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1488
1788
  deviceId: uuid,
1489
1789
  protocol: 'V1',
@@ -1497,6 +1797,7 @@ export default class ReactNativeBleTransport {
1497
1797
  if (expectedProtocol === 'V2') {
1498
1798
  if (await this.probeProtocolV2(uuid)) {
1499
1799
  this.deviceProtocol.set(uuid, 'V2');
1800
+ this.sessionProtocols.set(uuid, 'V2');
1500
1801
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1501
1802
  deviceId: uuid,
1502
1803
  protocol: 'V2',
@@ -1509,8 +1810,18 @@ export default class ReactNativeBleTransport {
1509
1810
 
1510
1811
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
1511
1812
  // influence probe order; a V2 hint probes V2 first and falls back to V1.
1512
- const probeOrder: ProtocolType[] =
1813
+ const sessionProtocol = this.sessionProtocols.get(uuid);
1814
+ const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
1815
+ const fullProbeOrder: ProtocolType[] =
1513
1816
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1817
+ // A device that already answered on a protocol in this session keeps answering on
1818
+ // it; while it is rebooting nothing answers at all, so probing the other protocol
1819
+ // only adds its timeout to every poll.
1820
+ const trustSessionProtocol =
1821
+ sessionProtocol !== undefined &&
1822
+ !protocolHint &&
1823
+ reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1824
+ const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
1514
1825
 
1515
1826
  for (let i = 0; i < probeOrder.length; i += 1) {
1516
1827
  const protocol = probeOrder[i];
@@ -1528,6 +1839,8 @@ export default class ReactNativeBleTransport {
1528
1839
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1529
1840
  if (detected) {
1530
1841
  this.deviceProtocol.set(uuid, protocol);
1842
+ this.sessionProtocols.set(uuid, protocol);
1843
+ this.protocolReprobeFailures.delete(uuid);
1531
1844
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1532
1845
  deviceId: uuid,
1533
1846
  protocol,
@@ -1537,7 +1850,16 @@ export default class ReactNativeBleTransport {
1537
1850
  }
1538
1851
  }
1539
1852
 
1853
+ if (trustSessionProtocol) {
1854
+ // Still silent on its own protocol: count it, and let the streak expire the
1855
+ // shortcut so a device that genuinely switched protocols is found again.
1856
+ this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1857
+ } else {
1858
+ this.protocolReprobeFailures.delete(uuid);
1859
+ }
1860
+
1540
1861
  this.deviceProtocol.delete(uuid);
1862
+ this.probingProtocols.delete(uuid);
1541
1863
  throw this.createProtocolDetectionError();
1542
1864
  }
1543
1865
 
@@ -1599,14 +1921,20 @@ export default class ReactNativeBleTransport {
1599
1921
  }
1600
1922
 
1601
1923
  try {
1602
- this.deviceProtocol.set(uuid, 'V1');
1924
+ this.probingProtocols.set(uuid, 'V1');
1603
1925
  // GetFeatures identifies Protocol V1 without resetting an existing wallet
1604
1926
  // session before Core has a chance to restore a hidden wallet.
1605
1927
  await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1928
+ this.probingProtocols.delete(uuid);
1606
1929
  return true;
1607
1930
  } catch (error) {
1608
1931
  this.clearProbeProtocol(uuid, 'V1');
1609
1932
  Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
1933
+ // A wedged write already dropped the link, so probing another protocol on it
1934
+ // would only fail against a torn-down transport: surface the real cause.
1935
+ if (isWedgedWriteError(error)) {
1936
+ throw error;
1937
+ }
1610
1938
  return false;
1611
1939
  }
1612
1940
  }
@@ -1616,7 +1944,7 @@ export default class ReactNativeBleTransport {
1616
1944
  return false;
1617
1945
  }
1618
1946
 
1619
- this.deviceProtocol.set(uuid, 'V2');
1947
+ this.probingProtocols.set(uuid, 'V2');
1620
1948
  this.protocolV2Assemblers.get(uuid)?.reset();
1621
1949
  const detected = await probeProtocolV2Helper({
1622
1950
  call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
@@ -1631,6 +1959,8 @@ export default class ReactNativeBleTransport {
1631
1959
  });
1632
1960
  if (!detected) {
1633
1961
  this.clearProbeProtocol(uuid, 'V2');
1962
+ } else {
1963
+ this.probingProtocols.delete(uuid);
1634
1964
  }
1635
1965
  return detected;
1636
1966
  }
@@ -1712,14 +2042,12 @@ export default class ReactNativeBleTransport {
1712
2042
  }
1713
2043
 
1714
2044
  private async writeProtocolV2Packet(
2045
+ uuid: string,
1715
2046
  transport: BleTransport,
1716
2047
  base64: string,
1717
2048
  context: ProtocolV2CallContext,
1718
2049
  assertCurrentGeneration: () => void
1719
2050
  ) {
1720
- const shouldUseWriteWithResponse =
1721
- transport.writeCharacteristic.isWritableWithResponse &&
1722
- (context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
1723
2051
  let attempt = 0;
1724
2052
  for (;;) {
1725
2053
  assertCurrentGeneration();
@@ -1727,11 +2055,21 @@ export default class ReactNativeBleTransport {
1727
2055
  throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
1728
2056
  }
1729
2057
  try {
1730
- if (shouldUseWriteWithResponse) {
1731
- await transport.writeCharacteristic.writeWithResponse(base64);
1732
- } else {
1733
- await transport.writeCharacteristic.writeWithoutResponse(base64);
1734
- }
2058
+ await this.writeBlePacket(
2059
+ uuid,
2060
+ base64,
2061
+ payload => transport.writeCharacteristic.writeWithoutResponse(payload),
2062
+ // Same rule as Protocol V1: a write from a superseded generation must not
2063
+ // tear down the link that the current generation is using.
2064
+ () => {
2065
+ try {
2066
+ assertCurrentGeneration();
2067
+ return !context.signal.aborted;
2068
+ } catch {
2069
+ return false;
2070
+ }
2071
+ }
2072
+ );
1735
2073
  assertCurrentGeneration();
1736
2074
  return;
1737
2075
  } catch (error) {
@@ -1754,6 +2092,7 @@ export default class ReactNativeBleTransport {
1754
2092
  }
1755
2093
 
1756
2094
  private async writeProtocolV2Frame(
2095
+ uuid: string,
1757
2096
  transport: BleTransport,
1758
2097
  frame: Uint8Array,
1759
2098
  context: ProtocolV2CallContext,
@@ -1766,25 +2105,19 @@ export default class ReactNativeBleTransport {
1766
2105
  androidPacketLength: tuning.androidPacketLength,
1767
2106
  mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1768
2107
  });
1769
- // Match Desktop BLE pacing so Pro2 firmware can finish the previous response
1770
- // before the next single-packet control command is written.
1771
- const initialDelayMs =
1772
- Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
1773
- ? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
1774
- : 0;
1775
2108
  await writeProtocolV2BleFrame({
1776
2109
  frame,
1777
2110
  packetCapacity,
1778
2111
  assertActive: assertCurrentGeneration,
1779
2112
  signal: context.signal,
1780
2113
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1781
- initialDelayMs,
1782
2114
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1783
2115
  burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1784
2116
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1785
2117
  wait: delay,
1786
2118
  writePacket: packet =>
1787
2119
  this.writeProtocolV2Packet(
2120
+ uuid,
1788
2121
  transport,
1789
2122
  Buffer.from(packet).toString('base64'),
1790
2123
  context,
@@ -1810,7 +2143,7 @@ export default class ReactNativeBleTransport {
1810
2143
  const tuning = getProtocolV2BleTuning();
1811
2144
  Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1812
2145
  name,
1813
- writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
2146
+ writeMode: 'withoutResponse',
1814
2147
  packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1815
2148
  });
1816
2149
  }
@@ -1849,7 +2182,13 @@ export default class ReactNativeBleTransport {
1849
2182
  writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
1850
2183
  assertCurrentGeneration();
1851
2184
  const currentTransport = this.getCachedTransport(uuid);
1852
- await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
2185
+ await this.writeProtocolV2Frame(
2186
+ uuid,
2187
+ currentTransport,
2188
+ frame,
2189
+ context,
2190
+ assertCurrentGeneration
2191
+ );
1853
2192
  },
1854
2193
  readFrame: async () => {
1855
2194
  assertCurrentGeneration();
@@ -1874,6 +2213,6 @@ export default class ReactNativeBleTransport {
1874
2213
  }
1875
2214
 
1876
2215
  getProtocolType(path: string): ProtocolType | undefined {
1877
- return this.deviceProtocol.get(path);
2216
+ return this.getActiveProtocol(path);
1878
2217
  }
1879
2218
  }