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

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,67 +81,82 @@ const retrieveInfos = (device) => {
79
81
  return infos;
80
82
  };
81
83
 
82
- type ReconnectionConfig = {
83
- pairingThreshold: number;
84
- delayAfterFirstPairing: number;
85
- };
86
- let reconnectionConfig: ReconnectionConfig | null | undefined = {
87
- pairingThreshold: 1000,
88
- delayAfterFirstPairing: 4000,
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
+ /**
95
+ * Returns the instance of the Bluetooth Low Energy Manager. It initializes it only
96
+ * when it's first needed, preventing the permission prompt happening prematurely.
97
+ * Important: Do NOT access the _bleManager variable directly.
98
+ * Use this function instead.
99
+ * @returns {BleManager} - The instance of the BleManager.
100
+ */
101
+ let _bleManager: BleManager | null = null;
102
+ const bleManagerInstance = (): BleManager => {
103
+ if (!_bleManager) {
104
+ _bleManager = new BleManager();
105
+ }
106
+
107
+ return _bleManager;
89
108
  };
90
- export function setReconnectionConfig(
91
- config: ReconnectionConfig | null | undefined
92
- ) {
93
- reconnectionConfig = config;
94
- }
95
109
 
96
- const delay = (ms) => new Promise((success) => setTimeout(success, ms));
110
+ const clearDisconnectTimeout = (deviceId: string): void => {
111
+ const cachedTransport = transportsCache[deviceId];
112
+ if (cachedTransport && cachedTransport.disconnectTimeout) {
113
+ log(TAG, "Clearing queued disconnect");
114
+ clearTimeout(cachedTransport.disconnectTimeout);
115
+ }
116
+ };
97
117
 
98
118
  async function open(deviceOrId: Device | string, needsReconnect: boolean) {
99
- let device;
119
+ let device: Device;
120
+ log(TAG, `open with ${deviceOrId}`);
100
121
 
101
122
  if (typeof deviceOrId === "string") {
102
123
  if (transportsCache[deviceOrId]) {
103
- log("ble-verbose", "Transport in cache, using that.");
124
+ log(TAG, "Transport in cache, using that.");
125
+ clearDisconnectTimeout(deviceOrId);
104
126
  return transportsCache[deviceOrId];
105
127
  }
106
128
 
107
- log("ble-verbose", `Tries to open device: ${deviceOrId}`);
129
+ log(TAG, `Tries to open device: ${deviceOrId}`);
108
130
  await awaitsBleOn(bleManagerInstance());
109
131
 
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
- }
132
+ // Returns a list of known devices by their identifiers
133
+ const devices = await bleManagerInstance().devices([deviceOrId]);
134
+ log(TAG, `found ${devices.length} devices`);
135
+ [device] = devices;
116
136
 
117
137
  if (!device) {
138
+ // Returns a list of the peripherals currently connected to the system
139
+ // which have discovered services, connected to system doesn't mean
140
+ // connected to our app, we check that below.
118
141
  const connectedDevices = await bleManagerInstance().connectedDevices(
119
142
  getBluetoothServiceUuids()
120
143
  );
121
144
  const connectedDevicesFiltered = connectedDevices.filter(
122
145
  (d) => d.id === deviceOrId
123
146
  );
124
- log(
125
- "ble-verbose",
126
- `found ${connectedDevicesFiltered.length} connected devices`
127
- );
147
+ log(TAG, `found ${connectedDevicesFiltered.length} connected devices`);
128
148
  [device] = connectedDevicesFiltered;
129
149
  }
130
150
 
131
151
  if (!device) {
132
- log("ble-verbose", `connectToDevice(${deviceOrId})`);
133
-
152
+ // We still don't have a device, so we attempt to connect to it.
153
+ log(TAG, `connectToDevice(${deviceOrId})`);
154
+ // Nb ConnectionOptions dropped since it's not used internally by ble-plx.
134
155
  try {
135
- device = await bleManagerInstance().connectToDevice(
136
- deviceOrId,
137
- connectOptions
138
- );
156
+ device = await bleManagerInstance().connectToDevice(deviceOrId);
139
157
  } catch (e: any) {
158
+ log(TAG, `error code ${e.errorCode}`);
140
159
  if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
141
- // eslint-disable-next-line require-atomic-updates
142
- connectOptions = {};
143
160
  device = await bleManagerInstance().connectToDevice(deviceOrId);
144
161
  } else {
145
162
  throw e;
@@ -151,18 +168,17 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
151
168
  throw new CantOpenDevice();
152
169
  }
153
170
  } else {
171
+ // It was already a Device
154
172
  device = deviceOrId;
155
173
  }
156
174
 
157
175
  if (!(await device.isConnected())) {
158
- log("ble-verbose", "not connected. connecting...");
159
-
176
+ log(TAG, "not connected. connecting...");
160
177
  try {
161
- await device.connect(connectOptions);
178
+ await device.connect();
162
179
  } catch (e: any) {
163
180
  if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
164
- // eslint-disable-next-line require-atomic-updates
165
- connectOptions = {};
181
+ // Retry once for this specific error.
166
182
  await device.connect();
167
183
  } else {
168
184
  throw e;
@@ -171,8 +187,8 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
171
187
  }
172
188
 
173
189
  await device.discoverAllServicesAndCharacteristics();
174
- let res = retrieveInfos(device);
175
- let characteristics;
190
+ let res: BluetoothInfos | undefined = retrieveInfos(device);
191
+ let characteristics: Characteristic[] | undefined;
176
192
 
177
193
  if (!res) {
178
194
  for (const uuid of getBluetoothServiceUuids()) {
@@ -200,9 +216,9 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
200
216
  throw new TransportError("service not found", "BLEServiceNotFound");
201
217
  }
202
218
 
203
- let writeC;
204
- let writeCmdC;
205
- let notifyC;
219
+ let writeC: Characteristic | null | undefined;
220
+ let writeCmdC: Characteristic | undefined;
221
+ let notifyC: Characteristic | null | undefined;
206
222
 
207
223
  for (const c of characteristics) {
208
224
  if (c.uuid === writeUuid) {
@@ -217,28 +233,28 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
217
233
  if (!writeC) {
218
234
  throw new TransportError(
219
235
  "write characteristic not found",
220
- "BLEChracteristicNotFound"
236
+ "BLECharacteristicNotFound"
221
237
  );
222
238
  }
223
239
 
224
240
  if (!notifyC) {
225
241
  throw new TransportError(
226
242
  "notify characteristic not found",
227
- "BLEChracteristicNotFound"
243
+ "BLECharacteristicNotFound"
228
244
  );
229
245
  }
230
246
 
231
247
  if (!writeC.isWritableWithResponse) {
232
248
  throw new TransportError(
233
249
  "write characteristic not writableWithResponse",
234
- "BLEChracteristicInvalid"
250
+ "BLECharacteristicInvalid"
235
251
  );
236
252
  }
237
253
 
238
254
  if (!notifyC.isNotifiable) {
239
255
  throw new TransportError(
240
256
  "notify characteristic not notifiable",
241
- "BLEChracteristicInvalid"
257
+ "BLECharacteristicInvalid"
242
258
  );
243
259
  }
244
260
 
@@ -246,12 +262,12 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
246
262
  if (!writeCmdC.isWritableWithoutResponse) {
247
263
  throw new TransportError(
248
264
  "write cmd characteristic not writableWithoutResponse",
249
- "BLEChracteristicInvalid"
265
+ "BLECharacteristicInvalid"
250
266
  );
251
267
  }
252
268
  }
253
269
 
254
- log("ble-verbose", `device.mtu=${device.mtu}`);
270
+ log(TAG, `device.mtu=${device.mtu}`);
255
271
  const notifyObservable = monitorCharacteristic(notifyC).pipe(
256
272
  catchError((e) => {
257
273
  // LL-9033 fw 2.0.2 introduced this case, we silence the inner unhandled error.
@@ -267,7 +283,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
267
283
  share()
268
284
  );
269
285
  const notif = notifyObservable.subscribe();
270
- const transport = new BluetoothTransport(
286
+ const transport = new BleTransport(
271
287
  device,
272
288
  writeC,
273
289
  writeCmdC,
@@ -275,24 +291,29 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
275
291
  deviceModel
276
292
  );
277
293
 
278
- await transport.requestConnectionPriority("High");
279
-
280
- const onDisconnect = (e) => {
294
+ // await transport.requestConnectionPriority("High");
295
+ // eslint-disable-next-line prefer-const
296
+ let disconnectedSub: Subscription;
297
+ const onDisconnect = (e: BleError | null) => {
298
+ transport.isConnected = false;
281
299
  transport.notYetDisconnected = false;
282
300
  notif.unsubscribe();
283
- disconnectedSub.remove();
301
+ disconnectedSub?.remove();
302
+
303
+ clearDisconnectTimeout(transport.id);
284
304
  delete transportsCache[transport.id];
285
- log("ble-verbose", `BleTransport(${transport.id}) disconnected`);
305
+ log(TAG, `BleTransport(${transport.id}) disconnected`);
286
306
  transport.emit("disconnect", e);
287
307
  };
288
308
 
289
309
  // eslint-disable-next-line require-atomic-updates
290
310
  transportsCache[transport.id] = transport;
291
- const disconnectedSub = device.onDisconnected((e) => {
311
+ const beforeMTUTime = Date.now();
312
+
313
+ disconnectedSub = device.onDisconnected((e) => {
292
314
  if (!transport.notYetDisconnected) return;
293
315
  onDisconnect(e);
294
316
  });
295
- const beforeMTUTime = Date.now();
296
317
 
297
318
  try {
298
319
  await transport.inferMTU();
@@ -309,7 +330,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
309
330
 
310
331
  if (needsReconnect) {
311
332
  // necessary time for the bonding workaround
312
- await BluetoothTransport.disconnect(transport.id).catch(() => {});
333
+ await BleTransport.disconnect(transport.id).catch(() => {});
313
334
  await delay(reconnectionConfig.delayAfterFirstPairing);
314
335
  }
315
336
  } else {
@@ -327,9 +348,11 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
327
348
  /**
328
349
  * react-native bluetooth BLE implementation
329
350
  * @example
330
- * import BluetoothTransport from "@ledgerhq/react-native-hw-transport-ble";
351
+ * import BleTransport from "@ledgerhq/react-native-hw-transport-ble";
331
352
  */
332
- export default class BluetoothTransport extends Transport {
353
+ const TAG = "ble-verbose";
354
+ export default class BleTransport extends Transport {
355
+ static disconnectTimeoutMs = 5000;
333
356
  /**
334
357
  *
335
358
  */
@@ -339,17 +362,36 @@ export default class BluetoothTransport extends Transport {
339
362
  /**
340
363
  *
341
364
  */
342
- static setLogLevel = (level: any) => {
343
- bleManagerInstance().setLogLevel(level);
365
+ static list = (): Promise<void[]> => {
366
+ throw new Error("not implemented");
344
367
  };
345
368
 
346
369
  /**
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
370
+ * Exposed method from the ble-plx library
371
+ * Sets new log level for native module's logging mechanism.
372
+ * @param string logLevel New log level to be set.
350
373
  */
351
- static observeState(observer: any) {
352
- const emitFromState = (type) => {
374
+ static setLogLevel = (logLevel: string): void => {
375
+ if (Object.values<string>(LogLevel).includes(logLevel)) {
376
+ bleManagerInstance().setLogLevel(logLevel as LogLevel);
377
+ } else {
378
+ throw new Error(`${logLevel} is not a valid LogLevel`);
379
+ }
380
+ };
381
+
382
+ /**
383
+ * Listen to state changes on the bleManagerInstance and notify the
384
+ * specified observer.
385
+ * @param observer
386
+ * @returns TransportSubscription
387
+ */
388
+ static observeState(
389
+ observer: Observer<{
390
+ type: string;
391
+ available: boolean;
392
+ }>
393
+ ): TransportSubscription {
394
+ const emitFromState = (type: string) => {
353
395
  observer.next({
354
396
  type,
355
397
  available: type === "PoweredOn",
@@ -357,24 +399,23 @@ export default class BluetoothTransport extends Transport {
357
399
  };
358
400
 
359
401
  bleManagerInstance().onStateChange(emitFromState, true);
402
+
360
403
  return {
361
404
  unsubscribe: () => {},
362
405
  };
363
406
  }
364
407
 
365
- static list = (): any => {
366
- throw new Error("not implemented");
367
- };
368
-
369
408
  /**
370
409
  * Scan for bluetooth Ledger devices
410
+ * @param observer Device is partial in order to avoid the live-common/this dep
411
+ * @returns TransportSubscription
371
412
  */
372
413
  static listen(
373
- observer: TransportObserver<DescriptorEvent<Device>, HwTransportError>
414
+ observer: TransportObserver<any, HwTransportError>
374
415
  ): TransportSubscription {
375
- log("ble-verbose", "listen...");
416
+ log(TAG, "listening for devices");
376
417
 
377
- let unsubscribed;
418
+ let unsubscribed: boolean;
378
419
 
379
420
  const stateSub = bleManagerInstance().onStateChange(async (state) => {
380
421
  if (state === "PoweredOn") {
@@ -383,29 +424,32 @@ export default class BluetoothTransport extends Transport {
383
424
  getBluetoothServiceUuids()
384
425
  );
385
426
  if (unsubscribed) return;
386
- await Promise.all(
387
- devices.map((d) =>
388
- BluetoothTransport.disconnect(d.id).catch(() => {})
389
- )
390
- );
427
+ if (devices.length) {
428
+ log(TAG, "disconnecting from devices");
429
+
430
+ await Promise.all(
431
+ devices.map((d) => BleTransport.disconnect(d.id).catch(() => {}))
432
+ );
433
+ }
434
+
391
435
  if (unsubscribed) return;
392
436
  bleManagerInstance().startDeviceScan(
393
437
  getBluetoothServiceUuids(),
394
438
  null,
395
- (bleError, device) => {
439
+ (bleError: BleError | null, scannedDevice: Device | null) => {
396
440
  if (bleError) {
397
441
  observer.error(mapBleErrorToHwTransportError(bleError));
398
442
  unsubscribe();
399
443
  return;
400
444
  }
401
445
 
402
- const res = retrieveInfos(device);
446
+ const res = retrieveInfos(scannedDevice);
403
447
  const deviceModel = res && res.deviceModel;
404
448
 
405
- if (device) {
449
+ if (scannedDevice) {
406
450
  observer.next({
407
451
  type: "add",
408
- descriptor: device,
452
+ descriptor: scannedDevice,
409
453
  deviceModel,
410
454
  });
411
455
  }
@@ -418,7 +462,8 @@ export default class BluetoothTransport extends Transport {
418
462
  unsubscribed = true;
419
463
  bleManagerInstance().stopDeviceScan();
420
464
  stateSub.remove();
421
- log("ble-verbose", "done listening.");
465
+
466
+ log(TAG, "done listening.");
422
467
  };
423
468
 
424
469
  return {
@@ -428,32 +473,37 @@ export default class BluetoothTransport extends Transport {
428
473
 
429
474
  /**
430
475
  * Open a BLE transport
431
- * @param {*} deviceOrId
476
+ * @param {Device | string} deviceOrId
432
477
  */
433
- static async open(deviceOrId: Device | string) {
478
+ static async open(deviceOrId: Device | string): Promise<BleTransport> {
434
479
  return open(deviceOrId, true);
435
480
  }
436
481
 
437
482
  /**
438
- * Globally disconnect a BLE device by its ID
483
+ * Exposed method from the ble-plx library
484
+ * Disconnects from {@link Device} if it's connected or cancels pending connection.
439
485
  */
440
- static disconnect = async (id: any) => {
441
- log("ble-verbose", `user disconnect(${id})`);
486
+ static disconnect = async (id: DeviceId): Promise<void> => {
487
+ log(TAG, `user disconnect(${id})`);
442
488
  await bleManagerInstance().cancelDeviceConnection(id);
489
+ log(TAG, "disconnected");
443
490
  };
444
- id: string;
491
+
445
492
  device: Device;
493
+ deviceModel: DeviceModel;
494
+ disconnectTimeout: null | ReturnType<typeof setTimeout> = null;
495
+ id: string;
496
+ isConnected = true;
446
497
  mtuSize = 20;
447
- writeCharacteristic: Characteristic;
448
- writeCmdCharacteristic: Characteristic;
449
498
  notifyObservable: Observable<any>;
450
- deviceModel: DeviceModel;
451
499
  notYetDisconnected = true;
500
+ writeCharacteristic: Characteristic;
501
+ writeCmdCharacteristic: Characteristic | undefined;
452
502
 
453
503
  constructor(
454
504
  device: Device,
455
505
  writeCharacteristic: Characteristic,
456
- writeCmdCharacteristic: Characteristic,
506
+ writeCmdCharacteristic: Characteristic | undefined,
457
507
  notifyObservable: Observable<any>,
458
508
  deviceModel: DeviceModel
459
509
  ) {
@@ -464,24 +514,31 @@ export default class BluetoothTransport extends Transport {
464
514
  this.writeCmdCharacteristic = writeCmdCharacteristic;
465
515
  this.notifyObservable = notifyObservable;
466
516
  this.deviceModel = deviceModel;
467
- log("ble-verbose", `BleTransport(${String(this.id)}) new instance`);
517
+
518
+ log(TAG, `BleTransport(${String(this.id)}) new instance`);
519
+ clearDisconnectTimeout(this.id);
468
520
  }
469
521
 
470
522
  /**
471
- * communicate with a BLE transport
523
+ * Send data to the device using a low level API.
524
+ * It's recommended to use the "send" method for a higher level API.
525
+ * @param {Buffer} apdu - The data to send.
526
+ * @returns {Promise<Buffer>} A promise that resolves with the response data from the device.
472
527
  */
473
528
  exchange = (apdu: Buffer): Promise<any> =>
474
529
  this.exchangeAtomicImpl(async () => {
475
530
  try {
476
531
  const msgIn = apdu.toString("hex");
477
532
  log("apdu", `=> ${msgIn}`);
533
+
478
534
  const data = await merge(
479
- // $FlowFixMe
480
535
  this.notifyObservable.pipe(receiveAPDU),
481
536
  sendAPDU(this.write, apdu, this.mtuSize)
482
537
  ).toPromise();
538
+
483
539
  const msgOut = data.toString("hex");
484
540
  log("apdu", `<= ${msgOut}`);
541
+
485
542
  return data;
486
543
  } catch (e: any) {
487
544
  log("ble-error", "exchange got " + String(e));
@@ -497,9 +554,13 @@ export default class BluetoothTransport extends Transport {
497
554
  }
498
555
  });
499
556
 
500
- // TODO we probably will do this at end of open
501
- async inferMTU() {
557
+ /**
558
+ * Negotiate with the device the maximum transfer unit for the ble frames
559
+ * @returns Promise<number>
560
+ */
561
+ async inferMTU(): Promise<number> {
502
562
  let { mtu } = this.device;
563
+
503
564
  await this.exchangeAtomicImpl(async () => {
504
565
  try {
505
566
  mtu =
@@ -516,7 +577,8 @@ export default class BluetoothTransport extends Transport {
516
577
  )
517
578
  ).toPromise()) + 3;
518
579
  } catch (e: any) {
519
- log("ble-error", "inferMTU got " + String(e));
580
+ log("ble-error", "inferMTU got " + JSON.stringify(e));
581
+
520
582
  await bleManagerInstance()
521
583
  .cancelDeviceConnection(this.id)
522
584
  .catch(() => {}); // but we ignore if disconnect worked.
@@ -527,31 +589,37 @@ export default class BluetoothTransport extends Transport {
527
589
 
528
590
  if (mtu > 23) {
529
591
  const mtuSize = mtu - 3;
530
- log(
531
- "ble-verbose",
532
- `BleTransport(${String(this.id)}) mtu set to ${String(mtuSize)}`
533
- );
592
+ log(TAG, `BleTransport(${this.id}) mtu set to ${mtuSize}`);
534
593
  this.mtuSize = mtuSize;
535
594
  }
536
595
 
537
596
  return this.mtuSize;
538
597
  }
539
598
 
599
+ /**
600
+ * Exposed method from the ble-plx library
601
+ * Request the connection priority for the given device.
602
+ * @param {"Balanced" | "High" | "LowPower"} connectionPriority: Connection priority.
603
+ * @returns {Promise<Device>} Connected device.
604
+ */
540
605
  async requestConnectionPriority(
541
606
  connectionPriority: "Balanced" | "High" | "LowPower"
542
- ) {
543
- await decoratePromiseErrors(
607
+ ): Promise<Device> {
608
+ return await decoratePromiseErrors(
544
609
  this.device.requestConnectionPriority(
545
- ConnectionPriority[connectionPriority]
610
+ ConnectionPriority[connectionPriority as keyof ConnectionPriority]
546
611
  )
547
612
  );
548
613
  }
549
614
 
550
- setScrambleKey() {}
551
-
552
- write = async (buffer: Buffer, txid?: string | null | undefined) => {
615
+ /**
616
+ * Do not call this directly unless you know what you're doing. Communication
617
+ * with a Ledger device should be through the {@link exchange} method.
618
+ * @param buffer
619
+ * @param txid
620
+ */
621
+ write = async (buffer: Buffer, txid?: string | undefined): Promise<void> => {
553
622
  log("ble-frame", "=> " + buffer.toString("hex"));
554
-
555
623
  if (!this.writeCmdCharacteristic) {
556
624
  try {
557
625
  await this.writeCharacteristic.writeWithResponse(
@@ -573,32 +641,42 @@ export default class BluetoothTransport extends Transport {
573
641
  }
574
642
  };
575
643
 
576
- async close() {
577
- if (this.exchangeBusyPromise) {
578
- await this.exchangeBusyPromise;
579
- }
644
+ /**
645
+ * We intentionally do not immediately close a transport connection.
646
+ * Instead, we queue the disconnect and wait for a future connection to dismiss the event.
647
+ * This approach prevents unnecessary disconnects and reconnects. We use the isConnected
648
+ * flag to ensure that we do not trigger a disconnect if the current cached transport has
649
+ * already been disconnected.
650
+ * @returns {Promise<void>}
651
+ */
652
+ async close(): Promise<void> {
653
+ let resolve: (value: void | PromiseLike<void>) => void;
654
+ const disconnectPromise = new Promise<void>((innerResolve) => {
655
+ resolve = innerResolve;
656
+ });
657
+
658
+ clearDisconnectTimeout(this.id);
659
+
660
+ log(TAG, "Queuing a disconnect");
661
+
662
+ this.disconnectTimeout = setTimeout(() => {
663
+ log(TAG, `Triggering a disconnect from ${this.id}`);
664
+ if (this.isConnected) {
665
+ BleTransport.disconnect(this.id)
666
+ .catch(() => {})
667
+ .finally(resolve);
668
+ } else {
669
+ resolve();
670
+ }
671
+ }, BleTransport.disconnectTimeoutMs);
672
+
673
+ // The closure will occur no later than 5s, triggered either by disconnection
674
+ // or the actual response of the apdu.
675
+ await Promise.race([
676
+ this.exchangeBusyPromise || Promise.resolve(),
677
+ disconnectPromise,
678
+ ]);
679
+
680
+ return;
580
681
  }
581
682
  }
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
- };