@ledgerhq/react-native-hw-transport-ble 6.28.3 → 6.28.4-next.1

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.
@@ -1,33 +1,38 @@
1
- /* eslint-disable prefer-template */
2
1
  import Transport from "@ledgerhq/hw-transport";
2
+ // ---------------------------------------------------------------------------------------------
3
+ // Since this is a react-native library and metro bundler does not support
4
+ // package exports yet (see: https://github.com/facebook/metro/issues/670)
5
+ // we need to import the file directly from the lib folder.
6
+ // Otherwise it would force the consumer of the lib to manually "tell" metro to resolve to /lib.
7
+ //
8
+ // TLDR: /!\ Do not remove the /lib part in the import statements below (@ledgerhq/devices/lib) ! /!\
9
+ // See: https://github.com/LedgerHQ/ledger-live/pull/879
10
+ import { sendAPDU } from "@ledgerhq/devices/lib/ble/sendAPDU";
11
+ import { receiveAPDU } from "@ledgerhq/devices/lib/ble/receiveAPDU";
12
+
3
13
  import type {
4
14
  Subscription as TransportSubscription,
5
15
  Observer as TransportObserver,
6
- DescriptorEvent,
7
16
  } from "@ledgerhq/hw-transport";
8
17
  import {
9
18
  BleManager,
10
19
  ConnectionPriority,
11
20
  BleErrorCode,
21
+ LogLevel,
22
+ DeviceId,
23
+ Device,
24
+ Characteristic,
12
25
  BleError,
26
+ Subscription,
13
27
  } from "react-native-ble-plx";
14
28
  import {
29
+ BluetoothInfos,
15
30
  getBluetoothServiceUuids,
16
31
  getInfosForServiceUuid,
17
32
  } from "@ledgerhq/devices";
18
33
  import type { DeviceModel } from "@ledgerhq/devices";
19
- // ---------------------------------------------------------------------------------------------
20
- // Since this is a react-native library and metro bundler does not support
21
- // package exports yet (see: https://github.com/facebook/metro/issues/670)
22
- // we need to import the file directly from the lib folder.
23
- // Otherwise it would force the consumer of the lib to manually "tell" metro to resolve to /lib.
24
- //
25
- // TLDR: /!\ Do not remove the /lib part in the import statements below (@ledgerhq/devices/lib) ! /!\
26
- // See: https://github.com/LedgerHQ/ledger-live/pull/879
27
- import { sendAPDU } from "@ledgerhq/devices/lib/ble/sendAPDU";
28
- import { receiveAPDU } from "@ledgerhq/devices/lib/ble/receiveAPDU";
29
34
  import { log } from "@ledgerhq/logs";
30
- import { Observable, defer, merge, from, of, throwError } from "rxjs";
35
+ import { Observable, defer, merge, from, of, throwError, Observer } from "rxjs";
31
36
  import {
32
37
  share,
33
38
  ignoreElements,
@@ -42,35 +47,32 @@ import {
42
47
  DisconnectedDeviceDuringOperation,
43
48
  PairingFailed,
44
49
  HwTransportError,
45
- HwTransportErrorType,
46
50
  } from "@ledgerhq/errors";
47
- import type { Device, Characteristic } from "./types";
48
51
  import { monitorCharacteristic } from "./monitorCharacteristic";
49
52
  import { awaitsBleOn } from "./awaitsBleOn";
50
- import { decoratePromiseErrors, remapError } from "./remapErrors";
51
- let connectOptions: Record<string, unknown> = {
52
- requestMTU: 156,
53
- connectionPriority: 1,
54
- };
55
- const transportsCache = {};
53
+ import {
54
+ decoratePromiseErrors,
55
+ mapBleErrorToHwTransportError,
56
+ remapError,
57
+ } from "./remapErrors";
58
+ import { ReconnectionConfig } from "./types";
56
59
 
57
- let _bleManager: BleManager | null = null;
58
60
  /**
59
- * Allows lazy initialization of BleManager
60
- * Useful for iOS to only ask for Bluetooth permission when needed
61
- *
62
- * Do not use _bleManager directly
63
- * Only use this instance getter inside BleTransport
61
+ * This is potentially not needed anymore, to be checked if the bug is still
62
+ * happening.
64
63
  */
65
- const bleManagerInstance = (): BleManager => {
66
- if (!_bleManager) {
67
- _bleManager = new BleManager();
68
- }
64
+ let reconnectionConfig: ReconnectionConfig | null | undefined = {
65
+ pairingThreshold: 1000,
66
+ delayAfterFirstPairing: 4000,
67
+ };
69
68
 
70
- return _bleManager;
69
+ export const setReconnectionConfig = (
70
+ config: ReconnectionConfig | null | undefined
71
+ ): void => {
72
+ reconnectionConfig = config;
71
73
  };
72
74
 
73
- const retrieveInfos = (device) => {
75
+ const retrieveInfos = (device: Device | null) => {
74
76
  if (!device || !device.serviceUUIDs) return;
75
77
  const [serviceUUID] = device.serviceUUIDs;
76
78
  if (!serviceUUID) return;
@@ -79,66 +81,95 @@ const retrieveInfos = (device) => {
79
81
  return infos;
80
82
  };
81
83
 
82
- type ReconnectionConfig = {
83
- pairingThreshold: number;
84
- delayAfterFirstPairing: number;
84
+ const delay = (ms: number | undefined) =>
85
+ new Promise((success) => setTimeout(success, ms));
86
+
87
+ /**
88
+ * A cache of Bluetooth transport instances associated with device IDs.
89
+ * Allows efficient storage and retrieval of previously initialized transports.
90
+ * @type {Object.<string, BluetoothTransport>}
91
+ */
92
+ const transportsCache: { [deviceId: string]: BleTransport } = {};
93
+
94
+ // connectOptions is actually used by react-native-ble-plx even if comment above ConnectionOptions says it's not used
95
+ let connectOptions: Record<string, unknown> = {
96
+ // 156 bytes to max the iOS < 10 limit (158 bytes)
97
+ // (185 bytes for iOS >= 10)(up to 512 bytes for Android, but could be blocked at 23 bytes)
98
+ requestMTU: 156,
99
+ // Priority 1 = high. TODO: Check firmware update over BLE PR before merging
100
+ connectionPriority: 1,
85
101
  };
86
- let reconnectionConfig: ReconnectionConfig | null | undefined = {
87
- pairingThreshold: 1000,
88
- delayAfterFirstPairing: 4000,
102
+
103
+ /**
104
+ * Returns the instance of the Bluetooth Low Energy Manager. It initializes it only
105
+ * when it's first needed, preventing the permission prompt happening prematurely.
106
+ * Important: Do NOT access the _bleManager variable directly.
107
+ * Use this function instead.
108
+ * @returns {BleManager} - The instance of the BleManager.
109
+ */
110
+ let _bleManager: BleManager | null = null;
111
+ const bleManagerInstance = (): BleManager => {
112
+ if (!_bleManager) {
113
+ _bleManager = new BleManager();
114
+ }
115
+
116
+ return _bleManager;
89
117
  };
90
- export function setReconnectionConfig(
91
- config: ReconnectionConfig | null | undefined
92
- ) {
93
- reconnectionConfig = config;
94
- }
95
118
 
96
- const delay = (ms) => new Promise((success) => setTimeout(success, ms));
119
+ const clearDisconnectTimeout = (deviceId: string): void => {
120
+ const cachedTransport = transportsCache[deviceId];
121
+ if (cachedTransport && cachedTransport.disconnectTimeout) {
122
+ log(TAG, "Clearing queued disconnect");
123
+ clearTimeout(cachedTransport.disconnectTimeout);
124
+ }
125
+ };
97
126
 
98
127
  async function open(deviceOrId: Device | string, needsReconnect: boolean) {
99
- let device;
128
+ let device: Device;
129
+ log(TAG, `open with ${deviceOrId}`);
100
130
 
101
131
  if (typeof deviceOrId === "string") {
102
132
  if (transportsCache[deviceOrId]) {
103
- log("ble-verbose", "Transport in cache, using that.");
133
+ log(TAG, "Transport in cache, using that.");
134
+ clearDisconnectTimeout(deviceOrId);
104
135
  return transportsCache[deviceOrId];
105
136
  }
106
137
 
107
- log("ble-verbose", `Tries to open device: ${deviceOrId}`);
138
+ log(TAG, `Tries to open device: ${deviceOrId}`);
108
139
  await awaitsBleOn(bleManagerInstance());
109
140
 
110
- if (!device) {
111
- // works for iOS but not Android
112
- const devices = await bleManagerInstance().devices([deviceOrId]);
113
- log("ble-verbose", `found ${devices.length} devices`);
114
- [device] = devices;
115
- }
141
+ // Returns a list of known devices by their identifiers
142
+ const devices = await bleManagerInstance().devices([deviceOrId]);
143
+ log(TAG, `found ${devices.length} devices`);
144
+ [device] = devices;
116
145
 
117
146
  if (!device) {
147
+ // Returns a list of the peripherals currently connected to the system
148
+ // which have discovered services, connected to system doesn't mean
149
+ // connected to our app, we check that below.
118
150
  const connectedDevices = await bleManagerInstance().connectedDevices(
119
151
  getBluetoothServiceUuids()
120
152
  );
121
153
  const connectedDevicesFiltered = connectedDevices.filter(
122
154
  (d) => d.id === deviceOrId
123
155
  );
124
- log(
125
- "ble-verbose",
126
- `found ${connectedDevicesFiltered.length} connected devices`
127
- );
156
+ log(TAG, `found ${connectedDevicesFiltered.length} connected devices`);
128
157
  [device] = connectedDevicesFiltered;
129
158
  }
130
159
 
131
160
  if (!device) {
132
- log("ble-verbose", `connectToDevice(${deviceOrId})`);
133
-
161
+ // We still don't have a device, so we attempt to connect to it.
162
+ log(TAG, `connectToDevice(${deviceOrId})`);
163
+ // Nb ConnectionOptions dropped since it's not used internally by ble-plx.
134
164
  try {
135
165
  device = await bleManagerInstance().connectToDevice(
136
166
  deviceOrId,
137
167
  connectOptions
138
168
  );
139
169
  } catch (e: any) {
170
+ log(TAG, `error code ${e.errorCode}`);
140
171
  if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
141
- // eslint-disable-next-line require-atomic-updates
172
+ // If the MTU update did not work, we try to connect without requesting for a specific MTU
142
173
  connectOptions = {};
143
174
  device = await bleManagerInstance().connectToDevice(deviceOrId);
144
175
  } else {
@@ -151,17 +182,17 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
151
182
  throw new CantOpenDevice();
152
183
  }
153
184
  } else {
185
+ // It was already a Device
154
186
  device = deviceOrId;
155
187
  }
156
188
 
157
189
  if (!(await device.isConnected())) {
158
- log("ble-verbose", "not connected. connecting...");
159
-
190
+ log(TAG, "not connected. connecting...");
160
191
  try {
161
192
  await device.connect(connectOptions);
162
193
  } catch (e: any) {
163
194
  if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
164
- // eslint-disable-next-line require-atomic-updates
195
+ // If the MTU update did not work, we try to connect without requesting for a specific MTU
165
196
  connectOptions = {};
166
197
  await device.connect();
167
198
  } else {
@@ -171,8 +202,8 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
171
202
  }
172
203
 
173
204
  await device.discoverAllServicesAndCharacteristics();
174
- let res = retrieveInfos(device);
175
- let characteristics;
205
+ let res: BluetoothInfos | undefined = retrieveInfos(device);
206
+ let characteristics: Characteristic[] | undefined;
176
207
 
177
208
  if (!res) {
178
209
  for (const uuid of getBluetoothServiceUuids()) {
@@ -200,9 +231,9 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
200
231
  throw new TransportError("service not found", "BLEServiceNotFound");
201
232
  }
202
233
 
203
- let writeC;
204
- let writeCmdC;
205
- let notifyC;
234
+ let writeC: Characteristic | null | undefined;
235
+ let writeCmdC: Characteristic | undefined;
236
+ let notifyC: Characteristic | null | undefined;
206
237
 
207
238
  for (const c of characteristics) {
208
239
  if (c.uuid === writeUuid) {
@@ -217,28 +248,28 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
217
248
  if (!writeC) {
218
249
  throw new TransportError(
219
250
  "write characteristic not found",
220
- "BLEChracteristicNotFound"
251
+ "BLECharacteristicNotFound"
221
252
  );
222
253
  }
223
254
 
224
255
  if (!notifyC) {
225
256
  throw new TransportError(
226
257
  "notify characteristic not found",
227
- "BLEChracteristicNotFound"
258
+ "BLECharacteristicNotFound"
228
259
  );
229
260
  }
230
261
 
231
262
  if (!writeC.isWritableWithResponse) {
232
263
  throw new TransportError(
233
264
  "write characteristic not writableWithResponse",
234
- "BLEChracteristicInvalid"
265
+ "BLECharacteristicInvalid"
235
266
  );
236
267
  }
237
268
 
238
269
  if (!notifyC.isNotifiable) {
239
270
  throw new TransportError(
240
271
  "notify characteristic not notifiable",
241
- "BLEChracteristicInvalid"
272
+ "BLECharacteristicInvalid"
242
273
  );
243
274
  }
244
275
 
@@ -246,12 +277,12 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
246
277
  if (!writeCmdC.isWritableWithoutResponse) {
247
278
  throw new TransportError(
248
279
  "write cmd characteristic not writableWithoutResponse",
249
- "BLEChracteristicInvalid"
280
+ "BLECharacteristicInvalid"
250
281
  );
251
282
  }
252
283
  }
253
284
 
254
- log("ble-verbose", `device.mtu=${device.mtu}`);
285
+ log(TAG, `device.mtu=${device.mtu}`);
255
286
  const notifyObservable = monitorCharacteristic(notifyC).pipe(
256
287
  catchError((e) => {
257
288
  // LL-9033 fw 2.0.2 introduced this case, we silence the inner unhandled error.
@@ -267,7 +298,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
267
298
  share()
268
299
  );
269
300
  const notif = notifyObservable.subscribe();
270
- const transport = new BluetoothTransport(
301
+ const transport = new BleTransport(
271
302
  device,
272
303
  writeC,
273
304
  writeCmdC,
@@ -275,24 +306,30 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
275
306
  deviceModel
276
307
  );
277
308
 
278
- await transport.requestConnectionPriority("High");
279
-
280
- const onDisconnect = (e) => {
309
+ // Keeping it as a comment for now but if no new bluetooth issues occur, we will be able to remove it
310
+ // await transport.requestConnectionPriority("High");
311
+ // eslint-disable-next-line prefer-const
312
+ let disconnectedSub: Subscription;
313
+ const onDisconnect = (e: BleError | null) => {
314
+ transport.isConnected = false;
281
315
  transport.notYetDisconnected = false;
282
316
  notif.unsubscribe();
283
- disconnectedSub.remove();
317
+ disconnectedSub?.remove();
318
+
319
+ clearDisconnectTimeout(transport.id);
284
320
  delete transportsCache[transport.id];
285
- log("ble-verbose", `BleTransport(${transport.id}) disconnected`);
321
+ log(TAG, `BleTransport(${transport.id}) disconnected`);
286
322
  transport.emit("disconnect", e);
287
323
  };
288
324
 
289
325
  // eslint-disable-next-line require-atomic-updates
290
326
  transportsCache[transport.id] = transport;
291
- const disconnectedSub = device.onDisconnected((e) => {
327
+ const beforeMTUTime = Date.now();
328
+
329
+ disconnectedSub = device.onDisconnected((e) => {
292
330
  if (!transport.notYetDisconnected) return;
293
331
  onDisconnect(e);
294
332
  });
295
- const beforeMTUTime = Date.now();
296
333
 
297
334
  try {
298
335
  await transport.inferMTU();
@@ -309,7 +346,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
309
346
 
310
347
  if (needsReconnect) {
311
348
  // necessary time for the bonding workaround
312
- await BluetoothTransport.disconnect(transport.id).catch(() => {});
349
+ await BleTransport.disconnect(transport.id).catch(() => {});
313
350
  await delay(reconnectionConfig.delayAfterFirstPairing);
314
351
  }
315
352
  } else {
@@ -327,9 +364,11 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
327
364
  /**
328
365
  * react-native bluetooth BLE implementation
329
366
  * @example
330
- * import BluetoothTransport from "@ledgerhq/react-native-hw-transport-ble";
367
+ * import BleTransport from "@ledgerhq/react-native-hw-transport-ble";
331
368
  */
332
- export default class BluetoothTransport extends Transport {
369
+ const TAG = "ble-verbose";
370
+ export default class BleTransport extends Transport {
371
+ static disconnectTimeoutMs = 5000;
333
372
  /**
334
373
  *
335
374
  */
@@ -339,17 +378,36 @@ export default class BluetoothTransport extends Transport {
339
378
  /**
340
379
  *
341
380
  */
342
- static setLogLevel = (level: any) => {
343
- bleManagerInstance().setLogLevel(level);
381
+ static list = (): Promise<void[]> => {
382
+ throw new Error("not implemented");
383
+ };
384
+
385
+ /**
386
+ * Exposed method from the ble-plx library
387
+ * Sets new log level for native module's logging mechanism.
388
+ * @param string logLevel New log level to be set.
389
+ */
390
+ static setLogLevel = (logLevel: string): void => {
391
+ if (Object.values<string>(LogLevel).includes(logLevel)) {
392
+ bleManagerInstance().setLogLevel(logLevel as LogLevel);
393
+ } else {
394
+ throw new Error(`${logLevel} is not a valid LogLevel`);
395
+ }
344
396
  };
345
397
 
346
398
  /**
347
- * TODO could add this concept in all transports
348
- * observe event with { available: bool, string } // available is generic, type is specific
349
- * an event is emit once and then listened
399
+ * Listen to state changes on the bleManagerInstance and notify the
400
+ * specified observer.
401
+ * @param observer
402
+ * @returns TransportSubscription
350
403
  */
351
- static observeState(observer: any) {
352
- const emitFromState = (type) => {
404
+ static observeState(
405
+ observer: Observer<{
406
+ type: string;
407
+ available: boolean;
408
+ }>
409
+ ): TransportSubscription {
410
+ const emitFromState = (type: string) => {
353
411
  observer.next({
354
412
  type,
355
413
  available: type === "PoweredOn",
@@ -357,24 +415,23 @@ export default class BluetoothTransport extends Transport {
357
415
  };
358
416
 
359
417
  bleManagerInstance().onStateChange(emitFromState, true);
418
+
360
419
  return {
361
420
  unsubscribe: () => {},
362
421
  };
363
422
  }
364
423
 
365
- static list = (): any => {
366
- throw new Error("not implemented");
367
- };
368
-
369
424
  /**
370
425
  * Scan for bluetooth Ledger devices
426
+ * @param observer Device is partial in order to avoid the live-common/this dep
427
+ * @returns TransportSubscription
371
428
  */
372
429
  static listen(
373
- observer: TransportObserver<DescriptorEvent<Device>, HwTransportError>
430
+ observer: TransportObserver<any, HwTransportError>
374
431
  ): TransportSubscription {
375
- log("ble-verbose", "listen...");
432
+ log(TAG, "listening for devices");
376
433
 
377
- let unsubscribed;
434
+ let unsubscribed: boolean;
378
435
 
379
436
  const stateSub = bleManagerInstance().onStateChange(async (state) => {
380
437
  if (state === "PoweredOn") {
@@ -383,29 +440,32 @@ export default class BluetoothTransport extends Transport {
383
440
  getBluetoothServiceUuids()
384
441
  );
385
442
  if (unsubscribed) return;
386
- await Promise.all(
387
- devices.map((d) =>
388
- BluetoothTransport.disconnect(d.id).catch(() => {})
389
- )
390
- );
443
+ if (devices.length) {
444
+ log(TAG, "disconnecting from devices");
445
+
446
+ await Promise.all(
447
+ devices.map((d) => BleTransport.disconnect(d.id).catch(() => {}))
448
+ );
449
+ }
450
+
391
451
  if (unsubscribed) return;
392
452
  bleManagerInstance().startDeviceScan(
393
453
  getBluetoothServiceUuids(),
394
454
  null,
395
- (bleError, device) => {
455
+ (bleError: BleError | null, scannedDevice: Device | null) => {
396
456
  if (bleError) {
397
457
  observer.error(mapBleErrorToHwTransportError(bleError));
398
458
  unsubscribe();
399
459
  return;
400
460
  }
401
461
 
402
- const res = retrieveInfos(device);
462
+ const res = retrieveInfos(scannedDevice);
403
463
  const deviceModel = res && res.deviceModel;
404
464
 
405
- if (device) {
465
+ if (scannedDevice) {
406
466
  observer.next({
407
467
  type: "add",
408
- descriptor: device,
468
+ descriptor: scannedDevice,
409
469
  deviceModel,
410
470
  });
411
471
  }
@@ -418,7 +478,8 @@ export default class BluetoothTransport extends Transport {
418
478
  unsubscribed = true;
419
479
  bleManagerInstance().stopDeviceScan();
420
480
  stateSub.remove();
421
- log("ble-verbose", "done listening.");
481
+
482
+ log(TAG, "done listening.");
422
483
  };
423
484
 
424
485
  return {
@@ -428,32 +489,37 @@ export default class BluetoothTransport extends Transport {
428
489
 
429
490
  /**
430
491
  * Open a BLE transport
431
- * @param {*} deviceOrId
492
+ * @param {Device | string} deviceOrId
432
493
  */
433
- static async open(deviceOrId: Device | string) {
494
+ static async open(deviceOrId: Device | string): Promise<BleTransport> {
434
495
  return open(deviceOrId, true);
435
496
  }
436
497
 
437
498
  /**
438
- * Globally disconnect a BLE device by its ID
499
+ * Exposed method from the ble-plx library
500
+ * Disconnects from {@link Device} if it's connected or cancels pending connection.
439
501
  */
440
- static disconnect = async (id: any) => {
441
- log("ble-verbose", `user disconnect(${id})`);
502
+ static disconnect = async (id: DeviceId): Promise<void> => {
503
+ log(TAG, `user disconnect(${id})`);
442
504
  await bleManagerInstance().cancelDeviceConnection(id);
505
+ log(TAG, "disconnected");
443
506
  };
444
- id: string;
507
+
445
508
  device: Device;
509
+ deviceModel: DeviceModel;
510
+ disconnectTimeout: null | ReturnType<typeof setTimeout> = null;
511
+ id: string;
512
+ isConnected = true;
446
513
  mtuSize = 20;
447
- writeCharacteristic: Characteristic;
448
- writeCmdCharacteristic: Characteristic;
449
514
  notifyObservable: Observable<any>;
450
- deviceModel: DeviceModel;
451
515
  notYetDisconnected = true;
516
+ writeCharacteristic: Characteristic;
517
+ writeCmdCharacteristic: Characteristic | undefined;
452
518
 
453
519
  constructor(
454
520
  device: Device,
455
521
  writeCharacteristic: Characteristic,
456
- writeCmdCharacteristic: Characteristic,
522
+ writeCmdCharacteristic: Characteristic | undefined,
457
523
  notifyObservable: Observable<any>,
458
524
  deviceModel: DeviceModel
459
525
  ) {
@@ -464,24 +530,31 @@ export default class BluetoothTransport extends Transport {
464
530
  this.writeCmdCharacteristic = writeCmdCharacteristic;
465
531
  this.notifyObservable = notifyObservable;
466
532
  this.deviceModel = deviceModel;
467
- log("ble-verbose", `BleTransport(${String(this.id)}) new instance`);
533
+
534
+ log(TAG, `BleTransport(${String(this.id)}) new instance`);
535
+ clearDisconnectTimeout(this.id);
468
536
  }
469
537
 
470
538
  /**
471
- * communicate with a BLE transport
539
+ * Send data to the device using a low level API.
540
+ * It's recommended to use the "send" method for a higher level API.
541
+ * @param {Buffer} apdu - The data to send.
542
+ * @returns {Promise<Buffer>} A promise that resolves with the response data from the device.
472
543
  */
473
544
  exchange = (apdu: Buffer): Promise<any> =>
474
545
  this.exchangeAtomicImpl(async () => {
475
546
  try {
476
547
  const msgIn = apdu.toString("hex");
477
548
  log("apdu", `=> ${msgIn}`);
549
+
478
550
  const data = await merge(
479
- // $FlowFixMe
480
551
  this.notifyObservable.pipe(receiveAPDU),
481
552
  sendAPDU(this.write, apdu, this.mtuSize)
482
553
  ).toPromise();
554
+
483
555
  const msgOut = data.toString("hex");
484
556
  log("apdu", `<= ${msgOut}`);
557
+
485
558
  return data;
486
559
  } catch (e: any) {
487
560
  log("ble-error", "exchange got " + String(e));
@@ -497,26 +570,30 @@ export default class BluetoothTransport extends Transport {
497
570
  }
498
571
  });
499
572
 
500
- // TODO we probably will do this at end of open
501
- async inferMTU() {
573
+ /**
574
+ * Negotiate with the device the maximum transfer unit for the ble frames
575
+ * @returns Promise<number>
576
+ */
577
+ async inferMTU(): Promise<number> {
502
578
  let { mtu } = this.device;
579
+
503
580
  await this.exchangeAtomicImpl(async () => {
504
581
  try {
505
- mtu =
506
- (await merge(
507
- this.notifyObservable.pipe(
508
- tap((maybeError) => {
509
- if (maybeError instanceof Error) throw maybeError;
510
- }),
511
- first((buffer) => buffer.readUInt8(0) === 0x08),
512
- map((buffer) => buffer.readUInt8(5))
513
- ),
514
- defer(() => from(this.write(Buffer.from([0x08, 0, 0, 0, 0])))).pipe(
515
- ignoreElements()
516
- )
517
- ).toPromise()) + 3;
582
+ mtu = await merge(
583
+ this.notifyObservable.pipe(
584
+ tap((maybeError) => {
585
+ if (maybeError instanceof Error) throw maybeError;
586
+ }),
587
+ first((buffer) => buffer.readUInt8(0) === 0x08),
588
+ map((buffer) => buffer.readUInt8(5))
589
+ ),
590
+ defer(() => from(this.write(Buffer.from([0x08, 0, 0, 0, 0])))).pipe(
591
+ ignoreElements()
592
+ )
593
+ ).toPromise();
518
594
  } catch (e: any) {
519
- log("ble-error", "inferMTU got " + String(e));
595
+ log("ble-error", "inferMTU got " + JSON.stringify(e));
596
+
520
597
  await bleManagerInstance()
521
598
  .cancelDeviceConnection(this.id)
522
599
  .catch(() => {}); // but we ignore if disconnect worked.
@@ -525,33 +602,38 @@ export default class BluetoothTransport extends Transport {
525
602
  }
526
603
  });
527
604
 
528
- if (mtu > 23) {
529
- const mtuSize = mtu - 3;
530
- log(
531
- "ble-verbose",
532
- `BleTransport(${String(this.id)}) mtu set to ${String(mtuSize)}`
533
- );
534
- this.mtuSize = mtuSize;
605
+ if (mtu > 20) {
606
+ this.mtuSize = mtu;
607
+ log(TAG, `BleTransport(${this.id}) mtu set to ${this.mtuSize}`);
535
608
  }
536
609
 
537
610
  return this.mtuSize;
538
611
  }
539
612
 
613
+ /**
614
+ * Exposed method from the ble-plx library
615
+ * Request the connection priority for the given device.
616
+ * @param {"Balanced" | "High" | "LowPower"} connectionPriority: Connection priority.
617
+ * @returns {Promise<Device>} Connected device.
618
+ */
540
619
  async requestConnectionPriority(
541
620
  connectionPriority: "Balanced" | "High" | "LowPower"
542
- ) {
543
- await decoratePromiseErrors(
621
+ ): Promise<Device> {
622
+ return await decoratePromiseErrors(
544
623
  this.device.requestConnectionPriority(
545
- ConnectionPriority[connectionPriority]
624
+ ConnectionPriority[connectionPriority as keyof ConnectionPriority]
546
625
  )
547
626
  );
548
627
  }
549
628
 
550
- setScrambleKey() {}
551
-
552
- write = async (buffer: Buffer, txid?: string | null | undefined) => {
629
+ /**
630
+ * Do not call this directly unless you know what you're doing. Communication
631
+ * with a Ledger device should be through the {@link exchange} method.
632
+ * @param buffer
633
+ * @param txid
634
+ */
635
+ write = async (buffer: Buffer, txid?: string | undefined): Promise<void> => {
553
636
  log("ble-frame", "=> " + buffer.toString("hex"));
554
-
555
637
  if (!this.writeCmdCharacteristic) {
556
638
  try {
557
639
  await this.writeCharacteristic.writeWithResponse(
@@ -573,32 +655,42 @@ export default class BluetoothTransport extends Transport {
573
655
  }
574
656
  };
575
657
 
576
- async close() {
577
- if (this.exchangeBusyPromise) {
578
- await this.exchangeBusyPromise;
579
- }
658
+ /**
659
+ * We intentionally do not immediately close a transport connection.
660
+ * Instead, we queue the disconnect and wait for a future connection to dismiss the event.
661
+ * This approach prevents unnecessary disconnects and reconnects. We use the isConnected
662
+ * flag to ensure that we do not trigger a disconnect if the current cached transport has
663
+ * already been disconnected.
664
+ * @returns {Promise<void>}
665
+ */
666
+ async close(): Promise<void> {
667
+ let resolve: (value: void | PromiseLike<void>) => void;
668
+ const disconnectPromise = new Promise<void>((innerResolve) => {
669
+ resolve = innerResolve;
670
+ });
671
+
672
+ clearDisconnectTimeout(this.id);
673
+
674
+ log(TAG, "Queuing a disconnect");
675
+
676
+ this.disconnectTimeout = setTimeout(() => {
677
+ log(TAG, `Triggering a disconnect from ${this.id}`);
678
+ if (this.isConnected) {
679
+ BleTransport.disconnect(this.id)
680
+ .catch(() => {})
681
+ .finally(resolve);
682
+ } else {
683
+ resolve();
684
+ }
685
+ }, BleTransport.disconnectTimeoutMs);
686
+
687
+ // The closure will occur no later than 5s, triggered either by disconnection
688
+ // or the actual response of the apdu.
689
+ await Promise.race([
690
+ this.exchangeBusyPromise || Promise.resolve(),
691
+ disconnectPromise,
692
+ ]);
693
+
694
+ return;
580
695
  }
581
696
  }
582
-
583
- const bleErrorToHwTransportError = new Map([
584
- [BleErrorCode.ScanStartFailed, HwTransportErrorType.BleScanStartFailed],
585
- [
586
- BleErrorCode.LocationServicesDisabled,
587
- HwTransportErrorType.BleLocationServicesDisabled,
588
- ],
589
- [
590
- BleErrorCode.BluetoothUnauthorized,
591
- HwTransportErrorType.BleBluetoothUnauthorized,
592
- ],
593
- ]);
594
-
595
- const mapBleErrorToHwTransportError = (
596
- bleError: BleError
597
- ): HwTransportError => {
598
- const message = `${bleError.message}. Origin: ${bleError.errorCode}`;
599
-
600
- const inferedType = bleErrorToHwTransportError.get(bleError.errorCode);
601
- const type = !inferedType ? HwTransportErrorType.Unknown : inferedType;
602
-
603
- return new HwTransportError(type, message);
604
- };